[FEAT] Finish auth and project saving
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [2.3.0] - 2026-01-01
|
||||||
|
- Add authentication
|
||||||
|
- You can now save projects and open them
|
||||||
|
|
||||||
## [2.2.0] - 2026-01-01
|
## [2.2.0] - 2026-01-01
|
||||||
- Add move sprite with arrow keys in preview
|
- Add move sprite with arrow keys in preview
|
||||||
- Add replace sprite to context menu
|
- Add replace sprite to context menu
|
||||||
|
|||||||
@@ -16,12 +16,7 @@
|
|||||||
<div class="hidden md:flex items-center gap-5">
|
<div class="hidden md:flex items-center gap-5">
|
||||||
<!-- Auth & Projects -->
|
<!-- Auth & Projects -->
|
||||||
<template v-if="authStore.user">
|
<template v-if="authStore.user">
|
||||||
<NavbarProjectActions
|
<NavbarProjectActions @save-project="handleQuickSave" @open-save-modal="openSaveModal" @open-new-project-modal="isNewProjectModalOpen = true" @open-project-list="isProjectListOpen = true" />
|
||||||
@save-project="handleQuickSave"
|
|
||||||
@open-save-modal="openSaveModal"
|
|
||||||
@open-new-project-modal="isNewProjectModalOpen = true"
|
|
||||||
@open-project-list="isProjectListOpen = true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="h-4 w-px bg-gray-200 dark:bg-gray-700"></div>
|
<div class="h-4 w-px bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
|
||||||
@@ -49,15 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Menu -->
|
<!-- Mobile Menu -->
|
||||||
<NavbarMobileMenu
|
<NavbarMobileMenu :is-open="isMobileMenuOpen" @close="isMobileMenuOpen = false" @open-help="$emit('open-help')" @save-project="handleQuickSave" @open-save-modal="openSaveModal" @open-new-project-modal="isNewProjectModalOpen = true" @open-project-list="isProjectListOpen = true" />
|
||||||
:is-open="isMobileMenuOpen"
|
|
||||||
@close="isMobileMenuOpen = false"
|
|
||||||
@open-help="$emit('open-help')"
|
|
||||||
@save-project="handleQuickSave"
|
|
||||||
@open-save-modal="openSaveModal"
|
|
||||||
@open-new-project-modal="isNewProjectModalOpen = true"
|
|
||||||
@open-project-list="isProjectListOpen = true"
|
|
||||||
/>
|
|
||||||
</nav>
|
</nav>
|
||||||
<AuthModal :is-open="isAuthModalOpen" @close="isAuthModalOpen = false" />
|
<AuthModal :is-open="isAuthModalOpen" @close="isAuthModalOpen = false" />
|
||||||
<ProjectList :is-open="isProjectListOpen" @close="isProjectListOpen = false" @open-project="handleOpenProject" />
|
<ProjectList :is-open="isProjectListOpen" @close="isProjectListOpen = false" @open-project="handleOpenProject" />
|
||||||
@@ -108,7 +95,7 @@
|
|||||||
|
|
||||||
const saveModalInitialName = computed(() => {
|
const saveModalInitialName = computed(() => {
|
||||||
if (saveMode.value === 'save-as') {
|
if (saveMode.value === 'save-as') {
|
||||||
return (projectStore.currentProject?.name ? projectStore.currentProject.name + ' (copy)' : '');
|
return projectStore.currentProject?.name ? projectStore.currentProject.name + ' (copy)' : '';
|
||||||
}
|
}
|
||||||
return projectStore.currentProject?.name;
|
return projectStore.currentProject?.name;
|
||||||
});
|
});
|
||||||
@@ -133,7 +120,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleCreateNewProject = (config: { width: number; height: number; columns: number; rows: number }) => {
|
const handleCreateNewProject = (config: { width: number; height: number; columns: number; rows: number }) => {
|
||||||
createProject(config);
|
createProject(config);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,19 +19,45 @@
|
|||||||
<div class="border-t border-gray-200 dark:border-gray-800 my-2 pt-2 px-3">
|
<div class="border-t border-gray-200 dark:border-gray-800 my-2 pt-2 px-3">
|
||||||
<p class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Project</p>
|
<p class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Project</p>
|
||||||
|
|
||||||
<button v-if="isEditorActive" @click="$emit('save-project'); $emit('close')" class="w-full text-left flex items-center gap-3 px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">
|
<button
|
||||||
|
v-if="isEditorActive"
|
||||||
|
@click="
|
||||||
|
$emit('save-project');
|
||||||
|
$emit('close');
|
||||||
|
"
|
||||||
|
class="w-full text-left flex items-center gap-3 px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||||
|
>
|
||||||
<i class="fas fa-save w-5"></i> Save project
|
<i class="fas fa-save w-5"></i> Save project
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="isEditorActive" @click="$emit('open-save-modal', 'save-as'); $emit('close')" class="w-full text-left flex items-center gap-3 px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">
|
<button
|
||||||
|
v-if="isEditorActive"
|
||||||
|
@click="
|
||||||
|
$emit('open-save-modal', 'save-as');
|
||||||
|
$emit('close');
|
||||||
|
"
|
||||||
|
class="w-full text-left flex items-center gap-3 px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||||
|
>
|
||||||
<i class="fas fa-clone w-5"></i> Save as...
|
<i class="fas fa-clone w-5"></i> Save as...
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button @click="$emit('open-new-project-modal'); $emit('close')" class="w-full text-left flex items-center gap-3 px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">
|
<button
|
||||||
|
@click="
|
||||||
|
$emit('open-new-project-modal');
|
||||||
|
$emit('close');
|
||||||
|
"
|
||||||
|
class="w-full text-left flex items-center gap-3 px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||||
|
>
|
||||||
<i class="fas fa-plus w-5"></i> New project
|
<i class="fas fa-plus w-5"></i> New project
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button @click="$emit('open-project-list'); $emit('close')" class="w-full text-left flex items-center gap-3 px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">
|
<button
|
||||||
|
@click="
|
||||||
|
$emit('open-project-list');
|
||||||
|
$emit('close');
|
||||||
|
"
|
||||||
|
class="w-full text-left flex items-center gap-3 px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||||
|
>
|
||||||
<i class="fas fa-folder-open w-5"></i> My projects
|
<i class="fas fa-folder-open w-5"></i> My projects
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,4 +56,3 @@
|
|||||||
const projectStore = useProjectStore();
|
const projectStore = useProjectStore();
|
||||||
const { isEditorActive } = useProjectManager();
|
const { isEditorActive } = useProjectManager();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4" @click.self="close">
|
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4" @click.self="close">
|
||||||
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full max-w-3xl overflow-hidden border border-gray-200 dark:border-gray-800 flex flex-col max-h-[85vh] animate-fade-in-up">
|
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full max-w-4xl overflow-hidden border border-gray-200 dark:border-gray-800 flex flex-col h-[85vh] animate-fade-in-up">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="px-6 py-5 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between bg-gray-50/50 dark:bg-gray-800/20">
|
<div class="px-6 py-5 border-b border-gray-100 dark:border-gray-800 flex flex-col gap-4 bg-gray-50/50 dark:bg-gray-800/20">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100">My Projects</h2>
|
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100">My Projects</h2>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">Manage your saved sprite sheets</p>
|
<p class="text-sm text-gray-500 dark:text-gray-400">Manage your saved sprite sheets</p>
|
||||||
@@ -14,9 +14,20 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="relative">
|
||||||
|
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search projects..."
|
||||||
|
class="w-full pl-10 pr-4 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 dark:focus:border-indigo-500 outline-none transition-all text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1 overflow-y-auto custom-scrollbar p-6 bg-gray-50 dark:bg-black/20">
|
<div class="flex-1 overflow-y-auto custom-scrollbar p-6 bg-gray-50 dark:bg-black/20">
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<div v-if="loading" class="flex flex-col items-center justify-center py-12">
|
<div v-if="loading" class="flex flex-col items-center justify-center py-12">
|
||||||
<div class="w-10 h-10 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mb-4"></div>
|
<div class="w-10 h-10 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mb-4"></div>
|
||||||
@@ -24,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div v-else-if="projects.length === 0" class="flex flex-col items-center justify-center py-12 text-center">
|
<div v-else-if="projects.length === 0" class="flex flex-col items-center justify-center py-12 text-center h-full">
|
||||||
<div class="w-20 h-20 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
<div class="w-20 h-20 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
||||||
<i class="fas fa-folder-open text-3xl text-gray-400 dark:text-gray-500"></i>
|
<i class="fas fa-folder-open text-3xl text-gray-400 dark:text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,63 +43,80 @@
|
|||||||
<p class="text-gray-500 dark:text-gray-400 max-w-xs mb-6">Create your first project or load an example to get started with Sprite Sheet Generator.</p>
|
<p class="text-gray-500 dark:text-gray-400 max-w-xs mb-6">Create your first project or load an example to get started with Sprite Sheet Generator.</p>
|
||||||
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button @click="loadExample" class="btn btn-secondary flex items-center gap-2">
|
<button @click="loadExample" class="btn btn-secondary flex items-center gap-2"><i class="fas fa-lightbulb"></i> Load Example</button>
|
||||||
<i class="fas fa-lightbulb"></i> Load Example
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Project Grid -->
|
<!-- No Search Results -->
|
||||||
<div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div v-else-if="filteredProjects.length === 0" class="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<div class="w-16 h-16 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
|
||||||
|
<i class="fas fa-search text-2xl text-gray-400 dark:text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-md font-bold text-gray-900 dark:text-gray-100 mb-1">No matches found</h3>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-sm">No projects match "{{ searchQuery }}"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Project List -->
|
||||||
|
<div v-else class="flex flex-col gap-3">
|
||||||
<div
|
<div
|
||||||
v-for="project in projects"
|
v-for="project in filteredProjects"
|
||||||
:key="project.id"
|
:key="project.id"
|
||||||
class="group bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-indigo-500 dark:hover:border-indigo-500 hover:shadow-lg dark:hover:shadow-indigo-500/10 transition-all cursor-pointer overflow-hidden flex flex-col h-[180px]"
|
class="group bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-indigo-500 dark:hover:border-indigo-500 hover:shadow-md dark:hover:shadow-indigo-500/10 transition-all cursor-pointer overflow-hidden flex flex-col sm:flex-row h-auto sm:h-32"
|
||||||
@click="selectProject(project)"
|
@click="selectProject(project)"
|
||||||
>
|
>
|
||||||
<!-- Card Header -->
|
<!-- Left: Preview Stripe -->
|
||||||
<div class="p-4 flex-1">
|
<div class="w-full sm:w-48 bg-gray-100 dark:bg-black/30 flex items-center justify-center relative overflow-hidden border-b sm:border-b-0 sm:border-r border-gray-100 dark:border-gray-700/50 p-2">
|
||||||
<div class="flex items-start justify-between mb-2">
|
<div class="absolute inset-0 bg-checkerboard opacity-50"></div>
|
||||||
<div class="w-10 h-10 rounded-lg bg-indigo-50 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-500 dark:text-indigo-400 font-bold text-lg shrink-0">
|
|
||||||
|
<!-- Sprite Stack for Preview -->
|
||||||
|
<div v-if="getProjectSprites(project, 1).length > 0" class="relative z-10 flex -space-x-4">
|
||||||
|
<div
|
||||||
|
v-for="(sprite, idx) in getProjectSprites(project, 3)"
|
||||||
|
:key="idx"
|
||||||
|
class="w-16 h-16 rounded-lg bg-white dark:bg-gray-700 border-2 border-white dark:border-gray-600 shadow-lg flex items-center justify-center overflow-hidden transform transition-transform group-hover:scale-105 group-hover:first:-rotate-6 group-hover:last:rotate-6"
|
||||||
|
:style="{ zIndex: 10 - idx }"
|
||||||
|
>
|
||||||
|
<img :src="sprite.base64" class="w-full h-full object-contain" style="image-rendering: pixelated" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fallback Icon -->
|
||||||
|
<div v-else class="relative z-10 w-16 h-16 rounded-lg bg-white dark:bg-gray-700 border-2 border-white dark:border-gray-600 shadow-lg flex items-center justify-center text-indigo-300 dark:text-indigo-400/50 font-bold text-2xl">
|
||||||
{{ project.name.charAt(0).toUpperCase() }}
|
{{ project.name.charAt(0).toUpperCase() }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- content -->
|
||||||
|
<div class="flex-1 p-4 flex flex-col justify-center min-w-0">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-gray-900 dark:text-gray-100 truncate text-lg mb-1 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">{{ project.name }}</h3>
|
||||||
|
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<span class="flex items-center gap-1"><i class="fas fa-layer-group opacity-70"></i> {{ project.data?.layers?.length || 0 }} layers</span>
|
||||||
|
<span class="flex items-center gap-1"><i class="fas fa-border-all opacity-70"></i> {{ project.data?.columns }} columns</span>
|
||||||
|
<span class="flex items-center gap-1"><i class="fas fa-images opacity-70"></i> {{ getSpriteCount(project) }} sprites</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button @click.stop="deleteProject(project.id)" class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 p-2 rounded-lg transition-all" title="Delete Project">
|
<button @click.stop="deleteProject(project.id)" class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 p-2 rounded-lg transition-all" title="Delete Project">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="font-bold text-gray-900 dark:text-gray-100 truncate mb-1">{{ project.name }}</h3>
|
<div class="mt-auto pt-3 flex items-center justify-between">
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
|
<span class="text-xs text-gray-400"> Updated {{ formatDate(project.updated) }} </span>
|
||||||
{{ project.data?.layers?.length || 0 }} layers • {{ project.data?.columns }}x{{ project.data?.columns }} grid
|
<span class="text-xs font-bold text-indigo-600 dark:text-indigo-400 flex items-center gap-1 transform translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all"> Open Project <i class="fas fa-arrow-right"></i> </span>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Card Footer -->
|
|
||||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-800/50 border-t border-gray-100 dark:border-gray-700/50 flex items-center justify-between text-xs">
|
|
||||||
<span class="text-gray-500 dark:text-gray-400 font-medium group-hover:text-indigo-500 transition-colors">
|
|
||||||
{{ formatDate(project.updated) }}
|
|
||||||
</span>
|
|
||||||
<span class="text-indigo-600 dark:text-indigo-400 font-bold opacity-0 group-hover:opacity-100 transform translate-x-2 group-hover:translate-x-0 transition-all">
|
|
||||||
Open <i class="fas fa-arrow-right ml-1"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Load Example Card (Visible even if lists are present, as a small helper at the end?) -->
|
|
||||||
<!-- OR keep it separate. Let's keep it simple for now and rely on empty state for the big button,
|
|
||||||
but maybe add a small text link at the bottom or top? -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="px-6 py-4 border-t border-gray-100 dark:border-gray-800 flex justify-between items-center bg-white dark:bg-gray-900">
|
<div class="px-6 py-4 border-t border-gray-100 dark:border-gray-800 flex justify-between items-center bg-white dark:bg-gray-900">
|
||||||
<p class="text-xs text-gray-400" v-if="projects.length > 0">
|
<p class="text-xs text-gray-400" v-if="projects.length > 0">Showing {{ filteredProjects.length }} of {{ projects.length }} project{{ projects.length !== 1 ? 's' : '' }}</p>
|
||||||
Showing {{ projects.length }} project{{ projects.length !== 1 ? 's' : '' }}
|
|
||||||
</p>
|
|
||||||
<div class="flex gap-2 ml-auto">
|
<div class="flex gap-2 ml-auto">
|
||||||
<button v-if="projects.length > 0" @click="loadExample" class="text-xs font-medium text-gray-500 hover:text-indigo-600 dark:hover:text-indigo-400 px-3 py-2">
|
<button v-if="projects.length > 0" @click="loadExample" class="text-xs font-medium text-gray-500 hover:text-indigo-600 dark:hover:text-indigo-400 px-3 py-2">Load Example</button>
|
||||||
Load Example
|
|
||||||
</button>
|
|
||||||
<button @click="close" class="btn btn-secondary text-sm">Close</button>
|
<button @click="close" class="btn btn-secondary text-sm">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,7 +126,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, toRefs, watch } from 'vue';
|
import { onMounted, toRefs, watch, ref, computed } from 'vue';
|
||||||
import { useProjectStore, type Project } from '@/stores/useProjectStore';
|
import { useProjectStore, type Project } from '@/stores/useProjectStore';
|
||||||
import { useProjectManager } from '@/composables/useProjectManager';
|
import { useProjectManager } from '@/composables/useProjectManager';
|
||||||
|
|
||||||
@@ -115,11 +143,20 @@
|
|||||||
const { projects, isLoading: loading } = toRefs(projectStore);
|
const { projects, isLoading: loading } = toRefs(projectStore);
|
||||||
const { createProject, loadProjectData } = useProjectManager();
|
const { createProject, loadProjectData } = useProjectManager();
|
||||||
|
|
||||||
|
const searchQuery = ref('');
|
||||||
|
|
||||||
|
const filteredProjects = computed(() => {
|
||||||
|
if (!searchQuery.value) return projects.value;
|
||||||
|
const query = searchQuery.value.toLowerCase();
|
||||||
|
return projects.value.filter(p => p.name.toLowerCase().includes(query));
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.isOpen,
|
() => props.isOpen,
|
||||||
isOpen => {
|
isOpen => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
projectStore.fetchProjects();
|
projectStore.fetchProjects();
|
||||||
|
searchQuery.value = ''; // Reset search on open
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -145,38 +182,57 @@
|
|||||||
return new Date(dateString).toLocaleDateString(undefined, {
|
return new Date(dateString).toLocaleDateString(undefined, {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric'
|
year: 'numeric',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadExample = async () => {
|
const loadExample = async () => {
|
||||||
// 1. Create a clean base project (this sets the grid size)
|
|
||||||
createProject({ width: 32, height: 32, columns: 8, rows: 8 });
|
createProject({ width: 32, height: 32, columns: 8, rows: 8 });
|
||||||
|
|
||||||
// 2. Load some dummy data if we want more than just an empty grid
|
|
||||||
// For now, let's just make sure it creates a fresh state and closes the modal
|
|
||||||
// Ideally we would inject some sprites here but since we don't have easy access to sprite IDs without looking them up
|
|
||||||
// We will just init an empty 8x8 project called "Example Project" if helpful,
|
|
||||||
// or strictly Unsaved (createProject does that).
|
|
||||||
|
|
||||||
// If we want to populate it:
|
|
||||||
/*
|
|
||||||
const exampleData = {
|
|
||||||
layers: [
|
|
||||||
{ id: 'l1', name: 'Body', visible: true, sprites: [] }, // etc
|
|
||||||
],
|
|
||||||
// ...
|
|
||||||
};
|
|
||||||
await loadProjectData(exampleData);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// For now, an empty 8x8 grid is a good "Example" starting point compared to nothing.
|
|
||||||
|
|
||||||
close();
|
close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProjectSprites = (project: Project, limit: number) => {
|
||||||
|
const sprites: any[] = [];
|
||||||
|
if (!project.data || !project.data.layers) return sprites;
|
||||||
|
|
||||||
|
// Iterate through layers to find sprites
|
||||||
|
for (const layer of project.data.layers as any[]) {
|
||||||
|
if (layer.sprites && layer.sprites.length > 0) {
|
||||||
|
for (const sprite of layer.sprites) {
|
||||||
|
if (sprites.length < limit) {
|
||||||
|
sprites.push(sprite);
|
||||||
|
} else {
|
||||||
|
return sprites;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sprites;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSpriteCount = (project: Project) => {
|
||||||
|
if (!project.data || !project.data.layers) return 0;
|
||||||
|
return (project.data.layers as any[]).reduce((acc: number, layer: any) => acc + (layer.sprites?.length || 0), 0);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.bg-checkerboard {
|
||||||
|
background-color: #fff;
|
||||||
|
background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%), linear-gradient(-45deg, #e5e5e5 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e5e5e5 75%), linear-gradient(-45deg, transparent 75%, #e5e5e5 75%);
|
||||||
|
background-size: 8px 8px;
|
||||||
|
background-position:
|
||||||
|
0 0,
|
||||||
|
0 4px,
|
||||||
|
4px -4px,
|
||||||
|
-4px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bg-checkerboard {
|
||||||
|
background-color: #374151;
|
||||||
|
background-image: linear-gradient(45deg, #4b5563 25%, transparent 25%), linear-gradient(-45deg, #4b5563 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #4b5563 75%), linear-gradient(-45deg, transparent 75%, #4b5563 75%);
|
||||||
|
}
|
||||||
|
|
||||||
.animate-fade-in-up {
|
.animate-fade-in-up {
|
||||||
animation: fadeInUp 0.3s ease-out;
|
animation: fadeInUp 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,22 @@ export const useProjectManager = () => {
|
|||||||
isEditorActive.value = false;
|
isEditorActive.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeProject = () => {
|
||||||
|
// Reset Layers
|
||||||
|
const newLayer = createEmptyLayer('Base');
|
||||||
|
layers.value = [newLayer];
|
||||||
|
activeLayerId.value = newLayer.id;
|
||||||
|
|
||||||
|
// Reset columns
|
||||||
|
columns.value = 4;
|
||||||
|
|
||||||
|
// Reset Project Store
|
||||||
|
projectStore.currentProject = null;
|
||||||
|
|
||||||
|
// Close Editor
|
||||||
|
isEditorActive.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isEditorActive,
|
isEditorActive,
|
||||||
createProject,
|
createProject,
|
||||||
@@ -106,6 +122,7 @@ export const useProjectManager = () => {
|
|||||||
saveProject,
|
saveProject,
|
||||||
saveAsProject,
|
saveAsProject,
|
||||||
closeEditor,
|
closeEditor,
|
||||||
|
closeProject,
|
||||||
loadProjectData,
|
loadProjectData,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
...currentProject.value!,
|
...currentProject.value!,
|
||||||
name: record.name,
|
name: record.name,
|
||||||
data: record.data,
|
data: record.data,
|
||||||
updated: record.updated
|
updated: record.updated,
|
||||||
};
|
};
|
||||||
await fetchProjects();
|
await fetchProjects();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -128,8 +128,13 @@
|
|||||||
<!-- Sidebar Header -->
|
<!-- Sidebar Header -->
|
||||||
<div class="px-5 py-4 border-b border-gray-200/50 dark:border-gray-700/50 flex items-center justify-between shrink-0 bg-gray-50/50 dark:bg-gray-800/10">
|
<div class="px-5 py-4 border-b border-gray-200/50 dark:border-gray-700/50 flex items-center justify-between shrink-0 bg-gray-50/50 dark:bg-gray-800/10">
|
||||||
<h2 class="text-sm font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">Editor Tools</h2>
|
<h2 class="text-sm font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">Editor Tools</h2>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button @click="closeProject" class="btn btn-secondary btn-sm w-8 h-8 p-0 justify-center border-gray-200 dark:border-gray-700 hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-900/20 dark:hover:text-red-400 dark:text-gray-400" title="Close Project">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
<button @click="openFileDialog" class="btn btn-primary btn-sm shadow-indigo-500/20" title="Add more sprites"><i class="fas fa-plus mr-1"></i> Add</button>
|
<button @click="openFileDialog" class="btn btn-primary btn-sm shadow-indigo-500/20" title="Add more sprites"><i class="fas fa-plus mr-1"></i> Add</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Scrollable Content -->
|
<!-- Scrollable Content -->
|
||||||
<div class="flex-1 overflow-y-auto overflow-x-hidden p-5 space-y-6 scrollbar-thin">
|
<div class="flex-1 overflow-y-auto overflow-x-hidden p-5 space-y-6 scrollbar-thin">
|
||||||
@@ -317,14 +322,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sidebar Footer (Export) -->
|
<!-- Sidebar Footer (Export) -->
|
||||||
<div class="p-4 border-t border-gray-200/50 dark:border-gray-700/50 bg-gray-50/50 dark:bg-gray-800/10 space-y-3 shrink-0">
|
<div class="p-4 border-t border-gray-200/50 dark:border-gray-700/50 bg-gray-50/50 dark:bg-gray-800/10 space-y-2 shrink-0">
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<button @click="downloadSpritesheet" class="btn btn-secondary btn-sm justify-start" title="Download PNG"><i class="fas fa-image text-indigo-500 w-4"></i> PNG</button>
|
<button @click="downloadSpritesheet" class="btn btn-secondary btn-sm justify-start" title="Download PNG"><i class="fas fa-image text-indigo-500 w-4"></i> PNG</button>
|
||||||
<button @click="exportSpritesheetJSON" class="btn btn-secondary btn-sm justify-start" title="Download JSON"><i class="fas fa-code text-indigo-500 w-4"></i> JSON</button>
|
<button @click="exportSpritesheetJSON" class="btn btn-secondary btn-sm justify-start" title="Download JSON"><i class="fas fa-code text-indigo-500 w-4"></i> JSON</button>
|
||||||
<button @click="openGifFpsModal" class="btn btn-secondary btn-sm justify-start" title="Export GIF"><i class="fas fa-film text-pink-500 w-4"></i> GIF</button>
|
<button @click="openGifFpsModal" class="btn btn-secondary btn-sm justify-start" title="Export GIF"><i class="fas fa-film text-pink-500 w-4"></i> GIF</button>
|
||||||
<button @click="downloadAsZip" class="btn btn-secondary btn-sm justify-start" title="Download ZIP"><i class="fas fa-file-archive text-yellow-500 w-4"></i> ZIP</button>
|
<button @click="downloadAsZip" class="btn btn-secondary btn-sm justify-start" title="Download ZIP"><i class="fas fa-file-archive text-yellow-500 w-4"></i> ZIP</button>
|
||||||
</div>
|
</div>
|
||||||
<button @click="openShareModal" class="btn btn-secondary w-full justify-start"><i class="fas fa-share-alt mr-2 text-indigo-500"></i> Share Project</button>
|
<button @click="openShareModal" class="btn btn-secondary w-full justify-start"><i class="fas fa-share-alt mr-2 text-indigo-500"></i> Share project</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -462,7 +467,7 @@
|
|||||||
|
|
||||||
useHomeViewSEO();
|
useHomeViewSEO();
|
||||||
|
|
||||||
const { isEditorActive } = useProjectManager();
|
const { isEditorActive, closeProject } = useProjectManager();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const { layers, visibleLayers, activeLayer, activeLayerId, columns, updateSpritePosition, updateSpriteInLayer, updateSpriteCell, removeSprite, removeSprites, replaceSprite, addSprite, processImageFiles, alignSprites, addLayer, removeLayer, moveLayer, rotateSprite, flipSprite, copySpriteToFrame } =
|
const { layers, visibleLayers, activeLayer, activeLayerId, columns, updateSpritePosition, updateSpriteInLayer, updateSpriteCell, removeSprite, removeSprites, replaceSprite, addSprite, processImageFiles, alignSprites, addLayer, removeLayer, moveLayer, rotateSprite, flipSprite, copySpriteToFrame } =
|
||||||
useLayers();
|
useLayers();
|
||||||
|
|||||||
Reference in New Issue
Block a user