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.EpisodeUiModel
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.model.MovieUiModel
|
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.screen.home.TvHomeScreen
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
@@ -385,9 +385,9 @@ class TvHomeContentTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sampleLibraries(): List<LibraryItem> {
|
private fun sampleLibraries(): List<LibraryUiModel> {
|
||||||
return listOf(
|
return listOf(
|
||||||
LibraryItem(
|
LibraryUiModel(
|
||||||
id = sampleLibraryId(),
|
id = sampleLibraryId(),
|
||||||
name = "Movies",
|
name = "Movies",
|
||||||
type = LibraryKind.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.onNodeWithTag
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import hu.bbara.purefin.model.LibraryKind
|
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 hu.bbara.purefin.ui.theme.AppTheme
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@@ -20,14 +20,14 @@ class TvLibrariesOverviewScreenTest {
|
|||||||
@Test
|
@Test
|
||||||
fun tvLibrariesOverviewScreen_opensSelectedLibrary() {
|
fun tvLibrariesOverviewScreen_opensSelectedLibrary() {
|
||||||
val libraries = listOf(
|
val libraries = listOf(
|
||||||
LibraryItem(
|
LibraryUiModel(
|
||||||
id = UUID.fromString("11111111-1111-1111-1111-111111111111"),
|
id = UUID.fromString("11111111-1111-1111-1111-111111111111"),
|
||||||
name = "Movies",
|
name = "Movies",
|
||||||
type = LibraryKind.MOVIES,
|
type = LibraryKind.MOVIES,
|
||||||
posterUrl = "",
|
posterUrl = "",
|
||||||
isEmpty = false
|
isEmpty = false
|
||||||
),
|
),
|
||||||
LibraryItem(
|
LibraryUiModel(
|
||||||
id = UUID.fromString("22222222-2222-2222-2222-222222222222"),
|
id = UUID.fromString("22222222-2222-2222-2222-222222222222"),
|
||||||
name = "Shows",
|
name = "Shows",
|
||||||
type = LibraryKind.SERIES,
|
type = LibraryKind.SERIES,
|
||||||
@@ -35,7 +35,7 @@ class TvLibrariesOverviewScreenTest {
|
|||||||
isEmpty = false
|
isEmpty = false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
var openedLibrary: LibraryItem? = null
|
var openedLibrary: LibraryUiModel? = null
|
||||||
|
|
||||||
composeRule.setContent {
|
composeRule.setContent {
|
||||||
AppTheme {
|
AppTheme {
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.model.MovieUiModel
|
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.TvFocusedItemHero
|
||||||
import hu.bbara.purefin.ui.screen.home.components.TvHomeContent
|
import hu.bbara.purefin.ui.screen.home.components.TvHomeContent
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvHomeScreen(
|
fun TvHomeScreen(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryUiModel>,
|
||||||
libraryContent: Map<UUID, List<MediaUiModel>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
continueWatching: List<MediaUiModel>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: 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.platform.testTag
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
||||||
@@ -32,7 +32,7 @@ internal const val TvHomeContentViewportTag = "tv-home-content-viewport"
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvHomeContent(
|
fun TvHomeContent(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryUiModel>,
|
||||||
libraryContent: Map<UUID, List<MediaUiModel>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
continueWatching: List<MediaUiModel>,
|
continueWatching: List<MediaUiModel>,
|
||||||
nextUp: List<MediaUiModel>,
|
nextUp: List<MediaUiModel>,
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package hu.bbara.purefin.ui.screen.home.components
|
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.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
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 TvDrawerItemTagPrefix = "tv-drawer-item-"
|
||||||
internal const val TvDrawerTitleTag = "tv-drawer-title"
|
internal const val TvDrawerTitleTag = "tv-drawer-title"
|
||||||
|
|
||||||
private val TvDrawerCollapsedWidth = 92.dp
|
|
||||||
private val TvDrawerExpandedWidth = 280.dp
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvNavigationDrawer(
|
fun TvNavigationDrawer(
|
||||||
destinations: List<TvDrawerDestinationItem>,
|
destinations: List<TvDrawerDestinationItem>,
|
||||||
@@ -52,9 +46,9 @@ fun TvNavigationDrawer(
|
|||||||
ProvideTvDrawerTheme {
|
ProvideTvDrawerTheme {
|
||||||
NavigationDrawer(
|
NavigationDrawer(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
drawerContent = {
|
drawerContent = { drawerValue ->
|
||||||
TvNavigationDrawerRail(
|
TvNavigationDrawerRail(
|
||||||
drawerValue = if (hasFocus) DrawerValue.Open else DrawerValue.Closed,
|
drawerValue = drawerValue,
|
||||||
destinations = destinations,
|
destinations = destinations,
|
||||||
selectedDestination = selectedDestination,
|
selectedDestination = selectedDestination,
|
||||||
onDestinationSelected = onDestinationSelected
|
onDestinationSelected = onDestinationSelected
|
||||||
@@ -137,54 +131,42 @@ private fun androidx.tv.material3.NavigationDrawerScope.TvNavigationDrawerRail(
|
|||||||
onDestinationSelected: (Route) -> Unit,
|
onDestinationSelected: (Route) -> Unit,
|
||||||
) {
|
) {
|
||||||
val expanded = drawerValue == DrawerValue.Open
|
val expanded = drawerValue == DrawerValue.Open
|
||||||
val drawerWidth = animateDpAsState(
|
|
||||||
targetValue = if (expanded) TvDrawerExpandedWidth else TvDrawerCollapsedWidth,
|
|
||||||
label = "tv-drawer-width"
|
|
||||||
)
|
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
Box(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(drawerWidth.value)
|
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.background(scheme.surface.copy(alpha = 0.96f))
|
.background(scheme.surface.copy(alpha = 0.96f))
|
||||||
|
.padding(horizontal = 12.dp, vertical = 24.dp)
|
||||||
|
.selectableGroup(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
TvDrawerHeader(
|
||||||
modifier = Modifier
|
expanded = expanded,
|
||||||
.fillMaxSize()
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||||
.padding(horizontal = 16.dp, vertical = 24.dp)
|
)
|
||||||
.selectableGroup(),
|
destinations.forEachIndexed { index, destination ->
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
val isSelected = destination.destination == selectedDestination
|
||||||
) {
|
NavigationDrawerItem(
|
||||||
TvDrawerHeader(
|
selected = isSelected,
|
||||||
expanded = expanded,
|
onClick = { onDestinationSelected(destination.destination) },
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
|
modifier = Modifier
|
||||||
)
|
.testTag("$TvDrawerItemTagPrefix$index")
|
||||||
destinations.forEachIndexed { index, destination ->
|
.semantics { selected = isSelected },
|
||||||
val isSelected = destination.destination == selectedDestination
|
leadingContent = {
|
||||||
NavigationDrawerItem(
|
Icon(
|
||||||
selected = isSelected,
|
imageVector = destination.icon,
|
||||||
onClick = { onDestinationSelected(destination.destination) },
|
contentDescription = destination.label
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
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.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.browse.home.LibraryItem
|
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||||
import hu.bbara.purefin.model.LibraryKind
|
import hu.bbara.purefin.model.LibraryKind
|
||||||
|
|
||||||
internal const val TvLibrariesOverviewItemTagPrefix = "tv-libraries-overview-item-"
|
internal const val TvLibrariesOverviewItemTagPrefix = "tv-libraries-overview-item-"
|
||||||
@@ -52,8 +52,8 @@ private val TvLibrariesOverviewCardShape = RoundedCornerShape(18.dp)
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvLibrariesOverviewScreen(
|
fun TvLibrariesOverviewScreen(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryUiModel>,
|
||||||
onLibrarySelected: (LibraryItem) -> Unit,
|
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
@@ -117,7 +117,7 @@ fun TvLibrariesOverviewScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TvLibraryOverviewCard(
|
private fun TvLibraryOverviewCard(
|
||||||
item: LibraryItem,
|
item: LibraryUiModel,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import hu.bbara.purefin.model.LibraryKind
|
|||||||
import hu.bbara.purefin.model.MediaKind
|
import hu.bbara.purefin.model.MediaKind
|
||||||
import hu.bbara.purefin.model.Movie
|
import hu.bbara.purefin.model.Movie
|
||||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
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.NextUpItem
|
||||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
@@ -104,8 +104,8 @@ class TvHomeHeroStateTest {
|
|||||||
assertEquals(continueWatching, registry.firstAvailableItem)
|
assertEquals(continueWatching, registry.firstAvailableItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sampleLibrary(id: UUID, isEmpty: Boolean = false): LibraryItem {
|
private fun sampleLibrary(id: UUID, isEmpty: Boolean = false): LibraryUiModel {
|
||||||
return LibraryItem(
|
return LibraryUiModel(
|
||||||
id = id,
|
id = id,
|
||||||
name = "Movies",
|
name = "Movies",
|
||||||
type = LibraryKind.MOVIES,
|
type = LibraryKind.MOVIES,
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package hu.bbara.purefin.ui.screen
|
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.core.tween
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -12,7 +10,6 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
import androidx.navigation3.runtime.NavBackStack
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
@@ -20,13 +17,11 @@ import androidx.navigation3.runtime.NavKey
|
|||||||
import androidx.navigation3.runtime.entryProvider
|
import androidx.navigation3.runtime.entryProvider
|
||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
import androidx.navigation3.scene.Scene
|
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationManager
|
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||||
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.libraries.LibrariesScreen
|
import hu.bbara.purefin.ui.screen.libraries.LibrariesScreen
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@@ -48,14 +43,6 @@ fun AppScreen(
|
|||||||
val currentRoute = backStack.lastOrNull() ?: AppTabRoute.Home
|
val currentRoute = backStack.lastOrNull() ?: AppTabRoute.Home
|
||||||
val selectedTab = currentRoute.toTabIndex()
|
val selectedTab = currentRoute.toTabIndex()
|
||||||
|
|
||||||
val libraryNavItems = libraries.map {
|
|
||||||
HomeNavItem(
|
|
||||||
id = it.id,
|
|
||||||
label = it.name,
|
|
||||||
posterUrl = it.posterUrl
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LifecycleResumeEffect(Unit) {
|
LifecycleResumeEffect(Unit) {
|
||||||
viewModel.onResumed()
|
viewModel.onResumed()
|
||||||
onPauseOrDispose { }
|
onPauseOrDispose { }
|
||||||
@@ -97,8 +84,8 @@ fun AppScreen(
|
|||||||
}
|
}
|
||||||
entry<AppTabRoute.Libraries>(metadata = appTabMetadata(AppTabRoute.Libraries)) {
|
entry<AppTabRoute.Libraries>(metadata = appTabMetadata(AppTabRoute.Libraries)) {
|
||||||
LibrariesScreen(
|
LibrariesScreen(
|
||||||
items = libraryNavItems,
|
items = libraries,
|
||||||
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.label) },
|
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.name) },
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = onTabSelected,
|
onTabSelected = onTabSelected,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
@@ -125,9 +112,9 @@ fun AppScreen(
|
|||||||
entryDecorators = listOf(
|
entryDecorators = listOf(
|
||||||
rememberSaveableStateHolderNavEntryDecorator()
|
rememberSaveableStateHolderNavEntryDecorator()
|
||||||
),
|
),
|
||||||
transitionSpec = { appTabTransition() },
|
transitionSpec = { fadeIn(tween(220)) togetherWith fadeOut(tween(220)) },
|
||||||
popTransitionSpec = { appTabTransition() },
|
popTransitionSpec = { fadeIn(tween(220)) togetherWith fadeOut(tween(220)) },
|
||||||
predictivePopTransitionSpec = { _ -> appTabTransition() },
|
predictivePopTransitionSpec = { _ -> fadeIn(tween(220)) togetherWith fadeOut(tween(220)) },
|
||||||
entryProvider = tabEntryProvider,
|
entryProvider = tabEntryProvider,
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
@@ -158,27 +145,6 @@ private fun Int.toAppTabRoute(): AppTabRoute = when (this) {
|
|||||||
else -> AppTabRoute.Home
|
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> =
|
private fun appTabMetadata(route: AppTabRoute): Map<String, Any> =
|
||||||
mapOf(APP_TAB_INDEX_METADATA to route.toTabIndex())
|
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.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
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.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.screen.AppBottomBar
|
import hu.bbara.purefin.ui.screen.AppBottomBar
|
||||||
import hu.bbara.purefin.ui.screen.home.components.HomeContent
|
import hu.bbara.purefin.ui.screen.home.components.HomeContent
|
||||||
@@ -23,7 +23,7 @@ import java.util.UUID
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryUiModel>,
|
||||||
libraryContent: Map<UUID, List<MediaUiModel>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
suggestions: List<MediaUiModel>,
|
suggestions: List<MediaUiModel>,
|
||||||
continueWatching: List<MediaUiModel>,
|
continueWatching: List<MediaUiModel>,
|
||||||
@@ -31,7 +31,7 @@ fun HomeScreen(
|
|||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onMediaSelected: (MediaUiModel) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
onLibrarySelected: (LibraryItem) -> Unit,
|
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||||
onProfileClick: () -> Unit,
|
onProfileClick: () -> Unit,
|
||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit,
|
||||||
onLogoutClick: () -> Unit,
|
onLogoutClick: () -> Unit,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ 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.ui.model.MediaUiModel
|
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.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
|
||||||
@@ -32,7 +32,7 @@ import java.util.UUID
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeContent(
|
fun HomeContent(
|
||||||
libraries: List<LibraryItem>,
|
libraries: List<LibraryUiModel>,
|
||||||
libraryContent: Map<UUID, List<MediaUiModel>>,
|
libraryContent: Map<UUID, List<MediaUiModel>>,
|
||||||
suggestions: List<MediaUiModel>,
|
suggestions: List<MediaUiModel>,
|
||||||
continueWatching: List<MediaUiModel>,
|
continueWatching: List<MediaUiModel>,
|
||||||
@@ -40,7 +40,7 @@ fun HomeContent(
|
|||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onMediaSelected: (MediaUiModel) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
onLibrarySelected: (LibraryItem) -> Unit,
|
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||||
onBrowseLibrariesClick: () -> Unit,
|
onBrowseLibrariesClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
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.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
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
|
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryPosterSection(
|
fun LibraryPosterSection(
|
||||||
library: LibraryItem,
|
library: LibraryUiModel,
|
||||||
items: List<MediaUiModel>,
|
items: List<MediaUiModel>,
|
||||||
onLibrarySelected: (LibraryItem) -> Unit,
|
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||||
onMediaSelected: (MediaUiModel) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
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.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
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.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.libraries.components.LibrariesContent
|
||||||
import hu.bbara.purefin.ui.screen.library.components.LibraryTopBar
|
import hu.bbara.purefin.ui.screen.library.components.LibraryTopBar
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibrariesScreen(
|
fun LibrariesScreen(
|
||||||
items: List<HomeNavItem>,
|
items: List<LibraryUiModel>,
|
||||||
onLibrarySelected: (HomeNavItem) -> Unit,
|
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||||
selectedTab: Int,
|
selectedTab: Int,
|
||||||
onTabSelected: (Int) -> Unit,
|
onTabSelected: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import androidx.compose.foundation.lazy.grid.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.ui.screen.home.components.HomeNavItem
|
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibrariesContent(
|
fun LibrariesContent(
|
||||||
items: List<HomeNavItem>,
|
items: List<LibraryUiModel>,
|
||||||
onLibrarySelected: (HomeNavItem) -> Unit,
|
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
@@ -26,7 +26,7 @@ fun LibrariesContent(
|
|||||||
) {
|
) {
|
||||||
items(items, key = { it.id }) { item ->
|
items(items, key = { it.id }) { item ->
|
||||||
LibraryListItem(
|
LibraryListItem(
|
||||||
item = item,
|
uiModel = item,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
onLibrarySelected(item)
|
onLibrarySelected(item)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,18 +11,18 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
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.common.image.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryListItem(
|
fun LibraryListItem(
|
||||||
item: HomeNavItem,
|
uiModel: LibraryUiModel,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier.fillMaxWidth()) {
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = item.posterUrl,
|
model = uiModel.posterUrl,
|
||||||
contentDescription = item.label,
|
contentDescription = uiModel.name,
|
||||||
contentScale = ContentScale.Inside,
|
contentScale = ContentScale.Inside,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -30,7 +30,7 @@ fun LibraryListItem(
|
|||||||
.clip(RoundedCornerShape(24.dp))
|
.clip(RoundedCornerShape(24.dp))
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = item.label,
|
text = uiModel.name,
|
||||||
style = MaterialTheme.typography.displaySmall
|
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.Route
|
||||||
import hu.bbara.purefin.navigation.SeriesDto
|
import hu.bbara.purefin.navigation.SeriesDto
|
||||||
import hu.bbara.purefin.ui.model.EpisodeUiModel
|
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.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.model.MovieUiModel
|
import hu.bbara.purefin.ui.model.MovieUiModel
|
||||||
import hu.bbara.purefin.ui.model.SeriesUiModel
|
import hu.bbara.purefin.ui.model.SeriesUiModel
|
||||||
@@ -51,12 +52,12 @@ class AppViewModel @Inject constructor(
|
|||||||
|
|
||||||
val libraries = homeRepository.libraries.map { libraries ->
|
val libraries = homeRepository.libraries.map { libraries ->
|
||||||
libraries.map {
|
libraries.map {
|
||||||
LibraryItem(
|
LibraryUiModel(
|
||||||
id = it.id,
|
id = it.id,
|
||||||
name = it.name,
|
name = it.name,
|
||||||
type = it.type,
|
type = it.type,
|
||||||
posterUrl = it.posterUrl,
|
posterUrl = it.posterUrl,
|
||||||
isEmpty = when(it.type) {
|
isEmpty = when (it.type) {
|
||||||
LibraryKind.MOVIES -> mediaCatalogReader.movies.value.isEmpty()
|
LibraryKind.MOVIES -> mediaCatalogReader.movies.value.isEmpty()
|
||||||
LibraryKind.SERIES -> mediaCatalogReader.series.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 kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.Response
|
import org.jellyfin.sdk.api.client.Response
|
||||||
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
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.itemsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.mediaSegmentsApi
|
import org.jellyfin.sdk.api.client.extensions.mediaSegmentsApi
|
||||||
@@ -250,6 +251,18 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
nextUpEpisodes
|
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) {
|
suspend fun getMediaSources(mediaId: UUID): List<MediaSourceInfo> = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) {
|
if (!ensureConfigured()) {
|
||||||
return@withContext emptyList()
|
return@withContext emptyList()
|
||||||
|
|||||||
Reference in New Issue
Block a user