From 3005d9310deb67ee8fc963edc273deb47e87d2f8 Mon Sep 17 00:00:00 2001 From: th Date: Sat, 25 Apr 2026 09:02:52 +0200 Subject: [PATCH] 1.1.4: 2-column export + welcome rework with auto-load + archive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Export dialog goes wide (xl modal) and splits into two columns: Project & code on the left (.ruw, runtime.js, animation.js, example.html), Media on the right with the per-export size + fps inputs followed by MP4 / WebM / GIF / APNG / APNG preview. Skip- hidden toggle still spans the full width up top. Single-column fallback below 720px. - Welcome dialog reworked: - Always auto-loads the last edited project on boot. The welcome dialog opens on top only when the new "Show on start" preference is enabled, or unconditionally when there are no recents (first-run flow). - Two-column body: Recent (max 5 thumbnail tiles, was 8) on the left, Archive (compact list view, no thumbnails) on the right. - Footer toggle binds to the new useShowWelcomeOnStart shared ref so the user can opt out of the boot dialog inline. - Document settings → Interface tab grows a matching "Show welcome screen on start" toggle so users who turned it off can flip it back on without diving into localStorage. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- src/App.vue | 29 +- src/components/DocumentSettings.vue | 12 + src/components/ExportDialog.vue | 346 +++++++++++++---------- src/components/WelcomeDialog.vue | 295 ++++++++++++++++--- src/composables/useShowWelcomeOnStart.ts | 31 ++ src/devtools/tests.ts | 130 ++++++++- src/i18n/messages.ts | 11 + src/lib/projectStore.ts | 5 +- src/version.ts | 2 +- 10 files changed, 652 insertions(+), 211 deletions(-) create mode 100644 src/composables/useShowWelcomeOnStart.ts diff --git a/package.json b/package.json index 596ed7b..4a3eff2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ruwio", "private": true, - "version": "1.1.3", + "version": "1.1.4", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.vue b/src/App.vue index 5f0db0e..ed9613f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -45,11 +45,13 @@ import { ProjectFileError, pickProjectFile } from './lib/projectFile' import { readAnyProjectFile } from './lib/ruwFile' import { getLastOpenedId, + getStoredProject, listRecentProjects, saveProjectToStore, setLastOpenedId, type StoredProject, } from './lib/projectStore' +import { showWelcomeOnStart } from './composables/useShowWelcomeOnStart' import { importImageFile } from './lib/fileLoader' import { sampleLayer } from './lib/interpolate' import { useClipartStore } from './stores/clipart' @@ -270,18 +272,27 @@ function showSavedToast(msg: string) { }, 2500) } +/** Boot policy: always restore the last edited project so the user + * picks up where they left off, regardless of the show-welcome + * preference. The welcome dialog still opens on top when the user + * has the flag enabled — picking a different project there just + * re-loads it through openFromRecent. With NO recents present, + * the dialog opens unconditionally so first-run users see the + * Import / New options. */ async function bootWelcome() { - const recents = await listRecentProjects() - if (recents.length > 0) { - welcomeOpen.value = true - const lastId = getLastOpenedId() - if (lastId) { - const last = recents.find((p) => p.id === lastId) - if (last) { - currentProjectName.value = last.name - } + const lastId = getLastOpenedId() + if (lastId) { + const stored = await getStoredProject(lastId) + if (stored?.project) { + await openFromRecent(stored) } } + const recents = await listRecentProjects() + if (recents.length === 0) { + welcomeOpen.value = true + return + } + if (showWelcomeOnStart.value) welcomeOpen.value = true } function isEditable(el: EventTarget | null): boolean { diff --git a/src/components/DocumentSettings.vue b/src/components/DocumentSettings.vue index 6b0ecb1..6b1eba3 100644 --- a/src/components/DocumentSettings.vue +++ b/src/components/DocumentSettings.vue @@ -11,6 +11,7 @@ import { useTheme } from '@/composables/useTheme' import type { Theme } from '@/composables/useTheme' import { useTooltipPref } from '@/composables/useTooltipPref' import { useAssetThumbsPref } from '@/composables/useAssetThumbsPref' +import { useShowWelcomeOnStart } from '@/composables/useShowWelcomeOnStart' import { showLocked } from '@/composables/useShowLocked' import { Modal } from '@/components/ui' import { setLocale, t, useI18n } from '@/i18n' @@ -23,6 +24,7 @@ const clipart = useClipartStore() const { theme, setTheme } = useTheme() const { enabled: tooltipsEnabled } = useTooltipPref() const { enabled: thumbsEnabled } = useAssetThumbsPref() +const { enabled: welcomeOnStart } = useShowWelcomeOnStart() const { locale } = useI18n() // Re-check the stored clipart folder whenever settings opens — covers the @@ -175,6 +177,16 @@ function applyTheme(t: Theme) { + +
{{ t('settings.appearanceTitle') }}
{{ t('settings.themeHint') }}
diff --git a/src/components/ExportDialog.vue b/src/components/ExportDialog.vue index fffc157..3fd5f49 100644 --- a/src/components/ExportDialog.vue +++ b/src/components/ExportDialog.vue @@ -381,7 +381,7 @@ function onUpdate(v: boolean) { - +
+
+

{{ t('export.colProject') }}

- + - + - + - + +
- +
+

{{ t('export.colMedia') }}

-
-
{{ t('export.targetSize') }}
- - × -
- {{ targetHeight }} - H -
- +
+
{{ t('export.targetSize') }}
+ + × +
+ {{ targetHeight }} + H +
+ +
+ +
+
{{ t('export.targetFps') }}
+ + {{ t('export.targetFpsHint') }} + +
+ + + + + + + + + + +
- -
-
{{ t('export.targetFps') }}
- - {{ t('export.targetFpsHint') }} - -
- - - - - -
@@ -596,6 +606,42 @@ function onUpdate(v: boolean) { padding: 12px; } +/* Two-column layout. The skip-hidden toggle still spans the full + width above; project / data exports live left, media exports + (with the per-export size + fps inputs) live right. Each column + stacks vertically and gaps follow the same 6px rhythm as the + single-column flow used to. */ +.export-cols { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + align-items: start; +} + +.export-col { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.col-heading { + margin: 0 0 4px; + font-size: 11px; + font-weight: 500; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.06em; +} + +@media (max-width: 720px) { + /* Stage tight viewports: collapse to a single column so the + options stay readable instead of cramming. */ + .export-cols { + grid-template-columns: 1fr; + } +} + .export-toggle { display: flex; align-items: center; diff --git a/src/components/WelcomeDialog.vue b/src/components/WelcomeDialog.vue index 301351d..6a1a19c 100644 --- a/src/components/WelcomeDialog.vue +++ b/src/components/WelcomeDialog.vue @@ -22,6 +22,7 @@ import { readBackupFile, } from '@/lib/backupFile' import { confirmDialog, Modal } from '@/components/ui' +import { showWelcomeOnStart } from '@/composables/useShowWelcomeOnStart' import { t } from '@/i18n' const props = defineProps<{ modelValue: boolean }>() @@ -33,14 +34,25 @@ const emit = defineEmits<{ }>() const recents = ref([]) +// Archive = the rest of the IDB store beyond the recent cap. +// Rendered as a flat list (no thumbnails) so the user can find old +// projects without scrolling through tile previews. +const archive = ref([]) const loading = ref(false) const recentRefs = ref([]) +const archiveRefs = ref([]) const newBtnRef = ref(null) async function reload() { loading.value = true try { - recents.value = await listRecentProjects() + const [topRecent, all] = await Promise.all([ + listRecentProjects(), + listAllProjects(), + ]) + recents.value = topRecent + const recentIds = new Set(topRecent.map((p) => p.id)) + archive.value = all.filter((p) => !recentIds.has(p.id)) } finally { loading.value = false } @@ -261,7 +273,7 @@ watch(