Files
RUWIO/src/components/WelcomeDialog.vue
T
2026-04-19 22:32:06 +02:00

307 lines
6.3 KiB
Vue

<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import {
PhDownloadSimple,
PhFileArrowUp,
PhFolderOpen,
PhPlus,
PhTrash,
} from '@phosphor-icons/vue'
import {
deleteStoredProject,
listRecentProjects,
type StoredProject,
} from '@/lib/projectStore'
import { confirmDialog, Modal } from '@/components/ui'
import { t } from '@/i18n'
const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
'open': [project: StoredProject]
'new': []
'import': []
'export': []
}>()
const recents = ref<StoredProject[]>([])
const loading = ref(false)
async function reload() {
loading.value = true
try {
recents.value = await listRecentProjects()
} finally {
loading.value = false
}
}
function formatDate(ts: number): string {
const d = new Date(ts)
const now = new Date()
const sameDay =
d.getFullYear() === now.getFullYear() &&
d.getMonth() === now.getMonth() &&
d.getDate() === now.getDate()
if (sameDay) {
return t('welcome.today', {
time: d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
})
}
return d.toLocaleString([], {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
function layerCount(p: StoredProject): number {
return p.scene?.layers?.length ?? 0
}
function openProject(p: StoredProject) {
emit('open', p)
emit('update:modelValue', false)
}
async function removeProject(e: MouseEvent, p: StoredProject) {
e.stopPropagation()
const ok = await confirmDialog({
title: t('welcome.deleteDialog.title'),
message: t('welcome.deleteDialog.message', { name: p.name }),
confirmLabel: t('welcome.deleteDialog.confirm'),
variant: 'danger',
})
if (!ok) return
await deleteStoredProject(p.id)
await reload()
}
function startFresh() {
emit('new')
emit('update:modelValue', false)
}
function importFile() {
emit('import')
emit('update:modelValue', false)
}
function exportFile() {
emit('export')
}
function close() {
emit('update:modelValue', false)
}
onMounted(() => {
if (props.modelValue) reload()
})
watch(
() => props.modelValue,
(v) => {
if (v) reload()
},
)
</script>
<template>
<Modal
:model-value="modelValue"
size="md"
@update:model-value="emit('update:modelValue', $event)"
>
<template #header>
<div class="hdr-text">
<h2>{{ t('welcome.heading') }}</h2>
<p class="sub">{{ t('welcome.subheading') }}</p>
</div>
</template>
<div class="body">
<div v-if="loading" class="empty">{{ t('welcome.loading') }}</div>
<div v-else-if="!recents.length" class="empty">
{{ t('welcome.empty') }}
</div>
<ul v-else class="recents">
<li
v-for="p in recents"
:key="p.id"
class="recent"
@click="openProject(p)"
>
<div class="thumb">
<img v-if="p.thumbnail" :src="p.thumbnail" alt="" />
<PhFolderOpen v-else :size="18" />
</div>
<div class="meta">
<div class="name">{{ p.name }}</div>
<div class="sub-info">
{{ t('welcome.layerCount', { n: layerCount(p) }) }} · {{ formatDate(p.savedAt) }}
</div>
</div>
<button
class="del"
:title="t('welcome.deleteTooltip')"
@click="removeProject($event, p)"
>
<PhTrash :size="13" />
</button>
</li>
</ul>
</div>
<template #footer>
<button class="ftr-btn" @click="startFresh">
<PhPlus :size="13" weight="bold" />
{{ t('welcome.newProject') }}
</button>
<button class="ftr-btn" @click="importFile">
<PhFileArrowUp :size="13" />
{{ t('welcome.importFile') }}
</button>
<button class="ftr-btn" @click="exportFile">
<PhDownloadSimple :size="13" />
{{ t('welcome.exportCurrent') }}
</button>
</template>
</Modal>
</template>
<style scoped>
.hdr-text h2 {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.sub {
margin: 2px 0 0;
font-size: 11px;
color: var(--text-dim);
}
.body {
padding: 8px 12px;
overflow: auto;
flex: 1;
}
.empty {
padding: 26px 12px;
color: var(--text-dim);
font-size: 12px;
text-align: center;
font-style: italic;
}
.recents {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.recent {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: var(--panel-alt);
border: 1px solid var(--border);
border-radius: var(--radius);
cursor: pointer;
transition: background 140ms var(--ease), border-color 140ms var(--ease);
}
.recent:hover {
background: var(--panel-hover);
border-color: var(--border-strong);
}
.thumb {
width: 44px;
height: 44px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg);
border-radius: var(--radius-sm);
color: var(--accent);
overflow: hidden;
}
.thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.meta {
flex: 1;
min-width: 0;
}
.name {
font-size: 13px;
font-weight: 500;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sub-info {
font-size: 11px;
color: var(--text-dim);
margin-top: 2px;
}
.del {
width: 26px;
height: 26px;
padding: 0;
background: transparent;
border: 1px solid transparent;
color: var(--text-dim);
border-radius: var(--radius-sm);
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 140ms var(--ease);
}
.recent:hover .del {
opacity: 1;
}
.del:hover {
background: var(--danger-soft);
color: var(--danger);
border-color: var(--danger);
}
.ftr-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 12px;
background: var(--panel);
border: 1px solid var(--border);
color: var(--text);
border-radius: var(--radius-sm);
font-size: 12px;
}
.ftr-btn:hover {
background: var(--panel-hover);
border-color: var(--border-strong);
}
</style>