|
|
@@ -0,0 +1,156 @@
|
|
|
+package com.joe.camera.webcam.ui
|
|
|
+
|
|
|
+import android.annotation.SuppressLint
|
|
|
+import android.graphics.Color as AndroidColor
|
|
|
+import android.webkit.WebChromeClient
|
|
|
+import android.webkit.WebView
|
|
|
+import android.webkit.WebViewClient
|
|
|
+import android.app.Activity
|
|
|
+import android.content.Context
|
|
|
+import android.content.ContextWrapper
|
|
|
+import android.view.WindowManager
|
|
|
+import androidx.activity.compose.BackHandler
|
|
|
+import androidx.camera.core.CameraSelector
|
|
|
+import androidx.camera.view.PreviewView
|
|
|
+import androidx.compose.foundation.background
|
|
|
+import androidx.compose.foundation.gestures.detectDragGestures
|
|
|
+import androidx.compose.foundation.gestures.detectTapGestures
|
|
|
+import androidx.compose.foundation.layout.*
|
|
|
+import androidx.compose.foundation.shape.CircleShape
|
|
|
+import androidx.compose.foundation.shape.RoundedCornerShape
|
|
|
+import androidx.compose.material.icons.Icons
|
|
|
+import androidx.compose.material.icons.filled.*
|
|
|
+import androidx.compose.material3.*
|
|
|
+import androidx.compose.runtime.*
|
|
|
+import androidx.compose.ui.Modifier
|
|
|
+import androidx.compose.ui.draw.alpha
|
|
|
+import androidx.compose.ui.draw.clip
|
|
|
+import androidx.compose.ui.graphics.Color
|
|
|
+import androidx.compose.ui.input.pointer.pointerInput
|
|
|
+import androidx.compose.ui.platform.LocalContext
|
|
|
+import androidx.compose.ui.platform.LocalDensity
|
|
|
+import androidx.compose.ui.unit.IntOffset
|
|
|
+import androidx.compose.ui.unit.dp
|
|
|
+import androidx.compose.ui.viewinterop.AndroidView
|
|
|
+import androidx.lifecycle.compose.LocalLifecycleOwner
|
|
|
+import androidx.core.view.WindowInsetsCompat
|
|
|
+import androidx.core.view.WindowInsetsControllerCompat
|
|
|
+import com.joe.camera.webcam.MainUiState
|
|
|
+import com.joe.camera.webcam.MainViewModel
|
|
|
+import com.joe.camera.webcam.model.CameraState
|
|
|
+import kotlin.math.roundToInt
|
|
|
+
|
|
|
+private const val DARK_CSS = """
|
|
|
+body,.kr-mobile-layout,.home-tab-bar-item{background:#212121!important}
|
|
|
+.logo-container,.item-wrapper:has(.home-ad-flow-information),.interact-area,.float-app-button-wrp,.float-home-button-wrp,.article-praise,.may-like-art-wrp{display:none!important}
|
|
|
+.show-login{padding-top:1rem!important}.home-tab-bar-wrap .home-tab-bar-item.active{color:#206cff!important}
|
|
|
+.home-fixed-area-item-title,.item-title,.body-title,#body-content,#body-content blockquote{color:white!important}
|
|
|
+.home-tab-bar-item,.item-bar,.author-name,.time{color:grey!important}
|
|
|
+"""
|
|
|
+
|
|
|
+@SuppressLint("SetJavaScriptEnabled")
|
|
|
+@OptIn(ExperimentalMaterial3Api::class)
|
|
|
+@Composable
|
|
|
+fun HomeScreen(viewModel: MainViewModel, state: MainUiState, onMedia: () -> Unit) {
|
|
|
+ val lifecycleOwner = LocalLifecycleOwner.current
|
|
|
+ var webView by remember { mutableStateOf<WebView?>(null) }
|
|
|
+ var progress by remember { mutableFloatStateOf(0f) }
|
|
|
+ var menuExpanded by remember { mutableStateOf(false) }
|
|
|
+ var settingsOpen by remember { mutableStateOf(false) }
|
|
|
+ var previewOpacity by remember { mutableFloatStateOf(0.02f) }
|
|
|
+ var previewWidth by remember { mutableFloatStateOf(80f) }
|
|
|
+ var previewX by remember { mutableFloatStateOf(100f) }
|
|
|
+ var previewY by remember { mutableFloatStateOf(200f) }
|
|
|
+ var buttonX by remember { mutableFloatStateOf(300f) }
|
|
|
+ var buttonY by remember { mutableFloatStateOf(580f) }
|
|
|
+ val density = LocalDensity.current
|
|
|
+ val activity = LocalContext.current.findActivity()
|
|
|
+
|
|
|
+ LaunchedEffect(state.blackScreen) {
|
|
|
+ activity?.window?.let { window ->
|
|
|
+ window.attributes = window.attributes.apply { screenBrightness = if (state.blackScreen) 0f else WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE }
|
|
|
+ WindowInsetsControllerCompat(window, window.decorView).apply {
|
|
|
+ if (state.blackScreen) hide(WindowInsetsCompat.Type.systemBars()) else show(WindowInsetsCompat.Type.systemBars())
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ BackHandler {
|
|
|
+ val view = webView
|
|
|
+ if (view?.canGoBack() == true) view.goBack() else view?.reload()
|
|
|
+ }
|
|
|
+
|
|
|
+ BoxWithConstraints(Modifier.fillMaxSize().background(Color(0xFF17171A))) {
|
|
|
+ val maxWidthPx = with(density) { maxWidth.toPx() }
|
|
|
+ val maxHeightPx = with(density) { maxHeight.toPx() }
|
|
|
+ AndroidView(
|
|
|
+ modifier = Modifier.fillMaxSize(),
|
|
|
+ factory = { context ->
|
|
|
+ WebView(context).apply {
|
|
|
+ setBackgroundColor(AndroidColor.rgb(33, 33, 33))
|
|
|
+ settings.javaScriptEnabled = true
|
|
|
+ settings.domStorageEnabled = true
|
|
|
+ webChromeClient = object : WebChromeClient() { override fun onProgressChanged(view: WebView?, newProgress: Int) { progress = newProgress / 100f } }
|
|
|
+ webViewClient = object : WebViewClient() {
|
|
|
+ override fun onPageFinished(view: WebView, url: String?) {
|
|
|
+ val escaped = DARK_CSS.replace("`", "\\`")
|
|
|
+ view.evaluateJavascript("document.getElementById('webcam-style')?.remove();const s=document.createElement('style');s.id='webcam-style';s.textContent=`$escaped`;document.head.appendChild(s);", null)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ loadUrl("https://www.36kr.com/")
|
|
|
+ webView = this
|
|
|
+ }
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if (progress in 0.001f..0.999f) LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth().height(2.dp))
|
|
|
+
|
|
|
+ if (!state.remoteMode) {
|
|
|
+ AndroidView(
|
|
|
+ modifier = Modifier
|
|
|
+ .offset { IntOffset(previewX.roundToInt(), previewY.roundToInt()) }
|
|
|
+ .width(previewWidth.dp)
|
|
|
+ .aspectRatio(9f / 16f)
|
|
|
+ .alpha(previewOpacity)
|
|
|
+ .clip(RoundedCornerShape((previewWidth / 9f).dp))
|
|
|
+ .pointerInput(Unit) { detectDragGestures { change, drag -> change.consume(); previewX = (previewX + drag.x).coerceIn(0f, maxWidthPx - 20); previewY = (previewY + drag.y).coerceIn(0f, maxHeightPx - 20) } },
|
|
|
+ factory = { context -> PreviewView(context).also { viewModel.bindCamera(lifecycleOwner, it) } },
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ Box(Modifier.offset { IntOffset(buttonX.roundToInt(), buttonY.roundToInt()) }) {
|
|
|
+ Surface(
|
|
|
+ modifier = Modifier.size(42.dp)
|
|
|
+ .pointerInput(Unit) { detectTapGestures(onTap = { viewModel.toggleRecording() }, onDoubleTap = { menuExpanded = !menuExpanded }, onLongPress = { settingsOpen = true }) }
|
|
|
+ .pointerInput(Unit) { detectDragGestures { change, drag -> change.consume(); buttonX = (buttonX + drag.x).coerceIn(0f, maxWidthPx - 42.dp.toPx()); buttonY = (buttonY + drag.y).coerceIn(0f, maxHeightPx - 42.dp.toPx()) } },
|
|
|
+ shape = CircleShape,
|
|
|
+ color = Color.White,
|
|
|
+ shadowElevation = 8.dp,
|
|
|
+ ) { Icon(Icons.Default.Menu, "控制菜单", modifier = Modifier.padding(9.dp), tint = if (state.cameraState == CameraState.RECORDING || state.cameraState == CameraState.WORKING) Color(0xFF03A9F4) else Color.Black) }
|
|
|
+ DropdownMenu(expanded = menuExpanded, onDismissRequest = { menuExpanded = false }) {
|
|
|
+ DropdownMenuItem(text = { Text("录像") }, leadingIcon = { Icon(Icons.Default.Videocam, null) }, onClick = { menuExpanded = false; viewModel.toggleRecording() })
|
|
|
+ DropdownMenuItem(text = { Text("拍照") }, leadingIcon = { Icon(Icons.Default.PhotoCamera, null) }, enabled = !state.remoteMode, onClick = { menuExpanded = false; viewModel.takePicture() })
|
|
|
+ DropdownMenuItem(text = { Text(if (state.previewPaused) "恢复预览" else "暂停预览") }, leadingIcon = { Icon(Icons.Default.Preview, null, tint = if (state.previewPaused) Color.Gray else Color(0xFF4CAF50)) }, enabled = !state.remoteMode, onClick = { menuExpanded = false; viewModel.togglePreview() })
|
|
|
+ DropdownMenuItem(text = { Text(if (state.remoteMode) "切换为本机" else "切换为遥控") }, leadingIcon = { Icon(Icons.Default.SettingsRemote, null, tint = if (state.remoteMode) Color(0xFF4CAF50) else Color.Gray) }, onClick = { menuExpanded = false; viewModel.toggleRemoteMode() })
|
|
|
+ DropdownMenuItem(text = { Text("媒体库") }, leadingIcon = { Icon(Icons.Default.Image, null) }, onClick = { menuExpanded = false; viewModel.loadMedia(); onMedia() })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (state.blackScreen) Box(Modifier.fillMaxSize().background(Color.Black).pointerInput(Unit) { detectTapGestures(onLongPress = { viewModel.toggleBlackScreen() }) })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (settingsOpen) ModalBottomSheet(onDismissRequest = { settingsOpen = false }) {
|
|
|
+ Column(Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 8.dp)) {
|
|
|
+ Text("预览透明度 ${(previewOpacity * 100).roundToInt()}%")
|
|
|
+ Slider(value = previewOpacity, onValueChange = { previewOpacity = it })
|
|
|
+ Text("预览宽度 ${previewWidth.roundToInt()} dp")
|
|
|
+ Slider(value = previewWidth, onValueChange = { previewWidth = it }, valueRange = 15f..360f)
|
|
|
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
|
|
+ TextButton(onClick = { viewModel.switchLens(false) }) { Text("BACK") }
|
|
|
+ if (viewModel.hasFrontCamera()) TextButton(onClick = { viewModel.switchLens(true) }) { Text("FRONT") }
|
|
|
+ }
|
|
|
+ Spacer(Modifier.height(20.dp))
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private tailrec fun Context.findActivity(): Activity? = when (this) { is Activity -> this; is ContextWrapper -> baseContext.findActivity(); else -> null }
|