Pārlūkot izejas kodu

code clean / icons for screens

media player fix

splash screen menu removal

Delete .gradle directory
Delete .idea directory
Create SECURITY.md
design improvements

git ignore items update

git ignore items update
Milan Jurkulak 3 gadi atpakaļ
vecāks
revīzija
3f5972ee1a
100 mainītis faili ar 1416 papildinājumiem un 930 dzēšanām
  1. 75 0
      .github/workflows/build-gradle-project.yml
  2. 45 0
      .gitignore
  3. 21 0
      SECURITY.md
  4. 5 6
      app/build.gradle.kts
  5. 2 4
      app/src/main/kotlin/org/mjdev/tvapp/activity/MainActivity.kt
  6. 11 11
      app/src/main/kotlin/org/mjdev/tvapp/api/MovieAPI.kt
  7. 4 2
      app/src/main/kotlin/org/mjdev/tvapp/base/activity/ComposableActivity.kt
  8. 0 25
      app/src/main/kotlin/org/mjdev/tvapp/base/helpers/Option.kt
  9. 25 0
      app/src/main/kotlin/org/mjdev/tvapp/base/helpers/Result.kt
  10. 1 0
      app/src/main/kotlin/org/mjdev/tvapp/base/helpers/StateContextWrapper.kt
  11. 27 0
      app/src/main/kotlin/org/mjdev/tvapp/base/media/IMediaPlayer.kt
  12. 2 0
      app/src/main/kotlin/org/mjdev/tvapp/base/media/MediaPlayerContainer.kt
  13. 16 6
      app/src/main/kotlin/org/mjdev/tvapp/base/media/MediaPlayerContainerDefaults.kt
  14. 17 12
      app/src/main/kotlin/org/mjdev/tvapp/base/media/MediaPlayerImpl.kt
  15. 10 0
      app/src/main/kotlin/org/mjdev/tvapp/base/media/PlayerControls.kt
  16. 6 4
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavGraphBuilderEx.kt
  17. 5 4
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavHostEx.kt
  18. 98 20
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/Screen.kt
  19. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/network/ConnectivityCallback.kt
  20. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/network/NetworkConnectivityServiceImpl.kt
  21. 34 0
      app/src/main/kotlin/org/mjdev/tvapp/base/page/Page.kt
  22. 26 0
      app/src/main/kotlin/org/mjdev/tvapp/base/page/Pager.kt
  23. 7 5
      app/src/main/kotlin/org/mjdev/tvapp/base/state/ScreenState.kt
  24. 54 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/CarouselCard.kt
  25. 46 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/MovieCard.kt
  26. 12 11
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Badge.kt
  27. 22 58
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/BigCarousel.kt
  28. 31 34
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Clock.kt
  29. 80 85
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ErrorMessage.kt
  30. 0 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/FocusableBox.kt
  31. 64 99
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Header.kt
  32. 83 83
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ListWithPreview.kt
  33. 12 13
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ScreenView.kt
  34. 20 38
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Tabs.kt
  35. 37 15
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Title.kt
  36. 5 4
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/UserPic.kt
  37. 8 23
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/image/CircleImage.kt
  38. 24 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/image/ImageAny.kt
  39. 8 3
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/Navigation.kt
  40. 0 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavigationRow.kt
  41. 19 33
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/text/CircleText.kt
  42. 3 3
      app/src/main/kotlin/org/mjdev/tvapp/repository/MovieRepository.kt
  43. 21 18
      app/src/main/kotlin/org/mjdev/tvapp/ui/components/FocusableCard.kt
  44. 3 9
      app/src/main/kotlin/org/mjdev/tvapp/ui/components/MediaPlayer.kt
  45. 97 95
      app/src/main/kotlin/org/mjdev/tvapp/ui/components/MyComposeView.kt
  46. 30 0
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/AboutPage.kt
  47. 122 0
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt
  48. 33 0
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/SubscriptionPage.kt
  49. 0 44
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/AboutScreen.kt
  50. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/DetailScreen.kt
  51. 22 97
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/MainScreen.kt
  52. 3 5
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/PlayerScreen.kt
  53. 101 0
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/SplashScreen.kt
  54. 0 44
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/SubscriptionScreen.kt
  55. 12 5
      app/src/main/res/values/strings.xml
  56. 0 3
      app/src/main/res/values/themes.xml
  57. 2 0
      buildSrc/.gitignore
  58. BIN
      buildSrc/.gradle/8.0/executionHistory/executionHistory.bin
  59. BIN
      buildSrc/.gradle/8.0/executionHistory/executionHistory.lock
  60. BIN
      buildSrc/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  61. BIN
      buildSrc/.gradle/file-system.probe
  62. BIN
      buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module
  63. BIN
      buildSrc/build/classes/kotlin/main/org/mjdev/gradle/extensions/DependencyHandlerKt.class
  64. BIN
      buildSrc/build/classes/kotlin/main/org/mjdev/gradle/helper/CustomEventLogger.class
  65. BIN
      buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin$Companion.class
  66. BIN
      buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin$apply$1.class
  67. BIN
      buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin$apply$3.class
  68. BIN
      buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin$apply$4.class
  69. BIN
      buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin.class
  70. 1 0
      buildSrc/build/kotlin/buildSrcjar-classes.txt
  71. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab
  72. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream
  73. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len
  74. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len
  75. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at
  76. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i
  77. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len
  78. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab
  79. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream
  80. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len
  81. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len
  82. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at
  83. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i
  84. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len
  85. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab
  86. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream
  87. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len
  88. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len
  89. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at
  90. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i
  91. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len
  92. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab
  93. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream
  94. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len
  95. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len
  96. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at
  97. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i
  98. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len
  99. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/inline-functions.tab
  100. BIN
      buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/inline-functions.tab.keystream

+ 75 - 0
.github/workflows/build-gradle-project.yml

@@ -0,0 +1,75 @@
+name: Build Gradle project
+# on settings
+on:
+  push:
+    branches:
+      - main
+# jobs
+jobs:
+  build-gradle-project:
+    runs-on: ubuntu-latest
+#permissions
+    permissions:
+      contents: write
+      deployments: write
+      pages: write
+# job steps
+    steps:
+      - name: Checkout project sources
+        id: checkout_project
+        uses: actions/checkout@v3
+# setup jdk 17
+      - name: Set up JDK 17
+        id: setup_java
+        uses: actions/setup-java@v3
+        with:
+          java-version: '17'
+          distribution: 'temurin'
+          cache: gradle
+# setup gradle
+      - name: Setup Gradle
+        id: setup_gradle
+        uses: gradle/gradle-build-action@v2
+# be sure it is executable
+      - name: Grant execute permission for gradlew
+        id: grant_gradle
+        run: chmod +x gradlew
+# generate version number to env
+      - name: Prepare Version Number
+        id: prepare_version
+        run: |
+          export GRADLE_PATH="./config/version.properties";
+          export GRADLE_FIELD_VMAJ="majorVersion"
+          export GRADLE_FIELD_VMIN="minorVersion"
+          export GRADLE_FIELD_VPAT="patchVersion"
+          export VERSION_VMAJ_TMP=$(grep $GRADLE_FIELD_VMAJ $GRADLE_PATH | cat $2)
+          export VERSION_VMIN_TMP=$(grep $GRADLE_FIELD_VMIN $GRADLE_PATH | cat $2)
+          export VERSION_VMPAT_TMP=$(grep $GRADLE_FIELD_VPAT $GRADLE_PATH | cat $2)
+          export VERSION_MAJOR=$(echo $VERSION_VMAJ_TMP | sed -e 's/^majorVersion=//'  -e 's/"$//')
+          export VERSION_MINOR=$(echo $VERSION_VMIN_TMP | sed -e 's/^minorVersion=//'  -e 's/"$//')
+          export VERSION_PATCH=$(echo $VERSION_VMPAT_TMP | sed -e 's/^patchVersion=//'  -e 's/"$//')
+          export APP_VERSION="$VERSION_MAJOR.$VERSION_MINOR.$VERSION_PATCH"
+          echo "Release Version : $APP_VERSION"
+          echo "GITHUB_APP_VERSION_NAME=$(echo $APP_VERSION)" >> $GITHUB_ENV
+# build apk for release
+      - name: Build Release APK
+        id: build_release_apk
+        run: ./gradlew assembleRelease
+# publish github release
+      - name: Create Release
+        id: release
+        uses: ncipollo/release-action@v1
+        with:
+          name: Release v ${{ env.GITHUB_APP_VERSION_NAME }}
+          allowUpdates: true
+          token: ${{ secrets.TOKEN }}
+          tag: release-${{ env.GITHUB_APP_VERSION_NAME }}
+          artifacts: './app/build/outputs/apk/release/app-release.apk, ./release-notes.txt'
+# update wiki pages
+      - name: Update wiki pages
+        id: update_wiki_pages
+        uses: OrlovM/Wiki-Action@v1
+        with:
+          path: 'wiki'
+          token: ${{ secrets.TOKEN }}
+# done

+ 45 - 0
.gitignore

@@ -0,0 +1,45 @@
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Log/OS Files
+*.log
+
+# Android Studio generated files and folders
+captures/
+.externalNativeBuild/
+.cxx/
+*.apk
+output.json
+
+# IntelliJ
+*.iml
+.idea/
+misc.xml
+deploymentTargetDropDown.xml
+render.experimental.xml
+
+# Keystore files
+*.jks
+*.keystore
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+# Android Profiling
+*.hprof
+/.gradle/
+/.idea/
+
+# buildSrc
+/buildSrc/.gradle/
+/buildSrc/build/
+buildSrc/.gradle/
+buildSrc/build/
+buildSrc/.gradle
+buildSrc/build
+/buildSrc/.gradle
+/buildSrc/build

+ 21 - 0
SECURITY.md

@@ -0,0 +1,21 @@
+# Security Policy
+
+## Supported Versions
+
+Use this section to tell people about which versions of your project are
+currently being supported with security updates.
+
+| Version | Supported          |
+| ------- | ------------------ |
+| 5.1.x   | :white_check_mark: |
+| 5.0.x   | :x:                |
+| 4.0.x   | :white_check_mark: |
+| < 4.0   | :x:                |
+
+## Reporting a Vulnerability
+
+Use this section to tell people how to report a vulnerability.
+
+Tell them where to go, how often they can expect to get an update on a
+reported vulnerability, what to expect if the vulnerability is accepted or
+declined, etc.

+ 5 - 6
app/build.gradle.kts

@@ -50,7 +50,7 @@ android {
     }
 
     defaultConfig {
-        applicationId = "org.mjdev.tvapp.tvcomposeintroduction"
+        applicationId = "org.mjdev.tvapp"
 
         minSdk = 21
         targetSdk = 33
@@ -127,20 +127,19 @@ dependencies {
     implementation(kotlin("reflect", "1.8.21"))
     // compose base libs
     implementation(
-        platform("androidx.compose:compose-bom:2023.06.00")
+        platform("androidx.compose:compose-bom:2023.06.01")
     )
     implementation("androidx.compose.ui:ui-tooling")
     implementation("androidx.activity:activity-compose:1.7.2")
-//    implementation( "com.google.android.material:material:1.9.0")
-//    implementation( "androidx.compose.ui:ui:1.7.2")
-//    implementation( "androidx.compose.material:material:1.7.2")
+    // more icons
+    implementation("androidx.compose.material:material-icons-extended")
     // tv compose
     implementation("androidx.tv:tv-foundation:1.0.0-alpha07")
     implementation("androidx.tv:tv-material:1.0.0-alpha07")
     // view model
     implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
     // navigation
-    implementation("androidx.navigation:navigation-compose:2.6.0")
+    implementation("androidx.navigation:navigation-compose:2.7.0-beta01")
     // async image loading
     implementation("io.coil-kt:coil-compose:2.4.0")
     // constraint layout

+ 2 - 4
app/src/main/kotlin/org/mjdev/tvapp/activity/MainActivity.kt

@@ -11,19 +11,17 @@ package org.mjdev.tvapp.activity
 import dagger.hilt.android.AndroidEntryPoint
 import org.mjdev.tvapp.base.activity.ComposableActivity
 import org.mjdev.tvapp.base.navigation.Screen.Companion.screen
-import org.mjdev.tvapp.ui.screens.AboutScreen
 import org.mjdev.tvapp.ui.screens.DetailScreen
 import org.mjdev.tvapp.ui.screens.MainScreen
 import org.mjdev.tvapp.ui.screens.PlayerScreen
-import org.mjdev.tvapp.ui.screens.SubscriptionScreen
+import org.mjdev.tvapp.ui.screens.SplashScreen
 
 @AndroidEntryPoint
 class MainActivity : ComposableActivity ({
 
+    screen(route = SplashScreen(), isStartScreen = true)
     screen(route = MainScreen(), isHomeScreen = true)
     screen(route = DetailScreen())
     screen(route = PlayerScreen())
-    screen(route = SubscriptionScreen())
-    screen(route = AboutScreen())
 
 })

+ 11 - 11
app/src/main/kotlin/org/mjdev/tvapp/api/MovieAPI.kt

@@ -8,8 +8,8 @@
 
 package org.mjdev.tvapp.api
 
-import org.mjdev.tvapp.base.helpers.Option
-import org.mjdev.tvapp.base.helpers.Option.Companion.unwrapOr
+import org.mjdev.tvapp.base.helpers.Result
+import org.mjdev.tvapp.base.helpers.Result.Companion.unwrapOr
 import org.mjdev.tvapp.data.Category
 import org.mjdev.tvapp.data.Movie
 import org.mjdev.tvapp.mock.MovieList
@@ -17,8 +17,8 @@ import org.mjdev.tvapp.mock.MovieList
 @Suppress("RedundantSuspendModifier")
 class MovieAPI {
 
-    suspend fun loadCategoryList(): Option<List<Category>> {
-        return Option.Some(MovieList.categories.map { name ->
+    suspend fun loadCategoryList(): Result<List<Category>> {
+        return Result.Success(MovieList.categories.map { name ->
             Category(
                 name = name,
                 movieList = getMovieListByCategory(name).unwrapOr(emptyList())
@@ -26,22 +26,22 @@ class MovieAPI {
         })
     }
 
-    suspend fun loadFeaturedMovieList(): Option<List<Movie>> {
-        return Option.Some(MovieList.featured)
+    suspend fun loadFeaturedMovieList(): Result<List<Movie>> {
+        return Result.Success(MovieList.featured)
     }
 
     suspend fun getMovieListByCategory(
         category: String
-    ): Option<List<Movie>> {
-        return Option.Some(MovieList.getByCategory(category))
+    ): Result<List<Movie>> {
+        return Result.Success(MovieList.getByCategory(category))
     }
 
     suspend fun findMovieById(
         id: Long
-    ): Option<Movie> {
+    ): Result<Movie> {
         return MovieList.findById(id).let { movie ->
-            if (movie == null) Option.None
-            else Option.Some(movie)
+            if (movie == null) Result.Empty
+            else Result.Success(movie)
         }
     }
 

+ 4 - 2
app/src/main/kotlin/org/mjdev/tvapp/base/activity/ComposableActivity.kt

@@ -14,12 +14,13 @@ import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
 import androidx.core.view.WindowCompat
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
 import androidx.navigation.compose.rememberNavController
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.MaterialTheme
 import androidx.tv.material3.NonInteractiveSurfaceDefaults
 import androidx.tv.material3.Surface
 import org.mjdev.tvapp.base.extensions.ContextExt.isATv
@@ -61,8 +62,9 @@ abstract class ComposableActivity(
 
                 Surface(
                     modifier = Modifier.fillMaxSize(),
+                    shape = RectangleShape,
                     colors = NonInteractiveSurfaceDefaults.colors(
-                        containerColor = MaterialTheme.colorScheme.background
+                        containerColor = Color.Black
                     )
                 ) {
 

+ 0 - 25
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/Option.kt

@@ -1,25 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
- */
-
-package org.mjdev.tvapp.base.helpers
-
-sealed class Option<out T> {
-
-    object None : Option<Nothing>()
-
-    data class Some<T>(val value: T) : Option<T>()
-
-    companion object {
-
-        fun <T> Option<T>.unwrapOr(defaultValue: T): T {
-            return (this as? Some)?.value ?: defaultValue
-        }
-
-    }
-
-}

+ 25 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/Result.kt

@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.helpers
+
+sealed class Result<out T> {
+
+    object Empty : Result<Nothing>()
+
+    data class Success<T>(val value: T) : Result<T>()
+
+    companion object {
+
+        fun <T> Result<T>.unwrapOr(defaultValue: T): T {
+            return (this as? Success)?.value ?: defaultValue
+        }
+
+    }
+
+}

+ 1 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/StateContextWrapper.kt

@@ -19,6 +19,7 @@ import androidx.savedstate.SavedStateRegistry
 import androidx.savedstate.SavedStateRegistryController
 import androidx.savedstate.SavedStateRegistryOwner
 
+// todo
 class StateContextWrapper(
     baseContext: Context
 ) : ContextWrapper(baseContext) , LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {

+ 27 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/media/IMediaPlayer.kt

@@ -32,4 +32,31 @@ interface IMediaPlayer {
 
     fun seekTo(value: Long)
 
+    companion object {
+
+        val EMPTY = object : IMediaPlayer {
+
+            @Composable
+            override fun PlayerView() {
+            }
+
+            override fun setMediaUri(uri: Uri) {}
+
+            override fun prepare() {}
+
+            override fun play() {}
+
+            override fun pause() {}
+
+            override fun resume() {}
+
+            override fun stop() {}
+
+            override fun dispose() {}
+
+            override fun seekTo(value: Long) {}
+        }
+
+    }
+
 }

+ 2 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/media/MediaPlayerContainer.kt

@@ -15,8 +15,10 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.Modifier
+import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.media.MediaPlayerState.Companion.rememberMediaPlayerState
 
+@TvPreview
 @Composable
 fun MediaPlayerContainer(
     modifier: Modifier = Modifier,

+ 16 - 6
app/src/main/kotlin/org/mjdev/tvapp/base/media/MediaPlayerContainerDefaults.kt

@@ -13,27 +13,37 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
 import androidx.media3.exoplayer.ExoPlayer
+import org.mjdev.tvapp.base.extensions.ComposeExt.isEditMode
 
 @Suppress("unused", "MemberVisibilityCanBePrivate")
 object MediaPlayerContainerDefaults {
 
-    val engines
+    val engines: Array<IMediaPlayer?>
         @Composable
         get() = arrayOf(
-            exoPlayer, internalPlayer
+            exoPlayer,
+            internalPlayer
         )
 
-    val exoPlayer
+    val exoPlayer: IMediaPlayer
         @Composable
         get() = run {
             val context = LocalContext.current
-            remember { ExoPlayerImpl(ExoPlayer.Builder(context).build()) }
+            val isEdit = isEditMode()
+            remember {
+                if (isEdit) IMediaPlayer.EMPTY
+                else ExoPlayerImpl(ExoPlayer.Builder(context).build())
+            }
         }
 
-    val internalPlayer
+    val internalPlayer: IMediaPlayer
         @Composable
         get() = run {
-            remember { NativePlayerImpl(MediaPlayer()) }
+            val isEdit = isEditMode()
+            remember {
+                if (isEdit) IMediaPlayer.EMPTY
+                else NativePlayerImpl(MediaPlayer())
+            }
         }
 
 }

+ 17 - 12
app/src/main/kotlin/org/mjdev/tvapp/base/media/MediaPlayerImpl.kt

@@ -19,38 +19,43 @@ import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.media3.common.MediaItem
+import androidx.media3.common.util.UnstableApi
 import androidx.media3.exoplayer.ExoPlayer
 import org.mjdev.tvapp.base.extensions.ComposeExt
+import org.mjdev.tvapp.base.extensions.ContextExt.isATv
 
 class ExoPlayerImpl(
     private val exoPlayer: ExoPlayer
 ) : IMediaPlayer {
 
+    @UnstableApi
     @Composable
     override fun PlayerView() {
 
         val isEdit = ComposeExt.isEditMode()
         val context = LocalContext.current
+        val isAtv = context.isATv
 
-        if (!isEdit) {
-
-            AndroidView(
+        if (isEdit) {
+            Box(
                 modifier = Modifier
-                    .background(Color.Transparent, RectangleShape)
-                    .fillMaxSize(),
-                factory = {
-                    androidx.media3.ui.PlayerView(context).apply {
-                        this.player = exoPlayer
-                    }
-                }
+                    .fillMaxSize()
+                    .background(Color.Black, RectangleShape)
             )
 
         } else {
 
-            Box(
+            AndroidView(
                 modifier = Modifier
-                    .background(Color.Black, RectangleShape)
                     .fillMaxSize()
+                    .background(Color.Transparent, RectangleShape),
+                factory = {
+                    androidx.media3.ui.PlayerView(context).apply {
+                        player = exoPlayer
+                        controllerAutoShow = !isAtv
+                        controllerHideOnTouch = !isAtv
+                    }
+                }
             )
 
         }

+ 10 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/media/PlayerControls.kt

@@ -0,0 +1,10 @@
+package org.mjdev.tvapp.base.media
+
+import androidx.compose.runtime.Composable
+import org.mjdev.tvapp.base.annotations.TvPreview
+
+// todo
+@TvPreview
+@Composable
+fun PlayerControls () {
+}

+ 6 - 4
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavGraphBuilderEx.kt

@@ -30,16 +30,18 @@ class NavGraphBuilderEx(
     val navHostController: NavHostController
 ) : NavGraphBuilder(provider, "none", route) {
 
-    var startDestinationRouteEx: String? = null
-
+    var splashDestinationRoute: String? = null
+    var homeDestinationRoute : String? = null
     var menuItems: MutableList<MenuItem> = mutableListOf()
 
     override fun build(): NavGraph {
         val navGraph = super.build()
-        if (startDestinationRouteEx == null) {
+        if (splashDestinationRoute == null && homeDestinationRoute == null) {
             throw(RuntimeException("One class in ComposeActivity should have start property set."))
         }
-        navGraph.setStartDestination(startDestinationRouteEx!!)
+        (splashDestinationRoute ?: homeDestinationRoute)?.also { route ->
+            navGraph.setStartDestination(route)
+        }
         return navGraph
     }
 

+ 5 - 4
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavHostEx.kt

@@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
 import org.mjdev.tvapp.base.extensions.NavExt.createGraph
 
 /**
@@ -30,15 +31,15 @@ fun NavHostEx(
     navController: NavHostController,
     builder: NavGraphBuilderEx.() -> Unit
 ) {
-    androidx.navigation.compose.NavHost(
-        navController,
-        remember(route, builder) {
+    NavHost(
+        navController = navController,
+        graph = remember(route, builder) {
             navController.createGraph(
                 route,
                 navController,
                 builder
             )
         },
-        modifier
+        modifier = modifier
     )
 }

+ 98 - 20
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/Screen.kt

@@ -6,9 +6,20 @@
  * w: https://mjdev.org
  */
 
+@file:Suppress("MemberVisibilityCanBePrivate")
+
 package org.mjdev.tvapp.base.navigation
 
+import android.os.Bundle
 import androidx.annotation.CallSuper
+import androidx.annotation.MainThread
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
@@ -16,10 +27,14 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.sp
 import androidx.navigation.NamedNavArgument
 import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavController
+import androidx.navigation.NavDeepLink
+import androidx.navigation.NavDestination
 import androidx.navigation.NavHostController
 import androidx.navigation.compose.composable
 import androidx.tv.material3.Text
@@ -37,7 +52,7 @@ import kotlin.reflect.full.createInstance
  * @constructor Create empty constructor for screen
  */
 @Suppress("unused")
-open class Screen {
+open class Screen : NavController.OnDestinationChangedListener {
 
     /**
      * Autogenerated path / route
@@ -78,6 +93,11 @@ open class Screen {
      */
     open val menuIcon: ImageVector? = null
 
+    /**
+     * Show once and hide on back pressed, remove from backstack
+     */
+    open val showOnce = false
+
     /**
      * Autogenerated menu item, if any
      * Generated only if [menuTitleResId] > 0
@@ -102,7 +122,7 @@ open class Screen {
 
     /**
      * Compose function for compose screen.
-     * This should be only a method overriden in super classes.
+     * This should be only a method overridden in super classes.
      *
      * @param navController Nav controller
      * @param backStackEntry Back stack entry
@@ -116,22 +136,43 @@ open class Screen {
         args: Map<String, Any?>
     ) {
 
+        if (showOnce) {
+            navController?.addOnDestinationChangedListener(this)
+        }
+
         Column(
-            modifier = Modifier.fillMaxSize(),
+            modifier = Modifier
+                .fillMaxSize()
+                .background(Color.DarkGray, RectangleShape),
             verticalArrangement = Arrangement.Center,
             horizontalAlignment = Alignment.CenterHorizontally
         ) {
+
             Text(
                 text = "Empty Screen",
                 fontSize = 32.sp,
                 color = Color.White
             )
+
         }
 
     }
 
+    override fun onDestinationChanged(
+        controller: NavController,
+        destination: NavDestination,
+        arguments: Bundle?
+    ) {
+        if (showOnce && destination.route == completeRoute) {
+            controller.popBackStack()
+        }
+    }
+
     companion object {
 
+        val SlideInVertically = slideInVertically(animationSpec = tween(1000))
+        val SlideOutVertically = slideOutVertically(animationSpec = tween(1000))
+
         /**
          * Helper function to get params from route.
          *
@@ -146,18 +187,8 @@ open class Screen {
             defaultValue: T
         ): T = (arguments?.get(argId) as? T?) ?: defaultValue
 
-        /**
-         * Open screen extension.
-         *
-         * Function opens a screen with parameters given, if any,
-         * from another screen.
-         *
-         * @param navController Nav controller
-         * @param values Values
-         * @param T T
-         */
-        inline fun <reified T : Screen> open(
-            navController: NavHostController?,
+        @MainThread
+        inline fun <reified T : Screen> NavHostController.open(
             vararg values: Any?
         ) {
             val instance = T::class.createInstance()
@@ -171,10 +202,33 @@ open class Screen {
                 }
                 routeImpl
             }.also { finalRoute ->
-                val currentRoute = navController?.currentRoute
+                val currentRoute = currentRoute
                 val equals = currentRoute?.equals(finalRoute)
                 if (equals != true) {
-                    navController?.navigate(finalRoute)
+                    navigate(finalRoute)
+                }
+            }
+        }
+
+        @MainThread
+        inline fun <reified T : Screen> NavHostController.openClear(
+            vararg values: Any?
+        ) {
+            val instance = T::class.createInstance()
+            instance.completeRoute.let { r ->
+                var routeImpl = r
+                instance.args.forEachIndexed { idx, arg ->
+                    routeImpl = routeImpl.replace(
+                        "{${arg.name}}",
+                        (values[idx] ?: "").toString()
+                    )
+                }
+                routeImpl
+            }.also { finalRoute ->
+                val currentRoute = currentRoute
+                val equals = currentRoute?.equals(finalRoute)
+                if (equals != true) {
+                    navigate(finalRoute)
                 }
             }
         }
@@ -215,17 +269,41 @@ open class Screen {
          */
         fun <T : Screen> NavGraphBuilderEx.screen(
             route: T,
-            isHomeScreen: Boolean = false
+            isHomeScreen: Boolean = false,
+            isStartScreen: Boolean = false,
+            deepLinks: List<NavDeepLink> = emptyList(),
+            enterTransition: (@JvmSuppressWildcards
+            AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = {
+                SlideInVertically
+            },
+            exitTransition: (@JvmSuppressWildcards
+            AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = {
+                SlideOutVertically
+            },
+            popEnterTransition: (@JvmSuppressWildcards
+            AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? =
+                enterTransition,
+            popExitTransition: (@JvmSuppressWildcards
+            AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? =
+                exitTransition,
         ) {
+            if (isStartScreen) {
+                splashDestinationRoute = route.completeRoute
+            }
             if (isHomeScreen) {
-                startDestinationRouteEx = route.completeRoute
+                homeDestinationRoute = route.completeRoute
             }
             route.menuItem?.also { menuItem ->
                 menuItems.add(menuItem)
             }
             composable(
                 route = route.completeRoute,
-                arguments = route.args
+                arguments = route.args,
+                enterTransition = enterTransition,
+                exitTransition = exitTransition,
+                deepLinks = deepLinks,
+                popEnterTransition = popEnterTransition,
+                popExitTransition = popExitTransition
             ) { be ->
                 val rArgs = mutableMapOf<String, Any?>().apply {
                     route.args.forEach { arg ->

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/network/ConectivityCallback.kt → app/src/main/kotlin/org/mjdev/tvapp/base/network/ConnectivityCallback.kt

@@ -11,7 +11,7 @@ package org.mjdev.tvapp.base.network
 import android.net.ConnectivityManager
 import android.net.Network
 
-class ConectivityCallback(
+class ConnectivityCallback(
     val trySend: (data: NetworkStatus) -> Unit
 ) : ConnectivityManager.NetworkCallback() {
 

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/network/NetworkConnectivityServiceImpl.kt

@@ -27,7 +27,7 @@ class NetworkConnectivityServiceImpl constructor(
         .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
 
     override val networkStatus: Flow<NetworkStatus> = callbackFlow {
-        val connectivityCallback = ConectivityCallback { status ->
+        val connectivityCallback = ConnectivityCallback { status ->
             trySend(status)
         }
         val request = NetworkRequest.Builder()

+ 34 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/page/Page.kt

@@ -0,0 +1,34 @@
+package org.mjdev.tvapp.base.page
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import org.mjdev.tvapp.R
+import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.state.ScreenState
+
+@TvPreview
+@Composable
+fun Page(
+    screenState: ScreenState? = null,
+    title: Any? = R.string.app_name,
+    block: @Composable () -> Unit = {},
+) {
+
+    Column(
+        Modifier
+            .fillMaxSize()
+            .background(Color.DarkGray, RectangleShape)
+    ) {
+
+        screenState?.titleState?.value = title
+
+        block()
+
+    }
+
+}

+ 26 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/page/Pager.kt

@@ -0,0 +1,26 @@
+package org.mjdev.tvapp.base.page
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import org.mjdev.tvapp.base.annotations.TvPreview
+
+@TvPreview
+@Composable
+fun Pager(
+    pages: @Composable () -> Unit = {}
+) {
+    Column(
+        modifier = Modifier
+            .fillMaxSize()
+            .background(Color.Red, RectangleShape)
+    ) {
+
+        pages()
+
+    }
+}

+ 7 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/state/ActivityViewState.kt → app/src/main/kotlin/org/mjdev/tvapp/base/state/ScreenState.kt

@@ -19,10 +19,10 @@ import androidx.compose.runtime.remember
  *
  * @param title
  * @param error
- * @constructor Create [ActivityViewState]
+ * @constructor Create [ScreenState]
  */
 @Suppress( "unused")
-class ActivityViewState(
+class ScreenState(
     title: Any? = null,
     error: Throwable? = null
 ) {
@@ -91,13 +91,15 @@ class ActivityViewState(
          *
          * @param title Title
          * @param error Error
-         * @return [ActivityViewState]
+         * @return [ScreenState]
          */
         @Composable
-        fun rememberActivityViewState(
+        fun rememberScreenState(
             title: Any? = null,
             error: Throwable? = null
-        ): ActivityViewState = remember { ActivityViewState(title, error) }
+        ): ScreenState = remember {
+            ScreenState(title, error)
+        }
 
     }
 

+ 54 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/CarouselCard.kt

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.ui.components.card
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.tv.material3.CardDefaults
+import androidx.tv.material3.CardScale
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.data.Movie
+import org.mjdev.tvapp.ui.components.FocusableCard
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@TvPreview
+@Composable
+@SuppressLint("ModifierParameter")
+fun CarouselCard(
+    movie: Movie? = null,
+    modifier: Modifier = Modifier,
+    onFocus: (movie: Movie?) -> Unit = {},
+    onClick: (movie: Movie?) -> Unit = {},
+    contentScale: ContentScale = ContentScale.Crop,
+    scale: CardScale = CardScale.None,
+) {
+
+    FocusableCard(
+        movie = movie,
+        scale = CardScale.None,
+        modifier = modifier,
+        onFocus = {
+            onFocus(movie)
+        },
+        onClick = {
+            onClick(movie)
+        },
+        shape = CardDefaults.shape(),
+        colors = CardDefaults.colors(),
+        border = CardDefaults.border(),
+        glow = CardDefaults.glow(),
+        interactionSource = remember { MutableInteractionSource() },
+    )
+
+}

+ 46 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/MovieCard.kt

@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.ui.components.card
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.data.Movie
+import org.mjdev.tvapp.ui.components.FocusableCard
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@TvPreview
+@Composable
+@SuppressLint("ModifierParameter")
+fun MovieCard(
+    movie: Movie? = null,
+    modifier: Modifier = Modifier,
+    onFocus: (movie: Movie?) -> Unit = {},
+    onClick: (movie: Movie?) -> Unit = {},
+) {
+
+    FocusableCard(
+        modifier = modifier
+            .widthIn(max = 320.dp)
+            .aspectRatio(16f / 9f),
+        movie = movie,
+        onFocus = {
+            onFocus(movie)
+        },
+        onClick = {
+            onClick(movie)
+        }
+    )
+
+}

+ 12 - 11
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Badge.kt

@@ -8,7 +8,6 @@
 
 package org.mjdev.tvapp.base.ui.components.complex
 
-import androidx.compose.foundation.layout.defaultMinSize
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
@@ -17,7 +16,9 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.MaterialTheme
 import org.mjdev.tvapp.base.ui.components.text.CircleText
@@ -31,26 +32,26 @@ fun Badge(
     count: Int = 0,
     backGroundColor: Color = MaterialTheme.colorScheme.secondary,
     borderColor: Color = Color.White,
-    borderSize: Dp = 2.dp,
+    borderSize: Dp = 1.dp,
     textColor: Color = Color.White,
+    textSize: TextUnit = 20.sp,
+    contentPadding: Dp = 2.dp,
+    onFocus: () -> Unit = {},
     onClick: () -> Unit = {}
 ) {
 
     FocusableBox(
-        modifier = modifier
-            .defaultMinSize(40.dp)
-            .clip(CircleShape)
+        modifier = modifier.clip(CircleShape),
     ) {
 
         CircleText(
-            modifier = modifier
-                    .defaultMinSize(40.dp)
-                    .clip(CircleShape),
+            modifier = modifier.clip(CircleShape),
+            contentPadding = contentPadding + borderSize,
             backGroundColor = backGroundColor,
-            borderSize = borderSize,
-            borderColor = borderColor,
             text = count.toString(),
-            textColor = textColor
+            textColor = textColor,
+            borderColor = borderColor,
+            borderSize = borderSize
         )
 
     }

+ 22 - 58
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/BigCarousel.kt

@@ -8,93 +8,57 @@
 
 package org.mjdev.tvapp.base.ui.components.complex
 
-import androidx.compose.foundation.background
-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.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.lazy.list.TvLazyListScope
-import androidx.tv.material3.Card
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.Carousel
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.MaterialTheme
-import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
-import org.mjdev.tvapp.base.interfaces.ItemWithImage
-import org.mjdev.tvapp.base.interfaces.ItemWithTitle
-import org.mjdev.tvapp.base.ui.components.image.ImageAny
-import org.mjdev.tvapp.base.ui.components.text.TextAny
+import org.mjdev.tvapp.base.ui.components.card.CarouselCard
+import org.mjdev.tvapp.data.Movie
 
+// todo swipe left and swipe right
 @OptIn(ExperimentalTvMaterial3Api::class)
 @TvPreview
 @Composable
-fun <T> BigCarousel(
+fun BigCarousel(
     modifier: Modifier = Modifier,
-    items: List<T> = emptyList(),
-    height: Dp = 376.dp,
-    onItemSelected: (T?) -> Unit = {}
-) = FocusableBox(
-    modifier = modifier
-        .fillMaxWidth()
-        .height(height),
-    focusedColor = Color.Transparent
+    items: List<Movie> = emptyList(),
+    height: Dp = 260.dp,
+    onItemSelected: (Movie?) -> Unit = {},
+    onItemClicked: (Movie?) -> Unit = {},
 ) {
 
     Carousel(
         itemCount = items.size,
         modifier = modifier
             .fillMaxWidth()
+//            .pointerInput(Unit) {
+//                detectHorizontalDragGestures { change, dragAmount ->
+                    // todo
+//                }
+//            }
             .height(height),
     ) { indexOfCarouselItem ->
-        val item = items[indexOfCarouselItem]
+        val movie = items[indexOfCarouselItem]
 
-        Card(
+        onItemSelected(movie)
+
+        CarouselCard(
+            modifier = modifier.fillMaxWidth().height(height),
+            contentScale = ContentScale.Crop,
             scale = CardScale.None,
-            modifier = modifier,
-            onClick = {
-                onItemSelected(item)
+            movie = movie,
+            onClick = { clickedItem ->
+                onItemClicked(clickedItem)
             }
-        ) {
-
-            ImageAny(
-                src = if (item is ItemWithImage) item.backgroundImageUrl else null,
-                contentDescription = null,
-                placeholder = R.drawable.placeholder,
-                contentScale = ContentScale.Crop,
-                modifier = Modifier.fillMaxSize(),
-            )
-
-        }
-
-        TextAny(
-            text = if (item is ItemWithTitle) item.title else "",
-            style =
-            MaterialTheme.typography.headlineLarge,
-            modifier = Modifier
-                .padding(36.dp)
-                .background(MaterialTheme.colorScheme.background)
         )
 
     }
 
-}
-
-@Suppress("unused")
-fun <T> TvLazyListScope.bigCarousel(
-    items: List<T?> = emptyList(),
-    height: Dp = 280.dp,
-    onItemSelected: (T?) -> Unit = {}
-) = item {
-    BigCarousel(
-        items = items,
-        height = height,
-        onItemSelected = onItemSelected
-    )
 }

+ 31 - 34
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Clock.kt

@@ -9,7 +9,7 @@
 package org.mjdev.tvapp.base.ui.components.complex
 
 import androidx.compose.foundation.background
-import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentSize
@@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.font.FontWeight
@@ -46,9 +47,8 @@ fun Clock(
     dateTextColor: Color = Color.White,
     horizontalAlignment: Alignment.Horizontal = Alignment.End,
     backgroundColor: Color = Color.Transparent,
-    borderColor: Color = Color.White,
-    borderSize: Dp = 0.dp,
     roundSize: Dp = 8.dp,
+    contentPadding: Dp = 2.dp,
     showTime: Boolean = true,
     showDate: Boolean = true,
     onClick: () -> Unit = {},
@@ -87,44 +87,41 @@ fun Clock(
     }.collectAsState(initial = "1.1.1970") else null
 
     FocusableBox(
-        modifier = modifier.padding(2.dp),
+        modifier = modifier.padding(contentPadding),
         shape = RoundedCornerShape(roundSize),
         onClick = onClick
     ) {
 
-        Column(
-            horizontalAlignment = horizontalAlignment,
+        Box(
+            Modifier
+                .background(backgroundColor, RoundedCornerShape(roundSize))
+                .wrapContentSize()
+                .clip(RoundedCornerShape(roundSize))
         ) {
 
-            if (showTime) TextAny(
-                modifier = modifier
-                    .wrapContentSize()
-                    .apply {
-                        if (borderSize > 0.dp) {
-                            border(borderSize, borderColor)
-                        }
-                    }
-                    .background(backgroundColor, RoundedCornerShape(roundSize)),
-                text = timeFlow?.value,
-                fontWeight = timeTextWeight,
-                fontSize = timeTextSize,
-                color = timeTextColor
-            )
+            Column(
+                modifier = Modifier.padding(8.dp, 2.dp, 8.dp, 2.dp),
+                horizontalAlignment = horizontalAlignment,
+            ) {
 
-            if (showDate) TextAny(
-                modifier = modifier
-                    .apply {
-                        if (borderSize > 0.dp) {
-                            border(borderSize, borderColor)
-                        }
-                    }
-                    .background(backgroundColor, RoundedCornerShape(roundSize)),
-                text = dateFlow?.value,
-                textAlign = TextAlign.End,
-                fontWeight = dateTextWeight,
-                fontSize = dateTextSize,
-                color = dateTextColor
-            )
+                if (showTime) TextAny(
+                    modifier = modifier.wrapContentSize(),
+                    text = timeFlow?.value,
+                    fontWeight = timeTextWeight,
+                    fontSize = timeTextSize,
+                    color = timeTextColor
+                )
+
+                if (showDate) TextAny(
+                    modifier = modifier.wrapContentSize(),
+                    text = dateFlow?.value,
+                    textAlign = TextAlign.End,
+                    fontWeight = dateTextWeight,
+                    fontSize = dateTextSize,
+                    color = dateTextColor
+                )
+
+            }
 
         }
 

+ 80 - 85
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ErrorMessage.kt

@@ -9,8 +9,7 @@
 package org.mjdev.tvapp.base.ui.components.complex
 
 import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentSize
@@ -29,7 +28,6 @@ import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.constraintlayout.compose.ConstraintLayout
-import androidx.tv.foundation.lazy.list.TvLazyListScope
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.LocalContentColor
 import androidx.tv.material3.LocalTextStyle
@@ -42,108 +40,108 @@ import org.mjdev.tvapp.base.ui.components.text.TextAny
 @Composable
 fun ErrorMessage(
     error: Throwable? = null,
+    contentPadding: Dp = 8.dp,
     title: Any? = null,
     fontSizeTitle: TextUnit = 24.sp,
     fontSizeMessage: TextUnit = 14.sp,
     fontWeight: FontWeight = FontWeight.Bold,
     color: Color = Color.White,
-    paddingTitle: PaddingValues = PaddingValues(4.dp),
-    paddingMessage: PaddingValues = PaddingValues(4.dp),
+    paddingTitle:Dp = 4.dp,
+    paddingMessage: Dp = paddingTitle,
     backgroundColor: Color = Color.Red,
     roundSize: Dp = 8.dp,
-    borderSize: Dp = 1.dp,
-    borderColor: Color = Color.White,
     cancelText: Any? = stringResource(id = R.string.dismiss),
+    dismissible: Boolean = true,
     onDismiss: () -> Unit = {}
 ) {
-    ConstraintLayout(
+    Box(
         modifier = Modifier
-            .apply {
-                if (borderSize > 0.dp) {
-                    border(borderSize, borderColor, RoundedCornerShape(roundSize))
-                }
-            }
             .fillMaxWidth()
-            .background(backgroundColor, RoundedCornerShape(roundSize))
+            .background(Color.DarkGray)
     ) {
 
-        val style = LocalTextStyle.current
+        ConstraintLayout(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(contentPadding)
+                .background(backgroundColor, RoundedCornerShape(roundSize))
+        ) {
 
-        val (_title, _message, _button) = createRefs()
+            val style = LocalTextStyle.current
 
-        val textColor = color.takeOrElse {
-            style.color.takeOrElse {
-                LocalContentColor.current
-            }
-        }
+            val (_title, _message, _button) = createRefs()
 
-        TextAny(
-            modifier = Modifier
-                .wrapContentSize()
-                .constrainAs(_title) {
-                    top.linkTo(parent.top)
-                    start.linkTo(parent.start, margin = roundSize)
-                    bottom.linkTo(parent.bottom)
+            val textColor = color.takeOrElse {
+                style.color.takeOrElse {
+                    LocalContentColor.current
                 }
-                .padding(paddingTitle),
-            text = title ?: stringResource(id = R.string.error),
-            fontWeight = fontWeight,
-            fontSize = fontSizeTitle,
-            color = color
-        )
-
-        BasicText(
-            modifier = Modifier
-                .wrapContentSize()
-                .constrainAs(_message) {
-                    top.linkTo(parent.top)
-                    start.linkTo(_title.end)
-                    end.linkTo(_button.start)
-                    bottom.linkTo(parent.bottom)
-                }
-                .padding(paddingMessage),
-            style = style.merge(
-                color = textColor,
-                fontSize = fontSizeMessage,
+            }
+
+            TextAny(
+                modifier = Modifier
+                    .wrapContentSize()
+                    .constrainAs(_title) {
+                        top.linkTo(parent.top)
+                        start.linkTo(parent.start, margin = roundSize)
+                        bottom.linkTo(parent.bottom)
+                    }
+                    .padding(paddingTitle),
+                text = title ?: stringResource(id = R.string.title_error),
                 fontWeight = fontWeight,
-                textAlign = TextAlign.Left,
-            ),
-            minLines = 1,
-            maxLines = 4,
-            text =
-            error?.message ?: (stringResource(id = R.string.error_unknown) +
+                fontSize = fontSizeTitle,
+                color = color
+            )
+
+            BasicText(
+                modifier = Modifier
+                    .wrapContentSize()
+                    .constrainAs(_message) {
+                        top.linkTo(parent.top)
+                        start.linkTo(_title.end)
+                        end.linkTo(_button.start)
+                        bottom.linkTo(parent.bottom)
+                    }
+                    .padding(paddingMessage),
+                style = style.merge(
+                    color = textColor,
+                    fontSize = fontSizeMessage,
+                    fontWeight = fontWeight,
+                    textAlign = TextAlign.Left,
+                ),
+                minLines = 1,
+                maxLines = 4,
+                text =
+                error?.message ?: (stringResource(id = R.string.error_unknown) +
                     "\nType: " +
                     (error?.javaClass?.simpleName ?: Exception::class.simpleName))
-        )
-// todo touchable
-        if (cancelText != null) {
-            Button(
-                modifier = Modifier
-                    .apply {
-                        if (borderSize > 0.dp) {
-                            border(borderSize, borderColor, RoundedCornerShape(roundSize))
-                        }
+            )
+
+            if (dismissible && (cancelText != null)) {
+
+                Button(
+                    modifier = Modifier
+                        .background(
+                            Color.White.copy(
+                                alpha = 0.5f
+                            ),
+                            RoundedCornerShape(roundSize)
+                        )
+                        .constrainAs(_button) {
+                            top.linkTo(parent.top, margin = 4.dp)
+                            end.linkTo(parent.end, margin = roundSize)
+                            bottom.linkTo(parent.bottom, margin = 4.dp)
+                        },
+                    onClick = {
+                        onDismiss()
                     }
-                    .background(
-                        Color.White.copy(
-                            alpha = 0.5f
-                        ),
-                        RoundedCornerShape(roundSize)
+                ) {
+
+                    TextAny(
+                        text = cancelText,
+                        color = color
                     )
-                    .constrainAs(_button) {
-                        top.linkTo(parent.top, margin = 4.dp)
-                        end.linkTo(parent.end, margin = roundSize)
-                        bottom.linkTo(parent.bottom, margin = 4.dp)
-                    },
-                onClick = {
-                    onDismiss()
-                }
-            ) {
 
-                TextAny(
-                    text = cancelText,
-                    color = color
-                )
+                }
 
             }
 
@@ -151,7 +149,4 @@ fun ErrorMessage(
 
     }
 
-}
-
-@Suppress("unused")
-fun TvLazyListScope.errorMessage(error: Throwable?) = item { ErrorMessage(error) }
+}

+ 0 - 2
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/FocusableBox.kt

@@ -53,8 +53,6 @@ fun FocusableBox(
     content: @Composable BoxScope.() -> Unit = {}
 ) {
 
-
-
     Surface(
         onClick,
         modifier

+ 64 - 99
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Header.kt

@@ -9,12 +9,10 @@
 package org.mjdev.tvapp.base.ui.components.complex
 
 import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.defaultMinSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
@@ -28,7 +26,6 @@ import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.constraintlayout.compose.ConstraintLayout
-import androidx.tv.foundation.lazy.list.TvLazyListScope
 import org.mjdev.tvapp.R
 
 @Preview
@@ -39,119 +36,87 @@ fun Header(
     fontSize: TextUnit = 24.sp,
     fontWeight: FontWeight = FontWeight.Bold,
     color: Color = Color.White,
-    backgroundColor: Color = Color(0xFF202020),
-    roundSize: Dp = 8.dp,
-    borderSize: Dp = 0.dp,
-    borderColor: Color = Color.White,
-    padding: PaddingValues = PaddingValues(roundSize),
-    contentPadding: PaddingValues = PaddingValues(2.dp),
+    backgroundColor: Color = Color.DarkGray,
+    roundSize: Dp = 0.dp,
+    padding: Dp = 4.dp,
+    contentPadding: Dp = 2.dp,
     messagesCount: Int = 0,
     onTitleClick: () -> Unit = {},
     onClockClick: () -> Unit = {},
     onMessageBadgeClick: () -> Unit = {},
     onUserPicClick: () -> Unit = {},
-) = ConstraintLayout(
-    modifier = modifier
-        .fillMaxWidth()
-        .background(backgroundColor, RoundedCornerShape(roundSize))
-        .apply {
-            if (borderSize > 0.dp) {
-                border(borderSize, borderColor, RoundedCornerShape(roundSize))
-            }
-        }
-        .padding(padding)
 ) {
 
-    val (_title, _clock, _badge, _settings) = createRefs()
+    ConstraintLayout(
+        modifier = modifier
+            .fillMaxWidth()
+            .background(backgroundColor, RoundedCornerShape(roundSize))
+            .padding(padding + roundSize)
+    ) {
+
+        val (_title, _clock, _badge, _settings) = createRefs()
 
-    Title(
-        modifier = Modifier
-            .constrainAs(_title) {
+        Title(
+            modifier = Modifier.constrainAs(_title) {
                 top.linkTo(parent.top)
-                start.linkTo(parent.start)
+                start.linkTo(parent.start, margin = contentPadding)
                 bottom.linkTo(parent.bottom)
-            }
-            .padding(contentPadding),
-        title = title ?: R.string.app_name,
-        fontWeight = fontWeight,
-        fontSize = fontSize,
-        color = color
-    )
+            },
+            title = title ?: R.string.app_name,
+            fontWeight = fontWeight,
+            fontSize = fontSize,
+            color = color
+        )
 
-    Clock(
-        modifier = Modifier
-            .constrainAs(_clock) {
+        Clock(
+            modifier = Modifier.constrainAs(_clock) {
                 top.linkTo(parent.top)
                 end.linkTo(_badge.start)
                 bottom.linkTo(parent.bottom)
-            }
-            .padding(contentPadding),
-        timeTextColor = color,
-        dateTextColor = color
-    )
+            },
+            backgroundColor = Color.Black.copy(alpha = 0.2f),
+            contentPadding = contentPadding,
+            timeTextColor = color,
+            dateTextColor = color,
+        )
 
-    Badge(
-        modifier = Modifier
-            .defaultMinSize(40.dp)
-            .constrainAs(_badge) {
-                top.linkTo(parent.top)
-                end.linkTo(_settings.start)
-                bottom.linkTo(parent.bottom)
+        Badge(
+            modifier = Modifier
+                .constrainAs(_badge) {
+                    top.linkTo(parent.top)
+                    end.linkTo(_settings.start)
+                    bottom.linkTo(parent.bottom)
+                }
+                .wrapContentSize()
+                .clip(CircleShape),
+            contentPadding = contentPadding,
+            count = messagesCount,
+            borderColor = Color.White,
+            borderSize = 1.dp,
+            textSize = 20.sp,
+            textColor = Color.White,
+            onClick = {
+                onMessageBadgeClick()
             }
-            .padding(contentPadding)
-            .clip(CircleShape),
-        count = messagesCount
-    )
+        )
 
-    UserPic(
-        modifier = Modifier
-            .size(64.dp)
-            .constrainAs(_settings) {
-                top.linkTo(parent.top)
-                end.linkTo(parent.end)
-                bottom.linkTo(parent.bottom)
+        UserPic(
+            modifier = Modifier
+                .size(56.dp)
+                .constrainAs(_settings) {
+                    top.linkTo(parent.top)
+                    end.linkTo(parent.end)
+                    bottom.linkTo(parent.bottom)
+                }
+                .clip(CircleShape),
+            borderColor = Color.White,
+            contentPadding = contentPadding,
+            borderSize = 1.dp,
+            onClick = {
+                onUserPicClick()
             }
-            .padding(contentPadding)
-            .clip(CircleShape),
-        onClick = { }
-    )
+        )
 
-}
+    }
 
-fun TvLazyListScope.header(
-    modifier: Modifier = Modifier,
-    title: Any? = R.string.app_name,
-    fontSize: TextUnit = 24.sp,
-    fontWeight: FontWeight = FontWeight.Bold,
-    color: Color = Color.White,
-    backgroundColor: Color = Color(0xFF202020),
-    roundSize: Dp = 8.dp,
-    borderSize: Dp = 0.dp,
-    borderColor: Color = Color.White,
-    padding: PaddingValues = PaddingValues(roundSize),
-    contentPadding: PaddingValues = PaddingValues(2.dp),
-    messagesCount: Int = 0,
-    onTitleClick: () -> Unit = {},
-    onClockClick: () -> Unit = {},
-    onMessageBadgeClick: () -> Unit = {},
-    onUserPicClick: () -> Unit = {},
-) = item {
-    Header(
-        modifier,
-        title,
-        fontSize,
-        fontWeight,
-        color,
-        backgroundColor,
-        roundSize,
-        borderSize,
-        borderColor,
-        padding,
-        contentPadding,
-        messagesCount,
-        onTitleClick,
-        onClockClick,
-        onMessageBadgeClick,
-        onUserPicClick
-    )
 }

+ 83 - 83
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ListWithPreview.kt

@@ -1,83 +1,83 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
- */
-
-package org.mjdev.tvapp.base.ui.components.complex
-
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.ImmersiveList
-import org.mjdev.tvapp.base.interfaces.ItemWithImage
-import org.mjdev.tvapp.base.ui.components.image.ImageAny
-import timber.log.Timber
-
-// todo
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Preview
-@Composable
-fun <T> ListWithPreview(
-    modifier: Modifier = Modifier,
-    items: List<T> = listOf(),
-    immersiveListHeight: Dp = 300.dp,
-    cardSpacing: Dp = 10.dp,
-    cardWidth: Dp = 200.dp,
-    cardHeight: Dp = 150.dp
-) {
-    val itemBackground: (index: Int) -> Any? = { index ->
-        (if (items.size > index)
-            (items[index] as? ItemWithImage?)?.backgroundImageUrl
-        else null) ?: Color.Transparent
-    }
-    ImmersiveList(
-        modifier = modifier
-            .height(immersiveListHeight + cardHeight / 2)
-            .fillMaxWidth(),
-        background = { index, _ ->
-            ImageAny(
-                modifier = Modifier
-                    .height(immersiveListHeight)
-                    .fillMaxWidth(),
-                src = itemBackground.invoke(index)
-            )
-        }
-    ) {
-        Row(horizontalArrangement = Arrangement.spacedBy(cardSpacing)) {
-            items.forEachIndexed { index, item ->
-                val isFocused = remember { mutableStateOf(false) }
-                ImageAny(
-                    modifier = Modifier
-                        .width(cardWidth)
-                        .height(cardHeight)
-                        .border(5.dp, Color.White.copy(alpha = if (isFocused.value) 1f else 0.3f))
-                        .onFocusChanged { focusState ->
-                            isFocused.value = focusState.isFocused
-                        }
-                        .immersiveListItem(index)
-                        .clickable {
-                            Timber.tag("ImmersiveList: Item $index was clicked")
-                        },
-                    src = itemBackground.invoke(index)
-                )
-            }
-        }
-    }
-}
+///*
+// * Copyright (c) Milan Jurkulák 2023.
+// * Contact:
+// * e: mimoccc@gmail.com
+// * e: mj@mjdev.org
+// * w: https://mjdev.org
+// */
+//
+//package org.mjdev.tvapp.base.ui.components.complex
+//
+//import androidx.compose.foundation.border
+//import androidx.compose.foundation.clickable
+//import androidx.compose.foundation.layout.Arrangement
+//import androidx.compose.foundation.layout.Row
+//import androidx.compose.foundation.layout.fillMaxWidth
+//import androidx.compose.foundation.layout.height
+//import androidx.compose.foundation.layout.width
+//import androidx.compose.runtime.Composable
+//import androidx.compose.runtime.mutableStateOf
+//import androidx.compose.runtime.remember
+//import androidx.compose.ui.Modifier
+//import androidx.compose.ui.focus.onFocusChanged
+//import androidx.compose.ui.graphics.Color
+//import androidx.compose.ui.tooling.preview.Preview
+//import androidx.compose.ui.unit.Dp
+//import androidx.compose.ui.unit.dp
+//import androidx.tv.material3.ExperimentalTvMaterial3Api
+//import androidx.tv.material3.ImmersiveList
+//import org.mjdev.tvapp.base.interfaces.ItemWithImage
+//import org.mjdev.tvapp.base.ui.components.image.ImageAny
+//import timber.log.Timber
+//
+//// todo
+//@OptIn(ExperimentalTvMaterial3Api::class)
+//@Preview
+//@Composable
+//fun <T> ListWithPreview(
+//    modifier: Modifier = Modifier,
+//    items: List<T> = listOf(),
+//    immersiveListHeight: Dp = 300.dp,
+//    cardSpacing: Dp = 10.dp,
+//    cardWidth: Dp = 200.dp,
+//    cardHeight: Dp = 150.dp
+//) {
+//    val itemBackground: (index: Int) -> Any? = { index ->
+//        (if (items.size > index)
+//            (items[index] as? ItemWithImage?)?.backgroundImageUrl
+//        else null) ?: Color.Transparent
+//    }
+//    ImmersiveList(
+//        modifier = modifier
+//            .height(immersiveListHeight + cardHeight / 2)
+//            .fillMaxWidth(),
+//        background = { index, _ ->
+//            ImageAny(
+//                modifier = Modifier
+//                    .height(immersiveListHeight)
+//                    .fillMaxWidth(),
+//                src = itemBackground.invoke(index)
+//            )
+//        }
+//    ) {
+//        Row(horizontalArrangement = Arrangement.spacedBy(cardSpacing)) {
+//            items.forEachIndexed { index, item ->
+//                val isFocused = remember { mutableStateOf(false) }
+//                ImageAny(
+//                    modifier = Modifier
+//                        .width(cardWidth)
+//                        .height(cardHeight)
+//                        .border(5.dp, Color.White.copy(alpha = if (isFocused.value) 1f else 0.3f))
+//                        .onFocusChanged { focusState ->
+//                            isFocused.value = focusState.isFocused
+//                        }
+//                        .immersiveListItem(index)
+//                        .clickable {
+//                            Timber.tag("ImmersiveList: Item $index was clicked")
+//                        },
+//                    src = itemBackground.invoke(index)
+//                )
+//            }
+//        }
+//    }
+//}

+ 12 - 13
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ScreenView.kt

@@ -12,9 +12,6 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material.icons.filled.Menu
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
@@ -24,47 +21,49 @@ import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.navigation.MenuItem
 import org.mjdev.tvapp.base.navigation.Screen
-import org.mjdev.tvapp.base.state.ActivityViewState
-import org.mjdev.tvapp.base.state.ActivityViewState.Companion.rememberActivityViewState
+import org.mjdev.tvapp.base.state.ScreenState
+import org.mjdev.tvapp.base.state.ScreenState.Companion.rememberScreenState
 import org.mjdev.tvapp.base.ui.components.navigation.Navigation
 
-@Suppress("TrailingComma", "UNUSED_ANONYMOUS_PARAMETER")
+@Suppress("TrailingComma", "UNUSED_ANONYMOUS_PARAMETER", "unused")
 @TvPreview
 @Composable
 fun ScreenView(
     navController: NavHostController? = null,
-    navigationIconMain: Any? = Icons.Filled.Menu,
-    navigationIconBack: Any? = Icons.Filled.ArrowBack,
     actions: @Composable RowScope.() -> Unit = {},
     title: Any? = R.string.app_name,
     menuItems: List<MenuItem> = listOf(),
     content: @Composable (
-        state: ActivityViewState,
+        state: ScreenState,
     ) -> Unit = { state ->
         Screen()
     }
 ) {
 
-    val activityViewState = rememberActivityViewState(title)
-    val errorState = remember { activityViewState.errorState }
+    val screenState = rememberScreenState(title)
+    val errorState = remember { screenState.errorState }
 
     Box(
         modifier = Modifier.fillMaxSize(),
-        contentAlignment = Alignment.BottomStart,
+        contentAlignment = Alignment.Center,
     ) {
+
         Navigation(
+            navController = navController,
             items = menuItems,
             content = {
                 Column(
                     modifier = Modifier.fillMaxSize()
                 ) {
-                    content(activityViewState)
+                    content(screenState)
                 }
             }
         )
+
         if (errorState.value != null) {
             ErrorMessage(errorState.value)
         }
+
     }
 
 }

+ 20 - 38
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Tabs.kt

@@ -18,11 +18,11 @@ import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import androidx.tv.foundation.lazy.list.TvLazyListScope
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.Tab
 import androidx.tv.material3.TabDefaults
@@ -41,8 +41,8 @@ fun <T> Tabs(
     fontSize: TextUnit = 12.sp,
     itemsSpacing: Dp = 8.dp,
     activeContentColor: Color = Color.Black,
-    selectedContentColor: Color = activeContentColor,
-    focusedContentColor: Color = activeContentColor,
+    selectedContentColor: Color = Color.White,
+    focusedContentColor: Color = Color.White,
     onItemClick: (index: Int) -> Unit = {}
 ) {
 
@@ -51,7 +51,7 @@ fun <T> Tabs(
     TabRow(
         modifier = modifier.fillMaxWidth(),
         selectedTabIndex = selectedTabIndex.value,
-
+        contentColor = activeContentColor,
         separator = {
             Spacer(modifier = Modifier.width(itemsSpacing))
         },
@@ -59,19 +59,22 @@ fun <T> Tabs(
 
         items.forEachIndexed { index, tab ->
 
+            val selected = index == selectedTabIndex.value
+            val colors = TabDefaults.pillIndicatorTabColors(
+                activeContentColor = activeContentColor,
+                contentColor = activeContentColor,
+                selectedContentColor = activeContentColor,
+                focusedContentColor = focusedContentColor,
+                focusedSelectedContentColor = focusedContentColor,
+                disabledActiveContentColor = activeContentColor,
+                disabledContentColor = activeContentColor,
+                disabledSelectedContentColor = selectedContentColor,
+            )
+
             Tab(
                 modifier = Modifier,
-                colors = TabDefaults.pillIndicatorTabColors(
-                    activeContentColor,
-                    activeContentColor.copy(alpha = 0.4f),
-                    selectedContentColor,
-                    focusedContentColor,
-                    focusedContentColor,
-                    activeContentColor,
-                    activeContentColor.copy(alpha = 0.4f),
-                    selectedContentColor,
-                ),
-                selected = index == selectedTabIndex.value,
+                colors = colors,
+                selected = selected,
                 onFocus = {
                     selectedTabIndex.value = index
                 },
@@ -88,7 +91,8 @@ fun <T> Tabs(
                         modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
                         text = if (tab is ItemWithTitle) tab.title else tab.toString(),
                         fontSize = fontSize,
-                        color = Color.Black
+                        color = if (selected) Color.Black else Color.White,
+                        fontWeight = FontWeight.Bold
                     )
 
                 }
@@ -99,26 +103,4 @@ fun <T> Tabs(
 
     }
 
-}
-
-fun <T> TvLazyListScope.tabs(
-    modifier: Modifier = Modifier,
-    items: List<T> = emptyList(),
-    fontSize: TextUnit = 12.sp,
-    itemsSpacing: Dp = 8.dp,
-    activeContentColor: Color = Color.Black,
-    selectedContentColor: Color = activeContentColor,
-    focusedContentColor: Color = activeContentColor,
-    onItemClick: (index: Int) -> Unit = {}
-) = item {
-    Tabs(
-        modifier,
-        items,
-        fontSize,
-        itemsSpacing,
-        activeContentColor,
-        selectedContentColor,
-        focusedContentColor,
-        onItemClick
-    )
 }

+ 37 - 15
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Title.kt

@@ -8,12 +8,17 @@
 
 package org.mjdev.tvapp.base.ui.components.complex
 
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment.Companion.CenterVertically
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shadow
@@ -25,9 +30,9 @@ import androidx.compose.ui.unit.sp
 import androidx.tv.material3.MaterialTheme
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.ui.components.image.CircleImage
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 
-@Suppress("LocalVariableName")
 @TvPreview
 @Composable
 fun Title(
@@ -49,6 +54,7 @@ fun Title(
     }
 
     FocusableBox(
+        modifier = modifier,
         isFocused = isFocused,
         onTouched = onTouched,
         focusedColor = Color.Transparent,
@@ -56,20 +62,36 @@ fun Title(
 
         isFocused.value.also { focused ->
 
-            TextAny(
-                modifier = modifier.padding(shadowSize),
-                style = MaterialTheme.typography.bodyMedium.copy(
-                    shadow = Shadow(
-                        color = if (focused) focusShadowColor else unfocusedShadowColor,
-                        offset = Offset(shadowSize.value, shadowSize.value),
-                        blurRadius = shadowSize.value
-                    )
-                ),
-                text = title ?: R.string.app_name,
-                fontWeight = fontWeight,
-                fontSize = fontSize,
-                color = color
-            )
+            Row(
+                modifier = modifier,
+                verticalAlignment = CenterVertically,
+            ) {
+
+                CircleImage(
+                    modifier = Modifier
+                        .height((fontSize.value * 1.4).dp)
+                        .aspectRatio(1f).scale(1.3f),
+                    borderColor = Color.White,
+                    borderSize = (-8).dp,
+                    src = R.mipmap.ic_launcher
+                )
+
+                TextAny(
+                    modifier = modifier.padding(shadowSize),
+                    style = MaterialTheme.typography.bodyMedium.copy(
+                        shadow = Shadow(
+                            color = if (focused) focusShadowColor else unfocusedShadowColor,
+                            offset = Offset(shadowSize.value, shadowSize.value),
+                            blurRadius = shadowSize.value
+                        )
+                    ),
+                    text = title ?: R.string.app_name,
+                    fontWeight = fontWeight,
+                    fontSize = fontSize,
+                    color = color
+                )
+
+            }
 
         }
 

+ 5 - 4
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/UserPic.kt

@@ -9,7 +9,7 @@
 package org.mjdev.tvapp.base.ui.components.complex
 
 import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -27,15 +27,15 @@ import org.mjdev.tvapp.base.ui.components.image.CircleImage
 @Composable
 fun UserPic(
     modifier: Modifier = Modifier,
-    shape: Shape = RoundedCornerShape(100),
+    shape: Shape = CircleShape,
     src: Any? = R.drawable.milanj,
     backGroundColor: Color = MaterialTheme.colorScheme.secondary,
     borderColor: Color = Color.White,
     borderSize: Dp = 2.dp,
+    contentPadding: Dp = 2.dp,
     onClick: () -> Unit = {}
 ) = FocusableBox(
-    modifier = modifier
-        .wrapContentSize(),
+    modifier = modifier.wrapContentSize(),
     shape = shape,
     onClick = onClick
 ) {
@@ -45,6 +45,7 @@ fun UserPic(
         backGroundColor = backGroundColor,
         borderSize = borderSize,
         borderColor = borderColor,
+        contentPadding = contentPadding + borderSize,
         src = src,
     )
 

+ 8 - 23
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/image/CircleImage.kt

@@ -9,7 +9,7 @@
 package org.mjdev.tvapp.base.ui.components.image
 
 import android.annotation.SuppressLint
-import android.graphics.drawable.ColorDrawable
+import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.padding
@@ -19,50 +19,35 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.MaterialTheme
 import org.mjdev.tvapp.R
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 @SuppressLint("ModifierParameter")
 @Preview
 @Composable
 fun CircleImage(
     modifier: Modifier = Modifier,
-    backGroundColor: Color = MaterialTheme.colorScheme.secondary,
-    borderColor: Color = Color.Black,
+    backGroundColor: Color = Color.Black,
+    borderColor: Color = Color.White,
     borderSize: Dp = 2.dp,
     contentDescription: String = "",
+    contentPadding: Dp = 2.dp,
     src: Any? = R.drawable.milanj
 ) {
 
     Box(
-        modifier = modifier,
-        contentAlignment = Alignment.Center
+        modifier = modifier.padding(contentPadding),
+        contentAlignment = Alignment.Center,
     ) {
 
-        ImageAny(
-            modifier = modifier
-                .padding(borderSize)
-                .clip(CircleShape),
-            src = ColorDrawable(backGroundColor.toArgb()),
-            contentDescription = contentDescription,
-            contentScale = ContentScale.Crop
-        )
-
         ImageAny(
             modifier = modifier
                 .clip(CircleShape)
-                .apply {
-                    if (borderSize > 0.dp) {
-                        border(borderSize, borderColor, CircleShape)
-                    }
-                },
+                .background(backGroundColor, CircleShape)
+                .border(borderSize, borderColor, CircleShape),
             src = src,
             contentDescription = contentDescription,
             contentScale = ContentScale.Crop,

+ 24 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/image/ImageAny.kt

@@ -18,14 +18,18 @@ import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.graphics.DefaultAlpha
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.core.content.ContextCompat
 import coil.compose.AsyncImage
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.extensions.DrawableExt.asImageBitmap
@@ -48,10 +52,29 @@ fun ImageAny(
     modifier = modifier
 ) {
 
+    val width = if(constraints.minWidth  ==0) 1 else constraints.minWidth
+    val height = if(constraints.minHeight  ==0) 1 else constraints.minHeight
+
+    val context = LocalContext.current
+
+    val placeholderDrawable : Drawable = (placeholder?.let { ph ->
+        ContextCompat.getDrawable(context, ph)
+    } ?: ColorDrawable(0))
+
     when (src) {
 
         null -> Image(
-            ColorDrawable(0).asImageBitmap(),
+            placeholderDrawable.asImageBitmap(width, height),
+            contentDescription,
+            modifier,
+            alignment,
+            contentScale,
+            alpha,
+            colorFilter
+        )
+
+        Color -> Image(
+            ColorDrawable((src as Color).toArgb()).asImageBitmap(width, height),
             contentDescription,
             modifier,
             alignment,

+ 8 - 3
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/Navigation.kt

@@ -27,6 +27,7 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.NavigationDrawer
@@ -43,6 +44,7 @@ const val SEARCH_ITEM = 65534
 @Composable
 fun Navigation(
     modifier: Modifier = Modifier,
+    navController: NavHostController? = null,
     items: List<MenuItem> = listOf(),
     backgroundColor: Color = Color(0xFF202020),
     roundSize: Dp = 8.dp,
@@ -52,7 +54,10 @@ fun Navigation(
     showSettings: Boolean = true,
     searchArrangement: Arrangement.Vertical = Arrangement.Top,
     settingsArrangement: Arrangement.Vertical = Arrangement.Bottom,
+    onSettingsItemFocused: () -> Unit = {},
+    onSearchItemFocused: () -> Unit = {},
     onDrawerItemClick: (id: Int) -> Unit = {},
+    onDrawerItemFocused: (id: Int) -> Unit = {},
     content: @Composable () -> Unit = {}
 ) {
 
@@ -84,13 +89,13 @@ fun Navigation(
 
                         NavigationRow(
                             id = SETTINGS_ITEM,
-                            focused = (focused.value == SETTINGS_ITEM),
                             drawerValue = state,
                             icon = Icons.Outlined.Settings,
                             text = "Settings",
                             onFocus = { id ->
                                 drawerState.setValue(DrawerValue.Open)
                                 focused.value = id
+                                onDrawerItemFocused(id)
                             },
                             onClick = { id ->
                                 onDrawerItemClick(id)
@@ -110,13 +115,13 @@ fun Navigation(
 
                         NavigationRow(
                             id = SEARCH_ITEM,
-                            focused = (focused.value == SEARCH_ITEM),
                             drawerValue = state,
                             icon = Icons.Outlined.Search,
                             text = "Search",
                             onFocus = { id ->
                                 drawerState.setValue(DrawerValue.Open)
                                 focused.value = id
+                                onDrawerItemFocused(id)
                             },
                             onClick = { id ->
                                 onDrawerItemClick(id)
@@ -136,13 +141,13 @@ fun Navigation(
                     items.forEachIndexed { idx, menuItem ->
                         NavigationRow(
                             id = idx,
-                            focused = (focused.value == idx),
                             drawerValue = state,
                             text = menuItem.menuText,
                             icon = menuItem.menuIcon,
                             onFocus = { id ->
                                 drawerState.setValue(DrawerValue.Open)
                                 focused.value = id
+                                onDrawerItemFocused(id)
                             },
                             onClick = { id ->
                                 onDrawerItemClick(id)

+ 0 - 2
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavigationRow.kt

@@ -30,13 +30,11 @@ import org.mjdev.tvapp.base.ui.components.complex.FocusableBox
 import org.mjdev.tvapp.base.ui.components.icon.IconAny
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 
-@Suppress("LocalVariableName")
 @Preview
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 fun NavigationRow(
     id: Int = -1,
-    focused: Boolean = false,
     drawerValue: DrawerValue = DrawerValue.Open,
     text: Any? = "test",
     icon: Any? = Icons.Default.AccountCircle,

+ 19 - 33
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/text/CircleText.kt

@@ -9,18 +9,18 @@
 package org.mjdev.tvapp.base.ui.components.text
 
 import android.annotation.SuppressLint
-import android.graphics.drawable.ColorDrawable
+import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.tooling.preview.Preview
@@ -30,56 +30,42 @@ import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.MaterialTheme
-import org.mjdev.tvapp.base.ui.components.image.ImageAny
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @SuppressLint("ModifierParameter")
 @Preview
 @Composable
 fun CircleText(
-    modifier: Modifier = Modifier.defaultMinSize(40.dp),
+    modifier: Modifier = Modifier,
     backGroundColor: Color = MaterialTheme.colorScheme.secondary,
     borderColor: Color = Color.White,
-    borderSize: Dp = 2.dp,
+    borderSize: Dp = 1.dp,
     text: String = "0",
     textColor: Color = Color.White,
-    textSize: TextUnit = 40.sp
+    textSize: TextUnit = 20.sp,
+    contentPadding: Dp = 2.dp,
 ) {
 
     Box(
         modifier = modifier
-            .defaultMinSize(40.dp)
-            .clip(CircleShape),
-        contentAlignment = Alignment.Center
+            .size(textSize.value.dp * 2)
+            .padding(contentPadding)
+            .aspectRatio(1f)
+            .clip(CircleShape)
+            .background(backGroundColor, CircleShape)
+            .border(borderSize, borderColor, CircleShape),
+        contentAlignment = Alignment.Center,
     ) {
 
-        ImageAny(
-            modifier = modifier
-                .clip(CircleShape),
-            src = ColorDrawable(backGroundColor.toArgb()),
-            contentDescription = null,
-            contentScale = ContentScale.Crop
-        )
-
-        ImageAny(
-            modifier = modifier
-                .clip(CircleShape).apply {
-                    if (borderSize > 0.dp) {
-                        border(borderSize, borderColor, CircleShape)
-                    }
-                },
-            src = ColorDrawable(0),
-            contentDescription = null,
-            contentScale = ContentScale.Crop,
-        )
-
         TextAny(
-            modifier = modifier,
+            modifier = Modifier,
             textAlign = TextAlign.Center,
             fontWeight = FontWeight.Bold,
             color = textColor,
-            text = text
+            text = text,
+            fontSize = textSize
         )
 
     }
+
 }

+ 3 - 3
app/src/main/kotlin/org/mjdev/tvapp/repository/MovieRepository.kt

@@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import org.mjdev.tvapp.api.MovieAPI
-import org.mjdev.tvapp.base.helpers.Option.Companion.unwrapOr
+import org.mjdev.tvapp.base.helpers.Result.Companion.unwrapOr
 import org.mjdev.tvapp.data.Category
 import org.mjdev.tvapp.data.Movie
 import java.lang.Integer.max
@@ -39,7 +39,7 @@ class MovieRepository @Inject constructor(
     private val categoryListMutex = Mutex()
 
     override suspend fun getFeaturedMovieList(): List<Movie> {
-        val movieList = dataSource.loadFeaturedMovieList().unwrapOr(listOf<Movie>())
+        val movieList = dataSource.loadFeaturedMovieList().unwrapOr(listOf())
         updateCache(movieList)
         return movieList
     }
@@ -76,7 +76,7 @@ class MovieRepository @Inject constructor(
         if (force || !categoryMovieListMap.contains(category)) {
             updateCache(
                 category = category,
-                movieList = dataSource.getMovieListByCategory(category.name).unwrapOr(listOf<Movie>())
+                movieList = dataSource.getMovieListByCategory(category.name).unwrapOr(listOf())
             )
         }
     }

+ 21 - 18
app/src/main/kotlin/org/mjdev/tvapp/ui/components/MovieCard.kt → app/src/main/kotlin/org/mjdev/tvapp/ui/components/FocusableCard.kt

@@ -1,17 +1,8 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
- */
-
 package org.mjdev.tvapp.ui.components
 
 import android.annotation.SuppressLint
-import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.widthIn
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -21,7 +12,12 @@ import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.dp
+import androidx.tv.material3.CardBorder
+import androidx.tv.material3.CardColors
 import androidx.tv.material3.CardDefaults
+import androidx.tv.material3.CardGlow
+import androidx.tv.material3.CardScale
+import androidx.tv.material3.CardShape
 import androidx.tv.material3.CompactCard
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.Glow
@@ -33,16 +29,23 @@ import org.mjdev.tvapp.base.ui.components.text.TextAny
 import org.mjdev.tvapp.data.Movie
 
 @OptIn(ExperimentalTvMaterial3Api::class)
+@SuppressLint("ModifierParameter")
 @TvPreview
 @Composable
-@SuppressLint("ModifierParameter")
-fun MovieCard(
+fun FocusableCard(
     movie: Movie? = null,
     modifier: Modifier = Modifier,
+    contentScale: ContentScale = ContentScale.Crop,
+    placeHolder: Int = R.drawable.placeholder,
+    scale: CardScale = CardDefaults.scale(),
+    shape: CardShape = CardDefaults.shape(),
+    colors: CardColors = CardDefaults.colors(),
+    border: CardBorder = CardDefaults.border(),
+    glow: CardGlow = CardDefaults.glow(),
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     onFocus: () -> Unit = {},
     onClick: (Movie?) -> Unit = {},
 ) {
-
     val focusState = remember { mutableStateOf<FocusState?>(null) }
     val isFocused: () -> Boolean = {
         ((focusState.value?.isFocused == true) || (focusState.value?.hasFocus == true))
@@ -50,8 +53,6 @@ fun MovieCard(
 
     CompactCard(
         modifier = modifier
-            .widthIn(max = 320.dp)
-            .aspectRatio(16f / 9f)
             .onFocusChanged { state ->
                 focusState.value = state
             }
@@ -60,10 +61,11 @@ fun MovieCard(
             },
         image = {
             ImageAny(
+                modifier = modifier,
                 src = movie?.cardImageUrl,
                 contentDescription = movie?.description,
-                contentScale = ContentScale.Crop,
-                placeholder = R.drawable.placeholder
+                contentScale = contentScale,
+                placeholder = placeHolder
             )
         },
         title = {
@@ -90,7 +92,8 @@ fun MovieCard(
                 elevation = 10.dp
             ),
             pressedGlow = Glow.None
-        )
+        ),
+        scale = scale,
     )
 
 }

+ 3 - 9
app/src/main/kotlin/org/mjdev/tvapp/ui/components/MediaPlayer.kt

@@ -9,13 +9,13 @@
 package org.mjdev.tvapp.ui.components
 
 import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.interfaces.ItemWithVideoUri
 import org.mjdev.tvapp.base.media.MediaPlayerContainer
 
-@Suppress("UNUSED_ANONYMOUS_PARAMETER")
 @SuppressLint("ModifierParameter")
 @TvPreview
 @Composable
@@ -25,14 +25,8 @@ fun MediaPlayer(
 ) {
 
     MediaPlayerContainer(
-        modifier = modifier,
-        uri = movie?.videoUri,
-        background = { state ->
-        },
-        mediaPlayerOverlay = { state ->
-        },
-        mediaPlayerControls = { state ->
-        }
+        modifier = modifier.fillMaxSize(),
+        uri = movie?.videoUri
     )
 
 }

+ 97 - 95
app/src/main/kotlin/org/mjdev/tvapp/ui/components/MyComposeView.kt

@@ -1,95 +1,97 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
- */
-
-package org.mjdev.tvapp.ui.components;
-
-import android.content.Context
-import android.graphics.PixelFormat
-import android.view.Gravity
-import android.view.View
-import android.view.WindowManager
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.AbstractComposeView
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LifecycleRegistry
-import androidx.lifecycle.ViewModelStore
-import androidx.lifecycle.ViewModelStoreOwner
-import androidx.lifecycle.setViewTreeLifecycleOwner
-import androidx.lifecycle.setViewTreeViewModelStoreOwner
-import androidx.savedstate.SavedStateRegistry
-import androidx.savedstate.SavedStateRegistryController
-import androidx.savedstate.SavedStateRegistryOwner
-import androidx.savedstate.setViewTreeSavedStateRegistryOwner
-
-class MyComposeView(val composeView: View) : AbstractComposeView(
-    composeView.context
-), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
-
-    private val savedStateRegistryController by lazy {
-        SavedStateRegistryController.create(this)
-    }
-
-    override val lifecycle: Lifecycle by lazy {
-        LifecycleRegistry(this)
-    }
-
-    override val viewModelStore: ViewModelStore by lazy {
-        ViewModelStore()
-    }
-
-    override val savedStateRegistry: SavedStateRegistry
-        get() = savedStateRegistryController.savedStateRegistry
-
-
-    private var viewShowing:Boolean = false
-
-    private fun handleLifecycleEvent(event: Lifecycle.Event) =
-        (lifecycle as LifecycleRegistry).handleLifecycleEvent(event)
-
-
-    private val windowManager by lazy {
-        composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
-    }
-
-    private var params: WindowManager.LayoutParams = WindowManager.LayoutParams()
-        .apply {
-            gravity = Gravity.TOP
-            type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
-            token = composeView.applicationWindowToken
-            width = WindowManager.LayoutParams.WRAP_CONTENT
-            height = WindowManager.LayoutParams.WRAP_CONTENT
-            format = PixelFormat.TRANSLUCENT
-            flags = flags or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
-        }
-
-    init {
-        composeView.setViewTreeLifecycleOwner(this)
-        composeView.setViewTreeViewModelStoreOwner(this)
-        composeView.setViewTreeSavedStateRegistryOwner(this)
-    }
-
-    @Composable
-    override fun Content() {
-        // Some Compose Drawing Function
-    }
-
-    fun show() {
-        if (viewShowing) dismiss()
-        windowManager.addView(this, params)
-        viewShowing = true
-    }
-
-    fun dismiss() {
-        if (!viewShowing) return
-        disposeComposition()
-        windowManager.removeViewImmediate(this)
-        viewShowing = false
-    }
-
-}
+///*
+// * Copyright (c) Milan Jurkulák 2023.
+// * Contact:
+// * e: mimoccc@gmail.com
+// * e: mj@mjdev.org
+// * w: https://mjdev.org
+// */
+//
+//package org.mjdev.tvapp.ui.components
+//
+//import android.content.Context
+//import android.graphics.PixelFormat
+//import android.view.Gravity
+//import android.view.View
+//import android.view.WindowManager
+//import androidx.compose.runtime.Composable
+//import androidx.compose.ui.platform.AbstractComposeView
+//import androidx.lifecycle.Lifecycle
+//import androidx.lifecycle.LifecycleOwner
+//import androidx.lifecycle.LifecycleRegistry
+//import androidx.lifecycle.ViewModelStore
+//import androidx.lifecycle.ViewModelStoreOwner
+//import androidx.lifecycle.setViewTreeLifecycleOwner
+//import androidx.lifecycle.setViewTreeViewModelStoreOwner
+//import androidx.savedstate.SavedStateRegistry
+//import androidx.savedstate.SavedStateRegistryController
+//import androidx.savedstate.SavedStateRegistryOwner
+//import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+//
+//class MyComposeView(
+//    val composeView: View
+//) : AbstractComposeView(
+//    composeView.context
+//), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
+//
+//    private val savedStateRegistryController by lazy {
+//        SavedStateRegistryController.create(this)
+//    }
+//
+//    override val lifecycle: Lifecycle by lazy {
+//        LifecycleRegistry(this)
+//    }
+//
+//    override val viewModelStore: ViewModelStore by lazy {
+//        ViewModelStore()
+//    }
+//
+//    override val savedStateRegistry: SavedStateRegistry
+//        get() = savedStateRegistryController.savedStateRegistry
+//
+//
+//    private var viewShowing: Boolean = false
+//
+//    private fun handleLifecycleEvent(event: Lifecycle.Event) =
+//        (lifecycle as LifecycleRegistry).handleLifecycleEvent(event)
+//
+//
+//    private val windowManager by lazy {
+//        composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+//    }
+//
+//    private var params: WindowManager.LayoutParams = WindowManager.LayoutParams()
+//        .apply {
+//            gravity = Gravity.TOP
+//            type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
+//            token = composeView.applicationWindowToken
+//            width = WindowManager.LayoutParams.WRAP_CONTENT
+//            height = WindowManager.LayoutParams.WRAP_CONTENT
+//            format = PixelFormat.TRANSLUCENT
+//            flags = flags or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+//        }
+//
+//    init {
+//        composeView.setViewTreeLifecycleOwner(this)
+//        composeView.setViewTreeViewModelStoreOwner(this)
+//        composeView.setViewTreeSavedStateRegistryOwner(this)
+//    }
+//
+//    @Composable
+//    override fun Content() {
+//        // Some Compose Drawing Function
+//    }
+//
+//    fun show() {
+//        if (viewShowing) dismiss()
+//        windowManager.addView(this, params)
+//        viewShowing = true
+//    }
+//
+//    fun dismiss() {
+//        if (!viewShowing) return
+//        disposeComposition()
+//        windowManager.removeViewImmediate(this)
+//        viewShowing = false
+//    }
+//
+//}

+ 30 - 0
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/AboutPage.kt

@@ -0,0 +1,30 @@
+package org.mjdev.tvapp.ui.pages
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.navigation.NavHostController
+import org.mjdev.tvapp.R
+import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.page.Page
+import org.mjdev.tvapp.base.state.ScreenState
+import org.mjdev.tvapp.base.ui.components.image.CircleImage
+
+@TvPreview
+@Composable
+fun AboutPage(
+    navController: NavHostController? = null,
+    screenState: ScreenState? = null,
+) = Page(
+    title = R.string.title_about
+) {
+
+    Column {
+
+        CircleImage(
+            src = Color.Black
+        )
+
+    }
+
+}

+ 122 - 0
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt

@@ -0,0 +1,122 @@
+package org.mjdev.tvapp.ui.pages
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+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.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import androidx.tv.foundation.lazy.list.items
+import androidx.tv.material3.Text
+import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.extensions.HiltExt.appViewModel
+import org.mjdev.tvapp.base.extensions.ModifierExt.touchable
+import org.mjdev.tvapp.base.extensions.StringExt.asException
+import org.mjdev.tvapp.base.navigation.Screen.Companion.open
+import org.mjdev.tvapp.base.page.Page
+import org.mjdev.tvapp.base.state.ScreenState
+import org.mjdev.tvapp.base.ui.components.complex.BigCarousel
+import org.mjdev.tvapp.base.ui.components.complex.ScrollableTvLazyRow
+import org.mjdev.tvapp.base.ui.components.complex.Tabs
+import org.mjdev.tvapp.data.Movie
+import org.mjdev.tvapp.base.ui.components.card.MovieCard
+import org.mjdev.tvapp.ui.screens.DetailScreen
+import org.mjdev.tvapp.ui.screens.PlayerScreen
+import org.mjdev.tvapp.viewmodel.MainViewModel
+
+@TvPreview
+@Composable
+fun MainPage(
+    navController: NavHostController? = null,
+    screenState: ScreenState? = null,
+) = Page {
+
+    val viewModel = appViewModel<MainViewModel>()
+
+    val categoryList = remember { viewModel.categoryList }.collectAsState()
+    val featuredMovieList = remember { viewModel.featuredMovieList }.collectAsState()
+
+    val onItemClick: (movie: Movie?) -> Unit = { movie ->
+        if (movie == null) {
+            viewModel.postError("No media found.".asException())
+        } else if (movie.hasVideoUri) {
+            navController?.open<PlayerScreen>(navController, movie.id)
+        } else {
+            navController?.open<DetailScreen>(navController, movie.id)
+        }
+    }
+
+    ScrollableTvLazyRow(
+        modifier = Modifier
+            .fillMaxSize()
+            .background(Color.DarkGray),
+        verticalArrangement = Arrangement.spacedBy(32.dp),
+        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
+    ) {
+
+
+        if (categoryList.value.isNotEmpty()) item {
+
+            Tabs(
+                items = categoryList.value.map { it.name }
+            )
+
+        }
+
+        if (featuredMovieList.value.isNotEmpty()) item {
+
+            BigCarousel(
+                modifier = Modifier.touchable(),
+                items = featuredMovieList.value,
+                onItemClicked = onItemClick
+            )
+
+        }
+
+        items(categoryList.value) { category ->
+
+            Text(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(0.dp, 16.dp),
+                text = category.name,
+                color = Color.White,
+                fontWeight = FontWeight.Bold
+            )
+
+            TvLazyRow(
+                horizontalArrangement = Arrangement.spacedBy(16.dp),
+                modifier = Modifier.height(200.dp),
+                contentPadding = PaddingValues(
+                    start = 16.dp,
+                    top = 16.dp,
+                    bottom = 16.dp,
+                    end = 16.dp
+                )
+            ) {
+
+                items(category.movieList) { item ->
+
+                    MovieCard(
+                        movie = item,
+                        onClick = onItemClick
+                    )
+
+                }
+
+            }
+
+        }
+    }
+
+}

+ 33 - 0
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/SubscriptionPage.kt

@@ -0,0 +1,33 @@
+package org.mjdev.tvapp.ui.pages
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavHostController
+import org.mjdev.tvapp.R
+import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.page.Page
+import org.mjdev.tvapp.base.state.ScreenState
+import org.mjdev.tvapp.base.ui.components.image.CircleImage
+
+@TvPreview
+@Composable
+fun SubscriptionPage(
+    navController: NavHostController? = null,
+    screenState: ScreenState? = null,
+) = Page(
+    title = R.string.title_subscription,
+) {
+
+    Column(
+        Modifier.fillMaxSize()
+    ) {
+
+        CircleImage(
+            src = R.drawable.milanj
+        )
+
+    }
+
+}

+ 0 - 44
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/AboutScreen.kt

@@ -1,44 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
- */
-
-package org.mjdev.tvapp.ui.screens
-
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.AccountCircle
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavHostController
-import org.mjdev.tvapp.R
-import org.mjdev.tvapp.base.annotations.TvPreview
-import org.mjdev.tvapp.base.navigation.MenuItem
-import org.mjdev.tvapp.base.navigation.Screen
-
-class AboutScreen : Screen() {
-
-    override val titleResId = R.string.title_about
-
-    override val menuTitleResId: Int = R.string.title_about
-
-    override val menuIcon: ImageVector get() = Icons.Filled.AccountCircle
-
-    @TvPreview
-    @Composable
-    override fun Compose() = super.Compose()
-
-    @Composable
-    override fun Compose(
-        navController: NavHostController?,
-        backStackEntry: NavBackStackEntry?,
-        menuItems: List<MenuItem>,
-        args: Map<String, Any?>
-    ) {
-
-    }
-
-}

+ 2 - 2
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/DetailScreen.kt

@@ -9,7 +9,7 @@
 package org.mjdev.tvapp.ui.screens
 
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Info
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.remember
@@ -44,7 +44,7 @@ class DetailScreen : Screen() {
 
     override val menuTitleResId: Int = -1
 
-    override val menuIcon: ImageVector get() = Icons.Filled.ArrowBack
+    override val menuIcon: ImageVector get() = Icons.Filled.Info
 
     @TvPreview
     @Composable

+ 22 - 97
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/MainScreen.kt

@@ -8,30 +8,17 @@
 
 package org.mjdev.tvapp.ui.screens
 
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-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.material.icons.Icons
 import androidx.compose.material.icons.filled.Home
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
 import androidx.navigation.NamedNavArgument
 import androidx.navigation.NavBackStackEntry
 import androidx.navigation.NavHostController
-import androidx.tv.foundation.lazy.list.TvLazyRow
-import androidx.tv.foundation.lazy.list.items
-import androidx.tv.material3.Text
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.HiltExt.appViewModel
@@ -39,13 +26,11 @@ import org.mjdev.tvapp.base.extensions.StringExt.asException
 import org.mjdev.tvapp.base.navigation.MenuItem
 import org.mjdev.tvapp.base.navigation.Screen
 import org.mjdev.tvapp.base.network.NetworkStatus
+import org.mjdev.tvapp.base.page.Pager
 import org.mjdev.tvapp.base.ui.components.complex.ErrorMessage
+import org.mjdev.tvapp.base.ui.components.complex.Header
 import org.mjdev.tvapp.base.ui.components.complex.ScreenView
-import org.mjdev.tvapp.base.ui.components.complex.ScrollableTvLazyRow
-import org.mjdev.tvapp.base.ui.components.complex.bigCarousel
-import org.mjdev.tvapp.base.ui.components.complex.header
-import org.mjdev.tvapp.base.ui.components.complex.tabs
-import org.mjdev.tvapp.ui.components.MovieCard
+import org.mjdev.tvapp.ui.pages.MainPage
 import org.mjdev.tvapp.viewmodel.MainViewModel
 
 class MainScreen : Screen() {
@@ -71,102 +56,42 @@ class MainScreen : Screen() {
     ) {
 
         val viewModel = appViewModel<MainViewModel>()
-
-        val categoryList = remember { viewModel.categoryList }.collectAsState()
-        val featuredMovieList = remember { viewModel.featuredMovieList }.collectAsState()
         val messages = remember { viewModel.messages }.collectAsState()
-        val networkState = remember { viewModel.networkInfo.networkStatus }.collectAsState(null)
+        val networkState = remember {
+            viewModel.networkInfo.networkStatus
+        }.collectAsState(null)
+//        val pageState = remember { mutableStateOf<Any?>(null) }
 
         ScreenView(
             navController = navController,
             title = stringResource(titleResId),
             menuItems = menuItems
-        ) { state ->
+        ) { screenState ->
 
             viewModel.handleError { error ->
-                state.error(error)
+                screenState.error(error)
             }
 
-            ScrollableTvLazyRow(
-                modifier = Modifier
-                    .fillMaxSize()
-                    .background(Color.DarkGray),
-                verticalArrangement = Arrangement.spacedBy(32.dp),
-                contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
-            ) {
-
-                header(
-                    title = state.titleState.value,
-                    backgroundColor = Color.DarkGray,
-                    messagesCount = messages.value.size
-                )
+            Header(
+                title = screenState.titleState.value,
+                messagesCount = messages.value.size
+            )
 
-                if (networkState.value !is NetworkStatus.Connected) {
-                    item {
-                        ErrorMessage(
-                            error = stringResource(R.string.no_network).asException(),
-                            backgroundColor = Color.Black
-                        )
-                    }
-                }
-
-                tabs(
-                    items = categoryList.value.map { it.name }
+            if (networkState.value !is NetworkStatus.Connected) {
+                ErrorMessage(
+                    error = stringResource(R.string.error_no_network).asException(),
+                    backgroundColor = Color.Black,
+                    dismissible = false
                 )
+            }
 
-                if (featuredMovieList.value.isNotEmpty()) {
-                    bigCarousel(
-                        items = featuredMovieList.value
-                    )
-                }
-
-                items(categoryList.value) { category ->
-
-                    Text(
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .padding(0.dp, 16.dp),
-                        text = category.name,
-                        color = Color.White,
-                        fontWeight = FontWeight.Bold
-                    )
-
-                    TvLazyRow(
-                        horizontalArrangement = Arrangement.spacedBy(16.dp),
-                        modifier = Modifier.height(200.dp),
-                        contentPadding = PaddingValues(
-                            start = 16.dp,
-                            top = 16.dp,
-                            bottom = 16.dp,
-                            end = 16.dp
-                        )
-                    ) {
-
-                        items(category.movieList) { item ->
-
-                            MovieCard(
-                                movie = item,
-                                onClick = { movie ->
-                                    if (movie == null) {
-                                        viewModel.postError(Exception("No media found."))
-                                    } else if (movie.hasVideoUri) {
-                                        open<PlayerScreen>(navController, movie.id)
-                                    } else {
-                                        open<DetailScreen>(navController, movie.id)
-                                    }
-                                }
-                            )
-
-                        }
-
-                    }
-
-                }
+            Pager {
+
+                MainPage(navController, screenState)
 
             }
 
         }
 
     }
-
 }

+ 3 - 5
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/PlayerScreen.kt

@@ -62,13 +62,11 @@ class PlayerScreen : Screen() {
 
         val viewModel = appViewModel<DetailViewModel>()
         val movieId: Long? = remember { args[movieId] as Long? }
-        val movieState = remember {
-            viewModel?.detailsLoadingState(movieId)
-        }?.collectAsState()
+        val movieState = remember { viewModel.detailsLoadingState(movieId) }.collectAsState()
 
-        fun postError(error: String) = viewModel?.postError(error.asException())
+        fun postError(error: String) = viewModel.postError(error.asException())
 
-        when (movieState?.value) {
+        when (movieState.value) {
             is DetailsLoadingState.Ready -> MediaPlayer(
                 modifier = Modifier.fillMaxSize(),
                 movie = (movieState.value as DetailsLoadingState.Ready).movie

+ 101 - 0
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/SplashScreen.kt

@@ -8,8 +8,109 @@
 
 package org.mjdev.tvapp.ui.screens
 
+import android.view.animation.OvershootInterpolator
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.sharp.Tv
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavHostController
+import kotlinx.coroutines.delay
+import org.mjdev.tvapp.R
+import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.navigation.MenuItem
 import org.mjdev.tvapp.base.navigation.Screen
+import org.mjdev.tvapp.base.ui.components.icon.IconAny
+import org.mjdev.tvapp.base.ui.components.text.TextAny
 
 class SplashScreen : Screen() {
 
+    override val showOnce: Boolean = true
+
+    @TvPreview
+    @Composable
+    override fun Compose() = super.Compose()
+
+    @Composable
+    override fun Compose(
+        navController: NavHostController?,
+        backStackEntry: NavBackStackEntry?,
+        menuItems: List<MenuItem>,
+        args: Map<String, Any?>
+    ) {
+
+        val scale = remember {
+            Animatable(0f)
+        }
+
+        LaunchedEffect(key1 = true) {
+            scale.animateTo(
+                targetValue = 0.7f,
+                animationSpec = tween(
+                    durationMillis = 800,
+                    easing = {
+                        OvershootInterpolator(4f).getInterpolation(it)
+                    })
+            )
+            delay(3000L)
+            navController?.addOnDestinationChangedListener(this@SplashScreen)
+            navController?.openClear<MainScreen>()
+        }
+
+        Box(
+            modifier = Modifier
+                .scale(scale.value)
+                .fillMaxSize()
+                .background(Color.Black, RectangleShape),
+            contentAlignment = Alignment.Center
+        ) {
+
+
+            IconAny(
+                modifier = Modifier.size(128.dp),
+                src = Icons.Sharp.Tv,
+                tint = Color.White.copy(alpha = 0.5f)
+            )
+
+            TextAny(
+                text = R.string.app_name,
+                fontSize = 64.sp,
+                fontWeight = FontWeight.Bold,
+                color = Color.White
+            )
+
+        }
+
+        Box(
+            modifier = Modifier
+                .fillMaxSize(),
+            contentAlignment = Alignment.BottomEnd
+        ) {
+
+            TextAny(
+                text = R.string.author,
+                fontSize = 18.sp,
+                fontWeight = FontWeight.Bold,
+                color = Color.White.copy(alpha = 0.5f)
+            )
+
+        }
+
+    }
+
 }

+ 0 - 44
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/SubscriptionScreen.kt

@@ -1,44 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
- */
-
-package org.mjdev.tvapp.ui.screens
-
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavHostController
-import org.mjdev.tvapp.R
-import org.mjdev.tvapp.base.annotations.TvPreview
-import org.mjdev.tvapp.base.navigation.MenuItem
-import org.mjdev.tvapp.base.navigation.Screen
-
-class SubscriptionScreen : Screen() {
-
-    override val titleResId = R.string.title_subscription
-
-    override val menuTitleResId: Int = R.string.title_subscription
-
-    override val menuIcon: ImageVector get() = Icons.Filled.ArrowBack
-
-    @TvPreview
-    @Composable
-    override fun Compose() = super.Compose()
-
-    @Composable
-    override fun Compose(
-        navController: NavHostController?,
-        backStackEntry: NavBackStackEntry?,
-        menuItems: List<MenuItem>,
-        args: Map<String, Any?>
-    ) {
-
-    }
-
-}

+ 12 - 5
app/src/main/res/values/strings.xml

@@ -11,14 +11,21 @@
 <resources>
 
     <string name="app_name">Tv App</string>
-    <string name="error">error</string>
-    <string name="error_unknown">Unknown error.</string>
-    <string name="dismiss">dismiss</string>
-    <string name="menu_item_home">home</string>
+
     <string name="title_movie_detail">movie detail</string>
     <string name="title_about">about</string>
     <string name="title_movie_player">movie player</string>
     <string name="title_subscription">subscription</string>
-    <string name="no_network">Internet connection is missing.</string>
+    <string name="title_error">error</string>
+
+    <string name="error_unknown">Unknown error.</string>
+
+    <string name="dismiss">dismiss</string>
+
+    <string name="menu_item_home">home</string>
+
+    <string name="error_no_network">Internet connection is missing.</string>
+
+    <string name="author">Created by Milan Jurkulak (C) 2023</string>
 
 </resources>

+ 0 - 3
app/src/main/res/values/themes.xml

@@ -13,9 +13,6 @@
     <style name="Theme.TvApp" parent="">
 
         <item name="android:windowNoTitle">true</item>
-        <item name="background">@color/black</item>
-        <item name="android:background">@color/black</item>
-        <item name="android:windowBackground">@color/black</item>
 
     </style>
 

+ 2 - 0
buildSrc/.gitignore

@@ -0,0 +1,2 @@
+/build
+/.gradle/

BIN
buildSrc/.gradle/8.0/executionHistory/executionHistory.bin


BIN
buildSrc/.gradle/8.0/executionHistory/executionHistory.lock


BIN
buildSrc/.gradle/buildOutputCleanup/buildOutputCleanup.lock


BIN
buildSrc/.gradle/file-system.probe


BIN
buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module


BIN
buildSrc/build/classes/kotlin/main/org/mjdev/gradle/extensions/DependencyHandlerKt.class


BIN
buildSrc/build/classes/kotlin/main/org/mjdev/gradle/helper/CustomEventLogger.class


BIN
buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin$Companion.class


BIN
buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin$apply$1.class


BIN
buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin$apply$3.class


BIN
buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin$apply$4.class


BIN
buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin.class


+ 1 - 0
buildSrc/build/kotlin/buildSrcjar-classes.txt

@@ -0,0 +1 @@
+/home/mimo/AndroidStudioProjects/tvapp/buildSrc/build/classes/kotlin/main/org/mjdev/gradle/extensions/DependencyHandlerKt.class:/home/mimo/AndroidStudioProjects/tvapp/buildSrc/build/classes/kotlin/main/org/mjdev/gradle/helper/CustomEventLogger.class:/home/mimo/AndroidStudioProjects/tvapp/buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin$Companion.class:/home/mimo/AndroidStudioProjects/tvapp/buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin$apply$1.class:/home/mimo/AndroidStudioProjects/tvapp/buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin$apply$3.class:/home/mimo/AndroidStudioProjects/tvapp/buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin$apply$4.class:/home/mimo/AndroidStudioProjects/tvapp/buildSrc/build/classes/kotlin/main/org/mjdev/gradle/plugin/MainAppPlugin.class

BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/inline-functions.tab


BIN
buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/inline-functions.tab.keystream


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels