mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
Compare commits
11 Commits
4c986432f6
...
84a95b0af6
| Author | SHA1 | Date | |
|---|---|---|---|
| 84a95b0af6 | |||
| 8a7577d5bb | |||
| 22c414b5e2 | |||
| 3d01f0faf3 | |||
| 3e3d99f1f2 | |||
| 790474d992 | |||
| 8a1d5ccfab | |||
| 72671d9ac3 | |||
| 5595a15820 | |||
| bd5e43114b | |||
| 8cc85d1525 |
70
AGENTS.md
Normal file
70
AGENTS.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Purefin AGENTS.md
|
||||
|
||||
Follow these instructions before making changes:
|
||||
|
||||
* Understand the relevant files before editing.
|
||||
* Always follow official Android recommendations and best practices.
|
||||
* Always include the pages you have read in your reply, even in planning mode.
|
||||
* Prefer small, targeted changes over broad refactors.
|
||||
* Preserve existing architecture and naming unless the task requires a change.
|
||||
* Do not introduce new dependencies without a clear reason.
|
||||
* Ask for clarification or leave a note when requirements are ambiguous.
|
||||
|
||||
---
|
||||
|
||||
## Repository Expectations
|
||||
|
||||
* Always use the currently used syntax in the codebase. Include examples for any naming you introduce.
|
||||
* Avoid touching unrelated files.
|
||||
* Do not fix unrelated existing problems in the codebase.
|
||||
* Prefer consistency with the existing codebase over idealized patterns.
|
||||
* When modifying a file, match its local style and conventions.
|
||||
* Favor readable, maintainable code over clever code.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
* Running a build is sufficient to verify your implementation.
|
||||
* Do not write tests unless explicitly asked to.
|
||||
|
||||
---
|
||||
|
||||
## Git and Commit Rules
|
||||
|
||||
When asked to prepare a commit:
|
||||
|
||||
* Use **Conventional Commits** format.
|
||||
* Format:
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
```
|
||||
* The description should not be too long, but must include any major change.
|
||||
* Use imperative mood.
|
||||
* Keep the subject concise.
|
||||
|
||||
**Common types:**
|
||||
|
||||
* `feat` – new feature
|
||||
* `fix` – bug fix
|
||||
* `refactor` – code change without feature or fix
|
||||
* `test` – adding or updating tests
|
||||
* `docs` – documentation changes
|
||||
* `chore` – maintenance tasks
|
||||
* `build` – build system changes
|
||||
* `ci` – CI/CD changes
|
||||
|
||||
---
|
||||
|
||||
## Preferred Workflow
|
||||
|
||||
1. Read the task carefully.
|
||||
2. Read relevant official Google/Android documentation.
|
||||
3. Inspect the relevant files and nearby code.
|
||||
4. Choose the simplest solution aligned with Google’s standards.
|
||||
5. Check if the code can be simplified:
|
||||
|
||||
* If yes, simplify it.
|
||||
* Do not reduce readability.
|
||||
6. Summarize the result, including anything not verified.
|
||||
@@ -22,7 +22,7 @@ import hu.bbara.purefin.model.Movie
|
||||
import hu.bbara.purefin.ui.model.EpisodeUiModel
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.model.MovieUiModel
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
import hu.bbara.purefin.ui.screen.home.TvHomeScreen
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -385,9 +385,9 @@ class TvHomeContentTest {
|
||||
)
|
||||
}
|
||||
|
||||
private fun sampleLibraries(): List<LibraryItem> {
|
||||
private fun sampleLibraries(): List<LibraryUiModel> {
|
||||
return listOf(
|
||||
LibraryItem(
|
||||
LibraryUiModel(
|
||||
id = sampleLibraryId(),
|
||||
name = "Movies",
|
||||
type = LibraryKind.MOVIES,
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performClick
|
||||
import hu.bbara.purefin.model.LibraryKind
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
@@ -20,14 +20,14 @@ class TvLibrariesOverviewScreenTest {
|
||||
@Test
|
||||
fun tvLibrariesOverviewScreen_opensSelectedLibrary() {
|
||||
val libraries = listOf(
|
||||
LibraryItem(
|
||||
LibraryUiModel(
|
||||
id = UUID.fromString("11111111-1111-1111-1111-111111111111"),
|
||||
name = "Movies",
|
||||
type = LibraryKind.MOVIES,
|
||||
posterUrl = "",
|
||||
isEmpty = false
|
||||
),
|
||||
LibraryItem(
|
||||
LibraryUiModel(
|
||||
id = UUID.fromString("22222222-2222-2222-2222-222222222222"),
|
||||
name = "Shows",
|
||||
type = LibraryKind.SERIES,
|
||||
@@ -35,7 +35,7 @@ class TvLibrariesOverviewScreenTest {
|
||||
isEmpty = false
|
||||
)
|
||||
)
|
||||
var openedLibrary: LibraryItem? = null
|
||||
var openedLibrary: LibraryUiModel? = null
|
||||
|
||||
composeRule.setContent {
|
||||
AppTheme {
|
||||
|
||||
@@ -13,14 +13,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.model.MovieUiModel
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
import hu.bbara.purefin.ui.screen.home.components.TvFocusedItemHero
|
||||
import hu.bbara.purefin.ui.screen.home.components.TvHomeContent
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
fun TvHomeScreen(
|
||||
libraries: List<LibraryItem>,
|
||||
libraries: List<LibraryUiModel>,
|
||||
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||
continueWatching: List<MediaUiModel>,
|
||||
nextUp: List<MediaUiModel>,
|
||||
|
||||
@@ -24,7 +24,7 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
import java.util.UUID
|
||||
|
||||
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
||||
@@ -32,7 +32,7 @@ internal const val TvHomeContentViewportTag = "tv-home-content-viewport"
|
||||
|
||||
@Composable
|
||||
fun TvHomeContent(
|
||||
libraries: List<LibraryItem>,
|
||||
libraries: List<LibraryUiModel>,
|
||||
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||
continueWatching: List<MediaUiModel>,
|
||||
nextUp: List<MediaUiModel>,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package hu.bbara.purefin.ui.screen.home.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.Image
|
||||
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
|
||||
@@ -13,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -38,9 +35,6 @@ import androidx.tv.material3.MaterialTheme as TvMaterialTheme
|
||||
internal const val TvDrawerItemTagPrefix = "tv-drawer-item-"
|
||||
internal const val TvDrawerTitleTag = "tv-drawer-title"
|
||||
|
||||
private val TvDrawerCollapsedWidth = 92.dp
|
||||
private val TvDrawerExpandedWidth = 280.dp
|
||||
|
||||
@Composable
|
||||
fun TvNavigationDrawer(
|
||||
destinations: List<TvDrawerDestinationItem>,
|
||||
@@ -52,9 +46,9 @@ fun TvNavigationDrawer(
|
||||
ProvideTvDrawerTheme {
|
||||
NavigationDrawer(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
drawerContent = {
|
||||
drawerContent = { drawerValue ->
|
||||
TvNavigationDrawerRail(
|
||||
drawerValue = if (hasFocus) DrawerValue.Open else DrawerValue.Closed,
|
||||
drawerValue = drawerValue,
|
||||
destinations = destinations,
|
||||
selectedDestination = selectedDestination,
|
||||
onDestinationSelected = onDestinationSelected
|
||||
@@ -137,54 +131,42 @@ private fun androidx.tv.material3.NavigationDrawerScope.TvNavigationDrawerRail(
|
||||
onDestinationSelected: (Route) -> Unit,
|
||||
) {
|
||||
val expanded = drawerValue == DrawerValue.Open
|
||||
val drawerWidth = animateDpAsState(
|
||||
targetValue = if (expanded) TvDrawerExpandedWidth else TvDrawerCollapsedWidth,
|
||||
label = "tv-drawer-width"
|
||||
)
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
Box(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(drawerWidth.value)
|
||||
.fillMaxHeight()
|
||||
.background(scheme.surface.copy(alpha = 0.96f))
|
||||
.padding(horizontal = 12.dp, vertical = 24.dp)
|
||||
.selectableGroup(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp, vertical = 24.dp)
|
||||
.selectableGroup(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
TvDrawerHeader(
|
||||
expanded = expanded,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
)
|
||||
destinations.forEachIndexed { index, destination ->
|
||||
val isSelected = destination.destination == selectedDestination
|
||||
NavigationDrawerItem(
|
||||
selected = isSelected,
|
||||
onClick = { onDestinationSelected(destination.destination) },
|
||||
modifier = Modifier
|
||||
.testTag("$TvDrawerItemTagPrefix$index")
|
||||
.semantics { selected = isSelected },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = destination.icon,
|
||||
contentDescription = destination.label
|
||||
)
|
||||
}
|
||||
) {
|
||||
if (expanded) {
|
||||
Text(
|
||||
text = destination.label,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
TvDrawerHeader(
|
||||
expanded = expanded,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
)
|
||||
destinations.forEachIndexed { index, destination ->
|
||||
val isSelected = destination.destination == selectedDestination
|
||||
NavigationDrawerItem(
|
||||
selected = isSelected,
|
||||
onClick = { onDestinationSelected(destination.destination) },
|
||||
modifier = Modifier
|
||||
.testTag("$TvDrawerItemTagPrefix$index")
|
||||
.semantics { selected = isSelected },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = destination.icon,
|
||||
contentDescription = destination.label
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = destination.label,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
import hu.bbara.purefin.model.LibraryKind
|
||||
|
||||
internal const val TvLibrariesOverviewItemTagPrefix = "tv-libraries-overview-item-"
|
||||
@@ -52,8 +52,8 @@ private val TvLibrariesOverviewCardShape = RoundedCornerShape(18.dp)
|
||||
|
||||
@Composable
|
||||
fun TvLibrariesOverviewScreen(
|
||||
libraries: List<LibraryItem>,
|
||||
onLibrarySelected: (LibraryItem) -> Unit,
|
||||
libraries: List<LibraryUiModel>,
|
||||
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
@@ -117,7 +117,7 @@ fun TvLibrariesOverviewScreen(
|
||||
|
||||
@Composable
|
||||
private fun TvLibraryOverviewCard(
|
||||
item: LibraryItem,
|
||||
item: LibraryUiModel,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import hu.bbara.purefin.model.LibraryKind
|
||||
import hu.bbara.purefin.model.MediaKind
|
||||
import hu.bbara.purefin.model.Movie
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -104,8 +104,8 @@ class TvHomeHeroStateTest {
|
||||
assertEquals(continueWatching, registry.firstAvailableItem)
|
||||
}
|
||||
|
||||
private fun sampleLibrary(id: UUID, isEmpty: Boolean = false): LibraryItem {
|
||||
return LibraryItem(
|
||||
private fun sampleLibrary(id: UUID, isEmpty: Boolean = false): LibraryUiModel {
|
||||
return LibraryUiModel(
|
||||
id = id,
|
||||
name = "Movies",
|
||||
type = LibraryKind.MOVIES,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package hu.bbara.purefin.ui.screen
|
||||
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -12,7 +10,6 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
@@ -20,13 +17,11 @@ import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.scene.Scene
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
||||
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||
import hu.bbara.purefin.ui.screen.download.DownloadsScreen
|
||||
import hu.bbara.purefin.ui.screen.home.HomeScreen
|
||||
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
||||
import hu.bbara.purefin.ui.screen.libraries.LibrariesScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -48,14 +43,6 @@ fun AppScreen(
|
||||
val currentRoute = backStack.lastOrNull() ?: AppTabRoute.Home
|
||||
val selectedTab = currentRoute.toTabIndex()
|
||||
|
||||
val libraryNavItems = libraries.map {
|
||||
HomeNavItem(
|
||||
id = it.id,
|
||||
label = it.name,
|
||||
posterUrl = it.posterUrl
|
||||
)
|
||||
}
|
||||
|
||||
LifecycleResumeEffect(Unit) {
|
||||
viewModel.onResumed()
|
||||
onPauseOrDispose { }
|
||||
@@ -97,8 +84,8 @@ fun AppScreen(
|
||||
}
|
||||
entry<AppTabRoute.Libraries>(metadata = appTabMetadata(AppTabRoute.Libraries)) {
|
||||
LibrariesScreen(
|
||||
items = libraryNavItems,
|
||||
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.label) },
|
||||
items = libraries,
|
||||
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.name) },
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = onTabSelected,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
@@ -125,9 +112,9 @@ fun AppScreen(
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator()
|
||||
),
|
||||
transitionSpec = { appTabTransition() },
|
||||
popTransitionSpec = { appTabTransition() },
|
||||
predictivePopTransitionSpec = { _ -> appTabTransition() },
|
||||
transitionSpec = { fadeIn(tween(220)) togetherWith fadeOut(tween(220)) },
|
||||
popTransitionSpec = { fadeIn(tween(220)) togetherWith fadeOut(tween(220)) },
|
||||
predictivePopTransitionSpec = { _ -> fadeIn(tween(220)) togetherWith fadeOut(tween(220)) },
|
||||
entryProvider = tabEntryProvider,
|
||||
modifier = modifier.fillMaxSize()
|
||||
)
|
||||
@@ -158,27 +145,6 @@ private fun Int.toAppTabRoute(): AppTabRoute = when (this) {
|
||||
else -> AppTabRoute.Home
|
||||
}
|
||||
|
||||
private fun AnimatedContentTransitionScope<Scene<AppTabRoute>>.appTabTransition(): ContentTransform {
|
||||
val initialIndex = initialState.metadata.appTabIndex()
|
||||
val targetIndex = targetState.metadata.appTabIndex()
|
||||
val animationSpec = tween<IntOffset>(durationMillis = 140)
|
||||
|
||||
return when {
|
||||
targetIndex > initialIndex -> {
|
||||
slideInHorizontally(animationSpec = animationSpec) { fullWidth -> fullWidth } togetherWith
|
||||
slideOutHorizontally(animationSpec = animationSpec) { fullWidth -> -fullWidth }
|
||||
}
|
||||
targetIndex < initialIndex -> {
|
||||
slideInHorizontally(animationSpec = animationSpec) { fullWidth -> -fullWidth } togetherWith
|
||||
slideOutHorizontally(animationSpec = animationSpec) { fullWidth -> fullWidth }
|
||||
}
|
||||
else -> {
|
||||
slideInHorizontally(animationSpec = animationSpec) { 0 } togetherWith
|
||||
slideOutHorizontally(animationSpec = animationSpec) { 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun appTabMetadata(route: AppTabRoute): Map<String, Any> =
|
||||
mapOf(APP_TAB_INDEX_METADATA to route.toTabIndex())
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.screen.AppBottomBar
|
||||
import hu.bbara.purefin.ui.screen.home.components.HomeContent
|
||||
@@ -23,7 +23,7 @@ import java.util.UUID
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
libraries: List<LibraryItem>,
|
||||
libraries: List<LibraryUiModel>,
|
||||
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||
suggestions: List<MediaUiModel>,
|
||||
continueWatching: List<MediaUiModel>,
|
||||
@@ -31,7 +31,7 @@ fun HomeScreen(
|
||||
isRefreshing: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
onLibrarySelected: (LibraryItem) -> Unit,
|
||||
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||
onProfileClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onLogoutClick: () -> Unit,
|
||||
|
||||
@@ -22,7 +22,7 @@ import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
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.library.LibraryPosterSection
|
||||
@@ -32,7 +32,7 @@ import java.util.UUID
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeContent(
|
||||
libraries: List<LibraryItem>,
|
||||
libraries: List<LibraryUiModel>,
|
||||
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||
suggestions: List<MediaUiModel>,
|
||||
continueWatching: List<MediaUiModel>,
|
||||
@@ -40,7 +40,7 @@ fun HomeContent(
|
||||
isRefreshing: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
onLibrarySelected: (LibraryItem) -> Unit,
|
||||
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||
onBrowseLibrariesClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package hu.bbara.purefin.ui.screen.home.components
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class HomeNavItem(
|
||||
val id: UUID,
|
||||
val label: String,
|
||||
val posterUrl: String,
|
||||
val selected: Boolean = false
|
||||
)
|
||||
|
||||
data class HomeUser(
|
||||
val name: String,
|
||||
val plan: String
|
||||
)
|
||||
@@ -12,14 +12,14 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||
|
||||
@Composable
|
||||
fun LibraryPosterSection(
|
||||
library: LibraryItem,
|
||||
library: LibraryUiModel,
|
||||
items: List<MediaUiModel>,
|
||||
onLibrarySelected: (LibraryItem) -> Unit,
|
||||
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
package hu.bbara.purefin.ui.screen.home.components.search
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
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.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.LocalFireDepartment
|
||||
import androidx.compose.material.icons.outlined.Menu
|
||||
import androidx.compose.material.icons.outlined.Movie
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material.icons.outlined.TrendingUp
|
||||
import androidx.compose.material.icons.outlined.Tune
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
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.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import hu.bbara.purefin.ui.common.image.PurefinLogo
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun HomeSearchFullScreen(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(scheme.background)
|
||||
.statusBarsPadding()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 20.dp)
|
||||
.padding(bottom = 28.dp)
|
||||
) {
|
||||
SearchHeader(modifier = Modifier.padding(top = 12.dp))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
SearchField()
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
SectionTitle(
|
||||
text = "Trending Searches",
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.LocalFireDepartment,
|
||||
contentDescription = null,
|
||||
tint = scheme.primary,
|
||||
modifier = Modifier.size(26.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
previewTrendingSearches.forEach { title ->
|
||||
TrendingSearchChip(title = title)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(34.dp))
|
||||
SectionTitle(text = "Browse Categories")
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
CategoryGrid(categories = previewCategories)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
border = BorderStroke(1.dp, scheme.primary.copy(alpha = 0.52f)),
|
||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = scheme.primary),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
Text(
|
||||
text = "View All Genres",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchHeader(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
IconButton(onClick = {}) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Menu,
|
||||
contentDescription = "Menu",
|
||||
tint = scheme.primary,
|
||||
modifier = Modifier.size(30.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
PurefinLogo(
|
||||
contentDescription = "PureFin",
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(38.dp)
|
||||
)
|
||||
Text(
|
||||
text = "PureFin",
|
||||
color = scheme.primary,
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontStyle = FontStyle.Italic
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = scheme.surfaceContainer,
|
||||
border = BorderStroke(1.dp, scheme.outlineVariant.copy(alpha = 0.28f)),
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Person,
|
||||
contentDescription = "Profile",
|
||||
tint = scheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(26.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchField(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
Surface(
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = scheme.surfaceContainer,
|
||||
border = BorderStroke(1.dp, scheme.outlineVariant.copy(alpha = 0.16f)),
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(68.dp)
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Search,
|
||||
contentDescription = null,
|
||||
tint = scheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(30.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
text = "Search movies, shows, genres...",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = scheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(34.dp)
|
||||
.width(1.dp)
|
||||
.background(scheme.outlineVariant.copy(alpha = 0.18f))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(14.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Tune,
|
||||
contentDescription = "Filter",
|
||||
tint = scheme.primary,
|
||||
modifier = Modifier.size(30.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionTitle(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: @Composable (() -> Unit)? = null
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
icon?.invoke()
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = scheme.onBackground,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrendingSearchChip(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
Surface(
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = scheme.surfaceContainer,
|
||||
border = BorderStroke(1.dp, scheme.outlineVariant.copy(alpha = 0.14f)),
|
||||
modifier = modifier
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(horizontal = 18.dp, vertical = 11.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.TrendingUp,
|
||||
contentDescription = null,
|
||||
tint = scheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = scheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CategoryGrid(
|
||||
categories: List<SearchCategory>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
categories.chunked(2).forEach { rowItems ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
rowItems.forEach { category ->
|
||||
CategoryCard(
|
||||
category = category,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
if (rowItems.size == 1) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CategoryCard(
|
||||
category: SearchCategory,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val shape = RoundedCornerShape(18.dp)
|
||||
|
||||
Surface(
|
||||
shape = shape,
|
||||
color = scheme.surfaceContainer,
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.aspectRatio(1.45f)
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
PurefinAsyncImage(
|
||||
model = category.imageUrl,
|
||||
contentDescription = category.name,
|
||||
fallbackIcon = Icons.Outlined.Movie,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Black.copy(alpha = 0.18f),
|
||||
Color.Black.copy(alpha = 0.42f),
|
||||
Color.Black.copy(alpha = 0.76f)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = category.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun HomeSearchFullScreenPreview() {
|
||||
AppTheme {
|
||||
HomeSearchFullScreen(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
||||
private data class SearchCategory(
|
||||
val name: String,
|
||||
val imageUrl: String
|
||||
)
|
||||
|
||||
private val previewTrendingSearches = listOf(
|
||||
"Dune",
|
||||
"Severance",
|
||||
"The Last of Us",
|
||||
"Blade Runner",
|
||||
"Foundation"
|
||||
)
|
||||
|
||||
private val previewCategories = listOf(
|
||||
SearchCategory(
|
||||
name = "Action",
|
||||
imageUrl = "https://images.unsplash.com/photo-1535016120720-40c646be5580"
|
||||
),
|
||||
SearchCategory(
|
||||
name = "Adventure",
|
||||
imageUrl = "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee"
|
||||
),
|
||||
SearchCategory(
|
||||
name = "Comedy",
|
||||
imageUrl = "https://images.unsplash.com/photo-1527224857830-43a7acc85260"
|
||||
),
|
||||
SearchCategory(
|
||||
name = "Fantasy",
|
||||
imageUrl = "https://images.unsplash.com/photo-1518709268805-4e9042af2176"
|
||||
),
|
||||
SearchCategory(
|
||||
name = "Romance",
|
||||
imageUrl = "https://images.unsplash.com/photo-1516589178581-6cd7833ae3b2"
|
||||
),
|
||||
SearchCategory(
|
||||
name = "Sci-Fi",
|
||||
imageUrl = "https://images.unsplash.com/photo-1446776811953-b23d57bd21aa"
|
||||
)
|
||||
)
|
||||
@@ -6,15 +6,15 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
import hu.bbara.purefin.ui.screen.AppBottomBar
|
||||
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
||||
import hu.bbara.purefin.ui.screen.libraries.components.LibrariesContent
|
||||
import hu.bbara.purefin.ui.screen.library.components.LibraryTopBar
|
||||
|
||||
@Composable
|
||||
fun LibrariesScreen(
|
||||
items: List<HomeNavItem>,
|
||||
onLibrarySelected: (HomeNavItem) -> Unit,
|
||||
items: List<LibraryUiModel>,
|
||||
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||
selectedTab: Int,
|
||||
onTabSelected: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
|
||||
@@ -9,12 +9,12 @@ import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
|
||||
@Composable
|
||||
fun LibrariesContent(
|
||||
items: List<HomeNavItem>,
|
||||
onLibrarySelected: (HomeNavItem) -> Unit,
|
||||
items: List<LibraryUiModel>,
|
||||
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
@@ -26,7 +26,7 @@ fun LibrariesContent(
|
||||
) {
|
||||
items(items, key = { it.id }) { item ->
|
||||
LibraryListItem(
|
||||
item = item,
|
||||
uiModel = item,
|
||||
modifier = Modifier.clickable {
|
||||
onLibrarySelected(item)
|
||||
}
|
||||
|
||||
@@ -11,18 +11,18 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
|
||||
@Composable
|
||||
fun LibraryListItem(
|
||||
item: HomeNavItem,
|
||||
uiModel: LibraryUiModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
PurefinAsyncImage(
|
||||
model = item.posterUrl,
|
||||
contentDescription = item.label,
|
||||
model = uiModel.posterUrl,
|
||||
contentDescription = uiModel.name,
|
||||
contentScale = ContentScale.Inside,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -30,7 +30,7 @@ fun LibraryListItem(
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
)
|
||||
Text(
|
||||
text = item.label,
|
||||
text = uiModel.name,
|
||||
style = MaterialTheme.typography.displaySmall
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import hu.bbara.purefin.navigation.NavigationManager
|
||||
import hu.bbara.purefin.navigation.Route
|
||||
import hu.bbara.purefin.navigation.SeriesDto
|
||||
import hu.bbara.purefin.ui.model.EpisodeUiModel
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.model.MovieUiModel
|
||||
import hu.bbara.purefin.ui.model.SeriesUiModel
|
||||
@@ -51,12 +52,12 @@ class AppViewModel @Inject constructor(
|
||||
|
||||
val libraries = homeRepository.libraries.map { libraries ->
|
||||
libraries.map {
|
||||
LibraryItem(
|
||||
LibraryUiModel(
|
||||
id = it.id,
|
||||
name = it.name,
|
||||
type = it.type,
|
||||
posterUrl = it.posterUrl,
|
||||
isEmpty = when(it.type) {
|
||||
isEmpty = when (it.type) {
|
||||
LibraryKind.MOVIES -> mediaCatalogReader.movies.value.isEmpty()
|
||||
LibraryKind.SERIES -> mediaCatalogReader.series.value.isEmpty()
|
||||
}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
package hu.bbara.purefin.feature.browse.home
|
||||
|
||||
import hu.bbara.purefin.image.ArtworkKind
|
||||
import hu.bbara.purefin.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.model.Episode
|
||||
import hu.bbara.purefin.model.LibraryKind
|
||||
import hu.bbara.purefin.model.MediaKind
|
||||
import hu.bbara.purefin.model.Movie
|
||||
import hu.bbara.purefin.model.Series
|
||||
import java.util.UUID
|
||||
|
||||
sealed interface FocusableItem {
|
||||
val imageUrl: String
|
||||
val primaryText: String
|
||||
val secondaryText: String
|
||||
val description: String
|
||||
val id: UUID
|
||||
val type: MediaKind
|
||||
}
|
||||
|
||||
data class NextUpItem(
|
||||
val episode: Episode
|
||||
) : FocusableItem {
|
||||
override val id: UUID = episode.id
|
||||
override val type: MediaKind
|
||||
get() = MediaKind.EPISODE
|
||||
override val imageUrl: String
|
||||
get() = ImageUrlBuilder.finishImageUrl(
|
||||
prefixImageUrl = episode.imageUrlPrefix,
|
||||
artworkKind = ArtworkKind.PRIMARY
|
||||
)
|
||||
override val primaryText: String = episode.title
|
||||
override val secondaryText: String = episode.releaseDate
|
||||
override val description: String
|
||||
get() = episode.synopsis
|
||||
}
|
||||
|
||||
data class LibraryItem(
|
||||
val id: UUID,
|
||||
val name: String,
|
||||
val type: LibraryKind,
|
||||
val posterUrl: String,
|
||||
val isEmpty: Boolean
|
||||
)
|
||||
|
||||
data class PosterItem(
|
||||
override val type: MediaKind,
|
||||
val movie: Movie? = null,
|
||||
val series: Series? = null,
|
||||
val episode: Episode? = null
|
||||
) : FocusableItem {
|
||||
override val id: UUID = when (type) {
|
||||
MediaKind.MOVIE -> movie!!.id
|
||||
MediaKind.EPISODE -> episode!!.id
|
||||
MediaKind.SERIES -> series!!.id
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
val title: String = when (type) {
|
||||
MediaKind.MOVIE -> movie!!.title
|
||||
MediaKind.EPISODE -> episode!!.title
|
||||
MediaKind.SERIES -> series!!.name
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
override val imageUrl: String = when (type) {
|
||||
MediaKind.MOVIE -> ImageUrlBuilder.finishImageUrl(
|
||||
prefixImageUrl = movie!!.imageUrlPrefix,
|
||||
artworkKind = ArtworkKind.PRIMARY
|
||||
)
|
||||
MediaKind.EPISODE -> ImageUrlBuilder.finishImageUrl(
|
||||
prefixImageUrl = episode!!.imageUrlPrefix,
|
||||
artworkKind = ArtworkKind.PRIMARY
|
||||
)
|
||||
MediaKind.SERIES -> ImageUrlBuilder.finishImageUrl(
|
||||
prefixImageUrl = series!!.imageUrlPrefix,
|
||||
artworkKind = ArtworkKind.PRIMARY
|
||||
)
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
override val primaryText: String
|
||||
get() = when (type) {
|
||||
MediaKind.MOVIE -> movie!!.title
|
||||
MediaKind.EPISODE -> episode!!.title
|
||||
MediaKind.SERIES -> series!!.name
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
override val secondaryText: String
|
||||
get() = when (type) {
|
||||
MediaKind.MOVIE -> movie!!.year
|
||||
MediaKind.EPISODE -> episode!!.releaseDate
|
||||
MediaKind.SERIES -> series!!.year
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
override val description: String
|
||||
get() = when (type) {
|
||||
MediaKind.MOVIE -> movie!!.synopsis
|
||||
MediaKind.EPISODE -> episode!!.synopsis
|
||||
MediaKind.SERIES -> series!!.synopsis
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
|
||||
fun watched() = when (type) {
|
||||
MediaKind.MOVIE -> movie!!.watched
|
||||
MediaKind.EPISODE -> episode!!.watched
|
||||
else -> throw IllegalArgumentException("Invalid type: $type")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package hu.bbara.purefin.ui.model
|
||||
|
||||
import hu.bbara.purefin.model.LibraryKind
|
||||
import java.util.UUID
|
||||
|
||||
data class LibraryUiModel(
|
||||
val id: UUID,
|
||||
val name: String,
|
||||
val type: LibraryKind,
|
||||
val posterUrl: String,
|
||||
val isEmpty: Boolean
|
||||
)
|
||||
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.Response
|
||||
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
||||
import org.jellyfin.sdk.api.client.extensions.genresApi
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
||||
import org.jellyfin.sdk.api.client.extensions.mediaSegmentsApi
|
||||
@@ -250,6 +251,18 @@ class JellyfinApiClient @Inject constructor(
|
||||
nextUpEpisodes
|
||||
}
|
||||
|
||||
suspend fun getGenres(id: UUID? = null) : List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
val result = api.genresApi.getGenres(
|
||||
userId = getUserId(),
|
||||
parentId = id,
|
||||
)
|
||||
Log.d("getGenres", result.toString())
|
||||
result.content.items
|
||||
}
|
||||
|
||||
suspend fun getMediaSources(mediaId: UUID): List<MediaSourceInfo> = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
|
||||
Reference in New Issue
Block a user