diff --git a/.env b/.env
new file mode 100644
index 0000000..73695e1
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+VITE_POCKETBASE_URL=https://pb1.adhd.sh
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..73695e1
--- /dev/null
+++ b/.env.example
@@ -0,0 +1 @@
+VITE_POCKETBASE_URL=https://pb1.adhd.sh
diff --git a/src/components/FeedbackModal.vue b/src/components/FeedbackModal.vue
index cccbf8d..c1fc49b 100644
--- a/src/components/FeedbackModal.vue
+++ b/src/components/FeedbackModal.vue
@@ -68,7 +68,7 @@
loading.value = true;
try {
- const res = await fetch('https://pb1.adhd.sh/api/collections/feedback/records', {
+ const res = await fetch(`${import.meta.env.VITE_POCKETBASE_URL}/api/collections/feedback/records`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
diff --git a/src/components/auth/AuthModal.vue b/src/components/auth/AuthModal.vue
new file mode 100644
index 0000000..078d0b8
--- /dev/null
+++ b/src/components/auth/AuthModal.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
{{ isLogin ? 'Welcome Back' : 'Create Account' }}
+
+ {{ isLogin ? 'Sign in to access your projects' : 'Join to save and manage your spritesheets' }}
+
+
+
+
+
+
+ {{ isLogin ? "Don't have an account?" : 'Already have an account?' }}
+
+
+
+
+
+
+
+
diff --git a/src/components/layout/Navbar.vue b/src/components/layout/Navbar.vue
index 14c9260..21fd310 100644
--- a/src/components/layout/Navbar.vue
+++ b/src/components/layout/Navbar.vue
@@ -23,6 +23,29 @@
@@ -80,8 +112,73 @@
import { ref } from 'vue';
import { RouterLink } from 'vue-router';
import DarkModeToggle from '../utilities/DarkModeToggle.vue';
+ import { useAuthStore } from '@/stores/useAuthStore';
+ import AuthModal from '@/components/auth/AuthModal.vue';
+ import ProjectList from '@/components/project/ProjectList.vue';
+ import SaveProjectModal from '@/components/project/SaveProjectModal.vue';
+ import { useProjectStore, type Project } from '@/stores/useProjectStore';
+ import { useSettingsStore } from '@/stores/useSettingsStore';
+ import { useExportLayers } from '@/composables/useExportLayers';
+ import { useLayers } from '@/composables/useLayers';
+ import { toRef } from 'vue';
- defineEmits(['open-help']);
+ defineEmits(['open-help']); // Removed 'open-project' since we handle it here
const isMobileMenuOpen = ref(false);
+ const isAuthModalOpen = ref(false);
+ const isProjectListOpen = ref(false);
+ const isSaveProjectModalOpen = ref(false);
+
+ const authStore = useAuthStore();
+ const projectStore = useProjectStore();
+ const settingsStore = useSettingsStore();
+ 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) => {
+ try {
+ 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 = () => {
+ isSaveProjectModalOpen.value = true;
+ }
+
+ const handleSaveProject = async (name: string) => {
+ try {
+ const data = await generateProjectJSON();
+
+ if (projectStore.currentProject && projectStore.currentProject.name === name) { // Simple check, ideally check ID but UI only exposes name edit on save for new projects or overwrite?
+ // Actually SaveProjectModal allows editing name.
+ // If it's the same project, we update.
+ await projectStore.updateProject(projectStore.currentProject.id, data);
+ // Update name if changed? updateProject signature might need name.
+ // current implementation of updateProject only updates data.
+ // To update name we need to change store or backend.
+ // For now, let's just update data.
+ } else {
+ await projectStore.createProject(name, data);
+ }
+ } catch (e) {
+ console.error(e);
+ alert("Failed to save project");
+ }
+ };
diff --git a/src/components/project/ProjectList.vue b/src/components/project/ProjectList.vue
new file mode 100644
index 0000000..41fbc4c
--- /dev/null
+++ b/src/components/project/ProjectList.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
My Projects
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/project/SaveProjectModal.vue b/src/components/project/SaveProjectModal.vue
new file mode 100644
index 0000000..e45aa90
--- /dev/null
+++ b/src/components/project/SaveProjectModal.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
diff --git a/src/composables/useExportLayers.ts b/src/composables/useExportLayers.ts
index c474220..4a46b35 100644
--- a/src/composables/useExportLayers.ts
+++ b/src/composables/useExportLayers.ts
@@ -106,13 +106,7 @@ export const useExportLayers = (layersRef: Ref
, columns: Ref, n
link.click();
};
- const exportSpritesheetJSON = async () => {
- const visibleLayers = getVisibleLayers();
- if (!visibleLayers.length || !visibleLayers.some(l => l.sprites.length)) {
- alert('Nothing to export. Please add sprites first.');
- return;
- }
-
+ const generateProjectJSON = async () => {
const layersData = await Promise.all(
layersRef.value.map(async layer => {
const sprites = await Promise.all(
@@ -131,7 +125,7 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n
})
);
- const json = {
+ return {
version: 2,
columns: columns.value,
negativeSpacingEnabled: negativeSpacingEnabled.value,
@@ -141,6 +135,16 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n
manualCellHeight: manualCellHeight?.value || 64,
layers: layersData,
};
+ };
+
+ const exportSpritesheetJSON = async () => {
+ const visibleLayers = getVisibleLayers();
+ if (!visibleLayers.length || !visibleLayers.some(l => l.sprites.length)) {
+ alert('Nothing to export. Please add sprites first.');
+ return;
+ }
+
+ const json = await generateProjectJSON();
const jsonString = JSON.stringify(json, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
@@ -151,10 +155,7 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n
URL.revokeObjectURL(url);
};
- const importSpritesheetJSON = async (jsonFile: File) => {
- const text = await jsonFile.text();
- const data = JSON.parse(text);
-
+ const loadProjectData = async (data: any) => {
const loadSprite = (spriteData: any) =>
new Promise(resolve => {
const img = new Image();
@@ -217,6 +218,12 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n
throw new Error('Invalid JSON format');
};
+ const importSpritesheetJSON = async (jsonFile: File) => {
+ const text = await jsonFile.text();
+ const data = JSON.parse(text);
+ await loadProjectData(data);
+ };
+
const downloadAsGif = (fps: number) => {
const visibleLayers = getVisibleLayers();
if (!visibleLayers.length || !visibleLayers.some(l => l.sprites.length)) {
@@ -314,5 +321,5 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n
URL.revokeObjectURL(url);
};
- return { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip };
+ return { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip, generateProjectJSON, loadProjectData };
};
diff --git a/src/composables/useShare.ts b/src/composables/useShare.ts
index 5134f8b..26aab45 100644
--- a/src/composables/useShare.ts
+++ b/src/composables/useShare.ts
@@ -1,7 +1,7 @@
import type { Ref } from 'vue';
import type { Layer } from '@/types/sprites';
-const POCKETBASE_URL = 'https://pb1.adhd.sh';
+const POCKETBASE_URL = import.meta.env.VITE_POCKETBASE_URL;
const COLLECTION = 'spritesheets';
export interface SpritesheetConfig {
diff --git a/src/stores/useAuthStore.ts b/src/stores/useAuthStore.ts
new file mode 100644
index 0000000..d259895
--- /dev/null
+++ b/src/stores/useAuthStore.ts
@@ -0,0 +1,39 @@
+import { defineStore } from 'pinia';
+import PocketBase from 'pocketbase';
+import { ref } from 'vue';
+
+export const useAuthStore = defineStore('auth', () => {
+ const pb = new PocketBase(import.meta.env.VITE_POCKETBASE_URL);
+ const user = ref(pb.authStore.model);
+
+ // Sync user state on change
+ pb.authStore.onChange(() => {
+ user.value = pb.authStore.model;
+ });
+
+ async function login(email: string, password: string) {
+ await pb.collection('users').authWithPassword(email, password);
+ }
+
+ async function register(email: string, password: string, passwordConfirm: string) {
+ await pb.collection('users').create({
+ email,
+ password,
+ passwordConfirm,
+ });
+ // Auto login after register
+ await login(email, password);
+ }
+
+ function logout() {
+ pb.authStore.clear();
+ }
+
+ return {
+ pb,
+ user,
+ login,
+ register,
+ logout,
+ };
+});
diff --git a/src/stores/useProjectStore.ts b/src/stores/useProjectStore.ts
new file mode 100644
index 0000000..6537a4f
--- /dev/null
+++ b/src/stores/useProjectStore.ts
@@ -0,0 +1,130 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import { useAuthStore } from './useAuthStore';
+
+export interface Project {
+ id: string;
+ name: string;
+ data: any; // Store the JSON export of the project here
+ created: string;
+ updated: string;
+}
+
+export const useProjectStore = defineStore('project', () => {
+ const authStore = useAuthStore();
+ const projects = ref([]);
+ const currentProject = ref(null);
+ const isLoading = ref(false);
+
+ async function fetchProjects() {
+ if (!authStore.user) return;
+ isLoading.value = true;
+ try {
+ const records = await authStore.pb.collection('projects').getList(1, 50, {
+ sort: '-updated',
+ });
+ projects.value = records.items.map((r: any) => ({
+ id: r.id,
+ name: r.name,
+ data: r.data,
+ created: r.created,
+ updated: r.updated,
+ }));
+ } catch (error) {
+ console.error('Failed to fetch projects:', error);
+ } finally {
+ isLoading.value = false;
+ }
+ }
+
+ async function createProject(name: string, data: any) {
+ if (!authStore.user) return;
+ isLoading.value = true;
+ try {
+ const record = await authStore.pb.collection('projects').create({
+ name,
+ data,
+ user: authStore.user.id,
+ });
+ currentProject.value = {
+ id: record.id,
+ name: record.name,
+ data: record.data,
+ created: record.created,
+ updated: record.updated,
+ };
+ await fetchProjects();
+ } catch (error) {
+ console.error('Failed to create project:', error);
+ throw error;
+ } finally {
+ isLoading.value = false;
+ }
+ }
+
+ async function updateProject(id: string, data: any) {
+ if (!authStore.user) return;
+ isLoading.value = true;
+ try {
+ const record = await authStore.pb.collection('projects').update(id, {
+ data,
+ });
+ currentProject.value = { ...currentProject.value!, data: record.data, updated: record.updated };
+ await fetchProjects();
+ } catch (error) {
+ console.error('Failed to update project:', error);
+ throw error;
+ } finally {
+ isLoading.value = false;
+ }
+ }
+
+ async function loadProject(id: string) {
+ if (!authStore.user) return;
+ isLoading.value = true;
+ try {
+ const record = await authStore.pb.collection('projects').getOne(id);
+ currentProject.value = {
+ id: record.id,
+ name: record.name,
+ data: record.data,
+ created: record.created,
+ updated: record.updated
+ };
+ } catch (error) {
+ console.error("Failed to load project", error);
+ throw error;
+ } finally {
+ isLoading.value = false;
+ }
+ }
+
+ async function deleteProject(id: string) {
+ if (!authStore.user) return;
+ if (!confirm('Are you sure you want to delete this project?')) return;
+
+ isLoading.value = true;
+ try {
+ await authStore.pb.collection('projects').delete(id);
+ if (currentProject.value?.id === id) {
+ currentProject.value = null;
+ }
+ await fetchProjects();
+ } catch (error) {
+ console.error('Failed to delete project:', error);
+ } finally {
+ isLoading.value = false;
+ }
+ }
+
+ return {
+ projects,
+ currentProject,
+ isLoading,
+ fetchProjects,
+ createProject,
+ updateProject,
+ loadProject,
+ deleteProject
+ };
+});