mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 09:34:50 +00:00
refactor: content tab styling (#7091)
* refactor: managed content card * fix: update filter pill styling to be chips-like * fix: modpack exporting * fixed: vanilla server projs + header * refactor: filters * refactor: detect external file icons + other metadata inside * refactor: support json5 metadata * fix: qa * fix: qa * fix: fmt + prepr * fix: fmt * fix: qa * fix: headers * feat: locking * fix: clippy * fix: qa * fix: frozen loc * fix: shared instances diff calc break * fix: env filter + remove categories
This commit is contained in:
@@ -8,103 +8,126 @@
|
||||
<template v-else>{{ label }}</template>
|
||||
</span>
|
||||
|
||||
<MultiSelect
|
||||
v-for="preview in appliedFilterPreviews"
|
||||
:key="preview.key"
|
||||
class="min-w-0 max-w-full"
|
||||
:model-value="getPreviewSelectedValues(preview.key)"
|
||||
:options="preview.options"
|
||||
:max-height="500"
|
||||
:clearable="false"
|
||||
:show-chevron="false"
|
||||
:fit-content="true"
|
||||
:searchable="preview.category.searchable"
|
||||
:search-placeholder="preview.category.searchPlaceholder"
|
||||
trigger-type="base"
|
||||
trigger-size="lg"
|
||||
:trigger-class="effectivePreviewTriggerClass"
|
||||
:dropdown-width="getPreviewDropdownWidth(preview.category)"
|
||||
:dropdown-min-width="getPreviewDropdownMinWidth(preview.category)"
|
||||
:checkbox-position="checkboxPosition"
|
||||
show-selection-actions
|
||||
@update:model-value="(nextValue) => setPreviewSelectedValues(preview.key, nextValue)"
|
||||
@open="openPreviewFilterDraft(preview.key)"
|
||||
@close="commitPreviewFilterDraft(preview.key)"
|
||||
>
|
||||
<template v-if="$slots['preview-top'] && preview.category.syntheticOptions?.length" #top>
|
||||
<slot
|
||||
name="preview-top"
|
||||
:category="preview.category"
|
||||
:selected-values="getPreviewSelectedValues(preview.key)"
|
||||
:set-selected-values="(values) => setPreviewSelectedValues(preview.key, values)"
|
||||
:close-menu="(event) => closePreviewFilterMenu(preview.key, event)"
|
||||
></slot>
|
||||
</template>
|
||||
<template v-if="$slots['search-actions']" #search-actions>
|
||||
<slot
|
||||
name="search-actions"
|
||||
:category="preview.category"
|
||||
:selected-values="getPreviewSelectedValues(preview.key)"
|
||||
:set-selected-values="(values) => setPreviewSelectedValues(preview.key, values)"
|
||||
:close-menu="(event) => closePreviewFilterMenu(preview.key, event)"
|
||||
></slot>
|
||||
</template>
|
||||
<template #input-content="{ isOpen, openDirection }">
|
||||
<div class="flex min-h-8 min-w-0 max-w-full items-center gap-2 sm:max-w-80">
|
||||
<FilterIcon
|
||||
v-if="showPreviewFilterIcon"
|
||||
class="size-5 shrink-0 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate">
|
||||
<span class="font-medium">{{ preview.label }}:</span>
|
||||
<span class="ml-1 font-semibold text-contrast">{{ preview.summary }}</span>
|
||||
</span>
|
||||
<div class="flex shrink-0 items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
class="flex cursor-pointer items-center justify-center rounded border-none bg-transparent p-0.5 text-secondary transition-colors hover:text-contrast"
|
||||
:aria-label="`Clear ${preview.label} filter`"
|
||||
@click.stop="clearFilterCategory(preview.key)"
|
||||
>
|
||||
<XIcon class="size-4 text-primary" />
|
||||
</button>
|
||||
<div class="h-5 w-[1px] shrink-0 bg-surface-5"></div>
|
||||
<ChevronLeftIcon
|
||||
class="size-5 shrink-0 text-secondary transition-transform duration-150"
|
||||
:class="isOpen ? (openDirection === 'down' ? 'rotate-90' : '-rotate-90') : '-rotate-90'"
|
||||
<template v-for="preview in appliedFilterPreviews" :key="preview.key">
|
||||
<Button
|
||||
v-if="preview.category.direct"
|
||||
type="outlined"
|
||||
native-type="button"
|
||||
size="lg"
|
||||
:class="effectivePreviewTriggerClass"
|
||||
:aria-label="`Clear ${preview.label} filter`"
|
||||
@click="clearFilterCategory(preview.key)"
|
||||
>
|
||||
<span class="font-semibold">{{ preview.label }}</span>
|
||||
<XIcon class="size-4" />
|
||||
</Button>
|
||||
<MultiSelect
|
||||
v-else
|
||||
class="min-w-0 max-w-full"
|
||||
:model-value="getPreviewSelectedValues(preview.key)"
|
||||
:options="preview.options"
|
||||
:max-height="500"
|
||||
:clearable="false"
|
||||
:show-chevron="false"
|
||||
:fit-content="true"
|
||||
:searchable="preview.category.searchable"
|
||||
:search-placeholder="preview.category.searchPlaceholder"
|
||||
trigger-type="base"
|
||||
trigger-size="lg"
|
||||
:trigger-class="effectivePreviewTriggerClass"
|
||||
:dropdown-width="getPreviewDropdownWidth(preview.category)"
|
||||
:dropdown-min-width="getPreviewDropdownMinWidth(preview.category)"
|
||||
:checkbox-position="checkboxPosition"
|
||||
show-selection-actions
|
||||
@update:model-value="(nextValue) => setPreviewSelectedValues(preview.key, nextValue)"
|
||||
@open="openPreviewFilterDraft(preview.key)"
|
||||
@close="commitPreviewFilterDraft(preview.key)"
|
||||
>
|
||||
<template v-if="$slots['preview-top'] && preview.category.syntheticOptions?.length" #top>
|
||||
<slot
|
||||
name="preview-top"
|
||||
:category="preview.category"
|
||||
:selected-values="getPreviewSelectedValues(preview.key)"
|
||||
:set-selected-values="(values) => setPreviewSelectedValues(preview.key, values)"
|
||||
:close-menu="(event) => closePreviewFilterMenu(preview.key, event)"
|
||||
></slot>
|
||||
</template>
|
||||
<template v-if="$slots['search-actions']" #search-actions>
|
||||
<slot
|
||||
name="search-actions"
|
||||
:category="preview.category"
|
||||
:selected-values="getPreviewSelectedValues(preview.key)"
|
||||
:set-selected-values="(values) => setPreviewSelectedValues(preview.key, values)"
|
||||
:close-menu="(event) => closePreviewFilterMenu(preview.key, event)"
|
||||
></slot>
|
||||
</template>
|
||||
<template #input-content="{ isOpen, openDirection }">
|
||||
<div class="flex min-h-8 min-w-0 max-w-full items-center gap-2 sm:max-w-80">
|
||||
<FilterIcon
|
||||
v-if="showPreviewFilterIcon"
|
||||
class="size-5 shrink-0 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<slot
|
||||
name="preview-content"
|
||||
:category="preview.category"
|
||||
:selected-values="getPreviewSelectedValues(preview.key)"
|
||||
:label="preview.label"
|
||||
:summary="preview.summary"
|
||||
>
|
||||
<span class="min-w-0 flex-1 truncate">
|
||||
<span class="font-medium">{{ preview.label }}:</span>
|
||||
<span class="ml-1 font-semibold text-contrast">{{ preview.summary }}</span>
|
||||
</span>
|
||||
</slot>
|
||||
<div class="flex shrink-0 items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
class="flex cursor-pointer items-center justify-center rounded border-none bg-transparent p-0.5 text-secondary transition-colors hover:text-contrast"
|
||||
:aria-label="`Clear ${preview.label} filter`"
|
||||
@click.stop="clearFilterCategory(preview.key)"
|
||||
>
|
||||
<XIcon class="size-4 text-primary" />
|
||||
</button>
|
||||
<div class="h-5 w-[1px] shrink-0 bg-surface-5"></div>
|
||||
<ChevronLeftIcon
|
||||
class="size-5 shrink-0 text-secondary transition-transform duration-150"
|
||||
:class="
|
||||
isOpen ? (openDirection === 'down' ? 'rotate-90' : '-rotate-90') : '-rotate-90'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="$slots['preview-footer']" #bottom>
|
||||
<slot
|
||||
name="preview-footer"
|
||||
:category="preview.category"
|
||||
:selected-values="getPreviewSelectedValues(preview.key)"
|
||||
:set-selected-values="(values) => setPreviewSelectedValues(preview.key, values)"
|
||||
:close-menu="(event) => closePreviewFilterMenu(preview.key, event)"
|
||||
></slot>
|
||||
</template>
|
||||
<template v-if="$slots.option" #option="{ item, selected, index }">
|
||||
<slot
|
||||
name="option"
|
||||
:category="preview.category"
|
||||
:option="item"
|
||||
:selected="selected"
|
||||
:index="index"
|
||||
></slot>
|
||||
</template>
|
||||
<template v-else-if="$slots['option-right']" #option-right="{ item, selected, index }">
|
||||
<slot
|
||||
name="option-right"
|
||||
:category="preview.category"
|
||||
:option="item"
|
||||
:selected="selected"
|
||||
:index="index"
|
||||
></slot>
|
||||
</template>
|
||||
</MultiSelect>
|
||||
</template>
|
||||
<template v-if="$slots['preview-footer']" #bottom>
|
||||
<slot
|
||||
name="preview-footer"
|
||||
:category="preview.category"
|
||||
:selected-values="getPreviewSelectedValues(preview.key)"
|
||||
:set-selected-values="(values) => setPreviewSelectedValues(preview.key, values)"
|
||||
:close-menu="(event) => closePreviewFilterMenu(preview.key, event)"
|
||||
></slot>
|
||||
</template>
|
||||
<template v-if="$slots.option" #option="{ item, selected, index }">
|
||||
<slot
|
||||
name="option"
|
||||
:category="preview.category"
|
||||
:option="item"
|
||||
:selected="selected"
|
||||
:index="index"
|
||||
></slot>
|
||||
</template>
|
||||
<template v-else-if="$slots['option-right']" #option-right="{ item, selected, index }">
|
||||
<slot
|
||||
name="option-right"
|
||||
:category="preview.category"
|
||||
:option="item"
|
||||
:selected="selected"
|
||||
:index="index"
|
||||
></slot>
|
||||
</template>
|
||||
</MultiSelect>
|
||||
</template>
|
||||
|
||||
<div class="flex h-10 min-w-0 max-w-full items-center gap-2">
|
||||
<Button
|
||||
@@ -132,7 +155,7 @@
|
||||
<div
|
||||
v-if="isAddMenuOpen && !isMobileActiveSubmenu"
|
||||
ref="menuContainer"
|
||||
class="fixed z-[9999] flex flex-col overflow-hidden rounded-[14px] border border-solid border-surface-5 bg-surface-4 shadow-2xl"
|
||||
class="fixed z-[9999] flex flex-col overflow-x-hidden overflow-y-auto rounded-[14px] border border-solid border-surface-5 bg-surface-4 shadow-2xl"
|
||||
:style="addMenuStyle"
|
||||
role="menu"
|
||||
@mousedown.stop
|
||||
@@ -145,15 +168,24 @@
|
||||
:ref="(element) => setCategoryButtonRef(category.key, element)"
|
||||
type="button"
|
||||
class="group/filter-menu-button flex h-12 w-full appearance-none items-center justify-between gap-1 border-0 px-4 text-left text-base font-semibold text-primary shadow-none transition-all duration-150 hover:brightness-110 focus:brightness-110 bg-surface-4"
|
||||
:class="category.key === activeCategoryKey ? '!brightness-110' : ''"
|
||||
role="menuitem"
|
||||
@click="activateCategory(category.key)"
|
||||
@mouseenter="handleCategoryMouseEnter(category.key)"
|
||||
@focus="handleCategoryFocus(category.key)"
|
||||
:class="
|
||||
category.key === activeCategoryKey || isDirectCategorySelected(category)
|
||||
? '!brightness-110'
|
||||
: ''
|
||||
"
|
||||
:role="category.direct ? 'menuitemcheckbox' : 'menuitem'"
|
||||
:aria-checked="category.direct ? isDirectCategorySelected(category) : undefined"
|
||||
@click="handleCategoryClick(category)"
|
||||
@mouseenter="handleCategoryMouseEnter(category)"
|
||||
@focus="handleCategoryFocus(category)"
|
||||
>
|
||||
<span>{{ category.label }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<ChevronRightIcon class="size-5 text-secondary" />
|
||||
<CheckIcon
|
||||
v-if="category.direct && isDirectCategorySelected(category)"
|
||||
class="size-5 text-brand"
|
||||
/>
|
||||
<ChevronRightIcon v-else-if="!category.direct" class="size-5 text-secondary" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -417,6 +449,7 @@ export type DropdownFilterBarCategory = {
|
||||
key: string
|
||||
label: string
|
||||
options: DropdownFilterBarItem[]
|
||||
direct?: boolean
|
||||
syntheticOptions?: DropdownFilterBarOption[]
|
||||
searchable?: boolean
|
||||
disableLocalOptionsFilter?: boolean
|
||||
@@ -461,6 +494,7 @@ type ViewportRect = {
|
||||
type MenuPositionOptions = {
|
||||
triggerRect: DOMRect
|
||||
dropdownRect: DOMRect
|
||||
dropdownHeight: number
|
||||
viewport: ViewportRect
|
||||
}
|
||||
|
||||
@@ -1341,7 +1375,42 @@ function getSubmenuWidthInPixels(category: DropdownFilterBarCategory): number {
|
||||
}
|
||||
|
||||
function getWidestSubmenuWidthInPixels(categories: DropdownFilterBarCategory[]): number {
|
||||
return Math.max(...categories.map((category) => getSubmenuWidthInPixels(category)), 0)
|
||||
return Math.max(
|
||||
...categories
|
||||
.filter((category) => !category.direct)
|
||||
.map((category) => getSubmenuWidthInPixels(category)),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
function getDirectCategoryOption(category: DropdownFilterBarCategory) {
|
||||
if (!category.direct) return undefined
|
||||
return category.options.find(isDropdownFilterOption)
|
||||
}
|
||||
|
||||
function isDirectCategorySelected(category: DropdownFilterBarCategory) {
|
||||
const option = getDirectCategoryOption(category)
|
||||
return !!option && isFilterValueSelected(category.key, option.value)
|
||||
}
|
||||
|
||||
function deactivateCategory() {
|
||||
clearPendingCategoryTimeout()
|
||||
pendingCategoryKey.value = null
|
||||
activeCategoryKey.value = null
|
||||
categorySearchQuery.value = ''
|
||||
hasSubmenuPosition.value = false
|
||||
}
|
||||
|
||||
function handleCategoryClick(category: DropdownFilterBarCategory) {
|
||||
const option = getDirectCategoryOption(category)
|
||||
if (!option) {
|
||||
activateCategory(category.key)
|
||||
return
|
||||
}
|
||||
|
||||
deactivateCategory()
|
||||
toggleFilterOption(category.key, option)
|
||||
scheduleAddMenuPositionUpdate()
|
||||
}
|
||||
|
||||
function activateCategory(categoryKey: string) {
|
||||
@@ -1357,18 +1426,27 @@ function activateCategory(categoryKey: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleCategoryFocus(categoryKey: string) {
|
||||
function handleCategoryFocus(category: DropdownFilterBarCategory) {
|
||||
if (category.direct) {
|
||||
deactivateCategory()
|
||||
return
|
||||
}
|
||||
if (isMobileAddMenuLayout.value) {
|
||||
return
|
||||
}
|
||||
|
||||
activateCategory(categoryKey)
|
||||
activateCategory(category.key)
|
||||
}
|
||||
|
||||
function handleCategoryMouseEnter(categoryKey: string) {
|
||||
function handleCategoryMouseEnter(category: DropdownFilterBarCategory) {
|
||||
if (category.direct) {
|
||||
deactivateCategory()
|
||||
return
|
||||
}
|
||||
if (isMobileAddMenuLayout.value) {
|
||||
return
|
||||
}
|
||||
const categoryKey = category.key
|
||||
|
||||
if (!activeCategoryKey.value) {
|
||||
activateCategory(categoryKey)
|
||||
@@ -1448,7 +1526,12 @@ function getViewportRect(): ViewportRect {
|
||||
}
|
||||
}
|
||||
|
||||
function getAddMenuPosition({ triggerRect, dropdownRect, viewport }: MenuPositionOptions) {
|
||||
function getAddMenuPosition({
|
||||
triggerRect,
|
||||
dropdownRect,
|
||||
dropdownHeight,
|
||||
viewport,
|
||||
}: MenuPositionOptions) {
|
||||
const dropdownWidth = Math.max(ADD_MENU_WIDTH, triggerRect.width)
|
||||
const positionedDropdownWidth = Math.max(dropdownRect.width, dropdownWidth)
|
||||
const triggerTop = triggerRect.top + viewport.offsetTop
|
||||
@@ -1463,18 +1546,24 @@ function getAddMenuPosition({ triggerRect, dropdownRect, viewport }: MenuPositio
|
||||
minLeft,
|
||||
viewportRight - positionedDropdownWidth - DROPDOWN_VIEWPORT_MARGIN,
|
||||
)
|
||||
const hasSpaceBelow =
|
||||
triggerBottom + dropdownRect.height + DROPDOWN_GAP + DROPDOWN_VIEWPORT_MARGIN <= viewportBottom
|
||||
const hasSpaceAbove =
|
||||
triggerTop - dropdownRect.height - DROPDOWN_GAP - DROPDOWN_VIEWPORT_MARGIN > viewportTop
|
||||
const opensUp = !hasSpaceBelow && hasSpaceAbove
|
||||
const spaceBelow = Math.max(
|
||||
0,
|
||||
viewportBottom - triggerBottom - DROPDOWN_GAP - DROPDOWN_VIEWPORT_MARGIN,
|
||||
)
|
||||
const spaceAbove = Math.max(0, triggerTop - viewportTop - DROPDOWN_GAP - DROPDOWN_VIEWPORT_MARGIN)
|
||||
const hasSpaceBelow = dropdownHeight <= spaceBelow
|
||||
const hasSpaceAbove = dropdownHeight <= spaceAbove
|
||||
const opensUp = !hasSpaceBelow && (hasSpaceAbove || spaceAbove > spaceBelow)
|
||||
const availableHeight = opensUp ? spaceAbove : spaceBelow
|
||||
const positionedDropdownHeight = Math.min(dropdownHeight, availableHeight)
|
||||
const top = opensUp
|
||||
? triggerTop - dropdownRect.height - DROPDOWN_GAP
|
||||
? triggerTop - positionedDropdownHeight - DROPDOWN_GAP
|
||||
: triggerBottom + DROPDOWN_GAP
|
||||
const left = Math.min(Math.max(minLeft, triggerLeft), maxLeft)
|
||||
|
||||
return {
|
||||
left: `${left}px`,
|
||||
maxHeight: `${availableHeight}px`,
|
||||
minWidth: `${triggerRect.width}px`,
|
||||
top: `${Math.max(viewportTop + DROPDOWN_VIEWPORT_MARGIN, top)}px`,
|
||||
width: `${dropdownWidth}px`,
|
||||
@@ -1601,6 +1690,7 @@ function updateAddMenuPosition(): boolean {
|
||||
addMenuStyle.value = getAddMenuPosition({
|
||||
triggerRect,
|
||||
dropdownRect,
|
||||
dropdownHeight: positioningElement.scrollHeight,
|
||||
viewport,
|
||||
})
|
||||
return true
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<FilterIcon class="size-5 text-secondary" />
|
||||
<button
|
||||
:class="pillClass(modelValue.length === 0)"
|
||||
:aria-pressed="modelValue.length === 0"
|
||||
@click="modelValue = []"
|
||||
>
|
||||
<slot name="all"> All </slot>
|
||||
</button>
|
||||
<button
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:class="pillClass(modelValue.includes(option.id))"
|
||||
:aria-pressed="modelValue.includes(option.id)"
|
||||
@click="toggle(option.id)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<FilterIcon class="size-5 shrink-0 text-secondary" />
|
||||
<div class="filter-pills__chips flex flex-wrap items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
:class="pillClass(modelValue.length === 0)"
|
||||
:aria-pressed="modelValue.length === 0"
|
||||
@click="modelValue = []"
|
||||
>
|
||||
<slot name="all"> All </slot>
|
||||
</button>
|
||||
<button
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
type="button"
|
||||
:class="pillClass(modelValue.includes(option.id))"
|
||||
:aria-pressed="modelValue.includes(option.id)"
|
||||
@click="toggle(option.id)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -36,10 +40,10 @@ defineProps<{
|
||||
|
||||
function pillClass(active: boolean) {
|
||||
return [
|
||||
'cursor-pointer rounded-full border border-solid px-3 py-1.5 text-base font-semibold leading-5 transition-all duration-100 active:scale-[0.97]',
|
||||
'cursor-pointer rounded-xl border border-solid px-3 py-1.5 text-sm font-medium leading-5 transition-all duration-100 active:scale-[0.97] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-brand-shadow',
|
||||
active
|
||||
? 'border-brand bg-brand-highlight text-brand'
|
||||
: 'border-surface-5 bg-surface-4 text-primary hover:bg-surface-5',
|
||||
: 'border-surface-5 bg-transparent text-primary hover:bg-surface-3',
|
||||
]
|
||||
}
|
||||
|
||||
@@ -51,3 +55,9 @@ function toggle(id: string) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-pills__chips {
|
||||
filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -46,7 +46,7 @@ const typeClasses: Record<ButtonType, string> = {
|
||||
colored:
|
||||
'button-frame--colored bg-[--button-color] text-[var(--color-accent-contrast)] [&>svg]:text-inherit',
|
||||
outlined:
|
||||
'button-frame--outlined bg-transparent text-[var(--button-color,var(--color-contrast))] [&>svg]:text-inherit',
|
||||
'button-frame--outlined bg-transparent text-[var(--button-color,var(--color-contrast))] [&>svg]:text-[var(--button-color,var(--color-base))]',
|
||||
quiet: 'button-frame--quiet bg-transparent [&>svg]:text-inherit',
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ async function handleSelectedProjectsLeaveResult(
|
||||
</PageHeaderMetadata>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<Admonition v-if="installContext.warning" type="warning" class="mb-1">
|
||||
<Admonition v-if="installContext.warning" type="warning" class="mt-4 mb-1">
|
||||
{{ installContext.warning }}
|
||||
</Admonition>
|
||||
</template>
|
||||
|
||||
@@ -224,6 +224,9 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
<template v-if="lockedMessages?.modLoader" #locked-mod_loader>
|
||||
{{ lockedMessages.modLoader }}
|
||||
</template>
|
||||
<template v-if="lockedMessages?.modLoader" #locked-shader_loader>
|
||||
{{ lockedMessages.modLoader }}
|
||||
</template>
|
||||
<template v-if="lockedMessages?.environment" #locked-environment>
|
||||
{{ lockedMessages.environment }}
|
||||
</template>
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
DownloadIcon,
|
||||
LockIcon,
|
||||
MoreVerticalIcon,
|
||||
SpinnerIcon,
|
||||
TrashExclamationIcon,
|
||||
TrashIcon,
|
||||
TriangleAlertIcon,
|
||||
UploadIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { computed, getCurrentInstance, ref } from 'vue'
|
||||
@@ -38,6 +40,14 @@ const messages = defineMessages({
|
||||
id: 'content.card.select-project',
|
||||
defaultMessage: 'Select {project}',
|
||||
},
|
||||
uploaded: {
|
||||
id: 'content.card.uploaded',
|
||||
defaultMessage: 'Uploaded',
|
||||
},
|
||||
frozen: {
|
||||
id: 'content.card.frozen',
|
||||
defaultMessage: 'This project is locked to its current version until unfrozen.',
|
||||
},
|
||||
})
|
||||
|
||||
interface Props {
|
||||
@@ -47,7 +57,9 @@ interface Props {
|
||||
versionLink?: string | RouteLocationRaw
|
||||
owner?: ContentOwner
|
||||
source?: ContentSource
|
||||
external?: boolean
|
||||
enabled?: boolean
|
||||
locked?: boolean
|
||||
installing?: boolean
|
||||
hasUpdate?: boolean
|
||||
isClientOnly?: boolean
|
||||
@@ -58,6 +70,7 @@ interface Props {
|
||||
disabledTooltip?: string | null
|
||||
toggleDisabled?: boolean
|
||||
toggleDisabledTooltip?: string | null
|
||||
hideToggle?: boolean
|
||||
showCheckbox?: boolean
|
||||
hideDelete?: boolean
|
||||
hideActions?: boolean
|
||||
@@ -70,7 +83,9 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
versionLink: undefined,
|
||||
owner: undefined,
|
||||
source: undefined,
|
||||
external: false,
|
||||
enabled: undefined,
|
||||
locked: false,
|
||||
installing: false,
|
||||
hasUpdate: false,
|
||||
isClientOnly: false,
|
||||
@@ -81,6 +96,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
disabledTooltip: undefined,
|
||||
toggleDisabled: false,
|
||||
toggleDisabledTooltip: undefined,
|
||||
hideToggle: false,
|
||||
showCheckbox: false,
|
||||
hideDelete: false,
|
||||
hideActions: false,
|
||||
@@ -243,7 +259,11 @@ const deleteHovered = ref(false)
|
||||
/>
|
||||
<span class="text-sm leading-5 text-secondary">{{ owner.name }}</span>
|
||||
</AutoLink>
|
||||
<template v-if="version">
|
||||
<span v-else-if="external" class="flex items-center gap-1 text-secondary">
|
||||
<UploadIcon class="size-4 shrink-0" />
|
||||
<span class="text-sm leading-5">{{ formatMessage(messages.uploaded) }}</span>
|
||||
</span>
|
||||
<template v-if="version && !external">
|
||||
<BulletDivider class="shrink-0 @[800px]:hidden" />
|
||||
<AutoLink
|
||||
:target="
|
||||
@@ -277,7 +297,7 @@ const deleteHovered = ref(false)
|
||||
typeof versionLink === 'string' && versionLink.startsWith('http') ? '_blank' : undefined
|
||||
"
|
||||
:to="versionLink"
|
||||
class="inline-flex min-w-0 font-medium leading-6 text-contrast !decoration-contrast"
|
||||
class="inline-flex min-w-0 font-semibold leading-6 text-contrast !decoration-contrast"
|
||||
:class="{ 'hover:underline': versionLink, 'cursor-pointer': versionLink }"
|
||||
>
|
||||
<span ref="versionNumberRef" class="truncate">{{
|
||||
@@ -309,11 +329,24 @@ const deleteHovered = ref(false)
|
||||
|
||||
<!-- Fixed width container to reserve space for update/switch version button -->
|
||||
<div
|
||||
v-if="hasUpdateListener || hasSwitchVersionListener"
|
||||
v-if="
|
||||
locked ||
|
||||
(hasUpdateListener && hasUpdate) ||
|
||||
(hasSwitchVersionListener && version && !hideSwitchVersion)
|
||||
"
|
||||
class="flex w-8 items-center justify-center"
|
||||
>
|
||||
<IconButton
|
||||
v-if="hasUpdate"
|
||||
v-if="locked"
|
||||
v-tooltip="formatMessage(messages.frozen)"
|
||||
type="quiet"
|
||||
:label="formatMessage(messages.frozen)"
|
||||
disabled
|
||||
>
|
||||
<LockIcon class="size-5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-else-if="hasUpdate"
|
||||
v-tooltip="
|
||||
isDisabled && disabledTooltip
|
||||
? disabledTooltip
|
||||
@@ -353,7 +386,7 @@ const deleteHovered = ref(false)
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
v-if="enabled !== undefined"
|
||||
v-if="enabled !== undefined && !hideToggle"
|
||||
v-tooltip="
|
||||
isToggleDisabled && (toggleDisabledTooltip || disabledTooltip)
|
||||
? (toggleDisabledTooltip ?? disabledTooltip)
|
||||
|
||||
@@ -265,7 +265,9 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
:version-link="item.versionLink"
|
||||
:owner="item.owner"
|
||||
:source="item.source"
|
||||
:external="item.external"
|
||||
:enabled="item.enabled"
|
||||
:locked="item.locked"
|
||||
:installing="item.installing"
|
||||
:has-update="item.hasUpdate"
|
||||
:is-client-only="item.isClientOnly"
|
||||
@@ -276,6 +278,7 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
:disabled-tooltip="item.disabledTooltip"
|
||||
:toggle-disabled="item.toggleDisabled"
|
||||
:toggle-disabled-tooltip="item.toggleDisabledTooltip"
|
||||
:hide-toggle="item.hideToggle"
|
||||
:show-checkbox="showSelection"
|
||||
:hide-delete="hideDelete || item.hideDelete"
|
||||
:hide-actions="!hasAnyActions"
|
||||
@@ -329,7 +332,9 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
:version-link="item.versionLink"
|
||||
:owner="item.owner"
|
||||
:source="item.source"
|
||||
:external="item.external"
|
||||
:enabled="item.enabled"
|
||||
:locked="item.locked"
|
||||
:installing="item.installing"
|
||||
:has-update="item.hasUpdate"
|
||||
:is-client-only="item.isClientOnly"
|
||||
@@ -340,6 +345,7 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
:disabled-tooltip="item.disabledTooltip"
|
||||
:toggle-disabled="item.toggleDisabled"
|
||||
:toggle-disabled-tooltip="item.toggleDisabledTooltip"
|
||||
:hide-toggle="item.hideToggle"
|
||||
:show-checkbox="showSelection"
|
||||
:hide-delete="hideDelete || item.hideDelete"
|
||||
:hide-actions="!hasAnyActions"
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BoxesIcon,
|
||||
ClockIcon,
|
||||
DownloadIcon,
|
||||
HeartIcon,
|
||||
MoreVerticalIcon,
|
||||
Settings2Icon,
|
||||
SpinnerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { computed, getCurrentInstance, onMounted, onUnmounted, ref } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
import AutoLink from '#ui/components/base/AutoLink.vue'
|
||||
import Avatar from '#ui/components/base/Avatar.vue'
|
||||
import BulletDivider from '#ui/components/base/BulletDivider.vue'
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
type OverflowMenuOption,
|
||||
TeleportOverflowMenu,
|
||||
} from '#ui/components/base/buttons'
|
||||
import TagTagItem from '#ui/components/base/TagTagItem.vue'
|
||||
import { useRelativeTime } from '#ui/composables/how-ago'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import type {
|
||||
ContentModpackCardCategory,
|
||||
ContentModpackCardProject,
|
||||
ContentModpackCardVersion,
|
||||
ContentOwner,
|
||||
} from '../types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
installationSettingsTooltip: {
|
||||
id: 'content.modpack-card.installation-settings',
|
||||
defaultMessage: 'Installation settings',
|
||||
},
|
||||
})
|
||||
|
||||
interface Props {
|
||||
project: ContentModpackCardProject
|
||||
projectLink?: string | RouteLocationRaw
|
||||
version?: ContentModpackCardVersion
|
||||
versionLink?: string | RouteLocationRaw
|
||||
owner?: ContentOwner
|
||||
categories?: ContentModpackCardCategory[]
|
||||
disabled?: boolean
|
||||
overflowOptions?: OverflowMenuOption[]
|
||||
hasUpdate?: boolean
|
||||
disabledText?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
projectLink: undefined,
|
||||
version: undefined,
|
||||
versionLink: undefined,
|
||||
owner: undefined,
|
||||
categories: undefined,
|
||||
disabled: false,
|
||||
overflowOptions: undefined,
|
||||
hasUpdate: false,
|
||||
disabledText: undefined,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: []
|
||||
content: []
|
||||
settings: []
|
||||
}>()
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const hasUpdateListener = computed(() => typeof instance?.vnode.props?.onUpdate === 'function')
|
||||
const hasContentListener = computed(() => typeof instance?.vnode.props?.onContent === 'function')
|
||||
const hasSettingsListener = computed(() => typeof instance?.vnode.props?.onSettings === 'function')
|
||||
|
||||
const formatTimeAgo = useRelativeTime()
|
||||
|
||||
const formatCompact = (n: number | undefined) => {
|
||||
if (n === undefined) return ''
|
||||
return new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 }).format(n)
|
||||
}
|
||||
|
||||
const collapsedOptions = computed<OverflowMenuOption[]>(() => {
|
||||
const options: OverflowMenuOption[] = []
|
||||
if (hasContentListener.value) {
|
||||
options.push({
|
||||
id: 'content',
|
||||
label: formatMessage(commonMessages.contentLabel),
|
||||
action: () => emit('content'),
|
||||
})
|
||||
}
|
||||
if (hasSettingsListener.value) {
|
||||
options.push({
|
||||
id: 'settings',
|
||||
label: formatMessage(messages.installationSettingsTooltip),
|
||||
action: () => emit('settings'),
|
||||
})
|
||||
}
|
||||
return options
|
||||
})
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const isExpanded = ref(true)
|
||||
let observer: ResizeObserver | null = null
|
||||
onMounted(() => {
|
||||
observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
isExpanded.value = entry.contentRect.width >= 700
|
||||
}
|
||||
})
|
||||
if (containerRef.value) observer.observe(containerRef.value)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
observer?.disconnect()
|
||||
observer = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="@container flex flex-col gap-4 rounded-[20px] bg-bg-raised p-6 shadow-md border border-solid border-surface-4"
|
||||
:class="{ 'opacity-50': disabled }"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-4">
|
||||
<AutoLink :to="projectLink" class="shrink-0">
|
||||
<Avatar :src="project.icon_url" :alt="project.title" size="5rem" no-shadow />
|
||||
</AutoLink>
|
||||
<div class="flex min-w-0 flex-col gap-1.5">
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<AutoLink
|
||||
:to="projectLink"
|
||||
class="truncate text-xl font-semibold text-contrast"
|
||||
:class="projectLink ? 'hover:underline' : ''"
|
||||
>
|
||||
{{ project.title }}
|
||||
</AutoLink>
|
||||
<span
|
||||
v-if="project.filename && (owner || version)"
|
||||
class="truncate text-secondary mb-2"
|
||||
>
|
||||
{{ project.filename }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="project.filename && !(owner || version)"
|
||||
class="flex min-h-8 min-w-0 items-center text-secondary"
|
||||
>
|
||||
<span class="truncate">{{ project.filename }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="owner || version"
|
||||
class="flex flex-nowrap items-center gap-2 overflow-hidden text-secondary"
|
||||
>
|
||||
<AutoLink
|
||||
v-if="owner"
|
||||
:to="owner.link"
|
||||
class="flex shrink-0 items-center gap-1.5 hover:underline"
|
||||
>
|
||||
<Avatar
|
||||
:src="owner.avatar_url"
|
||||
:alt="owner.name"
|
||||
size="2rem"
|
||||
:circle="owner.type === 'user'"
|
||||
no-shadow
|
||||
/>
|
||||
<span class="font-medium whitespace-nowrap">{{ owner.name }}</span>
|
||||
</AutoLink>
|
||||
<template v-if="version">
|
||||
<BulletDivider v-if="owner" />
|
||||
<AutoLink
|
||||
:to="versionLink"
|
||||
class="shrink-0 font-medium text-secondary !decoration-secondary whitespace-nowrap"
|
||||
:class="versionLink ? 'hover:underline' : ''"
|
||||
>
|
||||
{{ version.version_number }}
|
||||
</AutoLink>
|
||||
</template>
|
||||
<template v-if="version?.date_published">
|
||||
<BulletDivider />
|
||||
<div class="flex shrink-0 items-center gap-2 whitespace-nowrap">
|
||||
<ClockIcon class="size-5" />
|
||||
<span>{{ formatTimeAgo(new Date(version.date_published)) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<template v-if="disabled">
|
||||
<div class="flex items-center gap-2 text-secondary">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
<span class="font-semibold">{{
|
||||
disabledText ?? formatMessage(commonMessages.updatingLabel)
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Expanded actions visible at >= 700px -->
|
||||
<div class="hidden @[700px]:flex items-center gap-2">
|
||||
<Button
|
||||
v-if="hasUpdateListener && hasUpdate"
|
||||
type="quiet"
|
||||
color="green"
|
||||
class="flex items-center gap-2"
|
||||
@click="emit('update')"
|
||||
>
|
||||
<DownloadIcon class="!text-green" />
|
||||
<span class="font-semibold">{{ formatMessage(commonMessages.updateButton) }}</span>
|
||||
</Button>
|
||||
|
||||
<Button v-if="hasContentListener" type="base" @click="emit('content')">
|
||||
<BoxesIcon />
|
||||
{{ formatMessage(commonMessages.contentLabel) }}
|
||||
</Button>
|
||||
|
||||
<IconButton
|
||||
v-if="hasSettingsListener"
|
||||
v-tooltip="formatMessage(messages.installationSettingsTooltip)"
|
||||
type="outlined"
|
||||
:label="formatMessage(messages.installationSettingsTooltip)"
|
||||
@click="emit('settings')"
|
||||
>
|
||||
<Settings2Icon />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<!-- Collapsed actions visible at < 700px -->
|
||||
<div v-if="hasUpdate && hasUpdateListener" class="flex @[700px]:hidden">
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(commonMessages.updateButton)"
|
||||
type="quiet"
|
||||
color="green"
|
||||
:label="formatMessage(commonMessages.updateButton)"
|
||||
@click="emit('update')"
|
||||
>
|
||||
<DownloadIcon class="size-5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<TeleportOverflowMenu
|
||||
v-if="collapsedOptions.length"
|
||||
type="outlined"
|
||||
label="More options"
|
||||
:options="collapsedOptions"
|
||||
class="flex @[700px]:hidden"
|
||||
>
|
||||
<MoreVerticalIcon class="size-5" />
|
||||
<template #content>
|
||||
<BoxesIcon class="size-5" />
|
||||
{{ formatMessage(commonMessages.contentLabel) }}
|
||||
</template>
|
||||
<template #settings>
|
||||
<Settings2Icon class="size-5" />
|
||||
{{ formatMessage(messages.installationSettingsTooltip) }}
|
||||
</template>
|
||||
</TeleportOverflowMenu>
|
||||
|
||||
<TeleportOverflowMenu
|
||||
v-if="overflowOptions?.length"
|
||||
type="quiet"
|
||||
label="More options"
|
||||
class="hidden @[700px]:flex"
|
||||
:options="overflowOptions"
|
||||
>
|
||||
<MoreVerticalIcon class="size-5" />
|
||||
</TeleportOverflowMenu>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span v-if="project.description" class="text-secondary">
|
||||
{{ project.description }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-if="project.downloads != null || project.followers != null || categories?.length"
|
||||
class="flex flex-wrap items-center gap-3"
|
||||
>
|
||||
<div v-if="project.downloads != null" class="flex items-center gap-2 text-secondary">
|
||||
<DownloadIcon class="size-5" />
|
||||
<span class="font-medium">{{ formatCompact(project.downloads) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="project.followers != null" class="flex items-center gap-2 text-secondary">
|
||||
<HeartIcon class="size-5" />
|
||||
<span class="font-medium">{{ formatCompact(project.followers) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="categories?.length" class="flex flex-wrap items-center gap-1">
|
||||
<TagTagItem
|
||||
v-for="cat in categories"
|
||||
:key="cat.name"
|
||||
:tag="cat.name"
|
||||
:action="cat.action"
|
||||
hide-non-loader-icon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { EyeIcon, WrenchIcon } from '@modrinth/assets'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { Button, IconButton } from '#ui/components/base/buttons'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
|
||||
import type { ManagedContentCardData } from '../../types'
|
||||
import ManagedContentCardFooter from './managed-content-card-footer.vue'
|
||||
import ManagedContentCardSummary from './managed-content-card-summary.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: ManagedContentCardData
|
||||
disabled?: boolean
|
||||
disabledText?: string
|
||||
showViewContent?: boolean
|
||||
showSettings?: boolean
|
||||
showPrimaryAction?: boolean
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
disabledText: undefined,
|
||||
showViewContent: false,
|
||||
showSettings: false,
|
||||
showPrimaryAction: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'view-content': []
|
||||
settings: []
|
||||
'primary-action': [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
modpackContent: {
|
||||
id: 'content.managed-card.modpack-content',
|
||||
defaultMessage: 'Modpack content',
|
||||
},
|
||||
sharedContent: {
|
||||
id: 'content.managed-card.shared-content',
|
||||
defaultMessage: 'Shared content',
|
||||
},
|
||||
viewContent: {
|
||||
id: 'content.managed-card.view-content',
|
||||
defaultMessage: 'View content',
|
||||
},
|
||||
settings: {
|
||||
id: 'content.managed-card.settings',
|
||||
defaultMessage: 'Installation settings',
|
||||
},
|
||||
})
|
||||
|
||||
const title = computed(() =>
|
||||
formatMessage(props.data.kind === 'modpack' ? messages.modpackContent : messages.sharedContent),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="@container overflow-hidden rounded-[20px] border border-solid border-surface-4 shadow-[0_1px_1px_rgba(0,0,0,0.12)]"
|
||||
:aria-busy="disabled || undefined"
|
||||
>
|
||||
<div
|
||||
class="flex min-h-[94px] flex-col items-stretch justify-between gap-4 bg-surface-3 px-[18px] py-4 @[700px]:flex-row @[700px]:items-center"
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-1.5 pl-0.5">
|
||||
<h2 class="m-0 text-2xl font-semibold leading-8 text-contrast">{{ title }}</h2>
|
||||
<ManagedContentCardSummary :summary="data.summary" :installing="data.installing" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showViewContent || showSettings"
|
||||
class="flex shrink-0 items-center gap-2 self-start"
|
||||
>
|
||||
<Button
|
||||
v-if="showViewContent"
|
||||
type="base"
|
||||
size="lg"
|
||||
:disabled="disabled"
|
||||
@click="emit('view-content')"
|
||||
>
|
||||
<EyeIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.viewContent) }}
|
||||
</Button>
|
||||
<IconButton
|
||||
v-if="showSettings"
|
||||
type="outlined"
|
||||
size="lg"
|
||||
:disabled="disabled"
|
||||
:label="formatMessage(messages.settings)"
|
||||
@click="emit('settings')"
|
||||
>
|
||||
<WrenchIcon aria-hidden="true" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-surface-4" />
|
||||
|
||||
<ManagedContentCardFooter
|
||||
:data="data"
|
||||
:disabled="disabled"
|
||||
:disabled-text="disabledText"
|
||||
:show-primary-action="showPrimaryAction"
|
||||
@primary-action="emit('primary-action', $event)"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
ClockIcon,
|
||||
DownloadIcon,
|
||||
LockIcon,
|
||||
SpinnerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import AutoLink from '#ui/components/base/AutoLink.vue'
|
||||
import Avatar from '#ui/components/base/Avatar.vue'
|
||||
import BulletDivider from '#ui/components/base/BulletDivider.vue'
|
||||
import { Button } from '#ui/components/base/buttons'
|
||||
import { useFormatDateTime } from '#ui/composables/format-date-time'
|
||||
import { useRelativeTime } from '#ui/composables/how-ago'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
|
||||
import type { ManagedContentCardData } from '../../types'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: ManagedContentCardData
|
||||
showPrimaryAction?: boolean
|
||||
disabled?: boolean
|
||||
disabledText?: string
|
||||
}>(),
|
||||
{
|
||||
showPrimaryAction: false,
|
||||
disabled: false,
|
||||
disabledText: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'primary-action': [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatTimeAgo = useRelativeTime()
|
||||
const formatDateTime = useFormatDateTime({
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'short',
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
managedBy: {
|
||||
id: 'content.managed-card.managed-by',
|
||||
defaultMessage: 'Managed by',
|
||||
},
|
||||
server: {
|
||||
id: 'content.managed-card.server-suffix',
|
||||
defaultMessage: 'server',
|
||||
},
|
||||
updated: {
|
||||
id: 'content.managed-card.updated',
|
||||
defaultMessage: 'Updated {time}',
|
||||
},
|
||||
synced: {
|
||||
id: 'content.managed-card.synced',
|
||||
defaultMessage: 'Synced {time}',
|
||||
},
|
||||
switchVersion: {
|
||||
id: 'content.managed-card.switch-version',
|
||||
defaultMessage: 'Switch version',
|
||||
},
|
||||
updateAvailable: {
|
||||
id: 'content.managed-card.update-available',
|
||||
defaultMessage: 'Update available',
|
||||
},
|
||||
updating: {
|
||||
id: 'content.managed-card.updating',
|
||||
defaultMessage: 'Updating...',
|
||||
},
|
||||
})
|
||||
|
||||
const timestamp = computed(() => {
|
||||
if (props.data.kind === 'modpack') {
|
||||
return props.data.updatedAt
|
||||
}
|
||||
return props.data.syncedAt
|
||||
})
|
||||
|
||||
const timestampLabel = computed(() => {
|
||||
if (!timestamp.value) return null
|
||||
return formatMessage(props.data.kind === 'modpack' ? messages.updated : messages.synced, {
|
||||
time: formatTimeAgo(timestamp.value),
|
||||
})
|
||||
})
|
||||
|
||||
const timestampTooltip = computed(() =>
|
||||
timestamp.value ? formatDateTime(timestamp.value) : undefined,
|
||||
)
|
||||
|
||||
const showAction = computed(
|
||||
() =>
|
||||
props.showPrimaryAction &&
|
||||
(props.data.kind === 'modpack'
|
||||
? !!props.data.versionNumber
|
||||
: props.data.updateAvailable || props.disabled),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex min-h-14 flex-col items-start justify-between gap-x-4 gap-y-3 bg-surface-2 px-[18px] py-4 !font-semibold text-secondary @[700px]:flex-row @[700px]:items-center"
|
||||
>
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-1.5 @[700px]:flex-nowrap">
|
||||
<LockIcon aria-hidden="true" class="size-5 shrink-0" />
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-[5px] @[700px]:flex-nowrap">
|
||||
<span class="whitespace-nowrap">
|
||||
{{ formatMessage(messages.managedBy) }}
|
||||
</span>
|
||||
<AutoLink
|
||||
:to="data.manager.link"
|
||||
class="flex min-w-0 items-center gap-1.5 text-secondary"
|
||||
:class="data.manager.link ? 'hover:underline' : ''"
|
||||
>
|
||||
<Avatar
|
||||
:src="data.manager.iconUrl"
|
||||
:alt="data.manager.name"
|
||||
size="1.5rem"
|
||||
:circle="data.kind === 'shared-instance'"
|
||||
:tint-by="data.manager.name"
|
||||
no-shadow
|
||||
:class="data.kind === 'shared-instance' ? '' : '!rounded-[6px]'"
|
||||
/>
|
||||
<span class="break-words @[700px]:truncate @[700px]:whitespace-nowrap">
|
||||
{{ data.manager.name }}
|
||||
</span>
|
||||
</AutoLink>
|
||||
<span v-if="data.kind === 'server'" class="whitespace-nowrap">
|
||||
{{ formatMessage(messages.server) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="data.kind === 'modpack' && data.versionNumber">
|
||||
<BulletDivider class="!mx-0" />
|
||||
<AutoLink
|
||||
:to="data.versionLink"
|
||||
class="whitespace-nowrap text-secondary !decoration-secondary"
|
||||
:class="data.versionLink ? 'hover:underline' : ''"
|
||||
>
|
||||
{{ data.versionNumber }}
|
||||
</AutoLink>
|
||||
</template>
|
||||
|
||||
<template v-if="timestampLabel">
|
||||
<BulletDivider class="!mx-0" />
|
||||
<div
|
||||
v-tooltip="timestampTooltip"
|
||||
class="flex items-center whitespace-nowrap"
|
||||
:class="[data.kind === 'modpack' ? 'gap-2' : 'gap-1.5', 'cursor-help']"
|
||||
>
|
||||
<ClockIcon aria-hidden="true" class="size-5 shrink-0" />
|
||||
<span>{{ timestampLabel }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="showAction"
|
||||
type="quiet"
|
||||
:color="data.kind === 'modpack' ? undefined : 'green'"
|
||||
:disabled="disabled"
|
||||
class=""
|
||||
@click="emit('primary-action', $event)"
|
||||
>
|
||||
<SpinnerIcon v-if="disabled" aria-hidden="true" class="animate-spin" />
|
||||
<ArrowLeftRightIcon v-else-if="data.kind === 'modpack'" aria-hidden="true" />
|
||||
<DownloadIcon v-else aria-hidden="true" />
|
||||
<span>
|
||||
{{
|
||||
disabled
|
||||
? (disabledText ?? formatMessage(messages.updating))
|
||||
: data.kind === 'modpack'
|
||||
? formatMessage(messages.switchVersion)
|
||||
: formatMessage(messages.updateAvailable)
|
||||
}}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { BoxIcon, BracesIcon, GlassesIcon, PaintbrushIcon, PlugIcon } from '@modrinth/assets'
|
||||
import { type Component, computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import BulletDivider from '#ui/components/base/BulletDivider.vue'
|
||||
import { useFormatNumber } from '#ui/composables/format-number'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { formatProjectTypeSentence } from '#ui/utils/common-messages'
|
||||
|
||||
import type { ManagedContentSummaryItem, ManagedContentSummaryType } from '../../types'
|
||||
|
||||
const props = defineProps<{
|
||||
summary?: ManagedContentSummaryItem[]
|
||||
installing?: boolean
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatNumber = useFormatNumber()
|
||||
|
||||
const messages = defineMessages({
|
||||
loading: {
|
||||
id: 'content.managed-card.summary.loading',
|
||||
defaultMessage: 'Loading managed content summary',
|
||||
},
|
||||
empty: {
|
||||
id: 'content.managed-card.summary.empty',
|
||||
defaultMessage: 'No managed content',
|
||||
},
|
||||
installing: {
|
||||
id: 'content.managed-card.summary.installing',
|
||||
defaultMessage: 'Installing content...',
|
||||
},
|
||||
})
|
||||
|
||||
const icons: Record<ManagedContentSummaryType, Component> = {
|
||||
mod: BoxIcon,
|
||||
plugin: PlugIcon,
|
||||
datapack: BracesIcon,
|
||||
resourcepack: PaintbrushIcon,
|
||||
shader: GlassesIcon,
|
||||
}
|
||||
|
||||
const MINIMUM_LOADING_TIME = 500
|
||||
const minimumLoadingVisible = ref(false)
|
||||
let minimumLoadingTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
watch(
|
||||
() => props.summary === undefined,
|
||||
(isLoading) => {
|
||||
if (!isLoading) return
|
||||
|
||||
minimumLoadingVisible.value = true
|
||||
clearTimeout(minimumLoadingTimer)
|
||||
minimumLoadingTimer = setTimeout(() => {
|
||||
minimumLoadingVisible.value = false
|
||||
}, MINIMUM_LOADING_TIME)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => clearTimeout(minimumLoadingTimer))
|
||||
|
||||
const showLoading = computed(() => props.summary === undefined || minimumLoadingVisible.value)
|
||||
const visibleSummary = computed(() => props.summary?.filter((item) => item.count > 0))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="!installing && showLoading"
|
||||
class="flex h-6 items-center gap-2"
|
||||
role="status"
|
||||
:aria-label="formatMessage(messages.loading)"
|
||||
>
|
||||
<div class="h-5 w-20 animate-pulse rounded bg-surface-4" />
|
||||
<BulletDivider class="!mx-0" />
|
||||
<div class="h-5 w-32 animate-pulse rounded bg-surface-4" />
|
||||
<BulletDivider class="!mx-0" />
|
||||
<div class="h-5 w-20 animate-pulse rounded bg-surface-4" />
|
||||
</div>
|
||||
|
||||
<span v-else-if="installing" class="font-medium leading-6 text-secondary" role="status">
|
||||
{{ formatMessage(messages.installing) }}
|
||||
</span>
|
||||
|
||||
<div v-else-if="visibleSummary?.length" class="flex flex-wrap items-center gap-2 text-secondary">
|
||||
<template v-for="(item, index) in visibleSummary" :key="item.type">
|
||||
<BulletDivider v-if="index > 0" class="!mx-0" />
|
||||
<div class="flex items-center gap-1.5 whitespace-nowrap">
|
||||
<component :is="icons[item.type]" aria-hidden="true" class="size-5 shrink-0" />
|
||||
<span class="font-medium leading-6">
|
||||
{{ formatNumber(item.count) }}
|
||||
{{ formatProjectTypeSentence(formatMessage, item.type, item.count) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<span v-else class="font-medium leading-6 text-secondary">
|
||||
{{ formatMessage(messages.empty) }}
|
||||
</span>
|
||||
</template>
|
||||
+58
-85
@@ -3,7 +3,6 @@ import {
|
||||
ArrowLeftRightIcon,
|
||||
BoxIcon,
|
||||
ExternalIcon,
|
||||
FilterIcon,
|
||||
GlassesIcon,
|
||||
PaintbrushIcon,
|
||||
SearchIcon,
|
||||
@@ -17,6 +16,7 @@ import BulletDivider from '#ui/components/base/BulletDivider.vue'
|
||||
import type { OverflowMenuOption } from '#ui/components/base/buttons'
|
||||
import { ButtonLink } from '#ui/components/base/buttons'
|
||||
import Checkbox from '#ui/components/base/Checkbox.vue'
|
||||
import FilterPills from '#ui/components/base/FilterPills.vue'
|
||||
import StyledInput from '#ui/components/base/StyledInput.vue'
|
||||
import NewModal from '#ui/components/modal/NewModal.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
normalizeProjectType,
|
||||
} from '#ui/utils/common-messages'
|
||||
|
||||
import { getClientWarningType, isClientOnlyEnvironment } from '../../composables/content-filtering'
|
||||
import { getClientWarningType } from '../../composables/content-filtering'
|
||||
import type { ContentCardProject, ContentCardTableItem, ContentItem } from '../../types'
|
||||
import ContentCardTable from '../ContentCardTable.vue'
|
||||
import ContentSelectionBar from '../ContentSelectionBar.vue'
|
||||
@@ -38,24 +38,26 @@ const pageContext = injectPageContext(null)
|
||||
|
||||
interface Props {
|
||||
header?: string
|
||||
modpackName?: string
|
||||
modpackIconUrl?: string
|
||||
sourceName?: string
|
||||
sourceIconUrl?: string
|
||||
enableToggle?: boolean
|
||||
actionDisabled?: boolean
|
||||
actionDisabledTooltip?: string | null
|
||||
getOverflowOptions?: (item: ContentItem) => OverflowMenuOption[]
|
||||
switchVersion?: (item: ContentItem) => void
|
||||
showEnvironmentWarnings?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
header: undefined,
|
||||
modpackName: undefined,
|
||||
modpackIconUrl: undefined,
|
||||
sourceName: undefined,
|
||||
sourceIconUrl: undefined,
|
||||
enableToggle: false,
|
||||
actionDisabled: false,
|
||||
actionDisabledTooltip: undefined,
|
||||
getOverflowOptions: undefined,
|
||||
switchVersion: undefined,
|
||||
showEnvironmentWarnings: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -67,44 +69,44 @@ const emit = defineEmits<{
|
||||
|
||||
const messages = defineMessages({
|
||||
header: {
|
||||
id: 'instances.modpack-content-modal.header',
|
||||
defaultMessage: 'Modpack content',
|
||||
id: 'instances.managed-content-modal.header',
|
||||
defaultMessage: 'Managed content',
|
||||
},
|
||||
searchPlaceholder: {
|
||||
id: 'instances.modpack-content-modal.search-placeholder',
|
||||
id: 'instances.managed-content-modal.search-placeholder',
|
||||
defaultMessage: 'Search {count, number} {count, plural, one {project} other {projects}}',
|
||||
},
|
||||
loading: {
|
||||
id: 'instances.modpack-content-modal.loading',
|
||||
id: 'instances.managed-content-modal.loading',
|
||||
defaultMessage: 'Loading content...',
|
||||
},
|
||||
emptyTitle: {
|
||||
id: 'instances.modpack-content-modal.empty-title',
|
||||
id: 'instances.managed-content-modal.empty-title',
|
||||
defaultMessage: 'No content found',
|
||||
},
|
||||
emptyDescription: {
|
||||
id: 'instances.modpack-content-modal.empty-description',
|
||||
defaultMessage: 'This modpack does not include any additional content.',
|
||||
id: 'instances.managed-content-modal.empty-description',
|
||||
defaultMessage: 'This source does not include any managed content.',
|
||||
},
|
||||
noResults: {
|
||||
id: 'instances.modpack-content-modal.no-results',
|
||||
id: 'instances.managed-content-modal.no-results',
|
||||
defaultMessage: 'No projects match your search.',
|
||||
},
|
||||
externalContent: {
|
||||
id: 'instances.modpack-content-modal.external-content',
|
||||
id: 'instances.managed-content-modal.external-content',
|
||||
defaultMessage: 'External',
|
||||
},
|
||||
externalContentDescription: {
|
||||
id: 'instances.modpack-content-modal.external-content-description',
|
||||
id: 'instances.managed-content-modal.external-content-description',
|
||||
defaultMessage: 'This file is not published on Modrinth.',
|
||||
},
|
||||
openInSlicer: {
|
||||
id: 'instances.modpack-content-modal.open-in-slicer',
|
||||
id: 'instances.managed-content-modal.open-in-slicer',
|
||||
defaultMessage: 'Open in Slicer',
|
||||
},
|
||||
})
|
||||
|
||||
export interface ModpackContentModalState {
|
||||
export interface ManagedContentModalState {
|
||||
items: ContentItem[]
|
||||
searchQuery: string
|
||||
selectedFilters: string[]
|
||||
@@ -124,6 +126,7 @@ const selectedIds = ref<string[]>([])
|
||||
const selectedItems = computed(() =>
|
||||
items.value.filter((item) => selectedIds.value.includes(item.id)),
|
||||
)
|
||||
const toggleableSelectedItems = computed(() => selectedItems.value)
|
||||
|
||||
const allSelected = computed(() => {
|
||||
if (filteredItems.value.length === 0) return false
|
||||
@@ -173,7 +176,9 @@ const filterOptions = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
if (items.value.some((item) => getClientWarningType(item) !== null)) {
|
||||
if (
|
||||
items.value.some((item) => getClientWarningType(item, props.showEnvironmentWarnings) !== null)
|
||||
) {
|
||||
options.push({ id: 'warnings', label: 'Warnings' })
|
||||
}
|
||||
|
||||
@@ -193,15 +198,6 @@ const stats = computed(() => {
|
||||
return counts
|
||||
})
|
||||
|
||||
function toggleFilter(filterId: string) {
|
||||
const index = selectedFilters.value.indexOf(filterId)
|
||||
if (index === -1) {
|
||||
selectedFilters.value.push(filterId)
|
||||
} else {
|
||||
selectedFilters.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const attributeFilterIds = new Set(['disabled', 'warnings'])
|
||||
|
||||
const typeFilteredCount = computed(() => {
|
||||
@@ -213,7 +209,8 @@ const typeFilteredCount = computed(() => {
|
||||
if (typeFilters.length > 0 && !typeFilters.includes(normalizeProjectType(item.project_type)))
|
||||
return false
|
||||
if (hasDisabledFilter && item.enabled) return false
|
||||
if (hasWarningsFilter && getClientWarningType(item) === null) return false
|
||||
if (hasWarningsFilter && getClientWarningType(item, props.showEnvironmentWarnings) === null)
|
||||
return false
|
||||
return true
|
||||
}).length
|
||||
})
|
||||
@@ -236,7 +233,8 @@ const filteredItems = computed(() => {
|
||||
if (typeFilters.length > 0 && !typeFilters.includes(normalizeProjectType(item.project_type)))
|
||||
return false
|
||||
if (hasDisabledFilter && item.enabled) return false
|
||||
if (hasWarningsFilter && getClientWarningType(item) === null) return false
|
||||
if (hasWarningsFilter && getClientWarningType(item, props.showEnvironmentWarnings) === null)
|
||||
return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -244,19 +242,24 @@ const filteredItems = computed(() => {
|
||||
return sortContentItems(result, !query)
|
||||
})
|
||||
|
||||
function contentVersionLabel(item: ContentItem): string {
|
||||
if (item.embedded_metadata?.version) return item.embedded_metadata.version
|
||||
return formatMessage(commonMessages.unknownLabel)
|
||||
}
|
||||
|
||||
const tableItems = computed<ContentCardTableItem[]>(() =>
|
||||
filteredItems.value.map((item) => ({
|
||||
id: item.id,
|
||||
project: item.project ?? {
|
||||
id: item.id,
|
||||
slug: null,
|
||||
title: item.file_name,
|
||||
icon_url: null,
|
||||
title: item.embedded_metadata?.name ?? item.file_name,
|
||||
icon_url: item.embedded_metadata?.icon_url ?? null,
|
||||
},
|
||||
projectLink: !item.external && item.project?.id ? `/project/${item.project.id}` : undefined,
|
||||
version: item.version ?? {
|
||||
id: item.id,
|
||||
version_number: 'Unknown',
|
||||
version_number: contentVersionLabel(item),
|
||||
file_name: item.file_name,
|
||||
},
|
||||
owner: item.owner
|
||||
@@ -275,19 +278,17 @@ const tableItems = computed<ContentCardTableItem[]>(() =>
|
||||
}
|
||||
: undefined,
|
||||
...(props.enableToggle ? { enabled: item.enabled } : {}),
|
||||
locked: item.locked,
|
||||
installing: item.installing === true,
|
||||
toggleDisabled: props.actionDisabled,
|
||||
toggleDisabledTooltip: props.actionDisabled ? props.actionDisabledTooltip : undefined,
|
||||
isClientOnly:
|
||||
isClientOnlyEnvironment(item.environment) ||
|
||||
!!item.pack_client_retained ||
|
||||
!!item.pack_client_depends,
|
||||
clientWarning: getClientWarningType(item),
|
||||
isClientOnly: getClientWarningType(item, props.showEnvironmentWarnings) !== null,
|
||||
clientWarning: getClientWarningType(item, props.showEnvironmentWarnings),
|
||||
disabled:
|
||||
props.actionDisabled || disabledIds.value.has(item.file_name) || item.installing === true,
|
||||
disabledTooltip: props.actionDisabled ? props.actionDisabledTooltip : undefined,
|
||||
overflowOptions: [
|
||||
...(props.switchVersion
|
||||
...(props.switchVersion && !item.locked
|
||||
? [
|
||||
{
|
||||
id: 'switch-version',
|
||||
@@ -359,13 +360,13 @@ function handleEnabledChange(id: string, value: boolean) {
|
||||
|
||||
function bulkEnable() {
|
||||
if (props.actionDisabled) return
|
||||
emit('bulk:enable', [...selectedItems.value])
|
||||
emit('bulk:enable', [...toggleableSelectedItems.value])
|
||||
selectedIds.value = []
|
||||
}
|
||||
|
||||
function bulkDisable() {
|
||||
if (props.actionDisabled) return
|
||||
emit('bulk:disable', [...selectedItems.value])
|
||||
emit('bulk:disable', [...toggleableSelectedItems.value])
|
||||
selectedIds.value = []
|
||||
}
|
||||
|
||||
@@ -404,7 +405,7 @@ function handleHide() {
|
||||
emit('hide')
|
||||
}
|
||||
|
||||
function getState(): ModpackContentModalState | null {
|
||||
function getState(): ManagedContentModalState | null {
|
||||
if (!items.value.length) return null
|
||||
return {
|
||||
items: items.value,
|
||||
@@ -414,7 +415,7 @@ function getState(): ModpackContentModalState | null {
|
||||
}
|
||||
}
|
||||
|
||||
async function restore(state: ModpackContentModalState) {
|
||||
async function restore(state: ManagedContentModalState) {
|
||||
items.value = state.items.map((item) => ({ ...item }))
|
||||
searchQuery.value = state.searchQuery
|
||||
selectedFilters.value = state.selectedFilters
|
||||
@@ -466,10 +467,11 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
||||
>
|
||||
<template #title>
|
||||
<Avatar
|
||||
v-if="props.modpackIconUrl"
|
||||
:src="props.modpackIconUrl"
|
||||
v-if="props.sourceIconUrl"
|
||||
:src="props.sourceIconUrl"
|
||||
:alt="props.sourceName"
|
||||
size="3rem"
|
||||
:tint-by="props.modpackName"
|
||||
:tint-by="props.sourceName"
|
||||
/>
|
||||
<span class="text-lg font-extrabold text-contrast">
|
||||
{{ props.header ?? formatMessage(messages.header) }}
|
||||
@@ -484,43 +486,18 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
||||
clearable
|
||||
/>
|
||||
|
||||
<!-- Filters -->
|
||||
<div v-if="filterOptions.length > 0" class="flex items-center gap-2">
|
||||
<FilterIcon class="size-5 text-secondary shrink-0" />
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<button
|
||||
:aria-pressed="selectedFilters.length === 0"
|
||||
class="rounded-full border border-solid px-3 py-1.5 text-base font-semibold leading-5 transition-colors"
|
||||
:class="
|
||||
selectedFilters.length === 0
|
||||
? 'border-brand bg-brand-highlight text-brand'
|
||||
: 'border-surface-5 bg-surface-4 text-primary hover:bg-surface-5'
|
||||
"
|
||||
@click="selectedFilters = []"
|
||||
>
|
||||
{{ formatMessage(commonMessages.allProjectType) }}
|
||||
</button>
|
||||
<button
|
||||
v-for="option in filterOptions"
|
||||
:key="option.id"
|
||||
:aria-pressed="selectedFilters.includes(option.id)"
|
||||
class="rounded-full border border-solid px-3 py-1.5 text-base font-semibold leading-5 transition-colors"
|
||||
:class="
|
||||
selectedFilters.includes(option.id)
|
||||
? 'border-brand bg-brand-highlight text-brand'
|
||||
: 'border-surface-5 bg-surface-4 text-primary hover:bg-surface-5'
|
||||
"
|
||||
@click="toggleFilter(option.id)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<FilterPills
|
||||
v-if="filterOptions.length > 0"
|
||||
v-model="selectedFilters"
|
||||
:options="filterOptions"
|
||||
>
|
||||
<template #all>
|
||||
{{ formatMessage(commonMessages.allProjectType) }}
|
||||
</template>
|
||||
</FilterPills>
|
||||
</div>
|
||||
|
||||
<!-- Content area -->
|
||||
<div class="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||
<!-- Loading state -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="flex flex-col items-center justify-center flex-1 gap-2 text-secondary"
|
||||
@@ -529,7 +506,6 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
||||
<span class="text-sm">{{ formatMessage(messages.loading) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div
|
||||
v-else-if="items.length === 0"
|
||||
class="flex flex-col items-center justify-center flex-1 gap-2 text-center p-8"
|
||||
@@ -540,7 +516,6 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
||||
<span class="text-secondary">{{ formatMessage(messages.emptyDescription) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- No search results -->
|
||||
<div
|
||||
v-else-if="filteredItems.length === 0"
|
||||
class="flex flex-col items-center justify-center flex-1 gap-2 text-center p-8"
|
||||
@@ -548,7 +523,6 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
||||
<span class="text-secondary">{{ formatMessage(messages.noResults) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Content table -->
|
||||
<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"
|
||||
@@ -630,11 +604,9 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div
|
||||
class="flex items-center justify-between px-6 py-4 border-t border-solid border-0 border-surface-4 shrink-0"
|
||||
>
|
||||
<!-- Stats -->
|
||||
<div class="flex items-center gap-2">
|
||||
<template v-for="(count, type, idx) in stats" :key="type">
|
||||
<BulletDivider v-if="idx > 0" />
|
||||
@@ -662,6 +634,7 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
||||
<ContentSelectionBar
|
||||
v-if="props.enableToggle"
|
||||
:selected-items="selectedItems"
|
||||
:toggle-items="toggleableSelectedItems"
|
||||
:is-busy="props.actionDisabled"
|
||||
:busy-tooltip="props.actionDisabledTooltip"
|
||||
:hide-when-modal-open="false"
|
||||
@@ -13,10 +13,13 @@ export function isClientOnlyEnvironment(env?: string | null): boolean {
|
||||
return !!env && CLIENT_ONLY_ENVIRONMENTS.has(env)
|
||||
}
|
||||
|
||||
export function getClientWarningType(item: ContentItem): ClientWarningType | null {
|
||||
export function getClientWarningType(
|
||||
item: ContentItem,
|
||||
showEnvironmentWarnings = false,
|
||||
): ClientWarningType | null {
|
||||
if (item.pack_client_retained) return 'retained'
|
||||
if (item.pack_client_depends) return 'depends'
|
||||
if (isClientOnlyEnvironment(item.environment)) return 'environment'
|
||||
if (showEnvironmentWarnings && isClientOnlyEnvironment(item.environment)) return 'environment'
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -29,6 +32,8 @@ export interface ContentFilterConfig {
|
||||
showTypeFilters?: boolean
|
||||
showUpdateFilter?: boolean
|
||||
showWarningsFilter?: boolean
|
||||
showStatusFilters?: boolean
|
||||
showEnvironmentWarnings?: boolean
|
||||
isPackLocked?: Ref<boolean>
|
||||
persistKey?: string
|
||||
}
|
||||
@@ -88,15 +93,22 @@ export function useContentFilters(items: Ref<ContentItem[]>, config?: ContentFil
|
||||
options.push({ id: 'updates', label: formatMessage(messages.updates) })
|
||||
}
|
||||
|
||||
if (config?.showWarningsFilter && items.value.some((m) => getClientWarningType(m) !== null)) {
|
||||
if (
|
||||
config?.showWarningsFilter &&
|
||||
items.value.some(
|
||||
(item) => getClientWarningType(item, config.showEnvironmentWarnings) !== null,
|
||||
)
|
||||
) {
|
||||
options.push({ id: 'warnings', label: formatMessage(messages.warnings) })
|
||||
}
|
||||
|
||||
for (const status of availableStatusFilters.value) {
|
||||
options.push({
|
||||
id: status,
|
||||
label: formatMessage(status === 'enabled' ? messages.enabled : messages.disabled),
|
||||
})
|
||||
if (config?.showStatusFilters !== false) {
|
||||
for (const status of availableStatusFilters.value) {
|
||||
options.push({
|
||||
id: status,
|
||||
label: formatMessage(status === 'enabled' ? messages.enabled : messages.disabled),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
@@ -154,7 +166,11 @@ export function useContentFilters(items: Ref<ContentItem[]>, config?: ContentFil
|
||||
if (filter === 'updates' && !item.has_update) return false
|
||||
if (filter === 'enabled' && !item.enabled) return false
|
||||
if (filter === 'disabled' && item.enabled) return false
|
||||
if (filter === 'warnings' && getClientWarningType(item) === null) return false
|
||||
if (
|
||||
filter === 'warnings' &&
|
||||
getClientWarningType(item, config?.showEnvironmentWarnings) === null
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -3,4 +3,5 @@ export * from './changing-items'
|
||||
export * from './content-filtering'
|
||||
export * from './content-search'
|
||||
export * from './content-selection'
|
||||
export * from './use-content-metadata-filters'
|
||||
export * from './use-inline-backup'
|
||||
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
import { useSessionStorage } from '@vueuse/core'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import type {
|
||||
DropdownFilterBarCategory,
|
||||
DropdownFilterBarOption,
|
||||
} from '#ui/components/base/DropdownFilterBar.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
|
||||
import type { ContentItem } from '../types'
|
||||
import { getClientWarningType } from './content-filtering'
|
||||
|
||||
export type ContentMetadataFilterValue = Record<string, string[]>
|
||||
|
||||
interface MetadataFilterDefinition {
|
||||
key: string
|
||||
label: string
|
||||
searchable?: boolean
|
||||
direct?: boolean
|
||||
options?: DropdownFilterBarOption[]
|
||||
values: (item: ContentItem) => DropdownFilterBarOption[]
|
||||
}
|
||||
|
||||
interface ContentMetadataFilterConfig {
|
||||
showSharedContent?: Ref<boolean> | Readonly<Ref<boolean>>
|
||||
showEnvironmentWarnings?: boolean
|
||||
}
|
||||
|
||||
const openSourceLicenseIds = new Set([
|
||||
'0BSD',
|
||||
'AFL-3.0',
|
||||
'AGPL-3.0',
|
||||
'Apache-2.0',
|
||||
'Artistic-2.0',
|
||||
'BSD-2-Clause',
|
||||
'BSD-3-Clause',
|
||||
'BSL-1.0',
|
||||
'CDDL-1.0',
|
||||
'ECL-2.0',
|
||||
'EPL-1.0',
|
||||
'EPL-2.0',
|
||||
'EUPL-1.1',
|
||||
'EUPL-1.2',
|
||||
'GPL-2.0',
|
||||
'GPL-3.0',
|
||||
'ISC',
|
||||
'LGPL-2.1',
|
||||
'LGPL-3.0',
|
||||
'MIT',
|
||||
'MPL-2.0',
|
||||
'NCSA',
|
||||
'OSL-3.0',
|
||||
'PostgreSQL',
|
||||
'Python-2.0',
|
||||
'Unlicense',
|
||||
'UPL-1.0',
|
||||
'Zlib',
|
||||
])
|
||||
|
||||
type EnvironmentFilterValue = 'client' | 'server' | 'client_and_server' | 'singleplayer'
|
||||
|
||||
function getEnvironmentFilterValue(
|
||||
environment?: ContentItem['environment'],
|
||||
): EnvironmentFilterValue | undefined {
|
||||
switch (environment) {
|
||||
case 'client_only':
|
||||
return 'client'
|
||||
case 'server_only':
|
||||
case 'dedicated_server_only':
|
||||
return 'server'
|
||||
case 'client_and_server':
|
||||
case 'client_only_server_optional':
|
||||
case 'server_only_client_optional':
|
||||
case 'client_or_server':
|
||||
case 'client_or_server_prefers_both':
|
||||
return 'client_and_server'
|
||||
case 'singleplayer_only':
|
||||
return 'singleplayer'
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
author: {
|
||||
id: 'content.metadata-filter.author',
|
||||
defaultMessage: 'Author',
|
||||
},
|
||||
openSource: {
|
||||
id: 'content.metadata-filter.open-source',
|
||||
defaultMessage: 'Open source',
|
||||
},
|
||||
environment: {
|
||||
id: 'content.metadata-filter.environment',
|
||||
defaultMessage: 'Environment',
|
||||
},
|
||||
clientSideOnly: {
|
||||
id: 'project.settings.environment.client_only.title',
|
||||
defaultMessage: 'Client-side only',
|
||||
},
|
||||
serverSideOnly: {
|
||||
id: 'project.settings.environment.server_only.title',
|
||||
defaultMessage: 'Server-side only',
|
||||
},
|
||||
clientAndServer: {
|
||||
id: 'project.settings.environment.client_and_server.title',
|
||||
defaultMessage: 'Client and server',
|
||||
},
|
||||
singleplayerOnly: {
|
||||
id: 'project.settings.environment.singleplayer.title',
|
||||
defaultMessage: 'Singleplayer only',
|
||||
},
|
||||
state: {
|
||||
id: 'content.metadata-filter.state',
|
||||
defaultMessage: 'State',
|
||||
},
|
||||
updates: {
|
||||
id: 'content.metadata-filter.updates',
|
||||
defaultMessage: 'Updates',
|
||||
},
|
||||
warnings: {
|
||||
id: 'content.metadata-filter.warnings',
|
||||
defaultMessage: 'Warnings',
|
||||
},
|
||||
enabled: {
|
||||
id: 'content.metadata-filter.state.enabled',
|
||||
defaultMessage: 'Enabled',
|
||||
},
|
||||
disabled: {
|
||||
id: 'content.metadata-filter.state.disabled',
|
||||
defaultMessage: 'Disabled',
|
||||
},
|
||||
updateAvailable: {
|
||||
id: 'content.metadata-filter.update.available',
|
||||
defaultMessage: 'Update available',
|
||||
},
|
||||
upToDate: {
|
||||
id: 'content.metadata-filter.update.up-to-date',
|
||||
defaultMessage: 'Up to date',
|
||||
},
|
||||
clientRetained: {
|
||||
id: 'content.metadata-filter.warning.client-retained',
|
||||
defaultMessage: 'Client file retained',
|
||||
},
|
||||
clientDepends: {
|
||||
id: 'content.metadata-filter.warning.client-depends',
|
||||
defaultMessage: 'Client depends on file',
|
||||
},
|
||||
clientOnly: {
|
||||
id: 'content.metadata-filter.warning.client-only',
|
||||
defaultMessage: 'Client-only content',
|
||||
},
|
||||
noWarnings: {
|
||||
id: 'content.metadata-filter.warning.none',
|
||||
defaultMessage: 'No warnings',
|
||||
},
|
||||
external: {
|
||||
id: 'content.metadata-filter.source.external',
|
||||
defaultMessage: 'External',
|
||||
},
|
||||
sharedContent: {
|
||||
id: 'content.metadata-filter.shared-content',
|
||||
defaultMessage: 'Shared content',
|
||||
},
|
||||
})
|
||||
|
||||
export function useContentMetadataFilters(
|
||||
items: Ref<ContentItem[]>,
|
||||
persistKey?: string,
|
||||
config?: ContentMetadataFilterConfig,
|
||||
) {
|
||||
const { formatMessage } = useVIntl()
|
||||
const selectedMetadataFilters = persistKey
|
||||
? useSessionStorage<ContentMetadataFilterValue>(`content-metadata-filters:${persistKey}`, {})
|
||||
: ref<ContentMetadataFilterValue>({})
|
||||
|
||||
function option(value: string, label: string, searchTerms?: string[]): DropdownFilterBarOption {
|
||||
return { value, label, searchTerms }
|
||||
}
|
||||
|
||||
function isOpenSource(item: ContentItem) {
|
||||
const licenseId = item.project?.license?.id.replace(/-(?:only|or-later)$/, '')
|
||||
return !!licenseId && openSourceLicenseIds.has(licenseId)
|
||||
}
|
||||
|
||||
function isExternal(item: ContentItem) {
|
||||
return item.external || !item.project?.license
|
||||
}
|
||||
|
||||
function getEnvironmentFilterLabel(value: EnvironmentFilterValue) {
|
||||
switch (value) {
|
||||
case 'client':
|
||||
return formatMessage(messages.clientSideOnly)
|
||||
case 'server':
|
||||
return formatMessage(messages.serverSideOnly)
|
||||
case 'client_and_server':
|
||||
return formatMessage(messages.clientAndServer)
|
||||
case 'singleplayer':
|
||||
return formatMessage(messages.singleplayerOnly)
|
||||
}
|
||||
}
|
||||
|
||||
const definitions = computed<MetadataFilterDefinition[]>(() => [
|
||||
{
|
||||
key: 'author',
|
||||
label: formatMessage(messages.author),
|
||||
searchable: true,
|
||||
values: (item) =>
|
||||
item.owner
|
||||
? [option(`${item.owner.type}:${item.owner.id}`, item.owner.name, [item.owner.id])]
|
||||
: [],
|
||||
},
|
||||
{
|
||||
key: 'environment',
|
||||
label: formatMessage(messages.environment),
|
||||
options: [
|
||||
option('client', getEnvironmentFilterLabel('client')),
|
||||
option('server', getEnvironmentFilterLabel('server')),
|
||||
option('client_and_server', getEnvironmentFilterLabel('client_and_server')),
|
||||
option('singleplayer', getEnvironmentFilterLabel('singleplayer')),
|
||||
],
|
||||
values: (item) => {
|
||||
const value = getEnvironmentFilterValue(item.environment)
|
||||
return value ? [option(value, getEnvironmentFilterLabel(value))] : []
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
label: formatMessage(messages.state),
|
||||
values: (item) =>
|
||||
item.enabled === undefined
|
||||
? []
|
||||
: [
|
||||
item.enabled
|
||||
? option('enabled', formatMessage(messages.enabled))
|
||||
: option('disabled', formatMessage(messages.disabled)),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'updates',
|
||||
label: formatMessage(messages.updates),
|
||||
values: (item) => [
|
||||
item.has_update
|
||||
? option('available', formatMessage(messages.updateAvailable))
|
||||
: option('current', formatMessage(messages.upToDate)),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'warnings',
|
||||
label: formatMessage(messages.warnings),
|
||||
values: (item) => {
|
||||
const warning = getClientWarningType(item, config?.showEnvironmentWarnings)
|
||||
switch (warning) {
|
||||
case 'retained':
|
||||
return [option(warning, formatMessage(messages.clientRetained))]
|
||||
case 'depends':
|
||||
return [option(warning, formatMessage(messages.clientDepends))]
|
||||
case 'environment':
|
||||
return [option(warning, formatMessage(messages.clientOnly))]
|
||||
default:
|
||||
return [option('none', formatMessage(messages.noWarnings))]
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'open_source',
|
||||
label: formatMessage(messages.openSource),
|
||||
direct: true,
|
||||
values: (item) =>
|
||||
isOpenSource(item) ? [option('open_source', formatMessage(messages.openSource))] : [],
|
||||
},
|
||||
{
|
||||
key: 'external',
|
||||
label: formatMessage(messages.external),
|
||||
direct: true,
|
||||
values: (item) =>
|
||||
isExternal(item) ? [option('external', formatMessage(messages.external))] : [],
|
||||
},
|
||||
...(config?.showSharedContent?.value
|
||||
? [
|
||||
{
|
||||
key: 'shared_content',
|
||||
label: formatMessage(messages.sharedContent),
|
||||
direct: true,
|
||||
values: (item: ContentItem) =>
|
||||
['server_project', 'shared_instance'].includes(item.source_kind ?? '')
|
||||
? [option('shared_content', formatMessage(messages.sharedContent))]
|
||||
: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
])
|
||||
|
||||
const metadataFilterCategories = computed<DropdownFilterBarCategory[]>(() =>
|
||||
definitions.value
|
||||
.map((definition) => {
|
||||
let visibleOptions = definition.options
|
||||
if (!visibleOptions) {
|
||||
const options = new Map<string, DropdownFilterBarOption>()
|
||||
const optionMatchCounts = new Map<string, number>()
|
||||
for (const item of items.value) {
|
||||
const itemValues = new Map(
|
||||
definition.values(item).map((value) => [value.value, value] as const),
|
||||
)
|
||||
for (const value of itemValues.values()) {
|
||||
if (!options.has(value.value)) options.set(value.value, value)
|
||||
optionMatchCounts.set(value.value, (optionMatchCounts.get(value.value) ?? 0) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
visibleOptions = [...options.values()]
|
||||
.filter((option) => optionMatchCounts.get(option.value) !== items.value.length)
|
||||
.sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }))
|
||||
}
|
||||
|
||||
return {
|
||||
key: definition.key,
|
||||
label: definition.label,
|
||||
direct: definition.direct,
|
||||
searchable: definition.searchable,
|
||||
options: visibleOptions,
|
||||
}
|
||||
})
|
||||
.filter((category) => category.options.some((option) => !('type' in option))),
|
||||
)
|
||||
|
||||
watch(
|
||||
metadataFilterCategories,
|
||||
(categories) => {
|
||||
if (items.value.length === 0) return
|
||||
const availableValues = new Map(
|
||||
categories.map((category) => [
|
||||
category.key,
|
||||
new Set(
|
||||
category.options
|
||||
.filter((item): item is DropdownFilterBarOption => !('type' in item))
|
||||
.map((item) => item.value),
|
||||
),
|
||||
]),
|
||||
)
|
||||
const nextFilters: ContentMetadataFilterValue = {}
|
||||
for (const [key, values] of Object.entries(selectedMetadataFilters.value)) {
|
||||
const validValues = values.filter((value) => availableValues.get(key)?.has(value))
|
||||
if (validValues.length > 0) nextFilters[key] = validValues
|
||||
}
|
||||
if (JSON.stringify(nextFilters) !== JSON.stringify(selectedMetadataFilters.value)) {
|
||||
selectedMetadataFilters.value = nextFilters
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function applyMetadataFilters(source: ContentItem[]) {
|
||||
const activeFilters = Object.entries(selectedMetadataFilters.value).filter(
|
||||
([, values]) => values.length > 0,
|
||||
)
|
||||
if (activeFilters.length === 0) return source
|
||||
|
||||
const definitionsByKey = new Map(
|
||||
definitions.value.map((definition) => [definition.key, definition]),
|
||||
)
|
||||
return source.filter((item) =>
|
||||
activeFilters.every(([key, selectedValues]) => {
|
||||
const definition = definitionsByKey.get(key)
|
||||
if (!definition) return true
|
||||
const itemValues = definition.values(item).map((value) => value.value)
|
||||
return itemValues.some((value) => selectedValues.includes(value))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
selectedMetadataFilters,
|
||||
metadataFilterCategories,
|
||||
applyMetadataFilters,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
export { default as ContentCardItem } from './components/ContentCardItem.vue'
|
||||
export { default as ContentCard } from './components/ContentCardItem.vue'
|
||||
export { default as ContentCardTable } from './components/ContentCardTable.vue'
|
||||
export { default as ContentModpackCard } from './components/ContentModpackCard.vue'
|
||||
export { default as ManagedContentCard } from './components/managed-content-card/index.vue'
|
||||
export type { ManagedContentModalState } from './components/managed-content-modal/index.vue'
|
||||
export { default as ManagedContentModal } from './components/managed-content-modal/index.vue'
|
||||
export { default as ConfirmBulkUpdateModal } from './components/modals/ConfirmBulkUpdateModal.vue'
|
||||
export { default as ConfirmDeletionModal } from './components/modals/ConfirmDeletionModal.vue'
|
||||
export { default as ConfirmDisableModal } from './components/modals/ConfirmDisableModal.vue'
|
||||
@@ -18,11 +20,10 @@ export type {
|
||||
} from './components/modals/ContentInstallModal.vue'
|
||||
export { default as ContentInstallModal } from './components/modals/ContentInstallModal.vue'
|
||||
export { default as InlineBackupCreator } from './components/modals/InlineBackupCreator.vue'
|
||||
export type { ModpackContentModalState } from './components/modals/ModpackContentModal.vue'
|
||||
export { default as ModpackContentModal } from './components/modals/ModpackContentModal.vue'
|
||||
export { default as ContentCardLayout } from './layout.vue'
|
||||
export { default as ContentPageLayout } from './layout.vue'
|
||||
export * from './providers'
|
||||
export * from './types'
|
||||
export * from './utils/managed-content'
|
||||
export * from './utils/update-channels'
|
||||
export { default as ConfirmLeaveModal } from '#ui/components/modal/ConfirmLeaveModal.vue'
|
||||
|
||||
@@ -9,27 +9,31 @@ import {
|
||||
DownloadIcon,
|
||||
DropdownIcon,
|
||||
FileIcon,
|
||||
FilterIcon,
|
||||
FolderOpenIcon,
|
||||
LinkIcon,
|
||||
OrganizationIcon,
|
||||
RefreshCwIcon,
|
||||
SearchIcon,
|
||||
ShareIcon,
|
||||
TextCursorInputIcon,
|
||||
TrashIcon,
|
||||
UserIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { Button, TeleportOverflowMenu } from '#ui/components/base/buttons'
|
||||
import Avatar from '#ui/components/base/Avatar.vue'
|
||||
import { Button, type OverflowMenuOption, TeleportOverflowMenu } from '#ui/components/base/buttons'
|
||||
import DropdownFilterBar from '#ui/components/base/DropdownFilterBar.vue'
|
||||
import EmptyState from '#ui/components/base/EmptyState.vue'
|
||||
import FilterPills from '#ui/components/base/FilterPills.vue'
|
||||
import StyledInput from '#ui/components/base/StyledInput.vue'
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages, formatContentTypeSentence } from '#ui/utils/common-messages'
|
||||
|
||||
import ContentCardTable from './components/ContentCardTable.vue'
|
||||
import ContentModpackCard from './components/ContentModpackCard.vue'
|
||||
import ContentSelectionBar from './components/ContentSelectionBar.vue'
|
||||
import ManagedContentCard from './components/managed-content-card/index.vue'
|
||||
import ConfirmBulkUpdateModal from './components/modals/ConfirmBulkUpdateModal.vue'
|
||||
import ConfirmDeletionModal from './components/modals/ConfirmDeletionModal.vue'
|
||||
import ConfirmDisableModal from './components/modals/ConfirmDisableModal.vue'
|
||||
@@ -37,10 +41,10 @@ import ConfirmUnlinkModal from './components/modals/ConfirmUnlinkModal.vue'
|
||||
import ContentDependencyWarningModal from './components/modals/ContentDependencyWarningModal.vue'
|
||||
import {
|
||||
getClientWarningType,
|
||||
isClientOnlyEnvironment,
|
||||
useBulkOperation,
|
||||
useChangingItems,
|
||||
useContentFilters,
|
||||
useContentMetadataFilters,
|
||||
useContentSearch,
|
||||
useContentSelection,
|
||||
} from './composables'
|
||||
@@ -89,9 +93,13 @@ const messages = defineMessages({
|
||||
id: 'content.page-layout.upload-files',
|
||||
defaultMessage: 'Upload files',
|
||||
},
|
||||
sortAlphabetical: {
|
||||
id: 'content.page-layout.sort.alphabetical',
|
||||
defaultMessage: 'Alphabetical',
|
||||
sortAlphabeticalAscending: {
|
||||
id: 'content.page-layout.sort.alphabetical-ascending',
|
||||
defaultMessage: 'Name (A-Z)',
|
||||
},
|
||||
sortAlphabeticalDescending: {
|
||||
id: 'content.page-layout.sort.alphabetical-descending',
|
||||
defaultMessage: 'Name (Z-A)',
|
||||
},
|
||||
sortDateAddedNewest: {
|
||||
id: 'content.page-layout.sort.date-added-newest',
|
||||
@@ -101,6 +109,14 @@ const messages = defineMessages({
|
||||
id: 'content.page-layout.sort.date-added-oldest',
|
||||
defaultMessage: 'Oldest first',
|
||||
},
|
||||
filter: {
|
||||
id: 'content.page-layout.filter.add',
|
||||
defaultMessage: 'Filter',
|
||||
},
|
||||
authorCount: {
|
||||
id: 'content.page-layout.filter.author-count',
|
||||
defaultMessage: '{count, plural, one {# author} other {# authors}}',
|
||||
},
|
||||
updateAll: {
|
||||
id: 'content.page-layout.update-all',
|
||||
defaultMessage: 'Update all',
|
||||
@@ -166,22 +182,38 @@ type SortMode = 'alphabetical-asc' | 'alphabetical-desc' | 'date-added-newest' |
|
||||
const sortMode = ref<SortMode>('alphabetical-asc')
|
||||
|
||||
const sortLabels: Record<SortMode, () => string> = {
|
||||
'alphabetical-asc': () => formatMessage(messages.sortAlphabetical),
|
||||
'alphabetical-desc': () => formatMessage(messages.sortAlphabetical),
|
||||
'alphabetical-asc': () => formatMessage(messages.sortAlphabeticalAscending),
|
||||
'alphabetical-desc': () => formatMessage(messages.sortAlphabeticalDescending),
|
||||
'date-added-newest': () => formatMessage(messages.sortDateAddedNewest),
|
||||
'date-added-oldest': () => formatMessage(messages.sortDateAddedOldest),
|
||||
}
|
||||
|
||||
function cycleSortMode() {
|
||||
const modes: SortMode[] = [
|
||||
'alphabetical-asc',
|
||||
'alphabetical-desc',
|
||||
'date-added-newest',
|
||||
'date-added-oldest',
|
||||
]
|
||||
const idx = modes.indexOf(sortMode.value)
|
||||
sortMode.value = modes[(idx + 1) % modes.length]
|
||||
}
|
||||
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 sortedItems = computed(() => {
|
||||
const items = [...ctx.items.value]
|
||||
@@ -229,13 +261,125 @@ const { selectedFilters, filterOptions, toggleFilter, applyFilters } = useConten
|
||||
ctx.items,
|
||||
{
|
||||
showTypeFilters: true,
|
||||
showUpdateFilter: ctx.hasUpdateSupport,
|
||||
showWarningsFilter: true,
|
||||
showUpdateFilter: false,
|
||||
showWarningsFilter: false,
|
||||
showStatusFilters: false,
|
||||
showEnvironmentWarnings: ctx.showEnvironmentWarnings,
|
||||
isPackLocked: ctx.isPackLocked,
|
||||
persistKey: ctx.filterPersistKey,
|
||||
},
|
||||
)
|
||||
|
||||
const { selectedMetadataFilters, metadataFilterCategories, applyMetadataFilters } =
|
||||
useContentMetadataFilters(ctx.items, ctx.filterPersistKey, {
|
||||
showSharedContent: ctx.showSharedContentFilter,
|
||||
showEnvironmentWarnings: ctx.showEnvironmentWarnings,
|
||||
})
|
||||
|
||||
const metadataFilterAuthors = computed(() => {
|
||||
const authors = new Map<string, NonNullable<ContentItem['owner']>>()
|
||||
for (const item of ctx.items.value) {
|
||||
if (!item.owner) continue
|
||||
authors.set(`${item.owner.type}:${item.owner.id}`, item.owner)
|
||||
}
|
||||
return authors
|
||||
})
|
||||
|
||||
function getMetadataFilterAuthor(value: string) {
|
||||
return metadataFilterAuthors.value.get(value)
|
||||
}
|
||||
|
||||
function getMetadataFilterPreviewAuthor(selectedValues: string[]) {
|
||||
const [selectedValue] = selectedValues
|
||||
return selectedValues.length === 1 && selectedValue
|
||||
? getMetadataFilterAuthor(selectedValue)
|
||||
: undefined
|
||||
}
|
||||
|
||||
const metadataFilterPreviewAuthorLimit = 3
|
||||
const metadataFilterPreviewAuthorSize = 20
|
||||
const metadataFilterPreviewAuthorOffset = 14
|
||||
|
||||
function getMetadataFilterPreviewAuthorValues(selectedValues: string[]) {
|
||||
return selectedValues.slice(0, metadataFilterPreviewAuthorLimit)
|
||||
}
|
||||
|
||||
function getMetadataFilterPreviewAuthorOverflow(selectedValues: string[]) {
|
||||
return Math.max(0, selectedValues.length - metadataFilterPreviewAuthorLimit)
|
||||
}
|
||||
|
||||
function getMetadataFilterPreviewAuthorStackWidth(selectedValues: string[]) {
|
||||
const visibleCount = Math.min(selectedValues.length, metadataFilterPreviewAuthorLimit)
|
||||
if (visibleCount === 0) return 0
|
||||
return (
|
||||
metadataFilterPreviewAuthorSize +
|
||||
(visibleCount - 1 + (selectedValues.length > metadataFilterPreviewAuthorLimit ? 1 : 0)) *
|
||||
metadataFilterPreviewAuthorOffset
|
||||
)
|
||||
}
|
||||
|
||||
function isMetadataFilterOrganization(value: string) {
|
||||
return (
|
||||
getMetadataFilterAuthor(value)?.type === 'organization' || value.startsWith('organization:')
|
||||
)
|
||||
}
|
||||
|
||||
const metadataFilterTriggerClass =
|
||||
'!h-[34px] !rounded-xl !border !border-solid !border-surface-5 !bg-transparent !px-3 !text-sm !font-medium !text-primary !shadow-[0_1px_1.5px_rgba(0,0,0,0.15)] transition-all duration-100 active:scale-[0.97] hover:!bg-surface-3 focus-visible:!outline-none focus-visible:!ring-4 focus-visible:!ring-brand-shadow [&>svg]:!size-5'
|
||||
const metadataFilterPreviewTriggerClass =
|
||||
'!h-[34px] !rounded-xl !border !border-solid !border-brand !bg-brand-highlight !px-3 !text-sm !font-medium !text-brand !shadow-[0_1px_1.5px_rgba(0,0,0,0.15)] transition-all duration-100 active:scale-[0.97] hover:!bg-brand-highlight focus-visible:!outline-none focus-visible:!ring-4 focus-visible:!ring-brand-shadow [&>svg]:!size-5 [&>svg]:!text-brand'
|
||||
|
||||
const filterControlsRef = ref<HTMLElement | null>(null)
|
||||
const projectTypeFiltersRef = ref<HTMLElement | null>(null)
|
||||
const metadataFiltersRef = ref<HTMLElement | null>(null)
|
||||
const metadataFiltersWrapped = ref(false)
|
||||
let filterControlsResizeObserver: ResizeObserver | null = null
|
||||
|
||||
function updateMetadataFiltersWrapped() {
|
||||
metadataFiltersWrapped.value =
|
||||
!!projectTypeFiltersRef.value &&
|
||||
!!metadataFiltersRef.value &&
|
||||
metadataFiltersRef.value.offsetTop > projectTypeFiltersRef.value.offsetTop
|
||||
}
|
||||
|
||||
function observeFilterControls() {
|
||||
filterControlsResizeObserver?.disconnect()
|
||||
for (const element of [
|
||||
filterControlsRef.value,
|
||||
projectTypeFiltersRef.value,
|
||||
metadataFiltersRef.value,
|
||||
]) {
|
||||
if (element) filterControlsResizeObserver?.observe(element)
|
||||
}
|
||||
updateMetadataFiltersWrapped()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof ResizeObserver === 'undefined') return
|
||||
filterControlsResizeObserver = new ResizeObserver(updateMetadataFiltersWrapped)
|
||||
observeFilterControls()
|
||||
})
|
||||
|
||||
watch([filterControlsRef, projectTypeFiltersRef, metadataFiltersRef], observeFilterControls, {
|
||||
flush: 'post',
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
filterControlsResizeObserver?.disconnect()
|
||||
})
|
||||
|
||||
function updateFilterChips(nextFilters: string[]) {
|
||||
if (nextFilters.length === 0) {
|
||||
selectedFilters.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const changedFilter =
|
||||
nextFilters.find((filter) => !selectedFilters.value.includes(filter)) ??
|
||||
selectedFilters.value.find((filter) => !nextFilters.includes(filter))
|
||||
if (changedFilter) toggleFilter(changedFilter)
|
||||
}
|
||||
|
||||
const { selectedIds, selectedItems, clearSelection, removeFromSelection } = useContentSelection(
|
||||
ctx.items,
|
||||
getItemId,
|
||||
@@ -270,27 +414,29 @@ async function handleRefresh() {
|
||||
const filteredItems = computed(() => {
|
||||
const sorted = sortedItems.value
|
||||
const searched = search(sorted)
|
||||
return applyFilters(searched)
|
||||
return applyMetadataFilters(applyFilters(searched))
|
||||
})
|
||||
const tableItems = computed<ContentCardTableItem[]>(() => {
|
||||
const items = filteredItems.value.map((item) => {
|
||||
const base = ctx.mapToTableItem(item)
|
||||
const id = getItemId(item)
|
||||
const locked = base.locked ?? item.locked ?? false
|
||||
const clientWarning = 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,
|
||||
toggleDisabled: ctx.isBusy.value,
|
||||
toggleDisabledTooltip: ctx.isBusy.value ? (ctx.busyMessage?.value ?? null) : null,
|
||||
toggleDisabled: ctx.isBusy.value || base.toggleDisabled,
|
||||
toggleDisabledTooltip: ctx.isBusy.value
|
||||
? (ctx.busyMessage?.value ?? null)
|
||||
: base.toggleDisabledTooltip,
|
||||
installing: item.installing === true,
|
||||
hasUpdate: base.hasUpdate ?? item.has_update,
|
||||
isClientOnly:
|
||||
isClientOnlyEnvironment(item.environment) ||
|
||||
!!item.pack_client_retained ||
|
||||
!!item.pack_client_depends,
|
||||
clientWarning: getClientWarningType(item),
|
||||
isClientOnly: clientWarning !== null,
|
||||
clientWarning,
|
||||
hideDelete: base.hideDelete,
|
||||
hideSwitchVersion: base.hideSwitchVersion ?? !base.versionLink,
|
||||
overflowOptions: ctx.getOverflowOptions?.(item),
|
||||
@@ -310,7 +456,7 @@ const tableItems = computed<ContentCardTableItem[]>(() => {
|
||||
})
|
||||
|
||||
const hasOutdatedProjects = computed(() => {
|
||||
const outdated = ctx.items.value.filter((p) => p.has_update)
|
||||
const outdated = ctx.items.value.filter((p) => p.has_update && !p.locked)
|
||||
if (outdated.length > 0) {
|
||||
debug('hasOutdatedProjects: raw items with has_update=true', {
|
||||
count: outdated.length,
|
||||
@@ -533,9 +679,10 @@ async function confirmDelete() {
|
||||
}
|
||||
|
||||
async function promptDisableItems(items: ContentItem[]) {
|
||||
if (items.length === 0) return
|
||||
pendingDisableItems.value = items
|
||||
const warning = ctx.getDisableWarning?.(items) ?? null
|
||||
const toggleableItems = items.filter(canToggleItem)
|
||||
if (toggleableItems.length === 0) return
|
||||
pendingDisableItems.value = toggleableItems
|
||||
const warning = ctx.getDisableWarning?.(toggleableItems) ?? null
|
||||
if (warning) {
|
||||
pendingDisableWarning.value = warning
|
||||
confirmDisableModal.value?.show()
|
||||
@@ -642,12 +789,14 @@ async function bulkDisable() {
|
||||
}
|
||||
|
||||
function handleUpdateById(id: string) {
|
||||
const item = ctx.items.value.find((item) => getItemId(item) === id)
|
||||
if (item?.locked) return
|
||||
ctx.updateItem?.(id)
|
||||
}
|
||||
|
||||
function handleSwitchVersionById(id: string) {
|
||||
const item = ctx.items.value.find((i) => getItemId(i) === id)
|
||||
if (item) {
|
||||
if (item && !item.locked) {
|
||||
ctx.switchVersion?.(item)
|
||||
}
|
||||
}
|
||||
@@ -663,7 +812,7 @@ const hasBulkUpdateSupport = computed(
|
||||
|
||||
function promptUpdateAll(event?: MouseEvent) {
|
||||
if (!hasBulkUpdateSupport.value) return
|
||||
const items = ctx.items.value.filter((item) => item.has_update)
|
||||
const items = ctx.items.value.filter((item) => item.has_update && !item.locked)
|
||||
if (items.length === 0) return
|
||||
pendingBulkUpdateItems.value = items
|
||||
pendingBulkUpdateAll.value = true
|
||||
@@ -676,7 +825,7 @@ function promptUpdateAll(event?: MouseEvent) {
|
||||
|
||||
function promptUpdateSelected(event?: MouseEvent) {
|
||||
if (!hasBulkUpdateSupport.value) return
|
||||
const items = selectedItems.value.filter((item) => item.has_update)
|
||||
const items = selectedItems.value.filter((item) => item.has_update && !item.locked)
|
||||
if (items.length === 0) return
|
||||
pendingBulkUpdateItems.value = items
|
||||
pendingBulkUpdateAll.value = false
|
||||
@@ -769,28 +918,22 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<ContentModpackCard
|
||||
v-if="ctx.modpack.value"
|
||||
:project="ctx.modpack.value.project"
|
||||
:project-link="ctx.modpack.value.projectLink"
|
||||
:version="ctx.modpack.value.version"
|
||||
:version-link="ctx.modpack.value.versionLink"
|
||||
:owner="ctx.modpack.value.owner"
|
||||
:categories="ctx.modpack.value.categories"
|
||||
:has-update="ctx.modpack.value.hasUpdate"
|
||||
:disabled="ctx.modpack.value.disabled"
|
||||
:disabled-text="ctx.modpack.value.disabledText"
|
||||
v-on="{
|
||||
...(ctx.updateModpack ? { update: () => ctx.updateModpack?.() } : {}),
|
||||
...(ctx.viewModpackContent ? { content: () => ctx.viewModpackContent?.() } : {}),
|
||||
...(ctx.unlinkModpack ? { unlink: () => confirmUnlinkModal?.show() } : {}),
|
||||
...(ctx.openSettings ? { settings: () => ctx.openSettings?.() } : {}),
|
||||
}"
|
||||
<ManagedContentCard
|
||||
v-if="ctx.managedContent.value"
|
||||
:data="ctx.managedContent.value.card"
|
||||
:disabled="ctx.managedContent.value.disabled"
|
||||
:disabled-text="ctx.managedContent.value.disabledText"
|
||||
:show-view-content="!!ctx.viewManagedContent"
|
||||
:show-settings="!!ctx.openManagedContentSettings"
|
||||
:show-primary-action="!!ctx.runManagedContentPrimaryAction"
|
||||
@view-content="ctx.viewManagedContent?.()"
|
||||
@settings="ctx.openManagedContentSettings?.()"
|
||||
@primary-action="ctx.runManagedContentPrimaryAction?.($event)"
|
||||
/>
|
||||
|
||||
<template v-if="ctx.items.value.length > 0">
|
||||
<div class="flex flex-col gap-4">
|
||||
<span v-if="ctx.modpack.value" class="text-xl font-semibold text-contrast">
|
||||
<span v-if="ctx.managedContent.value" class="text-xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.additionalContent) }}
|
||||
</span>
|
||||
|
||||
@@ -847,85 +990,224 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="@container flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<FilterIcon class="size-5 text-secondary" />
|
||||
<button
|
||||
class="cursor-pointer rounded-full border border-solid px-3 py-1.5 text-base font-semibold leading-5 transition-all duration-100 active:scale-[0.97]"
|
||||
:class="
|
||||
selectedFilters.length === 0
|
||||
? 'border-brand bg-brand-highlight text-brand'
|
||||
: 'border-surface-5 bg-surface-4 text-primary hover:bg-surface-5'
|
||||
"
|
||||
:aria-pressed="selectedFilters.length === 0"
|
||||
@click="selectedFilters = []"
|
||||
>
|
||||
{{ formatMessage(commonMessages.allProjectType) }}
|
||||
</button>
|
||||
<button
|
||||
v-for="option in filterOptions"
|
||||
:key="option.id"
|
||||
class="cursor-pointer rounded-full border border-solid px-3 py-1.5 text-base font-semibold leading-5 transition-all duration-100 active:scale-[0.97]"
|
||||
:class="
|
||||
selectedFilters.includes(option.id)
|
||||
? 'border-brand bg-brand-highlight text-brand'
|
||||
: 'border-surface-5 bg-surface-4 text-primary hover:bg-surface-5'
|
||||
"
|
||||
:aria-pressed="selectedFilters.includes(option.id)"
|
||||
@click="toggleFilter(option.id)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
<div class="hidden @[900px]:block">
|
||||
<Button
|
||||
type="quiet"
|
||||
:aria-label="
|
||||
formatMessage(messages.sortByLabel, { mode: sortLabels[sortMode]() })
|
||||
"
|
||||
@click="cycleSortMode"
|
||||
<div class="@container flex items-start gap-2">
|
||||
<div ref="filterControlsRef" class="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
||||
<div ref="projectTypeFiltersRef" class="flex items-center gap-2">
|
||||
<TeleportOverflowMenu
|
||||
class="!h-[34px] !text-sm !font-medium"
|
||||
:icon-only="false"
|
||||
:label="formatMessage(messages.sortByLabel, { mode: sortLabels[sortMode]() })"
|
||||
:options="sortOptions"
|
||||
>
|
||||
<ArrowUpZAIcon v-if="sortMode === 'alphabetical-desc'" /><ClockArrowDownIcon
|
||||
v-else-if="sortMode === 'date-added-newest'"
|
||||
/><ClockArrowUpIcon
|
||||
v-else-if="sortMode === 'date-added-oldest'"
|
||||
/><ArrowDownAZIcon v-else />
|
||||
<ArrowUpZAIcon v-if="sortMode === 'alphabetical-desc'" />
|
||||
<ClockArrowDownIcon v-else-if="sortMode === 'date-added-newest'" />
|
||||
<ClockArrowUpIcon v-else-if="sortMode === 'date-added-oldest'" />
|
||||
<ArrowDownAZIcon v-else />
|
||||
{{ sortLabels[sortMode]() }}
|
||||
</Button>
|
||||
<DropdownIcon />
|
||||
</TeleportOverflowMenu>
|
||||
<div class="h-6 w-px shrink-0 bg-surface-5" />
|
||||
<FilterPills
|
||||
:model-value="selectedFilters"
|
||||
:options="filterOptions"
|
||||
@update:model-value="updateFilterChips"
|
||||
>
|
||||
<template #all>
|
||||
{{ formatMessage(commonMessages.allProjectType) }}
|
||||
</template>
|
||||
</FilterPills>
|
||||
</div>
|
||||
<div
|
||||
v-if="metadataFilterCategories.length > 0"
|
||||
ref="metadataFiltersRef"
|
||||
class="flex flex-wrap items-center gap-1.5 [&>div:last-of-type]:!h-[34px] [&>div:last-of-type]:!gap-1.5 [&_[data-button]]:!h-[34px]"
|
||||
>
|
||||
<div
|
||||
class="h-6 w-px shrink-0 bg-surface-5"
|
||||
:class="{
|
||||
hidden: metadataFiltersWrapped,
|
||||
'mr-0.5': !metadataFiltersWrapped,
|
||||
}"
|
||||
/>
|
||||
<DropdownFilterBar
|
||||
v-model="selectedMetadataFilters"
|
||||
:categories="metadataFilterCategories"
|
||||
:show-label="false"
|
||||
:add-label="formatMessage(messages.filter)"
|
||||
:add-button-class="metadataFilterTriggerClass"
|
||||
:preview-trigger-class="metadataFilterPreviewTriggerClass"
|
||||
add-button-size="sm"
|
||||
checkbox-position="right"
|
||||
apply-immediately
|
||||
>
|
||||
<template #preview-content="{ category, selectedValues, label, summary }">
|
||||
<div
|
||||
v-if="category.key === 'author'"
|
||||
class="flex min-w-0 flex-1 items-center gap-2"
|
||||
>
|
||||
<template v-if="selectedValues.length === 1">
|
||||
<span
|
||||
class="flex size-5 shrink-0 items-center justify-center overflow-hidden bg-surface-2 text-brand"
|
||||
:class="
|
||||
getMetadataFilterPreviewAuthor(selectedValues)?.type ===
|
||||
'organization'
|
||||
? 'rounded'
|
||||
: 'rounded-full'
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
v-if="getMetadataFilterPreviewAuthor(selectedValues)?.avatar_url"
|
||||
:src="getMetadataFilterPreviewAuthor(selectedValues)?.avatar_url"
|
||||
size="100%"
|
||||
:circle="
|
||||
getMetadataFilterPreviewAuthor(selectedValues)?.type !==
|
||||
'organization'
|
||||
"
|
||||
no-shadow
|
||||
class="!border-0"
|
||||
/>
|
||||
<OrganizationIcon
|
||||
v-else-if="
|
||||
getMetadataFilterPreviewAuthor(selectedValues)?.type ===
|
||||
'organization'
|
||||
"
|
||||
class="size-4"
|
||||
/>
|
||||
<UserIcon v-else class="size-4" />
|
||||
</span>
|
||||
<span class="min-w-0 truncate font-semibold text-contrast">
|
||||
{{ getMetadataFilterPreviewAuthor(selectedValues)?.name ?? summary }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="font-medium">{{ label }}:</span>
|
||||
<div
|
||||
class="relative h-5 shrink-0"
|
||||
:style="{
|
||||
width: `${getMetadataFilterPreviewAuthorStackWidth(selectedValues)}px`,
|
||||
}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
v-for="(value, index) in getMetadataFilterPreviewAuthorValues(
|
||||
selectedValues,
|
||||
)"
|
||||
:key="value"
|
||||
class="absolute top-0 flex size-5 items-center justify-center overflow-hidden border border-solid border-surface-3 bg-surface-4 text-brand"
|
||||
:class="
|
||||
isMetadataFilterOrganization(value) ? 'rounded' : 'rounded-full'
|
||||
"
|
||||
:style="{
|
||||
left: `${index * metadataFilterPreviewAuthorOffset}px`,
|
||||
zIndex:
|
||||
getMetadataFilterPreviewAuthorValues(selectedValues).length -
|
||||
index,
|
||||
}"
|
||||
>
|
||||
<Avatar
|
||||
v-if="getMetadataFilterAuthor(value)?.avatar_url"
|
||||
:src="getMetadataFilterAuthor(value)?.avatar_url"
|
||||
size="100%"
|
||||
:circle="!isMetadataFilterOrganization(value)"
|
||||
no-shadow
|
||||
class="!border-0"
|
||||
/>
|
||||
<OrganizationIcon
|
||||
v-else-if="isMetadataFilterOrganization(value)"
|
||||
class="size-3.5"
|
||||
/>
|
||||
<UserIcon v-else class="size-3.5" />
|
||||
</div>
|
||||
<div
|
||||
v-if="getMetadataFilterPreviewAuthorOverflow(selectedValues) > 0"
|
||||
class="absolute top-0 flex size-5 items-center justify-center rounded-full border border-solid border-surface-3 bg-surface-4 text-[10px] font-bold text-contrast"
|
||||
:style="{
|
||||
left: `${getMetadataFilterPreviewAuthorValues(selectedValues).length * metadataFilterPreviewAuthorOffset}px`,
|
||||
}"
|
||||
>
|
||||
+{{ getMetadataFilterPreviewAuthorOverflow(selectedValues) }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="min-w-0 truncate font-semibold text-contrast">
|
||||
{{
|
||||
formatMessage(messages.authorCount, { count: selectedValues.length })
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<span v-else class="min-w-0 flex-1 truncate">
|
||||
<span class="font-medium">{{ label }}:</span>
|
||||
<span class="ml-1 font-semibold text-contrast">{{ summary }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template #option="{ category, option, selected }">
|
||||
<div
|
||||
v-if="category.key === 'author'"
|
||||
class="flex min-w-0 flex-1 items-center gap-2"
|
||||
>
|
||||
<span
|
||||
v-tooltip="option.label"
|
||||
class="flex size-6 shrink-0 items-center justify-center overflow-hidden bg-surface-2 text-secondary"
|
||||
:class="
|
||||
getMetadataFilterAuthor(option.value)?.type === 'organization'
|
||||
? 'rounded'
|
||||
: 'rounded-full'
|
||||
"
|
||||
>
|
||||
<img
|
||||
v-if="getMetadataFilterAuthor(option.value)?.avatar_url"
|
||||
:src="getMetadataFilterAuthor(option.value)?.avatar_url"
|
||||
:alt="option.label"
|
||||
class="size-full object-cover"
|
||||
/>
|
||||
<OrganizationIcon
|
||||
v-else-if="
|
||||
getMetadataFilterAuthor(option.value)?.type === 'organization'
|
||||
"
|
||||
class="size-5"
|
||||
/>
|
||||
<UserIcon v-else class="size-5" />
|
||||
</span>
|
||||
<span
|
||||
v-tooltip="option.label"
|
||||
class="min-w-0 truncate font-semibold leading-tight"
|
||||
:class="selected ? 'text-contrast' : 'text-primary'"
|
||||
>
|
||||
{{ option.label }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-else
|
||||
class="min-w-0 truncate font-semibold leading-tight"
|
||||
:class="selected ? 'text-contrast' : 'text-primary'"
|
||||
>
|
||||
{{ option.label }}
|
||||
</span>
|
||||
</template>
|
||||
</DropdownFilterBar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="@[900px]:hidden">
|
||||
<Button
|
||||
type="quiet"
|
||||
:aria-label="
|
||||
formatMessage(messages.sortByLabel, { mode: sortLabels[sortMode]() })
|
||||
"
|
||||
@click="cycleSortMode"
|
||||
>
|
||||
<ArrowUpZAIcon v-if="sortMode === 'alphabetical-desc'" /><ClockArrowDownIcon
|
||||
v-else-if="sortMode === 'date-added-newest'"
|
||||
/><ClockArrowUpIcon
|
||||
v-else-if="sortMode === 'date-added-oldest'"
|
||||
/><ArrowDownAZIcon v-else />
|
||||
{{ sortLabels[sortMode]() }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<Button
|
||||
v-if="hasBulkUpdateSupport && hasOutdatedProjects"
|
||||
v-tooltip="formatMessage(messages.updateAll)"
|
||||
type="quiet"
|
||||
color="green"
|
||||
:disabled="isBulkOperating"
|
||||
class="hover:!bg-green focus-visible:!bg-green hover:!text-[var(--color-accent-contrast)] focus-visible:!text-[var(--color-accent-contrast)]"
|
||||
class="!text-sm !font-medium hover:!bg-green focus-visible:!bg-green hover:!text-[var(--color-accent-contrast)] focus-visible:!text-[var(--color-accent-contrast)]"
|
||||
@click="promptUpdateAll"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.updateAll) }}
|
||||
</Button>
|
||||
|
||||
<Button type="quiet" :disabled="refreshing" @click="handleRefresh">
|
||||
<Button
|
||||
type="quiet"
|
||||
:disabled="refreshing"
|
||||
class="!text-sm !font-medium"
|
||||
@click="handleRefresh"
|
||||
>
|
||||
<RefreshCwIcon :class="refreshing ? 'animate-spin' : ''" />
|
||||
{{ formatMessage(commonMessages.refreshButton) }}
|
||||
</Button>
|
||||
@@ -952,13 +1234,15 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
<template #heading>
|
||||
{{
|
||||
formatMessage(
|
||||
ctx.modpack.value ? messages.noExtraContentInstalled : messages.noContentInstalled,
|
||||
ctx.managedContent.value
|
||||
? messages.noExtraContentInstalled
|
||||
: messages.noContentInstalled,
|
||||
)
|
||||
}}
|
||||
</template>
|
||||
<template #description>
|
||||
{{
|
||||
ctx.modpack.value
|
||||
ctx.managedContent.value
|
||||
? formatMessage(messages.emptyModpackHint)
|
||||
: formatMessage(messages.emptyHint, {
|
||||
contentType: formatContentTypeSentence(
|
||||
@@ -1024,7 +1308,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
v-if="hasBulkUpdateSupport && selectedItems.some((m) => m.has_update)"
|
||||
v-if="hasBulkUpdateSupport && selectedItems.some((m) => m.has_update && !m.locked)"
|
||||
v-tooltip="formatMessage(commonMessages.updateButton)"
|
||||
type="quiet"
|
||||
color="green"
|
||||
@@ -1146,7 +1430,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
v-if="ctx.unlinkModpack"
|
||||
ref="confirmUnlinkModal"
|
||||
:server="ctx.deletionContext === 'server'"
|
||||
:backup-tip="ctx.modpack.value?.project.title"
|
||||
:backup-tip="ctx.managedContent.value?.card.manager.name"
|
||||
:action-disabled="ctx.isBusy.value"
|
||||
:action-disabled-tooltip="ctx.busyMessage?.value ?? undefined"
|
||||
@unlink="ctx.unlinkModpack!()"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
import type { OverflowMenuOption } from '#ui/components/base/buttons'
|
||||
import { createContext } from '#ui/providers/create-context'
|
||||
@@ -9,20 +8,11 @@ import type {
|
||||
ContentActionWarning,
|
||||
ContentCardTableItem,
|
||||
ContentItem,
|
||||
ContentModpackCardCategory,
|
||||
ContentModpackCardProject,
|
||||
ContentModpackCardVersion,
|
||||
ContentOwner,
|
||||
ManagedContentCardData,
|
||||
} from '../types'
|
||||
|
||||
export interface ContentModpackData {
|
||||
project: ContentModpackCardProject
|
||||
projectLink?: string | RouteLocationRaw
|
||||
version?: ContentModpackCardVersion
|
||||
versionLink?: string | RouteLocationRaw
|
||||
owner?: ContentOwner
|
||||
categories: ContentModpackCardCategory[]
|
||||
hasUpdate: boolean
|
||||
export interface ManagedContentData {
|
||||
card: ManagedContentCardData
|
||||
disabled?: boolean
|
||||
disabledText?: string
|
||||
}
|
||||
@@ -41,8 +31,8 @@ export interface ContentManagerContext {
|
||||
loading: Ref<boolean>
|
||||
error: Ref<Error | null>
|
||||
|
||||
// Modpack
|
||||
modpack: Ref<ContentModpackData | null> | ComputedRef<ContentModpackData | null>
|
||||
// Managed content
|
||||
managedContent: Ref<ManagedContentData | null> | ComputedRef<ManagedContentData | null>
|
||||
isPackLocked: Ref<boolean> | ComputedRef<boolean>
|
||||
|
||||
// Guards
|
||||
@@ -81,11 +71,11 @@ export interface ContentManagerContext {
|
||||
bulkUpdateItem?: (item: ContentItem) => Promise<void>
|
||||
bulkUpdateItems?: (items: ContentItem[]) => Promise<void>
|
||||
|
||||
// Modpack actions (optional)
|
||||
updateModpack?: () => void
|
||||
viewModpackContent?: () => void
|
||||
// Managed-content actions (optional)
|
||||
runManagedContentPrimaryAction?: (event?: MouseEvent) => void
|
||||
viewManagedContent?: () => void
|
||||
unlinkModpack?: () => void
|
||||
openSettings?: () => void
|
||||
openManagedContentSettings?: () => void
|
||||
|
||||
// Switch version (optional)
|
||||
switchVersion?: (item: ContentItem) => void
|
||||
@@ -104,12 +94,14 @@ export interface ContentManagerContext {
|
||||
|
||||
// Deletion context (controls modal variant)
|
||||
deletionContext?: 'instance' | 'server'
|
||||
showEnvironmentWarnings?: boolean
|
||||
|
||||
// Table item mapping (link generation differs per platform)
|
||||
mapToTableItem: (item: ContentItem) => ContentCardTableItem
|
||||
|
||||
// Filter persistence key — when set, selected filters are saved/restored via sessionStorage
|
||||
filterPersistKey?: string
|
||||
showSharedContentFilter?: Ref<boolean> | ComputedRef<boolean>
|
||||
}
|
||||
|
||||
export const [injectContentManager, provideContentManager] = createContext<ContentManagerContext>(
|
||||
|
||||
@@ -6,7 +6,8 @@ import type { OverflowMenuOption } from '#ui/components/base/buttons'
|
||||
export type ContentCardProject = Pick<
|
||||
Labrinth.Projects.v2.Project,
|
||||
'id' | 'slug' | 'title' | 'icon_url'
|
||||
>
|
||||
> &
|
||||
Partial<Pick<Labrinth.Projects.v2.Project, 'license' | 'categories' | 'additional_categories'>>
|
||||
|
||||
export type ContentCardVersion = Pick<Labrinth.Versions.v2.Version, 'id' | 'version_number'> & {
|
||||
file_name: string
|
||||
@@ -42,6 +43,13 @@ export interface ContentActionWarning {
|
||||
actionLabel: string
|
||||
}
|
||||
|
||||
export interface EmbeddedContentMetadata {
|
||||
name?: string | null
|
||||
version?: string | null
|
||||
icon_path?: string | null
|
||||
icon_url?: string | null
|
||||
}
|
||||
|
||||
export interface ContentCardTableItem {
|
||||
id: string
|
||||
project: ContentCardProject
|
||||
@@ -50,11 +58,14 @@ export interface ContentCardTableItem {
|
||||
versionLink?: string | RouteLocationRaw
|
||||
owner?: ContentOwner
|
||||
source?: ContentSource
|
||||
external?: boolean
|
||||
enabled?: boolean
|
||||
locked?: boolean
|
||||
disabled?: boolean
|
||||
disabledTooltip?: string | null
|
||||
toggleDisabled?: boolean
|
||||
toggleDisabledTooltip?: string | null
|
||||
hideToggle?: boolean
|
||||
installing?: boolean
|
||||
hasUpdate?: boolean
|
||||
isClientOnly?: boolean
|
||||
@@ -87,29 +98,56 @@ export interface ContentItem extends Omit<
|
||||
has_update: boolean
|
||||
update_version_id: string | null
|
||||
date_added?: string
|
||||
environment?: string
|
||||
environment?: Labrinth.Projects.v3.Environment
|
||||
pack_client_retained?: boolean
|
||||
pack_client_depends?: boolean
|
||||
installing?: boolean
|
||||
source_kind?: ContentSourceKind | null
|
||||
external?: boolean
|
||||
external_url?: string
|
||||
embedded_metadata?: EmbeddedContentMetadata | null
|
||||
}
|
||||
|
||||
export type ContentModpackCardProject = Pick<
|
||||
export type ManagedContentProject = Pick<
|
||||
Labrinth.Projects.v2.Project,
|
||||
'id' | 'slug' | 'title' | 'icon_url' | 'description'
|
||||
'id' | 'slug' | 'title' | 'icon_url'
|
||||
> & {
|
||||
downloads?: number | null
|
||||
followers?: number | null
|
||||
filename?: string | null
|
||||
}
|
||||
|
||||
export type ContentModpackCardVersion = Pick<
|
||||
export type ManagedContentVersion = Pick<
|
||||
Labrinth.Versions.v2.Version,
|
||||
'id' | 'version_number' | 'date_published'
|
||||
>
|
||||
|
||||
export type ContentModpackCardCategory = Labrinth.Tags.v2.Category & {
|
||||
action?: (event: MouseEvent) => void
|
||||
export type ManagedContentSummaryType = 'mod' | 'plugin' | 'datapack' | 'resourcepack' | 'shader'
|
||||
|
||||
export interface ManagedContentManager {
|
||||
name: string
|
||||
iconUrl?: string
|
||||
link?: string | RouteLocationRaw
|
||||
}
|
||||
|
||||
export interface ManagedContentSummaryItem {
|
||||
type: ManagedContentSummaryType
|
||||
count: number
|
||||
}
|
||||
|
||||
interface ManagedContentCardBase {
|
||||
manager: ManagedContentManager
|
||||
summary?: ManagedContentSummaryItem[]
|
||||
installing?: boolean
|
||||
}
|
||||
|
||||
export type ManagedContentCardData =
|
||||
| (ManagedContentCardBase & {
|
||||
kind: 'modpack'
|
||||
versionNumber?: string
|
||||
versionLink?: string | RouteLocationRaw
|
||||
updatedAt?: string
|
||||
})
|
||||
| (ManagedContentCardBase & {
|
||||
kind: 'server' | 'shared-instance'
|
||||
syncedAt?: number
|
||||
updateAvailable: boolean
|
||||
})
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { normalizeProjectType } from '#ui/utils/common-messages'
|
||||
|
||||
import type { ContentItem, ManagedContentSummaryItem, ManagedContentSummaryType } from '../types'
|
||||
|
||||
const summaryOrder: ManagedContentSummaryType[] = [
|
||||
'mod',
|
||||
'plugin',
|
||||
'datapack',
|
||||
'resourcepack',
|
||||
'shader',
|
||||
]
|
||||
|
||||
function normalizeSummaryType(type: string): ManagedContentSummaryType | null {
|
||||
const normalized = normalizeProjectType(type)
|
||||
switch (normalized) {
|
||||
case 'mod':
|
||||
case 'plugin':
|
||||
case 'datapack':
|
||||
case 'resourcepack':
|
||||
case 'shader':
|
||||
return normalized
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function dedupeManagedContentItems(items: ContentItem[]): ContentItem[] {
|
||||
const seenKeys = new Set<string>()
|
||||
return items.filter((item) => {
|
||||
const key = item.file_path
|
||||
? `path:${item.file_path}`
|
||||
: item.file_name
|
||||
? `name:${item.file_name}`
|
||||
: `id:${item.id}`
|
||||
if (seenKeys.has(key)) return false
|
||||
|
||||
seenKeys.add(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function summarizeManagedContent(items: ContentItem[]): ManagedContentSummaryItem[] {
|
||||
const counts = new Map<ManagedContentSummaryType, number>()
|
||||
for (const item of dedupeManagedContentItems(items)) {
|
||||
const type = normalizeSummaryType(item.project_type)
|
||||
if (type) counts.set(type, (counts.get(type) ?? 0) + 1)
|
||||
}
|
||||
|
||||
return summaryOrder.flatMap((type) => {
|
||||
const count = counts.get(type) ?? 0
|
||||
return count > 0 ? [{ type, count }] : []
|
||||
})
|
||||
}
|
||||
@@ -33,19 +33,15 @@ import {
|
||||
getStoredServerAddonInstallQueue,
|
||||
getTargetInstallPreferences,
|
||||
} from '../../../shared/browse-tab/composables/install-logic'
|
||||
import ManagedContentModal from '../../../shared/content-tab/components/managed-content-modal/index.vue'
|
||||
import ConfirmModpackUpdateModal from '../../../shared/content-tab/components/modals/ConfirmModpackUpdateModal.vue'
|
||||
import ConfirmUnlinkModal from '../../../shared/content-tab/components/modals/ConfirmUnlinkModal.vue'
|
||||
import ContentUpdaterModal from '../../../shared/content-tab/components/modals/content-updater-modal/index.vue'
|
||||
import ModpackContentModal from '../../../shared/content-tab/components/modals/ModpackContentModal.vue'
|
||||
import ContentPageLayout from '../../../shared/content-tab/layout.vue'
|
||||
import type { ContentModpackData } from '../../../shared/content-tab/providers/content-manager'
|
||||
import type { ManagedContentData } from '../../../shared/content-tab/providers/content-manager'
|
||||
import { provideContentManager } from '../../../shared/content-tab/providers/content-manager'
|
||||
import type {
|
||||
ContentItem,
|
||||
ContentModpackCardCategory,
|
||||
ContentModpackCardProject,
|
||||
ContentModpackCardVersion,
|
||||
} from '../../../shared/content-tab/types'
|
||||
import type { ContentItem } from '../../../shared/content-tab/types'
|
||||
import { summarizeManagedContent } from '../../../shared/content-tab/utils/managed-content'
|
||||
|
||||
type AddonWithUiState = Archon.Content.v1.Addon & { installing?: boolean }
|
||||
type ContentOwnerAvatarSource = {
|
||||
@@ -65,6 +61,10 @@ const props = withDefaults(
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
modpackContent: {
|
||||
id: 'hosting.content.managed-content.modpack-header',
|
||||
defaultMessage: 'Modpack content',
|
||||
},
|
||||
failedToRemoveContent: {
|
||||
id: 'hosting.content.failed-to-remove',
|
||||
defaultMessage: 'Failed to remove content',
|
||||
@@ -172,19 +172,21 @@ const modpackContentQuery = useQuery({
|
||||
})
|
||||
|
||||
const setupActionDisabled = computed(() => !canSetup.value || busyReasons.value.length > 0)
|
||||
const setupActionBusyMessage = computed(() => {
|
||||
if (!canSetup.value) return permissionDeniedMessage.value
|
||||
|
||||
const bannerCoversInstalling =
|
||||
const isInstallingContent = computed(
|
||||
() =>
|
||||
server.value?.status === 'installing' ||
|
||||
isSyncingContent.value ||
|
||||
busyReasons.value.some(
|
||||
(r) =>
|
||||
r.reason.id === 'servers.busy.installing' || r.reason.id === 'servers.busy.syncing-content',
|
||||
)
|
||||
),
|
||||
)
|
||||
const setupActionBusyMessage = computed(() => {
|
||||
if (!canSetup.value) return permissionDeniedMessage.value
|
||||
|
||||
const filteredReasons = busyReasons.value.filter((r) => {
|
||||
if (
|
||||
bannerCoversInstalling &&
|
||||
isInstallingContent.value &&
|
||||
(r.reason.id === 'servers.busy.installing' || r.reason.id === 'servers.busy.syncing-content')
|
||||
)
|
||||
return false
|
||||
@@ -256,54 +258,38 @@ const newestModpackUpdateVersion = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const modpack = computed<ContentModpackData | null>(() => {
|
||||
const managedContent = computed<ManagedContentData | null>(() => {
|
||||
const mp = contentQuery.data.value?.modpack
|
||||
if (!mp) return null
|
||||
const isLocal = mp.spec.platform === 'local_file'
|
||||
const project = projectQuery.data.value
|
||||
const projectId = isLocal ? null : mp.spec.project_id
|
||||
return {
|
||||
project: {
|
||||
id: projectId ?? mp.title ?? '',
|
||||
slug: project?.slug ?? projectId ?? '',
|
||||
title: mp.title ?? (isLocal ? mp.spec.name : projectId) ?? '',
|
||||
icon_url: mp.icon_url ?? undefined,
|
||||
description: mp.description ?? '',
|
||||
downloads: mp.downloads,
|
||||
followers: mp.followers,
|
||||
filename: isLocal ? mp.spec.filename : undefined,
|
||||
} as ContentModpackCardProject,
|
||||
projectLink: projectId ? `/project/${project?.slug ?? projectId}` : undefined,
|
||||
version: isLocal
|
||||
const addons = modpackContentQuery.data.value?.addons
|
||||
const summary = addons
|
||||
? summarizeManagedContent(addons.map(addonToContentItem))
|
||||
: modpackContentQuery.isLoading.value
|
||||
? undefined
|
||||
: ({
|
||||
id: mp.spec.version_id,
|
||||
version_number: mp.version_number ?? '',
|
||||
date_published: mp.date_published ?? '',
|
||||
} as ContentModpackCardVersion),
|
||||
versionLink:
|
||||
projectId && !isLocal
|
||||
? `/project/${project?.slug ?? projectId}/version/${mp.spec.version_id}`
|
||||
: undefined,
|
||||
owner: mp.owner
|
||||
? {
|
||||
id: mp.owner.id,
|
||||
name: mp.owner.name,
|
||||
type: mp.owner.type,
|
||||
avatar_url: getContentOwnerAvatarUrl(mp.owner),
|
||||
link:
|
||||
mp.owner.type === 'organization'
|
||||
? `/organization/${mp.owner.id}`
|
||||
: `/user/${mp.owner.id}`,
|
||||
}
|
||||
: undefined,
|
||||
categories: (project?.categories ?? []).map((name) => ({
|
||||
name,
|
||||
icon: name,
|
||||
project_type: 'modpack',
|
||||
header: 'categories',
|
||||
})) as ContentModpackCardCategory[],
|
||||
hasUpdate: !!mp.has_update || !!newestModpackUpdateVersion.value,
|
||||
: []
|
||||
const title = isLocal
|
||||
? (mp.title ?? mp.spec.name)
|
||||
: (project?.title ?? mp.title ?? projectId ?? '')
|
||||
return {
|
||||
card: {
|
||||
kind: 'modpack',
|
||||
installing: isInstallingContent.value,
|
||||
manager: {
|
||||
name: title,
|
||||
iconUrl: (isLocal ? mp.icon_url : (project?.icon_url ?? mp.icon_url)) ?? undefined,
|
||||
link: projectId ? `/project/${project?.slug ?? projectId}` : undefined,
|
||||
},
|
||||
summary,
|
||||
versionNumber: isLocal ? undefined : (mp.version_number ?? undefined),
|
||||
versionLink:
|
||||
projectId && mp.spec.platform === 'modrinth'
|
||||
? `/project/${project?.slug ?? projectId}/version/${mp.spec.version_id}`
|
||||
: undefined,
|
||||
updatedAt: isLocal ? undefined : (mp.date_published ?? undefined),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -329,6 +315,31 @@ const addonLookup = computed(() => {
|
||||
})
|
||||
|
||||
const pendingServerContentInstalls = ref<PendingServerContentInstall[]>([])
|
||||
const projectMetadataBatchSize = 800
|
||||
const contentProjectIds = computed(() =>
|
||||
[...(contentQuery.data.value?.addons ?? []), ...modpackAddons.value]
|
||||
.map((addon) => addon.project_id)
|
||||
.concat(pendingServerContentInstalls.value.map((item) => item.projectId))
|
||||
.filter((id): id is string => !!id)
|
||||
.filter((id, index, ids) => ids.indexOf(id) === index)
|
||||
.sort(),
|
||||
)
|
||||
const contentProjectsQuery = useQuery({
|
||||
queryKey: computed(() => ['labrinth', 'projects', 'v2', contentProjectIds.value]),
|
||||
queryFn: async () => {
|
||||
const batches = []
|
||||
for (let index = 0; index < contentProjectIds.value.length; index += projectMetadataBatchSize) {
|
||||
batches.push(contentProjectIds.value.slice(index, index + projectMetadataBatchSize))
|
||||
}
|
||||
return (
|
||||
await Promise.all(batches.map((ids) => client.labrinth.projects_v2.getMultiple(ids)))
|
||||
).flat()
|
||||
},
|
||||
enabled: computed(() => contentProjectIds.value.length > 0),
|
||||
})
|
||||
const contentProjectsById = computed(
|
||||
() => new Map((contentProjectsQuery.data.value ?? []).map((project) => [project.id, project])),
|
||||
)
|
||||
const lastStableContentKeys = ref<Set<string>>(new Set())
|
||||
const contentInstallBaselineKeys = ref<Set<string> | null>(null)
|
||||
const contentInstallAddedKeys = ref<Set<string>>(new Set())
|
||||
@@ -506,12 +517,14 @@ async function flushStoredServerInstalls() {
|
||||
}
|
||||
|
||||
function pendingInstallToContentItem(item: PendingServerContentInstall): ContentItem {
|
||||
const projectMetadata = contentProjectsById.value.get(item.projectId)
|
||||
return {
|
||||
project: {
|
||||
...(projectMetadata ?? {}),
|
||||
id: item.projectId,
|
||||
slug: item.slug ?? item.projectId,
|
||||
title: item.title,
|
||||
icon_url: item.iconUrl ?? undefined,
|
||||
slug: item.slug ?? projectMetadata?.slug ?? item.projectId,
|
||||
title: projectMetadata?.title ?? item.title,
|
||||
icon_url: item.iconUrl ?? projectMetadata?.icon_url ?? undefined,
|
||||
},
|
||||
version: {
|
||||
id: item.versionId,
|
||||
@@ -854,7 +867,7 @@ async function handleBulkDisable(items: ContentItem[]) {
|
||||
}
|
||||
|
||||
const modpackUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal>>()
|
||||
const modpackContentModal = ref<InstanceType<typeof ManagedContentModal>>()
|
||||
const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal>>()
|
||||
|
||||
const updatingProject = ref<ContentItem | null>(null)
|
||||
@@ -1002,12 +1015,16 @@ function handleUnknownFileContinue(dontShowAgain: boolean) {
|
||||
}
|
||||
|
||||
function addonToContentItem(addon: AddonWithUiState): ContentItem {
|
||||
const projectMetadata = addon.project_id
|
||||
? contentProjectsById.value.get(addon.project_id)
|
||||
: undefined
|
||||
return {
|
||||
project: {
|
||||
...(projectMetadata ?? {}),
|
||||
id: addon.project_id ?? addon.filename,
|
||||
slug: addon.project_id ?? addon.filename,
|
||||
title: friendlyAddonName(addon),
|
||||
icon_url: addon.icon_url ?? undefined,
|
||||
slug: projectMetadata?.slug ?? addon.project_id ?? addon.filename,
|
||||
title: projectMetadata?.title ?? friendlyAddonName(addon),
|
||||
icon_url: addon.icon_url ?? projectMetadata?.icon_url ?? undefined,
|
||||
},
|
||||
version: {
|
||||
id: addon.version?.id ?? addon.filename,
|
||||
@@ -1378,7 +1395,7 @@ provideContentManager({
|
||||
items: contentItems,
|
||||
loading: computed(() => contentQuery.isLoading.value),
|
||||
error: computed(() => contentQuery.error.value ?? null),
|
||||
modpack,
|
||||
managedContent,
|
||||
isPackLocked: ref(false),
|
||||
isBusy: setupActionDisabled,
|
||||
busyMessage: setupActionBusyMessage,
|
||||
@@ -1396,13 +1413,14 @@ provideContentManager({
|
||||
browse: handleBrowseContent,
|
||||
uploadFiles: handleUploadFiles,
|
||||
deletionContext: 'server',
|
||||
showEnvironmentWarnings: true,
|
||||
hasUpdateSupport: true,
|
||||
updateItem: handleUpdateItem,
|
||||
bulkUpdateItems: handleBulkUpdate,
|
||||
updateModpack: handleModpackUpdate,
|
||||
viewModpackContent: handleViewModpackContent,
|
||||
runManagedContentPrimaryAction: handleModpackUpdate,
|
||||
viewManagedContent: handleViewModpackContent,
|
||||
unlinkModpack: handleModpackUnlink,
|
||||
openSettings: () => openServerSettings({ tabId: 'installation' }),
|
||||
openManagedContentSettings: () => openServerSettings({ tabId: 'installation' }),
|
||||
switchVersion: handleSwitchVersion,
|
||||
getOverflowOptions,
|
||||
getItemId: getContentItemId,
|
||||
@@ -1423,6 +1441,7 @@ provideContentManager({
|
||||
owner: item.owner
|
||||
? { ...item.owner, link: item.owner.link ?? `/${item.owner.type}/${item.owner.id}` }
|
||||
: undefined,
|
||||
external: item.external ?? !hasModrinthProject,
|
||||
enabled: item.enabled,
|
||||
}
|
||||
},
|
||||
@@ -1448,11 +1467,13 @@ provideContentManager({
|
||||
:action-disabled-tooltip="setupActionBusyMessage ?? undefined"
|
||||
@unlink="handleModpackUnlinkConfirm"
|
||||
/>
|
||||
<ModpackContentModal
|
||||
<ManagedContentModal
|
||||
ref="modpackContentModal"
|
||||
:modpack-name="modpack?.project.title"
|
||||
:modpack-icon-url="modpack?.project.icon_url"
|
||||
:source-name="managedContent?.card.manager.name"
|
||||
:source-icon-url="managedContent?.card.manager.iconUrl"
|
||||
:header="formatMessage(messages.modpackContent)"
|
||||
enable-toggle
|
||||
show-environment-warnings
|
||||
:action-disabled="setupActionDisabled"
|
||||
:action-disabled-tooltip="setupActionBusyMessage ?? undefined"
|
||||
@update:enabled="handleModpackContentToggle"
|
||||
@@ -1476,11 +1497,13 @@ provideContentManager({
|
||||
:is-app="false"
|
||||
:project-type="updatingModpack ? 'modpack' : updatingProject?.project_type"
|
||||
:project-icon-url="
|
||||
updatingModpack ? modpack?.project.icon_url : updatingProject?.project?.icon_url
|
||||
updatingModpack
|
||||
? managedContent?.card.manager.iconUrl
|
||||
: updatingProject?.project?.icon_url
|
||||
"
|
||||
:project-name="
|
||||
updatingModpack
|
||||
? (modpack?.project.title ?? formatMessage(commonMessages.modpackLabel))
|
||||
? (managedContent?.card.manager.name ?? formatMessage(commonMessages.modpackLabel))
|
||||
: (updatingProject?.project?.title ?? updatingProject?.file_name)
|
||||
"
|
||||
:loading="loadingVersions"
|
||||
@@ -1499,7 +1522,7 @@ provideContentManager({
|
||||
ref="modpackUpdateModal"
|
||||
:downgrade="isModpackUpdateDowngrade"
|
||||
:backup-tip="
|
||||
[modpack?.project.title, pendingModpackUpdateVersion?.version_number]
|
||||
[managedContent?.card.manager.name, pendingModpackUpdateVersion?.version_number]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
"
|
||||
|
||||
@@ -548,18 +548,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Názvy projektů"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Abecedně"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Od nejnovějších"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Od nejstarších"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Seřadit {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Aktualizovat vše"
|
||||
},
|
||||
@@ -1832,18 +1820,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Vybrat ikonu"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Nebyl nalezen obsah"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Obsah modpacku"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Načítání obsahu..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Žádné projekty neodpovídají tvému vyhledávání."
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Mometálně"
|
||||
},
|
||||
@@ -4554,4 +4530,3 @@
|
||||
"defaultMessage": "Typ"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -395,12 +395,6 @@
|
||||
"content.page-layout.share.label": {
|
||||
"defaultMessage": "Del"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Ældst først"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Sorter efter {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Opdater alle"
|
||||
},
|
||||
@@ -806,9 +800,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Vælg ikon"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Ekstern"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Nuværrende"
|
||||
},
|
||||
@@ -2250,4 +2241,3 @@
|
||||
"defaultMessage": "Vanilla Shader"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -566,9 +566,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "Welt"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Installationseinstellungen"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Zusätzliche Inhalte"
|
||||
},
|
||||
@@ -620,18 +617,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Projektnamen"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alphabetisch"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Neuste zuerst"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Älteste zuerst"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Sortieren nach {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Alle aktualisieren"
|
||||
},
|
||||
@@ -2081,33 +2066,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Icon auswählen"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Dieses Modpack enthält keine zusätzlichen inhalte."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Keine Inhalte gefunden"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Extern"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Diese Datei ist nicht auf Modrinth veröffentlicht."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Modpack Inhalt"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Lade Inhalte..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Keine Projekte entsprechen deiner Suche."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "In Slicer öffnen"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Durchsuche {count, number} {count, plural, one {Projekt} other {Projekte}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Aktuell"
|
||||
},
|
||||
@@ -6231,4 +6189,3 @@
|
||||
"defaultMessage": "Typ"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -566,9 +566,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "Welt"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Installationseinstellungen"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Zusätzliche Inhalte"
|
||||
},
|
||||
@@ -620,18 +617,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Projektnamen"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alphabetisch"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Neuste zuerst"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Älteste zuerst"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Sortieren nach {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Alle aktualisieren"
|
||||
},
|
||||
@@ -2081,33 +2066,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Icon auswählen"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Dieses Modpack enthält keine zusätzlichen Inhalte."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Keine Inhalte gefunden"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Extern"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Diese Datei ist nicht auf Modrinth veröffentlicht."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Modpack-Inhalt"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Lade Inhalte..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Keine Projekte entsprechen deiner Suche."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "In Slicer öffnen"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Durchsuche {count, number} {count, plural, one {Projekt} other {Projekte}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Aktuell"
|
||||
},
|
||||
@@ -6231,4 +6189,3 @@
|
||||
"defaultMessage": "Typ"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -347,9 +347,15 @@
|
||||
"content-type.item.lowercase": {
|
||||
"defaultMessage": "{count, plural, one {item} other {items}}"
|
||||
},
|
||||
"content.card.frozen": {
|
||||
"defaultMessage": "This project is locked to its current version until unfrozen."
|
||||
},
|
||||
"content.card.select-project": {
|
||||
"defaultMessage": "Select {project}"
|
||||
},
|
||||
"content.card.uploaded": {
|
||||
"defaultMessage": "Uploaded"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
@@ -566,9 +572,96 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "world"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"content.managed-card.managed-by": {
|
||||
"defaultMessage": "Managed by"
|
||||
},
|
||||
"content.managed-card.modpack-content": {
|
||||
"defaultMessage": "Modpack content"
|
||||
},
|
||||
"content.managed-card.server-suffix": {
|
||||
"defaultMessage": "server"
|
||||
},
|
||||
"content.managed-card.settings": {
|
||||
"defaultMessage": "Installation settings"
|
||||
},
|
||||
"content.managed-card.shared-content": {
|
||||
"defaultMessage": "Shared content"
|
||||
},
|
||||
"content.managed-card.summary.empty": {
|
||||
"defaultMessage": "No managed content"
|
||||
},
|
||||
"content.managed-card.summary.installing": {
|
||||
"defaultMessage": "Installing content..."
|
||||
},
|
||||
"content.managed-card.summary.loading": {
|
||||
"defaultMessage": "Loading managed content summary"
|
||||
},
|
||||
"content.managed-card.switch-version": {
|
||||
"defaultMessage": "Switch version"
|
||||
},
|
||||
"content.managed-card.synced": {
|
||||
"defaultMessage": "Synced {time}"
|
||||
},
|
||||
"content.managed-card.update-available": {
|
||||
"defaultMessage": "Update available"
|
||||
},
|
||||
"content.managed-card.updated": {
|
||||
"defaultMessage": "Updated {time}"
|
||||
},
|
||||
"content.managed-card.updating": {
|
||||
"defaultMessage": "Updating..."
|
||||
},
|
||||
"content.managed-card.view-content": {
|
||||
"defaultMessage": "View content"
|
||||
},
|
||||
"content.metadata-filter.author": {
|
||||
"defaultMessage": "Author"
|
||||
},
|
||||
"content.metadata-filter.environment": {
|
||||
"defaultMessage": "Environment"
|
||||
},
|
||||
"content.metadata-filter.open-source": {
|
||||
"defaultMessage": "Open source"
|
||||
},
|
||||
"content.metadata-filter.shared-content": {
|
||||
"defaultMessage": "Shared content"
|
||||
},
|
||||
"content.metadata-filter.source.external": {
|
||||
"defaultMessage": "External"
|
||||
},
|
||||
"content.metadata-filter.state": {
|
||||
"defaultMessage": "State"
|
||||
},
|
||||
"content.metadata-filter.state.disabled": {
|
||||
"defaultMessage": "Disabled"
|
||||
},
|
||||
"content.metadata-filter.state.enabled": {
|
||||
"defaultMessage": "Enabled"
|
||||
},
|
||||
"content.metadata-filter.update.available": {
|
||||
"defaultMessage": "Update available"
|
||||
},
|
||||
"content.metadata-filter.update.up-to-date": {
|
||||
"defaultMessage": "Up to date"
|
||||
},
|
||||
"content.metadata-filter.updates": {
|
||||
"defaultMessage": "Updates"
|
||||
},
|
||||
"content.metadata-filter.warning.client-depends": {
|
||||
"defaultMessage": "Client depends on file"
|
||||
},
|
||||
"content.metadata-filter.warning.client-only": {
|
||||
"defaultMessage": "Client-only content"
|
||||
},
|
||||
"content.metadata-filter.warning.client-retained": {
|
||||
"defaultMessage": "Client file retained"
|
||||
},
|
||||
"content.metadata-filter.warning.none": {
|
||||
"defaultMessage": "No warnings"
|
||||
},
|
||||
"content.metadata-filter.warnings": {
|
||||
"defaultMessage": "Warnings"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Additional content"
|
||||
},
|
||||
@@ -593,6 +686,12 @@
|
||||
"content.page-layout.failed-to-load": {
|
||||
"defaultMessage": "Failed to load content"
|
||||
},
|
||||
"content.page-layout.filter.add": {
|
||||
"defaultMessage": "Filter"
|
||||
},
|
||||
"content.page-layout.filter.author-count": {
|
||||
"defaultMessage": "{count, plural, one {# author} other {# authors}}"
|
||||
},
|
||||
"content.page-layout.loading": {
|
||||
"defaultMessage": "Loading content..."
|
||||
},
|
||||
@@ -620,8 +719,11 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Project names"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alphabetical"
|
||||
"content.page-layout.sort.alphabetical-ascending": {
|
||||
"defaultMessage": "Name (A-Z)"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical-descending": {
|
||||
"defaultMessage": "Name (Z-A)"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Newest first"
|
||||
@@ -1772,6 +1874,9 @@
|
||||
"hosting.content.failed-to-upload": {
|
||||
"defaultMessage": "Failed to upload file"
|
||||
},
|
||||
"hosting.content.managed-content.modpack-header": {
|
||||
"defaultMessage": "Modpack content"
|
||||
},
|
||||
"hosting.loader.failed-to-change-version": {
|
||||
"defaultMessage": "Failed to change modpack version"
|
||||
},
|
||||
@@ -2081,31 +2186,31 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Select icon"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "This modpack does not include any additional content."
|
||||
"instances.managed-content-modal.empty-description": {
|
||||
"defaultMessage": "This source does not include any managed content."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"instances.managed-content-modal.empty-title": {
|
||||
"defaultMessage": "No content found"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"instances.managed-content-modal.external-content": {
|
||||
"defaultMessage": "External"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"instances.managed-content-modal.external-content-description": {
|
||||
"defaultMessage": "This file is not published on Modrinth."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Modpack content"
|
||||
"instances.managed-content-modal.header": {
|
||||
"defaultMessage": "Managed content"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"instances.managed-content-modal.loading": {
|
||||
"defaultMessage": "Loading content..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"instances.managed-content-modal.no-results": {
|
||||
"defaultMessage": "No projects match your search."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"instances.managed-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "Open in Slicer"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"instances.managed-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Search {count, number} {count, plural, one {project} other {projects}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
|
||||
@@ -566,9 +566,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "mundo"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Ajustes de instalación"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Contenido adicional"
|
||||
},
|
||||
@@ -620,18 +617,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Nombres de los proyectos"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alfabético"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Más recientes primero"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Más antiguo primero"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Ordenar por {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Actualizar todo"
|
||||
},
|
||||
@@ -2081,33 +2066,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Seleccionar ícono"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Este modpack no incluye ningún contenido adicional."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "No se encontró contenido"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Externo"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Este archivo no está publicado en Modrinth."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Contenido del modpack"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Cargando contenido..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Ningún proyecto coincide con tu búsqueda."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "Abrir en Slicer"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Buscar en {count, number} {count, plural, one {proyecto} other {proyectos}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Actual"
|
||||
},
|
||||
@@ -6231,4 +6189,3 @@
|
||||
"defaultMessage": "Tipo"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -566,9 +566,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": " mundo"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Configuración de la instalación"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Contenido adicional"
|
||||
},
|
||||
@@ -620,18 +617,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Nombres de los proyectos"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Por orden alfabético"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Más recientes primero"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Más antiguo primero"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Ordenar por {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Actualizar todo"
|
||||
},
|
||||
@@ -2081,33 +2066,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Seleccionar icono"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Este modpack no incluye contenido adicional."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "No se ha encontrado contenido"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Externo"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Este archivo no está publicado en Modrinth."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Contenido del modpack"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Cargando contenido..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "No hay proyectos que coincidan con tu búsqueda."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "Abrir en Slicer"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Buscar {count, number} {count, plural, one {proyecto} other {proyectos}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Actual"
|
||||
},
|
||||
|
||||
@@ -266,15 +266,6 @@
|
||||
"content.page-layout.share.project-links": {
|
||||
"defaultMessage": "Mga link ng proyekto"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alpabetiko"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Pinakabago ang mauuna"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Pinakaluma ang mauuna"
|
||||
},
|
||||
"content.selection-bar.all-already-disabled": {
|
||||
"defaultMessage": "Nakapatay na ang lahat ng piniling konento"
|
||||
},
|
||||
@@ -662,24 +653,6 @@
|
||||
"instances.content-install.no-instances": {
|
||||
"defaultMessage": "Walang mahanap na magkatugmang instansiya"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Hindi naglalaman ng karagdagang kontento ang modpack na ito."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Walang mahanap na kontento"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Kontentong modpack"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Inihahanda ang kontento..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Walang proyektong tumugma sa iyong paghanap."
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Maghanap sa {count, number} {count, plural, one {proyekto} other {na proyekto}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Kasalukuyan"
|
||||
},
|
||||
@@ -2763,4 +2736,3 @@
|
||||
"defaultMessage": "Uri"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -566,9 +566,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "monde"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Paramètres d'installation"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Contenu supplémentaire"
|
||||
},
|
||||
@@ -620,18 +617,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Noms des projets"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alphabétique"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Plus récent en premier"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Plus ancien en premier"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Trier par {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Tout mettre à jour"
|
||||
},
|
||||
@@ -2075,33 +2060,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Sélectionnez l'icône"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Aucun contenu supplémentaire n'est inclus dans ce modpack."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Aucun contenu trouvé"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Externe"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Ce fichier n'est pas publié sur Modrinth."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Contenu du modpack"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Chargement du contenu..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Aucun projet correspond à votre recherche."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "Ouvrir dans Slicer"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Rechercher {count, number} {count, plural, one {projet} other {projets}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Actuel"
|
||||
},
|
||||
|
||||
@@ -494,9 +494,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "világ"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Telepítési beállítások"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "További tartalom"
|
||||
},
|
||||
@@ -548,18 +545,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Projektnevek"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Ábécé sorrendben"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Legújabbak elöl"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Legrégebbiek elöl"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Rendezés {mode} szerint"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Összes frissítése"
|
||||
},
|
||||
@@ -1733,33 +1718,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Ikon feltöltése"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Ez a modcsomag nem tartalmaz további tartalmakat."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Nincsen tartalom"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Külső"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Ez a fájl nem szerepel a Modrinthon."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Modcsomag tartalma"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Tartalmak betöltése..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Nincsen olyan projekt ami keresnél."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "Megnyitás a Slicer-ben"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Keresés {count, number} projekt között"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Jelenlegi"
|
||||
},
|
||||
|
||||
@@ -503,18 +503,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Nama proyek"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Menurut abjad"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Terbaru dahulu"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Tertua dahulu"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Urutkan berdasarkan {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Perbarui semua"
|
||||
},
|
||||
@@ -1172,24 +1160,6 @@
|
||||
"instance.worlds.game_mode.unknown": {
|
||||
"defaultMessage": "Mode permainan tak dikenal"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Paket mod ini tidak termasuk konten tambahan."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Konten tidak ditemukan"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Konten paket mod"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Sedang memuat konten..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Tidak ada proyek yang cocok dengan pencarian Anda."
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Cari {count, number} {count, plural, other {proyek}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Saat ini"
|
||||
},
|
||||
@@ -2982,4 +2952,3 @@
|
||||
"defaultMessage": "Anda memiliki perubahan yang belum tersimpan."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -545,9 +545,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "mondo"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Impostazioni installazione"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Contenuto aggiuntivo"
|
||||
},
|
||||
@@ -599,18 +596,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Nomi dei progetti"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alfabetico"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Più recenti prima"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Meno recenti prima"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Ordina per {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Aggiorna tutto"
|
||||
},
|
||||
@@ -2039,33 +2024,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Scegli un'icona"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Questo pacchetto di mod non include alcun contenuto aggiuntivo."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Nessun contenuto trovato"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Esterno"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Questo file non è su Modrinth."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Contenuto del pacchetto"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Caricamento contenuto..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Nessun progetto corrisponde alla tua ricerca."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "Apri con Slicer"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Cerca tra {count, number} {count, plural, one {progetto} other {progetti}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Attuale"
|
||||
},
|
||||
|
||||
@@ -563,9 +563,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "ワールド"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "インストール設定"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "追加のコンテンツ"
|
||||
},
|
||||
@@ -617,18 +614,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "プロジェクト名"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "名前順"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "新しい順"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "古い順"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "{mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "すべて更新"
|
||||
},
|
||||
@@ -2078,33 +2063,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "アイコンを選択"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "このModパックに追加コンテンツは一切含まれていません。"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "コンテンツが見つかりません"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "外部"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "このファイルは Modrinth 上で公開されていません"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Modpackコンテンツ"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "コンテンツを読み込み中…"
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "該当するプロジェクトがありません。"
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "Slicer で開く"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "{count, number}件のプロジェクトを検索"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "現在"
|
||||
},
|
||||
|
||||
@@ -566,9 +566,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "세계"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "설치 설정"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "추가 콘텐츠"
|
||||
},
|
||||
@@ -620,18 +617,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "프로젝트 이름"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "알파벳순"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "최신순"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "오래된순"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "{mode}으로 정렬"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "모두 업데이트"
|
||||
},
|
||||
@@ -2081,33 +2066,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "아이콘 선택"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "이 모드팩에는 추가 콘텐츠가 포함되어 있지 않습니다."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "콘텐츠를 찾을 수 없습니다"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "외부"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "이 파일은 Modrinth에 게시되지 않았습니다."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "모드팩 콘텐츠"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "콘텐츠 불러오는 중..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "검색 결과와 일치하는 프로젝트가 없습니다."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "Slicer에서 열기"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "{count, number}개의 프로젝트 검색"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "현재"
|
||||
},
|
||||
|
||||
@@ -488,18 +488,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Nama projek"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Mengikut abjad"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Paling baharu dahulu"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Paling lama dahulu"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Isih mengikut {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Kemas kini semua"
|
||||
},
|
||||
@@ -1679,24 +1667,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Pilih ikon"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Pek mod ini tidak menyertakan sebarang kandungan tambahan."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Tiada kandungan dijumpai"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Kandungan pek mod"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Sedang memuat kandungan..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Tiada projek yang sepadan dengan carian anda."
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Cari {count, number} {count, plural, other {projek}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Semasa"
|
||||
},
|
||||
@@ -5055,4 +5025,3 @@
|
||||
"defaultMessage": "Jenis"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -566,9 +566,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "wereld"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Installatie-instellingen"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Extra inhoud"
|
||||
},
|
||||
@@ -620,18 +617,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Projectnamen"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alfabetisch"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Nieuwste eerst"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Oudste eerst"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Sorteer op {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Werk alles bij"
|
||||
},
|
||||
@@ -2078,33 +2063,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Selecteer pictogram"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Deze modpack bevat geen extra inhoud."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Geen inhoud gevonden"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Extern"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Dit bestand is niet gepubliceerd op Modrinth."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Inhoud van modpack"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Inhoud aan het laden..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Geen projecten voldoen aan je zoekopdracht."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "In Slicer openen"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Zoek naar {count, number} {count, plural, one {project} other {projecten}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Huidige"
|
||||
},
|
||||
|
||||
@@ -413,18 +413,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Prosjektnavn"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alfabetisk"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Nyeste først"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Eldste først"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Sorter etter {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Oppdater alle"
|
||||
},
|
||||
@@ -872,18 +860,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Velg ikon"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Ingen innhold funnet"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Laster inn innhold..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Ingen prosjekter samsvarte med søket ditt."
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Søk etter {count, number} {count, plural, one {prosjekt} other {prosjekter}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Nåværende"
|
||||
},
|
||||
@@ -2778,4 +2754,3 @@
|
||||
"defaultMessage": "Du har ulagrede endringer."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -566,9 +566,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "Twój świat"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Ustawienia instalacji"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Dodatkowa zawartość"
|
||||
},
|
||||
@@ -620,18 +617,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Nazwy projektów"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alfabetycznie"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Od najnowszych"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Od najstarszych"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Sortuj według: {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Zaktualizuj wszystko"
|
||||
},
|
||||
@@ -2072,30 +2057,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Wybierz ikonę"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Ta paczka modów nie zawiera żadnych innych treści."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Nie znaleziono treści"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Zewnętrzne"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Ten plik nie jest opublikowany na Modrinth."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Zawartość paczki modów"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Ładowanie treści..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Nie znaleziono żadnego projektu pasującego do Twojego wyszukiwania."
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Szukaj wśród {count, number} {count, plural, one {projektu} other {projektów}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Aktualny"
|
||||
},
|
||||
|
||||
@@ -566,9 +566,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "mundo"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Configurações de instalação"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Conteúdo adicional"
|
||||
},
|
||||
@@ -620,18 +617,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Nomes dos projetos"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alfabético"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Mais recentes primeiro"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Mais antigos primeiro"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Ordenar por {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Atualizar tudo"
|
||||
},
|
||||
@@ -2081,33 +2066,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Selecionar ícone"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Este pacote de mods não inclui nenhum conteúdo adicional."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Nenhum conteúdo encontrado"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Externo"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Este arquivo não está publicado no Modrinth."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Conteúdo do pacote de mods"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Carregando conteúdo..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Nenhum projeto corresponde com sua busca."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "Abrir no Slicer"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Busque {count, number} {count, plural, one {projeto} other {projetos}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Atual"
|
||||
},
|
||||
@@ -6231,4 +6189,3 @@
|
||||
"defaultMessage": "Tipo"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -398,18 +398,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Nomes dos projetos"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alfabético"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Mais recentes primeiro"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Mais antigos primeiro"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Ordenar por {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Atualizar tudo"
|
||||
},
|
||||
@@ -749,21 +737,6 @@
|
||||
"instance.worlds.game_mode.unknown": {
|
||||
"defaultMessage": "Modo de jogo desconhecido"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Este modpack não inclui nenhum conteúdo adicional."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Nenhum conteúdo encontrado"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Conteúdo do modpack"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "A carregar conteúdo..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Nenhum projeto corresponde à tua pesquisa."
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Atual"
|
||||
},
|
||||
@@ -2523,4 +2496,3 @@
|
||||
"defaultMessage": "Tens alterações por guardar."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -425,18 +425,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Nume ale proiectului"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alfabetic"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Cele noi mai întâi"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Cele vechi mai întâi"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Sortează cu {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Actualizează tot"
|
||||
},
|
||||
@@ -1022,24 +1010,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Selectează iconița"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Acest modpack nu include niciun conținut suplimentar."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Nu a fost găsit niciun conținut"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Conținutul Modpack"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Se încarcă materialul..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Niciun proiect nu corespunde căutării dvs."
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Căutare {count, number}{count, plural,one {proiect}other {proiecte}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Curent"
|
||||
},
|
||||
|
||||
@@ -536,9 +536,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "мир"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Настройки установки"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Дополнительный контент"
|
||||
},
|
||||
@@ -587,18 +584,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Названия проектов"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "По алфавиту"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Сначала новые"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Сначала старые"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Сортировка: {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Обновить всё"
|
||||
},
|
||||
@@ -1997,33 +1982,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Выбрать иконку"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "В сборке нет дополнительного контента."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Сборка пуста"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Сторонний"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Файл не опубликован на Modrinth."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Содержимое сборки"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Загрузка содержимого..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Не найдены проекты, соответствующие запросу."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "Открыть в Slicer"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Поиск по {count, number} {count, plural, one {проекту} other {проектам}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Текущая"
|
||||
},
|
||||
|
||||
@@ -584,18 +584,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Imena projekata"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alfabetski"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Najnovije prvo"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Najstarije prvo"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Sortiraj so {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Ažuiraj sve"
|
||||
},
|
||||
@@ -2030,24 +2018,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Izaberi ikonu"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Ovaj modpack ne uključuje nikakav dodatni sadržaj."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Nema pronađenog sadržaja"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Sadržaj modpacka"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Učitavanje sadržaja..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Nijedan projekat ne odgovara tvojoj pretrazi."
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Pretraga {count, number} {count, plural, one {projekta} few {projekta} other {projekata}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Upravo"
|
||||
},
|
||||
@@ -5781,4 +5751,3 @@
|
||||
"defaultMessage": "Tip"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -560,9 +560,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "värld"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Installationsinställningar"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Ytterligare innehåll"
|
||||
},
|
||||
@@ -614,18 +611,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Projektnamn"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alfabetisk"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Nyaste först"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Äldsta först"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Sortera i {mode} ordning"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Uppdatera alla"
|
||||
},
|
||||
@@ -1802,30 +1787,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Välj ikon"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Modpaketet innehåller inget ytterligare innehåll."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Inget innehåll hittades"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Den här filen är inte publiserad på Modrinth."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Modpaketets innehåll"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Laddar innehåll..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Inga projekt matchar din sökning."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "Öppna i Slicer"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Sök genom {count, number} projekt"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Nuvarande"
|
||||
},
|
||||
@@ -5463,4 +5424,3 @@
|
||||
"defaultMessage": "Typ"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -317,15 +317,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "เลือกไอคอน"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "ไม่พบเนื้อหา"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "เนื้อหาม็อดแพ็ค"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "กำลังโหลดเนื้อหา"
|
||||
},
|
||||
"label.available": {
|
||||
"defaultMessage": "ว่างอยู่ {amount}"
|
||||
},
|
||||
@@ -663,4 +654,3 @@
|
||||
"defaultMessage": "ภาษา"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -596,18 +596,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Proje isimleri"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Alfabetik"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Önce en yeni"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Önce en eski"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Sıralama şekli {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Tümünü güncelle"
|
||||
},
|
||||
@@ -2051,27 +2039,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Simge seç"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Bu mod paketi başka hiç bir ekstra işerik içermez."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "İçerik bulunamadı"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Harici"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Mod paketi içeriği"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "İçerik yükleniyor..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Aramanızla eşleşen proje yok."
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Search {count, number} {count, plural, one {project} other {projects}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Şu anki"
|
||||
},
|
||||
|
||||
@@ -563,9 +563,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "світ"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "Налаштування інсталяції"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "Додатковий уміст"
|
||||
},
|
||||
@@ -611,18 +608,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Назви проєктів"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "За абеткою"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Спочатку новіші"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Спочатку старіші"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Сортування: {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Оновити все"
|
||||
},
|
||||
@@ -2036,33 +2021,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Вибрати значок"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Збірка не включає додатковий уміст."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Умісту не знайдено"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "Зовнішні"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "Цей файл не опубліковано на Modrinth."
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Уміст збірки"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Завантаження вмісту…"
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "За вашим запитом не знайдено жодного проєкту."
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "Відкрити в Slicer"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Пошук {count, number} {count, plural, one {проєкту} other {проєктів}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Поточна"
|
||||
},
|
||||
|
||||
@@ -530,18 +530,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "Tên dự án"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "Thứ tự bảng chữ cái"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "Mới nhất"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "Cũ nhất"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "Sắp xếp theo {mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "Cập nhật tất cả"
|
||||
},
|
||||
@@ -1709,24 +1697,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "Chọn biểu tượng"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "Gói modpack này không bao gồm các nội dung bổ sung nào."
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "Không có nội dung nào được tìm thấy"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "Nội dung modpack"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "Đang tải nội dung..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "Không có dự án nào phù hợp với kết quả tìm kiếm."
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "Tìm {count, number} {count, plural, other {dự án}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "Hiện tại"
|
||||
},
|
||||
@@ -5079,4 +5049,3 @@
|
||||
"defaultMessage": "Đội ngũ Modrinth"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -566,9 +566,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "世界"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "安装设置"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "附加内容"
|
||||
},
|
||||
@@ -620,18 +617,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "项目名称"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "按字母顺序"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "按从新到旧顺序"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "按从旧到新顺序"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "按{mode}排序"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "更新所有"
|
||||
},
|
||||
@@ -2081,33 +2066,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "选择图标"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "这个整合包不包含任何额外内容。"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "未找到内容"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "外部来源"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "此文件未发布于 Modrinth 平台。"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "整合包内容"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "加载内容……"
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "没有符合搜索条件的项目。"
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "在 Slicer 中打开"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "搜索 {count, number} {count, plural, other {个项目}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "当前"
|
||||
},
|
||||
@@ -6231,4 +6189,3 @@
|
||||
"defaultMessage": "类型"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -566,9 +566,6 @@
|
||||
"content.inline-backup.world-label": {
|
||||
"defaultMessage": "世界"
|
||||
},
|
||||
"content.modpack-card.installation-settings": {
|
||||
"defaultMessage": "安裝設定"
|
||||
},
|
||||
"content.page-layout.additional-content": {
|
||||
"defaultMessage": "額外內容"
|
||||
},
|
||||
@@ -620,18 +617,6 @@
|
||||
"content.page-layout.share.project-names": {
|
||||
"defaultMessage": "專案名稱"
|
||||
},
|
||||
"content.page-layout.sort.alphabetical": {
|
||||
"defaultMessage": "字母順序"
|
||||
},
|
||||
"content.page-layout.sort.date-added-newest": {
|
||||
"defaultMessage": "最新在前"
|
||||
},
|
||||
"content.page-layout.sort.date-added-oldest": {
|
||||
"defaultMessage": "最舊在前"
|
||||
},
|
||||
"content.page-layout.sort.label": {
|
||||
"defaultMessage": "排序依據:{mode}"
|
||||
},
|
||||
"content.page-layout.update-all": {
|
||||
"defaultMessage": "更新全部"
|
||||
},
|
||||
@@ -2081,33 +2066,6 @@
|
||||
"instances.content-install.select-icon": {
|
||||
"defaultMessage": "選擇圖示"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-description": {
|
||||
"defaultMessage": "這個模組包不包含任何額外內容。"
|
||||
},
|
||||
"instances.modpack-content-modal.empty-title": {
|
||||
"defaultMessage": "找不到內容"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content": {
|
||||
"defaultMessage": "外部"
|
||||
},
|
||||
"instances.modpack-content-modal.external-content-description": {
|
||||
"defaultMessage": "這個檔案未在 Modrinth 上發布。"
|
||||
},
|
||||
"instances.modpack-content-modal.header": {
|
||||
"defaultMessage": "模組包內容"
|
||||
},
|
||||
"instances.modpack-content-modal.loading": {
|
||||
"defaultMessage": "正在載入內容..."
|
||||
},
|
||||
"instances.modpack-content-modal.no-results": {
|
||||
"defaultMessage": "沒有專案符合你的搜尋字詞。"
|
||||
},
|
||||
"instances.modpack-content-modal.open-in-slicer": {
|
||||
"defaultMessage": "在 Slicer 中開啟"
|
||||
},
|
||||
"instances.modpack-content-modal.search-placeholder": {
|
||||
"defaultMessage": "搜尋 {count, number} {count, plural, other {個專案}}"
|
||||
},
|
||||
"instances.updater-modal.badge.current": {
|
||||
"defaultMessage": "目前"
|
||||
},
|
||||
@@ -6231,4 +6189,3 @@
|
||||
"defaultMessage": "類型"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export {
|
||||
type ContentManagerContext,
|
||||
type ContentModpackData,
|
||||
injectContentManager,
|
||||
type ManagedContentData,
|
||||
provideContentManager,
|
||||
type UploadState,
|
||||
} from '../layouts/shared/content-tab/providers/content-manager'
|
||||
|
||||
@@ -119,6 +119,7 @@ const etfItem: ContentCardTableItem = {
|
||||
|
||||
const importedModItem: ContentCardTableItem = {
|
||||
id: 'imported123',
|
||||
external: true,
|
||||
project: {
|
||||
id: 'imported123',
|
||||
slug: 'import-mod',
|
||||
|
||||
@@ -1,633 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { fn } from 'storybook/test'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import NewModal from '../../components/modal/NewModal.vue'
|
||||
import ContentCardItem from '../../layouts/shared/content-tab/components/ContentCardItem.vue'
|
||||
import ContentModpackCard from '../../layouts/shared/content-tab/components/ContentModpackCard.vue'
|
||||
import type {
|
||||
ContentModpackCardCategory,
|
||||
ContentModpackCardProject,
|
||||
ContentModpackCardVersion,
|
||||
ContentOwner,
|
||||
} from '../../layouts/shared/content-tab/types'
|
||||
|
||||
// Real project data from Modrinth API
|
||||
const fabulouslyOptimizedProject: ContentModpackCardProject = {
|
||||
id: '1KVo5zza',
|
||||
slug: 'fabulously-optimized',
|
||||
title: 'Fabulously Optimized',
|
||||
icon_url:
|
||||
'https://cdn.modrinth.com/data/1KVo5zza/9f1ded4949c2a9db5ca382d3bcc912c7245486b4_96.webp',
|
||||
description:
|
||||
'Beautiful graphics, speedy performance and familiar features in a simple package. 1.21.11 beta!',
|
||||
downloads: 8708191,
|
||||
followers: 3762,
|
||||
}
|
||||
|
||||
const cobblemonProject: ContentModpackCardProject = {
|
||||
id: '5FFgwNNP',
|
||||
slug: 'cobblemon-fabric',
|
||||
title: 'Cobblemon Official Modpack [Fabric]',
|
||||
icon_url: 'https://cdn.modrinth.com/data/5FFgwNNP/e7f9ee2e9d361623847853fe2ddce42f519ee64f.png',
|
||||
description: 'The official modpack of the Cobblemon mod, for Fabric!',
|
||||
downloads: 4940845,
|
||||
followers: 2051,
|
||||
}
|
||||
|
||||
const simplyOptimizedProject: ContentModpackCardProject = {
|
||||
id: 'BYfVnHa7',
|
||||
slug: 'sop',
|
||||
title: 'Simply Optimized',
|
||||
icon_url: 'https://cdn.modrinth.com/data/BYfVnHa7/845e93223da7e8d1ed1a33364b5bdb4c316ac518.png',
|
||||
description:
|
||||
'The leading, well-researched optimization modpack with a focus on pure performance.',
|
||||
downloads: 2903242,
|
||||
followers: 1387,
|
||||
}
|
||||
|
||||
// Version data from Modrinth API
|
||||
const fabulouslyOptimizedVersion: ContentModpackCardVersion = {
|
||||
id: 'YEEXo8mO',
|
||||
version_number: '1.12.1',
|
||||
date_published: '2022-02-10T06:53:28.379507Z',
|
||||
}
|
||||
|
||||
const cobblemonVersion: ContentModpackCardVersion = {
|
||||
id: 'bpaivauC',
|
||||
version_number: '1.5.2',
|
||||
date_published: '2024-05-27T07:12:36.043005Z',
|
||||
}
|
||||
|
||||
// Owner data from Modrinth API
|
||||
const userOwner: ContentOwner = {
|
||||
id: '2avTeeAE',
|
||||
name: 'robotkoer',
|
||||
avatar_url: 'https://cdn.modrinth.com/user/2avTeeAE/icon.png',
|
||||
type: 'user',
|
||||
}
|
||||
|
||||
const cobblemonOwner: ContentOwner = {
|
||||
id: 'AEFONbAM',
|
||||
name: 'Reisen',
|
||||
avatar_url:
|
||||
'https://cdn.modrinth.com/user/AEFONbAM/9e97453507a8245981d5cd825280f23be44f15ac.jpeg',
|
||||
type: 'user',
|
||||
}
|
||||
|
||||
// Categories (using Labrinth.Tags.v2.Category structure with optional action)
|
||||
const optimizationCategories: ContentModpackCardCategory[] = [
|
||||
{ name: 'Fabric', icon: 'fabric', project_type: 'modpack', header: 'loaders' },
|
||||
{ name: 'Lightweight', icon: 'lightweight', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Multiplayer', icon: 'multiplayer', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Optimization', icon: 'optimization', project_type: 'modpack', header: 'categories' },
|
||||
]
|
||||
|
||||
const cobblemonCategories: ContentModpackCardCategory[] = [
|
||||
{ name: 'Adventure', icon: 'adventure', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Fabric', icon: 'fabric', project_type: 'modpack', header: 'loaders' },
|
||||
{ name: 'Lightweight', icon: 'lightweight', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Multiplayer', icon: 'multiplayer', project_type: 'modpack', header: 'categories' },
|
||||
]
|
||||
|
||||
const meta = {
|
||||
title: 'Instances/ContentModpackCard',
|
||||
component: ContentModpackCard,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
},
|
||||
argTypes: {
|
||||
project: {
|
||||
control: 'object',
|
||||
description:
|
||||
'Project information (id, slug, title, icon_url, description, downloads, followers)',
|
||||
},
|
||||
version: {
|
||||
control: 'object',
|
||||
description: 'Version information (id, version_number, date_published)',
|
||||
},
|
||||
owner: {
|
||||
control: 'object',
|
||||
description: 'Owner/author information (user or organization)',
|
||||
},
|
||||
categories: {
|
||||
control: 'object',
|
||||
description: 'Category tags with optional click actions',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Grays out the card when true',
|
||||
},
|
||||
overflowOptions: {
|
||||
control: 'object',
|
||||
description: 'Options for the overflow menu',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof ContentModpackCard>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// ============================================
|
||||
// All Types Overview
|
||||
// ============================================
|
||||
|
||||
export const AllTypes: Story = {
|
||||
args: {
|
||||
project: fabulouslyOptimizedProject,
|
||||
},
|
||||
render: () => ({
|
||||
components: { ContentModpackCard },
|
||||
setup() {
|
||||
const cards = [
|
||||
{
|
||||
label: 'Full featured (all actions)',
|
||||
project: fabulouslyOptimizedProject,
|
||||
version: fabulouslyOptimizedVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
hasUpdate: true,
|
||||
hasContent: true,
|
||||
hasUnlink: true,
|
||||
},
|
||||
{
|
||||
label: 'With update available only',
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: cobblemonOwner,
|
||||
categories: cobblemonCategories,
|
||||
hasUpdate: true,
|
||||
},
|
||||
{
|
||||
label: 'With content button only',
|
||||
project: simplyOptimizedProject,
|
||||
version: fabulouslyOptimizedVersion,
|
||||
owner: userOwner,
|
||||
hasContent: true,
|
||||
},
|
||||
{
|
||||
label: 'Minimal (project only)',
|
||||
project: fabulouslyOptimizedProject,
|
||||
},
|
||||
{
|
||||
label: 'With version info only',
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
},
|
||||
{
|
||||
label: 'With owner only',
|
||||
project: simplyOptimizedProject,
|
||||
owner: userOwner,
|
||||
},
|
||||
{
|
||||
label: 'Disabled state',
|
||||
project: fabulouslyOptimizedProject,
|
||||
version: fabulouslyOptimizedVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
return { cards }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="flex flex-col gap-6">
|
||||
<template v-for="card in cards" :key="card.label">
|
||||
<h3 class="text-sm font-medium text-secondary">{{ card.label }}</h3>
|
||||
<ContentModpackCard
|
||||
:project="card.project"
|
||||
:version="card.version"
|
||||
:owner="card.owner"
|
||||
:categories="card.categories"
|
||||
:disabled="card.disabled"
|
||||
@update="card.hasUpdate ? () => {} : undefined"
|
||||
@content="card.hasContent ? () => {} : undefined"
|
||||
@unlink="card.hasUnlink ? () => {} : undefined"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Basic Stories
|
||||
// ============================================
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
onUpdate: fn(),
|
||||
onContent: fn(),
|
||||
onUnlink: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const MinimalProjectOnly: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithVersion: Story = {
|
||||
args: {
|
||||
project: simplyOptimizedProject,
|
||||
version: fabulouslyOptimizedVersion,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithUserOwner: Story = {
|
||||
args: {
|
||||
project: simplyOptimizedProject,
|
||||
version: fabulouslyOptimizedVersion,
|
||||
owner: userOwner,
|
||||
categories: [
|
||||
{ name: 'Adventure', icon: 'adventure', project_type: 'modpack', header: 'categories' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const WithOrganizationOwner: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Action Button Stories
|
||||
// ============================================
|
||||
|
||||
export const WithUpdateButton: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
onUpdate: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const WithContentButton: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
onContent: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const WithUnlinkButton: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
onUnlink: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const WithAllActions: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
onUpdate: fn(),
|
||||
onContent: fn(),
|
||||
onUnlink: fn(),
|
||||
overflowOptions: [
|
||||
{ id: 'view', action: () => console.log('View') },
|
||||
{ id: 'settings', action: () => console.log('Settings') },
|
||||
{ divider: true },
|
||||
{ id: 'remove', action: () => console.log('Remove'), color: 'red' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// State Stories
|
||||
// ============================================
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const LongTitle: Story = {
|
||||
args: {
|
||||
project: {
|
||||
...cobblemonProject,
|
||||
title: 'Super Long Modpack Title That Should Display Properly On All Screen Sizes',
|
||||
description:
|
||||
'This is an extremely long description that should wrap properly and not break the layout. It contains lots of information about what this modpack includes and what makes it special compared to other modpacks available on the platform.',
|
||||
},
|
||||
version: cobblemonVersion,
|
||||
owner: {
|
||||
...userOwner,
|
||||
name: 'Really Long Organization Name Studios',
|
||||
},
|
||||
categories: [
|
||||
{ name: 'Adventure', icon: 'adventure', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Technology', icon: 'technology', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Magic', icon: 'magic', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Exploration', icon: 'exploration', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Multiplayer', icon: 'multiplayer', project_type: 'modpack', header: 'categories' },
|
||||
],
|
||||
onUpdate: fn(),
|
||||
onContent: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const NoDescription: Story = {
|
||||
args: {
|
||||
project: {
|
||||
...cobblemonProject,
|
||||
description: undefined,
|
||||
},
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
},
|
||||
}
|
||||
|
||||
export const NoStats: Story = {
|
||||
args: {
|
||||
project: {
|
||||
...cobblemonProject,
|
||||
downloads: undefined,
|
||||
followers: undefined,
|
||||
},
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Categories Stories
|
||||
// ============================================
|
||||
|
||||
export const WithClickableCategories: Story = {
|
||||
render: (args) => ({
|
||||
components: { ContentModpackCard },
|
||||
setup() {
|
||||
const clickedCategory = ref<string | null>(null)
|
||||
const categories: ContentModpackCardCategory[] = [
|
||||
{
|
||||
name: 'Adventure',
|
||||
icon: 'adventure',
|
||||
project_type: 'modpack',
|
||||
header: 'categories',
|
||||
action: () => (clickedCategory.value = 'Adventure'),
|
||||
},
|
||||
{
|
||||
name: 'Lightweight',
|
||||
icon: 'lightweight',
|
||||
project_type: 'modpack',
|
||||
header: 'categories',
|
||||
action: () => (clickedCategory.value = 'Lightweight'),
|
||||
},
|
||||
{
|
||||
name: 'Multiplayer',
|
||||
icon: 'multiplayer',
|
||||
project_type: 'modpack',
|
||||
header: 'categories',
|
||||
action: () => (clickedCategory.value = 'Multiplayer'),
|
||||
},
|
||||
]
|
||||
return { args, categories, clickedCategory }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="flex flex-col gap-4">
|
||||
<ContentModpackCard
|
||||
:project="args.project"
|
||||
:version="args.version"
|
||||
:owner="args.owner"
|
||||
:categories="categories"
|
||||
/>
|
||||
<div class="text-sm text-secondary">
|
||||
Clicked category: <strong>{{ clickedCategory || 'None' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Overflow Menu Stories
|
||||
// ============================================
|
||||
|
||||
export const WithOverflowMenu: Story = {
|
||||
render: (args) => ({
|
||||
components: { ContentModpackCard },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<ContentModpackCard v-bind="args">
|
||||
<template #view>View on Modrinth</template>
|
||||
<template #settings>Settings</template>
|
||||
<template #remove>Remove modpack</template>
|
||||
</ContentModpackCard>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
overflowOptions: [
|
||||
{ id: 'view', action: () => console.log('View') },
|
||||
{ id: 'settings', action: () => console.log('Settings') },
|
||||
{ divider: true },
|
||||
{ id: 'remove', action: () => console.log('Remove'), color: 'red' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Interactive Stories
|
||||
// ============================================
|
||||
|
||||
export const WithContentModal: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
},
|
||||
render: () => ({
|
||||
components: { ContentModpackCard, NewModal, ContentCardItem },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const modpackContent = [
|
||||
{
|
||||
project: {
|
||||
id: '1',
|
||||
slug: 'sodium',
|
||||
title: 'Sodium',
|
||||
icon_url:
|
||||
'https://cdn.modrinth.com/data/AANobbMI/295862f4724dc3f78df3447ad6072b2dcd3ef0c9_96.webp',
|
||||
},
|
||||
version: { id: 'v1', version_number: '0.8.2', file_name: 'sodium-fabric-0.8.2.jar' },
|
||||
},
|
||||
{
|
||||
project: {
|
||||
id: '2',
|
||||
slug: 'modmenu',
|
||||
title: 'Mod Menu',
|
||||
icon_url:
|
||||
'https://cdn.modrinth.com/data/mOgUt4GM/5a20ed1450a0e1e79a1fe04e61bb4e5878bf1d20.png',
|
||||
},
|
||||
version: { id: 'v2', version_number: '16.0.0', file_name: 'modmenu-16.0.0.jar' },
|
||||
},
|
||||
{
|
||||
project: {
|
||||
id: '3',
|
||||
slug: 'fabric-api',
|
||||
title: 'Fabric API',
|
||||
icon_url: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png',
|
||||
},
|
||||
version: { id: 'v3', version_number: '0.141.3', file_name: 'fabric-api-0.141.3.jar' },
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
cobblemonProject,
|
||||
cobblemonVersion,
|
||||
userOwner,
|
||||
optimizationCategories,
|
||||
modalRef,
|
||||
modpackContent,
|
||||
}
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
<ContentModpackCard
|
||||
:project="cobblemonProject"
|
||||
:version="cobblemonVersion"
|
||||
:owner="userOwner"
|
||||
:categories="optimizationCategories"
|
||||
@content="modalRef?.show()"
|
||||
@update="() => alert('Update clicked')"
|
||||
/>
|
||||
<NewModal ref="modalRef" header="Modpack Content">
|
||||
<div class="flex flex-col gap-4">
|
||||
<ContentCardItem
|
||||
v-for="item in modpackContent"
|
||||
:key="item.project.id"
|
||||
:project="item.project"
|
||||
:version="item.version"
|
||||
/>
|
||||
</div>
|
||||
</NewModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Responsive Stories
|
||||
// ============================================
|
||||
|
||||
export const ResponsiveView: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
},
|
||||
render: () => ({
|
||||
components: { ContentModpackCard },
|
||||
setup() {
|
||||
return {
|
||||
cobblemonProject,
|
||||
cobblemonVersion,
|
||||
userOwner,
|
||||
optimizationCategories,
|
||||
}
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="flex flex-col gap-8">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-secondary mb-2">Desktop (full width)</h3>
|
||||
<div class="w-full">
|
||||
<ContentModpackCard
|
||||
:project="cobblemonProject"
|
||||
:version="cobblemonVersion"
|
||||
:owner="userOwner"
|
||||
:categories="optimizationCategories"
|
||||
@update="() => {}"
|
||||
@content="() => {}"
|
||||
@unlink="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-secondary mb-2">Mobile (<640px)</h3>
|
||||
<div class="w-[360px]">
|
||||
<ContentModpackCard
|
||||
:project="cobblemonProject"
|
||||
:version="cobblemonVersion"
|
||||
:owner="userOwner"
|
||||
:categories="optimizationCategories"
|
||||
@update="() => {}"
|
||||
@content="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Edge Cases
|
||||
// ============================================
|
||||
|
||||
export const NoIcon: Story = {
|
||||
args: {
|
||||
project: {
|
||||
...cobblemonProject,
|
||||
icon_url: undefined,
|
||||
},
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
},
|
||||
}
|
||||
|
||||
export const NoOwnerAvatar: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: {
|
||||
...userOwner,
|
||||
avatar_url: undefined,
|
||||
},
|
||||
categories: optimizationCategories,
|
||||
},
|
||||
}
|
||||
|
||||
export const HighDownloadCounts: Story = {
|
||||
args: {
|
||||
project: {
|
||||
...cobblemonProject,
|
||||
downloads: 1234567890,
|
||||
followers: 9876543,
|
||||
},
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { fn } from 'storybook/test'
|
||||
|
||||
import ManagedContentCard from '../../layouts/shared/content-tab/components/managed-content-card/index.vue'
|
||||
import type { ManagedContentCardData } from '../../layouts/shared/content-tab/types'
|
||||
|
||||
const cobblemonIcon =
|
||||
'https://cdn.modrinth.com/data/5FFgwNNP/e7f9ee2e9d361623847853fe2ddce42f519ee64f.png'
|
||||
const serverIcon =
|
||||
'https://cdn.modrinth.com/data/PEMTFQFO/f4140c3a9042e4a825d47dbb64889dad9fdc066c.png'
|
||||
const userAvatar = 'https://cdn.modrinth.com/user/LnK8MbX7/icon.png'
|
||||
|
||||
const figmaSummary = [
|
||||
{ type: 'mod', count: 52 },
|
||||
{ type: 'resourcepack', count: 2 },
|
||||
{ type: 'shader', count: 1 },
|
||||
] satisfies ManagedContentCardData['summary']
|
||||
|
||||
const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString()
|
||||
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000
|
||||
|
||||
const modpackCard = {
|
||||
kind: 'modpack',
|
||||
manager: {
|
||||
name: 'Cobblemon Modpack',
|
||||
iconUrl: cobblemonIcon,
|
||||
link: '/modpack/cobblemon-fabric',
|
||||
},
|
||||
summary: figmaSummary,
|
||||
versionNumber: 'v1.6.1',
|
||||
versionLink: '/modpack/cobblemon-fabric/version/v1.6.1',
|
||||
updatedAt: twoDaysAgo,
|
||||
} satisfies ManagedContentCardData
|
||||
|
||||
const serverCard = {
|
||||
kind: 'server',
|
||||
manager: {
|
||||
name: 'Modrinth SMP',
|
||||
iconUrl: serverIcon,
|
||||
link: '/server/modrinth-smp',
|
||||
},
|
||||
summary: figmaSummary,
|
||||
syncedAt: fiveMinutesAgo,
|
||||
updateAvailable: true,
|
||||
} satisfies ManagedContentCardData
|
||||
|
||||
const sharedInstanceCard = {
|
||||
kind: 'shared-instance',
|
||||
manager: {
|
||||
name: 'Fetch',
|
||||
iconUrl: userAvatar,
|
||||
link: '/user/fetch',
|
||||
},
|
||||
summary: figmaSummary,
|
||||
syncedAt: fiveMinutesAgo,
|
||||
updateAvailable: true,
|
||||
} satisfies ManagedContentCardData
|
||||
|
||||
const meta = {
|
||||
title: 'Instances/ManagedContentCard',
|
||||
component: ManagedContentCard,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
},
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
template: '<div class="w-[1020px] max-w-full"><story /></div>',
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
showViewContent: true,
|
||||
showSettings: true,
|
||||
showPrimaryAction: true,
|
||||
onViewContent: fn(),
|
||||
onSettings: fn(),
|
||||
onPrimaryAction: fn(),
|
||||
},
|
||||
} satisfies Meta<typeof ManagedContentCard>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Modpack: Story = {
|
||||
args: {
|
||||
data: modpackCard,
|
||||
},
|
||||
}
|
||||
|
||||
export const Server: Story = {
|
||||
args: {
|
||||
data: serverCard,
|
||||
},
|
||||
}
|
||||
|
||||
export const SharedInstance: Story = {
|
||||
args: {
|
||||
data: sharedInstanceCard,
|
||||
},
|
||||
}
|
||||
|
||||
export const NarrowWidth: Story = {
|
||||
args: {
|
||||
data: serverCard,
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { ManagedContentCard },
|
||||
setup: () => ({ args }),
|
||||
template: /*html*/ `
|
||||
<div class="w-[620px] max-w-full">
|
||||
<ManagedContentCard v-bind="args" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const LongManagerName: Story = {
|
||||
args: {
|
||||
data: {
|
||||
...sharedInstanceCard,
|
||||
manager: {
|
||||
...sharedInstanceCard.manager,
|
||||
name: 'A shared-instance manager with a very long display name',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const MissingManagerIcon: Story = {
|
||||
args: {
|
||||
data: {
|
||||
...serverCard,
|
||||
manager: {
|
||||
name: 'Modrinth SMP',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const ImportedModpack: Story = {
|
||||
args: {
|
||||
data: {
|
||||
kind: 'modpack',
|
||||
manager: {
|
||||
name: 'Imported from Prism Launcher',
|
||||
},
|
||||
summary: figmaSummary,
|
||||
},
|
||||
showPrimaryAction: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const LoadingCounts: Story = {
|
||||
args: {
|
||||
data: {
|
||||
...modpackCard,
|
||||
summary: undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const ZeroContent: Story = {
|
||||
args: {
|
||||
data: {
|
||||
...sharedInstanceCard,
|
||||
summary: [],
|
||||
updateAvailable: false,
|
||||
},
|
||||
showPrimaryAction: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const ApplyingUpdate: Story = {
|
||||
args: {
|
||||
data: serverCard,
|
||||
disabled: true,
|
||||
disabledText: 'Updating...',
|
||||
},
|
||||
}
|
||||
|
||||
export const AccountLocked: Story = {
|
||||
args: {
|
||||
data: {
|
||||
...sharedInstanceCard,
|
||||
updateAvailable: false,
|
||||
},
|
||||
showPrimaryAction: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const EveryContentType: Story = {
|
||||
args: {
|
||||
data: {
|
||||
...serverCard,
|
||||
summary: [
|
||||
{ type: 'mod', count: 52 },
|
||||
{ type: 'plugin', count: 4 },
|
||||
{ type: 'datapack', count: 3 },
|
||||
{ type: 'resourcepack', count: 2 },
|
||||
{ type: 'shader', count: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
+34
-70
@@ -2,10 +2,9 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { Button } from '../../components/base/buttons'
|
||||
import ModpackContentModal from '../../layouts/shared/content-tab/components/modals/ModpackContentModal.vue'
|
||||
import ManagedContentModal from '../../layouts/shared/content-tab/components/managed-content-modal/index.vue'
|
||||
import type { ContentItem } from '../../layouts/shared/content-tab/types'
|
||||
|
||||
// Sample modpack content items (representing mods included in a modpack)
|
||||
const sodiumItem: ContentItem = {
|
||||
file_name: 'sodium-fabric-0.8.2+mc1.21.1.jar',
|
||||
file_path: '',
|
||||
@@ -201,7 +200,6 @@ const entityTextureFeaturesItem: ContentItem = {
|
||||
update_version_id: null,
|
||||
}
|
||||
|
||||
// Shader pack item
|
||||
const complementaryShaderItem: ContentItem = {
|
||||
file_name: 'ComplementaryReimagined_r5.3.zip',
|
||||
file_path: '',
|
||||
@@ -256,7 +254,6 @@ const bslShaderItem: ContentItem = {
|
||||
update_version_id: null,
|
||||
}
|
||||
|
||||
// Resource pack items
|
||||
const faithfulItem: ContentItem = {
|
||||
file_name: 'Faithful 32x - 1.21.zip',
|
||||
file_path: '',
|
||||
@@ -339,7 +336,6 @@ const stayTrueItem: ContentItem = {
|
||||
update_version_id: null,
|
||||
}
|
||||
|
||||
// Mixed content (mods + shaders + resource packs)
|
||||
const mixedModpackContent: ContentItem[] = [
|
||||
sodiumItem,
|
||||
lithiumItem,
|
||||
@@ -355,7 +351,6 @@ const mixedModpackContent: ContentItem[] = [
|
||||
stayTrueItem,
|
||||
]
|
||||
|
||||
// Mods only
|
||||
const modsOnlyContent: ContentItem[] = [
|
||||
sodiumItem,
|
||||
lithiumItem,
|
||||
@@ -366,7 +361,6 @@ const modsOnlyContent: ContentItem[] = [
|
||||
entityTextureFeaturesItem,
|
||||
]
|
||||
|
||||
// Large modpack content (40+ items for testing scrolling)
|
||||
const largeModpackContent: ContentItem[] = [
|
||||
...mixedModpackContent,
|
||||
...Array.from({ length: 35 }, (_, i) => ({
|
||||
@@ -393,35 +387,31 @@ const largeModpackContent: ContentItem[] = [
|
||||
]
|
||||
|
||||
const meta = {
|
||||
title: 'Instances/ModpackContentModal',
|
||||
component: ModpackContentModal,
|
||||
title: 'Instances/ManagedContentModal',
|
||||
component: ManagedContentModal,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies Meta<typeof ModpackContentModal>
|
||||
} satisfies Meta<typeof ManagedContentModal>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// ============================================
|
||||
// Basic Examples
|
||||
// ============================================
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { ModpackContentModal, Button },
|
||||
components: { ManagedContentModal, Button },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
|
||||
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show(mixedModpackContent)
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
<Button type="colored" color="brand" @click="openModal">View Modpack Content (Mixed)</Button>
|
||||
<ModpackContentModal
|
||||
<ManagedContentModal
|
||||
ref="modalRef"
|
||||
modpack-name="Cobblemon Official Modpack"
|
||||
modpack-icon-url="https://cdn.modrinth.com/data/5FFgwNNP/icon.png"
|
||||
source-name="Cobblemon Official Modpack"
|
||||
source-icon-url="https://cdn.modrinth.com/data/5FFgwNNP/icon.png"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
@@ -430,33 +420,28 @@ export const Default: Story = {
|
||||
|
||||
export const ModsOnly: Story = {
|
||||
render: () => ({
|
||||
components: { ModpackContentModal, Button },
|
||||
components: { ManagedContentModal, Button },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
|
||||
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show(modsOnlyContent)
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
<Button type="colored" color="brand" @click="openModal">View Modpack Content (Mods Only)</Button>
|
||||
<ModpackContentModal ref="modalRef" />
|
||||
<ManagedContentModal ref="modalRef" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Loading State
|
||||
// ============================================
|
||||
|
||||
export const LoadingState: Story = {
|
||||
render: () => ({
|
||||
components: { ModpackContentModal, Button },
|
||||
components: { ManagedContentModal, Button },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
|
||||
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
|
||||
const openModal = () => {
|
||||
modalRef.value?.showLoading()
|
||||
// Simulate loading delay
|
||||
setTimeout(() => {
|
||||
modalRef.value?.show(mixedModpackContent)
|
||||
}, 2000)
|
||||
@@ -466,71 +451,59 @@ export const LoadingState: Story = {
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
<Button type="colored" color="brand" @click="openModal">View Content (With Loading)</Button>
|
||||
<ModpackContentModal
|
||||
<ManagedContentModal
|
||||
ref="modalRef"
|
||||
modpack-name="Fabulously Optimized"
|
||||
modpack-icon-url="https://cdn.modrinth.com/data/1KVo5zza/icon.png"
|
||||
source-name="Fabulously Optimized"
|
||||
source-icon-url="https://cdn.modrinth.com/data/1KVo5zza/icon.png"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Empty State
|
||||
// ============================================
|
||||
|
||||
export const EmptyContent: Story = {
|
||||
render: () => ({
|
||||
components: { ModpackContentModal, Button },
|
||||
components: { ManagedContentModal, Button },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
|
||||
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show([])
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
<Button type="colored" color="brand" @click="openModal">View Empty Modpack</Button>
|
||||
<ModpackContentModal ref="modalRef" />
|
||||
<ManagedContentModal ref="modalRef" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Large Content List
|
||||
// ============================================
|
||||
|
||||
export const LargeModpack: Story = {
|
||||
render: () => ({
|
||||
components: { ModpackContentModal, Button },
|
||||
components: { ManagedContentModal, Button },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
|
||||
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show(largeModpackContent)
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
<Button type="colored" color="brand" @click="openModal">View Large Modpack (47 items)</Button>
|
||||
<ModpackContentModal
|
||||
<ManagedContentModal
|
||||
ref="modalRef"
|
||||
modpack-name="All the Mods 10"
|
||||
modpack-icon-url="https://cdn.modrinth.com/data/1KVo5zza/icon.png"
|
||||
source-name="All the Mods 10"
|
||||
source-icon-url="https://cdn.modrinth.com/data/1KVo5zza/icon.png"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Search Functionality
|
||||
// ============================================
|
||||
|
||||
export const SearchDemo: Story = {
|
||||
render: () => ({
|
||||
components: { ModpackContentModal, Button },
|
||||
components: { ManagedContentModal, Button },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
|
||||
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show(mixedModpackContent)
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
@@ -540,21 +513,17 @@ export const SearchDemo: Story = {
|
||||
Click the button and try searching for "sodium", "shader", or "faithful" to test the search functionality.
|
||||
</p>
|
||||
<Button type="colored" color="brand" @click="openModal">Test Search</Button>
|
||||
<ModpackContentModal ref="modalRef" />
|
||||
<ManagedContentModal ref="modalRef" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Filter Demo
|
||||
// ============================================
|
||||
|
||||
export const FilterDemo: Story = {
|
||||
render: () => ({
|
||||
components: { ModpackContentModal, Button },
|
||||
components: { ManagedContentModal, Button },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
|
||||
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show(mixedModpackContent)
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
@@ -564,22 +533,17 @@ export const FilterDemo: Story = {
|
||||
Click the button and try the filter chips (Mods, Shaders, Resource Packs) to filter content by type.
|
||||
</p>
|
||||
<Button type="colored" color="brand" @click="openModal">Test Filters</Button>
|
||||
<ModpackContentModal ref="modalRef" />
|
||||
<ManagedContentModal ref="modalRef" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Mixed Owner Types
|
||||
// ============================================
|
||||
|
||||
export const MixedOwnerTypes: Story = {
|
||||
render: () => ({
|
||||
components: { ModpackContentModal, Button },
|
||||
components: { ManagedContentModal, Button },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
|
||||
// Mix of user and organization owners
|
||||
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
|
||||
const mixedContent = [
|
||||
sodiumItem, // User owner
|
||||
fabricApiItem, // Organization owner
|
||||
@@ -595,7 +559,7 @@ export const MixedOwnerTypes: Story = {
|
||||
Shows content with different owner types: users (circular avatar) and organizations (rounded + icon).
|
||||
</p>
|
||||
<Button type="colored" color="brand" @click="openModal">View Mixed Owners</Button>
|
||||
<ModpackContentModal ref="modalRef" />
|
||||
<ManagedContentModal ref="modalRef" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
Reference in New Issue
Block a user