diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5216a01e96..82c7389086 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -116,9 +116,7 @@
android:label="@string/proxy" />
+ android:exported="false" />
diff --git a/app/src/main/java/com/github/kr328/clash/ProvidersActivity.kt b/app/src/main/java/com/github/kr328/clash/ProvidersActivity.kt
index da9bb35fcf..440165d16b 100644
--- a/app/src/main/java/com/github/kr328/clash/ProvidersActivity.kt
+++ b/app/src/main/java/com/github/kr328/clash/ProvidersActivity.kt
@@ -1,12 +1,10 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
-import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.ProvidersDesign
import com.github.kr328.clash.design.R
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.util.withClash
-import java.util.concurrent.TimeUnit
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
@@ -18,8 +16,6 @@ class ProvidersActivity : BaseActivity() {
setContentDesign(design)
- val ticker = ticker(TimeUnit.MINUTES.toMillis(1))
-
while (isActive) {
select {
events.onReceive {
@@ -55,9 +51,6 @@ class ProvidersActivity : BaseActivity() {
}
}
}
- if (activityStarted) {
- ticker.onReceive { design.updateElapsed() }
- }
}
}
}
diff --git a/design/src/main/java/com/github/kr328/clash/design/ProvidersDesign.kt b/design/src/main/java/com/github/kr328/clash/design/ProvidersDesign.kt
index 8c45aa097e..a943c8f156 100644
--- a/design/src/main/java/com/github/kr328/clash/design/ProvidersDesign.kt
+++ b/design/src/main/java/com/github/kr328/clash/design/ProvidersDesign.kt
@@ -2,15 +2,49 @@ package com.github.kr328.clash.design
import android.content.Context
import android.view.View
+import androidx.compose.foundation.background
+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.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
import com.github.kr328.clash.core.model.Provider
-import com.github.kr328.clash.design.adapter.ProviderAdapter
-import com.github.kr328.clash.design.databinding.DesignProvidersBinding
-import com.github.kr328.clash.design.util.applyFrom
-import com.github.kr328.clash.design.util.applyLinearAdapter
-import com.github.kr328.clash.design.util.bindAppBarElevation
-import com.github.kr328.clash.design.util.layoutInflater
-import com.github.kr328.clash.design.util.root
+import com.github.kr328.clash.design.component.MihomoScaffold
+import com.github.kr328.clash.design.ui.theme.MihomoTheme
+import com.github.kr328.clash.design.ui.theme.PreviewMihomo
+import com.github.kr328.clash.design.util.elapsedIntervalString
+import com.github.kr328.clash.design.util.type
+import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
class ProvidersDesign(context: Context, providers: List) :
@@ -19,45 +53,178 @@ class ProvidersDesign(context: Context, providers: List) :
data class Update(val index: Int, val provider: Provider) : Request()
}
- private val binding = DesignProvidersBinding.inflate(context.layoutInflater, context.root, false)
+ private val states =
+ mutableStateListOf().apply {
+ addAll(
+ providers.map {
+ ProviderItemState(provider = it, updatedAt = it.updatedAt, updating = false)
+ }
+ )
+ }
+
+ override val root: View by composeView {
+ MihomoTheme {
+ ProvidersScreen(
+ states = states,
+ onUpdateAll = ::requestUpdateAll,
+ onUpdate = { index, provider ->
+ states[index] = states[index].copy(updating = true)
+ requests.trySend(Request.Update(index, provider))
+ },
+ )
+ }
+ }
- override val root: View
- get() = binding.root
+ suspend fun notifyUpdated(index: Int) =
+ withContext(Dispatchers.Main) { states[index] = states[index].copy(updating = false) }
- private val adapter =
- ProviderAdapter(context, providers) { index, provider ->
- requests.trySend(Request.Update(index, provider))
+ suspend fun notifyChanged(index: Int) =
+ withContext(Dispatchers.Main) {
+ states[index] = states[index].copy(updating = false, updatedAt = System.currentTimeMillis())
}
- fun updateElapsed() {
- adapter.updateElapsed()
+ private fun requestUpdateAll() {
+ states.forEachIndexed { index, state ->
+ if (state.updating || state.provider.vehicleType == Provider.VehicleType.Inline) {
+ return@forEachIndexed
+ }
+
+ states[index] = state.copy(updating = true)
+ requests.trySend(Request.Update(index, state.provider))
+ }
}
+}
+
+private data class ProviderItemState(
+ val provider: Provider,
+ val updatedAt: Long,
+ val updating: Boolean,
+)
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ProvidersScreen(
+ states: List,
+ onUpdateAll: () -> Unit,
+ onUpdate: (Int, Provider) -> Unit,
+) {
+ var currentTime by remember { mutableLongStateOf(System.currentTimeMillis()) }
- suspend fun notifyUpdated(index: Int) {
- withContext(Dispatchers.Main) { adapter.notifyUpdated(index) }
+ LaunchedEffect(Unit) {
+ while (true) {
+ delay(1.minutes)
+ currentTime = System.currentTimeMillis()
+ }
}
- suspend fun notifyChanged(index: Int) {
- withContext(Dispatchers.Main) { adapter.notifyChanged(index) }
+ MihomoScaffold(
+ title = stringResource(R.string.providers),
+ actions = {
+ IconButton(onClick = onUpdateAll) {
+ Icon(
+ painter = painterResource(R.drawable.ic_baseline_sync),
+ contentDescription = stringResource(R.string.update_all),
+ )
+ }
+ },
+ ) { innerPadding ->
+ LazyColumn(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
+ itemsIndexed(
+ items = states,
+ key = { _, state -> "${state.provider.type}-${state.provider.name}" },
+ ) { index, state ->
+ ProviderItem(
+ state = state,
+ currentTime = currentTime,
+ onUpdate = { onUpdate(index, state.provider) },
+ )
+ }
+ }
}
+}
- init {
- binding.self = this
+@Composable
+private fun ProviderItem(state: ProviderItemState, currentTime: Long, onUpdate: () -> Unit) {
+ val context = LocalContext.current
+ val itemMinHeight = dimensionResource(R.dimen.item_min_height)
+ val itemHeaderMargin = dimensionResource(R.dimen.item_header_margin)
+ val itemTextMargin = dimensionResource(R.dimen.item_text_margin)
+ val itemMiddleMargin = dimensionResource(R.dimen.item_midden_margin)
- binding.activityBarLayout.applyFrom(context)
+ val canUpdate = state.provider.vehicleType != Provider.VehicleType.Inline
- binding.mainList.recyclerList.bindAppBarElevation(binding.activityBarLayout)
- binding.mainList.recyclerList.applyLinearAdapter(context, adapter)
- }
+ Row(
+ modifier =
+ Modifier.fillMaxWidth().heightIn(min = itemMinHeight).padding(start = itemHeaderMargin),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(text = state.provider.name, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Spacer(modifier = Modifier.height(itemTextMargin))
+ Text(text = state.provider.type(context), style = MaterialTheme.typography.bodyMedium)
+ }
- fun requestUpdateAll() {
- adapter.states
- .filter { !it.updating }
- .forEachIndexed { index, state ->
- state.updating = true
- if (state.provider.vehicleType != Provider.VehicleType.Inline) {
- requests.trySend(Request.Update(index, state.provider))
+ if (canUpdate) {
+ Text(
+ text = (currentTime - state.updatedAt).elapsedIntervalString(context),
+ modifier = Modifier.padding(end = itemMiddleMargin),
+ )
+ Box(
+ modifier =
+ Modifier.size(width = dimensionResource(R.dimen.divider_size), height = itemMinHeight)
+ .background(MaterialTheme.colorScheme.outline)
+ )
+ IconButton(
+ onClick = onUpdate,
+ enabled = !state.updating,
+ modifier = Modifier.padding(horizontal = 4.dp),
+ ) {
+ if (state.updating) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(dimensionResource(R.dimen.item_tailing_component_size)),
+ strokeWidth = 2.dp,
+ )
+ } else {
+ Icon(
+ painter = painterResource(R.drawable.ic_baseline_swap_vert),
+ contentDescription = stringResource(R.string.update),
+ )
}
}
+ }
}
}
+
+@PreviewMihomo
+@Composable
+private fun ProvidersScreenPreview() = MihomoTheme {
+ ProvidersScreen(
+ states =
+ listOf(
+ ProviderItemState(
+ provider =
+ Provider(
+ name = "Proxy Provider",
+ type = Provider.Type.Proxy,
+ vehicleType = Provider.VehicleType.HTTP,
+ updatedAt = System.currentTimeMillis() - 10 * 60 * 1000,
+ ),
+ updatedAt = System.currentTimeMillis() - 10 * 60 * 1000,
+ updating = false,
+ ),
+ ProviderItemState(
+ provider =
+ Provider(
+ name = "Inline Rules",
+ type = Provider.Type.Rule,
+ vehicleType = Provider.VehicleType.Inline,
+ updatedAt = System.currentTimeMillis() - 60 * 60 * 1000,
+ ),
+ updatedAt = System.currentTimeMillis() - 60 * 60 * 1000,
+ updating = false,
+ ),
+ ),
+ onUpdateAll = {},
+ onUpdate = { _, _ -> },
+ )
+}
diff --git a/design/src/main/java/com/github/kr328/clash/design/adapter/ProviderAdapter.kt b/design/src/main/java/com/github/kr328/clash/design/adapter/ProviderAdapter.kt
deleted file mode 100644
index e87329c8fd..0000000000
--- a/design/src/main/java/com/github/kr328/clash/design/adapter/ProviderAdapter.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.github.kr328.clash.design.adapter
-
-import android.content.Context
-import android.view.View
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import com.github.kr328.clash.core.model.Provider
-import com.github.kr328.clash.design.databinding.AdapterProviderBinding
-import com.github.kr328.clash.design.model.ProviderState
-import com.github.kr328.clash.design.ui.ObservableCurrentTime
-import com.github.kr328.clash.design.util.layoutInflater
-
-class ProviderAdapter(
- private val context: Context,
- providers: List,
- private val requestUpdate: (Int, Provider) -> Unit,
-) : RecyclerView.Adapter() {
- class Holder(val binding: AdapterProviderBinding) : RecyclerView.ViewHolder(binding.root)
-
- private val currentTime = ObservableCurrentTime()
-
- val states = providers.map { ProviderState(it, it.updatedAt, false) }
-
- fun updateElapsed() {
- currentTime.update()
- }
-
- fun notifyUpdated(index: Int) {
- states[index].apply { updating = false }
-
- notifyItemChanged(index)
- }
-
- fun notifyChanged(index: Int) {
- states[index].apply {
- updating = false
- updatedAt = System.currentTimeMillis()
- }
-
- notifyItemChanged(index)
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
- return Holder(
- AdapterProviderBinding.inflate(context.layoutInflater, parent, false).also {
- it.currentTime = currentTime
- }
- )
- }
-
- override fun onBindViewHolder(holder: Holder, position: Int) {
- val state = states[position]
-
- holder.binding.provider = state.provider
- holder.binding.state = state
- if (state.provider.vehicleType == Provider.VehicleType.Inline) {
- holder.binding.endView.visibility = View.GONE
- holder.binding.elapsedView.visibility = View.GONE
- holder.binding.divider.visibility = View.GONE
- } else {
- holder.binding.endView.visibility = View.VISIBLE
- holder.binding.elapsedView.visibility = View.VISIBLE
- holder.binding.update = View.OnClickListener {
- state.updating = true
- requestUpdate(position, state.provider)
- }
- }
- }
-
- override fun getItemCount(): Int {
- return states.size
- }
-}
diff --git a/design/src/main/res/layout/adapter_provider.xml b/design/src/main/res/layout/adapter_provider.xml
deleted file mode 100644
index 4625574f6e..0000000000
--- a/design/src/main/res/layout/adapter_provider.xml
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/design/src/main/res/layout/design_providers.xml b/design/src/main/res/layout/design_providers.xml
deleted file mode 100644
index c33e7e9473..0000000000
--- a/design/src/main/res/layout/design_providers.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file