1+ package com.example.hermesplay
2+
3+ import androidx.compose.foundation.background
4+ import androidx.compose.foundation.clickable
5+ import androidx.compose.foundation.layout.*
6+ import androidx.compose.foundation.lazy.grid.GridCells
7+ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
8+ import androidx.compose.foundation.lazy.grid.items
9+ import androidx.compose.foundation.shape.RoundedCornerShape
10+ import androidx.compose.material.icons.Icons
11+ import androidx.compose.material.icons.filled.ArrowBack
12+ import androidx.compose.material.icons.filled.Folder
13+ import androidx.compose.material.icons.filled.Home
14+ import androidx.compose.material.icons.filled.PlayArrow
15+ import androidx.compose.material.icons.filled.Refresh
16+ import androidx.compose.material.icons.filled.Sync
17+ import androidx.compose.material3.*
18+ import androidx.compose.runtime.*
19+ import androidx.compose.ui.Alignment
20+ import androidx.compose.ui.Modifier
21+ import androidx.compose.ui.draw.clip
22+ import androidx.compose.ui.graphics.Color
23+ import androidx.compose.ui.layout.ContentScale
24+ import androidx.compose.ui.platform.LocalContext
25+ import androidx.compose.ui.text.style.TextAlign
26+ import androidx.compose.ui.text.style.TextOverflow
27+ import androidx.compose.ui.unit.dp
28+ import coil.compose.AsyncImage
29+ import coil.request.ImageRequest
30+ import coil.request.videoFrameMillis
31+ import com.example.hermesplay.models.MediaItem
32+ import com.example.hermesplay.viewmodels.UiState
33+
34+ @OptIn(ExperimentalMaterial3Api ::class )
35+ @Composable
36+ fun DirectoryScreen (
37+ uiState : UiState ,
38+ onFolderClick : (MediaItem .Folder ) -> Unit ,
39+ onVideoClick : (MediaItem .Video ) -> Unit ,
40+ onNavigateUp : () -> Unit ,
41+ onJumpToRoot : () -> Unit ,
42+ onRefresh : () -> Unit ,
43+ onRescanAll : () -> Unit // NEW Callback
44+ ) {
45+ var gridSize by remember { mutableFloatStateOf(180f ) }
46+
47+ Scaffold (
48+ containerColor = Color .Black ,
49+ topBar = {
50+ TopAppBar (
51+ title = { Text (" HermesPlay" ) },
52+ colors = TopAppBarDefaults .topAppBarColors(
53+ containerColor = Color .Black ,
54+ titleContentColor = Color .White ,
55+ actionIconContentColor = Color .White
56+ ),
57+ actions = {
58+ Row (
59+ verticalAlignment = Alignment .CenterVertically ,
60+ modifier = Modifier .padding(end = 16 .dp).width(180 .dp)
61+ ) {
62+ Text (" Size" , color = Color .Gray , style = MaterialTheme .typography.labelMedium)
63+ Spacer (modifier = Modifier .width(8 .dp))
64+ Slider (
65+ value = gridSize,
66+ onValueChange = { gridSize = it },
67+ valueRange = 120f .. 400f ,
68+ colors = SliderDefaults .colors(thumbColor = Color .White , activeTrackColor = Color .DarkGray )
69+ )
70+ }
71+
72+ if (uiState is UiState .Success ) {
73+ if (uiState.isRoot) {
74+ // Show Global Rescan if at the root
75+ IconButton (onClick = onRescanAll) {
76+ Icon (Icons .Default .Sync , contentDescription = " Rescan All Folders" )
77+ }
78+ } else {
79+ // Show Folder Refresh and Home if in a sub-folder
80+ IconButton (onClick = onRefresh) {
81+ Icon (Icons .Default .Refresh , contentDescription = " Refresh Folder" )
82+ }
83+ IconButton (onClick = onJumpToRoot) {
84+ Icon (Icons .Default .Home , contentDescription = " Go to Root" )
85+ }
86+ }
87+ }
88+ }
89+ )
90+ }
91+ ) { paddingValues ->
92+ Box (modifier = Modifier .fillMaxSize().padding(paddingValues)) {
93+ when (uiState) {
94+ is UiState .Loading -> CircularProgressIndicator (modifier = Modifier .align(Alignment .Center ), color = Color .White )
95+ is UiState .Error -> Text (text = uiState.message, color = MaterialTheme .colorScheme.error, modifier = Modifier .align(Alignment .Center ))
96+ is UiState .Success -> {
97+ if (uiState.items.isEmpty() && uiState.isRoot) {
98+ Text (" This folder is empty!" , color = Color .White , modifier = Modifier .align(Alignment .Center ))
99+ } else {
100+ LazyVerticalGrid (
101+ columns = GridCells .Adaptive (minSize = gridSize.dp),
102+ contentPadding = PaddingValues (16 .dp),
103+ horizontalArrangement = Arrangement .spacedBy(16 .dp),
104+ verticalArrangement = Arrangement .spacedBy(16 .dp)
105+ ) {
106+ if (! uiState.isRoot) { item { BackNavigationCard (onClick = onNavigateUp) } }
107+
108+ items(uiState.items, key = { it.uri.toString() }) { item ->
109+ MediaItemCard (
110+ item = item,
111+ onClick = {
112+ when (item) {
113+ is MediaItem .Folder -> onFolderClick(item)
114+ is MediaItem .Video -> onVideoClick(item)
115+ }
116+ }
117+ )
118+ }
119+ }
120+ }
121+ }
122+ }
123+ }
124+ }
125+ }
126+
127+ @Composable
128+ fun BackNavigationCard (onClick : () -> Unit ) {
129+ Card (
130+ modifier = Modifier .fillMaxWidth().aspectRatio(1f ).clickable(onClick = onClick).clip(RoundedCornerShape (12 .dp)),
131+ elevation = CardDefaults .cardElevation(defaultElevation = 4 .dp),
132+ colors = CardDefaults .cardColors(containerColor = MaterialTheme .colorScheme.surfaceVariant)
133+ ) {
134+ Column (
135+ modifier = Modifier .fillMaxSize(),
136+ verticalArrangement = Arrangement .Center ,
137+ horizontalAlignment = Alignment .CenterHorizontally
138+ ) {
139+ Icon (Icons .Default .ArrowBack , contentDescription = " Go Back" , modifier = Modifier .size(64 .dp), tint = Color .White )
140+ Spacer (modifier = Modifier .height(8 .dp))
141+ Text (" Back" , style = MaterialTheme .typography.titleLarge, color = Color .White )
142+ }
143+ }
144+ }
145+
146+ @Composable
147+ fun MediaItemCard (item : MediaItem , onClick : () -> Unit ) {
148+ Card (
149+ modifier = Modifier .fillMaxWidth().aspectRatio(1f ).clickable(onClick = onClick).clip(RoundedCornerShape (12 .dp)),
150+ elevation = CardDefaults .cardElevation(defaultElevation = 4 .dp),
151+ colors = CardDefaults .cardColors(containerColor = MaterialTheme .colorScheme.surfaceVariant)
152+ ) {
153+ Box (modifier = Modifier .fillMaxSize()) {
154+ val context = LocalContext .current
155+
156+ // SMART LOADING: Only time-shift if we are forced to extract a video frame
157+ val imageRequest = remember(item.uri, item.thumbnailUri) {
158+ val isExtractingFromVideo = item.thumbnailUri == null || item.thumbnailUri == item.uri
159+
160+ ImageRequest .Builder (context)
161+ .data(item.thumbnailUri ? : item.uri)
162+ .apply {
163+ if (isExtractingFromVideo) {
164+ videoFrameMillis(60_000 )
165+ }
166+ }
167+ .crossfade(true )
168+ .build()
169+ }
170+
171+ AsyncImage (
172+ model = imageRequest,
173+ contentDescription = item.name,
174+ contentScale = ContentScale .Crop ,
175+ modifier = Modifier .fillMaxSize()
176+ )
177+
178+ Box (modifier = Modifier .align(Alignment .BottomCenter ).fillMaxWidth().background(Color .Black .copy(alpha = 0.7f )).padding(8 .dp)) {
179+ Text (text = item.name.substringBeforeLast(" ." ), color = Color .White , style = MaterialTheme .typography.labelLarge, maxLines = 2 , overflow = TextOverflow .Ellipsis , textAlign = TextAlign .Center , modifier = Modifier .fillMaxWidth())
180+ }
181+
182+ Box (modifier = Modifier .align(Alignment .TopEnd ).padding(8 .dp).background(Color .Black .copy(alpha = 0.6f ), RoundedCornerShape (8 .dp)).padding(4 .dp)) {
183+ when (item) {
184+ is MediaItem .Folder -> Icon (Icons .Default .Folder , contentDescription = " Folder" , tint = Color .Yellow )
185+ is MediaItem .Video -> Icon (Icons .Default .PlayArrow , contentDescription = " Play" , tint = Color .White )
186+ }
187+ }
188+ }
189+ }
190+ }
0 commit comments