Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,97 +1,37 @@
package com.github.kr328.clash.profile

import com.github.kr328.clash.R
import android.os.Bundle
import androidx.activity.compose.setContent
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.common.util.uuid
import com.github.kr328.clash.files.FilesActivity
import com.github.kr328.clash.profile.ui.PropertiesDesign
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.ui.DesignActivity
import com.github.kr328.clash.util.showExceptionSnackbar
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import com.github.kr328.clash.profile.ui.PropertiesScreen
import com.github.kr328.clash.ui.BaseActivity
import com.github.kr328.clash.ui.theme.MihomoTheme

class PropertiesActivity : DesignActivity<PropertiesDesign>() {
private var canceled: Boolean = false
private lateinit var original: Profile
class PropertiesActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

override suspend fun main() {
setResult(RESULT_CANCELED)

val uuid = intent.uuid ?: return finish()
val design = PropertiesDesign(this)

original = withProfile { queryByUUID(uuid) } ?: return finish()

design.profile = original

setContentDesign(design)

defer {
canceled = true

withProfile { release(uuid) }
}

while (isActive) {
select {
events.onReceive {
when (it) {
Event.ActivityStop -> {
val profile = design.profile

if (!canceled && profile != original) {
withProfile { patch(profile.uuid, profile.name, profile.source, profile.interval) }
}
}
Event.ServiceRecreated -> {
finish()
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
PropertiesDesign.Request.BrowseFiles -> {
startActivity(FilesActivity::class.intent.setUUID(uuid))
}
PropertiesDesign.Request.Commit -> {
design.verifyAndCommit()
val uuid = checkNotNull(intent.uuid) { "The uuid must not be null." }
Comment thread
Goooler marked this conversation as resolved.

setContent {
MihomoTheme {
PropertiesScreen(
uuid = uuid,
onBrowseFiles = { profileUuid ->
startActivity(FilesActivity::class.intent.setUUID(profileUuid))
},
onFinish = { success ->
if (success) {
setResult(RESULT_OK)
}
}
}
}
}
}

private suspend fun PropertiesDesign.verifyAndCommit() {
when {
profile.name.isBlank() -> {
snackbar(R.string.empty_name)
}
profile.type != Profile.Type.File && profile.source.isBlank() -> {
snackbar(R.string.invalid_url)
}
else -> {
try {
withProcessing { updateStatus ->
withProfile {
patch(profile.uuid, profile.name, profile.source, profile.interval)

coroutineScope { commit(profile.uuid) { launch { updateStatus(it) } } }
}
}

setResult(RESULT_OK)

finish()
} catch (e: Exception) {
showExceptionSnackbar(e)
}
finish()
},
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.github.kr328.clash.profile.ui

import android.app.Activity
import android.content.Context
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
Expand All @@ -25,22 +23,28 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.github.kr328.clash.R
import com.github.kr328.clash.core.model.FetchStatus
import com.github.kr328.clash.profile.vm.PropertiesViewModel
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.ui.Design
import com.github.kr328.clash.ui.component.MihomoScaffold
import com.github.kr328.clash.ui.component.ModelProgressBarDialog
import com.github.kr328.clash.ui.component.ModelProgressBarState
Expand All @@ -52,108 +56,72 @@ import com.github.kr328.clash.ui.theme.mihomoDimens
import com.github.kr328.clash.util.ValidatorAutoUpdateInterval
import com.github.kr328.clash.util.ValidatorHttpUrl
import com.github.kr328.clash.util.ValidatorNotBlank
import com.github.kr328.clash.util.toast
import java.util.UUID
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class PropertiesDesign(context: Context) : Design<PropertiesDesign.Request>(context) {
sealed interface Request {
data object Commit : Request

data object BrowseFiles : Request
}
@Composable
fun PropertiesScreen(
uuid: UUID,
modifier: Modifier = Modifier,
viewModel: PropertiesViewModel = viewModel(),
onBrowseFiles: (UUID) -> Unit,
onFinish: (Boolean) -> Unit,
) {
val lifecycleOwner = LocalLifecycleOwner.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val eventState by viewModel.eventState.collectAsStateWithLifecycle()
val context = LocalContext.current

private var profileState by mutableStateOf<Profile?>(null)
private var originalProfileState by mutableStateOf<Profile?>(null)
private var processingState by mutableStateOf(false)
private val progressBarState = ModelProgressBarState()
LaunchedEffect(uuid) { viewModel.init(uuid = uuid) }

@Composable
override fun Content() = MihomoTheme {
profileState?.let { profile ->
PropertiesScreen(
profile = profile,
processing = processingState,
progressBarState = progressBarState,
hasUnsavedChanges =
originalProfileState?.let { original -> hasUnsavedChanges(profile, original) } == true,
onBrowseFiles = { requests.trySend(Request.BrowseFiles) },
onCommit = { requests.trySend(Request.Commit) },
onRequestClose = { (context as? Activity)?.finish() },
onNameChanged = { name -> this@PropertiesDesign.profile = profile.copy(name = name) },
onUrlChanged = { url -> this@PropertiesDesign.profile = profile.copy(source = url) },
onIntervalChanged = { interval ->
this@PropertiesDesign.profile = profile.copy(interval = interval)
},
)
}
DisposableEffect(lifecycleOwner, viewModel) {
lifecycleOwner.lifecycle.addObserver(viewModel)
onDispose { lifecycleOwner.lifecycle.removeObserver(viewModel) }
}

var profile: Profile
get() = requireNotNull(profileState)
set(value) {
if (originalProfileState == null) {
originalProfileState = value.copy()
LaunchedEffect(eventState) {
when (val event = eventState) {
PropertiesViewModel.EventState.Idle -> Unit
is PropertiesViewModel.EventState.BrowseFiles -> {
onBrowseFiles(event.uuid)
}
profileState = value
}

suspend fun withProcessing(executeTask: suspend (suspend (FetchStatus) -> Unit) -> Unit) =
try {
withContext(Dispatchers.Main) {
processingState = true
progressBarState.visible = true
progressBarState.isIndeterminate = true
progressBarState.text = context.getString(R.string.initializing)
progressBarState.progress = 0
progressBarState.max = 0
is PropertiesViewModel.EventState.Finish -> {
onFinish(event.success)
}

executeTask { status -> withContext(Dispatchers.Main) { progressBarState.applyFrom(status) } }
} finally {
withContext(Dispatchers.Main) {
progressBarState.visible = false
progressBarState.text = null
processingState = false
is PropertiesViewModel.EventState.ShowMessage -> {
context.toast(event.message)
}
}

private fun hasUnsavedChanges(profile: Profile, original: Profile): Boolean {
return profile.name != original.name ||
profile.source != original.source ||
profile.interval != original.interval
viewModel.consumeEvent()
}

private fun ModelProgressBarState.applyFrom(status: FetchStatus) {
when (status.action) {
FetchStatus.Action.FetchConfiguration -> {
text = context.getString(R.string.format_fetching_configuration, status.args[0])
isIndeterminate = true
}
FetchStatus.Action.FetchProviders -> {
text = context.getString(R.string.format_fetching_provider, status.args[0])
isIndeterminate = false
max = status.max
progress = status.progress
}
FetchStatus.Action.Verifying -> {
text = context.getString(R.string.verifying)
isIndeterminate = false
max = status.max
progress = status.progress
}
}
val profile = uiState.profile
if (profile != null) {
PropertiesContent(
modifier = modifier,
profile = profile,
processing = uiState.processing,
progressState = uiState.progress,
hasUnsavedChanges = uiState.hasUnsavedChanges,
onBrowseFiles = viewModel::onBrowseFiles,
onCommit = viewModel::onCommit,
onRequestClose = viewModel::onRequestClose,
onNameChanged = viewModel::onNameChanged,
onUrlChanged = viewModel::onUrlChanged,
onIntervalChanged = viewModel::onIntervalChanged,
)
}
}

@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun PropertiesScreen(
private fun PropertiesContent(
modifier: Modifier = Modifier,
profile: Profile,
processing: Boolean,
progressBarState: ModelProgressBarState,
progressState: PropertiesViewModel.ProgressState,
hasUnsavedChanges: Boolean,
onBrowseFiles: () -> Unit,
onCommit: () -> Unit,
Expand All @@ -170,6 +138,15 @@ private fun PropertiesScreen(
var showInputUrlDialog by rememberSaveable { mutableStateOf(false) }
var showInputIntervalDialog by rememberSaveable { mutableStateOf(false) }

val progressBarState = remember { ModelProgressBarState() }
with(progressBarState) {
visible = progressState.visible
isIndeterminate = progressState.isIndeterminate
text = progressState.text
progress = progressState.progress
max = progressState.max
}

val onBack = {
when {
processing -> Unit
Expand All @@ -182,6 +159,7 @@ private fun PropertiesScreen(
BackHandler(onBack = onBack)

MihomoScaffold(
modifier = modifier,
title = stringResource(R.string.properties),
onBack = onBack,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
Expand Down Expand Up @@ -378,8 +356,8 @@ private fun PropertiesActionItem(

@PreviewMihomo
@Composable
private fun PropertiesScreenPreview() = MihomoTheme {
PropertiesScreen(
private fun PropertiesContentPreview() = MihomoTheme {
PropertiesContent(
profile =
Profile(
uuid = UUID(0, 0),
Expand All @@ -397,7 +375,7 @@ private fun PropertiesScreenPreview() = MihomoTheme {
pending = false,
),
processing = false,
progressBarState = ModelProgressBarState(),
progressState = PropertiesViewModel.ProgressState(),
hasUnsavedChanges = false,
onBrowseFiles = {},
onCommit = {},
Expand Down
Loading