Skip to content

Commit 1a20017

Browse files
authored
Create GalleryScreen.kt
1 parent a0240dc commit 1a20017

1 file changed

Lines changed: 288 additions & 0 deletions

File tree

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
package com.hackeros.app.ui.screens
2+
3+
import android.content.ContentValues
4+
import android.content.Context
5+
import android.os.Build
6+
import android.os.Environment
7+
import android.provider.MediaStore
8+
import android.widget.Toast
9+
import androidx.compose.foundation.background
10+
import androidx.compose.foundation.border
11+
import androidx.compose.foundation.clickable
12+
import androidx.compose.foundation.layout.*
13+
import androidx.compose.foundation.lazy.grid.GridCells
14+
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
15+
import androidx.compose.foundation.lazy.grid.items
16+
import androidx.compose.foundation.shape.CircleShape
17+
import androidx.compose.foundation.shape.RoundedCornerShape
18+
import androidx.compose.material.icons.Icons
19+
import androidx.compose.material.icons.filled.Close
20+
import androidx.compose.material.icons.filled.Download
21+
import androidx.compose.material.icons.filled.PhotoCamera
22+
import androidx.compose.material.icons.filled.WifiOff
23+
import androidx.compose.material3.*
24+
import androidx.compose.runtime.*
25+
import androidx.compose.ui.Alignment
26+
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.draw.clip
28+
import androidx.compose.ui.graphics.Color
29+
import androidx.compose.ui.layout.ContentScale
30+
import androidx.compose.ui.platform.LocalContext
31+
import androidx.compose.ui.text.font.FontFamily
32+
import androidx.compose.ui.text.font.FontWeight
33+
import androidx.compose.ui.unit.dp
34+
import androidx.compose.ui.unit.sp
35+
import androidx.compose.ui.window.Dialog
36+
import androidx.compose.ui.window.DialogProperties
37+
import coil.compose.AsyncImage
38+
import com.hackeros.app.data.model.GalleryImage
39+
import com.hackeros.app.ui.theme.LocalAppTheme
40+
import com.hackeros.app.ui.theme.backgroundColor
41+
import com.hackeros.app.ui.theme.mutedColor
42+
import com.hackeros.app.ui.theme.primaryColor
43+
import com.hackeros.app.utils.Translations
44+
import kotlinx.coroutines.Dispatchers
45+
import kotlinx.coroutines.launch
46+
import kotlinx.coroutines.withContext
47+
import java.io.InputStream
48+
import java.net.URL
49+
50+
@Composable
51+
fun GalleryScreen(
52+
images: List<GalleryImage>,
53+
loading: Boolean,
54+
error: Boolean,
55+
translations: Translations,
56+
onRetry: () -> Unit
57+
) {
58+
val theme = LocalAppTheme.current
59+
val t = translations
60+
val context = LocalContext.current
61+
val scope = rememberCoroutineScope()
62+
var selectedImage by remember { mutableStateOf<GalleryImage?>(null) }
63+
var downloading by remember { mutableStateOf(false) }
64+
65+
Column(modifier = Modifier.fillMaxSize()) {
66+
Column(modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 20.dp)) {
67+
Text(
68+
text = t.header_gallery,
69+
fontFamily = FontFamily.Monospace,
70+
fontWeight = FontWeight.Bold,
71+
fontSize = 28.sp,
72+
color = Color.White
73+
)
74+
Text(
75+
text = t.sub_gallery,
76+
fontSize = 13.sp,
77+
color = theme.mutedColor(),
78+
modifier = Modifier.padding(top = 2.dp)
79+
)
80+
}
81+
82+
when {
83+
loading -> Box(
84+
modifier = Modifier.fillMaxSize(),
85+
contentAlignment = Alignment.Center
86+
) {
87+
Column(horizontalAlignment = Alignment.CenterHorizontally) {
88+
CircularProgressIndicator(color = theme.primaryColor(), modifier = Modifier.size(48.dp))
89+
Spacer(Modifier.height(16.dp))
90+
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
91+
val alpha by infiniteTransition.animateFloat(
92+
initialValue = 0.3f, targetValue = 1f,
93+
animationSpec = infiniteRepeatable(
94+
androidx.compose.animation.core.tween(800),
95+
RepeatMode.Reverse
96+
),
97+
label = "alpha"
98+
)
99+
Text(
100+
text = t.gallery_loading,
101+
fontSize = 11.sp,
102+
fontFamily = FontFamily.Monospace,
103+
color = theme.primaryColor().copy(alpha = alpha)
104+
)
105+
}
106+
}
107+
error -> Box(
108+
modifier = Modifier.fillMaxSize().padding(32.dp),
109+
contentAlignment = Alignment.Center
110+
) {
111+
Column(horizontalAlignment = Alignment.CenterHorizontally) {
112+
Surface(
113+
shape = CircleShape,
114+
color = Color(0xFFEF4444).copy(alpha = 0.1f)
115+
) {
116+
Icon(Icons.Default.WifiOff, null,
117+
tint = Color(0xFFEF4444),
118+
modifier = Modifier.size(40.dp).padding(8.dp))
119+
}
120+
Spacer(Modifier.height(12.dp))
121+
Text(t.error_signal, color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
122+
Spacer(Modifier.height(16.dp))
123+
Button(onClick = onRetry,
124+
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEF4444))) {
125+
Text(t.retry, fontWeight = FontWeight.Bold)
126+
}
127+
}
128+
}
129+
images.isEmpty() -> Box(
130+
modifier = Modifier.fillMaxSize(),
131+
contentAlignment = Alignment.Center
132+
) {
133+
Column(horizontalAlignment = Alignment.CenterHorizontally) {
134+
Icon(Icons.Default.PhotoCamera, null,
135+
tint = theme.mutedColor().copy(alpha = 0.2f),
136+
modifier = Modifier.size(56.dp))
137+
Spacer(Modifier.height(12.dp))
138+
Text(t.gallery_empty, color = theme.mutedColor(), fontSize = 13.sp)
139+
}
140+
}
141+
else -> LazyVerticalGrid(
142+
columns = GridCells.Fixed(2),
143+
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 100.dp),
144+
horizontalArrangement = Arrangement.spacedBy(10.dp),
145+
verticalArrangement = Arrangement.spacedBy(10.dp)
146+
) {
147+
items(images, key = { it.sha }) { img ->
148+
Box(
149+
modifier = Modifier
150+
.fillMaxWidth()
151+
.aspectRatio(1f)
152+
.clip(RoundedCornerShape(14.dp))
153+
.border(1.dp, Color.White.copy(alpha = 0.05f), RoundedCornerShape(14.dp))
154+
.clickable { selectedImage = img }
155+
) {
156+
AsyncImage(
157+
model = img.download_url,
158+
contentDescription = img.name,
159+
contentScale = ContentScale.Crop,
160+
modifier = Modifier.fillMaxSize(),
161+
alpha = 0.85f
162+
)
163+
// Hover name overlay at bottom
164+
Box(
165+
modifier = Modifier
166+
.fillMaxWidth()
167+
.align(Alignment.BottomCenter)
168+
.background(
169+
androidx.compose.ui.graphics.Brush.verticalGradient(
170+
colors = listOf(Color.Transparent, Color.Black.copy(0.65f))
171+
)
172+
)
173+
.padding(8.dp)
174+
) {
175+
Text(
176+
text = img.name,
177+
fontSize = 9.sp,
178+
fontFamily = FontFamily.Monospace,
179+
color = Color.White,
180+
maxLines = 1
181+
)
182+
}
183+
}
184+
}
185+
}
186+
}
187+
}
188+
189+
// Fullscreen modal
190+
selectedImage?.let { img ->
191+
Dialog(
192+
onDismissRequest = { selectedImage = null },
193+
properties = DialogProperties(usePlatformDefaultWidth = false)
194+
) {
195+
Box(
196+
modifier = Modifier
197+
.fillMaxSize()
198+
.background(Color.Black.copy(alpha = 0.97f)),
199+
contentAlignment = Alignment.Center
200+
) {
201+
IconButton(
202+
onClick = { selectedImage = null },
203+
modifier = Modifier
204+
.align(Alignment.TopEnd)
205+
.padding(16.dp)
206+
.clip(CircleShape)
207+
.background(Color.White.copy(alpha = 0.1f))
208+
) {
209+
Icon(Icons.Default.Close, null, tint = Color.White.copy(alpha = 0.8f))
210+
}
211+
212+
Box(
213+
modifier = Modifier
214+
.fillMaxWidth(0.92f)
215+
.fillMaxHeight(0.72f)
216+
.clip(RoundedCornerShape(16.dp))
217+
.border(1.dp, Color.White.copy(alpha = 0.1f), RoundedCornerShape(16.dp))
218+
) {
219+
AsyncImage(
220+
model = img.download_url,
221+
contentDescription = img.name,
222+
contentScale = ContentScale.Fit,
223+
modifier = Modifier.fillMaxSize()
224+
)
225+
}
226+
227+
Button(
228+
onClick = {
229+
scope.launch {
230+
downloading = true
231+
withContext(Dispatchers.IO) {
232+
try {
233+
val stream: InputStream = URL(img.download_url).openStream()
234+
val bytes = stream.readBytes()
235+
stream.close()
236+
val fname = "HackerOS_Gallery_${img.name}"
237+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
238+
val values = ContentValues().apply {
239+
put(MediaStore.Images.Media.DISPLAY_NAME, fname)
240+
put(MediaStore.Images.Media.MIME_TYPE, "image/png")
241+
put(MediaStore.Images.Media.RELATIVE_PATH,
242+
Environment.DIRECTORY_DOWNLOADS)
243+
}
244+
val uri = context.contentResolver.insert(
245+
MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
246+
uri?.let {
247+
context.contentResolver.openOutputStream(it)
248+
?.use { os -> os.write(bytes) }
249+
}
250+
} else {
251+
val dir = Environment.getExternalStoragePublicDirectory(
252+
Environment.DIRECTORY_DOWNLOADS)
253+
dir.mkdirs()
254+
java.io.File(dir, fname).writeBytes(bytes)
255+
}
256+
} catch (_: Exception) {}
257+
}
258+
downloading = false
259+
Toast.makeText(context, "Saved!", Toast.LENGTH_SHORT).show()
260+
}
261+
},
262+
enabled = !downloading,
263+
colors = ButtonDefaults.buttonColors(containerColor = theme.primaryColor()),
264+
shape = RoundedCornerShape(14.dp),
265+
modifier = Modifier
266+
.align(Alignment.BottomCenter)
267+
.fillMaxWidth(0.85f)
268+
.padding(bottom = 40.dp)
269+
.height(56.dp)
270+
) {
271+
if (downloading) {
272+
CircularProgressIndicator(color = theme.backgroundColor(),
273+
modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
274+
Spacer(Modifier.width(12.dp))
275+
Text(t.downloading, fontWeight = FontWeight.Bold, fontSize = 15.sp,
276+
color = theme.backgroundColor())
277+
} else {
278+
Icon(Icons.Default.Download, null, tint = theme.backgroundColor(),
279+
modifier = Modifier.size(22.dp))
280+
Spacer(Modifier.width(12.dp))
281+
Text(t.download_save, fontWeight = FontWeight.Bold, fontSize = 15.sp,
282+
color = theme.backgroundColor())
283+
}
284+
}
285+
}
286+
}
287+
}
288+
}

0 commit comments

Comments
 (0)