diff --git a/apps/app-frontend/src/components/ui/instance_settings/SyncSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/SyncSettings.vue new file mode 100644 index 0000000..9032ecd --- /dev/null +++ b/apps/app-frontend/src/components/ui/instance_settings/SyncSettings.vue @@ -0,0 +1,607 @@ + + + diff --git a/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue b/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue index d7fa727..b52724e 100644 --- a/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue +++ b/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue @@ -7,6 +7,7 @@ import { FileArchiveIcon, InfoIcon, MonitorIcon, + RefreshCwIcon, WrenchIcon, } from '@modrinth/assets' import { @@ -23,6 +24,7 @@ import { computed, nextTick, ref, watch } from 'vue' import BackupSettings from '@/components/ui/instance_settings/BackupSettings.vue' import CoreComponentsSettings from '@/components/ui/instance_settings/CoreComponentsSettings.vue' import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue' +import SyncSettings from '@/components/ui/instance_settings/SyncSettings.vue' import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue' import InstallationSettings from '@/components/ui/instance_settings/InstallationSettings.vue' import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue' @@ -147,6 +149,14 @@ const tabs = computed(() => [ icon: CodeIcon, content: HooksSettings, }, + { + name: defineMessage({ + id: 'instance.settings.tabs.sync', + defaultMessage: 'Sync', + }), + icon: RefreshCwIcon, + content: SyncSettings, + }, ]) function getSupportedModpackLoaders() { diff --git a/apps/app-frontend/src/components/ui/settings/instances/SyncSourceModal.vue b/apps/app-frontend/src/components/ui/settings/instances/SyncSourceModal.vue new file mode 100644 index 0000000..c975cbf --- /dev/null +++ b/apps/app-frontend/src/components/ui/settings/instances/SyncSourceModal.vue @@ -0,0 +1,181 @@ + + + diff --git a/apps/app-frontend/src/components/ui/settings/instances/SyncedPacksModal.vue b/apps/app-frontend/src/components/ui/settings/instances/SyncedPacksModal.vue new file mode 100644 index 0000000..9298660 --- /dev/null +++ b/apps/app-frontend/src/components/ui/settings/instances/SyncedPacksModal.vue @@ -0,0 +1,81 @@ + + + diff --git a/apps/app-frontend/src/components/ui/settings/instances/command-history-modal.vue b/apps/app-frontend/src/components/ui/settings/instances/command-history-modal.vue new file mode 100644 index 0000000..222c478 --- /dev/null +++ b/apps/app-frontend/src/components/ui/settings/instances/command-history-modal.vue @@ -0,0 +1,93 @@ + + + diff --git a/apps/app-frontend/src/components/ui/settings/instances/servers-modal.vue b/apps/app-frontend/src/components/ui/settings/instances/servers-modal.vue new file mode 100644 index 0000000..0e28d2c --- /dev/null +++ b/apps/app-frontend/src/components/ui/settings/instances/servers-modal.vue @@ -0,0 +1,476 @@ + + + diff --git a/apps/app-frontend/src/helpers/game-options.ts b/apps/app-frontend/src/helpers/game-options.ts new file mode 100644 index 0000000..e491395 --- /dev/null +++ b/apps/app-frontend/src/helpers/game-options.ts @@ -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 + 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 }> { + return await invoke('plugin:instance|instance_get_game_setting_locale_labels', { + instanceId, + locale, + optionIds, + refreshSources, + }) +} + +export async function list_game_options_sync_sources(): Promise { + return await invoke('plugin:instance|instance_list_game_options_sync_sources') +} + +export async function get_synced_game_options_config(): Promise { + 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 { + return await invoke('plugin:instance|instance_preview_synced_game_option_changes', { request }) +} + +export async function save_synced_game_option_changes( + request: UpdateGameSettingsRequest, +): Promise { + return await invoke('plugin:instance|instance_save_synced_game_option_changes', { request }) +} + +export async function get_local_game_options_config( + instanceId: string, +): Promise { + 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 { + 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 { + return await invoke('plugin:instance|instance_save_local_game_option_changes', { + instanceId, + request, + }) +} diff --git a/apps/app-frontend/src/helpers/instance.ts b/apps/app-frontend/src/helpers/instance.ts index 537dc3d..8bef6ea 100644 --- a/apps/app-frontend/src/helpers/instance.ts +++ b/apps/app-frontend/src/helpers/instance.ts @@ -108,6 +108,104 @@ export async function get_many(instanceIds: string[]): Promise { 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 +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 { + return await invoke('plugin:instance|instance_get_synced_options') +} +export async function get_initialized_synced_options(): Promise { + 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 { + 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 { + return await invoke('plugin:instance|instance_set_instance_synced_option', { + instanceId, + option, + enabled, + resolution, + }) +} +export async function get_synced_options_overview( + instanceId: string, +): Promise { + return await invoke('plugin:instance|instance_get_synced_options_overview', { instanceId }) +} +export async function get_command_history(): Promise { + return await invoke('plugin:instance|instance_get_synced_command_history') +} +export async function set_command_history(contents: string): Promise { + 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 { + return await invoke('plugin:instance|instance_list_synced_servers') +} +export async function update_synced_server(server: SyncedServer): Promise { + await invoke('plugin:instance|instance_update_synced_server', { server }) +} +export async function remove_synced_server(id: string): Promise { + await invoke('plugin:instance|instance_remove_synced_server', { id }) +} + export async function get_projects( instanceId: string, cacheBehaviour?: CacheBehaviour, diff --git a/apps/app-frontend/src/helpers/synced-options.ts b/apps/app-frontend/src/helpers/synced-options.ts new file mode 100644 index 0000000..cf5b9f1 --- /dev/null +++ b/apps/app-frontend/src/helpers/synced-options.ts @@ -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 ?? '', + ) +} diff --git a/apps/app-frontend/src/helpers/synced-packs.ts b/apps/app-frontend/src/helpers/synced-packs.ts new file mode 100644 index 0000000..9a86fa6 --- /dev/null +++ b/apps/app-frontend/src/helpers/synced-packs.ts @@ -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 { + const items = await invoke('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 { + return invoke('plugin:instance|instance_get_pack_sync_preview', { instanceId, projectPath }) +} + +export function sync_pack(instanceId: string, projectPath: string): Promise { + return invoke('plugin:instance|instance_sync_pack', { instanceId, projectPath }) +} + +export function desync_pack( + instanceId: string, + packId: string, + mode: DesyncServerMode, +): Promise { + return invoke('plugin:instance|instance_desync_pack', { instanceId, packId, mode }) +} + +export function upload_synced_pack( + path: string, + projectType: SyncedPackType, + gameVersions: string[], +): Promise { + return invoke('plugin:instance|instance_upload_synced_pack', { path, projectType, gameVersions }) +} + +export function set_synced_pack_enabled(packId: string, enabled: boolean): Promise { + return invoke('plugin:instance|instance_set_synced_pack_enabled', { packId, enabled }) +} + +export function remove_synced_pack(packId: string): Promise { + return invoke('plugin:instance|instance_remove_synced_pack', { packId }) +} diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index 9373038..6bc7439 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -10165,5 +10165,191 @@ }, "settings.backups.repository.usage-description": { "message": "{stored} stored for {logical} of backup data across {count, plural, one {# snapshot} other {# snapshots}}." + }, + "app.settings.synced-options.choose-sync-source.command-history-description": { + "message": "Choose which instance to copy your command history from. This history is only used for the initial sync, and you can edit it from any instance afterward." + }, + "app.settings.synced-options.choose-sync-source.command-history-title": { + "message": "Choose command history source" + }, + "app.settings.synced-options.choose-sync-source.creative-hotbars-description": { + "message": "Choose which instance to copy your saved creative hotbars from. These hotbars are only used for the initial sync, and you can edit them from any instance afterward." + }, + "app.settings.synced-options.choose-sync-source.creative-hotbars-title": { + "message": "Choose creative hotbars source" + }, + "app.settings.synced-options.choose-sync-source.data-packs-title": { + "message": "Choose data packs source" + }, + "app.settings.synced-options.choose-sync-source.game-settings-description": { + "message": "Choose which instance to copy your game settings from. These settings are only used for the initial sync, and you can edit them from any instance afterward." + }, + "app.settings.synced-options.choose-sync-source.game-settings-title": { + "message": "Choose game settings source" + }, + "app.settings.synced-options.choose-sync-source.ineligible": { + "message": "You cannot choose this instance as a sync source" + }, + "app.settings.synced-options.choose-sync-source.load-error": { + "message": "Could not load sync sources. Please try again." + }, + "app.settings.synced-options.choose-sync-source.multiplayer-servers-description": { + "message": "Choose which instance to copy your multiplayer servers from. These servers are only used for the initial sync, and you can edit them from any instance afterward." + }, + "app.settings.synced-options.choose-sync-source.multiplayer-servers-title": { + "message": "Choose multiplayer servers source" + }, + "app.settings.synced-options.choose-sync-source.no-instances-found": { + "message": "No instances found" + }, + "app.settings.synced-options.choose-sync-source.resource-packs-title": { + "message": "Choose resource packs source" + }, + "app.settings.synced-options.choose-sync-source.retry": { + "message": "Try again" + }, + "app.settings.synced-options.choose-sync-source.search-placeholder": { + "message": "Search instance" + }, + "app.settings.synced-options.choose-sync-source.sync": { + "message": "Sync" + }, + "app.settings.synced-options.command-history": { + "message": "Sync command history" + }, + "app.settings.synced-options.command-history.description": { + "message": "Use the same command history across your instances." + }, + "app.settings.synced-options.command-history.editor-title": { + "message": "Edit command history" + }, + "app.settings.synced-options.creative-hotbars": { + "message": "Sync saved creative hotbars" + }, + "app.settings.synced-options.creative-hotbars.description": { + "message": "Use the same saved creative hotbars across your instances." + }, + "app.settings.synced-options.data-packs": { + "message": "Sync data packs" + }, + "app.settings.synced-options.data-packs.description": { + "message": "Use the same datapacks across your instances" + }, + "app.settings.synced-options.data-packs.none-synced-yet": { + "message": "You haven't synced any data packs yet" + }, + "app.settings.synced-options.edit.no-instances": { + "message": "Add an instance before editing synced data." + }, + "app.settings.synced-options.edit.no-synced-data": { + "message": "Choose a sync source before editing this setting." + }, + "app.settings.synced-options.game-settings": { + "message": "Sync game options" + }, + "app.settings.synced-options.game-settings.button": { + "message": "Edit game settings" + }, + "app.settings.synced-options.game-settings.description": { + "message": "Use the same game options across your instances." + }, + "app.settings.synced-options.game-settings.disabled-tooltip": { + "message": "Enable game settings sync before choosing individual settings." + }, + "app.settings.synced-options.game-settings.no-options-to-edit": { + "message": "You haven't got any options yet to edit" + }, + "app.settings.synced-options.multiplayer-servers": { + "message": "Sync multiplayer servers" + }, + "app.settings.synced-options.multiplayer-servers.address": { + "message": "Server address" + }, + "app.settings.synced-options.multiplayer-servers.copy-address": { + "message": "Copy address" + }, + "app.settings.synced-options.multiplayer-servers.count": { + "message": "{count, plural, one {# server} other {# servers}}" + }, + "app.settings.synced-options.multiplayer-servers.description": { + "message": "Use the same multiplayer servers across your instances." + }, + "app.settings.synced-options.multiplayer-servers.editor-title": { + "message": "Edit synced servers" + }, + "app.settings.synced-options.multiplayer-servers.empty": { + "message": "No user-added servers are currently synced." + }, + "app.settings.synced-options.multiplayer-servers.filter-offline": { + "message": "Offline" + }, + "app.settings.synced-options.multiplayer-servers.filter-online": { + "message": "Online" + }, + "app.settings.synced-options.multiplayer-servers.more-options": { + "message": "More server options" + }, + "app.settings.synced-options.multiplayer-servers.name": { + "message": "Server name" + }, + "app.settings.synced-options.multiplayer-servers.no-results": { + "message": "No synced servers match your search or filters." + }, + "app.settings.synced-options.multiplayer-servers.none-synced-yet": { + "message": "You haven't synced any servers yet" + }, + "app.settings.synced-options.multiplayer-servers.players-online": { + "message": "{count, number} online" + }, + "app.settings.synced-options.multiplayer-servers.search": { + "message": "Search {count, number} {count, plural, one {synced server} other {synced servers}}..." + }, + "app.settings.synced-options.multiplayer-servers.status": { + "message": "Status" + }, + "app.settings.synced-options.packs.disabled": { + "message": "Enable syncing before editing shared packs." + }, + "app.settings.synced-options.resource-packs": { + "message": "Sync resource packs" + }, + "app.settings.synced-options.resource-packs.description": { + "message": "Use the same resource packs across your instances." + }, + "app.settings.synced-options.resource-packs.none-synced-yet": { + "message": "You haven't synced any resource packs yet" + }, + "app.settings.synced-options.title": { + "message": "Options synced across all instances" + }, + "app.settings.synced-packs.data-packs": { + "message": "Data packs" + }, + "app.settings.synced-packs.disable": { + "message": "Disable" + }, + "app.settings.synced-packs.empty": { + "message": "No synced packs" + }, + "app.settings.synced-packs.enable": { + "message": "Enable" + }, + "app.settings.synced-packs.remove": { + "message": "Remove" + }, + "app.settings.synced-packs.resource-packs": { + "message": "Resource packs" + }, + "app.sync-instances-update.choose-source.data-packs": { + "message": "Choose which instance to copy your data packs from. These packs are only used for the initial sync, and you can edit them from any instance afterward." + }, + "app.sync-instances-update.choose-source.resource-packs": { + "message": "Choose which instance to copy your resource packs from. These packs are only used for the initial sync, and you can edit them from any instance afterward." + }, + "instance.settings.tabs.backups": { + "message": "Backups" + }, + "instance.settings.tabs.sync": { + "message": "Synchronization" } } diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index a2ffa43..c88a4d8 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -10929,5 +10929,67 @@ }, "settings.backups.repository.usage-description": { "message": "实际存储 {stored},对应 {logical} 的备份数据,共 {count, plural, other {# 个快照}}。" - } + }, + "app.settings.synced-options.choose-sync-source.command-history-description": { "message": "选择要从哪个实例复制命令历史记录。这些记录仅用于初始同步,之后可从任意实例编辑。" }, + "app.settings.synced-options.choose-sync-source.command-history-title": { "message": "选择命令历史记录来源" }, + "app.settings.synced-options.choose-sync-source.creative-hotbars-description": { "message": "选择要从哪个实例复制保存的创造模式快捷栏。这些快捷栏仅用于初始同步,之后可从任意实例编辑。" }, + "app.settings.synced-options.choose-sync-source.creative-hotbars-title": { "message": "选择创造模式快捷栏来源" }, + "app.settings.synced-options.choose-sync-source.data-packs-title": { "message": "选择数据包来源" }, + "app.settings.synced-options.choose-sync-source.game-settings-description": { "message": "选择要从哪个实例复制游戏设置。这些设置仅用于初始同步,之后可从任意实例编辑。" }, + "app.settings.synced-options.choose-sync-source.game-settings-title": { "message": "选择游戏设置来源" }, + "app.settings.synced-options.choose-sync-source.ineligible": { "message": "无法选择此实例作为同步来源" }, + "app.settings.synced-options.choose-sync-source.load-error": { "message": "无法加载同步来源,请重试。" }, + "app.settings.synced-options.choose-sync-source.multiplayer-servers-description": { "message": "选择要从哪个实例复制多人服务器列表。这些服务器仅用于初始同步,之后可从任意实例编辑。" }, + "app.settings.synced-options.choose-sync-source.multiplayer-servers-title": { "message": "选择多人服务器来源" }, + "app.settings.synced-options.choose-sync-source.no-instances-found": { "message": "没有找到实例" }, + "app.settings.synced-options.choose-sync-source.resource-packs-title": { "message": "选择资源包来源" }, + "app.settings.synced-options.choose-sync-source.retry": { "message": "重试" }, + "app.settings.synced-options.choose-sync-source.search-placeholder": { "message": "搜索实例" }, + "app.settings.synced-options.choose-sync-source.sync": { "message": "同步" }, + "app.settings.synced-options.command-history": { "message": "同步命令记录" }, + "app.settings.synced-options.command-history.description": { "message": "在不同的实例之间同步命令记录。" }, + "app.settings.synced-options.command-history.editor-title": { "message": "编辑指令历史记录" }, + "app.settings.synced-options.creative-hotbars": { "message": "同步保存的创造模式物品栏" }, + "app.settings.synced-options.creative-hotbars.description": { "message": "在不同的实例之间同步创造模式物品栏。" }, + "app.settings.synced-options.data-packs": { "message": "同步数据包" }, + "app.settings.synced-options.data-packs.description": { "message": "在所有实例间使用相同的数据包" }, + "app.settings.synced-options.data-packs.none-synced-yet": { "message": "尚未同步任何数据包" }, + "app.settings.synced-options.edit.no-instances": { "message": "请先添加实例,再编辑同步数据。" }, + "app.settings.synced-options.edit.no-synced-data": { "message": "请先选择同步来源,再编辑此设置。" }, + "app.settings.synced-options.game-settings": { "message": "同步游戏设置" }, + "app.settings.synced-options.game-settings.button": { "message": "编辑游戏设置" }, + "app.settings.synced-options.game-settings.description": { "message": "在所有实例间使用相同的游戏设置。" }, + "app.settings.synced-options.game-settings.disabled-tooltip": { "message": "请先启用游戏设置同步,再选择单个设置。" }, + "app.settings.synced-options.game-settings.no-options-to-edit": { "message": "目前没有可编辑的设置" }, + "app.settings.synced-options.multiplayer-servers": { "message": "同步多人模式服务器列表" }, + "app.settings.synced-options.multiplayer-servers.address": { "message": "服务器地址" }, + "app.settings.synced-options.multiplayer-servers.copy-address": { "message": "复制地址" }, + "app.settings.synced-options.multiplayer-servers.count": { "message": "{count, number} 个服务器" }, + "app.settings.synced-options.multiplayer-servers.description": { "message": "在不同的实例直接同步多人模式服务器列表。" }, + "app.settings.synced-options.multiplayer-servers.editor-title": { "message": "编辑同步的服务器" }, + "app.settings.synced-options.multiplayer-servers.empty": { "message": "当前未同步任何用户添加的服务器。" }, + "app.settings.synced-options.multiplayer-servers.filter-offline": { "message": "离线" }, + "app.settings.synced-options.multiplayer-servers.filter-online": { "message": "在线" }, + "app.settings.synced-options.multiplayer-servers.more-options": { "message": "更多服务器选项" }, + "app.settings.synced-options.multiplayer-servers.name": { "message": "服务器名称" }, + "app.settings.synced-options.multiplayer-servers.no-results": { "message": "没有匹配搜索或筛选条件的已同步服务器。" }, + "app.settings.synced-options.multiplayer-servers.none-synced-yet": { "message": "暂无已同步的服务器" }, + "app.settings.synced-options.multiplayer-servers.players-online": { "message": "{count, number} 人在线" }, + "app.settings.synced-options.multiplayer-servers.search": { "message": "搜索 {count, number} 个已同步服务器……" }, + "app.settings.synced-options.multiplayer-servers.status": { "message": "状态" }, + "app.settings.synced-options.packs.disabled": { "message": "请先启用同步,再编辑共享包。" }, + "app.settings.synced-options.resource-packs": { "message": "同步资源包" }, + "app.settings.synced-options.resource-packs.description": { "message": "在所有实例间使用相同的资源包" }, + "app.settings.synced-options.resource-packs.none-synced-yet": { "message": "尚未同步任何资源包" }, + "app.settings.synced-options.title": { "message": "跨所有实例同步的选项" }, + "app.settings.synced-packs.data-packs": { "message": "数据包" }, + "app.settings.synced-packs.disable": { "message": "禁用" }, + "app.settings.synced-packs.empty": { "message": "没有已同步的包" }, + "app.settings.synced-packs.enable": { "message": "启用" }, + "app.settings.synced-packs.remove": { "message": "移除" }, + "app.settings.synced-packs.resource-packs": { "message": "资源包" }, + "app.sync-instances-update.choose-source.data-packs": { "message": "选择要从哪个实例复制数据包。这些包仅用于初始化同步,之后可从任意实例进行编辑。" }, + "app.sync-instances-update.choose-source.resource-packs": { "message": "选择要从哪个实例复制资源包。这些包仅用于初始化同步,之后可从任意实例进行编辑。" }, + "instance.settings.tabs.backups": { "message": "备份" }, + "instance.settings.tabs.sync": { "message": "同步" } } diff --git a/packages/ui/src/components/base/buttons/CheckCircleButton.vue b/packages/ui/src/components/base/buttons/CheckCircleButton.vue new file mode 100644 index 0000000..53e1d20 --- /dev/null +++ b/packages/ui/src/components/base/buttons/CheckCircleButton.vue @@ -0,0 +1,53 @@ + + + diff --git a/packages/ui/src/components/base/index.ts b/packages/ui/src/components/base/index.ts index 877f82a..250489c 100644 --- a/packages/ui/src/components/base/index.ts +++ b/packages/ui/src/components/base/index.ts @@ -11,6 +11,7 @@ export { default as BulletDivider } from './BulletDivider.vue' export { default as Button } from './Button.vue' export { default as NewButton } from './buttons/Button.vue' export { default as IconButton } from './buttons/IconButton.vue' +export { default as CheckCircleButton } from './buttons/CheckCircleButton.vue' export type { ButtonColor, ButtonElementHandle, @@ -78,6 +79,7 @@ export { default as NavTabs } from './NavTabs.vue' export { default as OptionGroup } from './OptionGroup.vue' export type { Option as OverflowMenuOption } from './OverflowMenu.vue' export { default as OverflowMenu } from './OverflowMenu.vue' +export { default as TeleportOverflowMenu } from './TeleportOverflowMenu.vue' export { default as Page } from './Page.vue' export { default as Pagination } from './Pagination.vue' export { default as PopoutMenu } from './PopoutMenu.vue'