Format
This commit is contained in:
parent
22781b1883
commit
362efc7bda
@ -1,6 +1,11 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/prettierrc",
|
"semi": true,
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 100
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 300,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"vueIndentScriptAndStyle": true
|
||||||
}
|
}
|
||||||
|
38
package-lock.json
generated
38
package-lock.json
generated
@ -13,13 +13,13 @@
|
|||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
|
||||||
"@tsconfig/node22": "^22.0.1",
|
"@tsconfig/node22": "^22.0.1",
|
||||||
"@types/node": "^22.13.14",
|
"@types/node": "^22.13.14",
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"@vue/tsconfig": "^0.7.0",
|
"@vue/tsconfig": "^0.7.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"npm-run-all2": "^7.0.2",
|
"npm-run-all2": "^7.0.2",
|
||||||
"postcss": "^8.5.3",
|
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.1",
|
||||||
"typescript": "~5.8.0",
|
"typescript": "~5.8.0",
|
||||||
@ -897,6 +897,42 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ianvs/prettier-plugin-sort-imports": {
|
||||||
|
"version": "4.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.4.1.tgz",
|
||||||
|
"integrity": "sha512-F0/Hrcfpy8WuxlQyAWJTEren/uxKhYonOGY4OyWmwRdeTvkh9mMSCxowZLjNkhwi/2ipqCgtXwwOk7tW0mWXkA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/generator": "^7.26.2",
|
||||||
|
"@babel/parser": "^7.26.2",
|
||||||
|
"@babel/traverse": "^7.25.9",
|
||||||
|
"@babel/types": "^7.26.0",
|
||||||
|
"semver": "^7.5.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/compiler-sfc": "2.7.x || 3.x",
|
||||||
|
"prettier": "2 || 3"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/compiler-sfc": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ianvs/prettier-plugin-sort-imports/node_modules/semver": {
|
||||||
|
"version": "7.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||||
|
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.8",
|
"version": "0.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
|
||||||
"@tsconfig/node22": "^22.0.1",
|
"@tsconfig/node22": "^22.0.1",
|
||||||
"@types/node": "^22.13.14",
|
"@types/node": "^22.13.14",
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
|
@ -23,10 +23,6 @@ import HelpButton from './components/HelpButton.vue';
|
|||||||
const previewModalRef = ref<InstanceType<typeof PreviewModal> | null>(null);
|
const previewModalRef = ref<InstanceType<typeof PreviewModal> | null>(null);
|
||||||
|
|
||||||
const showHelpModal = () => {
|
const showHelpModal = () => {
|
||||||
alert('Keyboard Shortcuts:\n\n' +
|
alert('Keyboard Shortcuts:\n\n' + 'Shift + Drag: Fine-tune sprite position\n' + 'Space: Play/Pause animation\n' + 'Esc: Close preview modal\n' + 'Arrow Keys: Navigate frames when paused');
|
||||||
'Shift + Drag: Fine-tune sprite position\n' +
|
|
||||||
'Space: Play/Pause animation\n' +
|
|
||||||
'Esc: Close preview modal\n' +
|
|
||||||
'Arrow Keys: Navigate frames when paused');
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
@ -1 +1 @@
|
|||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
|
@ -5,11 +5,7 @@
|
|||||||
<h1 class="text-xl font-semibold text-gray-200">Spritesheet Creator</h1>
|
<h1 class="text-xl font-semibold text-gray-200">Spritesheet Creator</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button
|
<button @click="emit('toggleHelp')" class="p-2 bg-gray-700 border border-gray-600 rounded hover:border-blue-500 transition-colors" title="Keyboard Shortcuts">
|
||||||
@click="emit('toggleHelp')"
|
|
||||||
class="p-2 bg-gray-700 border border-gray-600 rounded hover:border-blue-500 transition-colors"
|
|
||||||
title="Keyboard Shortcuts"
|
|
||||||
>
|
|
||||||
<i class="fas fa-keyboard"></i>
|
<i class="fas fa-keyboard"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -18,6 +14,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'toggleHelp'): void
|
(e: 'toggleHelp'): void;
|
||||||
}>()
|
}>();
|
||||||
</script>
|
</script>
|
@ -1,24 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div @click="openFileDialog" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop" class="border-2 border-dashed border-gray-600 rounded-lg p-8 text-center cursor-pointer transition-all" :class="{ 'border-blue-500 bg-blue-500 bg-opacity-5': isDragOver }">
|
||||||
@click="openFileDialog"
|
|
||||||
@dragover.prevent="onDragOver"
|
|
||||||
@dragleave="onDragLeave"
|
|
||||||
@drop.prevent="onDrop"
|
|
||||||
class="border-2 border-dashed border-gray-600 rounded-lg p-8 text-center cursor-pointer transition-all"
|
|
||||||
:class="{'border-blue-500 bg-blue-500 bg-opacity-5': isDragOver}"
|
|
||||||
>
|
|
||||||
<i class="fas fa-cloud-upload-alt text-blue-500 text-3xl mb-4"></i>
|
<i class="fas fa-cloud-upload-alt text-blue-500 text-3xl mb-4"></i>
|
||||||
<p class="text-gray-400">
|
<p class="text-gray-400">Drag & drop sprite images here<br />or click to select files</p>
|
||||||
Drag & drop sprite images here<br>or click to select files
|
<input type="file" ref="fileInput" multiple accept="image/*" class="hidden" @change="onFileChange" />
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref="fileInput"
|
|
||||||
multiple
|
|
||||||
accept="image/*"
|
|
||||||
class="hidden"
|
|
||||||
@change="onFileChange"
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -27,7 +11,7 @@ import { ref } from 'vue';
|
|||||||
import { type Sprite, useSpritesheetStore } from '../composables/useSpritesheetStore';
|
import { type Sprite, useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'files-uploaded': [sprites: Sprite[]]
|
'files-uploaded': [sprites: Sprite[]];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const store = useSpritesheetStore();
|
const store = useSpritesheetStore();
|
||||||
@ -93,7 +77,7 @@ const createSpriteFromFile = (file: File, index: number): Promise<Sprite> => {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = e => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
@ -105,7 +89,7 @@ const createSpriteFromFile = (file: File, index: number): Promise<Sprite> => {
|
|||||||
y: 0,
|
y: 0,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
id: `sprite-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
id: `sprite-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
uploadOrder: index
|
uploadOrder: index,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button @click="emit('showHelp')" class="fixed bottom-5 right-5 w-12 h-12 bg-blue-500 text-white rounded-full flex items-center justify-center text-xl shadow-lg cursor-pointer transition-all hover:bg-blue-600 hover:-translate-y-1 z-40">
|
||||||
@click="emit('showHelp')"
|
|
||||||
class="fixed bottom-5 right-5 w-12 h-12 bg-blue-500 text-white rounded-full flex items-center justify-center text-xl shadow-lg cursor-pointer transition-all hover:bg-blue-600 hover:-translate-y-1 z-40"
|
|
||||||
>
|
|
||||||
<i class="fas fa-question"></i>
|
<i class="fas fa-question"></i>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
showHelp: []
|
showHelp: [];
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<div class="bg-gray-800 rounded-lg shadow-md overflow-hidden w-full">
|
<div class="bg-gray-800 rounded-lg shadow-md overflow-hidden w-full">
|
||||||
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600">
|
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600">
|
||||||
<div class="flex items-center gap-2 text-lg font-semibold">
|
<div class="flex items-center gap-2 text-lg font-semibold">
|
||||||
@ -8,22 +9,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="relative overflow-auto bg-gray-700 rounded border border-gray-600">
|
<div class="relative overflow-auto bg-gray-700 rounded border border-gray-600">
|
||||||
<canvas
|
<canvas ref="canvasEl" class="block mx-auto"></canvas>
|
||||||
ref="canvasEl"
|
|
||||||
class="block mx-auto"
|
|
||||||
></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tooltip -->
|
<!-- Tooltip -->
|
||||||
<div
|
<div v-if="isTooltipVisible" :style="tooltipStyle" class="absolute bg-gray-700 text-gray-200 px-3 py-2 rounded text-xs z-50 pointer-events-none shadow-md border border-gray-600">
|
||||||
v-if="isTooltipVisible"
|
|
||||||
:style="tooltipStyle"
|
|
||||||
class="absolute bg-gray-700 text-gray-200 px-3 py-2 rounded text-xs z-50 pointer-events-none shadow-md border border-gray-600"
|
|
||||||
>
|
|
||||||
{{ tooltipText }}
|
{{ tooltipText }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -40,7 +35,7 @@ const tooltipPosition = ref({ x: 0, y: 0 });
|
|||||||
|
|
||||||
const tooltipStyle = computed(() => ({
|
const tooltipStyle = computed(() => ({
|
||||||
left: `${tooltipPosition.value.x + 15}px`,
|
left: `${tooltipPosition.value.x + 15}px`,
|
||||||
top: `${tooltipPosition.value.y + 15}px`
|
top: `${tooltipPosition.value.y + 15}px`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const setupCheckerboardPattern = () => {
|
const setupCheckerboardPattern = () => {
|
||||||
@ -67,12 +62,7 @@ const handleMouseDown = (e: MouseEvent) => {
|
|||||||
// Find which sprite was clicked
|
// Find which sprite was clicked
|
||||||
for (let i = store.sprites.value.length - 1; i >= 0; i--) {
|
for (let i = store.sprites.value.length - 1; i >= 0; i--) {
|
||||||
const sprite = store.sprites.value[i];
|
const sprite = store.sprites.value[i];
|
||||||
if (
|
if (x >= sprite.x && x <= sprite.x + store.cellSize.width && y >= sprite.y && y <= sprite.y + store.cellSize.height) {
|
||||||
x >= sprite.x &&
|
|
||||||
x <= sprite.x + store.cellSize.width &&
|
|
||||||
y >= sprite.y &&
|
|
||||||
y <= sprite.y + store.cellSize.height
|
|
||||||
) {
|
|
||||||
store.draggedSprite.value = sprite;
|
store.draggedSprite.value = sprite;
|
||||||
store.dragOffset.x = x - sprite.x;
|
store.dragOffset.x = x - sprite.x;
|
||||||
store.dragOffset.y = y - sprite.y;
|
store.dragOffset.y = y - sprite.y;
|
||||||
@ -92,9 +82,7 @@ const handleMouseMove = (e: MouseEvent) => {
|
|||||||
const cellX = Math.floor(x / store.cellSize.width);
|
const cellX = Math.floor(x / store.cellSize.width);
|
||||||
const cellY = Math.floor(y / store.cellSize.height);
|
const cellY = Math.floor(y / store.cellSize.height);
|
||||||
|
|
||||||
if (canvasEl.value &&
|
if (canvasEl.value && cellX >= 0 && cellX < canvasEl.value.width / store.cellSize.width && cellY >= 0 && cellY < canvasEl.value.height / store.cellSize.height) {
|
||||||
cellX >= 0 && cellX < canvasEl.value.width / store.cellSize.width &&
|
|
||||||
cellY >= 0 && cellY < canvasEl.value.height / store.cellSize.height) {
|
|
||||||
isTooltipVisible.value = true;
|
isTooltipVisible.value = true;
|
||||||
tooltipText.value = `Cell: (${cellX}, ${cellY})`;
|
tooltipText.value = `Cell: (${cellX}, ${cellY})`;
|
||||||
tooltipPosition.value.x = e.clientX;
|
tooltipPosition.value.x = e.clientX;
|
||||||
|
@ -5,35 +5,32 @@
|
|||||||
'translate-y-0 opacity-100': notification.isVisible,
|
'translate-y-0 opacity-100': notification.isVisible,
|
||||||
'translate-y-24 opacity-0': !notification.isVisible,
|
'translate-y-24 opacity-0': !notification.isVisible,
|
||||||
'border-l-4 border-green-500': notification.type === 'success',
|
'border-l-4 border-green-500': notification.type === 'success',
|
||||||
'border-l-4 border-red-500': notification.type === 'error'
|
'border-l-4 border-red-500': notification.type === 'error',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="text-xl"
|
class="text-xl"
|
||||||
:class="{
|
:class="{
|
||||||
'fas fa-check-circle text-green-500': notification.type === 'success',
|
'fas fa-check-circle text-green-500': notification.type === 'success',
|
||||||
'fas fa-exclamation-circle text-red-500': notification.type === 'error'
|
'fas fa-exclamation-circle text-red-500': notification.type === 'error',
|
||||||
}"
|
}"
|
||||||
></i>
|
></i>
|
||||||
<span>{{ notification.message }}</span>
|
<span>{{ notification.message }}</span>
|
||||||
<button
|
<button @click="closeNotification" class="ml-2 text-gray-400 hover:text-gray-200">
|
||||||
@click="closeNotification"
|
|
||||||
class="ml-2 text-gray-400 hover:text-gray-200"
|
|
||||||
>
|
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue';
|
||||||
import { useSpritesheetStore } from '../composables/useSpritesheetStore'
|
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||||
|
|
||||||
const store = useSpritesheetStore()
|
const store = useSpritesheetStore();
|
||||||
|
|
||||||
const notification = computed(() => store.notification)
|
const notification = computed(() => store.notification);
|
||||||
|
|
||||||
const closeNotification = () => {
|
const closeNotification = () => {
|
||||||
store.notification.isVisible = false
|
store.notification.isVisible = false;
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
@ -1,13 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 transition-all duration-300" :class="{ 'opacity-0 invisible': !isModalOpen, 'opacity-100 visible': isModalOpen }" @click.self="closeModal">
|
||||||
class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 transition-all duration-300"
|
<div class="bg-gray-800 rounded-lg max-w-4xl max-h-[90vh] overflow-auto shadow-lg transform transition-transform duration-300" :class="{ '-translate-y-5': !isModalOpen, 'translate-y-0': isModalOpen }">
|
||||||
:class="{ 'opacity-0 invisible': !isModalOpen, 'opacity-100 visible': isModalOpen }"
|
|
||||||
@click.self="closeModal"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="bg-gray-800 rounded-lg max-w-4xl max-h-[90vh] overflow-auto shadow-lg transform transition-transform duration-300"
|
|
||||||
:class="{ '-translate-y-5': !isModalOpen, 'translate-y-0': isModalOpen }"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600">
|
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600">
|
||||||
<div class="flex items-center gap-2 text-lg font-semibold">
|
<div class="flex items-center gap-2 text-lg font-semibold">
|
||||||
<i class="fas fa-film text-blue-500"></i>
|
<i class="fas fa-film text-blue-500"></i>
|
||||||
@ -24,7 +17,9 @@
|
|||||||
<button
|
<button
|
||||||
@click="startAnimation"
|
@click="startAnimation"
|
||||||
:disabled="sprites.length === 0"
|
:disabled="sprites.length === 0"
|
||||||
:class="{'bg-blue-500 border-blue-500 hover:bg-blue-600 hover:border-blue-600': !animation.isPlaying}"
|
:class="{
|
||||||
|
'bg-blue-500 border-blue-500 hover:bg-blue-600 hover:border-blue-600': !animation.isPlaying,
|
||||||
|
}"
|
||||||
class="flex items-center gap-2 border rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
class="flex items-center gap-2 border rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<i class="fas fa-play"></i> Play
|
<i class="fas fa-play"></i> Play
|
||||||
@ -32,7 +27,9 @@
|
|||||||
<button
|
<button
|
||||||
@click="stopAnimation"
|
@click="stopAnimation"
|
||||||
:disabled="sprites.length === 0"
|
:disabled="sprites.length === 0"
|
||||||
:class="{'bg-blue-500 border-blue-500 hover:bg-blue-600 hover:border-blue-600': animation.isPlaying}"
|
:class="{
|
||||||
|
'bg-blue-500 border-blue-500 hover:bg-blue-600 hover:border-blue-600': animation.isPlaying,
|
||||||
|
}"
|
||||||
class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500"
|
class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500"
|
||||||
>
|
>
|
||||||
<i class="fas fa-pause"></i> Pause
|
<i class="fas fa-pause"></i> Pause
|
||||||
@ -44,15 +41,7 @@
|
|||||||
<label for="frame-slider">Frame:</label>
|
<label for="frame-slider">Frame:</label>
|
||||||
<span>{{ currentFrameDisplay }}</span>
|
<span>{{ currentFrameDisplay }}</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="range" id="frame-slider" v-model="currentFrame" :min="0" :max="Math.max(0, sprites.length - 1)" class="w-full h-1 bg-gray-600 rounded appearance-none focus:outline-none cursor-pointer" @input="handleFrameChange" />
|
||||||
type="range"
|
|
||||||
id="frame-slider"
|
|
||||||
v-model="currentFrame"
|
|
||||||
:min="0"
|
|
||||||
:max="Math.max(0, sprites.length - 1)"
|
|
||||||
class="w-full h-1 bg-gray-600 rounded appearance-none focus:outline-none cursor-pointer"
|
|
||||||
@input="handleFrameChange"
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 flex-grow">
|
<div class="flex flex-col gap-2 flex-grow">
|
||||||
@ -60,15 +49,7 @@
|
|||||||
<label for="framerate">Frame Rate:</label>
|
<label for="framerate">Frame Rate:</label>
|
||||||
<span>{{ animation.frameRate }} FPS</span>
|
<span>{{ animation.frameRate }} FPS</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type="range" id="framerate" v-model.number="animation.frameRate" min="1" max="30" class="w-full h-1 bg-gray-600 rounded appearance-none focus:outline-none cursor-pointer" @input="handleFrameRateChange" />
|
||||||
type="range"
|
|
||||||
id="framerate"
|
|
||||||
v-model.number="animation.frameRate"
|
|
||||||
min="1"
|
|
||||||
max="30"
|
|
||||||
class="w-full h-1 bg-gray-600 rounded appearance-none focus:outline-none cursor-pointer"
|
|
||||||
@input="handleFrameRateChange"
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -81,131 +62,134 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||||
import { useSpritesheetStore } from '../composables/useSpritesheetStore'
|
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||||
|
|
||||||
const store = useSpritesheetStore()
|
const store = useSpritesheetStore();
|
||||||
const animCanvas = ref<HTMLCanvasElement | null>(null)
|
const animCanvas = ref<HTMLCanvasElement | null>(null);
|
||||||
|
|
||||||
const isModalOpen = computed(() => store.isModalOpen.value)
|
const isModalOpen = computed(() => store.isModalOpen.value);
|
||||||
const sprites = computed(() => store.sprites.value)
|
const sprites = computed(() => store.sprites.value);
|
||||||
const animation = computed(() => store.animation)
|
const animation = computed(() => store.animation);
|
||||||
|
|
||||||
const currentFrame = ref(0)
|
const currentFrame = ref(0);
|
||||||
|
|
||||||
const currentFrameDisplay = computed(() => {
|
const currentFrameDisplay = computed(() => {
|
||||||
const totalFrames = Math.max(1, sprites.value.length)
|
const totalFrames = Math.max(1, sprites.value.length);
|
||||||
const frame = Math.min(currentFrame.value + 1, totalFrames)
|
const frame = Math.min(currentFrame.value + 1, totalFrames);
|
||||||
return `${frame} / ${totalFrames}`
|
return `${frame} / ${totalFrames}`;
|
||||||
})
|
});
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (!isModalOpen.value) return
|
if (!isModalOpen.value) return;
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
closeModal()
|
closeModal();
|
||||||
} else if (e.key === ' ' || e.key === 'Spacebar') {
|
} else if (e.key === ' ' || e.key === 'Spacebar') {
|
||||||
// Toggle play/pause
|
// Toggle play/pause
|
||||||
if (animation.value.isPlaying) {
|
if (animation.value.isPlaying) {
|
||||||
stopAnimation()
|
stopAnimation();
|
||||||
} else if (sprites.value.length > 0) {
|
} else if (sprites.value.length > 0) {
|
||||||
startAnimation()
|
startAnimation();
|
||||||
}
|
}
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
} else if (e.key === 'ArrowRight' && !animation.value.isPlaying && sprites.value.length > 0) {
|
} else if (e.key === 'ArrowRight' && !animation.value.isPlaying && sprites.value.length > 0) {
|
||||||
// Next frame
|
// Next frame
|
||||||
currentFrame.value = (currentFrame.value + 1) % sprites.value.length
|
currentFrame.value = (currentFrame.value + 1) % sprites.value.length;
|
||||||
updateFrame()
|
updateFrame();
|
||||||
} else if (e.key === 'ArrowLeft' && !animation.value.isPlaying && sprites.value.length > 0) {
|
} else if (e.key === 'ArrowLeft' && !animation.value.isPlaying && sprites.value.length > 0) {
|
||||||
// Previous frame
|
// Previous frame
|
||||||
currentFrame.value = (currentFrame.value - 1 + sprites.value.length) % sprites.value.length
|
currentFrame.value = (currentFrame.value - 1 + sprites.value.length) % sprites.value.length;
|
||||||
updateFrame()
|
updateFrame();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openModal = () => {
|
const openModal = () => {
|
||||||
if (sprites.value.length === 0) {
|
if (sprites.value.length === 0) {
|
||||||
store.showNotification('Please add sprites first', 'error')
|
store.showNotification('Please add sprites first', 'error');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
store.isModalOpen.value = true
|
store.isModalOpen.value = true;
|
||||||
|
|
||||||
// Show the current frame
|
// Show the current frame
|
||||||
if (!animation.value.isPlaying && sprites.value.length > 0) {
|
if (!animation.value.isPlaying && sprites.value.length > 0) {
|
||||||
store.renderAnimationFrame(currentFrame.value)
|
store.renderAnimationFrame(currentFrame.value);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
store.isModalOpen.value = false
|
store.isModalOpen.value = false;
|
||||||
|
|
||||||
// Stop animation if it's playing
|
// Stop animation if it's playing
|
||||||
if (animation.value.isPlaying) {
|
if (animation.value.isPlaying) {
|
||||||
stopAnimation()
|
stopAnimation();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const startAnimation = () => {
|
const startAnimation = () => {
|
||||||
if (sprites.value.length === 0) return
|
if (sprites.value.length === 0) return;
|
||||||
store.startAnimation()
|
store.startAnimation();
|
||||||
}
|
};
|
||||||
|
|
||||||
const stopAnimation = () => {
|
const stopAnimation = () => {
|
||||||
store.stopAnimation()
|
store.stopAnimation();
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleFrameChange = () => {
|
const handleFrameChange = () => {
|
||||||
// Stop any running animation
|
// Stop any running animation
|
||||||
if (animation.value.isPlaying) {
|
if (animation.value.isPlaying) {
|
||||||
stopAnimation()
|
stopAnimation();
|
||||||
}
|
|
||||||
updateFrame()
|
|
||||||
}
|
}
|
||||||
|
updateFrame();
|
||||||
|
};
|
||||||
|
|
||||||
const updateFrame = () => {
|
const updateFrame = () => {
|
||||||
animation.value.currentFrame = currentFrame.value
|
animation.value.currentFrame = currentFrame.value;
|
||||||
animation.value.manualUpdate = true
|
animation.value.manualUpdate = true;
|
||||||
store.renderAnimationFrame(currentFrame.value)
|
store.renderAnimationFrame(currentFrame.value);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleFrameRateChange = () => {
|
const handleFrameRateChange = () => {
|
||||||
// If animation is currently playing, restart it with the new frame rate
|
// If animation is currently playing, restart it with the new frame rate
|
||||||
if (animation.value.isPlaying) {
|
if (animation.value.isPlaying) {
|
||||||
stopAnimation()
|
stopAnimation();
|
||||||
startAnimation()
|
startAnimation();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (animCanvas.value) {
|
if (animCanvas.value) {
|
||||||
store.animation.canvas = animCanvas.value
|
store.animation.canvas = animCanvas.value;
|
||||||
store.animation.ctx = animCanvas.value.getContext('2d')
|
store.animation.ctx = animCanvas.value.getContext('2d');
|
||||||
|
|
||||||
// Initialize canvas size
|
// Initialize canvas size
|
||||||
animCanvas.value.width = 200
|
animCanvas.value.width = 200;
|
||||||
animCanvas.value.height = 200
|
animCanvas.value.height = 200;
|
||||||
|
|
||||||
// Setup keyboard shortcuts for the modal
|
// Setup keyboard shortcuts for the modal
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
})
|
});
|
||||||
|
|
||||||
// Keep currentFrame in sync with animation.currentFrame
|
// Keep currentFrame in sync with animation.currentFrame
|
||||||
watch(() => animation.value.currentFrame, (newVal) => {
|
watch(
|
||||||
currentFrame.value = newVal
|
() => animation.value.currentFrame,
|
||||||
})
|
newVal => {
|
||||||
|
currentFrame.value = newVal;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Expose openModal for external use
|
// Expose openModal for external use
|
||||||
defineExpose({ openModal })
|
defineExpose({ openModal });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
input[type="range"]::-webkit-slider-thumb {
|
input[type='range']::-webkit-slider-thumb {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
@ -214,7 +198,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="range"]::-moz-range-thumb {
|
input[type='range']::-moz-range-thumb {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
background: #0096ff;
|
background: #0096ff;
|
||||||
|
@ -40,32 +40,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="flex flex-wrap gap-3 mb-6">
|
<div class="flex flex-wrap gap-3 mb-6">
|
||||||
<button
|
<button @click="autoArrangeSprites" :disabled="sprites.length === 0" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">
|
||||||
@click="autoArrangeSprites"
|
|
||||||
:disabled="sprites.length === 0"
|
|
||||||
class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500"
|
|
||||||
>
|
|
||||||
<i class="fas fa-th"></i> Auto Arrange
|
<i class="fas fa-th"></i> Auto Arrange
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button @click="openPreviewModal" :disabled="sprites.length === 0" class="flex items-center gap-2 bg-blue-500 border border-blue-500 text-white rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:bg-blue-600 hover:border-blue-600">
|
||||||
@click="openPreviewModal"
|
|
||||||
:disabled="sprites.length === 0"
|
|
||||||
class="flex items-center gap-2 bg-blue-500 border border-blue-500 text-white rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:bg-blue-600 hover:border-blue-600"
|
|
||||||
>
|
|
||||||
<i class="fas fa-play"></i> Preview Animation
|
<i class="fas fa-play"></i> Preview Animation
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button @click="downloadSpritesheet" :disabled="sprites.length === 0" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">
|
||||||
@click="downloadSpritesheet"
|
|
||||||
:disabled="sprites.length === 0"
|
|
||||||
class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500"
|
|
||||||
>
|
|
||||||
<i class="fas fa-download"></i> Download
|
<i class="fas fa-download"></i> Download
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button @click="confirmClearAll" :disabled="sprites.length === 0" class="flex items-center gap-2 bg-red-600 border border-red-600 text-white rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:bg-red-700 hover:border-red-700">
|
||||||
@click="confirmClearAll"
|
|
||||||
:disabled="sprites.length === 0"
|
|
||||||
class="flex items-center gap-2 bg-red-600 border border-red-600 text-white rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:bg-red-700 hover:border-red-700"
|
|
||||||
>
|
|
||||||
<i class="fas fa-trash-alt"></i> Clear All
|
<i class="fas fa-trash-alt"></i> Clear All
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,39 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="sprites.length === 0" class="text-center text-gray-400 py-8">
|
<div v-if="sprites.length === 0" class="text-center text-gray-400 py-8">No sprites uploaded yet</div>
|
||||||
No sprites uploaded yet
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 max-h-72 overflow-y-auto pr-2">
|
<div v-else class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 max-h-72 overflow-y-auto pr-2">
|
||||||
<div
|
<div v-for="(sprite, index) in sprites" :key="sprite.id" @click="$emit('spriteClicked', sprite.id)" class="border border-gray-600 rounded bg-gray-700 p-2 text-center transition-all cursor-pointer hover:border-blue-500 hover:-translate-y-0.5 hover:shadow-md">
|
||||||
v-for="(sprite, index) in sprites"
|
<img :src="sprite.img.src" :alt="sprite.name" class="max-w-full max-h-16 mx-auto mb-2 bg-black bg-opacity-20 rounded" />
|
||||||
:key="sprite.id"
|
<div class="text-xs text-gray-400 truncate">{{ index + 1 }}. {{ truncateName(sprite.name) }}</div>
|
||||||
@click="$emit('spriteClicked', sprite.id)"
|
|
||||||
class="border border-gray-600 rounded bg-gray-700 p-2 text-center transition-all cursor-pointer hover:border-blue-500 hover:-translate-y-0.5 hover:shadow-md"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="sprite.img.src"
|
|
||||||
:alt="sprite.name"
|
|
||||||
class="max-w-full max-h-16 mx-auto mb-2 bg-black bg-opacity-20 rounded"
|
|
||||||
>
|
|
||||||
<div class="text-xs text-gray-400 truncate">
|
|
||||||
{{ index + 1 }}. {{ truncateName(sprite.name) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Sprite } from '../composables/useSpritesheetStore'
|
import type { Sprite } from '../composables/useSpritesheetStore';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
sprites: Sprite[]
|
sprites: Sprite[];
|
||||||
}>()
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
spriteClicked: [id: string]
|
spriteClicked: [id: string];
|
||||||
}>()
|
}>();
|
||||||
|
|
||||||
const truncateName = (name: string) => {
|
const truncateName = (name: string) => {
|
||||||
return name.length > 10 ? `${name.substring(0, 10)}...` : name
|
return name.length > 10 ? `${name.substring(0, 10)}...` : name;
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
@ -48,13 +48,13 @@ export function useSpritesheetStore() {
|
|||||||
lastFrameTime: 0,
|
lastFrameTime: 0,
|
||||||
animationId: null,
|
animationId: null,
|
||||||
slider: null,
|
slider: null,
|
||||||
manualUpdate: false
|
manualUpdate: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notification = reactive({
|
const notification = reactive({
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
message: '',
|
message: '',
|
||||||
type: 'success' as 'success' | 'error'
|
type: 'success' as 'success' | 'error',
|
||||||
});
|
});
|
||||||
|
|
||||||
function addSprites(newSprites: Sprite[]) {
|
function addSprites(newSprites: Sprite[]) {
|
||||||
@ -120,11 +120,7 @@ export function useSpritesheetStore() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sprites.value.forEach(sprite => {
|
sprites.value.forEach(sprite => {
|
||||||
ctx.value!.drawImage(
|
ctx.value!.drawImage(sprite.img, sprite.x, sprite.y);
|
||||||
sprite.img,
|
|
||||||
sprite.x,
|
|
||||||
sprite.y
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,12 +160,7 @@ export function useSpritesheetStore() {
|
|||||||
// Briefly flash the cell
|
// Briefly flash the cell
|
||||||
ctx.value.save();
|
ctx.value.save();
|
||||||
ctx.value.fillStyle = 'rgba(0, 150, 255, 0.3)';
|
ctx.value.fillStyle = 'rgba(0, 150, 255, 0.3)';
|
||||||
ctx.value.fillRect(
|
ctx.value.fillRect(cellX * cellSize.width, cellY * cellSize.height, cellSize.width, cellSize.height);
|
||||||
cellX * cellSize.width,
|
|
||||||
cellY * cellSize.height,
|
|
||||||
cellSize.width,
|
|
||||||
cellSize.height
|
|
||||||
);
|
|
||||||
ctx.value.restore();
|
ctx.value.restore();
|
||||||
|
|
||||||
// Reset after a short delay
|
// Reset after a short delay
|
||||||
@ -215,11 +206,7 @@ export function useSpritesheetStore() {
|
|||||||
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
|
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
|
||||||
|
|
||||||
sprites.value.forEach(sprite => {
|
sprites.value.forEach(sprite => {
|
||||||
tempCtx.drawImage(
|
tempCtx.drawImage(sprite.img, sprite.x, sprite.y);
|
||||||
sprite.img,
|
|
||||||
sprite.x,
|
|
||||||
sprite.y
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
@ -257,8 +244,7 @@ export function useSpritesheetStore() {
|
|||||||
function renderAnimationFrame(frameIndex: number) {
|
function renderAnimationFrame(frameIndex: number) {
|
||||||
if (sprites.value.length === 0 || !animation.canvas || !animation.ctx) return;
|
if (sprites.value.length === 0 || !animation.canvas || !animation.ctx) return;
|
||||||
|
|
||||||
if (animation.canvas.width !== cellSize.width ||
|
if (animation.canvas.width !== cellSize.width || animation.canvas.height !== cellSize.height) {
|
||||||
animation.canvas.height !== cellSize.height) {
|
|
||||||
animation.canvas.width = cellSize.width;
|
animation.canvas.width = cellSize.width;
|
||||||
animation.canvas.height = cellSize.height;
|
animation.canvas.height = cellSize.height;
|
||||||
}
|
}
|
||||||
@ -270,14 +256,10 @@ export function useSpritesheetStore() {
|
|||||||
const cellX = Math.floor(currentSprite.x / cellSize.width);
|
const cellX = Math.floor(currentSprite.x / cellSize.width);
|
||||||
const cellY = Math.floor(currentSprite.y / cellSize.height);
|
const cellY = Math.floor(currentSprite.y / cellSize.height);
|
||||||
|
|
||||||
const offsetX = currentSprite.x - (cellX * cellSize.width);
|
const offsetX = currentSprite.x - cellX * cellSize.width;
|
||||||
const offsetY = currentSprite.y - (cellY * cellSize.height);
|
const offsetY = currentSprite.y - cellY * cellSize.height;
|
||||||
|
|
||||||
animation.ctx.drawImage(
|
animation.ctx.drawImage(currentSprite.img, offsetX, offsetY);
|
||||||
currentSprite.img,
|
|
||||||
offsetX,
|
|
||||||
offsetY
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function animationLoop(timestamp?: number) {
|
function animationLoop(timestamp?: number) {
|
||||||
@ -338,6 +320,6 @@ export function useSpritesheetStore() {
|
|||||||
startAnimation,
|
startAnimation,
|
||||||
stopAnimation,
|
stopAnimation,
|
||||||
renderAnimationFrame,
|
renderAnimationFrame,
|
||||||
showNotification
|
showNotification,
|
||||||
};
|
};
|
||||||
}
|
}
|
14
src/main.ts
14
src/main.ts
@ -1,11 +1,11 @@
|
|||||||
import './assets/main.css'
|
import './assets/main.css';
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue';
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia';
|
||||||
import App from './App.vue'
|
import App from './App.vue';
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia());
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user