新增同步 helpers(synced-options / synced-packs / game-options),在 instance helper 中补充同步类型与 API。新增实例设置「同步」标签页(SyncSettings)及 SyncSource / SyncedPacks / servers / command-history 四个弹窗,并把 CheckCircleButton、TeleportOverflowMenu 从 ui 包导出。同步相关的 en-US / zh-CN 文案已补齐(各 62 条)。game-settings-modal 留待下一轮。
This commit is contained in:
1 parent
f7380f1590
commit
83ee962c16
14 files changed
+2238
-1
No files matched your search
@@ -0,0 +1,181 @@
|
||||
<script setup lang="ts">
|
||||
import { RefreshCwIcon, SearchIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
CheckCircleButton,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
NewModal,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
|
||||
import { getInstanceIconUrl } from '@/helpers/instance'
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
description: string
|
||||
sources: {
|
||||
id: string
|
||||
name: string
|
||||
icon_path?: string | null
|
||||
eligible: boolean
|
||||
}[]
|
||||
loading?: boolean
|
||||
pending?: boolean
|
||||
error?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: []
|
||||
close: []
|
||||
retry: []
|
||||
}>()
|
||||
|
||||
const selectedInstanceId = defineModel<string>({ required: true })
|
||||
const modal = useTemplateRef<InstanceType<typeof NewModal>>('modal')
|
||||
const search = ref('')
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
ineligible: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.ineligible',
|
||||
defaultMessage: 'You cannot choose this instance as a sync source',
|
||||
},
|
||||
search: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.search-placeholder',
|
||||
defaultMessage: 'Search instance',
|
||||
},
|
||||
empty: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.no-instances-found',
|
||||
defaultMessage: 'No instances found',
|
||||
},
|
||||
sync: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.sync',
|
||||
defaultMessage: 'Sync',
|
||||
},
|
||||
loadError: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.load-error',
|
||||
defaultMessage: 'Could not load sync sources. Please try again.',
|
||||
},
|
||||
retry: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.retry',
|
||||
defaultMessage: 'Try again',
|
||||
},
|
||||
})
|
||||
|
||||
const filteredSources = computed(() => {
|
||||
const query = search.value.trim().toLocaleLowerCase()
|
||||
return props.sources.filter((source) => source.name.toLocaleLowerCase().includes(query))
|
||||
})
|
||||
const canConfirm = computed(
|
||||
() =>
|
||||
!props.loading &&
|
||||
!props.pending &&
|
||||
!props.error &&
|
||||
props.sources.some((source) => source.id === selectedInstanceId.value && source.eligible),
|
||||
)
|
||||
|
||||
function show() {
|
||||
search.value = ''
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
return modal.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NewModal
|
||||
ref="modal"
|
||||
:header="title"
|
||||
no-padding
|
||||
actions-divider
|
||||
max-width="560px"
|
||||
width="560px"
|
||||
:disable-close="pending"
|
||||
:on-after-hide="() => emit('close')"
|
||||
>
|
||||
<p
|
||||
class="m-0 border-0 border-b border-solid border-surface-5 p-6 text-[var(--color-text-default)]"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
|
||||
<div class="flex h-[400px] flex-col gap-3 overflow-y-auto bg-surface-2 px-6 py-4">
|
||||
<StyledInput
|
||||
v-model="search"
|
||||
:icon="SearchIcon"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
:placeholder="formatMessage(messages.search)"
|
||||
:aria-label="formatMessage(messages.search)"
|
||||
class="shrink-0"
|
||||
/>
|
||||
|
||||
<div v-if="loading" class="flex flex-1 items-center justify-center" aria-busy="true">
|
||||
<SpinnerIcon class="size-5 animate-spin text-[var(--color-text-tertiary)]" />
|
||||
</div>
|
||||
<div v-else-if="error" class="flex flex-1 flex-col items-center justify-center gap-3">
|
||||
<p role="alert" class="m-0 text-center text-[var(--color-text-default)]">
|
||||
{{ formatMessage(messages.loadError) }}
|
||||
</p>
|
||||
<Button @click="emit('retry')">{{ formatMessage(messages.retry) }}</Button>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="filteredSources.length === 0"
|
||||
class="flex flex-1 items-center justify-center text-[var(--color-text-tertiary)]"
|
||||
>
|
||||
{{ formatMessage(messages.empty) }}
|
||||
</div>
|
||||
<div v-else role="radiogroup" :aria-label="title" class="flex flex-col gap-1">
|
||||
<div
|
||||
v-for="source in filteredSources"
|
||||
:key="source.id"
|
||||
v-tooltip="!source.eligible ? formatMessage(messages.ineligible) : undefined"
|
||||
>
|
||||
<CheckCircleButton
|
||||
:checked="selectedInstanceId === source.id"
|
||||
:disabled="pending || !source.eligible"
|
||||
class="min-h-10"
|
||||
@click="selectedInstanceId = source.id"
|
||||
>
|
||||
<span class="size-5 shrink-0 overflow-hidden rounded-[6px]">
|
||||
<Avatar
|
||||
:src="getInstanceIconUrl(source.icon_path)"
|
||||
:alt="source.name"
|
||||
:tint-by="source.id"
|
||||
size="1.25rem"
|
||||
no-shadow
|
||||
/>
|
||||
</span>
|
||||
<span class="min-w-0 flex-1 truncate">{{ source.name }}</span>
|
||||
</CheckCircleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="flex justify-end gap-2 p-2">
|
||||
<Button type="outlined" :disabled="pending" @click="hide">
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:disabled="!canConfirm"
|
||||
:loading="pending"
|
||||
@click="canConfirm && emit('confirm')"
|
||||
>
|
||||
<RefreshCwIcon :class="{ 'animate-spin': pending }" aria-hidden="true" />
|
||||
{{ formatMessage(messages.sync) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
Reference in new issue
Block a user