Enhance home screen functionality with catalog parsing and UI improvements

This commit is contained in:
tapframe 2026-03-04 09:10:00 +05:30
parent c8efcdc66b
commit 0732854d3e
18 changed files with 598 additions and 36 deletions

1
.gitignore vendored
View file

@ -17,3 +17,4 @@ captures
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
node_modules/
Docs

View file

@ -29,8 +29,11 @@ kotlin {
androidMain.dependencies {
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.activity.compose)
implementation(libs.ktor.client.android)
}
commonMain.dependencies {
implementation(libs.coil.compose)
implementation(libs.coil.network.ktor3)
implementation(libs.compose.runtime)
implementation(libs.compose.foundation)
implementation(libs.compose.material3)
@ -42,6 +45,9 @@ kotlin {
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.kotlinx.serialization.json)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}

View file

@ -3,4 +3,4 @@ package com.nuvio.app.core.ui
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
internal actual val nuvioPlatformExtraTopPadding: Dp = 12.dp
internal actual val nuvioPlatformExtraTopPadding: Dp = 0.dp

View file

@ -3,6 +3,9 @@ package com.nuvio.app
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
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.home.HomeScreen
@ -26,6 +29,11 @@ fun AppScreen(
@Composable
@Preview
fun App() {
setSingletonImageLoaderFactory { context ->
ImageLoader.Builder(context)
.crossfade(true)
.build()
}
NuvioTheme {
AppScreen(tab = AppScreenTab.Addons)
}

View file

@ -11,11 +11,11 @@ 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.asPaddingValues
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.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.width
@ -54,12 +54,12 @@ fun NuvioScreen(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.windowInsetsPadding(WindowInsets.safeDrawing),
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)),
contentPadding = PaddingValues(
start = 18.dp,
top = 12.dp + nuvioPlatformExtraTopPadding,
end = 18.dp,
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 22.dp,
bottom = 22.dp,
),
verticalArrangement = Arrangement.spacedBy(16.dp),
content = content,

View file

@ -5,4 +5,4 @@ internal expect object AddonStorage {
fun saveInstalledAddonUrls(urls: List<String>)
}
internal expect suspend fun httpGetText(url: String): String
expect suspend fun httpGetText(url: String): String

View file

@ -0,0 +1,56 @@
package com.nuvio.app.features.home
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 HomeCatalogParser {
private val json = Json {
ignoreUnknownKeys = true
}
fun parseCatalog(payload: String): List<MetaPreview> {
val root = json.parseToJsonElement(payload).jsonObject
return root.array("metas")
.mapNotNull { element ->
val meta = element as? JsonObject ?: return@mapNotNull null
val id = meta.string("id")
val type = meta.string("type")
val name = meta.string("name")
if (id.isNullOrBlank() || type.isNullOrBlank() || name.isNullOrBlank()) {
return@mapNotNull null
}
MetaPreview(
id = id,
type = type,
name = name,
poster = meta.string("poster"),
posterShape = meta.string("posterShape").toPosterShape(),
description = meta.string("description"),
releaseInfo = meta.string("releaseInfo"),
imdbRating = meta.string("imdbRating"),
genres = meta.array("genres").mapNotNull { genre ->
genre.jsonPrimitive.contentOrNull?.takeIf { it.isNotBlank() }
},
)
}
}
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 String?.toPosterShape(): PosterShape =
when (this?.lowercase()) {
"square" -> PosterShape.Square
"landscape" -> PosterShape.Landscape
else -> PosterShape.Poster
}
}

View file

@ -0,0 +1,45 @@
package com.nuvio.app.features.home
import com.nuvio.app.features.addons.ManagedAddon
data class MetaPreview(
val id: String,
val type: String,
val name: String,
val poster: String? = null,
val posterShape: PosterShape = PosterShape.Poster,
val description: String? = null,
val releaseInfo: String? = null,
val imdbRating: String? = null,
val genres: List<String> = emptyList(),
)
enum class PosterShape {
Poster,
Square,
Landscape,
}
data class HomeCatalogSection(
val key: String,
val title: String,
val subtitle: String,
val addonName: String,
val type: String,
val manifestUrl: String,
val catalogId: String,
val items: List<MetaPreview>,
)
data class HomeUiState(
val isLoading: Boolean = false,
val sections: List<HomeCatalogSection> = emptyList(),
val errorMessage: String? = null,
)
internal data class CatalogRequest(
val addon: ManagedAddon,
val catalogId: String,
val catalogName: String,
val type: String,
)

View file

@ -0,0 +1,110 @@
package com.nuvio.app.features.home
import com.nuvio.app.features.addons.ManagedAddon
import com.nuvio.app.features.addons.httpGetText
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
object HomeRepository {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
private var lastRequestKey: String? = null
fun refresh(addons: List<ManagedAddon>, force: Boolean = false) {
val requests = buildCatalogRequests(addons)
val requestKey = requests.joinToString(separator = "|") { request ->
"${request.addon.manifestUrl}:${request.type}:${request.catalogId}"
}
if (!force && requestKey == lastRequestKey) return
lastRequestKey = requestKey
if (requests.isEmpty()) {
_uiState.value = HomeUiState(
isLoading = false,
sections = emptyList(),
errorMessage = null,
)
return
}
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
scope.launch {
val results = requests.map { request ->
async {
runCatching { request.toSection() }
}
}.awaitAll()
val sections = results.mapNotNull { it.getOrNull() }
val firstFailure = results.firstNotNullOfOrNull { it.exceptionOrNull()?.message }
_uiState.value = HomeUiState(
isLoading = false,
sections = sections,
errorMessage = if (sections.isEmpty()) firstFailure else null,
)
}
}
private fun buildCatalogRequests(addons: List<ManagedAddon>): List<CatalogRequest> =
addons.mapNotNull { addon ->
val manifest = addon.manifest ?: return@mapNotNull null
addon to manifest
}.flatMap { (addon, manifest) ->
manifest.catalogs
.filter { catalog -> catalog.extra.none { it.isRequired } }
.map { catalog ->
CatalogRequest(
addon = addon,
catalogId = catalog.id,
catalogName = catalog.name,
type = catalog.type,
)
}
}
private suspend fun CatalogRequest.toSection(): HomeCatalogSection {
val manifest = requireNotNull(addon.manifest)
val catalogUrl = buildCatalogUrl(
manifestUrl = manifest.transportUrl,
type = type,
catalogId = catalogId,
)
val payload = httpGetText(catalogUrl)
val items = HomeCatalogParser.parseCatalog(payload).take(12)
require(items.isNotEmpty()) { "No feed items returned for $catalogName." }
return HomeCatalogSection(
key = "${manifest.id}:$type:$catalogId",
title = catalogName,
subtitle = manifest.name,
addonName = manifest.name,
type = type,
manifestUrl = manifest.transportUrl,
catalogId = catalogId,
items = items,
)
}
}
private fun buildCatalogUrl(
manifestUrl: String,
type: String,
catalogId: String,
): String {
val baseUrl = manifestUrl
.substringBefore("?")
.removeSuffix("/manifest.json")
return "$baseUrl/catalog/$type/$catalogId.json"
}

View file

@ -1,42 +1,100 @@
package com.nuvio.app.features.home
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.ui.NuvioIconActionButton
import com.nuvio.app.core.ui.NuvioScreen
import com.nuvio.app.core.ui.NuvioScreenHeader
import com.nuvio.app.core.ui.NuvioSectionLabel
import com.nuvio.app.core.ui.NuvioSurfaceCard
import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.home.components.HomeCatalogRowSection
import com.nuvio.app.features.home.components.HomeEmptyStateCard
@Composable
fun HomeScreen(
modifier: Modifier = Modifier,
) {
LaunchedEffect(Unit) {
AddonRepository.initialize()
}
val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle()
val homeUiState by HomeRepository.uiState.collectAsStateWithLifecycle()
val catalogRefreshKey = remember(addonsUiState.addons) {
addonsUiState.addons.mapNotNull { addon ->
val manifest = addon.manifest ?: return@mapNotNull null
buildString {
append(manifest.transportUrl)
append(':')
append(manifest.catalogs.joinToString(separator = ",") { catalog ->
"${catalog.type}:${catalog.id}:${catalog.extra.count { it.isRequired }}"
})
}
}
}
LaunchedEffect(catalogRefreshKey) {
HomeRepository.refresh(addonsUiState.addons)
}
NuvioScreen(modifier = modifier) {
item {
NuvioScreenHeader(title = "Home")
}
item {
NuvioSectionLabel(text = "OVERVIEW")
}
item {
NuvioSurfaceCard {
Text(
text = "Home screen content comes next.",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Shared cards, spacing and typography are now aligned with the Addons screen so the next feature can reuse the same shell.",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
NuvioScreenHeader(
title = "Home",
) {
NuvioIconActionButton(
icon = Icons.Rounded.Refresh,
contentDescription = "Refresh catalog rows",
onClick = { HomeRepository.refresh(addonsUiState.addons, force = true) },
)
}
}
when {
addonsUiState.addons.none { it.manifest != null } -> {
item {
HomeEmptyStateCard(
title = "No active addons",
message = "Install and validate at least one addon before loading catalog rows on Home.",
)
}
}
homeUiState.isLoading && homeUiState.sections.isEmpty() -> {
item {
HomeEmptyStateCard(
title = "Loading catalogs",
message = "Pulling feed-compatible catalogs from your installed addons.",
)
}
}
homeUiState.sections.isEmpty() -> {
item {
HomeEmptyStateCard(
title = "No home rows available",
message = homeUiState.errorMessage
?: "Installed addons do not currently expose board-compatible catalogs without required extras.",
)
}
}
else -> {
items(
count = homeUiState.sections.size,
key = { index -> homeUiState.sections[index].key },
) { index ->
HomeCatalogRowSection(
section = homeUiState.sections[index],
)
}
}
}
}
}

View file

@ -0,0 +1,40 @@
package com.nuvio.app.features.home.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.nuvio.app.features.home.HomeCatalogSection
@Composable
fun HomeCatalogRowSection(
section: HomeCatalogSection,
modifier: Modifier = Modifier,
onViewAllClick: (() -> Unit)? = null,
) {
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
HomeCatalogSectionHeader(
title = section.title,
subtitle = section.subtitle,
onViewAllClick = onViewAllClick,
)
LazyRow(
horizontalArrangement = Arrangement.spacedBy(14.dp),
) {
items(
items = section.items,
key = { item -> item.id },
) { item ->
HomePosterCard(item = item)
}
}
}
}

View file

@ -0,0 +1,100 @@
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
import androidx.compose.foundation.layout.Row
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.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material3.Icon
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.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
fun HomeCatalogSectionHeader(
title: String,
subtitle: String,
modifier: Modifier = Modifier,
onViewAllClick: (() -> Unit)? = null,
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top,
) {
Column(
modifier = Modifier.weight(1f),
) {
Text(
text = title,
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Box(
modifier = Modifier
.padding(top = 8.dp)
.width(82.dp)
.height(6.dp)
.background(
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(999.dp),
),
)
Text(
text = subtitle,
modifier = Modifier.padding(top = 10.dp),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
ViewAllPill(
onClick = onViewAllClick,
)
}
}
@Composable
private fun ViewAllPill(
onClick: (() -> Unit)?,
) {
Row(
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(20.dp),
)
.then(
if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier
)
.padding(horizontal = 18.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "View All",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
Icon(
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}

View file

@ -0,0 +1,90 @@
package com.nuvio.app.features.home.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.aspectRatio
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.nuvio.app.features.home.MetaPreview
import com.nuvio.app.features.home.PosterShape
@Composable
fun HomePosterCard(
item: MetaPreview,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.width(142.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(item.posterShape.aspectRatio)
.clip(RoundedCornerShape(22.dp))
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.Center,
) {
if (item.poster != null) {
AsyncImage(
model = item.poster,
contentDescription = item.name,
modifier = Modifier.matchParentSize(),
contentScale = ContentScale.Crop,
)
} else {
Text(
text = item.name,
modifier = Modifier.padding(horizontal = 14.dp),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}
}
Text(
text = item.name,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
val detailLine = listOfNotNull(item.releaseInfo, item.imdbRating?.let { "IMDb $it" }).joinToString("")
if (detailLine.isNotBlank()) {
Text(
text = detailLine,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} else {
Box(modifier = Modifier.height(0.dp))
}
}
}
private val PosterShape.aspectRatio: Float
get() = when (this) {
PosterShape.Poster -> 0.675f
PosterShape.Square -> 1f
PosterShape.Landscape -> 1.77f
}

View file

@ -0,0 +1,40 @@
package com.nuvio.app.features.home.components
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.nuvio.app.core.ui.NuvioPrimaryButton
import com.nuvio.app.core.ui.NuvioSurfaceCard
@Composable
fun HomeEmptyStateCard(
title: String,
message: String,
actionLabel: String? = null,
onActionClick: (() -> Unit)? = null,
) {
NuvioSurfaceCard {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = message,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (actionLabel != null && onActionClick != null) {
Spacer(modifier = Modifier.height(16.dp))
NuvioPrimaryButton(
text = actionLabel,
onClick = onActionClick,
)
}
}
}

View file

@ -11,7 +11,7 @@ private fun nuvioViewController(
AppScreen(tab = tab)
}
}.apply {
view.backgroundColor = UIColor.blackColor
view.backgroundColor = UIColor(red = 0.008, green = 0.016, blue = 0.016, alpha = 1.0)
}
fun HomeViewController() = nuvioViewController(tab = AppScreenTab.Home)

View file

@ -10,9 +10,11 @@ androidx-espresso = "3.7.0"
androidx-lifecycle = "2.9.6"
androidx-testExt = "1.3.0"
composeMultiplatform = "1.10.0"
coil = "3.4.0"
junit = "4.13.2"
kotlin = "2.3.0"
kotlinx-serialization = "1.8.1"
ktor = "3.4.1"
material3 = "1.10.0-alpha05"
[libraries]
@ -33,7 +35,11 @@ compose-material3 = { module = "org.jetbrains.compose.material3:material3", vers
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }

View file

@ -5,7 +5,7 @@ import ComposeApp
struct HomeComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let controller = MainViewControllerKt.HomeViewController()
controller.view.backgroundColor = .black
controller.view.backgroundColor = UIColor(red: 0.008, green: 0.016, blue: 0.016, alpha: 1.0)
return controller
}
@ -15,7 +15,7 @@ struct HomeComposeView: UIViewControllerRepresentable {
struct AddonsComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let controller = MainViewControllerKt.AddonsViewController()
controller.view.backgroundColor = .black
controller.view.backgroundColor = UIColor(red: 0.008, green: 0.016, blue: 0.016, alpha: 1.0)
return controller
}
@ -26,17 +26,19 @@ 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.black.ignoresSafeArea())
.toolbarBackground(Color.black, for: .tabBar)
.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)
}

View file

@ -6,7 +6,7 @@ struct iOSApp: App {
init() {
let tabAppearance = UITabBarAppearance()
tabAppearance.configureWithOpaqueBackground()
tabAppearance.backgroundColor = UIColor.black
tabAppearance.backgroundColor = UIColor(red: 0.039, green: 0.051, blue: 0.051, alpha: 1.0)
UITabBar.appearance().standardAppearance = tabAppearance
UITabBar.appearance().scrollEdgeAppearance = tabAppearance