Ver Fonte

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 há 3 anos atrás
pai
commit
3f5972ee1a
100 ficheiros alterados com 1416 adições e 930 exclusões
  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


Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff