This commit is contained in:
xds
2026-02-27 13:51:14 +03:00
parent 4f9807cfe7
commit 2ed2ee2937
5 changed files with 98 additions and 12 deletions

View File

@@ -29,12 +29,12 @@ const props = defineProps({
} }
}) })
const emit = defineEmits(['toggle-select', 'open-preview', 'toggle-like', 'delete', 'reuse-prompt', 'reuse-asset', 'use-result', 'toggle-overlay']) const emit = defineEmits(['toggle-select', 'open-preview', 'toggle-like', 'delete', 'reuse-prompt', 'reuse-asset', 'use-result', 'toggle-overlay', 'mark-nsfw'])
const isTemporarilyUnblurred = ref(false) const isTemporarilyUnblurred = ref(false)
const isBlurred = computed(() => { const isBlurred = computed(() => {
return props.generation.nsfw && !props.showNsfwGlobal && !isTemporarilyUnblurred.value return (props.generation.is_nsfw || props.generation.nsfw) && !props.showNsfwGlobal && !isTemporarilyUnblurred.value
}) })
const toggleBlur = () => { const toggleBlur = () => {
@@ -46,14 +46,6 @@ const handleImageClick = (e) => {
emit('toggle-select', props.generation.result_list[0]) emit('toggle-select', props.generation.result_list[0])
} else { } else {
if (isBlurred.value) { if (isBlurred.value) {
// If blurred, click might just unblur or do nothing?
// Let's let the button handle unblur, and click opens preview if unblurred?
// Or maybe click unblurs? Let's stick to button for unblur to be explicit.
// But if user clicks image, maybe show preview anyway?
// Usually blurred images shouldn't be previewed full size unless unblurred.
// Let's allow preview, but maybe preview also needs to handle blur?
// For now, let's just open preview. The preview modal might need its own blur logic or just show it.
// Let's assume preview shows it.
emit('open-preview', props.apiUrl + '/assets/' + props.generation.result_list[0]) emit('open-preview', props.apiUrl + '/assets/' + props.generation.result_list[0])
} else { } else {
emit('open-preview', props.apiUrl + '/assets/' + props.generation.result_list[0]) emit('open-preview', props.apiUrl + '/assets/' + props.generation.result_list[0])
@@ -138,6 +130,10 @@ const handleOverlayClick = () => {
<Button icon="pi pi-pencil" <Button icon="pi pi-pencil"
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"
@click.stop="emit('use-result', generation)" /> @click.stop="emit('use-result', generation)" />
<Button :icon="(generation.is_nsfw || generation.nsfw) ? 'pi pi-eye' : 'pi pi-eye-slash'"
class="!w-6 !h-6 !rounded-full !bg-white/20 !border-none !text-white text-[10px] hover:!bg-red-500 hover:!text-white"
@click.stop="emit('mark-nsfw', generation)"
v-tooltip.bottom="(generation.is_nsfw || generation.nsfw) ? 'Unmark NSFW' : 'Mark NSFW'" />
<Button icon="pi pi-trash" <Button icon="pi pi-trash"
class="!w-6 !h-6 !rounded-full !bg-red-500/20 !border-none !text-red-400 text-[10px] hover:!bg-red-500 hover:!text-white" class="!w-6 !h-6 !rounded-full !bg-red-500/20 !border-none !text-red-400 text-[10px] hover:!bg-red-500 hover:!text-white"
@click.stop="emit('delete', generation)" /> @click.stop="emit('delete', generation)" />

View File

@@ -69,6 +69,14 @@ export const aiService = {
return response.data return response.data
}, },
// Mark generation as NSFW
async markGenerationNsfw(generationId, isNsfw = true) {
const response = await api.post(`/generations/${generationId}/nsfw`, {
is_nsfw: isNsfw
})
return response.data
},
// Get usage statistics (runs, tokens, cost) // Get usage statistics (runs, tokens, cost)
async getUsageReport(breakdown = null, projectId = null) { async getUsageReport(breakdown = null, projectId = null) {
const params = {} const params = {}

View File

@@ -844,6 +844,29 @@ const useResultAsReference = (gen) => {
} }
} }
const markNsfw = async (gen) => {
// Determine new state (toggle)
const currentNsfw = gen.is_nsfw || gen.nsfw || false
const newNsfw = !currentNsfw
try {
await aiService.markGenerationNsfw(gen.id, newNsfw)
// Update local state
gen.is_nsfw = newNsfw
// Also update legacy property if present to keep UI consistent
if (gen.nsfw !== undefined) gen.nsfw = newNsfw
// If this is the currently displayed result, update it too
if (generatedResult.value && generatedResult.value.assets && generatedResult.value.assets.some(a => gen.result_list.includes(a.id))) {
generatedResult.value.is_nsfw = newNsfw
if (generatedResult.value.nsfw !== undefined) generatedResult.value.nsfw = newNsfw
}
} catch (e) {
console.error('Failed to toggle NSFW', e)
}
}
const triggerFileUpload = () => { const triggerFileUpload = () => {
if (fileInput.value) fileInput.value.click() if (fileInput.value) fileInput.value.click()
} }
@@ -1378,6 +1401,7 @@ const handleGenerate = async () => {
<span class="capitalize" <span class="capitalize"
:class="gen.status === 'done' ? 'text-green-500' : (gen.status === 'failed' ? 'text-red-500' : 'text-amber-500')">{{ :class="gen.status === 'done' ? 'text-green-500' : (gen.status === 'failed' ? 'text-red-500' : 'text-amber-500')">{{
gen.status }}</span> gen.status }}</span>
<Tag v-if="gen.is_nsfw || gen.nsfw" value="NSFW" severity="danger" class="!text-[8px] !py-0 !px-1" />
<i v-if="gen.failed_reason" <i v-if="gen.failed_reason"
v-tooltip.right="gen.failed_reason" v-tooltip.right="gen.failed_reason"
class="pi pi-exclamation-circle text-red-500" class="pi pi-exclamation-circle text-red-500"
@@ -1414,6 +1438,11 @@ const handleGenerate = async () => {
:disabled="gen.status !== 'done' || gen.result_list.length == 0" :disabled="gen.status !== 'done' || gen.result_list.length == 0"
@click.stop="useResultAsReference(gen)" @click.stop="useResultAsReference(gen)"
v-tooltip.bottom="'Use result as reference'" /> v-tooltip.bottom="'Use result as reference'" />
<Button :icon="(gen.is_nsfw || gen.nsfw) ? 'pi pi-eye' : 'pi pi-eye-slash'"
label="NSFW" size="small" text
class="!text-[10px] !py-0.5 !px-2 text-slate-400 hover:bg-white/5 flex-1"
@click.stop="markNsfw(gen)"
v-tooltip.bottom="(gen.is_nsfw || gen.nsfw) ? 'Unmark NSFW' : 'Mark NSFW'" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -752,6 +752,27 @@ const toggleMobileOverlay = (id) => {
} }
} }
const markNsfw = async (gen) => {
// if (!confirm('Are you sure you want to mark this generation as NSFW?')) return
const currentNsfw = gen.is_nsfw || gen.nsfw || false
const newNsfw = !currentNsfw
try {
await aiService.markGenerationNsfw(gen.id, newNsfw)
gen.is_nsfw = newNsfw
if (gen.nsfw !== undefined) gen.nsfw = newNsfw
if (gen.isGroup && gen.children) {
gen.children.forEach(c => {
c.is_nsfw = newNsfw
if (c.nsfw !== undefined) c.nsfw = newNsfw
})
}
} catch (e) {
console.error('Failed to toggle NSFW', e)
}
}
// --- Asset Picker Logic --- // --- Asset Picker Logic ---
const loadModalAssets = async () => { const loadModalAssets = async () => {
@@ -892,7 +913,7 @@ const confirmAddToAlbum = async () => {
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<img v-if="slotProps.option.avatar_image" :src="API_URL + slotProps.option.avatar_image" <img v-if="slotProps.option.avatar_image" :src="API_URL + slotProps.option.avatar_image"
class="w-5 h-5 rounded-full object-cover" /> class="w-5 h-5 rounded-full object-cover" />
<span class="">{{ slotProps.option.name }}</span> <span class="text-sm">{{ slotProps.option.name }}</span>
</div> </div>
</template> </template>
</Dropdown> </Dropdown>
@@ -955,6 +976,7 @@ const confirmAddToAlbum = async () => {
@reuse-asset="reuseAsset" @reuse-asset="reuseAsset"
@use-result="useResultAsAsset" @use-result="useResultAsAsset"
@toggle-overlay="toggleMobileOverlay" @toggle-overlay="toggleMobileOverlay"
@mark-nsfw="markNsfw"
/> />
</div> </div>
</div> </div>
@@ -979,6 +1001,7 @@ const confirmAddToAlbum = async () => {
@reuse-asset="reuseAsset" @reuse-asset="reuseAsset"
@use-result="useResultAsAsset" @use-result="useResultAsAsset"
@toggle-overlay="toggleMobileOverlay" @toggle-overlay="toggleMobileOverlay"
@mark-nsfw="markNsfw"
/> />
</template> </template>
</div> </div>

View File

@@ -958,6 +958,26 @@ watch(viewMode, (v) => {
} }
}) })
const markNsfw = async (gen) => {
// if (!confirm('Are you sure you want to mark this generation as NSFW?')) return
const currentNsfw = gen.is_nsfw || gen.nsfw || false
const newNsfw = !currentNsfw
try {
await aiService.markGenerationNsfw(gen.id, newNsfw)
gen.is_nsfw = newNsfw
if (gen.nsfw !== undefined) gen.nsfw = newNsfw
if (gen.isGroup && gen.children) {
gen.children.forEach(c => {
c.is_nsfw = newNsfw
if (c.nsfw !== undefined) c.nsfw = newNsfw
})
}
} catch (e) {
console.error('Failed to toggle NSFW', e)
}
}
</script> </script>
<template> <template>
@@ -1077,6 +1097,11 @@ watch(viewMode, (v) => {
text rounded size="small" text rounded size="small"
class="!w-7 !h-7 !text-slate-400 hover:!text-violet-400" class="!w-7 !h-7 !text-slate-400 hover:!text-violet-400"
v-tooltip.top="'Reuse Assets'" @click="reuseAssets(gen)" /> v-tooltip.top="'Reuse Assets'" @click="reuseAssets(gen)" />
<Button :icon="(gen.is_nsfw || gen.nsfw) ? 'pi pi-eye' : 'pi pi-eye-slash'"
text rounded size="small"
class="!w-7 !h-7 !text-slate-400 hover:!text-red-400"
v-tooltip.top="(gen.is_nsfw || gen.nsfw) ? 'Unmark NSFW' : 'Mark NSFW'"
@click="markNsfw(gen)" />
<Button icon="pi pi-trash" text rounded size="small" <Button icon="pi pi-trash" text rounded size="small"
class="!w-7 !h-7 !text-slate-400 hover:!text-red-400" class="!w-7 !h-7 !text-slate-400 hover:!text-red-400"
v-tooltip.top="'Delete'" @click="deleteGeneration(gen)" /> v-tooltip.top="'Delete'" @click="deleteGeneration(gen)" />
@@ -1216,6 +1241,11 @@ watch(viewMode, (v) => {
@click.stop="setAsReference(img.assetId)" /> @click.stop="setAsReference(img.assetId)" />
<Button icon="pi pi-refresh" rounded text size="small" <Button icon="pi pi-refresh" rounded text size="small"
class="!text-white hover:!bg-white/20" @click.stop="reusePrompt(img.gen)" /> class="!text-white hover:!bg-white/20" @click.stop="reusePrompt(img.gen)" />
<Button :icon="(img.gen.is_nsfw || img.gen.nsfw) ? 'pi pi-eye' : 'pi pi-eye-slash'"
rounded text size="small"
class="!text-white hover:!bg-red-500/20 hover:!text-red-400"
v-tooltip.top="(img.gen.is_nsfw || img.gen.nsfw) ? 'Unmark NSFW' : 'Mark NSFW'"
@click.stop="markNsfw(img.gen)" />
<Button icon="pi pi-trash" rounded text size="small" <Button icon="pi pi-trash" rounded text size="small"
class="!text-red-400 hover:!bg-red-500/20" class="!text-red-400 hover:!bg-red-500/20"
@click.stop="deleteAssetFromGeneration(img.gen, img.assetId)" /> @click.stop="deleteAssetFromGeneration(img.gen, img.assetId)" />