package dev.stave.ui.about
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import dev.stave.R
import dev.stave.ui.components.staveFocusable
import dev.stave.ui.theme.AccentAmber
import dev.stave.ui.theme.OnAmber
import dev.stave.ui.theme.OnMuted
import dev.stave.ui.theme.OnPrimary
import dev.stave.ui.theme.OnQuiet
import dev.stave.ui.theme.adaptiveScreenPaddingHorizontal
import dev.stave.ui.theme.adaptiveScreenPaddingVertical
import dev.stave.ui.theme.isCompactWidth
import dev.stave.ui.theme.withInterDigits
/**
* About screen with adaptive layout:
* - **Android TV** (uiMode == UI_MODE_TYPE_TELEVISION): the original
* side-by-side AboutExpanded layout, byte-identical to the pre-phone-port
* baseline.
* - **Phone landscape** (orientation == LANDSCAPE, not TV): horizontal
* AboutExpanded layout — large badge on the left, info on the right —
* same shape as the pre-phone-port About page used to have. The vertical
* AboutCompact layout looked correct in portrait but felt cramped in
* landscape where the wide-and-short viewport begs for a horizontal split.
* - **Phone portrait** (anything else): vertical AboutCompact layout with a
* larger badge sized to the available shorter dimension.
*
* Tablets in landscape get the AboutExpanded treatment too; tablets in
* portrait fall through to AboutCompact. If a tablet-specific layout is
* needed later we can split that out.
*/
@Composable
fun AboutScreen(onBack: () -> Unit) {
BackHandler(onBack = onBack)
val configuration = androidx.compose.ui.platform.LocalConfiguration.current
val isAndroidTv =
(configuration.uiMode and android.content.res.Configuration.UI_MODE_TYPE_MASK) ==
android.content.res.Configuration.UI_MODE_TYPE_TELEVISION
val isLandscape = configuration.orientation ==
android.content.res.Configuration.ORIENTATION_LANDSCAPE
if (isAndroidTv || isLandscape) {
AboutExpanded()
} else {
AboutCompact()
}
}
@Composable
private fun AboutCompact() {
val config = androidx.compose.ui.platform.LocalConfiguration.current
// Badge at "full resolution" — fills the smaller screen dimension up
// to a generous fraction so the source's 512x512 detail isn't down-
// scaled. Portrait phones get ~80% width (so on a 411dp-wide Pixel 8a
// that's 329dp, rendered at 2.7x density ≈ 888px — slightly upscaled
// from the 512px source, comfortable for the generous brand mark).
// Height-limited paths (landscape phone routed to compact, short
// tablets) clamp to 65% of the height so it doesn't push the rest
// of the column off the screen. No hard dp cap — the user wants the
// badge to dominate the page, not sit in a tidy 320dp circle.
val badgeSize = minOf(
config.screenWidthDp.dp * 0.80f,
config.screenHeightDp.dp * 0.65f,
)
Column(
Modifier
.fillMaxSize()
.padding(
horizontal = adaptiveScreenPaddingHorizontal(),
vertical = adaptiveScreenPaddingVertical(),
)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(R.drawable.earhorn_badge),
contentDescription = null,
modifier = Modifier.size(badgeSize),
)
Spacer(Modifier.height(20.dp))
Text(
"Earhorn Music",
style = MaterialTheme.typography.headlineMedium,
color = OnPrimary,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(4.dp))
Text(
"A UPnP music player for Android TV, phones, GNU/Linux, and Android Auto",
style = MaterialTheme.typography.bodyMedium,
color = OnMuted,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.height(20.dp))
Text(
withInterDigits(appVersionLabel()),
style = MaterialTheme.typography.labelLarge,
color = AccentAmber,
)
Spacer(Modifier.height(24.dp))
Text(
"A Faveat Software Project",
style = MaterialTheme.typography.bodyMedium,
color = OnQuiet,
)
Spacer(Modifier.height(20.dp))
val context = androidx.compose.ui.platform.LocalContext.current
val interaction = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
Box(
Modifier
.background(AccentAmber, androidx.compose.foundation.shape.RoundedCornerShape(50))
.staveFocusable(interaction, cornerRadiusDp = 50)
.clickable(interaction, indication = null) {
runCatching {
context.startActivity(
android.content.Intent(
android.content.Intent.ACTION_VIEW,
android.net.Uri.parse("https://ko-fi.com/faveat"),
).addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
)
}
}
.padding(horizontal = 28.dp, vertical = 12.dp),
contentAlignment = Alignment.Center,
) {
Text("Say Thanks ☕", style = MaterialTheme.typography.titleSmall, color = OnAmber)
}
}
}
@Composable
private fun AboutExpanded() {
Row(
Modifier
.fillMaxSize()
.padding(
horizontal = adaptiveScreenPaddingHorizontal(),
vertical = adaptiveScreenPaddingVertical(),
),
horizontalArrangement = Arrangement.spacedBy(48.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Left: badge — fills 85% of available height as a square so its
// visual presence matches the portrait (Compact) layout's badge.
// Earlier value was 0.5f which produced a ~200dp badge in landscape
// vs ~329dp in portrait — the disparity made the brand mark feel
// diminished in landscape. The text Column to the right has
// weight(1f) so it still claims all remaining horizontal width;
// there's enough room on a landscape phone (~800–900dp wide) even
// with the badge consuming ~350dp.
Image(
painter = painterResource(R.drawable.earhorn_badge),
contentDescription = null,
modifier = Modifier
.fillMaxHeight(0.85f)
.aspectRatio(1f),
)
// Right: text details, vertically centered.
Column(
Modifier.weight(1f),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Center,
) {
Text(
"Earhorn Music",
style = MaterialTheme.typography.displayLarge,
color = OnPrimary,
)
Spacer(Modifier.height(4.dp))
Text(
"A UPnP music player for Android TV",
style = MaterialTheme.typography.titleMedium,
color = OnMuted,
)
Spacer(Modifier.height(24.dp))
Text(
withInterDigits(appVersionLabel()),
style = MaterialTheme.typography.labelLarge,
color = AccentAmber,
)
Spacer(Modifier.height(28.dp))
Text(
"A Faveat Software Project",
style = MaterialTheme.typography.bodyMedium,
color = OnQuiet,
)
Spacer(Modifier.height(24.dp))
val context = androidx.compose.ui.platform.LocalContext.current
val interaction = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
Box(
Modifier
.background(AccentAmber, androidx.compose.foundation.shape.RoundedCornerShape(50))
.staveFocusable(interaction, cornerRadiusDp = 50)
.clickable(interaction, indication = null) {
runCatching {
context.startActivity(
android.content.Intent(
android.content.Intent.ACTION_VIEW,
android.net.Uri.parse("https://ko-fi.com/faveat"),
).addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
)
}
}
.padding(horizontal = 28.dp, vertical = 12.dp),
contentAlignment = Alignment.Center,
) {
Text("Say Thanks ☕", style = MaterialTheme.typography.titleSmall, color = OnAmber)
}
}
}
}
/** The app's real version, read from the installed package at runtime.
* Derived — never hardcoded — so version bumps in build.gradle.kts
* propagate here automatically. (The previous hardcoded "Version 0.0.1"
* survived at least one real release unchanged.) Includes the flavor's
* versionNameSuffix, so libre builds truthfully show e.g. "0.1.1-libre".
*/
@Composable
private fun appVersionLabel(): String {
val context = androidx.compose.ui.platform.LocalContext.current
return remember {
runCatching {
"Version " + context.packageManager
.getPackageInfo(context.packageName, 0)
.versionName
}.getOrDefault("Version ?")
}
}