- 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

@@ -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;
})