mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-01 00:59:27 +00:00
feat: Implement navigation and details screen for media items
- Added navigation support using Jetpack Compose Navigation. - Created MetaDetailsScreen to display detailed information about media items. - Introduced MetaDetailsRepository for fetching and managing media details. - Added components for displaying action buttons, cast, and meta information. - Updated HomeScreen and HomePosterCard to handle poster click events. - Integrated new UI components into the existing app structure. - Added platform-specific insets for better layout handling. - Updated Gradle dependencies for navigation and serialization support.
This commit is contained in:
parent
0732854d3e
commit
dde6b2212f
22 changed files with 844 additions and 121 deletions
|
|
@ -6,6 +6,7 @@ plugins {
|
|||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
|
|
@ -44,6 +45,7 @@ kotlin {
|
|||
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
||||
implementation(libs.androidx.lifecycle.runtimeCompose)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
}
|
||||
iosMain.dependencies {
|
||||
implementation(libs.ktor.client.darwin)
|
||||
|
|
|
|||
|
|
@ -4,24 +4,6 @@ import android.os.Bundle
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Extension
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.nuvio.app.core.ui.NuvioTheme
|
||||
import com.nuvio.app.features.addons.AddonStorage
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
|
@ -31,45 +13,7 @@ class MainActivity : ComponentActivity() {
|
|||
AddonStorage.initialize(applicationContext)
|
||||
|
||||
setContent {
|
||||
AndroidAppRoot()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AppAndroidPreview() {
|
||||
AndroidAppRoot()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AndroidAppRoot() {
|
||||
NuvioTheme {
|
||||
var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Addons) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
AppScreen(
|
||||
tab = selectedTab,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
NavigationBar(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == AppScreenTab.Home,
|
||||
onClick = { selectedTab = AppScreenTab.Home },
|
||||
icon = { Icon(Icons.Rounded.Home, contentDescription = null) },
|
||||
label = { Text("Home") },
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == AppScreenTab.Addons,
|
||||
onClick = { selectedTab = AppScreenTab.Addons },
|
||||
icon = { Icon(Icons.Rounded.Extension, contentDescription = null) },
|
||||
label = { Text("Addons") },
|
||||
)
|
||||
}
|
||||
App()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.dp
|
||||
|
||||
internal actual val nuvioPlatformExtraTopPadding: Dp = 0.dp
|
||||
internal actual val nuvioPlatformExtraBottomPadding: Dp = 0.dp
|
||||
|
|
|
|||
|
|
@ -1,14 +1,43 @@
|
|||
package com.nuvio.app
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Extension
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.toRoute
|
||||
import coil3.ImageLoader
|
||||
import coil3.compose.setSingletonImageLoaderFactory
|
||||
import coil3.request.crossfade
|
||||
import com.nuvio.app.core.ui.NuvioTheme
|
||||
import com.nuvio.app.features.addons.AddonsScreen
|
||||
import com.nuvio.app.features.details.MetaDetailsRepository
|
||||
import com.nuvio.app.features.details.MetaDetailsScreen
|
||||
import com.nuvio.app.features.home.HomeScreen
|
||||
import com.nuvio.app.features.home.MetaPreview
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
object TabsRoute
|
||||
|
||||
@Serializable
|
||||
data class DetailRoute(val type: String, val id: String)
|
||||
|
||||
enum class AppScreenTab {
|
||||
Home,
|
||||
|
|
@ -19,9 +48,13 @@ enum class AppScreenTab {
|
|||
fun AppScreen(
|
||||
tab: AppScreenTab,
|
||||
modifier: Modifier = Modifier,
|
||||
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
||||
) {
|
||||
when (tab) {
|
||||
AppScreenTab.Home -> HomeScreen(modifier = modifier)
|
||||
AppScreenTab.Home -> HomeScreen(
|
||||
modifier = modifier,
|
||||
onPosterClick = onPosterClick,
|
||||
)
|
||||
AppScreenTab.Addons -> AddonsScreen(modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +68,59 @@ fun App() {
|
|||
.build()
|
||||
}
|
||||
NuvioTheme {
|
||||
AppScreen(tab = AppScreenTab.Addons)
|
||||
val navController = rememberNavController()
|
||||
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
|
||||
var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Addons) }
|
||||
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
bottomBar = {
|
||||
if (currentRoute == TabsRoute::class.qualifiedName) {
|
||||
NavigationBar(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == AppScreenTab.Home,
|
||||
onClick = { selectedTab = AppScreenTab.Home },
|
||||
icon = { Icon(Icons.Rounded.Home, contentDescription = null) },
|
||||
label = { Text("Home") },
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == AppScreenTab.Addons,
|
||||
onClick = { selectedTab = AppScreenTab.Addons },
|
||||
icon = { Icon(Icons.Rounded.Extension, contentDescription = null) },
|
||||
label = { Text("Addons") },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) { innerPadding ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = TabsRoute,
|
||||
) {
|
||||
composable<TabsRoute> {
|
||||
AppScreen(
|
||||
tab = selectedTab,
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
onPosterClick = { meta ->
|
||||
navController.navigate(DetailRoute(type = meta.type, id = meta.id))
|
||||
},
|
||||
)
|
||||
}
|
||||
composable<DetailRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<DetailRoute>()
|
||||
MetaDetailsScreen(
|
||||
type = route.type,
|
||||
id = route.id,
|
||||
onBack = {
|
||||
MetaDetailsRepository.clear()
|
||||
navController.popBackStack()
|
||||
},
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,16 +10,11 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
|
|
@ -53,13 +48,12 @@ fun NuvioScreen(
|
|||
LazyColumn(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)),
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
contentPadding = PaddingValues(
|
||||
start = 18.dp,
|
||||
top = 12.dp + nuvioPlatformExtraTopPadding,
|
||||
end = 18.dp,
|
||||
bottom = 22.dp,
|
||||
bottom = 22.dp + nuvioPlatformExtraBottomPadding,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
content = content,
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ package com.nuvio.app.core.ui
|
|||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
internal expect val nuvioPlatformExtraTopPadding: Dp
|
||||
internal expect val nuvioPlatformExtraBottomPadding: Dp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package com.nuvio.app.features.details
|
||||
|
||||
data class MetaDetails(
|
||||
val id: String,
|
||||
val type: String,
|
||||
val name: String,
|
||||
val poster: String? = null,
|
||||
val background: String? = null,
|
||||
val logo: String? = null,
|
||||
val description: String? = null,
|
||||
val releaseInfo: String? = null,
|
||||
val imdbRating: String? = null,
|
||||
val runtime: String? = null,
|
||||
val genres: List<String> = emptyList(),
|
||||
val director: List<String> = emptyList(),
|
||||
val cast: List<String> = emptyList(),
|
||||
val country: String? = null,
|
||||
val awards: String? = null,
|
||||
val language: String? = null,
|
||||
val website: String? = null,
|
||||
val links: List<MetaLink> = emptyList(),
|
||||
)
|
||||
|
||||
data class MetaLink(
|
||||
val name: String,
|
||||
val category: String,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
data class MetaDetailsUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val meta: MetaDetails? = null,
|
||||
val errorMessage: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.nuvio.app.features.details
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
internal object MetaDetailsParser {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun parse(payload: String): MetaDetails {
|
||||
val root = json.parseToJsonElement(payload).jsonObject
|
||||
val meta = root["meta"]?.jsonObject ?: error("Missing 'meta' in response")
|
||||
|
||||
return MetaDetails(
|
||||
id = meta.requiredString("id"),
|
||||
type = meta.requiredString("type"),
|
||||
name = meta.requiredString("name"),
|
||||
poster = meta.string("poster"),
|
||||
background = meta.string("background"),
|
||||
logo = meta.string("logo"),
|
||||
description = meta.string("description"),
|
||||
releaseInfo = meta.string("releaseInfo"),
|
||||
imdbRating = meta.string("imdbRating"),
|
||||
runtime = meta.string("runtime"),
|
||||
genres = meta.stringList("genres"),
|
||||
director = meta.stringList("director"),
|
||||
cast = meta.stringList("cast"),
|
||||
country = meta.string("country"),
|
||||
awards = meta.string("awards"),
|
||||
language = meta.string("language"),
|
||||
website = meta.string("website"),
|
||||
links = meta.links(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun JsonObject.requiredString(name: String): String =
|
||||
this[name]?.jsonPrimitive?.contentOrNull ?: error("Missing required field '$name'")
|
||||
|
||||
private fun JsonObject.string(name: String): String? =
|
||||
this[name]?.jsonPrimitive?.contentOrNull
|
||||
|
||||
private fun JsonObject.array(name: String): JsonArray =
|
||||
this[name] as? JsonArray ?: JsonArray(emptyList())
|
||||
|
||||
private fun JsonObject.stringList(name: String): List<String> =
|
||||
array(name).mapNotNull { it.jsonPrimitive.contentOrNull?.takeIf(String::isNotBlank) }
|
||||
|
||||
private fun JsonObject.links(): List<MetaLink> =
|
||||
array("links").mapNotNull { element ->
|
||||
val link = element as? JsonObject ?: return@mapNotNull null
|
||||
val linkName = link.string("name") ?: return@mapNotNull null
|
||||
val category = link.string("category") ?: return@mapNotNull null
|
||||
val url = link.string("url") ?: return@mapNotNull null
|
||||
MetaLink(name = linkName, category = category, url = url)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.nuvio.app.features.details
|
||||
|
||||
import com.nuvio.app.features.addons.AddonManifest
|
||||
import com.nuvio.app.features.addons.AddonRepository
|
||||
import com.nuvio.app.features.addons.httpGetText
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object MetaDetailsRepository {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val _uiState = MutableStateFlow(MetaDetailsUiState())
|
||||
val uiState: StateFlow<MetaDetailsUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun load(type: String, id: String) {
|
||||
_uiState.value = MetaDetailsUiState(isLoading = true)
|
||||
|
||||
scope.launch {
|
||||
val manifests = AddonRepository.uiState.value.addons
|
||||
.mapNotNull { it.manifest }
|
||||
.filter { manifest ->
|
||||
manifest.resources.any { resource ->
|
||||
resource.name == "meta" &&
|
||||
resource.types.contains(type) &&
|
||||
(resource.idPrefixes.isEmpty() || resource.idPrefixes.any { id.startsWith(it) })
|
||||
}
|
||||
}
|
||||
|
||||
if (manifests.isEmpty()) {
|
||||
_uiState.value = MetaDetailsUiState(
|
||||
errorMessage = "No addon provides meta for this content.",
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
for (manifest in manifests) {
|
||||
val result = tryFetchMeta(manifest, type, id)
|
||||
if (result != null) {
|
||||
_uiState.value = MetaDetailsUiState(meta = result)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
_uiState.value = MetaDetailsUiState(
|
||||
errorMessage = "Could not load details from any addon.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_uiState.value = MetaDetailsUiState()
|
||||
}
|
||||
|
||||
private suspend fun tryFetchMeta(
|
||||
manifest: AddonManifest,
|
||||
type: String,
|
||||
id: String,
|
||||
): MetaDetails? {
|
||||
return try {
|
||||
val baseUrl = manifest.transportUrl
|
||||
.substringBefore("?")
|
||||
.removeSuffix("/manifest.json")
|
||||
val url = "$baseUrl/meta/$type/$id.json"
|
||||
val payload = httpGetText(url)
|
||||
MetaDetailsParser.parse(payload)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package com.nuvio.app.features.details
|
||||
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.nuvio.app.core.ui.nuvioPlatformExtraBottomPadding
|
||||
import com.nuvio.app.features.details.components.DetailActionButtons
|
||||
import com.nuvio.app.features.details.components.DetailCastSection
|
||||
import com.nuvio.app.features.details.components.DetailHero
|
||||
import com.nuvio.app.features.details.components.DetailMetaInfo
|
||||
|
||||
@Composable
|
||||
fun MetaDetailsScreen(
|
||||
type: String,
|
||||
id: String,
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val uiState by MetaDetailsRepository.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(type, id) {
|
||||
MetaDetailsRepository.load(type, id)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
) {
|
||||
when {
|
||||
uiState.isLoading -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
||||
uiState.errorMessage != null -> {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Failed to load",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = uiState.errorMessage.orEmpty(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
uiState.meta != null -> {
|
||||
val meta = uiState.meta!!
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
DetailHero(meta = meta)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
DetailActionButtons()
|
||||
|
||||
DetailMetaInfo(meta = meta)
|
||||
|
||||
DetailCastSection(cast = meta.cast)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp + nuvioPlatformExtraBottomPadding))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Back button overlay
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top))
|
||||
.padding(start = 12.dp, top = 8.dp)
|
||||
.size(40.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.background.copy(alpha = 0.5f),
|
||||
shape = CircleShape,
|
||||
)
|
||||
.clickable(onClick = onBack),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.size(22.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.nuvio.app.features.details.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BookmarkBorder
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun DetailActionButtons(
|
||||
modifier: Modifier = Modifier,
|
||||
onPlayClick: () -> Unit = {},
|
||||
onSaveClick: () -> Unit = {},
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Button(
|
||||
onClick = onPlayClick,
|
||||
modifier = Modifier
|
||||
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.onBackground,
|
||||
contentColor = MaterialTheme.colorScheme.background,
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(22.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "Play",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onSaveClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.BookmarkBorder,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "Save",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.nuvio.app.features.details.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun DetailCastSection(
|
||||
cast: List<String>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (cast.isEmpty()) return
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Cast",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
items(cast) { name ->
|
||||
CastItem(name = name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CastItem(
|
||||
name: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = CircleShape,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = name.initials(),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.initials(): String {
|
||||
val parts = trim().split(" ").filter { it.isNotBlank() }
|
||||
return when {
|
||||
parts.size >= 2 -> "${parts.first().first().uppercaseChar()}${parts.last().first().uppercaseChar()}"
|
||||
parts.size == 1 -> parts.first().take(2).uppercase()
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package com.nuvio.app.features.details.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
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.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.nuvio.app.features.details.MetaDetails
|
||||
|
||||
@Composable
|
||||
fun DetailHero(
|
||||
meta: MetaDetails,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
val imageUrl = meta.background ?: meta.poster
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = meta.name,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(0.75f),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(0.75f)
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
)
|
||||
}
|
||||
|
||||
// Gradient overlay at the bottom
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(0.75f),
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(260.dp)
|
||||
.align(Alignment.BottomCenter)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
MaterialTheme.colorScheme.background.copy(alpha = 0.7f),
|
||||
MaterialTheme.colorScheme.background,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 18.dp)
|
||||
.padding(bottom = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (meta.logo != null) {
|
||||
AsyncImage(
|
||||
model = meta.logo,
|
||||
contentDescription = "${meta.name} logo",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.6f)
|
||||
.height(80.dp),
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = meta.name,
|
||||
style = MaterialTheme.typography.displayLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
if (meta.genres.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = meta.genres.take(3).joinToString(" \u2022 "),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
package com.nuvio.app.features.details.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.nuvio.app.features.details.MetaDetails
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun DetailMetaInfo(
|
||||
meta: MetaDetails,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
// Year, Runtime, IMDb rating row
|
||||
val infoParts = buildList {
|
||||
meta.releaseInfo?.let { add(it) }
|
||||
meta.runtime?.let { add(it.uppercase()) }
|
||||
}
|
||||
if (infoParts.isNotEmpty() || meta.imdbRating != null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
infoParts.forEach { part ->
|
||||
Text(
|
||||
text = part,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
if (meta.imdbRating != null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = ImdbYellow,
|
||||
) {
|
||||
Text(
|
||||
text = "IMDb",
|
||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||
style = MaterialTheme.typography.labelMedium.copy(
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
letterSpacing = 0.sp,
|
||||
),
|
||||
color = ImdbBlack,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(5.dp))
|
||||
Text(
|
||||
text = meta.imdbRating,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = ImdbYellow,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Director
|
||||
if (meta.director.isNotEmpty()) {
|
||||
Row {
|
||||
Text(
|
||||
text = "Director: ",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = meta.director.joinToString(", "),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
if (!meta.description.isNullOrBlank()) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Column {
|
||||
Text(
|
||||
text = meta.description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = if (expanded) Int.MAX_VALUE else 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
lineHeight = 22.sp,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = if (expanded) "Show Less" else "Show More ▾",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.clickable { expanded = !expanded },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ImdbYellow = Color(0xFFF5C518)
|
||||
private val ImdbBlack = Color(0xFF000000)
|
||||
|
|
@ -18,6 +18,7 @@ import com.nuvio.app.features.home.components.HomeEmptyStateCard
|
|||
@Composable
|
||||
fun HomeScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
AddonRepository.initialize()
|
||||
|
|
@ -92,6 +93,7 @@ fun HomeScreen(
|
|||
) { index ->
|
||||
HomeCatalogRowSection(
|
||||
section = homeUiState.sections[index],
|
||||
onPosterClick = onPosterClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.nuvio.app.features.home.HomeCatalogSection
|
||||
import com.nuvio.app.features.home.MetaPreview
|
||||
|
||||
@Composable
|
||||
fun HomeCatalogRowSection(
|
||||
section: HomeCatalogSection,
|
||||
modifier: Modifier = Modifier,
|
||||
onViewAllClick: (() -> Unit)? = null,
|
||||
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
|
|
@ -33,7 +35,10 @@ fun HomeCatalogRowSection(
|
|||
items = section.items,
|
||||
key = { item -> item.id },
|
||||
) { item ->
|
||||
HomePosterCard(item = item)
|
||||
HomePosterCard(
|
||||
item = item,
|
||||
onClick = onPosterClick?.let { { it(item) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.nuvio.app.features.home.components
|
||||
|
||||
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
|
||||
|
|
@ -28,9 +29,12 @@ import com.nuvio.app.features.home.PosterShape
|
|||
fun HomePosterCard(
|
||||
item: MetaPreview,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.width(142.dp),
|
||||
modifier = modifier
|
||||
.width(142.dp)
|
||||
.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -2,18 +2,11 @@ package com.nuvio.app
|
|||
|
||||
import androidx.compose.ui.window.ComposeUIViewController
|
||||
import platform.UIKit.UIColor
|
||||
import com.nuvio.app.core.ui.NuvioTheme
|
||||
|
||||
private fun nuvioViewController(
|
||||
tab: AppScreenTab,
|
||||
) = ComposeUIViewController {
|
||||
NuvioTheme {
|
||||
AppScreen(tab = tab)
|
||||
}
|
||||
private val nuvioBackgroundColor = UIColor(red = 0.008, green = 0.016, blue = 0.016, alpha = 1.0)
|
||||
|
||||
fun MainViewController() = ComposeUIViewController {
|
||||
App()
|
||||
}.apply {
|
||||
view.backgroundColor = UIColor(red = 0.008, green = 0.016, blue = 0.016, alpha = 1.0)
|
||||
view.backgroundColor = nuvioBackgroundColor
|
||||
}
|
||||
|
||||
fun HomeViewController() = nuvioViewController(tab = AppScreenTab.Home)
|
||||
|
||||
fun AddonsViewController() = nuvioViewController(tab = AppScreenTab.Addons)
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.dp
|
||||
|
||||
internal actual val nuvioPlatformExtraTopPadding: Dp = 0.dp
|
||||
internal actual val nuvioPlatformExtraBottomPadding: Dp = 0.dp
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ android-compileSdk = "36"
|
|||
android-minSdk = "24"
|
||||
android-targetSdk = "36"
|
||||
androidx-activity = "1.12.2"
|
||||
androidx-navigation = "2.9.0"
|
||||
androidx-appcompat = "1.7.1"
|
||||
androidx-core = "1.17.0"
|
||||
androidx-espresso = "3.7.0"
|
||||
|
|
@ -26,6 +27,7 @@ androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "an
|
|||
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||
androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
|
||||
compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" }
|
||||
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
||||
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||
|
|
@ -47,3 +49,4 @@ androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
|||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
|
||||
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
|
|
|
|||
|
|
@ -2,19 +2,9 @@ import UIKit
|
|||
import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct HomeComposeView: UIViewControllerRepresentable {
|
||||
struct ComposeView: UIViewControllerRepresentable {
|
||||
func makeUIViewController(context: Context) -> UIViewController {
|
||||
let controller = MainViewControllerKt.HomeViewController()
|
||||
controller.view.backgroundColor = UIColor(red: 0.008, green: 0.016, blue: 0.016, alpha: 1.0)
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
|
||||
}
|
||||
|
||||
struct AddonsComposeView: UIViewControllerRepresentable {
|
||||
func makeUIViewController(context: Context) -> UIViewController {
|
||||
let controller = MainViewControllerKt.AddonsViewController()
|
||||
let controller = MainViewControllerKt.MainViewController()
|
||||
controller.view.backgroundColor = UIColor(red: 0.008, green: 0.016, blue: 0.016, alpha: 1.0)
|
||||
return controller
|
||||
}
|
||||
|
|
@ -24,22 +14,7 @@ struct AddonsComposeView: UIViewControllerRepresentable {
|
|||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
TabView {
|
||||
HomeComposeView()
|
||||
.ignoresSafeArea()
|
||||
.tabItem {
|
||||
Label("Home", systemImage: "house.fill")
|
||||
}
|
||||
|
||||
AddonsComposeView()
|
||||
.ignoresSafeArea()
|
||||
.tabItem {
|
||||
Label("Addons", systemImage: "puzzlepiece.extension.fill")
|
||||
}
|
||||
}
|
||||
.background(Color(red: 0.008, green: 0.016, blue: 0.016).ignoresSafeArea())
|
||||
.toolbarBackground(Color(red: 0.039, green: 0.051, blue: 0.051), for: .tabBar)
|
||||
.toolbarBackground(.visible, for: .tabBar)
|
||||
.toolbarColorScheme(.dark, for: .tabBar)
|
||||
ComposeView()
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,7 @@
|
|||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
struct iOSApp: App {
|
||||
init() {
|
||||
let tabAppearance = UITabBarAppearance()
|
||||
tabAppearance.configureWithOpaqueBackground()
|
||||
tabAppearance.backgroundColor = UIColor(red: 0.039, green: 0.051, blue: 0.051, alpha: 1.0)
|
||||
|
||||
UITabBar.appearance().standardAppearance = tabAppearance
|
||||
UITabBar.appearance().scrollEdgeAppearance = tabAppearance
|
||||
UITabBar.appearance().unselectedItemTintColor = UIColor(white: 0.58, alpha: 1.0)
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
|
|
|
|||
Loading…
Reference in a new issue