feat(instance-sync): screenshots sync (#7218)

* pnpm prepr

* feat(instance-sync): screenshots sync

* fix: prepr + fmt

* fix: qa

* feat: screenshit editor

* feat: screenshot* editor qa

* fix: qa

* fix: lint

* fix: aecsocket rev

* qa: a11y tab navigation focus is cut off

* qa: Change “Edited” badge to be bg-highlight-green

* qa: friends list input style wrong

* qa: toolbar width + labels

* qa: multiselect changes

* qa: anim changes

* qa: card hover colors

* qa: copy feedback

* qa: hide date

* qa: instance icon in overflow/contextmenu

* qa: spacing

* qa: group empty state text

* qa: drag preview badge

* qa: group deletion skip warning if no screenshots

* qa: redesign editor

* qa: remove screenshot parent/edited badge stuff

* qa: control font size consistency

* qa: viewer copy control

* qa: editor fixes

* qa: redirect on disable screenshot sync

* qa: margin police

* fix: crop

* fix: qa

* feat: initial start on basic instance file syncing (#7220)

* qa: final

* qa: final 2

* fix: lint + prepr

* fix: copy

* fix: screenshot editing outside of app causing ghost files in db + sync fail rollback impl

* chore: split up

* fix: fmt

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
This commit is contained in:
Calum H.
2026-08-27 16:49:46 +00:00
committed by GitHub
co-authored by tdgao
parent 2bd108c278
commit 7c67cca7a7
217 changed files with 17139 additions and 1887 deletions
@@ -67,7 +67,7 @@ const router = useRouter()
const { addNotification } = injectNotificationManager()
const emit = defineEmits<{
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete' | 'desync'): void
(e: 'open-folder', world: SingleplayerWorld): void
}>()
@@ -95,6 +95,8 @@ const props = withDefaults(
}
managed?: boolean
showPlayButton?: boolean
cardBackground?: 'raised' | 'surface-2'
// Instance
instanceId?: string
@@ -117,6 +119,8 @@ const props = withDefaults(
gameMode: undefined,
managed: false,
showPlayButton: true,
cardBackground: 'raised',
instanceId: undefined,
instanceName: undefined,
@@ -218,6 +222,10 @@ const messages = defineMessages({
id: 'instance.worlds.copy_address',
defaultMessage: 'Copy address',
},
desync: {
id: 'instance.worlds.desync_server',
defaultMessage: 'Desync',
},
viewInstance: {
id: 'instance.worlds.view_instance',
defaultMessage: 'View instance',
@@ -270,7 +278,10 @@ const messages = defineMessages({
const cardOptions = useTemplateRef('cardOptions')
const showStop = computed(
() => (props.playingWorld || (locked.value && props.playingInstance)) && !props.startingInstance,
() =>
props.showPlayButton &&
(props.playingWorld || (locked.value && props.playingInstance)) &&
!props.startingInstance,
)
const playDisabled = computed(
() =>
@@ -327,6 +338,17 @@ const overflowOptions = computed((): ButtonMenuOption[] => [
shown: props.world.type === 'server',
action: () => copyToClipboard((props.world as ServerWorld).address),
},
{
id: 'desync',
label: formatMessage(messages.desync),
icon: XIcon,
action: () => emit('desync'),
shown:
!props.instanceId &&
props.world.type === 'server' &&
(props.world as ServerWorld).source === 'user_synced' &&
!!(props.world as ServerWorld).server_id,
},
{
id: 'edit',
label: formatMessage(commonMessages.editButton),
@@ -404,6 +426,7 @@ const contextMenuOptions = computed((): ButtonMenuOption[] => [
label: formatMessage(commonMessages.stopButton),
icon: StopCircleIcon,
tone: 'red',
shown: props.showPlayButton,
action: () => emit('stop'),
}
: {
@@ -411,11 +434,12 @@ const contextMenuOptions = computed((): ButtonMenuOption[] => [
label: formatMessage(messages.playWorld),
icon: PlayIcon,
tone: 'brand',
shown: props.showPlayButton,
disabled: playDisabled.value,
tooltip: playTooltip.value ?? undefined,
action: () => emit('play'),
},
{ type: 'divider' },
{ type: 'divider', shown: props.showPlayButton },
...overflowOptions.value,
])
@@ -433,9 +457,11 @@ function openContextMenu(event: MouseEvent) {
/>
</template>
<div
class="clickable-card grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised border border-solid border-surface-4 smart-clickable:highlight-on-hover rounded-[20px] transition-[filter] ease-out [--hover-brightness:1.25] min-h-20"
class="clickable-card grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 border border-solid border-surface-4 smart-clickable:highlight-on-hover rounded-[20px] transition-[filter] ease-out [--hover-brightness:1.25] min-h-20"
:class="{
'world-item-highlighted': highlighted,
'bg-bg-raised': cardBackground === 'raised',
'bg-surface-2': cardBackground === 'surface-2',
}"
>
<Avatar
@@ -592,12 +618,17 @@ function openContextMenu(event: MouseEvent) {
</template>
</div>
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
<Button v-if="showStop" type="colored" color="red" @click="emit('stop')">
<Button
v-if="showPlayButton && showStop"
type="colored"
color="red"
@click="emit('stop')"
>
<StopCircleIcon aria-hidden="true" />
{{ formatMessage(commonMessages.stopButton) }}
</Button>
<Button
v-else
v-else-if="showPlayButton"
v-tooltip="playTooltip"
:disabled="playDisabled"
type="colored"
@@ -16,6 +16,7 @@ const { formatMessage } = useVIntl()
const props = defineProps<{
world: World | null
otherSyncedInstanceCount: number
}>()
const emit = defineEmits<{
@@ -44,6 +45,11 @@ const messages = defineMessages({
defaultMessage:
'This server will be removed from your server list and from the in-game server list. You can add it again later if you know the address.',
},
syncedServerRemovalNotice: {
id: 'app.instance.worlds.remove-server-modal.synced-removal-notice',
defaultMessage:
'This server will also be removed from {count, plural, one {# other instance} other {# other instances}} because its synced.',
},
deleteWorldWarningBody: {
id: 'app.instance.worlds.delete-world-modal.warning-body',
defaultMessage:
@@ -62,6 +68,9 @@ const messages = defineMessages({
const modal = ref<InstanceType<typeof NewModal>>()
const isServer = computed(() => props.world?.type === 'server')
const isSyncedServer = computed(
() => props.world?.type === 'server' && props.world.source === 'user_synced',
)
const isSingleplayer = computed(() => props.world?.type === 'singleplayer')
const titleMessage = computed(() =>
isServer.value ? messages.removeServerTitle : messages.deleteWorldTitle,
@@ -102,6 +111,13 @@ defineExpose({ show, hide })
>
{{ formatMessage(warningBodyMessage) }}
</Admonition>
<p v-if="isSyncedServer" class="m-0 text-secondary">
{{
formatMessage(messages.syncedServerRemovalNotice, {
count: otherSyncedInstanceCount,
})
}}
</p>
</div>
<template #actions>
@@ -0,0 +1,74 @@
<script setup lang="ts">
import { LinkIcon, TrashIcon, XIcon } from '@modrinth/assets'
import { Button, commonMessages, defineMessages, NewModal, useVIntl } from '@modrinth/ui'
import { ref } from 'vue'
import type { DesyncServerMode, ServerWorld } from '@/helpers/worlds'
const { formatMessage } = useVIntl()
const modal = ref<InstanceType<typeof NewModal> | null>(null)
const server = ref<ServerWorld | null>(null)
const emit = defineEmits<{
confirm: [server: ServerWorld, mode: DesyncServerMode]
}>()
const messages = defineMessages({
title: {
id: 'instance.worlds.desync-server.title',
defaultMessage: 'Desync server',
},
description: {
id: 'instance.worlds.desync-server.description',
defaultMessage:
'Do you want to keep this server in other synced instances, or remove it from them?',
},
keep: {
id: 'instance.worlds.desync-server.keep',
defaultMessage: 'Keep',
},
remove: {
id: 'instance.worlds.desync-server.remove',
defaultMessage: 'Remove',
},
})
function show(value: ServerWorld) {
server.value = value
modal.value?.show()
}
function confirm(mode: DesyncServerMode) {
if (server.value) emit('confirm', server.value, mode)
modal.value?.hide()
}
defineExpose({ show })
</script>
<template>
<NewModal ref="modal" :header="formatMessage(messages.title)" max-width="540px">
<p class="m-0 text-secondary">{{ formatMessage(messages.description) }}</p>
<template #actions>
<div class="flex justify-end gap-2">
<Button type="outlined" class="whitespace-nowrap" @click="modal?.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</Button>
<Button class="whitespace-nowrap" @click="confirm('keep_in_other_instances')">
<LinkIcon />
{{ formatMessage(messages.keep) }}
</Button>
<Button
type="colored"
color="red"
class="whitespace-nowrap"
@click="confirm('remove_from_other_instances')"
>
<TrashIcon />
{{ formatMessage(messages.remove) }}
</Button>
</div>
</template>
</NewModal>
</template>