cc12458 10 місяців тому
батько
коміт
b59cffd8fc

+ 1 - 0
app/build.gradle.kts

@@ -134,6 +134,7 @@ dependencies {
   implementation(libs.androidx.ui.graphics)
   implementation(libs.androidx.ui.tooling.preview)
   implementation(libs.androidx.material3)
+  implementation(libs.androidx.material.icons.extended)
   testImplementation(libs.junit)
   androidTestImplementation(libs.androidx.junit)
   androidTestImplementation(libs.androidx.espresso.core)

+ 4 - 3
app/src/main/java/com/hzliuzhi/applet/container/navigation/Host.kt

@@ -8,9 +8,9 @@ import androidx.compose.ui.platform.LocalContext
 import androidx.navigation.NavHostController
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.rememberNavController
-
 import com.hzliuzhi.applet.browser.navigation.browser
 import com.hzliuzhi.applet.container.R
+import com.hzliuzhi.applet.core.store.SettingStore
 
 @Composable
 fun Host(
@@ -32,7 +32,8 @@ fun rememberRoute(): Any {
   val context = LocalContext.current.applicationContext
   return remember {
     runCatching {
-      context.resources.getString(R.string.app_screen).toRoute()
-    }.getOrNull() ?: AppRoute.Home
+      val screen = SettingStore.getInstance(context).screen ?: context.resources.getString(R.string.app_screen)
+      screen.toRoute()
+    }.getOrNull() ?: AppRoute.Launcher
   }
 }

+ 21 - 0
app/src/main/java/com/hzliuzhi/applet/container/navigation/Route.kt

@@ -1,16 +1,22 @@
 package com.hzliuzhi.applet.container.navigation
 
+import android.widget.Toast
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
 import androidx.navigation.NavController
 import androidx.navigation.NavGraphBuilder
 import androidx.navigation.compose.composable
 import com.hzliuzhi.applet.container.ui.HomeScreen
+import com.hzliuzhi.applet.container.ui.LauncherScreen
 import kotlinx.serialization.Serializable
 
 sealed class AppRoute {
   @Serializable
   data object Home : AppRoute()
+
+  @Serializable
+  data object Launcher : AppRoute()
 }
 
 fun NavGraphBuilder.app(navController: NavController) {
@@ -19,4 +25,19 @@ fun NavGraphBuilder.app(navController: NavController) {
       modifier = Modifier.fillMaxSize()
     )
   }
+  composable<AppRoute.Launcher> { _ ->
+    var context = LocalContext.current
+    LauncherScreen(
+      modifier = Modifier.fillMaxSize(),
+      start = { it ->
+        runCatching {
+          "browser/kiosk?url=$it".also { url ->
+            url.toRoute()?.also { navController.navigate(it) } ?: throw IllegalArgumentException("无效的路由")
+          }
+        }.onFailure {
+          Toast.makeText(context, "跳转失败: ${it.message}", Toast.LENGTH_SHORT).show()
+        }.getOrNull()
+      }
+    )
+  }
 }

+ 234 - 0
app/src/main/java/com/hzliuzhi/applet/container/ui/LauncherScreen.kt

@@ -0,0 +1,234 @@
+package com.hzliuzhi.applet.container.ui
+
+import android.content.Intent
+import android.provider.Settings
+import android.util.Patterns
+import android.widget.Toast
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Send
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.QrCode
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+import androidx.core.net.toUri
+import com.hzliuzhi.applet.core.store.SettingStore
+
+
+@Composable
+fun LauncherScreen(
+  modifier: Modifier = Modifier,
+  start: (String) -> String?,
+  scan: () -> Unit = {},
+) {
+  val context = LocalContext.current
+  var text by remember { mutableStateOf("https://www.bing.com/") }
+  var storeEnabled by remember { mutableStateOf(true) }
+
+  Column(
+    modifier = modifier
+      .fillMaxSize()
+      .padding(24.dp),
+    verticalArrangement = Arrangement.Center
+  ) {
+    LauncherInputField(
+      value = text,
+      onValueChange = { text = it },
+      onDone = {
+        start(text)?.also {
+          SettingStore.getInstance(context).screen = if (storeEnabled) it else null
+        }
+      },
+      onScan = scan
+    )
+    Spacer(modifier = Modifier.height(24.dp))
+    StorageCard(
+      enabled = storeEnabled,
+      enabledChange = { storeEnabled = it }
+    )
+  }
+}
+
+@Composable
+fun LauncherInputField(
+  value: String,
+  onValueChange: (String) -> Unit,
+  onDone: () -> Unit,
+  onScan: () -> Unit,
+  modifier: Modifier = Modifier,
+) {
+  val context = LocalContext.current
+  val isUrl = remember(value) { Patterns.WEB_URL.matcher(value).matches() }
+
+  val keyboardController = LocalSoftwareKeyboardController.current
+
+  fun done() {
+    if (isUrl) {
+      keyboardController?.hide()
+      onDone()
+    } else Toast.makeText(context, "请输入合法的网址", Toast.LENGTH_SHORT).show()
+  }
+
+
+  OutlinedTextField(
+    value = value,
+    onValueChange = onValueChange,
+    label = { Text("请输入URL") },
+    singleLine = true,
+    isError = value.isNotBlank() && !isUrl,
+    modifier = modifier.fillMaxWidth(),
+    keyboardOptions = KeyboardOptions(
+      keyboardType = KeyboardType.Uri,
+      imeAction = ImeAction.Done
+    ),
+    keyboardActions = KeyboardActions(onDone = { done() }),
+    trailingIcon = {
+      if (value.isBlank()) {
+        Icon(
+          imageVector = Icons.Filled.QrCode,
+          contentDescription = "扫码",
+          modifier = Modifier.clickable {
+            onScan()
+            keyboardController?.hide()
+          }
+        )
+      } else {
+        Icon(
+          imageVector = Icons.AutoMirrored.Filled.Send,
+          contentDescription = "完成",
+          modifier = Modifier.clickable { done() }
+        )
+      }
+    }
+  )
+}
+
+@Composable
+fun StorageCard(
+  enabled: Boolean,
+  enabledChange: (Boolean) -> Unit,
+) {
+  val context = LocalContext.current
+
+  val appName = runCatching {
+    val info = context.applicationInfo
+    info.labelRes.takeIf { it != 0 }?.let { context.getString(it) } ?: info.nonLocalizedLabel?.toString()
+  }.getOrNull() ?: "本App"
+
+  Card(
+    modifier = Modifier.fillMaxWidth(),
+    shape = MaterialTheme.shapes.medium,
+  ) {
+    Column(
+      modifier = Modifier.padding(start = 12.dp, end = 12.dp)
+    ) {
+      // 头部:标题+开关
+      Row(
+        modifier = Modifier.fillMaxWidth(),
+        verticalAlignment = Alignment.CenterVertically
+      ) {
+        Text(
+          text = "存储数据",
+          style = MaterialTheme.typography.titleMedium,
+          modifier = Modifier.weight(1f)
+        )
+        Switch(
+          checked = enabled,
+          onCheckedChange = enabledChange
+        )
+      }
+      if (enabled) {
+        Spacer(modifier = Modifier.height(8.dp))
+        Row(verticalAlignment = Alignment.CenterVertically) {
+          Icon(
+            imageVector = Icons.Default.Info,
+            contentDescription = null,
+            tint = MaterialTheme.colorScheme.secondary
+          )
+          Spacer(modifier = Modifier.width(8.dp))
+          Text(
+            text = "如需清除缓存,请在",
+            style = MaterialTheme.typography.bodyMedium,
+            color = MaterialTheme.colorScheme.secondary
+          )
+          Text(
+            text = "设置中操作",
+            style = MaterialTheme.typography.bodyMedium.copy(color = Color(0xFF1976D2), textDecoration = TextDecoration.Underline),
+            modifier = Modifier
+              .clickable {
+                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+                  data = ("package:" + context.packageName).toUri()
+                }
+                context.startActivity(intent)
+              }
+          )
+        }
+        Spacer(modifier = Modifier.height(12.dp))
+        Text(
+          text = "清除缓存步骤:",
+          style = MaterialTheme.typography.bodyMedium,
+          color = MaterialTheme.colorScheme.primary
+        )
+        Spacer(modifier = Modifier.height(8.dp))
+        Column {
+          val steps = listOf(
+            "打开系统设置",
+            "找到应用管理",
+            "选择$appName",
+            "进入存储与缓存",
+            "点击清除缓存"
+          )
+          steps.forEachIndexed { idx, step ->
+            Row(verticalAlignment = Alignment.CenterVertically) {
+              Surface(
+                shape = MaterialTheme.shapes.small,
+                color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
+                modifier = Modifier.size(24.dp)
+              ) {
+                Box(contentAlignment = Alignment.Center) {
+                  Text("${idx + 1}", style = MaterialTheme.typography.labelMedium)
+                }
+              }
+              Spacer(modifier = Modifier.width(8.dp))
+              Text(step, style = MaterialTheme.typography.bodySmall)
+            }
+            if (idx < steps.lastIndex) Spacer(modifier = Modifier.height(6.dp))
+          }
+        }
+        Spacer(modifier = Modifier.height(8.dp))
+      }
+    }
+  }
+}

+ 32 - 0
core/src/main/java/com/hzliuzhi/applet/core/store/SettingStore.kt

@@ -0,0 +1,32 @@
+package com.hzliuzhi.applet.core.store
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.core.content.edit
+
+class SettingStore private constructor(private val sp: SharedPreferences) {
+
+
+  var screen: String?
+    get() = sp.getString(KEY_SCREEN, null)?.takeUnless { it.isBlank() }
+    set(value) {
+      sp.edit { value?.takeUnless { it.isBlank() }?.also { putString(KEY_SCREEN, it) } ?: remove(KEY_SCREEN) }
+    }
+
+  companion object {
+    private const val KEY_SCREEN = "screen"
+
+    @Volatile
+    private var instance: SettingStore? = null
+    fun getInstance(context: Context): SettingStore {
+      return instance ?: synchronized(this) {
+        instance ?: run {
+          val sp = context.applicationContext.getSharedPreferences("app_setting", Context.MODE_PRIVATE)
+          SettingStore(sp).also { instance = it }
+        }
+      }
+    }
+  }
+
+
+}

+ 1 - 0
gradle/libs.versions.toml

@@ -19,6 +19,7 @@ webkit = "1.14.0"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
 androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
 junit = { group = "junit", name = "junit", version.ref = "junit" }
 androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }