You've already forked AstralRinth
279 lines
8.5 KiB
Vue
279 lines
8.5 KiB
Vue
<script setup lang="ts">
|
|
import { PowerIcon, PowerOffIcon, XIcon } from '@modrinth/assets'
|
|
import { computed } from 'vue'
|
|
|
|
import Avatar from '#ui/components/base/Avatar.vue'
|
|
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
|
import FloatingActionBar from '#ui/components/base/FloatingActionBar.vue'
|
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
|
import { commonMessages, formatContentTypeSentence } from '#ui/utils/common-messages'
|
|
|
|
import type { BulkOperationType } from '../composables/bulk-operations'
|
|
import type { ContentItem } from '../types'
|
|
|
|
const { formatMessage } = useVIntl()
|
|
|
|
const messages = defineMessages({
|
|
selectedCount: {
|
|
id: 'content.selection-bar.selected-count',
|
|
defaultMessage: '{count, number} {contentType} selected',
|
|
},
|
|
selectedCountSimple: {
|
|
id: 'content.selection-bar.selected-count-simple',
|
|
defaultMessage: '{count, number} selected',
|
|
},
|
|
bulkEnabling: {
|
|
id: 'content.selection-bar.bulk.enabling',
|
|
defaultMessage: 'Enabling {progress}/{total} {contentType}...',
|
|
},
|
|
bulkEnablingWaiting: {
|
|
id: 'content.selection-bar.bulk.enabling-waiting',
|
|
defaultMessage: 'Enabling {contentType}...',
|
|
},
|
|
bulkDisabling: {
|
|
id: 'content.selection-bar.bulk.disabling',
|
|
defaultMessage: 'Disabling {progress}/{total} {contentType}...',
|
|
},
|
|
bulkDisablingWaiting: {
|
|
id: 'content.selection-bar.bulk.disabling-waiting',
|
|
defaultMessage: 'Disabling {contentType}...',
|
|
},
|
|
bulkUpdating: {
|
|
id: 'content.selection-bar.bulk.updating',
|
|
defaultMessage: 'Updating {progress}/{total} {contentType}...',
|
|
},
|
|
bulkUpdatingWaiting: {
|
|
id: 'content.selection-bar.bulk.updating-waiting',
|
|
defaultMessage: 'Updating {contentType}...',
|
|
},
|
|
bulkDeleting: {
|
|
id: 'content.selection-bar.bulk.deleting',
|
|
defaultMessage: 'Deleting {progress}/{total} {contentType}...',
|
|
},
|
|
bulkDeletingWaiting: {
|
|
id: 'content.selection-bar.bulk.deleting-waiting',
|
|
defaultMessage: 'Deleting {contentType}...',
|
|
},
|
|
allAlreadyEnabled: {
|
|
id: 'content.selection-bar.all-already-enabled',
|
|
defaultMessage: 'All selected content is already enabled',
|
|
},
|
|
allAlreadyDisabled: {
|
|
id: 'content.selection-bar.all-already-disabled',
|
|
defaultMessage: 'All selected content is already disabled',
|
|
},
|
|
})
|
|
|
|
interface Props {
|
|
selectedItems: ContentItem[]
|
|
contentTypeLabel?: string
|
|
isBusy?: boolean
|
|
busyTooltip?: string | null
|
|
isBulkOperating?: boolean
|
|
bulkOperation?: BulkOperationType | null
|
|
bulkProgress?: number
|
|
bulkTotal?: number
|
|
bulkWaiting?: boolean
|
|
ariaLabel?: string
|
|
getItemId?: (item: ContentItem) => string
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
contentTypeLabel: undefined,
|
|
isBusy: false,
|
|
busyTooltip: undefined,
|
|
isBulkOperating: false,
|
|
bulkOperation: null,
|
|
bulkProgress: 0,
|
|
bulkTotal: 0,
|
|
bulkWaiting: false,
|
|
ariaLabel: undefined,
|
|
getItemId: undefined,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
clear: []
|
|
enable: []
|
|
disable: []
|
|
}>()
|
|
|
|
const shown = computed(() => props.selectedItems.length > 0 || props.isBulkOperating)
|
|
const iconStackOffset = 24
|
|
const visibleItems = computed(() => props.selectedItems.slice(0, 3))
|
|
const overflowCount = computed(() => Math.max(0, props.selectedItems.length - 3))
|
|
const iconStackWidth = computed(() => {
|
|
if (props.selectedItems.length === 0) return 0
|
|
return 32 + (visibleItems.value.length - 1 + (overflowCount.value > 0 ? 1 : 0)) * iconStackOffset
|
|
})
|
|
|
|
function resolveItemId(item: ContentItem) {
|
|
return props.getItemId?.(item) ?? item.file_path ?? item.file_name ?? item.id
|
|
}
|
|
|
|
const allDisabled = computed(() => props.selectedItems.every((m) => !m.enabled))
|
|
const allEnabled = computed(() => props.selectedItems.every((m) => m.enabled))
|
|
|
|
const selectedCountText = computed(() => {
|
|
const count = props.selectedItems.length || props.bulkTotal
|
|
if (props.contentTypeLabel) {
|
|
return formatMessage(messages.selectedCount, {
|
|
count,
|
|
contentType: formatContentTypeSentence(formatMessage, props.contentTypeLabel, count),
|
|
})
|
|
}
|
|
return formatMessage(messages.selectedCountSimple, { count })
|
|
})
|
|
|
|
const bulkProgressMessage = computed(() => {
|
|
if (!props.bulkOperation) return ''
|
|
const messageMap = {
|
|
enable: props.bulkWaiting ? messages.bulkEnablingWaiting : messages.bulkEnabling,
|
|
disable: props.bulkWaiting ? messages.bulkDisablingWaiting : messages.bulkDisabling,
|
|
update: props.bulkWaiting ? messages.bulkUpdatingWaiting : messages.bulkUpdating,
|
|
delete: props.bulkWaiting ? messages.bulkDeletingWaiting : messages.bulkDeleting,
|
|
}
|
|
return formatMessage(messageMap[props.bulkOperation], {
|
|
progress: props.bulkProgress,
|
|
total: props.bulkTotal,
|
|
contentType: formatContentTypeSentence(formatMessage, props.contentTypeLabel, props.bulkTotal),
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<FloatingActionBar :shown="shown" :aria-label="ariaLabel" hide-when-modal-open>
|
|
<div class="flex items-center gap-0.5">
|
|
<div
|
|
v-if="selectedItems.length > 0"
|
|
class="relative h-8 shrink-0"
|
|
:style="{ width: `${iconStackWidth}px` }"
|
|
aria-hidden="true"
|
|
>
|
|
<div
|
|
v-for="(item, index) in visibleItems"
|
|
:key="resolveItemId(item)"
|
|
v-tooltip="item.project?.title ?? item.file_name"
|
|
class="absolute top-0 flex h-8 w-8 items-center justify-center overflow-hidden rounded-lg border-[1.5px] border-solid border-surface-3 bg-surface-4"
|
|
:style="{ left: `${index * iconStackOffset}px`, zIndex: visibleItems.length - index }"
|
|
>
|
|
<Avatar
|
|
:src="item.project?.icon_url"
|
|
:alt="item.project?.title ?? item.file_name"
|
|
:tint-by="resolveItemId(item)"
|
|
size="100%"
|
|
no-shadow
|
|
class="selected-content-avatar"
|
|
/>
|
|
</div>
|
|
<div
|
|
v-if="overflowCount > 0"
|
|
class="absolute top-0 flex h-8 w-8 items-center justify-center rounded-lg border-[1.5px] border-solid border-surface-3 bg-surface-4 text-xs font-bold text-contrast"
|
|
:style="{ left: `${visibleItems.length * iconStackOffset}px`, zIndex: 0 }"
|
|
>
|
|
+{{ overflowCount }}
|
|
</div>
|
|
</div>
|
|
|
|
<span class="px-3 py-2 text-base font-semibold text-contrast tabular-nums">
|
|
{{ selectedCountText }}
|
|
</span>
|
|
<div class="mx-0.5 h-6 w-px bg-surface-5" />
|
|
<ButtonStyled type="transparent">
|
|
<button
|
|
v-tooltip="formatMessage(commonMessages.clearButton)"
|
|
class="!text-primary"
|
|
:disabled="isBulkOperating"
|
|
:class="{ 'opacity-60 pointer-events-none': isBulkOperating }"
|
|
@click="emit('clear')"
|
|
>
|
|
<XIcon class="hidden cq-show-icon" />
|
|
<span class="bar-label">{{ formatMessage(commonMessages.clearButton) }}</span>
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
|
|
<div v-if="!isBulkOperating" class="ml-auto flex items-center gap-0.5">
|
|
<slot name="actions" />
|
|
|
|
<ButtonStyled type="transparent">
|
|
<button
|
|
v-tooltip="
|
|
isBusy && busyTooltip
|
|
? busyTooltip
|
|
: allEnabled
|
|
? formatMessage(messages.allAlreadyEnabled)
|
|
: formatMessage(commonMessages.enableButton)
|
|
"
|
|
:disabled="isBusy || allEnabled"
|
|
@click="emit('enable')"
|
|
>
|
|
<PowerIcon />
|
|
<span class="bar-label">{{ formatMessage(commonMessages.enableButton) }}</span>
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled type="transparent">
|
|
<button
|
|
v-tooltip="
|
|
isBusy && busyTooltip
|
|
? busyTooltip
|
|
: allDisabled
|
|
? formatMessage(messages.allAlreadyDisabled)
|
|
: formatMessage(commonMessages.disableButton)
|
|
"
|
|
:disabled="isBusy || allDisabled"
|
|
@click="emit('disable')"
|
|
>
|
|
<PowerOffIcon />
|
|
<span class="bar-label">{{ formatMessage(commonMessages.disableButton) }}</span>
|
|
</button>
|
|
</ButtonStyled>
|
|
|
|
<slot name="actions-end" />
|
|
</div>
|
|
|
|
<div v-else class="ml-auto flex items-center" aria-live="polite">
|
|
<span class="px-4 py-2.5 text-base font-semibold text-secondary tabular-nums">
|
|
{{ bulkProgressMessage }}
|
|
</span>
|
|
</div>
|
|
|
|
<div v-if="isBulkOperating" class="absolute bottom-0 left-0 right-0 h-1">
|
|
<div
|
|
class="h-full rounded-l-full bg-brand transition-[width] duration-200 ease-in-out"
|
|
:class="{ 'animate-indeterminate': bulkWaiting }"
|
|
:style="
|
|
!bulkWaiting
|
|
? { width: `${bulkTotal > 0 ? (bulkProgress / bulkTotal) * 100 : 0}%` }
|
|
: undefined
|
|
"
|
|
role="progressbar"
|
|
:aria-valuenow="bulkWaiting ? undefined : bulkProgress"
|
|
:aria-valuemin="0"
|
|
:aria-valuemax="bulkTotal"
|
|
style="box-shadow: 0px -2px 4px 0px rgba(27, 217, 106, 0.1)"
|
|
/>
|
|
</div>
|
|
</FloatingActionBar>
|
|
</template>
|
|
|
|
<style scoped>
|
|
@keyframes indeterminate {
|
|
0% {
|
|
width: 20%;
|
|
margin-left: -20%;
|
|
}
|
|
100% {
|
|
width: 60%;
|
|
margin-left: 100%;
|
|
}
|
|
}
|
|
|
|
.animate-indeterminate {
|
|
animation: indeterminate 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
:deep(.selected-content-avatar) {
|
|
background-color: var(--color-button-bg);
|
|
}
|
|
</style>
|