11package com.hackeros.app.ui.screens
22
3+ import android.content.ClipData
4+ import android.content.ClipboardManager
5+ import android.content.Context
6+ import android.widget.Toast
37import androidx.compose.foundation.background
48import androidx.compose.foundation.border
59import androidx.compose.foundation.clickable
@@ -164,6 +168,7 @@ fun GamesStoreScreen(
164168 }
165169
166170 selectedGame?.let { game ->
171+ val uriHandler = androidx.compose.ui.platform.LocalUriHandler .current
167172 GameDetailDialog (
168173 game = game,
169174 installState = installStates[game.id],
@@ -172,7 +177,12 @@ fun GamesStoreScreen(
172177 onDismiss = { selectedGame = null },
173178 onDownload = { onDownload(game) },
174179 onInstall = { onInstall(game.id) },
175- onOpen = { onOpen(game.packageName) }
180+ onOpen = { onOpen(game.packageName) },
181+ onOpenRepo = {
182+ val url = game.sourceUrl
183+ ? : game.repoOwner?.let { owner -> game.repoName?.let { repo -> " https://github.com/$owner /$repo " } }
184+ if (url != null ) try { uriHandler.openUri(url) } catch (_: Exception ) {}
185+ }
176186 )
177187 }
178188}
@@ -240,12 +250,14 @@ private fun GameRow(
240250
241251 GameActionButton (
242252 small = true ,
253+ game = game,
243254 installState = installState,
244255 installed = installed,
245256 translations = t,
246257 onDownload = { },
247258 onInstall = { },
248259 onOpen = { },
260+ onOpenRepo = { },
249261 disabled = true
250262 )
251263 }
@@ -254,19 +266,41 @@ private fun GameRow(
254266@Composable
255267private fun GameActionButton (
256268 small : Boolean ,
269+ game : CommunityGame ,
257270 installState : MainViewModel .ApkUpdateState ? ,
258271 installed : Boolean ,
259272 translations : Translations ,
260273 onDownload : () -> Unit ,
261274 onInstall : () -> Unit ,
262275 onOpen : () -> Unit ,
276+ onOpenRepo : () -> Unit ,
263277 disabled : Boolean = false
264278) {
265279 val theme = LocalAppTheme .current
266280 val t = translations
267281 val height = if (small) 32 .dp else 46 .dp
268282 val fontSize = if (small) 10 .sp else 12 .sp
269283
284+ if (game.isSourceBuild) {
285+ // Source-only entries never claim to silently "compile" arbitrary code on the phone -
286+ // there's no Android toolchain for that on-device. Instead this opens the repository,
287+ // and the full build/clone commands are shown (copyable) in the detail dialog below.
288+ Button (
289+ onClick = onOpenRepo, enabled = ! disabled,
290+ colors = ButtonDefaults .buttonColors(containerColor = Color .White .copy(alpha = 0.08f )),
291+ shape = RoundedCornerShape (10 .dp),
292+ contentPadding = PaddingValues (horizontal = 12 .dp),
293+ modifier = Modifier .height(height)
294+ ) {
295+ Icon (Icons .Default .Code , null , tint = theme.textColor(), modifier = Modifier .size(if (small) 12 .dp else 14 .dp))
296+ if (! small) {
297+ Spacer (Modifier .width(6 .dp))
298+ Text (t.games_source_build_title, fontWeight = FontWeight .Bold , fontSize = fontSize, color = theme.textColor())
299+ }
300+ }
301+ return
302+ }
303+
270304 when {
271305 installState is MainViewModel .ApkUpdateState .Downloading -> {
272306 Box (
@@ -330,7 +364,8 @@ private fun GameDetailDialog(
330364 onDismiss : () -> Unit ,
331365 onDownload : () -> Unit ,
332366 onInstall : () -> Unit ,
333- onOpen : () -> Unit
367+ onOpen : () -> Unit ,
368+ onOpenRepo : () -> Unit
334369) {
335370 val theme = LocalAppTheme .current
336371 val t = translations
@@ -405,14 +440,20 @@ private fun GameDetailDialog(
405440 Box (modifier = Modifier .fillMaxWidth()) {
406441 GameActionButton (
407442 small = false ,
443+ game = game,
408444 installState = installState,
409445 installed = installed,
410446 translations = t,
411447 onDownload = onDownload,
412448 onInstall = onInstall,
413- onOpen = onOpen
449+ onOpen = onOpen,
450+ onOpenRepo = onOpenRepo
414451 )
415452 }
453+ if (game.isSourceBuild) {
454+ Spacer (Modifier .height(14 .dp))
455+ SourceBuildCard (game = game, translations = t)
456+ }
416457 if (installState is MainViewModel .ApkUpdateState .ReadyToInstall && ! installState.verified) {
417458 Spacer (Modifier .height(8 .dp))
418459 Text (" ⚠ " + t.update_unverified, fontSize = 9 .sp, color = Color (0xFFF59E0B ))
@@ -426,6 +467,77 @@ private fun GameDetailDialog(
426467 }
427468}
428469
470+ @Composable
471+ private fun SourceBuildCard (game : CommunityGame , translations : Translations ) {
472+ val theme = LocalAppTheme .current
473+ val t = translations
474+ val context = androidx.compose.ui.platform.LocalContext .current
475+
476+ Column (
477+ modifier = Modifier
478+ .fillMaxWidth()
479+ .clip(RoundedCornerShape (12 .dp))
480+ .background(theme.backgroundColor())
481+ .border(1 .dp, Color .White .copy(alpha = 0.06f ), RoundedCornerShape (12 .dp))
482+ .padding(14 .dp)
483+ ) {
484+ Row (verticalAlignment = Alignment .CenterVertically ) {
485+ Icon (Icons .Default .Code , null , tint = theme.primaryColor(), modifier = Modifier .size(14 .dp))
486+ Spacer (Modifier .width(6 .dp))
487+ Text (t.games_source_build_title, fontWeight = FontWeight .Bold , fontSize = 12 .sp, color = theme.textColor())
488+ }
489+ Spacer (Modifier .height(6 .dp))
490+ Text (t.games_source_build_desc, fontSize = 11 .sp, color = theme.mutedColor(), lineHeight = 15 .sp)
491+
492+ if (game.repoOwner != null && game.repoName != null ) {
493+ Spacer (Modifier .height(8 .dp))
494+ Text (
495+ " ${game.repoOwner} /${game.repoName} (${game.repoBranch} )" ,
496+ fontFamily = FontFamily .Monospace , fontSize = 11 .sp, color = theme.primaryColor()
497+ )
498+ }
499+
500+ val commands = game.buildCommands.ifEmpty {
501+ if (game.repoOwner != null && game.repoName != null ) listOf (
502+ " git clone --branch ${game.repoBranch} https://github.com/${game.repoOwner} /${game.repoName} .git" ,
503+ " cd ${game.repoName} "
504+ ) else emptyList()
505+ }
506+
507+ if (commands.isNotEmpty()) {
508+ Spacer (Modifier .height(10 .dp))
509+ Column (
510+ modifier = Modifier
511+ .fillMaxWidth()
512+ .clip(RoundedCornerShape (8 .dp))
513+ .background(Color .Black .copy(alpha = 0.35f ))
514+ .padding(10 .dp)
515+ ) {
516+ commands.forEach { cmd ->
517+ Text (cmd, fontFamily = FontFamily .Monospace , fontSize = 10 .sp, color = theme.textColor(),
518+ modifier = Modifier .padding(vertical = 1 .dp))
519+ }
520+ }
521+ Spacer (Modifier .height(8 .dp))
522+ Row (
523+ modifier = Modifier
524+ .clip(RoundedCornerShape (8 .dp))
525+ .clickable {
526+ val cm = context.getSystemService(Context .CLIPBOARD_SERVICE ) as ClipboardManager
527+ cm.setPrimaryClip(ClipData .newPlainText(" build-commands" , commands.joinToString(" \n " )))
528+ Toast .makeText(context, t.toast_copied, Toast .LENGTH_SHORT ).show()
529+ }
530+ .padding(vertical = 4 .dp, horizontal = 2 .dp),
531+ verticalAlignment = Alignment .CenterVertically ,
532+ horizontalArrangement = Arrangement .spacedBy(6 .dp)
533+ ) {
534+ Icon (Icons .Default .ContentCopy , null , tint = theme.primaryColor(), modifier = Modifier .size(12 .dp))
535+ Text (t.games_source_copy_commands, fontSize = 11 .sp, color = theme.primaryColor())
536+ }
537+ }
538+ }
539+ }
540+
429541@Composable
430542private fun MetaLabel (label : String , value : String , mutedColor : Color , textColor : Color ) {
431543 Column {
0 commit comments