Skip to content

Commit 935db4e

Browse files
committed
Readd the image switcher, this time without videos and reddit, but with proper loading of image at startup, optimization and everything else
1 parent 9bffe55 commit 935db4e

File tree

14 files changed

+326
-12
lines changed

14 files changed

+326
-12
lines changed

components/Navbar.vue

+8-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import CustomDropdown from './Navbar/CustomDropdown.vue';
88
import MobileFullscreenModal from './Modal/MobileFullscreenModal.vue';
99
import NavbarUser from './Navbar/User.vue';
1010
import LanguageSelector from './Navbar/LanguageSelector.vue';
11+
import BackgroundSwitcher from './Navbar/BackgroundSwitcher.vue';
1112
1213
const Search = markRaw(SearchComponent);
1314
@@ -197,13 +198,13 @@ const navbarLinks = computed(() => {
197198
colorMode.preference = colorMode.preference === 'dark' ? 'light' : 'dark';
198199
},
199200
},
200-
//{
201-
// label: t('navbar.backgroundSelector.label'),
202-
// icon: 'i-heroicons-photo',
203-
// component: BackgroundSwitcher,
204-
// position: 'right',
205-
// mobile: true,
206-
//},
201+
{
202+
label: t('navbar.backgroundSelector.label'),
203+
icon: 'i-heroicons-photo',
204+
component: BackgroundSwitcher,
205+
position: 'right',
206+
mobile: true,
207+
},
207208
{
208209
label: t('navbar.information.label'),
209210
icon: 'lucide:info',
+278
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue';
3+
import CustomDropdown from './CustomDropdown.vue';
4+
import MobileFullscreenModal from '../Modal/MobileFullscreenModal.vue';
5+
6+
// Use the composables
7+
const { currentBackground, setSiteBackground } = siteBackground();
8+
9+
// Get the backgrounds that are available from the API
10+
const bgData = await useFetch('/api/site/backgrounds');
11+
const backgrounds = bgData.data;
12+
13+
// Track dropdown state
14+
const isDropdownOpen = ref(false);
15+
16+
// Track if we're displaying in fullscreen mode (for mobile)
17+
const isFullscreen = ref(false);
18+
19+
// Emit events for parent components
20+
const emit = defineEmits(['fullscreen-opened', 'fullscreen-closed', 'background-selected']);
21+
22+
// Props for component
23+
const props = defineProps({
24+
// Whether the component is being used on mobile
25+
isMobile: {
26+
type: Boolean,
27+
default: false
28+
},
29+
// Whether to open in fullscreen mode (controlled by parent)
30+
fullscreenOpen: {
31+
type: Boolean,
32+
default: false
33+
}
34+
});
35+
36+
// Watch for changes in fullscreen prop from parent
37+
watch(() => props.fullscreenOpen, (value) => {
38+
isFullscreen.value = value;
39+
});
40+
41+
// Check if a background is the current one - compare directly to the ref value
42+
const isCurrentBackground = (path: string): boolean => {
43+
return currentBackground.value === path;
44+
};
45+
46+
// Handle background selection
47+
const selectBackground = (path: string) => {
48+
setSiteBackground(path);
49+
50+
// Close dropdown or fullscreen mode
51+
isDropdownOpen.value = false;
52+
53+
if (isFullscreen.value) {
54+
emit('background-selected', path);
55+
emit('fullscreen-closed');
56+
isFullscreen.value = false;
57+
}
58+
};
59+
60+
// Open fullscreen mode
61+
const openFullscreen = () => {
62+
isFullscreen.value = true;
63+
emit('fullscreen-opened');
64+
};
65+
66+
// Close fullscreen mode
67+
const closeFullscreen = () => {
68+
isFullscreen.value = false;
69+
emit('fullscreen-closed');
70+
};
71+
72+
// Improved body scroll locking for mobile fullscreen view
73+
watch(isFullscreen, (isOpen) => {
74+
if (process.client) {
75+
const body = document.body;
76+
77+
if (isOpen) {
78+
// Store current scroll position
79+
const scrollY = window.scrollY;
80+
body.style.position = 'fixed';
81+
body.style.width = '100%';
82+
body.style.top = `-${scrollY}px`;
83+
body.style.overflow = 'hidden';
84+
body.classList.add('modal-open');
85+
body.dataset.scrollPosition = String(scrollY);
86+
87+
// Calculate scrollbar width and set custom property
88+
const scrollbarWidth = window.innerWidth - body.clientWidth;
89+
document.documentElement.style.setProperty('--scrollbar-width', `${scrollbarWidth}px`);
90+
} else {
91+
// Restore scroll position
92+
const scrollY = body.dataset.scrollPosition || '0';
93+
body.style.position = '';
94+
body.style.width = '';
95+
body.style.top = '';
96+
body.style.overflow = '';
97+
body.classList.remove('modal-open');
98+
delete body.dataset.scrollPosition;
99+
document.documentElement.style.setProperty('--scrollbar-width', '0px');
100+
window.scrollTo(0, parseInt(scrollY, 10));
101+
}
102+
}
103+
});
104+
105+
// Clean up on unmount
106+
onUnmounted(() => {
107+
if (process.client && document.body.classList.contains('modal-open')) {
108+
// Restore scroll if component unmounts while modal is open
109+
const scrollY = document.body.dataset.scrollPosition || '0';
110+
document.body.style.position = '';
111+
document.body.style.width = '';
112+
document.body.style.top = '';
113+
document.body.style.overflow = '';
114+
document.body.classList.remove('modal-open');
115+
document.documentElement.style.setProperty('--scrollbar-width', '0px');
116+
delete document.body.dataset.scrollPosition;
117+
window.scrollTo(0, parseInt(scrollY, 10));
118+
}
119+
});
120+
</script>
121+
122+
<template>
123+
<div class="background-switcher">
124+
<!-- For desktop: Use CustomDropdown -->
125+
<div v-if="!isMobile" class="desktop-switcher">
126+
<CustomDropdown
127+
v-model="isDropdownOpen"
128+
width="320px"
129+
:max-height="80"
130+
:smart-position="true"
131+
>
132+
<!-- Trigger Button -->
133+
<template #trigger>
134+
<UButton
135+
color="primary"
136+
variant="ghost"
137+
aria-label="Change background"
138+
>
139+
<UIcon name="i-heroicons-photo" class="text-xl text-black dark:text-white" />
140+
</UButton>
141+
</template>
142+
143+
<!-- Dropdown Content -->
144+
<div class="p-2">
145+
<div class="flex items-center justify-between mb-4">
146+
<h3 class="text-base font-medium">
147+
{{ $t('background.title', 'Select Background') }}
148+
</h3>
149+
</div>
150+
151+
<!-- Loading state -->
152+
<div v-if="loading" class="p-4 text-center">
153+
<UIcon name="i-heroicons-arrow-path" class="animate-spin text-xl mb-2" />
154+
<p class="text-sm text-gray-500 dark:text-gray-400">{{ $t('background.loading', 'Loading backgrounds...') }}</p>
155+
</div>
156+
157+
<!-- Background Grid -->
158+
<div v-else class="grid grid-cols-2 gap-2">
159+
<button
160+
v-for="bg in backgrounds"
161+
:key="bg.path"
162+
@click="selectBackground(bg.path)"
163+
class="bg-thumb-btn relative overflow-hidden rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-(--ui-focus-ring)"
164+
:class="{'ring-2 ring-(--ui-focus-ring)': isCurrentBackground(bg.path)}"
165+
>
166+
<!-- Image Thumbnail -->
167+
<div class="aspect-video bg-(--ui-bg-muted) overflow-hidden">
168+
<NuxtImg
169+
:src="bg.path"
170+
width="180"
171+
loading="lazy"
172+
format="webp"
173+
fit="cover"
174+
quality="80"
175+
class="w-full h-full object-cover"
176+
/>
177+
</div>
178+
179+
<!-- Image Name Overlay -->
180+
<div class="absolute bottom-0 left-0 right-0 bg-black/70 py-1 px-2">
181+
<div class="flex items-center justify-between">
182+
<UIcon
183+
v-if="isCurrentBackground(bg.path)"
184+
name="i-heroicons-check"
185+
class="text-(--ui-primary) text-xs"
186+
/>
187+
</div>
188+
</div>
189+
</button>
190+
</div>
191+
</div>
192+
</CustomDropdown>
193+
</div>
194+
195+
<!-- For mobile: Just show the button that opens fullscreen view -->
196+
<div v-else>
197+
<UButton
198+
color="primary"
199+
variant="ghost"
200+
aria-label="Change background"
201+
@click="openFullscreen"
202+
>
203+
<UIcon name="i-heroicons-photo" class="text-xl text-black dark:text-white" />
204+
</UButton>
205+
</div>
206+
207+
<!-- Mobile Fullscreen Modal -->
208+
<MobileFullscreenModal
209+
:open="isFullscreen"
210+
:title="$t('background.title', 'Select Background')"
211+
@close="closeFullscreen"
212+
>
213+
<!-- Loading state -->
214+
<div v-if="loading" class="p-8 text-center">
215+
<UIcon name="i-heroicons-arrow-path" class="animate-spin text-2xl mb-3" />
216+
<p class="text-gray-500 dark:text-gray-400">{{ $t('background.loading', 'Loading backgrounds...') }}</p>
217+
</div>
218+
219+
<!-- Main content slot -->
220+
<div v-else class="grid grid-cols-2 gap-4">
221+
<button
222+
v-for="bg in backgrounds"
223+
:key="bg.path"
224+
@click="selectBackground(bg.path)"
225+
class="bg-thumb-btn relative overflow-hidden rounded-lg focus:outline-none shadow-sm"
226+
:class="{'ring-2 ring-primary-500': isCurrentBackground(bg.path)}"
227+
>
228+
<!-- Image Thumbnail -->
229+
<div class="aspect-video bg-(--ui-bg-muted) overflow-hidden">
230+
<NuxtImg
231+
:src="bg.path"
232+
loading="lazy"
233+
format="webp"
234+
fit="cover"
235+
quality="80"
236+
class="w-full h-full object-cover"
237+
/>
238+
</div>
239+
240+
<!-- Image Name Overlay -->
241+
<div class="absolute bottom-0 left-0 right-0 bg-black/70 py-2 px-3">
242+
<div class="flex items-center justify-between">
243+
<span class="text-sm text-white truncate">{{ bg.name }}</span>
244+
<UIcon
245+
v-if="isCurrentBackground(bg.path)"
246+
name="i-heroicons-check"
247+
class="text-primary-500"
248+
/>
249+
</div>
250+
</div>
251+
</button>
252+
</div>
253+
</MobileFullscreenModal>
254+
</div>
255+
</template>
256+
257+
<style scoped>
258+
.aspect-video {
259+
aspect-ratio: 16/9;
260+
}
261+
262+
.bg-thumb-btn {
263+
transition: all 0.2s ease;
264+
}
265+
266+
.bg-thumb-btn:hover {
267+
transform: scale(1.02);
268+
}
269+
270+
/* Shadow for buttons in grid */
271+
.shadow-sm {
272+
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
273+
}
274+
275+
:root.dark .shadow-sm {
276+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3);
277+
}
278+
</style>

composables/siteBackground.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ export function siteBackground() {
33
const cookie = useCookie('siteBackground');
44

55
// Create a reactive ref to track the current background
6-
const currentBackground = ref<string>(cookie.value || '/backgrounds/images/bg2.png');
6+
// Make it globally accessible with useState instead of a local ref
7+
const currentBackground = useState<string>('currentBackground', () =>
8+
cookie.value || '/backgrounds/bg2.png'
9+
);
710

811
// Watch for changes to the ref and update the cookie
912
watch(currentBackground, (newValue) => {
1013
cookie.value = newValue;
1114
});
1215

1316
const getSiteBackground = () => {
14-
return currentBackground.value;
17+
return currentBackground;
1518
}
1619

1720
const setSiteBackground = (path: string) => {
@@ -30,6 +33,8 @@ export function siteBackground() {
3033
return {
3134
getOptimizedImageUrl,
3235
getSiteBackground,
33-
setSiteBackground
36+
setSiteBackground,
37+
// Also expose the ref directly for more direct access
38+
currentBackground
3439
}
3540
}

layouts/default.vue

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
import BackgroundViewer from '~/components/Global/BackgroundViewer.vue';
33
const { getOptimizedImageUrl, getSiteBackground } = siteBackground();
44
5+
// Get the reactive ref directly from getSiteBackground()
6+
const backgroundRef = getSiteBackground();
7+
8+
// Create a computed that reacts to changes in the ref
59
const backgroundUrl = computed(() => {
6-
return getOptimizedImageUrl(getSiteBackground());
10+
return getOptimizedImageUrl(backgroundRef.value);
711
});
812
913
// Preload the optimized image
@@ -18,7 +22,8 @@ useHead({
1822
<div :style="{
1923
backgroundImage: `url(${backgroundUrl})`,
2024
backgroundSize: 'cover',
21-
backgroundPosition: 'center'
25+
backgroundPosition: 'center',
26+
backgroundRepeat: 'no-repeat'
2227
}">
2328
<UContainer id="content" class="content mx-auto">
2429
<div id="inner-content" class="inner-content">
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

public/backgrounds/bg8.jpg

762 KB
Loading

public/backgrounds/videos/bg8.mp4

-11 MB
Binary file not shown.
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { defineEventHandler } from 'h3';
4+
5+
/**
6+
* API endpoint to get all available background images
7+
*
8+
* @returns Array of background image objects
9+
*/
10+
export default defineEventHandler(async () => {
11+
// Path to the public backgrounds directory
12+
const publicDir = path.resolve(process.cwd() + '/public/backgrounds');
13+
14+
// Read the directory
15+
const files = await fs.promises.readdir(publicDir);
16+
17+
// Filter for image files and format the response
18+
const backgrounds = files
19+
.filter(file => /\.(jpg|jpeg|png|webp)$/i.test(file))
20+
.map(file => ({
21+
path: `/backgrounds/${file}`
22+
}));
23+
24+
return backgrounds;
25+
});

0 commit comments

Comments
 (0)