Selaa lähdekoodia

web scrapper pre-implement

web scrapper pre-implement
Milan Jurkulak 2 vuotta sitten
vanhempi
sitoutus
95baf66bd6
87 muutettua tiedostoa jossa 1462 lisäystä ja 741 poistoa
  1. 38 42
      app/build.gradle.kts
  2. 4 4
      app/src/main/kotlin/org/mjdev/tvapp/activity/IPTVActivity.kt
  3. 10 0
      app/src/main/kotlin/org/mjdev/tvapp/data/local/Category.kt
  4. 11 0
      app/src/main/kotlin/org/mjdev/tvapp/data/local/Country.kt
  5. 11 0
      app/src/main/kotlin/org/mjdev/tvapp/data/local/Message.kt
  6. 5 6
      app/src/main/kotlin/org/mjdev/tvapp/data/local/Movie.kt
  7. 37 4
      app/src/main/kotlin/org/mjdev/tvapp/data/local/TVChannel.kt
  8. 20 0
      app/src/main/kotlin/org/mjdev/tvapp/data/local/User.kt
  9. 9 0
      app/src/main/kotlin/org/mjdev/tvapp/data/remote/Category.kt
  10. 23 0
      app/src/main/kotlin/org/mjdev/tvapp/data/remote/Channel.kt
  11. 11 0
      app/src/main/kotlin/org/mjdev/tvapp/data/remote/Country.kt
  12. 12 0
      app/src/main/kotlin/org/mjdev/tvapp/data/remote/Epg.kt
  13. 9 0
      app/src/main/kotlin/org/mjdev/tvapp/data/remote/Language.kt
  14. 10 0
      app/src/main/kotlin/org/mjdev/tvapp/data/remote/Stream.kt
  15. 5 8
      app/src/main/kotlin/org/mjdev/tvapp/module/ProvideModule.kt
  16. 70 41
      app/src/main/kotlin/org/mjdev/tvapp/sync/SyncAdapter.kt
  17. 1 0
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt
  18. 1 3
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/PluginsPage.kt
  19. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/GalleryScreen.kt
  20. 0 6
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/LoadingScreen.kt
  21. 4 10
      app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt
  22. 2 2
      build.gradle.kts
  23. 2 1
      gradle.properties
  24. 1 1
      gradle/wrapper/gradle-wrapper.properties
  25. 48 50
      tvlib/build.gradle.kts
  26. 0 8
      tvlib/src/main/kotlin/org/mjdev/tvlib/activity/ComposableActivity.kt
  27. 9 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/adblock/AdBlock.kt
  28. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/adblock/HttpCacheInterceptor.kt
  29. 21 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/data/github/remote/Asset.kt
  30. 24 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/data/github/remote/Release.kt
  31. 26 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/data/github/remote/User.kt
  32. 4 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ComposeExt.kt
  33. 8 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ContextExt.kt
  34. 20 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt
  35. 11 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/apps/AppsManager.kt
  36. 9 8
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/AudioItem.kt
  37. 3 14
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/CachingCursor.kt
  38. 9 8
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/PhotoItem.kt
  39. 8 8
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/VideoItem.kt
  40. 2 5
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/media/ItemType.kt
  41. 49 49
      tvlib/src/main/kotlin/org/mjdev/tvlib/payments/Settings.kt
  42. 8 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/screen/Screen.kt
  43. 24 20
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/Card.kt
  44. 2 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/ItemCard.kt
  45. 11 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/PhotoCard.kt
  46. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/CarouselCard.kt
  47. 61 65
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/complex/FocusableCard.kt
  48. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/Gallery.kt
  49. 8 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gridview/TvGridView.kt
  50. 53 51
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/PhotoImage.kt
  51. 4 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/ExoPlayerImpl.kt
  52. 5 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/IMediaPlayer.kt
  53. 3 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerContainer.kt
  54. 4 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/NativePlayerImpl.kt
  55. 2 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/PlayerControlView.kt
  56. 7 10
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavDrawerContent.kt
  57. 4 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/SettingsDrawer.kt
  58. 4 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/AppUpdateAvailableMessage.kt
  59. 12 5
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/BrowseView.kt
  60. 4 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/CategoryRow.kt
  61. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/CursorRow.kt
  62. 55 55
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/Loading.kt
  63. 80 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/NeonCircle.kt
  64. 118 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/NeonRectangle.kt
  65. 120 17
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/web/WebView.kt
  66. 4 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/theme/Theme.kt
  67. 3 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/theme/ThemeHelper.kt
  68. 39 43
      tvlib/src/main/kotlin/org/mjdev/tvlib/updater/AppUpdater.kt
  69. 10 22
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/CustomWebChromeClient.kt
  70. 4 19
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/CustomWebViewClient.kt
  71. 26 33
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/WebClient.kt
  72. 25 19
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/WebScrapper.kt
  73. 5 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/AssetsLoader.kt
  74. 0 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/CacheExtensionConfig.kt
  75. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/NetUtils.kt
  76. 2 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/OKHttpFile.kt
  77. 3 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/TimeUtils.kt
  78. 6 5
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/WebViewCacheInterceptor.kt
  79. 3 8
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlAnchor.kt
  80. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlElement.kt
  81. 3 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlMeta.kt
  82. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlPage.kt
  83. 4 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlScript.kt
  84. 29 22
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlVideo.kt
  85. 17 15
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/javascript/JSI.kt
  86. 5 5
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/javascript/JSIListener.kt
  87. 130 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/WebVideoScrapper.kt

+ 38 - 42
app/build.gradle.kts

@@ -160,7 +160,11 @@ android {
     }
 
     packaging {
-        resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}")
+        resources.excludes.apply {
+            add("/META-INF/{AL2.0,LGPL2.1}")
+            add("/META-INF/DEPENDENCIES")
+            add("/mozilla/public-suffix-list.txt")
+        }
     }
 
     lint {
@@ -183,7 +187,7 @@ android {
 
 dependencies {
     // todo remove
-    implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.20-RC")
+    implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.21")
     // startups
     implementation("androidx.startup:startup-runtime:1.1.1")
     // tv library by mjde milan jurkulak
@@ -192,20 +196,20 @@ dependencies {
     implementation("androidx.core:core-ktx:1.12.0")
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
     // compose base libs
-    implementation(platform("androidx.compose:compose-bom:2023.10.00"))
+    implementation(platform("androidx.compose:compose-bom:2023.10.01"))
     implementation("androidx.compose.ui:ui-tooling")
-    implementation("androidx.activity:activity-compose:1.8.0")
+    implementation("androidx.activity:activity-compose:1.8.1")
     // more icons
-    implementation("androidx.compose.material:material-icons-extended:1.5.3")
+    implementation("androidx.compose.material:material-icons-extended:1.5.4")
     // tv compose
     implementation("androidx.tv:tv-foundation:1.0.0-alpha10")
     implementation("androidx.tv:tv-material:1.0.0-alpha10")
     // dagger - hilt
-    implementation("com.google.dagger:dagger-android:2.48.1")
-    implementation("com.google.dagger:dagger-android-support:2.48.1")
-    implementation("com.google.dagger:dagger:2.48.1")
-    implementation("com.google.dagger:hilt-android:2.48.1")
-    implementation("androidx.hilt:hilt-navigation-compose:1.1.0-beta01")
+    implementation("com.google.dagger:dagger-android:2.49")
+    implementation("com.google.dagger:dagger-android-support:2.49")
+    implementation("com.google.dagger:dagger:2.49")
+    implementation("com.google.dagger:hilt-android:2.49")
+    implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
     // moshi json
     implementation("com.squareup.moshi:moshi:1.15.0")
     implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
@@ -217,14 +221,14 @@ dependencies {
     implementation("com.jakewharton.timber:timber:5.0.1")
     implementation("com.android.volley:volley:1.2.1")
     // kapt
-    kapt("com.google.dagger:dagger-compiler:2.48.1")
-    kapt("com.google.dagger:dagger-android-processor:2.48.1")
-    kapt("com.google.dagger:hilt-compiler:2.48.1")
+    kapt("com.google.dagger:dagger-compiler:2.49")
+    kapt("com.google.dagger:dagger-android-processor:2.49")
+    kapt("com.google.dagger:hilt-compiler:2.49")
     ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")
     // view model
     implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
     // navigation
-    implementation("androidx.navigation:navigation-compose:2.7.4")
+    implementation("androidx.navigation:navigation-compose:2.7.5")
     // retrofit
     implementation("com.squareup.retrofit2:retrofit:2.9.0")
     implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
@@ -238,7 +242,7 @@ dependencies {
     // previews
     debugImplementation("androidx.customview:customview-poolingcontainer:1.0.0")
     // lottie
-    implementation("com.airbnb.android:lottie-compose:6.1.0")
+    implementation("com.airbnb.android:lottie-compose:6.2.0")
     // oauth
     implementation("com.auth0.android:auth0:2.10.2")
     implementation("com.auth0.android:jwtdecode:2.0.2")
@@ -247,34 +251,26 @@ dependencies {
     implementation("androidx.glance:glance-material:1.0.0")
     implementation("androidx.glance:glance-material3:1.0.0")
     // fix duplicates
-    implementation("androidx.work:work-runtime-ktx:2.8.1")
+    implementation("androidx.work:work-runtime-ktx:2.9.0")
     // exoplayer
-    implementation("androidx.media3:media3-exoplayer:1.1.1")
-    implementation("androidx.media3:media3-exoplayer-dash:1.1.1")
-    implementation("androidx.media3:media3-exoplayer-hls:1.1.1")
-    implementation("androidx.media3:media3-exoplayer-rtsp:1.1.1")
-//    implementation("androidx.media3:media3-exoplayer-ima:1.1.0")
-    implementation("androidx.media3:media3-datasource-cronet:1.1.1")
-    implementation("androidx.media3:media3-datasource-okhttp:1.1.1")
-    implementation("androidx.media3:media3-datasource-rtmp:1.1.1")
-    implementation("androidx.media3:media3-ui:1.1.1")
-//    implementation("androidx.media3:media3-ui-leanback:1.1.0")
-    implementation("androidx.media3:media3-session:1.1.1")
-    implementation("androidx.media3:media3-extractor:1.1.1")
-    implementation("androidx.media3:media3-cast:1.1.1")
-    implementation("androidx.media3:media3-exoplayer-workmanager:1.1.1")
-    implementation("androidx.media3:media3-transformer:1.1.1")
-    implementation("androidx.media3:media3-database:1.1.1")
-    implementation("androidx.media3:media3-decoder:1.1.1")
-    implementation("androidx.media3:media3-datasource:1.1.1")
-    implementation("androidx.media3:media3-common:1.1.1")
-    // images
-    implementation("io.coil-kt:coil-base:2.4.0")
-    implementation("io.coil-kt:coil-gif:2.4.0")
-    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.10")
+    implementation("androidx.media3:media3-exoplayer:1.2.0")
+    implementation("androidx.media3:media3-exoplayer-dash:1.2.0")
+    implementation("androidx.media3:media3-exoplayer-hls:1.2.0")
+    implementation("androidx.media3:media3-exoplayer-rtsp:1.2.0")
+    implementation("androidx.media3:media3-datasource-cronet:1.2.0")
+    implementation("androidx.media3:media3-datasource-okhttp:1.2.0")
+    implementation("androidx.media3:media3-datasource-rtmp:1.2.0")
+    implementation("androidx.media3:media3-ui:1.2.0")
+    implementation("androidx.media3:media3-session:1.2.0")
+    implementation("androidx.media3:media3-extractor:1.2.0")
+    implementation("androidx.media3:media3-cast:1.2.0")
+    implementation("androidx.media3:media3-exoplayer-workmanager:1.2.0")
+    implementation("androidx.media3:media3-transformer:1.2.0")
+    implementation("androidx.media3:media3-database:1.2.0")
+    implementation("androidx.media3:media3-decoder:1.2.0")
+    implementation("androidx.media3:media3-datasource:1.2.0")
+    implementation("androidx.media3:media3-common:1.2.0")
+//    implementation("net.engawapg.lib:zoomable:1.5.2")
     // libs mismatch
     constraints {
         implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.0").apply {

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

@@ -12,7 +12,7 @@ import android.content.Intent
 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.tvapp.ui.screens.GalleryScreen
 import org.mjdev.tvlib.extensions.NavGraphBuilderExt.screen
 import org.mjdev.tvlib.navigation.NavGraphBuilderEx
 import org.mjdev.tvapp.ui.screens.IPTVScreen
@@ -40,7 +40,7 @@ class IPTVActivity : TvActivity() {
             isStartScreen = true,
         )
         screen(route = IPTVScreen())
-        screen(route = DetailScreen())
+        screen(route = GalleryScreen())
 
     }
 
@@ -52,8 +52,8 @@ class IPTVActivity : TvActivity() {
             is Movie -> navController.openAsTop<IPTVScreen>(data)
             is ItemAudio -> navController.openAsTop<IPTVScreen>(data)
             is ItemVideo -> navController.openAsTop<IPTVScreen>(data)
-            is ItemPhoto -> navController.openAsTop<DetailScreen>(data)
-            else -> navController.openAsTop<DetailScreen>(data)
+            is ItemPhoto -> navController.openAsTop<GalleryScreen>(data)
+            else -> navController.openAsTop<GalleryScreen>(data)
         }
     }
 

+ 10 - 0
app/src/main/kotlin/org/mjdev/tvapp/data/local/Category.kt

@@ -47,4 +47,14 @@ class Category() :
 
     override fun toString(): String = "Category[$title]"
 
+    override fun equals(other: Any?): Boolean =
+        if (other !is Category) false else (other.hashCode() == hashCode())
+
+    override fun hashCode(): Int {
+        var result = id.hashCode()
+        result = 31 * result + (title?.hashCode() ?: 0)
+        result = 31 * result + (subtitle?.hashCode() ?: 0)
+        result = 31 * result + (image?.hashCode() ?: 0)
+        return result
+    }
 }

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

@@ -34,4 +34,15 @@ class Country {
     @Json(name = "flag")
     var flag: String? = null
 
+    override fun equals(other: Any?): Boolean =
+        if (other !is Country) false else (other.hashCode() == hashCode())
+
+    override fun hashCode(): Int {
+        var result = id.hashCode()
+        result = 31 * result + (code?.hashCode() ?: 0)
+        result = 31 * result + (name?.hashCode() ?: 0)
+        result = 31 * result + (languages?.hashCode() ?: 0)
+        result = 31 * result + (flag?.hashCode() ?: 0)
+        return result
+    }
 }

+ 11 - 0
app/src/main/kotlin/org/mjdev/tvapp/data/local/Message.kt

@@ -36,4 +36,15 @@ class Message : ItemWithTitle<String>, ItemWithMessage<String>, ItemWithId {
     @Json(name = "deleted")
     val deleted: Boolean = false
 
+    override fun equals(other: Any?): Boolean =
+        if (other !is Message) false else (other.hashCode() == hashCode())
+
+    override fun hashCode(): Int {
+        var result = id.hashCode()
+        result = 31 * result + (title?.hashCode() ?: 0)
+        result = 31 * result + (message?.hashCode() ?: 0)
+        result = 31 * result + read.hashCode()
+        result = 31 * result + deleted.hashCode()
+        return result
+    }
 }

+ 5 - 6
app/src/main/kotlin/org/mjdev/tvapp/data/local/Movie.kt

@@ -74,14 +74,11 @@ class Movie :
     override var image: String? = null
         get() = field ?: uri
 
-    override fun equals(other: Any?): Boolean {
-        if (other == null || other !is Movie) return false
-        return uri == other.uri
-    }
+    override fun equals(other: Any?): Boolean =
+        if (other !is Movie) false else other.hashCode() == hashCode()
 
     override fun hashCode(): Int {
         var result = id.hashCode()
-        result = 31 * result + (category?.hashCode() ?: 0)
         result = 31 * result + (studio?.hashCode() ?: 0)
         result = 31 * result + (country?.hashCode() ?: 0)
         result = 31 * result + isNsfw.hashCode()
@@ -89,7 +86,9 @@ class Movie :
         result = 31 * result + (description?.hashCode() ?: 0)
         result = 31 * result + (uri?.hashCode() ?: 0)
         result = 31 * result + (subtitle?.hashCode() ?: 0)
+        result = 31 * result + (category?.hashCode() ?: 0)
+        result = 31 * result + (background?.hashCode() ?: 0)
+        result = 31 * result + (image?.hashCode() ?: 0)
         return result
     }
-
 }

+ 37 - 4
app/src/main/kotlin/org/mjdev/tvapp/data/local/TVChannel.kt

@@ -36,12 +36,13 @@ class TVChannel() :
     ItemWithDescription<String>,
     ItemVideo {
 
-    constructor(stream: Stream, channel: Channel?) : this() {
-
+    constructor(
+        stream: Stream,
+        channel: Channel?
+    ) : this() {
         this.streamUrl = stream.url
         this.httpReferrer = stream.httpReferrer ?: ""
         this.userAgent = stream.userAgent ?: ""
-
         this.channelId = channel?.id ?: ""
         this.name = channel?.name ?: ""
         this.altNames = channel?.altNames ?: emptyList()
@@ -59,7 +60,6 @@ class TVChannel() :
         this.replacedBy = channel?.replacedBy ?: ""
         this.subdivision = channel?.subdivision ?: ""
         this.website = channel?.website ?: ""
-
     }
 
     @Id
@@ -138,6 +138,39 @@ class TVChannel() :
 
     override fun toString(): String = "Channel[$title : $uri]"
 
+    override fun equals(other: Any?): Boolean =
+        if (other !is TVChannel) false else other.hashCode() == hashCode()
+
+    override fun hashCode(): Int {
+        var result = id.hashCode()
+        result = 31 * result + (channelId?.hashCode() ?: 0)
+        result = 31 * result + (name?.hashCode() ?: 0)
+        result = 31 * result + (altNames?.hashCode() ?: 0)
+        result = 31 * result + (network?.hashCode() ?: 0)
+        result = 31 * result + (owners?.hashCode() ?: 0)
+        result = 31 * result + (country?.hashCode() ?: 0)
+        result = 31 * result + (subdivision?.hashCode() ?: 0)
+        result = 31 * result + (city?.hashCode() ?: 0)
+        result = 31 * result + (broadcastArea?.hashCode() ?: 0)
+        result = 31 * result + (languages?.hashCode() ?: 0)
+        result = 31 * result + (categories?.hashCode() ?: 0)
+        result = 31 * result + (isNsfw?.hashCode() ?: 0)
+        result = 31 * result + (launched?.hashCode() ?: 0)
+        result = 31 * result + (closed?.hashCode() ?: 0)
+        result = 31 * result + (replacedBy?.hashCode() ?: 0)
+        result = 31 * result + (website?.hashCode() ?: 0)
+        result = 31 * result + (logo?.hashCode() ?: 0)
+        result = 31 * result + (streamUrl?.hashCode() ?: 0)
+        result = 31 * result + (httpReferrer?.hashCode() ?: 0)
+        result = 31 * result + (userAgent?.hashCode() ?: 0)
+        result = 31 * result + (description?.hashCode() ?: 0)
+        result = 31 * result + (image?.hashCode() ?: 0)
+        result = 31 * result + (subtitle?.hashCode() ?: 0)
+        result = 31 * result + (title?.hashCode() ?: 0)
+        result = 31 * result + (uri?.hashCode() ?: 0)
+        return result
+    }
+
     companion object {
 
         fun TVChannel.asMovie(category: String): Movie = Movie().also { m ->

+ 20 - 0
app/src/main/kotlin/org/mjdev/tvapp/data/local/User.kt

@@ -60,4 +60,24 @@ class User {
     @Json(name = "emailVerified", ignore = true)
     var emailVerified : Boolean = false
 
+    override fun equals(other: Any?): Boolean =
+        if (other !is User) false else other.hashCode() == hashCode()
+
+    override fun hashCode(): Int {
+        var result = id.hashCode()
+        result = 31 * result + (sub?.hashCode() ?: 0)
+        result = 31 * result + (givenName?.hashCode() ?: 0)
+        result = 31 * result + (familyName?.hashCode() ?: 0)
+        result = 31 * result + (nickname?.hashCode() ?: 0)
+        result = 31 * result + (fullName?.hashCode() ?: 0)
+        result = 31 * result + (pictureUrl?.hashCode() ?: 0)
+        result = 31 * result + (locale?.hashCode() ?: 0)
+        result = 31 * result + (lastUpdated?.hashCode() ?: 0)
+        result = 31 * result + pinCode.hashCode()
+        result = 31 * result + settingsJson.hashCode()
+        result = 31 * result + age
+        result = 31 * result + (email?.hashCode() ?: 0)
+        result = 31 * result + emailVerified.hashCode()
+        return result
+    }
 }

+ 9 - 0
app/src/main/kotlin/org/mjdev/tvapp/data/remote/Category.kt

@@ -10,6 +10,7 @@ package org.mjdev.tvapp.data.remote
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.mjdev.tvapp.data.local.User
 
 @Suppress("unused")
 @JsonClass(generateAdapter = true)
@@ -21,4 +22,12 @@ class Category {
     @Json(name = "name")
     var name: String? = null
 
+    override fun equals(other: Any?): Boolean =
+        if (other !is Category) false else other.hashCode() == hashCode()
+
+    override fun hashCode(): Int {
+        var result = code?.hashCode() ?: 0
+        result = 31 * result + (name?.hashCode() ?: 0)
+        return result
+    }
 }

+ 23 - 0
app/src/main/kotlin/org/mjdev/tvapp/data/remote/Channel.kt

@@ -66,4 +66,27 @@ class Channel {
     @Json(name = "logo")
     var logo: String? = null
 
+    override fun equals(other: Any?): Boolean =
+        if (other !is Channel) false else other.hashCode() == hashCode()
+
+    override fun hashCode(): Int {
+        var result = id?.hashCode() ?: 0
+        result = 31 * result + (name?.hashCode() ?: 0)
+        result = 31 * result + (altNames?.hashCode() ?: 0)
+        result = 31 * result + (network?.hashCode() ?: 0)
+        result = 31 * result + (owners?.hashCode() ?: 0)
+        result = 31 * result + (country?.hashCode() ?: 0)
+        result = 31 * result + (subdivision?.hashCode() ?: 0)
+        result = 31 * result + (city?.hashCode() ?: 0)
+        result = 31 * result + (broadcastArea?.hashCode() ?: 0)
+        result = 31 * result + (languages?.hashCode() ?: 0)
+        result = 31 * result + (categories?.hashCode() ?: 0)
+        result = 31 * result + (isNsfw?.hashCode() ?: 0)
+        result = 31 * result + (launched?.hashCode() ?: 0)
+        result = 31 * result + (closed?.hashCode() ?: 0)
+        result = 31 * result + (replacedBy?.hashCode() ?: 0)
+        result = 31 * result + (website?.hashCode() ?: 0)
+        result = 31 * result + (logo?.hashCode() ?: 0)
+        return result
+    }
 }

+ 11 - 0
app/src/main/kotlin/org/mjdev/tvapp/data/remote/Country.kt

@@ -27,4 +27,15 @@ class Country {
     @Json(name = "flag")
     var flag: String? = null
 
+    override fun equals(other: Any?): Boolean =
+        if (other !is Country) false else other.hashCode() == hashCode()
+
+    override fun hashCode(): Int {
+        var result = code?.hashCode() ?: 0
+        result = 31 * result + (name?.hashCode() ?: 0)
+        result = 31 * result + (languages?.hashCode() ?: 0)
+        result = 31 * result + (flag?.hashCode() ?: 0)
+        return result
+    }
+
 }

+ 12 - 0
app/src/main/kotlin/org/mjdev/tvapp/data/remote/Epg.kt

@@ -33,4 +33,16 @@ class Epg {
     @Json(name = "url")
     val url: String? = null
 
+    override fun equals(other: Any?): Boolean =
+        if (other !is Epg) false else other.hashCode() == hashCode()
+
+    override fun hashCode(): Int {
+        var result = id.hashCode()
+        result = 31 * result + (channel?.hashCode() ?: 0)
+        result = 31 * result + (site?.hashCode() ?: 0)
+        result = 31 * result + (lang?.hashCode() ?: 0)
+        result = 31 * result + (days ?: 0)
+        result = 31 * result + (url?.hashCode() ?: 0)
+        return result
+    }
 }

+ 9 - 0
app/src/main/kotlin/org/mjdev/tvapp/data/remote/Language.kt

@@ -21,4 +21,13 @@ class Language {
     @Json(name = "name")
     var name : String? = null
 
+    override fun equals(other: Any?): Boolean =
+        if (other !is Language) false else other.hashCode() == hashCode()
+
+    override fun hashCode(): Int {
+        var result = code?.hashCode() ?: 0
+        result = 31 * result + (name?.hashCode() ?: 0)
+        return result
+    }
+
 }

+ 10 - 0
app/src/main/kotlin/org/mjdev/tvapp/data/remote/Stream.kt

@@ -27,4 +27,14 @@ class Stream {
     @Json(name = "user_agent")
     var userAgent: String? = null
 
+    override fun equals(other: Any?): Boolean =
+        if (other !is Stream) false else other.hashCode() == hashCode()
+
+    override fun hashCode(): Int {
+        var result = channelId?.hashCode() ?: 0
+        result = 31 * result + (url?.hashCode() ?: 0)
+        result = 31 * result + (httpReferrer?.hashCode() ?: 0)
+        result = 31 * result + (userAgent?.hashCode() ?: 0)
+        return result
+    }
 }

+ 5 - 8
app/src/main/kotlin/org/mjdev/tvapp/module/ProvideModule.kt

@@ -19,9 +19,6 @@ 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
@@ -122,10 +119,10 @@ class ProvideModule {
         retrofit: Retrofit
     ): ApiService = retrofit.create(ApiService::class.java)
 
-    @Provides
-    @Singleton
-    fun provideAppScope(): CoroutineScope {
-        return CoroutineScope(SupervisorJob() + Dispatchers.Main)
-    }
+//    @Provides
+//    @Singleton
+//    fun provideAppScope(): CoroutineScope {
+//        return CoroutineScope(SupervisorJob() + Dispatchers.Main)
+//    }
 
 }

+ 70 - 41
app/src/main/kotlin/org/mjdev/tvapp/sync/SyncAdapter.kt

@@ -7,31 +7,30 @@ import android.content.Context
 import android.content.SyncResult
 import android.os.Bundle
 import io.objectbox.Box
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
 import org.mjdev.tvapp.data.local.Movie
 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.IO
+import org.mjdev.tvlib.extensions.GlobalExt.launch
 import org.mjdev.tvlib.extensions.GlobalExt.safeGet
 import org.mjdev.tvlib.interfaces.ItemWithUri.Companion.hasUri
 import timber.log.Timber
 
-@Suppress("unused", "PrivatePropertyName")
+@Suppress("unused")
 class SyncAdapter(
     context: Context,
     val apiService: ApiService,
     val dao: DAO
 ) : AbstractThreadedSyncAdapter(context, true, false) {
 
-    private val READ_COUNT = 8
+    private val movieDao: Box<Movie> get() = dao.movieDao
 
-    private val coroutineScope by lazy {
-        CoroutineScope(Dispatchers.IO)
-    }
+    private val urls = listOf(
+        "https://prehraj.to/hledej/filmy",
+//        "https://meatyhunks.com",
+    )
 
-    private val movieDao: Box<Movie> get() = dao.movieDao
 
     override fun onPerformSync(
         account: Account?,
@@ -39,36 +38,34 @@ class SyncAdapter(
         authority: String?,
         provider: ContentProviderClient?,
         syncResult: SyncResult?
-    ) {
+    ) = launch(IO) {
         try {
-            coroutineScope.launch {
-
-                val streams = apiService.streams().safeGet()
-                val channels = apiService.channels().safeGet()
-
-                val oldMovies = movieDao.all
-                val newMovies = mutableListOf<Movie>()
-
-                streams.forEach { stream ->
-                    channels.firstOrNull { channel ->
-                        channel.id == stream.channelId
-                    }.also { channel ->
-                        (channel?.categories ?: listOf(null)).forEach { ctg ->
-                            newMovies.add(Movie().apply {
-                                title = channel?.name ?: stream.channelId
-                                uri = stream.url
-                                category = ctg
-                                image = channel?.logo
-                                country = channel?.country
-                                isNsfw = channel?.isNsfw ?: false
-                            })
-                        }
+
+            val streams = apiService.streams().safeGet()
+            val channels = apiService.channels().safeGet()
+
+            val oldMovies = movieDao.all
+            val newMovies = mutableListOf<Movie>()
+
+            streams.forEach { stream ->
+                channels.firstOrNull { channel ->
+                    channel.id == stream.channelId
+                }.also { channel ->
+                    (channel?.categories ?: listOf(null)).forEach { ctg ->
+                        newMovies.add(Movie().apply {
+                            title = channel?.name ?: stream.channelId
+                            uri = stream.url
+                            category = ctg
+                            image = channel?.logo
+                            country = channel?.country
+                            isNsfw = channel?.isNsfw ?: false
+                        })
                     }
                 }
+            }
 
-                val syncItems = SyncItems(oldMovies, newMovies)
-
-                syncItems.toAdd.forEach { movie ->
+            with(SyncItems(oldMovies, newMovies) ) {
+                toAdd.forEach { movie ->
                     try {
                         dao.movieDao.tx {
                             put(movie)
@@ -78,8 +75,7 @@ class SyncAdapter(
                         Timber.e(e)
                     }
                 }
-
-                syncItems.toUpdate.forEach { movie ->
+                toUpdate.forEach { movie ->
                     try {
                         dao.movieDao.tx {
                             put(movie)
@@ -89,8 +85,7 @@ class SyncAdapter(
                         Timber.e(e)
                     }
                 }
-
-                syncItems.toRemove.forEach { movie ->
+                toRemove.forEach { movie ->
                     try {
                         dao.movieDao.tx {
                             remove(movie)
@@ -100,8 +95,42 @@ class SyncAdapter(
                         Timber.e(e)
                     }
                 }
-
             }
+
+            // todo non ui thread
+//            launch(UI) {
+//                WebScrapper(context, urls) { page, videoURL ->
+//                    try {
+//                        val auth = page?.url?.toUri()?.authority ?: ""
+//                        val movie = Movie().apply {
+//                            this.category = auth
+//                            this.title = page?.title?.replace(auth, "")
+//                            this.image = videoURL
+//                            this.uri = videoURL
+//                        }
+//                        dao.movieDao.put(movie)
+//                        Timber.d("Movie: $movie stored.")
+//                    } catch (e: Exception) {
+//                        Timber.e(e)
+//                    }
+//                }.start()
+//            }
+
+//            WebVideoScrapper(
+//                baseUrls = urls,
+//                onVideoFound = { video ->
+//                    dao.movieDao.put(Movie().apply {
+//                        category = video.category
+//                        title = video.title
+//                        image = video.thumb
+//                        uri = video.url
+//                    })
+//                },
+//                onFinish = { videos ->
+//
+//                }
+//            ).start()
+
         } catch (e: Exception) {
             Timber.e(e)
         }
@@ -135,7 +164,7 @@ class SyncAdapter(
             }
 
             oldMovies.filter { oldMovie ->
-                 (newMovies.count { newMovie ->
+                (newMovies.count { newMovie ->
                     newMovie.uri.contentEquals(oldMovie.uri)
                 } == 0)
             }.also { addMovies ->

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

@@ -103,6 +103,7 @@ class MainPage : Page() {
             modifier = Modifier.fillMaxSize(),
         ) {
             BrowseView(
+                isDebug = BuildConfig.DEBUG,
                 modifier = Modifier.fillMaxSize(),
                 appIcon = R.mipmap.ic_launcher_foreground,
                 userIcon = user.value?.pictureUrl ?: org.mjdev.tvlib.R.drawable.person,

+ 1 - 3
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/PluginsPage.kt

@@ -32,7 +32,6 @@ import org.mjdev.tvlib.ui.components.text.TextAny
 import org.mjdev.tvlib.ui.components.card.Card
 
 class PluginsPage : Page() {
-
     override val title: Int = R.string.title_plugins
     override val icon: ImageVector = Icons.Default.SettingsInputComponent
 
@@ -53,7 +52,7 @@ class PluginsPage : Page() {
                 aspectRatio = null,
                 colors = CardDefaults.colors(
                     containerColor = Color(0xff242424)
-                ),
+                )
             ) {
                 Row(
                     modifier = Modifier
@@ -84,5 +83,4 @@ class PluginsPage : Page() {
             }
         }
     }
-
 }

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

@@ -28,7 +28,7 @@ import org.mjdev.tvlib.navigation.AnyType
 import org.mjdev.tvlib.screen.Screen
 import org.mjdev.tvlib.ui.components.gallery.Gallery
 
-class DetailScreen : Screen() {
+class GalleryScreen : Screen() {
 
     private val data = "data"
 

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

@@ -43,12 +43,6 @@ class LoadingScreen : Screen() {
                 .background(Color.Black, RectangleShape),
             contentAlignment = Alignment.Center,
         ) {
-            TextAny(
-                text = stringResource(id = title),
-                color = Color.White,
-                fontSize = 32.sp,
-                fontWeight = FontWeight.Bold
-            )
         }
     }
 

+ 4 - 10
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt

@@ -70,15 +70,9 @@ class MainViewModel @Inject constructor(
     // todo : move to cursor
     val featuredMovieList = flow {
         mutableListOf<Any?>().apply {
-            addAll(localAudioCursor.takeIf(ITEMS_FROM_CATEGORY) {
-                hasImage
-            })
-            addAll(localVideoCursor.takeIf(ITEMS_FROM_CATEGORY) {
-                hasImage
-            })
-            addAll(localPhotoCursor.takeIf(ITEMS_FROM_CATEGORY) {
-                hasImage
-            })
+            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 ->
             emit(result)
@@ -91,7 +85,7 @@ class MainViewModel @Inject constructor(
         dao.movieDao.all.sortedBy { m ->
             m.category
         }.asMap { m ->
-            Pair(m.category, m)
+            Pair(m.category ?: "-", m)
         }.also { map ->
             emit(map)
         }

+ 2 - 2
build.gradle.kts

@@ -26,8 +26,8 @@ buildscript {
         classpath("com.android.tools:r8:8.1.56")
         classpath("io.objectbox:objectbox-gradle-plugin:3.6.0")
         classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
-        classpath("com.android.tools.build:gradle:8.1.2")
-        classpath("com.android.tools.build:gradle-api:8.1.2")
+        classpath("com.android.tools.build:gradle:8.2.0")
+        classpath("com.android.tools.build:gradle-api:8.1.3")
 //        classpath("com.newrelic.agent.android:agent-gradle-plugin:5.9.0")
         classpath ("app.cash.paparazzi:paparazzi-gradle-plugin:1.3.1")
     }

+ 2 - 1
gradle.properties

@@ -25,4 +25,5 @@ android.defaults.buildfeatures.buildconfig=true
 org.gradle.unsafe.configuration-cache=false
 android.suppressUnsupportedCompileSdk=34
 android.nonFinalResIds=true
-gradle.testLauncherAPI.enabled=false
+gradle.testLauncherAPI.enabled=false
+#org.gradle.warning.mode=all

+ 1 - 1
gradle/wrapper/gradle-wrapper.properties

@@ -1,6 +1,6 @@
 #Wed Sep 27 16:14:20 CEST 2023
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists

+ 48 - 50
tvlib/build.gradle.kts

@@ -98,31 +98,29 @@ android {
 }
 
 dependencies {
-    // todo remove
-    implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.20-RC")
     // 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")
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
-    // reflect / debug purposes only
-    implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.10")
+    // reflect / todo : remove
+    implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.21")
     // window manager
-    implementation("androidx.window:window:1.1.0")
+    implementation("androidx.window:window:1.2.0")
     // compose base libs
-    implementation(platform("androidx.compose:compose-bom:2023.10.00"))
+    implementation(platform("androidx.compose:compose-bom:2023.10.01"))
     implementation("androidx.compose.ui:ui-tooling")
-    implementation("androidx.activity:activity-compose:1.8.0")
+    implementation("androidx.activity:activity-compose:1.8.1")
     // more icons
-    implementation("androidx.compose.material:material-icons-extended:1.5.3")
+    implementation("androidx.compose.material:material-icons-extended:1.5.4")
     // tv compose
     implementation("androidx.tv:tv-foundation:1.0.0-alpha10")
     implementation("androidx.tv:tv-material:1.0.0-alpha10")
     // view model
     implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
     // navigation
-    implementation("androidx.navigation:navigation-compose:2.7.4")
+    implementation("androidx.navigation:navigation-compose:2.7.5")
     // okhttp
     implementation(platform("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11"))
     implementation("com.squareup.okhttp3:okhttp")
@@ -139,17 +137,12 @@ dependencies {
     // sandwich
     implementation("com.github.skydoves:sandwich:1.3.9")
     // image loading
-//    implementation("io.coil-kt:coil-base:2.4.0")
-//    implementation("io.coil-kt:coil-gif:2.4.0")
-//    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.10")
-    implementation("com.github.bumptech.glide:okhttp3-integration:4.15.1")
-    implementation("com.github.skydoves:landscapist-glide:2.2.10")
-    implementation("com.github.skydoves:landscapist-transformation:2.2.10")
-    implementation("com.github.skydoves:landscapist-palette:2.2.10")
-    implementation("com.github.skydoves:landscapist-placeholder:2.2.10")
+    implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
+    implementation("com.github.bumptech.glide:compose:1.0.0-beta01")
+    implementation("com.github.skydoves:landscapist-glide:2.2.12")
+    implementation("com.github.skydoves:landscapist-transformation:2.2.12")
+    implementation("com.github.skydoves:landscapist-palette:2.2.12")
+    implementation("com.github.skydoves:landscapist-placeholder:2.2.12")
     // zxing
     implementation("com.google.zxing:core:3.5.2")
     // svg
@@ -157,39 +150,39 @@ dependencies {
     // debug
     implementation("com.jakewharton.timber:timber:5.0.1")
     // dagger core
-    implementation("com.google.dagger:dagger:2.48.1")
+    implementation("com.google.dagger:dagger:2.49")
     implementation("androidx.compose.material3:material3:1.1.2")
-    kapt("com.google.dagger:dagger-compiler:2.48.1")
+    kapt("com.google.dagger:dagger-compiler:2.49")
     // dagger android
-    implementation("com.google.dagger:dagger-android:2.48.1")
-    implementation("com.google.dagger:dagger-android-support:2.48.1")
-    kapt("com.google.dagger:dagger-android-processor:2.48.1")
+    implementation("com.google.dagger:dagger-android:2.49")
+    implementation("com.google.dagger:dagger-android-support:2.49")
+    kapt("com.google.dagger:dagger-android-processor:2.49")
     // dagger - hilt
-    implementation("com.google.dagger:hilt-android:2.48.1")
-    implementation("androidx.hilt:hilt-navigation-compose:1.1.0-beta01")
-    kapt("com.google.dagger:hilt-compiler:2.48.1")
+    implementation("com.google.dagger:hilt-android:2.49")
+    implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
+    kapt("com.google.dagger:hilt-compiler:2.49")
     // encrypt data
     implementation("com.scottyab:aescrypt:0.0.1")
     // exoplayer
-    implementation("androidx.media3:media3-exoplayer:1.1.1")
-    implementation("androidx.media3:media3-exoplayer-dash:1.1.1")
-    implementation("androidx.media3:media3-exoplayer-hls:1.1.1")
-    implementation("androidx.media3:media3-exoplayer-rtsp:1.1.1")
+    implementation("androidx.media3:media3-exoplayer:1.2.0")
+    implementation("androidx.media3:media3-exoplayer-dash:1.2.0")
+    implementation("androidx.media3:media3-exoplayer-hls:1.2.0")
+    implementation("androidx.media3:media3-exoplayer-rtsp:1.2.0")
 //    implementation("androidx.media3:media3-exoplayer-ima:1.1.0")
-    implementation("androidx.media3:media3-datasource-cronet:1.1.1")
-    implementation("androidx.media3:media3-datasource-okhttp:1.1.1")
-    implementation("androidx.media3:media3-datasource-rtmp:1.1.1")
-    implementation("androidx.media3:media3-ui:1.1.1")
+    implementation("androidx.media3:media3-datasource-cronet:1.2.0")
+    implementation("androidx.media3:media3-datasource-okhttp:1.2.0")
+    implementation("androidx.media3:media3-datasource-rtmp:1.2.0")
+    implementation("androidx.media3:media3-ui:1.2.0")
 //    implementation("androidx.media3:media3-ui-leanback:1.1.0")
-    implementation("androidx.media3:media3-session:1.1.1")
-    implementation("androidx.media3:media3-extractor:1.1.1")
-    implementation("androidx.media3:media3-cast:1.1.1")
-    implementation("androidx.media3:media3-exoplayer-workmanager:1.1.1")
-    implementation("androidx.media3:media3-transformer:1.1.1")
-    implementation("androidx.media3:media3-database:1.1.1")
-    implementation("androidx.media3:media3-decoder:1.1.1")
-    implementation("androidx.media3:media3-datasource:1.1.1")
-    implementation("androidx.media3:media3-common:1.1.1")
+    implementation("androidx.media3:media3-session:1.2.0")
+    implementation("androidx.media3:media3-extractor:1.2.0")
+    implementation("androidx.media3:media3-cast:1.2.0")
+    implementation("androidx.media3:media3-exoplayer-workmanager:1.2.0")
+    implementation("androidx.media3:media3-transformer:1.2.0")
+    implementation("androidx.media3:media3-database:1.2.0")
+    implementation("androidx.media3:media3-decoder:1.2.0")
+    implementation("androidx.media3:media3-datasource:1.2.0")
+    implementation("androidx.media3:media3-common:1.2.0")
     // permission
     implementation("com.google.accompanist:accompanist-permissions:0.33.2-alpha")
     // for previews
@@ -197,21 +190,26 @@ dependencies {
     // pallette
     implementation("androidx.palette:palette-ktx:1.0.0")
     // lottie
-    implementation("com.airbnb.android:lottie-compose:6.1.0")
+    implementation("com.airbnb.android:lottie-compose:6.2.0")
     // exif info
     implementation("androidx.exifinterface:exifinterface:1.3.6")
     // anr
     implementation("com.github.anrwatchdog:anrwatchdog:1.4.0")
     // jsoup
-    implementation("org.jsoup:jsoup:1.16.1")
+    implementation("org.jsoup:jsoup:1.17.1")
     // dm
     implementation("com.dailymotion.dailymotion-sdk-android:sdk:0.2.12")
     // dynamic theme
     implementation("com.google.android.material:material:1.10.0")
     // stripe payments
-    implementation("com.stripe:stripe-android:20.32.1")
-    implementation("com.stripe:stripecardscan:20.32.1")
-    implementation("com.stripe:financial-connections:20.32.1")
+//    implementation("com.stripe:stripe-android:20.33.0")
+//    implementation("com.stripe:stripecardscan:20.33.0")
+//    implementation("com.stripe:financial-connections:20.33.0")
+    // scrape
+    implementation("it.skrape:skrapeit:1.2.2")
+    implementation("it.skrape:skrapeit-browser-fetcher:1.2.2")
+//    implementation("org.jsoup:jsoup:1.16.1")
+//    implementation("net.sourceforge.htmlunit:htmlunit-android:2.67.0")
     // yt
     // ...
     // constraints

+ 0 - 8
tvlib/src/main/kotlin/org/mjdev/tvlib/activity/ComposableActivity.kt

@@ -73,10 +73,6 @@ open class ComposableActivity : ComponentActivity() {
             }
     }
 
-//    val imageLoader: ImageLoader by lazy {
-//        createImageLoader(this)
-//    }
-
     override fun onCreate(savedInstanceState: Bundle?) {
         Timber.d("Activity ${this::class.simpleName} created.")
         super.onCreate(savedInstanceState)
@@ -178,8 +174,4 @@ open class ComposableActivity : ComponentActivity() {
             }
         }
     }
-
-//    open fun createImageLoader(): ImageLoader? =
-//        (application as? TvApplication)?.createImageLoader()
-
 }

+ 9 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/adblock/AdBlock.kt → tvlib/src/main/kotlin/org/mjdev/tvlib/adblock/AdBlock.kt

@@ -1,6 +1,14 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
 @file:Suppress("unused")
 
-package org.mjdev.tvlib.webclient.adblock
+package org.mjdev.tvlib.adblock
 
 import android.net.Uri
 import org.mjdev.tvlib.webclient.cache.MimeTypeMapUtils
@@ -72,7 +80,6 @@ class AdBlock(inputStream: InputStream) {
             fileWithData: File = File("file:///android_asset/adblock.txt")
         ) = AdBlock(fileWithData.inputStream())
 
-
         val instance by lazy { adBlock() }
 
     }

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/adblock/HttpCacheInterceptor.kt → tvlib/src/main/kotlin/org/mjdev/tvlib/adblock/HttpCacheInterceptor.kt

@@ -6,7 +6,7 @@
  *  w: https://mjdev.org
  */
 
-package org.mjdev.tvlib.webclient.adblock
+package org.mjdev.tvlib.adblock
 
 import android.text.TextUtils
 import okhttp3.Interceptor

+ 21 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/data/github/remote/Asset.kt

@@ -55,9 +55,29 @@ data class Asset(
 
     @Json(name = "browser_download_url")
     val downloadUrl: String = ""
-
 ) {
 
+    override fun equals(other: Any?): Boolean =
+        if (other !is Asset) false else other.hashCode() == hashCode()
+
+    override fun hashCode(): Int {
+        var result = id.hashCode()
+        result = 31 * result + nodeId.hashCode()
+        result = 31 * result + name.hashCode()
+        result = 31 * result + url.hashCode()
+        result = 31 * result + label.hashCode()
+        result = 31 * result + uploader.hashCode()
+        result = 31 * result + author.hashCode()
+        result = 31 * result + contentType.hashCode()
+        result = 31 * result + state.hashCode()
+        result = 31 * result + size.hashCode()
+        result = 31 * result + downloadCount.hashCode()
+        result = 31 * result + createdAt.hashCode()
+        result = 31 * result + updatedAt.hashCode()
+        result = 31 * result + downloadUrl.hashCode()
+        return result
+    }
+
     val isAPK : Boolean get() = contentType
         .contentEquals("application/vnd.android.package-archive")
 

+ 24 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/data/github/remote/Release.kt

@@ -67,6 +67,30 @@ data class Release(
     val zipUrl: String = ""
 
 ) {
+    override fun equals(other: Any?): Boolean =
+        if (other !is Release) false else other.hashCode() == hashCode()
+
+    override fun hashCode(): Int {
+        var result = id.hashCode()
+        result = 31 * result + url.hashCode()
+        result = 31 * result + assetsUrl.hashCode()
+        result = 31 * result + uploadUrl.hashCode()
+        result = 31 * result + htmlUrl.hashCode()
+        result = 31 * result + author.hashCode()
+        result = 31 * result + nodeId.hashCode()
+        result = 31 * result + tagName.hashCode()
+        result = 31 * result + targetCommitish.hashCode()
+        result = 31 * result + name.hashCode()
+        result = 31 * result + draft.hashCode()
+        result = 31 * result + preRelease.hashCode()
+        result = 31 * result + createdAt.hashCode()
+        result = 31 * result + publishedAt.hashCode()
+        result = 31 * result + assets.hashCode()
+        result = 31 * result + tarUrl.hashCode()
+        result = 31 * result + zipUrl.hashCode()
+        return result
+    }
+
     val hasAssets: Boolean get() = assets.isNotEmpty()
 
     val hasAPK: Boolean

+ 26 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/data/github/remote/User.kt

@@ -68,4 +68,29 @@ data class User(
     @Json(name = "site_admin")
     val siteAdmin: Boolean = false
 
-)
+) {
+    override fun equals(other: Any?): Boolean =
+        if (other !is User) false else other.hashCode() == hashCode()
+
+    override fun hashCode(): Int {
+        var result = id.hashCode()
+        result = 31 * result + nodeId.hashCode()
+        result = 31 * result + login.hashCode()
+        result = 31 * result + avatarUrl.hashCode()
+        result = 31 * result + gravatarId.hashCode()
+        result = 31 * result + url.hashCode()
+        result = 31 * result + htmlUrl.hashCode()
+        result = 31 * result + followersUrl.hashCode()
+        result = 31 * result + followingUrl.hashCode()
+        result = 31 * result + gistsUrl.hashCode()
+        result = 31 * result + starredUrl.hashCode()
+        result = 31 * result + subscriptionsUrl.hashCode()
+        result = 31 * result + organizationsUrl.hashCode()
+        result = 31 * result + reposUrl.hashCode()
+        result = 31 * result + eventsUrl.hashCode()
+        result = 31 * result + receivedEventsUrl.hashCode()
+        result = 31 * result + type.hashCode()
+        result = 31 * result + siteAdmin.hashCode()
+        return result
+    }
+}

+ 4 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ComposeExt.kt

@@ -7,7 +7,6 @@
  */
 
 @file:Suppress("unused")
-
 package org.mjdev.tvlib.extensions
 
 import android.annotation.SuppressLint
@@ -50,6 +49,7 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import org.mjdev.tvlib.extensions.BitmapExt.majorColor
+import org.mjdev.tvlib.extensions.ContextExt.isTV
 import org.mjdev.tvlib.extensions.MediaItemExt.uri
 import org.mjdev.tvlib.helpers.media.ItemType
 import org.mjdev.tvlib.interfaces.ItemPhoto
@@ -73,6 +73,9 @@ object ComposeExt {
         (orientation == Configuration.ORIENTATION_PORTRAIT) || (screenHeightDp > screenWidthDp)
     }
 
+    @Composable
+    fun isTV(): Boolean = LocalContext.current.isTV
+
     // todo more types
     @Composable
     fun Any?.asException(): Exception? = when (this) {

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

@@ -9,14 +9,22 @@
 package org.mjdev.tvlib.extensions
 
 import android.app.Activity
+import android.app.UiModeManager
 import android.content.Context
 import android.content.ContextWrapper
+import android.content.res.Configuration
 import android.text.format.DateFormat
 import timber.log.Timber
 import java.util.Date
 
 object ContextExt {
 
+    val Context.isTV: 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())
 

+ 20 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt

@@ -17,14 +17,34 @@ import com.skydoves.sandwich.message
 import com.skydoves.sandwich.onError
 import com.skydoves.sandwich.onException
 import com.skydoves.sandwich.toFlow
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
 import timber.log.Timber
 import java.net.MalformedURLException
 import java.net.URL
+import kotlin.coroutines.CoroutineContext
 
 @Suppress("unused")
 object GlobalExt {
 
+    val UI = Dispatchers.Main
+    val IO = Dispatchers.IO
+
+    fun launch(
+        context: CoroutineContext = IO,
+        start: CoroutineStart = CoroutineStart.DEFAULT,
+        block: suspend CoroutineScope.() -> Unit
+    ) {
+        CoroutineScope(context).launch(
+            context = context,
+            start = start,
+            block = block
+        )
+    }
+
     fun <T> T.postDelayed(
         delay: Long,
         block: T.() -> Unit
@@ -77,5 +97,4 @@ object GlobalExt {
         } catch (e: MalformedURLException) {
             false
         }
-
 }

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

@@ -88,4 +88,14 @@ data class App(
     override var title: String?,
     override var image: Drawable?,
     override var intent: Intent?,
-) : ItemWithTitle<String>, ItemWithImage<Drawable>, ItemWithIntent
+) : ItemWithTitle<String>, ItemWithImage<Drawable>, ItemWithIntent {
+    override fun equals(other: Any?): Boolean =
+        if (other !is App) false else other.hashCode() == hashCode()
+
+    override fun hashCode(): Int {
+        var result = title?.hashCode() ?: 0
+        result = 31 * result + (image?.hashCode() ?: 0)
+        result = 31 * result + (intent?.hashCode() ?: 0)
+        return result
+    }
+}

+ 9 - 8
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/AudioItem.kt

@@ -12,6 +12,7 @@ import android.database.Cursor
 import android.net.Uri
 import android.provider.MediaStore
 import org.mjdev.tvlib.extensions.CursorExt.asMap
+import org.mjdev.tvlib.helpers.apps.App
 import org.mjdev.tvlib.interfaces.ItemAudio
 import org.mjdev.tvlib.interfaces.ItemWithDate
 import org.mjdev.tvlib.interfaces.ItemWithDescription
@@ -58,17 +59,19 @@ class AudioItem() :
 //        }
     }
 
-    override fun equals(other: Any?): Boolean {
-        if (other == null || other !is AudioItem) return false
-        return uri == other.uri
-    }
+    override fun equals(other: Any?): Boolean =
+        if (other !is AudioItem) false else other.hashCode() == hashCode()
 
     override fun hashCode(): Int {
-        return uri?.hashCode() ?: 0
+        var result = title?.hashCode() ?: 0
+        result = 31 * result + (uri?.hashCode() ?: 0)
+        result = 31 * result + (image?.hashCode() ?: 0)
+        result = 31 * result + (date?.hashCode() ?: 0)
+        result = 31 * result + (description?.hashCode() ?: 0)
+        return result
     }
 
     companion object {
-
         const val SORT_ORDER_DATE_DESC: String = MediaStore.Audio.Media.DATE_ADDED + " DESC"
         const val SORT_ORDER_DATE_ASC: String = MediaStore.Audio.Media.DATE_ADDED + " ASC"
 
@@ -79,7 +82,5 @@ class AudioItem() :
             MediaStore.Audio.Media.TITLE,
             MediaStore.Audio.Media.DATA,
         )
-
     }
-
 }

+ 3 - 14
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/CachingCursor.kt

@@ -28,13 +28,10 @@ import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.platform.LocalContext
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
 import org.mjdev.tvlib.extensions.CursorExt.isNotEmpty
+import org.mjdev.tvlib.extensions.GlobalExt.IO
+import org.mjdev.tvlib.extensions.GlobalExt.launch
 import timber.log.Timber
-import kotlin.coroutines.CoroutineContext
 
 @Suppress("FunctionName")
 open class CachingCursor<T>(
@@ -253,18 +250,10 @@ open class CachingCursor<T>(
 
     @Deprecated("Deprecated in Java")
     override fun requery(): Boolean {
-        launch {
-            _requery()
-        }
+        launch { _requery() }
         return true
     }
 
-    private fun launch(
-        context: CoroutineContext = Dispatchers.IO,
-        start: CoroutineStart = CoroutineStart.DEFAULT,
-        block: suspend CoroutineScope.() -> Unit
-    ) = CoroutineScope(context).launch(context, start, block)
-
     @Suppress("SpellCheckingInspection", "DEPRECATION")
     private fun _requery(): Boolean {
         cache.clear()

+ 9 - 8
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/PhotoItem.kt

@@ -61,17 +61,20 @@ class PhotoItem() :
 //        }
     }
 
-    override fun equals(other: Any?): Boolean {
-        if (other == null || other !is PhotoItem) return false
-        return uri == other.uri
-    }
+    override fun equals(other: Any?): Boolean =
+        if (other !is PhotoItem) false else other.hashCode() == hashCode()
 
     override fun hashCode(): Int {
-        return uri?.hashCode() ?: 0
+        var result = title?.hashCode() ?: 0
+        result = 31 * result + (uri?.hashCode() ?: 0)
+        result = 31 * result + (image?.hashCode() ?: 0)
+        result = 31 * result + (background?.hashCode() ?: 0)
+        result = 31 * result + (date?.hashCode() ?: 0)
+        result = 31 * result + (description?.hashCode() ?: 0)
+        return result
     }
 
     companion object {
-
         const val SORT_ORDER_DATE_DESC: String = MediaStore.Images.Media.DATE_ADDED + " DESC"
         const val SORT_ORDER_DATE_ASC: String = MediaStore.Images.Media.DATE_ADDED + " ASC"
 
@@ -82,7 +85,5 @@ class PhotoItem() :
             MediaStore.Images.Media.TITLE,
             MediaStore.Images.Media.DATA,
         )
-
     }
-
 }

+ 8 - 8
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/VideoItem.kt

@@ -58,17 +58,19 @@ class VideoItem() :
 //        }
     }
 
-    override fun equals(other: Any?): Boolean {
-        if (other == null || other !is VideoItem) return false
-        return uri == other.uri
-    }
+    override fun equals(other: Any?): Boolean =
+        if (other !is VideoItem) false else other.hashCode() == hashCode()
 
     override fun hashCode(): Int {
-        return uri?.hashCode() ?: 0
+        var result = title?.hashCode() ?: 0
+        result = 31 * result + (uri?.hashCode() ?: 0)
+        result = 31 * result + (image?.hashCode() ?: 0)
+        result = 31 * result + (date?.hashCode() ?: 0)
+        result = 31 * result + (description?.hashCode() ?: 0)
+        return result
     }
 
     companion object {
-
         const val SORT_ORDER_DATE_DESC: String = MediaStore.Video.Media.DATE_ADDED + " DESC"
         const val SORT_ORDER_DATE_ASC: String = MediaStore.Video.Media.DATE_ADDED + " ASC"
 
@@ -79,7 +81,5 @@ class VideoItem() :
             MediaStore.Video.Media.TITLE,
             MediaStore.Video.Media.DATA,
         )
-
     }
-
 }

+ 2 - 5
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/media/ItemType.kt

@@ -22,11 +22,9 @@ enum class ItemType {
     Photo;
 
     companion object {
-
         // todo more types
         operator fun invoke(item: Any?): ItemType {
-            return if (item == null) Undefined
-            else when (item) {
+            return if (item == null) Undefined else when (item) {
                 is ItemAudio -> Audio
                 is ItemVideo -> Video
                 is ItemPhoto -> Photo
@@ -36,10 +34,9 @@ enum class ItemType {
                     MediaMetadata.MEDIA_TYPE_ALBUM -> Photo
                     else -> Undefined
                 }
+
                 else -> Undefined
             }
         }
-
     }
-
 }

+ 49 - 49
tvlib/src/main/kotlin/org/mjdev/tvlib/payments/Settings.kt

@@ -1,49 +1,49 @@
-/*
- *  Copyright (c) Milan Jurkulák 2023.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
-package org.mjdev.tvlib.payments
-
-import android.content.Context
-import android.content.pm.PackageManager
-
-class Settings(context: Context) {
-    private val appContext = context.applicationContext
-    private val backendMetadata = getMetadata(METADATA_KEY_BACKEND_URL_KEY)
-    private val googlePlacesMetadata = getMetadata(METADATA_KEY_GOOGLE_PLACES_API_KEY)
-
-    val playgroundBackendUrl: String
-        get() {
-            return backendMetadata ?: BASE_URL
-        }
-
-    val googlePlacesApiKey: String?
-        get() {
-            return googlePlacesMetadata
-        }
-
-    private fun getMetadata(key: String): String? {
-        return appContext.packageManager
-            .getApplicationInfo(appContext.packageName, PackageManager.GET_META_DATA)
-            .metaData
-            .getString(key)
-            .takeIf { it?.isNotBlank() == true }
-    }
-
-    internal companion object {
-
-        private const val BASE_URL = "https://stp-mobile-playground-backend-v7.stripedemos.com/"
-
-        private const val METADATA_KEY_BACKEND_URL_KEY =
-            "com.stripe.android.paymentsheet.example.metadata.backend_url"
-
-        private const val METADATA_KEY_GOOGLE_PLACES_API_KEY =
-            "com.stripe.android.paymentsheet.example.metadata.google_places_api_key"
-
-    }
-
-}
+///*
+// *  Copyright (c) Milan Jurkulák 2023.
+// *  Contact:
+// *  e: mimoccc@gmail.com
+// *  e: mj@mjdev.org
+// *  w: https://mjdev.org
+// */
+//
+//package org.mjdev.tvlib.payments
+//
+//import android.content.Context
+//import android.content.pm.PackageManager
+//
+//class Settings(context: Context) {
+//    private val appContext = context.applicationContext
+//    private val backendMetadata = getMetadata(METADATA_KEY_BACKEND_URL_KEY)
+//    private val googlePlacesMetadata = getMetadata(METADATA_KEY_GOOGLE_PLACES_API_KEY)
+//
+//    val playgroundBackendUrl: String
+//        get() {
+//            return backendMetadata ?: BASE_URL
+//        }
+//
+//    val googlePlacesApiKey: String?
+//        get() {
+//            return googlePlacesMetadata
+//        }
+//
+//    private fun getMetadata(key: String): String? {
+//        return appContext.packageManager
+//            .getApplicationInfo(appContext.packageName, PackageManager.GET_META_DATA)
+//            .metaData
+//            .getString(key)
+//            .takeIf { it?.isNotBlank() == true }
+//    }
+//
+//    internal companion object {
+//
+//        private const val BASE_URL = "https://stp-mobile-playground-backend-v7.stripedemos.com/"
+//
+//        private const val METADATA_KEY_BACKEND_URL_KEY =
+//            "com.stripe.android.paymentsheet.example.metadata.backend_url"
+//
+//        private const val METADATA_KEY_GOOGLE_PLACES_API_KEY =
+//            "com.stripe.android.paymentsheet.example.metadata.google_places_api_key"
+//
+//    }
+//
+//}

+ 8 - 3
tvlib/src/main/kotlin/org/mjdev/tvlib/screen/Screen.kt

@@ -20,6 +20,8 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.navigationBarsPadding
 import androidx.compose.foundation.layout.statusBarsPadding
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AddToQueue
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -38,9 +40,11 @@ import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ModifierExt.onlyPortrait
 import org.mjdev.tvlib.navigation.MenuItem
 
-@Suppress("unused", "LeakingThis")
+@Suppress("unused", "LeakingThis", "PrivatePropertyName")
 open class Screen {
 
+    private val TAG = this::class.simpleName.toString()
+
     open val route: String get() = (this::class.simpleName ?: "none")
 
     open val routeArgs: List<NamedNavArgument> = emptyList()
@@ -49,7 +53,8 @@ open class Screen {
 
     open val menuTitle: Any? = null
 
-    open val menuIcon: ImageVector? = null
+    open val menuIcon: ImageVector? = Icons.Filled.AddToQueue
+
 
     open val backgroundColor: Color = Color.DarkGray
 
@@ -98,7 +103,7 @@ open class Screen {
             horizontalAlignment = Alignment.CenterHorizontally
         ) {
             Text(
-                text = "Empty Screen",
+                text = (title ?: TAG).toString(),
                 fontSize = 64.sp,
                 fontWeight = FontWeight.Bold,
                 color = Color.White

+ 24 - 20
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/Card.kt

@@ -6,6 +6,8 @@
  *  w: https://mjdev.org
  */
 
+@file:Suppress("PreviewShouldNotBeCalledRecursively")
+
 package org.mjdev.tvlib.ui.components.card
 
 import android.annotation.SuppressLint
@@ -61,26 +63,28 @@ fun Card(
     aspectRatio: Float? = 16f / 9f,
     interactionSource: MutableInteractionSource = rememberMutableInteractionSource(),
     content: @Composable ColumnScope.() -> Unit = {}
-) = androidx.tv.material3.Card(
-    onClick = onClick,
-    modifier = modifier
-        .recomposeHighlighter()
-        .widthIn(max = 320.dp)
-        .conditional(aspectRatio != null) {
-            tvAspectRatio(aspectRatio)
-        }
-        .clickable {
-            onClick()
-        },
-    onLongClick = onLongClick,
-    shape = shape,
-    colors = colors,
-    scale = scale,
-    border = border,
-    glow = glow,
-    interactionSource = interactionSource,
-    content = content
-)
+) {
+    androidx.tv.material3.Card(
+        onClick = onClick,
+        modifier = modifier
+            .recomposeHighlighter()
+            .widthIn(max = 320.dp)
+            .conditional(aspectRatio != null) {
+                tvAspectRatio(aspectRatio)
+            }
+            .clickable {
+                onClick()
+            },
+        onLongClick = onLongClick,
+        shape = shape,
+        colors = colors,
+        scale = scale,
+        border = border,
+        glow = glow,
+        interactionSource = interactionSource,
+        content = content
+    )
+}
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 val CardDefaults.NO_BORDER: CardBorder

+ 2 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/ItemCard.kt

@@ -69,6 +69,7 @@ fun ItemCard(
     ),
     focusRequester: FocusRequester = rememberFocusRequester(item),
     titlePadding: PaddingValues = PaddingValues(8.dp),
+    showTitle:Boolean = true,
     onFocus: (item: Any?) -> Unit = {},
     onClick: (item: Any?) -> Unit = {},
 ) = FocusableCard(
@@ -90,4 +91,5 @@ fun ItemCard(
     titlePadding = titlePadding,
     onFocus = onFocus,
     onClick = onClick,
+    showTitle = showTitle,
 )

+ 11 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/PhotoCard.kt

@@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp
 import androidx.tv.material3.CardDefaults
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
@@ -33,6 +34,7 @@ import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.interfaces.ItemWithDescription
 import org.mjdev.tvlib.interfaces.ItemWithImage
+import org.mjdev.tvlib.ui.components.image.ImageAny
 import org.mjdev.tvlib.ui.components.image.PhotoImage
 
 @SuppressLint("ModifierParameter")
@@ -54,6 +56,12 @@ fun PhotoCard(
     contrast: Float = 5f,
     @FloatRange(from = -255.0, to = 255.0)
     brightness: Float = -255f,
+    placeholder: @Composable () -> Unit = {
+        ImageAny(
+            modifier = modifier,
+            src = R.drawable.broken_image
+        )
+    },
     imageRenderer: @Composable () -> Unit = {
         PhotoImage(
             modifier = Modifier.fillMaxSize(),
@@ -61,14 +69,14 @@ fun PhotoCard(
             contentScale = contentScale,
             contrast = contrast,
             brightness = brightness,
+            placeholder = placeholder,
             borderColor = if (focusState.isFocused) Color.Green else Color.Black,
             contentDescription = (item as? ItemWithDescription<*>)?.description?.toString(),
         )
     },
+    showTitle: Boolean = true,
     cardWidth: Dp = computeCardWidth(),
     aspectRatio: Float = 16f / 9f,
-    // todo not yet implemented
-    placeholder: @Composable () -> Unit = {},
     scale: CardScale = CardDefaults.scale(),
     titlePadding: PaddingValues = PaddingValues(8.dp),
     onFocus: (item: Any?) -> Unit = {},
@@ -89,5 +97,6 @@ fun PhotoCard(
         titlePadding = titlePadding,
         onFocus = onFocus,
         onClick = onClick,
+        showTitle = showTitle
     )
 }

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/CarouselCard.kt

@@ -73,6 +73,7 @@ fun CarouselCard(
         focusState = focusState,
         contentScale = contentScale,
         scale = scale,
+        showTitle = false,
         imageRenderer = imageRenderer,
         titlePadding = titlePadding,
         onFocus = onFocus,

+ 61 - 65
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/complex/FocusableCard.kt

@@ -74,6 +74,7 @@ fun FocusableCard(
     colors: CardColors = CardDefaults.colors(),
     border: CardBorder = CardDefaults.colorFocusBorder(Color.Green),
     glow: CardGlow = CardDefaults.colorFocusGlow(Color.Green),
+    showTitle: Boolean = true,
     imageRenderer: @Composable () -> Unit = {
         ImageAny(
             modifier = Modifier.fillMaxSize(),
@@ -97,76 +98,80 @@ fun FocusableCard(
     titlePadding: PaddingValues = PaddingValues(8.dp),
     cardWidth: Dp = computeCardWidth(),
     aspectRatio: Float = 16f / 9f,
+    isEdit: Boolean = isEditMode(),
     onClick: (item: Any?) -> Unit = {},
+) = Box(
+    modifier = modifier
+        .recomposeHighlighter()
+        .size(
+            width = cardWidth,
+            height = cardWidth / aspectRatio
+        )
+        .focusState(focusState)
+        .onFocusChanged { state ->
+            onFocusChange(state)
+        }
+        .requestFocusOnTouch(focusRequester) {
+            if (focusState.isFocused) {
+                onClick(item)
+            }
+        }
+        .conditional(isEdit) {
+            defaultMinSize(80.dp)
+        }
 ) {
-    val isEdit = isEditMode()
     CompactCard(
         scale = scale,
         shape = shape,
         colors = colors,
         border = border,
         glow = glow,
-        modifier = modifier
-            .recomposeHighlighter()
-            .size(
-                width = cardWidth,
-                height = cardWidth / aspectRatio
-            )
-            .focusState(focusState)
-            .onFocusChanged { state ->
-                onFocusChange(state)
-            }
-            .requestFocusOnTouch(focusRequester) {
-                if (focusState.isFocused) {
-                    onClick(item)
-                }
-            }
-            .conditional(isEdit) {
-                defaultMinSize(80.dp)
-            },
-        image = {
-            imageRenderer()
-        },
+        modifier = Modifier.fillMaxSize(),
+        image = { imageRenderer() },
         title = {
-            Box(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .background(
-                        if (focusState.isFocused)
-                            textBackgroundSelected
-                        else
-                            textBackgroundUnselected,
-                        RectangleShape
+            if (showTitle) {
+                Box(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .background(
+                            if (focusState.isFocused)
+                                textBackgroundSelected
+                            else
+                                textBackgroundUnselected,
+                            RectangleShape
+                        )
+                ) {
+                    TextAny(
+                        modifier = Modifier.padding(titlePadding),
+                        maxLines = 1,
+                        color = textColor,
+                        style = MaterialTheme.typography.bodyMedium,
+                        text = if (isEdit) "title" else (item as? ItemWithTitle<*>)?.title
                     )
-            ) {
-                TextAny(
-                    modifier = Modifier.padding(titlePadding),
-                    maxLines = 1,
-                    color = textColor,
-                    style = MaterialTheme.typography.bodyMedium,
-                    text = if (isEdit) "title" else (item as? ItemWithTitle<*>)?.title
-                )
+                }
             }
         },
         subtitle = {
-            Box(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .background(
-                        if (focusState.isFocused)
-                            textBackgroundSelected
-                        else
-                            textBackgroundUnselected,
-                        RectangleShape
+            if (showTitle) {
+                Box(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .background(
+                            if (focusState.isFocused)
+                                textBackgroundSelected
+                            else
+                                textBackgroundUnselected,
+                            RectangleShape
+                        )
+                ) {
+                    AutoHideEmptyText(
+                        modifier = Modifier.padding(titlePadding),
+                        maxLines = 1,
+                        color = textColor,
+                        style = MaterialTheme.typography.bodySmall,
+                        text = if (isEdit) "subtitle" else (item as? ItemWithSubtitle<*>)?.subtitle
                     )
-            ) {
-                AutoHideEmptyText(
-                    modifier = Modifier.padding(titlePadding),
-                    maxLines = 1,
-                    color = textColor,
-                    style = MaterialTheme.typography.bodySmall,
-                    text = if (isEdit) "subtitle" else (item as? ItemWithSubtitle<*>)?.subtitle
-                )
+                }
             }
         },
         description = {
@@ -176,13 +181,4 @@ fun FocusableCard(
             onClick(item)
         },
     )
-//    LaunchedEffect(focused) {
-//        try {
-//            if (focused) {
-//                focusRequester.requestFocus()
-//            }
-//        } catch (e: Exception) {
-//            Timber.e(e)
-//        }
-//    }
 }

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/Gallery.kt

@@ -103,6 +103,7 @@ fun Gallery(
     val imageSrc = rememberImageFromItem(itemState.value)
 
     BoxWithControls(
+        modifier = modifier,
         src = itemState.value,
         controlsState = controlsState,
         controls = { src, bckColor, state ->

+ 8 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gridview/TvGridView.kt

@@ -70,7 +70,10 @@ fun TvGridView(
             contentPadding = PaddingValues(16.dp),
             state = state,
         ) {
-            items(items) { item ->
+            items(
+                items = items,
+                key = { item -> item.hashCode() }
+            ) { item ->
                 contentOfItem(item)
             }
         }
@@ -84,7 +87,10 @@ fun TvGridView(
             contentPadding = PaddingValues(16.dp),
             state = state,
         ) {
-            items(items) { item ->
+            items(
+                items = items,
+                key = { item -> item.hashCode() }
+            ) { item ->
                 contentOfItem(item)
             }
         }

+ 53 - 51
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/PhotoImage.kt

@@ -71,56 +71,58 @@ fun PhotoImage(
     onImageStateChanged: (state: GlideImageState) -> Unit = {}
 ) {
     val isEdit = isEditMode()
-    GlideImage(
-        imageModel = {
-            src
-        },
-        modifier = modifier
-            .defaultMinSize(80.dp)
-            .recomposeHighlighter()
-            .conditional(isEdit) {
-                aspectRatio(16f / 9f)
-            }
-            .border(
-                BorderStroke(borderSize, borderColor),
-                shape
+    if (src == null) {
+        placeholder()
+    } else {
+        GlideImage(
+            imageModel = {                src            },
+            modifier = modifier
+                .defaultMinSize(80.dp)
+                .recomposeHighlighter()
+                .conditional(isEdit) {
+                    aspectRatio(16f / 9f)
+                }
+                .border(
+                    BorderStroke(borderSize, borderColor),
+                    shape
+                ),
+            imageOptions = ImageOptions(
+                contentScale = contentScale,
+                alignment = alignment,
+                contentDescription = contentDescription,
+                alpha = alpha,
+                colorFilter = colorFilter,
             ),
-        imageOptions = ImageOptions(
-            contentScale = contentScale,
-            alignment = alignment,
-            contentDescription = contentDescription,
-            alpha = alpha,
-            colorFilter = colorFilter,
-        ),
-        failure = { failure ->
-            Timber.e(failure.reason)
-            placeholder()
-        },
-        onImageStateChanged = { state ->
-            onImageStateChanged.invoke(state)
-        },
-        success = { state, _ ->
-            val bitmap = state.imageBitmap?.asAndroidBitmap()
-            Box(
-                modifier = modifier
-                    .recomposeHighlighter()
-                    .background(bitmap.majorColor(backgroundColor), shape),
-            ) {
-                ImageAny(
-                    src = bitmap
-                        ?.toDrawable()
-                        ?.asPhoto(),
-                    modifier = modifier.recomposeHighlighter(),
-                    alignment = alignment,
-                    contentScale = contentScale,
-                    alpha = alpha,
-                    colorFilter = contrastAndBrightness(
-                        contrast,
-                        brightness
-                    ),
-                    contentDescription = contentDescription,
-                )
-            }
-        }
-    )
+            onImageStateChanged = { state ->
+                onImageStateChanged.invoke(state)
+            },
+            success = { state, _ ->
+                val bitmap = state.imageBitmap?.asAndroidBitmap()
+                Box(
+                    modifier = modifier
+                        .recomposeHighlighter()
+                        .background(bitmap.majorColor(backgroundColor), shape),
+                ) {
+                    ImageAny(
+                        src = bitmap
+                            ?.toDrawable()
+                            ?.asPhoto(),
+                        modifier = modifier.recomposeHighlighter(),
+                        alignment = alignment,
+                        contentScale = contentScale,
+                        alpha = alpha,
+                        colorFilter = contrastAndBrightness(
+                            contrast,
+                            brightness
+                        ),
+                        contentDescription = contentDescription,
+                    )
+                }
+            },
+            failure = { failure ->
+                Timber.e(failure.reason)
+                placeholder()
+            },
+        )
+    }
 }

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

@@ -846,4 +846,8 @@ class ExoPlayerImpl(
         exoPlayer.setDeviceMuted(muted, flags)
     }
 
+    override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) {
+        exoPlayer.setAudioAttributes(audioAttributes, handleAudioFocus)
+    }
+
 }

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

@@ -533,6 +533,11 @@ interface IMediaPlayer : Player {
             }
 
             override fun setDeviceMuted(muted: Boolean, flags: Int) {}
+            override fun setAudioAttributes(
+                audioAttributes: AudioAttributes,
+                handleAudioFocus: Boolean
+            ) {
+            }
 
         }
 

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

@@ -47,9 +47,9 @@ fun MediaPlayerContainer(
                 .recomposeHighlighter()
         ) {
             state.player.GetPlayerView()
-            PlayerControlView(
-                state = state
-            )
+//            PlayerControlView(
+//                state = state
+//            )
         }
         DisposableEffect(Unit) {
             state.startPlay()

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

@@ -615,4 +615,8 @@ class NativePlayerImpl(
         TODO("Not yet implemented")
     }
 
+    override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) {
+        TODO("Not yet implemented")
+    }
+
 }

+ 2 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/PlayerControlView.kt

@@ -9,17 +9,15 @@
 package org.mjdev.tvlib.ui.components.media
 
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
 import org.mjdev.tvlib.annotations.Previews
-import org.mjdev.tvlib.ui.components.media.MediaPlayerState.Companion.rememberMediaPlayerState
 
 // todo new ui
 @Previews
 @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
 @Composable
 fun PlayerControlView(
-    modifier: Modifier = Modifier,
-    state: MediaPlayerState = rememberMediaPlayerState(),
+//    modifier: Modifier = Modifier,
+//    state: MediaPlayerState = rememberMediaPlayerState(),
 ) {
 //    val isEdit = isEditMode()
 //    val infoVisible = remember { mutableStateOf(isEdit) }

+ 7 - 10
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavDrawerContent.kt

@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxHeight
 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
@@ -22,8 +21,6 @@ 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
@@ -42,13 +39,13 @@ import org.mjdev.tvlib.navigation.NavHostControllerEx
 @Previews
 @Composable
 fun NavDrawerContent(
-    backgroundColor: Color = Color.DarkGray,
-    shape: Shape = RoundedCornerShape(
-        topStart = 0.dp,
-        topEnd = 16.dp,
-        bottomEnd = 16.dp,
-        bottomStart = 0.dp
-    ),
+//    backgroundColor: Color = Color.DarkGray,
+//    shape: Shape = RoundedCornerShape(
+//        topStart = 0.dp,
+//        topEnd = 16.dp,
+//        bottomEnd = 16.dp,
+//        bottomStart = 0.dp
+//    ),
     navController: NavHostControllerEx = rememberNavControllerEx(),
     onDrawerItemClick: (id: Int) -> Unit = { id ->
         navController.menuItem(id).let { menuItem ->

+ 4 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/SettingsDrawer.kt

@@ -14,6 +14,7 @@ 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 org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt.rememberDrawerState
@@ -23,7 +24,9 @@ import org.mjdev.tvlib.extensions.ComposeExt.rememberDrawerState
 @Composable
 fun SettingsDrawer(
     modifier: Modifier = Modifier,
-    drawerState: DrawerState = rememberDrawerState(),
+    drawerState: DrawerState = rememberDrawerState(
+        initialValue = DrawerValue.Closed
+    ),
     onTouchOutside: () -> Unit = {},
     content: @Composable () -> Unit = {
         Box(modifier = Modifier.fillMaxSize())

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

@@ -15,6 +15,7 @@ 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.BuildConfig
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.extensions.ComposeExt.asException
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
@@ -31,9 +32,11 @@ fun AppUpdateAvailableMessage(
     backgroundColor: Color = Color(0, 132, 0, 255),
     buttonText: Any? = R.string.bt_update,
     dismissState: MutableState<Boolean> = mutableStateOf(false),
+    isDebug:Boolean = BuildConfig.DEBUG,
     appUpdater: AppUpdater = rememberAppUpdater(
         githubUser = githubUser,
-        githubRepository = githubRepository
+        githubRepository = githubRepository,
+        isDebug = isDebug
     ),
     onButtonClick: () -> Unit = { appUpdater.updateApp() },
 ) {

+ 12 - 5
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/BrowseView.kt

@@ -24,6 +24,7 @@ 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.BuildConfig
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.interfaces.ItemWithTitle
@@ -62,7 +63,8 @@ fun BrowseView(
     onItemFocused: (item: Any?) -> Unit = {},
     onItemClicked: (item: Any?) -> Unit = {},
     state: TvLazyListState = rememberTvLazyListState(),
-    isEdit: Boolean = isEditMode()
+    isEdit: Boolean = isEditMode(),
+    isDebug: Boolean = BuildConfig.DEBUG
 ) {
     val coroutineScope = rememberCoroutineScope()
     ScrollableTvLazyColumn(
@@ -99,6 +101,7 @@ fun BrowseView(
                 }
                 if (isEdit || showAppUpdateState) {
                     AppUpdateAvailableMessage(
+                        isDebug = isDebug,
                         githubUser = githubUser,
                         githubRepository = githubRepository,
                         title = R.string.title_app_update,
@@ -143,10 +146,14 @@ fun BrowseView(
                 }
             )
         }
-        items(customRows) { row -> row.invoke() }
-        items(categoriesAndItemsMap.map { entry ->
-            Pair(entry.key, entry.value)
-        }) { entry ->
+        items(
+            items = customRows,
+            key = { item -> item.hashCode() }
+        ) { row -> row.invoke() }
+        items(
+            items = categoriesAndItemsMap.map { entry -> Pair(entry.key, entry.value) },
+            key = { item -> item.hashCode() }
+        ) { entry ->
             CategoryRow(
                 title = (entry.first as? ItemWithTitle<*>)?.title ?: entry.first.toString(),
                 items = entry.second,

+ 4 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/CategoryRow.kt

@@ -92,7 +92,10 @@ fun CategoryRow(
             state = rowState,
             contentPadding = padding.value.dp,
         ) {
-            items(items) { item ->
+            items(
+                items = items,
+                key = { item -> item.hashCode() }
+            ) { item ->
                 contentOfItem(item)
             }
         }

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/CursorRow.kt

@@ -109,7 +109,7 @@ fun CursorRow(
             ) {
                 items(
                     count = cursor.count,
-                    key = { idx -> idx }
+                    key = { item -> item.hashCode() }
                 ) { idx ->
                     PhotoCard(
                         item = cursor.getData(idx),

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

@@ -1,55 +1,55 @@
-/*
- * 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.tv
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.sp
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.MaterialTheme
-import org.mjdev.tvlib.R
-import org.mjdev.tvlib.annotations.Previews
-import org.mjdev.tvlib.ui.components.text.TextAny
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Previews
-@Composable
-fun Loading(
-    modifier: Modifier = Modifier,
-    backgroundColor: Color = Color.Black,
-    shape: Shape = RectangleShape,
-    text: Any? = R.string.msg_loading,
-    textColor: Color = Color.White,
-    textSize: TextUnit = 20.sp,
-    textStyle: TextStyle = MaterialTheme.typography.headlineMedium,
-    contentAlignment: Alignment = Alignment.Center
-) {
-    Box(
-        modifier = modifier
-            .fillMaxSize()
-            .background(backgroundColor, shape),
-        contentAlignment = contentAlignment
-    ) {
-        TextAny(
-            text = text,
-            color = textColor,
-            fontSize = textSize,
-            style = textStyle
-        )
-    }
-}
+///*
+// * 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.tv
+//
+//import androidx.compose.foundation.background
+//import androidx.compose.foundation.layout.Box
+//import androidx.compose.foundation.layout.fillMaxSize
+//import androidx.compose.runtime.Composable
+//import androidx.compose.ui.Alignment
+//import androidx.compose.ui.Modifier
+//import androidx.compose.ui.graphics.Color
+//import androidx.compose.ui.graphics.RectangleShape
+//import androidx.compose.ui.graphics.Shape
+//import androidx.compose.ui.text.TextStyle
+//import androidx.compose.ui.unit.TextUnit
+//import androidx.compose.ui.unit.sp
+//import androidx.tv.material3.ExperimentalTvMaterial3Api
+//import androidx.tv.material3.MaterialTheme
+//import org.mjdev.tvlib.R
+//import org.mjdev.tvlib.annotations.Previews
+//import org.mjdev.tvlib.ui.components.text.TextAny
+//
+//@OptIn(ExperimentalTvMaterial3Api::class)
+//@Previews
+//@Composable
+//fun Loading(
+//    modifier: Modifier = Modifier,
+//    backgroundColor: Color = Color.Black,
+//    shape: Shape = RectangleShape,
+//    text: Any? = R.string.msg_loading,
+//    textColor: Color = Color.White,
+//    textSize: TextUnit = 20.sp,
+//    textStyle: TextStyle = MaterialTheme.typography.headlineMedium,
+//    contentAlignment: Alignment = Alignment.Center
+//) {
+//    Box(
+//        modifier = modifier
+//            .fillMaxSize()
+//            .background(backgroundColor, shape),
+//        contentAlignment = contentAlignment
+//    ) {
+//        TextAny(
+//            text = text,
+//            color = textColor,
+//            fontSize = textSize,
+//            style = textStyle
+//        )
+//    }
+//}

+ 80 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/NeonCircle.kt

@@ -0,0 +1,80 @@
+package org.mjdev.tvlib.ui.components.tv
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.PaintingStyle
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.min
+import org.mjdev.tvlib.annotations.Previews
+
+@Previews
+@Composable
+fun NeonCircle(
+    modifier: Modifier = Modifier,
+    colorCircle: Color = Color.Green,
+    colorStroke: Color = Color.Green,
+    padding: Dp = 40.dp,
+    strokeWidth: Dp = 12.dp,
+    circleWidth: Dp = 3.dp
+) {
+    val paint = remember {
+        Paint().apply {
+            style = PaintingStyle.Stroke
+            this.strokeWidth = strokeWidth.value
+        }
+    }
+    val frameworkPaint = remember {
+        paint.asFrameworkPaint().apply {
+            color = colorStroke.copy(alpha = 0f).toArgb()
+        }
+    }
+    BoxWithConstraints(
+        modifier = modifier.fillMaxSize(),
+        contentAlignment = Alignment.Center
+    ) {
+        val baseSize = min(maxWidth, maxHeight)
+        val size = min(maxWidth, maxHeight) - (circleWidth + padding)
+        val pad = padding / 2
+        Canvas(
+            modifier = Modifier.size(baseSize, baseSize)
+        ) {
+            drawIntoCanvas { canvas ->
+                frameworkPaint.setShadowLayer(
+                    circleWidth.toPx(),
+                    pad.toPx(),
+                    pad.toPx(),
+                    colorStroke.copy(alpha = .5f).toArgb()
+                )
+                canvas.drawOval(
+                    left = 0f,
+                    top = 0f,
+                    right = size.toPx(),
+                    bottom = size.toPx(),
+                    paint = paint
+                )
+                drawOval(
+                    colorCircle,
+                    topLeft = Offset(pad.toPx(), pad.toPx()),
+                    size = Size(size.toPx(), size.toPx()),
+                    alpha = 1f,
+                    colorFilter = null,
+                    style = Stroke(width = circleWidth.toPx())
+                )
+            }
+        }
+    }
+}

+ 118 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/NeonRectangle.kt

@@ -0,0 +1,118 @@
+package org.mjdev.tvlib.ui.components.tv
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.PaintingStyle
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.CardDefaults
+import androidx.tv.material3.CardGlow
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import org.mjdev.tvlib.annotations.Previews
+
+@Previews
+@Composable
+fun NeonRectangle(
+    modifier: Modifier = Modifier,
+    colorRect: Color = Color.Green,
+    colorStroke: Color = Color.Green,
+    padding: Dp = 40.dp,
+    strokeWidth: Dp = 12.dp,
+    circleWidth: Dp = 3.dp,
+    roundCorner: Dp = 10.dp
+) {
+    val paint = remember {
+        Paint().apply {
+            style = PaintingStyle.Stroke
+            this.strokeWidth = strokeWidth.value
+        }
+    }
+    val frameworkPaint = remember {
+        paint.asFrameworkPaint().apply {
+            color = colorStroke.copy(alpha = 0f).toArgb()
+        }
+    }
+    BoxWithConstraints(
+        modifier = modifier.fillMaxSize(),
+        contentAlignment = Alignment.Center
+    ) {
+        val baseSize = DpSize(maxWidth, maxHeight)
+        val size = DpSize(
+            maxWidth - (circleWidth + padding),
+            maxHeight - (circleWidth + padding)
+        )
+        val pad = padding / 2
+        Canvas(
+            modifier = Modifier.size(baseSize)
+        ) {
+            drawIntoCanvas { canvas ->
+                frameworkPaint.setShadowLayer(
+                    circleWidth.toPx(),
+                    pad.toPx(),
+                    pad.toPx(),
+                    colorStroke.copy(alpha = .5f).toArgb()
+                )
+                canvas.drawRoundRect(
+                    left = 0f,
+                    top = 0f,
+                    right = size.width.toPx(),
+                    bottom = size.height.toPx(),
+                    radiusX = roundCorner.toPx(),
+                    radiusY = roundCorner.toPx(),
+                    paint = paint
+                )
+                drawRoundRect(
+                    color = colorRect,
+                    topLeft = Offset(pad.toPx(), pad.toPx()),
+                    size = Size(size.width.toPx(), size.height.toPx()),
+                    cornerRadius = CornerRadius(
+                        roundCorner.toPx(),
+                        roundCorner.toPx()
+                    ),
+                    colorFilter = null,
+                    style = Stroke(width = circleWidth.toPx())
+                )
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+fun NeonRectangle(
+    modifier: Modifier = Modifier,
+    glow: CardGlow = CardDefaults.glow()
+) {
+    // todo
+    val colorRect: Color = Color.Green
+    val colorStroke: Color = Color.Green
+    val strokeWidth: Dp = 10.dp
+    val circleWidth: Dp = 3.dp
+    val roundCorner: Dp = 12.dp
+    NeonRectangle(
+        modifier = modifier,
+        padding = 0.dp,
+        colorRect=colorRect,
+        colorStroke=colorStroke,
+        strokeWidth=strokeWidth,
+        circleWidth=circleWidth,
+        roundCorner=roundCorner
+    )
+}
+
+

+ 120 - 17
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/web/WebView.kt

@@ -9,17 +9,32 @@
 package org.mjdev.tvlib.ui.components.web
 
 import android.annotation.SuppressLint
+import android.app.AlertDialog
+import android.content.Context
+import android.content.DialogInterface
 import android.graphics.drawable.ColorDrawable
+import android.os.Build
+import android.os.Message
+import android.util.AttributeSet
+import android.view.WindowManager
+import android.webkit.CookieManager
 import android.webkit.WebChromeClient
+import android.webkit.WebSettings
+import android.webkit.WebSettings.LOAD_DEFAULT
 import android.webkit.WebView
+import android.webkit.WebView.WebViewTransport
 import android.webkit.WebViewClient
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.viewinterop.AndroidView
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
+import timber.log.Timber
+
+
+private const val USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) " +
+        "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
 
-@Suppress("LocalVariableName")
 @SuppressLint("SetJavaScriptEnabled")
 @Previews
 @Composable
@@ -28,26 +43,114 @@ fun WebView(
     url: String = "about:blank",
     enableJavaScript: Boolean = true
 ) {
-    val USER_AGENT =
-        "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0"
     val isEdit = isEditMode()
+
     AndroidView(
         modifier = modifier,
         factory = { context ->
-            WebView(context).apply {
-                background = ColorDrawable(0)
-                if (!isEdit) {
-                    webChromeClient = WebChromeClient().apply {
-                        with(settings) {
-                            javaScriptEnabled = enableJavaScript
-                            allowContentAccess = true
-                            userAgentString = USER_AGENT
-                        }
-                    }
-                    webViewClient = WebViewClient()
-                }
-                loadUrl(url)
+            CustomWebView(
+                context = context,
+                inEditMode = isEdit
+            )
+        },
+        update = { view -> view.loadUrl(url) }
+    )
+}
+
+@SuppressLint("SetJavaScriptEnabled", "ViewConstructor")
+class CustomWebView(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    val inEditMode: Boolean = false
+) : WebView(context, attrs, defStyleAttr) {
+    private val cookieManager: CookieManager by lazy { CookieManager.getInstance() }
+
+    init {
+        background = ColorDrawable(0)
+        if (!inEditMode) {
+            cookieManager.setAcceptCookie(true)
+            cookieManager.setAcceptThirdPartyCookies(this, true)
+            webChromeClient = CustomChromeClient(context)
+            webViewClient = WebViewClient()
+            settings.cacheMode = LOAD_DEFAULT
+            settings.domStorageEnabled = true
+            settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
+            settings.useWideViewPort = true
+            settings.javaScriptEnabled = true
+            settings.javaScriptCanOpenWindowsAutomatically = true
+            settings.allowContentAccess = true
+            settings.allowFileAccess = true
+            settings.blockNetworkImage = false
+            settings.blockNetworkLoads = false
+            settings.builtInZoomControls = false
+            settings.cacheMode = LOAD_DEFAULT
+            settings.databaseEnabled = true
+            settings.displayZoomControls = false
+            settings.domStorageEnabled = true
+            settings.useWideViewPort = true
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                settings.isAlgorithmicDarkeningAllowed = true
             }
+            settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
+            settings.loadWithOverviewMode = true
+            settings.userAgentString = USER_AGENT
+            settings.setSupportMultipleWindows(true)
+            scrollBarStyle = SCROLLBARS_INSIDE_OVERLAY
         }
-    )
+    }
+}
+
+class CustomChromeClient(
+    val context: Context
+) : WebChromeClient() {
+    private val cookieManager: CookieManager by lazy { CookieManager.getInstance() }
+    private val webViewPopUp by lazy { WebView(context) }
+    private val builder: AlertDialog by lazy { AlertDialog.Builder(context).create() }
+
+    @SuppressLint("SetJavaScriptEnabled")
+    override fun onCreateWindow(
+        view: WebView?,
+        isDialog: Boolean,
+        isUserGesture: Boolean,
+        resultMsg: Message
+    ): Boolean {
+        webViewPopUp.isVerticalScrollBarEnabled = false
+        webViewPopUp.isHorizontalScrollBarEnabled = false
+        webViewPopUp.webChromeClient = CustomChromeClient(context)
+        webViewPopUp.settings.javaScriptEnabled = true
+        webViewPopUp.settings.userAgentString = USER_AGENT
+        builder.setTitle("")
+        builder.setView(webViewPopUp)
+        builder.setButton(DialogInterface.BUTTON_POSITIVE, "Close") { dialog, _ ->
+            webViewPopUp.destroy()
+            dialog.dismiss()
+        }
+        builder.show()
+        builder.window?.clearFlags(
+            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+                    WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
+        )
+        cookieManager.setAcceptCookie(true)
+        cookieManager.setAcceptThirdPartyCookies(webViewPopUp, true)
+        cookieManager.setAcceptThirdPartyCookies(view, true)
+        val transport = resultMsg.obj as WebViewTransport
+        transport.webView = webViewPopUp
+        resultMsg.sendToTarget()
+        return true
+    }
+
+    override fun onCloseWindow(window: WebView) {
+        try {
+            webViewPopUp.destroy()
+        } catch (e: Exception) {
+            Timber.e(e)
+        }
+        try {
+            builder.dismiss()
+        } catch (e: Exception) {
+            Timber.e(e)
+        }
+    }
+
 }

+ 4 - 3
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/theme/Theme.kt

@@ -10,10 +10,11 @@ package org.mjdev.tvlib.ui.theme
 
 import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.runtime.Composable
+import org.mjdev.tvlib.extensions.ComposeExt.isTV
 
 @Composable
 fun TVAppTheme(
-    useDarkTheme: Boolean = isSystemInDarkTheme(),
-    dynamicColor: Boolean = true,
+    useDarkTheme: Boolean = isSystemInDarkTheme() || isTV(),
+    dynamicColors: Boolean = !isTV(),
     content: @Composable () -> Unit,
-) = ThemeHelper (useDarkTheme, dynamicColor, content)
+) = ThemeHelper (useDarkTheme, dynamicColors, content)

+ 3 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/theme/ThemeHelper.kt

@@ -44,12 +44,13 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.MaterialTheme
 import androidx.tv.material3.darkColorScheme
 import androidx.tv.material3.lightColorScheme
+import org.mjdev.tvlib.extensions.ComposeExt.isTV
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 fun ThemeHelper(
-    useDarkTheme: Boolean = isSystemInDarkTheme(),
-    dynamicColors: Boolean = true,
+    useDarkTheme: Boolean = isSystemInDarkTheme() || isTV(),
+    dynamicColors: Boolean = !isTV(),
     content: @Composable () -> Unit,
 ) {
     val context = LocalContext.current

+ 39 - 43
tvlib/src/main/kotlin/org/mjdev/tvlib/updater/AppUpdater.kt

@@ -25,7 +25,6 @@ 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.launch
 import okhttp3.OkHttpClient
@@ -43,38 +42,32 @@ class AppUpdater(
     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 keyName: String = repoName,
+    private val isDebug: Boolean = BuildConfig.DEBUG,
+    private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO),
 ) {
-
-    private val service by lazy {
-        updateService(githubUser, githubRepository)
-    }
-
     private val prefs by lazy {
         context.getSharedPreferences(repoName, MODE_PRIVATE)
     }
 
     private val releases: Flow<List<Release>> = flow {
+        val service = updateService(isDebug, githubUser, githubRepository)
         emit(service.releases().getOrNull() ?: emptyList())
-    }.flowOn(Dispatchers.IO)
+    }
 
-    private val lastRelease: Flow<Release?> =
-        releases.map {
-            it.filter { r ->
-                r.hasAPK
-            }.minByOrNull { r ->
-                r.publishedAt
-            }
+    private val lastRelease: Flow<Release?> = releases.map {
+        it.filter { r ->
+            r.hasAPK
+        }.minByOrNull { r ->
+            r.publishedAt
         }
+    }
 
     private val serverTimestamp: Flow<Long> =
-        lastRelease.map { r ->
-            r?.publishedAt?.asTimestamp ?: 0L
-        }
+        lastRelease.map { r -> r?.publishedAt?.asTimestamp ?: 0L }
 
-    private val installedTimestamp: Flow<Long> = serverTimestamp.map { st ->
-        prefs.getLong(keyName, st)
-    }
+    private val installedTimestamp: Flow<Long> =
+        serverTimestamp.map { st -> prefs.getLong(keyName, st) }
 
     val isUpdateAvailable: Flow<Boolean> = serverTimestamp.map { st ->
         val lt = installedTimestamp.firstOrNull() ?: 0L
@@ -85,18 +78,16 @@ class AppUpdater(
     }
 
     @MainThread
-    fun updateApp() {
-        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)
-                }
+    fun updateApp() = coroutineScope.launch {
+        releases.collectLatest { rs ->
+            rs.filter { r ->
+                r.hasAPK
+            }.maxByOrNull { r ->
+                r.publishedAt.asTimestamp
+            }?.let { release ->
+                release.apkAsset?.downloadUrl
+            }?.also { url ->
+                downloadFile(url)
             }
         }
     }
@@ -117,11 +108,12 @@ class AppUpdater(
         }
 
         private fun okHttpClient(
+            isDebug: Boolean = false,
             cacheInterceptor: CacheInterceptor = CacheInterceptor(),
             httpLoggingInterceptor: HttpLoggingInterceptor = httpLoggingInterceptor(),
         ): OkHttpClient = OkHttpClient.Builder().apply {
             addNetworkInterceptor(cacheInterceptor)
-            if (BuildConfig.DEBUG) {
+            if (isDebug) {
                 addInterceptor(httpLoggingInterceptor)
             }
         }.build()
@@ -138,29 +130,33 @@ class AppUpdater(
             .build()
 
         fun updateService(
+            isDebug: Boolean = false,
             githubUser: String = "mimoccc",
             githubRepository: String = "tvapp",
             baseUrl: String = "https://api.github.com/repos/$githubUser/$githubRepository/",
-            retrofit: Retrofit = retrofit(okHttpClient(), baseUrl)
+            retrofit: Retrofit = retrofit(okHttpClient(isDebug = isDebug), baseUrl)
         ): AppUpdateService = retrofit.create(AppUpdateService::class.java)
 
         @Composable
         fun rememberAppUpdater(
             githubUser: String = "mimoccc",
             githubRepository: String = "tvapp",
-        ): AppUpdater {
-            val context = LocalContext.current
-            return remember(context) {
-                AppUpdater(context, githubUser, githubRepository)
+            isDebug: Boolean = BuildConfig.DEBUG
+        ): AppUpdater = LocalContext.current.let { context ->
+            remember(context) {
+                AppUpdater(
+                    context = context,
+                    githubUser = githubUser,
+                    githubRepository = githubRepository,
+                    isDebug = isDebug
+                )
             }
         }
 
         private val String.asTimestamp: Long
             get() = runCatching {
-                SimpleDateFormat(
-                    "yyyy-MM-dd'T'HH:mm:ss'Z'",
-                    Locale.US
-                ).parse(this)?.time ?: 0L
+                SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
+                    .parse(this)?.time ?: 0L
             }.getOrDefault(0L)
     }
 

+ 10 - 22
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/CustomWebChromeClient.kt

@@ -10,12 +10,11 @@ import timber.log.Timber
 
 @SuppressLint("SetJavaScriptEnabled")
 @Suppress("Deprecation")
-class CustomWebChromeClient(private val webView: WebClient) : WebChromeClient() {
+class CustomWebChromeClient(
+    private val webView: WebClient
+) : WebChromeClient() {
+
     private val settings: WebSettings get() = webView.settings
-    var icon: Bitmap? = null
-        internal set
-    var title: String? = null
-        internal set
 
     init {
         settings.apply {
@@ -126,11 +125,9 @@ class CustomWebChromeClient(private val webView: WebClient) : WebChromeClient()
     }
 
     override fun onReceivedIcon(view: WebView?, icon: Bitmap?) {
-        this.icon = icon
     }
 
     override fun onReceivedTitle(view: WebView?, title: String?) {
-        this.title = title
     }
 
     override fun onReceivedTouchIconUrl(
@@ -151,8 +148,13 @@ class CustomWebChromeClient(private val webView: WebClient) : WebChromeClient()
         return super.onShowFileChooser(webView, filePathCallback, fileChooserParams)
     }
 
-    @Deprecated("Deprecated in Java")
+    @Deprecated("Deprecated in Java", ReplaceWith(
+        "Timber.e(\"\$sourceID \$lineNumber : \$message\")",
+        "timber.log.Timber"
+    )
+    )
     override fun onConsoleMessage(message: String?, lineNumber: Int, sourceID: String?) {
+        Timber.e("$sourceID $lineNumber : $message")
     }
 
     @Deprecated("Deprecated in Java")
@@ -166,20 +168,6 @@ class CustomWebChromeClient(private val webView: WebClient) : WebChromeClient()
     ) {
     }
 
-    @Deprecated("Deprecated in Java",
-        ReplaceWith("super.onJsTimeout()", "android.webkit.WebChromeClient")
-    )
-    override fun onJsTimeout(): Boolean {
-        return super.onJsTimeout()
-    }
-
-//    override fun onReachedMaxAppCacheSize(
-//        requiredStorage: Long,
-//        quota: Long,
-//        quotaUpdater: WebStorage.QuotaUpdater?
-//    ) {
-//    }
-
     @Deprecated("Deprecated in Java")
     override fun onShowCustomView(
         view: View?,

+ 4 - 19
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/CustomWebViewClient.kt

@@ -8,22 +8,15 @@ import android.view.KeyEvent
 import android.webkit.*
 import org.mjdev.tvlib.webclient.html.HtmlPage
 import org.mjdev.tvlib.webclient.javascript.JSI
-import org.mjdev.tvlib.webclient.javascript.JSIListener
 import org.mjdev.tvlib.webclient.cache.WebViewCacheInterceptorInst
 import timber.log.Timber
 
 open class CustomWebViewClient(
     private val webView: WebClient,
-//    var adBlock: AdBlock? = null
-) : WebViewClient(), JSIListener {
-
-    val jsi by lazy {
-        JSI(webView, this)
-    }
-
-    val loadedResources by lazy {
-        ArrayList<String>()
-    }
+    private val onGotHtml: (htmlPage: HtmlPage, loadedResources: List<String>) -> Unit,
+) : WebViewClient() {
+    val loadedResources = mutableListOf<String>()
+    val jsi: JSI by lazy { JSI(webView, onGotHtml) }
 
     @Synchronized
     override fun shouldInterceptRequest(
@@ -31,10 +24,6 @@ open class CustomWebViewClient(
         request: WebResourceRequest?
     ): WebResourceResponse? {
         val uri = request?.url?.toString()
-//        if (adBlock?.isBlocked(uri) == true) {
-//            Timber.d("Blocked url: ${request?.url}")
-//            return null
-//        } else {
         uri?.let {
             if (!loadedResources.contains(it)) {
                 loadedResources.add(it)
@@ -42,7 +31,6 @@ open class CustomWebViewClient(
         }
         Timber.d("Loading url: $uri")
         return WebViewCacheInterceptorInst.instance?.interceptRequest(request)
-//        }
     }
 
     @Synchronized
@@ -145,7 +133,4 @@ open class CustomWebViewClient(
         }
     }
 
-    override fun onGotHtml(htmlPage: HtmlPage, loadedResources: List<String>) {
-    }
-
 }

+ 26 - 33
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/WebClient.kt

@@ -1,54 +1,40 @@
 package org.mjdev.tvlib.webclient
 
+import android.annotation.SuppressLint
 import android.content.Context
-import android.graphics.Bitmap
 import android.view.ViewGroup
 import android.webkit.*
 import org.mjdev.tvlib.webclient.html.HtmlPage
 import org.mjdev.tvlib.webclient.javascript.JSI
 
-@Suppress("unused")
+@SuppressLint("ViewConstructor")
+@Suppress("unused", "MemberVisibilityCanBePrivate")
 class WebClient constructor(
     context: Context,
+    val block: (page: HtmlPage?, resources: List<String>) -> Unit
 ) : WebView(context) {
-
-    val js: JSI get() = webViewClient.jsi
-
+    val webChromeClient: CustomWebChromeClient by lazy {
+        CustomWebChromeClient(this)
+    }
     val webViewClient: CustomWebViewClient by lazy {
-        object : CustomWebViewClient(this) {
-            override fun onGotHtml(htmlPage: HtmlPage, loadedResources: List<String>) {
-                this@WebClient.htmlPage = htmlPage
-                if (htmlPage.url != "about:blank") {
-                    listener?.invoke(
-                        this@WebClient,
-                        htmlPage,
-                        loadedResources
-                    )
-                }
-            }
+        CustomWebViewClient(this) { page, res ->
+            onGotHtml(page, res)
         }
     }
-
-    private val webChromeClient: CustomWebChromeClient by lazy {
-        CustomWebChromeClient(this)
-    }
-
-    val pageIcon: Bitmap? get() = webChromeClient.icon
-
-    val pageTitle: String? get() = webChromeClient.title
-
-    var htmlPage: HtmlPage? = null
-        internal set
-
-    var listener: (WebClient.(page: HtmlPage?, resources: List<String>) -> Unit)? = null
+    val js: JSI get() = webViewClient.jsi
 
     init {
         setWebChromeClient(webChromeClient)
         setWebViewClient(webViewClient)
     }
 
-    fun getPage(url: String, block: WebClient.(page: HtmlPage?, resources: List<String>) -> Unit) {
-        this.listener = block
+    fun onGotHtml(htmlPage: HtmlPage, loadedResources: List<String>) {
+        if (htmlPage.url != "about:blank") {
+            block(htmlPage, loadedResources)
+        }
+    }
+
+    fun getPage(url: String) {
         loadUrl(url)
     }
 
@@ -56,12 +42,19 @@ class WebClient constructor(
         loadUrl("about:blank")
     }
 
-    fun attachTo(viewGroup: ViewGroup?, layoutParams: ViewGroup.LayoutParams): WebClient {
-        (parent as ViewGroup?)?.removeView(this)
+    fun attachViewTo(
+        viewGroup: ViewGroup?,
+        layoutParams: ViewGroup.LayoutParams
+    ): WebClient {
+        detachView()
         viewGroup?.addView(this, layoutParams)
         return this
     }
 
+    fun detachView() {
+        (parent as ViewGroup?)?.removeView(this)
+    }
+
     companion object {
         const val USER_AGENT =
             "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0"

+ 25 - 19
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/WebScrapper.kt

@@ -5,44 +5,50 @@ import org.mjdev.tvlib.webclient.html.HtmlAnchor
 import org.mjdev.tvlib.webclient.html.HtmlPage
 import org.mjdev.tvlib.webclient.html.HtmlVideo
 import java.util.*
+import kotlin.collections.ArrayDeque
 
 @Suppress("unused")
 class WebScrapper(
-    val context: Context,
-    urls: List<String> = listOf("https://prehraj.to/hledej/filmy"),
-    private val block: (page: HtmlPage?, data: Any?) -> Unit
+    context: Context,
+    urls: List<String> = listOf(),
+    private val block: (page: HtmlPage?, videoURL: String?) -> Unit
 ) {
 
-    private val webClient: WebClient by lazy {
-        WebClient(context)
+    @Suppress("UNUSED_ANONYMOUS_PARAMETER")
+    private val webClient by lazy {
+        WebClient(context) { page: HtmlPage?, resources: List<String>? ->
+            if (page != null) {
+                parsePage(page, page.videos, page.anchors)
+            }
+        }
     }
 
-    private val parsePages: Deque<String> = LinkedList()
+    private val parsePages: ArrayDeque<String> = ArrayDeque<String>().apply {
+        addAll(urls)
+    }
 
-    init {
-        parsePages.addAll(urls)
-        parseNextPage()
+    @Throws(IllegalStateException::class)
+    fun start() {
+        if (parsePages.size > 0) {
+            parseNextPage()
+        } else {
+            throw (IllegalStateException("No url to parse"))
+        }
     }
 
     private fun parseNextPage() {
         if (parsePages.size > 0) {
-            parsePageFromUrl(parsePages.pop())
+            parsePageFromUrl(parsePages.removeFirst())
         }
     }
 
-    @Synchronized
-    fun parsePageFromUrl(pageUrl: String?) {
+    private fun parsePageFromUrl(pageUrl: String?) {
         if (pageUrl != null) {
-            webClient.getPage(pageUrl) { page, _ ->
-                if (page != null) {
-                    parsePage(page, page.videos, page.anchors)
-                }
-            }
+            webClient.getPage(pageUrl)
         }
     }
 
-    @Synchronized
-    fun parsePage(page: HtmlPage, videos: List<HtmlVideo>, anchors: List<HtmlAnchor>) {
+    private fun parsePage(page: HtmlPage, videos: List<HtmlVideo>, anchors: List<HtmlAnchor>) {
         anchors.map { anchor ->
             anchor.fullHref(page)
         }.forEach { href ->

+ 5 - 3
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/AssetsLoader.kt

@@ -11,7 +11,7 @@ import java.net.URL
 import java.util.*
 import java.util.concurrent.CopyOnWriteArraySet
 
-internal class AssetsLoader {
+class AssetsLoader {
 
     private var context: Context? = null
     private var assetResSet: CopyOnWriteArraySet<String>? = null
@@ -82,7 +82,9 @@ internal class AssetsLoader {
         if (!isSuffixMod)
             return this
         if ((assetResSet?.size ?: 0) == 0)
-            Thread { initResourceNoneRecursion(dir) }.start()
+            Thread {
+                initResourceNoneRecursion(dir)
+            }.start()
         return this
     }
 
@@ -106,7 +108,7 @@ internal class AssetsLoader {
     private fun initResourceNoneRecursion(dir: String): AssetsLoader {
         try {
             val list = LinkedList<String>()
-            val resData =context?.assets?.list(dir) ?: emptyArray()
+            val resData = context?.assets?.list(dir) ?: emptyArray()
             for (res in resData) {
                 val sub = dir + File.separator + res
                 val tmp = context?.assets?.list(sub) ?: emptyArray()

+ 0 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/CacheExtensionConfig.kt

@@ -1,7 +1,6 @@
 package org.mjdev.tvlib.webclient.cache
 
 import android.text.TextUtils
-import java.lang.reflect.Modifier.STATIC
 import java.util.Locale
 
 class CacheExtensionConfig {

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

@@ -26,7 +26,7 @@ object NetUtils {
             val port = url.port
             ou = url.protocol + "://" + url.host + if (port == -1) "" else ":$port"
         } catch (e: Exception) {
-          Timber.e(e)
+            Timber.e(e)
         }
         return ou
     }

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

@@ -21,7 +21,7 @@ object OKHttpFile {
         if (entryFile.exists() && bodyFile.exists()) {
             try {
                 val fr = BufferedReader(FileReader(entryFile), 1024)
-                var line : String
+                var line: String
                 var isGzip = false
                 while (fr.readLine().also { line = it } != null) {
                     if (line.contains("Content-Encoding") &&
@@ -39,7 +39,7 @@ object OKHttpFile {
                     source.buffer().inputStream()
                 }
             } catch (e: Exception) {
-              Timber.e(e)
+                Timber.e(e)
             }
         }
         return null

+ 3 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/TimeUtils.kt

@@ -56,6 +56,7 @@ object TimeUtils {
         return null
     }
 
-    val currentTime: String @SuppressLint("SimpleDateFormat")
-    get() = SimpleDateFormat(DATE_FORMAT).format(Date())
+    val currentTime: String
+        @SuppressLint("SimpleDateFormat")
+        get() = SimpleDateFormat(DATE_FORMAT).format(Date())
 }

+ 6 - 5
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/WebViewCacheInterceptor.kt

@@ -11,7 +11,7 @@ import okhttp3.Dns
 import okhttp3.OkHttpClient
 import okhttp3.Request
 import okhttp3.Response
-import org.mjdev.tvlib.webclient.adblock.AdBlockInterceptor
+import org.mjdev.tvlib.adblock.AdBlockInterceptor
 import timber.log.Timber
 import java.io.File
 import java.io.FileInputStream
@@ -24,7 +24,7 @@ import javax.net.ssl.SSLSocketFactory
 import javax.net.ssl.X509TrustManager
 
 // todo trust manager
-@Suppress("unused")
+@Suppress("unused", "MemberVisibilityCanBePrivate")
 class WebViewCacheInterceptor(builder: Builder) : WebViewRequestInterceptor {
     override val cachePath: File
     private val dynamicCacheFile: File?
@@ -242,7 +242,8 @@ class WebViewCacheInterceptor(builder: Builder) : WebViewRequestInterceptor {
                 )
             ) else Timber.d(String.format("from server: %s", url))
             val mimeType: String? = MimeTypeMapUtils.getMimeTypeFromUrl(url)
-            val webResourceResponse = WebResourceResponse(mimeType, "", response?.body?.byteStream())
+            val webResourceResponse =
+                WebResourceResponse(mimeType, "", response?.body?.byteStream())
             if (response?.code == 504 && !NetUtils.isConnected(context)) return null
             var message = response?.message
             if (message.isNullOrEmpty()) message = "OK"
@@ -276,11 +277,11 @@ class WebViewCacheInterceptor(builder: Builder) : WebViewRequestInterceptor {
         var sslSocketFactory: SSLSocketFactory? = null
         var x509TrustManager: X509TrustManager? = null
         var resourceInterceptor: ResourceInterceptor? = null
-        private set
+            private set
         var assetsDir: String = ""
         var isSuffixMod = false
         var dns: Dns? = null
-        private set
+            private set
 
         init {
             cacheFile = File(context.cacheDir.toString(), "CacheWebViewCache")

+ 3 - 8
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlAnchor.kt

@@ -10,14 +10,9 @@ class HtmlAnchor(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
     val href get() = attr("href")
 
     fun fullHref(page: HtmlPage): String {
-        val href = this.href.let {
-            if(it.first() == '/') it.replaceFirst("/", "")
-            else it
-        }
-        return if(href.isUrl) {
-            href
-        } else {
-            "${page.url}$href"
+        val href = this.href.let { link ->
+            if (link.first() == '/') link.replaceFirst("/", "") else link
         }
+        return if (href.isUrl) href else "${page.url}$href"
     }
 }

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlElement.kt

@@ -49,7 +49,7 @@ abstract class HtmlElement(var element: Element, val jsi: JSI) {
         } ?: newInstance(jsi)
     }
 
-    fun attr(attrName: String): String =  element.attr(attrName)
+    fun attr(attrName: String): String = element.attr(attrName)
 
     fun attr(attrName: String, value: Any?) {
         element.attr(attrName, value.toString())

+ 3 - 3
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlMeta.kt

@@ -2,11 +2,11 @@ package org.mjdev.tvlib.webclient.html
 
 import org.mjdev.tvlib.webclient.javascript.JSI
 import org.jsoup.nodes.Element
-import org.mjdev.tvlib.webclient.html.HtmlElement
 
+@Suppress("MemberVisibilityCanBePrivate", "unused")
 class HtmlMeta(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
     override val tag: String get() = "meta"
 
-    val charset : String get() = attr("charset")
-    val content : String get() = attr("content")
+    val charset: String get() = attr("charset")
+    val content: String get() = attr("content")
 }

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlPage.kt

@@ -2,6 +2,7 @@ package org.mjdev.tvlib.webclient.html
 
 import org.mjdev.tvlib.webclient.javascript.JSI
 
+@Suppress("MemberVisibilityCanBePrivate")
 class HtmlPage(val url: String?, val html: String?, val jsi: JSI) {
     val documentElement get() = HtmlDocument(html, jsi)
 

+ 4 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlScript.kt

@@ -2,12 +2,12 @@ package org.mjdev.tvlib.webclient.html
 
 import org.mjdev.tvlib.webclient.javascript.JSI
 import org.jsoup.nodes.Element
-import org.mjdev.tvlib.webclient.html.HtmlElement
 
+@Suppress("unused")
 class HtmlScript(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
     override val tag: String get() = "script"
 
-    val type:String get() = attr("type")
-    val async:String get() = attr("async")
-    val src :String get() = attr("src")
+    val type: String get() = attr("type")
+    val async: String get() = attr("async")
+    val src: String get() = attr("src")
 }

+ 29 - 22
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlVideo.kt

@@ -4,6 +4,7 @@ import android.webkit.MimeTypeMap
 import org.mjdev.tvlib.webclient.javascript.JSI
 import org.jsoup.nodes.Element
 
+@Suppress("unused", "SameParameterValue", "MemberVisibilityCanBePrivate")
 class HtmlVideo(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
     override val tag: String get() = "video"
 
@@ -14,19 +15,26 @@ class HtmlVideo(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
     val tabindex: String get() = attr("tabindex")
     val controls: String get() = attr("controls")
 
-    val sources: List<String> get() = childs.filterIsInstance<HtmlSource>().let { list ->
-        if (list.isEmpty()) {
-            listOf(attr("src"))
-        } else {
-            list.map { htmlSource ->
-                htmlSource.src
+    val sources: List<String>
+        get() = mutableListOf<String>().apply {
+            if (attr("src").isNotEmpty()) add(attr("src"))
+            childs.filterIsInstance<HtmlSource>().let { list ->
+                if (list.isEmpty()) {
+                    listOf(attr("src"))
+                } else {
+                    list.map { htmlSource ->
+                        htmlSource.src
+                    }
+                }
+            }.also { list ->
+                addAll(list)
             }
         }
-    }
 
-    val videoStreams: List<String> get() = sources.filter {
-        isVideoLink(it)
-    }
+    val videoStreams: List<String>
+        get() = sources.filter {
+            isVideoLink(it)
+        }
 
     val hasStreams: Boolean get() = videoStreams.isNotEmpty()
 
@@ -52,7 +60,7 @@ class HtmlVideo(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
             "css"
         )
 
-        fun String.nonVideoExluded(): Boolean {
+        fun String.nonVideoExcluded(): Boolean {
             val ext = MimeTypeMap.getFileExtensionFromUrl(this)
             return !VIDEO_EXCLUDE_EXTENSIONS.contains(ext)
         }
@@ -61,18 +69,17 @@ class HtmlVideo(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
             val pairs = map {
                 Pair(indexOf(VIDEO_EXTENSIONS, it), it)
             }
-            val filterred = pairs.filter {
-                (it.first >= 0) && it.second.nonVideoExluded()
-            }
-            val sorted = filterred.sortedBy {
-                it.first
-            }
-            return sorted.map {
-                it.second
+            val filtered = pairs.filter {
+                (it.first >= 0) && it.second.nonVideoExcluded()
             }
+            val sorted = filtered.sortedBy { p -> p.first }
+            return sorted.map { p -> p.second }
         }
 
-        private fun indexOf(where: List<String>, what: String): Int {
+        private fun indexOf(
+            where: List<String>,
+            what: String
+        ): Int {
             for (i in where.indices) {
                 if (what.contains(where[i])) {
                     return i
@@ -82,9 +89,9 @@ class HtmlVideo(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
         }
 
         fun isVideoLink(url: String?): Boolean {
-            val isExtOK: Boolean = url?.nonVideoExluded() ?: true
+            val isExtOK: Boolean = url?.nonVideoExcluded() ?: true
             return isExtOK && url?.let { u ->
-                VIDEO_EXTENSIONS.map { vext -> (u.contains(vext)) }.isNotEmpty()
+                VIDEO_EXTENSIONS.map { ext -> (u.contains(ext)) }.isNotEmpty()
             } ?: false
         }
     }

+ 17 - 15
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/javascript/JSI.kt

@@ -3,15 +3,12 @@ package org.mjdev.tvlib.webclient.javascript
 import android.webkit.JavascriptInterface
 import org.mjdev.tvlib.webclient.WebClient
 import org.mjdev.tvlib.webclient.html.HtmlPage
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
 import timber.log.Timber
 
-@Suppress("SameParameterValue")
+@Suppress("SameParameterValue", "MemberVisibilityCanBePrivate")
 class JSI(
-    private val webView: WebClient,
-    val listener: JSIListener
+    val webView: WebClient,
+    val onGotHtml : (htmlPage: HtmlPage, loadedResources: List<String>) -> Unit
 ) {
     val name: String get() = JSI::class.simpleName!!.lowercase()
 
@@ -19,11 +16,15 @@ class JSI(
         webView.addJavascriptInterface(this, name)
     }
 
+    // ---------------------------------------------------------------------------------------------
+
     private fun callJavascript(s: String) {
         Timber.e("Calling javascript command: $s")
         webView.loadUrl("javascript:$s")
     }
 
+    // ---------------------------------------------------------------------------------------------
+
     private fun callJSI(fnc: String, vararg params: String?) = callJavascript(
         StringBuilder().apply {
             append("jsi.$fnc(")
@@ -34,6 +35,8 @@ class JSI(
         }.toString()
     )
 
+    // ---------------------------------------------------------------------------------------------
+
     fun getHtml() {
         callJSI(
             "onHtml",
@@ -45,15 +48,14 @@ class JSI(
 
     @JavascriptInterface
     fun onHtml(html: String?) {
-        CoroutineScope(Dispatchers.Main).launch {
-            listener.onGotHtml(
-                HtmlPage(
-                    webView.url,
-                    html,
-                    webView.webViewClient.jsi
-                ), webView.webViewClient.loadedResources
-            )
-        }
+        onGotHtml(
+            HtmlPage(
+                webView.url,
+                html,
+                webView.webViewClient.jsi
+            ),
+            webView.webViewClient.loadedResources
+        )
     }
 
 //    @JavascriptInterface

+ 5 - 5
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/javascript/JSIListener.kt

@@ -1,7 +1,7 @@
-package org.mjdev.tvlib.webclient.javascript
+//package org.mjdev.tvlib.webclient.javascript
 
-import org.mjdev.tvlib.webclient.html.HtmlPage
+//import org.mjdev.tvlib.webclient.html.HtmlPage
 
-interface JSIListener {
-    fun onGotHtml(htmlPage: HtmlPage, loadedResources: List<String>)
-}
+//interface JSIListener {
+//    fun onGotHtml(htmlPage: HtmlPage, loadedResources: List<String>)
+//}

+ 130 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/WebVideoScrapper.kt

@@ -0,0 +1,130 @@
+///*
+// *  Copyright (c) Milan Jurkulák 2023.
+// *  Contact:
+// *  e: mimoccc@gmail.com
+// *  e: mj@mjdev.org
+// *  w: https://mjdev.org
+// */
+//
+//package org.mjdev.tvlib.webscrapper
+//
+//import androidx.core.net.toUri
+//import com.gargoylesoftware.htmlunit.BrowserVersion
+//import it.skrape.core.htmlDocument
+//import it.skrape.fetcher.BrowserFetcher
+//import it.skrape.fetcher.response
+//import it.skrape.fetcher.skrape
+//import it.skrape.selects.DomTreeElement
+//import org.jsoup.nodes.Document
+//import timber.log.Timber
+//
+//class WebVideoScrapper(
+//    private val baseUrls: List<String>,
+//    private val onVideoFound: (video: Video) -> Unit = {},
+//    private val onFinish: (videos: List<Video>) -> Unit = {}
+//) {
+//
+//    private val links: ArrayDeque<String> = ArrayDeque<String>().apply {
+//        addAll(baseUrls)
+//    }
+//
+//    private val videos = mutableListOf<Video>()
+//
+//    fun start() {
+//        while (links.isNotEmpty()) {
+//            val uri = links.removeFirstOrNull()
+//            if (uri != null) {
+//                Timber.e("Parsing :  $uri")
+//                parse(uri)
+//            }
+//        }
+//        onFinish(videos)
+//    }
+//
+//    private fun parse(uri: String) {
+//        try {
+//            skrape(BrowserFetcher) {
+//                request {
+//                    url = uri
+//                    sslRelaxed = true
+//                    followRedirects = true
+//                    timeout = 5000
+//                    userAgent = BrowserVersion.BEST_SUPPORTED.userAgent
+//                }
+//                response {
+//                    htmlDocument {
+//                        eachVideo.forEach { src ->
+//                            val url = normalizedUrl("${document.baseUri()}/$src")
+//                            val title = parseTitle(document, url)
+//                            val category = parseCategory(document, url)
+//                            val thumb = parseThumb(document, url)
+//                            val video = Video(
+//                                title = title,
+//                                category = category,
+//                                url = url,
+//                                thumb = thumb
+//                            )
+//                            videos.add(video)
+//                            onVideoFound(video)
+//                        }
+//                        eachLink.values.forEach { href ->
+//                            links.add(normalizedUrl("${document.baseUri()}/$href"))
+//                        }
+//                    }
+//                }
+//            }
+//        } catch (e: Exception) {
+//            Timber.e(e)
+//        }
+//    }
+//
+//    // todo : intelligent
+//    @Suppress("UNUSED_PARAMETER")
+//    private fun parseThumb(document: Document, url: String): String {
+//        return url
+//    }
+//
+//    // todo : intelligent
+//    @Suppress("UNUSED_PARAMETER")
+//    private fun parseCategory(document: Document, url: String): String {
+//        val auth = document.location().toUri().authority.toString()
+//        return auth
+//    }
+//
+//    // todo : intelligent
+//    @Suppress("UNUSED_PARAMETER")
+//    private fun parseTitle(document: Document, url: String): String {
+//        val auth = document.location().toUri().authority.toString()
+//        return document.title().replace(auth, "", ignoreCase = true)
+//    }
+//
+//    private fun normalizedUrl(u: String): String = u
+//        .replace("//", "/")
+//        .let { url ->
+//            if ((!url.startsWith("https://")) && url.startsWith("https:/")) {
+//                url.replace("https:/", "https://")
+//            } else url
+//        }.let { url ->
+//            if ((!url.startsWith("http://")) && url.startsWith("http:/")) {
+//                url.replace("http:/", "http://")
+//            } else url
+//        }
+//
+//    data class Video(
+//        val url: String,
+//        val title: String,
+//        val category: String,
+//        val thumb: String
+//    )
+//}
+//
+//val DomTreeElement.eachVideo: List<String>
+//    get() {
+//        return allElements.filter { e ->
+//            (e.tagName == "video") || (e.tagName == "source")
+//        }.filter { e ->
+//            e.hasAttribute("src")
+//        }.map { e ->
+//            e.attribute("src")
+//        }
+//    }