[FEAT] Add sharing function, UI enhancement
This commit is contained in:
119
src/components/ShareModal.vue
Normal file
119
src/components/ShareModal.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<Modal :is-open="isOpen" @close="close" title="Share spritesheet" :initialWidth="500" :initialHeight="280">
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex-1 overflow-auto p-4 space-y-4 dark:bg-gray-800">
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center py-8">
|
||||
<i class="fas fa-circle-notch fa-spin text-3xl text-gray-400 dark:text-gray-500 mb-3"></i>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">Uploading spritesheet...</p>
|
||||
</div>
|
||||
|
||||
<!-- Success state -->
|
||||
<div v-else-if="shareUrl" class="space-y-4">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">Your spritesheet is ready to share! Copy the link below:</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="text" :value="shareUrl" readonly class="flex-1 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 text-sm font-mono" @focus="($event.target as HTMLInputElement).select()" />
|
||||
<button @click="copyToClipboard" class="px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium transition-colors" :title="copied ? 'Copied!' : 'Copy to clipboard'">
|
||||
<i :class="copied ? 'fas fa-check' : 'fas fa-copy'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="copied" class="text-sm text-green-600 dark:text-green-400">Link copied to clipboard!</p>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div v-else-if="error" class="space-y-4">
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
|
||||
<button @click="retry" class="px-4 py-2 rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-100 text-sm font-medium transition-colors"><i class="fas fa-redo mr-2"></i>Try again</button>
|
||||
</div>
|
||||
|
||||
<!-- Initial state (shouldn't normally be visible) -->
|
||||
<div v-else class="flex flex-col items-center justify-center py-8">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">Preparing to share...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 p-3 flex justify-end">
|
||||
<button type="button" class="px-4 py-2 rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-100" @click="close">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import Modal from './utilities/Modal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
shareFunction: () => Promise<{ id: string; url: string }>;
|
||||
}>();
|
||||
const emit = defineEmits<{ (e: 'close'): void }>();
|
||||
|
||||
const loading = ref(false);
|
||||
const shareUrl = ref('');
|
||||
const error = ref('');
|
||||
const copied = ref(false);
|
||||
|
||||
const close = () => emit('close');
|
||||
|
||||
const performShare = async () => {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
shareUrl.value = '';
|
||||
copied.value = false;
|
||||
|
||||
try {
|
||||
const result = await props.shareFunction();
|
||||
shareUrl.value = result.url;
|
||||
} catch (e: any) {
|
||||
console.error('Failed to share spritesheet:', e);
|
||||
error.value = 'Failed to share spritesheet. Please try again later.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const retry = () => {
|
||||
performShare();
|
||||
};
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl.value);
|
||||
copied.value = true;
|
||||
setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 2000);
|
||||
} catch {
|
||||
// Fallback for older browsers
|
||||
const input = document.createElement('input');
|
||||
input.value = shareUrl.value;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(input);
|
||||
copied.value = true;
|
||||
setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// Start sharing when modal opens
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
isOpen => {
|
||||
if (isOpen) {
|
||||
performShare();
|
||||
} else {
|
||||
// Reset state when closing
|
||||
loading.value = false;
|
||||
shareUrl.value = '';
|
||||
error.value = '';
|
||||
copied.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="spritesheet-preview w-full h-full">
|
||||
<div class="flex flex-col lg:flex-row gap-4 h-full">
|
||||
<div class="flex-1 min-w-0 flex flex-col">
|
||||
<div class="flex flex-col lg:flex-row gap-4 h-full min-h-0">
|
||||
<div class="flex-1 min-w-0 flex flex-col min-h-0">
|
||||
<div
|
||||
class="relative bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg overflow-auto flex-1 min-h-[300px] max-h-[calc(100vh-12rem)] shadow-sm hover:shadow-md transition-shadow duration-200"
|
||||
class="relative bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg overflow-auto flex-1 min-h-[200px] sm:min-h-[300px] max-h-[50vh] lg:max-h-none shadow-sm hover:shadow-md transition-shadow duration-200"
|
||||
@mousemove="drag"
|
||||
@mouseup="stopDrag"
|
||||
@mouseleave="stopDrag"
|
||||
@@ -82,10 +82,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:w-80 xl:w-96 flex-shrink-0">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden">
|
||||
<div class="lg:w-80 xl:w-96 flex-shrink-0 lg:h-full lg:min-h-0 flex flex-col">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden flex-1 flex flex-col lg:overflow-y-auto">
|
||||
<!-- Playback Controls -->
|
||||
<div class="p-4 border-b border-gray-100 dark:border-gray-700">
|
||||
<div class="p-4 border-b border-gray-100 dark:border-gray-700 flex-shrink-0">
|
||||
<h3 class="text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-3">Playback</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="togglePlayback" class="flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2.5 rounded-lg transition-all cursor-pointer flex-1 shadow-sm active:scale-95">
|
||||
@@ -108,7 +108,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Animation Settings -->
|
||||
<div class="p-4 border-b border-gray-100 dark:border-gray-700 space-y-5">
|
||||
<div class="p-4 border-b border-gray-100 dark:border-gray-700 space-y-5 flex-shrink-0">
|
||||
<h3 class="text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-1">Animation</h3>
|
||||
|
||||
<!-- Frame Navigation -->
|
||||
@@ -131,7 +131,7 @@
|
||||
</div>
|
||||
|
||||
<!-- View Options -->
|
||||
<div class="p-4 space-y-5">
|
||||
<div class="p-4 space-y-5 flex-1">
|
||||
<h3 class="text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-1">View Options</h3>
|
||||
|
||||
<!-- Zoom Control -->
|
||||
@@ -185,7 +185,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Current frame offset display -->
|
||||
<div v-if="currentFrameSprite" class="px-4 pb-4">
|
||||
<div v-if="currentFrameSprite" class="px-4 pb-4 flex-shrink-0">
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-100 dark:border-gray-700 flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Offset</span>
|
||||
<span class="text-xs font-mono font-bold text-gray-700 dark:text-gray-200">X: {{ currentFrameSprite.x }} <span class="text-gray-300 dark:text-gray-600 mx-1">|</span> Y: {{ currentFrameSprite.y }}</span>
|
||||
@@ -193,7 +193,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Frame Selection (when Compare sprites is enabled) -->
|
||||
<div v-if="showAllSprites" class="border-t border-gray-100 dark:border-gray-700 p-4">
|
||||
<div v-if="showAllSprites" class="border-t border-gray-100 dark:border-gray-700 p-4 flex-shrink-0">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-wider">Visible Frames</h3>
|
||||
<div class="flex gap-1">
|
||||
@@ -513,4 +513,36 @@
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for controls panel */
|
||||
.custom-scrollbar::-webkit-scrollbar,
|
||||
.lg\:overflow-y-auto::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track,
|
||||
.lg\:overflow-y-auto::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb,
|
||||
.lg\:overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover,
|
||||
.lg\:overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(156, 163, 175, 0.8);
|
||||
}
|
||||
|
||||
:global(.dark) .custom-scrollbar::-webkit-scrollbar-thumb,
|
||||
:global(.dark) .lg\:overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(75, 85, 99, 0.5);
|
||||
}
|
||||
|
||||
:global(.dark) .custom-scrollbar::-webkit-scrollbar-thumb:hover,
|
||||
:global(.dark) .lg\:overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(75, 85, 99, 0.8);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user