fix: 完善整合包同步与启动器交互
Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled
Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled
This commit is contained in:
@ -0,0 +1,240 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, NewModal, useVIntl } from '@modrinth/ui'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import MinecraftLoginModal from '@/components/ui/MinecraftLoginModal.vue'
|
||||
import {
|
||||
openSkinSiteLogin,
|
||||
requestSkinSitePlayers,
|
||||
skinSitePlayers,
|
||||
skinSiteStatus,
|
||||
skinSiteUser,
|
||||
} from '@/composables/skin-site-session'
|
||||
import { users } from '@/helpers/auth'
|
||||
import { getInstanceMode } from '@/helpers/hosted-packs'
|
||||
import {
|
||||
registerInstancePlayerPicker,
|
||||
saveInstancePlayer,
|
||||
waitForSkinSiteSession,
|
||||
type InstancePlayer,
|
||||
type PlayerChoice,
|
||||
} from '@/helpers/instance-player'
|
||||
import { playerMessages as messages } from './instance-player-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const microsoftLogin = ref<InstanceType<typeof MinecraftLoginModal>>()
|
||||
const router = useRouter()
|
||||
const accounts = ref<PlayerChoice[]>([])
|
||||
const active = ref(false)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
const instanceId = ref('')
|
||||
const locked = ref<InstancePlayer | null>(null)
|
||||
const awaitingSkinLogin = ref(false)
|
||||
let hideForLogin = false
|
||||
let generation = 0
|
||||
let resolveSelection: ((player: InstancePlayer) => void) | undefined
|
||||
let rejectSelection: ((reason: Error) => void) | undefined
|
||||
const choices = computed(() => {
|
||||
const skin: PlayerChoice[] =
|
||||
skinSiteStatus.value === 'signed-in'
|
||||
? skinSitePlayers.value
|
||||
.filter((player) => !player.isMojang)
|
||||
.map((player) => ({
|
||||
id: player.uuid,
|
||||
name: player.name,
|
||||
account_type: 'yggdrasil',
|
||||
skin_site_user: skinSiteUser.value?.uuid,
|
||||
head: player.headDataUrl,
|
||||
}))
|
||||
: []
|
||||
return [...skin, ...accounts.value].filter(
|
||||
(player) =>
|
||||
!locked.value ||
|
||||
(player.id.replaceAll('-', '') === locked.value.id.replaceAll('-', '') &&
|
||||
player.account_type === locked.value.account_type &&
|
||||
(player.skin_site_user ?? null) === (locked.value.skin_site_user ?? null)),
|
||||
)
|
||||
})
|
||||
|
||||
async function refresh() {
|
||||
const revision = ++generation
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const [available, mode] = await Promise.all([users(), getInstanceMode(instanceId.value)])
|
||||
if (revision !== generation) return
|
||||
accounts.value = available
|
||||
.filter(
|
||||
(account) =>
|
||||
account.account_type === 'microsoft' ||
|
||||
(mode === 'local' && account.account_type === 'offline'),
|
||||
)
|
||||
.map((account) => ({
|
||||
id: account.profile.id,
|
||||
name: account.profile.name,
|
||||
account_type: account.account_type,
|
||||
}))
|
||||
await waitForSkinSiteSession()
|
||||
if (revision !== generation) return
|
||||
if (skinSiteStatus.value === 'signed-in') await requestSkinSitePlayers()
|
||||
if (skinSiteStatus.value === 'error') throw new Error('无法确认皮肤站登录状态,请重试。')
|
||||
} catch (cause) {
|
||||
if (revision === generation)
|
||||
error.value = cause instanceof Error ? cause.message : String(cause)
|
||||
} finally {
|
||||
if (revision === generation) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function cancelled() {
|
||||
if (hideForLogin) {
|
||||
hideForLogin = false
|
||||
return
|
||||
}
|
||||
if (!active.value) return
|
||||
active.value = false
|
||||
awaitingSkinLogin.value = false
|
||||
generation++
|
||||
rejectSelection?.(new Error('已取消选择实例玩家'))
|
||||
resolveSelection = undefined
|
||||
rejectSelection = undefined
|
||||
}
|
||||
|
||||
async function select(player: PlayerChoice) {
|
||||
if (loading.value || saving.value || !active.value) return
|
||||
saving.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const { head: _, ...binding } = player
|
||||
await saveInstancePlayer(instanceId.value, binding)
|
||||
active.value = false
|
||||
resolveSelection?.(binding)
|
||||
resolveSelection = undefined
|
||||
rejectSelection = undefined
|
||||
modal.value?.hide()
|
||||
} catch (cause) {
|
||||
error.value = cause instanceof Error ? cause.message : String(cause)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function signInSkinSite() {
|
||||
awaitingSkinLogin.value = true
|
||||
hideForLogin = true
|
||||
openSkinSiteLogin()
|
||||
modal.value?.hide()
|
||||
void router.push('/starlight-skin')
|
||||
}
|
||||
|
||||
const unregister = registerInstancePlayerPicker((id, saved) => {
|
||||
if (active.value) return Promise.reject(new Error('请先完成当前实例的玩家选择。'))
|
||||
instanceId.value = id
|
||||
locked.value = saved
|
||||
accounts.value = []
|
||||
awaitingSkinLogin.value = false
|
||||
active.value = true
|
||||
modal.value?.show()
|
||||
void refresh()
|
||||
return new Promise((resolve, reject) => {
|
||||
resolveSelection = resolve
|
||||
rejectSelection = reject
|
||||
})
|
||||
})
|
||||
watch([skinSiteStatus, () => skinSiteUser.value?.uuid], () => {
|
||||
if (!active.value || saving.value) return
|
||||
if (
|
||||
awaitingSkinLogin.value &&
|
||||
skinSiteStatus.value === 'signed-in' &&
|
||||
(!locked.value?.skin_site_user || locked.value.skin_site_user === skinSiteUser.value?.uuid)
|
||||
) {
|
||||
awaitingSkinLogin.value = false
|
||||
modal.value?.show()
|
||||
}
|
||||
void refresh()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
unregister()
|
||||
cancelled()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NewModal
|
||||
ref="modal"
|
||||
:header="formatMessage(messages.title)"
|
||||
:closable="!saving"
|
||||
:on-hide="cancelled"
|
||||
>
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="m-0 text-secondary">
|
||||
{{
|
||||
locked
|
||||
? formatMessage(messages.restore, { name: locked.name })
|
||||
: formatMessage(messages.remember)
|
||||
}}
|
||||
</p>
|
||||
<p v-if="loading" class="m-0" role="status">{{ formatMessage(messages.loading) }}</p>
|
||||
<template v-else>
|
||||
<ButtonStyled v-for="player in choices" :key="`${player.account_type}:${player.id}`">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="saving"
|
||||
class="flex items-center gap-3"
|
||||
@click="select(player)"
|
||||
>
|
||||
<img
|
||||
v-if="player.head"
|
||||
:src="player.head"
|
||||
alt=""
|
||||
class="size-8 rounded-md [image-rendering:pixelated]"
|
||||
/>
|
||||
<span>{{ player.name }}</span
|
||||
><span class="text-secondary">{{
|
||||
formatMessage(
|
||||
player.account_type === 'microsoft'
|
||||
? messages.licensed
|
||||
: player.account_type === 'offline'
|
||||
? messages.offline
|
||||
: messages.skin,
|
||||
)
|
||||
}}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<p v-if="!choices.length && !error && !locked" class="m-0 text-secondary">
|
||||
{{ formatMessage(skinSiteStatus === 'signed-in' ? messages.empty : messages.signIn) }}
|
||||
</p>
|
||||
</template>
|
||||
<p v-if="error" class="m-0 text-red" role="alert">{{ error }}</p>
|
||||
<ButtonStyled v-if="error"
|
||||
><button :disabled="loading || saving" @click="refresh">
|
||||
{{ formatMessage(messages.retry) }}
|
||||
</button></ButtonStyled
|
||||
>
|
||||
<ButtonStyled
|
||||
v-if="
|
||||
skinSiteStatus === 'signed-out' ||
|
||||
(locked?.skin_site_user && locked.skin_site_user !== skinSiteUser?.uuid)
|
||||
"
|
||||
><button :disabled="saving" @click="signInSkinSite">
|
||||
{{ formatMessage(messages.skinLogin) }}
|
||||
</button></ButtonStyled
|
||||
>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="text-secondary">{{ formatMessage(messages.or) }}</span>
|
||||
<ButtonStyled
|
||||
><button :disabled="loading || saving" @click="microsoftLogin?.showDeviceLogin()">
|
||||
{{ formatMessage(messages.useMicrosoft) }}
|
||||
</button></ButtonStyled
|
||||
>
|
||||
</div>
|
||||
<p v-if="saving" class="m-0" role="status">{{ formatMessage(messages.saving) }}</p>
|
||||
</div>
|
||||
</NewModal>
|
||||
<MinecraftLoginModal ref="microsoftLogin" @complete="refresh" />
|
||||
</template>
|
||||
@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, useVIntl } from '@modrinth/ui'
|
||||
import { onUnmounted, ref, watch } from 'vue'
|
||||
import {
|
||||
chooseInstancePlayer,
|
||||
getInstancePlayer,
|
||||
onInstancePlayerChanged,
|
||||
type InstancePlayer,
|
||||
} from '@/helpers/instance-player'
|
||||
import { playerMessages as messages } from './instance-player-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<{ instanceId: string }>()
|
||||
const player = ref<InstancePlayer | null>(null)
|
||||
const busy = ref(false)
|
||||
const error = ref('')
|
||||
let generation = 0
|
||||
const stop = onInstancePlayerChanged((id, saved) => {
|
||||
if (id === props.instanceId) {
|
||||
generation++
|
||||
player.value = saved
|
||||
busy.value = false
|
||||
error.value = ''
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
generation++
|
||||
stop()
|
||||
})
|
||||
watch(
|
||||
() => props.instanceId,
|
||||
async (id) => {
|
||||
const revision = ++generation
|
||||
player.value = null
|
||||
error.value = ''
|
||||
busy.value = true
|
||||
try {
|
||||
const saved = await getInstancePlayer(id)
|
||||
if (generation === revision) player.value = saved
|
||||
} catch (cause) {
|
||||
if (generation === revision) error.value = String(cause)
|
||||
} finally {
|
||||
if (generation === revision) busy.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
async function change() {
|
||||
if (busy.value) return
|
||||
const revision = generation
|
||||
const id = props.instanceId
|
||||
busy.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const saved = await chooseInstancePlayer(id)
|
||||
if (generation === revision) player.value = saved
|
||||
} catch (cause) {
|
||||
if (generation === revision)
|
||||
error.value = cause instanceof Error ? cause.message : String(cause)
|
||||
} finally {
|
||||
if (generation === revision) busy.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="mb-6 flex flex-col gap-2" data-onboarding-id="instance-player-settings">
|
||||
<h3 class="m-0 text-contrast">{{ formatMessage(messages.setting) }}</h3>
|
||||
<p class="m-0 text-secondary">
|
||||
{{ player ? player.name : formatMessage(messages.firstLaunch) }}
|
||||
</p>
|
||||
<ButtonStyled
|
||||
><button :disabled="busy" @click="change">
|
||||
{{ formatMessage(busy ? messages.loading : messages.change) }}
|
||||
</button></ButtonStyled
|
||||
>
|
||||
<p v-if="error" class="m-0 text-red" role="alert">{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
@ -133,14 +133,18 @@ function openDownloads() {
|
||||
</section>
|
||||
</div>
|
||||
<template #actions>
|
||||
<ButtonStyled
|
||||
><button @click="openDownloads">
|
||||
{{ formatMessage(messages.downloads) }}
|
||||
</button></ButtonStyled
|
||||
>
|
||||
<ButtonStyled
|
||||
><button @click="modal?.hide()">{{ formatMessage(messages.close) }}</button></ButtonStyled
|
||||
>
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<ButtonStyled>
|
||||
<button @click="openDownloads">
|
||||
{{ formatMessage(messages.downloads) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="modal?.hide()">
|
||||
{{ formatMessage(messages.close) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import { defineMessages } from '@modrinth/ui'
|
||||
|
||||
export const playerMessages = defineMessages({
|
||||
title: { id: 'app.instance-player.title', defaultMessage: 'Choose an instance player' },
|
||||
remember: {
|
||||
id: 'app.instance-player.remember',
|
||||
defaultMessage:
|
||||
'This instance will keep using your choice. Switch players in instance settings.',
|
||||
},
|
||||
restore: {
|
||||
id: 'app.instance-player.restore',
|
||||
defaultMessage: 'Sign in again as {name}. To choose someone else, open instance settings.',
|
||||
},
|
||||
loading: { id: 'app.instance-player.loading', defaultMessage: 'Loading signed-in players…' },
|
||||
licensed: { id: 'app.instance-player.licensed', defaultMessage: 'Minecraft account' },
|
||||
skin: { id: 'app.instance-player.skin', defaultMessage: 'Skin site player' },
|
||||
offline: { id: 'app.instance-player.offline', defaultMessage: 'Offline player' },
|
||||
empty: {
|
||||
id: 'app.instance-player.empty',
|
||||
defaultMessage: 'No available skin site players. You can create a player on the skin site.',
|
||||
},
|
||||
signIn: { id: 'app.instance-player.sign-in', defaultMessage: 'Sign in to choose a player.' },
|
||||
retry: { id: 'app.instance-player.retry', defaultMessage: 'Retry' },
|
||||
skinLogin: { id: 'app.instance-player.skin-login', defaultMessage: 'Sign in to skin site' },
|
||||
or: { id: 'app.instance-player.or', defaultMessage: 'Or' },
|
||||
useMicrosoft: {
|
||||
id: 'app.instance-player.use-microsoft',
|
||||
defaultMessage: 'Use a Minecraft account',
|
||||
},
|
||||
saving: { id: 'app.instance-player.saving', defaultMessage: 'Saving instance player…' },
|
||||
setting: { id: 'app.instance-player.setting', defaultMessage: 'Instance player' },
|
||||
firstLaunch: {
|
||||
id: 'app.instance-player.first-launch',
|
||||
defaultMessage: 'Choose a player on first launch',
|
||||
},
|
||||
change: { id: 'app.instance-player.change', defaultMessage: 'Switch instance player' },
|
||||
})
|
||||
@ -22,7 +22,8 @@ const { formatMessage } = useVIntl()
|
||||
const viewport = ref<HTMLElement>()
|
||||
const grid = ref<HTMLElement>()
|
||||
const view = ref({ left: 0, top: 0, width: 0, height: 0 })
|
||||
const brushCursor = ref({ x: 0, y: 0, color: '', visible: false })
|
||||
const brushCursor = ref<string>()
|
||||
const brushActive = computed(() => props.playing && props.selected >= 0)
|
||||
const large = computed(() => props.puzzle.difficulty !== 'easy')
|
||||
const visibleCells = computed(() =>
|
||||
props.puzzle.answer.flatMap((color, i) =>
|
||||
@ -87,37 +88,21 @@ function onWheel(event: WheelEvent) {
|
||||
void changeZoom(props.zoom + (event.deltaY < 0 ? 0.1 : -0.1))
|
||||
}
|
||||
|
||||
function moveBrushCursor(event: PointerEvent) {
|
||||
if (event.pointerType === 'touch') {
|
||||
hideBrushCursor()
|
||||
return
|
||||
}
|
||||
const cell = (event.target as Element | null)?.closest<HTMLButtonElement>('.mine-cell')
|
||||
const visible = Boolean(
|
||||
props.selected >= 0 && cell && !cell.disabled && !cell.classList.contains('mine-open'),
|
||||
)
|
||||
brushCursor.value = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
color: visible ? getComputedStyle(event.currentTarget as HTMLElement).color : '',
|
||||
visible,
|
||||
}
|
||||
let cursorColor = ''
|
||||
function updateBrushCursor() {
|
||||
if (!brushActive.value || !grid.value) return
|
||||
const color = getComputedStyle(grid.value).color
|
||||
if (color === cursorColor) return
|
||||
cursorColor = color
|
||||
// Let the native cursor follow the pointer independently of Vue and board rendering.
|
||||
// The hotspot is the same brush tip as the previous 32px floating SVG.
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="${color}" d="M358.681 586.386s-90.968 49.4-94.488 126.827c-3.519 77.428-77.427 133.74-102.063 140.778s360.157 22.971 332.002-142.444l-135.45-125.16zm169.099 52.56c14.016 13.601 17.565 32.675 7.929 42.606-9.635 9.93-28.81 6.954-42.823-6.647l-92.767-88.518c-14.015-13.6-17.565-32.675-7.929-42.605 9.636-9.93 28.81-6.955 42.824 6.646l92.766 88.518zm321.734-465.083c-25.144-17.055-47.741-1.763-57.477 3.805-29.097 19.485-237.243 221.77-327.69 315.194-11.105 14.8-18.59 26.294 34.663 79.546 44.95 44.95 65.896 42.012 88.66 22.603 37.906-37.906 199.299-262.926 258.92-348.713 9.792-14.092 29.851-54.17 2.924-72.435z"/></svg>`
|
||||
brushCursor.value = `url("data:image/svg+xml,${encodeURIComponent(svg)}") 5 27, crosshair`
|
||||
}
|
||||
|
||||
function hideBrushCursor() {
|
||||
brushCursor.value.visible = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.playing, props.selected] as const,
|
||||
([playing, selected]) => {
|
||||
if (!playing || selected < 0) hideBrushCursor()
|
||||
else if (brushCursor.value.visible && grid.value) {
|
||||
brushCursor.value.color = getComputedStyle(grid.value).color
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
watch(() => [props.playing, props.selected], updateBrushCursor, {
|
||||
flush: 'post',
|
||||
})
|
||||
|
||||
function label(index: number) {
|
||||
const values = {
|
||||
@ -135,6 +120,7 @@ function label(index: number) {
|
||||
|
||||
let observer: ResizeObserver | undefined
|
||||
onMounted(() => {
|
||||
updateBrushCursor()
|
||||
observer = new ResizeObserver(updateView)
|
||||
if (viewport.value) observer.observe(viewport.value)
|
||||
})
|
||||
@ -142,7 +128,10 @@ onScopeDispose(() => observer?.disconnect())
|
||||
defineExpose({
|
||||
centerFirstClue,
|
||||
setView,
|
||||
getView: () => ({ left: viewport.value?.scrollLeft ?? 0, top: viewport.value?.scrollTop ?? 0 }),
|
||||
getView: () => ({
|
||||
left: viewport.value?.scrollLeft ?? 0,
|
||||
top: viewport.value?.scrollTop ?? 0,
|
||||
}),
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -152,16 +141,18 @@ defineExpose({
|
||||
<div
|
||||
ref="grid"
|
||||
class="mine-grid"
|
||||
:class="{ 'mine-grid-small': !large, 'mine-grid-brush-active': selected >= 0 }"
|
||||
:class="{
|
||||
'mine-grid-small': !large,
|
||||
'mine-grid-brush-active': brushActive,
|
||||
}"
|
||||
:style="{
|
||||
'--mine-size': puzzle.size,
|
||||
'--mine-cell-size': `${40 * zoom}px`,
|
||||
'--mine-brush-cursor': brushCursor,
|
||||
color: selected >= 0 ? `var(--mine-color-${selected})` : undefined,
|
||||
}"
|
||||
:aria-label="formatMessage(messages.board, { size: puzzle.size })"
|
||||
@pointermove="moveBrushCursor"
|
||||
@pointerleave="hideBrushCursor"
|
||||
@pointercancel="hideBrushCursor"
|
||||
@pointerenter="updateBrushCursor"
|
||||
>
|
||||
<button
|
||||
v-for="(_, i) in puzzle.answer"
|
||||
@ -190,24 +181,6 @@ defineExpose({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<svg
|
||||
v-show="brushCursor.visible"
|
||||
class="mine-brush-cursor"
|
||||
:style="{
|
||||
left: `${brushCursor.x}px`,
|
||||
top: `${brushCursor.y}px`,
|
||||
color: brushCursor.color,
|
||||
}"
|
||||
viewBox="0 0 1024 1024"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M358.681 586.386s-90.968 49.4-94.488 126.827c-3.519 77.428-77.427 133.74-102.063 140.778s360.157 22.971 332.002-142.444l-135.45-125.16zm169.099 52.56c14.016 13.601 17.565 32.675 7.929 42.606-9.635 9.93-28.81 6.954-42.823-6.647l-92.767-88.518c-14.015-13.6-17.565-32.675-7.929-42.605 9.636-9.93 28.81-6.955 42.824 6.646l92.766 88.518zm321.734-465.083c-25.144-17.055-47.741-1.763-57.477 3.805-29.097 19.485-237.243 221.77-327.69 315.194-11.105 14.8-18.59 26.294 34.663 79.546 44.95 44.95 65.896 42.012 88.66 22.603 37.906-37.906 199.299-262.926 258.92-348.713 9.792-14.092 29.851-54.17 2.924-72.435z"
|
||||
/>
|
||||
</svg>
|
||||
</Teleport>
|
||||
<div v-if="large" class="mine-overview">
|
||||
<svg
|
||||
class="mine-map"
|
||||
@ -260,7 +233,10 @@ defineExpose({
|
||||
|
||||
<style scoped>
|
||||
.mine-navigation {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
display: grid;
|
||||
gap: var(--gap-md);
|
||||
}
|
||||
@ -268,6 +244,9 @@ defineExpose({
|
||||
grid-template-columns: minmax(0, 1fr) 7rem;
|
||||
}
|
||||
.mine-viewport {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
max-height: min(52vh, 32rem);
|
||||
min-width: 0;
|
||||
@ -308,17 +287,9 @@ defineExpose({
|
||||
user-select: none;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.mine-grid-brush-active,
|
||||
.mine-grid-brush-active .mine-cell:not(.mine-open):not(:disabled) {
|
||||
cursor: none;
|
||||
}
|
||||
.mine-brush-cursor {
|
||||
position: fixed;
|
||||
z-index: 2147483647;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
filter: drop-shadow(0 1px 1px rgb(0 0 0 / 45%));
|
||||
pointer-events: none;
|
||||
transform: translate(-16%, -84%);
|
||||
cursor: var(--mine-brush-cursor, crosshair);
|
||||
}
|
||||
.mine-cell:hover:not(:disabled) {
|
||||
background: var(--surface-5);
|
||||
@ -392,12 +363,4 @@ defineExpose({
|
||||
width: 4.5rem;
|
||||
}
|
||||
}
|
||||
@media (pointer: coarse) {
|
||||
.mine-grid-brush-active .mine-cell:not(.mine-open):not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.mine-brush-cursor {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -301,6 +301,7 @@ defineExpose({ show })
|
||||
:header="formatMessage(messages.title)"
|
||||
width="56rem"
|
||||
max-width="56rem"
|
||||
scrollable
|
||||
:closable="false"
|
||||
:close-on-esc="false"
|
||||
actions-divider
|
||||
@ -455,15 +456,21 @@ defineExpose({ show })
|
||||
justify-content: space-between;
|
||||
}
|
||||
.mine-frame {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
border: 2px solid var(--mine-ink);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--gap-md);
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.mine-status {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
margin: 0 0 var(--gap-md);
|
||||
color: var(--color-contrast);
|
||||
font-size: 0.875rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.mine-actions {
|
||||
justify-content: flex-end;
|
||||
|
||||
@ -18,6 +18,7 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import InstanceIcon from '@/components/ui/InstanceIcon.vue'
|
||||
import InstanceModeSettings from '@/components/instance/InstanceModeSettings.vue'
|
||||
import InstancePlayerSettings from '@/components/instance/InstancePlayerSettings.vue'
|
||||
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
||||
import { install_duplicate_instance } from '@/helpers/install'
|
||||
import { edit, edit_icon, get_full_path, remove } from '@/helpers/instance'
|
||||
@ -381,6 +382,7 @@ const messages = defineMessages({
|
||||
|
||||
<template>
|
||||
<InstanceModeSettings class="mb-6" :instance-id="instance.id" />
|
||||
<InstancePlayerSettings :instance-id="instance.id" />
|
||||
<ConfirmDeleteInstanceModal
|
||||
ref="deleteConfirmModal"
|
||||
:symlink-target="instance.symlink_target"
|
||||
|
||||
@ -242,7 +242,7 @@ export const onboardingMessages = defineMessages({
|
||||
instanceActionsDescription: {
|
||||
id: 'app.onboarding.instance-actions.description',
|
||||
defaultMessage:
|
||||
'Launch, stop, repair, configure, export, or open the instance from its header.',
|
||||
'Launch or configure this instance here. Choose a player on first launch; the instance remembers your choice until you switch it in settings.',
|
||||
},
|
||||
instanceTabsTitle: {
|
||||
id: 'app.onboarding.instance-tabs.title',
|
||||
|
||||
Reference in New Issue
Block a user