albums
This commit is contained in:
274
src/views/AlbumDetailView.vue
Normal file
274
src/views/AlbumDetailView.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAlbumStore } from '../stores/albums'
|
||||
import { albumService } from '../services/albumService'
|
||||
import { aiService } from '../services/aiService'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import Button from 'primevue/button'
|
||||
import ConfirmDialog from 'primevue/confirmdialog'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import Image from 'primevue/image'
|
||||
import Dialog from 'primevue/dialog'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const albumStore = useAlbumStore()
|
||||
const { currentAlbum, loading } = storeToRefs(albumStore)
|
||||
const confirm = useConfirm()
|
||||
|
||||
const generations = ref([])
|
||||
const loadingGenerations = ref(false)
|
||||
|
||||
// Gen Picker State
|
||||
const isGenerationPickerVisible = ref(false)
|
||||
const availableGenerations = ref([])
|
||||
const loadingAvailableGenerations = ref(false)
|
||||
const selectedGenerations = ref([])
|
||||
const API_URL = import.meta.env.VITE_API_URL
|
||||
|
||||
onMounted(async () => {
|
||||
const id = route.params.id
|
||||
if (id) {
|
||||
await albumStore.fetchAlbum(id)
|
||||
if (currentAlbum.value) {
|
||||
fetchGenerations(id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const fetchGenerations = async (albumId) => {
|
||||
loadingGenerations.value = true
|
||||
try {
|
||||
const response = await albumService.getAlbumGenerations(albumId, 100) // Increase limit for now
|
||||
generations.value = response.data.generations || [] // Adjust based on actual API response structure
|
||||
// If API returns list directly:
|
||||
if (Array.isArray(response.data)) {
|
||||
generations.value = response.data
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load generations', e)
|
||||
} finally {
|
||||
loadingGenerations.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = () => {
|
||||
confirm.require({
|
||||
message: 'Are you sure you want to delete this album? This action cannot be undone.',
|
||||
header: 'Delete Album',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: async () => {
|
||||
await albumStore.deleteAlbum(currentAlbum.value.id)
|
||||
router.push('/albums')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const copyPrompt = (prompt) => {
|
||||
navigator.clipboard.writeText(prompt)
|
||||
// Could add a toast notification here
|
||||
}
|
||||
|
||||
// --- Generation Picker ---
|
||||
const openGenerationPicker = async () => {
|
||||
isGenerationPickerVisible.value = true
|
||||
selectedGenerations.value = []
|
||||
await loadAvailableGenerations()
|
||||
}
|
||||
|
||||
const loadAvailableGenerations = async () => {
|
||||
loadingAvailableGenerations.value = true
|
||||
try {
|
||||
// Fetch recent generations to add
|
||||
const response = await aiService.getGenerations(50, 0)
|
||||
availableGenerations.value = response.generations || []
|
||||
} catch (e) {
|
||||
console.error('Failed to load available generations', e)
|
||||
} finally {
|
||||
loadingAvailableGenerations.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleGenerationSelection = (gen) => {
|
||||
const index = selectedGenerations.value.findIndex(g => g.id === gen.id)
|
||||
if (index === -1) {
|
||||
selectedGenerations.value.push(gen)
|
||||
} else {
|
||||
selectedGenerations.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const addSelectedGenerations = async () => {
|
||||
if (selectedGenerations.value.length === 0) return
|
||||
|
||||
try {
|
||||
// Add sequentially
|
||||
for (const gen of selectedGenerations.value) {
|
||||
await albumService.addGenerationToAlbum(currentAlbum.value.id, gen.id)
|
||||
}
|
||||
isGenerationPickerVisible.value = false
|
||||
// Refresh album generations
|
||||
await fetchGenerations(currentAlbum.value.id)
|
||||
} catch (e) {
|
||||
console.error('Failed to add generations to album', e)
|
||||
}
|
||||
}
|
||||
|
||||
const removeGeneration = (gen) => {
|
||||
confirm.require({
|
||||
message: 'Are you sure you want to remove this generation from the album?',
|
||||
header: 'Remove Generation',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: async () => {
|
||||
await albumStore.removeGenerationFromAlbum(currentAlbum.value.id, gen.id)
|
||||
await fetchGenerations(currentAlbum.value.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full p-8 overflow-y-auto text-slate-100">
|
||||
<ConfirmDialog></ConfirmDialog>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="flex flex-col gap-8">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<Skeleton width="15rem" height="3rem" class="mb-4" />
|
||||
<Skeleton width="20rem" height="1.5rem" />
|
||||
</div>
|
||||
<Skeleton width="8rem" height="2.5rem" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<Skeleton v-for="i in 8" :key="i" height="16rem" class="rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="currentAlbum">
|
||||
<!-- Header -->
|
||||
<header class="flex justify-between items-start mb-8 pb-8 border-b border-white/10">
|
||||
<div>
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<Button icon="pi pi-arrow-left" text rounded @click="router.push('/albums')"
|
||||
class="!text-slate-400 hover:!text-white hover:!bg-white/10" />
|
||||
<h1 class="text-4xl font-bold m-0">{{ currentAlbum.name }}</h1>
|
||||
</div>
|
||||
<p class="text-slate-400 text-lg ml-14 max-w-2xl">{{ currentAlbum.description }}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button label="Add Generation" icon="pi pi-plus" text @click="openGenerationPicker" />
|
||||
<Button label="Delete Album" icon="pi pi-trash" severity="danger" text @click="confirmDelete" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Generations Grid -->
|
||||
<div v-if="loadingGenerations" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
<Skeleton v-for="i in 8" :key="i" height="20rem" class="rounded-xl" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="generations.length === 0"
|
||||
class="flex flex-col items-center justify-center py-20 text-slate-500">
|
||||
<i class="pi pi-image text-5xl mb-4 opacity-50"></i>
|
||||
<p class="text-xl">No generations in this album yet.</p>
|
||||
<p class="text-sm mt-2">Generate images and add them to this album!</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
<!-- We need to adapt this based on actual generation object structure -->
|
||||
<div v-for="gen in generations" :key="gen.id"
|
||||
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">
|
||||
<Image :src="gen.result || API_URL + `/assets/${gen.result_list[0]}` + '?thumbnail=true'"
|
||||
preview class="w-full h-full object-cover" imageClass="w-full h-full object-cover" />
|
||||
|
||||
<!-- Overlay Actions -->
|
||||
<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">
|
||||
<div class="pointer-events-auto flex gap-2">
|
||||
<Button icon="pi pi-copy" rounded text class="!text-white hover:!bg-white/20"
|
||||
v-tooltip.top="'Copy Prompt'" @click="copyPrompt(gen.prompt)" />
|
||||
<Button icon="pi pi-trash" rounded text
|
||||
class="!text-white hover:!bg-red-500/80 hover:!text-white"
|
||||
v-tooltip.top="'Remove from Album'" @click="removeGeneration(gen)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3">
|
||||
<p class="text-xs text-slate-400 line-clamp-2" :title="gen.prompt">
|
||||
{{ gen.prompt }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col items-center justify-center h-full text-slate-400">
|
||||
<i class="pi pi-exclamation-circle text-4xl mb-4 text-red-400"></i>
|
||||
<p class="text-xl">Album not found</p>
|
||||
<Button label="Back to Albums" class="mt-4 p-button-text" @click="router.push('/albums')" />
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="isGenerationPickerVisible" modal header="Add Generations"
|
||||
:style="{ width: '80vw', maxWidth: '1000px' }"
|
||||
:pt="{ root: { class: '!bg-slate-900 !border !border-white/10' }, header: { class: '!bg-slate-900 !border-b !border-white/5 !text-white' }, content: { class: '!bg-slate-900 !p-4' }, footer: { class: '!bg-slate-900 !border-t !border-white/5 !p-4' }, closeButton: { class: '!text-slate-400 hover:!text-white' } }">
|
||||
|
||||
<div class="h-[60vh] overflow-y-auto custom-scrollbar">
|
||||
<div v-if="loadingAvailableGenerations" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
<Skeleton v-for="i in 10" :key="i" height="150px" class="!bg-slate-800 rounded-xl" />
|
||||
</div>
|
||||
<div v-else-if="availableGenerations.length > 0"
|
||||
class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
<div v-for="gen in availableGenerations" :key="gen.id" @click="toggleGenerationSelection(gen)"
|
||||
class="relative aspect-[2/3] rounded-xl overflow-hidden cursor-pointer border-2 transition-all group"
|
||||
:class="selectedGenerations.some(g => g.id === gen.id) ? 'border-violet-500 ring-2 ring-violet-500/30' : 'border-transparent hover:border-white/20'">
|
||||
|
||||
<img v-if="gen.result_list && gen.result_list.length > 0"
|
||||
:src="gen.result_list[0].includes('http') ? gen.result_list[0] : (gen.result || API_URL + `/assets/${gen.result_list[0]}` + '?thumbnail=true')"
|
||||
class="w-full h-full object-cover" />
|
||||
<!-- Fallback for no result -->
|
||||
<div v-else class="w-full h-full bg-slate-800 flex items-center justify-center text-slate-500">
|
||||
<i class="pi pi-image text-2xl"></i>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 right-0 p-2 bg-black/60 backdrop-blur-sm">
|
||||
<p class="text-[10px] text-white line-clamp-2">{{ gen.prompt }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Checkmark -->
|
||||
<div v-if="selectedGenerations.some(g => g.id === gen.id)"
|
||||
class="absolute top-2 right-2 w-6 h-6 bg-violet-500 rounded-full flex items-center justify-center shadow-lg animate-in zoom-in duration-200">
|
||||
<i class="pi pi-check text-white text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col items-center justify-center h-full text-slate-500">
|
||||
<p>No generations found.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button label="Cancel" @click="isGenerationPickerVisible = false"
|
||||
class="!text-slate-300 hover:!bg-white/5" text />
|
||||
<Button :label="'Add (' + selectedGenerations.length + ')'" @click="addSelectedGenerations"
|
||||
class="!bg-violet-600 !border-none hover:!bg-violet-500" />
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user