This commit is contained in:
2026-10-29 16:36:43 +08:00
parent 0cf352cabb
commit 2582f9ba3e
11 changed files with 428 additions and 49 deletions
@@ -5,15 +5,17 @@ import android.content.Intent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import net.bluemangoo.indpjsk.data.CaptureProfile
import net.bluemangoo.indpjsk.data.MixinProfiles
import net.bluemangoo.indpjsk.data.PredefinedProfiles
import net.bluemangoo.indpjsk.data.SettingsStore
import net.bluemangoo.indpjsk.state.CaptureStateHolder
import net.bluemangoo.indpjsk.state.CaptureStatus
import net.bluemangoo.indpjsk.upload.UploadStore
import net.bluemangoo.indpjsk.vpn.CaptureVpnService
import net.bluemangoo.indpjsk.vpn.VpnLog
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import java.io.File
@@ -27,24 +29,28 @@ data class CaptureUiState(
class CaptureViewModel : ViewModel() {
val uiState: StateFlow<CaptureUiState> = CaptureStateHolder.state
.map { s ->
CaptureUiState(
profiles = PredefinedProfiles.all,
selectedProfile = s.profile,
status = s.status,
lastCapturedFile = s.lastCapturedFile,
errorMessage = s.errorMessage
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = CaptureUiState(
profiles = PredefinedProfiles.all,
selectedProfile = PredefinedProfiles.all.firstOrNull()
)
private fun allProfiles(): List<CaptureProfile> = MixinProfiles.merged()
val uiState: StateFlow<CaptureUiState> = combine(
CaptureStateHolder.state,
UploadStore.revision,
) { s, _ ->
val profiles = allProfiles()
CaptureUiState(
profiles = profiles,
selectedProfile = profiles.firstOrNull { it.id == s.profile.id } ?: s.profile,
status = s.status,
lastCapturedFile = s.lastCapturedFile,
errorMessage = s.errorMessage
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = CaptureUiState(
profiles = allProfiles(),
selectedProfile = PredefinedProfiles.all.firstOrNull()
)
)
fun selectProfile(profile: CaptureProfile) {
CaptureStateHolder.selectProfile(profile)
@@ -28,6 +28,7 @@ import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import net.bluemangoo.indpjsk.R
import net.bluemangoo.indpjsk.data.MixinProfiles
import net.bluemangoo.indpjsk.upload.*
import org.json.JSONObject
@@ -216,10 +217,6 @@ private fun UploadTableRow(
}
}
/**
* 添加 / 编辑共用的界面。
* 只有构造函数需要的字段需要用户填写;额外信息(extraInfo)只在下方展示。
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun UploadEditScreen(
@@ -246,7 +243,7 @@ private fun UploadEditScreen(
}
var urlMode by remember(initial) {
mutableStateOf(
when (val url = (original as? UploadProfileCustom)?.uploadUrl) {
when ((original as? UploadProfileCustom)?.uploadUrl) {
is UploadUrlEach -> UploadStore.URL_MODE_EACH
is UploadUrlGeneral -> UploadStore.URL_MODE_GENERAL
null -> UploadStore.URL_MODE_GENERAL
@@ -270,6 +267,13 @@ private fun UploadEditScreen(
(original as? UploadProfileCustom)?.customHeaders?.map { it.key to it.value } ?: emptyList()
)
}
var mixinText by remember(initial) {
mutableStateOf(
(original as? UploadProfileCustom)?.mixin
?.let { UploadStore.encodeMixin(it).toString() }
?: ""
)
}
var typeExpanded by remember { mutableStateOf(false) }
var urlModeExpanded by remember { mutableStateOf(false) }
@@ -291,7 +295,13 @@ private fun UploadEditScreen(
else -> null
}
val canSave = idError == null && codeError == null
val parsedMixin = runCatching { UploadStore.parseMixin(mixinText) }
val mixinError = parsedMixin.exceptionOrNull()
?.let { "Mixin JSON 解析失败:${it.message}" }
?: parsedMixin.getOrNull()?.let { MixinProfiles.validate(it) }
val canSave = idError == null && codeError == null && mixinError == null
fun buildProfile(): UploadProfile = when (type) {
UploadProfileType.HARUKI -> UploadProfileHaruki(trimmedId, uploadCode.trim())
@@ -312,6 +322,7 @@ private fun UploadEditScreen(
.map { it.first.trim() to it.second.trim() }
.filter { it.first.isNotEmpty() }
.toMap(),
mixin = parsedMixin.getOrNull(),
)
}
@@ -340,7 +351,6 @@ private fun UploadEditScreen(
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// ID 和类型放在同一行
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
@@ -481,6 +491,12 @@ private fun UploadEditScreen(
HeadersEditor(headers = headers, onChange = { headers = it })
MixinEditor(
value = mixinText,
onValueChange = { mixinText = it.prependIndent().replace("\n", " ") },
error = mixinError,
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(
onClick = {
@@ -550,7 +566,14 @@ private fun UploadEditScreen(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 160.dp),
placeholder = { Text("{ ... }") },
placeholder = {
Text(
text = "{\"type\":\"CUSTOM\",\"id\":\"my-upload\"," +
"\"uploadUrl\":{\"mode\":\"general\",\"url\":\"https://example.com/upload\"}," +
"\"headers\":{}}",
style = MaterialTheme.typography.bodySmall
)
},
textStyle = MaterialTheme.typography.bodySmall
)
importError?.let {
@@ -585,6 +608,9 @@ private fun UploadEditScreen(
}
}
headers = imported.customHeaders.map { it.key to it.value }
mixinText = imported.mixin
?.let { UploadStore.encodeMixin(it).toString() }
?: ""
importDialog = false
}
@@ -702,6 +728,31 @@ private fun HeadersEditor(
}
}
@Composable
private fun MixinEditor(
value: String,
onValueChange: (String) -> Unit,
error: String?,
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
FormRow("Mixin") {
CompactTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.weight(1f),
isError = error != null,
)
}
if (error != null) {
Text(
text = error,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
}
}
@Composable
private fun FormRow(
label: String,
@@ -803,7 +854,7 @@ private fun CompactDropdown(
}
}
private val helpLinkRegex = Regex("""\[([^\]]+)]\(([^)]+)\)""")
private val helpLinkRegex = Regex("""\[([^]]+)]\(([^)]+)\)""")
@Composable
private fun HelpText(text: String) {