Skip to content

Commit c9fe73d

Browse files
authored
Update Gallery.tsx
1 parent 6dc0395 commit c9fe73d

1 file changed

Lines changed: 183 additions & 22 deletions

File tree

source-code/components/Gallery.tsx

Lines changed: 183 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,101 @@
1-
import React from 'react';
2-
import { Aperture, Construction } from 'lucide-react';
1+
import React, { useState, useEffect } from 'react';
2+
import { Aperture, Loader2, WifiOff, X, Maximize2, Download } from 'lucide-react';
33
import { Language } from '../types';
44
import { TRANSLATIONS } from '../utils/translations';
5+
import { GALLERY_API_URL } from '../constants';
6+
import { Filesystem, Directory } from '@capacitor/filesystem';
7+
import { Toast } from '@capacitor/toast';
58

69
interface GalleryProps {
710
language: Language;
811
}
912

13+
interface GalleryImage {
14+
name: string;
15+
path: string;
16+
sha: string;
17+
size: number;
18+
url: string;
19+
html_url: string;
20+
git_url: string;
21+
download_url: string;
22+
type: string;
23+
}
24+
1025
export const Gallery: React.FC<GalleryProps> = ({ language }) => {
1126
const t = TRANSLATIONS[language];
27+
const [images, setImages] = useState<GalleryImage[]>([]);
28+
const [loading, setLoading] = useState(true);
29+
const [error, setError] = useState(false);
30+
const [selectedImage, setSelectedImage] = useState<GalleryImage | null>(null);
31+
const [downloading, setDownloading] = useState(false);
32+
33+
useEffect(() => {
34+
const fetchImages = async () => {
35+
try {
36+
setLoading(true);
37+
// This hits https://api.github.com/repos/HackerOS-Linux-System/HackerOS-App/contents/gallery
38+
// which corresponds to /tree/main/gallery in the UI
39+
const response = await fetch(GALLERY_API_URL);
40+
41+
if (!response.ok) throw new Error("Failed to fetch gallery");
42+
43+
const data = await response.json();
44+
45+
// Filter only images
46+
if (Array.isArray(data)) {
47+
const imageFiles = data.filter((item: any) =>
48+
item.type === 'file' &&
49+
/\.(jpg|jpeg|png|gif|webp)$/i.test(item.name)
50+
);
51+
setImages(imageFiles);
52+
} else {
53+
setImages([]);
54+
}
55+
} catch (err) {
56+
console.error(err);
57+
setError(true);
58+
} finally {
59+
setLoading(false);
60+
}
61+
};
62+
63+
fetchImages();
64+
}, []);
65+
66+
const handleDownload = async (url: string, name: string) => {
67+
setDownloading(true);
68+
try {
69+
const response = await fetch(url);
70+
const blob = await response.blob();
71+
const reader = new FileReader();
72+
73+
reader.readAsDataURL(blob);
74+
reader.onloadend = async () => {
75+
const base64data = reader.result as string;
76+
try {
77+
await Filesystem.writeFile({
78+
path: `HackerOS_Gallery_${name}`,
79+
data: base64data,
80+
directory: Directory.Documents
81+
});
82+
await Toast.show({ text: 'Saved to Documents', duration: 'short' });
83+
} catch (e) {
84+
// Fallback for browser
85+
const link = document.createElement('a');
86+
link.href = base64data;
87+
link.download = name;
88+
document.body.appendChild(link);
89+
link.click();
90+
document.body.removeChild(link);
91+
}
92+
setDownloading(false);
93+
};
94+
} catch (e) {
95+
setDownloading(false);
96+
await Toast.show({ text: 'Download failed', duration: 'short' });
97+
}
98+
};
1299

13100
return (
14101
<div className="pb-24 pt-2 h-screen flex flex-col">
@@ -17,30 +104,104 @@ export const Gallery: React.FC<GalleryProps> = ({ language }) => {
17104
<p className="text-muted text-sm">{t.sub_gallery}</p>
18105
</div>
19106

20-
<div className="flex-1 flex flex-col items-center justify-center px-6 text-center space-y-6">
21-
<div className="relative">
22-
<div className="absolute inset-0 bg-primary/20 blur-2xl rounded-full" />
23-
<div className="relative bg-card/50 border border-white/5 p-8 rounded-full backdrop-blur-md">
24-
<Aperture size={48} className="text-primary animate-spin-slow" style={{ animationDuration: '10s' }} />
107+
<div className="flex-1 px-4 overflow-y-auto">
108+
{loading ? (
109+
<div className="flex flex-col items-center justify-center h-64 space-y-4">
110+
<div className="relative">
111+
<div className="absolute inset-0 bg-primary/20 blur-xl rounded-full"></div>
112+
<Loader2 className="animate-spin relative z-10 text-primary" size={48} />
113+
</div>
114+
<p className="font-mono text-xs animate-pulse text-muted">{t.gallery_loading}</p>
115+
</div>
116+
) : error ? (
117+
<div className="flex flex-col items-center justify-center h-64 text-red-400 text-center px-6">
118+
<div className="bg-red-500/10 p-4 rounded-full mb-4 ring-1 ring-red-500/20">
119+
<WifiOff size={32} />
120+
</div>
121+
<p className="font-bold mb-2">{t.error_signal}</p>
122+
<button
123+
onClick={() => window.location.reload()}
124+
className="mt-4 px-6 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg text-sm font-bold"
125+
>
126+
{t.retry}
127+
</button>
128+
</div>
129+
) : images.length === 0 ? (
130+
<div className="flex flex-col items-center justify-center h-64 text-muted">
131+
<Aperture size={48} className="opacity-20 mb-4" />
132+
<p>{t.gallery_empty}</p>
133+
</div>
134+
) : (
135+
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 pb-20">
136+
{images.map((img) => (
137+
<button
138+
key={img.sha}
139+
onClick={() => setSelectedImage(img)}
140+
className="group relative aspect-square rounded-xl overflow-hidden bg-card/40 border border-white/5 hover:border-primary/50 transition-all duration-300"
141+
>
142+
<img
143+
src={img.download_url}
144+
alt={img.name}
145+
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110 opacity-80 group-hover:opacity-100"
146+
loading="lazy"
147+
/>
148+
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex items-end p-3">
149+
<span className="text-[10px] font-mono text-white truncate w-full text-left">
150+
{img.name}
151+
</span>
152+
</div>
153+
</button>
154+
))}
25155
</div>
26-
</div>
156+
)}
157+
</div>
27158

28-
<div className="space-y-2">
29-
<h3 className="text-xl font-bold text-white flex items-center justify-center gap-2">
30-
<Construction size={20} className="text-yellow-500" />
31-
{t.construction_title}
32-
</h3>
33-
<p className="text-muted text-sm max-w-xs mx-auto leading-relaxed">
34-
{t.construction_desc}
35-
</p>
36-
</div>
159+
{/* Fullscreen Modal */}
160+
{selectedImage && (
161+
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/95 backdrop-blur-xl p-0 animate-in fade-in duration-200">
162+
<button
163+
onClick={() => setSelectedImage(null)}
164+
className="absolute top-8 right-6 p-3 text-white/70 hover:text-white rounded-full bg-white/10 z-10 backdrop-blur-md"
165+
>
166+
<X size={24} />
167+
</button>
37168

38-
<div className="pt-8">
39-
<span className="px-3 py-1 rounded-full bg-white/5 border border-white/10 text-[10px] font-mono text-muted uppercase tracking-widest">
40-
Module: 0x4_GALLERY
41-
</span>
169+
<div className="relative w-full h-full flex flex-col items-center justify-center">
170+
<div className="relative w-full max-w-[90vw] max-h-[70vh] rounded-lg overflow-hidden shadow-[0_0_50px_-12px_rgba(var(--color-primary),0.3)] border border-white/10">
171+
<img
172+
src={selectedImage.download_url}
173+
alt={selectedImage.name}
174+
className="w-full h-full object-contain bg-black"
175+
/>
176+
</div>
177+
178+
<div className="absolute bottom-12 w-full px-8">
179+
<button
180+
disabled={downloading}
181+
onClick={(e) => {
182+
e.stopPropagation();
183+
handleDownload(selectedImage.download_url, selectedImage.name);
184+
}}
185+
className={`w-full flex items-center justify-center gap-3 px-6 py-4 rounded-xl font-bold text-lg shadow-[0_0_20px_-5px_rgb(var(--color-primary))] transition-all active:scale-95
186+
${downloading ? 'bg-primary/50 cursor-wait' : 'bg-primary hover:bg-primary/90 text-background'}
187+
`}
188+
>
189+
{downloading ? (
190+
<>
191+
<Loader2 size={24} className="animate-spin" />
192+
<span>{t.downloading}</span>
193+
</>
194+
) : (
195+
<>
196+
<Download size={24} />
197+
<span>{t.download_save}</span>
198+
</>
199+
)}
200+
</button>
201+
</div>
202+
</div>
42203
</div>
43-
</div>
204+
)}
44205
</div>
45206
);
46207
};

0 commit comments

Comments
 (0)