diff --git a/src/components/GenerationImage.vue b/src/components/GenerationImage.vue
index a125ea5..77b8507 100644
--- a/src/components/GenerationImage.vue
+++ b/src/components/GenerationImage.vue
@@ -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 isBlurred = computed(() => {
- return props.generation.nsfw && !props.showNsfwGlobal && !isTemporarilyUnblurred.value
+ return (props.generation.is_nsfw || props.generation.nsfw) && !props.showNsfwGlobal && !isTemporarilyUnblurred.value
})
const toggleBlur = () => {
@@ -46,14 +46,6 @@ const handleImageClick = (e) => {
emit('toggle-select', props.generation.result_list[0])
} else {
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])
} else {
emit('open-preview', props.apiUrl + '/assets/' + props.generation.result_list[0])
@@ -138,6 +130,10 @@ const handleOverlayClick = () => {
+
diff --git a/src/services/aiService.js b/src/services/aiService.js
index 974ceae..6497a7a 100644
--- a/src/services/aiService.js
+++ b/src/services/aiService.js
@@ -69,6 +69,14 @@ export const aiService = {
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)
async getUsageReport(breakdown = null, projectId = null) {
const params = {}
diff --git a/src/views/CharacterDetailView.vue b/src/views/CharacterDetailView.vue
index a375fe9..3ae3fb0 100644
--- a/src/views/CharacterDetailView.vue
+++ b/src/views/CharacterDetailView.vue
@@ -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 = () => {
if (fileInput.value) fileInput.value.click()
}
@@ -1378,6 +1401,7 @@ const handleGenerate = async () => {
{{
gen.status }}
+
{
:disabled="gen.status !== 'done' || gen.result_list.length == 0"
@click.stop="useResultAsReference(gen)"
v-tooltip.bottom="'Use result as reference'" />
+
diff --git a/src/views/FlexibleGenerationView.vue b/src/views/FlexibleGenerationView.vue
index 5a42626..56e2961 100644
--- a/src/views/FlexibleGenerationView.vue
+++ b/src/views/FlexibleGenerationView.vue
@@ -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 ---
const loadModalAssets = async () => {
@@ -892,7 +913,7 @@ const confirmAddToAlbum = async () => {
![]()
-
{{ slotProps.option.name }}
+
{{ slotProps.option.name }}
@@ -955,6 +976,7 @@ const confirmAddToAlbum = async () => {
@reuse-asset="reuseAsset"
@use-result="useResultAsAsset"
@toggle-overlay="toggleMobileOverlay"
+ @mark-nsfw="markNsfw"
/>
@@ -979,6 +1001,7 @@ const confirmAddToAlbum = async () => {
@reuse-asset="reuseAsset"
@use-result="useResultAsAsset"
@toggle-overlay="toggleMobileOverlay"
+ @mark-nsfw="markNsfw"
/>
diff --git a/src/views/IdeaDetailView.vue b/src/views/IdeaDetailView.vue
index 3782132..aba4e93 100644
--- a/src/views/IdeaDetailView.vue
+++ b/src/views/IdeaDetailView.vue
@@ -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)
+ }
+}
@@ -1077,6 +1097,11 @@ watch(viewMode, (v) => {
text rounded size="small"
class="!w-7 !h-7 !text-slate-400 hover:!text-violet-400"
v-tooltip.top="'Reuse Assets'" @click="reuseAssets(gen)" />
+
@@ -1216,6 +1241,11 @@ watch(viewMode, (v) => {
@click.stop="setAsReference(img.assetId)" />
+
@@ -1678,4 +1708,4 @@ watch(viewMode, (v) => {
.fade-leave-to {
opacity: 0;
}
-
+
\ No newline at end of file