[FEAT] Format code
This commit is contained in:
@@ -227,7 +227,7 @@
|
|||||||
(e: 'removeSprite', id: string): void;
|
(e: 'removeSprite', id: string): void;
|
||||||
(e: 'removeSprites', ids: string[]): void;
|
(e: 'removeSprites', ids: string[]): void;
|
||||||
(e: 'replaceSprite', id: string, file: File): void;
|
(e: 'replaceSprite', id: string, file: File): void;
|
||||||
(e: 'addSprite', file: File): void;
|
(e: 'addSprite', file: File, index?: number): void;
|
||||||
(e: 'addSpriteWithResize', file: File): void;
|
(e: 'addSpriteWithResize', file: File): void;
|
||||||
(e: 'rotateSprite', id: string, angle: number): void;
|
(e: 'rotateSprite', id: string, angle: number): void;
|
||||||
(e: 'flipSprite', id: string, direction: 'horizontal' | 'vertical'): void;
|
(e: 'flipSprite', id: string, direction: 'horizontal' | 'vertical'): void;
|
||||||
@@ -263,6 +263,7 @@
|
|||||||
handleTouchStart,
|
handleTouchStart,
|
||||||
handleTouchMove,
|
handleTouchMove,
|
||||||
findSpriteAtPosition,
|
findSpriteAtPosition,
|
||||||
|
findCellAtPosition,
|
||||||
calculateMaxDimensions,
|
calculateMaxDimensions,
|
||||||
} = useDragSprite({
|
} = useDragSprite({
|
||||||
sprites: computed(() => props.layers.find(l => l.id === props.activeLayerId)?.sprites ?? []),
|
sprites: computed(() => props.layers.find(l => l.id === props.activeLayerId)?.sprites ?? []),
|
||||||
@@ -293,6 +294,7 @@
|
|||||||
const showContextMenu = ref(false);
|
const showContextMenu = ref(false);
|
||||||
const contextMenuX = ref(0);
|
const contextMenuX = ref(0);
|
||||||
const contextMenuY = ref(0);
|
const contextMenuY = ref(0);
|
||||||
|
const contextMenuIndex = ref<number | null>(null);
|
||||||
const contextMenuSpriteId = ref<string | null>(null);
|
const contextMenuSpriteId = ref<string | null>(null);
|
||||||
const selectedSpriteIds = ref<Set<string>>(new Set());
|
const selectedSpriteIds = ref<Set<string>>(new Set());
|
||||||
const replacingSpriteId = ref<string | null>(null);
|
const replacingSpriteId = ref<string | null>(null);
|
||||||
@@ -383,6 +385,7 @@
|
|||||||
if (!pos) return;
|
if (!pos) return;
|
||||||
|
|
||||||
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
|
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
|
||||||
|
contextMenuIndex.value = findCellAtPosition(pos.x, pos.y)?.index ?? null;
|
||||||
contextMenuSpriteId.value = clickedSprite?.id || null;
|
contextMenuSpriteId.value = clickedSprite?.id || null;
|
||||||
|
|
||||||
if (clickedSprite) {
|
if (clickedSprite) {
|
||||||
@@ -529,13 +532,18 @@
|
|||||||
if (replacingSpriteId.value) {
|
if (replacingSpriteId.value) {
|
||||||
emit('replaceSprite', replacingSpriteId.value, file);
|
emit('replaceSprite', replacingSpriteId.value, file);
|
||||||
} else {
|
} else {
|
||||||
emit('addSprite', file);
|
if (contextMenuIndex.value !== null) {
|
||||||
|
emit('addSprite', file, contextMenuIndex.value);
|
||||||
|
} else {
|
||||||
|
emit('addSprite', file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('Please select an image file.');
|
alert('Please select an image file.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
replacingSpriteId.value = null;
|
replacingSpriteId.value = null;
|
||||||
|
contextMenuIndex.value = null;
|
||||||
input.value = '';
|
input.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,131 +1,107 @@
|
|||||||
<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/50 backdrop-blur-sm" @click.self="close">
|
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" @click.self="close">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-md p-6 relative border border-gray-200 dark:border-gray-700">
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-md p-6 relative border border-gray-200 dark:border-gray-700">
|
||||||
<button @click="close" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
|
<button @click="close" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="text-center mb-6">
|
|
||||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ isLogin ? 'Welcome Back' : 'Create Account' }}</h2>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{{ isLogin ? 'Sign in to access your projects' : 'Join to save and manage your spritesheets' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
|
|
||||||
<input
|
|
||||||
v-model="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
placeholder="you@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
|
|
||||||
<input
|
|
||||||
v-model="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
minlength="8"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!isLogin">
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Confirm Password</label>
|
|
||||||
<input
|
|
||||||
v-model="passwordConfirm"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
minlength="8"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="error" class="text-red-500 text-sm text-center bg-red-50 dark:bg-red-900/20 p-2 rounded">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
:disabled="loading"
|
|
||||||
class="w-full btn btn-primary py-2.5 flex justify-center items-center"
|
|
||||||
>
|
|
||||||
<i v-if="loading" class="fas fa-spinner fa-spin mr-2"></i>
|
|
||||||
{{ isLogin ? 'Sign In' : 'Create Account' }}
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="mt-6 text-center text-sm text-gray-600 dark:text-gray-400">
|
<div class="text-center mb-6">
|
||||||
{{ isLogin ? "Don't have an account?" : 'Already have an account?' }}
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ isLogin ? 'Welcome Back' : 'Create Account' }}</h2>
|
||||||
<button @click="toggleMode" class="text-indigo-600 dark:text-indigo-400 font-medium hover:underline">
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{ isLogin ? 'Sign up' : 'Sign in' }}
|
{{ isLogin ? 'Sign in to access your projects' : 'Join to save and manage your spritesheets' }}
|
||||||
</button>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
|
||||||
|
<input v-model="email" type="email" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" placeholder="you@example.com" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
|
||||||
|
<input v-model="password" type="password" required minlength="8" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" placeholder="••••••••" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isLogin">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Confirm Password</label>
|
||||||
|
<input v-model="passwordConfirm" type="password" required minlength="8" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" placeholder="••••••••" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="text-red-500 text-sm text-center bg-red-50 dark:bg-red-900/20 p-2 rounded">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" :disabled="loading" class="w-full btn btn-primary py-2.5 flex justify-center items-center">
|
||||||
|
<i v-if="loading" class="fas fa-spinner fa-spin mr-2"></i>
|
||||||
|
{{ isLogin ? 'Sign In' : 'Create Account' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-6 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ isLogin ? "Don't have an account?" : 'Already have an account?' }}
|
||||||
|
<button @click="toggleMode" class="text-indigo-600 dark:text-indigo-400 font-medium hover:underline">
|
||||||
|
{{ isLogin ? 'Sign up' : 'Sign in' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useAuthStore } from '@/stores/useAuthStore';
|
import { useAuthStore } from '@/stores/useAuthStore';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: 'close'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const isLogin = ref(true);
|
const isLogin = ref(true);
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const passwordConfirm = ref('');
|
const passwordConfirm = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
emit('close');
|
emit('close');
|
||||||
error.value = '';
|
error.value = '';
|
||||||
email.value = '';
|
email.value = '';
|
||||||
password.value = '';
|
password.value = '';
|
||||||
passwordConfirm.value = '';
|
passwordConfirm.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleMode = () => {
|
const toggleMode = () => {
|
||||||
isLogin.value = !isLogin.value;
|
isLogin.value = !isLogin.value;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
if (isLogin.value) {
|
if (isLogin.value) {
|
||||||
await authStore.login(email.value, password.value);
|
await authStore.login(email.value, password.value);
|
||||||
} else {
|
} else {
|
||||||
if (password.value !== passwordConfirm.value) {
|
if (password.value !== passwordConfirm.value) {
|
||||||
throw new Error("Passwords don't match");
|
throw new Error("Passwords don't match");
|
||||||
|
}
|
||||||
|
await authStore.register(email.value, password.value, passwordConfirm.value);
|
||||||
}
|
}
|
||||||
await authStore.register(email.value, password.value, passwordConfirm.value);
|
close();
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.message || 'An error occurred';
|
||||||
|
// Better PB error handling
|
||||||
|
if (e?.data?.message) error.value = e.data.message;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
close();
|
};
|
||||||
} catch (e: any) {
|
|
||||||
error.value = e.message || 'An error occurred';
|
|
||||||
// Better PB error handling
|
|
||||||
if(e?.data?.message) error.value = e.data.message;
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,22 +16,16 @@
|
|||||||
<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 @open-save-modal="openSaveModal" @open-new-project-modal="isNewProjectModalOpen = true" @open-project-list="isProjectListOpen = true" />
|
||||||
@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>
|
||||||
|
|
||||||
<!-- User Dropdown -->
|
<!-- User Dropdown -->
|
||||||
<NavbarUserMenu @open-auth-modal="isAuthModalOpen = true" />
|
<NavbarUserMenu @open-auth-modal="isAuthModalOpen = true" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<button @click="isAuthModalOpen = true" class="btn btn-primary btn-sm shadow-indigo-500/20 shadow-lg">
|
<button @click="isAuthModalOpen = true" class="btn btn-primary btn-sm shadow-indigo-500/20 shadow-lg">Login / Register</button>
|
||||||
Login / Register
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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>
|
||||||
@@ -50,36 +44,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Menu -->
|
<!-- Mobile Menu -->
|
||||||
<NavbarMobileMenu
|
<NavbarMobileMenu :is-open="isMobileMenuOpen" @close="isMobileMenuOpen = false" @open-help="$emit('open-help')" />
|
||||||
:is-open="isMobileMenuOpen"
|
|
||||||
@close="isMobileMenuOpen = false"
|
|
||||||
@open-help="$emit('open-help')"
|
|
||||||
/>
|
|
||||||
</nav>
|
</nav>
|
||||||
<AuthModal
|
<AuthModal :is-open="isAuthModalOpen" @close="isAuthModalOpen = false" />
|
||||||
:is-open="isAuthModalOpen"
|
<ProjectList :is-open="isProjectListOpen" @close="isProjectListOpen = false" @open-project="handleOpenProject" />
|
||||||
@close="isAuthModalOpen = false"
|
<SaveProjectModal :is-open="isSaveProjectModalOpen" :initial-name="projectStore.currentProject?.name" @close="isSaveProjectModalOpen = false" @save="handleSaveProject" />
|
||||||
/>
|
<NewProjectModal :is-open="isNewProjectModalOpen" @close="isNewProjectModalOpen = false" @create="handleCreateNewProject" />
|
||||||
<ProjectList
|
|
||||||
:is-open="isProjectListOpen"
|
|
||||||
@close="isProjectListOpen = false"
|
|
||||||
@open-project="handleOpenProject"
|
|
||||||
/>
|
|
||||||
<SaveProjectModal
|
|
||||||
:is-open="isSaveProjectModalOpen"
|
|
||||||
:initial-name="projectStore.currentProject?.name"
|
|
||||||
@close="isSaveProjectModalOpen = false"
|
|
||||||
@save="handleSaveProject"
|
|
||||||
/>
|
|
||||||
<NewProjectModal
|
|
||||||
:is-open="isNewProjectModalOpen"
|
|
||||||
@close="isNewProjectModalOpen = false"
|
|
||||||
@create="handleCreateNewProject"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, toRef } from 'vue';
|
import { ref } from 'vue';
|
||||||
import DarkModeToggle from '../utilities/DarkModeToggle.vue';
|
import DarkModeToggle from '../utilities/DarkModeToggle.vue';
|
||||||
import { useAuthStore } from '@/stores/useAuthStore';
|
import { useAuthStore } from '@/stores/useAuthStore';
|
||||||
import AuthModal from '@/components/auth/AuthModal.vue';
|
import AuthModal from '@/components/auth/AuthModal.vue';
|
||||||
@@ -87,9 +61,7 @@
|
|||||||
import SaveProjectModal from '@/components/project/SaveProjectModal.vue';
|
import SaveProjectModal from '@/components/project/SaveProjectModal.vue';
|
||||||
import NewProjectModal from '@/components/project/NewProjectModal.vue';
|
import NewProjectModal from '@/components/project/NewProjectModal.vue';
|
||||||
import { useProjectStore, type Project } from '@/stores/useProjectStore';
|
import { useProjectStore, type Project } from '@/stores/useProjectStore';
|
||||||
import { useSettingsStore } from '@/stores/useSettingsStore';
|
import { useProjectManager } from '@/composables/useProjectManager';
|
||||||
import { useExportLayers } from '@/composables/useExportLayers';
|
|
||||||
import { useLayers, createEmptyLayer } from '@/composables/useLayers';
|
|
||||||
|
|
||||||
// Sub-components
|
// Sub-components
|
||||||
import NavbarLogo from './navbar/NavbarLogo.vue';
|
import NavbarLogo from './navbar/NavbarLogo.vue';
|
||||||
@@ -109,66 +81,25 @@
|
|||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const projectStore = useProjectStore();
|
const projectStore = useProjectStore();
|
||||||
const settingsStore = useSettingsStore();
|
const { createProject, openProject, saveProject } = useProjectManager();
|
||||||
const { layers, columns, activeLayerId } = useLayers();
|
|
||||||
|
|
||||||
const { generateProjectJSON, loadProjectData } = useExportLayers(
|
|
||||||
layers,
|
|
||||||
columns,
|
|
||||||
toRef(settingsStore, 'negativeSpacingEnabled'),
|
|
||||||
activeLayerId,
|
|
||||||
toRef(settingsStore, 'backgroundColor'),
|
|
||||||
toRef(settingsStore, 'manualCellSizeEnabled'),
|
|
||||||
toRef(settingsStore, 'manualCellWidth'),
|
|
||||||
toRef(settingsStore, 'manualCellHeight')
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOpenProject = async (project: Project) => {
|
const handleOpenProject = async (project: Project) => {
|
||||||
try {
|
await openProject(project);
|
||||||
if (project.data) {
|
|
||||||
await loadProjectData(project.data);
|
|
||||||
}
|
|
||||||
projectStore.currentProject = project;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to open project", e);
|
|
||||||
alert("Failed to open project data");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openSaveModal = () => {
|
const openSaveModal = () => {
|
||||||
isSaveProjectModalOpen.value = true;
|
isSaveProjectModalOpen.value = true;
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSaveProject = async (name: string) => {
|
const handleSaveProject = async (name: string) => {
|
||||||
try {
|
try {
|
||||||
const data = await generateProjectJSON();
|
await saveProject(name);
|
||||||
|
} catch {
|
||||||
if (projectStore.currentProject && projectStore.currentProject.name === name) {
|
// Error handled in composable
|
||||||
await projectStore.updateProject(projectStore.currentProject.id, data);
|
}
|
||||||
} else {
|
|
||||||
await projectStore.createProject(name, data);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
alert("Failed to save project");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateNewProject = (config: { width: number; height: number; columns: number; rows: number }) => {
|
const handleCreateNewProject = (config: { width: number; height: number; columns: number; rows: number }) => {
|
||||||
// 1. Reset Settings
|
createProject(config);
|
||||||
settingsStore.setManualCellSize(config.width, config.height);
|
|
||||||
settingsStore.manualCellSizeEnabled = true;
|
|
||||||
|
|
||||||
// 2. Reset Layers
|
|
||||||
const newLayer = createEmptyLayer('Base');
|
|
||||||
layers.value = [newLayer];
|
|
||||||
activeLayerId.value = newLayer.id;
|
|
||||||
|
|
||||||
// 3. Set Columns
|
|
||||||
columns.value = config.columns;
|
|
||||||
|
|
||||||
// 4. Reset Project Store
|
|
||||||
projectStore.currentProject = null;
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterLink } from 'vue-router';
|
import { RouterLink } from 'vue-router';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,5 +10,5 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterLink } from 'vue-router';
|
import { RouterLink } from 'vue-router';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -36,11 +36,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterLink } from 'vue-router';
|
import { RouterLink } from 'vue-router';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits(['close', 'open-help']);
|
defineEmits(['close', 'open-help']);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,20 +3,13 @@
|
|||||||
<div class="flex items-center gap-2 group">
|
<div class="flex items-center gap-2 group">
|
||||||
<div class="text-right hidden lg:block">
|
<div class="text-right hidden lg:block">
|
||||||
<p class="text-[10px] uppercase tracking-wider font-bold text-gray-400 dark:text-gray-500 mb-[-2px]">Current Project</p>
|
<p class="text-[10px] uppercase tracking-wider font-bold text-gray-400 dark:text-gray-500 mb-[-2px]">Current Project</p>
|
||||||
<button
|
<button @click="$emit('open-save-modal')" class="text-sm font-bold text-gray-800 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors truncate max-w-[160px]" title="Rename Project">
|
||||||
@click="$emit('open-save-modal')"
|
|
||||||
class="text-sm font-bold text-gray-800 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors truncate max-w-[160px]"
|
|
||||||
title="Rename Project"
|
|
||||||
>
|
|
||||||
{{ projectStore.currentProject?.name || 'Untitled Project' }}
|
{{ projectStore.currentProject?.name || 'Untitled Project' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile/Tablet simplified view -->
|
<!-- Mobile/Tablet simplified view -->
|
||||||
<button
|
<button @click="$emit('open-save-modal')" class="lg:hidden text-sm font-bold text-gray-800 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors truncate max-w-[120px]">
|
||||||
@click="$emit('open-save-modal')"
|
|
||||||
class="lg:hidden text-sm font-bold text-gray-800 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors truncate max-w-[120px]"
|
|
||||||
>
|
|
||||||
{{ projectStore.currentProject?.name || 'Untitled' }}
|
{{ projectStore.currentProject?.name || 'Untitled' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,23 +17,23 @@
|
|||||||
<div class="h-8 w-px bg-gray-200 dark:bg-gray-700 mx-1"></div>
|
<div class="h-8 w-px bg-gray-200 dark:bg-gray-700 mx-1"></div>
|
||||||
|
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button @click="$emit('open-save-modal')" class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-all" title="Save Project">
|
<button @click="$emit('open-save-modal')" class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-all" title="Save Project">
|
||||||
<i class="fas fa-save"></i>
|
<i class="fas fa-save"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="$emit('open-new-project-modal')" class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-all" title="New Project">
|
<button @click="$emit('open-new-project-modal')" class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-all" title="New Project">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="$emit('open-project-list')" class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-all" title="My Projects">
|
<button @click="$emit('open-project-list')" class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-all" title="My Projects">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useProjectStore } from '@/stores/useProjectStore';
|
import { useProjectStore } from '@/stores/useProjectStore';
|
||||||
|
|
||||||
defineEmits(['open-save-modal', 'open-project-list', 'open-new-project-modal']);
|
defineEmits(['open-save-modal', 'open-project-list', 'open-new-project-modal']);
|
||||||
|
|
||||||
const projectStore = useProjectStore();
|
const projectStore = useProjectStore();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DarkModeToggle from '../../utilities/DarkModeToggle.vue';
|
import DarkModeToggle from '../../utilities/DarkModeToggle.vue';
|
||||||
|
|
||||||
defineEmits(['open-help']);
|
defineEmits(['open-help']);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,76 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="authStore.user" class="flex items-center gap-4">
|
<div v-if="authStore.user" class="flex items-center gap-4">
|
||||||
<div class="relative" ref="userMenuRef">
|
<div class="relative" ref="userMenuRef">
|
||||||
<button
|
<button @click="isUserMenuOpen = !isUserMenuOpen" class="w-9 h-9 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 text-white font-bold text-sm flex items-center justify-center shadow-md hover:shadow-lg transition-all border-2 border-white dark:border-gray-800">
|
||||||
@click="isUserMenuOpen = !isUserMenuOpen"
|
{{ userInitials }}
|
||||||
class="w-9 h-9 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 text-white font-bold text-sm flex items-center justify-center shadow-md hover:shadow-lg transition-all border-2 border-white dark:border-gray-800"
|
</button>
|
||||||
>
|
|
||||||
{{ userInitials }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition ease-out duration-200"
|
enter-active-class="transition ease-out duration-200"
|
||||||
enter-from-class="transform opacity-0 scale-95"
|
enter-from-class="transform opacity-0 scale-95"
|
||||||
enter-to-class="transform opacity-100 scale-100"
|
enter-to-class="transform opacity-100 scale-100"
|
||||||
leave-active-class="transition ease-in duration-75"
|
leave-active-class="transition ease-in duration-75"
|
||||||
leave-from-class="transform opacity-100 scale-100"
|
leave-from-class="transform opacity-100 scale-100"
|
||||||
leave-to-class="transform opacity-0 scale-95"
|
leave-to-class="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<div v-if="isUserMenuOpen" class="absolute right-0 mt-2 w-56 bg-white dark:bg-gray-900 rounded-xl shadow-xl border border-gray-200 dark:border-gray-800 overflow-hidden z-50">
|
<div v-if="isUserMenuOpen" class="absolute right-0 mt-2 w-56 bg-white dark:bg-gray-900 rounded-xl shadow-xl border border-gray-200 dark:border-gray-800 overflow-hidden z-50">
|
||||||
<div class="p-4 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50">
|
<div class="p-4 border-b border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/50">
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold tracking-wider mb-1">Signed in as</p>
|
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold tracking-wider mb-1">Signed in as</p>
|
||||||
<p class="text-sm font-medium text-gray-900 dark:text-white truncate" :title="authStore.user.email">{{ authStore.user.email }}</p>
|
<p class="text-sm font-medium text-gray-900 dark:text-white truncate" :title="authStore.user.email">{{ authStore.user.email }}</p>
|
||||||
</div>
|
|
||||||
<div class="p-1">
|
|
||||||
<button
|
|
||||||
@click="handleLogout"
|
|
||||||
class="w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400 rounded-lg transition-colors flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<i class="fas fa-sign-out-alt"></i> Sign out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
<div class="p-1">
|
||||||
|
<button @click="handleLogout" class="w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400 rounded-lg transition-colors flex items-center gap-2">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<button @click="$emit('open-auth-modal')" class="btn btn-primary btn-sm shadow-indigo-500/20 shadow-lg">
|
<button @click="$emit('open-auth-modal')" class="btn btn-primary btn-sm shadow-indigo-500/20 shadow-lg">Login / Register</button>
|
||||||
Login / Register
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||||
import { useAuthStore } from '@/stores/useAuthStore';
|
import { useAuthStore } from '@/stores/useAuthStore';
|
||||||
|
|
||||||
defineEmits(['open-auth-modal']);
|
defineEmits(['open-auth-modal']);
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const isUserMenuOpen = ref(false);
|
const isUserMenuOpen = ref(false);
|
||||||
const userMenuRef = ref<HTMLElement | null>(null);
|
const userMenuRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
const userInitials = computed(() => {
|
const userInitials = computed(() => {
|
||||||
if (!authStore.user?.email) return '??';
|
if (!authStore.user?.email) return '??';
|
||||||
return authStore.user.email.substring(0, 2).toUpperCase();
|
return authStore.user.email.substring(0, 2).toUpperCase();
|
||||||
});
|
});
|
||||||
|
|
||||||
const closeUserMenu = (e: MouseEvent) => {
|
const closeUserMenu = (e: MouseEvent) => {
|
||||||
if (userMenuRef.value && !userMenuRef.value.contains(e.target as Node)) {
|
if (userMenuRef.value && !userMenuRef.value.contains(e.target as Node)) {
|
||||||
isUserMenuOpen.value = false;
|
isUserMenuOpen.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
isUserMenuOpen.value = false;
|
isUserMenuOpen.value = false;
|
||||||
authStore.logout();
|
authStore.logout();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('click', closeUserMenu);
|
document.addEventListener('click', closeUserMenu);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('click', closeUserMenu);
|
document.removeEventListener('click', closeUserMenu);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,58 +4,31 @@
|
|||||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl p-6 w-full max-w-sm border border-gray-200 dark:border-gray-700">
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl p-6 w-full max-w-sm border border-gray-200 dark:border-gray-700">
|
||||||
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-gray-100">New Project</h2>
|
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-gray-100">New Project</h2>
|
||||||
<form @submit.prevent="handleSubmit">
|
<form @submit.prevent="handleSubmit">
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 uppercase tracking-wider">Sprite Width</label>
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 uppercase tracking-wider">Sprite Width</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input v-model.number="width" type="number" min="1" required class="w-full pl-3 pr-8 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" />
|
||||||
v-model.number="width"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
required
|
|
||||||
class="w-full pl-3 pr-8 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
/>
|
|
||||||
<span class="absolute right-3 top-2 text-gray-400 text-xs">px</span>
|
<span class="absolute right-3 top-2 text-gray-400 text-xs">px</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 uppercase tracking-wider">Sprite Height</label>
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 uppercase tracking-wider">Sprite Height</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input v-model.number="height" type="number" min="1" required class="w-full pl-3 pr-8 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" />
|
||||||
v-model.number="height"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
required
|
|
||||||
class="w-full pl-3 pr-8 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
/>
|
|
||||||
<span class="absolute right-3 top-2 text-gray-400 text-xs">px</span>
|
<span class="absolute right-3 top-2 text-gray-400 text-xs">px</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 uppercase tracking-wider">Columns</label>
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 uppercase tracking-wider">Columns</label>
|
||||||
<input
|
<input v-model.number="columns" type="number" min="1" max="20" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" />
|
||||||
v-model.number="columns"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="20"
|
|
||||||
required
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 uppercase tracking-wider">Rows</label>
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 uppercase tracking-wider">Rows</label>
|
||||||
<input
|
<input v-model.number="rows" type="number" min="1" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" title="Rows are dynamic but this sets an initial expectation" />
|
||||||
v-model.number="rows"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
required
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
title="Rows are dynamic but this sets an initial expectation"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,46 +43,49 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: 'close'): void;
|
||||||
(e: 'create', config: { width: number; height: number; columns: number; rows: number }): void;
|
(e: 'create', config: { width: number; height: number; columns: number; rows: number }): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const width = ref(64);
|
const width = ref(64);
|
||||||
const height = ref(64);
|
const height = ref(64);
|
||||||
const columns = ref(8);
|
const columns = ref(8);
|
||||||
const rows = ref(8);
|
const rows = ref(8);
|
||||||
|
|
||||||
const isValid = computed(() => {
|
const isValid = computed(() => {
|
||||||
return width.value > 0 && height.value > 0 && columns.value > 0 && rows.value > 0;
|
return width.value > 0 && height.value > 0 && columns.value > 0 && rows.value > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset to defaults when opened
|
// Reset to defaults when opened
|
||||||
watch(() => props.isOpen, (val) => {
|
watch(
|
||||||
if (val) {
|
() => props.isOpen,
|
||||||
width.value = 64;
|
val => {
|
||||||
height.value = 64;
|
if (val) {
|
||||||
columns.value = 8;
|
width.value = 64;
|
||||||
rows.value = 8;
|
height.value = 64;
|
||||||
}
|
columns.value = 8;
|
||||||
});
|
rows.value = 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const close = () => emit('close');
|
const close = () => emit('close');
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (isValid.value) {
|
if (isValid.value) {
|
||||||
emit('create', {
|
emit('create', {
|
||||||
width: width.value,
|
width: width.value,
|
||||||
height: height.value,
|
height: height.value,
|
||||||
columns: columns.value,
|
columns: columns.value,
|
||||||
rows: rows.value
|
rows: rows.value,
|
||||||
});
|
});
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,104 +1,102 @@
|
|||||||
<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/50 backdrop-blur-sm" @click.self="close">
|
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" @click.self="close">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-2xl p-6 relative border border-gray-200 dark:border-gray-700 max-h-[80vh] flex flex-col">
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-2xl p-6 relative border border-gray-200 dark:border-gray-700 max-h-[80vh] flex flex-col">
|
||||||
<div class="flex items-center justify-between mb-6 shrink-0">
|
<div class="flex items-center justify-between mb-6 shrink-0">
|
||||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">My Projects</h2>
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">My Projects</h2>
|
||||||
<button @click="close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
|
<button @click="close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto custom-scrollbar p-1">
|
|
||||||
<div v-if="loading" class="flex justify-center py-8">
|
|
||||||
<i class="fas fa-spinner fa-spin text-2xl text-indigo-500"></i>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="projects.length === 0" class="text-center py-12 text-gray-500 dark:text-gray-400">
|
<div class="flex-1 overflow-y-auto custom-scrollbar p-1">
|
||||||
<i class="fas fa-folder-open text-4xl mb-3 opacity-50"></i>
|
<div v-if="loading" class="flex justify-center py-8">
|
||||||
<p>No projects found. Save your work to see it here!</p>
|
<i class="fas fa-spinner fa-spin text-2xl text-indigo-500"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div v-else-if="projects.length === 0" class="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||||
<div
|
<i class="fas fa-folder-open text-4xl mb-3 opacity-50"></i>
|
||||||
v-for="project in projects"
|
<p>No projects found. Save your work to see it here!</p>
|
||||||
:key="project.id"
|
</div>
|
||||||
class="group p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 hover:border-indigo-500 dark:hover:border-indigo-500 transition-all cursor-pointer relative"
|
|
||||||
@click="selectProject(project)"
|
<div v-else class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
>
|
<div v-for="project in projects" :key="project.id" class="group p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 hover:border-indigo-500 dark:hover:border-indigo-500 transition-all cursor-pointer relative" @click="selectProject(project)">
|
||||||
<div class="flex items-start justify-between mb-2">
|
<div class="flex items-start justify-between mb-2">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h3 class="font-bold text-gray-900 dark:text-gray-100 truncate pr-2">{{ project.name }}</h3>
|
<h3 class="font-bold text-gray-900 dark:text-gray-100 truncate pr-2">{{ project.name }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="opacity-0 group-hover:opacity-100 transition-opacity">
|
<div class="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button @click.stop="deleteProject(project.id)" class="text-gray-400 hover:text-red-500 p-1" title="Delete Project">
|
<button @click.stop="deleteProject(project.id)" class="text-gray-400 hover:text-red-500 p-1" title="Delete Project">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400 flex flex-col gap-1">
|
<div class="text-xs text-gray-500 dark:text-gray-400 flex flex-col gap-1">
|
||||||
<span><i class="fas fa-clock mr-1"></i> Updated: {{ formatDate(project.updated) }}</span>
|
<span><i class="fas fa-clock mr-1"></i> Updated: {{ formatDate(project.updated) }}</span>
|
||||||
<span><i class="fas fa-layer-group mr-1"></i> Layers: {{ project.data?.layers?.length || 0 }}</span>
|
<span><i class="fas fa-layer-group mr-1"></i> Layers: {{ project.data?.layers?.length || 0 }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 flex justify-end shrink-0">
|
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 flex justify-end shrink-0">
|
||||||
<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>
|
</div>
|
||||||
</div>
|
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, toRefs, watch } from 'vue';
|
import { onMounted, toRefs, watch } from 'vue';
|
||||||
import { useProjectStore, type Project } from '@/stores/useProjectStore';
|
import { useProjectStore, type Project } from '@/stores/useProjectStore';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: 'close'): void;
|
||||||
(e: 'open-project', project: Project): void;
|
(e: 'open-project', project: Project): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const projectStore = useProjectStore();
|
const projectStore = useProjectStore();
|
||||||
const { projects, isLoading: loading } = toRefs(projectStore);
|
const { projects, isLoading: loading } = toRefs(projectStore);
|
||||||
|
|
||||||
watch(() => props.isOpen, (isOpen) => {
|
watch(
|
||||||
if (isOpen) {
|
() => props.isOpen,
|
||||||
projectStore.fetchProjects();
|
isOpen => {
|
||||||
}
|
if (isOpen) {
|
||||||
});
|
projectStore.fetchProjects();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.isOpen) {
|
if (props.isOpen) {
|
||||||
projectStore.fetchProjects();
|
projectStore.fetchProjects();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const close = () => emit('close');
|
|
||||||
|
|
||||||
const selectProject = (project: Project) => {
|
|
||||||
emit('open-project', project);
|
|
||||||
close();
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteProject = async (id: string) => {
|
|
||||||
await projectStore.deleteProject(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
return new Date(dateString).toLocaleDateString(undefined, {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
const close = () => emit('close');
|
||||||
|
|
||||||
|
const selectProject = (project: Project) => {
|
||||||
|
emit('open-project', project);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProject = async (id: string) => {
|
||||||
|
await projectStore.deleteProject(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,64 +1,57 @@
|
|||||||
<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/50 backdrop-blur-sm" @click.self="close">
|
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" @click.self="close">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl p-6 w-full max-w-sm border border-gray-200 dark:border-gray-700">
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl p-6 w-full max-w-sm border border-gray-200 dark:border-gray-700">
|
||||||
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-gray-100">Save Project</h2>
|
<h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-gray-100">Save Project</h2>
|
||||||
<form @submit.prevent="handleSubmit">
|
<form @submit.prevent="handleSubmit">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Project Name</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Project Name</label>
|
||||||
<input
|
<input v-model="projectName" type="text" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white" placeholder="My Awesome Spritesheet" ref="inputRef" />
|
||||||
v-model="projectName"
|
</div>
|
||||||
type="text"
|
<div class="flex justify-end gap-3">
|
||||||
required
|
<button type="button" @click="close" class="btn btn-secondary">Cancel</button>
|
||||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white"
|
<button type="submit" class="btn btn-primary" :disabled="!projectName.trim()">Save</button>
|
||||||
placeholder="My Awesome Spritesheet"
|
</div>
|
||||||
ref="inputRef"
|
</form>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="flex justify-end gap-3">
|
|
||||||
<button type="button" @click="close" class="btn btn-secondary">Cancel</button>
|
|
||||||
<button type="submit" class="btn btn-primary" :disabled="!projectName.trim()">Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick } from 'vue';
|
import { ref, watch, nextTick } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
initialName?: string;
|
initialName?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: 'close'): void;
|
||||||
(e: 'save', name: string): void;
|
(e: 'save', name: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const projectName = ref('');
|
const projectName = ref('');
|
||||||
const inputRef = ref<HTMLInputElement | null>(null);
|
const inputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.isOpen,
|
() => props.isOpen,
|
||||||
(isOpen) => {
|
isOpen => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
projectName.value = props.initialName || '';
|
projectName.value = props.initialName || '';
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
inputRef.value?.focus();
|
inputRef.value?.focus();
|
||||||
inputRef.value?.select();
|
inputRef.value?.select();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const close = () => emit('close');
|
const close = () => emit('close');
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (projectName.value.trim()) {
|
if (projectName.value.trim()) {
|
||||||
emit('save', projectName.value);
|
emit('save', projectName.value);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -113,18 +113,15 @@ export function useDragSprite(options: DragSpriteOptions) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const findCellAtPosition = (x: number, y: number): CellPosition | null => {
|
const findCellAtPosition = (x: number, y: number): CellPosition | null => {
|
||||||
const sprites = getSprites();
|
|
||||||
const columns = getColumns();
|
const columns = getColumns();
|
||||||
const { maxWidth, maxHeight } = calculateMaxDimensions();
|
const { maxWidth, maxHeight } = calculateMaxDimensions();
|
||||||
const col = Math.floor(x / maxWidth);
|
const col = Math.floor(x / maxWidth);
|
||||||
const row = Math.floor(y / maxHeight);
|
const row = Math.floor(y / maxHeight);
|
||||||
|
|
||||||
const totalRows = Math.ceil(sprites.length / columns);
|
// Allow dropping anywhere in the columns, assuming infinite rows effectively
|
||||||
if (col >= 0 && col < columns && row >= 0 && row < totalRows) {
|
if (col >= 0 && col < columns && row >= 0) {
|
||||||
const index = row * columns + col;
|
const index = row * columns + col;
|
||||||
if (index < sprites.length) {
|
return { col, row, index };
|
||||||
return { col, row, index };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,20 @@ const layers = ref<Layer[]>([createEmptyLayer('Base')]);
|
|||||||
const activeLayerId = ref<string>(layers.value[0].id);
|
const activeLayerId = ref<string>(layers.value[0].id);
|
||||||
const columns = ref(4);
|
const columns = ref(4);
|
||||||
|
|
||||||
|
const createEmptySprite = (): Sprite => ({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
file: new File([], 'empty'),
|
||||||
|
img: new Image(),
|
||||||
|
url: '',
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
rotation: 0,
|
||||||
|
flipX: false,
|
||||||
|
flipY: false,
|
||||||
|
});
|
||||||
|
|
||||||
export const useLayers = () => {
|
export const useLayers = () => {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
@@ -103,17 +117,66 @@ export const useLayers = () => {
|
|||||||
if (!l) return;
|
if (!l) return;
|
||||||
const currentIndex = l.sprites.findIndex(s => s.id === id);
|
const currentIndex = l.sprites.findIndex(s => s.id === id);
|
||||||
if (currentIndex === -1 || currentIndex === newIndex) return;
|
if (currentIndex === -1 || currentIndex === newIndex) return;
|
||||||
|
|
||||||
const next = [...l.sprites];
|
const next = [...l.sprites];
|
||||||
if (newIndex < next.length) {
|
|
||||||
const moving = { ...next[currentIndex] };
|
// Remove the moving sprite first
|
||||||
const target = { ...next[newIndex] };
|
const [moving] = next.splice(currentIndex, 1);
|
||||||
next[currentIndex] = target;
|
|
||||||
next[newIndex] = moving;
|
// Determine the actual index to insert at, considering we removed one item
|
||||||
} else {
|
// If the target index was greater than current index, it shifts down by 1 in the original array perspective?
|
||||||
const [moved] = next.splice(currentIndex, 1);
|
// Actually simpler: we just want to put 'moving' at 'newIndex' in the final array.
|
||||||
next.splice(newIndex, 0, moved);
|
|
||||||
|
// If newIndex is beyond the current bounds (after removal), fill with placeholders
|
||||||
|
while (next.length < newIndex) {
|
||||||
|
next.push(createEmptySprite());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now insert
|
||||||
|
// If newIndex is within bounds, we might be swapping if there was something there
|
||||||
|
// But the DragSprite logic implies we are "moving to this cell".
|
||||||
|
// If there is existing content at newIndex, we should swap or splice?
|
||||||
|
// The previous implementation did a swap if newIndex < length (before removal).
|
||||||
|
|
||||||
|
// Let's stick to the "swap" logic if there's a sprite there, or "move" if we are reordering.
|
||||||
|
// Wait, Drag and Drop usually implies "insert here" or "swap with this".
|
||||||
|
// useDragSprite says: "if allowCellSwap... updateSpriteCell".
|
||||||
|
|
||||||
|
// The original logic:
|
||||||
|
// if (newIndex < next.length) -> swap
|
||||||
|
// else -> splice (move)
|
||||||
|
|
||||||
|
// Re-evaluating original logic:
|
||||||
|
// next has NOT had the item removed yet in the original logic 'if' block.
|
||||||
|
|
||||||
|
// Let's implement robust swap/move logic.
|
||||||
|
// 1. If target is empty placeholder -> just move there (replace placeholder).
|
||||||
|
// 2. If target has sprite -> swap.
|
||||||
|
// 3. If target is out of bounds -> pad and move.
|
||||||
|
|
||||||
|
if (newIndex < l.sprites.length) {
|
||||||
|
// Perform Swap
|
||||||
|
const target = l.sprites[newIndex];
|
||||||
|
const moving = l.sprites[currentIndex];
|
||||||
|
|
||||||
|
// Clone array
|
||||||
|
const newSprites = [...l.sprites];
|
||||||
|
newSprites[currentIndex] = target;
|
||||||
|
newSprites[newIndex] = moving;
|
||||||
|
l.sprites = newSprites;
|
||||||
|
} else {
|
||||||
|
// Move to previously empty/non-existent cell
|
||||||
|
const newSprites = [...l.sprites];
|
||||||
|
// Remove from old pos
|
||||||
|
const [moved] = newSprites.splice(currentIndex, 1);
|
||||||
|
// Pad
|
||||||
|
while (newSprites.length < newIndex) {
|
||||||
|
newSprites.push(createEmptySprite());
|
||||||
|
}
|
||||||
|
// Insert (or push if equal length)
|
||||||
|
newSprites.splice(newIndex, 0, moved);
|
||||||
|
l.sprites = newSprites;
|
||||||
}
|
}
|
||||||
l.sprites = next;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeSprite = (id: string) => {
|
const removeSprite = (id: string) => {
|
||||||
@@ -203,7 +266,7 @@ export const useLayers = () => {
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSprite = (file: File) => {
|
const addSprite = (file: File, index?: number) => {
|
||||||
const l = activeLayer.value;
|
const l = activeLayer.value;
|
||||||
if (!l) return;
|
if (!l) return;
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -224,7 +287,28 @@ export const useLayers = () => {
|
|||||||
flipX: false,
|
flipX: false,
|
||||||
flipY: false,
|
flipY: false,
|
||||||
};
|
};
|
||||||
l.sprites = [...l.sprites, next];
|
|
||||||
|
const currentSprites = [...l.sprites];
|
||||||
|
|
||||||
|
if (typeof index === 'number') {
|
||||||
|
// If index is provided, insert there (padding if needed)
|
||||||
|
while (currentSprites.length < index) {
|
||||||
|
currentSprites.push(createEmptySprite());
|
||||||
|
}
|
||||||
|
// If valid index, replace if empty or splice?
|
||||||
|
// "Adds it not in the one I selected".
|
||||||
|
// If I select a cell, I expect it to go there.
|
||||||
|
// If the cell is empty (placeholder), replace it.
|
||||||
|
// If the cell has a sprite, maybe insert/shift?
|
||||||
|
// Usually "Add" implies append, but context menu "Add sprite" on a cell implies "Put it here".
|
||||||
|
// Let's Insert (Shift others) for safety, or check if empty.
|
||||||
|
// But simpler: just splice it in.
|
||||||
|
currentSprites.splice(index, 0, next);
|
||||||
|
} else {
|
||||||
|
// No index, append to end
|
||||||
|
currentSprites.push(next);
|
||||||
|
}
|
||||||
|
l.sprites = currentSprites;
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
console.error('Failed to load sprite image:', file.name);
|
console.error('Failed to load sprite image:', file.name);
|
||||||
@@ -299,19 +383,7 @@ export const useLayers = () => {
|
|||||||
|
|
||||||
// Expand the sprites array if necessary with empty placeholder sprites
|
// Expand the sprites array if necessary with empty placeholder sprites
|
||||||
while (targetLayer.sprites.length < targetFrameIndex) {
|
while (targetLayer.sprites.length < targetFrameIndex) {
|
||||||
targetLayer.sprites.push({
|
targetLayer.sprites.push(createEmptySprite());
|
||||||
id: crypto.randomUUID(),
|
|
||||||
file: new File([], 'empty'),
|
|
||||||
img: new Image(),
|
|
||||||
url: '',
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
rotation: 0,
|
|
||||||
flipX: false,
|
|
||||||
flipY: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace or insert the sprite at the target index
|
// Replace or insert the sprite at the target index
|
||||||
|
|||||||
95
src/composables/useProjectManager.ts
Normal file
95
src/composables/useProjectManager.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { ref, toRef, watch } from 'vue';
|
||||||
|
import { useLayers, createEmptyLayer, getMaxDimensionsAcrossLayers } from '@/composables/useLayers';
|
||||||
|
import { useSettingsStore } from '@/stores/useSettingsStore';
|
||||||
|
import { useProjectStore, type Project } from '@/stores/useProjectStore';
|
||||||
|
import { useExportLayers } from '@/composables/useExportLayers';
|
||||||
|
|
||||||
|
// Global state for editor visibility
|
||||||
|
const isEditorActive = ref(false);
|
||||||
|
|
||||||
|
export const useProjectManager = () => {
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const projectStore = useProjectStore();
|
||||||
|
const { layers, columns, activeLayerId, visibleLayers } = useLayers();
|
||||||
|
|
||||||
|
const { generateProjectJSON, loadProjectData } = useExportLayers(
|
||||||
|
layers,
|
||||||
|
columns,
|
||||||
|
toRef(settingsStore, 'negativeSpacingEnabled'),
|
||||||
|
activeLayerId,
|
||||||
|
toRef(settingsStore, 'backgroundColor'),
|
||||||
|
toRef(settingsStore, 'manualCellSizeEnabled'),
|
||||||
|
toRef(settingsStore, 'manualCellWidth'),
|
||||||
|
toRef(settingsStore, 'manualCellHeight')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Watch for sprites to automatically open editor (legacy behavior support)
|
||||||
|
watch(
|
||||||
|
() => layers.value.some(l => l.sprites.length > 0),
|
||||||
|
hasSprites => {
|
||||||
|
if (hasSprites) isEditorActive.value = true;
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const createProject = (config: { width: number; height: number; columns: number; rows: number }) => {
|
||||||
|
// 1. Reset Settings
|
||||||
|
settingsStore.setManualCellSize(config.width, config.height);
|
||||||
|
settingsStore.manualCellSizeEnabled = true;
|
||||||
|
|
||||||
|
// 2. Reset Layers
|
||||||
|
const newLayer = createEmptyLayer('Base');
|
||||||
|
layers.value = [newLayer];
|
||||||
|
activeLayerId.value = newLayer.id;
|
||||||
|
|
||||||
|
// 3. Set Columns
|
||||||
|
columns.value = config.columns;
|
||||||
|
|
||||||
|
// 4. Reset Project Store
|
||||||
|
projectStore.currentProject = null;
|
||||||
|
|
||||||
|
// 5. Force Editor Active
|
||||||
|
isEditorActive.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openProject = async (project: Project) => {
|
||||||
|
try {
|
||||||
|
if (project.data) {
|
||||||
|
await loadProjectData(project.data);
|
||||||
|
}
|
||||||
|
projectStore.currentProject = project;
|
||||||
|
isEditorActive.value = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to open project', e);
|
||||||
|
alert('Failed to open project data');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveProject = async (name: string) => {
|
||||||
|
try {
|
||||||
|
const data = await generateProjectJSON();
|
||||||
|
|
||||||
|
if (projectStore.currentProject && projectStore.currentProject.name === name) {
|
||||||
|
await projectStore.updateProject(projectStore.currentProject.id, data);
|
||||||
|
} else {
|
||||||
|
await projectStore.createProject(name, data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert('Failed to save project');
|
||||||
|
throw e; // Re-throw to let caller know
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditor = () => {
|
||||||
|
isEditorActive.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isEditorActive,
|
||||||
|
createProject,
|
||||||
|
openProject,
|
||||||
|
saveProject,
|
||||||
|
closeEditor,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -80,23 +80,23 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadProject(id: string) {
|
async function loadProject(id: string) {
|
||||||
if (!authStore.user) return;
|
if (!authStore.user) return;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const record = await authStore.pb.collection('projects').getOne(id);
|
const record = await authStore.pb.collection('projects').getOne(id);
|
||||||
currentProject.value = {
|
currentProject.value = {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
name: record.name,
|
name: record.name,
|
||||||
data: record.data,
|
data: record.data,
|
||||||
created: record.created,
|
created: record.created,
|
||||||
updated: record.updated
|
updated: record.updated,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load project", error);
|
console.error('Failed to load project', error);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteProject(id: string) {
|
async function deleteProject(id: string) {
|
||||||
@@ -125,6 +125,6 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
createProject,
|
createProject,
|
||||||
updateProject,
|
updateProject,
|
||||||
loadProject,
|
loadProject,
|
||||||
deleteProject
|
deleteProject,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="flex flex-col flex-1 h-full min-h-0 relative">
|
<main class="flex flex-col flex-1 h-full min-h-0 relative">
|
||||||
<!-- Welcome / Empty State -->
|
<!-- Welcome / Empty State -->
|
||||||
<div v-if="!layers.some(l => l.sprites.length)" class="flex-1 flex flex-col items-center justify-center pb-12">
|
<div v-if="!isEditorActive" class="flex-1 flex flex-col items-center justify-center pb-12">
|
||||||
<div class="w-full max-w-[90rem] px-4 sm:px-6 lg:px-8 flex flex-col gap-8 lg:gap-12 items-start">
|
<div class="w-full max-w-[90rem] px-4 sm:px-6 lg:px-8 flex flex-col gap-8 lg:gap-12 items-start">
|
||||||
<!-- Top Row: Upload Field & Video Side by Side -->
|
<!-- Top Row: Upload Field & Video Side by Side -->
|
||||||
<div class="w-full grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8">
|
<div class="w-full grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8">
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Editor Interface -->
|
<!-- Main Editor Interface -->
|
||||||
<div v-else class="flex flex-col lg:flex-row gap-6 h-full min-h-[600px] lg:overflow-hidden">
|
<div v-else-if="isEditorActive" class="flex flex-col lg:flex-row gap-6 h-full min-h-[600px] lg:overflow-hidden">
|
||||||
<!-- Sidebar Controls -->
|
<!-- Sidebar Controls -->
|
||||||
<aside class="flex flex-col w-full lg:w-[340px] gap-4 shrink-0 lg:overflow-hidden">
|
<aside class="flex flex-col w-full lg:w-[340px] gap-4 shrink-0 lg:overflow-hidden">
|
||||||
<div class="glass-panel rounded-xl flex flex-col h-full lg:overflow-hidden border border-gray-200 dark:border-gray-700/60 shadow-lg">
|
<div class="glass-panel rounded-xl flex flex-col h-full lg:overflow-hidden border border-gray-200 dark:border-gray-700/60 shadow-lg">
|
||||||
@@ -456,11 +456,13 @@
|
|||||||
import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing';
|
import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing';
|
||||||
import { useHomeViewSEO } from './HomeView.seo';
|
import { useHomeViewSEO } from './HomeView.seo';
|
||||||
import { useZoom } from '@/composables/useZoom';
|
import { useZoom } from '@/composables/useZoom';
|
||||||
|
import { useProjectManager } from '@/composables/useProjectManager';
|
||||||
import type { SpriteFile } from '@/types/sprites';
|
import type { SpriteFile } from '@/types/sprites';
|
||||||
import tutVideo from '@/assets/tut2.mp4';
|
import tutVideo from '@/assets/tut2.mp4';
|
||||||
|
|
||||||
useHomeViewSEO();
|
useHomeViewSEO();
|
||||||
|
|
||||||
|
const { isEditorActive } = 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