diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 08f1c2760c..22c7c95916 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -109,9 +109,7 @@
android:exported="false" />
+ android:exported="false" />
diff --git a/design/src/main/java/com/github/kr328/clash/design/AccessControlDesign.kt b/design/src/main/java/com/github/kr328/clash/design/AccessControlDesign.kt
index 794786cfd0..e736a50d8a 100644
--- a/design/src/main/java/com/github/kr328/clash/design/AccessControlDesign.kt
+++ b/design/src/main/java/com/github/kr328/clash/design/AccessControlDesign.kt
@@ -7,6 +7,7 @@ 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.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -27,6 +28,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
@@ -158,53 +160,61 @@ private fun AccessControlScreen(
var showMenu by remember { mutableStateOf(false) }
if (showMenu) {
- AccessControlMenuSheet(
- sort = sort,
- reverse = reverse,
- showSystemApps = showSystemApps,
- onDismiss = { showMenu = false },
- onSelectAll = {
- showMenu = false
- onSelectAll()
- },
- onSelectNone = {
- showMenu = false
- onSelectNone()
- },
- onSelectInvert = {
- showMenu = false
- onSelectInvert()
- },
- onImport = {
- showMenu = false
- onImport()
- },
- onExport = {
- showMenu = false
- onExport()
- },
- onUpdateSort = {
- sort = it
- onUpdateSort(it)
- },
- onUpdateReverse = {
- reverse = it
- onUpdateReverse(it)
- },
- onUpdateShowSystemApps = {
- showSystemApps = it
- onUpdateShowSystemApps(it)
- },
- )
+ ModalBottomSheet(
+ onDismissRequest = { showMenu = false },
+ sheetState = rememberModalBottomSheetState(),
+ ) {
+ AccessControlMenuContent(
+ sort = sort,
+ reverse = reverse,
+ showSystemApps = showSystemApps,
+ onSelectAll = {
+ showMenu = false
+ onSelectAll()
+ },
+ onSelectNone = {
+ showMenu = false
+ onSelectNone()
+ },
+ onSelectInvert = {
+ showMenu = false
+ onSelectInvert()
+ },
+ onImport = {
+ showMenu = false
+ onImport()
+ },
+ onExport = {
+ showMenu = false
+ onExport()
+ },
+ onUpdateSort = {
+ sort = it
+ showMenu = false
+ onUpdateSort(it)
+ },
+ onUpdateReverse = {
+ reverse = it
+ showMenu = false
+ onUpdateReverse(it)
+ },
+ onUpdateShowSystemApps = {
+ showSystemApps = it
+ showMenu = false
+ onUpdateShowSystemApps(it)
+ },
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ }
}
if (showSearch) {
- AccessControlSearchSheet(
- apps = apps,
- selected = selected,
- onToggleApp = onToggleApp,
- onDismiss = { showSearch = false },
- )
+ ModalBottomSheet(
+ onDismissRequest = { showSearch = false },
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+ ) {
+ AccessControlSearchContent(apps = apps, selected = selected, onToggleApp = onToggleApp)
+ }
}
MihomoScaffold(
@@ -237,63 +247,8 @@ private fun AccessControlScreen(
}
}
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun AccessControlMenuSheet(
- sort: AppInfoSort,
- reverse: Boolean,
- showSystemApps: Boolean,
- modifier: Modifier = Modifier,
- onDismiss: () -> Unit,
- onSelectAll: () -> Unit,
- onSelectNone: () -> Unit,
- onSelectInvert: () -> Unit,
- onImport: () -> Unit,
- onExport: () -> Unit,
- onUpdateSort: (AppInfoSort) -> Unit,
- onUpdateReverse: (Boolean) -> Unit,
- onUpdateShowSystemApps: (Boolean) -> Unit,
-) {
- ModalBottomSheet(
- modifier = modifier,
- onDismissRequest = onDismiss,
- sheetState = rememberModalBottomSheetState(),
- ) {
- AccessControlMenuContent(
- sort = sort,
- reverse = reverse,
- showSystemApps = showSystemApps,
- onSelectAll = onSelectAll,
- onSelectNone = onSelectNone,
- onSelectInvert = onSelectInvert,
- onImport = onImport,
- onExport = onExport,
- onUpdateSort = onUpdateSort,
- onUpdateReverse = onUpdateReverse,
- onUpdateShowSystemApps = onUpdateShowSystemApps,
- )
- Spacer(modifier = Modifier.height(16.dp))
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun AccessControlSearchSheet(
- apps: List,
- selected: Set,
- onToggleApp: (String) -> Unit,
- onDismiss: () -> Unit,
-) {
- ModalBottomSheet(
- onDismissRequest = onDismiss,
- sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
- ) {
- AccessControlSearchContent(apps = apps, selected = selected, onToggleApp = onToggleApp)
- }
-}
-
@Composable
-private fun AccessControlSearchContent(
+private fun ColumnScope.AccessControlSearchContent(
apps: List,
selected: Set,
onToggleApp: (String) -> Unit,
@@ -346,7 +301,7 @@ private fun AccessControlSearchContent(
}
@Composable
-private fun AccessControlMenuContent(
+private fun ColumnScope.AccessControlMenuContent(
sort: AppInfoSort,
reverse: Boolean,
showSystemApps: Boolean,
@@ -543,43 +498,51 @@ private fun AccessControlScreenPreview() = MihomoTheme {
@PreviewMihomo
@Composable
private fun AccessControlMenuSheetPreview() = MihomoTheme {
- AccessControlMenuContent(
- sort = AppInfoSort.Label,
- reverse = false,
- showSystemApps = true,
- onSelectAll = {},
- onSelectNone = {},
- onSelectInvert = {},
- onImport = {},
- onExport = {},
- onUpdateSort = {},
- onUpdateReverse = {},
- onUpdateShowSystemApps = {},
- )
+ Surface {
+ Column {
+ AccessControlMenuContent(
+ sort = AppInfoSort.Label,
+ reverse = false,
+ showSystemApps = true,
+ onSelectAll = {},
+ onSelectNone = {},
+ onSelectInvert = {},
+ onImport = {},
+ onExport = {},
+ onUpdateSort = {},
+ onUpdateReverse = {},
+ onUpdateShowSystemApps = {},
+ )
+ }
+ }
}
@PreviewMihomo
@Composable
private fun AccessControlSearchSheetPreview() = MihomoTheme {
- AccessControlSearchContent(
- apps =
- listOf(
- AppInfo(
- packageName = "com.example.alpha",
- label = "Alpha",
- icon = Color.Gray.toArgb().toDrawable(),
- installTime = 1_700_000_000_000,
- updateDate = 1_710_000_000_000,
- ),
- AppInfo(
- packageName = "com.example.beta",
- label = "Beta",
- icon = Color.DarkGray.toArgb().toDrawable(),
- installTime = 1_690_000_000_000,
- updateDate = 1_715_000_000_000,
- ),
- ),
- selected = setOf("com.example.alpha"),
- onToggleApp = {},
- )
+ Surface {
+ Column {
+ AccessControlSearchContent(
+ apps =
+ listOf(
+ AppInfo(
+ packageName = "com.example.alpha",
+ label = "Alpha",
+ icon = Color.Gray.toArgb().toDrawable(),
+ installTime = 1_700_000_000_000,
+ updateDate = 1_710_000_000_000,
+ ),
+ AppInfo(
+ packageName = "com.example.beta",
+ label = "Beta",
+ icon = Color.DarkGray.toArgb().toDrawable(),
+ installTime = 1_690_000_000_000,
+ updateDate = 1_715_000_000_000,
+ ),
+ ),
+ selected = setOf("com.example.alpha"),
+ onToggleApp = {},
+ )
+ }
+ }
}
diff --git a/design/src/main/java/com/github/kr328/clash/design/ProxyDesign.kt b/design/src/main/java/com/github/kr328/clash/design/ProxyDesign.kt
index dfd31c5a1e..2d6b24828d 100644
--- a/design/src/main/java/com/github/kr328/clash/design/ProxyDesign.kt
+++ b/design/src/main/java/com/github/kr328/clash/design/ProxyDesign.kt
@@ -1,32 +1,86 @@
package com.github.kr328.clash.design
import android.content.Context
-import android.content.res.ColorStateList
import android.view.View
import android.widget.Toast
-import androidx.viewpager2.widget.ViewPager2
+import androidx.annotation.ColorInt
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+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.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+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.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.toggleable
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.PrimaryScrollableTabRow
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Tab
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
import com.github.kr328.clash.core.model.Proxy
+import com.github.kr328.clash.core.model.ProxySort
import com.github.kr328.clash.core.model.TunnelState
-import com.github.kr328.clash.design.adapter.ProxyAdapter
-import com.github.kr328.clash.design.adapter.ProxyPageAdapter
-import com.github.kr328.clash.design.component.ProxyMenu
+import com.github.kr328.clash.design.component.MihomoScaffold
import com.github.kr328.clash.design.component.ProxyViewConfig
-import com.github.kr328.clash.design.databinding.DesignProxyBinding
+import com.github.kr328.clash.design.component.ProxyViewState
import com.github.kr328.clash.design.model.ProxyState
import com.github.kr328.clash.design.store.UiStore
-import com.github.kr328.clash.design.util.applyFrom
-import com.github.kr328.clash.design.util.layoutInflater
-import com.github.kr328.clash.design.util.resolveThemedColor
-import com.github.kr328.clash.design.util.root
-import com.google.android.material.tabs.TabLayoutMediator
+import com.github.kr328.clash.design.ui.theme.MihomoTheme
+import com.github.kr328.clash.design.ui.theme.PreviewMihomo
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ProxyDesign(
context: Context,
- overrideMode: TunnelState.Mode?,
- groupNames: List,
- uiStore: UiStore,
+ private val overrideMode: TunnelState.Mode?,
+ private val groupNames: List,
+ private val uiStore: UiStore,
) : Design(context) {
sealed class Request {
object ReloadAll : Request()
@@ -42,31 +96,59 @@ class ProxyDesign(
data class UrlTest(val index: Int) : Request()
}
- private val binding = DesignProxyBinding.inflate(context.layoutInflater, context.root, false)
-
- private var config = ProxyViewConfig(context, uiStore.proxyLine)
-
- private val menu: ProxyMenu by lazy {
- ProxyMenu(context, binding.menuView, overrideMode, uiStore, requests) {
- config.proxyLine = uiStore.proxyLine
+ private val config = ProxyViewConfig(context, uiStore.proxyLine)
+ private val groups = List(groupNames.size) { ProxyGroupUiState() }
+ private val initialPage = groupNames.indexOf(uiStore.proxyLastGroup).coerceAtLeast(0)
+
+ private var currentPage by
+ mutableIntStateOf(initialPage.coerceAtMost((groupNames.size - 1).coerceAtLeast(0)))
+ private var proxyLine by mutableIntStateOf(uiStore.proxyLine)
+ private var excludeNotSelectable by mutableStateOf(uiStore.proxyExcludeNotSelectable)
+ private var proxySort by mutableStateOf(uiStore.proxySort)
+ private var selectedMode by mutableStateOf(overrideMode)
+
+ override val root: View by composeView {
+ MihomoTheme {
+ ProxyScreen(
+ groupNames = groupNames,
+ groups = groups,
+ currentPage = currentPage,
+ proxyLine = proxyLine,
+ excludeNotSelectable = excludeNotSelectable,
+ proxySort = proxySort,
+ overrideMode = selectedMode,
+ initialPage = initialPage,
+ onPageChanged = { index ->
+ currentPage = index
+ groupNames.getOrNull(index)?.let { uiStore.proxyLastGroup = it }
+ },
+ onUrlTest = ::requestUrlTesting,
+ onExcludeNotSelectableChanged = { enabled ->
+ excludeNotSelectable = enabled
+ uiStore.proxyExcludeNotSelectable = enabled
+ requests.trySend(Request.ReLaunch)
+ },
+ onProxyLineChanged = { line ->
+ proxyLine = line
+ uiStore.proxyLine = line
+ config.proxyLine = line
+ groups.forEach { it.refresh() }
+ requests.trySend(Request.ReloadAll)
+ },
+ onProxySortChanged = { sort ->
+ proxySort = sort
+ uiStore.proxySort = sort
+ requests.trySend(Request.ReloadAll)
+ },
+ onOverrideModeSelected = { mode ->
+ selectedMode = mode
+ requests.trySend(Request.PatchMode(mode))
+ },
+ onProxySelected = { index, name -> requests.trySend(Request.Select(index, name)) },
+ )
}
}
- private val adapter: ProxyPageAdapter
- get() = binding.pagesView.adapter!! as ProxyPageAdapter
-
- private var horizontalScrolling = false
- private val verticalBottomScrolled: Boolean
- get() = adapter.states[binding.pagesView.currentItem].bottom
-
- private var urlTesting: Boolean
- get() = adapter.states[binding.pagesView.currentItem].urlTesting
- set(value) {
- adapter.states[binding.pagesView.currentItem].urlTesting = value
- }
-
- override val root: View = binding.root
-
suspend fun updateGroup(
position: Int,
proxies: List,
@@ -74,105 +156,584 @@ class ProxyDesign(
parent: ProxyState,
links: Map,
) {
- adapter.updateAdapter(position, proxies, selectable, parent, links)
-
- adapter.states[position].urlTesting = false
+ val states =
+ withContext(Dispatchers.Default) {
+ proxies.map { proxy ->
+ ProxyViewState(config, proxy, parent, if (proxy.type.group) links[proxy.name] else null)
+ }
+ }
- updateUrlTestButtonStatus()
+ withContext(Dispatchers.Main) {
+ groups[position].apply {
+ rawStates = states
+ this.selectable = selectable
+ urlTesting = false
+ refresh()
+ }
+ }
}
- suspend fun requestRedrawVisible() {
- withContext(Dispatchers.Main) { adapter.requestRedrawVisible() }
- }
+ suspend fun requestRedrawVisible() =
+ withContext(Dispatchers.Main) { groups.getOrNull(currentPage)?.refresh() }
- suspend fun showModeSwitchTips() {
+ suspend fun showModeSwitchTips() =
withContext(Dispatchers.Main) {
Toast.makeText(context, R.string.mode_switch_tips, Toast.LENGTH_LONG).show()
}
- }
- init {
- binding.self = this
+ private fun requestUrlTesting() {
+ if (groups.isEmpty()) return
- binding.activityBarLayout.applyFrom(context)
+ val page = currentPage.coerceIn(groups.indices)
- binding.menuView.setOnClickListener { menu.show() }
+ groups[page].urlTesting = true
+ requests.trySend(Request.UrlTest(page))
+ }
+}
- if (groupNames.isEmpty()) {
- binding.emptyView.visibility = View.VISIBLE
+private data class ProxyItemUiState(
+ val key: String,
+ val title: String,
+ val subtitle: String,
+ val delayText: String,
+ @get:ColorInt val background: Int,
+ val controls: Int,
+)
+
+private fun ProxyViewState.toUiState() =
+ ProxyItemUiState(
+ key = proxy.name,
+ title = title,
+ subtitle = subtitle,
+ delayText = delayText,
+ background = background,
+ controls = controls,
+ )
+
+private class ProxyGroupUiState {
+ var items by mutableStateOf>(emptyList())
+ var selectable by mutableStateOf(false)
+ var urlTesting by mutableStateOf(false)
+ var rawStates: List = emptyList()
+ var refreshVersion by mutableIntStateOf(0)
+
+ fun refresh() {
+ refreshVersion++
+ }
+}
- binding.urlTestView.visibility = View.GONE
- binding.tabLayoutView.visibility = View.GONE
- binding.elevationView.visibility = View.GONE
- binding.pagesView.visibility = View.GONE
- binding.urlTestFloatView.visibility = View.GONE
- } else {
- binding.urlTestFloatView.supportImageTintList =
- ColorStateList.valueOf(
- context.resolveThemedColor(com.google.android.material.R.attr.colorOnPrimary)
- )
+@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
+@Composable
+private fun ProxyScreen(
+ groupNames: List,
+ groups: List,
+ currentPage: Int,
+ proxyLine: Int,
+ excludeNotSelectable: Boolean,
+ proxySort: ProxySort,
+ overrideMode: TunnelState.Mode?,
+ initialPage: Int,
+ onPageChanged: (Int) -> Unit,
+ onUrlTest: () -> Unit,
+ onExcludeNotSelectableChanged: (Boolean) -> Unit,
+ onProxyLineChanged: (Int) -> Unit,
+ onProxySortChanged: (ProxySort) -> Unit,
+ onOverrideModeSelected: (TunnelState.Mode?) -> Unit,
+ onProxySelected: (Int, String) -> Unit,
+) {
+ var menuVisible by remember { mutableStateOf(false) }
+ val currentGroup = groups.getOrNull(currentPage)
+ val showUrlTestAction = groupNames.isNotEmpty()
+
+ if (menuVisible) {
+ ModalBottomSheet(onDismissRequest = { menuVisible = false }) {
+ ProxyMenuSheetContent(
+ overrideMode = overrideMode,
+ excludeNotSelectable = excludeNotSelectable,
+ proxyLine = proxyLine,
+ proxySort = proxySort,
+ onExcludeNotSelectableChanged = {
+ menuVisible = false
+ onExcludeNotSelectableChanged(it)
+ },
+ onProxyLineChanged = {
+ menuVisible = false
+ onProxyLineChanged(it)
+ },
+ onProxySortChanged = {
+ menuVisible = false
+ onProxySortChanged(it)
+ },
+ onOverrideModeSelected = {
+ menuVisible = false
+ onOverrideModeSelected(it)
+ },
+ )
+ }
+ }
- binding.pagesView.apply {
- adapter =
- ProxyPageAdapter(
- surface,
- config,
- List(groupNames.size) { index ->
- ProxyAdapter(config) { name -> requests.trySend(Request.Select(index, name)) }
- },
- ) {
- if (it == currentItem) updateUrlTestButtonStatus()
+ MihomoScaffold(
+ title = stringResource(R.string.proxy),
+ actions = {
+ if (showUrlTestAction) {
+ if (currentGroup?.urlTesting == true) {
+ CircularProgressIndicator(
+ modifier = Modifier.padding(horizontal = 12.dp).size(24.dp),
+ strokeWidth = 2.dp,
+ )
+ } else {
+ IconButton(onClick = onUrlTest) {
+ Icon(
+ painter = painterResource(R.drawable.ic_baseline_flash_on),
+ contentDescription = stringResource(R.string.delay_test),
+ )
}
+ }
+ }
- registerOnPageChangeCallback(
- object : ViewPager2.OnPageChangeCallback() {
- override fun onPageScrollStateChanged(state: Int) {
- horizontalScrolling = state != ViewPager2.SCROLL_STATE_IDLE
+ IconButton(onClick = { menuVisible = true }) {
+ Icon(
+ painter = painterResource(R.drawable.ic_baseline_more_vert),
+ contentDescription = stringResource(R.string.more),
+ )
+ }
+ },
+ ) { innerPadding ->
+ Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
+ if (groupNames.isEmpty()) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Text(
+ text = stringResource(R.string.proxy_empty_tips),
+ style = MaterialTheme.typography.titleMedium,
+ )
+ }
+ } else {
+ ProxyPagerContent(
+ groupNames = groupNames,
+ groups = groups,
+ proxyLine = proxyLine,
+ initialPage = initialPage,
+ currentPage = currentPage,
+ onPageChanged = onPageChanged,
+ onProxySelected = onProxySelected,
+ )
+ }
+ }
+ }
+}
- updateUrlTestButtonStatus()
- }
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun ProxyPagerContent(
+ groupNames: List,
+ groups: List,
+ proxyLine: Int,
+ initialPage: Int,
+ currentPage: Int,
+ onPageChanged: (Int) -> Unit,
+ onProxySelected: (Int, String) -> Unit,
+) {
+ val pagerState = rememberPagerState(initialPage = initialPage, pageCount = { groupNames.size })
+ val scope = rememberCoroutineScope()
+
+ LaunchedEffect(pagerState) { snapshotFlow { pagerState.currentPage }.collect(onPageChanged) }
+
+ LaunchedEffect(currentPage) {
+ if (currentPage != pagerState.currentPage) pagerState.scrollToPage(currentPage)
+ }
- override fun onPageSelected(position: Int) {
- uiStore.proxyLastGroup = groupNames[position]
- }
- }
+ Column(modifier = Modifier.fillMaxSize()) {
+ PrimaryScrollableTabRow(selectedTabIndex = pagerState.currentPage, edgePadding = 0.dp) {
+ groupNames.forEachIndexed { index, name ->
+ Tab(
+ selected = pagerState.currentPage == index,
+ onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
+ text = { Text(text = name, maxLines = 1, overflow = TextOverflow.Ellipsis) },
)
}
+ }
+
+ HorizontalDivider()
- TabLayoutMediator(binding.tabLayoutView, binding.pagesView) { tab, index ->
- tab.text = groupNames[index]
+ HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page ->
+ ProxyGroupPage(
+ index = page,
+ proxyLine = proxyLine,
+ group = groups[page],
+ onProxySelected = onProxySelected,
+ )
+ }
+ }
+}
+
+@Composable
+private fun ProxyGroupPage(
+ index: Int,
+ proxyLine: Int,
+ group: ProxyGroupUiState,
+ onProxySelected: (Int, String) -> Unit,
+) {
+ val refreshVersion = group.refreshVersion
+ val rawStates = group.rawStates
+ val previewItems = group.items
+ val useRawStates = rawStates.isNotEmpty()
+
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(columnsForProxyLine(proxyLine)),
+ state = rememberLazyGridState(),
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(12.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ items(
+ count = if (useRawStates) rawStates.size else previewItems.size,
+ key = { itemIndex ->
+ if (useRawStates) {
+ rawStates[itemIndex].proxy.name
+ } else {
+ previewItems[itemIndex].key
}
- .attach()
+ },
+ ) { itemIndex ->
+ val item =
+ if (useRawStates) {
+ val state = rawStates[itemIndex]
+ var uiState by
+ remember(state.proxy.name) { mutableStateOf(state.apply { update(true) }.toUiState()) }
+
+ LaunchedEffect(refreshVersion) {
+ state.update(true)
+ uiState = state.toUiState()
+ }
- val initialPosition = groupNames.indexOf(uiStore.proxyLastGroup)
+ uiState
+ } else {
+ previewItems[itemIndex]
+ }
- binding.pagesView.post {
- if (initialPosition > 0) binding.pagesView.setCurrentItem(initialPosition, false)
- }
+ ProxyItemCard(
+ item = item,
+ proxyLine = proxyLine,
+ selectable = group.selectable,
+ onClick = { onProxySelected(index, item.key) },
+ )
+ }
+ }
+}
+
+@Composable
+private fun ProxyItemCard(
+ item: ProxyItemUiState,
+ proxyLine: Int,
+ selectable: Boolean,
+ onClick: () -> Unit,
+) {
+ val shape = RoundedCornerShape(if (proxyLine == 1) 0.dp else 5.dp)
+ val modifier =
+ Modifier.fillMaxWidth()
+ .then(if (proxyLine == 1) Modifier else Modifier.shadow(elevation = 2.dp, shape = shape))
+ .clip(shape)
+ .background(Color(item.background))
+ .clickable(enabled = selectable, onClick = onClick)
+ .padding(horizontal = if (proxyLine == 3) 12.dp else 15.dp, vertical = 14.dp)
+
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Text(
+ text = item.title,
+ color = Color(item.controls),
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Medium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Text(
+ text = item.subtitle,
+ color = Color(item.controls),
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+
+ if (item.delayText.isNotEmpty()) {
+ Text(
+ text = item.delayText,
+ color = Color(item.controls),
+ style = MaterialTheme.typography.bodyMedium,
+ maxLines = 1,
+ )
}
}
+}
+
+@Composable
+private fun ColumnScope.ProxyMenuSheetContent(
+ overrideMode: TunnelState.Mode?,
+ excludeNotSelectable: Boolean,
+ proxyLine: Int,
+ proxySort: ProxySort,
+ onExcludeNotSelectableChanged: (Boolean) -> Unit,
+ onProxyLineChanged: (Int) -> Unit,
+ onProxySortChanged: (ProxySort) -> Unit,
+ onOverrideModeSelected: (TunnelState.Mode?) -> Unit,
+) {
+ ProxyMenuSection(title = stringResource(R.string.filter)) {
+ ProxyMenuCheckboxRow(
+ title = stringResource(R.string.not_selectable),
+ checked = excludeNotSelectable,
+ onClick = { onExcludeNotSelectableChanged(!excludeNotSelectable) },
+ )
+ }
+
+ ProxyMenuSection(title = stringResource(R.string.mode)) {
+ ProxyMenuRadioRow(
+ title = stringResource(R.string.dont_modify),
+ selected = overrideMode == null,
+ onClick = { onOverrideModeSelected(null) },
+ )
+ ProxyMenuRadioRow(
+ title = stringResource(R.string.direct_mode),
+ selected = overrideMode == TunnelState.Mode.Direct,
+ onClick = { onOverrideModeSelected(TunnelState.Mode.Direct) },
+ )
+ ProxyMenuRadioRow(
+ title = stringResource(R.string.global_mode),
+ selected = overrideMode == TunnelState.Mode.Global,
+ onClick = { onOverrideModeSelected(TunnelState.Mode.Global) },
+ )
+ ProxyMenuRadioRow(
+ title = stringResource(R.string.rule_mode),
+ selected = overrideMode == TunnelState.Mode.Rule,
+ onClick = { onOverrideModeSelected(TunnelState.Mode.Rule) },
+ )
+ }
+
+ ProxyMenuSection(title = stringResource(R.string.layout)) {
+ ProxyMenuRadioRow(
+ title = stringResource(R.string.single),
+ selected = proxyLine == 1,
+ onClick = { onProxyLineChanged(1) },
+ )
+ ProxyMenuRadioRow(
+ title = stringResource(R.string.doubles),
+ selected = proxyLine == 2,
+ onClick = { onProxyLineChanged(2) },
+ )
+ ProxyMenuRadioRow(
+ title = stringResource(R.string.multiple),
+ selected = proxyLine == 3,
+ onClick = { onProxyLineChanged(3) },
+ )
+ }
+
+ ProxyMenuSection(title = stringResource(R.string.sort)) {
+ ProxyMenuRadioRow(
+ title = stringResource(R.string.default_),
+ selected = proxySort == ProxySort.Default,
+ onClick = { onProxySortChanged(ProxySort.Default) },
+ )
+ ProxyMenuRadioRow(
+ title = stringResource(R.string.name),
+ selected = proxySort == ProxySort.Title,
+ onClick = { onProxySortChanged(ProxySort.Title) },
+ )
+ ProxyMenuRadioRow(
+ title = stringResource(R.string.delay),
+ selected = proxySort == ProxySort.Delay,
+ onClick = { onProxySortChanged(ProxySort.Delay) },
+ )
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+}
+
+@Composable
+private fun ProxyMenuSection(title: String, content: @Composable () -> Unit) {
+ Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(top = 8.dp, bottom = 8.dp),
+ )
+ content()
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+}
- fun requestUrlTesting() {
- urlTesting = true
+@Composable
+private fun ProxyMenuCheckboxRow(title: String, checked: Boolean, onClick: () -> Unit) {
+ Row(
+ modifier =
+ Modifier.fillMaxWidth()
+ .toggleable(value = checked, onValueChange = { onClick() }, role = Role.Checkbox)
+ .padding(vertical = 6.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Checkbox(checked = checked, onCheckedChange = null)
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(text = title, style = MaterialTheme.typography.bodyLarge)
+ }
+}
- requests.trySend(Request.UrlTest(binding.pagesView.currentItem))
+@Composable
+private fun ProxyMenuRadioRow(title: String, selected: Boolean, onClick: () -> Unit) {
+ Row(
+ modifier =
+ Modifier.fillMaxWidth()
+ .selectable(selected = selected, onClick = onClick, role = Role.RadioButton)
+ .padding(vertical = 6.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ RadioButton(selected = selected, onClick = null)
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(text = title, style = MaterialTheme.typography.bodyLarge)
+ }
+}
- updateUrlTestButtonStatus()
+private fun columnsForProxyLine(proxyLine: Int): Int =
+ when (proxyLine) {
+ 1 -> 1
+ 2 -> 2
+ else -> 3
}
- private fun updateUrlTestButtonStatus() {
- if (verticalBottomScrolled || horizontalScrolling || urlTesting) {
- binding.urlTestFloatView.hide()
- } else {
- binding.urlTestFloatView.show()
+@PreviewMihomo
+@Composable
+private fun ProxyScreenPreview() = MihomoTheme {
+ val selectedBackground = MaterialTheme.colorScheme.primary.toArgb()
+ val selectedControls = MaterialTheme.colorScheme.onPrimary.toArgb()
+ val unselectedBackground = MaterialTheme.colorScheme.surface.toArgb()
+ val unselectedControls = MaterialTheme.colorScheme.onSurface.toArgb()
+ val groups =
+ remember(selectedBackground, selectedControls, unselectedBackground, unselectedControls) {
+ listOf(
+ ProxyGroupUiState().apply {
+ selectable = true
+ items =
+ listOf(
+ ProxyItemUiState(
+ key = "auto",
+ title = "Auto",
+ subtitle = "URLTest(HK-01)",
+ delayText = "48",
+ background = selectedBackground,
+ controls = selectedControls,
+ ),
+ ProxyItemUiState(
+ key = "hk-01",
+ title = "Hong Kong 01",
+ subtitle = "BGP | 1.2x",
+ delayText = "62",
+ background = unselectedBackground,
+ controls = unselectedControls,
+ ),
+ ProxyItemUiState(
+ key = "jp-01",
+ title = "Japan 01",
+ subtitle = "Tokyo | IPLC",
+ delayText = "89",
+ background = unselectedBackground,
+ controls = unselectedControls,
+ ),
+ ProxyItemUiState(
+ key = "sg-01",
+ title = "Singapore 01",
+ subtitle = "Premium",
+ delayText = "74",
+ background = unselectedBackground,
+ controls = unselectedControls,
+ ),
+ )
+ },
+ ProxyGroupUiState().apply {
+ selectable = true
+ urlTesting = true
+ items =
+ listOf(
+ ProxyItemUiState(
+ key = "fallback-a",
+ title = "Fallback A",
+ subtitle = "Selector(Node-2)",
+ delayText = "128",
+ background = unselectedBackground,
+ controls = unselectedControls,
+ ),
+ ProxyItemUiState(
+ key = "fallback-b",
+ title = "Fallback B",
+ subtitle = "Selector(Node-4)",
+ delayText = "156",
+ background = unselectedBackground,
+ controls = unselectedControls,
+ ),
+ )
+ },
+ )
}
- if (urlTesting) {
- binding.urlTestView.visibility = View.GONE
- binding.urlTestProgressView.visibility = View.VISIBLE
- } else {
- binding.urlTestView.visibility = View.VISIBLE
- binding.urlTestProgressView.visibility = View.GONE
+ ProxyScreen(
+ groupNames = listOf("Auto", "Fallback"),
+ groups = groups,
+ currentPage = 0,
+ proxyLine = 2,
+ excludeNotSelectable = false,
+ proxySort = ProxySort.Delay,
+ overrideMode = TunnelState.Mode.Rule,
+ initialPage = 0,
+ onPageChanged = {},
+ onUrlTest = {},
+ onExcludeNotSelectableChanged = {},
+ onProxyLineChanged = {},
+ onProxySortChanged = {},
+ onOverrideModeSelected = {},
+ onProxySelected = { _, _ -> },
+ )
+}
+
+@PreviewMihomo
+@Composable
+private fun ProxyScreenEmptyPreview() = MihomoTheme {
+ ProxyScreen(
+ groupNames = emptyList(),
+ groups = emptyList(),
+ currentPage = 0,
+ proxyLine = 2,
+ excludeNotSelectable = false,
+ proxySort = ProxySort.Default,
+ overrideMode = null,
+ initialPage = 0,
+ onPageChanged = {},
+ onUrlTest = {},
+ onExcludeNotSelectableChanged = {},
+ onProxyLineChanged = {},
+ onProxySortChanged = {},
+ onOverrideModeSelected = {},
+ onProxySelected = { _, _ -> },
+ )
+}
+
+@PreviewMihomo
+@Composable
+private fun ProxyMenuSheetContentPreview() = MihomoTheme {
+ Surface {
+ Column {
+ ProxyMenuSheetContent(
+ overrideMode = TunnelState.Mode.Rule,
+ excludeNotSelectable = false,
+ proxyLine = 2,
+ proxySort = ProxySort.Delay,
+ onExcludeNotSelectableChanged = {},
+ onProxyLineChanged = {},
+ onProxySortChanged = {},
+ onOverrideModeSelected = {},
+ )
}
}
}
diff --git a/design/src/main/java/com/github/kr328/clash/design/adapter/ProxyAdapter.kt b/design/src/main/java/com/github/kr328/clash/design/adapter/ProxyAdapter.kt
deleted file mode 100644
index a411f87c82..0000000000
--- a/design/src/main/java/com/github/kr328/clash/design/adapter/ProxyAdapter.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.github.kr328.clash.design.adapter
-
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import com.github.kr328.clash.design.component.ProxyView
-import com.github.kr328.clash.design.component.ProxyViewConfig
-import com.github.kr328.clash.design.component.ProxyViewState
-
-class ProxyAdapter(private val config: ProxyViewConfig, private val clicked: (String) -> Unit) :
- RecyclerView.Adapter() {
- class Holder(val view: ProxyView) : RecyclerView.ViewHolder(view)
-
- var selectable: Boolean = false
- var states: List = emptyList()
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
- return Holder(ProxyView(config.context, config))
- }
-
- override fun onBindViewHolder(holder: Holder, position: Int) {
- val current = states[position]
-
- holder.view.apply {
- state = current
-
- setOnClickListener { clicked(current.proxy.name) }
-
- val isSelector = selectable
-
- isFocusable = isSelector
- isClickable = isSelector
-
- current.update(true)
- }
- }
-
- override fun getItemCount(): Int {
- return states.size
- }
-}
diff --git a/design/src/main/java/com/github/kr328/clash/design/adapter/ProxyPageAdapter.kt b/design/src/main/java/com/github/kr328/clash/design/adapter/ProxyPageAdapter.kt
deleted file mode 100644
index a4fa2d6c4d..0000000000
--- a/design/src/main/java/com/github/kr328/clash/design/adapter/ProxyPageAdapter.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package com.github.kr328.clash.design.adapter
-
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import com.github.kr328.clash.core.model.Proxy
-import com.github.kr328.clash.design.R
-import com.github.kr328.clash.design.component.ProxyPageFactory
-import com.github.kr328.clash.design.component.ProxyViewConfig
-import com.github.kr328.clash.design.component.ProxyViewState
-import com.github.kr328.clash.design.model.ProxyPageState
-import com.github.kr328.clash.design.model.ProxyState
-import com.github.kr328.clash.design.ui.Surface
-import com.github.kr328.clash.design.util.addScrolledToBottomObserver
-import com.github.kr328.clash.design.util.bindInsets
-import com.github.kr328.clash.design.util.firstVisibleView
-import com.github.kr328.clash.design.util.getPixels
-import com.github.kr328.clash.design.util.invalidateChildren
-import com.github.kr328.clash.design.util.swapDataSet
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-class ProxyPageAdapter(
- private val surface: Surface,
- private val config: ProxyViewConfig,
- private val adapters: List,
- private val stateChanged: (Int) -> Unit,
-) : RecyclerView.Adapter() {
- private val factory = ProxyPageFactory(config)
- private var parent: RecyclerView? = null
-
- val states = List(adapters.size) { ProxyPageState() }
-
- suspend fun updateAdapter(
- position: Int,
- proxies: List,
- selectable: Boolean,
- parent: ProxyState,
- links: Map,
- ) {
- val states =
- withContext(Dispatchers.Default) {
- proxies.map {
- val link = if (it.type.group) links[it.name] else null
-
- ProxyViewState(config, it, parent, link)
- }
- }
-
- withContext(Dispatchers.Main) {
- adapters[position].apply {
- this.selectable = selectable
- this.swapDataSet(this::states, states, false)
- }
-
- requestRedrawVisible()
- }
- }
-
- fun requestRedrawVisible() {
- factory.fromRoot(parent?.firstVisibleView ?: return).recyclerView.invalidateChildren()
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProxyPageFactory.Holder {
- val holder = factory.newInstance()
-
- val toolbarHeight = config.context.getPixels(R.dimen.toolbar_height)
- val tabHeight = config.context.getPixels(R.dimen.tab_layout_height)
-
- holder.recyclerView.bindInsets(surface, toolbarHeight + tabHeight)
- holder.recyclerView.addScrolledToBottomObserver { view, bottom ->
- val position = view.position
- val state = states[position]
-
- if (state.bottom != bottom) {
- state.bottom = bottom
-
- stateChanged(position)
- }
- }
-
- return holder
- }
-
- override fun onBindViewHolder(holder: ProxyPageFactory.Holder, position: Int) {
- val adapter = adapters[position]
-
- states[position].bottom = false
-
- holder.recyclerView.apply {
- this.position = position
- this.swapAdapter(adapter, false)
- }
- }
-
- override fun getItemCount(): Int {
- return adapters.size
- }
-
- override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
- this.parent = recyclerView
-
- recyclerView.isFocusable = false
- }
-
- override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
- this.parent = null
- }
-
- private var RecyclerView.position: Int
- get() {
- return tag as? Int ?: -1
- }
- set(value) {
- tag = value
- }
-}
diff --git a/design/src/main/java/com/github/kr328/clash/design/component/ProxyMenu.kt b/design/src/main/java/com/github/kr328/clash/design/component/ProxyMenu.kt
deleted file mode 100644
index 7aa363faf4..0000000000
--- a/design/src/main/java/com/github/kr328/clash/design/component/ProxyMenu.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-package com.github.kr328.clash.design.component
-
-import android.content.Context
-import android.view.MenuItem
-import android.view.View
-import androidx.appcompat.widget.PopupMenu
-import com.github.kr328.clash.core.model.ProxySort
-import com.github.kr328.clash.core.model.TunnelState
-import com.github.kr328.clash.design.ProxyDesign
-import com.github.kr328.clash.design.R
-import com.github.kr328.clash.design.store.UiStore
-import kotlinx.coroutines.channels.Channel
-
-class ProxyMenu(
- context: Context,
- menuView: View,
- mode: TunnelState.Mode?,
- private val uiStore: UiStore,
- private val requests: Channel,
- private val updateConfig: () -> Unit,
-) : PopupMenu.OnMenuItemClickListener {
- private val menu = PopupMenu(context, menuView)
-
- fun show() {
- menu.show()
- }
-
- override fun onMenuItemClick(item: MenuItem): Boolean {
- item.isChecked = !item.isChecked
-
- when (item.itemId) {
- R.id.not_selectable -> {
- uiStore.proxyExcludeNotSelectable = item.isChecked
-
- requests.trySend(ProxyDesign.Request.ReLaunch)
- }
- R.id.single -> {
- uiStore.proxyLine = 1
-
- updateConfig()
-
- requests.trySend(ProxyDesign.Request.ReloadAll)
- }
- R.id.doubles -> {
- uiStore.proxyLine = 2
-
- updateConfig()
-
- requests.trySend(ProxyDesign.Request.ReloadAll)
- }
- R.id.multiple -> {
- uiStore.proxyLine = 3
-
- updateConfig()
-
- requests.trySend(ProxyDesign.Request.ReloadAll)
- }
- R.id.default_ -> {
- uiStore.proxySort = ProxySort.Default
-
- requests.trySend(ProxyDesign.Request.ReloadAll)
- }
- R.id.name -> {
- uiStore.proxySort = ProxySort.Title
-
- requests.trySend(ProxyDesign.Request.ReloadAll)
- }
- R.id.delay -> {
- uiStore.proxySort = ProxySort.Delay
-
- requests.trySend(ProxyDesign.Request.ReloadAll)
- }
- R.id.dont_modify -> {
- requests.trySend(ProxyDesign.Request.PatchMode(null))
- }
- R.id.direct_mode -> {
- requests.trySend(ProxyDesign.Request.PatchMode(TunnelState.Mode.Direct))
- }
- R.id.global_mode -> {
- requests.trySend(ProxyDesign.Request.PatchMode(TunnelState.Mode.Global))
- }
- R.id.rule_mode -> {
- requests.trySend(ProxyDesign.Request.PatchMode(TunnelState.Mode.Rule))
- }
- else -> return false
- }
-
- return true
- }
-
- init {
- menu.menuInflater.inflate(R.menu.menu_proxy, menu.menu)
-
- menu.menu.apply {
- findItem(R.id.not_selectable).isChecked = uiStore.proxyExcludeNotSelectable
-
- when (uiStore.proxyLine) {
- 1 -> findItem(R.id.single).isChecked = true
- 2 -> findItem(R.id.doubles).isChecked = true
- 3 -> findItem(R.id.multiple).isChecked = true
- }
-
- when (uiStore.proxySort) {
- ProxySort.Default -> findItem(R.id.default_).isChecked = true
- ProxySort.Title -> findItem(R.id.name).isChecked = true
- ProxySort.Delay -> findItem(R.id.delay).isChecked = true
- }
-
- when (mode) {
- null -> findItem(R.id.dont_modify).isChecked = true
- TunnelState.Mode.Direct -> findItem(R.id.direct_mode).isChecked = true
- TunnelState.Mode.Global -> findItem(R.id.global_mode).isChecked = true
- TunnelState.Mode.Rule -> findItem(R.id.rule_mode).isChecked = true
- else -> {}
- }
- }
-
- menu.setOnMenuItemClickListener(this)
- }
-}
diff --git a/design/src/main/java/com/github/kr328/clash/design/component/ProxyPageFactory.kt b/design/src/main/java/com/github/kr328/clash/design/component/ProxyPageFactory.kt
deleted file mode 100644
index 62045277eb..0000000000
--- a/design/src/main/java/com/github/kr328/clash/design/component/ProxyPageFactory.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.github.kr328.clash.design.component
-
-import android.view.View
-import android.view.ViewGroup
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.github.kr328.clash.design.view.VerticalScrollableHost
-
-class ProxyPageFactory(private val config: ProxyViewConfig) {
- class Holder(val recyclerView: RecyclerView, val root: View) : RecyclerView.ViewHolder(root)
-
- private val childrenPool = RecyclerView.RecycledViewPool()
-
- fun newInstance(): Holder {
- val root =
- VerticalScrollableHost(config.context).apply {
- layoutParams =
- ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT,
- )
- }
-
- val recyclerView =
- RecyclerView(config.context).apply {
- layoutParams =
- ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT,
- )
- }
-
- root.addView(recyclerView)
-
- recyclerView.apply {
- layoutManager =
- GridLayoutManager(config.context, 6).apply {
- spanSizeLookup =
- object : GridLayoutManager.SpanSizeLookup() {
- override fun getSpanSize(position: Int): Int {
- var grids = 0
- when (config.proxyLine) {
- 2 -> grids = 3
- 3 -> grids = 2
- }
- return if (config.proxyLine == 1) 6 else grids
- }
- }
- }
-
- setRecycledViewPool(childrenPool)
-
- clipToPadding = false
- }
-
- return Holder(recyclerView, root).apply { root.tag = this }
- }
-
- fun fromRoot(root: View): Holder {
- return root.tag!! as Holder
- }
-}
diff --git a/design/src/main/java/com/github/kr328/clash/design/component/ProxyView.kt b/design/src/main/java/com/github/kr328/clash/design/component/ProxyView.kt
deleted file mode 100644
index c954517d77..0000000000
--- a/design/src/main/java/com/github/kr328/clash/design/component/ProxyView.kt
+++ /dev/null
@@ -1,184 +0,0 @@
-package com.github.kr328.clash.design.component
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.Paint
-import android.graphics.Path
-import android.view.View
-import com.github.kr328.clash.common.compat.getDrawableCompat
-
-class ProxyView(context: Context, config: ProxyViewConfig) : View(context) {
-
- init {
- background = context.getDrawableCompat(config.clickableBackground)
- }
-
- var state: ProxyViewState? = null
-
- constructor(context: Context) : this(context, ProxyViewConfig(context, 2))
-
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- val state = state ?: return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
-
- val width =
- when (MeasureSpec.getMode(widthMeasureSpec)) {
- MeasureSpec.UNSPECIFIED -> resources.displayMetrics.widthPixels
- MeasureSpec.AT_MOST,
- MeasureSpec.EXACTLY -> MeasureSpec.getSize(widthMeasureSpec)
- else -> throw IllegalArgumentException("invalid measure spec")
- }
-
- state.paint.apply {
- reset()
-
- textSize = state.config.textSize
-
- getTextBounds("Stub!", 0, 1, state.rect)
- }
-
- val textHeight = state.rect.height()
- val exceptHeight =
- (state.config.layoutPadding * 2 +
- state.config.contentPadding * 2 +
- textHeight * 2 +
- state.config.textMargin)
- .toInt()
-
- val height =
- when (MeasureSpec.getMode(heightMeasureSpec)) {
- MeasureSpec.UNSPECIFIED -> exceptHeight
- MeasureSpec.AT_MOST,
- MeasureSpec.EXACTLY -> exceptHeight.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec))
- else -> throw IllegalArgumentException("invalid measure spec")
- }
-
- setMeasuredDimension(width, height)
- }
-
- override fun draw(canvas: Canvas) {
- val state = state ?: return super.draw(canvas)
-
- if (state.update(false)) postInvalidate()
-
- val width = width.toFloat()
- val height = height.toFloat()
-
- val paint = state.paint
-
- paint.reset()
-
- paint.color = state.background
- paint.style = Paint.Style.FILL
-
- // draw background
- canvas.apply {
- if (state.config.proxyLine == 1) {
- drawRect(0f, 0f, width, height, paint)
- } else {
- val path = state.path
-
- path.reset()
-
- path.addRoundRect(
- state.config.layoutPadding,
- state.config.layoutPadding,
- width - state.config.layoutPadding,
- height - state.config.layoutPadding,
- state.config.cardRadius,
- state.config.cardRadius,
- Path.Direction.CW,
- )
-
- paint.setShadowLayer(
- state.config.cardRadius,
- state.config.cardOffset,
- state.config.cardOffset,
- state.config.shadow,
- )
-
- drawPath(path, paint)
-
- clipPath(path)
- }
- }
-
- super.draw(canvas)
- }
-
- override fun onDraw(canvas: Canvas) {
- super.onDraw(canvas)
-
- val state = state ?: return
-
- val paint = state.paint
-
- val width = width.toFloat()
- val height = height.toFloat()
-
- paint.textSize = state.config.textSize
-
- // measure delay text bounds
- val delayCount =
- paint.breakText(
- state.delayText,
- false,
- (width - state.config.layoutPadding * 2 - state.config.contentPadding * 2).coerceAtLeast(
- 0f
- ),
- null,
- )
-
- state.paint.getTextBounds(state.delayText, 0, delayCount, state.rect)
-
- val delayWidth = state.rect.width()
-
- val mainTextWidth =
- (width -
- state.config.layoutPadding * 2 -
- state.config.contentPadding * 2 -
- delayWidth -
- state.config.textMargin * 2)
- .coerceAtLeast(0f)
-
- // measure title text bounds
- val titleCount = paint.breakText(state.title, false, mainTextWidth, null)
-
- // measure subtitle text bounds
- val subtitleCount = paint.breakText(state.subtitle, false, mainTextWidth, null)
-
- // text draw measure
- val textOffset = (paint.descent() + paint.ascent()) / 2
-
- paint.reset()
-
- paint.textSize = state.config.textSize
- paint.isAntiAlias = true
- paint.color = state.controls
-
- // draw delay
- canvas.apply {
- val x = width - state.config.layoutPadding - state.config.contentPadding - delayWidth
- val y = height / 2f - textOffset
-
- drawText(state.delayText, 0, delayCount, x, y, paint)
- }
-
- // draw title
- canvas.apply {
- val x = state.config.layoutPadding + state.config.contentPadding
- val y =
- state.config.layoutPadding + (height - state.config.layoutPadding * 2) / 3f - textOffset
-
- drawText(state.title, 0, titleCount, x, y, paint)
- }
-
- // draw subtitle
- canvas.apply {
- val x = state.config.layoutPadding + state.config.contentPadding
- val y =
- state.config.layoutPadding + (height - state.config.layoutPadding * 2) / 3f * 2 - textOffset
-
- drawText(state.subtitle, 0, subtitleCount, x, y, paint)
- }
- }
-}
diff --git a/design/src/main/res/layout/design_proxy.xml b/design/src/main/res/layout/design_proxy.xml
deleted file mode 100644
index 306444f971..0000000000
--- a/design/src/main/res/layout/design_proxy.xml
+++ /dev/null
@@ -1,134 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file