From dde6b2212fdfb2c3e652c270b6a5bc551d58d048 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:00:12 +0530 Subject: [PATCH] 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. --- composeApp/build.gradle.kts | 2 + .../kotlin/com/nuvio/app/MainActivity.kt | 58 +------- .../core/ui/NuvioPlatformInsets.android.kt | 1 + .../commonMain/kotlin/com/nuvio/app/App.kt | 90 +++++++++++- .../com/nuvio/app/core/ui/NuvioComponents.kt | 10 +- .../nuvio/app/core/ui/NuvioPlatformInsets.kt | 1 + .../app/features/details/MetaDetailsModels.kt | 34 +++++ .../app/features/details/MetaDetailsParser.kt | 59 ++++++++ .../features/details/MetaDetailsRepository.kt | 74 ++++++++++ .../app/features/details/MetaDetailsScreen.kt | 136 ++++++++++++++++++ .../details/components/DetailActionButtons.kt | 81 +++++++++++ .../details/components/DetailCastSection.kt | 94 ++++++++++++ .../features/details/components/DetailHero.kt | 112 +++++++++++++++ .../details/components/DetailMetaInfo.kt | 133 +++++++++++++++++ .../com/nuvio/app/features/home/HomeScreen.kt | 2 + .../home/components/HomeCatalogSection.kt | 7 +- .../home/components/HomePosterCard.kt | 6 +- .../com/nuvio/app/MainViewController.kt | 17 +-- .../app/core/ui/NuvioPlatformInsets.ios.kt | 1 + gradle/libs.versions.toml | 3 + iosApp/iosApp/ContentView.swift | 33 +---- iosApp/iosApp/iOSApp.swift | 11 -- 22 files changed, 844 insertions(+), 121 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsModels.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsRepository.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailActionButtons.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailHero.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index ed51abf1..e24dba21 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -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) diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt index 76b922b9..71214631 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt @@ -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() } } } diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.android.kt index f9886e4b..58480710 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.android.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index d1b86b95..a8bf7052 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -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 { + AppScreen( + tab = selectedTab, + modifier = Modifier.padding(innerPadding), + onPosterClick = { meta -> + navController.navigate(DetailRoute(type = meta.type, id = meta.id)) + }, + ) + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + MetaDetailsScreen( + type = route.type, + id = route.id, + onBack = { + MetaDetailsRepository.clear() + navController.popBackStack() + }, + modifier = Modifier.padding(innerPadding), + ) + } + } + } } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioComponents.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioComponents.kt index a0a55147..eee214cc 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioComponents.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioComponents.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.kt index 80c924e5..c3daea65 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsModels.kt new file mode 100644 index 00000000..c42273b4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsModels.kt @@ -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 = emptyList(), + val director: List = emptyList(), + val cast: List = emptyList(), + val country: String? = null, + val awards: String? = null, + val language: String? = null, + val website: String? = null, + val links: List = 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, +) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt new file mode 100644 index 00000000..08497dcc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt @@ -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 = + array(name).mapNotNull { it.jsonPrimitive.contentOrNull?.takeIf(String::isNotBlank) } + + private fun JsonObject.links(): List = + 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) + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsRepository.kt new file mode 100644 index 00000000..53e50cc9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsRepository.kt @@ -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 = _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 + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt new file mode 100644 index 00000000..e151d439 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt @@ -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), + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailActionButtons.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailActionButtons.kt new file mode 100644 index 00000000..8d515bf7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailActionButtons.kt @@ -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, + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt new file mode 100644 index 00000000..b0ab71fd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt @@ -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, + 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 -> "" + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailHero.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailHero.kt new file mode 100644 index 00000000..d2f83225 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailHero.kt @@ -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, + ) + } + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt new file mode 100644 index 00000000..e0735e6b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt @@ -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) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt index b5fdaa57..38bbc36d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt @@ -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, ) } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeCatalogSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeCatalogSection.kt index 93fa0e29..8e6b13b5 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeCatalogSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeCatalogSection.kt @@ -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) } }, + ) } } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt index 8cccc484..e4f3f55e 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt @@ -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( diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/MainViewController.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/MainViewController.kt index 100f1017..349dee5b 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/MainViewController.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/MainViewController.kt @@ -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) diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.ios.kt index f9886e4b..58480710 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NuvioPlatformInsets.ios.kt @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e132c7c7..c03e90e5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index 015bdfc2..52da3297 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -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() } } diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index 64f6d056..3f816b82 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -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()