فهرست منبع

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements

code improvements
Milan Jurkulak 3 سال پیش
والد
کامیت
acd08740a3
95فایلهای تغییر یافته به همراه1404 افزوده شده و 1098 حذف شده
  1. 10 20
      app/build.gradle.kts
  2. 35 1
      app/objectbox-models/default.json
  3. 36 2
      app/objectbox-models/default.json.bak
  4. 29 13
      app/src/main/AndroidManifest.xml
  5. 2 13
      app/src/main/kotlin/org/mjdev/tvapp/activity/IPTVActivity.kt
  6. 2 13
      app/src/main/kotlin/org/mjdev/tvapp/activity/MainActivity.kt
  7. 6 3
      app/src/main/kotlin/org/mjdev/tvapp/app/Application.kt
  8. 37 0
      app/src/main/kotlin/org/mjdev/tvapp/data/local/Country.kt
  9. 15 1
      app/src/main/kotlin/org/mjdev/tvapp/database/DAO.kt
  10. 22 0
      app/src/main/kotlin/org/mjdev/tvapp/helpers/SyncAdapterInitializer.kt
  11. 10 2
      app/src/main/kotlin/org/mjdev/tvapp/module/ProvideModule.kt
  12. 1 0
      app/src/main/kotlin/org/mjdev/tvapp/sync/SyncAdapter.kt
  13. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/sync/SyncService.kt
  14. 1 2
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/DetailScreen.kt
  15. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/IPTVScreen.kt
  16. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/LoadingScreen.kt
  17. 4 4
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/MainScreen.kt
  18. 5 3
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/SplashScreen.kt
  19. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/WebScreen.kt
  20. 2 3
      app/src/main/kotlin/org/mjdev/tvapp/viewmodel/IPTVViewModel.kt
  21. 48 56
      app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt
  22. 1 1
      app/src/main/res/values-cs/strings.xml
  23. 1 1
      app/src/main/res/values-sk/strings.xml
  24. 0 15
      app/src/main/res/values/colors.xml
  25. 0 31
      app/src/main/res/values/themes.xml
  26. 1 1
      config/version.properties
  27. 12 18
      tvlib/build.gradle.kts
  28. 58 65
      tvlib/src/main/kotlin/org/mjdev/tvlib/activity/ComposableActivity.kt
  29. 133 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/activity/TvActivity.kt
  30. 20 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/application/TvApplication.kt
  31. 14 6
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ComposeExt.kt
  32. 23 8
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ContextExt.kt
  33. 9 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt
  34. 7 16
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/HiltExt.kt
  35. 44 23
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ModifierExt.kt
  36. 9 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/NavExt.kt
  37. 7 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/NavGraphBuilderExt.kt
  38. 3 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/apps/AppsManager.kt
  39. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/coil/AlbumArtDecoder.kt
  40. 19 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/CachingCursor.kt
  41. 0 15
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/other/ConfigChangeCallback.kt
  42. 0 162
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/other/SystemUIController.kt
  43. 4 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemAudio.kt
  44. 4 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemPhoto.kt
  45. 4 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemVideo.kt
  46. 6 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithBackground.kt
  47. 5 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithDate.kt
  48. 6 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithDescription.kt
  49. 4 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithId.kt
  50. 6 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithImage.kt
  51. 5 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithIntent.kt
  52. 5 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithMessage.kt
  53. 6 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithSubtitle.kt
  54. 6 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithTitle.kt
  55. 3 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithUri.kt
  56. 0 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/navigation/NavHostControllerEx.kt
  57. 0 30
      tvlib/src/main/kotlin/org/mjdev/tvlib/network/ConnectivityCallback.kt
  58. 39 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/network/NetworkConnectivityService.kt
  59. 0 68
      tvlib/src/main/kotlin/org/mjdev/tvlib/network/NetworkConnectivityServiceImpl.kt
  60. 3 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/network/NetworkStatus.kt
  61. 7 26
      tvlib/src/main/kotlin/org/mjdev/tvlib/screen/Screen.kt
  62. 3 7
      tvlib/src/main/kotlin/org/mjdev/tvlib/screen/ScreenWithPages.kt
  63. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/service/PlaybackService.kt
  64. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/DailyMotionVideoView.kt
  65. 20 20
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/button/Button.kt
  66. 19 17
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/BigCarousel.kt
  67. 33 29
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/Gallery.kt
  68. 138 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/icon/ConversationReceiptIcon.kt
  69. 4 10
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/ExoPlayerImpl.kt
  70. 1 5
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/IMediaPlayer.kt
  71. 11 7
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerContainer.kt
  72. 4 10
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/NativePlayerImpl.kt
  73. 17 18
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/ModalNavigationDrawer.kt
  74. 29 23
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavDrawerContent.kt
  75. 52 38
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/Navigation.kt
  76. 7 6
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavigationDrawer.kt
  77. 2 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavigationRow.kt
  78. 2 7
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/SettingsDrawer.kt
  79. 3 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/Page.kt
  80. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/TvPager.kt
  81. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/screen/EmptyScreen.kt
  82. 1 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/screen/ScreenView.kt
  83. 8 8
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/AppUpdateAvailableMessage.kt
  84. 24 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/BrowseView.kt
  85. 55 79
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/ErrorMessage.kt
  86. 1 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/theme/Theme.kt
  87. 44 9
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/theme/ThemeHelper.kt
  88. 62 107
      tvlib/src/main/kotlin/org/mjdev/tvlib/updater/AppUpdater.kt
  89. 1 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/NetUtils.kt
  90. 27 0
      tvlib/src/main/res/values-v23/themes.xml
  91. 27 0
      tvlib/src/main/res/values-v27/themes.xml
  92. 27 0
      tvlib/src/main/res/values-v29/themes.xml
  93. 10 11
      tvlib/src/main/res/values/colors.xml
  94. 3 3
      tvlib/src/main/res/values/strings.xml
  95. 21 17
      tvlib/src/main/res/values/themes.xml

+ 10 - 20
app/build.gradle.kts

@@ -64,23 +64,9 @@ android {
 
         signingConfig = signingConfigs[SIGNING_CONFIG_NAME]
 
-        buildConfigField(
-            "String",
-            "IPTV_API_URL",
-            "\"https://iptv-org.github.io/api/\""
-        )
-
-        buildConfigField(
-            "String",
-            "GITHUB_USER",
-            "\"mimoccc\""
-        )
-
-        buildConfigField(
-            "String",
-            "GITHUB_REPOSITORY",
-            "\"tvapp\""
-        )
+        buildConfigField("String", "IPTV_API_URL", "\"https://iptv-org.github.io/api/\"")
+        buildConfigField("String", "GITHUB_USER", "\"mimoccc\"")
+        buildConfigField("String", "GITHUB_REPOSITORY", "\"tvapp\"")
 
         manifestPlaceholders.apply {
             put("auth0Domain", "@string/com_auth0_domain")
@@ -113,8 +99,8 @@ android {
 
         release {
             applicationIdSuffix = ""
-            isDebuggable = true
-            isMinifyEnabled = false
+            isDebuggable = false
+            isMinifyEnabled = true
             proguardFiles(
                 getDefaultProguardFile("proguard-android-optimize.txt"),
                 "proguard-rules.pro"
@@ -196,6 +182,10 @@ android {
 }
 
 dependencies {
+    // todo remove
+    implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.20-Beta2")
+    // startups
+    implementation("androidx.startup:startup-runtime:1.1.1")
     // tv library by mjde milan jurkulak
     implementation(project(mapOf("path" to ":tvlib")))
     // base libs
@@ -280,7 +270,7 @@ dependencies {
     implementation("androidx.media3:media3-common:1.1.1")
     // images
     implementation("io.coil-kt:coil-base:2.4.0")
-    implementation("com.github.skydoves:landscapist-coil:2.2.9")
+    implementation("com.github.skydoves:landscapist-coil:2.2.10")
     // libs mismatch
     constraints {
         implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.0").apply {

+ 35 - 1
app/objectbox-models/default.json

@@ -322,9 +322,43 @@
         }
       ],
       "relations": []
+    },
+    {
+      "id": "6:8612359832519392835",
+      "lastPropertyId": "5:3948204030396009919",
+      "name": "Country",
+      "properties": [
+        {
+          "id": "1:5421436411115442406",
+          "name": "id",
+          "type": 6,
+          "flags": 1
+        },
+        {
+          "id": "2:5924521886771099371",
+          "name": "code",
+          "type": 9
+        },
+        {
+          "id": "3:3406479931615019718",
+          "name": "name",
+          "type": 9
+        },
+        {
+          "id": "4:2189339425724407936",
+          "name": "languages",
+          "type": 30
+        },
+        {
+          "id": "5:3948204030396009919",
+          "name": "flag",
+          "type": 9
+        }
+      ],
+      "relations": []
     }
   ],
-  "lastEntityId": "5:6626640907745307806",
+  "lastEntityId": "6:8612359832519392835",
   "lastIndexId": "0:0",
   "lastRelationId": "0:0",
   "lastSequenceId": "0:0",

+ 36 - 2
app/objectbox-models/default.json.bak

@@ -39,7 +39,7 @@
     },
     {
       "id": "2:7141750768191734097",
-      "lastPropertyId": "12:6138526348322531011",
+      "lastPropertyId": "13:503445141634537419",
       "name": "Movie",
       "properties": [
         {
@@ -92,6 +92,11 @@
           "name": "id",
           "type": 6,
           "flags": 1
+        },
+        {
+          "id": "13:503445141634537419",
+          "name": "subtitle",
+          "type": 9
         }
       ],
       "relations": []
@@ -288,9 +293,38 @@
         }
       ],
       "relations": []
+    },
+    {
+      "id": "5:6626640907745307806",
+      "lastPropertyId": "4:4895179631110667602",
+      "name": "Category",
+      "properties": [
+        {
+          "id": "1:3008860336305693215",
+          "name": "id",
+          "type": 6,
+          "flags": 1
+        },
+        {
+          "id": "2:1550404233910431310",
+          "name": "title",
+          "type": 9
+        },
+        {
+          "id": "3:3840804705832268977",
+          "name": "subtitle",
+          "type": 9
+        },
+        {
+          "id": "4:4895179631110667602",
+          "name": "image",
+          "type": 9
+        }
+      ],
+      "relations": []
     }
   ],
-  "lastEntityId": "4:1987880888030669889",
+  "lastEntityId": "5:6626640907745307806",
   "lastIndexId": "0:0",
   "lastRelationId": "0:0",
   "lastSequenceId": "0:0",

+ 29 - 13
app/src/main/AndroidManifest.xml

@@ -62,9 +62,11 @@
             android:clearTaskOnLaunch="true"
             android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
             android:exported="true"
-            android:theme="@style/Theme.TvApp"
             android:finishOnTaskLaunch="true"
-            android:launchMode="singleTask">
+            android:launchMode="singleTask"
+            android:windowLayoutInDisplayCutoutMode="default"
+            android:screenOrientation="fullSensor"
+            android:theme="@style/Theme.TvApp">
 
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -95,16 +97,18 @@
             android:clearTaskOnLaunch="true"
             android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
             android:exported="true"
-            android:theme="@style/Theme.TvApp"
             android:finishOnTaskLaunch="true"
             android:label="@string/title_iptv"
-            android:launchMode="singleTask">
+            android:launchMode="singleTask"
+            android:windowLayoutInDisplayCutoutMode="default"
+            android:screenOrientation="fullSensor"
+            android:theme="@style/Theme.TvApp">
 
-<!--            <intent-filter>-->
-<!--                <action android:name="android.intent.action.MAIN" />-->
+            <!--            <intent-filter>-->
+            <!--                <action android:name="android.intent.action.MAIN" />-->
 
-<!--                <category android:name="android.intent.category.LAUNCHER" />-->
-<!--            </intent-filter>-->
+            <!--                <category android:name="android.intent.category.LAUNCHER" />-->
+            <!--            </intent-filter>-->
 
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -119,10 +123,12 @@
             android:clearTaskOnLaunch="true"
             android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
             android:exported="true"
-            android:theme="@style/Theme.TvApp"
             android:finishOnTaskLaunch="true"
+            android:windowLayoutInDisplayCutoutMode="default"
             android:label="@string/title_web"
-            android:launchMode="singleTask">
+            android:launchMode="singleTask"
+            android:screenOrientation="fullSensor"
+            android:theme="@style/Theme.TvApp">
 
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -131,7 +137,7 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
 
-<!--ebug-->
+            <!--ebug-->
 
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -172,8 +178,8 @@
 
         <service
             android:name=".sync.SyncService"
-            android:process=":sync"
             android:exported="true"
+            android:process=":sync"
             tools:ignore="ExportedService">
 
             <intent-filter>
@@ -205,10 +211,20 @@
             android:authorities="@string/sync_auth"
             android:enabled="true"
             android:exported="true"
-            android:syncable="true"
             android:process=":provider"
+            android:syncable="true"
             tools:ignore="ExportedContentProvider" />
 
+        <provider
+            android:name="androidx.startup.InitializationProvider"
+            android:authorities="${applicationId}.androidx-startup"
+            android:exported="false"
+            tools:node="merge">
+            <meta-data
+                android:name="org.mjdev.tvapp.helpers.SyncAdapterInitializer"
+                android:value="androidx.startup" />
+        </provider>
+
         <receiver
             android:name=".widget.MyAppWidgetReceiver"
             android:exported="true">

+ 2 - 13
app/src/main/kotlin/org/mjdev/tvapp/activity/IPTVActivity.kt

@@ -13,31 +13,20 @@ import androidx.compose.runtime.Composable
 import dagger.hilt.android.AndroidEntryPoint
 import org.mjdev.tvapp.data.local.Movie
 import org.mjdev.tvapp.ui.screens.DetailScreen
-import org.mjdev.tvlib.activity.ComposableActivity
 import org.mjdev.tvlib.extensions.NavGraphBuilderExt.screen
 import org.mjdev.tvlib.navigation.NavGraphBuilderEx
 import org.mjdev.tvapp.ui.screens.IPTVScreen
 import org.mjdev.tvapp.ui.screens.LoadingScreen
+import org.mjdev.tvlib.activity.TvActivity
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.NavControllerExt.openAsTop
-import org.mjdev.tvlib.helpers.other.SystemUIController
 import org.mjdev.tvlib.interfaces.ItemAudio
 import org.mjdev.tvlib.interfaces.ItemPhoto
 import org.mjdev.tvlib.interfaces.ItemVideo
 import org.mjdev.tvlib.navigation.NavHostControllerEx
 
 @AndroidEntryPoint
-class IPTVActivity : ComposableActivity() {
-
-    private val systemUIController by lazy {
-        SystemUIController(this) {
-            if (isLandscape) hideSystemUI() else showSystemUI()
-        }
-    }
-
-    init {
-        systemUIController.register()
-    }
+class IPTVActivity : TvActivity() {
 
     @Previews
     @Composable

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

@@ -10,26 +10,15 @@ package org.mjdev.tvapp.activity
 
 import androidx.compose.runtime.Composable
 import dagger.hilt.android.AndroidEntryPoint
-import org.mjdev.tvlib.activity.ComposableActivity
 import org.mjdev.tvlib.extensions.NavGraphBuilderExt.screen
 import org.mjdev.tvlib.navigation.NavGraphBuilderEx
 import org.mjdev.tvapp.ui.screens.MainScreen
 import org.mjdev.tvapp.ui.screens.SplashScreen
+import org.mjdev.tvlib.activity.TvActivity
 import org.mjdev.tvlib.annotations.Previews
-import org.mjdev.tvlib.helpers.other.SystemUIController
 
 @AndroidEntryPoint
-class MainActivity : ComposableActivity() {
-
-    private val systemUIController by lazy {
-        SystemUIController(this) {
-            if (isLandscape) hideSystemUI() else showSystemUI()
-        }
-    }
-
-    init {
-        systemUIController.register()
-    }
+class MainActivity : TvActivity() {
 
     @Previews
     @Composable

+ 6 - 3
app/src/main/kotlin/org/mjdev/tvapp/app/Application.kt

@@ -8,18 +8,21 @@
 
 package org.mjdev.tvapp.app
 
-import android.app.Application
 import dagger.hilt.android.HiltAndroidApp
 import org.mjdev.tvapp.sync.SyncService
 import org.mjdev.tvapp.sync.SyncService.Companion.createSyncAccount
+import org.mjdev.tvlib.application.TvApplication
+import org.mjdev.tvlib.extensions.GlobalExt.postDelayed
 
 @HiltAndroidApp
-class Application : Application() {
+class Application : TvApplication() {
 
     override fun onCreate() {
         super.onCreate()
         createSyncAccount()?.also { account ->
-            SyncService.requestSync(account)
+            postDelayed(20000) {
+                SyncService.requestSync(account)
+            }
         }
     }
 

+ 37 - 0
app/src/main/kotlin/org/mjdev/tvapp/data/local/Country.kt

@@ -0,0 +1,37 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.data.local
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import io.objectbox.annotation.Entity
+import io.objectbox.annotation.Id
+
+@Suppress("unused")
+@Entity
+@JsonClass(generateAdapter = true)
+class Country {
+
+    @Id
+    @Json(name = "id")
+    var id: Long = 0
+
+    @Json(name = "code")
+    var code: String? = null
+
+    @Json(name = "name")
+    var name: String? = null
+
+    @Json(name = "languages")
+    var languages: List<String>? = null
+
+    @Json(name = "flag")
+    var flag: String? = null
+
+}

+ 15 - 1
app/src/main/kotlin/org/mjdev/tvapp/database/DAO.kt

@@ -10,6 +10,7 @@ import io.objectbox.kotlin.awaitCallInTx
 import io.objectbox.kotlin.boxFor
 import org.mjdev.tvapp.BuildConfig
 import org.mjdev.tvapp.data.local.Category
+import org.mjdev.tvapp.data.local.Country
 import org.mjdev.tvapp.data.local.Message
 import org.mjdev.tvapp.data.local.Movie
 import org.mjdev.tvapp.data.local.MyObjectBox
@@ -31,11 +32,20 @@ class DAO(
             transaction(this)
         }
 
+        // todo remove reflection
+        inline fun <reified T> Box<T>.findById(id: Long?): T? = if (id == null) all.firstOrNull()
+        else all.firstOrNull { o -> o.property("id") == id }
+
+        // todo remove reflection
+        inline fun <reified T> T.property(name: String): Any? = T::class.members.firstOrNull {
+            it.name == name
+        }?.call(this)
+
     }
 
     private val store: BoxStore by lazy {
         MyObjectBox.builder()
-            .androidContext(context.applicationContext)
+            .androidContext(context)
             .name(DATABASE_NAME)
             .build()
     }
@@ -56,4 +66,8 @@ class DAO(
         store.boxFor()
     }
 
+    val countriesDao: Box<Country> by lazy {
+        store.boxFor()
+    }
+
 }

+ 22 - 0
app/src/main/kotlin/org/mjdev/tvapp/helpers/SyncAdapterInitializer.kt

@@ -0,0 +1,22 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.helpers
+
+import android.content.Context
+import androidx.startup.Initializer
+
+// todo
+@Suppress("unused")
+class SyncAdapterInitializer : Initializer<Unit> {
+    override fun create(context: Context) {
+    }
+    override fun dependencies(): List<Class<out Initializer<*>>> {
+        return emptyList()
+    }
+}

+ 10 - 2
app/src/main/kotlin/org/mjdev/tvapp/module/ProvideModule.kt

@@ -19,6 +19,9 @@ import dagger.Provides
 import dagger.hilt.InstallIn
 import dagger.hilt.android.qualifiers.ApplicationContext
 import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
 import okhttp3.OkHttpClient
 import okhttp3.logging.HttpLoggingInterceptor
 import org.mjdev.tvapp.BuildConfig
@@ -28,7 +31,6 @@ import org.mjdev.tvlib.helpers.cursor.PhotoCursor
 import org.mjdev.tvlib.helpers.cursor.VideoCursor
 import org.mjdev.tvlib.network.CacheInterceptor
 import org.mjdev.tvlib.network.NetworkConnectivityService
-import org.mjdev.tvlib.network.NetworkConnectivityServiceImpl
 import org.mjdev.tvapp.database.DAO
 import retrofit2.Retrofit
 import retrofit2.converter.moshi.MoshiConverterFactory
@@ -74,7 +76,7 @@ class ProvideModule {
     fun providesNetworkConnectivityService(
         @ApplicationContext
         context: Context
-    ): NetworkConnectivityService = NetworkConnectivityServiceImpl(context)
+    ): NetworkConnectivityService = NetworkConnectivityService(context)
 
     @Singleton
     @Provides
@@ -120,4 +122,10 @@ class ProvideModule {
         retrofit: Retrofit
     ): ApiService = retrofit.create(ApiService::class.java)
 
+    @Provides
+    @Singleton
+    fun provideAppScope(): CoroutineScope {
+        return CoroutineScope(SupervisorJob() + Dispatchers.Main)
+    }
+
 }

+ 1 - 0
app/src/main/kotlin/org/mjdev/tvapp/sync/SyncAdapter.kt

@@ -15,6 +15,7 @@ import org.mjdev.tvapp.database.DAO
 import org.mjdev.tvapp.database.DAO.Companion.tx
 import org.mjdev.tvapp.repository.ApiService
 import org.mjdev.tvlib.extensions.GlobalExt.safeGet
+import org.mjdev.tvlib.interfaces.ItemWithUri.Companion.hasUri
 import timber.log.Timber
 
 @Suppress("unused", "PrivatePropertyName")

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/sync/SyncService.kt

@@ -30,7 +30,7 @@ class SyncService : Service() {
         super.onCreate()
         synchronized(sSyncAdapterLock) {
             sSyncAdapter = sSyncAdapter ?: SyncAdapter(
-                context = applicationContext,
+                context = this,
                 apiService = apiService,
                 dao = dao
             )

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

@@ -47,8 +47,7 @@ class DetailScreen : Screen() {
 
     @Previews
     @Composable
-    override fun ComposeScreen() {
-
+    override fun Content() {
         val viewModel: DetailViewModel = appViewModel { context ->
             DetailViewModel.mock(context)
         }

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/IPTVScreen.kt

@@ -51,7 +51,7 @@ class IPTVScreen : Screen() {
 
     @Previews
     @Composable
-    override fun ComposeScreen() {
+    override fun Content() {
 
         val viewModel: IPTVViewModel = appViewModel { context ->
             IPTVViewModel.mock(context)

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/LoadingScreen.kt

@@ -36,7 +36,7 @@ class LoadingScreen : Screen() {
 
     @Previews
     @Composable
-    override fun ComposeScreen() {
+    override fun Content() {
         Box(
             modifier = Modifier
                 .fillMaxSize()

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

@@ -29,10 +29,6 @@ class MainScreen : ScreenWithPages() {
     override val menuIcon: ImageVector = Icons.Default.Home
     override val immersive: Boolean = false
 
-    @Previews
-    @Composable
-    override fun Compose() = super.Compose()
-
     override val pages: (PagerScope.() -> Unit) = {
 
         page(SearchPage(), menuGravity = Top)
@@ -46,4 +42,8 @@ class MainScreen : ScreenWithPages() {
 
     }
 
+    @Previews
+    @Composable
+    override fun Content()  = super.Content()
+
 }

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

@@ -36,6 +36,7 @@ import org.mjdev.tvapp.R
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.NavControllerExt.openAsTop
+import org.mjdev.tvlib.extensions.NavExt.rememberNavControllerEx
 import org.mjdev.tvlib.permission.rememberPermissionManager
 import org.mjdev.tvlib.screen.Screen
 import org.mjdev.tvlib.ui.components.text.TextAny
@@ -50,13 +51,14 @@ class SplashScreen : Screen() {
     @OptIn(ExperimentalPermissionsApi::class)
     @Previews
     @Composable
-    override fun ComposeScreen() {
+    override fun Content() {
 
         val isEdit = isEditMode()
-        val scale = remember (isEdit) { Animatable(if (isEdit) 1f else 0f) }
+        val scale = remember(isEdit) { Animatable(if (isEdit) 1f else 0f) }
         val permissionManager = rememberPermissionManager()
         val composition = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.splash))
         val progress = animateLottieCompositionAsState(composition.value)
+        val navController = rememberNavControllerEx()
 
         Box(
             modifier = Modifier
@@ -96,7 +98,7 @@ class SplashScreen : Screen() {
                     })
             )
             delay(1000L)
-            navController?.openAsTop<MainScreen>()
+            navController.openAsTop<MainScreen>()
         }
     }
 

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/WebScreen.kt

@@ -44,7 +44,7 @@ class WebScreen : Screen() {
 
     @Previews
     @Composable
-    override fun ComposeScreen() {
+    override fun Content() {
 
         val data: Any? = remember(args) { args[data] }
 

+ 2 - 3
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/IPTVViewModel.kt

@@ -17,14 +17,13 @@ import org.mjdev.tvapp.data.local.Movie
 import org.mjdev.tvlib.helpers.cursor.AudioCursor
 import org.mjdev.tvlib.helpers.cursor.PhotoCursor
 import org.mjdev.tvlib.helpers.cursor.VideoCursor
-import org.mjdev.tvlib.network.NetworkConnectivityService
 import org.mjdev.tvlib.viewmodel.BaseViewModel
 import org.mjdev.tvapp.database.DAO
 import org.mjdev.tvlib.extensions.MediaItemExt.mediaItem
 import org.mjdev.tvlib.interfaces.ItemAudio
 import org.mjdev.tvlib.interfaces.ItemPhoto
 import org.mjdev.tvlib.interfaces.ItemVideo
-import org.mjdev.tvlib.network.NetworkConnectivityServiceImpl
+import org.mjdev.tvlib.network.NetworkConnectivityService
 import javax.inject.Inject
 import kotlin.reflect.KClass
 
@@ -81,7 +80,7 @@ class IPTVViewModel @Inject constructor() : BaseViewModel() {
             context: Context
         ): IPTVViewModel = IPTVViewModel().apply {
             dao = DAO(context)
-            networkInfo = NetworkConnectivityServiceImpl(context)
+            networkInfo = NetworkConnectivityService(context)
             localAudioCursor = AudioCursor(context)
             localVideoCursor = VideoCursor(context)
             localPhotoCursor = PhotoCursor(context)

+ 48 - 56
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt

@@ -12,21 +12,24 @@ import android.content.ComponentName
 import android.content.Context
 import dagger.hilt.android.lifecycle.HiltViewModel
 import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.channelFlow
 import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
 import org.mjdev.tvapp.BuildConfig
 import org.mjdev.tvapp.activity.MainActivity
-import org.mjdev.tvlib.extensions.ListExt.asMap
+import org.mjdev.tvapp.data.local.Country
 import org.mjdev.tvlib.helpers.cursor.AudioCursor
 import org.mjdev.tvlib.helpers.cursor.PhotoCursor
 import org.mjdev.tvlib.helpers.cursor.VideoCursor
 import org.mjdev.tvlib.viewmodel.BaseViewModel
-import org.mjdev.tvapp.data.local.Message
 import org.mjdev.tvapp.data.local.Movie
-import org.mjdev.tvapp.data.local.Movie_
 import org.mjdev.tvapp.database.DAO
+import org.mjdev.tvapp.database.DAO.Companion.findById
+import org.mjdev.tvlib.extensions.ListExt.asMap
 import org.mjdev.tvlib.helpers.apps.appsManager
+import org.mjdev.tvlib.interfaces.ItemWithImage.Companion.hasImage
 import javax.inject.Inject
 
 @HiltViewModel
@@ -35,6 +38,14 @@ class MainViewModel @Inject constructor(
     context: Context
 ) : BaseViewModel() {
 
+    @Suppress("PrivatePropertyName")
+    private val ITEMS_FROM_CATEGORY = 3
+
+    private val excludedActivity = ComponentName(
+        BuildConfig.APPLICATION_ID,
+        MainActivity::class.java.name
+    )
+
     @Inject
     lateinit var dao: DAO
 
@@ -47,74 +58,55 @@ class MainViewModel @Inject constructor(
     @Inject
     lateinit var localPhotoCursor: PhotoCursor
 
-//    private val noCategoryString by lazy {
-//        context.getString(R.string.title_no_category)
-//    }
+    // todo : move to cursor
+    val apps = appsManager(context, excludedActivity).stateInViewModel()
 
-    val apps = appsManager(
-        context, ComponentName(
-            BuildConfig.APPLICATION_ID,
-            MainActivity::class.java.name
-        )
-    ).stateInViewModel()
-
-    val messages: StateFlow<List<Message>> = channelFlow {
+    // todo : move to cursor
+    val messages = channelFlow {
         send(dao.messagesDao.all)
-    }.stateInViewModel()
+    }.flowOn(Dispatchers.IO).stateInViewModel()
 
-    val featuredMovieList: StateFlow<List<Any?>> = channelFlow {
+    // todo latest items, anr removal
+    // todo : move to cursor
+    val featuredMovieList = flow {
         mutableListOf<Any?>().apply {
-            // todo latest
-            addAll(dao.movieDao.query().build().find(0, 8))
-            if (localAudioCursor.count > 0) {
-                add(localAudioCursor.getData(0))
-            }
-            if (localVideoCursor.count > 0) {
-                add(localVideoCursor.getData(0))
-            }
-            if (localPhotoCursor.count > 0) {
-                add(localPhotoCursor.getData(0))
-            }
+            addAll(localAudioCursor.takeIf(ITEMS_FROM_CATEGORY) {
+                hasImage
+            })
+            addAll(localVideoCursor.takeIf(ITEMS_FROM_CATEGORY) {
+                hasImage
+            })
+            addAll(localPhotoCursor.takeIf(ITEMS_FROM_CATEGORY) {
+                hasImage
+            })
+            addAll(dao.movieDao.all.take(ITEMS_FROM_CATEGORY))
         }.also { result ->
-            send(result)
+            emit(result)
         }
-    }.stateInViewModel()
-
-    val movieList: StateFlow<Map<String, List<Movie>>> = flow {
-        dao.movieDao.all
-// todo anr
-//            .map { movie ->
-//            movie.apply {
-//                category = category ?: noCategoryString
-//            }
-//        }
-        .sortedBy { m ->
+    }.flowOn(Dispatchers.IO).stateInViewModel()
+
+    // todo improve anr
+    // todo : move to cursor
+    val movieList = flow {
+        dao.movieDao.all.sortedBy { m ->
             m.category
         }.asMap { m ->
             Pair(m.category, m)
         }.also { map ->
             emit(map)
         }
-    }.stateInViewModel()
-
-    val countryList: StateFlow<List<String>> = flow {
-        dao.movieDao.all.filter { it.country != null }.map {
-            it.country.toString()
-        }.distinct().sortedBy {
-            it
-        }.also {
-            emit(it)
-        }
-    }.stateInViewModel()
+    }.flowOn(Dispatchers.IO).stateInViewModel()
+
+    // todo in sync adapter
+    // todo : move to cursor
+    val countryList: StateFlow<List<Country>> = flow {
+        emit(dao.countriesDao.all)
+    }.flowOn(Dispatchers.IO).stateInViewModel()
 
     fun findMovie(
         id: Long?,
         findBlock: (movie: Movie?) -> Unit
-    ): Movie? = dao.movieDao.query()
-        .equal(Movie_.id, id ?: -1)
-        .build()
-        .findFirst()
-        .apply(findBlock)
+    ): Movie? = dao.movieDao.findById(id).apply(findBlock)
 
     companion object {
 
@@ -130,4 +122,4 @@ class MainViewModel @Inject constructor(
 
     }
 
-}
+}

+ 1 - 1
app/src/main/res/values-cs/strings.xml

@@ -35,7 +35,7 @@
 
     <string name="title_about">O aplikaci</string>
     <string name="title_apps">Aplikace</string>
-    <string name="title_app_update">Update</string>
+    <string name="title_app_update">Aktualizace</string>
     <string name="title_error">Chyba</string>
     <string name="title_home">Domov</string>
     <string name="title_movie_detail">Detail videa</string>

+ 1 - 1
app/src/main/res/values-sk/strings.xml

@@ -35,7 +35,7 @@
 
     <string name="title_about">O aplikácií</string>
     <string name="title_apps">Aplikácie</string>
-    <string name="title_app_update">Update</string>
+    <string name="title_app_update">Aktualizácia</string>
     <string name="title_error">Chyba</string>
     <string name="title_home">Domov</string>
     <string name="title_movie_detail">Detail videa</string>

+ 0 - 15
app/src/main/res/values/colors.xml

@@ -1,15 +0,0 @@
-<!--
-/*
- * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
- */
--->
-
-<resources>
-
-    <color name="black">#ff000000</color>
-
-</resources>

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

@@ -1,31 +0,0 @@
-<!--
-/*
- * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
- */
--->
-
-<resources xmlns:tools="http://schemas.android.com/tools">
-
-    <style name="Theme.TvApp" parent="android:Theme.Material.Light.NoActionBar.TranslucentDecor">
-        <item name="android:windowNoTitle">true</item>
-        <item name="android:windowBackground">#FF444444</item>
-        <item name="android:navigationBarColor">#00000000</item>
-<!--        <item name="statusBarBackground">#00000000</item>-->
-        <item name="android:statusBarColor">#00000000</item>
-        <item name="android:colorBackground">#FF444444</item>
-        <item name="android:windowTranslucentStatus">true</item>
-        <item name="android:windowIsTranslucent">true</item>
-        <item name="android:windowTranslucentNavigation">true</item>
-        <item name="android:windowFullscreen">false</item>
-        <item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">always</item>
-    </style>
-
-    <style name="Theme.TvApp.Fullscreen" parent="Theme.TvApp">
-        <item name="android:windowFullscreen">true</item>
-    </style>
-
-</resources>

+ 1 - 1
config/version.properties

@@ -8,5 +8,5 @@
 # suppress inspection "SpellCheckingInspection" for whole file
 # suppress inspection "UnusedProperty" for whole file
 majorVersion=1
-minorVersion=0
+minorVersion=1
 patchVersion=0

+ 12 - 18
tvlib/build.gradle.kts

@@ -39,23 +39,15 @@ android {
                 getDefaultProguardFile("proguard-android-optimize.txt"),
                 "proguard-rules.pro"
             )
-            buildConfigField(
-                "Boolean",
-                "RECOMPOSE_ENABLED",
-                "false"
-            )
+            buildConfigField("Boolean", "RECOMPOSE_ENABLED", "false")
         }
         release {
-            isMinifyEnabled = false
+            isMinifyEnabled = true
             proguardFiles(
                 getDefaultProguardFile("proguard-android-optimize.txt"),
                 "proguard-rules.pro"
             )
-            buildConfigField(
-                "Boolean",
-                "RECOMPOSE_ENABLED",
-                "false"
-            )
+            buildConfigField("Boolean", "RECOMPOSE_ENABLED", "false")
         }
         create("recomposing") {
             isMinifyEnabled = false
@@ -63,11 +55,7 @@ android {
                 getDefaultProguardFile("proguard-android-optimize.txt"),
                 "proguard-rules.pro"
             )
-            buildConfigField(
-                "Boolean",
-                "RECOMPOSE_ENABLED",
-                "true"
-            )
+            buildConfigField("Boolean", "RECOMPOSE_ENABLED", "true")
         }
     }
 
@@ -110,6 +98,10 @@ android {
 }
 
 dependencies {
+    // todo remove
+    implementation(kotlin("reflect"))
+    // startups
+    implementation("androidx.startup:startup-runtime:1.1.1")
     // base libs
     implementation("androidx.core:core-ktx:1.12.0")
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
@@ -122,7 +114,7 @@ dependencies {
     implementation("androidx.compose.ui:ui-tooling")
     implementation("androidx.activity:activity-compose:1.7.2")
     // more icons
-    implementation("androidx.compose.material:material-icons-extended:1.5.1")
+    implementation("androidx.compose.material:material-icons-extended:1.5.2")
     // tv compose
     implementation("androidx.tv:tv-foundation:1.0.0-alpha09")
     implementation("androidx.tv:tv-material:1.0.0-alpha09")
@@ -150,7 +142,7 @@ dependencies {
     implementation("io.coil-kt:coil-svg:2.4.0")
     implementation("io.coil-kt:coil-video:2.4.0")
     implementation("io.coil-kt:coil-compose:2.4.0")
-    implementation("com.github.skydoves:landscapist-coil:2.2.9")
+    implementation("com.github.skydoves:landscapist-coil:2.2.10")
     // debug
     implementation("com.jakewharton.timber:timber:5.0.1")
     // dagger core
@@ -203,6 +195,8 @@ dependencies {
     implementation("org.jsoup:jsoup:1.16.1")
     // dm
     implementation("com.dailymotion.dailymotion-sdk-android:sdk:0.2.12")
+    // dynamic theme
+//    implementation("com.google.android.material:material:1.9.0")
     // yt
     // ...
     // constraints

+ 58 - 65
tvlib/src/main/kotlin/org/mjdev/tvlib/activity/ComposableActivity.kt

@@ -11,6 +11,7 @@ package org.mjdev.tvlib.activity
 import android.content.Intent
 import android.content.res.Configuration
 import android.os.Bundle
+import android.view.WindowManager
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.annotation.CallSuper
@@ -19,13 +20,11 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.core.view.WindowCompat
+import androidx.core.view.WindowCompat.setDecorFitsSystemWindows
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.MaterialTheme
 import androidx.tv.material3.NonInteractiveSurfaceDefaults
@@ -35,15 +34,14 @@ import com.github.anrwatchdog.ANRWatchDog
 import org.mjdev.tvlib.BuildConfig
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt.createImageLoader
-import org.mjdev.tvlib.extensions.ContextExt.isATv
-import org.mjdev.tvlib.extensions.ModifierExt.detectSwipe
+import org.mjdev.tvlib.extensions.ModifierExt.swipeGestures
 import org.mjdev.tvlib.extensions.NavExt.navControllerEx
 import org.mjdev.tvlib.extensions.NavGraphBuilderExt.screen
-import org.mjdev.tvlib.helpers.other.ConfigChangeCallback
 import org.mjdev.tvlib.navigation.NavGraphBuilderEx
 import org.mjdev.tvlib.navigation.NavHostControllerEx
 import org.mjdev.tvlib.ui.components.navigation.NavHostEx
 import org.mjdev.tvlib.ui.components.screen.EmptyScreen
+import org.mjdev.tvlib.ui.theme.TVAppTheme
 import timber.log.Timber
 
 // todo theme
@@ -51,16 +49,14 @@ import timber.log.Timber
 open class ComposableActivity : ComponentActivity() {
 
     @Suppress("PropertyName")
-    val ANR_TIMEOUT = 5000
+    val ANR_TIMEOUT = 2000
 
     val activityResultListeners = mutableListOf<ActivityResultHandler<*>>()
-    val configChangeCallbacks = mutableListOf<ConfigChangeCallback>()
 
     open val navGraphBuilder: NavGraphBuilderEx.() -> Unit = {
         screen(route = EmptyScreen())
     }
 
-    open val backgroundColor = Color(0xff202020)
     open val roundCornerSize: Dp = 0.dp
     open val backgroundShape: Shape = RoundedCornerShape(roundCornerSize)
 
@@ -82,43 +78,19 @@ open class ComposableActivity : ComponentActivity() {
         createImageLoader(this)
     }
 
-    @Previews
-    @OptIn(ExperimentalTvMaterial3Api::class)
-    @Composable
-    @CallSuper
-    open fun Compose() {
-        MaterialTheme {
-            Surface(
-                modifier = Modifier
-                    .background(backgroundColor, backgroundShape)
-                    .fillMaxSize()
-                    .pointerInput(Unit) {
-                        detectSwipe(
-                            onSwipeRight = {
-                                navController.openMenu()
-                            },
-                            onSwipeLeft = {
-                                navController.closeMenu()
-                            }
-                        )
-                    },
-                shape = RectangleShape,
-                colors = NonInteractiveSurfaceDefaults.colors(
-                    containerColor = backgroundColor
-                )
-            ) {
-                NavHostEx(
-                    modifier = Modifier.fillMaxSize(),
-                    navController = navController,
-                    builder = navGraphBuilder,
-                )
-            }
+    override fun onCreate(savedInstanceState: Bundle?) {
+        Timber.d("Activity ${this::class.simpleName} created.")
+        super.onCreate(savedInstanceState)
+        setDecorFitsSystemWindows(window, false)
+        window.setFlags(
+            WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
+            WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+        )
+        if (BuildConfig.DEBUG) {
+            anrWatchDog.start()
         }
-        if (intent != null) {
-            if ((lastIntent == null) || (lastIntent != intent)) {
-                lastIntent = intent
-                onIntent(navController, intent)
-            }
+        setContent {
+            Compose()
         }
     }
 
@@ -132,27 +104,9 @@ open class ComposableActivity : ComponentActivity() {
         }
     }
 
+    @CallSuper
     override fun onConfigurationChanged(newConfig: Configuration) {
         super.onConfigurationChanged(newConfig)
-        Timber.d("Config change")
-        configChangeCallbacks.forEach { cb ->
-            cb.invoke(newConfig)
-        }
-    }
-
-    open fun onIntent(navController: NavHostControllerEx, intent: Intent?) {
-    }
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        Timber.d("Activity ${this::class.simpleName} created.")
-        super.onCreate(savedInstanceState)
-        if (BuildConfig.DEBUG) {
-            anrWatchDog.start()
-        }
-        WindowCompat.setDecorFitsSystemWindows(window, !isATv)
-        setContent {
-            Compose()
-        }
     }
 
     @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
@@ -169,7 +123,6 @@ open class ComposableActivity : ComponentActivity() {
 
     override fun onDestroy() {
         super.onDestroy()
-        configChangeCallbacks.clear()
         activityResultListeners.clear()
     }
 
@@ -183,4 +136,44 @@ open class ComposableActivity : ComponentActivity() {
         onActivityResult
     )
 
+    open fun onIntent(navController: NavHostControllerEx, intent: Intent?) {
+    }
+
+    @Previews
+    @OptIn(ExperimentalTvMaterial3Api::class)
+    @Composable
+    @CallSuper
+    open fun Compose() {
+        TVAppTheme {
+            Surface(
+                modifier = Modifier
+                    .background(MaterialTheme.colorScheme.background, backgroundShape)
+                    .fillMaxSize()
+                    .swipeGestures(
+                        onSwipeRight = {
+                            navController.openMenu()
+                        },
+                        onSwipeLeft = {
+                            navController.closeMenu()
+                        }
+                    ),
+                shape = RectangleShape,
+                colors = NonInteractiveSurfaceDefaults.colors(
+                    containerColor = MaterialTheme.colorScheme.background
+                )
+            ) {
+                NavHostEx(
+                    modifier = Modifier.fillMaxSize(),
+                    builder = navGraphBuilder,
+                )
+            }
+        }
+        if (intent != null) {
+            if ((lastIntent == null) || (lastIntent != intent)) {
+                lastIntent = intent
+                onIntent(navController, intent)
+            }
+        }
+    }
+
 }

+ 133 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/activity/TvActivity.kt

@@ -0,0 +1,133 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.activity
+
+import android.content.res.Configuration
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import android.view.WindowInsets
+import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+import androidx.compose.runtime.Composable
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
+import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+import org.mjdev.tvlib.annotations.Previews
+
+@Suppress("MemberVisibilityCanBePrivate")
+open class TvActivity : ComposableActivity(), View.OnApplyWindowInsetsListener {
+
+    var systemUIVisible: Boolean = true
+
+    val decorView
+        get() = window.decorView
+
+    val windowInsetsController
+        get() = WindowCompat.getInsetsController(window, decorView)
+
+    var systemBarsBehavior: Int
+        get() = windowInsetsController.systemBarsBehavior
+        set(value) {
+            windowInsetsController.systemBarsBehavior = value
+        }
+
+    val configuration: Configuration?
+        get() = resources?.configuration
+
+    val isLandscape
+        get() = (configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) ||
+                ((configuration?.screenWidthDp ?: 0) > (configuration?.screenHeightDp ?: 0))
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        decorView.setOnApplyWindowInsetsListener(this)
+        with(window.attributes) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                    layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+                } else {
+                    layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+                }
+            }
+        }
+        super.onCreate(savedInstanceState)
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        decorView.setOnApplyWindowInsetsListener(null)
+    }
+
+    override fun onConfigurationChanged(newConfig: Configuration) {
+        super.onConfigurationChanged(newConfig)
+        decorView.requestApplyInsets()
+    }
+
+    override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets {
+        WindowCompat.setDecorFitsSystemWindows(window, !isLandscape)
+        if (isLandscape) {
+            systemUIVisible = false
+            if (systemBarsBehavior != BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) {
+                systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                if (insets.isVisible(WindowInsetsCompat.Type.navigationBars())) {
+                    windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
+                }
+                if (insets.isVisible(WindowInsetsCompat.Type.statusBars())) {
+                    windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
+                }
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                    if (insets.isVisible(WindowInsetsCompat.Type.displayCutout())) {
+                        windowInsetsController.hide(WindowInsetsCompat.Type.displayCutout())
+                    }
+                }
+            } else {
+                windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
+                windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                    windowInsetsController.hide(WindowInsetsCompat.Type.displayCutout())
+                }
+            }
+            v.onApplyWindowInsets(insets)
+        } else {
+            systemUIVisible = true
+            if (systemBarsBehavior != BEHAVIOR_DEFAULT) {
+                systemBarsBehavior = BEHAVIOR_DEFAULT
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                if (!insets.isVisible(WindowInsetsCompat.Type.navigationBars())) {
+                    windowInsetsController.show(WindowInsetsCompat.Type.navigationBars())
+                }
+                if (!insets.isVisible(WindowInsetsCompat.Type.statusBars())) {
+                    windowInsetsController.show(WindowInsetsCompat.Type.statusBars())
+                }
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                    if (!insets.isVisible(WindowInsetsCompat.Type.displayCutout())) {
+                        windowInsetsController.show(WindowInsetsCompat.Type.displayCutout())
+                    }
+                }
+            } else {
+                windowInsetsController.show(WindowInsetsCompat.Type.navigationBars())
+                windowInsetsController.show(WindowInsetsCompat.Type.statusBars())
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                    windowInsetsController.show(WindowInsetsCompat.Type.displayCutout())
+                }
+            }
+            v.onApplyWindowInsets(insets)
+        }
+        return insets
+    }
+
+    @Previews
+    @Composable
+    override fun Compose() = super.Compose()
+
+}

+ 20 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/application/TvApplication.kt

@@ -0,0 +1,20 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+@file:Suppress("RedundantOverride")
+
+package org.mjdev.tvlib.application
+
+import android.app.Application
+
+open class TvApplication : Application() {
+    override fun onCreate() {
+        super.onCreate()
+//        DynamicColors.applyToActivitiesIfAvailable(this)
+    }
+}

+ 14 - 6
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ComposeExt.kt

@@ -26,6 +26,7 @@ import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.platform.LocalConfiguration
@@ -49,7 +50,7 @@ import okhttp3.Cache
 import okhttp3.OkHttpClient
 import okhttp3.logging.HttpLoggingInterceptor
 import org.mjdev.tvlib.activity.ComposableActivity
-import org.mjdev.tvlib.extensions.ContextExt.isATv
+import org.mjdev.tvlib.extensions.ContextExt.activity
 import org.mjdev.tvlib.helpers.coil.AlbumArtDecoder
 import org.mjdev.tvlib.network.CacheInterceptor
 import java.io.File
@@ -70,9 +71,6 @@ object ComposeExt {
         (orientation == Configuration.ORIENTATION_PORTRAIT) || (screenHeightDp > screenWidthDp)
     }
 
-    @Composable
-    fun isAtv(): Boolean = LocalContext.current.isATv
-
     // todo more types
     @Composable
     fun Any?.asException(): Exception? = when (this) {
@@ -90,6 +88,16 @@ object ComposeExt {
     val DrawerState.isClosed: Boolean
         get() = currentValue == DrawerValue.Closed
 
+    @Composable
+    @ExperimentalTvMaterial3Api
+    fun rememberDrawerState(
+        initialValue: DrawerValue = if (isEditMode()) DrawerValue.Open else DrawerValue.Closed
+    ): DrawerState {
+        return rememberSaveable(saver = DrawerState.Saver) {
+            DrawerState(initialValue)
+        }
+    }
+
     @Composable
     fun <T> rememberDerivedStateOf(
         function: () -> T,
@@ -184,7 +192,7 @@ object ComposeExt {
     @Composable
     fun rememberImageLoader(): ImageLoader {
         val context = LocalContext.current
-        val activity = context as? ComposableActivity
+        val activity = runCatching { context.activity<ComposableActivity>() }.getOrNull()
         val cachedImageLoader = activity?.imageLoader
         return cachedImageLoader ?: remember(context) {
             createImageLoader(context)
@@ -205,7 +213,7 @@ object ComposeExt {
                 .cache(
                     Cache(
                         directory = File(
-                            context.applicationContext.cacheDir,
+                            context.cacheDir,
                             "http_cache"
                         ),
                         maxSize = 1024L * 1024L * 1024L

+ 23 - 8
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ContextExt.kt

@@ -8,24 +8,39 @@
 
 package org.mjdev.tvlib.extensions
 
-import android.app.UiModeManager
+import android.app.Activity
 import android.content.Context
-import android.content.res.Configuration
+import android.content.ContextWrapper
 import android.text.format.DateFormat
+import timber.log.Timber
 import java.util.Date
 
 object ContextExt {
 
-    val Context.isATv: Boolean
-        get() {
-            val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
-            return uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
-        }
-
     val Context.timeAsString: String
         get() = DateFormat.getTimeFormat(this).format(Date())
 
     val Context.dateAsString: String
         get() = DateFormat.getDateFormat(this).format(Date())
 
+    inline fun <reified T : Activity> Context.activity(
+        messageBlock: (context: Context) -> String = { ctx ->
+            "Expected an activity context but instead found: $ctx"
+        }
+    ): T = let { context ->
+        var ctx: Context? = context
+        while (ctx != null && ctx is ContextWrapper) {
+            if (ctx is T) {
+                return@let ctx
+            }
+            ctx = try {
+                ctx.baseContext
+            } catch (e: Exception) {
+                Timber.e(e)
+                null
+            }
+        }
+        throw IllegalStateException(messageBlock(this))
+    }
+
 }

+ 9 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt

@@ -8,6 +8,8 @@
 
 package org.mjdev.tvlib.extensions
 
+import android.os.Handler
+import android.os.Looper
 import androidx.compose.runtime.MutableState
 import com.skydoves.sandwich.ApiResponse
 import com.skydoves.sandwich.getOrNull
@@ -23,6 +25,13 @@ import java.net.URL
 @Suppress("unused")
 object GlobalExt {
 
+    fun <T> T.postDelayed(
+        delay: Long,
+        block: T.() -> Unit
+    ) = Handler(Looper.myLooper() ?: Looper.getMainLooper()).postDelayed({
+        block.invoke(this)
+    }, delay)
+
     @Suppress("MemberVisibilityCanBePrivate")
     class CodeException(
         val code: Int = -1,

+ 7 - 16
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/HiltExt.kt

@@ -10,7 +10,6 @@ package org.mjdev.tvlib.extensions
 
 import android.app.Activity
 import android.content.Context
-import android.content.ContextWrapper
 import android.os.Bundle
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.platform.LocalContext
@@ -30,6 +29,7 @@ import dagger.hilt.android.internal.builders.ViewModelComponentBuilder
 import dagger.hilt.android.internal.lifecycle.HiltViewModelFactory
 import dagger.hilt.android.internal.lifecycle.HiltViewModelMap
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
+import org.mjdev.tvlib.extensions.ContextExt.activity
 import kotlin.reflect.KClass
 import kotlin.reflect.full.companionObject
 import kotlin.reflect.full.functions
@@ -41,9 +41,9 @@ object HiltExt {
     inline fun <reified VM : ViewModel> appViewModel(
         viewModelStoreOwner: ViewModelStoreOwner? = null,
         key: String? = null,
-        mockFnName :String = String.format("mock%s", VM::class.simpleName),
+        mockFnName: String = String.format("mock%s", VM::class.simpleName),
         companion: KClass<*>? = VM::class.companionObject,
-        noinline modelMockFnc: ((context: Context) -> VM)? =  { context ->
+        noinline modelMockFnc: ((context: Context) -> VM)? = { context ->
             companion?.functions?.firstOrNull { fn ->
                 fn.name == mockFnName
             }?.call(companion, context) as VM
@@ -83,21 +83,12 @@ object HiltExt {
         modelCreator: MutableMap<KClass<*>, (context: Context) -> Any>.() -> Unit = {}
     ): ViewModelProvider.Factory {
         val navBackStackEntry: NavBackStackEntry? = viewModelStoreOwner as? NavBackStackEntry
-        return if (navBackStackEntry == null || isEditMode())
+        return if (isEditMode() || navBackStackEntry == null)
             createInternal(context, modelCreator)
         else {
-            val activity = context.let {
-                var ctx = it
-                while (ctx is ContextWrapper) {
-                    if (ctx is Activity) {
-                        return@let ctx
-                    }
-                    ctx = ctx.baseContext
-                }
-                throw IllegalStateException(
-                    "Expected an activity context for creating a HiltViewModelFactory for a " +
-                        "NavBackStackEntry but instead found: $ctx"
-                )
+            val activity: Activity = context.activity {
+                "Expected an activity context for creating a HiltViewModelFactory for a " +
+                        "NavBackStackEntry but instead found: $context"
             }
             createInternal(
                 activity,

+ 44 - 23
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ModifierExt.kt

@@ -24,6 +24,7 @@ import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
@@ -67,27 +68,20 @@ object ModifierExt {
         }
     }
 
+    @Composable
+    fun Modifier.onEditMode(
+        other: @Composable Modifier.() -> Modifier
+    ) = conditional(ComposeExt.isEditMode(), other)
+
     @Composable
     fun Modifier.onlyPortrait(
-        condition: Boolean = isPortraitMode(),
         other: @Composable Modifier.() -> Modifier
-    ): Modifier {
-        return when (condition) {
-            true -> this.then(other.invoke(this))
-            else -> this
-        }
-    }
+    ) = conditional(isPortraitMode(), other)
 
     @Composable
     fun Modifier.onlyLandscape(
-        condition: Boolean = isLandscapeMode(),
         other: @Composable Modifier.() -> Modifier
-    ): Modifier {
-        return when (condition) {
-            true -> this.then(other.invoke(this))
-            else -> this
-        }
-    }
+    ) = conditional(isLandscapeMode(), other)
 
     @Composable
     fun Modifier.tvAspectRatio(
@@ -223,14 +217,41 @@ object ModifierExt {
         }
     )
 
-    suspend fun PointerInputScope.detectSwipe(
+    fun Modifier.swipeGestures(
+        swipeState: MutableIntState = mutableIntStateOf(-1),
+        onSwipeLeft: (dragAmount: Offset) -> Unit = {},
+        onSwipeRight: (dragAmount: Offset) -> Unit = {},
+        onSwipeUp: (dragAmount: Offset) -> Unit = {},
+        onSwipeDown: (dragAmount: Offset) -> Unit = {},
+        onDoubleTap: (Offset) -> Unit = {},
+        onTap: (Offset) -> Unit = {},
+        dragValue: MutableState<Offset> = mutableStateOf(Offset.Zero)
+    ) = pointerInput(Unit) {
+        detectTapGestures(
+            onTap = onTap,
+            onDoubleTap = onDoubleTap,
+        )
+    }.pointerInput(Unit) {
+        detectSwipe(
+            swipeState = swipeState,
+            onSwipeLeft = onSwipeLeft,
+            onSwipeRight = onSwipeRight,
+            onSwipeUp = onSwipeUp,
+            onSwipeDown = onSwipeDown,
+            dragValue = dragValue
+        )
+    }
+
+    private suspend fun PointerInputScope.detectSwipe(
         swipeState: MutableIntState = mutableIntStateOf(-1),
-        onSwipeLeft: () -> Unit = {},
-        onSwipeRight: () -> Unit = {},
-        onSwipeUp: () -> Unit = {},
-        onSwipeDown: () -> Unit = {},
+        onSwipeLeft: (dragAmount: Offset) -> Unit = {},
+        onSwipeRight: (dragAmount: Offset) -> Unit = {},
+        onSwipeUp: (dragAmount: Offset) -> Unit = {},
+        onSwipeDown: (dragAmount: Offset) -> Unit = {},
+        dragValue: MutableState<Offset> = mutableStateOf(Offset.Zero)
     ) = detectDragGestures(
         onDrag = { change, dragAmount ->
+            dragValue.value = dragAmount
             change.consume()
             val (x, y) = dragAmount
             if (abs(x) > abs(y)) {
@@ -247,10 +268,10 @@ object ModifierExt {
         },
         onDragEnd = {
             when (swipeState.intValue) {
-                0 -> onSwipeRight()
-                1 -> onSwipeLeft()
-                2 -> onSwipeDown()
-                3 -> onSwipeUp()
+                0 -> onSwipeRight(dragValue.value)
+                1 -> onSwipeLeft(dragValue.value)
+                2 -> onSwipeDown(dragValue.value)
+                3 -> onSwipeUp(dragValue.value)
             }
         }
     )

+ 9 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/NavExt.kt

@@ -20,6 +20,8 @@ import androidx.navigation.NavigatorProvider
 import androidx.navigation.compose.ComposeNavigator
 import androidx.navigation.compose.DialogNavigator
 import org.mjdev.tvlib.activity.ComposableActivity
+import org.mjdev.tvlib.extensions.ContextExt.activity
+import org.mjdev.tvlib.navigation.MenuItem
 import org.mjdev.tvlib.navigation.NavGraphBuilderEx
 import org.mjdev.tvlib.navigation.NavHostControllerEx
 
@@ -47,7 +49,7 @@ object NavExt {
         vararg navigators: Navigator<out NavDestination>
     ): NavHostControllerEx {
         val context = LocalContext.current
-        val activity = context as? ComposableActivity
+        val activity = runCatching { context.activity<ComposableActivity>() }.getOrNull()
         val navigatorCached = activity?.navController
         // todo navigators check
         return (navigatorCached ?: rememberSaveable(
@@ -62,8 +64,13 @@ object NavExt {
         }
     }
 
+    @Composable
+    fun rememberNavControllerEx(menuItems: List<MenuItem>) = rememberNavControllerEx().apply {
+        addMenuItem(*menuItems.toTypedArray())
+    }
+
     fun navControllerEx(
-        context:Context,
+        context: Context,
         vararg navigators: Navigator<out NavDestination>
     ): NavHostControllerEx {
         return createNavController(context).apply {

+ 7 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/NavGraphBuilderExt.kt

@@ -63,7 +63,13 @@ object NavGraphBuilderExt {
                     put(arg.name, be.arg(arg.name, null))
                 }
             }
-            route.Compose(navHostController, be, rArgs)
+            route.apply {
+                // todo improve ?
+                navHostController.menuState.value = !route.immersive
+                //
+                route.backStackEntry = be
+                route.args = rArgs
+            }.recompose()
         }
     }
 

+ 3 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/apps/AppsManager.kt

@@ -16,8 +16,10 @@ import android.graphics.drawable.Drawable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.flowOn
 import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithIntent
 import org.mjdev.tvlib.interfaces.ItemWithTitle
@@ -80,7 +82,7 @@ fun appsManager(
     }.also { list ->
         send(list)
     }
-}
+}.flowOn(Dispatchers.IO)
 
 data class App(
     override var title: String?,

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/coil/AlbumArtDecoder.kt

@@ -47,7 +47,7 @@ class AlbumArtDecoder(
     private val httpCache by lazy {
         Cache(
             directory = File(
-                context.applicationContext.cacheDir,
+                context.cacheDir,
                 "http_cache"
             ),
             maxSize = 1024L * 1024L * 1024L

+ 19 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/CachingCursor.kt

@@ -127,6 +127,25 @@ open class CachingCursor<T>(
         } > 0
     }
 
+    fun take(count: Int, idx: Int = 0): List<T> = mutableListOf<T>().apply {
+        (idx..(idx + count)).forEach { current ->
+            add(get(current))
+        }
+    }
+
+    fun takeIf(
+        count: Int,
+        idx: Int = 0,
+        condition: T.() -> Boolean
+    ): List<T> = mutableListOf<T>().apply {
+        var index = idx
+        while ((index < this@CachingCursor.count) && (size < count)) {
+            val item = _get(index)
+            if ((item != null) && condition(item)) add(item)
+            index++
+        }
+    }
+
     override fun close() {
         cache.clear()
         cursor?.unregisterContentObserver(observer)

+ 0 - 15
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/other/ConfigChangeCallback.kt

@@ -1,15 +0,0 @@
-/*
- *  Copyright (c) Milan Jurkulák 2023.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
-package org.mjdev.tvlib.helpers.other
-
-import android.content.res.Configuration
-
-interface ConfigChangeCallback {
-    fun invoke(newConfig: Configuration)
-}

+ 0 - 162
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/other/SystemUIController.kt

@@ -1,162 +0,0 @@
-/*
- *  Copyright (c) Milan Jurkulák 2023.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
-package org.mjdev.tvlib.helpers.other
-
-import android.annotation.SuppressLint
-import android.content.res.Configuration
-import android.view.View
-import android.view.WindowInsets.Type.navigationBars
-import android.view.WindowInsets.Type.statusBars
-import android.view.WindowInsetsController.BEHAVIOR_DEFAULT
-import android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
-import android.view.WindowManager
-import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
-import androidx.lifecycle.DefaultLifecycleObserver
-import androidx.lifecycle.LifecycleOwner
-import org.mjdev.tvlib.activity.ComposableActivity
-
-@Suppress("DEPRECATION", "unused", "MemberVisibilityCanBePrivate")
-class SystemUIController(
-    val activity: ComposableActivity?,
-    val onChanges: SystemUIController.() -> Unit
-) : ConfigChangeCallback, DefaultLifecycleObserver {
-
-    val window
-        get() = runCatching {
-            activity?.window
-        }.getOrNull()
-
-    var configuration: Configuration? = runCatching {
-        activity?.resources?.configuration
-    }.getOrNull()
-
-    var systemUIVisible: Boolean = true
-
-    val isLandscape
-        get() = (configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) ||
-                ((configuration?.screenWidthDp ?: 0) > (configuration?.screenHeightDp ?: 0))
-
-
-    val isPortrait
-        get() = (configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) ||
-                ((configuration?.screenHeightDp ?: 0) > (configuration?.screenWidthDp ?: 0))
-
-    init {
-        activity?.lifecycle?.addObserver(this)
-    }
-
-    fun register() {
-        activity?.configChangeCallbacks?.also {
-            it.remove(this)
-            it.add(this)
-        }
-    }
-
-    private fun unregister() {
-        activity?.configChangeCallbacks?.remove(this)
-    }
-
-    override fun onResume(owner: LifecycleOwner) {
-        super.onResume(owner)
-        register()
-    }
-
-    override fun onDestroy(owner: LifecycleOwner) {
-        super.onDestroy(owner)
-        unregister()
-        activity?.lifecycle?.removeObserver(this)
-    }
-
-    override fun invoke(newConfig: Configuration) {
-        this.configuration = newConfig
-        onChanges(this)
-    }
-
-    @SuppressLint("NewApi")
-    fun hideSystemUI() {
-        if (systemUIVisible) {
-            systemUIVisible = false
-
-            window?.setFlags(
-                WindowManager.LayoutParams.FLAG_FULLSCREEN,
-                WindowManager.LayoutParams.FLAG_FULLSCREEN
-            )
-
-            try {
-                window?.decorView?.systemUiVisibility =
-                    View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-            } catch (t: Throwable) {
-                t.printStackTrace()
-            }
-
-            try {
-                window?.attributes?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
-            } catch (t: Throwable) {
-                t.printStackTrace()
-            }
-
-            try {
-                window?.setDecorFitsSystemWindows(false)
-                window?.insetsController?.apply {
-                    hide(statusBars() or navigationBars())
-                    systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
-                }
-            } catch (t: Throwable) {
-                t.printStackTrace()
-            }
-        }
-    }
-
-    @SuppressLint("InlinedApi")
-    fun showSystemUI() {
-        if (!systemUIVisible) {
-            systemUIVisible = true
-            window?.clearFlags(
-                WindowManager.LayoutParams.FLAG_FULLSCREEN
-            )
-
-            try {
-                window?.decorView?.systemUiVisibility =
-                    View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_VISIBLE
-            } catch (t: Throwable) {
-                t.printStackTrace()
-            }
-
-            try {
-                window?.attributes?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
-            } catch (t: Throwable) {
-                t.printStackTrace()
-            }
-
-            try {
-                window?.setDecorFitsSystemWindows(true)
-                window?.insetsController?.apply {
-                    show(statusBars() or navigationBars())
-                    systemBarsBehavior = BEHAVIOR_DEFAULT
-                }
-            } catch (t: Throwable) {
-                t.printStackTrace()
-            }
-        }
-    }
-
-//    companion object {
-//
-//        @Composable
-//        fun rememberSystemUIController(
-//            onChanges: SystemUIController.() -> Unit
-//        ): SystemUIController {
-//            val isEdit = isEditMode()
-//            val activity = if (isEdit) null else LocalContext.current as ComposableActivity
-//            return remember { SystemUIController(activity, onChanges) }
-//        }
-//
-//    }
-
-}

+ 4 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemAudio.kt

@@ -11,4 +11,8 @@ package org.mjdev.tvlib.interfaces
 @Suppress("unused")
 interface ItemAudio {
     val uri: Any?
+
+    companion object {
+        val ItemAudio.hasImage get() = uri?.toString()?.isNotEmpty() == true
+    }
 }

+ 4 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemPhoto.kt

@@ -11,4 +11,8 @@ package org.mjdev.tvlib.interfaces
 @Suppress("unused")
 interface ItemPhoto {
     val uri: Any?
+
+    companion object {
+        val ItemPhoto.hasImage get() = uri?.toString()?.isNotEmpty() == true
+    }
 }

+ 4 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemVideo.kt

@@ -11,4 +11,8 @@ package org.mjdev.tvlib.interfaces
 @Suppress("unused")
 interface ItemVideo {
     val uri: Any?
+
+    companion object {
+        val ItemVideo.hasImage get() = uri?.toString()?.isNotEmpty() == true
+    }
 }

+ 6 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithBackground.kt

@@ -8,6 +8,12 @@
 
 package org.mjdev.tvlib.interfaces
 
+@Suppress("unused")
 interface ItemWithBackground<T> {
     val background: T?
+
+    companion object {
+        val <T> ItemWithBackground<T>.hasBackground
+            get() = background?.toString()?.isNotEmpty() == true
+    }
 }

+ 5 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithDate.kt

@@ -8,6 +8,11 @@
 
 package org.mjdev.tvlib.interfaces
 
+@Suppress("unused")
 interface ItemWithDate {
     var date: String?
+
+    companion object {
+        val ItemWithDate.hasDate get() = date?.isNotEmpty() == true
+    }
 }

+ 6 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithDescription.kt

@@ -8,6 +8,12 @@
 
 package org.mjdev.tvlib.interfaces
 
+@Suppress("unused")
 interface ItemWithDescription<T> {
     val description: T?
+
+    companion object {
+        val <T> ItemWithDescription<T>.hasDescription
+            get() = description?.toString()?.isNotEmpty() == true
+    }
 }

+ 4 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithId.kt

@@ -8,8 +8,11 @@
 
 package org.mjdev.tvlib.interfaces
 
+@Suppress("unused")
 interface ItemWithId {
     val id: Long
 
-    val hasId: Boolean get() = (id != -1L)
+    companion object {
+        val ItemWithId.hasId: Boolean get() = (id != -1L)
+    }
 }

+ 6 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithImage.kt

@@ -8,6 +8,11 @@
 
 package org.mjdev.tvlib.interfaces
 
+@Suppress("unused")
 interface ItemWithImage<T> {
-    val image : T?
+    val image: T?
+
+    companion object {
+        val <T> ItemWithImage<T>.hasImage get() = image?.toString()?.isNotEmpty() == true
+    }
 }

+ 5 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithIntent.kt

@@ -10,6 +10,11 @@ package org.mjdev.tvlib.interfaces
 
 import android.content.Intent
 
+@Suppress("unused")
 interface ItemWithIntent {
     val intent: Intent?
+
+    companion object {
+        val ItemWithIntent.hasIntent get() = (intent != null)
+    }
 }

+ 5 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithMessage.kt

@@ -8,6 +8,11 @@
 
 package org.mjdev.tvlib.interfaces
 
+@Suppress("unused")
 interface ItemWithMessage<T> {
     val message: T?
+
+    companion object {
+        val <T> ItemWithMessage<T>.hasMessage get() = message?.toString()?.isNotEmpty() == true
+    }
 }

+ 6 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithSubtitle.kt

@@ -8,6 +8,11 @@
 
 package org.mjdev.tvlib.interfaces
 
-interface ItemWithSubtitle <T>{
+@Suppress("unused")
+interface ItemWithSubtitle<T> {
     val subtitle: T?
+
+    companion object {
+        val <T> ItemWithSubtitle<T>.hasSubtitle get() = subtitle?.toString()?.isNotEmpty() == true
+    }
 }

+ 6 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithTitle.kt

@@ -8,6 +8,11 @@
 
 package org.mjdev.tvlib.interfaces
 
+@Suppress("unused")
 interface ItemWithTitle<T> {
-    val title : T?
+    val title: T?
+
+    companion object {
+        val <T> ItemWithTitle<T>.hasTitle get() = title?.toString()?.isNotEmpty() == true
+    }
 }

+ 3 - 3
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithUri.kt

@@ -9,9 +9,9 @@
 package org.mjdev.tvlib.interfaces
 
 interface ItemWithUri<T> {
-
     val uri: T?
 
-    val hasUri get() = (uri != null) && (uri.toString().isNotEmpty())
-
+    companion object {
+        val <T> ItemWithUri<T>.hasUri get() = uri?.toString()?.isNotEmpty() == true
+    }
 }

+ 0 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/navigation/NavHostControllerEx.kt

@@ -133,8 +133,6 @@ class NavHostControllerEx(
         menuDrawerState.setValue(DrawerValue.Closed)
     }
 
-    val isMenuEnabled: Boolean get() = menuState.value
-
     inline fun <reified T : Activity> startActivity(
         vararg params: Pair<String, Serializable?>
     ) {

+ 0 - 30
tvlib/src/main/kotlin/org/mjdev/tvlib/network/ConnectivityCallback.kt

@@ -1,30 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
-package org.mjdev.tvlib.network
-
-import android.net.ConnectivityManager
-import android.net.Network
-
-class ConnectivityCallback(
-    val trySend: (data: NetworkStatus) -> Unit
-) : ConnectivityManager.NetworkCallback() {
-
-    override fun onAvailable(network: Network) {
-        trySend(NetworkStatus.Connected(network))
-    }
-
-    override fun onUnavailable() {
-        trySend(NetworkStatus.Disconnected)
-    }
-
-    override fun onLost(network: Network) {
-        trySend(NetworkStatus.Disconnected)
-    }
-
-}

+ 39 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/network/NetworkConnectivityService.kt

@@ -8,14 +8,49 @@
 
 package org.mjdev.tvlib.network
 
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flowOn
 
-interface NetworkConnectivityService {
+class NetworkConnectivityService constructor(
+    val context: Context
+) {
 
-    val networkStatus: Flow<NetworkStatus?>
+    private val connectivityManager by lazy {
+        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+    }
+
+    val networkStatus = callbackFlow {
+        val request = NetworkRequest.Builder()
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+            .build()
+        val networkStatusCallback = object : ConnectivityManager.NetworkCallback() {
+            override fun onUnavailable() {
+                trySend(NetworkStatus.Disconnected()).isSuccess
+            }
+
+            override fun onAvailable(network: Network) {
+                trySend(NetworkStatus.Connected(network)).isSuccess
+            }
+
+            override fun onLost(network: Network) {
+                trySend(NetworkStatus.Disconnected(network)).isSuccess
+            }
+        }
+        connectivityManager.registerNetworkCallback(request, networkStatusCallback)
+        awaitClose {
+            connectivityManager.unregisterNetworkCallback(networkStatusCallback)
+        }
+    }.flowOn(Dispatchers.IO)
 
     companion object {
 
@@ -23,7 +58,7 @@ interface NetworkConnectivityService {
         fun rememberNetworkService(): NetworkConnectivityService {
             val context = LocalContext.current
             return remember(context) {
-                NetworkConnectivityServiceImpl(context)
+                NetworkConnectivityService(context)
             }
         }
 

+ 0 - 68
tvlib/src/main/kotlin/org/mjdev/tvlib/network/NetworkConnectivityServiceImpl.kt

@@ -1,68 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
-package org.mjdev.tvlib.network
-
-import android.content.Context
-import android.net.ConnectivityManager
-import android.net.NetworkCapabilities
-import android.net.NetworkRequest
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.isActive
-import timber.log.Timber
-
-class NetworkConnectivityServiceImpl constructor(
-    val context: Context
-) : NetworkConnectivityService {
-
-    private val connectivityManager by lazy {
-        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
-    }
-
-    private val request by lazy {
-        NetworkRequest.Builder()
-            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
-            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
-            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
-            .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
-            .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
-            .build()
-    }
-
-    private var networkState: NetworkStatus? = null
-
-    private val connectivityCallback = ConnectivityCallback { status ->
-        networkState = status
-    }
-
-    override val networkStatus: Flow<NetworkStatus?> = callbackFlow {
-        try {
-            connectivityManager.registerNetworkCallback(request, connectivityCallback)
-        } catch (e: Exception) {
-            Timber.e(e)
-        }
-        while (isActive) {
-            trySend(networkState)
-            delay(200L)
-        }
-        awaitClose {
-            try {
-                connectivityManager.unregisterNetworkCallback(connectivityCallback)
-            } catch (e: Exception) {
-                Timber.e(e)
-            }
-        }
-    }.distinctUntilChanged().flowOn(Dispatchers.IO)
-
-}

+ 3 - 3
tvlib/src/main/kotlin/org/mjdev/tvlib/network/NetworkStatus.kt

@@ -16,11 +16,11 @@ import androidx.compose.runtime.State
 @Suppress("unused")
 sealed class NetworkStatus {
 
-    object Unknown : NetworkStatus()
+    data object Unknown : NetworkStatus()
 
-    class Connected(val network: Network?) : NetworkStatus()
+    class Connected(val network: Network? = null) : NetworkStatus()
 
-    object Disconnected : NetworkStatus()
+    class Disconnected(val network: Network? = null) : NetworkStatus()
 
 }
 

+ 7 - 26
tvlib/src/main/kotlin/org/mjdev/tvlib/screen/Screen.kt

@@ -10,6 +10,7 @@
 
 package org.mjdev.tvlib.screen
 
+import android.annotation.SuppressLint
 import androidx.annotation.CallSuper
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
@@ -35,9 +36,7 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.Text
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ModifierExt.onlyPortrait
-import org.mjdev.tvlib.extensions.NavExt.rememberNavControllerEx
 import org.mjdev.tvlib.navigation.MenuItem
-import org.mjdev.tvlib.navigation.NavHostControllerEx
 
 @Suppress("unused", "LeakingThis")
 open class Screen {
@@ -69,47 +68,29 @@ open class Screen {
             menuRoute = route
         ) else null
 
-    var navController: NavHostControllerEx? = null
     var backStackEntry: NavBackStackEntry? = null
     var args: Map<String, Any?> = emptyMap()
 
+    @SuppressLint("ComposableNaming")
     @Previews
     @Composable
     @CallSuper
-    open fun Compose() {
-        val navController = rememberNavControllerEx()
-        Compose(
-            navController,
-            null,
-            mapOf()
-        )
-    }
-
-    @Composable
-    open fun Compose(
-        navController: NavHostControllerEx,
-        backStackEntry: NavBackStackEntry?,
-        args: Map<String, Any?>
-    ) {
-        this.navController = navController
-        this.backStackEntry = backStackEntry
-        this.args = args
-        navController.menuState.value = !immersive
+    open fun recompose() {
         Box(
             modifier = Modifier
                 .fillMaxSize()
                 .onlyPortrait {
-                    navigationBarsPadding()
+                    // todo not working on lollipop
+                    navigationBarsPadding().statusBarsPadding()
                 }
-                .statusBarsPadding()
         ) {
-            ComposeScreen()
+            Content()
         }
     }
 
     @OptIn(ExperimentalTvMaterial3Api::class)
     @Composable
-    open fun ComposeScreen() {
+    open fun Content() {
         Column(
             modifier = Modifier
                 .fillMaxSize()

+ 3 - 7
tvlib/src/main/kotlin/org/mjdev/tvlib/screen/ScreenWithPages.kt

@@ -8,9 +8,9 @@
 
 package org.mjdev.tvlib.screen
 
+import androidx.annotation.CallSuper
 import androidx.compose.runtime.Composable
 import org.mjdev.tvlib.annotations.Previews
-import org.mjdev.tvlib.extensions.NavExt.rememberNavControllerEx
 import org.mjdev.tvlib.ui.components.page.Page.Companion.EMPTY_PAGE
 import org.mjdev.tvlib.ui.components.page.PagerScope
 import org.mjdev.tvlib.ui.components.page.TvPager
@@ -25,15 +25,11 @@ open class ScreenWithPages : Screen() {
 
     @Previews
     @Composable
-    override fun ComposeScreen() {
-        val navController = navController ?: rememberNavControllerEx()
+    @CallSuper
+    override fun Content() {
         TvPager(
-            navController = navController,
             startIndex = startPageIndex,
             pages = pages,
-            onPageChange = { _, _ ->
-                // todo, page state & update menu index
-            }
         )
     }
 

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/service/PlaybackService.kt

@@ -19,7 +19,7 @@ class PlaybackService : MediaSessionService() {
     private var mediaSession: MediaSession? = null
 
     val player by lazy {
-        ExoPlayer.Builder(applicationContext)
+        ExoPlayer.Builder(baseContext)
             .setAudioAttributes(
                 AudioAttributes.Builder()
                     .setUsage(C.USAGE_MEDIA)

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/DailyMotionVideoView.kt

@@ -68,7 +68,7 @@ class DailyMotionVideoView @JvmOverloads constructor(
     private val httpCache by lazy {
         Cache(
             directory = File(
-                context.applicationContext.cacheDir,
+                context.cacheDir,
                 "http_cache"
             ),
             maxSize = 1024L * 1024L * 1024L

+ 20 - 20
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/button/Button.kt

@@ -22,6 +22,7 @@ import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
@@ -59,28 +60,27 @@ fun Button(
     scale: ButtonScale = ButtonDefaults.NO_SCALE,
     focusState: MutableState<FocusState?> = rememberFocusState(),
     content: @Composable RowScope.() -> Unit = {
-        TextAny(text = text)
+        TextAny(
+            text = text,
+            color = color,
+            textAlign = TextAlign.Center,
+            maxLines = 1
+        )
     },
     onClick: () -> Unit = {},
-) {
-
-    Button(
-        modifier = modifier
-            .recomposeHighlighter()
-            .border(BorderStroke(borderSize, borderColor), shape)
-            .clickable {
-               onClick()
-            },
-        glow = glow,
-        scale = scale,
-        onClick = onClick,
-        colors = ButtonDefaults.colors(containerColor = containerColor),
-        shape = ButtonDefaults.shape(shape),
-        contentPadding = contentPadding,
-        content = content
-    )
-
-}
+) = Button(
+    modifier = modifier
+        .border(BorderStroke(borderSize, borderColor), shape)
+        .clickable { onClick() }
+        .recomposeHighlighter(),
+    glow = glow,
+    scale = scale,
+    onClick = onClick,
+    colors = ButtonDefaults.colors(containerColor = containerColor),
+    shape = ButtonDefaults.shape(shape),
+    contentPadding = contentPadding,
+    content = content
+)
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 val ButtonDefaults.NO_GLOW

+ 19 - 17
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/BigCarousel.kt

@@ -17,7 +17,7 @@ import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.unit.dp
@@ -26,8 +26,8 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
-import org.mjdev.tvlib.extensions.ModifierExt.detectSwipe
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
+import org.mjdev.tvlib.extensions.ModifierExt.swipeGestures
 import org.mjdev.tvlib.ui.components.card.FocusHelper
 
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -41,6 +41,8 @@ fun BigCarousel(
     carouselState: CarouselState = remember(items) { CarouselState() },
     onItemSelected: ((movie: Any?) -> Unit)? = null,
     onItemClicked: ((movie: Any?) -> Unit)? = null,
+    onSwipeUp: (dragAmount: Offset) -> Unit = {},
+    onSwipeDown: (dragAmount: Offset) -> Unit = {},
 ) {
     val height = LocalConfiguration.current.let { config ->
         if (config.orientation == Configuration.ORIENTATION_PORTRAIT) config.screenWidthDp * 0.4f
@@ -53,27 +55,27 @@ fun BigCarousel(
         carouselState = carouselState,
         itemCount = items.size,
         modifier = modifier
-            .recomposeHighlighter()
             .focusRequester(focusRequester)
             .fillMaxWidth()
             .height(height.dp)
             .onFocusChanged { state ->
                 cardFocusState.value = state
             }
-            .pointerInput(Unit) {
-                detectSwipe(
-                    onSwipeLeft = {
-                        carouselState.moveToNextItem(items.size)
-                        cardFocusState.value = FocusHelper(true)
-                        focusRequester.requestFocus()
-                    },
-                    onSwipeRight = {
-                        carouselState.moveToPreviousItem(items.size)
-                        cardFocusState.value = FocusHelper(true)
-                        focusRequester.requestFocus()
-                    },
-                )
-            },
+            .swipeGestures(
+                onSwipeLeft = {
+                    carouselState.moveToNextItem(items.size)
+                    cardFocusState.value = FocusHelper(true)
+                    focusRequester.requestFocus()
+                },
+                onSwipeRight = {
+                    carouselState.moveToPreviousItem(items.size)
+                    cardFocusState.value = FocusHelper(true)
+                    focusRequester.requestFocus()
+                },
+                onSwipeDown = onSwipeDown,
+                onSwipeUp = onSwipeUp
+            )
+            .recomposeHighlighter(),
     ) { indexOfCarouselItem ->
         val selectedItem = items[indexOfCarouselItem]
         onItemSelected?.invoke(selectedItem)

+ 33 - 29
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/Gallery.kt

@@ -22,7 +22,6 @@ import androidx.compose.animation.slideInVertically
 import androidx.compose.animation.slideOutVertically
 import androidx.compose.foundation.background
 import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
@@ -47,7 +46,6 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.key.onKeyEvent
-import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.dp
 import androidx.tv.foundation.lazy.list.TvLazyListState
@@ -58,7 +56,7 @@ import kotlinx.coroutines.delay
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 import org.mjdev.tvlib.extensions.GlobalExt.toggle
-import org.mjdev.tvlib.extensions.ModifierExt.detectSwipe
+import org.mjdev.tvlib.extensions.ModifierExt.swipeGestures
 import org.mjdev.tvlib.interfaces.ItemPhoto
 import org.mjdev.tvlib.interfaces.ItemWithBackground
 import org.mjdev.tvlib.interfaces.ItemWithImage
@@ -149,6 +147,18 @@ fun Gallery(
 ) {
     val initialized = remember { mutableStateOf(false) }
     val imageState: MutableState<Any?> = remember { mutableStateOf(Color.DarkGray) }
+    val nextItem: () -> Unit = {
+        if (currentItemIndex.intValue < (list.size - 1)) {
+            currentItemIndex.intValue += 1
+            imageState.value = list[currentItemIndex.intValue]
+        }
+    }
+    val prevItem: () -> Unit = {
+        if (currentItemIndex.intValue > 0) {
+            currentItemIndex.intValue -= 1
+            imageState.value = list[currentItemIndex.intValue]
+        }
+    }
     Box(
         modifier = modifier.background(Color.Black, RectangleShape),
         contentAlignment = Alignment.BottomCenter,
@@ -173,32 +183,26 @@ fun Gallery(
                             .fillMaxSize()
                             .focusable()
                             .focusRequester(focusRequester)
-                            .pointerInput(Unit) {
-                                detectTapGestures(
-                                    onDoubleTap = {
-                                        switchImageScale()
-                                    },
-                                    onTap = {
-                                        infoVisible.toggle()
-                                    }
-                                )
-                            }
-                            .pointerInput(Unit) {
-                                detectSwipe(
-                                    onSwipeLeft = {
-                                        if (currentItemIndex.intValue < (list.size - 1)) {
-                                            currentItemIndex.intValue += 1
-                                            imageState.value = list[currentItemIndex.intValue]
-                                        }
-                                    },
-                                    onSwipeRight = {
-                                        if (currentItemIndex.intValue > 0) {
-                                            currentItemIndex.intValue -= 1
-                                            imageState.value = list[currentItemIndex.intValue]
-                                        }
-                                    },
-                                )
-                            }
+                            .swipeGestures(
+                                onDoubleTap = {
+                                    switchImageScale()
+                                },
+                                onTap = {
+                                    infoVisible.toggle()
+                                },
+                                onSwipeLeft = {
+                                    nextItem()
+                                },
+                                onSwipeRight = {
+                                    prevItem()
+                                },
+                                onSwipeUp = {
+                                    nextItem()
+                                },
+                                onSwipeDown = {
+                                    prevItem()
+                                },
+                            )
                             .onKeyEvent { ev -> handleKey(ev) }
                     )
                     customOverlay(list[currentItemIndex.intValue])

+ 138 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/icon/ConversationReceiptIcon.kt

@@ -0,0 +1,138 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui.components.icon
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
+
+@Preview
+@Composable
+fun ConversationReceiptIcon(
+    backgroundColor: Color = Color.Black,
+    borderColor: Color = Color.Black,
+    tint: Color = Color.White,
+    borderSize: Dp = 2.dp,
+    centerIcon: ImageVector = Icons.Filled.Check,
+    size: Dp = 32.dp,
+    shape: Shape = RoundedCornerShape(size),
+    innerPadding: Dp = 4.dp,
+    contentDescription: String = "",
+) {
+    val colorFilter = if (tint == Color.Unspecified) null else ColorFilter.tint(tint)
+    Box(
+        modifier = Modifier
+            .size(size)
+            .clip(shape)
+            .background(backgroundColor, shape)
+            .border(
+                BorderStroke(
+                    borderSize,
+                    borderColor
+                ),
+                shape
+            ),
+    ) {
+        Image(
+            modifier = Modifier
+                .padding(innerPadding)
+                .size(size)
+                .background(backgroundColor, shape),
+            imageVector = centerIcon,
+            contentDescription = contentDescription,
+            contentScale = ContentScale.Inside,
+            colorFilter = colorFilter
+        )
+    }
+}
+
+@Preview
+@Composable
+fun TextDashed() {
+    Box(
+        modifier = Modifier.fillMaxWidth(),
+        contentAlignment = Alignment.Center
+    ) {
+        Text(
+            modifier = Modifier
+                .width(227.dp)
+                .background(
+                    Color.Black,
+                    shape = DottedShape(
+                        step = 4.dp,
+                        width = 0.75.dp,
+                    )
+                ),
+            textAlign = TextAlign.Center,
+            text = "Unread messages"
+        )
+    }
+}
+
+data class DottedShape(
+    val step: Dp,
+    val width: Dp = 1.dp,
+    val gravity: Alignment.Vertical = Alignment.Bottom
+) : Shape {
+    override fun createOutline(
+        size: Size,
+        layoutDirection: LayoutDirection,
+        density: Density
+    ) = Outline.Generic(Path().apply {
+        val gravityY = when (gravity) {
+            Alignment.Bottom -> size.height - width.value
+            Alignment.Top -> 0f
+            else -> 0f
+        }
+        val stepPx = with(density) { step.toPx() }
+        val stepsCount = (size.width / stepPx).roundToInt()
+        val actualStep = size.width / stepsCount
+        val dotSize = Size(width = actualStep / 2, height = width.value)
+        for (i in 0 until stepsCount) {
+            addRect(
+                Rect(
+                    offset = Offset(x = i * actualStep, y = gravityY),
+                    size = dotSize
+                )
+            )
+        }
+        close()
+    })
+}

+ 4 - 10
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/ExoPlayerImpl.kt

@@ -26,13 +26,11 @@ import android.view.TextureView
 import android.view.View
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.key.onPreviewKeyEvent
-import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.core.content.getSystemService
 import androidx.media3.common.AudioAttributes
@@ -259,12 +257,8 @@ class ExoPlayerImpl(
     @Suppress("UNUSED_VARIABLE")
     @UnstableApi
     @Composable
-    override fun GetPlayerView(
-        modifier: Modifier
-    ) {
+    override fun GetPlayerView() {
         val context = LocalContext.current
-        val width = LocalConfiguration.current.screenWidthDp
-        val height = LocalConfiguration.current.screenHeightDp
         val isEdit = isEditMode()
         val lifecycleObserver = rememberPlayPauseLifeCycleObserver(
             context,
@@ -273,8 +267,8 @@ class ExoPlayerImpl(
         )
         Box(
             modifier = Modifier
+                .fillMaxSize()
                 .recomposeHighlighter()
-                .size(width.dp, height.dp)
                 .onPreviewKeyEvent { ev ->
                     val controller = playerView.controller
                     if (controller?.isShown == true) {
@@ -289,7 +283,7 @@ class ExoPlayerImpl(
             if (!isEdit) {
                 AndroidView(
                     modifier = Modifier
-                        .size(width.dp, height.dp)
+                        .fillMaxSize()
                         .recomposeHighlighter(),
                     factory = {
                         playerView

+ 1 - 5
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/IMediaPlayer.kt

@@ -16,7 +16,6 @@ import android.view.SurfaceHolder
 import android.view.SurfaceView
 import android.view.TextureView
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
 import androidx.media3.common.AudioAttributes
 import androidx.media3.common.DeviceInfo
 import androidx.media3.common.MediaItem
@@ -39,9 +38,7 @@ interface IMediaPlayer : Player {
     val context: Context
 
     @Composable
-    fun GetPlayerView(
-        modifier: Modifier
-    )
+    fun GetPlayerView()
 
     fun setMediaUri(uri: Uri)
 
@@ -66,7 +63,6 @@ interface IMediaPlayer : Player {
 
             @Composable
             override fun GetPlayerView(
-                modifier: Modifier
             ) {
             }
 

+ 11 - 7
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerContainer.kt

@@ -18,6 +18,7 @@ import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.Modifier
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
+import org.mjdev.tvlib.extensions.ModifierExt.swipeGestures
 import org.mjdev.tvlib.ui.components.media.MediaPlayerState.Companion.rememberMediaPlayerState
 
 @SuppressLint("UnsafeOptInUsageError")
@@ -27,18 +28,21 @@ fun MediaPlayerContainer(
     modifier: Modifier = Modifier,
     state: MediaPlayerState = rememberMediaPlayerState(),
 ) {
+    val nextItem: () -> Unit = { state.player.seekToNext() }
+    val prevItem: () -> Unit = { state.player.seekToPrevious() }
     Box(
         modifier = modifier
-            .recomposeHighlighter()
             .fillMaxSize()
+            .swipeGestures(
+                onSwipeLeft = { nextItem() },
+                onSwipeRight = { prevItem() },
+                onSwipeUp = { nextItem() },
+                onSwipeDown = { prevItem() },
+            )
+            .recomposeHighlighter()
     ) {
-        state.player.GetPlayerView(
-            modifier = modifier.recomposeHighlighter()
-        )
+        state.player.GetPlayerView()
         PlayerControlView(
-            modifier = modifier
-                .fillMaxSize()
-                .recomposeHighlighter(),
             state = state
         )
     }

+ 4 - 10
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/NativePlayerImpl.kt

@@ -18,11 +18,9 @@ import android.view.SurfaceHolder
 import android.view.SurfaceView
 import android.view.TextureView
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.media3.common.AudioAttributes
 import androidx.media3.common.DeviceInfo
@@ -49,21 +47,17 @@ class NativePlayerImpl(
 ) : IMediaPlayer {
 
     @Composable
-    override fun GetPlayerView(
-        modifier: Modifier
-    ) {
-        val width = LocalConfiguration.current.screenWidthDp
-        val height = LocalConfiguration.current.screenHeightDp
+    override fun GetPlayerView() {
         val isEdit = isEditMode()
         Box(
             modifier = Modifier
+                .fillMaxSize()
                 .recomposeHighlighter()
-                .size(width.dp, height.dp)
         ) {
             if (!isEdit) {
                 AndroidView(
                     modifier = Modifier
-                        .size(width.dp, height.dp)
+                        .fillMaxSize()
                         .recomposeHighlighter(),
                     factory = { context ->
                         SurfaceView(context).apply {

+ 17 - 18
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/ModalNavigationDrawer.kt

@@ -25,28 +25,28 @@ import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
 import androidx.tv.material3.DrawerState
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.rememberDrawerState
 import org.mjdev.tvlib.annotations.Previews
-import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
+import org.mjdev.tvlib.extensions.ComposeExt.rememberDrawerState
 
 @ExperimentalTvMaterial3Api
 @Previews
 @Composable
 fun ModalNavigationDrawer(
     modifier: Modifier = Modifier,
-    drawerState: DrawerState = rememberDrawerState(
-        if (isEditMode()) DrawerValue.Open else DrawerValue.Closed
-    ),
+    drawerState: DrawerState = rememberDrawerState(),
     scrimColor: Color = Color.Black.copy(alpha = 0.5f),
-    closeDrawerWidth: Dp = 200.dp,
+    visible: Boolean = true,
+    closeDrawerWidth: Dp = if (visible) 200.dp else 0.dp,
     contentAlignment: Alignment = Alignment.TopStart,
     closedDrawerWidth: MutableState<Dp?> = remember { mutableStateOf(null) },
+    localDensity: Density = LocalDensity.current,
     onTouchOutside: () -> Unit = {},
     drawerContent: @Composable (DrawerValue) -> Unit = {
         Box(
@@ -57,9 +57,8 @@ fun ModalNavigationDrawer(
         Box(
             modifier = Modifier.fillMaxSize()
         )
-    },
+    }
 ) {
-    val localDensity = LocalDensity.current
     Box(
         modifier = modifier,
         contentAlignment = contentAlignment
@@ -91,19 +90,19 @@ fun ModalNavigationDrawer(
             modifier = when (contentAlignment) {
                 Alignment.TopStart -> Modifier
                     .padding(
-                    closedDrawerWidth.value ?: closeDrawerWidth,
-                    0.dp,
-                    0.dp,
-                    0.dp
-                )
+                        closedDrawerWidth.value ?: closeDrawerWidth,
+                        0.dp,
+                        0.dp,
+                        0.dp
+                    )
 
                 Alignment.TopEnd -> Modifier
                     .padding(
-                    0.dp,
-                    0.dp,
-                    closedDrawerWidth.value ?: closeDrawerWidth,
-                    0.dp
-                )
+                        0.dp,
+                        0.dp,
+                        closedDrawerWidth.value ?: closeDrawerWidth,
+                        0.dp
+                    )
 
                 else -> Modifier.padding(0.dp)
             },

+ 29 - 23
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavDrawerContent.kt

@@ -9,26 +9,28 @@
 package org.mjdev.tvlib.ui.components.navigation
 
 import android.annotation.SuppressLint
-import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableIntState
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.tv.material3.DrawerState
+import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvlib.annotations.Previews
-import org.mjdev.tvlib.extensions.ComposeExt.isClosed
-import org.mjdev.tvlib.extensions.ComposeExt.isPortraitMode
+import org.mjdev.tvlib.extensions.ModifierExt.onlyPortrait
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.NavControllerExt.open
 import org.mjdev.tvlib.extensions.NavExt.rememberNavControllerEx
@@ -72,6 +74,9 @@ fun NavDrawerContent(
     itemId: (menuItem: MenuItem) -> Int = { menuItem ->
         navController.indexOfMenuItem(menuItem)
     },
+    // todo, padding
+    minPortraitMenuWidth: Dp = 4.dp,
+    menuDrawerState: DrawerState = navController.menuDrawerState
 ) {
     val topItems = navController.menuItems.filter { mi ->
         mi.menuGravity == MenuItem.Gravity.Top
@@ -82,26 +87,27 @@ fun NavDrawerContent(
     val centerItems = navController.menuItems.filter { mi ->
         mi.menuGravity == MenuItem.Gravity.Center
     }
-    val isPortrait = isPortraitMode()
-    val isClosed = navController.menuDrawerState.isClosed
+    // todo
+    val paddingState = remember(menuDrawerState.currentValue) {
+        derivedStateOf {
+            when(menuDrawerState.currentValue) {
+                DrawerValue.Closed -> minPortraitMenuWidth
+                DrawerValue.Open -> 0.dp
+            }
+        }
+    }
     Box(
         modifier = Modifier
-            .recomposeHighlighter()
-            .background(
-                if (isPortrait) {
-                    if (isClosed) Color.Transparent
-                    else backgroundColor
-                } else {
-                    backgroundColor
-                }, shape
-            )
-            .padding(0.dp, 4.dp, 0.dp, 4.dp)
-            .fillMaxHeight(),
+            .fillMaxHeight()
+            .onlyPortrait {
+                widthIn(min = paddingState.value)
+            }
+            .recomposeHighlighter(),
     ) {
         Column(
             modifier = Modifier
-                .recomposeHighlighter()
-                .fillMaxHeight(),
+                .fillMaxHeight()
+                .recomposeHighlighter(),
             verticalArrangement = Arrangement.Bottom,
             horizontalAlignment = Alignment.Start,
         ) {
@@ -128,8 +134,8 @@ fun NavDrawerContent(
         }
         Column(
             modifier = Modifier
-                .recomposeHighlighter()
-                .fillMaxHeight(),
+                .fillMaxHeight()
+                .recomposeHighlighter(),
             verticalArrangement = Arrangement.Top,
             horizontalAlignment = Alignment.Start,
         ) {
@@ -156,8 +162,8 @@ fun NavDrawerContent(
         }
         Column(
             modifier = Modifier
-                .recomposeHighlighter()
-                .fillMaxHeight(),
+                .fillMaxHeight()
+                .recomposeHighlighter(),
             verticalArrangement = Arrangement.Center,
             horizontalAlignment = Alignment.Start,
         ) {

+ 52 - 38
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/Navigation.kt

@@ -9,18 +9,24 @@
 package org.mjdev.tvlib.ui.components.navigation
 
 import android.annotation.SuppressLint
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.navigationBarsPadding
 import androidx.compose.foundation.layout.statusBarsPadding
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
+import org.mjdev.tvlib.extensions.ComposeExt.isLandscapeMode
+import org.mjdev.tvlib.extensions.ComposeExt.isOpen
 import org.mjdev.tvlib.extensions.ComposeExt.isPortraitMode
+import org.mjdev.tvlib.extensions.ModifierExt.onlyPortrait
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.NavExt.rememberNavControllerEx
 import org.mjdev.tvlib.navigation.MenuItem
@@ -33,18 +39,18 @@ import org.mjdev.tvlib.ui.components.page.Page
 @Composable
 fun Navigation(
     modifier: Modifier = Modifier,
-    navController: NavHostControllerEx = rememberNavControllerEx(),
-    content: @Composable () -> Unit = { Page() },
     menuItems: List<MenuItem> = if (isEditMode()) listOf(
         MenuItem.MENU_ITEM_EXIT,
         MenuItem.MENU_ITEM_SETTINGS,
         MenuItem.MENU_ITEM_SEARCH
     ) else listOf(),
     modal: Boolean = isPortraitMode(),
+    backgroundColor: Color = Color.DarkGray,
+    navController: NavHostControllerEx = rememberNavControllerEx(menuItems),
+    minPortraitMenuWidth: Dp = 4.dp,
+    content: @Composable () -> Unit = { Page() },
 ) {
-    navController.addMenuItem(*menuItems.toTypedArray())
-    val isEdit: Boolean = isEditMode()
-    if (isEdit) navController.openMenu()
+    if (isEditMode()) navController.openMenu()
     val mainContent: @Composable () -> Unit = {
         Box(
             modifier
@@ -54,9 +60,31 @@ fun Navigation(
             content()
         }
     }
+    val drawerContent: @Composable (DrawerValue) -> Unit = { state ->
+        Box(
+            modifier = Modifier
+                .background(
+                    if (isLandscapeMode() || navController.menuDrawerState.isOpen)
+                        backgroundColor
+                    else
+                        Color.Transparent
+                )
+                .recomposeHighlighter()
+        ) {
+            NavDrawerContent(
+                minPortraitMenuWidth = minPortraitMenuWidth,
+                navController = navController,
+            )
+        }
+        navController.menuDrawerState.setValue(state)
+    }
     Box(
         modifier
             .fillMaxSize()
+            .onlyPortrait {
+                // todo not working on lollipop
+                navigationBarsPadding().statusBarsPadding()
+            }
             .recomposeHighlighter()
     ) {
         SettingsDrawer(
@@ -66,39 +94,25 @@ fun Navigation(
                 navController.closeSettings()
             }
         ) {
-            if (navController.isMenuEnabled) {
-                val drawerContent: @Composable (DrawerValue) -> Unit = { state ->
-                    Box(
-                        modifier = Modifier
-                            .navigationBarsPadding()
-                            .statusBarsPadding()
-                    ) {
-                        NavDrawerContent(
-                            navController = navController,
-                        )
-                    }
-                    navController.menuDrawerState.setValue(state)
-                }
-                if (modal) {
-                    ModalNavigationDrawer(
-                        modifier = modifier.fillMaxHeight(),
-                        drawerState = navController.menuDrawerState,
-                        content = mainContent,
-                        drawerContent = drawerContent,
-                        onTouchOutside = {
-                            navController.closeMenu()
-                        }
-                    )
-                } else {
-                    NavigationDrawer(
-                        modifier = modifier.fillMaxHeight(),
-                        drawerState = navController.menuDrawerState,
-                        content = mainContent,
-                        drawerContent = drawerContent
-                    )
-                }
+            if (modal) {
+                ModalNavigationDrawer(
+                    modifier = modifier.fillMaxSize(),
+                    drawerState = navController.menuDrawerState,
+                    content = mainContent,
+                    drawerContent = drawerContent,
+                    onTouchOutside = {
+                        navController.closeMenu()
+                    },
+                    visible = navController.menuState.value,
+                )
             } else {
-                mainContent()
+                NavigationDrawer(
+                    modifier = modifier.fillMaxSize(),
+                    drawerState = navController.menuDrawerState,
+                    content = mainContent,
+                    drawerContent = drawerContent,
+                    visible = navController.menuState.value,
+                )
             }
         }
     }

+ 7 - 6
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavigationDrawer.kt

@@ -21,22 +21,23 @@ import androidx.compose.ui.unit.dp
 import androidx.tv.material3.DrawerState
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.rememberDrawerState
 import org.mjdev.tvlib.annotations.Previews
-import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
+import org.mjdev.tvlib.extensions.ComposeExt.rememberDrawerState
+import org.mjdev.tvlib.extensions.ModifierExt.conditional
 
 @ExperimentalTvMaterial3Api
 @Previews
 @Composable
 fun NavigationDrawer(
     modifier: Modifier = Modifier,
-    drawerState: DrawerState = rememberDrawerState(
-        if (isEditMode()) DrawerValue.Open else DrawerValue.Closed
-    ),
+    visible: Boolean = true,
+    drawerState: DrawerState = rememberDrawerState(),
     drawerContent: @Composable (DrawerValue) -> Unit = {
         Box(
             modifier = Modifier
-                .width(200.dp)
+                .conditional(visible) {
+                    width(200.dp)
+                }
                 .background(Color.LightGray, RectangleShape)
         )
     },

+ 2 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavigationRow.kt

@@ -38,13 +38,13 @@ import androidx.compose.ui.unit.dp
 import androidx.tv.material3.DrawerState
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.rememberDrawerState
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ComposeExt.isFocused
 import org.mjdev.tvlib.extensions.ComposeExt.isLandscapeMode
 import org.mjdev.tvlib.extensions.ComposeExt.isOpen
 import org.mjdev.tvlib.extensions.ComposeExt.isPortraitMode
+import org.mjdev.tvlib.extensions.ComposeExt.rememberDrawerState
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
 import org.mjdev.tvlib.extensions.ModifierExt.focusState
@@ -73,7 +73,7 @@ fun NavigationRow(
     strokeWidth: Dp = 1.dp,
     margin: Dp = 0.dp,
     padding: Dp = 4.dp,
-    drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
+    drawerState: DrawerState = rememberDrawerState(),
     focused: Boolean = isEditMode(),
     focusState: MutableState<FocusState?> = rememberFocusState(
         text,

+ 2 - 7
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/SettingsDrawer.kt

@@ -14,21 +14,16 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.tv.material3.DrawerState
-import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.rememberDrawerState
 import org.mjdev.tvlib.annotations.Previews
-import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
+import org.mjdev.tvlib.extensions.ComposeExt.rememberDrawerState
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Previews
 @Composable
 fun SettingsDrawer(
     modifier: Modifier = Modifier,
-    drawerState: DrawerState = rememberDrawerState(
-        if (isEditMode()) DrawerValue.Open
-        else DrawerValue.Closed
-    ),
+    drawerState: DrawerState = rememberDrawerState(),
     onTouchOutside: () -> Unit = {},
     content: @Composable () -> Unit = {
         Box(modifier = Modifier.fillMaxSize())

+ 3 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/Page.kt

@@ -61,7 +61,8 @@ open class Page {
                     modifier = Modifier
                         .fillMaxSize()
                         .focusRequester(focusRequester)
-                        .recomposeHighlighter()
+                        .recomposeHighlighter(),
+                    contentAlignment = Alignment.Center
                 ) {
                     Content()
                 }
@@ -79,9 +80,7 @@ open class Page {
             horizontalAlignment = Alignment.CenterHorizontally,
         ) {
             TextAny(
-                modifier = Modifier
-                    .fillMaxSize()
-                    .recomposeHighlighter(),
+                modifier = Modifier.recomposeHighlighter(),
                 text = title ?: "Empty Page",
                 textAlign = TextAlign.Center,
                 fontSize = 32.sp,

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/TvPager.kt

@@ -42,7 +42,6 @@ import org.mjdev.tvlib.ui.components.page.Page.Companion.EMPTY_PAGE
 @Previews
 @Composable
 fun TvPager(
-    navController: NavHostControllerEx = rememberNavControllerEx(),
     startIndex: Int = 0,
     roundCornerSize: Dp = 0.dp,
     backGroundColor: Color = Color.Transparent,
@@ -54,6 +53,7 @@ fun TvPager(
     }
 ) {
 
+    val navController: NavHostControllerEx = rememberNavControllerEx()
     val currentPage = remember { mutableStateOf<Page?>(null) }
     val coroutineScope = rememberCoroutineScope()
     val pagerScope = rememberPagerScope(navController, startIndex, pages, onPageChange)

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/screen/EmptyScreen.kt

@@ -18,7 +18,7 @@ internal class EmptyScreen : Screen() {
 
     @Previews
     @Composable
-    override fun Compose() = super.Compose()
+    override fun Content() = super.Content()
 
     companion object {
 

+ 1 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/screen/ScreenView.kt

@@ -34,9 +34,8 @@ fun ScreenView(
             .recomposeHighlighter(),
         contentAlignment = Alignment.Center,
     ) {
-        if (navController.isMenuEnabled) {
+        if (navController.menuState.value) {
             Navigation(
-                navController = navController,
                 content = content
             )
         } else content()

+ 8 - 8
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/AppUpdateAvailableMessage.kt

@@ -10,18 +10,18 @@ package org.mjdev.tvlib.ui.components.tv
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.tooling.preview.Preview
 import org.mjdev.tvlib.R
-import org.mjdev.tvlib.annotations.Previews
-import org.mjdev.tvlib.extensions.ComposeExt
 import org.mjdev.tvlib.extensions.ComposeExt.asException
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.updater.AppUpdater
 import org.mjdev.tvlib.updater.AppUpdater.Companion.rememberAppUpdater
 
-@Previews
+@Preview
 @Composable
 fun AppUpdateAvailableMessage(
     title: Any? = R.string.title_app_update,
@@ -29,16 +29,16 @@ fun AppUpdateAvailableMessage(
     githubUser: String = "mimoccc",
     githubRepository: String = "tvapp",
     backgroundColor: Color = Color(0, 132, 0, 255),
-    buttonText:Any? = R.string.bt_update,
+    buttonText: Any? = R.string.bt_update,
+    dismissState: MutableState<Boolean> = mutableStateOf(false),
     appUpdater: AppUpdater = rememberAppUpdater(
         githubUser = githubUser,
         githubRepository = githubRepository
     ),
-    dismissState: MutableState<Boolean> = mutableStateOf(false),
-    onButtonClick : ()->Unit = { appUpdater.updateApp() },
+    onButtonClick: () -> Unit = { appUpdater.updateApp() },
 ) {
-    val isEdit = isEditMode()
-    if (isEdit || appUpdater.isUpdateAvailable) {
+    val updateAvailable by appUpdater.isUpdateAvailable.collectAsState(false)
+    if (isEditMode() || updateAvailable) {
         ErrorMessage(
             title = title,
             dismissText = buttonText,

+ 24 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/BrowseView.kt

@@ -8,6 +8,7 @@
 
 package org.mjdev.tvlib.ui.components.tv
 
+import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
@@ -17,8 +18,12 @@ import androidx.compose.foundation.layout.heightIn
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.list.TvLazyListState
+import androidx.tv.foundation.lazy.list.rememberTvLazyListState
+import kotlinx.coroutines.launch
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.interfaces.ItemWithTitle
@@ -55,15 +60,18 @@ fun BrowseView(
     onMessageBadgeClicked: () -> Unit = {},
     onUserPicClicked: () -> Unit = {},
     onItemFocused: (item: Any?) -> Unit = {},
-    onItemClicked: (item: Any?) -> Unit = {}
+    onItemClicked: (item: Any?) -> Unit = {},
+    state: TvLazyListState = rememberTvLazyListState(),
+    isEdit: Boolean = isEditMode()
 ) {
-    val isEdit = isEditMode()
+    val coroutineScope = rememberCoroutineScope()
     ScrollableTvLazyColumn(
         modifier = modifier
             .fillMaxSize()
             .recomposeHighlighter(),
         verticalArrangement = verticalArrangement,
-        contentPadding = contentPadding
+        contentPadding = contentPadding,
+        state = state
     ) {
         if (isEdit || showHeader) item {
             Header(
@@ -120,7 +128,19 @@ fun BrowseView(
                 modifier = Modifier.recomposeHighlighter(),
                 items = featuredItems,
                 onItemSelected = onItemFocused,
-                onItemClicked = onItemClicked
+                onItemClicked = onItemClicked,
+                onSwipeUp = { dragOffset ->
+                    coroutineScope.launch {
+                        // todo, better scroll
+                        state.scrollBy(-dragOffset.y)
+                    }
+                },
+                onSwipeDown = { dragOffset ->
+                    coroutineScope.launch {
+                        // todo, better scroll
+                        state.scrollBy(-dragOffset.y)
+                    }
+                }
             )
         }
         items(customRows) { row -> row.invoke() }

+ 55 - 79
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/ErrorMessage.kt

@@ -9,14 +9,14 @@
 package org.mjdev.tvlib.ui.components.tv
 
 import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
@@ -24,7 +24,6 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.graphics.takeOrElse
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextAlign
@@ -33,8 +32,6 @@ 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.LocalContentColor
-import androidx.tv.material3.LocalTextStyle
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.ui.components.button.Button
 import org.mjdev.tvlib.R
@@ -45,93 +42,72 @@ import org.mjdev.tvlib.ui.components.text.TextAny
 @Previews
 @Composable
 fun ErrorMessage(
-    error: Throwable? = null,
-    contentPadding: Dp = 2.dp,
     title: Any? = null,
+    error: Throwable? = Exception("An unknown error happen, please contact developer, if issue does not disappear."),
+    dismissText: Any? = R.string.bt_dismiss,
+    color: Color = Color.White,
+    backgroundColor: Color = Color.Red,
     fontSizeTitle: TextUnit = 18.sp,
     fontSizeMessage: TextUnit = 12.sp,
+    fontSizeButton: TextUnit = 12.sp,
     fontWeight: FontWeight = FontWeight.Bold,
-    color: Color = Color.White,
-    paddingTitle: Dp = 4.dp,
-    paddingMessage: Dp = paddingTitle,
-    backgroundColor: Color = Color.Red,
+    paddingTitle: Dp = 8.dp,
+    paddingContent: Dp = 4.dp,
     roundSize: Dp = 8.dp,
-    dismissText: Any? = R.string.bt_dismiss,
-    shape: Shape = RoundedCornerShape(roundSize),
+    borderSize: Dp = 1.dp,
+    borderColor: Color = Color.DarkGray,
+    backgroundShape: Shape = RoundedCornerShape(roundSize),
     dismissible: Boolean = true,
     dismissState: MutableState<Boolean> = mutableStateOf(false),
-    dismissOnClick: () -> Unit = {},
-    customButtons: @Composable () -> Unit = {},
+    dismissOnClick: () -> Unit = {}
 ) {
     if (!dismissState.value) {
-        Row(
-            modifier = Modifier
-                .fillMaxWidth()
-                .background(backgroundColor, shape)
-                .padding(contentPadding)
-                .recomposeHighlighter(),
-            verticalAlignment = Alignment.CenterVertically,
-        ) {
-            val style = LocalTextStyle.current
-            val textColor = color.takeOrElse {
-                style.color.takeOrElse {
-                    LocalContentColor.current
-                }
-            }
-            TextAny(
+        BoxWithConstraints {
+            Row(
                 modifier = Modifier
-                    .recomposeHighlighter()
-                    .wrapContentSize()
-                    .padding(paddingTitle),
-                text = title ?: stringResource(id = R.string.title_error),
-                fontWeight = fontWeight,
-                fontSize = fontSizeTitle,
-                color = color
-            )
-            Spacer(
-                modifier = Modifier.weight(1f, true)
-            )
-            BasicText(
-                modifier = Modifier
-                    .recomposeHighlighter()
-                    .wrapContentSize()
-                    .padding(paddingMessage),
-                style = style.merge(
-                    color = textColor,
-                    fontSize = fontSizeMessage,
+                    .width(constraints.maxWidth.dp)
+                    .padding(paddingContent)
+                    .border(borderSize, borderColor, backgroundShape)
+                    .background(backgroundColor, backgroundShape)
+                    .recomposeHighlighter(),
+                verticalAlignment = Alignment.CenterVertically,
+            ) {
+                TextAny(
+                    modifier = Modifier
+                        .padding(paddingTitle)
+                        .recomposeHighlighter(),
+                    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))
-            )
-            if (dismissible && (dismissText != null)) {
-                Spacer(
-                    modifier = Modifier.weight(1f, true)
+                    fontSize = fontSizeTitle,
+                    color = color
                 )
-                customButtons()
-                Button(
+                TextAny(
                     modifier = Modifier
-                        .recomposeHighlighter()
-                        .padding(4.dp),
-                    contentPadding = PaddingValues(
-                        8.dp,
-                        4.dp
-                    ),
-                    containerColor = backgroundColor,
-                    onClick = {
-                        dismissState.value = true
-                        dismissOnClick.invoke()
-                    },
-                    content = {
-                        TextAny(
-                            text = dismissText,
-                            color = color,
-                            maxLines = 1
-                        )
-                    }
+                        .fillMaxWidth()
+                        .weight(1f, false)
+                        .recomposeHighlighter(),
+                    color = color,
+                    fontSize = fontSizeMessage,
+                    fontWeight = fontWeight,
+                    textAlign = TextAlign.Left,
+                    softWrap = true,
+                    text = error?.message ?: (stringResource(id = R.string.error_unknown))
                 )
+                if (dismissible && (dismissText != null)) {
+                    Button(
+                        modifier = Modifier
+                            .padding(4.dp)
+                            .recomposeHighlighter(),
+                        text = dismissText,
+                        fontSize = fontSizeButton,
+                        contentPadding = PaddingValues(8.dp, 0.dp),
+                        containerColor = Color.Black.copy(alpha = 0.2f),
+                        onClick = {
+                            dismissState.value = true
+                            dismissOnClick.invoke()
+                        }
+                    )
+                }
             }
         }
     }

+ 1 - 2
app/src/main/kotlin/org/mjdev/tvapp/ui/theme/Theme.kt → tvlib/src/main/kotlin/org/mjdev/tvlib/ui/theme/Theme.kt

@@ -6,12 +6,11 @@
  * w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.ui.theme
+package org.mjdev.tvlib.ui.theme
 
 import android.os.Build
 import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.runtime.Composable
-import org.mjdev.tvlib.ui.theme.ThemeHelper
 
 @Composable
 fun TVAppTheme(

+ 44 - 9
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/theme/ThemeHelper.kt

@@ -10,20 +10,32 @@
 
 package org.mjdev.tvlib.ui.theme
 
+import android.app.Activity
 import android.os.Build
+import androidx.compose.foundation.Indication
+import androidx.compose.foundation.IndicationInstance
+import androidx.compose.foundation.LocalIndication
+import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.text.TextStyle
 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.core.view.WindowCompat
 import androidx.tv.material3.ColorScheme
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.MaterialTheme
@@ -38,7 +50,12 @@ fun ThemeHelper(
     dynamicColor: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S,
     content: @Composable () -> Unit,
 ) {
-//    val context = LocalContext.current
+    val context = LocalContext.current
+    val view = LocalView.current
+    val activity = context as? Activity
+    val window = activity?.window
+    val isEditMode = view.isInEditMode
+
     val colorScheme: ColorScheme = when {
 //        dynamicColor -> {
 //            if (useDarkTheme)
@@ -55,21 +72,37 @@ fun ThemeHelper(
         }
     }
 
-//    val view = LocalView.current
-//    if (!view.isInEditMode) {
-//        SideEffect {
-//            (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
-//            ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = useDarkTheme
-//        }
-//    }
+    if (!isEditMode) {
+        SideEffect {
+            window?.apply {
+                navigationBarColor = Color.Transparent.toArgb()
+                statusBarColor = Color.Transparent.toArgb()
+                WindowCompat.getInsetsController(window, view).apply {
+                    isAppearanceLightStatusBars = !useDarkTheme
+                    isAppearanceLightNavigationBars = !useDarkTheme
+                }
+            }
+        }
+    }
 
     MaterialTheme(
         colorScheme = colorScheme,
-        content = content
+        content = {
+            CompositionLocalProvider(
+                LocalIndication provides NoRipple,
+                content = content,
+            )
+        },
     )
 
 }
 
+private object NoRipple : Indication, IndicationInstance {
+    @Composable
+    override fun rememberUpdatedInstance(interactionSource: InteractionSource) = this
+    override fun ContentDrawScope.drawIndication() = drawContent()
+}
+
 val LocalCardShape = staticCompositionLocalOf<Shape> {
     RoundedCornerShape(8.dp)
 }
@@ -86,10 +119,12 @@ class ThemeProvider {
     val detailTextPaddingVertical: Dp = 32.dp
     val detailsTextColor: Color = Color.White
     val detailTextBackgroundColor: Color = Color.Black
+
     @OptIn(ExperimentalTvMaterial3Api::class)
     val detailsTitleTextStyle: TextStyle
         @Composable
         get() = MaterialTheme.typography.headlineSmall
+
     @OptIn(ExperimentalTvMaterial3Api::class)
     val detailsTextStyle: TextStyle
         @Composable

+ 62 - 107
tvlib/src/main/kotlin/org/mjdev/tvlib/updater/AppUpdater.kt

@@ -9,18 +9,25 @@
 package org.mjdev.tvlib.updater
 
 import android.content.Context
+import android.content.Context.MODE_PRIVATE
+import android.content.Intent
 import androidx.annotation.MainThread
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
+import androidx.core.net.toUri
 import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
 import com.skydoves.sandwich.adapters.ApiResponseCallAdapterFactory
 import com.skydoves.sandwich.getOrNull
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.launch
 import okhttp3.OkHttpClient
 import okhttp3.logging.HttpLoggingInterceptor
 import org.mjdev.tvlib.BuildConfig
@@ -28,14 +35,15 @@ import org.mjdev.tvlib.data.github.remote.Release
 import org.mjdev.tvlib.network.CacheInterceptor
 import retrofit2.Retrofit
 import retrofit2.converter.moshi.MoshiConverterFactory
-import timber.log.Timber
-import java.util.Date
+import java.text.SimpleDateFormat
+import java.util.Locale
 
-// todo better logic
 class AppUpdater(
     private val context: Context,
     private val githubUser: String = "mimoccc",
     private val githubRepository: String = "tvapp",
+    private val repoName: String = AppUpdater::class.simpleName ?: "AppUpdater",
+    private val keyName: String = repoName
 ) {
 
     private val service by lazy {
@@ -43,116 +51,64 @@ class AppUpdater(
     }
 
     private val prefs by lazy {
-        context.applicationContext.getSharedPreferences(
-            AppUpdater::class.simpleName,
-            Context.MODE_PRIVATE
-        )
+        context.getSharedPreferences(repoName, MODE_PRIVATE)
     }
 
     private val releases: Flow<List<Release>> = flow {
         emit(service.releases().getOrNull() ?: emptyList())
-    }
-
-    private val lastRelease
-        get() = releases.map { list ->
-            list.filter { it.hasAPK }.maxOfOrNull { it.publishedAt }
+    }.flowOn(Dispatchers.IO)
+
+    private val lastRelease: Flow<Release?> =
+        releases.map {
+            it.filter { r ->
+                r.hasAPK
+            }.minByOrNull { r ->
+                r.publishedAt
+            }
         }
 
-    private val serverTimestamp: Long
-        get() = runBlocking {
-            lastRelease.firstOrNull()?.asTimestamp ?: 0L
+    private val serverTimestamp: Flow<Long> =
+        lastRelease.map { r ->
+            r?.publishedAt?.asTimestamp ?: 0L
         }
 
-    private val installedTimestamp
-        get() = prefs.getLong(AppUpdater::class.simpleName, serverTimestamp)
-
-//    private val externalFile: File
-//        get() = File(
-//            context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
-//            "$githubRepository-update.apk"
-//        )
+    private val installedTimestamp: Flow<Long> = serverTimestamp.map { st ->
+        prefs.getLong(keyName, st)
+    }
 
-    val isUpdateAvailable: Boolean get() = serverTimestamp > installedTimestamp
+    val isUpdateAvailable: Flow<Boolean> = serverTimestamp.map { st ->
+        val lt = installedTimestamp.firstOrNull() ?: 0L
+        if (lt == 0L) {
+            prefs.edit().putLong(keyName, st).apply()
+            false
+        } else (st > lt)
+    }
 
     @MainThread
     fun updateApp() {
-        // todo
-//        CoroutineScope(Dispatchers.IO).launch {
-//            releases.collectLatest { rs ->
-//                rs.filter { r ->
-//                    r.hasAPK
-//                }.maxByOrNull { r ->
-//                    r.publishedAt.asTimestamp
-//                }?.let { release ->
-//                    release.apkAsset?.downloadUrl
-//                }?.also { url ->
-//                    downloadFile(url) {
-//                        prefs.edit()
-//                            .putLong(AppUpdater::class.simpleName, lastReleaseTimestamp)
-//                            .apply()
-////                        CoroutineScope(Dispatchers.Main).launch {
-////                            startUpdate()
-////                        }
-//                    }
-//                }
-//            }
-//        }
+        CoroutineScope(Dispatchers.IO).launch {
+            releases.collectLatest { rs ->
+                rs.filter { r ->
+                    r.hasAPK
+                }.maxByOrNull { r ->
+                    r.publishedAt.asTimestamp
+                }?.let { release ->
+                    release.apkAsset?.downloadUrl
+                }?.also { url ->
+                    downloadFile(url)
+                }
+            }
+        }
     }
 
-//    @AnyThread
-//    private fun downloadFile(
-//        url: String,
-//        finalizeBlock: () -> Unit
-//    ) {
-//        // todo simply open download yet, until finished
-//        try {
-//            context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
-//                flags = Intent.FLAG_ACTIVITY_NEW_TASK
-//            })
-//            finalizeBlock()
-//        } catch (e: ActivityNotFoundException) {
-//            Timber.e(e)
-//        }
-//        // todo
-////        httpClient.download(url) { result ->
-////            result.onSuccess { data ->
-////                FileOutputStream(externalFile).apply {
-////                    write(data)
-////                    flush()
-////                    close()
-////                    finalizeBlock()
-////                }
-////            }.onFailure { e ->
-////                Timber.e(e)
-////            }
-////        }
-//    }
-
-//    @AnyThread
-//    private suspend fun startUpdate() {
-//        val packageInstaller = PackageInstaller.getInstance(context)
-//        val apkUri = Uri.fromFile(externalFile)
-//        val fileName = externalFile.name
-//        try {
-//            when (val result = packageInstaller.createSession(apkUri) {
-//                confirmation = Confirmation.DEFERRED
-//                installerType = InstallerType.SESSION_BASED
-//                name = fileName
-//                requireUserAction = false
-//                notification {
-//                    title = NotificationString.resource(R.string.title_app_update)
-//                    contentText =
-//                        NotificationString.resource(R.string.text_update_message, fileName)
-//                    // todo icon =
-//                }
-//            }.await()) {
-//                is SessionResult.Success -> Timber.d("Success")
-//                is SessionResult.Error -> Timber.e(result.cause.message)
-//            }
-//        } catch (e: Exception) {
-//            Timber.e(e)
-//        }
-//    }
+    private fun downloadFile(url: String) {
+        Intent(Intent.ACTION_VIEW).apply {
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK
+            data = url.toUri()
+        }.also { intent ->
+            context.startActivity(intent)
+        }
+    }
 
     companion object {
 
@@ -199,14 +155,13 @@ class AppUpdater(
             }
         }
 
-        @Suppress("DEPRECATION")
         private val String.asTimestamp: Long
-            get() = try {
-                Date.parse(this)
-            } catch (e: Exception) {
-                Timber.e(e)
-                0L
-            }
+            get() = runCatching {
+                SimpleDateFormat(
+                    "yyyy-MM-dd'T'HH:mm:ss'Z'",
+                    Locale.US
+                ).parse(this)?.time ?: 0L
+            }.getOrDefault(0L)
     }
 
 }

+ 1 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/NetUtils.kt

@@ -12,8 +12,7 @@ object NetUtils {
 
     @Suppress("Deprecation")
     fun isConnected(context: Context): Boolean {
-        val cm = context.applicationContext
-            .getSystemService(Activity.CONNECTIVITY_SERVICE) as ConnectivityManager
+        val cm = context.getSystemService(Activity.CONNECTIVITY_SERVICE) as ConnectivityManager
         return cm.activeNetworkInfo.let { info ->
             info != null && info.isConnected
         }

+ 27 - 0
tvlib/src/main/res/values-v23/themes.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~  Copyright (c) Milan Jurkulák 2023.
+  ~  Contact:
+  ~  e: mimoccc@gmail.com
+  ~  e: mj@mjdev.org
+  ~  w: https://mjdev.org
+  -->
+<resources>
+    <style name="Theme.TvApp" parent="android:Theme.Material.NoActionBar.TranslucentDecor">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:navigationBarColor">@color/transparent</item>
+        <item name="android:statusBarColor">@color/transparent</item>
+        <!--<item name="android:enforceNavigationBarContrast">false</item>-->
+        <!--<item name="android:enforceStatusBarContrast">false</item>-->
+        <!--<item name="android:navigationBarDividerColor">-->
+        <!--    @color/transparent-->
+        <!--</item>-->
+        <item name="android:windowLightStatusBar">false</item>
+        <!--<item name="android:windowLightNavigationBar">false</item>-->
+        <item name="android:windowTranslucentStatus">true</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowTranslucentNavigation">true</item>
+        <!--<item name="android:windowLayoutInDisplayCutoutMode">always</item>-->
+        <item name="android:windowFullscreen">false</item>
+    </style>
+</resources>

+ 27 - 0
tvlib/src/main/res/values-v27/themes.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~  Copyright (c) Milan Jurkulák 2023.
+  ~  Contact:
+  ~  e: mimoccc@gmail.com
+  ~  e: mj@mjdev.org
+  ~  w: https://mjdev.org
+  -->
+<resources>
+    <style name="Theme.TvApp" parent="android:Theme.Material.NoActionBar.TranslucentDecor">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:navigationBarColor">@color/transparent</item>
+        <item name="android:statusBarColor">@color/transparent</item>
+        <!--<item name="android:enforceNavigationBarContrast">false</item>-->
+        <!--<item name="android:enforceStatusBarContrast">false</item>-->
+        <item name="android:navigationBarDividerColor">
+            @color/transparent
+        </item>
+        <item name="android:windowLightStatusBar">false</item>
+        <item name="android:windowLightNavigationBar">false</item>
+        <item name="android:windowTranslucentStatus">true</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowTranslucentNavigation">true</item>
+<!--        <item name="android:windowLayoutInDisplayCutoutMode">always</item>-->
+        <item name="android:windowFullscreen">false</item>
+    </style>
+</resources>

+ 27 - 0
tvlib/src/main/res/values-v29/themes.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~  Copyright (c) Milan Jurkulák 2023.
+  ~  Contact:
+  ~  e: mimoccc@gmail.com
+  ~  e: mj@mjdev.org
+  ~  w: https://mjdev.org
+  -->
+<resources>
+    <style name="Theme.TvApp" parent="android:Theme.Material.NoActionBar.TranslucentDecor">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:navigationBarColor">@color/transparent</item>
+        <item name="android:statusBarColor">@color/transparent</item>
+        <item name="android:enforceNavigationBarContrast">false</item>
+        <item name="android:enforceStatusBarContrast">false</item>
+        <item name="android:navigationBarDividerColor">
+            @color/transparent
+        </item>
+        <item name="android:windowLightStatusBar">false</item>
+        <item name="android:windowLightNavigationBar">false</item>
+        <item name="android:windowTranslucentStatus">true</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowTranslucentNavigation">true</item>
+<!--        <item name="android:windowLayoutInDisplayCutoutMode">always</item>-->
+        <item name="android:windowFullscreen">false</item>
+    </style>
+</resources>

+ 10 - 11
tvlib/src/main/res/values/colors.xml

@@ -1,15 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
-/*
- * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
- */
--->
-
+  ~  Copyright (c) Milan Jurkulák 2023.
+  ~  Contact:
+  ~  e: mimoccc@gmail.com
+  ~  e: mj@mjdev.org
+  ~  w: https://mjdev.org
+  -->
 <resources>
-
     <color name="black">#ff000000</color>
-
+    <color name="white">#ffffffff</color>
+    <color name="dark_gray">#ff444444</color>
+    <color name="transparent">#00000000</color>
 </resources>

+ 3 - 3
tvlib/src/main/res/values/strings.xml

@@ -1,11 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (c) Milan Jurkulák 2023.
+  ~  Copyright (c) Milan Jurkulák 2023.
   ~  Contact:
   ~  e: mimoccc@gmail.com
   ~  e: mj@mjdev.org
   ~  w: https://mjdev.org
   -->
-
 <resources>
 
     <string name="app_name">Tv App</string>
@@ -22,7 +22,7 @@
     <string name="menu_title_home">Home</string>
 
     <string name="msg_loading">Loading…</string>
-    <string name="msg_update_available">New app version is available.</string>
+    <string name="msg_update_available">New app version is available, click button to download.</string>
     <string name="msg_no_items">No items …</string>
 
     <string name="text_app_description">\n\n\nSimple TV Application for AndroidTV or SetTopBox with Android or any tablet or mobile device.\nAll in one solution for mobile app responsive to device.\nThis app is just an simple demonstration of Jetpack Compose and all other modern architecture components.\n\n\n</string>

+ 21 - 17
tvlib/src/main/res/values/themes.xml

@@ -1,23 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
-/*
- * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
- */
--->
-
+  ~  Copyright (c) Milan Jurkulák 2023.
+  ~  Contact:
+  ~  e: mimoccc@gmail.com
+  ~  e: mj@mjdev.org
+  ~  w: https://mjdev.org
+  -->
 <resources>
-
-    <style name="Theme.TvApp" parent="android:Theme.Material.Light.NoActionBar.TranslucentDecor">
-
+    <style name="Theme.TvApp" parent="android:Theme.Material.NoActionBar.TranslucentDecor">
         <item name="android:windowNoTitle">true</item>
-        <item name="android:windowBackground">@color/black</item>
-        <item name="android:navigationBarColor">@color/black</item>
-        <item name="android:colorBackground">@color/black</item>
+        <item name="android:navigationBarColor">@color/transparent</item>
+        <item name="android:statusBarColor">@color/transparent</item>
+        <!--<item name="android:enforceNavigationBarContrast">false</item>-->
+        <!--<item name="android:enforceStatusBarContrast">false</item>-->
+        <!--<item name="android:navigationBarDividerColor">-->
+        <!--    @color/transparent-->
+        <!--</item>-->
+        <!--<item name="android:windowLightStatusBar">false</item>-->
+        <!--<item name="android:windowLightNavigationBar">false</item>-->
+        <item name="android:windowTranslucentStatus">true</item>
+        <item name="android:windowIsTranslucent">true</item>
         <item name="android:windowTranslucentNavigation">true</item>
-
+        <!--<item name="android:windowLayoutInDisplayCutoutMode">always</item>-->
+        <item name="android:windowFullscreen">false</item>
     </style>
-
 </resources>