likes
This commit is contained in:
@@ -3,6 +3,7 @@ import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
|||||||
import Dialog from 'primevue/dialog'
|
import Dialog from 'primevue/dialog'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import Tag from 'primevue/tag'
|
import Tag from 'primevue/tag'
|
||||||
|
import { dataService } from '../services/dataService'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
@@ -23,11 +24,40 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:visible', 'reusePrompt', 'reuseAsset', 'useResultAsAsset'])
|
const emit = defineEmits(['update:visible', 'reusePrompt', 'reuseAsset', 'useResultAsAsset', 'liked'])
|
||||||
|
|
||||||
const previewIndex = ref(0)
|
const previewIndex = ref(0)
|
||||||
const previewImage = computed(() => props.previewImages[previewIndex.value] || null)
|
const previewImage = computed(() => props.previewImages[previewIndex.value] || null)
|
||||||
|
|
||||||
|
// Like state management
|
||||||
|
const isTogglingLike = ref(false)
|
||||||
|
const localLikedStates = ref({}) // id -> bool
|
||||||
|
|
||||||
|
const isLiked = computed(() => {
|
||||||
|
if (!previewImage.value?.gen) return false
|
||||||
|
const id = previewImage.value.gen.id || previewImage.value.gen._id
|
||||||
|
if (localLikedStates.value[id] !== undefined) return localLikedStates.value[id]
|
||||||
|
return previewImage.value.gen.is_liked || false
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleLike = async () => {
|
||||||
|
const id = previewImage.value?.gen?.id || previewImage.value?.gen?._id
|
||||||
|
if (!id || isTogglingLike.value) return
|
||||||
|
|
||||||
|
isTogglingLike.value = true
|
||||||
|
try {
|
||||||
|
const response = await dataService.toggleLike(id)
|
||||||
|
// Assume response returns the new state or we just toggle it
|
||||||
|
const newState = response.is_liked !== undefined ? response.is_liked : !isLiked.value
|
||||||
|
localLikedStates.value[id] = newState
|
||||||
|
emit('liked', { id, is_liked: newState })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to toggle like', e)
|
||||||
|
} finally {
|
||||||
|
isTogglingLike.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reset index when modal opens
|
// Reset index when modal opens
|
||||||
watch(() => props.visible, (newVal) => {
|
watch(() => props.visible, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
@@ -125,6 +155,13 @@ const onUseResultAsAsset = () => {
|
|||||||
class="max-w-full max-h-[85vh] object-contain shadow-2xl transition-transform duration-300"
|
class="max-w-full max-h-[85vh] object-contain shadow-2xl transition-transform duration-300"
|
||||||
draggable="false" />
|
draggable="false" />
|
||||||
|
|
||||||
|
<!-- Like Button Overlay -->
|
||||||
|
<button v-if="previewImage?.gen" @click.stop="toggleLike"
|
||||||
|
class="absolute top-6 left-6 z-30 w-12 h-12 rounded-full backdrop-blur-md flex items-center justify-center transition-all hover:scale-110 active:scale-90 border border-white/10"
|
||||||
|
:class="isLiked ? 'bg-pink-500 text-white border-pink-400 shadow-[0_0_20px_rgba(236,72,153,0.4)]' : 'bg-black/40 text-white/70 hover:text-white'">
|
||||||
|
<i :class="isLiked ? 'pi pi-heart-fill' : 'pi pi-heart'" class="text-xl"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Image Index Badge -->
|
<!-- Image Index Badge -->
|
||||||
<div v-if="previewImages.length > 1"
|
<div v-if="previewImages.length > 1"
|
||||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 z-20 bg-black/60 backdrop-blur-md text-white text-xs font-mono px-3 py-1.5 rounded-full border border-white/10">
|
class="absolute bottom-4 left-1/2 -translate-x-1/2 z-20 bg-black/60 backdrop-blur-md text-white text-xs font-mono px-3 py-1.5 rounded-full border border-white/10">
|
||||||
@@ -158,6 +195,13 @@ const onUseResultAsAsset = () => {
|
|||||||
<div v-if="previewImage?.gen" class="flex flex-col gap-2">
|
<div v-if="previewImage?.gen" class="flex flex-col gap-2">
|
||||||
<label class="text-[10px] font-bold text-slate-500 uppercase tracking-widest px-1">Actions</label>
|
<label class="text-[10px] font-bold text-slate-500 uppercase tracking-widest px-1">Actions</label>
|
||||||
<div class="grid grid-cols-1 gap-2">
|
<div class="grid grid-cols-1 gap-2">
|
||||||
|
<Button :label="isLiked ? 'Liked' : 'Like'" :icon="isLiked ? 'pi pi-heart-fill' : 'pi pi-heart'"
|
||||||
|
@click="toggleLike" :loading="isTogglingLike"
|
||||||
|
:class="[
|
||||||
|
'!text-sm !justify-start',
|
||||||
|
isLiked ? '!bg-pink-600/20 !border-pink-500/30 !text-pink-400 hover:!bg-pink-600/30' : '!bg-slate-800/50 !border-white/10 !text-slate-300 hover:!bg-slate-800'
|
||||||
|
]" />
|
||||||
|
|
||||||
<Button label="Reuse Prompt" icon="pi pi-refresh" @click="onReusePrompt"
|
<Button label="Reuse Prompt" icon="pi pi-refresh" @click="onReusePrompt"
|
||||||
class="!bg-violet-600/10 !border-violet-500/30 !text-violet-400 hover:!bg-violet-600/20 !text-sm !justify-start" />
|
class="!bg-violet-600/10 !border-violet-500/30 !text-violet-400 hover:!bg-violet-600/20 !text-sm !justify-start" />
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ export const dataService = {
|
|||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleLike: async (id) => {
|
||||||
|
const response = await api.post(`/generations/${id}/like`)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
|
||||||
deleteGeneration: async (id) => {
|
deleteGeneration: async (id) => {
|
||||||
const response = await api.delete(`/generations/${id}`)
|
const response = await api.delete(`/generations/${id}`)
|
||||||
return response.data
|
return response.data
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import ConfirmDialog from 'primevue/confirmdialog'
|
|||||||
import { useConfirm } from 'primevue/useconfirm'
|
import { useConfirm } from 'primevue/useconfirm'
|
||||||
import Dialog from 'primevue/dialog'
|
import Dialog from 'primevue/dialog'
|
||||||
|
|
||||||
|
import GenerationPreviewModal from '../components/GenerationPreviewModal.vue'
|
||||||
|
import { dataService } from '../services/dataService'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const albumStore = useAlbumStore()
|
const albumStore = useAlbumStore()
|
||||||
@@ -20,6 +23,32 @@ const confirm = useConfirm()
|
|||||||
const generations = ref([])
|
const generations = ref([])
|
||||||
const loadingGenerations = ref(false)
|
const loadingGenerations = ref(false)
|
||||||
|
|
||||||
|
// Preview Modal State
|
||||||
|
const isImagePreviewVisible = ref(false)
|
||||||
|
const previewIndex = ref(0)
|
||||||
|
const previewImages = computed(() => {
|
||||||
|
return generations.value.map(gen => ({
|
||||||
|
url: API_URL + '/assets/' + (gen.result_list?.[0] || ''),
|
||||||
|
assetId: gen.result_list?.[0],
|
||||||
|
is_liked: gen.liked_assets?.includes(gen.result_list?.[0]),
|
||||||
|
gen: gen
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleLiked = ({ id, is_liked }) => {
|
||||||
|
// Update local state in generations
|
||||||
|
generations.value.forEach(gen => {
|
||||||
|
if (gen.result_list?.includes(id)) {
|
||||||
|
if (!gen.liked_assets) gen.liked_assets = []
|
||||||
|
if (is_liked) {
|
||||||
|
if (!gen.liked_assets.includes(id)) gen.liked_assets.push(id)
|
||||||
|
} else {
|
||||||
|
gen.liked_assets = gen.liked_assets.filter(aid => aid !== id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Gen Picker State
|
// Gen Picker State
|
||||||
const isGenerationPickerVisible = ref(false)
|
const isGenerationPickerVisible = ref(false)
|
||||||
const availableGenerations = ref([])
|
const availableGenerations = ref([])
|
||||||
@@ -130,10 +159,9 @@ const removeGeneration = (gen) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Image Preview ---
|
// --- Image Preview ---
|
||||||
const isImagePreviewVisible = ref(false)
|
const openImagePreview = (gen) => {
|
||||||
const previewImage = ref(null)
|
const idx = generations.value.findIndex(g => g.id === gen.id)
|
||||||
const openImagePreview = (url) => {
|
previewIndex.value = idx >= 0 ? idx : 0
|
||||||
previewImage.value = { url }
|
|
||||||
isImagePreviewVisible.value = true
|
isImagePreviewVisible.value = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -191,11 +219,17 @@ const openImagePreview = (url) => {
|
|||||||
class="glass-panel rounded-xl overflow-hidden group relative transition-all hover:bg-white/5">
|
class="glass-panel rounded-xl overflow-hidden group relative transition-all hover:bg-white/5">
|
||||||
|
|
||||||
<div class="aspect-[2/3] w-full bg-slate-800 relative overflow-hidden cursor-pointer"
|
<div class="aspect-[2/3] w-full bg-slate-800 relative overflow-hidden cursor-pointer"
|
||||||
@click="gen.result_list && gen.result_list.length > 0 ? openImagePreview(API_URL + '/assets/' + gen.result_list[0]) : null">
|
@click="gen.result_list && gen.result_list.length > 0 ? openImagePreview(gen) : null">
|
||||||
<img v-if="gen.result_list && gen.result_list.length > 0"
|
<img v-if="gen.result_list && gen.result_list.length > 0"
|
||||||
:src="gen.result || API_URL + `/assets/${gen.result_list[0]}` + '?thumbnail=true'"
|
:src="gen.result || API_URL + `/assets/${gen.result_list[0]}` + '?thumbnail=true'"
|
||||||
class="w-full h-full object-cover" />
|
class="w-full h-full object-cover" />
|
||||||
|
|
||||||
|
<!-- Liked Badge -->
|
||||||
|
<div v-if="gen.liked_assets?.includes(gen.result_list?.[0])"
|
||||||
|
class="absolute top-2 right-2 z-10 w-6 h-6 rounded-full bg-pink-500 shadow-lg flex items-center justify-center border border-pink-400">
|
||||||
|
<i class="pi pi-heart-fill text-white text-[10px]"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Overlay Actions -->
|
<!-- Overlay Actions -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2 pointer-events-none">
|
class="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2 pointer-events-none">
|
||||||
@@ -273,16 +307,13 @@ const openImagePreview = (url) => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- Image Preview Modal -->
|
<!-- Image Preview Modal -->
|
||||||
<Dialog v-model:visible="isImagePreviewVisible" modal dismissableMask
|
<GenerationPreviewModal
|
||||||
:style="{ width: '90vw', maxWidth: '1000px', background: 'transparent', boxShadow: 'none' }"
|
v-model:visible="isImagePreviewVisible"
|
||||||
:pt="{ root: { class: '!bg-transparent !border-none !shadow-none' }, header: { class: '!hidden' }, content: { class: '!bg-transparent !p-0' } }">
|
:preview-images="previewImages"
|
||||||
<div class="relative flex items-center justify-center" @click="isImagePreviewVisible = false">
|
:initial-index="previewIndex"
|
||||||
<img v-if="previewImage" :src="previewImage.url"
|
:api-url="API_URL"
|
||||||
class="max-w-full max-h-[85vh] object-contain rounded-xl shadow-2xl" />
|
@liked="handleLiked"
|
||||||
<Button icon="pi pi-times" @click="isImagePreviewVisible = false" rounded text
|
/>
|
||||||
class="!absolute -top-4 -right-4 !text-white !bg-black/50 hover:!bg-black/70 !w-10 !h-10" />
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import InputText from 'primevue/inputtext'
|
|||||||
import Textarea from 'primevue/textarea'
|
import Textarea from 'primevue/textarea'
|
||||||
import Image from 'primevue/image'
|
import Image from 'primevue/image'
|
||||||
|
|
||||||
|
import GenerationPreviewModal from '../components/GenerationPreviewModal.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -26,6 +28,28 @@ const activeFilter = ref('all')
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const API_URL = import.meta.env.VITE_API_URL
|
const API_URL = import.meta.env.VITE_API_URL
|
||||||
|
|
||||||
|
// Preview Modal State
|
||||||
|
const isImagePreviewVisible = ref(false)
|
||||||
|
const previewIndex = ref(0)
|
||||||
|
const previewImages = computed(() => {
|
||||||
|
return assets.value.map(asset => ({
|
||||||
|
url: API_URL + (asset.url || asset.link),
|
||||||
|
assetId: asset.id,
|
||||||
|
is_liked: asset.is_liked || asset.liked,
|
||||||
|
name: asset.name,
|
||||||
|
created_at: asset.created_at,
|
||||||
|
// For assets, we might not have a full 'gen' object, but modal expects it for technical data
|
||||||
|
gen: asset.generation_id ? { id: asset.generation_id, prompt: asset.prompt } : null
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleLiked = ({ id, is_liked }: { id: string, is_liked: boolean }) => {
|
||||||
|
const asset = assets.value.find(a => a.id === id)
|
||||||
|
if (asset) {
|
||||||
|
asset.is_liked = is_liked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Albums Logic
|
// Albums Logic
|
||||||
const albumStore = useAlbumStore()
|
const albumStore = useAlbumStore()
|
||||||
const { albums, loading: albumsLoading } = storeToRefs(albumStore)
|
const { albums, loading: albumsLoading } = storeToRefs(albumStore)
|
||||||
@@ -34,7 +58,6 @@ const showCreateDialog = ref(false)
|
|||||||
const newAlbum = ref({ name: '', description: '' })
|
const newAlbum = ref({ name: '', description: '' })
|
||||||
const submittingAlbum = ref(false)
|
const submittingAlbum = ref(false)
|
||||||
|
|
||||||
const selectedAsset = ref<Asset | null>(null)
|
|
||||||
const isModalVisible = ref(false)
|
const isModalVisible = ref(false)
|
||||||
|
|
||||||
const first = ref(0)
|
const first = ref(0)
|
||||||
@@ -69,8 +92,9 @@ const handleFileUpload = async (event: Event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openModal = (asset: Asset) => {
|
const openModal = (asset: Asset) => {
|
||||||
selectedAsset.value = asset
|
const idx = assets.value.findIndex(a => a.id === asset.id)
|
||||||
isModalVisible.value = true
|
previewIndex.value = idx >= 0 ? idx : 0
|
||||||
|
isImagePreviewVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadAssets = async () => {
|
const loadAssets = async () => {
|
||||||
@@ -245,6 +269,12 @@ const formatDate = (dateString: string) => {
|
|||||||
:alt="asset.name"
|
:alt="asset.name"
|
||||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
|
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
|
||||||
|
|
||||||
|
<!-- Liked Badge -->
|
||||||
|
<div v-if="asset.is_liked || asset.liked"
|
||||||
|
class="absolute top-2 left-2 z-10 w-6 h-6 rounded-full bg-pink-500 shadow-lg flex items-center justify-center border border-pink-400">
|
||||||
|
<i class="pi pi-heart-fill text-white text-[10px]"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Type Badge -->
|
<!-- Type Badge -->
|
||||||
<div v-if="asset.type !== 'image'"
|
<div v-if="asset.type !== 'image'"
|
||||||
class="absolute top-2 right-2 bg-black/60 backdrop-blur-sm px-1.5 py-0.5 rounded text-[10px] uppercase font-bold text-white z-10 opacity-70 group-hover:opacity-100 transition-opacity">
|
class="absolute top-2 right-2 bg-black/60 backdrop-blur-sm px-1.5 py-0.5 rounded text-[10px] uppercase font-bold text-white z-10 opacity-70 group-hover:opacity-100 transition-opacity">
|
||||||
@@ -368,17 +398,13 @@ const formatDate = (dateString: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog v-model:visible="isModalVisible" modal dismissableMask header="Asset View"
|
<GenerationPreviewModal
|
||||||
:style="{ width: '90vw', maxWidth: '800px' }" class="glass-panel rounded-2xl">
|
v-model:visible="isImagePreviewVisible"
|
||||||
<div v-if="selectedAsset" class="flex flex-col items-center">
|
:preview-images="previewImages"
|
||||||
<img :src="selectedAsset.link ? API_URL + selectedAsset.link : (selectedAsset.url ? API_URL + selectedAsset.url : 'https://via.placeholder.com/800')"
|
:initial-index="previewIndex"
|
||||||
:alt="selectedAsset.name" class="max-w-full max-h-[70vh] rounded-xl object-contain shadow-2xl" />
|
:api-url="API_URL"
|
||||||
<div class="mt-6 text-center">
|
@liked="handleLiked"
|
||||||
<h2 class="text-2xl font-bold mb-2">{{ selectedAsset.name }}</h2>
|
/>
|
||||||
<p class="text-slate-400">{{ formatDate(selectedAsset.created_at) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<!-- Create Album Dialog -->
|
<!-- Create Album Dialog -->
|
||||||
<Dialog v-model:visible="showCreateDialog" modal header="Create New Album" :style="{ width: '500px' }"
|
<Dialog v-model:visible="showCreateDialog" modal header="Create New Album" :style="{ width: '500px' }"
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import Paginator from 'primevue/paginator'
|
|||||||
import MultiSelect from 'primevue/multiselect'
|
import MultiSelect from 'primevue/multiselect'
|
||||||
import Dropdown from 'primevue/dropdown'
|
import Dropdown from 'primevue/dropdown'
|
||||||
|
|
||||||
|
import GenerationPreviewModal from '../components/GenerationPreviewModal.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const character = ref(null)
|
const character = ref(null)
|
||||||
@@ -35,6 +37,51 @@ const historyFirst = ref(0)
|
|||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const API_URL = import.meta.env.VITE_API_URL
|
const API_URL = import.meta.env.VITE_API_URL
|
||||||
|
|
||||||
|
// Preview Modal State
|
||||||
|
const isImagePreviewVisible = ref(false)
|
||||||
|
const previewIndex = ref(0)
|
||||||
|
const previewImages = ref([])
|
||||||
|
|
||||||
|
const openImagePreview = (images, index = 0) => {
|
||||||
|
previewImages.value = images
|
||||||
|
previewIndex.value = index
|
||||||
|
isImagePreviewVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLiked = ({ id, is_liked }) => {
|
||||||
|
// Update local state in history
|
||||||
|
historyGenerations.value.forEach(gen => {
|
||||||
|
if (gen.result_list?.includes(id)) {
|
||||||
|
if (!gen.liked_assets) gen.liked_assets = []
|
||||||
|
if (is_liked) {
|
||||||
|
if (!gen.liked_assets.includes(id)) gen.liked_assets.push(id)
|
||||||
|
} else {
|
||||||
|
gen.liked_assets = gen.liked_assets.filter(aid => aid !== id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Update in character assets
|
||||||
|
characterAssets.value.forEach(asset => {
|
||||||
|
if (asset.id === id) {
|
||||||
|
asset.is_liked = is_liked
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Update in generatedResult if it's the one liked
|
||||||
|
if (generatedResult.value) {
|
||||||
|
if (generatedResult.value.type === 'assets') {
|
||||||
|
const found = generatedResult.value.assets.find(a => a.id === id)
|
||||||
|
if (found) {
|
||||||
|
if (!generatedResult.value.liked_assets) generatedResult.value.liked_assets = []
|
||||||
|
if (is_liked) {
|
||||||
|
if (!generatedResult.value.liked_assets.includes(id)) generatedResult.value.liked_assets.push(id)
|
||||||
|
} else {
|
||||||
|
generatedResult.value.liked_assets = generatedResult.value.liked_assets.filter(aid => aid !== id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selectedEnvironment = ref(null)
|
const selectedEnvironment = ref(null)
|
||||||
const isEnvModalVisible = ref(false)
|
const isEnvModalVisible = ref(false)
|
||||||
const isEnvAssetPickerVisible = ref(false)
|
const isEnvAssetPickerVisible = ref(false)
|
||||||
@@ -282,8 +329,14 @@ const openModal = (asset) => {
|
|||||||
toggleBulkSelection(asset.id)
|
toggleBulkSelection(asset.id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selectedAsset.value = asset
|
const idx = characterAssets.value.findIndex(a => a.id === asset.id)
|
||||||
isModalVisible.value = true
|
const images = characterAssets.value.map(a => ({
|
||||||
|
url: API_URL + a.url,
|
||||||
|
assetId: a.id,
|
||||||
|
is_liked: a.is_liked || a.liked,
|
||||||
|
gen: a.generation_id ? { id: a.generation_id, prompt: a.prompt } : null
|
||||||
|
}))
|
||||||
|
openImagePreview(images, idx >= 0 ? idx : 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleBulkSelection = (id) => {
|
const toggleBulkSelection = (id) => {
|
||||||
@@ -1267,7 +1320,7 @@ const handleGenerate = async () => {
|
|||||||
:class="gen.children.length > 2 ? 'grid-cols-4' : 'grid-cols-2'">
|
:class="gen.children.length > 2 ? 'grid-cols-4' : 'grid-cols-2'">
|
||||||
<div v-for="child in gen.children" :key="child.id"
|
<div v-for="child in gen.children" :key="child.id"
|
||||||
class="relative aspect-[9/16] rounded-md overflow-hidden bg-black/30 border border-white/5 group/child"
|
class="relative aspect-[9/16] rounded-md overflow-hidden bg-black/30 border border-white/5 group/child"
|
||||||
@click.stop="restoreGeneration(child)">
|
@click.stop="openImagePreview([{ url: API_URL + '/assets/' + child.result_list[0], assetId: child.result_list[0], is_liked: child.liked_assets?.includes(child.result_list[0]), gen: child }])">
|
||||||
|
|
||||||
<img v-if="child.result_list && child.result_list.length > 0"
|
<img v-if="child.result_list && child.result_list.length > 0"
|
||||||
:src="API_URL + '/assets/' + child.result_list[0] + '?thumbnail=true'"
|
:src="API_URL + '/assets/' + child.result_list[0] + '?thumbnail=true'"
|
||||||
@@ -1300,10 +1353,11 @@ const handleGenerate = async () => {
|
|||||||
<div v-else class="flex gap-3 w-full">
|
<div v-else class="flex gap-3 w-full">
|
||||||
<div class="w-12 h-12 rounded bg-black/40 border border-white/10 flex-shrink-0 mt-0.5 relative z-0"
|
<div class="w-12 h-12 rounded bg-black/40 border border-white/10 flex-shrink-0 mt-0.5 relative z-0"
|
||||||
@mouseenter="gen.result_list && gen.result_list[0] ? onThumbnailEnter($event, API_URL + '/assets/' + gen.result_list[0] + '?thumbnail=true') : null"
|
@mouseenter="gen.result_list && gen.result_list[0] ? onThumbnailEnter($event, API_URL + '/assets/' + gen.result_list[0] + '?thumbnail=true') : null"
|
||||||
@mouseleave="onThumbnailLeave">
|
@mouseleave="onThumbnailLeave"
|
||||||
|
@click.stop="openImagePreview([{ url: API_URL + '/assets/' + gen.result_list[0], assetId: gen.result_list[0], is_liked: gen.liked_assets?.includes(gen.result_list[0]), gen: gen }])">
|
||||||
<img v-if="gen.result_list && gen.result_list.length > 0"
|
<img v-if="gen.result_list && gen.result_list.length > 0"
|
||||||
:src="API_URL + '/assets/' + gen.result_list[0] + '?thumbnail=true'"
|
:src="API_URL + '/assets/' + gen.result_list[0] + '?thumbnail=true'"
|
||||||
class="w-full h-full object-cover rounded opacity-100" />
|
class="w-full h-full object-cover rounded opacity-100 cursor-pointer" />
|
||||||
<div v-else
|
<div v-else
|
||||||
class="w-full h-full flex items-center justify-center text-slate-700 overflow-hidden rounded">
|
class="w-full h-full flex items-center justify-center text-slate-700 overflow-hidden rounded">
|
||||||
<i class="pi pi-image text-lg" />
|
<i class="pi pi-image text-lg" />
|
||||||
@@ -1559,18 +1613,16 @@ const handleGenerate = async () => {
|
|||||||
Character not found.
|
Character not found.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog v-model:visible="isModalVisible" modal dismissableMask header="Asset View"
|
<GenerationPreviewModal
|
||||||
:style="{ width: '90vw', maxWidth: '800px' }" class="glass-panel rounded-2xl">
|
v-model:visible="isImagePreviewVisible"
|
||||||
<div v-if="selectedAsset" class="flex flex-col items-center">
|
:preview-images="previewImages"
|
||||||
<img :src="selectedAsset.link ? API_URL + selectedAsset.link : (selectedAsset.url ? API_URL + selectedAsset.url : 'https://via.placeholder.com/800')"
|
:initial-index="previewIndex"
|
||||||
:alt="selectedAsset.name"
|
:api-url="API_URL"
|
||||||
class="max-w-full max-h-[70vh] rounded-xl object-contain shadow-2xl" />
|
@reuse-prompt="reusePrompt"
|
||||||
<div class="mt-6 text-center">
|
@reuse-asset="reuseAsset"
|
||||||
<h2 class="text-2xl font-bold mb-2">{{ selectedAsset.name }}</h2>
|
@use-result-as-asset="useResultAsReference"
|
||||||
<p class="text-slate-400">{{ selectedAsset.type }}</p>
|
@liked="handleLiked"
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
<!-- Asset Selection Modal (Global) -->
|
<!-- Asset Selection Modal (Global) -->
|
||||||
<Dialog v-model:visible="isAssetSelectionVisible" modal header="Select Reference Assets"
|
<Dialog v-model:visible="isAssetSelectionVisible" modal header="Select Reference Assets"
|
||||||
:style="{ width: '80vw', maxWidth: '1000px' }" class="glass-panel rounded-2xl">
|
:style="{ width: '80vw', maxWidth: '1000px' }" class="glass-panel rounded-2xl">
|
||||||
|
|||||||
@@ -592,6 +592,8 @@ const allPreviewImages = computed(() => {
|
|||||||
for (const assetId of gen.result_list) {
|
for (const assetId of gen.result_list) {
|
||||||
images.push({
|
images.push({
|
||||||
url: API_URL + '/assets/' + assetId,
|
url: API_URL + '/assets/' + assetId,
|
||||||
|
assetId: assetId,
|
||||||
|
is_liked: gen.liked_assets?.includes(assetId),
|
||||||
genId: gen.id,
|
genId: gen.id,
|
||||||
prompt: gen.prompt,
|
prompt: gen.prompt,
|
||||||
gen: gen
|
gen: gen
|
||||||
@@ -602,6 +604,25 @@ const allPreviewImages = computed(() => {
|
|||||||
return images
|
return images
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleLiked = ({ id, is_liked }) => {
|
||||||
|
// Update local state in history
|
||||||
|
historyGenerations.value.forEach(gen => {
|
||||||
|
if (gen.id === id) {
|
||||||
|
gen.is_liked = is_liked
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleLike = async (gen) => {
|
||||||
|
if (!gen || !gen.id) return
|
||||||
|
try {
|
||||||
|
const response = await dataService.toggleLike(gen.id)
|
||||||
|
handleLiked({ id: gen.id, is_liked: response.is_liked })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to toggle like', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const openImagePreview = (url) => {
|
const openImagePreview = (url) => {
|
||||||
const idx = allPreviewImages.value.findIndex(img => img.url === url)
|
const idx = allPreviewImages.value.findIndex(img => img.url === url)
|
||||||
previewIndex.value = idx >= 0 ? idx : 0
|
previewIndex.value = idx >= 0 ? idx : 0
|
||||||
@@ -896,6 +917,12 @@ const confirmAddToAlbum = async () => {
|
|||||||
:src="API_URL + '/assets/' + child.result_list[0] + '?thumbnail=true'"
|
:src="API_URL + '/assets/' + child.result_list[0] + '?thumbnail=true'"
|
||||||
class="w-full h-full object-cover" />
|
class="w-full h-full object-cover" />
|
||||||
|
|
||||||
|
<!-- Child: Liked badge -->
|
||||||
|
<div v-if="child.is_liked"
|
||||||
|
class="absolute top-1 right-1 z-20 w-4 h-4 rounded-full bg-pink-500 shadow-lg flex items-center justify-center border border-pink-400">
|
||||||
|
<i class="pi pi-heart-fill text-white text-[6px]"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Child: processing -->
|
<!-- Child: processing -->
|
||||||
<div v-else-if="['processing', 'starting', 'running'].includes(child.status)"
|
<div v-else-if="['processing', 'starting', 'running'].includes(child.status)"
|
||||||
class="w-full h-full flex flex-col items-center justify-center relative overflow-hidden bg-slate-800/50">
|
class="w-full h-full flex flex-col items-center justify-center relative overflow-hidden bg-slate-800/50">
|
||||||
@@ -942,6 +969,10 @@ const confirmAddToAlbum = async () => {
|
|||||||
|
|
||||||
<!-- Top right: edit, delete -->
|
<!-- Top right: edit, delete -->
|
||||||
<div class="flex justify-end items-start gap-0.5">
|
<div class="flex justify-end items-start gap-0.5">
|
||||||
|
<Button :icon="child.is_liked ? 'pi pi-heart-fill' : 'pi pi-heart'" size="small"
|
||||||
|
class="!w-5 !h-5 !rounded-full !border-none !text-[8px] transition-colors"
|
||||||
|
:class="child.is_liked ? '!bg-pink-500 !text-white' : '!bg-white/20 !text-white hover:!bg-pink-500'"
|
||||||
|
@click.stop="toggleLike(child)" />
|
||||||
<Button icon="pi pi-pencil" size="small"
|
<Button icon="pi pi-pencil" size="small"
|
||||||
class="!w-5 !h-5 !rounded-full !bg-white/20 !border-none !text-white !text-[8px] hover:!bg-violet-500"
|
class="!w-5 !h-5 !rounded-full !bg-white/20 !border-none !text-white !text-[8px] hover:!bg-violet-500"
|
||||||
@click.stop="useResultAsAsset(child)" />
|
@click.stop="useResultAsAsset(child)" />
|
||||||
@@ -1000,6 +1031,12 @@ const confirmAddToAlbum = async () => {
|
|||||||
class="w-full h-full object-cover cursor-pointer"
|
class="w-full h-full object-cover cursor-pointer"
|
||||||
@click.stop="isSelectMode ? toggleImageSelection(item.result_list[0]) : openImagePreview(API_URL + '/assets/' + item.result_list[0])" />
|
@click.stop="isSelectMode ? toggleImageSelection(item.result_list[0]) : openImagePreview(API_URL + '/assets/' + item.result_list[0])" />
|
||||||
|
|
||||||
|
<!-- Liked badge for single item -->
|
||||||
|
<div v-if="item.is_liked"
|
||||||
|
class="absolute top-2 right-2 z-20 w-6 h-6 rounded-full bg-pink-500 shadow-lg flex items-center justify-center border border-pink-400">
|
||||||
|
<i class="pi pi-heart-fill text-white text-[10px]"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- FAILED: error display -->
|
<!-- FAILED: error display -->
|
||||||
<div v-else-if="item.status === 'failed'"
|
<div v-else-if="item.status === 'failed'"
|
||||||
class="w-full h-full flex flex-col items-center justify-between p-3 text-center bg-red-500/10 border border-red-500/20 relative group">
|
class="w-full h-full flex flex-col items-center justify-between p-3 text-center bg-red-500/10 border border-red-500/20 relative group">
|
||||||
@@ -1064,6 +1101,10 @@ const confirmAddToAlbum = async () => {
|
|||||||
<div
|
<div
|
||||||
class="flex justify-end items-start translate-y-[-10px] group-hover:translate-y-0 transition-transform duration-200 w-full z-10">
|
class="flex justify-end items-start translate-y-[-10px] group-hover:translate-y-0 transition-transform duration-200 w-full z-10">
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
|
<Button :icon="item.is_liked ? 'pi pi-heart-fill' : 'pi pi-heart'"
|
||||||
|
class="!w-6 !h-6 !rounded-full !border-none !text-[10px] transition-colors"
|
||||||
|
:class="item.is_liked ? '!bg-pink-500 !text-white' : '!bg-white/20 !text-white hover:!bg-pink-500'"
|
||||||
|
@click.stop="toggleLike(item)" />
|
||||||
<Button v-if="item.result_list && item.result_list.length > 0"
|
<Button v-if="item.result_list && item.result_list.length > 0"
|
||||||
icon="pi pi-pencil" v-tooltip.left="'Edit (Use Result)'"
|
icon="pi pi-pencil" v-tooltip.left="'Edit (Use Result)'"
|
||||||
class="!w-6 !h-6 !rounded-full !bg-white/20 !border-none !text-white text-[10px] hover:!bg-violet-500"
|
class="!w-6 !h-6 !rounded-full !bg-white/20 !border-none !text-white text-[10px] hover:!bg-violet-500"
|
||||||
@@ -1370,6 +1411,7 @@ const confirmAddToAlbum = async () => {
|
|||||||
@reuse-prompt="reusePrompt"
|
@reuse-prompt="reusePrompt"
|
||||||
@reuse-asset="reuseAsset"
|
@reuse-asset="reuseAsset"
|
||||||
@use-result-as-asset="useResultAsAsset"
|
@use-result-as-asset="useResultAsAsset"
|
||||||
|
@liked="handleLiked"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dialog v-model:visible="isAssetPickerVisible" modal header="Select Assets"
|
<Dialog v-model:visible="isAssetPickerVisible" modal header="Select Assets"
|
||||||
|
|||||||
@@ -674,6 +674,11 @@ const groupedGenerations = computed(() => {
|
|||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getChildByAssetId = (group, assetId) => {
|
||||||
|
if (!group.isGroup) return group;
|
||||||
|
return group.children.find(c => c.result_list?.includes(assetId)) || group;
|
||||||
|
}
|
||||||
|
|
||||||
const hasActiveGenerations = computed(() => {
|
const hasActiveGenerations = computed(() => {
|
||||||
return generations.value.some(g => ['processing', 'starting', 'running'].includes(g.status))
|
return generations.value.some(g => ['processing', 'starting', 'running'].includes(g.status))
|
||||||
})
|
})
|
||||||
@@ -688,7 +693,8 @@ const allGalleryImages = computed(() => {
|
|||||||
assetId,
|
assetId,
|
||||||
url: API_URL + '/assets/' + assetId,
|
url: API_URL + '/assets/' + assetId,
|
||||||
thumbnailUrl: API_URL + '/assets/' + assetId + '?thumbnail=true',
|
thumbnailUrl: API_URL + '/assets/' + assetId + '?thumbnail=true',
|
||||||
gen
|
gen,
|
||||||
|
is_liked: gen.liked_assets?.includes(assetId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -889,6 +895,25 @@ async function confirmAddToPlan() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLiked = ({ id, is_liked }) => {
|
||||||
|
// Update local state in generations
|
||||||
|
generations.value.forEach(gen => {
|
||||||
|
if (gen.id === id) {
|
||||||
|
gen.is_liked = is_liked
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleLike = async (gen) => {
|
||||||
|
if (!gen || !gen.id) return
|
||||||
|
try {
|
||||||
|
const response = await dataService.toggleLike(gen.id)
|
||||||
|
handleLiked({ id: gen.id, is_liked: response.is_liked })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to toggle like', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Exit select mode when switching to feed
|
// Exit select mode when switching to feed
|
||||||
watch(viewMode, (v) => {
|
watch(viewMode, (v) => {
|
||||||
if (v !== 'gallery') {
|
if (v !== 'gallery') {
|
||||||
@@ -994,6 +1019,10 @@ watch(viewMode, (v) => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex gap-1 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity">
|
class="flex gap-1 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<Button :icon="gen.is_liked ? 'pi pi-heart-fill' : 'pi pi-heart'" text rounded size="small"
|
||||||
|
class="!w-7 !h-7 !p-0"
|
||||||
|
:class="gen.is_liked ? '!text-pink-500' : '!text-slate-400 hover:!text-pink-500'"
|
||||||
|
v-tooltip.top="gen.is_liked ? 'Unlike' : 'Like'" @click="toggleLike(gen)" />
|
||||||
<Button icon="pi pi-copy" text rounded size="small"
|
<Button icon="pi pi-copy" text rounded size="small"
|
||||||
class="!w-7 !h-7 !text-slate-400 hover:!text-white"
|
class="!w-7 !h-7 !text-slate-400 hover:!text-white"
|
||||||
v-tooltip.top="'Reuse Prompt'" @click="reusePrompt(gen)" />
|
v-tooltip.top="'Reuse Prompt'" @click="reusePrompt(gen)" />
|
||||||
@@ -1016,19 +1045,32 @@ watch(viewMode, (v) => {
|
|||||||
class="relative group/img cursor-pointer aspect-[4/3]">
|
class="relative group/img cursor-pointer aspect-[4/3]">
|
||||||
<img :src="API_URL + '/assets/' + res + '?thumbnail=true'"
|
<img :src="API_URL + '/assets/' + res + '?thumbnail=true'"
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
@click="openImagePreview(gen.result_list.map(r => ({ url: API_URL + '/assets/' + r, gen: gen })), resIdx)" />
|
@click="openImagePreview(gen.result_list.map(r => ({ url: API_URL + '/assets/' + r, gen: getChildByAssetId(gen, r), assetId: r, is_liked: getChildByAssetId(gen, r).is_liked })), resIdx)" />
|
||||||
|
|
||||||
|
<!-- Liked indicator -->
|
||||||
|
<div v-if="getChildByAssetId(gen, res).is_liked"
|
||||||
|
class="absolute top-1.5 left-1.5 w-5 h-5 rounded-full bg-pink-500 shadow-lg flex items-center justify-center border border-pink-400 z-10">
|
||||||
|
<i class="pi pi-heart-fill text-white text-[8px]"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Per-image hover overlay -->
|
<!-- Per-image hover overlay -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover/img:opacity-100 transition-opacity duration-200 pointer-events-none">
|
class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover/img:opacity-100 transition-opacity duration-200 pointer-events-none">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-1 right-1 flex gap-1 opacity-0 group-hover/img:opacity-100 transition-opacity duration-200">
|
class="absolute bottom-1 right-1 flex gap-1 opacity-0 group-hover/img:opacity-100 transition-opacity duration-200">
|
||||||
|
<button @click.stop="toggleLike(getChildByAssetId(gen, res))"
|
||||||
|
class="w-6 h-6 rounded-md backdrop-blur-sm flex items-center justify-center text-white transition-all hover:scale-110 shadow-lg"
|
||||||
|
:class="getChildByAssetId(gen, res).is_liked ? 'bg-pink-500 hover:bg-pink-400' : 'bg-slate-700/80 hover:bg-pink-500'"
|
||||||
|
v-tooltip.top="getChildByAssetId(gen, res).is_liked ? 'Unlike' : 'Like'">
|
||||||
|
<i :class="getChildByAssetId(gen, res).is_liked ? 'pi pi-heart-fill' : 'pi pi-heart'" style="font-size: 10px"></i>
|
||||||
|
</button>
|
||||||
<button @click.stop="setAsReference(res)"
|
<button @click.stop="setAsReference(res)"
|
||||||
class="w-6 h-6 rounded-md bg-violet-600/80 hover:bg-violet-500 backdrop-blur-sm flex items-center justify-center text-white transition-all hover:scale-110 shadow-lg"
|
class="w-6 h-6 rounded-md bg-violet-600/80 hover:bg-violet-500 backdrop-blur-sm flex items-center justify-center text-white transition-all hover:scale-110 shadow-lg"
|
||||||
v-tooltip.top="'Use as Reference'">
|
v-tooltip.top="'Use as Reference'">
|
||||||
<i class="pi pi-pencil" style="font-size: 10px"></i>
|
<i class="pi pi-pencil" style="font-size: 10px"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click.stop="deleteAssetFromGeneration(gen, res)"
|
<button @click.stop="deleteAssetFromGeneration(getChildByAssetId(gen, res), res)"
|
||||||
class="w-6 h-6 rounded-md bg-red-600/80 hover:bg-red-500 backdrop-blur-sm flex items-center justify-center text-white transition-all hover:scale-110 shadow-lg"
|
class="w-6 h-6 rounded-md bg-red-600/80 hover:bg-red-500 backdrop-blur-sm flex items-center justify-center text-white transition-all hover:scale-110 shadow-lg"
|
||||||
v-tooltip.top="'Remove Image'">
|
v-tooltip.top="'Remove Image'">
|
||||||
<i class="pi pi-trash" style="font-size: 10px"></i>
|
<i class="pi pi-trash" style="font-size: 10px"></i>
|
||||||
@@ -1099,6 +1141,12 @@ watch(viewMode, (v) => {
|
|||||||
|
|
||||||
<img :src="img.thumbnailUrl" class="w-full h-full object-cover" />
|
<img :src="img.thumbnailUrl" class="w-full h-full object-cover" />
|
||||||
|
|
||||||
|
<!-- Liked Badge -->
|
||||||
|
<div v-if="img.is_liked"
|
||||||
|
class="absolute top-2 right-2 z-10 w-6 h-6 rounded-full bg-pink-500 shadow-lg flex items-center justify-center border border-pink-400">
|
||||||
|
<i class="pi pi-heart-fill text-white text-[10px]"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Selection checkmark (always visible in select mode) -->
|
<!-- Selection checkmark (always visible in select mode) -->
|
||||||
<div v-if="isSelectMode"
|
<div v-if="isSelectMode"
|
||||||
class="absolute top-2 left-2 w-7 h-7 rounded-full flex items-center justify-center transition-all shadow-lg z-10"
|
class="absolute top-2 left-2 w-7 h-7 rounded-full flex items-center justify-center transition-all shadow-lg z-10"
|
||||||
@@ -1111,6 +1159,11 @@ watch(viewMode, (v) => {
|
|||||||
class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-end p-3">
|
class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-end p-3">
|
||||||
<p class="text-xs text-white line-clamp-2 mb-2">{{ img.gen.prompt }}</p>
|
<p class="text-xs text-white line-clamp-2 mb-2">{{ img.gen.prompt }}</p>
|
||||||
<div class="flex gap-2 justify-end">
|
<div class="flex gap-2 justify-end">
|
||||||
|
<Button :icon="img.gen.is_liked ? 'pi pi-heart-fill' : 'pi pi-heart'" rounded text size="small"
|
||||||
|
class="!text-white transition-colors"
|
||||||
|
:class="img.gen.is_liked ? '!text-pink-500' : 'hover:!text-pink-500 hover:!bg-white/20'"
|
||||||
|
v-tooltip.top="img.gen.is_liked ? 'Unlike' : 'Like'"
|
||||||
|
@click.stop="toggleLike(img.gen)" />
|
||||||
<Button icon="pi pi-pencil" rounded text size="small"
|
<Button icon="pi pi-pencil" rounded text size="small"
|
||||||
class="!text-white hover:!bg-white/20" v-tooltip.top="'Use as Reference'"
|
class="!text-white hover:!bg-white/20" v-tooltip.top="'Use as Reference'"
|
||||||
@click.stop="setAsReference(img.assetId)" />
|
@click.stop="setAsReference(img.assetId)" />
|
||||||
@@ -1425,6 +1478,7 @@ watch(viewMode, (v) => {
|
|||||||
@reuse-prompt="reusePrompt"
|
@reuse-prompt="reusePrompt"
|
||||||
@reuse-asset="reuseAssets"
|
@reuse-asset="reuseAssets"
|
||||||
@use-result-as-asset="useResultAsAsset"
|
@use-result-as-asset="useResultAsAsset"
|
||||||
|
@liked="handleLiked"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Add to Content Plan Dialog -->
|
<!-- Add to Content Plan Dialog -->
|
||||||
|
|||||||
@@ -338,11 +338,34 @@ const handleImprovePrompt = async () => {
|
|||||||
const isImagePreviewVisible = ref(false)
|
const isImagePreviewVisible = ref(false)
|
||||||
const previewImage = ref(null)
|
const previewImage = ref(null)
|
||||||
|
|
||||||
const openImagePreview = (url, name = 'Image Preview', createdAt = null, gen = null) => {
|
const openImagePreview = (url, name = 'Image Preview', createdAt = null, gen = null, assetId = null, isLiked = false) => {
|
||||||
previewImage.value = { url, name, createdAt, gen }
|
previewImage.value = { url, name, createdAt, gen, assetId, is_liked: isLiked }
|
||||||
isImagePreviewVisible.value = true
|
isImagePreviewVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLiked = ({ id, is_liked }) => {
|
||||||
|
// Update local state in history
|
||||||
|
historyGenerations.value.forEach(gen => {
|
||||||
|
if (gen.result_list?.includes(id)) {
|
||||||
|
if (!gen.liked_assets) gen.liked_assets = []
|
||||||
|
if (is_liked) {
|
||||||
|
if (!gen.liked_assets.includes(id)) gen.liked_assets.push(id)
|
||||||
|
} else {
|
||||||
|
gen.liked_assets = gen.liked_assets.filter(aid => aid !== id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Also update generatedResult if it's the one liked
|
||||||
|
if (generatedResult.value && generatedResult.value.id === id || generatedResult.value?.result_list?.includes(id)) {
|
||||||
|
if (!generatedResult.value.liked_assets) generatedResult.value.liked_assets = []
|
||||||
|
if (is_liked) {
|
||||||
|
if (!generatedResult.value.liked_assets.includes(id)) generatedResult.value.liked_assets.push(id)
|
||||||
|
} else {
|
||||||
|
generatedResult.value.liked_assets = generatedResult.value.liked_assets.filter(aid => aid !== id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const undoImprovePrompt = () => {
|
const undoImprovePrompt = () => {
|
||||||
if (previousPrompt.value) {
|
if (previousPrompt.value) {
|
||||||
const temp = prompt.value
|
const temp = prompt.value
|
||||||
@@ -596,7 +619,7 @@ onMounted(() => {
|
|||||||
v-if="generatedResult.type === 'assets' && generatedResult.assets && generatedResult.assets.length > 0">
|
v-if="generatedResult.type === 'assets' && generatedResult.assets && generatedResult.assets.length > 0">
|
||||||
<!-- Displaying the first asset as main preview -->
|
<!-- Displaying the first asset as main preview -->
|
||||||
<img :src="API_URL + '/assets/' + generatedResult.assets[0].id"
|
<img :src="API_URL + '/assets/' + generatedResult.assets[0].id"
|
||||||
@click="openImagePreview(API_URL + '/assets/' + generatedResult.assets[0].id, 'Generated Result', new Date().toISOString(), generatedResult)"
|
@click="openImagePreview(API_URL + '/assets/' + generatedResult.assets[0].id, 'Generated Result', new Date().toISOString(), generatedResult, generatedResult.assets[0].id, generatedResult.liked_assets?.includes(generatedResult.assets[0].id))"
|
||||||
class="w-full h-full object-contain cursor-pointer hover:scale-[1.01] transition-transform duration-300" />
|
class="w-full h-full object-contain cursor-pointer hover:scale-[1.01] transition-transform duration-300" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -657,13 +680,20 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div class="flex-1 overflow-y-auto pr-2 custom-scrollbar flex flex-col gap-2">
|
<div class="flex-1 overflow-y-auto pr-2 custom-scrollbar flex flex-col gap-2">
|
||||||
<div v-for="gen in historyGenerations" :key="gen.id"
|
<div v-for="gen in historyGenerations" :key="gen.id"
|
||||||
class="glass-panel p-2 rounded-lg border border-white/5 flex flex-col gap-2 hover:bg-white/10 transition-colors group">
|
class="glass-panel p-2 rounded-lg border border-white/5 flex flex-col gap-2 hover:bg-white/10 transition-colors group relative">
|
||||||
|
<!-- Liked badge on history item -->
|
||||||
|
<div v-if="gen.liked_assets?.length > 0"
|
||||||
|
class="absolute -top-1 -right-1 w-5 h-5 rounded-full bg-pink-500 shadow-lg flex items-center justify-center border border-pink-400 z-10">
|
||||||
|
<i class="pi pi-heart-fill text-white text-[8px]"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3 items-start cursor-pointer" @click="restoreGeneration(gen)">
|
<div class="flex gap-3 items-start cursor-pointer" @click="restoreGeneration(gen)">
|
||||||
<div
|
<div
|
||||||
class="w-12 h-12 rounded bg-black/40 border border-white/10 overflow-hidden flex-shrink-0 mt-0.5">
|
class="w-12 h-12 rounded bg-black/40 border border-white/10 overflow-hidden flex-shrink-0 mt-0.5 relative group/hist">
|
||||||
<img v-if="gen.result_list && gen.result_list.length > 0"
|
<img v-if="gen.result_list && gen.result_list.length > 0"
|
||||||
:src="API_URL + '/assets/' + gen.result_list[0] + '?thumbnail=true'"
|
:src="API_URL + '/assets/' + gen.result_list[0] + '?thumbnail=true'"
|
||||||
class="w-full h-full object-cover" />
|
class="w-full h-full object-cover"
|
||||||
|
@click.stop="openImagePreview(API_URL + '/assets/' + gen.result_list[0], 'History Entry', gen.created_at, gen, gen.result_list[0], gen.liked_assets?.includes(gen.result_list[0]))" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-xs text-slate-300 truncate font-medium">{{ gen.prompt }}</p>
|
<p class="text-xs text-slate-300 truncate font-medium">{{ gen.prompt }}</p>
|
||||||
@@ -757,6 +787,7 @@ onMounted(() => {
|
|||||||
@reuse-prompt="reusePrompt"
|
@reuse-prompt="reusePrompt"
|
||||||
@reuse-asset="reuseAsset"
|
@reuse-asset="reuseAsset"
|
||||||
@use-result-as-asset="useResultAsReference"
|
@use-result-as-asset="useResultAsReference"
|
||||||
|
@liked="handleLiked"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user