diff --git a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt index b4c1e6b..253b6d8 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt @@ -1,16 +1,19 @@ package com.github.kr328.clash import android.os.Bundle -import androidx.core.view.isEmpty +import android.util.DisplayMetrics +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView import com.github.kr328.clash.adapter.AbstractProxyAdapter import com.github.kr328.clash.adapter.GridProxyAdapter -import com.github.kr328.clash.core.model.ProxyGroup -import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.adapter.ProxyChipAdapter import com.github.kr328.clash.remote.withClash import com.github.kr328.clash.utils.ProxySorter -import com.google.android.material.chip.Chip import kotlinx.android.synthetic.main.activity_proxies.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -18,6 +21,7 @@ class ProxiesActivity : BaseActivity() { private val activity: ProxiesActivity get() = this private var maskedGroup: MutableSet = mutableSetOf() + private val updateChipChannel = Channel(Channel.CONFLATED) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -34,6 +38,58 @@ class ProxiesActivity : BaseActivity() { } } } + + mainList.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + updateChipChannel.offer(Unit) + } + }) + + chipList.adapter = ProxyChipAdapter(this) { + launch { + (mainList.adapter!! as AbstractProxyAdapter).getGroupPosition(it)?.also { + val scroller = (object : LinearSmoothScroller(activity) { + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } + + override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float { + return 40f / displayMetrics!!.densityDpi + } + + init { + targetPosition = it + } + }) + + mainList.layoutManager?.startSmoothScroll(scroller) + } + } + } + chipList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + chipList.itemAnimator?.changeDuration = 80 + + launch { + var currentChecked = "" + + while (isActive) { + updateChipChannel.receive() + + val currentGroup = (mainList.adapter!! as AbstractProxyAdapter).getCurrentGroup() + + if (currentChecked == currentGroup) + continue + + currentChecked = currentGroup + + (chipList.adapter!! as ProxyChipAdapter).apply { + selected = currentChecked + + (chipList.layoutManager!! as LinearLayoutManager) + .scrollToPositionWithOffset(chips.indexOf(currentChecked), 0) + } + } + } } override fun onStart() { @@ -58,45 +114,13 @@ class ProxiesActivity : BaseActivity() { sorter.sort(proxies.toList()) } - if (chipGroup.isEmpty()) { - filtered.map(ProxyGroup::name).forEach { - val chip = Chip(activity).apply { - text = it - - chipBackgroundColor = getColorStateList(R.color.proxies_chip_colors) - rippleColor = getColorStateList(R.color.proxies_chip_colors) - setTextColor(getColorStateList(R.color.proxies_chip_text_colors)) - checkedIcon = null - - isCheckable = true - isClickable = true - isFocusable = true - isChecked = !maskedGroup.contains(it) - - setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - maskedGroup.remove(it) - } else { - maskedGroup.add(it) - } - - refreshList(it) - } - } - - chipGroup.addView(chip) - } - } - (mainList.adapter!! as AbstractProxyAdapter).apply { root = filtered.filter { !maskedGroup.contains(it.name) } applyChange() - - if (scrollTo != null) { - scrollToGroup(scrollTo) - } } + + (chipList.adapter!! as ProxyChipAdapter).chips = filtered.map { it.name } } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt index 35b9fa7..06c980a 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt @@ -7,5 +7,6 @@ interface AbstractProxyAdapter { var onSelectProxyListener: suspend (String, String) -> Unit suspend fun applyChange() - suspend fun scrollToGroup(name: String) + suspend fun getGroupPosition(name: String): Int? + suspend fun getCurrentGroup(): String } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt index 3615a64..465b482 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt @@ -14,7 +14,6 @@ import com.github.kr328.clash.ProxiesActivity import com.github.kr328.clash.R import com.github.kr328.clash.core.model.Proxy import com.github.kr328.clash.core.model.ProxyGroup -import com.github.kr328.clash.core.utils.Log import com.google.android.material.card.MaterialCardView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -25,13 +24,18 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) RecyclerView.Adapter(), AbstractProxyAdapter { private interface RenderInfo { val name: String + val group: String + } + + private data class ProxyGroupInfo(override val name: String) : RenderInfo { + override val group: String + get() = name } - private data class ProxyGroupInfo(override val name: String) : RenderInfo private data class ProxyInfo( override val name: String, val type: Proxy.Type, - val group: String, + override val group: String, val selectable: Boolean, val delay: Short, val active: Boolean @@ -115,17 +119,24 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) } } - override suspend fun scrollToGroup(name: String) { - val position = withContext(Dispatchers.Default) { + override suspend fun getGroupPosition(name: String): Int? { + return withContext(Dispatchers.Default) { renderList.mapIndexed { index, p -> - if ( p is ProxyGroupInfo && p.name == name ) + if (p is ProxyGroupInfo && p.name == name) index else -1 }.singleOrNull { it >= 0 } - } ?: return + } + } - layoutManager.scrollToPositionWithOffset(position, 0) + override suspend fun getCurrentGroup(): String { + val position = layoutManager.findFirstCompletelyVisibleItemPosition() + + if (position < 0) + return "" + + return renderList[position].group } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt new file mode 100644 index 0000000..b3061f7 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt @@ -0,0 +1,75 @@ +package com.github.kr328.clash.adapter + +import android.content.Context +import android.graphics.Color +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.recyclerview.widget.RecyclerView +import com.github.kr328.clash.R +import com.google.android.material.card.MaterialCardView + +class ProxyChipAdapter( + private val context: Context, + private val onClick: (String) -> Unit +) : + RecyclerView.Adapter() { + var chips = listOf() + var selected: String = "" + set(value) { + val lastIndex = chips.indexOf(field) + val newIndex = chips.indexOf(value) + + field = value + + if (lastIndex >= 0) + notifyItemChanged(lastIndex) + if (newIndex >= 0) + notifyItemChanged(newIndex) + } + + @ColorInt + private val colorOnSurface: Int + + init { + val typedValue = TypedValue() + + context.theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true) + colorOnSurface = typedValue.data + } + + class Holder(root: View) : RecyclerView.ViewHolder(root) { + val card: MaterialCardView = root.findViewById(R.id.root) + val title: TextView = root.findViewById(android.R.id.title) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { + val layoutInflater = LayoutInflater.from(context) + + return Holder(layoutInflater.inflate(R.layout.adapter_proxies_chip, parent, false)) + } + + override fun getItemCount(): Int { + return chips.size + } + + override fun onBindViewHolder(holder: Holder, position: Int) { + val current = chips[position] + + holder.title.text = current + holder.card.setOnClickListener { + onClick(current) + } + + if (selected == current) { + holder.title.setTextColor(Color.WHITE) + holder.card.setCardBackgroundColor(context.getColor(R.color.primaryCardColorStarted)) + } else { + holder.title.setTextColor(colorOnSurface) + holder.card.setCardBackgroundColor(context.getColor(R.color.chipBackgroundColor)) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/simple_alpha.xml b/app/src/main/res/anim/simple_alpha.xml new file mode 100644 index 0000000..e370f2b --- /dev/null +++ b/app/src/main/res/anim/simple_alpha.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_proxies.xml b/app/src/main/res/layout/activity_proxies.xml index 70d6b38..b8d0140 100644 --- a/app/src/main/res/layout/activity_proxies.xml +++ b/app/src/main/res/layout/activity_proxies.xml @@ -17,20 +17,11 @@ android:layout_height="wrap_content" /> - - - - + android:layout_marginBottom="10dp"/> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/page_proxies.xml b/app/src/main/res/layout/page_proxies.xml new file mode 100644 index 0000000..f14841a --- /dev/null +++ b/app/src/main/res/layout/page_proxies.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dbcda18..01c9651 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,6 +74,14 @@ New Profile Edit Profile Clash Start Failure + Refresh + Group Order + Proxy Order + Default + Delay + Direct + Global + Rule Start URL Provider Failure diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 46e83aa..648da75 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -2,7 +2,6 @@ package com.github.kr328.clash.core import android.content.Context import bridge.Bridge -import bridge.EventPoll import com.github.kr328.clash.core.event.EventStream import com.github.kr328.clash.core.event.LogEvent import com.github.kr328.clash.core.model.General @@ -15,7 +14,6 @@ import com.github.kr328.clash.core.transact.ProxyGroupCollectionImpl import kotlinx.coroutines.CompletableDeferred import java.io.File import java.io.InputStream -import java.util.concurrent.CompletableFuture object Clash { private var initialized = false diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt index da51f1e..aa22ba8 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt @@ -1,8 +1,5 @@ package com.github.kr328.clash.core.model -import android.os.Parcel -import android.os.Parcelable -import com.github.kr328.clash.core.serialization.MergedParcels import kotlinx.serialization.Serializable @Serializable diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupList.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupList.kt index 3f09873..194af0d 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupList.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupList.kt @@ -6,7 +6,7 @@ import com.github.kr328.clash.core.serialization.MergedParcels import kotlinx.serialization.Serializable @Serializable -data class ProxyGroupList(val list: List): Parcelable { +data class ProxyGroupList(val list: List) : Parcelable { override fun writeToParcel(parcel: Parcel, flags: Int) { MergedParcels.dump(serializer(), this, parcel) } diff --git a/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt b/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt index a609774..3221d53 100644 --- a/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt +++ b/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt @@ -1,7 +1,6 @@ package com.github.kr328.clash.core.serialization import android.os.Parcel -import com.github.kr328.clash.core.utils.Log import kotlinx.serialization.* import kotlinx.serialization.modules.EmptyModule import kotlinx.serialization.modules.SerialModule @@ -18,8 +17,6 @@ object MergedParcels : AbstractSerialFormat(EmptyModule) { parcel.writeStringList(encoder.getStringList()) parcel.appendFrom(data, 0, data.dataSize()) - - Log.i("Send ${parcel.dataSize()} bytes") } finally { data.recycle() } diff --git a/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt b/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt index 2319d6d..5319f91 100644 --- a/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt +++ b/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt @@ -2,7 +2,6 @@ package com.github.kr328.clash.core.transact import bridge.DoneCallback import kotlinx.coroutines.CompletableDeferred -import java.util.concurrent.CompletableFuture class DoneCallbackImpl : DoneCallback, CompletableDeferred by CompletableDeferred() { override fun doneWithError(e: Exception?) { diff --git a/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt b/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt index d23f4d4..c003b54 100644 --- a/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt +++ b/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt @@ -3,10 +3,21 @@ package com.github.kr328.clash.core.utils import com.github.kr328.clash.core.Constants.TAG object Log { - fun i(message: String, throwable: Throwable? = null) = android.util.Log.i(TAG, message, throwable) - fun w(message: String, throwable: Throwable? = null) = android.util.Log.w(TAG, message, throwable) - fun e(message: String, throwable: Throwable? = null) = android.util.Log.e(TAG, message, throwable) - fun d(message: String, throwable: Throwable? = null) = android.util.Log.d(TAG, message, throwable) - fun v(message: String, throwable: Throwable? = null) = android.util.Log.v(TAG, message, throwable) - fun wtf(message: String, throwable: Throwable? = null) = android.util.Log.wtf(TAG, message, throwable) + fun i(message: String, throwable: Throwable? = null) = + android.util.Log.i(TAG, message, throwable) + + fun w(message: String, throwable: Throwable? = null) = + android.util.Log.w(TAG, message, throwable) + + fun e(message: String, throwable: Throwable? = null) = + android.util.Log.e(TAG, message, throwable) + + fun d(message: String, throwable: Throwable? = null) = + android.util.Log.d(TAG, message, throwable) + + fun v(message: String, throwable: Throwable? = null) = + android.util.Log.v(TAG, message, throwable) + + fun wtf(message: String, throwable: Throwable? = null) = + android.util.Log.wtf(TAG, message, throwable) }