mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 19:46:33 +00:00
feat: add compact mode for library
This commit is contained in:
@@ -30,6 +30,7 @@ import type {
|
|||||||
} from '@/components/ui/library/use-library'
|
} from '@/components/ui/library/use-library'
|
||||||
import { useLibrary } from '@/components/ui/library/use-library'
|
import { useLibrary } from '@/components/ui/library/use-library'
|
||||||
import { FAVORITES_GROUP_ID, MAX_INSTANCE_GROUP_NAME_LENGTH } from '@/helpers/instance-groups'
|
import { FAVORITES_GROUP_ID, MAX_INSTANCE_GROUP_NAME_LENGTH } from '@/helpers/instance-groups'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
|
|
||||||
const INSTANCE_GRID_OBSERVER_ACTIVATION_DELAY = 500
|
const INSTANCE_GRID_OBSERVER_ACTIVATION_DELAY = 500
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@ const props = withDefaults(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
const themeStore = useTheming()
|
||||||
|
const compactMode = computed(() => themeStore.getFeatureFlag('compact_instance_cards'))
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
const {
|
const {
|
||||||
isSectionCollapsed,
|
isSectionCollapsed,
|
||||||
@@ -475,7 +478,12 @@ onMounted(startInstanceGridResizeObserver)
|
|||||||
<div ref="instanceGridContent">
|
<div ref="instanceGridContent">
|
||||||
<TransitionGroup
|
<TransitionGroup
|
||||||
tag="section"
|
tag="section"
|
||||||
class="grid min-h-[45px] w-full grid-cols-[repeat(auto-fill,minmax(min(10rem,100%),1fr))] max-xl:grid-cols-[repeat(auto-fill,minmax(min(8rem,100%),1fr))] gap-3 overflow-y-auto scroll-smooth"
|
class="grid min-h-[45px] w-full gap-3 overflow-y-auto scroll-smooth"
|
||||||
|
:class="
|
||||||
|
compactMode
|
||||||
|
? 'grid-cols-[repeat(auto-fill,minmax(min(15rem,100%),1fr))]'
|
||||||
|
: 'grid-cols-[repeat(auto-fill,minmax(min(10rem,100%),1fr))] max-xl:grid-cols-[repeat(auto-fill,minmax(min(8rem,100%),1fr))]'
|
||||||
|
"
|
||||||
move-class="transition-transform duration-200 ease-out motion-reduce:transition-none"
|
move-class="transition-transform duration-200 ease-out motion-reduce:transition-none"
|
||||||
enter-active-class="transition-[opacity,transform] duration-[150ms] ease-out motion-reduce:transition-none"
|
enter-active-class="transition-[opacity,transform] duration-[150ms] ease-out motion-reduce:transition-none"
|
||||||
enter-from-class="opacity-0"
|
enter-from-class="opacity-0"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { computed, ref } from 'vue'
|
|||||||
|
|
||||||
import { getInstanceIconUrl } from '@/helpers/instance'
|
import { getInstanceIconUrl } from '@/helpers/instance'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -16,6 +17,8 @@ const props = withDefaults(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const iconSrc = computed(() => getInstanceIconUrl(props.instance.icon_path))
|
const iconSrc = computed(() => getInstanceIconUrl(props.instance.icon_path))
|
||||||
|
const themeStore = useTheming()
|
||||||
|
const compactMode = computed(() => themeStore.getFeatureFlag('compact_instance_cards'))
|
||||||
|
|
||||||
const nameRef = ref<HTMLElement | null>(null)
|
const nameRef = ref<HTMLElement | null>(null)
|
||||||
const versionRef = ref<HTMLElement | null>(null)
|
const versionRef = ref<HTMLElement | null>(null)
|
||||||
@@ -23,30 +26,40 @@ const versionRef = ref<HTMLElement | null>(null)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="relative flex w-full min-w-0 select-none flex-col items-start justify-end gap-3 overflow-clip rounded-[20px] border border-solid bg-surface-3 p-3 text-left transition-all"
|
class="relative flex w-full min-w-0 select-none overflow-clip border border-solid bg-surface-3 text-left transition-all"
|
||||||
:class="{
|
:class="{
|
||||||
|
'flex-row items-center justify-start gap-2.5 rounded-xl p-2.5': compactMode,
|
||||||
|
'flex-col items-start justify-end gap-3 rounded-[20px] p-3': !compactMode,
|
||||||
'[border-color:color-mix(in_srgb,var(--color-text-primary)_40%,transparent)] brightness-110':
|
'[border-color:color-mix(in_srgb,var(--color-text-primary)_40%,transparent)] brightness-110':
|
||||||
selected,
|
selected,
|
||||||
'border-surface-4': !selected,
|
'border-surface-4': !selected,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative flex aspect-square min-w-full shrink-0 items-center overflow-clip rounded-2xl"
|
class="relative flex shrink-0 items-center overflow-clip"
|
||||||
|
:class="compactMode ? 'size-10 rounded-lg' : 'aspect-square min-w-full rounded-2xl'"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
class="pointer-events-none !rounded-2xl outline-none"
|
class="pointer-events-none outline-none"
|
||||||
|
:class="compactMode ? '!rounded-lg' : '!rounded-2xl'"
|
||||||
size="100%"
|
size="100%"
|
||||||
:src="iconSrc"
|
:src="iconSrc"
|
||||||
:tint-by="instance.id"
|
:tint-by="instance.id"
|
||||||
alt=""
|
alt=""
|
||||||
no-shadow
|
no-shadow
|
||||||
/>
|
/>
|
||||||
<slot name="loading" />
|
<slot name="loading" :compact="compactMode" />
|
||||||
<div class="absolute bottom-1.5 right-1.5 z-[1] flex size-12 items-center justify-center">
|
<div
|
||||||
<slot name="leading" />
|
class="absolute z-[1] flex items-center justify-center"
|
||||||
|
:class="compactMode ? 'inset-0' : 'bottom-1.5 right-1.5 size-12'"
|
||||||
|
>
|
||||||
|
<slot name="leading" :compact="compactMode" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex min-w-0 w-full flex-col items-start justify-center gap-1 px-0.5">
|
<div
|
||||||
|
class="flex min-w-0 w-full flex-col items-start justify-center gap-1 px-0.5"
|
||||||
|
:class="{ 'pr-10': compactMode }"
|
||||||
|
>
|
||||||
<p
|
<p
|
||||||
ref="nameRef"
|
ref="nameRef"
|
||||||
v-tooltip="truncatedTooltip(nameRef, instance.name)"
|
v-tooltip="truncatedTooltip(nameRef, instance.name)"
|
||||||
@@ -62,6 +75,6 @@ const versionRef = ref<HTMLElement | null>(null)
|
|||||||
{{ instance.loader }} {{ instance.game_version }}
|
{{ instance.loader }} {{ instance.game_version }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<slot name="overlay" />
|
<slot name="overlay" :compact="compactMode" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ onMounted(() => {
|
|||||||
@mouseenter="checkProcess"
|
@mouseenter="checkProcess"
|
||||||
@pointerdown="handlePointerDown"
|
@pointerdown="handlePointerDown"
|
||||||
>
|
>
|
||||||
<template #loading>
|
<template #loading="{ compact }">
|
||||||
<div
|
<div
|
||||||
v-if="loadingIndicatorVisible"
|
v-if="loadingIndicatorVisible"
|
||||||
class="pointer-events-none absolute inset-0 z-[1] flex items-center justify-center"
|
class="pointer-events-none absolute inset-0 z-[1] flex items-center justify-center"
|
||||||
@@ -293,13 +293,17 @@ onMounted(() => {
|
|||||||
<div class="absolute inset-0 bg-surface-1 opacity-30" />
|
<div class="absolute inset-0 bg-surface-1 opacity-30" />
|
||||||
<SpinnerIcon
|
<SpinnerIcon
|
||||||
v-tooltip="formatMessage(modLoading ? messages.loading : messages.installing)"
|
v-tooltip="formatMessage(modLoading ? messages.loading : messages.installing)"
|
||||||
class="relative size-[30%] animate-spin text-contrast"
|
class="relative animate-spin text-contrast"
|
||||||
|
:class="compact ? 'size-5' : 'size-[30%]'"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #leading>
|
<template #leading="{ compact }">
|
||||||
<div class="relative flex size-12 shrink-0 items-center justify-center">
|
<div
|
||||||
|
class="relative flex shrink-0 items-center justify-center"
|
||||||
|
:class="compact ? 'size-10' : 'size-12'"
|
||||||
|
>
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
<IconButton
|
<IconButton
|
||||||
v-if="playing"
|
v-if="playing"
|
||||||
@@ -307,7 +311,7 @@ onMounted(() => {
|
|||||||
:label="formatMessage(messages.stop)"
|
:label="formatMessage(messages.stop)"
|
||||||
type="colored"
|
type="colored"
|
||||||
color="red"
|
color="red"
|
||||||
size="lg"
|
:size="compact ? 'md' : 'lg'"
|
||||||
@click="(e) => stop(e, 'InstanceCard')"
|
@click="(e) => stop(e, 'InstanceCard')"
|
||||||
@mouseenter="checkProcess"
|
@mouseenter="checkProcess"
|
||||||
>
|
>
|
||||||
@@ -325,7 +329,7 @@ onMounted(() => {
|
|||||||
:label="formatMessage(messages.repair)"
|
:label="formatMessage(messages.repair)"
|
||||||
type="colored"
|
type="colored"
|
||||||
color="brand"
|
color="brand"
|
||||||
size="lg"
|
:size="compact ? 'md' : 'lg'"
|
||||||
class="origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
|
class="origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
|
||||||
@click="(e) => repair(e)"
|
@click="(e) => repair(e)"
|
||||||
>
|
>
|
||||||
@@ -342,7 +346,7 @@ onMounted(() => {
|
|||||||
:label="formatMessage(messages.play)"
|
:label="formatMessage(messages.play)"
|
||||||
type="colored"
|
type="colored"
|
||||||
color="brand"
|
color="brand"
|
||||||
size="lg"
|
:size="compact ? 'md' : 'lg'"
|
||||||
class="origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
|
class="origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
|
||||||
@click="(e) => play(e, 'InstanceCard')"
|
@click="(e) => play(e, 'InstanceCard')"
|
||||||
@mouseenter="checkProcess"
|
@mouseenter="checkProcess"
|
||||||
@@ -352,10 +356,11 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #overlay>
|
<template #overlay="{ compact }">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="selection-button group/selection absolute right-2 top-1.5 z-[2] flex size-[50px] cursor-pointer items-start pt-4 justify-center border-0 bg-transparent p-0"
|
class="selection-button group/selection absolute z-[2] flex size-[50px] cursor-pointer items-start pt-4 justify-center border-0 bg-transparent p-0"
|
||||||
|
:class="compact ? '-right-1 -top-1' : 'right-2 top-1.5'"
|
||||||
:aria-label="formatMessage(selected ? messages.deselect : messages.select)"
|
:aria-label="formatMessage(selected ? messages.deselect : messages.select)"
|
||||||
:aria-pressed="selected"
|
:aria-pressed="selected"
|
||||||
@click.stop="toggleSelection"
|
@click.stop="toggleSelection"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const themeStore = useTheming()
|
|||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
|
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
|
||||||
|
const compactInstanceCardsFlag: FeatureFlag = 'compact_instance_cards'
|
||||||
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
|
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
|
||||||
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
|
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
|
||||||
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
|
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
|
||||||
@@ -60,6 +61,14 @@ const messages = defineMessages({
|
|||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Show recently played worlds or instances in the "Jump in" section on the Home page.',
|
'Show recently played worlds or instances in the "Jump in" section on the Home page.',
|
||||||
},
|
},
|
||||||
|
compactModeTitle: {
|
||||||
|
id: 'app.appearance-settings.compact-mode.title',
|
||||||
|
defaultMessage: 'Compact mode',
|
||||||
|
},
|
||||||
|
compactModeDescription: {
|
||||||
|
id: 'app.appearance-settings.compact-mode.description',
|
||||||
|
defaultMessage: 'Display library instances in a compact row layout.',
|
||||||
|
},
|
||||||
showPlayTimeTitle: {
|
showPlayTimeTitle: {
|
||||||
id: 'app.appearance-settings.show-play-time.title',
|
id: 'app.appearance-settings.show-play-time.title',
|
||||||
defaultMessage: 'Show play time',
|
defaultMessage: 'Show play time',
|
||||||
@@ -172,6 +181,26 @@ watch(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||||
|
{{ formatMessage(messages.compactModeTitle) }}
|
||||||
|
</h3>
|
||||||
|
<p class="m-0 mt-1">{{ formatMessage(messages.compactModeDescription) }}</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
id="compact-mode"
|
||||||
|
:model-value="themeStore.getFeatureFlag(compactInstanceCardsFlag)"
|
||||||
|
@update:model-value="
|
||||||
|
() => {
|
||||||
|
const newValue = !themeStore.getFeatureFlag(compactInstanceCardsFlag)
|
||||||
|
themeStore.featureFlags[compactInstanceCardsFlag] = newValue
|
||||||
|
settings.feature_flags[compactInstanceCardsFlag] = newValue
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const DEFAULT_FEATURE_FLAGS = {
|
|||||||
pride_fundraiser: true,
|
pride_fundraiser: true,
|
||||||
i18n_debug: false,
|
i18n_debug: false,
|
||||||
show_instance_play_time: true,
|
show_instance_play_time: true,
|
||||||
|
compact_instance_cards: false,
|
||||||
advanced_filters_collapsed: true,
|
advanced_filters_collapsed: true,
|
||||||
always_show_copy_details: false,
|
always_show_copy_details: false,
|
||||||
hide_installed_modpacks: false,
|
hide_installed_modpacks: false,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ pub enum FeatureFlag {
|
|||||||
ServerProjectQa,
|
ServerProjectQa,
|
||||||
I18nDebug,
|
I18nDebug,
|
||||||
ShowInstancePlayTime,
|
ShowInstancePlayTime,
|
||||||
|
CompactInstanceCards,
|
||||||
SkipNonEssentialWarnings,
|
SkipNonEssentialWarnings,
|
||||||
AdvancedFiltersCollapsed,
|
AdvancedFiltersCollapsed,
|
||||||
AlwaysShowCopyDetails,
|
AlwaysShowCopyDetails,
|
||||||
|
|||||||
Reference in New Issue
Block a user