mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
Compare commits
4 Commits
b67c8076f1
...
18844fcd38
| Author | SHA1 | Date | |
|---|---|---|---|
| 18844fcd38 | |||
| 9591b72273 | |||
| 29747a1031 | |||
| ad930e899f |
@@ -52,9 +52,20 @@ kotlin {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":core:model"))
|
implementation(project(":core:model"))
|
||||||
|
implementation(project(":core:image"))
|
||||||
|
implementation(project(":core:ui"))
|
||||||
implementation(project(":core:data"))
|
implementation(project(":core:data"))
|
||||||
|
implementation(project(":core:navigation"))
|
||||||
|
implementation(project(":core:download"))
|
||||||
implementation(project(":core:player"))
|
implementation(project(":core:player"))
|
||||||
implementation(project(":feature:shared"))
|
implementation(project(":data:jellyfin"))
|
||||||
|
implementation(project(":data:offline"))
|
||||||
|
implementation(project(":data:catalog"))
|
||||||
|
implementation(project(":feature:browse"))
|
||||||
|
implementation(project(":feature:search"))
|
||||||
|
implementation(project(":feature:content"))
|
||||||
|
implementation(project(":feature:downloads"))
|
||||||
|
implementation(project(":feature:login"))
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
@@ -83,8 +94,6 @@ dependencies {
|
|||||||
implementation(libs.media3.datasource.okhttp)
|
implementation(libs.media3.datasource.okhttp)
|
||||||
implementation(libs.androidx.navigation3.runtime)
|
implementation(libs.androidx.navigation3.runtime)
|
||||||
implementation(libs.androidx.navigation3.ui)
|
implementation(libs.androidx.navigation3.ui)
|
||||||
implementation(libs.androidx.room.ktx)
|
|
||||||
ksp(libs.androidx.room.compiler)
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.common.ui.components
|
package hu.bbara.purefin.ui.common.media
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.content.episode
|
package hu.bbara.purefin.ui.screen.episode
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
@@ -16,6 +16,7 @@ import androidx.compose.ui.input.key.Key
|
|||||||
import hu.bbara.purefin.core.model.CastMember
|
import hu.bbara.purefin.core.model.CastMember
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
|
import hu.bbara.purefin.ui.screen.episode.components.EpisodePlayButtonTag
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -17,15 +17,15 @@ import androidx.compose.ui.test.pressKey
|
|||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
|
import hu.bbara.purefin.core.model.LibraryKind
|
||||||
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.tv.home.TvHomeScreen
|
import hu.bbara.purefin.ui.screen.home.TvHomeScreen
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -298,7 +298,7 @@ class TvHomeContentTest {
|
|||||||
private fun sampleContinueWatching(): List<ContinueWatchingItem> {
|
private fun sampleContinueWatching(): List<ContinueWatchingItem> {
|
||||||
return listOf(
|
return listOf(
|
||||||
ContinueWatchingItem(
|
ContinueWatchingItem(
|
||||||
type = BaseItemKind.MOVIE,
|
type = MediaKind.MOVIE,
|
||||||
movie = sampleMovie(
|
movie = sampleMovie(
|
||||||
id = "11111111-1111-1111-1111-111111111111",
|
id = "11111111-1111-1111-1111-111111111111",
|
||||||
title = "Blade Runner 2049",
|
title = "Blade Runner 2049",
|
||||||
@@ -311,7 +311,7 @@ class TvHomeContentTest {
|
|||||||
private fun sampleContinueWatchingRow(): List<ContinueWatchingItem> {
|
private fun sampleContinueWatchingRow(): List<ContinueWatchingItem> {
|
||||||
return listOf(
|
return listOf(
|
||||||
ContinueWatchingItem(
|
ContinueWatchingItem(
|
||||||
type = BaseItemKind.MOVIE,
|
type = MediaKind.MOVIE,
|
||||||
movie = sampleMovie(
|
movie = sampleMovie(
|
||||||
id = "11111111-1111-1111-1111-111111111111",
|
id = "11111111-1111-1111-1111-111111111111",
|
||||||
title = "Blade Runner 2049",
|
title = "Blade Runner 2049",
|
||||||
@@ -319,7 +319,7 @@ class TvHomeContentTest {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
ContinueWatchingItem(
|
ContinueWatchingItem(
|
||||||
type = BaseItemKind.MOVIE,
|
type = MediaKind.MOVIE,
|
||||||
movie = sampleMovie(
|
movie = sampleMovie(
|
||||||
id = "55555555-5555-5555-5555-555555555555",
|
id = "55555555-5555-5555-5555-555555555555",
|
||||||
title = "Mad Max: Fury Road",
|
title = "Mad Max: Fury Road",
|
||||||
@@ -363,7 +363,7 @@ class TvHomeContentTest {
|
|||||||
LibraryItem(
|
LibraryItem(
|
||||||
id = sampleLibraryId(),
|
id = sampleLibraryId(),
|
||||||
name = "Movies",
|
name = "Movies",
|
||||||
type = CollectionType.MOVIES,
|
type = LibraryKind.MOVIES,
|
||||||
posterUrl = "",
|
posterUrl = "",
|
||||||
isEmpty = false
|
isEmpty = false
|
||||||
)
|
)
|
||||||
@@ -376,7 +376,7 @@ class TvHomeContentTest {
|
|||||||
return mapOf(
|
return mapOf(
|
||||||
libraryId to listOf(
|
libraryId to listOf(
|
||||||
PosterItem(
|
PosterItem(
|
||||||
type = BaseItemKind.MOVIE,
|
type = MediaKind.MOVIE,
|
||||||
movie = sampleMovie(
|
movie = sampleMovie(
|
||||||
id = "44444444-4444-4444-4444-444444444444",
|
id = "44444444-4444-4444-4444-444444444444",
|
||||||
title = "Arrival",
|
title = "Arrival",
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
package hu.bbara.purefin.tv.library.ui
|
package hu.bbara.purefin.ui.screen.libraries
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.core.model.LibraryKind
|
||||||
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -23,14 +23,14 @@ class TvLibrariesOverviewScreenTest {
|
|||||||
LibraryItem(
|
LibraryItem(
|
||||||
id = UUID.fromString("11111111-1111-1111-1111-111111111111"),
|
id = UUID.fromString("11111111-1111-1111-1111-111111111111"),
|
||||||
name = "Movies",
|
name = "Movies",
|
||||||
type = CollectionType.MOVIES,
|
type = LibraryKind.MOVIES,
|
||||||
posterUrl = "",
|
posterUrl = "",
|
||||||
isEmpty = false
|
isEmpty = false
|
||||||
),
|
),
|
||||||
LibraryItem(
|
LibraryItem(
|
||||||
id = UUID.fromString("22222222-2222-2222-2222-222222222222"),
|
id = UUID.fromString("22222222-2222-2222-2222-222222222222"),
|
||||||
name = "Shows",
|
name = "Shows",
|
||||||
type = CollectionType.TVSHOWS,
|
type = LibraryKind.SERIES,
|
||||||
posterUrl = "",
|
posterUrl = "",
|
||||||
isEmpty = false
|
isEmpty = false
|
||||||
)
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.content.movie
|
package hu.bbara.purefin.ui.screen.movie
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
@@ -16,6 +16,7 @@ import androidx.compose.ui.input.key.Key
|
|||||||
import hu.bbara.purefin.core.model.CastMember
|
import hu.bbara.purefin.core.model.CastMember
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
|
import hu.bbara.purefin.ui.screen.movie.components.MoviePlayButtonTag
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.player.components
|
package hu.bbara.purefin.ui.screen.player.components
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
@@ -44,10 +44,10 @@ import hu.bbara.purefin.core.player.model.PlayerUiState
|
|||||||
import hu.bbara.purefin.core.player.model.QueueItemUi
|
import hu.bbara.purefin.core.player.model.QueueItemUi
|
||||||
import hu.bbara.purefin.core.player.model.TrackOption
|
import hu.bbara.purefin.core.player.model.TrackOption
|
||||||
import hu.bbara.purefin.core.player.model.TrackType
|
import hu.bbara.purefin.core.player.model.TrackType
|
||||||
import hu.bbara.purefin.tv.player.TV_HIDDEN_STOP_FEEDBACK_MS
|
import hu.bbara.purefin.ui.screen.player.TV_HIDDEN_STOP_FEEDBACK_MS
|
||||||
import hu.bbara.purefin.tv.player.TvPlayerHiddenStopFeedback
|
import hu.bbara.purefin.ui.screen.player.TvPlayerHiddenStopFeedback
|
||||||
import hu.bbara.purefin.tv.player.TvPlayerHiddenStopFeedbackTag
|
import hu.bbara.purefin.ui.screen.player.TvPlayerHiddenStopFeedbackTag
|
||||||
import hu.bbara.purefin.tv.player.handleTvPlayerRootKeyEvent
|
import hu.bbara.purefin.ui.screen.player.handleTvPlayerRootKeyEvent
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.content.series
|
package hu.bbara.purefin.ui.screen.series
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
@@ -16,6 +16,8 @@ import hu.bbara.purefin.core.model.Episode
|
|||||||
import hu.bbara.purefin.core.model.Season
|
import hu.bbara.purefin.core.model.Season
|
||||||
import hu.bbara.purefin.core.model.Series
|
import hu.bbara.purefin.core.model.Series
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
|
import hu.bbara.purefin.ui.screen.series.components.SeriesFirstSeasonTabTag
|
||||||
|
import hu.bbara.purefin.ui.screen.series.components.SeriesPlayButtonTag
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
android:required="true" />
|
android:required="true" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".TvApplication"
|
android:name="hu.bbara.purefin.tv.TvApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:banner="@drawable/banner"
|
android:banner="@drawable/banner"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Purefin">
|
android:theme="@style/Theme.Purefin">
|
||||||
<activity
|
<activity
|
||||||
android:name=".TvActivity"
|
android:name="hu.bbara.purefin.tv.TvActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.Purefin">
|
android:theme="@style/Theme.Purefin">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
package hu.bbara.purefin.common.ui
|
|
||||||
|
|
||||||
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.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
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.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Person
|
|
||||||
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.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
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.TextUnit
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
|
||||||
import hu.bbara.purefin.core.model.CastMember
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MediaMetaChip(
|
|
||||||
text: String,
|
|
||||||
background: Color = MaterialTheme.colorScheme.surfaceVariant,
|
|
||||||
border: Color = Color.Transparent,
|
|
||||||
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.height(28.dp)
|
|
||||||
.wrapContentHeight(Alignment.CenterVertically)
|
|
||||||
.clip(RoundedCornerShape(6.dp))
|
|
||||||
.background(background)
|
|
||||||
.border(width = 1.dp, color = border, shape = RoundedCornerShape(6.dp))
|
|
||||||
.padding(horizontal = 12.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
color = textColor,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MediaCastRow(
|
|
||||||
cast: List<CastMember>,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
cardWidth: Dp = 96.dp,
|
|
||||||
nameSize: TextUnit = 12.sp,
|
|
||||||
roleSize: TextUnit = 10.sp
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
val mutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
|
|
||||||
|
|
||||||
LazyRow(
|
|
||||||
modifier = modifier,
|
|
||||||
contentPadding = PaddingValues(horizontal = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
items(cast) { member ->
|
|
||||||
Column(modifier = Modifier.width(cardWidth)) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.aspectRatio(4f / 5f)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(scheme.surfaceVariant)
|
|
||||||
) {
|
|
||||||
if (member.imageUrl == null) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(scheme.surfaceVariant.copy(alpha = 0.6f)),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Person,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = mutedStrong
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PurefinAsyncImage(
|
|
||||||
model = member.imageUrl,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
Text(
|
|
||||||
text = member.name,
|
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = nameSize,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = member.role,
|
|
||||||
color = mutedStrong,
|
|
||||||
fontSize = roleSize,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package hu.bbara.purefin.common.ui
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
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.TextUnit
|
|
||||||
import androidx.compose.ui.unit.TextUnit.Companion.Unspecified
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MediaSynopsis(
|
|
||||||
synopsis: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
title: String = "Synopsis",
|
|
||||||
titleColor: Color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
bodyColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
titleFontSize: TextUnit = 18.sp,
|
|
||||||
bodyFontSize: TextUnit = 15.sp,
|
|
||||||
bodyLineHeight: TextUnit? = 22.sp,
|
|
||||||
titleSpacing: Dp = 12.dp,
|
|
||||||
collapsedLines: Int = 3,
|
|
||||||
collapseInitially: Boolean = true
|
|
||||||
) {
|
|
||||||
var isExpanded by remember(synopsis) { mutableStateOf(!collapseInitially) }
|
|
||||||
var isOverflowing by remember(synopsis) { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val containerModifier = if (isOverflowing) {
|
|
||||||
modifier.clickable(role = Role.Button) { isExpanded = !isExpanded }
|
|
||||||
} else {
|
|
||||||
modifier
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(modifier = containerModifier) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
color = titleColor,
|
|
||||||
fontSize = titleFontSize,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(titleSpacing))
|
|
||||||
Text(
|
|
||||||
text = synopsis,
|
|
||||||
color = bodyColor,
|
|
||||||
fontSize = bodyFontSize,
|
|
||||||
lineHeight = bodyLineHeight ?: Unspecified,
|
|
||||||
maxLines = if (isExpanded) Int.MAX_VALUE else collapsedLines,
|
|
||||||
overflow = if (isExpanded) TextOverflow.Clip else TextOverflow.Ellipsis,
|
|
||||||
onTextLayout = { result ->
|
|
||||||
val overflowed = if (isExpanded) {
|
|
||||||
result.lineCount > collapsedLines
|
|
||||||
} else {
|
|
||||||
result.hasVisualOverflow || result.lineCount > collapsedLines
|
|
||||||
}
|
|
||||||
if (overflowed != isOverflowing) {
|
|
||||||
isOverflowing = overflowed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
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
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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 // Slot API
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier,
|
|
||||||
enabled = enabled,
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary
|
|
||||||
),
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package hu.bbara.purefin.common.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
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.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.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MediaActionButton(
|
|
||||||
backgroundColor: Color,
|
|
||||||
iconColor: Color,
|
|
||||||
icon: ImageVector,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
height: Dp,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
|
||||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.1f else 1.0f, label = "scale")
|
|
||||||
val borderColor by animateColorAsState(targetValue = if (isFocused) scheme.primary else Color.Transparent, label = "border")
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
|
||||||
.size(height)
|
|
||||||
.border(if (isFocused) 2.5.dp else 0.dp, borderColor, CircleShape)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(backgroundColor.copy(alpha = 0.6f))
|
|
||||||
.onFocusChanged { isFocused = it.isFocused }
|
|
||||||
.clickable { onClick() },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(imageVector = icon, contentDescription = null, tint = iconColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package hu.bbara.purefin.common.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
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.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.ClosedCaption
|
|
||||||
import androidx.compose.material.icons.outlined.ExpandMore
|
|
||||||
import androidx.compose.material.icons.outlined.VolumeUp
|
|
||||||
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.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.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MediaPlaybackSettings(
|
|
||||||
backgroundColor: Color,
|
|
||||||
foregroundColor: Color,
|
|
||||||
audioTrack: String,
|
|
||||||
subtitles: String,
|
|
||||||
audioIcon: ImageVector = Icons.Outlined.VolumeUp,
|
|
||||||
subtitleIcon: ImageVector = Icons.Outlined.ClosedCaption,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
MediaSettingDropdown(
|
|
||||||
backgroundColor = backgroundColor,
|
|
||||||
foregroundColor = foregroundColor,
|
|
||||||
label = "Audio Track",
|
|
||||||
value = audioTrack,
|
|
||||||
icon = audioIcon
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
MediaSettingDropdown(
|
|
||||||
backgroundColor = backgroundColor,
|
|
||||||
foregroundColor = foregroundColor,
|
|
||||||
label = "Subtitles",
|
|
||||||
value = subtitles,
|
|
||||||
icon = subtitleIcon
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MediaSettingDropdown(
|
|
||||||
backgroundColor: Color,
|
|
||||||
foregroundColor: Color,
|
|
||||||
label: String,
|
|
||||||
value: String,
|
|
||||||
icon: ImageVector
|
|
||||||
) {
|
|
||||||
Row (
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
color = foregroundColor,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(38.dp)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(backgroundColor)
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon, contentDescription = null, tint = MaterialTheme.colorScheme.onBackground
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
|
||||||
Text(text = value, color = foregroundColor, fontSize = 14.sp)
|
|
||||||
}
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.ExpandMore,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = foregroundColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package hu.bbara.purefin.common.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
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.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.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
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
|
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
|
||||||
val scale by animateFloatAsState(targetValue = if (isFocused) 1.1f else 1.0f, label = "scale")
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
|
||||||
.size(size.dp)
|
|
||||||
.border(if (isFocused) 2.5.dp else 0.dp, if (isFocused) scheme.onPrimary else Color.Transparent, CircleShape)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(if (isFocused) scheme.primary else scheme.secondary)
|
|
||||||
.onFocusChanged { isFocused = it.isFocused }
|
|
||||||
.clickable { onClick() },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = contentDescription,
|
|
||||||
tint = scheme.onSecondary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package hu.bbara.purefin.common.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.EnterTransition
|
|
||||||
import androidx.compose.animation.ExitTransition
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
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 kotlinx.coroutines.delay
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A composable that displays content for a specified duration after the value becomes null.
|
|
||||||
*
|
|
||||||
* @param value The value to display. When set to a non-null value, it will be shown immediately.
|
|
||||||
* When set to null, the previously shown value will remain visible for [hideAfterMillis]
|
|
||||||
* before being hidden.
|
|
||||||
* @param hideAfterMillis The duration in milliseconds to keep showing the last value after [value] becomes null.
|
|
||||||
* Defaults to 1000ms (1 second).
|
|
||||||
* @param content The composable content to display, receiving the current non-null value.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun <T> EmptyValueTimedVisibility(
|
|
||||||
value: T?,
|
|
||||||
hideAfterMillis: Long = 1_000,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
content: @Composable (T) -> Unit
|
|
||||||
) {
|
|
||||||
val shownValue = remember { mutableStateOf<T?>(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(value) {
|
|
||||||
if (value == null) {
|
|
||||||
delay(hideAfterMillis)
|
|
||||||
shownValue.value = null
|
|
||||||
}
|
|
||||||
shownValue.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
shownValue.value?.let {
|
|
||||||
content(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays [content] whenever [value] changes and hides it after [hideAfterMillis]
|
|
||||||
* milliseconds without further updates.
|
|
||||||
*
|
|
||||||
* @param value The value whose changes should trigger visibility.
|
|
||||||
* @param hideAfterMillis Duration in milliseconds after which the content will be hidden
|
|
||||||
* if [value] has not changed again.
|
|
||||||
* @param content The composable to render while visible.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun <T> ValueChangeTimedVisibility(
|
|
||||||
value: T,
|
|
||||||
hideAfterMillis: Long = 1_000,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
content: @Composable (T) -> Unit
|
|
||||||
) {
|
|
||||||
var displayedValue by remember { mutableStateOf(value) }
|
|
||||||
var isVisible by remember { mutableStateOf(false) }
|
|
||||||
var hasInitialValue by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(value) {
|
|
||||||
displayedValue = value
|
|
||||||
if (!hasInitialValue) {
|
|
||||||
hasInitialValue = true
|
|
||||||
return@LaunchedEffect
|
|
||||||
}
|
|
||||||
|
|
||||||
isVisible = true
|
|
||||||
delay(hideAfterMillis)
|
|
||||||
isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = isVisible,
|
|
||||||
modifier = modifier,
|
|
||||||
enter = EnterTransition.None,
|
|
||||||
exit = ExitTransition.None
|
|
||||||
) {
|
|
||||||
content(displayedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.navigation
|
package hu.bbara.purefin.modules
|
||||||
|
|
||||||
import androidx.navigation3.runtime.EntryProviderScope
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
@@ -6,7 +6,14 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.components.ActivityRetainedComponent
|
import dagger.hilt.android.components.ActivityRetainedComponent
|
||||||
import dagger.multibindings.IntoSet
|
import dagger.multibindings.IntoSet
|
||||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
|
import hu.bbara.purefin.navigation.tvEpisodeSection
|
||||||
|
import hu.bbara.purefin.navigation.tvHomeSection
|
||||||
|
import hu.bbara.purefin.navigation.tvLibrarySection
|
||||||
|
import hu.bbara.purefin.navigation.tvLoginSection
|
||||||
|
import hu.bbara.purefin.navigation.tvMovieSection
|
||||||
|
import hu.bbara.purefin.navigation.tvPlayerSection
|
||||||
|
import hu.bbara.purefin.navigation.tvSeriesSection
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(ActivityRetainedComponent::class)
|
@InstallIn(ActivityRetainedComponent::class)
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package hu.bbara.purefin.tv.di
|
package hu.bbara.purefin.modules
|
||||||
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import hu.bbara.purefin.core.data.client.PlaybackProfileFamily
|
import hu.bbara.purefin.core.data.PlaybackProfileFamily
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package hu.bbara.purefin.tv.navigation
|
package hu.bbara.purefin.navigation
|
||||||
|
|
||||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
|
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
|
|
||||||
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
||||||
staticCompositionLocalOf { error("NavigationManager not provided") }
|
staticCompositionLocalOf { error("NavigationManager not provided") }
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package hu.bbara.purefin.tv.navigation
|
package hu.bbara.purefin.navigation
|
||||||
|
|
||||||
import androidx.navigation3.runtime.EntryProviderScope
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
import hu.bbara.purefin.app.content.episode.TvEpisodeScreen
|
import hu.bbara.purefin.ui.screen.episode.TvEpisodeScreen
|
||||||
import hu.bbara.purefin.app.content.movie.TvMovieScreen
|
import hu.bbara.purefin.ui.screen.movie.TvMovieScreen
|
||||||
import hu.bbara.purefin.app.content.series.TvSeriesScreen
|
import hu.bbara.purefin.ui.screen.series.TvSeriesScreen
|
||||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
import hu.bbara.purefin.login.ui.LoginScreen
|
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
||||||
import hu.bbara.purefin.tv.TvAppScreen
|
import hu.bbara.purefin.ui.screen.TvAppScreen
|
||||||
import hu.bbara.purefin.tv.library.ui.TvLibraryScreen
|
import hu.bbara.purefin.ui.screen.library.TvLibraryScreen
|
||||||
import hu.bbara.purefin.tv.player.TvPlayerScreen
|
import hu.bbara.purefin.ui.screen.player.TvPlayerScreen
|
||||||
|
|
||||||
fun EntryProviderScope<Route>.tvHomeSection() {
|
fun EntryProviderScope<Route>.tvHomeSection() {
|
||||||
entry<Route.Home> {
|
entry<Route.Home> {
|
||||||
@@ -36,16 +36,16 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
|||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
import coil3.util.DebugLogger
|
import coil3.util.DebugLogger
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
import hu.bbara.purefin.core.data.SessionBootstrapper
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor
|
|
||||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||||
import hu.bbara.purefin.feature.shared.navigation.NavigationCommand
|
import hu.bbara.purefin.core.navigation.NavigationCommand
|
||||||
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
|
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
import hu.bbara.purefin.login.ui.LoginScreen
|
import hu.bbara.purefin.data.jellyfin.client.JellyfinAuthInterceptor
|
||||||
import hu.bbara.purefin.tv.navigation.LocalNavigationBackStack
|
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
||||||
import hu.bbara.purefin.tv.navigation.LocalNavigationManager
|
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||||
|
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
||||||
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
import hu.bbara.purefin.ui.theme.backgroundDark
|
import hu.bbara.purefin.ui.theme.backgroundDark
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -66,7 +66,7 @@ class TvActivity : ComponentActivity() {
|
|||||||
lateinit var navigationManager: NavigationManager
|
lateinit var navigationManager: NavigationManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var jellyfinApiClient: JellyfinApiClient
|
lateinit var sessionBootstrapper: SessionBootstrapper
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var authInterceptor: JellyfinAuthInterceptor
|
lateinit var authInterceptor: JellyfinAuthInterceptor
|
||||||
@@ -94,7 +94,7 @@ class TvActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun init() {
|
private suspend fun init() {
|
||||||
jellyfinApiClient.updateApiClient()
|
sessionBootstrapper.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureImageLoader() {
|
private fun configureImageLoader() {
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
package hu.bbara.purefin.tv.di
|
|
||||||
|
|
||||||
import androidx.media3.datasource.DataSource
|
|
||||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
|
||||||
import hu.bbara.purefin.core.data.download.MediaDownloadController
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
object TvPlaybackModule {
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun providePlaybackDataSourceFactory(
|
|
||||||
okHttpDataSourceFactory: OkHttpDataSource.Factory
|
|
||||||
): DataSource.Factory {
|
|
||||||
return okHttpDataSourceFactory
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideMediaDownloadController(): MediaDownloadController {
|
|
||||||
return TvNoOpMediaDownloadController
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object TvNoOpMediaDownloadController : MediaDownloadController {
|
|
||||||
private val notDownloaded = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
|
|
||||||
|
|
||||||
override fun observeActiveDownloads(): Flow<Map<String, Float>> = flowOf(emptyMap())
|
|
||||||
|
|
||||||
override fun observeDownloadState(contentId: String) = notDownloaded
|
|
||||||
|
|
||||||
override suspend fun downloadMovie(movieId: UUID) = Unit
|
|
||||||
|
|
||||||
override suspend fun cancelDownload(movieId: UUID) = Unit
|
|
||||||
|
|
||||||
override suspend fun downloadEpisode(episodeId: UUID) = Unit
|
|
||||||
|
|
||||||
override suspend fun downloadEpisodes(episodeIds: List<UUID>) = Unit
|
|
||||||
|
|
||||||
override suspend fun cancelEpisodeDownload(episodeId: UUID) = Unit
|
|
||||||
|
|
||||||
override suspend fun enableSmartDownload(seriesId: UUID) = Unit
|
|
||||||
|
|
||||||
override suspend fun syncSmartDownloads() = Unit
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.common.ui.components
|
package hu.bbara.purefin.ui.common.button
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package hu.bbara.purefin.ui.common.card
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
|
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||||
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PosterCard(
|
||||||
|
item: PosterItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
imageModifier: Modifier = Modifier,
|
||||||
|
posterWidth: Dp = 144.dp,
|
||||||
|
showSecondaryText: Boolean = false,
|
||||||
|
indicatorSize: Int = 28,
|
||||||
|
indicatorPadding: Dp = 8.dp,
|
||||||
|
onFocusedItem: (FocusableItem) -> Unit = {},
|
||||||
|
onMovieSelected: (UUID) -> Unit,
|
||||||
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
) {
|
||||||
|
PosterCardContent(
|
||||||
|
model = item.toPosterCardModel(),
|
||||||
|
onClick = { item.open(onMovieSelected, onSeriesSelected, onEpisodeSelected) },
|
||||||
|
modifier = modifier,
|
||||||
|
imageModifier = imageModifier,
|
||||||
|
posterWidth = posterWidth,
|
||||||
|
showSecondaryText = showSecondaryText,
|
||||||
|
indicatorSize = indicatorSize,
|
||||||
|
indicatorPadding = indicatorPadding,
|
||||||
|
onFocused = { onFocusedItem(item) },
|
||||||
|
focusedScale = 1.07f,
|
||||||
|
focusedBorderWidth = 2.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PosterItem.toPosterCardModel(): PosterCardModel {
|
||||||
|
return PosterCardModel(
|
||||||
|
title = title,
|
||||||
|
secondaryText = secondaryText,
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
mediaKind = type,
|
||||||
|
badge = when (type) {
|
||||||
|
MediaKind.MOVIE -> {
|
||||||
|
val movie = requireNotNull(movie)
|
||||||
|
PosterCardBadge.WatchState(
|
||||||
|
watched = movie.watched,
|
||||||
|
started = (movie.progress ?: 0.0) > 0.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaKind.EPISODE -> {
|
||||||
|
val episode = requireNotNull(episode)
|
||||||
|
PosterCardBadge.WatchState(
|
||||||
|
watched = episode.watched,
|
||||||
|
started = (episode.progress ?: 0.0) > 0.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaKind.SERIES -> PosterCardBadge.UnwatchedEpisodes(
|
||||||
|
count = requireNotNull(series).unwatchedEpisodeCount
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> PosterCardBadge.None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PosterItem.open(
|
||||||
|
onMovieSelected: (UUID) -> Unit,
|
||||||
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
|
) {
|
||||||
|
when (type) {
|
||||||
|
MediaKind.MOVIE -> onMovieSelected(id)
|
||||||
|
MediaKind.SERIES -> onSeriesSelected(id)
|
||||||
|
MediaKind.EPISODE -> {
|
||||||
|
val ep = requireNotNull(episode)
|
||||||
|
onEpisodeSelected(ep.seriesId, ep.seasonId, ep.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||||
|
|
||||||
package hu.bbara.purefin.common.ui.components
|
package hu.bbara.purefin.ui.common.media
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.BringIntoViewSpec
|
import androidx.compose.foundation.gestures.BringIntoViewSpec
|
||||||
@@ -31,9 +31,9 @@ import androidx.compose.ui.platform.LocalConfiguration
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.common.ui.MediaSynopsis
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
|
|
||||||
internal val MediaDetailHorizontalPadding = 48.dp
|
internal val MediaDetailHorizontalPadding = 48.dp
|
||||||
private val MediaDetailHeaderTopPadding = 104.dp
|
private val MediaDetailHeaderTopPadding = 104.dp
|
||||||
@@ -151,8 +151,8 @@ internal fun TvMediaDetailBodyBox(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun tvMediaDetailBackgroundImageUrl(imageUrlPrefix: String?): String {
|
internal fun tvMediaDetailBackgroundImageUrl(imageUrlPrefix: String?): String {
|
||||||
val primaryImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
|
val primaryImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
|
||||||
val backdropImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.BACKDROP)
|
val backdropImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.BACKDROP)
|
||||||
return backdropImageUrl.ifBlank { primaryImageUrl }
|
return backdropImageUrl.ifBlank { primaryImageUrl }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.common.ui.components
|
package hu.bbara.purefin.ui.common.media
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
@@ -37,6 +37,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||||
|
|
||||||
internal data class MediaDetailsTopBarShortcut(
|
internal data class MediaDetailsTopBarShortcut(
|
||||||
val label: String,
|
val label: String,
|
||||||
@@ -53,6 +54,7 @@ internal fun MediaDetailsTopBar(
|
|||||||
backFocusRequester: FocusRequester? = null,
|
backFocusRequester: FocusRequester? = null,
|
||||||
downFocusRequester: FocusRequester? = null
|
downFocusRequester: FocusRequester? = null
|
||||||
) {
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
val downModifier = if (downFocusRequester != null) {
|
val downModifier = if (downFocusRequester != null) {
|
||||||
Modifier.focusProperties { down = downFocusRequester }
|
Modifier.focusProperties { down = downFocusRequester }
|
||||||
} else {
|
} else {
|
||||||
@@ -77,7 +79,11 @@ internal fun MediaDetailsTopBar(
|
|||||||
icon = Icons.AutoMirrored.Outlined.ArrowBack,
|
icon = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||||
contentDescription = "Back",
|
contentDescription = "Back",
|
||||||
onClick = onBack,
|
onClick = onBack,
|
||||||
modifier = backModifier.then(downModifier)
|
modifier = backModifier.then(downModifier),
|
||||||
|
focusedScale = 1.1f,
|
||||||
|
focusedBorderWidth = 2.5.dp,
|
||||||
|
focusedBorderColor = scheme.primary,
|
||||||
|
focusedBackgroundColor = scheme.primary.copy(alpha = 0.25f)
|
||||||
)
|
)
|
||||||
if (shortcut != null) {
|
if (shortcut != null) {
|
||||||
GhostTextButton(
|
GhostTextButton(
|
||||||
@@ -92,13 +98,21 @@ internal fun MediaDetailsTopBar(
|
|||||||
icon = Icons.Outlined.Cast,
|
icon = Icons.Outlined.Cast,
|
||||||
contentDescription = "Cast",
|
contentDescription = "Cast",
|
||||||
onClick = onCastClick,
|
onClick = onCastClick,
|
||||||
modifier = downModifier
|
modifier = downModifier,
|
||||||
|
focusedScale = 1.1f,
|
||||||
|
focusedBorderWidth = 2.5.dp,
|
||||||
|
focusedBorderColor = scheme.primary,
|
||||||
|
focusedBackgroundColor = scheme.primary.copy(alpha = 0.25f)
|
||||||
)
|
)
|
||||||
GhostIconButton(
|
GhostIconButton(
|
||||||
icon = Icons.Outlined.MoreVert,
|
icon = Icons.Outlined.MoreVert,
|
||||||
contentDescription = "More",
|
contentDescription = "More",
|
||||||
onClick = onMoreClick,
|
onClick = onMoreClick,
|
||||||
modifier = downModifier
|
modifier = downModifier,
|
||||||
|
focusedScale = 1.1f,
|
||||||
|
focusedBorderWidth = 2.5.dp,
|
||||||
|
focusedBorderColor = scheme.primary,
|
||||||
|
focusedBackgroundColor = scheme.primary.copy(alpha = 0.25f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv
|
package hu.bbara.purefin.ui.screen
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -15,12 +15,12 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
import hu.bbara.purefin.feature.shared.home.AppViewModel
|
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
||||||
import hu.bbara.purefin.tv.home.TvHomeScreen
|
import hu.bbara.purefin.ui.screen.home.TvHomeScreen
|
||||||
import hu.bbara.purefin.tv.home.ui.TvDrawerDestination
|
import hu.bbara.purefin.ui.screen.home.components.TvDrawerDestination
|
||||||
import hu.bbara.purefin.tv.home.ui.TvDrawerDestinationItem
|
import hu.bbara.purefin.ui.screen.home.components.TvDrawerDestinationItem
|
||||||
import hu.bbara.purefin.tv.home.ui.TvNavigationDrawer
|
import hu.bbara.purefin.ui.screen.home.components.TvNavigationDrawer
|
||||||
import hu.bbara.purefin.tv.library.ui.TvLibrariesOverviewScreen
|
import hu.bbara.purefin.ui.screen.libraries.TvLibrariesOverviewScreen
|
||||||
import hu.bbara.purefin.core.model.LibraryKind
|
import hu.bbara.purefin.core.model.LibraryKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.content.episode
|
package hu.bbara.purefin.ui.screen.episode
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -13,20 +13,21 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.MediaCastRow
|
import hu.bbara.purefin.ui.common.media.MediaCastRow
|
||||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection
|
import hu.bbara.purefin.ui.common.media.MediaDetailOverviewSection
|
||||||
import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection
|
import hu.bbara.purefin.ui.common.media.MediaDetailPlaybackSection
|
||||||
import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
|
import hu.bbara.purefin.ui.common.media.MediaDetailSectionTitle
|
||||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox
|
import hu.bbara.purefin.ui.common.media.TvMediaDetailBodyBox
|
||||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
|
import hu.bbara.purefin.ui.common.media.TvMediaDetailScaffold
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.ui.screen.episode.components.TvEpisodeHeroSection
|
||||||
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.tv.navigation.LocalNavigationManager
|
import hu.bbara.purefin.core.navigation.EpisodeDto
|
||||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||||
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
import hu.bbara.purefin.feature.content.episode.EpisodeScreenViewModel
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvEpisodeScreen(
|
fun TvEpisodeScreen(
|
||||||
@@ -87,7 +88,7 @@ internal fun TvEpisodeScreenContent(
|
|||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
TvMediaDetailBodyBox(
|
TvMediaDetailBodyBox(
|
||||||
backgroundImageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
backgroundImageUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
modifier = it
|
modifier = it
|
||||||
) {
|
) {
|
||||||
TvEpisodeHeroSection(
|
TvEpisodeHeroSection(
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
package hu.bbara.purefin.app.content.episode
|
package hu.bbara.purefin.ui.screen.episode.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -20,13 +18,14 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaResumeButton
|
import hu.bbara.purefin.ui.common.media.MediaMetadataFlowRow
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlayButtonText
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
|
|
||||||
internal const val EpisodePlayButtonTag = "episode-play-button"
|
internal const val EpisodePlayButtonTag = "episode-play-button"
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TvEpisodeHeroSection(
|
internal fun TvEpisodeHeroSection(
|
||||||
episode: Episode,
|
episode: Episode,
|
||||||
@@ -71,33 +70,26 @@ internal fun TvEpisodeHeroSection(
|
|||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(18.dp))
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
FlowRow(
|
MediaMetadataFlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
items = listOf(episode.releaseDate, episode.rating, episode.runtime, episode.format),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
highlightedItem = episode.format
|
||||||
) {
|
)
|
||||||
MediaMetaChip(text = episode.releaseDate)
|
|
||||||
MediaMetaChip(text = episode.rating)
|
|
||||||
MediaMetaChip(text = episode.runtime)
|
|
||||||
MediaMetaChip(
|
|
||||||
text = episode.format,
|
|
||||||
background = scheme.primary.copy(alpha = 0.2f),
|
|
||||||
border = scheme.primary.copy(alpha = 0.35f),
|
|
||||||
textColor = scheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
MediaResumeButton(
|
MediaResumeButton(
|
||||||
text = episode.playButtonText(),
|
text = mediaPlayButtonText(episode.progress, episode.watched),
|
||||||
progress = episode.progress?.div(100)?.toFloat() ?: 0f,
|
progress = mediaPlaybackProgress(episode.progress),
|
||||||
onClick = onPlay,
|
onClick = onPlay,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.sizeIn(minWidth = 216.dp, maxWidth = 240.dp)
|
.sizeIn(minWidth = 216.dp, maxWidth = 240.dp)
|
||||||
.focusRequester(playFocusRequester)
|
.focusRequester(playFocusRequester)
|
||||||
.testTag(EpisodePlayButtonTag)
|
.testTag(EpisodePlayButtonTag),
|
||||||
|
focusedScale = 1.08f,
|
||||||
|
focusHaloColor = scheme.primary.copy(alpha = 0.22f),
|
||||||
|
focusBorderWidth = 3.dp,
|
||||||
|
focusBorderColor = scheme.onBackground,
|
||||||
|
overlayBorderWidth = 2.dp,
|
||||||
|
overlayBorderColor = scheme.primary.copy(alpha = 0.95f),
|
||||||
|
focusable = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Episode.playButtonText(): String {
|
|
||||||
return if ((progress ?: 0.0) > 0.0 && !watched) "Resume" else "Play"
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.home
|
package hu.bbara.purefin.ui.screen.home
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
@@ -9,13 +9,13 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.tv.home.ui.TvFocusedItemHero
|
import hu.bbara.purefin.ui.screen.home.components.TvFocusedItemHero
|
||||||
import hu.bbara.purefin.tv.home.ui.TvHomeContent
|
import hu.bbara.purefin.ui.screen.home.components.TvHomeContent
|
||||||
import hu.bbara.purefin.tv.home.ui.rememberTvHomeHeroState
|
import hu.bbara.purefin.ui.screen.home.components.rememberTvHomeHeroState
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
private const val TvHomeHeroHeightFraction = 0.32f
|
private const val TvHomeHeroHeightFraction = 0.32f
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.core.model.Series
|
import hu.bbara.purefin.core.model.Series
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
internal data class TvFocusedHeroModel(
|
internal data class TvFocusedHeroModel(
|
||||||
@@ -42,7 +42,7 @@ internal fun FocusableItem.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun backdropImageUrl(imageUrlPrefix: String?, fallbackImageUrl: String): String {
|
internal fun backdropImageUrl(imageUrlPrefix: String?, fallbackImageUrl: String): String {
|
||||||
val backdropImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.BACKDROP)
|
val backdropImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.BACKDROP)
|
||||||
return backdropImageUrl.ifBlank { fallbackImageUrl }
|
return backdropImageUrl.ifBlank { fallbackImageUrl }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ private fun Movie.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
|||||||
id = id,
|
id = id,
|
||||||
backdropImageUrl = backdropImageUrl(
|
backdropImageUrl = backdropImageUrl(
|
||||||
imageUrlPrefix = imageUrlPrefix,
|
imageUrlPrefix = imageUrlPrefix,
|
||||||
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
|
fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
|
||||||
),
|
),
|
||||||
title = title,
|
title = title,
|
||||||
metadata = listOf(year, rating, runtime, format).compactMetadata(),
|
metadata = listOf(year, rating, runtime, format).compactMetadata(),
|
||||||
@@ -63,7 +63,7 @@ private fun Episode.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
|||||||
id = id,
|
id = id,
|
||||||
backdropImageUrl = backdropImageUrl(
|
backdropImageUrl = backdropImageUrl(
|
||||||
imageUrlPrefix = imageUrlPrefix,
|
imageUrlPrefix = imageUrlPrefix,
|
||||||
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
|
fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
|
||||||
),
|
),
|
||||||
title = title,
|
title = title,
|
||||||
metadata = listOf(
|
metadata = listOf(
|
||||||
@@ -87,7 +87,7 @@ private fun Series.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
|||||||
id = id,
|
id = id,
|
||||||
backdropImageUrl = backdropImageUrl(
|
backdropImageUrl = backdropImageUrl(
|
||||||
imageUrlPrefix = imageUrlPrefix,
|
imageUrlPrefix = imageUrlPrefix,
|
||||||
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
|
fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
|
||||||
),
|
),
|
||||||
title = name,
|
title = name,
|
||||||
metadata = listOf(year, seasonLabel(seasonCount), unwatchedText).compactMetadata(),
|
metadata = listOf(year, seasonLabel(seasonCount), unwatchedText).compactMetadata(),
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -23,7 +23,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
|
||||||
internal const val TvHomeHeroTitleTag = "tv-home-hero-title"
|
internal const val TvHomeHeroTitleTag = "tv-home-hero-title"
|
||||||
internal const val TvHomeHeroProgressLabelTag = "tv-home-hero-progress-label"
|
internal const val TvHomeHeroProgressLabelTag = "tv-home-hero-progress-label"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||||
|
|
||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.BringIntoViewSpec
|
import androidx.compose.foundation.gestures.BringIntoViewSpec
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||||
|
|
||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
||||||
@@ -24,11 +24,11 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
internal data class TvHomeItemRegistry(
|
internal data class TvHomeItemRegistry(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
@@ -44,17 +44,17 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.common.ui.PosterCard
|
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||||
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
|
|
||||||
private val TvHomeSectionsThumbShape = RoundedCornerShape(20.dp)
|
private val TvHomeSectionsThumbShape = RoundedCornerShape(20.dp)
|
||||||
private val TvHomeSectionsHorizontalPadding = 32.dp
|
private val TvHomeSectionsHorizontalPadding = 32.dp
|
||||||
@@ -93,14 +93,14 @@ fun TvContinueWatchingSection(
|
|||||||
title = item.primaryText,
|
title = item.primaryText,
|
||||||
supporting = item.secondaryText,
|
supporting = item.secondaryText,
|
||||||
imageUrl = when (item.type) {
|
imageUrl = when (item.type) {
|
||||||
MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
MediaKind.MOVIE -> ImageUrlBuilder.finishImageUrl(
|
||||||
prefixImageUrl = item.movie?.imageUrlPrefix,
|
prefixImageUrl = item.movie?.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
artworkKind = ArtworkKind.PRIMARY
|
||||||
)
|
)
|
||||||
|
|
||||||
MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
MediaKind.EPISODE -> ImageUrlBuilder.finishImageUrl(
|
||||||
prefixImageUrl = item.episode?.imageUrlPrefix,
|
prefixImageUrl = item.episode?.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
artworkKind = ArtworkKind.PRIMARY
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.library.ui
|
package hu.bbara.purefin.ui.screen.libraries
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -43,8 +43,8 @@ import androidx.compose.ui.platform.testTag
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.core.model.LibraryKind
|
import hu.bbara.purefin.core.model.LibraryKind
|
||||||
|
|
||||||
internal const val TvLibrariesOverviewItemTagPrefix = "tv-libraries-overview-item-"
|
internal const val TvLibrariesOverviewItemTagPrefix = "tv-libraries-overview-item-"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.library.ui
|
package hu.bbara.purefin.ui.screen.library
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -22,11 +22,11 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.PosterCard
|
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinIconButton
|
import hu.bbara.purefin.ui.common.button.PurefinIconButton
|
||||||
import hu.bbara.purefin.feature.shared.navigation.LibraryDto
|
import hu.bbara.purefin.core.navigation.LibraryDto
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
|
import hu.bbara.purefin.feature.browse.library.LibraryViewModel
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -64,6 +64,7 @@ private fun TvLibraryTopBar(
|
|||||||
title: String,
|
title: String,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -74,7 +75,11 @@ private fun TvLibraryTopBar(
|
|||||||
PurefinIconButton(
|
PurefinIconButton(
|
||||||
icon = Icons.AutoMirrored.Outlined.ArrowBack,
|
icon = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||||
contentDescription = "Back",
|
contentDescription = "Back",
|
||||||
onClick = onBack
|
onClick = onBack,
|
||||||
|
focusedScale = 1.1f,
|
||||||
|
focusedBorderWidth = 2.5.dp,
|
||||||
|
focusedBorderColor = scheme.onPrimary,
|
||||||
|
focusedBackgroundColor = scheme.primary
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package hu.bbara.purefin.ui.screen.login
|
||||||
|
|
||||||
|
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.Modifier
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import hu.bbara.purefin.feature.login.LoginViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginScreen(
|
||||||
|
viewModel: LoginViewModel = hiltViewModel(),
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
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()
|
||||||
|
|
||||||
|
val state = remember(serverUrl, username, password, errorMessage) {
|
||||||
|
LoginContentState(
|
||||||
|
serverUrl = serverUrl,
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
errorMessage = errorMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val callbacks = remember(viewModel, coroutineScope) {
|
||||||
|
LoginContentCallbacks(
|
||||||
|
onServerUrlChange = {
|
||||||
|
viewModel.clearError()
|
||||||
|
viewModel.setUrl(it)
|
||||||
|
},
|
||||||
|
onUsernameChange = {
|
||||||
|
viewModel.clearError()
|
||||||
|
viewModel.setUsername(it)
|
||||||
|
},
|
||||||
|
onPasswordChange = {
|
||||||
|
viewModel.clearError()
|
||||||
|
viewModel.setPassword(it)
|
||||||
|
},
|
||||||
|
onConnect = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
isLoggingIn = true
|
||||||
|
try {
|
||||||
|
viewModel.login()
|
||||||
|
} finally {
|
||||||
|
isLoggingIn = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginContent(
|
||||||
|
state = state,
|
||||||
|
callbacks = callbacks,
|
||||||
|
isLoggingIn = isLoggingIn,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.content.movie
|
package hu.bbara.purefin.ui.screen.movie
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -13,17 +13,18 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.MediaCastRow
|
import hu.bbara.purefin.ui.common.media.MediaCastRow
|
||||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection
|
import hu.bbara.purefin.ui.common.media.MediaDetailOverviewSection
|
||||||
import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection
|
import hu.bbara.purefin.ui.common.media.MediaDetailPlaybackSection
|
||||||
import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
|
import hu.bbara.purefin.ui.common.media.MediaDetailSectionTitle
|
||||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox
|
import hu.bbara.purefin.ui.common.media.TvMediaDetailBodyBox
|
||||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
|
import hu.bbara.purefin.ui.common.media.TvMediaDetailScaffold
|
||||||
import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl
|
import hu.bbara.purefin.ui.common.media.tvMediaDetailBackgroundImageUrl
|
||||||
import hu.bbara.purefin.feature.shared.navigation.MovieDto
|
import hu.bbara.purefin.ui.screen.movie.components.TvMovieHeroSection
|
||||||
|
import hu.bbara.purefin.core.navigation.MovieDto
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
|
import hu.bbara.purefin.feature.content.movie.MovieScreenViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvMovieScreen(
|
fun TvMovieScreen(
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
package hu.bbara.purefin.app.content.movie
|
package hu.bbara.purefin.ui.screen.movie.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -20,13 +18,14 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaResumeButton
|
import hu.bbara.purefin.ui.common.media.MediaMetadataFlowRow
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlayButtonText
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
|
|
||||||
internal const val MoviePlayButtonTag = "movie-play-button"
|
internal const val MoviePlayButtonTag = "movie-play-button"
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TvMovieHeroSection(
|
internal fun TvMovieHeroSection(
|
||||||
movie: Movie,
|
movie: Movie,
|
||||||
@@ -51,33 +50,26 @@ internal fun TvMovieHeroSection(
|
|||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(18.dp))
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
FlowRow(
|
MediaMetadataFlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
items = listOf(movie.year, movie.rating, movie.runtime, movie.format),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
highlightedItem = movie.format
|
||||||
) {
|
)
|
||||||
MediaMetaChip(text = movie.year)
|
|
||||||
MediaMetaChip(text = movie.rating)
|
|
||||||
MediaMetaChip(text = movie.runtime)
|
|
||||||
MediaMetaChip(
|
|
||||||
text = movie.format,
|
|
||||||
background = scheme.primary.copy(alpha = 0.2f),
|
|
||||||
border = scheme.primary.copy(alpha = 0.35f),
|
|
||||||
textColor = scheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
MediaResumeButton(
|
MediaResumeButton(
|
||||||
text = movie.playButtonText(),
|
text = mediaPlayButtonText(movie.progress, movie.watched),
|
||||||
progress = movie.progress?.div(100)?.toFloat() ?: 0f,
|
progress = mediaPlaybackProgress(movie.progress),
|
||||||
onClick = onPlay,
|
onClick = onPlay,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.sizeIn(minWidth = 216.dp, maxWidth = 240.dp)
|
.sizeIn(minWidth = 216.dp, maxWidth = 240.dp)
|
||||||
.focusRequester(playFocusRequester)
|
.focusRequester(playFocusRequester)
|
||||||
.testTag(MoviePlayButtonTag)
|
.testTag(MoviePlayButtonTag),
|
||||||
|
focusedScale = 1.08f,
|
||||||
|
focusHaloColor = scheme.primary.copy(alpha = 0.22f),
|
||||||
|
focusBorderWidth = 3.dp,
|
||||||
|
focusBorderColor = scheme.onBackground,
|
||||||
|
overlayBorderWidth = 2.dp,
|
||||||
|
overlayBorderColor = scheme.primary.copy(alpha = 0.95f),
|
||||||
|
focusable = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Movie.playButtonText(): String {
|
|
||||||
return if ((progress ?: 0.0) > 0.0 && !watched) "Resume" else "Play"
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.player
|
package hu.bbara.purefin.ui.screen.player
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
@@ -50,10 +50,10 @@ import androidx.media3.ui.AspectRatioFrameLayout
|
|||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import hu.bbara.purefin.core.player.viewmodel.ControlsAutoHideBlocker
|
import hu.bbara.purefin.core.player.viewmodel.ControlsAutoHideBlocker
|
||||||
import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel
|
import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel
|
||||||
import hu.bbara.purefin.tv.player.components.TvPlayerControlsOverlay
|
import hu.bbara.purefin.ui.screen.player.components.TvPlayerControlsOverlay
|
||||||
import hu.bbara.purefin.tv.player.components.TvPlayerLoadingErrorEndCard
|
import hu.bbara.purefin.ui.screen.player.components.TvPlayerLoadingErrorEndCard
|
||||||
import hu.bbara.purefin.tv.player.components.TvTrackPanelType
|
import hu.bbara.purefin.ui.screen.player.components.TvTrackPanelType
|
||||||
import hu.bbara.purefin.tv.player.components.TvTrackSelectionPanel
|
import hu.bbara.purefin.ui.screen.player.components.TvTrackSelectionPanel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
private const val TV_CONTROLS_AUTO_HIDE_MS = 5_000L
|
private const val TV_CONTROLS_AUTO_HIDE_MS = 5_000L
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.player.components
|
package hu.bbara.purefin.ui.screen.player.components
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.player.components
|
package hu.bbara.purefin.ui.screen.player.components
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package hu.bbara.purefin.ui.screen.player.components
|
||||||
|
|
||||||
|
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.core.player.model.PlayerUiState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TvPlayerLoadingErrorEndCard(
|
||||||
|
uiState: PlayerUiState,
|
||||||
|
onRetry: () -> Unit,
|
||||||
|
onNext: () -> Unit,
|
||||||
|
onReplay: () -> Unit,
|
||||||
|
onDismissError: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
PlayerLoadingErrorEndCardContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onRetry = onRetry,
|
||||||
|
onNext = onNext,
|
||||||
|
onReplay = onReplay,
|
||||||
|
onDismissError = onDismissError,
|
||||||
|
modifier = modifier,
|
||||||
|
showBufferWhileError = false,
|
||||||
|
cardPadding = 24.dp,
|
||||||
|
cardSpacing = 16.dp,
|
||||||
|
backgroundAlpha = 0.92f,
|
||||||
|
headlineStyle = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.player.components
|
package hu.bbara.purefin.ui.screen.player.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package hu.bbara.purefin.tv.player.components
|
package hu.bbara.purefin.ui.screen.player.components
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -18,15 +17,12 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
import androidx.compose.ui.input.key.KeyEventType
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.key
|
import androidx.compose.ui.input.key.key
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
import androidx.compose.ui.input.key.type
|
import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.core.player.model.MarkerType
|
|
||||||
import hu.bbara.purefin.core.player.model.TimedMarker
|
import hu.bbara.purefin.core.player.model.TimedMarker
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -43,13 +39,8 @@ internal fun TvPlayerSeekBar(
|
|||||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
val safeDuration = durationMs.takeIf { it > 0 } ?: 1L
|
val safeDuration = durationMs.takeIf { it > 0 } ?: 1L
|
||||||
val position = positionMs.coerceIn(0, safeDuration)
|
val position = positionMs.coerceIn(0, safeDuration)
|
||||||
val bufferRatio = (bufferedMs.toFloat() / safeDuration).coerceIn(0f, 1f)
|
|
||||||
val progressRatio = (position.toFloat() / safeDuration).coerceIn(0f, 1f)
|
|
||||||
val combinedMarkers = chapterMarkers.map { it.copy(type = MarkerType.CHAPTER) } +
|
|
||||||
adMarkers.map { it.copy(type = MarkerType.AD) }
|
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
@@ -90,52 +81,17 @@ internal fun TvPlayerSeekBar(
|
|||||||
.focusable(),
|
.focusable(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Canvas(
|
PlayerSeekBarTrack(
|
||||||
modifier = Modifier
|
positionMs = position,
|
||||||
.fillMaxSize()
|
durationMs = safeDuration,
|
||||||
.padding(horizontal = 2.dp, vertical = 10.dp)
|
bufferedMs = bufferedMs,
|
||||||
) {
|
chapterMarkers = chapterMarkers,
|
||||||
val trackHeight = if (isFocused) 6f else 4f
|
adMarkers = adMarkers,
|
||||||
val trackTop = size.height / 2 - trackHeight / 2
|
modifier = Modifier.fillMaxSize(),
|
||||||
drawRect(
|
isFocused = isFocused,
|
||||||
color = scheme.onSurface.copy(alpha = 0.2f),
|
thumbRadius = 7.dp,
|
||||||
size = Size(width = size.width, height = trackHeight),
|
focusedThumbRadius = 9.dp,
|
||||||
topLeft = Offset(0f, trackTop)
|
focusedThumbHaloRadiusDelta = 4.dp
|
||||||
)
|
)
|
||||||
drawRect(
|
|
||||||
color = scheme.onSurface.copy(alpha = 0.4f),
|
|
||||||
size = Size(width = bufferRatio * size.width, height = trackHeight),
|
|
||||||
topLeft = Offset(0f, trackTop)
|
|
||||||
)
|
|
||||||
val progressWidth = progressRatio * size.width
|
|
||||||
drawRect(
|
|
||||||
color = scheme.primary,
|
|
||||||
size = Size(width = progressWidth, height = trackHeight),
|
|
||||||
topLeft = Offset(0f, trackTop)
|
|
||||||
)
|
|
||||||
val thumbRadius = if (isFocused) 9.dp.toPx() else 7.dp.toPx()
|
|
||||||
val thumbCenter = Offset(progressWidth.coerceIn(0f, size.width), size.height / 2)
|
|
||||||
drawCircle(
|
|
||||||
color = scheme.primary,
|
|
||||||
radius = thumbRadius,
|
|
||||||
center = thumbCenter
|
|
||||||
)
|
|
||||||
if (isFocused) {
|
|
||||||
drawCircle(
|
|
||||||
color = scheme.primary.copy(alpha = 0.3f),
|
|
||||||
radius = thumbRadius + 4.dp.toPx(),
|
|
||||||
center = thumbCenter
|
|
||||||
)
|
|
||||||
}
|
|
||||||
combinedMarkers.forEach { marker ->
|
|
||||||
val x = (marker.positionMs.toFloat() / safeDuration) * size.width
|
|
||||||
val color = if (marker.type == MarkerType.AD) scheme.secondary else scheme.primary
|
|
||||||
drawRect(
|
|
||||||
color = color,
|
|
||||||
topLeft = Offset(x - 1f, size.height / 2 - 6f),
|
|
||||||
size = Size(width = 2f, height = 12f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.tv.player.components
|
package hu.bbara.purefin.ui.screen.player.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.content.series
|
package hu.bbara.purefin.ui.screen.series
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -16,16 +16,21 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.common.ui.components.MediaDetailOverviewSection
|
import hu.bbara.purefin.ui.common.media.MediaDetailOverviewSection
|
||||||
import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
|
import hu.bbara.purefin.ui.common.media.MediaDetailSectionTitle
|
||||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox
|
import hu.bbara.purefin.ui.common.media.TvMediaDetailBodyBox
|
||||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
|
import hu.bbara.purefin.ui.common.media.TvMediaDetailScaffold
|
||||||
import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl
|
import hu.bbara.purefin.ui.common.media.tvMediaDetailBackgroundImageUrl
|
||||||
import hu.bbara.purefin.feature.shared.navigation.SeriesDto
|
import hu.bbara.purefin.ui.screen.series.components.CastRow
|
||||||
|
import hu.bbara.purefin.ui.screen.series.components.SeriesFirstSeasonTabTag
|
||||||
|
import hu.bbara.purefin.ui.screen.series.components.TvEpisodeCarousel
|
||||||
|
import hu.bbara.purefin.ui.screen.series.components.TvSeasonTabs
|
||||||
|
import hu.bbara.purefin.ui.screen.series.components.TvSeriesHeroSection
|
||||||
|
import hu.bbara.purefin.core.navigation.SeriesDto
|
||||||
import hu.bbara.purefin.core.model.Season
|
import hu.bbara.purefin.core.model.Season
|
||||||
import hu.bbara.purefin.core.model.Series
|
import hu.bbara.purefin.core.model.Series
|
||||||
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
|
import hu.bbara.purefin.feature.content.series.SeriesViewModel
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.content.series
|
package hu.bbara.purefin.ui.screen.series.components
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -8,8 +8,6 @@ import androidx.compose.foundation.horizontalScroll
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
@@ -53,34 +51,30 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.MediaCastRow
|
import hu.bbara.purefin.ui.common.media.MediaCastRow
|
||||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaResumeButton
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
|
||||||
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
import hu.bbara.purefin.ui.common.media.MediaMetadataFlowRow
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlayButtonText
|
||||||
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.core.model.CastMember
|
import hu.bbara.purefin.core.model.CastMember
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.core.model.Season
|
import hu.bbara.purefin.core.model.Season
|
||||||
import hu.bbara.purefin.core.model.Series
|
import hu.bbara.purefin.core.model.Series
|
||||||
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
|
import hu.bbara.purefin.feature.content.series.SeriesViewModel
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
|
|
||||||
internal const val SeriesPlayButtonTag = "series-play-button"
|
internal const val SeriesPlayButtonTag = "series-play-button"
|
||||||
internal const val SeriesFirstSeasonTabTag = "series-first-season-tab"
|
internal const val SeriesFirstSeasonTabTag = "series-first-season-tab"
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TvSeriesMetaChips(series: Series) {
|
internal fun TvSeriesMetaChips(series: Series) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
MediaMetadataFlowRow(
|
||||||
FlowRow(
|
items = listOf(series.year, "${series.seasonCount} Seasons")
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
)
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
MediaMetaChip(text = series.year)
|
|
||||||
MediaMetaChip(text = "${series.seasonCount} Seasons")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -241,14 +235,21 @@ internal fun TvSeriesHeroSection(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
MediaResumeButton(
|
MediaResumeButton(
|
||||||
text = nextUpEpisode.playButtonText(),
|
text = mediaPlayButtonText(nextUpEpisode.progress, nextUpEpisode.watched),
|
||||||
progress = nextUpEpisode.progress?.div(100)?.toFloat() ?: 0f,
|
progress = mediaPlaybackProgress(nextUpEpisode.progress),
|
||||||
onClick = { onPlayEpisode(nextUpEpisode) },
|
onClick = { onPlayEpisode(nextUpEpisode) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.sizeIn(minWidth = 216.dp, maxWidth = 240.dp)
|
.sizeIn(minWidth = 216.dp, maxWidth = 240.dp)
|
||||||
.focusRequester(playFocusRequester)
|
.focusRequester(playFocusRequester)
|
||||||
.focusProperties { down = firstContentFocusRequester }
|
.focusProperties { down = firstContentFocusRequester }
|
||||||
.testTag(SeriesPlayButtonTag)
|
.testTag(SeriesPlayButtonTag),
|
||||||
|
focusedScale = 1.08f,
|
||||||
|
focusHaloColor = scheme.primary.copy(alpha = 0.22f),
|
||||||
|
focusBorderWidth = 3.dp,
|
||||||
|
focusBorderColor = scheme.onBackground,
|
||||||
|
overlayBorderWidth = 2.dp,
|
||||||
|
overlayBorderColor = scheme.primary.copy(alpha = 0.95f),
|
||||||
|
focusable = true
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
@@ -296,7 +297,7 @@ private fun TvEpisodeCard(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
model = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
@@ -335,7 +336,7 @@ private fun TvEpisodeCard(
|
|||||||
.align(Alignment.BottomStart)
|
.align(Alignment.BottomStart)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
WatchStateIndicator(
|
WatchStateBadge(
|
||||||
watched = episode.watched,
|
watched = episode.watched,
|
||||||
started = (episode.progress ?: 0.0) > 0.0,
|
started = (episode.progress ?: 0.0) > 0.0,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -376,10 +377,6 @@ internal fun CastRow(cast: List<CastMember>, modifier: Modifier = Modifier) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Episode.playButtonText(): String {
|
|
||||||
return if ((progress ?: 0.0) > 0.0 && !watched) "Resume" else "Play"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Episode.heroStatusText(): String {
|
private fun Episode.heroStatusText(): String {
|
||||||
return if ((progress ?: 0.0) > 0.0 && !watched) "Continue Watching" else "Up Next"
|
return if ((progress ?: 0.0) > 0.0 && !watched) "Continue Watching" else "Up Next"
|
||||||
}
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package hu.bbara.purefin.app.content.episode
|
|
||||||
|
|
||||||
import hu.bbara.purefin.core.data.navigation.Route
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertNotNull
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class EpisodeTopBarShortcutTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `home route exposes series shortcut`() {
|
|
||||||
var clicked = false
|
|
||||||
|
|
||||||
val shortcut = episodeTopBarShortcut(Route.Home) {
|
|
||||||
clicked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNotNull(shortcut)
|
|
||||||
assertEquals("Series", shortcut?.label)
|
|
||||||
|
|
||||||
shortcut?.onClick?.invoke()
|
|
||||||
|
|
||||||
assertTrue(clicked)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `non home route hides series shortcut`() {
|
|
||||||
val shortcut = episodeTopBarShortcut(
|
|
||||||
previousRoute = Route.PlayerRoute(mediaId = "episode-1"),
|
|
||||||
onSeriesClick = {}
|
|
||||||
)
|
|
||||||
|
|
||||||
assertNull(shortcut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package hu.bbara.purefin.tv.di
|
|
||||||
|
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class TvNoOpMediaDownloadControllerTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `tv no-op download controller always reports no downloads`() = runBlocking {
|
|
||||||
assertEquals(emptyMap<String, Float>(), TvNoOpMediaDownloadController.observeActiveDownloads().first())
|
|
||||||
assertEquals(
|
|
||||||
DownloadState.NotDownloaded,
|
|
||||||
TvNoOpMediaDownloadController.observeDownloadState("movie-1").value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +1,34 @@
|
|||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.core.model.Series
|
import hu.bbara.purefin.core.model.Series
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class TvFocusedItemHeroTest {
|
class TvFocusedItemHeroTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun movieItem_mapsProgressMetadataAndBackdrop() {
|
fun movieItem_mapsMetadataAndBackdrop() {
|
||||||
val movie = sampleMovie(progress = 42.0)
|
val movie = sampleMovie(progress = 42.0)
|
||||||
val item = ContinueWatchingItem(
|
val item = ContinueWatchingItem(
|
||||||
type = BaseItemKind.MOVIE,
|
type = MediaKind.MOVIE,
|
||||||
movie = movie
|
movie = movie
|
||||||
)
|
)
|
||||||
|
|
||||||
val hero = item.toTvFocusedHeroModel()
|
val hero = item.toTvFocusedHeroModel()
|
||||||
|
|
||||||
assertEquals("Continue watching", hero.eyebrowText)
|
|
||||||
assertEquals("Blade Runner 2049", hero.title)
|
assertEquals("Blade Runner 2049", hero.title)
|
||||||
assertEquals("2017 • 16+ • 164m • 4K", hero.metadataText)
|
assertEquals("2017 • 16+ • 164m • 4K", hero.metadataText)
|
||||||
assertEquals(0.42f, hero.progressFraction!!, 0.0001f)
|
|
||||||
assertEquals("42%", hero.progressLabel)
|
|
||||||
assertNull(hero.watchedText)
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.BACKDROP),
|
ImageUrlBuilder.finishImageUrl(movie.imageUrlPrefix, ArtworkKind.BACKDROP),
|
||||||
hero.backdropImageUrl
|
hero.backdropImageUrl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -50,7 +45,7 @@ class TvFocusedItemHeroTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun episodeItem_prefersWatchedStateOverProgress() {
|
fun episodeItem_mapsEpisodeMetadata() {
|
||||||
val item = NextUpItem(
|
val item = NextUpItem(
|
||||||
episode = sampleEpisode(
|
episode = sampleEpisode(
|
||||||
progress = 96.0,
|
progress = 96.0,
|
||||||
@@ -60,29 +55,21 @@ class TvFocusedItemHeroTest {
|
|||||||
|
|
||||||
val hero = item.toTvFocusedHeroModel()
|
val hero = item.toTvFocusedHeroModel()
|
||||||
|
|
||||||
assertEquals("Next up", hero.eyebrowText)
|
|
||||||
assertEquals("The Very Pulse of the Machine", hero.title)
|
assertEquals("The Very Pulse of the Machine", hero.title)
|
||||||
assertEquals("Episode 3 • 2022-05-20 • 16m • 12+ • HD", hero.metadataText)
|
assertEquals("Episode 3 • 2022-05-20 • 16m • 12+ • HD", hero.metadataText)
|
||||||
assertEquals("Watched", hero.watchedText)
|
|
||||||
assertNull(hero.progressFraction)
|
|
||||||
assertNull(hero.progressLabel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun seriesItem_usesSeasonAndUnwatchedMetadataWithoutProgress() {
|
fun seriesItem_usesSeasonAndUnwatchedMetadata() {
|
||||||
val item = PosterItem(
|
val item = PosterItem(
|
||||||
type = BaseItemKind.SERIES,
|
type = MediaKind.SERIES,
|
||||||
series = sampleSeries()
|
series = sampleSeries()
|
||||||
)
|
)
|
||||||
|
|
||||||
val hero = item.toTvFocusedHeroModel()
|
val hero = item.toTvFocusedHeroModel()
|
||||||
|
|
||||||
assertEquals("Series", hero.eyebrowText)
|
|
||||||
assertEquals("Love, Death & Robots", hero.title)
|
assertEquals("Love, Death & Robots", hero.title)
|
||||||
assertEquals("2019 • 3 seasons • 5 unwatched", hero.metadataText)
|
assertEquals("2019 • 3 seasons • 5 unwatched", hero.metadataText)
|
||||||
assertNull(hero.watchedText)
|
|
||||||
assertNull(hero.progressFraction)
|
|
||||||
assertNull(hero.progressLabel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sampleMovie(progress: Double? = 42.0): Movie {
|
private fun sampleMovie(progress: Double? = 42.0): Movie {
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
package hu.bbara.purefin.tv.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
|
import hu.bbara.purefin.core.model.LibraryKind
|
||||||
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
@@ -108,7 +108,7 @@ class TvHomeHeroStateTest {
|
|||||||
return LibraryItem(
|
return LibraryItem(
|
||||||
id = id,
|
id = id,
|
||||||
name = "Movies",
|
name = "Movies",
|
||||||
type = CollectionType.MOVIES,
|
type = LibraryKind.MOVIES,
|
||||||
posterUrl = "",
|
posterUrl = "",
|
||||||
isEmpty = isEmpty
|
isEmpty = isEmpty
|
||||||
)
|
)
|
||||||
@@ -116,7 +116,7 @@ class TvHomeHeroStateTest {
|
|||||||
|
|
||||||
private fun continueWatchingItem(id: String, title: String): ContinueWatchingItem {
|
private fun continueWatchingItem(id: String, title: String): ContinueWatchingItem {
|
||||||
return ContinueWatchingItem(
|
return ContinueWatchingItem(
|
||||||
type = BaseItemKind.MOVIE,
|
type = MediaKind.MOVIE,
|
||||||
movie = sampleMovie(id = id, title = title)
|
movie = sampleMovie(id = id, title = title)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ class TvHomeHeroStateTest {
|
|||||||
|
|
||||||
private fun posterItem(id: String, libraryId: UUID): PosterItem {
|
private fun posterItem(id: String, libraryId: UUID): PosterItem {
|
||||||
return PosterItem(
|
return PosterItem(
|
||||||
type = BaseItemKind.MOVIE,
|
type = MediaKind.MOVIE,
|
||||||
movie = sampleMovie(id = id, title = "Arrival", libraryId = libraryId)
|
movie = sampleMovie(id = id, title = "Arrival", libraryId = libraryId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -51,10 +51,20 @@ kotlin {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":core:model"))
|
implementation(project(":core:model"))
|
||||||
|
implementation(project(":core:image"))
|
||||||
|
implementation(project(":core:ui"))
|
||||||
implementation(project(":core:data"))
|
implementation(project(":core:data"))
|
||||||
implementation(project(":feature:download"))
|
implementation(project(":core:navigation"))
|
||||||
|
implementation(project(":core:download"))
|
||||||
implementation(project(":core:player"))
|
implementation(project(":core:player"))
|
||||||
implementation(project(":feature:shared"))
|
implementation(project(":data:jellyfin"))
|
||||||
|
implementation(project(":data:offline"))
|
||||||
|
implementation(project(":data:catalog"))
|
||||||
|
implementation(project(":feature:browse"))
|
||||||
|
implementation(project(":feature:search"))
|
||||||
|
implementation(project(":feature:content"))
|
||||||
|
implementation(project(":feature:downloads"))
|
||||||
|
implementation(project(":feature:login"))
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
@@ -83,8 +93,6 @@ dependencies {
|
|||||||
implementation(libs.media3.datasource.okhttp)
|
implementation(libs.media3.datasource.okhttp)
|
||||||
implementation(libs.androidx.navigation3.runtime)
|
implementation(libs.androidx.navigation3.runtime)
|
||||||
implementation(libs.androidx.navigation3.ui)
|
implementation(libs.androidx.navigation3.ui)
|
||||||
implementation(libs.androidx.room.ktx)
|
|
||||||
ksp(libs.androidx.room.compiler)
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Theme.Purefin" />
|
android:theme="@style/Theme.Purefin" />
|
||||||
<service
|
<service
|
||||||
android:name="hu.bbara.purefin.feature.download.PurefinDownloadService"
|
android:name="hu.bbara.purefin.core.download.PurefinDownloadService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -36,15 +36,15 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
|||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
import coil3.util.DebugLogger
|
import coil3.util.DebugLogger
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
import hu.bbara.purefin.core.data.SessionBootstrapper
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
import hu.bbara.purefin.data.jellyfin.client.JellyfinAuthInterceptor
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor
|
|
||||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||||
import hu.bbara.purefin.feature.shared.navigation.NavigationCommand
|
|
||||||
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
|
|
||||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationManager
|
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||||
|
import hu.bbara.purefin.core.navigation.NavigationCommand
|
||||||
|
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||||
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
import hu.bbara.purefin.ui.theme.backgroundDark
|
import hu.bbara.purefin.ui.theme.backgroundDark
|
||||||
@@ -65,7 +65,7 @@ class PurefinActivity : ComponentActivity() {
|
|||||||
lateinit var navigationManager: NavigationManager
|
lateinit var navigationManager: NavigationManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var jellyfinApiClient: JellyfinApiClient
|
lateinit var sessionBootstrapper: SessionBootstrapper
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var authInterceptor: JellyfinAuthInterceptor
|
lateinit var authInterceptor: JellyfinAuthInterceptor
|
||||||
@@ -93,7 +93,7 @@ class PurefinActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun init() {
|
private suspend fun init() {
|
||||||
jellyfinApiClient.updateApiClient()
|
sessionBootstrapper.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureImageLoader() {
|
private fun configureImageLoader() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.components.ActivityRetainedComponent
|
import dagger.hilt.android.components.ActivityRetainedComponent
|
||||||
import dagger.multibindings.IntoSet
|
import dagger.multibindings.IntoSet
|
||||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
import hu.bbara.purefin.navigation.appRouteEntryBuilder
|
import hu.bbara.purefin.navigation.appRouteEntryBuilder
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import dagger.Module
|
|||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import hu.bbara.purefin.core.data.client.PlaybackProfileFamily
|
import hu.bbara.purefin.core.data.PlaybackProfileFamily
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package hu.bbara.purefin.navigation
|
|||||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
|
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
|
|
||||||
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
||||||
staticCompositionLocalOf { error("NavigationManager not provided") }
|
staticCompositionLocalOf { error("NavigationManager not provided") }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package hu.bbara.purefin.navigation
|
package hu.bbara.purefin.navigation
|
||||||
|
|
||||||
import androidx.navigation3.runtime.EntryProviderScope
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
import hu.bbara.purefin.ui.screen.library.LibraryScreen
|
import hu.bbara.purefin.ui.screen.library.LibraryScreen
|
||||||
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
||||||
import hu.bbara.purefin.ui.screen.AppScreen
|
import hu.bbara.purefin.ui.screen.AppScreen
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.feature.shared.search.SearchViewModel
|
import hu.bbara.purefin.feature.search.SearchViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultTopBar(
|
fun DefaultTopBar(
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package hu.bbara.purefin.ui.common.bar
|
|
||||||
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package hu.bbara.purefin.ui.common.button
|
|
||||||
|
|
||||||
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 GhostIconButton(
|
|
||||||
icon: ImageVector,
|
|
||||||
contentDescription: String,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.size(52.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(scheme.background.copy(alpha = 0.65f))
|
|
||||||
.clickable { onClick() },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = contentDescription,
|
|
||||||
tint = scheme.onBackground
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package hu.bbara.purefin.ui.common.button
|
|
||||||
|
|
||||||
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.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 MediaActionButton(
|
|
||||||
backgroundColor: Color,
|
|
||||||
iconColor: Color,
|
|
||||||
icon: ImageVector,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
height: Dp,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.size(height)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(backgroundColor.copy(alpha = 0.6f))
|
|
||||||
.clickable { onClick() },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(imageVector = icon, contentDescription = null, tint = iconColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package hu.bbara.purefin.ui.common.button
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
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.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
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.clip
|
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MediaResumeButton(
|
|
||||||
text: String,
|
|
||||||
progress: Float = 0f,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val primaryColor = MaterialTheme.colorScheme.primary
|
|
||||||
val onPrimaryColor = MaterialTheme.colorScheme.onPrimary
|
|
||||||
|
|
||||||
BoxWithConstraints(
|
|
||||||
modifier = modifier
|
|
||||||
.height(52.dp)
|
|
||||||
.clip(RoundedCornerShape(50))
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
) {
|
|
||||||
// Bottom layer: inverted colors (visible for the remaining %)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(onPrimaryColor),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
ButtonContent(text = text, color = Color.White)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Top layer: primary colors, clipped to the progress %
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.drawWithContent {
|
|
||||||
val clipWidth = size.width * progress
|
|
||||||
clipRect(
|
|
||||||
left = 0f,
|
|
||||||
top = 0f,
|
|
||||||
right = clipWidth,
|
|
||||||
bottom = size.height
|
|
||||||
) {
|
|
||||||
this@drawWithContent.drawContent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(primaryColor),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
ButtonContent(text = text, color = Color.White)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ButtonContent(text: String, color: Color) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Text(text, color = color, fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Icon(Icons.Filled.PlayArrow, null, tint = color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package hu.bbara.purefin.ui.common.button
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,10 @@
|
|||||||
package hu.bbara.purefin.ui.common.card
|
package hu.bbara.purefin.ui.common.card
|
||||||
|
|
||||||
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.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
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.feature.shared.home.PosterItem
|
|
||||||
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator
|
|
||||||
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
|
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
|
||||||
import java.util.UUID
|
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PosterCard(
|
fun PosterCard(
|
||||||
@@ -38,83 +14,58 @@ fun PosterCard(
|
|||||||
onSeriesSelected: (UUID) -> Unit,
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
PosterCardContent(
|
||||||
val context = LocalContext.current
|
model = item.toPosterCardModel(),
|
||||||
val density = LocalDensity.current
|
onClick = { item.open(onMovieSelected, onSeriesSelected, onEpisodeSelected) },
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val posterWidth = 144.dp
|
internal fun PosterItem.toPosterCardModel(): PosterCardModel {
|
||||||
val posterHeight = posterWidth * 3 / 2
|
return PosterCardModel(
|
||||||
|
title = title,
|
||||||
fun openItem(posterItem: PosterItem) {
|
secondaryText = secondaryText,
|
||||||
when (posterItem.type) {
|
imageUrl = imageUrl,
|
||||||
MediaKind.MOVIE -> onMovieSelected(posterItem.id)
|
mediaKind = type,
|
||||||
MediaKind.SERIES -> onSeriesSelected(posterItem.id)
|
badge = when (type) {
|
||||||
MediaKind.EPISODE -> {
|
MediaKind.MOVIE -> {
|
||||||
val ep = posterItem.episode!!
|
val movie = requireNotNull(movie)
|
||||||
onEpisodeSelected(ep.seriesId, ep.seasonId, ep.id)
|
PosterCardBadge.WatchState(
|
||||||
}
|
watched = movie.watched,
|
||||||
else -> {}
|
started = (movie.progress ?: 0.0) > 0.0
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
MediaKind.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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
MediaKind.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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
MediaKind.SERIES -> UnwatchedEpisodeIndicator(
|
|
||||||
size = 28,
|
|
||||||
modifier = Modifier.align(Alignment.TopEnd)
|
|
||||||
.padding(8.dp),
|
|
||||||
unwatchedCount = item.series!!.unwatchedEpisodeCount
|
|
||||||
)
|
)
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MediaKind.EPISODE -> {
|
||||||
|
val episode = requireNotNull(episode)
|
||||||
|
PosterCardBadge.WatchState(
|
||||||
|
watched = episode.watched,
|
||||||
|
started = (episode.progress ?: 0.0) > 0.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaKind.SERIES -> PosterCardBadge.UnwatchedEpisodes(
|
||||||
|
count = requireNotNull(series).unwatchedEpisodeCount
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> PosterCardBadge.None
|
||||||
}
|
}
|
||||||
Text(
|
)
|
||||||
text = item.title,
|
}
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = 13.sp,
|
private fun PosterItem.open(
|
||||||
fontWeight = FontWeight.Medium,
|
onMovieSelected: (UUID) -> Unit,
|
||||||
modifier = Modifier.padding(top = 8.dp, start = 4.dp, end = 4.dp, bottom = 8.dp),
|
onSeriesSelected: (UUID) -> Unit,
|
||||||
maxLines = 1,
|
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||||
overflow = TextOverflow.Ellipsis
|
) {
|
||||||
)
|
when (type) {
|
||||||
|
MediaKind.MOVIE -> onMovieSelected(id)
|
||||||
|
MediaKind.SERIES -> onSeriesSelected(id)
|
||||||
|
MediaKind.EPISODE -> {
|
||||||
|
val ep = requireNotNull(episode)
|
||||||
|
onEpisodeSelected(ep.seriesId, ep.seasonId, ep.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
import hu.bbara.purefin.feature.shared.home.AppViewModel
|
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
||||||
import hu.bbara.purefin.ui.screen.download.DownloadsScreen
|
import hu.bbara.purefin.ui.screen.download.DownloadsScreen
|
||||||
import hu.bbara.purefin.ui.screen.home.HomeScreen
|
import hu.bbara.purefin.ui.screen.home.HomeScreen
|
||||||
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.feature.shared.download.ActiveDownloadItem
|
import hu.bbara.purefin.feature.downloads.ActiveDownloadItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DownloadingItemRow(
|
internal fun DownloadingItemRow(
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.ui.common.card.PosterCard
|
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||||
import hu.bbara.purefin.feature.shared.download.DownloadsViewModel
|
import hu.bbara.purefin.feature.downloads.DownloadsViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadsContent(
|
fun DownloadsContent(
|
||||||
|
|||||||
@@ -5,11 +5,8 @@ import android.os.Build
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -34,22 +31,22 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.ui.common.media.MediaMetaChip
|
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.ui.common.media.MediaMetadataFlowRow
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.download.DownloadState
|
||||||
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
|
import hu.bbara.purefin.core.navigation.EpisodeDto
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
||||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
import hu.bbara.purefin.core.model.CastMember
|
import hu.bbara.purefin.core.model.CastMember
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
import hu.bbara.purefin.feature.content.episode.EpisodeScreenViewModel
|
||||||
import hu.bbara.purefin.ui.screen.episode.components.EpisodeDetails
|
import hu.bbara.purefin.ui.screen.episode.components.EpisodeDetails
|
||||||
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBar
|
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBar
|
||||||
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBarShortcut
|
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBarShortcut
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -167,7 +164,7 @@ private fun EpisodeHeroSection(
|
|||||||
.height(sectionHeight)
|
.height(sectionHeight)
|
||||||
) {
|
) {
|
||||||
MediaHero(
|
MediaHero(
|
||||||
imageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
imageUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
backgroundColor = scheme.background,
|
backgroundColor = scheme.background,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
@@ -211,25 +208,12 @@ private fun EpisodeHeroSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EpisodeMetaChips(episode: Episode) {
|
private fun EpisodeMetaChips(episode: Episode) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
MediaMetadataFlowRow(
|
||||||
|
items = listOf(episode.releaseDate, episode.rating, episode.runtime, episode.format),
|
||||||
FlowRow(
|
highlightedItem = episode.format
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
)
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
MediaMetaChip(text = episode.releaseDate)
|
|
||||||
MediaMetaChip(text = episode.rating)
|
|
||||||
MediaMetaChip(text = episode.runtime)
|
|
||||||
MediaMetaChip(
|
|
||||||
text = episode.format,
|
|
||||||
background = scheme.primary.copy(alpha = 0.2f),
|
|
||||||
border = scheme.primary.copy(alpha = 0.3f),
|
|
||||||
textColor = scheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
|
|||||||
@@ -36,11 +36,13 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.ui.common.media.MediaCastRow
|
import hu.bbara.purefin.ui.common.media.MediaCastRow
|
||||||
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlayButtonText
|
||||||
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||||
import hu.bbara.purefin.ui.common.button.MediaActionButton
|
import hu.bbara.purefin.ui.common.button.MediaActionButton
|
||||||
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
||||||
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.core.download.DownloadState
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
import hu.bbara.purefin.player.PlayerActivity
|
||||||
|
|
||||||
@@ -134,8 +136,8 @@ internal fun EpisodeDetails(
|
|||||||
|
|
||||||
Row() {
|
Row() {
|
||||||
MediaResumeButton(
|
MediaResumeButton(
|
||||||
text = if (episode.progress == null) "Play" else "Resume",
|
text = mediaPlayButtonText(episode.progress, episode.watched),
|
||||||
progress = episode.progress?.div(100)?.toFloat() ?: 0f,
|
progress = mediaPlaybackProgress(episode.progress),
|
||||||
onClick = playAction,
|
onClick = playAction,
|
||||||
modifier = Modifier.sizeIn(maxWidth = 200.dp)
|
modifier = Modifier.sizeIn(maxWidth = 200.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ import androidx.compose.ui.Modifier
|
|||||||
import hu.bbara.purefin.ui.screen.home.components.HomeContent
|
import hu.bbara.purefin.ui.screen.home.components.HomeContent
|
||||||
import hu.bbara.purefin.ui.screen.home.components.HomeTopBar
|
import hu.bbara.purefin.ui.screen.home.components.HomeTopBar
|
||||||
import hu.bbara.purefin.ui.screen.home.components.search.HomeSearchOverlay
|
import hu.bbara.purefin.ui.screen.home.components.search.HomeSearchOverlay
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
import hu.bbara.purefin.feature.browse.home.SuggestedItem
|
||||||
import hu.bbara.purefin.ui.screen.AppBottomBar
|
import hu.bbara.purefin.ui.screen.AppBottomBar
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
import hu.bbara.purefin.feature.browse.home.SuggestedItem
|
||||||
import hu.bbara.purefin.ui.screen.home.components.continuewatching.ContinueWatchingSection
|
import hu.bbara.purefin.ui.screen.home.components.continuewatching.ContinueWatchingSection
|
||||||
import hu.bbara.purefin.ui.screen.home.components.featured.SuggestionsSection
|
import hu.bbara.purefin.ui.screen.home.components.featured.SuggestionsSection
|
||||||
import hu.bbara.purefin.ui.screen.home.components.library.LibraryPosterSection
|
import hu.bbara.purefin.ui.screen.home.components.library.LibraryPosterSection
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ContinueWatchingCard(
|
internal fun ContinueWatchingCard(
|
||||||
@@ -53,13 +53,13 @@ internal fun ContinueWatchingCard(
|
|||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
val imageUrl = when (item.type) {
|
val imageUrl = when (item.type) {
|
||||||
MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
MediaKind.MOVIE -> ImageUrlBuilder.finishImageUrl(
|
||||||
prefixImageUrl = item.movie?.imageUrlPrefix,
|
prefixImageUrl = item.movie?.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
artworkKind = ArtworkKind.PRIMARY
|
||||||
)
|
)
|
||||||
MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
MediaKind.EPISODE -> ImageUrlBuilder.finishImageUrl(
|
||||||
prefixImageUrl = item.episode?.imageUrlPrefix,
|
prefixImageUrl = item.episode?.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
artworkKind = ArtworkKind.PRIMARY
|
||||||
)
|
)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
import hu.bbara.purefin.feature.browse.home.SuggestedItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SuggestionCard(
|
internal fun SuggestionCard(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
import hu.bbara.purefin.feature.browse.home.SuggestedItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SuggestionsSection(
|
fun SuggestionsSection(
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
|
||||||
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator
|
|
||||||
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
|
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
|
||||||
import java.util.UUID
|
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
|
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeBadge
|
||||||
|
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
|
||||||
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun HomeBrowseCard(
|
internal fun HomeBrowseCard(
|
||||||
@@ -94,7 +94,7 @@ internal fun HomeBrowseCard(
|
|||||||
when (item.type) {
|
when (item.type) {
|
||||||
MediaKind.MOVIE -> {
|
MediaKind.MOVIE -> {
|
||||||
val movie = item.movie!!
|
val movie = item.movie!!
|
||||||
WatchStateIndicator(
|
WatchStateBadge(
|
||||||
size = 28,
|
size = 28,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
@@ -106,7 +106,7 @@ internal fun HomeBrowseCard(
|
|||||||
|
|
||||||
MediaKind.EPISODE -> {
|
MediaKind.EPISODE -> {
|
||||||
val episode = item.episode!!
|
val episode = item.episode!!
|
||||||
WatchStateIndicator(
|
WatchStateBadge(
|
||||||
size = 28,
|
size = 28,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
@@ -117,7 +117,7 @@ internal fun HomeBrowseCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
MediaKind.SERIES -> {
|
MediaKind.SERIES -> {
|
||||||
UnwatchedEpisodeIndicator(
|
UnwatchedEpisodeBadge(
|
||||||
size = 28,
|
size = 28,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun NextUpCard(
|
internal fun NextUpCard(
|
||||||
@@ -57,7 +57,7 @@ internal fun NextUpCard(
|
|||||||
.background(scheme.surface)
|
.background(scheme.surface)
|
||||||
) {
|
) {
|
||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = JellyfinImageHelper.finishImageUrl(item.episode.imageUrlPrefix, ImageType.PRIMARY),
|
model = ImageUrlBuilder.finishImageUrl(item.episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
contentDescription = item.primaryText,
|
contentDescription = item.primaryText,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.feature.shared.search.SearchViewModel
|
import hu.bbara.purefin.feature.search.SearchViewModel
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
import hu.bbara.purefin.feature.shared.search.SearchResult
|
import hu.bbara.purefin.feature.search.SearchResult
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.ui.common.card.PosterCard
|
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||||
import hu.bbara.purefin.ui.common.button.PurefinIconButton
|
import hu.bbara.purefin.ui.common.button.PurefinIconButton
|
||||||
import hu.bbara.purefin.feature.shared.navigation.LibraryDto
|
import hu.bbara.purefin.core.navigation.LibraryDto
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
|
import hu.bbara.purefin.feature.browse.library.LibraryViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryScreen(
|
fun LibraryScreen(
|
||||||
|
|||||||
@@ -1,29 +1,5 @@
|
|||||||
package hu.bbara.purefin.ui.screen.login
|
package hu.bbara.purefin.ui.screen.login
|
||||||
|
|
||||||
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.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -31,17 +7,9 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
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 androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.ui.common.textfield.PurefinComplexTextField
|
import hu.bbara.purefin.feature.login.LoginViewModel
|
||||||
import hu.bbara.purefin.ui.common.textfield.PurefinPasswordField
|
|
||||||
import hu.bbara.purefin.ui.common.button.PurefinTextButton
|
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
|
||||||
import hu.bbara.purefin.feature.shared.login.LoginViewModel
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -49,166 +17,52 @@ fun LoginScreen(
|
|||||||
viewModel: LoginViewModel = hiltViewModel(),
|
viewModel: LoginViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
// Observe ViewModel state
|
|
||||||
val serverUrl by viewModel.url.collectAsState()
|
val serverUrl by viewModel.url.collectAsState()
|
||||||
val username by viewModel.username.collectAsState()
|
val username by viewModel.username.collectAsState()
|
||||||
val password by viewModel.password.collectAsState()
|
val password by viewModel.password.collectAsState()
|
||||||
val errorMessage by viewModel.errorMessage.collectAsState()
|
val errorMessage by viewModel.errorMessage.collectAsState()
|
||||||
var isLoggingIn by remember { mutableStateOf(false) }
|
var isLoggingIn by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (isLoggingIn) {
|
val state = remember(serverUrl, username, password, errorMessage) {
|
||||||
PurefinWaitingScreen(modifier = modifier)
|
LoginContentState(
|
||||||
} else {
|
serverUrl = serverUrl,
|
||||||
Column(
|
username = username,
|
||||||
modifier = modifier
|
password = password,
|
||||||
.fillMaxSize()
|
errorMessage = errorMessage
|
||||||
.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, // Replace with actual logo resource
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
val callbacks = remember(viewModel, coroutineScope) {
|
||||||
|
LoginContentCallbacks(
|
||||||
|
onServerUrlChange = {
|
||||||
|
viewModel.clearError()
|
||||||
|
viewModel.setUrl(it)
|
||||||
|
},
|
||||||
|
onUsernameChange = {
|
||||||
|
viewModel.clearError()
|
||||||
|
viewModel.setUsername(it)
|
||||||
|
},
|
||||||
|
onPasswordChange = {
|
||||||
|
viewModel.clearError()
|
||||||
|
viewModel.setPassword(it)
|
||||||
|
},
|
||||||
|
onConnect = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
isLoggingIn = true
|
||||||
|
try {
|
||||||
|
viewModel.login()
|
||||||
|
} finally {
|
||||||
|
isLoggingIn = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginContent(
|
||||||
|
state = state,
|
||||||
|
callbacks = callbacks,
|
||||||
|
isLoggingIn = isLoggingIn,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import android.os.Build
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -32,19 +30,19 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.ui.common.media.MediaMetaChip
|
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.ui.common.media.MediaMetadataFlowRow
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.download.DownloadState
|
||||||
import hu.bbara.purefin.feature.shared.navigation.MovieDto
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
|
import hu.bbara.purefin.core.navigation.MovieDto
|
||||||
import hu.bbara.purefin.core.model.CastMember
|
import hu.bbara.purefin.core.model.CastMember
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
|
import hu.bbara.purefin.feature.content.movie.MovieScreenViewModel
|
||||||
import hu.bbara.purefin.ui.screen.movie.components.MovieDetails
|
import hu.bbara.purefin.ui.screen.movie.components.MovieDetails
|
||||||
import hu.bbara.purefin.ui.screen.movie.components.MovieTopBar
|
import hu.bbara.purefin.ui.screen.movie.components.MovieTopBar
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -139,7 +137,7 @@ fun MediaHeroSection(
|
|||||||
.height(sectionHeight)
|
.height(sectionHeight)
|
||||||
) {
|
) {
|
||||||
MediaHero(
|
MediaHero(
|
||||||
imageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
|
imageUrl = ImageUrlBuilder.finishImageUrl(movie.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
backgroundColor = MaterialTheme.colorScheme.background,
|
backgroundColor = MaterialTheme.colorScheme.background,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
@@ -170,20 +168,10 @@ fun MediaHeroSection(
|
|||||||
lineHeight = 38.sp
|
lineHeight = 38.sp
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
FlowRow(
|
MediaMetadataFlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
items = listOf(movie.year, movie.rating, movie.runtime, movie.format),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
highlightedItem = movie.format
|
||||||
) {
|
)
|
||||||
MediaMetaChip(text = movie.year)
|
|
||||||
MediaMetaChip(text = movie.rating)
|
|
||||||
MediaMetaChip(text = movie.runtime)
|
|
||||||
MediaMetaChip(
|
|
||||||
text = movie.format,
|
|
||||||
background = scheme.primary.copy(alpha = 0.2f),
|
|
||||||
border = scheme.primary.copy(alpha = 0.3f),
|
|
||||||
textColor = scheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.ui.common.media.MediaCastRow
|
import hu.bbara.purefin.ui.common.media.MediaCastRow
|
||||||
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress
|
||||||
|
import hu.bbara.purefin.ui.common.media.mediaPlayButtonText
|
||||||
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||||
import hu.bbara.purefin.ui.common.button.MediaActionButton
|
import hu.bbara.purefin.ui.common.button.MediaActionButton
|
||||||
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
||||||
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.core.download.DownloadState
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
import hu.bbara.purefin.player.PlayerActivity
|
||||||
|
|
||||||
@@ -92,8 +94,8 @@ internal fun MovieDetails(
|
|||||||
|
|
||||||
Row() {
|
Row() {
|
||||||
MediaResumeButton(
|
MediaResumeButton(
|
||||||
text = if (movie.progress == null) "Play" else "Resume",
|
text = mediaPlayButtonText(movie.progress, movie.watched),
|
||||||
progress = movie.progress?.div(100)?.toFloat() ?: 0f,
|
progress = mediaPlaybackProgress(movie.progress),
|
||||||
onClick = playAction,
|
onClick = playAction,
|
||||||
modifier = Modifier.sizeIn(maxWidth = 200.dp)
|
modifier = Modifier.sizeIn(maxWidth = 200.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user