feat: 跨实例同步移植第8轮(前端 helpers + 同步设置 UI)
Sync source to CNB / Sync Git ref (push) Has been cancelled

新增同步 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:
AxTps committed 2026-10-04 08:51:45 +08:00
1 parent f7380f1590
commit 83ee962c16
14 files changed
+2238 -1

No files matched your search

@@ -0,0 +1,244 @@
import { queryOptions } from '@tanstack/vue-query'
import { invoke } from '@tauri-apps/api/core'
export type GameOptionCanonicalValue =
| { type: 'bool'; value: boolean }
| { type: 'integer'; value: number }
| { type: 'decimal'; value: string }
| { type: 'enum'; value: string }
| { type: 'text'; value: string }
| { type: 'string_list'; value: string[] }
| { type: 'key_binding'; value: string }
| { type: 'external_raw'; value: string }
export type GameOptionValueState = 'canonical' | 'uniform_local' | 'mixed' | 'unset' | 'invalid'
export type GameOptionCompatibilityStatus =
| 'ready'
| 'deferred'
| 'waiting_for_file'
| 'waiting_for_base'
| 'not_available'
| 'unsupported_value'
| 'unmappable'
| 'catalog_uncovered'
| 'controlled'
| 'degraded'
export type GameOptionMappingKind = 'direct' | 'legacy' | 'migrated' | 'lossless' | 'lossy'
export type GameOptionCompatibilityReason =
| 'launcher_controlled'
| 'catalog_uncovered'
| 'inspection_failed'
| 'waiting_for_options_file'
| 'unsupported_value'
| 'migrates_on_write'
| 'waiting_for_compatible_base'
| 'missing_setting'
export type GameOptionCompatibilityBucket = {
instance_count: number
write_keys: string[]
eventual_keys: string[]
game_versions: string[]
status: GameOptionCompatibilityStatus
mapping?: GameOptionMappingKind | null
reason?: GameOptionCompatibilityReason | null
}
export type GameOptionCompatibilitySummary = {
total_participating: number
will_receive: number
write_now: number
left_local: number
buckets: GameOptionCompatibilityBucket[]
}
export type GameOptionEditorChoice = {
value: string
}
export type GameOptionEditor = {
type:
| 'bool'
| 'integer'
| 'decimal'
| 'enum'
| 'text'
| 'string_list'
| 'key_binding'
| 'external_raw'
min?: number | null
max?: number | null
step?: number | null
unit?: string | null
choices?: GameOptionEditorChoice[]
}
export type GameSettingCategory = {
id: string
is_custom?: boolean
}
export type GameOptionValidationError =
'missing_value' | 'no_compatible_instances' | 'invalid_value' | 'changed_since_opened'
export type EditableGameSetting = {
option_id: string
category_id: string
kind: 'vanilla' | 'external'
raw_key?: string | null
sync_enabled: boolean
canonical_value?: GameOptionCanonicalValue | null
value_state: GameOptionValueState
option_revision: number
editor: GameOptionEditor
compatibility: GameOptionCompatibilitySummary
validation_error?: GameOptionValidationError | null
controlled?: boolean
}
export type GameSettingsEditorState = {
summary_revision: string
canonical_revision: number
catalog_revision: number
total_participating: number
categories: GameSettingCategory[]
settings: EditableGameSetting[]
}
export type GameSettingChange = {
option_id: string
base_option_revision: number
sync_enabled?: boolean
canonical_value?: GameOptionCanonicalValue | null
}
export type UpdateGameSettingsRequest = {
expected_summary_revision: string
expected_canonical_revision: number
expected_catalog_revision: number
changes: GameSettingChange[]
}
export type GameOptionsSourceCandidate = {
source_id: string
instance_id: string
name: string
icon_path?: string | null
game_version?: string | null
eligible: boolean
disabled_reason?: GameOptionsSourceDisabledReason | null
recognized_setting_count: number
custom_setting_count: number
}
export type GameOptionsSourceDisabledReason =
| 'installing_or_updating'
| 'unsupported_version'
| 'missing_options_file'
| 'no_syncable_settings'
| 'unreadable_options_file'
export type SaveGameSettingsResult = {
state?: GameSettingsEditorState
applied: number
migrated: number
deferred: number
unsupported: number
failed: number
conflicts?: string[]
}
export type GameSettingLocaleLabel = {
label: string
choices: Record<string, string>
source?: GameSettingSource | null
}
export type GameSettingSource = {
instance_id: string
file_name: string
file_path: string
project: {
id: string
title: string
icon_url: string | null
} | null
}
export async function get_game_setting_locale_labels(
instanceId: string | undefined,
locale: string,
optionIds: string[],
refreshSources = false,
): Promise<{ settings: Record<string, GameSettingLocaleLabel> }> {
return await invoke('plugin:instance|instance_get_game_setting_locale_labels', {
instanceId,
locale,
optionIds,
refreshSources,
})
}
export async function list_game_options_sync_sources(): Promise<GameOptionsSourceCandidate[]> {
return await invoke('plugin:instance|instance_list_game_options_sync_sources')
}
export async function get_synced_game_options_config(): Promise<GameSettingsEditorState> {
return await invoke('plugin:instance|instance_get_synced_game_options_config')
}
export const gameSettingsKeys = {
synced: ['game-settings', 'synced'] as const,
local: (instanceId: string) => ['game-settings', 'local', instanceId] as const,
}
export function gameSettingsQueryOptions(instanceId?: string) {
return queryOptions({
queryKey: instanceId ? gameSettingsKeys.local(instanceId) : gameSettingsKeys.synced,
queryFn: () =>
instanceId ? get_local_game_options_config(instanceId) : get_synced_game_options_config(),
staleTime: 30_000,
retry: false,
})
}
export async function preview_synced_game_option_changes(
request: UpdateGameSettingsRequest,
): Promise<GameSettingsEditorState> {
return await invoke('plugin:instance|instance_preview_synced_game_option_changes', { request })
}
export async function save_synced_game_option_changes(
request: UpdateGameSettingsRequest,
): Promise<SaveGameSettingsResult> {
return await invoke('plugin:instance|instance_save_synced_game_option_changes', { request })
}
export async function get_local_game_options_config(
instanceId: string,
): Promise<GameSettingsEditorState> {
return await invoke('plugin:instance|instance_get_local_game_options_config', { instanceId })
}
export async function preview_local_game_option_changes(
instanceId: string,
request: UpdateGameSettingsRequest,
): Promise<GameSettingsEditorState> {
return await invoke('plugin:instance|instance_preview_local_game_option_changes', {
instanceId,
request,
})
}
export async function save_local_game_option_changes(
instanceId: string,
request: UpdateGameSettingsRequest,
): Promise<SaveGameSettingsResult> {
return await invoke('plugin:instance|instance_save_local_game_option_changes', {
instanceId,
request,
})
}
+98
View File
@@ -108,6 +108,104 @@ export async function get_many(instanceIds: string[]): Promise<GameInstance[]> {
return await invoke('plugin:instance|instance_get_many', { instanceIds })
}
export function adaptContentItems(items: ContentItem[]): ContentItem[] {
return items.map((item) => {
const embeddedMetadata = item.embedded_metadata
if (!embeddedMetadata?.icon_path) return item
return {
...item,
embedded_metadata: {
...embeddedMetadata,
icon_url: convertFileSrc(embeddedMetadata.icon_path),
},
}
})
}
export type SyncedOption =
| 'game_options'
| 'command_history'
| 'multiplayer_servers'
| 'creative_hotbars'
| 'screenshots'
| 'resource_packs'
| 'data_packs'
export type GlobalSyncedOptions = Record<SyncedOption, boolean>
export type SyncedOptionCapability = {
option: SyncedOption
supported: boolean
disabled_reason: string | null
}
export type SyncedOptionJoinPreview = { action: string }
export type SyncedOptionsOverview = {
global_options: GlobalSyncedOptions
capabilities: SyncedOptionCapability[]
}
export async function get_global_synced_options(): Promise<GlobalSyncedOptions> {
return await invoke('plugin:instance|instance_get_synced_options')
}
export async function get_initialized_synced_options(): Promise<GlobalSyncedOptions> {
return await invoke('plugin:instance|instance_get_initialized_synced_options')
}
/**
* Every synced option is currently available on every instance. The parameter
* stays so callers keep passing the option they are asking about, which is what
* a real implementation will need.
*/
export function isSyncedOptionAvailable(_option: SyncedOption): boolean {
return true
}
export async function set_global_synced_option(
option: SyncedOption,
enabled: boolean,
baseInstanceId?: string | null,
): Promise<GlobalSyncedOptions> {
return await invoke('plugin:instance|instance_set_synced_option', {
option,
enabled,
baseInstanceId,
})
}
export async function set_instance_synced_option(
instanceId: string,
option: SyncedOption,
enabled: boolean,
resolution?: string | null,
): Promise<GameInstance> {
return await invoke('plugin:instance|instance_set_instance_synced_option', {
instanceId,
option,
enabled,
resolution,
})
}
export async function get_synced_options_overview(
instanceId: string,
): Promise<SyncedOptionsOverview> {
return await invoke('plugin:instance|instance_get_synced_options_overview', { instanceId })
}
export async function get_command_history(): Promise<string> {
return await invoke('plugin:instance|instance_get_synced_command_history')
}
export async function set_command_history(contents: string): Promise<string> {
return await invoke('plugin:instance|instance_set_synced_command_history', { contents })
}
export type SyncedServer = {
id: string
name: string
address: string
accept_textures: boolean | null
}
export async function list_synced_servers(): Promise<SyncedServer[]> {
return await invoke('plugin:instance|instance_list_synced_servers')
}
export async function update_synced_server(server: SyncedServer): Promise<void> {
await invoke('plugin:instance|instance_update_synced_server', { server })
}
export async function remove_synced_server(id: string): Promise<void> {
await invoke('plugin:instance|instance_remove_synced_server', { id })
}
export async function get_projects(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
@@ -0,0 +1,61 @@
import { queryOptions } from '@tanstack/vue-query'
import { list_game_options_sync_sources } from './game-options'
import {
get_command_history,
get_global_synced_options,
get_initialized_synced_options,
list_synced_servers,
} from './instance'
import type { GameInstance } from './types'
export const syncedOptionsKeys = {
global: ['global-synced-options'] as const,
initialized: ['initialized-synced-options'] as const,
gameSources: ['game-options-sync-sources'] as const,
set: ['global-synced-options', 'set'] as const,
servers: ['synced-servers', 'list'] as const,
commandHistory: ['synced-options', 'command-history'] as const,
}
export function globalSyncedOptionsQueryOptions() {
return queryOptions({
queryKey: syncedOptionsKeys.global,
queryFn: get_global_synced_options,
})
}
export function initializedSyncedOptionsQueryOptions() {
return queryOptions({
queryKey: syncedOptionsKeys.initialized,
queryFn: get_initialized_synced_options,
})
}
export function gameOptionsSyncSourcesQueryOptions() {
return queryOptions({
queryKey: syncedOptionsKeys.gameSources,
queryFn: list_game_options_sync_sources,
staleTime: 0,
})
}
export function syncedServersQueryOptions() {
return queryOptions({
queryKey: syncedOptionsKeys.servers,
queryFn: list_synced_servers,
})
}
export function commandHistoryQueryOptions() {
return queryOptions({
queryKey: syncedOptionsKeys.commandHistory,
queryFn: get_command_history,
})
}
export function canSourceMultiplayerServers(instance: GameInstance): boolean {
return !['server_project', 'server_project_modpack', 'modrinth_hosting'].includes(
instance.link?.type ?? '',
)
}
@@ -0,0 +1,83 @@
import type { ContentItem } from '@modrinth/ui'
import { queryOptions } from '@tanstack/vue-query'
import { invoke } from '@tauri-apps/api/core'
import { adaptContentItems, isSyncedOptionAvailable } from './instance'
export type SyncedPackType = 'resourcepack' | 'datapack'
export type SyncedPackAction = 'enable' | 'disable'
export type DesyncServerMode = 'keep_in_other_instances' | 'remove_from_other_instances'
export function isSyncedPackTypeAvailable(projectType: ContentItem['project_type']): boolean {
return (
['resourcepack', 'datapack'].includes(projectType) &&
isSyncedOptionAvailable(projectType === 'resourcepack' ? 'resource_packs' : 'data_packs')
)
}
export interface PackSyncPreview {
pack: ContentItem
instances: {
instance_id: string
name: string
game_version: string
compatible: boolean
participating: boolean
}[]
}
export const syncedPackKeys = {
all: ['synced-packs'] as const,
list: (type: SyncedPackType) => ['synced-packs', 'list', type] as const,
}
export function syncedPackQueryOptions(type: SyncedPackType) {
return queryOptions({
queryKey: syncedPackKeys.list(type),
queryFn: () => list_synced_packs(type),
enabled: isSyncedPackTypeAvailable(type),
})
}
export async function list_synced_packs(projectType: SyncedPackType): Promise<ContentItem[]> {
const items = await invoke<ContentItem[]>('plugin:instance|instance_list_synced_packs', {
projectType,
})
return adaptContentItems(items).map((item) => ({ ...item, external: !item.project }))
}
export function get_pack_sync_preview(
instanceId: string,
projectPath: string,
): Promise<PackSyncPreview> {
return invoke('plugin:instance|instance_get_pack_sync_preview', { instanceId, projectPath })
}
export function sync_pack(instanceId: string, projectPath: string): Promise<void> {
return invoke('plugin:instance|instance_sync_pack', { instanceId, projectPath })
}
export function desync_pack(
instanceId: string,
packId: string,
mode: DesyncServerMode,
): Promise<void> {
return invoke('plugin:instance|instance_desync_pack', { instanceId, packId, mode })
}
export function upload_synced_pack(
path: string,
projectType: SyncedPackType,
gameVersions: string[],
): Promise<void> {
return invoke('plugin:instance|instance_upload_synced_pack', { path, projectType, gameVersions })
}
export function set_synced_pack_enabled(packId: string, enabled: boolean): Promise<void> {
return invoke('plugin:instance|instance_set_synced_pack_enabled', { packId, enabled })
}
export function remove_synced_pack(packId: string): Promise<void> {
return invoke('plugin:instance|instance_remove_synced_pack', { packId })
}