This commit is contained in:
xds
2026-02-12 17:54:14 +03:00
parent 2eda7526ef
commit 0406b175e9
2 changed files with 69 additions and 11 deletions

View File

@@ -9,7 +9,6 @@ import Skeleton from 'primevue/skeleton'
import Button from 'primevue/button' import Button from 'primevue/button'
import ConfirmDialog from 'primevue/confirmdialog' import ConfirmDialog from 'primevue/confirmdialog'
import { useConfirm } from 'primevue/useconfirm' import { useConfirm } from 'primevue/useconfirm'
import Image from 'primevue/image'
import Dialog from 'primevue/dialog' import Dialog from 'primevue/dialog'
const route = useRoute() const route = useRoute()
@@ -129,6 +128,14 @@ const removeGeneration = (gen) => {
} }
}) })
} }
// --- Image Preview ---
const isImagePreviewVisible = ref(false)
const previewImage = ref(null)
const openImagePreview = (url) => {
previewImage.value = { url }
isImagePreviewVisible.value = true
}
</script> </script>
<template> <template>
@@ -183,9 +190,11 @@ const removeGeneration = (gen) => {
<div v-for="gen in generations" :key="gen.id" <div v-for="gen in generations" :key="gen.id"
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"> <div class="aspect-[2/3] w-full bg-slate-800 relative overflow-hidden cursor-pointer"
<Image :src="gen.result || API_URL + `/assets/${gen.result_list[0]}` + '?thumbnail=true'" @click="gen.result_list && gen.result_list.length > 0 ? openImagePreview(API_URL + '/assets/' + gen.result_list[0]) : null">
preview class="w-full h-full object-cover" imageClass="w-full h-full object-cover" /> <img v-if="gen.result_list && gen.result_list.length > 0"
:src="gen.result || API_URL + `/assets/${gen.result_list[0]}` + '?thumbnail=true'"
class="w-full h-full object-cover" />
<!-- Overlay Actions --> <!-- Overlay Actions -->
<div <div
@@ -230,7 +239,7 @@ const removeGeneration = (gen) => {
:class="selectedGenerations.some(g => g.id === gen.id) ? 'border-violet-500 ring-2 ring-violet-500/30' : 'border-transparent hover:border-white/20'"> :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" <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')" :src="gen.result_list[0].includes('http') ? gen.result_list[0] : (gen.result || API_URL + `/assets/${gen.result_list[0]}`)"
class="w-full h-full object-cover" /> class="w-full h-full object-cover" />
<!-- Fallback for no result --> <!-- Fallback for no result -->
<div v-else class="w-full h-full bg-slate-800 flex items-center justify-center text-slate-500"> <div v-else class="w-full h-full bg-slate-800 flex items-center justify-center text-slate-500">
@@ -263,6 +272,18 @@ const removeGeneration = (gen) => {
</template> </template>
</Dialog> </Dialog>
<!-- Image Preview Modal -->
<Dialog v-model:visible="isImagePreviewVisible" modal dismissableMask
:style="{ width: '90vw', maxWidth: '1000px', background: 'transparent', boxShadow: 'none' }"
:pt="{ root: { class: '!bg-transparent !border-none !shadow-none' }, header: { class: '!hidden' }, content: { class: '!bg-transparent !p-0' } }">
<div class="relative flex items-center justify-center" @click="isImagePreviewVisible = false">
<img v-if="previewImage" :src="previewImage.url"
class="max-w-full max-h-[85vh] object-contain rounded-xl shadow-2xl" />
<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>

View File

@@ -54,6 +54,7 @@ const historyFirst = ref(0)
const isSettingsVisible = ref(false) const isSettingsVisible = ref(false)
const isSubmitting = ref(false) const isSubmitting = ref(false)
const activeOverlayId = ref(null) // For mobile tap-to-show overlay const activeOverlayId = ref(null) // For mobile tap-to-show overlay
const filterCharacter = ref(null) // Character filter for gallery
// Options // Options
const qualityOptions = ref([ const qualityOptions = ref([
@@ -120,6 +121,14 @@ watch([prompt, selectedCharacter, selectedAssets, quality, aspectRatio, sendToTe
saveSettings() saveSettings()
}, { deep: true }) }, { deep: true })
// Watcher for character filter — reload history when filter changes
watch(filterCharacter, async () => {
historyGenerations.value = []
historyTotal.value = 0
historyFirst.value = 0
await refreshHistory()
})
// --- Data Loading --- // --- Data Loading ---
const loadData = async () => { const loadData = async () => {
@@ -127,7 +136,7 @@ const loadData = async () => {
const [charsRes, assetsRes, historyRes] = await Promise.all([ const [charsRes, assetsRes, historyRes] = await Promise.all([
dataService.getCharacters(), // Assuming this exists and returns list dataService.getCharacters(), // Assuming this exists and returns list
dataService.getAssets(100, 0, 'all'), // Load a batch of assets dataService.getAssets(100, 0, 'all'), // Load a batch of assets
aiService.getGenerations(historyRows.value, historyFirst.value) aiService.getGenerations(historyRows.value, historyFirst.value, filterCharacter.value?.id)
]) ])
// Characters // Characters
@@ -197,7 +206,7 @@ const loadData = async () => {
const refreshHistory = async () => { const refreshHistory = async () => {
try { try {
const response = await aiService.getGenerations(historyRows.value, 0) const response = await aiService.getGenerations(historyRows.value, 0, filterCharacter.value?.id)
if (response && response.generations) { if (response && response.generations) {
// Update existing items and add new ones at the top // Update existing items and add new ones at the top
const newGenerations = [] const newGenerations = []
@@ -352,7 +361,7 @@ const loadMoreHistory = async () => {
try { try {
const nextOffset = historyGenerations.value.length const nextOffset = historyGenerations.value.length
const response = await aiService.getGenerations(historyRows.value, nextOffset) const response = await aiService.getGenerations(historyRows.value, nextOffset, filterCharacter.value?.id)
if (response && response.generations) { if (response && response.generations) {
const newGenerations = response.generations.filter(gen => const newGenerations = response.generations.filter(gen =>
@@ -602,6 +611,32 @@ const confirmAddToAlbum = async () => {
<span class="text-xs text-slate-500 border-l border-white/10 pl-3">History</span> <span class="text-xs text-slate-500 border-l border-white/10 pl-3">History</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Dropdown v-model="filterCharacter" :options="characters" optionLabel="name"
placeholder="All Characters" showClear
class="!w-48 !bg-slate-800/60 !border-white/10 !text-white !rounded-xl !text-sm" :pt="{
root: { class: '!bg-slate-800/60 !h-8' },
input: { class: '!text-white !text-xs !py-1 !px-2' },
trigger: { class: '!text-slate-400 !w-6' },
panel: { class: '!bg-slate-800 !border-white/10' },
item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white !text-xs !py-1.5' },
clearIcon: { class: '!text-slate-400 hover:!text-white' }
}">
<template #value="slotProps">
<div v-if="slotProps.value" class="flex items-center gap-1.5">
<img v-if="slotProps.value.avatar_image" :src="API_URL + slotProps.value.avatar_image"
class="w-5 h-5 rounded-full object-cover" />
<span class="text-xs">{{ slotProps.value.name }}</span>
</div>
<span v-else class="text-xs text-slate-400">{{ slotProps.placeholder }}</span>
</template>
<template #option="slotProps">
<div class="flex items-center gap-2">
<img v-if="slotProps.option.avatar_image" :src="API_URL + slotProps.option.avatar_image"
class="w-6 h-6 rounded-full object-cover" />
<span>{{ slotProps.option.name }}</span>
</div>
</template>
</Dropdown>
<Button icon="pi pi-refresh" @click="refreshHistory" rounded text <Button icon="pi pi-refresh" @click="refreshHistory" rounded text
class="!text-slate-400 hover:!bg-white/10 !w-8 !h-8 md:hidden" /> class="!text-slate-400 hover:!bg-white/10 !w-8 !h-8 md:hidden" />
<Button icon="pi pi-cog" @click="isSettingsVisible = true" rounded text <Button icon="pi pi-cog" @click="isSettingsVisible = true" rounded text
@@ -645,7 +680,7 @@ const confirmAddToAlbum = async () => {
<i class="pi pi-spin pi-spinner text-violet-500 text-xl mb-2 relative z-10"></i> <i class="pi pi-spin pi-spinner text-violet-500 text-xl mb-2 relative z-10"></i>
<span class="text-[10px] text-violet-300/70 relative z-10 capitalize">{{ gen.status <span class="text-[10px] text-violet-300/70 relative z-10 capitalize">{{ gen.status
}}...</span> }}...</span>
<span v-if="gen.progress" <span v-if="gen.progress"
class="text-[9px] text-violet-400/60 font-mono mt-1 relative z-10">{{ class="text-[9px] text-violet-400/60 font-mono mt-1 relative z-10">{{
gen.progress }}%</span> gen.progress }}%</span>
@@ -790,7 +825,8 @@ const confirmAddToAlbum = async () => {
class="flex items-center gap-2 mt-2 px-1 animate-in fade-in slide-in-from-top-1"> class="flex items-center gap-2 mt-2 px-1 animate-in fade-in slide-in-from-top-1">
<Checkbox v-model="useProfileImage" :binary="true" inputId="use-profile-img" /> <Checkbox v-model="useProfileImage" :binary="true" inputId="use-profile-img" />
<label for="use-profile-img" <label for="use-profile-img"
class="text-xs text-slate-300 cursor-pointer select-none">Use Character class="text-xs text-slate-300 cursor-pointer select-none">Use
Character
Photo</label> Photo</label>
</div> </div>
</div> </div>
@@ -801,7 +837,8 @@ const confirmAddToAlbum = async () => {
<div @click="openAssetPicker" <div @click="openAssetPicker"
class="w-full bg-slate-800 border border-white/10 rounded-xl p-3 min-h-[46px] cursor-pointer hover:bg-slate-700/50 transition-colors flex flex-wrap gap-2"> class="w-full bg-slate-800 border border-white/10 rounded-xl p-3 min-h-[46px] cursor-pointer hover:bg-slate-700/50 transition-colors flex flex-wrap gap-2">
<span v-if="selectedAssets.length === 0" <span v-if="selectedAssets.length === 0"
class="text-slate-400 text-sm py-0.5">Select Assets</span> class="text-slate-400 text-sm py-0.5">Select
Assets</span>
<div v-for="asset in selectedAssets" :key="asset.id" <div v-for="asset in selectedAssets" :key="asset.id"
class="px-2 py-1 bg-violet-600/30 border border-violet-500/30 text-violet-200 text-xs rounded-md flex items-center gap-2 animate-in fade-in zoom-in duration-200" class="px-2 py-1 bg-violet-600/30 border border-violet-500/30 text-violet-200 text-xs rounded-md flex items-center gap-2 animate-in fade-in zoom-in duration-200"
@click.stop> @click.stop>