Răsfoiți Sursa

Refactor video playback features and enhance UI controls

- Updated AndroidManifest to use round icon resource.
- Added new video control features including zoom and pan functionality in VideoScreen.
- Improved VideoViewModel to handle video state and actions more effectively.
- Integrated new event handling for video controls and position tracking.
- Introduced new drawable resources for video control icons.
- Enhanced media handling in WebView and related components for better user experience.
codeskraps 1 săptămână în urmă
părinte
comite
1fddc9ff81
36 a modificat fișierele cu 405 adăugiri și 164 ștergeri
  1. 1 1
      app/src/main/AndroidManifest.xml
  2. BIN
      app/src/main/ic_launcher-playstore.png
  3. 8 0
      app/src/main/java/com/codeskraps/sbrowser/MainActivity.kt
  4. 87 37
      app/src/main/java/com/codeskraps/sbrowser/feature/video/VideoViewModel.kt
  5. 192 18
      app/src/main/java/com/codeskraps/sbrowser/feature/video/components/VideoScreen.kt
  6. 3 1
      app/src/main/java/com/codeskraps/sbrowser/feature/video/mvi/VideoAction.kt
  7. 6 1
      app/src/main/java/com/codeskraps/sbrowser/feature/video/mvi/VideoEvent.kt
  8. 11 4
      app/src/main/java/com/codeskraps/sbrowser/feature/video/mvi/VideoState.kt
  9. 8 4
      app/src/main/java/com/codeskraps/sbrowser/feature/webview/MediaWebViewModel.kt
  10. 13 67
      app/src/main/java/com/codeskraps/sbrowser/feature/webview/media/HandleVideo.kt
  11. 30 30
      app/src/main/java/com/codeskraps/sbrowser/feature/webview/media/MediaWebView.kt
  12. 2 1
      app/src/main/java/com/codeskraps/sbrowser/feature/webview/media/MediaWebViewClient.kt
  13. 5 0
      app/src/main/res/drawable/add.xml
  14. 5 0
      app/src/main/res/drawable/fast_forward.xml
  15. 5 0
      app/src/main/res/drawable/fast_rewind.xml
  16. 5 0
      app/src/main/res/drawable/pause.xml
  17. 5 0
      app/src/main/res/drawable/play_arrow.xml
  18. 5 0
      app/src/main/res/drawable/remove.xml
  19. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  20. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  21. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.webp
  22. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
  23. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  24. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.webp
  25. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
  26. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  27. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  28. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
  29. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  30. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  31. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
  32. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  33. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  34. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
  35. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  36. 4 0
      app/src/main/res/values/ic_launcher_background.xml

+ 1 - 1
app/src/main/AndroidManifest.xml

@@ -14,7 +14,7 @@
         android:fullBackupContent="@xml/backup_rules"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
-        android:roundIcon="@mipmap/ic_launcher"
+        android:roundIcon="@mipmap/ic_launcher_round"
         android:supportsRtl="true"
         android:theme="@style/Theme.SBrowser"
         tools:targetApi="31">

BIN
app/src/main/ic_launcher-playstore.png


+ 8 - 0
app/src/main/java/com/codeskraps/sbrowser/MainActivity.kt

@@ -28,6 +28,7 @@ import com.codeskraps.sbrowser.feature.settings.components.SettingsScreen
 import com.codeskraps.sbrowser.feature.splash.SplashViewModel
 import com.codeskraps.sbrowser.feature.video.VideoViewModel
 import com.codeskraps.sbrowser.feature.video.components.VideoScreen
+import com.codeskraps.sbrowser.feature.video.mvi.VideoAction
 import com.codeskraps.sbrowser.feature.webview.MediaWebViewModel
 import com.codeskraps.sbrowser.feature.webview.components.WebViewScreen
 import com.codeskraps.sbrowser.feature.webview.media.ClearCookies
@@ -35,6 +36,7 @@ import com.codeskraps.sbrowser.feature.webview.media.MediaWebView
 import com.codeskraps.sbrowser.feature.webview.mvi.MediaWebViewEvent
 import com.codeskraps.sbrowser.navigation.Screen
 import com.codeskraps.sbrowser.ui.theme.SBrowserTheme
+import com.codeskraps.sbrowser.util.components.ObserveAsEvents
 import dagger.hilt.android.AndroidEntryPoint
 import javax.inject.Inject
 
@@ -145,6 +147,12 @@ class MainActivity : ComponentActivity() {
                                 state = state,
                                 handleEvent = viewModel.state::handleEvent
                             )
+
+                            ObserveAsEvents(flow = viewModel.action) { action ->
+                                when (action) {
+                                    VideoAction.NavigateBack -> navController.popBackStack()
+                                }
+                            }
                         }
                     }
                 }

+ 87 - 37
app/src/main/java/com/codeskraps/sbrowser/feature/video/VideoViewModel.kt

@@ -8,9 +8,15 @@ import com.codeskraps.sbrowser.feature.video.mvi.VideoState
 import com.codeskraps.sbrowser.umami.domain.AnalyticsRepository
 import com.codeskraps.sbrowser.util.StateReducerViewModel
 import dagger.hilt.android.lifecycle.HiltViewModel
-import javax.inject.Inject
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+private const val MIN_SCALE = 1f
+private const val MAX_SCALE = 3f
+private const val ZOOM_STEP = 0.25f
+private const val SAVED_POSITION = "saved_position"
 
 @HiltViewModel
 class VideoViewModel @Inject constructor(
@@ -18,69 +24,113 @@ class VideoViewModel @Inject constructor(
     private val analyticsRepository: AnalyticsRepository
 ) : StateReducerViewModel<VideoState, VideoEvent, VideoAction>(VideoState.initial) {
 
-    private var lastTrackedPosition: Long = 0
+    private var controlsJob: Job? = null
 
     init {
         savedStateHandle.get<String>("url")?.run {
             state.handleEvent(VideoEvent.Load(this))
         }
+        // Restore saved position if available
+        savedStateHandle.get<Long>(SAVED_POSITION)?.let { position ->
+            state.handleEvent(VideoEvent.Position(position))
+        }
     }
 
     override fun reduceState(currentState: VideoState, event: VideoEvent): VideoState {
         return when (event) {
             is VideoEvent.Load -> onLoad(currentState, event.url)
             is VideoEvent.Position -> onPosition(currentState, event.position)
-            is VideoEvent.Duration -> onDuration(currentState, event.duration)
+            is VideoEvent.UpdateScale -> onUpdateScale(currentState, event.scale)
+            is VideoEvent.UpdatePan -> onUpdatePan(currentState, event.x, event.y)
+            is VideoEvent.ShowControls -> onShowControls(currentState, event.visible)
+            VideoEvent.ZoomIn -> onZoomIn(currentState)
+            VideoEvent.ZoomOut -> onZoomOut(currentState)
+            VideoEvent.Exit -> onExit(currentState)
+        }
+    }
+
+    private fun onPosition(currentState: VideoState, position: Long): VideoState {
+        // Save position to handle configuration changes
+        savedStateHandle[SAVED_POSITION] = position
+        return currentState.copy(position = position)
+    }
+
+    private fun onShowControls(currentState: VideoState, visible: Boolean): VideoState {
+        controlsJob?.cancel()
+        if (visible) {
+            controlsJob = viewModelScope.launch {
+                kotlinx.coroutines.delay(3000)
+                state.handleEvent(VideoEvent.ShowControls(false))
+            }
         }
+        return currentState.copy(showControls = visible)
     }
 
     private fun onLoad(currentState: VideoState, url: String): VideoState {
         savedStateHandle.remove<String>("url")
-        
+
+        // Ensure URL starts with http:// or https://
+        val videoUrl = when {
+            url.startsWith("http://") || url.startsWith("https://") -> url
+            url.startsWith("//") -> "https:$url"
+            url.startsWith("/") -> {
+                // If it starts with a single slash, we need to determine if it's a full path or relative
+                if (url.contains("://")) url.substring(1) else "https://$url"
+            }
+
+            else -> "https://$url"
+        }.trim()
+
         viewModelScope.launch(Dispatchers.IO) {
             analyticsRepository.trackPageView("video-player/$url")
             analyticsRepository.trackEvent(
                 eventName = "video_started",
-                eventData = mapOf("url" to url)
+                eventData = mapOf(
+                    "url" to videoUrl,
+                    "original_url" to url
+                )
             )
         }
-        
-        return currentState.copy(url = url)
+
+        return currentState.copy(url = videoUrl)
     }
 
-    private fun onPosition(currentState: VideoState, position: Long): VideoState {
-        // Only track position changes every 15 seconds to avoid excessive events
-        if (position - lastTrackedPosition >= 15_000 && currentState.duration > 0) {
-            viewModelScope.launch(Dispatchers.IO) {
-                val progressPercent = (position.toFloat() / currentState.duration * 100).toInt()
-
-                analyticsRepository.trackEvent(
-                    eventName = "video_progress",
-                    eventData = mapOf(
-                        "url" to currentState.url,
-                        "position_seconds" to (position / 1000).toString(),
-                        "duration_seconds" to (currentState.duration / 1000).toString(),
-                        "position_percent" to "$progressPercent%"
-                    )
-                )
-            }
-            lastTrackedPosition = position
+    private fun onUpdateScale(currentState: VideoState, newScale: Float): VideoState {
+        val clampedScale = newScale.coerceIn(MIN_SCALE, MAX_SCALE)
+        return currentState.copy(scale = clampedScale)
+    }
+
+    private fun onUpdatePan(currentState: VideoState, x: Float, y: Float): VideoState {
+        // Only allow panning when zoomed in
+        return if (currentState.scale > MIN_SCALE) {
+            // Calculate maximum pan distance based on scale
+            val maxPan = (currentState.scale - 1f) * 1000f // Larger value for more movement range
+
+            // Clamp the pan values
+            val newPanX = x.coerceIn(-maxPan, maxPan)
+            val newPanY = y.coerceIn(-maxPan, maxPan)
+
+            currentState.copy(panX = newPanX, panY = newPanY)
+        } else {
+            // Reset pan when not zoomed in
+            currentState.copy(panX = 0f, panY = 0f)
         }
-        return currentState.copy(position = position)
     }
 
-    private fun onDuration(currentState: VideoState, duration: Long): VideoState {
-        if (duration > 0) {
-            viewModelScope.launch(Dispatchers.IO) {
-                analyticsRepository.trackEvent(
-                    eventName = "video_duration_set",
-                    eventData = mapOf(
-                        "url" to currentState.url,
-                        "duration_seconds" to (duration / 1000).toString()
-                    )
-                )
-            }
+    private fun onZoomIn(currentState: VideoState): VideoState {
+        val newScale = (currentState.scale + ZOOM_STEP).coerceAtMost(MAX_SCALE)
+        return currentState.copy(scale = newScale)
+    }
+
+    private fun onZoomOut(currentState: VideoState): VideoState {
+        val newScale = (currentState.scale - ZOOM_STEP).coerceAtLeast(MIN_SCALE)
+        return currentState.copy(scale = newScale)
+    }
+
+    private fun onExit(currentState: VideoState): VideoState {
+        viewModelScope.launch {
+            actionChannel.send(VideoAction.NavigateBack)
         }
-        return currentState.copy(duration = duration)
+        return currentState.copy(url = "")
     }
 }

+ 192 - 18
app/src/main/java/com/codeskraps/sbrowser/feature/video/components/VideoScreen.kt

@@ -1,56 +1,230 @@
 package com.codeskraps.sbrowser.feature.video.components
 
+import android.util.Log
+import android.widget.FrameLayout
+import androidx.activity.compose.BackHandler
+import androidx.annotation.OptIn
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.gestures.rememberTransformableState
+import androidx.compose.foundation.gestures.transformable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
-import androidx.lifecycle.compose.LifecycleResumeEffect
 import androidx.media3.common.MediaItem
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.datasource.DefaultDataSource
 import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.exoplayer.source.ProgressiveMediaSource
 import androidx.media3.ui.PlayerView
+import com.codeskraps.sbrowser.R
 import com.codeskraps.sbrowser.feature.video.mvi.VideoEvent
 import com.codeskraps.sbrowser.feature.video.mvi.VideoState
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.geometry.Offset
 
+private const val TAG = "VideoScreen"
+private const val MIN_SCALE = 1f
+private const val MAX_SCALE = 4f
+
+@OptIn(UnstableApi::class)
 @Composable
 fun VideoScreen(
     state: VideoState,
     handleEvent: (VideoEvent) -> Unit
 ) {
+    BackHandler {
+        handleEvent(VideoEvent.Exit)
+    }
+
     if (state.url.isNotBlank()) {
         val context = LocalContext.current
         val exoPlayer = remember {
             ExoPlayer.Builder(context).build().apply {
-                setMediaItem(MediaItem.fromUri(state.url))
-                playWhenReady = true
+                val dataSourceFactory = DefaultDataSource.Factory(context)
+                val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
+                    .createMediaSource(MediaItem.fromUri(state.url))
+
+                Log.d(TAG, "Loading video URL: ${state.url}")
+                
+                setMediaSource(mediaSource)
+                playWhenReady = true  // Start paused initially
                 prepare()
-            }
-        }
 
-        LifecycleResumeEffect(exoPlayer) {
-            onPauseOrDispose {
-                handleEvent(VideoEvent.Position(exoPlayer.currentPosition))
+                // Add position listener
+                addListener(object : androidx.media3.common.Player.Listener {
+                    override fun onPositionDiscontinuity(
+                        oldPosition: androidx.media3.common.Player.PositionInfo,
+                        newPosition: androidx.media3.common.Player.PositionInfo,
+                        reason: Int
+                    ) {
+                        super.onPositionDiscontinuity(oldPosition, newPosition, reason)
+                        handleEvent(VideoEvent.Position(currentPosition))
+                    }
+                })
             }
         }
 
         DisposableEffect(Unit) {
             onDispose {
+                // Save position before disposing
+                handleEvent(VideoEvent.Position(exoPlayer.currentPosition))
                 exoPlayer.release()
             }
         }
 
-        AndroidView(
-            factory = { ctx ->
-                PlayerView(ctx).apply {
-                    player = exoPlayer
+        // Restore position when it changes in state
+        DisposableEffect(state.position) {
+            exoPlayer.seekTo(state.position)
+            onDispose { }
+        }
+
+        Box(
+            modifier = Modifier
+                .fillMaxSize()
+                .background(androidx.compose.ui.graphics.Color.Black)
+                // Handle show controls
+                .pointerInput(Unit) {
+                    awaitPointerEventScope {
+                        while (true) {
+                            awaitPointerEvent()
+                            handleEvent(VideoEvent.ShowControls(true))
+                        }
+                    }
                 }
-            },
-            update = { _ ->
-                exoPlayer.seekTo(state.position)
-            },
-            modifier = Modifier.fillMaxSize()
-        )
+        ) {
+            val transformableState = rememberTransformableState { zoomChange, offsetChange, _ ->
+                // Update scale
+                val newScale = (state.scale * zoomChange).coerceIn(MIN_SCALE, MAX_SCALE)
+                if (newScale != state.scale) {
+                    handleEvent(VideoEvent.UpdateScale(newScale))
+                }
+                
+                // Update pan if zoomed in
+                if (state.scale > MIN_SCALE) {
+                    handleEvent(VideoEvent.UpdatePan(
+                        state.panX + offsetChange.x,
+                        state.panY + offsetChange.y
+                    ))
+                }
+            }
+
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .graphicsLayer(
+                        scaleX = state.scale,
+                        scaleY = state.scale,
+                        translationX = state.panX,
+                        translationY = state.panY
+                    )
+                    .transformable(state = transformableState)
+            ) {
+                AndroidView(
+                    factory = { ctx ->
+                        FrameLayout(ctx).apply {
+                            layoutParams = FrameLayout.LayoutParams(
+                                FrameLayout.LayoutParams.MATCH_PARENT,
+                                FrameLayout.LayoutParams.MATCH_PARENT
+                            )
+
+                            val playerView = PlayerView(ctx).apply {
+                                player = exoPlayer
+                                useController = true
+                                controllerShowTimeoutMs = 3000
+                                controllerHideOnTouch = true
+                                resizeMode = androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
+                                
+                                layoutParams = FrameLayout.LayoutParams(
+                                    FrameLayout.LayoutParams.MATCH_PARENT,
+                                    FrameLayout.LayoutParams.MATCH_PARENT
+                                ).apply {
+                                    gravity = android.view.Gravity.CENTER
+                                }
+                            }
+                            addView(playerView)
+                        }
+                    },
+                    update = { container ->
+                        val playerView = container.getChildAt(0) as PlayerView
+                        playerView.player = exoPlayer
+                    },
+                    modifier = Modifier.fillMaxSize()
+                )
+            }
+
+            // Zoom controls overlay with animation
+            AnimatedVisibility(
+                visible = state.showControls,
+                enter = androidx.compose.animation.fadeIn(),
+                exit = androidx.compose.animation.fadeOut(),
+                modifier = Modifier.fillMaxSize()
+            ) {
+                Box(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .padding(16.dp)
+                ) {
+                    Column(
+                        modifier = Modifier
+                            .align(Alignment.BottomEnd)
+                            .padding(bottom = 64.dp)
+                    ) {
+                        IconButton(
+                            onClick = { handleEvent(VideoEvent.ZoomIn) },
+                            modifier = Modifier
+                                .size(40.dp)
+                                .background(
+                                    MaterialTheme.colorScheme.surface.copy(alpha = 0.7f),
+                                    CircleShape
+                                )
+                        ) {
+                            Icon(
+                                painter = painterResource(R.drawable.add),
+                                contentDescription = "Zoom In",
+                                tint = MaterialTheme.colorScheme.onSurface
+                            )
+                        }
+                        
+                        Spacer(modifier = Modifier.height(8.dp))
+                        
+                        IconButton(
+                            onClick = { handleEvent(VideoEvent.ZoomOut) },
+                            modifier = Modifier
+                                .size(40.dp)
+                                .background(
+                                    MaterialTheme.colorScheme.surface.copy(alpha = 0.7f),
+                                    CircleShape
+                                )
+                        ) {
+                            Icon(
+                                painter = painterResource(R.drawable.remove),
+                                contentDescription = "Zoom Out",
+                                tint = MaterialTheme.colorScheme.onSurface
+                            )
+                        }
+                    }
+                }
+            }
+        }
     }
 }

+ 3 - 1
app/src/main/java/com/codeskraps/sbrowser/feature/video/mvi/VideoAction.kt

@@ -1,3 +1,5 @@
 package com.codeskraps.sbrowser.feature.video.mvi
 
-sealed interface VideoAction
+sealed interface VideoAction {
+    data object NavigateBack : VideoAction
+}

+ 6 - 1
app/src/main/java/com/codeskraps/sbrowser/feature/video/mvi/VideoEvent.kt

@@ -2,6 +2,11 @@ package com.codeskraps.sbrowser.feature.video.mvi
 
 sealed interface VideoEvent {
     data class Load(val url: String) : VideoEvent
+    data class UpdateScale(val scale: Float) : VideoEvent
+    data class UpdatePan(val x: Float, val y: Float) : VideoEvent
+    data class ShowControls(val visible: Boolean) : VideoEvent
     data class Position(val position: Long) : VideoEvent
-    data class Duration(val duration: Long) : VideoEvent
+    data object ZoomIn : VideoEvent
+    data object ZoomOut : VideoEvent
+    data object Exit : VideoEvent
 }

+ 11 - 4
app/src/main/java/com/codeskraps/sbrowser/feature/video/mvi/VideoState.kt

@@ -1,15 +1,22 @@
 package com.codeskraps.sbrowser.feature.video.mvi
 
+import org.jsoup.nodes.Range.Position
+
 data class VideoState(
     val url: String,
-    val position: Long,
-    val duration: Long
+    val scale: Float = 1f,
+    val panX: Float = 0f,
+    val panY: Float = 0f,
+    val position: Long = 0L,
+    val showControls: Boolean = true
 ) {
     companion object {
         val initial = VideoState(
             url = "",
-            position = 0,
-            duration = 0
+            scale = 1f,
+            panX = 0f,
+            panY = 0f,
+            showControls = true
         )
     }
 }

+ 8 - 4
app/src/main/java/com/codeskraps/sbrowser/feature/webview/MediaWebViewModel.kt

@@ -106,11 +106,12 @@ class MediaWebViewModel @Inject constructor(
     }
 
     private fun onPermission(currentState: MediaWebViewState): MediaWebViewState {
+        val url = mediaWebView.url
         viewModelScope.launch(Dispatchers.IO) {
             analyticsRepository.trackEvent(
                 eventName = "permission_request",
                 eventData = mapOf(
-                    "url" to (mediaWebView.url ?: ""),
+                    "url" to (url ?: ""),
                     "type" to "notification"
                 )
             )
@@ -120,10 +121,11 @@ class MediaWebViewModel @Inject constructor(
     }
 
     private fun onDownloadService(currentState: MediaWebViewState): MediaWebViewState {
+        val url = mediaWebView.url
         viewModelScope.launch(Dispatchers.IO) {
             analyticsRepository.trackEvent(
                 eventName = "download_initiated",
-                eventData = mapOf("url" to (mediaWebView.url ?: ""))
+                eventData = mapOf("url" to (url ?: ""))
             )
             actionChannel.send(MediaWebViewAction.DownloadService)
         }
@@ -131,10 +133,11 @@ class MediaWebViewModel @Inject constructor(
     }
 
     private fun onActionView(currentState: MediaWebViewState): MediaWebViewState {
+        val url = mediaWebView.url
         viewModelScope.launch(Dispatchers.IO) {
             analyticsRepository.trackEvent(
                 eventName = "action_view",
-                eventData = mapOf("url" to (mediaWebView.url ?: ""))
+                eventData = mapOf("url" to (url ?: ""))
             )
             actionChannel.send(MediaWebViewAction.ActionView)
         }
@@ -142,11 +145,12 @@ class MediaWebViewModel @Inject constructor(
     }
 
     private fun onVideoPlayer(currentState: MediaWebViewState, url: String): MediaWebViewState {
+        val currentUrl = mediaWebView.url
         viewModelScope.launch(Dispatchers.IO) {
             analyticsRepository.trackEvent(
                 eventName = "video_player_launch",
                 eventData = mapOf(
-                    "current_url" to (mediaWebView.url ?: ""),
+                    "current_url" to (currentUrl ?: ""),
                     "video_url" to url
                 )
             )

+ 13 - 67
app/src/main/java/com/codeskraps/sbrowser/feature/webview/media/HandleVideo.kt

@@ -1,48 +1,25 @@
 package com.codeskraps.sbrowser.feature.webview.media
 
+import android.net.Uri
 import android.text.TextUtils
 import android.util.Log
 import org.jsoup.Jsoup
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import org.jsoup.select.Elements
+import java.io.IOException
 import java.util.regex.Matcher
 import java.util.regex.Pattern
 
+
 class HandleVideo {
     companion object {
         private val TAG = HandleVideo::class.java.simpleName
-        private val VIDEO_EXTENSIONS = arrayOf(".mp4", ".m3u8", ".webm", ".mov")
     }
 
     operator fun invoke(url: String, result: (String) -> Unit) {
         runCatching {
             val doc: Document = Jsoup.connect(url).get()
-            
-            // 1. Check for HTML5 video elements
-            doc.select("video source").forEach { source ->
-                source.attr("src").let { videoUrl ->
-                    if (isVideoUrl(videoUrl)) result(videoUrl)
-                }
-            }
-            
-            // 2. Check for video elements directly
-            doc.select("video").forEach { video ->
-                video.attr("src").let { videoUrl ->
-                    if (isVideoUrl(videoUrl)) result(videoUrl)
-                }
-            }
-
-            // 3. Check for iframes that might contain videos
-            doc.select("iframe").forEach { iframe ->
-                iframe.attr("src").let { iframeUrl ->
-                    if (iframeUrl.contains("player") || iframeUrl.contains("embed")) {
-                        result(iframeUrl)
-                    }
-                }
-            }
-
-            // Original player div check
             var metalinks: Elements = doc.select("div[id=player]")
 
             if (!metalinks.isEmpty()) {
@@ -78,7 +55,6 @@ class HandleVideo {
                 }
             }
 
-            // Check for play button
             metalinks = doc.select("a[id=play]")
 
             if (!metalinks.isEmpty()) {
@@ -88,14 +64,6 @@ class HandleVideo {
                 }
             }
 
-            // Check all links for video extensions
-            doc.select("a").forEach { link ->
-                link.attr("href").let { href ->
-                    if (isVideoUrl(href)) result(href)
-                }
-            }
-
-            // Script check for setVideoUrlHigh
             Log.e(TAG, "script")
             metalinks = doc.select("script")
 
@@ -106,30 +74,16 @@ class HandleVideo {
                     val html: String = iterator.next().html()
                     Log.w(TAG, "new:$html")
 
-                    if (!TextUtils.isEmpty(html)) {
-                        // Check for setVideoUrlHigh
-                        if (html.contains("setVideoUrlHigh")) {
-                            val lines =
-                                html.split("\\r\\n|\\n|\\r".toRegex()).dropLastWhile { it.isEmpty() }
-                                    .toTypedArray()
-
-                            for (line in lines) {
-                                if (line.contains("setVideoUrlHigh")) {
-                                    Log.w(TAG, "setVideoUrlHigh:$line")
-                                    result(line.substring(line.indexOf('(') + 2, line.indexOf(')') - 1))
-                                }
-                            }
-                        }
-                        
-                        // Look for any URLs that might be video files
-                        val urlPattern = Pattern.compile(
-                            "https?://[^\\s<>\"']*?(?:${VIDEO_EXTENSIONS.joinToString("|")})",
-                            Pattern.CASE_INSENSITIVE
-                        )
-                        val matcher = urlPattern.matcher(html)
-                        while (matcher.find()) {
-                            matcher.group().let { videoUrl ->
-                                if (isVideoUrl(videoUrl)) result(videoUrl)
+                    if (!TextUtils.isEmpty(html) && html.contains("setVideoUrlHigh")) {
+                        val lines =
+                            html.split("\\r\\n|\\n|\\r".toRegex()).dropLastWhile { it.isEmpty() }
+                                .toTypedArray()
+
+                        for (line in lines) {
+
+                            if (line.contains("setVideoUrlHigh")) {
+                                Log.w(TAG, "setVideoUrlHigh:$line")
+                                result(line.substring(line.indexOf('(') + 2, line.indexOf(')') - 1))
                             }
                         }
                     }
@@ -139,12 +93,4 @@ class HandleVideo {
             Log.e(TAG, "Handled - HandleVideo:$e", e)
         }
     }
-
-    private fun isVideoUrl(url: String): Boolean {
-        val lowercaseUrl = url.lowercase()
-        return VIDEO_EXTENSIONS.any { ext -> lowercaseUrl.endsWith(ext) } ||
-                lowercaseUrl.contains("video") ||
-                lowercaseUrl.contains("/media/") ||
-                lowercaseUrl.contains("stream")
-    }
 }

+ 30 - 30
app/src/main/java/com/codeskraps/sbrowser/feature/webview/media/MediaWebView.kt

@@ -25,32 +25,28 @@ import androidx.lifecycle.LifecycleOwner
 
 @SuppressLint("SetJavaScriptEnabled")
 class MediaWebView @Inject constructor(
-    private val application: Application,
+    application: Application,
     private val mediaWebViewPreferences: MediaWebViewPreferences
 ) : DefaultLifecycleObserver {
 
-    private var _webView: InternalWebView? = null
-    private val webView: InternalWebView
-        get() = _webView ?: InternalWebView(application).also {
-            it.setupWebView()
-            _webView = it
-        }
+    private val webView: InternalWebView = InternalWebView(application).apply {
+        setupWebView()
+    }
 
     private var initLoad: Boolean = false
     val attachView: View
         get() = webView
     val url: String?
-        get() = _webView?.url
+        get() = webView.url
     val title: String?
-        get() = _webView?.title
-    val settings: WebSettings?
-        get() = _webView?.settings
+        get() = webView.title
+    val settings: WebSettings
+        get() = webView.settings
     val cookieManager: CookieManager
         get() = CookieManager.getInstance()
 
     override fun onDestroy(owner: LifecycleOwner) {
-        _webView?.destroy()
-        _webView = null
+        webView.destroy()
         super.onDestroy(owner)
     }
 
@@ -62,18 +58,18 @@ class MediaWebView @Inject constructor(
     }
 
     fun setUrlListener(urlListener: ((String) -> Unit)?) {
-        (_webView?.webViewClient as? MediaWebViewClient)?.urlListener = urlListener
+        (webView.webViewClient as? MediaWebViewClient)?.urlListener = urlListener
     }
 
     fun setHandleListener(handleEvent: ((MediaWebViewEvent) -> Unit)?) {
-        (_webView?.webChromeClient as? MediaWebChromeClient)?.handleEvent = handleEvent
-        (_webView?.webViewClient as? MediaWebViewClient)?.handleEvent = handleEvent
-        _webView?.javascriptInterface?.handleEvent = handleEvent
+        (webView.webChromeClient as? MediaWebChromeClient)?.handleEvent = handleEvent
+        (webView.webViewClient as? MediaWebViewClient)?.handleEvent = handleEvent
+        webView.javascriptInterface.handleEvent = handleEvent
     }
 
     fun detachView() {
-        _webView?.parent?.let {
-            (it as ViewGroup).removeView(_webView)
+        webView.parent?.let {
+            (it as ViewGroup).removeView(webView)
         }
     }
 
@@ -82,26 +78,26 @@ class MediaWebView @Inject constructor(
     }
 
     fun stopLoading() {
-        _webView?.stopLoading()
+        webView.stopLoading()
     }
 
     fun reload() {
-        _webView?.reload()
+        webView.reload()
     }
 
-    fun canGoBack(): Boolean = _webView?.canGoBack() ?: false
-    fun canGoForward(): Boolean = _webView?.canGoForward() ?: false
+    fun canGoBack(): Boolean = webView.canGoBack()
+    fun canGoForward(): Boolean = webView.canGoForward()
 
     fun goBack() {
-        if (canGoBack()) _webView?.goBack()
+        if (canGoBack()) webView.goBack()
     }
 
     fun goForward() {
-        if (canGoForward()) _webView?.goForward()
+        if (canGoForward()) webView.goForward()
     }
 
     fun capturePicture(): ByteArray? {
-        return _webView?.let { webView ->
+        return webView.let { webView ->
             runCatching {
                 createBitmap(300, 300).let { bitmap ->
                     Canvas(bitmap).also { canvas ->
@@ -120,7 +116,11 @@ class MediaWebView @Inject constructor(
     inner class InternalWebView : WebView {
         constructor(context: Context?) : super(context!!)
         constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs)
-        constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context!!, attrs, defStyleAttr)
+        constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
+            context!!,
+            attrs,
+            defStyleAttr
+        )
 
         val javascriptInterface = WebScriptInterface()
 
@@ -133,7 +133,7 @@ class MediaWebView @Inject constructor(
         fun setupWebView() {
             webViewClient = MediaWebViewClient()
             webChromeClient = MediaWebChromeClient()
-            
+
             // Enable hardware acceleration and scrolling
             setLayerType(View.LAYER_TYPE_HARDWARE, null)
             ViewCompat.setNestedScrollingEnabled(this, true)
@@ -187,14 +187,14 @@ class MediaWebView @Inject constructor(
             setEnableSmoothTransition(true)
             blockNetworkImage = false
             blockNetworkLoads = false
-            
+
             // Additional rendering settings
             layoutAlgorithm = WebSettings.LayoutAlgorithm.NORMAL
             standardFontFamily = "sans-serif"
             defaultTextEncodingName = "UTF-8"
             defaultFontSize = 16
             minimumFontSize = 8
-            
+
             // Modern web features
             setupModernWebFeatures()
 

+ 2 - 1
app/src/main/java/com/codeskraps/sbrowser/feature/webview/media/MediaWebViewClient.kt

@@ -6,6 +6,7 @@ import android.net.http.SslCertificate
 import android.net.http.SslError
 import android.util.Log
 import android.webkit.MimeTypeMap
+import android.webkit.RenderProcessGoneDetail
 import android.webkit.SslErrorHandler
 import android.webkit.WebResourceError
 import android.webkit.WebResourceRequest
@@ -118,7 +119,7 @@ class MediaWebViewClient : WebViewClient() {
         request: WebResourceRequest?
     ): Boolean {
         if (view == null || request == null) return false
-        
+
         val url = request.url.toString()
         if (url.isBlank()) return false
 

+ 5 - 0
app/src/main/res/drawable/add.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+    
+</vector>

+ 5 - 0
app/src/main/res/drawable/fast_forward.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z"/>
+    
+</vector>

+ 5 - 0
app/src/main/res/drawable/fast_rewind.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z"/>
+    
+</vector>

+ 5 - 0
app/src/main/res/drawable/pause.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
+    
+</vector>

+ 5 - 0
app/src/main/res/drawable/play_arrow.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>
+    
+</vector>

+ 5 - 0
app/src/main/res/drawable/remove.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M19,13H5v-2h14v2z"/>
+    
+</vector>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon>

BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp


BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp


BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp


+ 4 - 0
app/src/main/res/values/ic_launcher_background.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="ic_launcher_background">#102840</color>
+</resources>