Ver Fonte

oauth preimplementation

Milan Jurkulak há 3 anos atrás
pai
commit
f324456d31

+ 8 - 0
app/build.gradle.kts

@@ -67,6 +67,11 @@ android {
             "IPTV_API_URL",
             "\"https://iptv-org.github.io/api/\""
         )
+
+        manifestPlaceholders.apply {
+            put("auth0Domain", "@string/com_auth0_domain")
+            put("auth0Scheme", "demo")
+        }
     }
 
     buildTypes {
@@ -228,6 +233,9 @@ dependencies {
     debugImplementation("androidx.customview:customview-poolingcontainer:1.0.0")
     // lottie
     implementation("com.airbnb.android:lottie-compose:6.1.0")
+    // oauth
+    implementation ("com.auth0.android:auth0:2.10.1")
+    implementation ("com.auth0.android:jwtdecode:2.0.2")
     // libs mismatch
     constraints {
         implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.0")?.apply {

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

@@ -209,9 +209,88 @@
         }
       ],
       "relations": []
+    },
+    {
+      "id": "4:1987880888030669889",
+      "lastPropertyId": "14:6587709986428549544",
+      "name": "User",
+      "properties": [
+        {
+          "id": "1:8489281024233193571",
+          "name": "id",
+          "type": 6,
+          "flags": 1
+        },
+        {
+          "id": "2:3068422320211502547",
+          "name": "sub",
+          "type": 9
+        },
+        {
+          "id": "3:7880080743593438568",
+          "name": "givenName",
+          "type": 9
+        },
+        {
+          "id": "4:7091128848714558596",
+          "name": "familyName",
+          "type": 9
+        },
+        {
+          "id": "5:3275722102836586086",
+          "name": "nickname",
+          "type": 9
+        },
+        {
+          "id": "6:539261702815051564",
+          "name": "fullName",
+          "type": 9
+        },
+        {
+          "id": "7:5697902777673765209",
+          "name": "pictureUrl",
+          "type": 9
+        },
+        {
+          "id": "8:1398381634980613029",
+          "name": "locale",
+          "type": 9
+        },
+        {
+          "id": "9:7614335227423573086",
+          "name": "lastUpdated",
+          "type": 9
+        },
+        {
+          "id": "10:3865959571559170686",
+          "name": "pinCode",
+          "type": 9
+        },
+        {
+          "id": "11:5012672523655215079",
+          "name": "settingsJson",
+          "type": 9
+        },
+        {
+          "id": "12:7864280554037368021",
+          "name": "age",
+          "type": 5
+        },
+        {
+          "id": "13:3813998053361806387",
+          "name": "email",
+          "type": 9
+        },
+        {
+          "id": "14:6587709986428549544",
+          "name": "emailVerified",
+          "type": 1
+        }
+      ],
+      "relations": []
     }
   ],
-  "lastEntityId": "3:9057081142853951475",
+  "lastEntityId": "4:1987880888030669889",
   "lastIndexId": "0:0",
   "lastRelationId": "0:0",
   "lastSequenceId": "0:0",

+ 79 - 9
app/objectbox-models/default.json.bak

@@ -39,7 +39,7 @@
     },
     {
       "id": "2:7141750768191734097",
-      "lastPropertyId": "11:3647500054699987607",
+      "lastPropertyId": "12:6138526348322531011",
       "name": "Movie",
       "properties": [
         {
@@ -77,12 +77,6 @@
           "name": "uri",
           "type": 9
         },
-        {
-          "id": "9:1211648868008137353",
-          "name": "_id",
-          "type": 6,
-          "flags": 1
-        },
         {
           "id": "10:3350489478053652777",
           "name": "country",
@@ -92,6 +86,12 @@
           "id": "11:3647500054699987607",
           "name": "isNsfw",
           "type": 1
+        },
+        {
+          "id": "12:6138526348322531011",
+          "name": "id",
+          "type": 6,
+          "flags": 1
         }
       ],
       "relations": []
@@ -209,9 +209,78 @@
         }
       ],
       "relations": []
+    },
+    {
+      "id": "4:1987880888030669889",
+      "lastPropertyId": "12:7864280554037368021",
+      "name": "User",
+      "properties": [
+        {
+          "id": "1:8489281024233193571",
+          "name": "id",
+          "type": 6,
+          "flags": 1
+        },
+        {
+          "id": "2:3068422320211502547",
+          "name": "sub",
+          "type": 9
+        },
+        {
+          "id": "3:7880080743593438568",
+          "name": "givenName",
+          "type": 9
+        },
+        {
+          "id": "4:7091128848714558596",
+          "name": "familyName",
+          "type": 9
+        },
+        {
+          "id": "5:3275722102836586086",
+          "name": "nickname",
+          "type": 9
+        },
+        {
+          "id": "6:539261702815051564",
+          "name": "fullName",
+          "type": 9
+        },
+        {
+          "id": "7:5697902777673765209",
+          "name": "pictureUrl",
+          "type": 9
+        },
+        {
+          "id": "8:1398381634980613029",
+          "name": "locale",
+          "type": 9
+        },
+        {
+          "id": "9:7614335227423573086",
+          "name": "lastUpdated",
+          "type": 9
+        },
+        {
+          "id": "10:3865959571559170686",
+          "name": "pinCode",
+          "type": 9
+        },
+        {
+          "id": "11:5012672523655215079",
+          "name": "settingsJson",
+          "type": 9
+        },
+        {
+          "id": "12:7864280554037368021",
+          "name": "age",
+          "type": 5
+        }
+      ],
+      "relations": []
     }
   ],
-  "lastEntityId": "3:9057081142853951475",
+  "lastEntityId": "4:1987880888030669889",
   "lastIndexId": "0:0",
   "lastRelationId": "0:0",
   "lastSequenceId": "0:0",
@@ -220,7 +289,8 @@
   "retiredEntityUids": [],
   "retiredIndexUids": [],
   "retiredPropertyUids": [
-    2336450843545835846
+    2336450843545835846,
+    1211648868008137353
   ],
   "retiredRelationUids": [],
   "version": 1

+ 50 - 3
app/src/main/AndroidManifest.xml

@@ -77,18 +77,65 @@
         </activity>
 
         <activity
-            android:label="@string/title_iptv"
             android:name=".activity.IPTVActivity"
-            android:exported="true">
+            android:exported="true"
+            android:label="@string/title_iptv">
+
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
+            </intent-filter>
+
+        </activity>
+
+        <activity
+            android:name=".activity.WebActivity"
+            android:exported="true"
+            android:label="@string/title_web">
+
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.APP_BROWSER"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
 
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
 
-                <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.LAUNCHER" />
                 <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
             </intent-filter>
 
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+            </intent-filter>
+
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:mimeType="text/html" />
+                <data android:mimeType="text/plain" />
+                <data android:mimeType="application/xhtml+xml" />
+            </intent-filter>
+
+            <intent-filter>
+                <action android:name="android.intent.action.SEND" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="text/plain" />
+            </intent-filter>
+
         </activity>
 
         <service

+ 46 - 0
app/src/main/kotlin/org/mjdev/tvapp/activity/WebActivity.kt

@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.activity
+
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.runtime.Composable
+import dagger.hilt.android.AndroidEntryPoint
+import org.mjdev.tvlib.activity.ComposableActivity
+import org.mjdev.tvlib.annotations.TvPreview
+import org.mjdev.tvlib.extensions.NavGraphBuilderExt.screen
+import org.mjdev.tvlib.navigation.NavGraphBuilderEx
+import org.mjdev.tvapp.ui.screens.WebScreen
+import org.mjdev.tvlib.extensions.NavControllerExt.openAsTop
+import org.mjdev.tvlib.extensions.StringExt.parseUri
+import org.mjdev.tvlib.navigation.NavHostControllerEx
+
+@AndroidEntryPoint
+class WebActivity : ComposableActivity() {
+
+    @TvPreview
+    @Composable
+    override fun Compose() = super.Compose()
+
+    override val navGraphBuilder: NavGraphBuilderEx.() -> Unit = {
+
+        screen(
+            route = WebScreen(),
+            isHomeScreen = true,
+            isStartScreen = true,
+        )
+
+    }
+
+    override fun onIntent(navController: NavHostControllerEx, intent: Intent?) {
+        val data : Uri = intent?.data ?: "about:blank".parseUri()
+        navController.openAsTop<WebScreen>(data)
+    }
+
+}

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

@@ -0,0 +1,63 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.data.local
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import io.objectbox.annotation.Entity
+import io.objectbox.annotation.Id
+
+@Entity
+@JsonClass(generateAdapter = true)
+class User {
+
+    @Id
+    @Json(name = "id")
+    var id: Long = 0
+
+    @Json(name = "sub")
+    var sub: String? = ""
+
+    @Json(name = "given_name")
+    var givenName: String? = ""
+
+    @Json(name = "family_name")
+    var familyName: String? = ""
+
+    @Json(name = "nickname")
+    var nickname: String? = ""
+
+    @Json(name = "name")
+    var fullName: String? = ""
+
+    @Json(name = "picture")
+    var pictureUrl: String? = ""
+
+    @Json(name = "locale")
+    var locale: String? = ""
+
+    @Json(name = "updated_at")
+    var lastUpdated: String? = ""
+
+    @Json(name = "pin_code", ignore = true)
+    var pinCode: String = ""
+
+    @Json(name = "settings_json", ignore = true)
+    var settingsJson: String = ""
+
+    @Json(name = "age", ignore = true)
+    var age: Int = -1
+
+    @Json(name = "email", ignore = true)
+    var email:String? = ""
+
+    @Json(name = "emailVerified", ignore = true)
+    var emailVerified : Boolean = false
+
+}

+ 162 - 0
app/src/main/kotlin/org/mjdev/tvapp/helpers/AuthManager.kt

@@ -0,0 +1,162 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.helpers
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import com.auth0.android.Auth0
+import com.auth0.android.authentication.AuthenticationException
+import com.auth0.android.callback.Callback
+import com.auth0.android.jwt.JWT
+import com.auth0.android.provider.BrowserPicker
+import com.auth0.android.provider.CustomTabsOptions
+import com.auth0.android.provider.WebAuthProvider
+import com.auth0.android.result.Credentials
+import org.mjdev.tvapp.BuildConfig
+import org.mjdev.tvapp.R
+import org.mjdev.tvapp.data.local.User
+import timber.log.Timber
+
+@Suppress("unused")
+class AuthManager(
+    val context: Context,
+    val onUserChange: AuthManager.(user: User?) -> Unit = {}
+) {
+
+    var idToken: String? = null
+
+    val isUserLoggedIn: Boolean get() = ((idToken != null) && (user != null))
+
+    private val scheme get() = context.getString(R.string.com_auth0_scheme)
+
+    private val clientId get() = context.getString(R.string.com_auth0_client_id)
+
+    private val domain get() = context.getString(R.string.com_auth0_domain)
+
+    private val account: Auth0 by lazy { Auth0(clientId, domain) }
+
+    val user: User?
+        get() = try {
+            JWT(idToken.toString()).let { jwt ->
+                User().apply {
+                    sub = jwt.subject ?: ""
+                    fullName = jwt.getClaim("name").asString() ?: ""
+                    email = jwt.getClaim("email").asString() ?: ""
+                    emailVerified = jwt.getClaim("email_verified").asBoolean() ?: false
+                    pictureUrl = jwt.getClaim("picture").asString() ?: ""
+                    lastUpdated = jwt.getClaim("updated_at").asString() ?: ""
+                    givenName = jwt.getClaim("given_name").asString() ?: ""
+                    familyName = jwt.getClaim("family_name").asString() ?: ""
+                    nickname = jwt.getClaim("nickname").asString() ?: ""
+                    locale = jwt.getClaim("locale").asString() ?: "en"
+                }
+            }
+        } catch (e: Exception) {
+            Timber.e(e)
+            null
+        }
+
+    init {
+        idToken = context.getSharedPreferences(SAVE_KEY, Context.MODE_PRIVATE)
+            .getString(SAVE_KEY, null)
+        onUserChange(this, user)
+    }
+
+    private fun saveToken() {
+        context.getSharedPreferences(SAVE_KEY, Context.MODE_PRIVATE).also { sp ->
+            sp.edit().putString(SAVE_KEY, idToken).apply()
+        }
+    }
+
+    fun login() {
+        WebAuthProvider
+            .login(account)
+            .withScheme(scheme)
+            .withTrustedWebActivity()
+            .withCustomTabsOptions(
+                CustomTabsOptions.newBuilder()
+                    .showTitle(false)
+                    .withBrowserPicker(
+                        BrowserPicker.newBuilder()
+                            .withAllowedPackages(listOf(BuildConfig.APPLICATION_ID))
+                            .build()
+                    )
+                    .build()
+            )
+            .start(
+                context,
+                object : Callback<Credentials, AuthenticationException> {
+                    override fun onFailure(error: AuthenticationException) {
+                        idToken = ""
+                        saveToken()
+                        onUserChange(this@AuthManager, user)
+                        Timber.e("Error occurred in login(): $error")
+                    }
+
+                    override fun onSuccess(result: Credentials) {
+                        idToken = result.idToken
+                        saveToken()
+                        onUserChange(this@AuthManager, user)
+                        Timber.d("User successfully logged in.")
+                    }
+                }
+            )
+    }
+
+    fun logout() {
+        WebAuthProvider
+            .logout(account)
+            .withScheme(scheme)
+            .withTrustedWebActivity()
+            .withCustomTabsOptions(
+                CustomTabsOptions.newBuilder()
+                    .showTitle(false)
+                    .withBrowserPicker(
+                        BrowserPicker.newBuilder()
+                            .withAllowedPackages(listOf(BuildConfig.APPLICATION_ID))
+                            .build()
+                    )
+                    .build()
+            )
+            .start(
+                context,
+                object : Callback<Void?, AuthenticationException> {
+                    override fun onFailure(error: AuthenticationException) {
+                        Timber.e("Error occurred in logout(): $error")
+                    }
+
+                    override fun onSuccess(result: Void?) {
+                        idToken = ""
+                        saveToken()
+                        onUserChange(this@AuthManager, user)
+                    }
+                }
+            )
+    }
+
+    companion object {
+
+        private const val SAVE_KEY = "AuthManager"
+
+        @SuppressLint("RememberReturnType", "ComposableNaming")
+        @Composable
+        fun rememberAuthManager(
+            onUserChange: AuthManager.(user: User?) -> Unit
+        ) = LocalContext.current.let { ctx ->
+            remember {
+                AuthManager(ctx, onUserChange)
+            }
+        }
+
+    }
+
+}

+ 34 - 29
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt

@@ -25,6 +25,8 @@ import androidx.compose.ui.layout.ContentScale
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.activity.IPTVActivity
 import org.mjdev.tvapp.activity.IPTVActivity.Companion.IPTV_DATA
+import org.mjdev.tvapp.data.local.User
+import org.mjdev.tvapp.helpers.AuthManager.Companion.rememberAuthManager
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.HiltExt.appViewModel
 import org.mjdev.tvlib.interfaces.ItemWithId
@@ -94,6 +96,9 @@ class MainPage : Page() {
             errorState.value = error
         }
 
+        val user = remember { mutableStateOf<User?>(null) }
+        val authManager = rememberAuthManager { u -> user.value = u }
+
         Box(
             modifier = Modifier.fillMaxSize(),
         ) {
@@ -108,7 +113,7 @@ class MainPage : Page() {
             BrowseView(
                 modifier = Modifier.fillMaxSize(),
                 appIcon = R.mipmap.ic_launcher,
-                userIcon = org.mjdev.tvlib.R.drawable.person,
+                userIcon = user.value?.pictureUrl ?: org.mjdev.tvlib.R.drawable.person,
                 title = titleState.value,
                 messages = messages.value,
                 categories = countryList.value,
@@ -121,37 +126,37 @@ class MainPage : Page() {
                 },
                 onItemFocused = onItemSelect,
                 onItemClicked = onItemClick,
-                customRows =
-                mutableListOf<@Composable () -> Unit>().apply {
-                    if (viewModel.appsList.size > 0) {
-                        add { AppsRow(apps = viewModel.appsList) }
+                onUserPicClicked = {
+                    if (authManager.isUserLoggedIn) {
+                        // todo show details / settings
+                    } else {
+                        authManager.login()
+                    }
+                },
+                customRows = mutableListOf<@Composable () -> Unit>().apply {
+                    if (viewModel.appsList.size > 0) add {
+                        AppsRow(apps = viewModel.appsList)
                     }
-                    if (viewModel.localAudioCursor.count > 0) {
-                        add {
-                            LocalAudioRow(
-                                cursor = viewModel.localAudioCursor,
-                                openItem = { item -> onItemClick(item) },
-                                onItemFocus = onItemSelect,
-                            )
-                        }
+                    if (viewModel.localAudioCursor.count > 0) add {
+                        LocalAudioRow(
+                            cursor = viewModel.localAudioCursor,
+                            openItem = { item -> onItemClick(item) },
+                            onItemFocus = onItemSelect,
+                        )
                     }
-                    if (viewModel.localVideoCursor.count > 0) {
-                        add {
-                            LocalVideoRow(
-                                cursor = viewModel.localVideoCursor,
-                                openItem = { item -> onItemClick(item) },
-                                onItemFocus = onItemSelect,
-                            )
-                        }
+                    if (viewModel.localVideoCursor.count > 0) add {
+                        LocalVideoRow(
+                            cursor = viewModel.localVideoCursor,
+                            openItem = { item -> onItemClick(item) },
+                            onItemFocus = onItemSelect,
+                        )
                     }
-                    if (viewModel.localPhotoCursor.count > 0) {
-                        add {
-                            LocalPhotosRow(
-                                cursor = viewModel.localPhotoCursor,
-                                openItem = { item -> onItemClick(item) },
-                                onItemFocus = onItemSelect,
-                            )
-                        }
+                    if (viewModel.localPhotoCursor.count > 0) add {
+                        LocalPhotosRow(
+                            cursor = viewModel.localPhotoCursor,
+                            openItem = { item -> onItemClick(item) },
+                            onItemFocus = onItemSelect,
+                        )
                     }
                 }
             )

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

@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.ui.screens
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.NetworkCell
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.navigation.navArgument
+import org.mjdev.tvapp.R
+import org.mjdev.tvlib.annotations.TvPreview
+import org.mjdev.tvlib.navigation.AnyType
+import org.mjdev.tvlib.screen.Screen
+import org.mjdev.tvlib.ui.components.web.WebView
+
+class WebScreen : Screen() {
+
+    private val data = "data"
+
+    override val title = R.string.title_web
+    override val menuIcon: ImageVector get() = Icons.Filled.NetworkCell
+    override val immersive: Boolean = true
+    override val showOnce: Boolean = true
+
+    override val routeArgs = listOf(
+        navArgument(data) {
+            nullable = true
+            type = AnyType
+        }
+    )
+
+    @TvPreview
+    @Composable
+    override fun ComposeScreen() {
+
+        val data: Any? = remember { args[data] }
+
+        Box(
+            modifier = Modifier
+                .fillMaxSize()
+                .background(Color.Black, RectangleShape),
+        ) {
+            WebView(
+                modifier = Modifier.fillMaxSize(),
+                url = data?.toString() ?: "about:blank"
+            )
+        }
+
+    }
+
+}

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

@@ -39,6 +39,7 @@
     <string name="title_video_local">Lokálni video subory</string>
     <string name="title_photo_local">Lokálni fotografie</string>
     <string name="title_iptv">Televize</string>
+    <string name="title_web">Internet</string>
 
     <string name="author">Vytvorené autorem Milan Jurkulák © 2023</string>
 

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

@@ -39,6 +39,7 @@
     <string name="title_video_local">Lokálne video súbory</string>
     <string name="title_photo_local">Lokálne fotografie</string>
     <string name="title_iptv">Televízia</string>
+    <string name="title_web">Internet</string>
 
     <string name="author">Vytvorené autorom Milan Jurkulák © 2023</string>
 

+ 13 - 0
app/src/main/res/values/com_auth0_domain.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~  Copyright (c) Milan Jurkulák 2023.
+  ~  Contact:
+  ~  e: mimoccc@gmail.com
+  ~  e: mj@mjdev.org
+  ~  w: https://mjdev.org
+  -->
+<resources>
+    <string name="com_auth0_client_id" translatable="false">ELZOvhRqguKMIv8lTXGUlOhrKLVunQ2C</string>
+    <string name="com_auth0_domain" translatable="false">dev-580urwpf6d0owqho.us.auth0.com</string>
+    <string name="com_auth0_scheme" translatable="false">demo</string>
+</resources>

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

@@ -39,6 +39,7 @@
     <string name="title_video_local">Local video files</string>
     <string name="title_photo_local">Local photo files</string>
     <string name="title_iptv">Television</string>
+    <string name="title_web">Internet</string>
 
     <string name="author">Created by Milan Jurkulak © 2023</string>
 

+ 45 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/web/WebView.kt

@@ -0,0 +1,45 @@
+/*
+ *  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.web
+
+import android.annotation.SuppressLint
+import android.graphics.drawable.ColorDrawable
+import android.webkit.WebChromeClient
+import android.webkit.WebView
+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.TvPreview
+
+@SuppressLint("SetJavaScriptEnabled")
+@TvPreview
+@Composable
+fun WebView(
+    modifier: Modifier = Modifier,
+    url: String = "about:blank",
+    enableJavaScript:Boolean = true
+) {
+    AndroidView(
+        modifier = modifier,
+        factory = { context ->
+            WebView(context).apply {
+                background = ColorDrawable(0)
+                webChromeClient = WebChromeClient().apply{
+                    with(settings) {
+                        javaScriptEnabled = enableJavaScript
+                        allowContentAccess = true
+                    }
+                }
+                webViewClient = WebViewClient()
+                loadUrl(url)
+            }
+        }
+    )
+}