finished the upgrade to filament 4. completely revamped the frontend with codex, now it looks great!
This commit is contained in:
@@ -50,9 +50,8 @@
|
|||||||
<p class="text-sm font-normal text-slate-500 dark:text-slate-400">Lass die KI dein Motiv verzaubern.</p>
|
<p class="text-sm font-normal text-slate-500 dark:text-slate-400">Lass die KI dein Motiv verzaubern.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="flex h-12 w-12 items-center justify-center rounded-full bg-white/60 text-slate-900 shadow-md dark:bg-slate-800/70 dark:text-white">
|
<span class="flex h-12 w-12 items-center justify-center rounded-full bg-white/60 text-slate-900 shadow-md dark:bg-slate-800/70 dark:text-white">
|
||||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904a2.25 2.25 0 0 1 0-3.808l.99-.572a1.125 1.125 0 0 0 0-1.948l-.99-.572a2.25 2.25 0 0 1 0-3.808l5.25-3.038A2.25 2.25 0 0 1 17.625 3v18a2.25 2.25 0 0 1-3.563 1.844l-4.249-2.457Z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v3m0 12v3m9-9h-3M6 12H3m13.5-6 1.5 1.5M7.5 18 6 19.5M18 7.5 19.5 6M6 4.5 4.5 6m9.75 3.75-2.122 2.122a2.25 2.25 0 0 0 0 3.182l2.122 2.121a2.25 2.25 0 0 0 3.182 0l2.121-2.121a2.25 2.25 0 0 0 0-3.182l-2.121-2.122a2.25 2.25 0 0 0-3.182 0Z" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.25a.75.75 0 0 1 .75.75v12a.75.75 0 0 1-1.5 0v-12a.75.75 0 0 1 .75-.75Zm13.5 0a.75.75 0 0 1 .75.75v12a.75.75 0 0 1-1.5 0v-12a.75.75 0 0 1 .75-.75Z" />
|
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="stylesContainer" class="flex-1 space-y-3 overflow-y-auto pr-1">
|
<div class="flex-1 space-y-3 overflow-y-auto pr-1">
|
||||||
<p v-if="isLoading" class="rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-center text-sm text-slate-300">
|
<p v-if="isLoading" class="rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-center text-sm text-slate-300">
|
||||||
Stile werden geladen …
|
Stile werden geladen …
|
||||||
</p>
|
</p>
|
||||||
@@ -31,7 +31,8 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="style.preview_image"
|
v-if="style.preview_image"
|
||||||
:data-src="'/storage/' + style.preview_image"
|
:src="buildPreviewPath(style.preview_image)"
|
||||||
|
loading="lazy"
|
||||||
class="h-24 w-24 flex-none rounded-2xl object-cover opacity-90 transition group-hover:opacity-100"
|
class="h-24 w-24 flex-none rounded-2xl object-cover opacity-90 transition group-hover:opacity-100"
|
||||||
alt="Style preview"
|
alt="Style preview"
|
||||||
/>
|
/>
|
||||||
@@ -53,13 +54,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
const styles = ref([]);
|
const styles = ref([]);
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
const loadError = ref(null);
|
const loadError = ref(null);
|
||||||
const stylesContainer = ref(null);
|
|
||||||
let observer = null;
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
image_id: {
|
image_id: {
|
||||||
@@ -70,31 +69,6 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emits = defineEmits(['styleSelected', 'close']);
|
const emits = defineEmits(['styleSelected', 'close']);
|
||||||
|
|
||||||
const hydrateObserver = () => {
|
|
||||||
if (!stylesContainer.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (observer) {
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
observer = new IntersectionObserver(
|
|
||||||
(entries, obs) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
const img = entry.target;
|
|
||||||
img.src = img.dataset.src;
|
|
||||||
obs.unobserve(img);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ root: stylesContainer.value }
|
|
||||||
);
|
|
||||||
|
|
||||||
stylesContainer.value.querySelectorAll('img[data-src]').forEach((img) => observer.observe(img));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchStyles = () => {
|
const fetchStyles = () => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
loadError.value = null;
|
loadError.value = null;
|
||||||
@@ -102,7 +76,6 @@ const fetchStyles = () => {
|
|||||||
.get('/api/styles')
|
.get('/api/styles')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
styles.value = response.data;
|
styles.value = response.data;
|
||||||
nextTick(hydrateObserver);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error fetching styles:', error);
|
console.error('Error fetching styles:', error);
|
||||||
@@ -117,13 +90,9 @@ const selectStyle = (style) => {
|
|||||||
emits('styleSelected', style, props.image_id);
|
emits('styleSelected', style, props.image_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildPreviewPath = (preview) => `/storage/${preview}`;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchStyles();
|
fetchStyles();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (observer) {
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -163,6 +163,24 @@ let refreshTimer = null;
|
|||||||
|
|
||||||
const getImageIdentifier = (image) => image?.id ?? image?.image_id ?? null;
|
const getImageIdentifier = (image) => image?.id ?? image?.image_id ?? null;
|
||||||
|
|
||||||
|
const extractFilenameFromHeader = (disposition) => {
|
||||||
|
if (!disposition) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filenameMatch = disposition.match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i);
|
||||||
|
if (!filenameMatch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedName = filenameMatch[1] || filenameMatch[2];
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(encodedName);
|
||||||
|
} catch (error) {
|
||||||
|
return encodedName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.images,
|
() => props.images,
|
||||||
(newImages) => {
|
(newImages) => {
|
||||||
@@ -306,8 +324,12 @@ const downloadImage = (imageParam = null) => {
|
|||||||
const blob = new Blob([response.data]);
|
const blob = new Blob([response.data]);
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
|
const disposition = response.headers?.['content-disposition'];
|
||||||
|
const filenameFromHeader = extractFilenameFromHeader(disposition);
|
||||||
|
const fallbackName = image.name || `stylegallery_${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
||||||
|
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = image.name || 'image';
|
link.download = filenameFromHeader || fallbackName;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
|
|||||||
Reference in New Issue
Block a user