- Galerien sind nun eine Entität - es kann mehrere geben

- Neues Sparkbooth-Upload-Feature: Endpoint /api/sparkbooth/upload (Token-basiert pro Galerie), Controller Api/SparkboothUploadController, Migration 2026_01_21_000001_add_upload_fields_to_galleries_table.php mit Upload-Flags/Token/Expiry;
    Galerie-Modell und Factory/Seeder entsprechend erweitert.
  - Filament: Neue Setup-Seite SparkboothSetup (mit View) zur schnellen Galerie- und Token-Erstellung inkl. QR/Endpoint/Snippet;
    Galerie-Link-Views nutzen jetzt simple-qrcode (Composer-Dependency hinzugefügt) und bieten PNG-Download.
  - Galerie-Tabelle: Slug/Pfad-Spalten entfernt, Action „Link-Details“ mit Modal; Created-at-Spalte hinzugefügt.
  - Zugriffshärtung: Galerie-IDs in API (ImageController, Download/Print) geprüft; GalleryAccess/Middleware + Gallery-Modell/Slug-UUID
    eingeführt; GalleryAccess-Inertia-Seite.
  - UI/UX: LoadingSpinner/StyledImageDisplay verbessert, Delete-Confirm, Übersetzungen ergänzt.
This commit is contained in:
2025-12-04 07:52:50 +01:00
parent 52dc61ca16
commit f5da8ed877
49 changed files with 2243 additions and 165 deletions

View File

@@ -47,17 +47,17 @@
<button
type="button"
class="flex w-full items-center justify-between gap-3 rounded-2xl border px-4 py-3 text-left font-semibold text-slate-900 transition focus:outline-none focus-visible:ring-2 dark:text-white"
:class="[aiAvailable ? 'border-white/20 bg-white/40 hover:border-emerald-400 hover:bg-white/70 dark:border-white/10 dark:bg-white/5' : 'border-rose-200 bg-rose-50 cursor-not-allowed opacity-70']"
:disabled="!aiAvailable"
:class="[effectiveAiAvailable ? 'border-white/20 bg-white/40 hover:border-emerald-400 hover:bg-white/70 dark:border-white/10 dark:bg-white/5' : 'border-rose-200 bg-rose-50 cursor-not-allowed opacity-70']"
:disabled="!effectiveAiAvailable"
@click="showStyleSelectorView = true"
>
<div class="flex items-center gap-3">
<span class="h-2 w-2 rounded-full"
:class="aiAvailable ? 'bg-emerald-500' : 'bg-rose-500'"></span>
:class="effectiveAiAvailable ? 'bg-emerald-500' : 'bg-rose-500'"></span>
<div>
<p class="text-base">Stile anzeigen</p>
<p class="text-sm font-normal text-slate-500 dark:text-slate-400">
{{ aiAvailable ? 'Lass die KI dein Motiv verzaubern' : 'AI-Dienste derzeit nicht verfügbar' }}
{{ effectiveAiAvailable ? 'Lass die KI dein Motiv verzaubern' : 'AI-Dienste derzeit nicht verfügbar' }}
</p>
</div>
</div>
@@ -126,6 +126,9 @@
<StyleSelector
class="max-h-[520px] flex-1"
:image_id="image?.id ?? image?.image_id"
:allow-ai-styles="props.allowAiStyles"
:gallery-slug="props.gallerySlug"
:ai-available-override="props.aiAvailable"
@styleSelected="handleStyleSelected"
@close="$emit('close')"
/>
@@ -149,12 +152,31 @@ const props = defineProps({
type: Object,
required: true,
},
allowAiStyles: {
type: Boolean,
default: true,
},
showPrintButton: {
type: Boolean,
default: true,
},
gallerySlug: {
type: String,
default: 'default',
},
aiAvailable: {
type: Boolean,
default: true,
},
});
const emits = defineEmits(['close', 'print', 'styleSelected', 'download']);
const settings = computed(() => page.props.settings || {});
const showPrintButton = computed(() => settings.value.show_print_button ?? true);
const showPrintButton = computed(() => {
const fromSettings = settings.value.show_print_button ?? true;
return fromSettings && props.showPrintButton;
});
const shouldShowDownload = computed(() => {
const hostname = window.location.hostname;
@@ -166,23 +188,9 @@ const shouldShowDownload = computed(() => {
});
const showStyleSelectorView = ref(false);
const aiAvailable = ref(false);
const effectiveAiAvailable = computed(() => props.allowAiStyles && props.aiAvailable);
const checkAiStatus = async () => {
try {
const response = await axios.get('/api/ai-status');
aiAvailable.value = response.data.some(provider => provider.available);
} catch (error) {
console.error('Error checking AI status:', error);
aiAvailable.value = false;
}
};
onMounted(() => {
checkAiStatus();
// Check every 5 minutes
setInterval(checkAiStatus, 300000);
});
onMounted(() => {});
watch(
() => props.image,

View File

@@ -1,38 +1,25 @@
<template>
<div class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded-lg shadow-lg text-center flex flex-col items-center">
<div class="loader ease-linear rounded-full border-4 border-t-4 border-gray-200 h-12 w-12 mb-4"></div>
<p class="text-gray-700 text-lg">{{ __('api.loading_spinner.processing_image') }}</p>
<p v-if="progress > 0" class="text-gray-700 text-sm mt-2">{{ progress }}%</p>
<div class="fixed inset-0 z-[200] flex items-center justify-center bg-slate-900/80 backdrop-blur">
<div class="relative flex w-full max-w-md flex-col items-center gap-4 rounded-3xl border border-white/10 bg-white/95 p-8 text-center shadow-2xl dark:bg-slate-900/95">
<div class="relative">
<div class="h-16 w-16 rounded-full border-4 border-white/40 border-t-emerald-400 animate-spin"></div>
<div class="absolute inset-0 flex items-center justify-center text-sm font-semibold text-emerald-600 dark:text-emerald-300">
{{ progress ? `${progress}%` : '...' }}
</div>
</div>
<div class="space-y-1">
<p class="text-lg font-semibold text-slate-900 dark:text-white">{{ __('api.loading_spinner.processing_image') }}</p>
<p class="text-sm text-slate-500 dark:text-slate-300">{{ __('api.loading_spinner.processing_wait') }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
defineProps({
progress: {
type: Number,
default: 0,
},
});
</script>
<style scoped>
.loader {
border-top-color: #3498db; /* Blue color for the spinner */
-webkit-animation: spinner 1.5s linear infinite;
animation: spinner 1.5s linear infinite;
}
@-webkit-keyframes spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@@ -66,6 +66,18 @@ const props = defineProps({
type: [Number, String],
required: true,
},
allowAiStyles: {
type: Boolean,
default: true,
},
gallerySlug: {
type: String,
default: 'default',
},
aiAvailableOverride: {
type: Boolean,
default: null,
},
});
const emits = defineEmits(['styleSelected', 'close']);
@@ -75,22 +87,39 @@ const fetchStyles = async () => {
loadError.value = null;
try {
// Check AI availability first
const aiStatusResponse = await axios.get('/api/ai-status');
const aiStatus = aiStatusResponse.data;
aiAvailable.value = aiStatus.some(provider => provider.available);
if (!aiAvailable.value) {
loadError.value = 'AI-Dienste sind derzeit nicht verfügbar. Bitte versuchen Sie es später erneut.';
if (!props.allowAiStyles) {
aiAvailable.value = false;
loadError.value = 'Stilwechsel ist für diese Galerie deaktiviert.';
isLoading.value = false;
return;
}
if (props.aiAvailableOverride !== null) {
aiAvailable.value = props.aiAvailableOverride;
} else {
aiAvailable.value = true;
}
// Fetch styles only if AI is available
const stylesResponse = await axios.get('/api/styles');
styles.value = stylesResponse.data.filter(style => {
// Only show styles from available providers
return style.ai_model && style.ai_model.api_provider && style.ai_model.api_provider.enabled;
const stylesResponse = await axios.get('/api/styles', {
params: { gallery: props.gallerySlug },
});
const payload = Array.isArray(stylesResponse.data) ? stylesResponse.data : [];
styles.value = payload.filter((style) => {
if (! props.allowAiStyles) {
return false;
}
const aiModel = style.ai_model || style.aiModel || {};
const provider = aiModel.primary_api_provider || aiModel.api_provider || {};
if (provider.enabled === false) {
return false;
}
return aiModel && (provider || aiModel);
});
} catch (error) {
@@ -105,7 +134,17 @@ const selectStyle = (style) => {
emits('styleSelected', style, props.image_id);
};
const buildPreviewPath = (preview) => `/storage/${preview}`;
const buildPreviewPath = (preview) => {
if (! preview) {
return '';
}
if (preview.startsWith('http://') || preview.startsWith('https://') || preview.startsWith('/')) {
return preview;
}
return `/storage/${preview}`;
};
onMounted(() => {
fetchStyles();

View File

@@ -1,41 +1,77 @@
<template>
<div class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
<div class="bg-white p-4 rounded-lg shadow-lg max-w-3xl w-full text-center">
<h2 class="text-xl font-bold mb-4">{{ __('api.styled_image_display.title') }}</h2>
<img :src="image.path" alt="Styled Image" class="max-w-full h-auto mx-auto mb-4 rounded-md" />
<div class="flex justify-center space-x-4">
<button
@click="$emit('keep', image)"
class="px-6 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 focus:outline-hidden focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"
>
{{ __('api.styled_image_display.keep_button') }}
</button>
<button
@click="$emit('delete', image)"
class="px-6 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 focus:outline-hidden focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
>
{{ __('api.styled_image_display.delete_button') }}
</button>
</div>
<div class="fixed inset-0 z-[210] flex items-center justify-center bg-slate-900/80 backdrop-blur">
<div class="w-full max-w-4xl overflow-hidden rounded-3xl border border-white/10 bg-white/95 shadow-2xl ring-1 ring-black/10 dark:bg-slate-900/95">
<div class="flex flex-col gap-6 p-6 sm:p-8">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-xs uppercase tracking-[0.35em] text-slate-500 dark:text-slate-400">Ergebnis</p>
<h2 class="text-2xl font-semibold text-slate-900 dark:text-white">{{ __('api.styled_image_display.title') }}</h2>
<p class="text-sm text-slate-500 dark:text-slate-300">{{ __('api.styled_image_display.keep_hint') }}</p>
</div>
<button
type="button"
class="rounded-full border border-white/20 bg-white/10 p-2 text-slate-900 shadow-sm transition hover:border-slate-300 hover:text-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-300 dark:text-white"
@click="$emit('close')"
aria-label="Schließen"
>
<font-awesome-icon :icon="['fas', 'xmark']" class="h-5 w-5" />
</button>
</div>
<div class="overflow-hidden rounded-2xl border border-slate-200/60 bg-slate-50 dark:border-white/10 dark:bg-white/5">
<img :src="image.path" alt="Styled Image" class="mx-auto max-h-[60vh] w-full object-contain" />
</div>
<div class="flex flex-wrap items-center justify-end gap-3">
<button
type="button"
class="rounded-2xl border border-slate-200 bg-white px-5 py-2.5 text-sm font-semibold text-slate-800 shadow-sm transition hover:border-emerald-300 hover:text-emerald-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-300 dark:border-white/20 dark:bg-white/10 dark:text-white"
@click="$emit('keep', image)"
>
{{ __('api.styled_image_display.keep_button') }}
</button>
<button
v-if="!confirmingDelete"
type="button"
class="rounded-2xl border border-rose-200 bg-rose-50 px-5 py-2.5 text-sm font-semibold text-rose-700 shadow-sm transition hover:border-rose-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-300 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-100"
@click="confirmingDelete = true"
>
{{ __('api.styled_image_display.delete_button') }}
</button>
<div v-else class="flex items-center gap-2 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-2.5 text-sm text-rose-700 shadow-sm dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-100">
<span>{{ __('api.styled_image_display.delete_confirm') }}</span>
<button
type="button"
class="rounded-lg bg-rose-600 px-3 py-1 text-white shadow-sm transition hover:bg-rose-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
@click="$emit('delete', image)"
>
{{ __('api.styled_image_display.delete_button') }}
</button>
<button
type="button"
class="rounded-lg border border-slate-200 px-3 py-1 text-slate-700 transition hover:border-slate-300 dark:border-white/20 dark:text-white"
@click="confirmingDelete = false"
>
{{ __('settings.cancel_button') }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
import { ref } from 'vue';
const props = defineProps({
image: {
type: Object,
required: true,
},
defineProps({
image: {
type: Object,
required: true,
},
});
const emits = defineEmits(['keep', 'delete']);
defineEmits(['keep', 'delete', 'close']);
console.log('StyledImageDisplay: image prop:', props.image);
const confirmingDelete = ref(false);
</script>
<style scoped>
/* Add any specific styles for the modal here if needed */
</style>

View File

@@ -0,0 +1,137 @@
<template>
<Head :title="__('api.gallery.access_title')" />
<div class="min-h-screen bg-gradient-to-br from-slate-100 via-white to-slate-200 text-slate-900 transition-colors duration-500 dark:from-slate-950 dark:via-slate-900 dark:to-slate-950 dark:text-slate-100">
<div class="flex min-h-screen items-center justify-center px-4 py-12">
<div class="w-full max-w-xl space-y-8 rounded-3xl border border-slate-200/70 bg-white/80 p-8 shadow-2xl backdrop-blur dark:border-white/10 dark:bg-white/5">
<div class="space-y-3">
<p class="text-xs uppercase tracking-[0.35em] text-slate-500 dark:text-slate-400">
Style Gallery
</p>
<h1 class="text-3xl font-semibold text-slate-900 dark:text-white">
{{ __('api.gallery.access_title') }}
</h1>
<p v-if="expired" class="text-sm text-rose-600 dark:text-rose-300">
{{ __('api.gallery.expired') }}
</p>
<p v-else class="text-sm text-slate-600 dark:text-slate-300">
{{ props.flashMessage || __('api.gallery.password_required') }}
</p>
</div>
<div class="flex flex-wrap gap-2 text-xs text-slate-600 dark:text-slate-300">
<span v-if="formattedExpiresAt" class="inline-flex items-center gap-2 rounded-full bg-slate-100 px-3 py-1 dark:bg-white/10">
<span class="h-2 w-2 rounded-full bg-amber-400"></span>
{{ __('api.gallery.expires_at_hint', { datetime: formattedExpiresAt }) }}
</span>
<span v-if="props.accessDurationMinutes" class="inline-flex items-center gap-2 rounded-full bg-slate-100 px-3 py-1 dark:bg-white/10">
<span class="h-2 w-2 rounded-full bg-emerald-400"></span>
{{ __('api.gallery.duration_hint', { minutes: props.accessDurationMinutes }) }}
</span>
</div>
<form v-if="requiresPassword && !expired" @submit.prevent="submit" class="space-y-4">
<div class="space-y-2">
<label for="password" class="text-sm font-medium text-slate-800 dark:text-slate-200">
{{ __('api.gallery.password_label') }}
</label>
<input
id="password"
v-model="form.password"
type="password"
class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-base text-slate-900 shadow-sm transition focus:border-emerald-400 focus:outline-none focus:ring-2 focus:ring-emerald-400/40 dark:border-white/10 dark:bg-white/5 dark:text-white"
:aria-invalid="Boolean(form.errors.password)"
/>
<p v-if="form.errors.password" class="text-sm text-rose-500">
{{ form.errors.password }}
</p>
</div>
<button
type="submit"
class="flex w-full items-center justify-center rounded-2xl bg-emerald-500 px-4 py-3 text-base font-semibold text-white shadow-lg shadow-emerald-500/30 transition hover:bg-emerald-600 focus:outline-none focus:ring-2 focus:ring-emerald-400 focus:ring-offset-2 focus:ring-offset-white disabled:opacity-60 dark:focus:ring-offset-slate-900"
:disabled="form.processing"
>
{{ form.processing ? '...' : __('api.gallery.submit_password') }}
</button>
</form>
<div v-else-if="!expired" class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-emerald-800 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-100">
{{ __('api.gallery.password_required') }}
</div>
<div v-if="expired" class="space-y-3 rounded-2xl border border-rose-200 bg-rose-50 p-4 text-rose-700 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-100">
<p>{{ __('api.gallery.expired') }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { Head, useForm } from '@inertiajs/vue3';
import { computed } from 'vue';
const props = defineProps({
requiresPassword: {
type: Boolean,
default: false,
},
gallery: {
type: Object,
default: null,
},
expiresAt: {
type: [String, Date, null],
default: null,
},
accessDurationMinutes: {
type: [Number, null],
default: null,
},
expired: {
type: Boolean,
default: false,
},
flashMessage: {
type: String,
default: null,
},
});
const requiresPassword = computed(() => Boolean(props.requiresPassword));
const form = useForm({
password: '',
});
const formattedExpiresAt = computed(() => {
if (!props.expiresAt) {
return null;
}
const date = new Date(props.expiresAt);
if (Number.isNaN(date.getTime())) {
return null;
}
return new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date);
});
const submit = () => {
const hasGallery = Boolean(props.gallery?.slug);
const routeName = hasGallery ? 'gallery.access.store' : 'gallery.access.default.store';
const params = hasGallery ? { gallery: props.gallery.slug } : {};
form.post(route(routeName, params), {
preserveScroll: true,
onError: () => {
form.password = '';
},
});
};
</script>

View File

@@ -32,7 +32,7 @@
<span>{{ currentTheme === 'light' ? __('api.dark_mode') : __('api.light_mode') }}</span>
</button>
<div
v-if="!aiAvailable"
v-if="!effectiveAiAvailable"
class="inline-flex items-center gap-2 rounded-full border border-rose-200 bg-rose-50 px-3 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-rose-700 shadow-sm dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-200"
>
<span class="h-2 w-2 rounded-full bg-rose-500"></span>
@@ -71,6 +71,10 @@
<ImageContextMenu
v-if="currentOverlayComponent === 'contextMenu' && selectedImage"
:image="selectedImage"
:allow-ai-styles="allowAiStyles"
:show-print-button="showPrintButton"
:gallery-slug="gallerySlug"
:ai-available="effectiveAiAvailable"
@close="closeOverlays"
@print="printImage"
@download="downloadImage"
@@ -103,7 +107,7 @@
</template>
<script setup>
import { Head } from '@inertiajs/vue3';
import { Head, usePage } from '@inertiajs/vue3';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import axios from 'axios';
import GalleryGrid from '../Components/GalleryGrid.vue';
@@ -118,12 +122,22 @@ const props = defineProps({
type: String,
default: 'Gallery',
},
gallery: {
type: Object,
default: () => ({}),
},
images: {
type: Array,
default: () => [],
},
});
const page = usePage();
const settings = computed(() => page.props.settings || {});
const gallerySlug = computed(() => props.gallery?.slug || 'default');
const allowAiStyles = computed(() => props.gallery?.allow_ai_styles !== false);
const allowPrint = computed(() => props.gallery?.allow_print !== false);
const images = ref([]);
const imagesPerPage = 12;
const currentPage = ref(1);
@@ -194,6 +208,10 @@ const paginatedImages = computed(() => {
});
const refreshIntervalSeconds = computed(() => Math.max(1, Math.round(refreshIntervalMs.value / 1000)));
const showPrintButton = computed(() => {
return (settings.value.show_print_button ?? true) && allowPrint.value;
});
const effectiveAiAvailable = computed(() => allowAiStyles.value && aiAvailable.value);
const formattedLastRefresh = computed(() =>
new Intl.DateTimeFormat(undefined, {
hour: '2-digit',
@@ -236,8 +254,15 @@ const toggleTheme = () => {
};
const checkAiStatus = () => {
if (!allowAiStyles.value) {
aiAvailable.value = false;
return;
}
axios
.get('/api/ai-status')
.get('/api/ai-status', {
params: { gallery: gallerySlug.value },
})
.then((response) => {
aiAvailable.value = response.data.some((provider) => provider.available);
})
@@ -258,7 +283,9 @@ const fetchImages = (options = { silent: false }) => {
}
axios
.get('/api/images')
.get('/api/images', {
params: { gallery: gallerySlug.value },
})
.then((response) => {
if (Array.isArray(response.data)) {
images.value = response.data;
@@ -294,7 +321,7 @@ const showContextMenu = (image) => {
};
const printImage = () => {
if (!selectedImage.value) {
if (!selectedImage.value || !showPrintButton.value) {
return;
}
currentOverlayComponent.value = 'printQuantityModal';
@@ -311,7 +338,7 @@ const downloadImage = (imageParam = null) => {
axios
.post(
'/api/download-image',
{ image_path: image.path },
{ image_path: image.path, gallery: gallerySlug.value },
{ responseType: 'blob' }
)
.then((response) => {
@@ -348,6 +375,7 @@ const handlePrintConfirmed = (quantity) => {
image_id: identifier,
image_path: selectedImage.value.path,
quantity,
gallery: gallerySlug.value,
})
.then((response) => {
console.log('Print request sent successfully:', response.data);
@@ -364,6 +392,11 @@ const handlePrintConfirmed = (quantity) => {
};
const applyStyle = (style, imageId) => {
if (!allowAiStyles.value) {
showToast('Stilwechsel ist für diese Galerie deaktiviert.', 'error');
return;
}
const targetImageId = imageId ?? getImageIdentifier(selectedImage.value);
if (!targetImageId) {
showToast('Kein Bild ausgewählt.', 'error');
@@ -379,6 +412,7 @@ const applyStyle = (style, imageId) => {
.post('/api/images/style-change', {
image_id: targetImageId,
style_id: style.id,
gallery: gallerySlug.value,
})
.then((response) => {
const promptId = response.data.prompt_id;
@@ -386,7 +420,9 @@ const applyStyle = (style, imageId) => {
if (plugin === 'ComfyUi') {
axios
.get(`/api/comfyui-url?style_id=${style.id}`)
.get('/api/comfyui-url', {
params: { style_id: style.id, gallery: gallerySlug.value },
})
.then((comfyResponse) => {
const comfyUiBaseUrl = comfyResponse.data.comfyui_url;
const wsUrl = `ws://${new URL(comfyUiBaseUrl).host}/ws`;
@@ -403,7 +439,9 @@ const applyStyle = (style, imageId) => {
if (processingProgress.value >= 100) {
axios
.get(`/api/images/fetch-styled/${promptId}`)
.get(`/api/images/fetch-styled/${promptId}`, {
params: { gallery: gallerySlug.value },
})
.then((imageResponse) => {
styledImage.value = imageResponse.data.styled_image;
currentOverlayComponent.value = 'styledImageDisplay';
@@ -443,7 +481,9 @@ const applyStyle = (style, imageId) => {
} else {
const pollForStyledImage = () => {
axios
.get(`/api/images/fetch-styled/${promptId}`)
.get(`/api/images/fetch-styled/${promptId}`, {
params: { gallery: gallerySlug.value },
})
.then((imageResponse) => {
styledImage.value = imageResponse.data.styled_image;
currentOverlayComponent.value = 'styledImageDisplay';
@@ -482,8 +522,28 @@ const keepStyledImage = () => {
};
const deleteStyledImage = () => {
currentOverlayComponent.value = null;
styledImage.value = null;
if (!styledImage.value?.id) {
currentOverlayComponent.value = null;
styledImage.value = null;
return;
}
axios
.delete(`/api/images/${styledImage.value.id}/styled`, {
params: { gallery: gallerySlug.value },
})
.then(() => {
showToast('Bild gelöscht.', 'success');
fetchImages({ silent: true });
})
.catch((error) => {
console.error('Error deleting styled image:', error);
showToast(error.response?.data?.error || 'Bild konnte nicht gelöscht werden.', 'error');
})
.finally(() => {
currentOverlayComponent.value = null;
styledImage.value = null;
});
};
const prevPage = () => {
@@ -541,7 +601,9 @@ onMounted(() => {
}
axios
.get('/api/image-refresh-interval')
.get('/api/image-refresh-interval', {
params: { gallery: gallerySlug.value },
})
.then((response) => {
startAutoRefresh(response.data.interval * 1000);
})
@@ -551,7 +613,9 @@ onMounted(() => {
});
axios
.get('/api/max-copies-setting')
.get('/api/max-copies-setting', {
params: { gallery: gallerySlug.value },
})
.then((response) => {
maxCopiesSetting.value = response.data.max_copies;
})