Skip to content

Commit 5955df7

Browse files
authored
Update DocumentationScreen.kt
1 parent 3eed580 commit 5955df7

1 file changed

Lines changed: 180 additions & 8 deletions

File tree

source-code/app/src/main/java/com/hackeros/app/ui/screens/DocumentationScreen.kt

Lines changed: 180 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,29 @@ fun DocumentationScreen(
5353
var selectedTabKey by remember { mutableStateOf<String?>(null) }
5454
var searchQuery by remember { mutableStateOf("") }
5555

56+
// Non-null while a native documentation "detail" sub-page is open (see LinkLineView) - e.g.
57+
// tapping "Official HackerScript documentation" pushes this instead of opening the external
58+
// browser, so the whole flow stays natively inside the app with no WebView involved and
59+
// nothing ever "teleports" the user away. Cleared whenever the underlying doc page changes
60+
// (e.g. a language switch) so a stale detail page is never shown on top of new content.
61+
var openDetailKey by remember { mutableStateOf<String?>(null) }
62+
5663
// Reset the active tab whenever a new page loads (e.g. after a language change).
5764
LaunchedEffect(docPage) {
5865
if (docPage != null && (selectedTabKey == null || docPage.tabs.none { it.key == selectedTabKey })) {
5966
selectedTabKey = docPage.tabs.firstOrNull()?.key
6067
}
68+
openDetailKey = null
69+
}
70+
71+
if (openDetailKey != null) {
72+
DocDetailScreen(
73+
detailKey = openDetailKey!!,
74+
currentLanguage = currentLanguage,
75+
translations = t,
76+
onBack = { openDetailKey = null }
77+
)
78+
return
6179
}
6280

6381
Column(modifier = Modifier.fillMaxSize()) {
@@ -186,11 +204,16 @@ fun DocumentationScreen(
186204
val activeTab = filteredTabs.find { it.key == selectedTabKey } ?: filteredTabs.firstOrNull()
187205
if (activeTab != null) {
188206
LazyColumn(
189-
contentPadding = PaddingValues(start = 20.dp, end = 20.dp, bottom = 100.dp),
207+
// Bumped from 100.dp: the last block in a tab (often exactly this
208+
// kind of LinkLine) could end up positioned right underneath the
209+
// floating bottom nav bar on some devices/insets, which both hid it
210+
// visually and blocked its touches - this padding is now generous
211+
// enough that the last item always clears the nav bar entirely.
212+
contentPadding = PaddingValues(start = 20.dp, end = 20.dp, bottom = 150.dp),
190213
verticalArrangement = Arrangement.spacedBy(10.dp)
191214
) {
192215
items(activeTab.blocks) { block ->
193-
DocBlockView(block, theme, t)
216+
DocBlockView(block, theme, t, onOpenNativeDetail = { key -> openDetailKey = key })
194217
}
195218
}
196219
}
@@ -219,7 +242,12 @@ private fun blockTextOf(tab: DocTab): String = buildString {
219242
}
220243

221244
@Composable
222-
private fun DocBlockView(block: DocBlock, theme: com.hackeros.app.data.model.AppTheme, translations: Translations) {
245+
private fun DocBlockView(
246+
block: DocBlock,
247+
theme: com.hackeros.app.data.model.AppTheme,
248+
translations: Translations,
249+
onOpenNativeDetail: (String) -> Unit = {}
250+
) {
223251
when (block) {
224252
is DocBlock.Heading2 -> Text(
225253
block.text, fontSize = 19.sp, fontWeight = FontWeight.Bold,
@@ -266,7 +294,18 @@ private fun DocBlockView(block: DocBlock, theme: com.hackeros.app.data.model.App
266294
}
267295
is DocBlock.Command -> CommandBlockView(block.text, theme, translations)
268296
is DocBlock.CodeSample -> CodeSampleView(block.text, theme, translations)
269-
is DocBlock.LinkLine -> LinkLineView(block.labelHtml, block.url, theme)
297+
is DocBlock.LinkLine -> LinkLineView(
298+
labelHtml = block.labelHtml,
299+
url = block.url,
300+
theme = theme,
301+
isNative = block.nativeDetailKey != null,
302+
onClick = {
303+
if (block.nativeDetailKey != null) {
304+
onOpenNativeDetail(block.nativeDetailKey)
305+
true
306+
} else false
307+
}
308+
)
270309
is DocBlock.ToolsTable -> ToolsTableView(block.rows, theme)
271310
DocBlock.Divider -> HorizontalDivider(color = Color.White.copy(alpha = 0.06f))
272311
}
@@ -316,16 +355,37 @@ private fun CodeSampleView(code: String, theme: com.hackeros.app.data.model.AppT
316355
}
317356

318357
@Composable
319-
private fun LinkLineView(labelHtml: String, url: String, theme: com.hackeros.app.data.model.AppTheme) {
358+
private fun LinkLineView(
359+
labelHtml: String,
360+
url: String,
361+
theme: com.hackeros.app.data.model.AppTheme,
362+
isNative: Boolean = false,
363+
onClick: () -> Boolean = { false }
364+
) {
320365
val uriHandler = androidx.compose.ui.platform.LocalUriHandler.current
321366
Row(
322367
modifier = Modifier
323-
.clickable { try { uriHandler.openUri(url) } catch (_: Exception) {} }
324-
.padding(vertical = 2.dp),
368+
.fillMaxWidth()
369+
.clip(RoundedCornerShape(8.dp))
370+
.clickable {
371+
// onClick() returns true when this link was handled natively in-app (see
372+
// DocBlock.LinkLine.nativeDetailKey) - the external browser is only ever a
373+
// fallback for links that don't have a native destination.
374+
if (!onClick()) {
375+
try { uriHandler.openUri(url) } catch (_: Exception) {}
376+
}
377+
}
378+
// A larger touch target than the text itself, and enough vertical padding that
379+
// this row can never end up sitting flush against - or clipped by - the bottom
380+
// nav bar even as the very last item in a tab.
381+
.padding(vertical = 10.dp, horizontal = 2.dp),
325382
verticalAlignment = Alignment.CenterVertically,
326383
horizontalArrangement = Arrangement.spacedBy(6.dp)
327384
) {
328-
Icon(Icons.Default.OpenInNew, null, tint = theme.primaryColor(), modifier = Modifier.size(13.dp))
385+
Icon(
386+
if (isNative) Icons.Default.ArrowForward else Icons.Default.OpenInNew,
387+
null, tint = theme.primaryColor(), modifier = Modifier.size(13.dp)
388+
)
329389
InlineHtmlText(html = labelHtml, color = theme.primaryColor(), fontSize = 12.sp)
330390
}
331391
}
@@ -358,3 +418,115 @@ private fun copyToClipboard(context: Context, text: String, translations: Transl
358418
cm.setPrimaryClip(ClipData.newPlainText("command", text))
359419
Toast.makeText(context, translations.toast_copied, Toast.LENGTH_SHORT).show()
360420
}
421+
422+
/**
423+
* A fully native, in-app "detail" sub-page for a documentation link (see
424+
* DocBlock.LinkLine.nativeDetailKey / DocContentParser.detailTabsFor). Tapping the link that
425+
* opens this never leaves the app, never opens a browser tab, and doesn't use any WebView - this
426+
* whole screen is plain Compose UI, exactly like the rest of the Documentation tab. It has its
427+
* own small tab bar (reusing the same look as the main doc screen) plus a back button that
428+
* simply clears the local navigation state - nothing "teleports" anywhere.
429+
*/
430+
@Composable
431+
private fun DocDetailScreen(
432+
detailKey: String,
433+
currentLanguage: Language,
434+
translations: Translations,
435+
onBack: () -> Unit
436+
) {
437+
val theme = LocalAppTheme.current
438+
val t = translations
439+
val tabs = remember(detailKey) {
440+
com.hackeros.app.data.docs.DocContentParser.detailTabsFor(detailKey).orEmpty()
441+
}
442+
val title = remember(detailKey) { com.hackeros.app.data.docs.DocContentParser.detailTitleFor(detailKey) }
443+
var selectedTabKey by remember(detailKey) { mutableStateOf(tabs.firstOrNull()?.key) }
444+
val activeTab = tabs.find { it.key == selectedTabKey } ?: tabs.firstOrNull()
445+
446+
Column(modifier = Modifier.fillMaxSize()) {
447+
Row(
448+
modifier = Modifier
449+
.fillMaxWidth()
450+
.padding(start = 12.dp, end = 24.dp, top = 8.dp, bottom = 4.dp),
451+
verticalAlignment = Alignment.CenterVertically
452+
) {
453+
IconButton(onClick = onBack) {
454+
Icon(Icons.Default.ArrowBack, contentDescription = t.doc_detail_back, tint = Color.White)
455+
}
456+
Column(modifier = Modifier.padding(start = 4.dp)) {
457+
Text(
458+
text = title,
459+
fontFamily = FontFamily.Monospace,
460+
fontWeight = FontWeight.Bold,
461+
fontSize = 22.sp,
462+
color = Color.White
463+
)
464+
Text(
465+
text = t.doc_detail_native_notice,
466+
fontSize = 11.sp,
467+
color = theme.mutedColor()
468+
)
469+
}
470+
}
471+
472+
if (currentLanguage != Language.PL) {
473+
Box(Modifier.padding(horizontal = 20.dp, vertical = 6.dp)) {
474+
Row(
475+
modifier = Modifier
476+
.fillMaxWidth()
477+
.clip(RoundedCornerShape(10.dp))
478+
.background(theme.primaryColor().copy(alpha = 0.08f))
479+
.border(1.dp, theme.primaryColor().copy(alpha = 0.25f), RoundedCornerShape(10.dp))
480+
.padding(horizontal = 14.dp, vertical = 10.dp),
481+
verticalAlignment = Alignment.CenterVertically,
482+
horizontalArrangement = Arrangement.spacedBy(8.dp)
483+
) {
484+
Icon(Icons.Default.Info, null, tint = theme.primaryColor(), modifier = Modifier.size(14.dp))
485+
Text(t.doc_detail_pl_only_notice, fontSize = 11.sp, color = theme.primaryColor())
486+
}
487+
}
488+
}
489+
490+
if (tabs.isEmpty()) {
491+
Box(Modifier.fillMaxWidth().padding(24.dp), contentAlignment = Alignment.Center) {
492+
Text(t.doc_no_search_results, color = theme.mutedColor(), fontSize = 12.sp)
493+
}
494+
return@Column
495+
}
496+
497+
LazyRow(
498+
contentPadding = PaddingValues(horizontal = 20.dp),
499+
horizontalArrangement = Arrangement.spacedBy(8.dp),
500+
modifier = Modifier.padding(bottom = 12.dp)
501+
) {
502+
items(tabs, key = { it.key }) { tab ->
503+
val active = tab.key == selectedTabKey
504+
Box(
505+
modifier = Modifier
506+
.clip(RoundedCornerShape(20.dp))
507+
.background(if (active) theme.primaryColor() else Color.White.copy(alpha = 0.06f))
508+
.clickable { selectedTabKey = tab.key }
509+
.padding(horizontal = 16.dp, vertical = 8.dp)
510+
) {
511+
Text(
512+
tab.label,
513+
fontSize = 12.sp,
514+
fontWeight = FontWeight.Bold,
515+
color = if (active) theme.backgroundColor() else theme.mutedColor()
516+
)
517+
}
518+
}
519+
}
520+
521+
if (activeTab != null) {
522+
LazyColumn(
523+
contentPadding = PaddingValues(start = 20.dp, end = 20.dp, bottom = 150.dp),
524+
verticalArrangement = Arrangement.spacedBy(10.dp)
525+
) {
526+
items(activeTab.blocks) { block ->
527+
DocBlockView(block, theme, t)
528+
}
529+
}
530+
}
531+
}
532+
}

0 commit comments

Comments
 (0)