finished the upgrade to filament 4. completely revamped the frontend with codex, now it looks great!
This commit is contained in:
@@ -1,109 +1,226 @@
|
||||
<template>
|
||||
<Head title="Start" />
|
||||
<div class="home">
|
||||
<div class="main-content">
|
||||
<div class="gallery-container" @touchstart="handleTouchStart" @touchend="handleTouchEnd">
|
||||
<h1 class="text-2xl font-bold text-center my-4">{{ props.galleryHeading }}</h1>
|
||||
<div class="absolute top-4 right-4">
|
||||
<button @click="toggleTheme" class="theme-toggle-button">
|
||||
{{ currentTheme === 'light' ? __('api.dark_mode') : __('api.light_mode') }}
|
||||
</button>
|
||||
<Head title="Start" />
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-slate-100">
|
||||
<div class="mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-8 sm:px-6 lg:px-8">
|
||||
<header class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-2xl backdrop-blur">
|
||||
<div class="flex flex-wrap items-start justify-between gap-6">
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-[0.35em] text-slate-400">Live Gallery</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-white sm:text-4xl">
|
||||
{{ props.galleryHeading }}
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-slate-400">Touch-friendly wall with the freshest images.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-4 py-2 text-sm font-semibold text-white transition hover:border-emerald-400 hover:text-emerald-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-400 disabled:opacity-60"
|
||||
@click="handleManualRefresh"
|
||||
:disabled="isRefreshing"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
:class="{ 'animate-spin': isRefreshing }"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992V4.355m0 0-2.852 2.853A8.25 8.25 0 0 0 4.5 12c0 1.007.176 1.973.5 2.869m2.977 4.784H3v4.993m0 0 2.853-2.853A8.25 8.25 0 0 0 19.5 12c0-1.007-.176-1.973-.5-2.869"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ isRefreshing ? 'Aktualisiere…' : 'Jetzt aktualisieren' }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-2 text-sm font-semibold text-white transition hover:border-cyan-300 hover:text-cyan-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<svg v-if="currentTheme === 'light'" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 15A9.75 9.75 0 1 1 9 2.25 7.5 7.5 0 0 0 21.75 15Z" />
|
||||
</svg>
|
||||
<svg v-else class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-3.773-4.227-1.591 1.591M5.25 12H3m4.227-3.773L5.636 6.636M12 8.25a3.75 3.75 0 1 0 0 7.5 3.75 3.75 0 0 0 0-7.5Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ currentTheme === 'light' ? __('api.dark_mode') : __('api.light_mode') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-wrap items-center gap-4 text-xs text-slate-300">
|
||||
<div class="inline-flex items-center gap-2 rounded-full bg-emerald-400/10 px-3 py-1 text-emerald-200">
|
||||
<span class="h-2 w-2 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||
Auto-Refresh · alle {{ refreshIntervalSeconds }}s
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-slate-400"></span>
|
||||
Letzte Synchronisierung: {{ formattedLastRefresh }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5h7.5m-9 5.25h10.5M6 15h12m-9 5.25h6" />
|
||||
</svg>
|
||||
Einfach über die Galerie wischen, um Seiten zu wechseln.
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section
|
||||
class="rounded-3xl border border-white/5 bg-white/5 p-4 shadow-2xl backdrop-blur touch-pan-y"
|
||||
@touchstart.passive="handleTouchStart"
|
||||
@touchmove.passive="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
>
|
||||
<GalleryGrid :images="paginatedImages" @imageTapped="showContextMenu" :translations="props.translations" />
|
||||
<Navigation
|
||||
:currentPage="currentPage"
|
||||
:totalPages="totalPages"
|
||||
@prevPage="prevPage"
|
||||
@nextPage="nextPage"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Navigation :currentPage="currentPage" :totalPages="totalPages" @prevPage="prevPage" @nextPage="nextPage" />
|
||||
</div>
|
||||
|
||||
<ImageContextMenu
|
||||
v-if="currentOverlayComponent === 'contextMenu'"
|
||||
:position="contextMenuPosition"
|
||||
v-if="currentOverlayComponent === 'contextMenu' && selectedImage"
|
||||
:image="selectedImage"
|
||||
@close="currentOverlayComponent = null; selectedImage = null"
|
||||
@close="closeOverlays"
|
||||
@print="printImage"
|
||||
@download="downloadImage"
|
||||
@changeStyle="showStyleSelector"
|
||||
@styleSelected="applyStyle"
|
||||
/>
|
||||
|
||||
<StyleSelector
|
||||
v-if="currentOverlayComponent === 'styleSelector'"
|
||||
:image_id="selectedImage.id"
|
||||
@styleSelected="applyStyle"
|
||||
@back="goBackToContextMenu"
|
||||
@close="currentOverlayComponent = null; selectedImage = null"
|
||||
/>
|
||||
|
||||
<div v-if="errorMessage" class="fixed bottom-4 right-4 bg-red-500 text-white p-4 rounded-lg shadow-lg z-50">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<StyledImageDisplay
|
||||
v-if="currentOverlayComponent === 'styledImageDisplay'"
|
||||
v-if="currentOverlayComponent === 'styledImageDisplay' && styledImage"
|
||||
:image="styledImage"
|
||||
@keep="keepStyledImage"
|
||||
@delete="deleteStyledImage"
|
||||
/>
|
||||
|
||||
<LoadingSpinner v-if="isLoading" :progress="processingProgress" />
|
||||
|
||||
<PrintQuantityModal
|
||||
v-if="currentOverlayComponent === 'printQuantityModal'"
|
||||
@close="currentOverlayComponent = null"
|
||||
v-if="currentOverlayComponent === 'printQuantityModal' && selectedImage"
|
||||
@close="currentOverlayComponent = 'contextMenu'"
|
||||
@printConfirmed="handlePrintConfirmed"
|
||||
:maxCopies="maxCopiesSetting"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="toastMessage"
|
||||
:class="['fixed bottom-6 right-6 z-[120] rounded-2xl px-4 py-3 text-sm font-semibold text-white shadow-2xl', toastBackground]"
|
||||
>
|
||||
{{ toastMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from 'vue';
|
||||
import { Head, router } from '@inertiajs/vue3';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
import GalleryGrid from '../Components/GalleryGrid.vue';
|
||||
import ImageContextMenu from '../Components/ImageContextMenu.vue';
|
||||
import Navigation from '../Components/Navigation.vue';
|
||||
import PrintQuantityModal from '../Components/PrintQuantityModal.vue';
|
||||
import StyledImageDisplay from '../Components/StyledImageDisplay.vue';
|
||||
import LoadingSpinner from '../Components/LoadingSpinner.vue';
|
||||
|
||||
const props = defineProps({
|
||||
galleryHeading: String,
|
||||
translations: Object,
|
||||
galleryHeading: {
|
||||
type: String,
|
||||
default: 'Gallery',
|
||||
},
|
||||
translations: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
images: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const images = ref([]);
|
||||
let fetchInterval = null;
|
||||
|
||||
const fetchImages = () => {
|
||||
axios.get('/api/images')
|
||||
.then(response => {
|
||||
images.value = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching images:', error);
|
||||
showError(error.response?.data?.error || 'An unknown error occurred.');
|
||||
});
|
||||
};
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import Navigation from '../Components/Navigation.vue';
|
||||
import GalleryGrid from '../Components/GalleryGrid.vue';
|
||||
import ImageContextMenu from '../Components/ImageContextMenu.vue';
|
||||
import StyleSelector from '../Components/StyleSelector.vue';
|
||||
import StyledImageDisplay from '../Components/StyledImageDisplay.vue'; // Import the new component
|
||||
import LoadingSpinner from '../Components/LoadingSpinner.vue'; // Import the new component
|
||||
import PrintQuantityModal from '../Components/PrintQuantityModal.vue'; // Import the new component
|
||||
import axios from 'axios';
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const imagesPerPage = 12;
|
||||
const currentPage = ref(1);
|
||||
const currentOverlayComponent = ref(null); // null, 'contextMenu', 'styleSelector', 'styledImageDisplay'
|
||||
const contextMenuPosition = ref({ x: 0, y: 0 });
|
||||
const currentOverlayComponent = ref(null);
|
||||
const selectedImage = ref(null);
|
||||
const styledImage = ref(null); // To store the newly styled image
|
||||
const processingProgress = ref(0); // To store the progress percentage
|
||||
const errorMessage = ref(null); // New ref for error messages
|
||||
const isLoading = ref(false); // New ref for loading state
|
||||
const currentTheme = ref('light'); // New ref for current theme
|
||||
const maxCopiesSetting = ref(10); // Default to 10, will be fetched from backend
|
||||
const styledImage = ref(null);
|
||||
const processingProgress = ref(0);
|
||||
const isLoading = ref(false);
|
||||
const currentTheme = ref('light');
|
||||
const maxCopiesSetting = ref(10);
|
||||
const isRefreshing = ref(false);
|
||||
const lastRefreshedAt = ref(new Date());
|
||||
const refreshIntervalMs = ref(5000);
|
||||
const toastMessage = ref(null);
|
||||
const toastVariant = ref('info');
|
||||
let toastTimer = null;
|
||||
let refreshTimer = null;
|
||||
|
||||
let touchStartX = 0;
|
||||
let touchEndX = 0;
|
||||
const getImageIdentifier = (image) => image?.id ?? image?.image_id ?? null;
|
||||
|
||||
watch(
|
||||
() => props.images,
|
||||
(newImages) => {
|
||||
if (Array.isArray(newImages)) {
|
||||
images.value = newImages;
|
||||
lastRefreshedAt.value = new Date();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => images.value.length,
|
||||
() => {
|
||||
if (currentPage.value > totalPages.value) {
|
||||
currentPage.value = totalPages.value || 1;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const totalPages = computed(() => {
|
||||
return Math.max(1, Math.ceil(images.value.length / imagesPerPage));
|
||||
});
|
||||
|
||||
const paginatedImages = computed(() => {
|
||||
const start = (currentPage.value - 1) * imagesPerPage;
|
||||
return images.value.slice(start, start + imagesPerPage);
|
||||
});
|
||||
|
||||
const refreshIntervalSeconds = computed(() => Math.max(1, Math.round(refreshIntervalMs.value / 1000)));
|
||||
const formattedLastRefresh = computed(() =>
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}).format(lastRefreshedAt.value)
|
||||
);
|
||||
|
||||
const toastBackground = computed(() => {
|
||||
if (toastVariant.value === 'error') {
|
||||
return 'bg-rose-500/90';
|
||||
}
|
||||
if (toastVariant.value === 'success') {
|
||||
return 'bg-emerald-500/90';
|
||||
}
|
||||
return 'bg-slate-800/90';
|
||||
});
|
||||
|
||||
const showToast = (message, variant = 'error') => {
|
||||
toastMessage.value = message;
|
||||
toastVariant.value = variant;
|
||||
if (toastTimer) {
|
||||
clearTimeout(toastTimer);
|
||||
}
|
||||
toastTimer = setTimeout(() => {
|
||||
toastMessage.value = null;
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
const applyTheme = (theme) => {
|
||||
document.documentElement.classList.remove('light', 'dark');
|
||||
@@ -117,231 +234,240 @@ const toggleTheme = () => {
|
||||
applyTheme(newTheme);
|
||||
};
|
||||
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(images.value.length / imagesPerPage);
|
||||
});
|
||||
|
||||
const paginatedImages = computed(() => {
|
||||
const start = (currentPage.value - 1) * imagesPerPage;
|
||||
const end = start + imagesPerPage;
|
||||
return images.value.slice(start, end);
|
||||
});
|
||||
|
||||
const showError = (message) => {
|
||||
errorMessage.value = message;
|
||||
setTimeout(() => {
|
||||
errorMessage.value = null;
|
||||
}, 5000); // Clear error after 5 seconds
|
||||
const closeOverlays = () => {
|
||||
currentOverlayComponent.value = null;
|
||||
selectedImage.value = null;
|
||||
};
|
||||
|
||||
const showContextMenu = (image, event) => {
|
||||
const fetchImages = (options = { silent: false }) => {
|
||||
if (!options.silent) {
|
||||
isRefreshing.value = true;
|
||||
}
|
||||
|
||||
router.reload({
|
||||
only: ['images'],
|
||||
preserveScroll: true,
|
||||
onSuccess: (page) => {
|
||||
if (Array.isArray(page.props.images)) {
|
||||
images.value = page.props.images;
|
||||
lastRefreshedAt.value = new Date();
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
showToast('Die Galerie konnte nicht aktualisiert werden.', 'error');
|
||||
},
|
||||
onFinish: () => {
|
||||
if (!options.silent) {
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const startAutoRefresh = (intervalMs) => {
|
||||
refreshIntervalMs.value = intervalMs;
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
}
|
||||
refreshTimer = setInterval(() => fetchImages({ silent: true }), intervalMs);
|
||||
};
|
||||
|
||||
const handleManualRefresh = () => {
|
||||
fetchImages({ silent: false });
|
||||
};
|
||||
|
||||
const showContextMenu = (image) => {
|
||||
selectedImage.value = image;
|
||||
contextMenuPosition.value = { x: event.clientX, y: event.clientY };
|
||||
currentOverlayComponent.value = 'contextMenu';
|
||||
};
|
||||
|
||||
const printImage = () => {
|
||||
console.log('Showing print quantity modal for image:', selectedImage.value);
|
||||
if (!selectedImage.value) {
|
||||
return;
|
||||
}
|
||||
currentOverlayComponent.value = 'printQuantityModal';
|
||||
};
|
||||
|
||||
const downloadImage = (image) => {
|
||||
console.log('Starting download for image:', image);
|
||||
|
||||
// Show loading message
|
||||
showError('Download wird vorbereitet...');
|
||||
|
||||
// Use axios to make a POST request to trigger backend download
|
||||
axios.post('/api/download-image', {
|
||||
image_path: image.path,
|
||||
}, {
|
||||
responseType: 'blob' // Important for file downloads
|
||||
})
|
||||
.then(response => {
|
||||
// Create blob URL from response
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Create temporary link and trigger download
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = image.name || 'image';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// Clean up blob URL
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
showError('Download gestartet!');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error downloading image:', error);
|
||||
showError(error.response?.data?.error || 'Download fehlgeschlagen.');
|
||||
});
|
||||
const downloadImage = (imageParam = null) => {
|
||||
const image = imageParam || selectedImage.value;
|
||||
if (!image) {
|
||||
return;
|
||||
}
|
||||
|
||||
showToast('Download wird vorbereitet…', 'info');
|
||||
|
||||
axios
|
||||
.post(
|
||||
'/api/download-image',
|
||||
{ image_path: image.path },
|
||||
{ responseType: 'blob' }
|
||||
)
|
||||
.then((response) => {
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = image.name || 'image';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
showToast('Download gestartet!', 'success');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error downloading image:', error);
|
||||
showToast(error.response?.data?.error || 'Download fehlgeschlagen.', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
const handlePrintConfirmed = (quantity) => {
|
||||
console.log(`Printing ${quantity} copies of image:`, selectedImage.value);
|
||||
currentOverlayComponent.value = null; // Close the modal
|
||||
const identifier = getImageIdentifier(selectedImage.value);
|
||||
if (!identifier) {
|
||||
showToast('Kein Bild ausgewählt.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/print-image', {
|
||||
image_id: selectedImage.value.id,
|
||||
image_path: selectedImage.value.path,
|
||||
quantity: quantity,
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Print request sent successfully:', response.data);
|
||||
showError('Print request sent successfully!');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error sending print request:', error);
|
||||
showError(error.response?.data?.error || 'Failed to send print request.');
|
||||
});
|
||||
};
|
||||
|
||||
const showStyleSelector = () => {
|
||||
currentOverlayComponent.value = 'styleSelector';
|
||||
};
|
||||
|
||||
const goBackToContextMenu = () => {
|
||||
currentOverlayComponent.value = 'contextMenu';
|
||||
axios
|
||||
.post('/api/print-image', {
|
||||
image_id: identifier,
|
||||
image_path: selectedImage.value.path,
|
||||
quantity,
|
||||
})
|
||||
.then((response) => {
|
||||
console.log('Print request sent successfully:', response.data);
|
||||
showToast('Druckauftrag wurde übermittelt.', 'success');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error sending print request:', error);
|
||||
showToast(error.response?.data?.error || 'Druckauftrag fehlgeschlagen.', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
currentOverlayComponent.value = null;
|
||||
selectedImage.value = null;
|
||||
});
|
||||
};
|
||||
|
||||
const applyStyle = (style, imageId) => {
|
||||
console.log('Applying style:', style.title, 'to image:', imageId);
|
||||
currentOverlayComponent.value = null; // Close style selector immediately
|
||||
isLoading.value = true; // Show loading spinner
|
||||
processingProgress.value = 0; // Reset progress
|
||||
const targetImageId = imageId ?? getImageIdentifier(selectedImage.value);
|
||||
if (!targetImageId) {
|
||||
showToast('Kein Bild ausgewählt.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send style change request to backend first
|
||||
axios.post('/api/images/style-change', {
|
||||
image_id: imageId,
|
||||
style_id: style.id,
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Style change request sent:', response.data);
|
||||
// Store the prompt_id and plugin from the backend response
|
||||
const promptId = response.data.prompt_id;
|
||||
const plugin = response.data.plugin;
|
||||
console.log('Applying style:', style.title, 'to image:', targetImageId);
|
||||
currentOverlayComponent.value = null;
|
||||
isLoading.value = true;
|
||||
processingProgress.value = 0;
|
||||
|
||||
// Handle different plugins differently
|
||||
if (plugin === 'ComfyUi') {
|
||||
// For ComfyUI, use WebSocket for progress monitoring
|
||||
axios.get(`/api/comfyui-url?style_id=${style.id}`)
|
||||
.then(comfyResponse => {
|
||||
const comfyUiBaseUrl = comfyResponse.data.comfyui_url;
|
||||
const wsUrl = `ws://${new URL(comfyUiBaseUrl).host}/ws`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
axios
|
||||
.post('/api/images/style-change', {
|
||||
image_id: targetImageId,
|
||||
style_id: style.id,
|
||||
})
|
||||
.then((response) => {
|
||||
const promptId = response.data.prompt_id;
|
||||
const plugin = response.data.plugin;
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected to ComfyUI.');
|
||||
if (plugin === 'ComfyUi') {
|
||||
axios
|
||||
.get(`/api/comfyui-url?style_id=${style.id}`)
|
||||
.then((comfyResponse) => {
|
||||
const comfyUiBaseUrl = comfyResponse.data.comfyui_url;
|
||||
const wsUrl = `ws://${new URL(comfyUiBaseUrl).host}/ws`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.type === 'progress') {
|
||||
console.log('ComfyUI Progress Message:', message);
|
||||
const { value, max } = message.data;
|
||||
const progress = (max > 0) ? (value / max) * 100 : 0;
|
||||
if (message.data.prompt_id === promptId) {
|
||||
processingProgress.value = progress;
|
||||
ws.onopen = () => {
|
||||
ws.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.type === 'progress') {
|
||||
const { value, max } = message.data;
|
||||
const progress = max > 0 ? (value / max) * 100 : 0;
|
||||
if (message.data.prompt_id === promptId) {
|
||||
processingProgress.value = progress;
|
||||
|
||||
if (processingProgress.value >= 100) {
|
||||
console.log('Frontend: Progress reached 100%. Attempting to fetch final image.', { promptId: promptId });
|
||||
// Fetch the final styled image from the backend
|
||||
axios.get(`/api/images/fetch-styled/${promptId}`)
|
||||
.then(imageResponse => {
|
||||
console.log('Frontend: Successfully fetched styled image.', imageResponse.data);
|
||||
styledImage.value = imageResponse.data.styled_image;
|
||||
currentOverlayComponent.value = 'styledImageDisplay';
|
||||
fetchImages(); // Refresh gallery
|
||||
})
|
||||
.catch(imageError => {
|
||||
console.error('Frontend: Error fetching styled image:', imageError.response?.data?.error || imageError.message);
|
||||
showError(imageError.response?.data?.error || 'Failed to fetch styled image.');
|
||||
})
|
||||
.finally(() => {
|
||||
console.log('Frontend: Final fetch process completed.');
|
||||
isLoading.value = false;
|
||||
processingProgress.value = 0;
|
||||
ws.close();
|
||||
});
|
||||
if (processingProgress.value >= 100) {
|
||||
axios
|
||||
.get(`/api/images/fetch-styled/${promptId}`)
|
||||
.then((imageResponse) => {
|
||||
styledImage.value = imageResponse.data.styled_image;
|
||||
currentOverlayComponent.value = 'styledImageDisplay';
|
||||
fetchImages({ silent: true });
|
||||
})
|
||||
.catch((imageError) => {
|
||||
console.error('Frontend: Error fetching styled image:', imageError.response?.data?.error || imageError.message);
|
||||
showToast(imageError.response?.data?.error || 'Failed to fetch styled image.', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
processingProgress.value = 0;
|
||||
ws.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('Received unexpected WebSocket message type:', message.type, message);
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
showError('WebSocket connection error.');
|
||||
isLoading.value = false;
|
||||
processingProgress.value = 0;
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket closed.');
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching ComfyUI URL:', error);
|
||||
showError(error.response?.data?.error || 'Failed to get ComfyUI URL.');
|
||||
isLoading.value = false;
|
||||
});
|
||||
} else {
|
||||
// For other plugins, use polling approach
|
||||
const pollForStyledImage = () => {
|
||||
axios.get(`/api/images/fetch-styled/${promptId}`)
|
||||
.then(imageResponse => {
|
||||
console.log('Frontend: Successfully fetched styled image.', imageResponse.data);
|
||||
styledImage.value = imageResponse.data.styled_image;
|
||||
currentOverlayComponent.value = 'styledImageDisplay';
|
||||
fetchImages(); // Refresh gallery
|
||||
isLoading.value = false;
|
||||
processingProgress.value = 0;
|
||||
})
|
||||
.catch(imageError => {
|
||||
console.error('Frontend: Error fetching styled image:', imageError.response?.data?.error || imageError.message);
|
||||
// If the image is not ready yet, continue polling
|
||||
if (imageError.response?.status === 404) {
|
||||
// Update progress if available
|
||||
if (imageError.response?.data?.progress !== undefined) {
|
||||
processingProgress.value = imageError.response.data.progress;
|
||||
}
|
||||
// Continue polling
|
||||
setTimeout(pollForStyledImage, 2000); // Poll every 2 seconds
|
||||
} else {
|
||||
showError(imageError.response?.data?.error || 'Failed to fetch styled image.');
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
showToast('WebSocket connection error.', 'error');
|
||||
isLoading.value = false;
|
||||
processingProgress.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket closed.');
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching ComfyUI URL:', error);
|
||||
showToast(error.response?.data?.error || 'Failed to get ComfyUI URL.', 'error');
|
||||
isLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// Start polling for the styled image
|
||||
pollForStyledImage();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error applying style:', error);
|
||||
showError(error.response?.data?.error || 'Failed to apply style.');
|
||||
isLoading.value = false;
|
||||
processingProgress.value = 0;
|
||||
});
|
||||
} else {
|
||||
const pollForStyledImage = () => {
|
||||
axios
|
||||
.get(`/api/images/fetch-styled/${promptId}`)
|
||||
.then((imageResponse) => {
|
||||
styledImage.value = imageResponse.data.styled_image;
|
||||
currentOverlayComponent.value = 'styledImageDisplay';
|
||||
fetchImages({ silent: true });
|
||||
isLoading.value = false;
|
||||
processingProgress.value = 0;
|
||||
})
|
||||
.catch((imageError) => {
|
||||
if (imageError.response?.status === 404) {
|
||||
if (imageError.response?.data?.progress !== undefined) {
|
||||
processingProgress.value = imageError.response.data.progress;
|
||||
}
|
||||
setTimeout(pollForStyledImage, 2000);
|
||||
} else {
|
||||
showToast(imageError.response?.data?.error || 'Failed to fetch styled image.', 'error');
|
||||
isLoading.value = false;
|
||||
processingProgress.value = 0;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
pollForStyledImage();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error applying style:', error);
|
||||
showToast(error.response?.data?.error || 'Failed to apply style.', 'error');
|
||||
isLoading.value = false;
|
||||
processingProgress.value = 0;
|
||||
});
|
||||
};
|
||||
|
||||
const keepStyledImage = (imageToKeep) => {
|
||||
console.log('Keeping styled image:', imageToKeep);
|
||||
// Implement API call to mark image as kept/permanent if needed
|
||||
currentOverlayComponent.value = null; // Close the display
|
||||
const keepStyledImage = () => {
|
||||
currentOverlayComponent.value = null;
|
||||
styledImage.value = null;
|
||||
};
|
||||
|
||||
const deleteStyledImage = (imageToDelete) => {
|
||||
console.log('Deleting styled image:', imageToDelete);
|
||||
// Implement API call to delete the temporary styled image
|
||||
currentOverlayComponent.value = null; // Close the display
|
||||
const deleteStyledImage = () => {
|
||||
currentOverlayComponent.value = null;
|
||||
styledImage.value = null;
|
||||
};
|
||||
|
||||
const prevPage = () => {
|
||||
@@ -356,28 +482,39 @@ const nextPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const touchStartX = ref(0);
|
||||
const touchCurrentX = ref(0);
|
||||
const swipeThreshold = 60;
|
||||
|
||||
const handleTouchStart = (event) => {
|
||||
touchStartX = event.touches[0].clientX;
|
||||
if (!event.touches.length) {
|
||||
return;
|
||||
}
|
||||
touchStartX.value = event.touches[0].clientX;
|
||||
touchCurrentX.value = touchStartX.value;
|
||||
};
|
||||
|
||||
const handleTouchMove = (event) => {
|
||||
if (!event.touches.length) {
|
||||
return;
|
||||
}
|
||||
touchCurrentX.value = event.touches[0].clientX;
|
||||
};
|
||||
|
||||
const handleTouchEnd = (event) => {
|
||||
touchEndX = event.changedTouches[0].clientX;
|
||||
handleSwipeGesture();
|
||||
};
|
||||
const endX = event.changedTouches?.[0]?.clientX ?? touchCurrentX.value;
|
||||
const delta = endX - touchStartX.value;
|
||||
|
||||
const handleSwipeGesture = () => {
|
||||
const swipeThreshold = 50; // Minimum distance for a swipe
|
||||
if (touchEndX < touchStartX - swipeThreshold) {
|
||||
// Swiped left
|
||||
nextPage();
|
||||
} else if (touchEndX > touchStartX + swipeThreshold) {
|
||||
// Swiped right
|
||||
prevPage();
|
||||
if (Math.abs(delta) > swipeThreshold) {
|
||||
if (delta < 0) {
|
||||
nextPage();
|
||||
} else {
|
||||
prevPage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// Apply theme from localStorage on mount
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
applyTheme(savedTheme);
|
||||
@@ -387,84 +524,32 @@ onMounted(() => {
|
||||
applyTheme('light');
|
||||
}
|
||||
|
||||
fetchImages();
|
||||
|
||||
// Fetch image refresh interval from API
|
||||
axios.get('/api/image-refresh-interval')
|
||||
.then(response => {
|
||||
const interval = response.data.interval * 1000;
|
||||
fetchInterval = setInterval(fetchImages, interval);
|
||||
axios
|
||||
.get('/api/image-refresh-interval')
|
||||
.then((response) => {
|
||||
startAutoRefresh(response.data.interval * 1000);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error fetching image refresh interval:', error);
|
||||
fetchInterval = setInterval(fetchImages, 5000); // Fallback to 5 seconds
|
||||
startAutoRefresh(5000);
|
||||
});
|
||||
|
||||
// Fetch max number of copies setting from API
|
||||
axios.get('/api/max-copies-setting')
|
||||
.then(response => {
|
||||
axios
|
||||
.get('/api/max-copies-setting')
|
||||
.then((response) => {
|
||||
maxCopiesSetting.value = response.data.max_copies;
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error fetching max copies setting:', error);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(fetchInterval);
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
}
|
||||
if (toastTimer) {
|
||||
clearTimeout(toastTimer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
.gallery-container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.theme-toggle-button {
|
||||
margin-top: 10px;
|
||||
padding: 8px 15px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.theme-toggle-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.image-visibility-toggle-button {
|
||||
margin-top: 10px;
|
||||
padding: 8px 15px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.image-visibility-toggle-button:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user