feat: add :app-tv module as Android TV starting point
Creates a new :app-tv application module that reuses shared business logic from :core:model, :core:data, :core:player, and :feature:shared, with its own TV-specific UI layer. - TvApplication + TvActivity with full Hilt + Navigation3 wiring - TvHomePage reusing HomePageViewModel from :feature:shared - Copied common UI components (PosterCard, WaitingScreen, etc.) - Copied login screen and form components - TV navigation module with Home + Login route entry builders - Material3 theme copied from mobile (ready for TV customization) - AndroidManifest with LAUNCHER + LEANBACK_LAUNCHER intent filters - Excludes :feature:download (downloads are mobile-only)
90
app-tv/build.gradle.kts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
alias(libs.plugins.hilt)
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "hu.bbara.purefin.tv"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "hu.bbara.purefin.tv"
|
||||||
|
minSdk = 29
|
||||||
|
targetSdk = 36
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(JvmTarget.JVM_11)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":core:model"))
|
||||||
|
implementation(project(":core:data"))
|
||||||
|
implementation(project(":core:player"))
|
||||||
|
implementation(project(":feature:shared"))
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
|
||||||
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
|
implementation(libs.androidx.compose.ui)
|
||||||
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
implementation(libs.androidx.compose.material3)
|
||||||
|
implementation(libs.androidx.compose.material.icons.extended)
|
||||||
|
implementation(libs.jellyfin.core)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
implementation(libs.hilt)
|
||||||
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
|
implementation(libs.datastore)
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
implementation(libs.logging.interceptor)
|
||||||
|
implementation(libs.androidx.compose.foundation)
|
||||||
|
implementation(libs.coil.compose)
|
||||||
|
implementation(libs.coil.network.okhttp)
|
||||||
|
implementation(libs.medi3.exoplayer)
|
||||||
|
implementation(libs.medi3.ffmpeg.decoder)
|
||||||
|
implementation(libs.media3.datasource.okhttp)
|
||||||
|
implementation(libs.androidx.navigation3.runtime)
|
||||||
|
implementation(libs.androidx.navigation3.ui)
|
||||||
|
implementation(libs.androidx.room.ktx)
|
||||||
|
ksp(libs.androidx.room.compiler)
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
|
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||||
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
ksp(libs.hilt.compiler)
|
||||||
|
}
|
||||||
21
app-tv/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
39
app-tv/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.touchscreen"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".TvApplication"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@mipmap/purefin_logo"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/purefin_logo_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.Purefin">
|
||||||
|
<activity
|
||||||
|
android:name=".TvActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.Purefin">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
120
app-tv/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package hu.bbara.purefin.common.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
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.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
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 coil3.request.ImageRequest
|
||||||
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator
|
||||||
|
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
||||||
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PosterCard(
|
||||||
|
item: PosterItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onMovieSelected: (UUID) -> Unit,
|
||||||
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val context = LocalContext.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
val posterWidth = 144.dp
|
||||||
|
val posterHeight = posterWidth * 3 / 2
|
||||||
|
|
||||||
|
fun openItem(posterItem: PosterItem) {
|
||||||
|
when (posterItem.type) {
|
||||||
|
BaseItemKind.MOVIE -> onMovieSelected(posterItem.id)
|
||||||
|
BaseItemKind.SERIES -> onSeriesSelected(posterItem.id)
|
||||||
|
BaseItemKind.EPISODE -> {
|
||||||
|
val ep = posterItem.episode!!
|
||||||
|
onEpisodeSelected(ep.seriesId, ep.seasonId, ep.id)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageRequest = ImageRequest.Builder(context)
|
||||||
|
.data(item.imageUrl)
|
||||||
|
.size(with(density) { posterWidth.roundToPx() }, with(density) { posterHeight.roundToPx() })
|
||||||
|
.build()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(posterWidth)
|
||||||
|
) {
|
||||||
|
Box() {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = imageRequest,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(2f / 3f)
|
||||||
|
.clip(RoundedCornerShape(14.dp))
|
||||||
|
.border(1.dp, scheme.outlineVariant.copy(alpha = 0.3f), RoundedCornerShape(14.dp))
|
||||||
|
.background(scheme.surfaceVariant)
|
||||||
|
.clickable(onClick = { openItem(item) }),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
when (item.type) {
|
||||||
|
BaseItemKind.MOVIE -> {
|
||||||
|
val m = item.movie!!
|
||||||
|
WatchStateIndicator(
|
||||||
|
size = 28,
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd)
|
||||||
|
.padding(8.dp),
|
||||||
|
watched = m.watched,
|
||||||
|
started = (m.progress ?: 0.0) > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BaseItemKind.EPISODE -> {
|
||||||
|
val ep = item.episode!!
|
||||||
|
WatchStateIndicator(
|
||||||
|
size = 28,
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd)
|
||||||
|
.padding(8.dp),
|
||||||
|
watched = ep.watched,
|
||||||
|
started = (ep.progress ?: 0.0) > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BaseItemKind.SERIES -> UnwatchedEpisodeIndicator(
|
||||||
|
size = 28,
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd)
|
||||||
|
.padding(8.dp),
|
||||||
|
unwatchedCount = item.series!!.unwatchedEpisodeCount
|
||||||
|
)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = item.title,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(top = 8.dp, start = 4.dp, end = 4.dp, bottom = 8.dp),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package hu.bbara.purefin.common.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PurefinComplexTextField(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
placeholder: String,
|
||||||
|
leadingIcon: ImageVector? = null,
|
||||||
|
trailingIcon: ImageVector? = null,
|
||||||
|
visualTransformation: VisualTransformation = VisualTransformation.None
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
TextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp)),
|
||||||
|
placeholder = { Text(placeholder, color = scheme.onSurfaceVariant) },
|
||||||
|
leadingIcon = if (leadingIcon != null) {
|
||||||
|
{ Icon(leadingIcon, contentDescription = null, tint = scheme.onSurfaceVariant) }
|
||||||
|
} else null,
|
||||||
|
trailingIcon = if (trailingIcon != null) {
|
||||||
|
{ Icon(Icons.Default.Visibility, contentDescription = null, tint = scheme.onSurfaceVariant) }
|
||||||
|
} else null,
|
||||||
|
visualTransformation = visualTransformation,
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = scheme.surfaceVariant,
|
||||||
|
unfocusedContainerColor = scheme.surfaceVariant,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
cursorColor = scheme.primary,
|
||||||
|
focusedTextColor = scheme.onSurface,
|
||||||
|
unfocusedTextColor = scheme.onSurface
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package hu.bbara.purefin.common.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PurefinPasswordField(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
placeholder: String,
|
||||||
|
leadingIcon: ImageVector,
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
val showField = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
TextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(12.dp)),
|
||||||
|
placeholder = { Text(placeholder, color = scheme.onSurfaceVariant) },
|
||||||
|
leadingIcon = { Icon(leadingIcon, contentDescription = null, tint = scheme.onSurfaceVariant) },
|
||||||
|
trailingIcon =
|
||||||
|
{
|
||||||
|
IconButton(
|
||||||
|
onClick = { showField.value = !showField.value },
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Visibility, contentDescription = null, tint = scheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visualTransformation = if (showField.value) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = scheme.surfaceVariant,
|
||||||
|
unfocusedContainerColor = scheme.surfaceVariant,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
cursorColor = scheme.primary,
|
||||||
|
focusedTextColor = scheme.onSurface,
|
||||||
|
unfocusedTextColor = scheme.onSurface
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package hu.bbara.purefin.common.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PurefinTextButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
content: @Composable RowScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary
|
||||||
|
),
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
199
app-tv/src/main/java/hu/bbara/purefin/common/ui/WaitingScreen.kt
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
package hu.bbara.purefin.common.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Movie
|
||||||
|
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.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PurefinWaitingScreen(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val accentColor = scheme.primary
|
||||||
|
val backgroundColor = scheme.background
|
||||||
|
val surfaceColor = scheme.surface
|
||||||
|
val textPrimary = scheme.onSurface
|
||||||
|
val textSecondary = scheme.onSurfaceVariant
|
||||||
|
|
||||||
|
val transition = rememberInfiniteTransition(label = "waiting-pulse")
|
||||||
|
val pulseScale = transition.animateFloat(
|
||||||
|
initialValue = 0.9f,
|
||||||
|
targetValue = 1.15f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(durationMillis = 1400, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "pulse-scale"
|
||||||
|
)
|
||||||
|
val pulseAlpha = transition.animateFloat(
|
||||||
|
initialValue = 0.2f,
|
||||||
|
targetValue = 0.6f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(durationMillis = 1400, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "pulse-alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
val gradient = Brush.radialGradient(
|
||||||
|
colors = listOf(
|
||||||
|
accentColor.copy(alpha = 0.28f),
|
||||||
|
backgroundColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(gradient)
|
||||||
|
.padding(24.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(28.dp))
|
||||||
|
.background(surfaceColor.copy(alpha = 0.92f))
|
||||||
|
.padding(horizontal = 28.dp, vertical = 32.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(86.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = pulseScale.value
|
||||||
|
scaleY = pulseScale.value
|
||||||
|
}
|
||||||
|
.alpha(pulseAlpha.value)
|
||||||
|
.border(
|
||||||
|
width = 2.dp,
|
||||||
|
color = accentColor.copy(alpha = 0.6f),
|
||||||
|
shape = RoundedCornerShape(26.dp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(RoundedCornerShape(22.dp))
|
||||||
|
.background(accentColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Movie,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = scheme.onPrimary,
|
||||||
|
modifier = Modifier.size(40.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Just a moment",
|
||||||
|
color = textPrimary,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "I am doing all I can...",
|
||||||
|
color = textSecondary,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
WaitingDots(accentColor = accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WaitingDots(accentColor: Color, modifier: Modifier = Modifier) {
|
||||||
|
val transition = rememberInfiniteTransition(label = "waiting-dots")
|
||||||
|
val firstAlpha = transition.animateFloat(
|
||||||
|
initialValue = 0.3f,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(durationMillis = 700, delayMillis = 0, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "dot-1"
|
||||||
|
)
|
||||||
|
val secondAlpha = transition.animateFloat(
|
||||||
|
initialValue = 0.3f,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(durationMillis = 700, delayMillis = 140, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "dot-2"
|
||||||
|
)
|
||||||
|
val thirdAlpha = transition.animateFloat(
|
||||||
|
initialValue = 0.3f,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(durationMillis = 700, delayMillis = 280, easing = FastOutSlowInEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "dot-3"
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
WaitingDot(alpha = firstAlpha.value, color = accentColor)
|
||||||
|
WaitingDot(alpha = secondAlpha.value, color = accentColor)
|
||||||
|
WaitingDot(alpha = thirdAlpha.value, color = accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WaitingDot(alpha: Float, color: Color) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(10.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
val scale = 0.7f + (alpha * 0.3f)
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
}
|
||||||
|
.alpha(alpha)
|
||||||
|
.background(color, CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A progress bar component for displaying media playback progress.
|
||||||
|
*
|
||||||
|
* @param progress The progress value between 0f and 1f, where 0f is no progress and 1f is complete.
|
||||||
|
* @param foregroundColor The color of the progress indicator.
|
||||||
|
* @param backgroundColor The color of the background/unfilled portion of the progress bar.
|
||||||
|
* @param modifier The modifier to be applied to the Box. Modifier should contain the Alignment.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MediaProgressBar(
|
||||||
|
progress: Float,
|
||||||
|
foregroundColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
backgroundColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier: Modifier
|
||||||
|
) {
|
||||||
|
if (progress == 0f) return
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.padding(bottom = 8.dp, start = 8.dp, end = 8.dp)
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(4.dp)
|
||||||
|
.background(backgroundColor.copy(alpha = 0.2f))
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.fillMaxWidth(progress)
|
||||||
|
.background(foregroundColor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async image that falls back to theme-synced color blocks so loading/error states
|
||||||
|
* stay aligned with PurefinTheme's colorScheme.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun PurefinAsyncImage(
|
||||||
|
model: Any?,
|
||||||
|
contentDescription: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentScale: ContentScale = ContentScale.Crop
|
||||||
|
) {
|
||||||
|
val placeholderPainter = ColorPainter(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
|
||||||
|
// Convert empty string to null to properly trigger fallback
|
||||||
|
val effectiveModel = when {
|
||||||
|
model is String && model.isEmpty() -> null
|
||||||
|
else -> model
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = effectiveModel,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
modifier = modifier,
|
||||||
|
contentScale = contentScale,
|
||||||
|
placeholder = placeholderPainter,
|
||||||
|
error = placeholderPainter,
|
||||||
|
fallback = placeholderPainter
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PurefinIconButton(
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Int = 52
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(scheme.secondary)
|
||||||
|
.clickable { onClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = scheme.onSecondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchField(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
placeholder: String,
|
||||||
|
backgroundColor: Color,
|
||||||
|
textColor: Color,
|
||||||
|
cursorColor: Color,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(32.dp)),
|
||||||
|
placeholder = { Text(placeholder, color = scheme.onSurfaceVariant) },
|
||||||
|
leadingIcon =
|
||||||
|
{ Icon(imageVector = Icons.Outlined.Search, contentDescription = null, tint = scheme.onSurfaceVariant) },
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = backgroundColor,
|
||||||
|
unfocusedContainerColor = backgroundColor,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
cursorColor = cursorColor,
|
||||||
|
focusedTextColor = textColor,
|
||||||
|
unfocusedTextColor = textColor,
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
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.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UnwatchedEpisodeIndicator(
|
||||||
|
unwatchedCount: Int,
|
||||||
|
foregroundColor: Color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
backgroundColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
size: Int = 24,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
if (unwatchedCount == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier
|
||||||
|
.border(1.dp, backgroundColor.copy(alpha = 0.8f), CircleShape)
|
||||||
|
.background(backgroundColor.copy(alpha = 0.8f), CircleShape)
|
||||||
|
.size(size.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (unwatchedCount > 9) "9+" else unwatchedCount.toString(),
|
||||||
|
color = foregroundColor.copy(alpha = 0.8f),
|
||||||
|
fontWeight = FontWeight.W900,
|
||||||
|
fontSize = 15.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package hu.bbara.purefin.common.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Check
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WatchStateIndicator(
|
||||||
|
watched: Boolean,
|
||||||
|
started: Boolean,
|
||||||
|
watchedColor: Color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
watchedBackgroundColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
startedColor: Color = MaterialTheme.colorScheme.onSecondary,
|
||||||
|
startedBackgroundColor: Color = MaterialTheme.colorScheme.secondary,
|
||||||
|
size: Int = 24,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (watched.not() && started.not()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val foregroundColor = if (watched) watchedColor.copy(alpha = 0.8f) else startedColor.copy(alpha = 0.3f)
|
||||||
|
val backgroundColor = if (watched) watchedBackgroundColor.copy(alpha = 0.8f) else startedBackgroundColor.copy(alpha = 0.3f)
|
||||||
|
val borderColor = if (watched) watchedBackgroundColor.copy(alpha = 0.8f) else startedBackgroundColor.copy(alpha = 0.8f)
|
||||||
|
|
||||||
|
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier
|
||||||
|
.border(1.dp, borderColor, CircleShape)
|
||||||
|
.background(backgroundColor, CircleShape)
|
||||||
|
.size(size.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
) {
|
||||||
|
if (watched) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Check,
|
||||||
|
contentDescription = "Check",
|
||||||
|
tint = foregroundColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(1.dp)
|
||||||
|
.matchParentSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun WatchStateIndicatorPreview() {
|
||||||
|
Column() {
|
||||||
|
WatchStateIndicator(
|
||||||
|
watched = false,
|
||||||
|
started = false
|
||||||
|
)
|
||||||
|
WatchStateIndicator(
|
||||||
|
watched = true,
|
||||||
|
started = false
|
||||||
|
)
|
||||||
|
WatchStateIndicator(
|
||||||
|
watched = false,
|
||||||
|
started = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
214
app-tv/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package hu.bbara.purefin.login.ui
|
||||||
|
|
||||||
|
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.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Lock
|
||||||
|
import androidx.compose.material.icons.filled.Movie
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material.icons.filled.Storage
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import hu.bbara.purefin.common.ui.PurefinComplexTextField
|
||||||
|
import hu.bbara.purefin.common.ui.PurefinPasswordField
|
||||||
|
import hu.bbara.purefin.common.ui.PurefinTextButton
|
||||||
|
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||||
|
import hu.bbara.purefin.feature.shared.login.LoginViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginScreen(
|
||||||
|
viewModel: LoginViewModel = hiltViewModel(),
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
// Observe ViewModel state
|
||||||
|
val serverUrl by viewModel.url.collectAsState()
|
||||||
|
val username by viewModel.username.collectAsState()
|
||||||
|
val password by viewModel.password.collectAsState()
|
||||||
|
val errorMessage by viewModel.errorMessage.collectAsState()
|
||||||
|
var isLoggingIn by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
if (isLoggingIn) {
|
||||||
|
PurefinWaitingScreen(modifier = modifier)
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(scheme.background)
|
||||||
|
.padding(24.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.weight(0.5f))
|
||||||
|
|
||||||
|
// Logo Section
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(100.dp)
|
||||||
|
.background(scheme.primary, RoundedCornerShape(24.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Movie,
|
||||||
|
contentDescription = "Logo",
|
||||||
|
tint = scheme.onPrimary,
|
||||||
|
modifier = Modifier.size(60.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Jellyfin",
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(top = 16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "PERSONAL MEDIA SYSTEM",
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
letterSpacing = 2.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
|
// Form Section
|
||||||
|
Text(
|
||||||
|
text = "Connect to Server",
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.align(Alignment.Start)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Enter your details to access your library",
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Start)
|
||||||
|
.padding(bottom = 24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (errorMessage != null) {
|
||||||
|
Text(
|
||||||
|
text = errorMessage!!,
|
||||||
|
color = scheme.error,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
scheme.errorContainer,
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.padding(12.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
PurefinComplexTextField(
|
||||||
|
label = "Server URL",
|
||||||
|
value = serverUrl,
|
||||||
|
onValueChange = {
|
||||||
|
viewModel.clearError()
|
||||||
|
viewModel.setUrl(it)
|
||||||
|
},
|
||||||
|
placeholder = "http://192.168.1.100:8096",
|
||||||
|
leadingIcon = Icons.Default.Storage
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
PurefinComplexTextField(
|
||||||
|
label = "Username",
|
||||||
|
value = username,
|
||||||
|
onValueChange = {
|
||||||
|
viewModel.clearError()
|
||||||
|
viewModel.setUsername(it)
|
||||||
|
},
|
||||||
|
placeholder = "Enter your username",
|
||||||
|
leadingIcon = Icons.Default.Person
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
PurefinPasswordField(
|
||||||
|
label = "Password",
|
||||||
|
value = password,
|
||||||
|
onValueChange = {
|
||||||
|
viewModel.clearError()
|
||||||
|
viewModel.setPassword(it)
|
||||||
|
},
|
||||||
|
placeholder = "••••••••",
|
||||||
|
leadingIcon = Icons.Default.Lock,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
PurefinTextButton(
|
||||||
|
content = { Text("Connect") },
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
isLoggingIn = true
|
||||||
|
try {
|
||||||
|
viewModel.login()
|
||||||
|
} finally {
|
||||||
|
isLoggingIn = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(0.5f))
|
||||||
|
|
||||||
|
// Footer Links
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
TextButton(onClick = {}) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(Icons.Default.Search, contentDescription = null, tint = scheme.onSurfaceVariant, modifier = Modifier.size(18.dp))
|
||||||
|
Text(" Discover Servers", color = scheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextButton(onClick = {}) {
|
||||||
|
Text("Need Help?", color = scheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
183
app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package hu.bbara.purefin.tv
|
||||||
|
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||||
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
|
import androidx.navigation3.runtime.entryProvider
|
||||||
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
|
import androidx.navigation3.ui.NavDisplay
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.disk.DiskCache
|
||||||
|
import coil3.memory.MemoryCache
|
||||||
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import coil3.util.DebugLogger
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||||
|
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
||||||
|
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor
|
||||||
|
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
|
||||||
|
import hu.bbara.purefin.core.data.navigation.NavigationCommand
|
||||||
|
import hu.bbara.purefin.core.data.navigation.NavigationManager
|
||||||
|
import hu.bbara.purefin.core.data.navigation.Route
|
||||||
|
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||||
|
import hu.bbara.purefin.login.ui.LoginScreen
|
||||||
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okio.Path.Companion.toPath
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class TvActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var entryBuilders: Set<@JvmSuppressWildcards EntryProviderScope<Route>.() -> Unit>
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userSessionRepository: UserSessionRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var navigationManager: NavigationManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var jellyfinApiClient: JellyfinApiClient
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var authInterceptor: JellyfinAuthInterceptor
|
||||||
|
|
||||||
|
private val imageOkHttpClient: OkHttpClient by lazy {
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.addNetworkInterceptor(authInterceptor)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
lifecycleScope.launch { init() }
|
||||||
|
configureImageLoader()
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContent {
|
||||||
|
AppTheme {
|
||||||
|
MainApp(
|
||||||
|
userSessionRepository = userSessionRepository,
|
||||||
|
entryBuilders = entryBuilders,
|
||||||
|
navigationManager = navigationManager
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun init() {
|
||||||
|
jellyfinApiClient.updateApiClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureImageLoader() {
|
||||||
|
SingletonImageLoader.setSafe { context ->
|
||||||
|
val builder = ImageLoader.Builder(context)
|
||||||
|
.components {
|
||||||
|
add(
|
||||||
|
OkHttpNetworkFetcherFactory(
|
||||||
|
callFactory = { imageOkHttpClient }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.memoryCache {
|
||||||
|
MemoryCache.Builder()
|
||||||
|
.maxSizePercent(context, 0.20)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.diskCache {
|
||||||
|
DiskCache.Builder()
|
||||||
|
.directory(context.cacheDir.resolve("image_cache").absolutePath.toPath())
|
||||||
|
.maxSizeBytes(30_000_000)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.crossfade(true)
|
||||||
|
|
||||||
|
if (isDebuggable()) {
|
||||||
|
builder.logger(DebugLogger())
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isDebuggable(): Boolean =
|
||||||
|
(applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainApp(
|
||||||
|
userSessionRepository: UserSessionRepository,
|
||||||
|
entryBuilders: Set<@JvmSuppressWildcards EntryProviderScope<Route>.() -> Unit>,
|
||||||
|
navigationManager: NavigationManager
|
||||||
|
) {
|
||||||
|
var sessionLoaded by remember { mutableStateOf(false) }
|
||||||
|
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
userSessionRepository.isLoggedIn.collect {
|
||||||
|
sessionLoaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionLoaded) {
|
||||||
|
PurefinWaitingScreen(modifier = Modifier.fillMaxSize())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoggedIn) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val backStack = rememberNavBackStack(Route.Home) as NavBackStack<Route>
|
||||||
|
val appEntryProvider = entryProvider {
|
||||||
|
entryBuilders.forEach { builder -> builder() }
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(navigationManager, backStack) {
|
||||||
|
navigationManager.commands.collect { command ->
|
||||||
|
when (command) {
|
||||||
|
NavigationCommand.Pop -> if (backStack.size > 1) backStack.removeLastOrNull()
|
||||||
|
is NavigationCommand.Navigate -> backStack.add(command.route)
|
||||||
|
is NavigationCommand.ReplaceAll -> {
|
||||||
|
backStack.clear()
|
||||||
|
backStack.add(command.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalNavigationManager provides navigationManager) {
|
||||||
|
NavDisplay(
|
||||||
|
backStack = backStack,
|
||||||
|
onBack = { navigationManager.pop() },
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
entryDecorators = listOf(
|
||||||
|
rememberSaveableStateHolderNavEntryDecorator(),
|
||||||
|
rememberViewModelStoreNavEntryDecorator(),
|
||||||
|
),
|
||||||
|
entryProvider = appEntryProvider
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LoginScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package hu.bbara.purefin.tv
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
|
||||||
|
@HiltAndroidApp
|
||||||
|
class TvApplication : Application()
|
||||||
108
app-tv/src/main/java/hu/bbara/purefin/tv/home/TvHomePage.kt
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package hu.bbara.purefin.tv.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Collections
|
||||||
|
import androidx.compose.material.icons.outlined.Movie
|
||||||
|
import androidx.compose.material.icons.outlined.Tv
|
||||||
|
import androidx.compose.material3.DrawerValue
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalDrawerSheet
|
||||||
|
import androidx.compose.material3.ModalNavigationDrawer
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.rememberDrawerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
|
import hu.bbara.purefin.feature.shared.home.HomePageViewModel
|
||||||
|
import hu.bbara.purefin.tv.home.ui.TvHomeContent
|
||||||
|
import hu.bbara.purefin.tv.home.ui.TvHomeDrawerContent
|
||||||
|
import hu.bbara.purefin.tv.home.ui.TvHomeMockData
|
||||||
|
import hu.bbara.purefin.tv.home.ui.TvHomeNavItem
|
||||||
|
import hu.bbara.purefin.tv.home.ui.TvHomeTopBar
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvHomePage(
|
||||||
|
viewModel: HomePageViewModel = hiltViewModel(),
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val libraries = viewModel.libraries.collectAsState().value
|
||||||
|
val isOfflineMode = viewModel.isOfflineMode.collectAsState().value
|
||||||
|
val libraryNavItems = libraries.map {
|
||||||
|
TvHomeNavItem(
|
||||||
|
id = it.id,
|
||||||
|
label = it.name,
|
||||||
|
icon = when (it.type) {
|
||||||
|
CollectionType.MOVIES -> Icons.Outlined.Movie
|
||||||
|
CollectionType.TVSHOWS -> Icons.Outlined.Tv
|
||||||
|
else -> Icons.Outlined.Collections
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val continueWatching = viewModel.continueWatching.collectAsState()
|
||||||
|
val nextUp = viewModel.nextUp.collectAsState()
|
||||||
|
val latestLibraryContent = viewModel.latestLibraryContent.collectAsState()
|
||||||
|
|
||||||
|
LifecycleResumeEffect(Unit) {
|
||||||
|
viewModel.onResumed()
|
||||||
|
onPauseOrDispose { }
|
||||||
|
}
|
||||||
|
|
||||||
|
ModalNavigationDrawer(
|
||||||
|
drawerState = drawerState,
|
||||||
|
drawerContent = {
|
||||||
|
ModalDrawerSheet(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(280.dp)
|
||||||
|
.fillMaxSize(),
|
||||||
|
drawerContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
drawerContentColor = MaterialTheme.colorScheme.onBackground
|
||||||
|
) {
|
||||||
|
TvHomeDrawerContent(
|
||||||
|
title = "Jellyfin",
|
||||||
|
subtitle = "Library Dashboard",
|
||||||
|
primaryNavItems = libraryNavItems,
|
||||||
|
secondaryNavItems = TvHomeMockData.secondaryNavItems,
|
||||||
|
user = TvHomeMockData.user,
|
||||||
|
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.label) },
|
||||||
|
onLogout = viewModel::logout
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
topBar = {
|
||||||
|
TvHomeTopBar(
|
||||||
|
onMenuClick = { coroutineScope.launch { drawerState.open() } },
|
||||||
|
isOfflineMode = isOfflineMode,
|
||||||
|
onToggleOfflineMode = viewModel::toggleOfflineMode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
TvHomeContent(
|
||||||
|
libraries = libraries,
|
||||||
|
libraryContent = latestLibraryContent.value,
|
||||||
|
continueWatching = continueWatching.value,
|
||||||
|
nextUp = nextUp.value,
|
||||||
|
onMovieSelected = viewModel::onMovieSelected,
|
||||||
|
onSeriesSelected = viewModel::onSeriesSelected,
|
||||||
|
onEpisodeSelected = viewModel::onEpisodeSelected,
|
||||||
|
modifier = Modifier.padding(innerPadding)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package hu.bbara.purefin.tv.home.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvHomeAvatar(
|
||||||
|
size: Dp,
|
||||||
|
borderWidth: Dp,
|
||||||
|
borderColor: Color,
|
||||||
|
backgroundColor: Color,
|
||||||
|
icon: ImageVector,
|
||||||
|
iconTint: Color,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.border(borderWidth, borderColor, CircleShape)
|
||||||
|
.background(backgroundColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = iconTint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package hu.bbara.purefin.tv.home.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||||
|
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||||
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvHomeContent(
|
||||||
|
libraries: List<LibraryItem>,
|
||||||
|
libraryContent: Map<UUID, List<PosterItem>>,
|
||||||
|
continueWatching: List<ContinueWatchingItem>,
|
||||||
|
nextUp: List<NextUpItem>,
|
||||||
|
onMovieSelected: (UUID) -> Unit,
|
||||||
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
TvContinueWatchingSection(
|
||||||
|
items = continueWatching,
|
||||||
|
onMovieSelected = onMovieSelected,
|
||||||
|
onEpisodeSelected = onEpisodeSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
TvNextUpSection(
|
||||||
|
items = nextUp,
|
||||||
|
onEpisodeSelected = onEpisodeSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
items(
|
||||||
|
items = libraries.filter { libraryContent[it.id]?.isEmpty() != true },
|
||||||
|
key = { it.id }
|
||||||
|
) { item ->
|
||||||
|
TvLibraryPosterSection(
|
||||||
|
title = item.name,
|
||||||
|
items = libraryContent[item.id] ?: emptyList(),
|
||||||
|
action = "See All",
|
||||||
|
onMovieSelected = onMovieSelected,
|
||||||
|
onSeriesSelected = onSeriesSelected,
|
||||||
|
onEpisodeSelected = onEpisodeSelected
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
204
app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeDrawer.kt
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
package hu.bbara.purefin.tv.home.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.outlined.Person
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
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.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
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvHomeDrawerContent(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
primaryNavItems: List<TvHomeNavItem>,
|
||||||
|
secondaryNavItems: List<TvHomeNavItem>,
|
||||||
|
user: TvHomeUser,
|
||||||
|
onLibrarySelected: (TvHomeNavItem) -> Unit,
|
||||||
|
onLogout: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier.fillMaxSize()) {
|
||||||
|
TvHomeDrawerHeader(
|
||||||
|
title = title,
|
||||||
|
subtitle = subtitle
|
||||||
|
)
|
||||||
|
TvHomeDrawerNav(
|
||||||
|
primaryItems = primaryNavItems,
|
||||||
|
secondaryItems = secondaryNavItems,
|
||||||
|
onLibrarySelected = onLibrarySelected
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
TvHomeDrawerFooter(user = user, onLogout = onLogout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvHomeDrawerHeader(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 24.dp, end = 16.dp, top = 24.dp, bottom = 20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.background(scheme.primary, RoundedCornerShape(12.dp)),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PlayArrow,
|
||||||
|
contentDescription = "Play",
|
||||||
|
tint = scheme.onPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.padding(start = 12.dp)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider(color = scheme.onSurfaceVariant.copy(alpha = 0.2f))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvHomeDrawerNav(
|
||||||
|
primaryItems: List<TvHomeNavItem>,
|
||||||
|
secondaryItems: List<TvHomeNavItem>,
|
||||||
|
onLibrarySelected: (TvHomeNavItem) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
primaryItems.forEach { item ->
|
||||||
|
TvHomeDrawerNavItem(item = item, onLibrarySelected = onLibrarySelected)
|
||||||
|
}
|
||||||
|
if (secondaryItems.isNotEmpty()) {
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
secondaryItems.forEach { item ->
|
||||||
|
TvHomeDrawerNavItem(item = item, onLibrarySelected = onLibrarySelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvHomeDrawerNavItem(
|
||||||
|
item: TvHomeNavItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onLibrarySelected: (TvHomeNavItem) -> Unit
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val background = if (item.selected) scheme.primary.copy(alpha = 0.12f) else Color.Transparent
|
||||||
|
val tint = if (item.selected) scheme.primary else scheme.onSurfaceVariant
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
.background(background, RoundedCornerShape(12.dp))
|
||||||
|
.clickable { onLibrarySelected(item) }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = item.icon,
|
||||||
|
contentDescription = item.label,
|
||||||
|
tint = tint
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = item.label,
|
||||||
|
color = if (item.selected) scheme.primary else scheme.onBackground,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(start = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvHomeDrawerFooter(
|
||||||
|
user: TvHomeUser,
|
||||||
|
onLogout: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.background(scheme.surfaceVariant, RoundedCornerShape(12.dp))
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TvHomeAvatar(
|
||||||
|
size = 32.dp,
|
||||||
|
borderWidth = 1.dp,
|
||||||
|
borderColor = scheme.outlineVariant,
|
||||||
|
backgroundColor = scheme.primaryContainer,
|
||||||
|
icon = Icons.Outlined.Person,
|
||||||
|
iconTint = scheme.onBackground
|
||||||
|
)
|
||||||
|
Column(modifier = Modifier.padding(start = 12.dp)
|
||||||
|
.clickable { onLogout() }) {
|
||||||
|
Text(
|
||||||
|
text = user.name,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = user.plan,
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package hu.bbara.purefin.tv.home.ui
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Home
|
||||||
|
import androidx.compose.material.icons.outlined.Movie
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
import androidx.compose.material.icons.outlined.Tv
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
|
||||||
|
object TvHomeMockData {
|
||||||
|
val user = TvHomeUser(name = "Alex User", plan = "Premium Account")
|
||||||
|
|
||||||
|
val primaryNavItems = listOf(
|
||||||
|
TvHomeNavItem(id = UUID.randomUUID(), label = "Home", icon = Icons.Outlined.Home, selected = true),
|
||||||
|
TvHomeNavItem(id = UUID.randomUUID(), label = "Movies", icon = Icons.Outlined.Movie),
|
||||||
|
TvHomeNavItem(id = UUID.randomUUID(), label = "TV Shows", icon = Icons.Outlined.Tv),
|
||||||
|
TvHomeNavItem(id = UUID.randomUUID(), label = "Search", icon = Icons.Outlined.Search)
|
||||||
|
)
|
||||||
|
|
||||||
|
val secondaryNavItems = listOf(
|
||||||
|
TvHomeNavItem(id = UUID.randomUUID(), label = "Settings", icon = Icons.Outlined.Settings)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package hu.bbara.purefin.tv.home.ui
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
|
||||||
|
data class TvHomeNavItem(
|
||||||
|
val id: UUID,
|
||||||
|
val label: String,
|
||||||
|
val icon: ImageVector,
|
||||||
|
val selected: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TvHomeUser(
|
||||||
|
val name: String,
|
||||||
|
val plan: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
package hu.bbara.purefin.tv.home.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
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.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.PlayArrow
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonColors
|
||||||
|
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.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
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 coil3.request.ImageRequest
|
||||||
|
import hu.bbara.purefin.common.ui.PosterCard
|
||||||
|
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
||||||
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
|
import org.jellyfin.sdk.model.UUID
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import kotlin.math.nextUp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvContinueWatchingSection(
|
||||||
|
items: List<ContinueWatchingItem>,
|
||||||
|
onMovieSelected: (UUID) -> Unit,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
if (items.isEmpty()) return
|
||||||
|
TvSectionHeader(
|
||||||
|
title = "Continue Watching",
|
||||||
|
action = null
|
||||||
|
)
|
||||||
|
LazyRow(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(items = items) { item ->
|
||||||
|
TvContinueWatchingCard(
|
||||||
|
item = item,
|
||||||
|
onMovieSelected = onMovieSelected,
|
||||||
|
onEpisodeSelected = onEpisodeSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvContinueWatchingCard(
|
||||||
|
item: ContinueWatchingItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onMovieSelected: (UUID) -> Unit,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
val imageUrl = when (item.type) {
|
||||||
|
BaseItemKind.MOVIE -> item.movie?.heroImageUrl
|
||||||
|
BaseItemKind.EPISODE -> item.episode?.heroImageUrl
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val cardWidth = 280.dp
|
||||||
|
val cardHeight = cardWidth * 9 / 16
|
||||||
|
|
||||||
|
fun openItem(item: ContinueWatchingItem) {
|
||||||
|
when (item.type) {
|
||||||
|
BaseItemKind.MOVIE -> onMovieSelected(item.movie!!.id)
|
||||||
|
BaseItemKind.EPISODE -> {
|
||||||
|
val episode = item.episode!!
|
||||||
|
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageRequest = ImageRequest.Builder(context)
|
||||||
|
.data(imageUrl)
|
||||||
|
.size(with(density) { cardWidth.roundToPx() }, with(density) { cardHeight.roundToPx() })
|
||||||
|
.build()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.width(cardWidth)
|
||||||
|
.wrapContentHeight()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(cardWidth)
|
||||||
|
.aspectRatio(16f / 9f)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.border(1.dp, scheme.outlineVariant.copy(alpha = 0.3f), RoundedCornerShape(16.dp))
|
||||||
|
.background(scheme.surfaceVariant)
|
||||||
|
) {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = imageRequest,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clickable {
|
||||||
|
openItem(item)
|
||||||
|
},
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
MediaProgressBar(
|
||||||
|
progress = item.progress.toFloat().nextUp().div(100),
|
||||||
|
foregroundColor = scheme.onSurface,
|
||||||
|
backgroundColor = scheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.padding(top = 12.dp)) {
|
||||||
|
Text(
|
||||||
|
text = item.primaryText,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = item.secondaryText,
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvNextUpSection(
|
||||||
|
items: List<NextUpItem>,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
if (items.isEmpty()) return
|
||||||
|
TvSectionHeader(
|
||||||
|
title = "Next Up",
|
||||||
|
action = null
|
||||||
|
)
|
||||||
|
LazyRow(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = items, key = { it.id }) { item ->
|
||||||
|
TvNextUpCard(
|
||||||
|
item = item,
|
||||||
|
onEpisodeSelected = onEpisodeSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvNextUpCard(
|
||||||
|
item: NextUpItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
val imageUrl = item.episode.heroImageUrl
|
||||||
|
|
||||||
|
val cardWidth = 280.dp
|
||||||
|
val cardHeight = cardWidth * 9 / 16
|
||||||
|
|
||||||
|
fun openItem(item: NextUpItem) {
|
||||||
|
val episode = item.episode
|
||||||
|
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageRequest = ImageRequest.Builder(context)
|
||||||
|
.data(imageUrl)
|
||||||
|
.size(with(density) { cardWidth.roundToPx() }, with(density) { cardHeight.roundToPx() })
|
||||||
|
.build()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.width(cardWidth)
|
||||||
|
.wrapContentHeight()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(cardWidth)
|
||||||
|
.aspectRatio(16f / 9f)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.border(1.dp, scheme.outlineVariant.copy(alpha = 0.3f), RoundedCornerShape(16.dp))
|
||||||
|
.background(scheme.surfaceVariant)
|
||||||
|
) {
|
||||||
|
PurefinAsyncImage(
|
||||||
|
model = imageRequest,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clickable {
|
||||||
|
openItem(item)
|
||||||
|
},
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(end = 8.dp, bottom = 16.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(scheme.secondary)
|
||||||
|
.size(36.dp),
|
||||||
|
onClick = {
|
||||||
|
openItem(item)
|
||||||
|
},
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
|
containerColor = scheme.secondary,
|
||||||
|
contentColor = scheme.onSecondary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.PlayArrow,
|
||||||
|
contentDescription = "Play",
|
||||||
|
modifier = Modifier.size(28.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.padding(top = 12.dp)) {
|
||||||
|
Text(
|
||||||
|
text = item.primaryText,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = item.secondaryText,
|
||||||
|
color = scheme.onSurfaceVariant,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvLibraryPosterSection(
|
||||||
|
title: String,
|
||||||
|
items: List<PosterItem>,
|
||||||
|
action: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onMovieSelected: (UUID) -> Unit,
|
||||||
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
) {
|
||||||
|
TvSectionHeader(
|
||||||
|
title = title,
|
||||||
|
action = action
|
||||||
|
)
|
||||||
|
LazyRow(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = items, key = { it.id }) { item ->
|
||||||
|
PosterCard(
|
||||||
|
item = item,
|
||||||
|
onMovieSelected = onMovieSelected,
|
||||||
|
onSeriesSelected = onSeriesSelected,
|
||||||
|
onEpisodeSelected = onEpisodeSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvSectionHeader(
|
||||||
|
title: String,
|
||||||
|
action: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onActionClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
if (action != null) {
|
||||||
|
Text(
|
||||||
|
text = action,
|
||||||
|
color = scheme.primary,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
modifier = Modifier.clickable { onActionClick() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package hu.bbara.purefin.tv.home.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Cloud
|
||||||
|
import androidx.compose.material.icons.outlined.CloudOff
|
||||||
|
import androidx.compose.material.icons.outlined.Menu
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import hu.bbara.purefin.common.ui.components.PurefinIconButton
|
||||||
|
import hu.bbara.purefin.common.ui.components.SearchField
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TvHomeTopBar(
|
||||||
|
onMenuClick: () -> Unit,
|
||||||
|
isOfflineMode: Boolean,
|
||||||
|
onToggleOfflineMode: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(scheme.background.copy(alpha = 0.95f))
|
||||||
|
.zIndex(1f)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.statusBarsPadding()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
|
||||||
|
) {
|
||||||
|
PurefinIconButton(
|
||||||
|
icon = Icons.Outlined.Menu,
|
||||||
|
contentDescription = "Menu",
|
||||||
|
onClick = onMenuClick,
|
||||||
|
)
|
||||||
|
SearchField(
|
||||||
|
value = "",
|
||||||
|
onValueChange = {},
|
||||||
|
placeholder = "Search",
|
||||||
|
backgroundColor = scheme.secondaryContainer,
|
||||||
|
textColor = scheme.onSecondaryContainer,
|
||||||
|
cursorColor = scheme.onSecondaryContainer,
|
||||||
|
modifier = Modifier.weight(1.0f, true),
|
||||||
|
)
|
||||||
|
PurefinIconButton(
|
||||||
|
icon = if (isOfflineMode) Icons.Outlined.CloudOff else Icons.Outlined.Cloud,
|
||||||
|
contentDescription = if (isOfflineMode) "Switch to Online" else "Switch to Offline",
|
||||||
|
onClick = onToggleOfflineMode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package hu.bbara.purefin.tv.navigation
|
||||||
|
|
||||||
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.components.ActivityRetainedComponent
|
||||||
|
import dagger.multibindings.IntoSet
|
||||||
|
import hu.bbara.purefin.core.data.navigation.Route
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(ActivityRetainedComponent::class)
|
||||||
|
object TvNavigationModule {
|
||||||
|
|
||||||
|
@IntoSet
|
||||||
|
@Provides
|
||||||
|
fun provideTvHomeEntryBuilder(): EntryProviderScope<Route>.() -> Unit = {
|
||||||
|
tvHomeSection()
|
||||||
|
}
|
||||||
|
|
||||||
|
@IntoSet
|
||||||
|
@Provides
|
||||||
|
fun provideTvLoginEntryBuilder(): EntryProviderScope<Route>.() -> Unit = {
|
||||||
|
tvLoginSection()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package hu.bbara.purefin.tv.navigation
|
||||||
|
|
||||||
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
|
import hu.bbara.purefin.core.data.navigation.Route
|
||||||
|
import hu.bbara.purefin.login.ui.LoginScreen
|
||||||
|
import hu.bbara.purefin.tv.home.TvHomePage
|
||||||
|
|
||||||
|
fun EntryProviderScope<Route>.tvHomeSection() {
|
||||||
|
entry<Route.Home> {
|
||||||
|
TvHomePage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EntryProviderScope<Route>.tvLoginSection() {
|
||||||
|
entry<Route.LoginRoute> {
|
||||||
|
LoginScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
218
app-tv/src/main/java/hu/bbara/purefin/ui/theme/Color.kt
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
package hu.bbara.purefin.ui.theme
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val primaryLight = Color(0xFF8A5021)
|
||||||
|
val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
|
val primaryContainerLight = Color(0xFFFFDCC5)
|
||||||
|
val onPrimaryContainerLight = Color(0xFF6D390B)
|
||||||
|
val secondaryLight = Color(0xFF755945)
|
||||||
|
val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
|
val secondaryContainerLight = Color(0xFFFFDCC5)
|
||||||
|
val onSecondaryContainerLight = Color(0xFF5B412F)
|
||||||
|
val tertiaryLight = Color(0xFF5E6135)
|
||||||
|
val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
|
val tertiaryContainerLight = Color(0xFFE4E6AE)
|
||||||
|
val onTertiaryContainerLight = Color(0xFF464920)
|
||||||
|
val errorLight = Color(0xFFBA1A1A)
|
||||||
|
val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
val backgroundLight = Color(0xFFFFF8F5)
|
||||||
|
val onBackgroundLight = Color(0xFF221A15)
|
||||||
|
val surfaceLight = Color(0xFFFFF8F5)
|
||||||
|
val onSurfaceLight = Color(0xFF221A15)
|
||||||
|
val surfaceVariantLight = Color(0xFFF3DFD2)
|
||||||
|
val onSurfaceVariantLight = Color(0xFF52443B)
|
||||||
|
val outlineLight = Color(0xFF84746A)
|
||||||
|
val outlineVariantLight = Color(0xFFD6C3B7)
|
||||||
|
val scrimLight = Color(0xFF000000)
|
||||||
|
val inverseSurfaceLight = Color(0xFF382F29)
|
||||||
|
val inverseOnSurfaceLight = Color(0xFFFEEEE4)
|
||||||
|
val inversePrimaryLight = Color(0xFFFFB783)
|
||||||
|
val surfaceDimLight = Color(0xFFE7D7CE)
|
||||||
|
val surfaceBrightLight = Color(0xFFFFF8F5)
|
||||||
|
val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
val surfaceContainerLowLight = Color(0xFFFFF1E9)
|
||||||
|
val surfaceContainerLight = Color(0xFFFBEBE1)
|
||||||
|
val surfaceContainerHighLight = Color(0xFFF5E5DC)
|
||||||
|
val surfaceContainerHighestLight = Color(0xFFF0DFD6)
|
||||||
|
|
||||||
|
val primaryLightMediumContrast = Color(0xFF572A00)
|
||||||
|
val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||||
|
val primaryContainerLightMediumContrast = Color(0xFF9B5E2E)
|
||||||
|
val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||||
|
val secondaryLightMediumContrast = Color(0xFF493120)
|
||||||
|
val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||||
|
val secondaryContainerLightMediumContrast = Color(0xFF856753)
|
||||||
|
val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||||
|
val tertiaryLightMediumContrast = Color(0xFF363911)
|
||||||
|
val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||||
|
val tertiaryContainerLightMediumContrast = Color(0xFF6D7042)
|
||||||
|
val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||||
|
val errorLightMediumContrast = Color(0xFF740006)
|
||||||
|
val onErrorLightMediumContrast = Color(0xFFFFFFFF)
|
||||||
|
val errorContainerLightMediumContrast = Color(0xFFCF2C27)
|
||||||
|
val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||||
|
val backgroundLightMediumContrast = Color(0xFFFFF8F5)
|
||||||
|
val onBackgroundLightMediumContrast = Color(0xFF221A15)
|
||||||
|
val surfaceLightMediumContrast = Color(0xFFFFF8F5)
|
||||||
|
val onSurfaceLightMediumContrast = Color(0xFF17100B)
|
||||||
|
val surfaceVariantLightMediumContrast = Color(0xFFF3DFD2)
|
||||||
|
val onSurfaceVariantLightMediumContrast = Color(0xFF40342B)
|
||||||
|
val outlineLightMediumContrast = Color(0xFF5E5046)
|
||||||
|
val outlineVariantLightMediumContrast = Color(0xFF7A6A60)
|
||||||
|
val scrimLightMediumContrast = Color(0xFF000000)
|
||||||
|
val inverseSurfaceLightMediumContrast = Color(0xFF382F29)
|
||||||
|
val inverseOnSurfaceLightMediumContrast = Color(0xFFFEEEE4)
|
||||||
|
val inversePrimaryLightMediumContrast = Color(0xFFFFB783)
|
||||||
|
val surfaceDimLightMediumContrast = Color(0xFFD3C4BB)
|
||||||
|
val surfaceBrightLightMediumContrast = Color(0xFFFFF8F5)
|
||||||
|
val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
|
||||||
|
val surfaceContainerLowLightMediumContrast = Color(0xFFFFF1E9)
|
||||||
|
val surfaceContainerLightMediumContrast = Color(0xFFF5E5DC)
|
||||||
|
val surfaceContainerHighLightMediumContrast = Color(0xFFEADAD1)
|
||||||
|
val surfaceContainerHighestLightMediumContrast = Color(0xFFDECFC6)
|
||||||
|
|
||||||
|
val primaryLightHighContrast = Color(0xFF492200)
|
||||||
|
val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val primaryContainerLightHighContrast = Color(0xFF703C0D)
|
||||||
|
val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val secondaryLightHighContrast = Color(0xFF3D2717)
|
||||||
|
val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val secondaryContainerLightHighContrast = Color(0xFF5D4431)
|
||||||
|
val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val tertiaryLightHighContrast = Color(0xFF2C2E07)
|
||||||
|
val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val tertiaryContainerLightHighContrast = Color(0xFF494C22)
|
||||||
|
val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val errorLightHighContrast = Color(0xFF600004)
|
||||||
|
val onErrorLightHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val errorContainerLightHighContrast = Color(0xFF98000A)
|
||||||
|
val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val backgroundLightHighContrast = Color(0xFFFFF8F5)
|
||||||
|
val onBackgroundLightHighContrast = Color(0xFF221A15)
|
||||||
|
val surfaceLightHighContrast = Color(0xFFFFF8F5)
|
||||||
|
val onSurfaceLightHighContrast = Color(0xFF000000)
|
||||||
|
val surfaceVariantLightHighContrast = Color(0xFFF3DFD2)
|
||||||
|
val onSurfaceVariantLightHighContrast = Color(0xFF000000)
|
||||||
|
val outlineLightHighContrast = Color(0xFF362A22)
|
||||||
|
val outlineVariantLightHighContrast = Color(0xFF54463E)
|
||||||
|
val scrimLightHighContrast = Color(0xFF000000)
|
||||||
|
val inverseSurfaceLightHighContrast = Color(0xFF382F29)
|
||||||
|
val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val inversePrimaryLightHighContrast = Color(0xFFFFB783)
|
||||||
|
val surfaceDimLightHighContrast = Color(0xFFC5B6AD)
|
||||||
|
val surfaceBrightLightHighContrast = Color(0xFFFFF8F5)
|
||||||
|
val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val surfaceContainerLowLightHighContrast = Color(0xFFFEEEE4)
|
||||||
|
val surfaceContainerLightHighContrast = Color(0xFFF0DFD6)
|
||||||
|
val surfaceContainerHighLightHighContrast = Color(0xFFE1D1C8)
|
||||||
|
val surfaceContainerHighestLightHighContrast = Color(0xFFD3C4BB)
|
||||||
|
|
||||||
|
val primaryDark = Color(0xFFFFB783)
|
||||||
|
val onPrimaryDark = Color(0xFF4F2500)
|
||||||
|
val primaryContainerDark = Color(0xFF6D390B)
|
||||||
|
val onPrimaryContainerDark = Color(0xFFFFDCC5)
|
||||||
|
val secondaryDark = Color(0xFFE4BFA7)
|
||||||
|
val onSecondaryDark = Color(0xFF422B1B)
|
||||||
|
val secondaryContainerDark = Color(0xFF5B412F)
|
||||||
|
val onSecondaryContainerDark = Color(0xFFFFDCC5)
|
||||||
|
val tertiaryDark = Color(0xFFC7CA94)
|
||||||
|
val onTertiaryDark = Color(0xFF30330B)
|
||||||
|
val tertiaryContainerDark = Color(0xFF464920)
|
||||||
|
val onTertiaryContainerDark = Color(0xFFE4E6AE)
|
||||||
|
val errorDark = Color(0xFFFFB4AB)
|
||||||
|
val onErrorDark = Color(0xFF690005)
|
||||||
|
val errorContainerDark = Color(0xFF93000A)
|
||||||
|
val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
val backgroundDark = Color(0xFF19120D)
|
||||||
|
val onBackgroundDark = Color(0xFFF0DFD6)
|
||||||
|
val surfaceDark = Color(0xFF19120D)
|
||||||
|
val onSurfaceDark = Color(0xFFF0DFD6)
|
||||||
|
val surfaceVariantDark = Color(0xFF52443B)
|
||||||
|
val onSurfaceVariantDark = Color(0xFFD6C3B7)
|
||||||
|
val outlineDark = Color(0xFF9F8D83)
|
||||||
|
val outlineVariantDark = Color(0xFF52443B)
|
||||||
|
val scrimDark = Color(0xFF000000)
|
||||||
|
val inverseSurfaceDark = Color(0xFFF0DFD6)
|
||||||
|
val inverseOnSurfaceDark = Color(0xFF382F29)
|
||||||
|
val inversePrimaryDark = Color(0xFF8A5021)
|
||||||
|
val surfaceDimDark = Color(0xFF19120D)
|
||||||
|
val surfaceBrightDark = Color(0xFF413731)
|
||||||
|
val surfaceContainerLowestDark = Color(0xFF140D08)
|
||||||
|
val surfaceContainerLowDark = Color(0xFF221A15)
|
||||||
|
val surfaceContainerDark = Color(0xFF261E18)
|
||||||
|
val surfaceContainerHighDark = Color(0xFF312822)
|
||||||
|
val surfaceContainerHighestDark = Color(0xFF3C332D)
|
||||||
|
|
||||||
|
val primaryDarkMediumContrast = Color(0xFFFFD4B7)
|
||||||
|
val onPrimaryDarkMediumContrast = Color(0xFF3F1C00)
|
||||||
|
val primaryContainerDarkMediumContrast = Color(0xFFC5814E)
|
||||||
|
val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||||
|
val secondaryDarkMediumContrast = Color(0xFFFBD5BC)
|
||||||
|
val onSecondaryDarkMediumContrast = Color(0xFF362111)
|
||||||
|
val secondaryContainerDarkMediumContrast = Color(0xFFAB8A74)
|
||||||
|
val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||||
|
val tertiaryDarkMediumContrast = Color(0xFFDDE0A9)
|
||||||
|
val onTertiaryDarkMediumContrast = Color(0xFF252803)
|
||||||
|
val tertiaryContainerDarkMediumContrast = Color(0xFF919463)
|
||||||
|
val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||||
|
val errorDarkMediumContrast = Color(0xFFFFD2CC)
|
||||||
|
val onErrorDarkMediumContrast = Color(0xFF540003)
|
||||||
|
val errorContainerDarkMediumContrast = Color(0xFFFF5449)
|
||||||
|
val onErrorContainerDarkMediumContrast = Color(0xFF000000)
|
||||||
|
val backgroundDarkMediumContrast = Color(0xFF19120D)
|
||||||
|
val onBackgroundDarkMediumContrast = Color(0xFFF0DFD6)
|
||||||
|
val surfaceDarkMediumContrast = Color(0xFF19120D)
|
||||||
|
val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF)
|
||||||
|
val surfaceVariantDarkMediumContrast = Color(0xFF52443B)
|
||||||
|
val onSurfaceVariantDarkMediumContrast = Color(0xFFEDD8CC)
|
||||||
|
val outlineDarkMediumContrast = Color(0xFFC1AEA3)
|
||||||
|
val outlineVariantDarkMediumContrast = Color(0xFF9E8D82)
|
||||||
|
val scrimDarkMediumContrast = Color(0xFF000000)
|
||||||
|
val inverseSurfaceDarkMediumContrast = Color(0xFFF0DFD6)
|
||||||
|
val inverseOnSurfaceDarkMediumContrast = Color(0xFF312822)
|
||||||
|
val inversePrimaryDarkMediumContrast = Color(0xFF6F3A0C)
|
||||||
|
val surfaceDimDarkMediumContrast = Color(0xFF19120D)
|
||||||
|
val surfaceBrightDarkMediumContrast = Color(0xFF4D423C)
|
||||||
|
val surfaceContainerLowestDarkMediumContrast = Color(0xFF0C0603)
|
||||||
|
val surfaceContainerLowDarkMediumContrast = Color(0xFF241C16)
|
||||||
|
val surfaceContainerDarkMediumContrast = Color(0xFF2F2620)
|
||||||
|
val surfaceContainerHighDarkMediumContrast = Color(0xFF3A312B)
|
||||||
|
val surfaceContainerHighestDarkMediumContrast = Color(0xFF463C35)
|
||||||
|
|
||||||
|
val primaryDarkHighContrast = Color(0xFFFFECE2)
|
||||||
|
val onPrimaryDarkHighContrast = Color(0xFF000000)
|
||||||
|
val primaryContainerDarkHighContrast = Color(0xFFFEB27A)
|
||||||
|
val onPrimaryContainerDarkHighContrast = Color(0xFF170700)
|
||||||
|
val secondaryDarkHighContrast = Color(0xFFFFECE2)
|
||||||
|
val onSecondaryDarkHighContrast = Color(0xFF000000)
|
||||||
|
val secondaryContainerDarkHighContrast = Color(0xFFE0BBA3)
|
||||||
|
val onSecondaryContainerDarkHighContrast = Color(0xFF170700)
|
||||||
|
val tertiaryDarkHighContrast = Color(0xFFF1F4BB)
|
||||||
|
val onTertiaryDarkHighContrast = Color(0xFF000000)
|
||||||
|
val tertiaryContainerDarkHighContrast = Color(0xFFC3C691)
|
||||||
|
val onTertiaryContainerDarkHighContrast = Color(0xFF0B0C00)
|
||||||
|
val errorDarkHighContrast = Color(0xFFFFECE9)
|
||||||
|
val onErrorDarkHighContrast = Color(0xFF000000)
|
||||||
|
val errorContainerDarkHighContrast = Color(0xFFFFAEA4)
|
||||||
|
val onErrorContainerDarkHighContrast = Color(0xFF220001)
|
||||||
|
val backgroundDarkHighContrast = Color(0xFF19120D)
|
||||||
|
val onBackgroundDarkHighContrast = Color(0xFFF0DFD6)
|
||||||
|
val surfaceDarkHighContrast = Color(0xFF19120D)
|
||||||
|
val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val surfaceVariantDarkHighContrast = Color(0xFF52443B)
|
||||||
|
val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF)
|
||||||
|
val outlineDarkHighContrast = Color(0xFFFFECE2)
|
||||||
|
val outlineVariantDarkHighContrast = Color(0xFFD2BFB3)
|
||||||
|
val scrimDarkHighContrast = Color(0xFF000000)
|
||||||
|
val inverseSurfaceDarkHighContrast = Color(0xFFF0DFD6)
|
||||||
|
val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
|
||||||
|
val inversePrimaryDarkHighContrast = Color(0xFF6F3A0C)
|
||||||
|
val surfaceDimDarkHighContrast = Color(0xFF19120D)
|
||||||
|
val surfaceBrightDarkHighContrast = Color(0xFF594E47)
|
||||||
|
val surfaceContainerLowestDarkHighContrast = Color(0xFF000000)
|
||||||
|
val surfaceContainerLowDarkHighContrast = Color(0xFF261E18)
|
||||||
|
val surfaceContainerDarkHighContrast = Color(0xFF382F29)
|
||||||
|
val surfaceContainerHighDarkHighContrast = Color(0xFF433933)
|
||||||
|
val surfaceContainerHighestDarkHighContrast = Color(0xFF4F453E)
|
||||||
275
app-tv/src/main/java/hu/bbara/purefin/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
package hu.bbara.purefin.ui.theme
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
private val lightScheme = lightColorScheme(
|
||||||
|
primary = primaryLight,
|
||||||
|
onPrimary = onPrimaryLight,
|
||||||
|
primaryContainer = primaryContainerLight,
|
||||||
|
onPrimaryContainer = onPrimaryContainerLight,
|
||||||
|
secondary = secondaryLight,
|
||||||
|
onSecondary = onSecondaryLight,
|
||||||
|
secondaryContainer = secondaryContainerLight,
|
||||||
|
onSecondaryContainer = onSecondaryContainerLight,
|
||||||
|
tertiary = tertiaryLight,
|
||||||
|
onTertiary = onTertiaryLight,
|
||||||
|
tertiaryContainer = tertiaryContainerLight,
|
||||||
|
onTertiaryContainer = onTertiaryContainerLight,
|
||||||
|
error = errorLight,
|
||||||
|
onError = onErrorLight,
|
||||||
|
errorContainer = errorContainerLight,
|
||||||
|
onErrorContainer = onErrorContainerLight,
|
||||||
|
background = backgroundLight,
|
||||||
|
onBackground = onBackgroundLight,
|
||||||
|
surface = surfaceLight,
|
||||||
|
onSurface = onSurfaceLight,
|
||||||
|
surfaceVariant = surfaceVariantLight,
|
||||||
|
onSurfaceVariant = onSurfaceVariantLight,
|
||||||
|
outline = outlineLight,
|
||||||
|
outlineVariant = outlineVariantLight,
|
||||||
|
scrim = scrimLight,
|
||||||
|
inverseSurface = inverseSurfaceLight,
|
||||||
|
inverseOnSurface = inverseOnSurfaceLight,
|
||||||
|
inversePrimary = inversePrimaryLight,
|
||||||
|
surfaceDim = surfaceDimLight,
|
||||||
|
surfaceBright = surfaceBrightLight,
|
||||||
|
surfaceContainerLowest = surfaceContainerLowestLight,
|
||||||
|
surfaceContainerLow = surfaceContainerLowLight,
|
||||||
|
surfaceContainer = surfaceContainerLight,
|
||||||
|
surfaceContainerHigh = surfaceContainerHighLight,
|
||||||
|
surfaceContainerHighest = surfaceContainerHighestLight,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val darkScheme = darkColorScheme(
|
||||||
|
primary = primaryDark,
|
||||||
|
onPrimary = onPrimaryDark,
|
||||||
|
primaryContainer = primaryContainerDark,
|
||||||
|
onPrimaryContainer = onPrimaryContainerDark,
|
||||||
|
secondary = secondaryDark,
|
||||||
|
onSecondary = onSecondaryDark,
|
||||||
|
secondaryContainer = secondaryContainerDark,
|
||||||
|
onSecondaryContainer = onSecondaryContainerDark,
|
||||||
|
tertiary = tertiaryDark,
|
||||||
|
onTertiary = onTertiaryDark,
|
||||||
|
tertiaryContainer = tertiaryContainerDark,
|
||||||
|
onTertiaryContainer = onTertiaryContainerDark,
|
||||||
|
error = errorDark,
|
||||||
|
onError = onErrorDark,
|
||||||
|
errorContainer = errorContainerDark,
|
||||||
|
onErrorContainer = onErrorContainerDark,
|
||||||
|
background = backgroundDark,
|
||||||
|
onBackground = onBackgroundDark,
|
||||||
|
surface = surfaceDark,
|
||||||
|
onSurface = onSurfaceDark,
|
||||||
|
surfaceVariant = surfaceVariantDark,
|
||||||
|
onSurfaceVariant = onSurfaceVariantDark,
|
||||||
|
outline = outlineDark,
|
||||||
|
outlineVariant = outlineVariantDark,
|
||||||
|
scrim = scrimDark,
|
||||||
|
inverseSurface = inverseSurfaceDark,
|
||||||
|
inverseOnSurface = inverseOnSurfaceDark,
|
||||||
|
inversePrimary = inversePrimaryDark,
|
||||||
|
surfaceDim = surfaceDimDark,
|
||||||
|
surfaceBright = surfaceBrightDark,
|
||||||
|
surfaceContainerLowest = surfaceContainerLowestDark,
|
||||||
|
surfaceContainerLow = surfaceContainerLowDark,
|
||||||
|
surfaceContainer = surfaceContainerDark,
|
||||||
|
surfaceContainerHigh = surfaceContainerHighDark,
|
||||||
|
surfaceContainerHighest = surfaceContainerHighestDark,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val mediumContrastLightColorScheme = lightColorScheme(
|
||||||
|
primary = primaryLightMediumContrast,
|
||||||
|
onPrimary = onPrimaryLightMediumContrast,
|
||||||
|
primaryContainer = primaryContainerLightMediumContrast,
|
||||||
|
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
|
||||||
|
secondary = secondaryLightMediumContrast,
|
||||||
|
onSecondary = onSecondaryLightMediumContrast,
|
||||||
|
secondaryContainer = secondaryContainerLightMediumContrast,
|
||||||
|
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
|
||||||
|
tertiary = tertiaryLightMediumContrast,
|
||||||
|
onTertiary = onTertiaryLightMediumContrast,
|
||||||
|
tertiaryContainer = tertiaryContainerLightMediumContrast,
|
||||||
|
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
|
||||||
|
error = errorLightMediumContrast,
|
||||||
|
onError = onErrorLightMediumContrast,
|
||||||
|
errorContainer = errorContainerLightMediumContrast,
|
||||||
|
onErrorContainer = onErrorContainerLightMediumContrast,
|
||||||
|
background = backgroundLightMediumContrast,
|
||||||
|
onBackground = onBackgroundLightMediumContrast,
|
||||||
|
surface = surfaceLightMediumContrast,
|
||||||
|
onSurface = onSurfaceLightMediumContrast,
|
||||||
|
surfaceVariant = surfaceVariantLightMediumContrast,
|
||||||
|
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
|
||||||
|
outline = outlineLightMediumContrast,
|
||||||
|
outlineVariant = outlineVariantLightMediumContrast,
|
||||||
|
scrim = scrimLightMediumContrast,
|
||||||
|
inverseSurface = inverseSurfaceLightMediumContrast,
|
||||||
|
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
|
||||||
|
inversePrimary = inversePrimaryLightMediumContrast,
|
||||||
|
surfaceDim = surfaceDimLightMediumContrast,
|
||||||
|
surfaceBright = surfaceBrightLightMediumContrast,
|
||||||
|
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
|
||||||
|
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
|
||||||
|
surfaceContainer = surfaceContainerLightMediumContrast,
|
||||||
|
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
|
||||||
|
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val highContrastLightColorScheme = lightColorScheme(
|
||||||
|
primary = primaryLightHighContrast,
|
||||||
|
onPrimary = onPrimaryLightHighContrast,
|
||||||
|
primaryContainer = primaryContainerLightHighContrast,
|
||||||
|
onPrimaryContainer = onPrimaryContainerLightHighContrast,
|
||||||
|
secondary = secondaryLightHighContrast,
|
||||||
|
onSecondary = onSecondaryLightHighContrast,
|
||||||
|
secondaryContainer = secondaryContainerLightHighContrast,
|
||||||
|
onSecondaryContainer = onSecondaryContainerLightHighContrast,
|
||||||
|
tertiary = tertiaryLightHighContrast,
|
||||||
|
onTertiary = onTertiaryLightHighContrast,
|
||||||
|
tertiaryContainer = tertiaryContainerLightHighContrast,
|
||||||
|
onTertiaryContainer = onTertiaryContainerLightHighContrast,
|
||||||
|
error = errorLightHighContrast,
|
||||||
|
onError = onErrorLightHighContrast,
|
||||||
|
errorContainer = errorContainerLightHighContrast,
|
||||||
|
onErrorContainer = onErrorContainerLightHighContrast,
|
||||||
|
background = backgroundLightHighContrast,
|
||||||
|
onBackground = onBackgroundLightHighContrast,
|
||||||
|
surface = surfaceLightHighContrast,
|
||||||
|
onSurface = onSurfaceLightHighContrast,
|
||||||
|
surfaceVariant = surfaceVariantLightHighContrast,
|
||||||
|
onSurfaceVariant = onSurfaceVariantLightHighContrast,
|
||||||
|
outline = outlineLightHighContrast,
|
||||||
|
outlineVariant = outlineVariantLightHighContrast,
|
||||||
|
scrim = scrimLightHighContrast,
|
||||||
|
inverseSurface = inverseSurfaceLightHighContrast,
|
||||||
|
inverseOnSurface = inverseOnSurfaceLightHighContrast,
|
||||||
|
inversePrimary = inversePrimaryLightHighContrast,
|
||||||
|
surfaceDim = surfaceDimLightHighContrast,
|
||||||
|
surfaceBright = surfaceBrightLightHighContrast,
|
||||||
|
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
|
||||||
|
surfaceContainerLow = surfaceContainerLowLightHighContrast,
|
||||||
|
surfaceContainer = surfaceContainerLightHighContrast,
|
||||||
|
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
|
||||||
|
surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val mediumContrastDarkColorScheme = darkColorScheme(
|
||||||
|
primary = primaryDarkMediumContrast,
|
||||||
|
onPrimary = onPrimaryDarkMediumContrast,
|
||||||
|
primaryContainer = primaryContainerDarkMediumContrast,
|
||||||
|
onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
|
||||||
|
secondary = secondaryDarkMediumContrast,
|
||||||
|
onSecondary = onSecondaryDarkMediumContrast,
|
||||||
|
secondaryContainer = secondaryContainerDarkMediumContrast,
|
||||||
|
onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
|
||||||
|
tertiary = tertiaryDarkMediumContrast,
|
||||||
|
onTertiary = onTertiaryDarkMediumContrast,
|
||||||
|
tertiaryContainer = tertiaryContainerDarkMediumContrast,
|
||||||
|
onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
|
||||||
|
error = errorDarkMediumContrast,
|
||||||
|
onError = onErrorDarkMediumContrast,
|
||||||
|
errorContainer = errorContainerDarkMediumContrast,
|
||||||
|
onErrorContainer = onErrorContainerDarkMediumContrast,
|
||||||
|
background = backgroundDarkMediumContrast,
|
||||||
|
onBackground = onBackgroundDarkMediumContrast,
|
||||||
|
surface = surfaceDarkMediumContrast,
|
||||||
|
onSurface = onSurfaceDarkMediumContrast,
|
||||||
|
surfaceVariant = surfaceVariantDarkMediumContrast,
|
||||||
|
onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
|
||||||
|
outline = outlineDarkMediumContrast,
|
||||||
|
outlineVariant = outlineVariantDarkMediumContrast,
|
||||||
|
scrim = scrimDarkMediumContrast,
|
||||||
|
inverseSurface = inverseSurfaceDarkMediumContrast,
|
||||||
|
inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
|
||||||
|
inversePrimary = inversePrimaryDarkMediumContrast,
|
||||||
|
surfaceDim = surfaceDimDarkMediumContrast,
|
||||||
|
surfaceBright = surfaceBrightDarkMediumContrast,
|
||||||
|
surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
|
||||||
|
surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
|
||||||
|
surfaceContainer = surfaceContainerDarkMediumContrast,
|
||||||
|
surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
|
||||||
|
surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val highContrastDarkColorScheme = darkColorScheme(
|
||||||
|
primary = primaryDarkHighContrast,
|
||||||
|
onPrimary = onPrimaryDarkHighContrast,
|
||||||
|
primaryContainer = primaryContainerDarkHighContrast,
|
||||||
|
onPrimaryContainer = onPrimaryContainerDarkHighContrast,
|
||||||
|
secondary = secondaryDarkHighContrast,
|
||||||
|
onSecondary = onSecondaryDarkHighContrast,
|
||||||
|
secondaryContainer = secondaryContainerDarkHighContrast,
|
||||||
|
onSecondaryContainer = onSecondaryContainerDarkHighContrast,
|
||||||
|
tertiary = tertiaryDarkHighContrast,
|
||||||
|
onTertiary = onTertiaryDarkHighContrast,
|
||||||
|
tertiaryContainer = tertiaryContainerDarkHighContrast,
|
||||||
|
onTertiaryContainer = onTertiaryContainerDarkHighContrast,
|
||||||
|
error = errorDarkHighContrast,
|
||||||
|
onError = onErrorDarkHighContrast,
|
||||||
|
errorContainer = errorContainerDarkHighContrast,
|
||||||
|
onErrorContainer = onErrorContainerDarkHighContrast,
|
||||||
|
background = backgroundDarkHighContrast,
|
||||||
|
onBackground = onBackgroundDarkHighContrast,
|
||||||
|
surface = surfaceDarkHighContrast,
|
||||||
|
onSurface = onSurfaceDarkHighContrast,
|
||||||
|
surfaceVariant = surfaceVariantDarkHighContrast,
|
||||||
|
onSurfaceVariant = onSurfaceVariantDarkHighContrast,
|
||||||
|
outline = outlineDarkHighContrast,
|
||||||
|
outlineVariant = outlineVariantDarkHighContrast,
|
||||||
|
scrim = scrimDarkHighContrast,
|
||||||
|
inverseSurface = inverseSurfaceDarkHighContrast,
|
||||||
|
inverseOnSurface = inverseOnSurfaceDarkHighContrast,
|
||||||
|
inversePrimary = inversePrimaryDarkHighContrast,
|
||||||
|
surfaceDim = surfaceDimDarkHighContrast,
|
||||||
|
surfaceBright = surfaceBrightDarkHighContrast,
|
||||||
|
surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
|
||||||
|
surfaceContainerLow = surfaceContainerLowDarkHighContrast,
|
||||||
|
surfaceContainer = surfaceContainerDarkHighContrast,
|
||||||
|
surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
|
||||||
|
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class ColorFamily(
|
||||||
|
val color: Color,
|
||||||
|
val onColor: Color,
|
||||||
|
val colorContainer: Color,
|
||||||
|
val onColorContainer: Color
|
||||||
|
)
|
||||||
|
|
||||||
|
val unspecified_scheme = ColorFamily(
|
||||||
|
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppTheme(
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
dynamicColor: Boolean = false,
|
||||||
|
content: @Composable() () -> Unit
|
||||||
|
) {
|
||||||
|
val colorScheme = when {
|
||||||
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
darkTheme -> darkScheme
|
||||||
|
else -> lightScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = AppTypography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
5
app-tv/src/main/java/hu/bbara/purefin/ui/theme/Type.kt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package hu.bbara.purefin.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
|
||||||
|
val AppTypography = Typography()
|
||||||
74
app-tv/src/main/res/drawable/purefin_logo_background.xml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector
|
||||||
|
android:height="108dp"
|
||||||
|
android:width="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
</vector>
|
||||||
5
app-tv/src/main/res/mipmap-anydpi-v26/purefin_logo.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/purefin_logo_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/purefin_logo_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/purefin_logo_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/purefin_logo_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
6
app-tv/src/main/res/mipmap-anydpi/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app-tv/src/main/res/mipmap-hdpi/purefin_logo.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app-tv/src/main/res/mipmap-hdpi/purefin_logo_foreground.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
app-tv/src/main/res/mipmap-hdpi/purefin_logo_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app-tv/src/main/res/mipmap-mdpi/purefin_logo.webp
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
app-tv/src/main/res/mipmap-mdpi/purefin_logo_foreground.webp
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
app-tv/src/main/res/mipmap-mdpi/purefin_logo_round.webp
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
app-tv/src/main/res/mipmap-xhdpi/purefin_logo.webp
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
app-tv/src/main/res/mipmap-xhdpi/purefin_logo_foreground.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
app-tv/src/main/res/mipmap-xhdpi/purefin_logo_round.webp
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
app-tv/src/main/res/mipmap-xxhdpi/purefin_logo.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
app-tv/src/main/res/mipmap-xxhdpi/purefin_logo_foreground.webp
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
app-tv/src/main/res/mipmap-xxhdpi/purefin_logo_round.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
app-tv/src/main/res/mipmap-xxxhdpi/purefin_logo.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
app-tv/src/main/res/mipmap-xxxhdpi/purefin_logo_foreground.webp
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
app-tv/src/main/res/mipmap-xxxhdpi/purefin_logo_round.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
3
app-tv/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Purefin</string>
|
||||||
|
</resources>
|
||||||
5
app-tv/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Theme.Purefin" parent="android:Theme.Material.Light.NoActionBar" />
|
||||||
|
</resources>
|
||||||
13
app-tv/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample backup rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/guide/topics/data/autobackup
|
||||||
|
for details.
|
||||||
|
Note: This file is ignored for devices older than API 31
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore
|
||||||
|
-->
|
||||||
|
<full-backup-content>
|
||||||
|
<!--
|
||||||
|
<include domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="device.xml"/>
|
||||||
|
-->
|
||||||
|
</full-backup-content>
|
||||||
19
app-tv/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample data extraction rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||||
|
for details.
|
||||||
|
-->
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
-->
|
||||||
|
</cloud-backup>
|
||||||
|
<!--
|
||||||
|
<device-transfer>
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
</device-transfer>
|
||||||
|
-->
|
||||||
|
</data-extraction-rules>
|
||||||
@@ -21,6 +21,7 @@ dependencyResolutionManagement {
|
|||||||
|
|
||||||
rootProject.name = "Purefin"
|
rootProject.name = "Purefin"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
include(":app-tv")
|
||||||
include(":core:model")
|
include(":core:model")
|
||||||
include(":core:data")
|
include(":core:data")
|
||||||
include(":feature:download")
|
include(":feature:download")
|
||||||
|
|||||||