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' ;
33import { Language } from '../types' ;
44import { 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
69interface 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+
1025export 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+ / \. ( j p g | j p e g | p n g | g i f | w e b p ) $ / 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