Add CI/CD configuration and API documentation
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<manifest package="com.github.kr328.clash.design" />
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import com.github.kr328.clash.design.adapter.AppAdapter
|
||||
import com.github.kr328.clash.design.component.AccessControlMenu
|
||||
import com.github.kr328.clash.design.databinding.DesignAccessControlBinding
|
||||
import com.github.kr328.clash.design.databinding.DialogSearchBinding
|
||||
import com.github.kr328.clash.design.dialog.FullScreenDialog
|
||||
import com.github.kr328.clash.design.model.AppInfo
|
||||
import com.github.kr328.clash.design.store.UiStore
|
||||
import com.github.kr328.clash.design.util.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
||||
class AccessControlDesign(
|
||||
context: Context,
|
||||
uiStore: UiStore,
|
||||
private val selected: MutableSet<String>,
|
||||
) : Design<AccessControlDesign.Request>(context) {
|
||||
enum class Request {
|
||||
ReloadApps,
|
||||
SelectAll,
|
||||
SelectNone,
|
||||
SelectInvert,
|
||||
Import,
|
||||
Export,
|
||||
}
|
||||
|
||||
private val binding = DesignAccessControlBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
private val adapter = AppAdapter(context, selected)
|
||||
|
||||
private val menu: AccessControlMenu by lazy {
|
||||
AccessControlMenu(context, binding.menuView, uiStore, requests)
|
||||
}
|
||||
|
||||
val apps: List<AppInfo>
|
||||
get() = adapter.apps
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
suspend fun patchApps(apps: List<AppInfo>) {
|
||||
adapter.swapDataSet(adapter::apps, apps, false)
|
||||
}
|
||||
|
||||
suspend fun rebindAll() {
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.rebindAll()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.mainList.recyclerList.also {
|
||||
it.bindAppBarElevation(binding.activityBarLayout)
|
||||
it.applyLinearAdapter(context, adapter)
|
||||
}
|
||||
|
||||
binding.menuView.setOnClickListener {
|
||||
menu.show()
|
||||
}
|
||||
|
||||
binding.searchView.setOnClickListener {
|
||||
launch {
|
||||
try {
|
||||
requestSearch()
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
rebindAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestSearch() {
|
||||
coroutineScope {
|
||||
val binding = DialogSearchBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
val adapter = AppAdapter(context, selected)
|
||||
val dialog = FullScreenDialog(context)
|
||||
val filter = Channel<Unit>(Channel.CONFLATED)
|
||||
|
||||
dialog.setContentView(binding.root)
|
||||
|
||||
binding.surface = dialog.surface
|
||||
binding.mainList.applyLinearAdapter(context, adapter)
|
||||
binding.keywordView.addTextChangedListener {
|
||||
filter.trySend(Unit)
|
||||
}
|
||||
binding.closeView.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
cancel()
|
||||
}
|
||||
|
||||
dialog.setOnShowListener {
|
||||
binding.keywordView.requestTextInput()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
|
||||
while (isActive) {
|
||||
filter.receive()
|
||||
|
||||
val keyword = binding.keywordView.text?.toString() ?: ""
|
||||
|
||||
val apps: List<AppInfo> = if (keyword.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
withContext(Dispatchers.Default) {
|
||||
apps.filter {
|
||||
it.label.contains(keyword, ignoreCase = true) ||
|
||||
it.packageName.contains(keyword, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapter.patchDataSet(adapter::apps, apps, false, AppInfo::packageName)
|
||||
|
||||
delay(200)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import com.github.kr328.clash.design.databinding.DesignSettingsCommonBinding
|
||||
import com.github.kr328.clash.design.preference.category
|
||||
import com.github.kr328.clash.design.preference.clickable
|
||||
import com.github.kr328.clash.design.preference.preferenceScreen
|
||||
import com.github.kr328.clash.design.preference.tips
|
||||
import com.github.kr328.clash.design.util.applyFrom
|
||||
import com.github.kr328.clash.design.util.bindAppBarElevation
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.root
|
||||
|
||||
class ApkBrokenDesign(context: Context) : Design<ApkBrokenDesign.Request>(context) {
|
||||
data class Request(val url: String)
|
||||
|
||||
private val binding = DesignSettingsCommonBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
init {
|
||||
binding.surface = surface
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.scrollRoot.bindAppBarElevation(binding.activityBarLayout)
|
||||
|
||||
val screen = preferenceScreen(context) {
|
||||
tips(R.string.application_broken_tips)
|
||||
|
||||
category(R.string.reinstall)
|
||||
|
||||
clickable(
|
||||
title = R.string.github_releases,
|
||||
summary = R.string.meta_github_url
|
||||
) {
|
||||
clicked {
|
||||
requests.trySend(Request(context.getString(R.string.meta_github_url)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.content.addView(screen.root)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import com.github.kr328.clash.design.databinding.DesignAppCrashedBinding
|
||||
import com.github.kr328.clash.design.util.applyFrom
|
||||
import com.github.kr328.clash.design.util.bindAppBarElevation
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.root
|
||||
|
||||
class AppCrashedDesign(context: Context) : Design<Unit>(context) {
|
||||
private val binding = DesignAppCrashedBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
fun setAppLogs(logs: String) {
|
||||
binding.logsView.text = logs
|
||||
}
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.scrollRoot.bindAppBarElevation(binding.activityBarLayout)
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import com.github.kr328.clash.design.databinding.DesignSettingsCommonBinding
|
||||
import com.github.kr328.clash.design.model.Behavior
|
||||
import com.github.kr328.clash.design.model.DarkMode
|
||||
import com.github.kr328.clash.design.preference.*
|
||||
import com.github.kr328.clash.design.store.UiStore
|
||||
import com.github.kr328.clash.design.util.applyFrom
|
||||
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.service.store.ServiceStore
|
||||
|
||||
class AppSettingsDesign(
|
||||
context: Context,
|
||||
uiStore: UiStore,
|
||||
srvStore: ServiceStore,
|
||||
behavior: Behavior,
|
||||
running: Boolean,
|
||||
) : Design<AppSettingsDesign.Request>(context) {
|
||||
enum class Request {
|
||||
ReCreateAllActivities
|
||||
}
|
||||
|
||||
private val binding = DesignSettingsCommonBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
init {
|
||||
binding.surface = surface
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.scrollRoot.bindAppBarElevation(binding.activityBarLayout)
|
||||
|
||||
val screen = preferenceScreen(context) {
|
||||
category(R.string.behavior)
|
||||
|
||||
switch(
|
||||
value = behavior::autoRestart,
|
||||
icon = R.drawable.ic_baseline_restore,
|
||||
title = R.string.auto_restart,
|
||||
summary = R.string.allow_clash_auto_restart,
|
||||
)
|
||||
|
||||
category(R.string.interface_)
|
||||
|
||||
selectableList(
|
||||
value = uiStore::darkMode,
|
||||
values = DarkMode.values(),
|
||||
valuesText = arrayOf(
|
||||
R.string.follow_system_android_10,
|
||||
R.string.always_light,
|
||||
R.string.always_dark
|
||||
),
|
||||
icon = R.drawable.ic_baseline_brightness_4,
|
||||
title = R.string.dark_mode
|
||||
) {
|
||||
listener = OnChangedListener {
|
||||
requests.trySend(Request.ReCreateAllActivities)
|
||||
}
|
||||
}
|
||||
|
||||
category(R.string.service)
|
||||
|
||||
switch(
|
||||
value = srvStore::dynamicNotification,
|
||||
icon = R.drawable.ic_baseline_domain,
|
||||
title = R.string.show_traffic,
|
||||
summary = R.string.show_traffic_summary
|
||||
) {
|
||||
enabled = !running
|
||||
}
|
||||
}
|
||||
|
||||
binding.content.addView(screen.root)
|
||||
}
|
||||
}
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
// import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.github.kr328.clash.common.compat.isAllowForceDarkCompat
|
||||
import com.github.kr328.clash.common.compat.isLightNavigationBarCompat
|
||||
import com.github.kr328.clash.common.compat.isLightStatusBarsCompat
|
||||
import com.github.kr328.clash.common.compat.isSystemBarsTranslucentCompat
|
||||
import com.github.kr328.clash.design.adapter.OrdersDataAdapter
|
||||
import com.github.kr328.clash.design.adapter.PlanDataAdapter
|
||||
import com.github.kr328.clash.design.adapter.TicketDetailAdapter
|
||||
import com.github.kr328.clash.design.adapter.TicketsDataAdapter
|
||||
import com.github.kr328.clash.design.databinding.ActivityBaseListBinding
|
||||
// import com.github.kr328.clash.design.databinding.DesignSettingsCommonBinding
|
||||
import com.github.kr328.clash.design.ui.DayNight
|
||||
import com.github.kr328.clash.design.ui.Surface
|
||||
import com.github.kr328.clash.design.util.applyFrom
|
||||
// import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.resolveThemedBoolean
|
||||
import com.github.kr328.clash.design.util.resolveThemedColor
|
||||
import com.github.kr328.clash.design.util.root
|
||||
import com.github.kr328.clash.design.util.setOnInsertsChangedListener
|
||||
// import com.github.kr328.clash.design.view.ActivityBarLayout
|
||||
import com.github.kr328.clash.network.OrderData
|
||||
import com.github.kr328.clash.network.PlanData
|
||||
import com.github.kr328.clash.network.TicketMessage
|
||||
import com.github.kr328.clash.network.TicketsData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
// 这是一个通用的 Activity 用于加载和刷新列表数据
|
||||
abstract class BaseListActivity<T> : AppCompatActivity() {
|
||||
|
||||
private var dayNight: DayNight = DayNight.Day
|
||||
|
||||
val surface = Surface()
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||
private lateinit var adapter: RecyclerView.Adapter<*>
|
||||
|
||||
private var currentPage = 1
|
||||
private var isLoading = false
|
||||
// 子类需要实现这些方法
|
||||
abstract fun createAdapter(): RecyclerView.Adapter<*>
|
||||
abstract suspend fun loadData(page: Int, callback: (List<T>?, Throwable?) -> Unit)
|
||||
|
||||
|
||||
abstract fun onCreate()
|
||||
|
||||
var mainBinding: ActivityBaseListBinding? = null
|
||||
|
||||
private var onRightClick: (() -> Unit)? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
applyDayNight()
|
||||
|
||||
|
||||
val binding = ActivityBaseListBinding
|
||||
.inflate(this.layoutInflater, this.root, false)
|
||||
|
||||
binding.activityBarLayout.applyFrom(this)
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.surface = surface
|
||||
|
||||
mainBinding = binding
|
||||
|
||||
recyclerView = binding.baseRecyclerView
|
||||
swipeRefreshLayout =binding.baseSwipeRefreshLayout
|
||||
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
adapter = createAdapter()
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
|
||||
onCreate()
|
||||
|
||||
this.window.decorView.setOnInsertsChangedListener {
|
||||
if (surface.insets != it) {
|
||||
surface.insets = it
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
swipeRefreshLayout.isRefreshing = true
|
||||
}
|
||||
loadData(1) { data, error ->
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
if (error != null) {
|
||||
showError(error)
|
||||
} else {
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
initList(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
refereshData()
|
||||
|
||||
}
|
||||
|
||||
fun requestRightClick(){
|
||||
// 使用安全调用,确保在 onRightClick 不为 null 时执行
|
||||
println("requestRightClick...")
|
||||
onRightClick?.invoke()
|
||||
println("requestRightClick invoke()...")
|
||||
}
|
||||
|
||||
fun setRightButtonVisible(onRightClickEvent: (() -> Unit) ){
|
||||
|
||||
println("setRightButtonVisible...")
|
||||
mainBinding?.rightButton?.visibility = View.VISIBLE
|
||||
onRightClick = onRightClickEvent
|
||||
mainBinding?.rightButton?.setOnClickListener {
|
||||
requestRightClick()
|
||||
}
|
||||
}
|
||||
|
||||
fun refereshData(){
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// 加载数据
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
swipeRefreshLayout.isRefreshing = true
|
||||
}
|
||||
loadData(currentPage) { data, error ->
|
||||
if (error != null) {
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
showError(error)
|
||||
} else {
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
initList(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun referTitle(tit: String){
|
||||
title = tit
|
||||
mainBinding?.activityBarLayout?.applyFrom(this)
|
||||
}
|
||||
private fun applyDayNight(config: Configuration = resources.configuration) {
|
||||
// val dayNight = theme.applyStyle(R.style.AppThemeLight, true) //默认白天模式
|
||||
|
||||
/*
|
||||
val dayNight = queryDayNight(config)
|
||||
when (dayNight) {
|
||||
DayNight.Night -> theme.applyStyle(R.style.AppThemeDark, true)
|
||||
DayNight.Day -> theme.applyStyle(R.style.AppThemeLight, true)
|
||||
}*/
|
||||
|
||||
window.isAllowForceDarkCompat = false
|
||||
window.isSystemBarsTranslucentCompat = true
|
||||
|
||||
window.statusBarColor = resolveThemedColor(android.R.attr.statusBarColor)
|
||||
window.navigationBarColor = resolveThemedColor(android.R.attr.navigationBarColor)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
window.isLightStatusBarsCompat = resolveThemedBoolean(android.R.attr.windowLightStatusBar)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 27) {
|
||||
window.isLightNavigationBarCompat = resolveThemedBoolean(android.R.attr.windowLightNavigationBar)
|
||||
}
|
||||
|
||||
this.dayNight = DayNight.Night //dayNight
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 初始化列表数据。
|
||||
* 如果数据为空,显示提示信息;否则,在UI线程上更新适配器数据。
|
||||
*
|
||||
* @param data 要显示的数据列表,可能为null
|
||||
*/
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun initList(data: List<T>?) {
|
||||
if (data.isNullOrEmpty()) {
|
||||
showEmptyDataToast()
|
||||
} else {
|
||||
updateAdapterDataOnUiThread(data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在UI线程上显示"没有更多数据"的Toast提示。
|
||||
*/
|
||||
private fun showEmptyDataToast() {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, "没有更多数据", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在UI线程上更新适配器数据。
|
||||
* 这个方法作为一个中间层,避免在runOnUiThread的lambda中直接捕获外部变量。
|
||||
*
|
||||
* @param data 要更新到适配器的数据列表
|
||||
*/
|
||||
private fun updateAdapterDataOnUiThread(data: List<T>) {
|
||||
runOnUiThread {
|
||||
doUpdateAdapterData(data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行实际的适配器数据更新操作。
|
||||
* 根据适配器类型,将数据设置到相应的适配器中,并通知数据集变化。
|
||||
*
|
||||
* @param data 要更新到适配器的数据列表
|
||||
*/
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun doUpdateAdapterData(data: List<T>) {
|
||||
when (adapter) {
|
||||
is PlanDataAdapter -> (adapter as PlanDataAdapter).setData(data as List<PlanData>)
|
||||
is OrdersDataAdapter -> (adapter as OrdersDataAdapter).setData(data as List<OrderData>)
|
||||
is TicketsDataAdapter -> (adapter as TicketsDataAdapter).setData(data as List<TicketsData>)
|
||||
is TicketDetailAdapter -> (adapter as TicketDetailAdapter).setData(data as List<TicketMessage>)
|
||||
else -> println("Unknown adapter type.")
|
||||
}
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun updateList(data: List<T>?) {
|
||||
|
||||
if (data.isNullOrEmpty()) {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, "没有更多数据", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
// 更新列表
|
||||
runOnUiThread {
|
||||
// 更新适配器的数据
|
||||
// adapter.addData(data) // 假设你实现了一个 setData 方法来更新适配器的数据
|
||||
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun showError(error: Throwable) {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, "加载失败: ${error.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
// 调用此方法来处理列表项点击
|
||||
// fun onListItemClicked(item: T) {
|
||||
// onItemClicked(item)
|
||||
// }
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.github.kr328.clash.design.ProfileDesign.Request
|
||||
import com.github.kr328.clash.design.databinding.ActivityConfigorderBinding
|
||||
import com.github.kr328.clash.design.databinding.ActivityPlanItemBinding
|
||||
import com.github.kr328.clash.design.databinding.ActivityProfileBinding
|
||||
import com.github.kr328.clash.design.ui.ToastDuration
|
||||
import com.github.kr328.clash.design.util.applyFrom
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.root
|
||||
import com.github.kr328.clash.network.ApiClient
|
||||
import com.github.kr328.clash.network.ApiService
|
||||
import com.github.kr328.clash.network.ConfigResponse
|
||||
import com.github.kr328.clash.network.PlanData
|
||||
import com.github.kr328.clash.network.SaveOrderRequest
|
||||
import com.github.kr328.clash.network.SubmitOrderResponse
|
||||
import com.github.kr328.clash.network.safeApiRequestCall
|
||||
import com.github.kr328.clash.utity.LoadingDialog
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ConfigOrderDesign (context: Context) : Design<ConfigOrderDesign.Request>(context) {
|
||||
|
||||
private var currentChoose: Map<String,String>? = null
|
||||
public var trade_no:String = ""
|
||||
enum class Request {
|
||||
SubmitOrder,
|
||||
}
|
||||
|
||||
private val binding = ActivityConfigorderBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
}
|
||||
|
||||
fun request(request: Request) {
|
||||
if (request == Request.SubmitOrder){
|
||||
//提交订单
|
||||
LoadingDialog.show(context, "正在下单中...")
|
||||
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val apiService = ApiClient.retrofit.create(ApiService::class.java)
|
||||
|
||||
safeApiRequestCall { apiService.saveOrder(PreferenceManager.loginauthData, SaveOrderRequest("${currentChoose?.get("period")}",(currentChoose?.get("plan_id") ?: "0").toInt(),""))}.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
LoadingDialog.hide()
|
||||
}
|
||||
|
||||
if(it != null && it.isSuccessful){
|
||||
if (it.body()?.data != null){
|
||||
//tradeNO 订单号 成功
|
||||
trade_no = it.body()?.data ?: ""
|
||||
requests.trySend(request)
|
||||
}
|
||||
}else{
|
||||
val errorinfo = (it?.errorBody()?.string())
|
||||
if (errorinfo != null){
|
||||
//解析错错误信息
|
||||
|
||||
val gson = Gson()
|
||||
val submitResponse = gson.fromJson(errorinfo, SubmitOrderResponse::class.java)
|
||||
|
||||
if (submitResponse?.message != null){
|
||||
withContext(Dispatchers.Main) {
|
||||
showToast("下单失败:${submitResponse.message}", ToastDuration.Long)
|
||||
}
|
||||
}
|
||||
}else{
|
||||
withContext(Dispatchers.Main) {
|
||||
showToast("下单失败:请求数据失败", ToastDuration.Long)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fillData(plan: PlanData?){
|
||||
|
||||
|
||||
binding.configorderPlanname = "商品名称:${plan?.name}"
|
||||
if (plan?.onetime_price != null && plan.onetime_price > 0) {
|
||||
binding.configorderPlantype = "类型/周期:一次性"
|
||||
binding.configorderPlantype2 = "一次性"
|
||||
}else{
|
||||
binding.configorderPlantype = "类型/周期:按月"
|
||||
binding.configorderPlantype2 = "按月"
|
||||
}
|
||||
|
||||
binding.configorderPlanname2 = "${plan?.name}"
|
||||
|
||||
binding.configorderPlantransfer = "商品流量:${plan?.transfer_enable} GB"
|
||||
if (plan?.onetime_price !=null) {
|
||||
binding.configorderPlanamount = "¥ ${plan?.onetime_price.toFloat()/100}0"
|
||||
}else{
|
||||
if (plan?.month_price !=null) {
|
||||
binding.configorderPlanamount = "¥ ${plan?.month_price.toFloat() / 100}0"
|
||||
}
|
||||
}
|
||||
|
||||
var selectedIndex = 0
|
||||
if (plan?.onetime_price != null && plan.onetime_price > 0) {
|
||||
//一次性
|
||||
val map = HashMap<String,String>()
|
||||
map.put("type","一次性");//存储key和value
|
||||
map.put("amount","${plan.onetime_price}");
|
||||
map.put("period","onetime_price");
|
||||
map.put("plan_id","${plan.id}");
|
||||
|
||||
val frameLayout = ActivityPlanItemBinding.inflate(context.layoutInflater, context.root, false)
|
||||
frameLayout.typeTextView.text = "一次性"
|
||||
frameLayout.amountTextView.text = "¥ ${ (map.get("amount")?.toDouble() ?: 0.0)/100}0"
|
||||
|
||||
binding.container.addView(frameLayout.root)
|
||||
|
||||
val child = binding.container.getChildAt(0)
|
||||
child.background = ContextCompat.getDrawable(
|
||||
context, R.drawable.card_border_selected
|
||||
)
|
||||
currentChoose = map
|
||||
|
||||
}else{
|
||||
val map = HashMap<String,String>()
|
||||
map.put("type","按月");//存储key和value
|
||||
map.put("amount","${plan?.month_price ?: 0}");
|
||||
map.put("period","month_price");
|
||||
map.put("plan_id","${plan?.id}");
|
||||
|
||||
currentChoose = map
|
||||
val map2 = HashMap<String,String>()
|
||||
map2.put("type","按季");//存储key和value
|
||||
map2.put("amount","${plan?.quarter_price ?: 0}");
|
||||
map2.put("period","quarter_price");
|
||||
map2.put("plan_id","${plan?.id}");
|
||||
|
||||
val map3 = HashMap<String,String>()
|
||||
map3.put("type","半年");//存储key和value
|
||||
map3.put("amount","${plan?.half_year_price ?: 0}");
|
||||
map3.put("period","half_year_price");
|
||||
map3.put("plan_id","${plan?.id}");
|
||||
|
||||
|
||||
val map4 = HashMap<String,String>()
|
||||
map4.put("type","一年");//存储key和value
|
||||
map4.put("amount","${plan?.year_price ?: 0}");
|
||||
map4.put("period","year_price");
|
||||
map4.put("plan_id","${plan?.id}");
|
||||
|
||||
val list = ArrayList<Map<String,String>>()
|
||||
list.add(map)
|
||||
|
||||
if((plan?.quarter_price ?: 0) > 0){
|
||||
list.add(map2)
|
||||
}
|
||||
|
||||
if((plan?.half_year_price ?: 0) > 0){
|
||||
list.add(map3)
|
||||
}
|
||||
|
||||
if((plan?.year_price ?: 0) > 0){
|
||||
list.add(map4)
|
||||
}
|
||||
|
||||
|
||||
for (i in list.indices) {
|
||||
// 设置点击事件和单选逻辑
|
||||
val frameLayout = ActivityPlanItemBinding.inflate(context.layoutInflater, context.root, false)
|
||||
val item = list[i]
|
||||
frameLayout.typeTextView.text = item.get("type")
|
||||
frameLayout.amountTextView.text ="¥ ${ (item.get("amount")?.toDouble() ?: 0.0)/100}0"
|
||||
|
||||
|
||||
|
||||
|
||||
frameLayout.planitemFrameLayout.setOnClickListener {
|
||||
selectedIndex = i
|
||||
currentChoose = item
|
||||
|
||||
binding.configorderPlantype2 = item.get("type")
|
||||
binding.configorderPlanamount = "¥ ${ (item.get("amount")?.toDouble() ?: 0.0)/100}0"
|
||||
|
||||
for (j in 0 until binding.container.childCount) {
|
||||
val child = binding.container.getChildAt(j)
|
||||
println("selectedIndex: ${selectedIndex} ${ child.background} ")
|
||||
child.background = ContextCompat.getDrawable(
|
||||
context,
|
||||
if (j == selectedIndex) R.drawable.card_border_selected else R.drawable.card_border
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.container.addView(frameLayout.root)
|
||||
|
||||
|
||||
if (i == 0){
|
||||
//默认选中第一个
|
||||
|
||||
frameLayout.planitemFrameLayout.background = ContextCompat.getDrawable(
|
||||
context, R.drawable.card_border_selected
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.github.kr328.clash.design.ui.Surface
|
||||
import com.github.kr328.clash.design.ui.ToastDuration
|
||||
import com.github.kr328.clash.design.util.setOnInsertsChangedListener
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
abstract class Design<R>(val context: Context) :
|
||||
CoroutineScope by CoroutineScope(Dispatchers.Unconfined) {
|
||||
abstract val root: View
|
||||
|
||||
val surface = Surface()
|
||||
val requests: Channel<R> = Channel(Channel.UNLIMITED)
|
||||
|
||||
suspend fun showToast(
|
||||
resId: Int,
|
||||
duration: ToastDuration,
|
||||
configure: Snackbar.() -> Unit = {}
|
||||
) {
|
||||
return showToast(context.getString(resId), duration, configure)
|
||||
}
|
||||
|
||||
suspend fun showToast(
|
||||
message: CharSequence,
|
||||
duration: ToastDuration,
|
||||
configure: Snackbar.() -> Unit = {}
|
||||
) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Snackbar.make(
|
||||
root,
|
||||
message,
|
||||
when (duration) {
|
||||
ToastDuration.Short -> Snackbar.LENGTH_SHORT
|
||||
ToastDuration.Long -> Snackbar.LENGTH_LONG
|
||||
ToastDuration.Indefinite -> Snackbar.LENGTH_INDEFINITE
|
||||
}
|
||||
).apply(configure).show()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
when (context) {
|
||||
is AppCompatActivity -> {
|
||||
context.window.decorView.setOnInsertsChangedListener {
|
||||
if (surface.insets != it) {
|
||||
surface.insets = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import com.github.kr328.clash.design.adapter.FileAdapter
|
||||
import com.github.kr328.clash.design.databinding.DesignFilesBinding
|
||||
import com.github.kr328.clash.design.databinding.DialogFilesMenuBinding
|
||||
import com.github.kr328.clash.design.dialog.AppBottomSheetDialog
|
||||
import com.github.kr328.clash.design.dialog.requestModelTextInput
|
||||
import com.github.kr328.clash.design.model.File
|
||||
import com.github.kr328.clash.design.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FilesDesign(context: Context) : Design<FilesDesign.Request>(context) {
|
||||
sealed class Request {
|
||||
data class OpenFile(val file: File) : Request()
|
||||
data class OpenDirectory(val file: File) : Request()
|
||||
data class RenameFile(val file: File) : Request()
|
||||
data class DeleteFile(val file: File) : Request()
|
||||
data class ImportFile(val file: File?) : Request()
|
||||
data class ExportFile(val file: File) : Request()
|
||||
|
||||
object PopStack : Request()
|
||||
}
|
||||
|
||||
private val binding = DesignFilesBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
private val adapter: FileAdapter = FileAdapter(context, this::requestOpen, this::requestMore)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
var configurationEditable: Boolean
|
||||
get() = binding.configurationEditable
|
||||
set(value) {
|
||||
binding.configurationEditable = value
|
||||
}
|
||||
|
||||
suspend fun swapFiles(files: List<File>, currentInBaseDir: Boolean) {
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.swapDataSet(adapter::files, files)
|
||||
binding.currentInBaseDir = currentInBaseDir
|
||||
}
|
||||
}
|
||||
|
||||
fun updateElapsed() {
|
||||
adapter.updateElapsed()
|
||||
}
|
||||
|
||||
suspend fun requestFileName(name: String): String {
|
||||
return context.requestModelTextInput(
|
||||
initial = name,
|
||||
title = context.getText(R.string.file_name),
|
||||
hint = context.getText(R.string.file_name),
|
||||
error = context.getText(R.string.invalid_file_name),
|
||||
validator = ValidatorFileName,
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.mainList.recyclerList.also {
|
||||
it.applyLinearAdapter(context, adapter)
|
||||
it.bindAppBarElevation(binding.activityBarLayout)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestOpen(file: File) {
|
||||
if (file.isDirectory) {
|
||||
requests.trySend(Request.OpenDirectory(file))
|
||||
} else {
|
||||
requests.trySend(Request.OpenFile(file))
|
||||
}
|
||||
}
|
||||
|
||||
fun requestRename(dialog: Dialog, file: File) {
|
||||
requests.trySend(Request.RenameFile(file))
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
fun requestImport(dialog: Dialog, file: File) {
|
||||
requests.trySend(Request.ImportFile(file))
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
fun requestExport(dialog: Dialog, file: File) {
|
||||
requests.trySend(Request.ExportFile(file))
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
fun requestDelete(dialog: Dialog, file: File) {
|
||||
requests.trySend(Request.DeleteFile(file))
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
fun requestNew() {
|
||||
requests.trySend(Request.ImportFile(null))
|
||||
}
|
||||
|
||||
private fun requestMore(file: File) {
|
||||
val dialog = AppBottomSheetDialog(context)
|
||||
|
||||
val binding = DialogFilesMenuBinding.inflate(context.layoutInflater)
|
||||
|
||||
binding.master = this
|
||||
binding.self = dialog
|
||||
binding.file = file
|
||||
binding.currentInBase = this.binding.currentInBaseDir
|
||||
binding.configurationEditable = this.binding.configurationEditable
|
||||
|
||||
dialog.setContentView(binding.root)
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import com.github.kr328.clash.design.ProxyDesign.Request
|
||||
import com.github.kr328.clash.design.databinding.ActivityH5webviewBinding
|
||||
import com.github.kr328.clash.design.databinding.DesignSettingsCommonBinding
|
||||
import com.github.kr328.clash.design.util.applyFrom
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.root
|
||||
|
||||
|
||||
class H5WebViewDesign(
|
||||
context: Context,
|
||||
openLink: (Uri) -> Unit,
|
||||
) : Design<H5WebViewDesign.Request>(context) {
|
||||
private val binding = ActivityH5webviewBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
enum class Request {
|
||||
OpenURL,ReferWebView
|
||||
}
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
init {
|
||||
binding.surface = surface
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
|
||||
binding.webview.webViewClient= WebViewClient()//目标的网页仍然在当前WebView中显
|
||||
|
||||
binding.webview.settings.apply {
|
||||
javaScriptEnabled = true // 启用 JavaScript
|
||||
domStorageEnabled = true // 启用 DOM 存储
|
||||
loadWithOverviewMode = true
|
||||
useWideViewPort = true
|
||||
allowContentAccess = true
|
||||
allowFileAccess = true
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
binding.webview.settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
binding.webview.canGoBack()
|
||||
// 设置 WebChromeClient 以更新加载进度
|
||||
binding.webview.webChromeClient = object : WebChromeClient() {
|
||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||
super.onProgressChanged(view, newProgress)
|
||||
|
||||
// 显示进度条
|
||||
if (newProgress < 100) {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.progressBar.progress = newProgress
|
||||
} else {
|
||||
// 隐藏进度条
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun requestRefereshTesting() {
|
||||
// urlTesting = true
|
||||
binding.webview.reload()
|
||||
}
|
||||
|
||||
fun breaseURL(url: String){
|
||||
|
||||
binding.webview.loadUrl(url)//.将网址传入
|
||||
|
||||
}
|
||||
|
||||
fun refereshApplyFrom(){
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import com.github.kr328.clash.design.MainDesign.Request
|
||||
import com.github.kr328.clash.design.databinding.DesignSettingsCommonBinding
|
||||
import com.github.kr328.clash.design.preference.NullableTextAdapter
|
||||
import com.github.kr328.clash.design.preference.TextAdapter
|
||||
import com.github.kr328.clash.design.preference.category
|
||||
import com.github.kr328.clash.design.preference.clickable
|
||||
import com.github.kr328.clash.design.preference.editableText
|
||||
import com.github.kr328.clash.design.preference.editableTextMap
|
||||
import com.github.kr328.clash.design.preference.preferenceScreen
|
||||
import com.github.kr328.clash.design.preference.tips
|
||||
import com.github.kr328.clash.design.ui.ToastDuration
|
||||
import com.github.kr328.clash.design.util.applyFrom
|
||||
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.network.ApiClient
|
||||
import com.github.kr328.clash.network.ApiService
|
||||
import com.github.kr328.clash.network.LoginRequest
|
||||
import com.github.kr328.clash.network.PublicResponse
|
||||
import com.github.kr328.clash.network.safeApiRequestCall
|
||||
import com.github.kr328.clash.utity.LoadingDialog
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URLEncoder
|
||||
|
||||
class HelpDesign(
|
||||
context: Context,
|
||||
openLink: (Uri) -> Unit,
|
||||
) : Design<HelpDesign.Request>(context) {
|
||||
private val binding = DesignSettingsCommonBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
enum class Request {
|
||||
CommitSuccess,
|
||||
}
|
||||
|
||||
fun request(request: HelpDesign.Request) {
|
||||
requests.trySend(request)
|
||||
}
|
||||
|
||||
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
init {
|
||||
binding.surface = surface
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
/*
|
||||
binding.scrollRoot.bindAppBarElevation(binding.activityBarLayout)
|
||||
|
||||
|
||||
data class ConfigurationOverride(
|
||||
|
||||
var title: String? = null,
|
||||
var content: String? = null
|
||||
|
||||
)
|
||||
val configuration = ConfigurationOverride()
|
||||
|
||||
val screen = preferenceScreen(context) {
|
||||
|
||||
|
||||
|
||||
editableText(
|
||||
value = configuration::title,
|
||||
adapter = NullableTextAdapter.String,
|
||||
title = R.string.name,
|
||||
placeholder = R.string.name,
|
||||
empty = R.string.default_
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration::content,
|
||||
adapter = NullableTextAdapter.String,
|
||||
title = R.string.more,
|
||||
placeholder = R.string.more,
|
||||
empty = R.string.default_
|
||||
)
|
||||
|
||||
tips(R.string.tips_help)
|
||||
|
||||
|
||||
clickable(
|
||||
title = R.string.github_issues,
|
||||
summary = R.string.github_issues_zeus
|
||||
) {
|
||||
clicked {
|
||||
openLink(Uri.parse("https://github.com/nicolastinkl"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
category(R.string.document)
|
||||
|
||||
|
||||
clickable(
|
||||
title = R.string.clash_wiki,
|
||||
summary = R.string.clash_wiki_url
|
||||
) {
|
||||
clicked {
|
||||
openLink(Uri.parse(context.getString(R.string.clash_wiki_url)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
clickable(
|
||||
title = R.string.clash_meta_wiki,
|
||||
summary = R.string.clash_meta_wiki_url
|
||||
) {
|
||||
clicked {
|
||||
openLink(Uri.parse(context.getString(R.string.clash_meta_wiki_url)))
|
||||
}
|
||||
}
|
||||
|
||||
category(R.string.sources)
|
||||
|
||||
clickable(
|
||||
title = R.string.clash_meta_core,
|
||||
summary = R.string.clash_meta_core_url
|
||||
) {
|
||||
clicked {
|
||||
openLink(Uri.parse(context.getString(R.string.clash_meta_core_url)))
|
||||
}
|
||||
}
|
||||
|
||||
clickable(
|
||||
title = R.string.clash_meta_for_android,
|
||||
summary = R.string.meta_github_url
|
||||
) {
|
||||
clicked {
|
||||
openLink(Uri.parse(context.getString(R.string.meta_github_url)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.content.addView(screen.root) */
|
||||
|
||||
binding.gongdanButton.setOnClickListener {
|
||||
val gondanSubText = binding.gondanSubText.text.toString()
|
||||
val gondanSubContent = binding.gondanSubContent.text.toString()
|
||||
|
||||
|
||||
if (gondanSubText.isEmpty() || gondanSubContent.isEmpty()) {
|
||||
Toast.makeText(context, "请输入工单标题和内容", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
|
||||
// Hide the keyboard
|
||||
val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
val currentFocusView = binding.root
|
||||
if (currentFocusView != null) {
|
||||
inputMethodManager.hideSoftInputFromWindow(currentFocusView.windowToken, 0)
|
||||
}
|
||||
performLogin(gondanSubText, gondanSubContent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun encodeString(input: String): String {
|
||||
val encoded = StringBuilder()
|
||||
|
||||
input.forEach { char ->
|
||||
when {
|
||||
char.isWhitespace() -> {
|
||||
// 编码空格为 %20
|
||||
encoded.append("")
|
||||
}
|
||||
char.isLetterOrDigit() || char.toInt() in 0x4E00..0x9FFF -> {
|
||||
// 保留字母、数字和中文字符
|
||||
encoded.append(char)
|
||||
}
|
||||
else -> {
|
||||
// 其他字符使用 URLEncoder 编码
|
||||
encoded.append(URLEncoder.encode(char.toString(), "UTF-8"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return encoded.toString()
|
||||
}
|
||||
|
||||
private fun performLogin(email: String, password: String) {
|
||||
// Perform login logic here, possibly calling an API
|
||||
|
||||
// Show the loading indicator with a custom message
|
||||
LoadingDialog.show(context, "正在提交...")
|
||||
|
||||
val apiServiceApp = ApiClient.retrofit.create(ApiService::class.java)
|
||||
|
||||
//获取 Config 数据
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val fieldMap = mutableMapOf<String, String>()
|
||||
val email1 = encodeString(email)
|
||||
val password1 =encodeString(password)
|
||||
fieldMap.put("subject",""+email1)
|
||||
fieldMap.put("level","1")
|
||||
fieldMap.put("message",""+password1)
|
||||
|
||||
safeApiRequestCall {
|
||||
apiServiceApp.saveTicket(PreferenceManager.loginauthData, request = fieldMap)}.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
LoadingDialog.hide()
|
||||
}
|
||||
if (it != null && it.isSuccessful) {
|
||||
showToast("提交成功",ToastDuration.Long)
|
||||
requests.trySend(Request.CommitSuccess)
|
||||
}else{
|
||||
val errorinfo = it?.errorBody()?.string()
|
||||
|
||||
if (errorinfo != null){
|
||||
//解析错错误信息
|
||||
|
||||
val gson = Gson()
|
||||
val submitResponse = gson.fromJson(errorinfo, PublicResponse::class.java)
|
||||
|
||||
if (submitResponse?.message != null){
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "提交失败: ${submitResponse.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}else{
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "提交失败", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.design.databinding.ActivityInvitationBinding
|
||||
import com.github.kr328.clash.design.databinding.ActivityInviteItemBinding
|
||||
import com.github.kr328.clash.design.databinding.ActivityPlanItemBinding
|
||||
import com.github.kr328.clash.design.databinding.ActivitySubmitorderBinding
|
||||
import com.github.kr328.clash.design.network.APIGlobalObject
|
||||
import com.github.kr328.clash.design.ui.ToastDuration
|
||||
import com.github.kr328.clash.design.util.applyFrom
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.root
|
||||
import com.github.kr328.clash.design.util.showCustomDialog
|
||||
import com.github.kr328.clash.network.ApiClient
|
||||
import com.github.kr328.clash.network.ApiService
|
||||
import com.github.kr328.clash.network.PaymentData
|
||||
import com.github.kr328.clash.network.PublicResponse
|
||||
import com.github.kr328.clash.network.safeApiRequestCall
|
||||
import com.github.kr328.clash.utity.LoadingDialog
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class InvitationDesign (context: Context) : Design<InvitationDesign.Request>(context) {
|
||||
|
||||
enum class Request {
|
||||
SubmitGenNewInviteCpde,
|
||||
}
|
||||
|
||||
private var firstLink:String = ""
|
||||
private val binding = ActivityInvitationBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.CreatenewInvitecodeButton.setOnClickListener {
|
||||
LoadingDialog.show(context, "正在生成中...")
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val apiService = ApiClient.retrofit.create(ApiService::class.java)
|
||||
safeApiRequestCall { apiService.saveinvite(PreferenceManager.loginauthData)}.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
LoadingDialog.hide()
|
||||
}
|
||||
|
||||
if (it != null && it.isSuccessful) {
|
||||
withContext(Dispatchers.Main) {
|
||||
requestdataDisible()
|
||||
}
|
||||
}else{
|
||||
val errorinfo = it?.errorBody()?.string()
|
||||
|
||||
if (errorinfo != null){
|
||||
//解析错错误信息
|
||||
|
||||
val gson = Gson()
|
||||
val submitResponse = gson.fromJson(errorinfo, PublicResponse::class.java)
|
||||
|
||||
if (submitResponse?.message != null){
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "生成失败: ${submitResponse.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}else{
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "生成失败", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.shareLinkButton.setOnClickListener {
|
||||
|
||||
showDiag(firstLink)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun showDiag(code:String){
|
||||
val message = "目前为止我用过最好的梯子,播放Youtube、Netflix高清视频从未如此轻松。\n\n" +
|
||||
"下载链接(推荐使用Chrome浏览器访问):${PreferenceManager.getConfigFromPreferences(context)?.websiteURL}\n\n" +
|
||||
"安装后打开填写我的邀请码:${code} 你能多得3天会员!\n"
|
||||
|
||||
context.showCustomDialog(
|
||||
title = "推荐文案",
|
||||
message = message ,
|
||||
positiveButtonText = "立即分享",
|
||||
negativeButtonText = "复制推荐文案",
|
||||
onPositiveClick = {
|
||||
|
||||
launch {
|
||||
|
||||
// 创建分享意图
|
||||
val shareIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, message) // 分享的文字内容
|
||||
type = "text/plain"
|
||||
}
|
||||
|
||||
// 启动分享选择器
|
||||
context.startActivity(Intent.createChooser(shareIntent, "分享给朋友"))
|
||||
}
|
||||
|
||||
},
|
||||
onNegativeClick = {
|
||||
// 清理缓存,清除节点信息,切换界面
|
||||
launch {
|
||||
copytoSysString(message)
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
fun request(request: Request) {
|
||||
|
||||
}
|
||||
|
||||
fun formatTimestamp(timestamp: Long): String {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) // 定义格式
|
||||
val date = Date(timestamp) // 将时间戳转为 Date 对象
|
||||
return dateFormat.format(date) // 格式化为字符串
|
||||
}
|
||||
|
||||
private suspend fun copytoSys(code: String ){
|
||||
withContext(Dispatchers.Main) {
|
||||
showDiag(code)
|
||||
}
|
||||
|
||||
// var cm: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
// var str:ClipData= ClipData.newPlainText("Label",PreferenceManager.getConfigFromPreferences(context)?.mainregisterURL+code)
|
||||
//
|
||||
// cm.setPrimaryClip(str)
|
||||
// showToast("复制成功到剪切板",ToastDuration.Long)
|
||||
}
|
||||
|
||||
private suspend fun copytoSysString(code: String ){
|
||||
var cm: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
var str:ClipData= ClipData.newPlainText("Label",code)
|
||||
|
||||
cm.setPrimaryClip(str)
|
||||
showToast("复制成功到剪切板",ToastDuration.Long)
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun requestdataDisible(){
|
||||
|
||||
LoadingDialog.show(context, "正在获取中...")
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val apiService = ApiClient.retrofit.create(ApiService::class.java)
|
||||
safeApiRequestCall { apiService.getinviteList(PreferenceManager.loginauthData)}.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
LoadingDialog.hide()
|
||||
}
|
||||
|
||||
if (it != null && it.isSuccessful){
|
||||
if (it.body()?.data?.stat != null){
|
||||
val listdata = it.body()?.data?.stat
|
||||
|
||||
var index = 0
|
||||
|
||||
listdata?.forEach { item ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (index == 0) {
|
||||
binding.inviteZhucenum = "${item}人"
|
||||
} else if (index == 1) {
|
||||
binding.inviteQuerenzhong = "¥${item}.00"
|
||||
} else if (index == 2) {
|
||||
binding.inviteZhucenumleiji = "¥${item}.00"
|
||||
} else if (index == 3) {
|
||||
binding.inviteBili = "${item}%"
|
||||
|
||||
}
|
||||
}
|
||||
index ++
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val codes = it.body()?.data?.codes
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.container.removeAllViews()
|
||||
var index = 0
|
||||
codes?.forEach { item ->
|
||||
|
||||
if (index == 0){
|
||||
firstLink = item.code ?: ""
|
||||
}
|
||||
val frameLayout = ActivityInviteItemBinding.inflate(
|
||||
context.layoutInflater,
|
||||
context.root,
|
||||
false
|
||||
)
|
||||
|
||||
frameLayout.nameTextView.text = item.code
|
||||
// frameLayout.timeTextView.text = "${formatTimestamp((item.created_at ?: 0) * 1000L)}"
|
||||
|
||||
frameLayout.planitemFrameLayout.setOnClickListener {
|
||||
println("click ${item}")
|
||||
GlobalScope.launch {
|
||||
this@InvitationDesign.copytoSys(item.code ?: "")
|
||||
}
|
||||
|
||||
}
|
||||
frameLayout.copyInvitecodeButton.setOnClickListener {
|
||||
GlobalScope.launch {
|
||||
this@InvitationDesign.copytoSys(item.code ?: "")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
binding.container.addView(frameLayout.root)
|
||||
index ++
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.kr328.clash.core.model.LogMessage
|
||||
import com.github.kr328.clash.design.adapter.LogMessageAdapter
|
||||
import com.github.kr328.clash.design.databinding.DesignLogcatBinding
|
||||
import com.github.kr328.clash.design.ui.ToastDuration
|
||||
import com.github.kr328.clash.design.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class LogcatDesign(
|
||||
context: Context,
|
||||
private val streaming: Boolean,
|
||||
) : Design<LogcatDesign.Request>(context) {
|
||||
enum class Request {
|
||||
Close, Delete, Export
|
||||
}
|
||||
|
||||
private val binding = DesignLogcatBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
private val adapter = LogMessageAdapter(context) {
|
||||
launch {
|
||||
val data = ClipData.newPlainText("log_message", it.message)
|
||||
|
||||
context.getSystemService<ClipboardManager>()?.setPrimaryClip(data)
|
||||
|
||||
showToast(R.string.copied, ToastDuration.Short)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun patchMessages(messages: List<LogMessage>, removed: Int, appended: Int) {
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.messages = messages
|
||||
|
||||
adapter.notifyItemRangeInserted(adapter.messages.size, appended)
|
||||
adapter.notifyItemRangeRemoved(0, removed)
|
||||
|
||||
if (streaming && binding.recyclerList.isTop) {
|
||||
binding.recyclerList.scrollToPosition(messages.size - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
binding.streaming = streaming
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.recyclerList.bindAppBarElevation(binding.activityBarLayout)
|
||||
|
||||
binding.recyclerList.layoutManager = LinearLayoutManager(context).apply {
|
||||
if (streaming) {
|
||||
reverseLayout = true
|
||||
stackFromEnd = true
|
||||
}
|
||||
}
|
||||
binding.recyclerList.adapter = adapter
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import com.github.kr328.clash.design.adapter.LogFileAdapter
|
||||
import com.github.kr328.clash.design.databinding.DesignLogsBinding
|
||||
import com.github.kr328.clash.design.model.LogFile
|
||||
import com.github.kr328.clash.design.util.*
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class LogsDesign(context: Context) : Design<LogsDesign.Request>(context) {
|
||||
sealed class Request {
|
||||
object StartLogcat : Request()
|
||||
object DeleteAll : Request()
|
||||
|
||||
data class OpenFile(val file: LogFile) : Request()
|
||||
}
|
||||
|
||||
private val binding = DesignLogsBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
private val adapter = LogFileAdapter(context) {
|
||||
requests.trySend(Request.OpenFile(it))
|
||||
}
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
suspend fun patchLogs(logs: List<LogFile>) {
|
||||
adapter.patchDataSet(adapter::logs, logs, false, LogFile::fileName)
|
||||
}
|
||||
|
||||
suspend fun requestDeleteAll(): Boolean {
|
||||
return withContext(Dispatchers.Main) {
|
||||
suspendCancellableCoroutine { ctx ->
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.delete_all_logs)
|
||||
.setMessage(R.string.delete_all_logs_warn)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> ctx.resume(true) }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
.setOnDismissListener { if (!ctx.isCompleted) ctx.resume(false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.recyclerList.applyLinearAdapter(context, adapter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.github.kr328.clash.common.util.ticker
|
||||
import com.github.kr328.clash.core.Clash
|
||||
import com.github.kr328.clash.core.model.TunnelState
|
||||
import com.github.kr328.clash.core.util.trafficDownload
|
||||
import com.github.kr328.clash.core.util.trafficTotal
|
||||
import com.github.kr328.clash.core.util.trafficUpload
|
||||
import com.github.kr328.clash.design.adapter.ImageSliderImagesAdapter
|
||||
import com.github.kr328.clash.design.component.ProxyMenu
|
||||
import com.github.kr328.clash.design.component.ProxyViewConfig
|
||||
import com.github.kr328.clash.design.databinding.DesignAboutBinding
|
||||
import com.github.kr328.clash.design.databinding.DesignMainBinding
|
||||
import com.github.kr328.clash.design.preference.selectableList
|
||||
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.github.kr328.clash.service.model.AccessControlMode
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.util.concurrent.TimeUnit
|
||||
import com.github.kr328.clash.design.preference.*
|
||||
import com.github.kr328.clash.design.ui.ToastDuration
|
||||
import com.github.kr328.clash.service.store.ServiceStore
|
||||
import com.github.kr328.clash.utity.LoadingDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainDesign(context: Context) : Design<MainDesign.Request>(context) ,PopupMenu.OnMenuItemClickListener {
|
||||
enum class Request {
|
||||
ToggleStatus,
|
||||
OpenProxy,
|
||||
OpenProfiles,
|
||||
OpenProviders,
|
||||
OpenLogs,
|
||||
OpenSettings,
|
||||
OpenHelp,
|
||||
OpenAbout,
|
||||
OpenSettingsDIY,
|
||||
OpenSettingsKEFU,
|
||||
OpenModeDirect,
|
||||
OpenChangeMode,
|
||||
OpenModeGlobal,
|
||||
OpenModeRule,
|
||||
OpenModeMenu,
|
||||
OpenModeSheet
|
||||
}
|
||||
private var menu:PopupMenu? = null
|
||||
private val binding = DesignMainBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
suspend fun setProfileName(name: String?) {
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.profileName = name
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun setSelectNodeName(name: String?) {
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.selectnodeName = name
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setClashRunning(running: Boolean) {
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.clashRunning = running
|
||||
if (running){
|
||||
binding.connectionButton.setAnimation("51a05581.json")
|
||||
binding.serverSelection.visibility = View.VISIBLE
|
||||
|
||||
// withContext(Dispatchers.Main){
|
||||
// LoadingDialog.hide()
|
||||
// }
|
||||
binding.trafficStats.visibility = View.VISIBLE
|
||||
// binding.modeImg1.visibility = View.VISIBLE
|
||||
// binding.modeImg2.visibility = View.VISIBLE
|
||||
// binding.menuView.visibility = View.VISIBLE
|
||||
binding.connectionButtonText.setText("点击断开")
|
||||
binding.connectionButton.loop(false)
|
||||
binding.connectionButtonText.setTextColor(ContextCompat.getColor(context, R.color.white))
|
||||
}else{
|
||||
binding.trafficStats.visibility = View.GONE
|
||||
binding.connectionButton.setAnimation("1d2a0fe5.json")
|
||||
binding.connectionButton.loop(true)
|
||||
binding.serverSelection.visibility = View.GONE
|
||||
binding.connectionButtonText.setText("点击连接")
|
||||
binding.connectionButtonText.setTextColor(ContextCompat.getColor(context,R.color.black))
|
||||
// binding.modeImg1.visibility = View.GONE
|
||||
// binding.modeImg2.visibility = View.GONE
|
||||
// binding.menuView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun setUploadedSeep(value: Long){
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.uploadedseed = value.trafficUpload()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun setDownloadedSeep(value: Long){
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.downloadseed = value.trafficDownload()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setForwarded(value: Long) {
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.forwarded = value.trafficTotal()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setMode(mode: TunnelState.Mode) {
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.mode = when (mode) {
|
||||
TunnelState.Mode.Direct -> context.getString(R.string.direct_mode)
|
||||
TunnelState.Mode.Global -> context.getString(R.string.global_mode)
|
||||
TunnelState.Mode.Rule -> context.getString(R.string.rule_mode)
|
||||
else -> context.getString(R.string.rule_mode)
|
||||
}
|
||||
|
||||
when (mode) {
|
||||
// TunnelState.Mode.Direct -> {
|
||||
// binding.modeSelectionbutton1.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#2b9a45"))
|
||||
// }
|
||||
// TunnelState.Mode.Global -> binding.modeSelectionbutton2.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#2b9a45"))
|
||||
// TunnelState.Mode.Rule -> binding.modeSelectionbutton3.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#2b9a45"))
|
||||
// else -> binding.modeSelectionbutton3.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#2b9a45"))
|
||||
|
||||
TunnelState.Mode.Direct -> binding.selectnodeName = context.getString(R.string.direct_mode)
|
||||
TunnelState.Mode.Global -> binding.selectnodeName = context.getString(R.string.global_mode)
|
||||
TunnelState.Mode.Rule -> binding.selectnodeName = context.getString(R.string.rule_mode)
|
||||
else -> binding.selectnodeName = context.getString(R.string.direct_mode)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setHasProviders(has: Boolean) {
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.hasProviders = has
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun showAbout(versionName: String) {
|
||||
withContext(Dispatchers.Main) {
|
||||
val binding = DesignAboutBinding.inflate(context.layoutInflater).apply {
|
||||
this.versionName = versionName
|
||||
}
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setView(binding.root)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.colorClashStarted = context.resolveThemedColor(R.attr.colorPrimary)
|
||||
binding.colorClashStopped = context.resolveThemedColor(R.attr.colorClashStopped)
|
||||
|
||||
|
||||
// menu = PopupMenu(context, binding.menuView)
|
||||
// menu?.menuInflater?.inflate(R.menu.activity_menu_mode, menu?.menu)
|
||||
// menu?.setOnMenuItemClickListener(this)
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
item.isChecked = !item.isChecked
|
||||
when (item.itemId) {
|
||||
R.id.global_mode -> {
|
||||
requests.trySend(Request.OpenModeGlobal)
|
||||
}
|
||||
R.id.rule_mode -> {
|
||||
requests.trySend(Request.OpenModeRule)
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun request(request: Request) {
|
||||
requests.trySend(request)
|
||||
}
|
||||
|
||||
suspend fun startAutoScroll(viewPager2: androidx.viewpager2.widget.ViewPager2,nextItem: Int){
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
viewPager2.setCurrentItem(nextItem, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
suspend fun OpenModeSheet(){
|
||||
|
||||
// 创建 BottomSheetDialog
|
||||
val bottomSheetDialog = BottomSheetDialog(context)
|
||||
|
||||
// 加载自定义的底部布局
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.bottom_sheet_layout, null)
|
||||
bottomSheetDialog.setContentView(view)
|
||||
|
||||
|
||||
view.findViewById<TextView>(R.id.mode_rule).setOnClickListener {
|
||||
// 处理点击事件
|
||||
bottomSheetDialog.dismiss()
|
||||
requests.trySend(Request.OpenModeRule)
|
||||
|
||||
}
|
||||
view.findViewById<TextView>(R.id.mode_global).setOnClickListener {
|
||||
// 处理点击事件
|
||||
bottomSheetDialog.dismiss()
|
||||
requests.trySend(Request.OpenModeGlobal)
|
||||
|
||||
}
|
||||
// 显示 BottomSheetDialog
|
||||
bottomSheetDialog.show()
|
||||
}
|
||||
|
||||
suspend fun OpenModeMenu(){
|
||||
|
||||
menu?.show()
|
||||
|
||||
}
|
||||
|
||||
suspend fun startBannsers() {
|
||||
withContext(Dispatchers.Main) {
|
||||
//sart ad bannsers
|
||||
val viewPager2 = binding.adBanner
|
||||
val tabLayout = binding.tabLayout
|
||||
// 创建图片资源列表
|
||||
/* val imageList = listOf(
|
||||
R.drawable.ad_banner,
|
||||
R.drawable.ad_banner2,
|
||||
R.drawable.ad_banner3
|
||||
)*/
|
||||
|
||||
val imageList = PreferenceManager.getConfigFromPreferences(context)?.banners
|
||||
|
||||
//binding.menuView.text = PreferenceManager.modeName
|
||||
// 设置适配器
|
||||
//val adapter = ImageSliderAdapter(imageList)
|
||||
|
||||
if (imageList != null){
|
||||
|
||||
viewPager2.adapter = ImageSliderImagesAdapter(imageList){ position ->
|
||||
// 点击图片时的处理逻辑
|
||||
|
||||
}
|
||||
// 设置 TabLayoutMediator 来同步 ViewPager2 和 TabLayout
|
||||
TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
|
||||
// Tab 的自定义逻辑可以放在这里
|
||||
}.attach()
|
||||
}
|
||||
|
||||
val uncheckedColor = ColorStateList(
|
||||
arrayOf(intArrayOf(-android.R.attr.state_checked)), // 未选中状态
|
||||
intArrayOf(Color.parseColor("#40383838")) // 未选中颜色
|
||||
)
|
||||
|
||||
val checkedColor = ColorStateList(
|
||||
arrayOf(intArrayOf(android.R.attr.state_checked)), // 选中状态
|
||||
intArrayOf(Color.parseColor("#2b9a45")) // 选中颜色
|
||||
)
|
||||
|
||||
|
||||
/*
|
||||
val screen = preferenceScreen(context) {
|
||||
val vpnDependencies: MutableList<Preference> = mutableListOf()
|
||||
val srvStore = ServiceStore(context)
|
||||
val selectMode = selectableList(
|
||||
value = srvStore::accessControlMode,
|
||||
values = AccessControlMode.values(),
|
||||
valuesText = arrayOf(
|
||||
R.string.direct_mode,
|
||||
R.string.rule_mode,
|
||||
R.string.global_mode
|
||||
),
|
||||
title = R.string.rule_mode,
|
||||
configure = vpnDependencies::add,
|
||||
)
|
||||
}
|
||||
binding.linearLayoutMode.addView(screen.root)
|
||||
*/
|
||||
binding.modeSelection.addOnButtonCheckedListener { group, checkedId, isChecked ->
|
||||
|
||||
//
|
||||
// if(button.id == checkedId){
|
||||
// button.backgroundTintList = checkedColor // 选中状态颜色
|
||||
// }else{
|
||||
// button.backgroundTintList = uncheckedColor // 未选中状态颜色
|
||||
// }
|
||||
|
||||
/*
|
||||
for (buttonId in listOf(binding.modeSelectionbutton1,binding.modeSelectionbutton2,binding.modeSelectionbutton3)) {
|
||||
if (buttonId.id != checkedId) {
|
||||
// binding.modeSelection.uncheck(buttonId.id)
|
||||
buttonId.backgroundTintList = uncheckedColor
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val button = group.findViewById<MaterialButton>(checkedId)
|
||||
button.backgroundTintList = checkedColor // 选中状态颜色
|
||||
*/
|
||||
|
||||
// binding.modeSelectionbutton1.invalidate()
|
||||
// binding.modeSelectionbutton2.invalidate()
|
||||
// binding.modeSelectionbutton3.invalidate()
|
||||
|
||||
// Reset all buttons to default background tint
|
||||
// binding.modeSelectionbutton1.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#40383838"))
|
||||
// binding.modeSelectionbutton2.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#40383838"))
|
||||
// binding.modeSelectionbutton3.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#40383838"))
|
||||
//
|
||||
|
||||
|
||||
when (checkedId) {
|
||||
R.id.modeSelectionbutton1 -> {
|
||||
request(Request.OpenModeDirect)
|
||||
binding.modeSelectionbutton1.backgroundTintList = checkedColor // 选中状态颜色
|
||||
// binding.modeSelectionbutton1.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#2b9a45"))
|
||||
}
|
||||
|
||||
R.id.modeSelectionbutton2 -> {
|
||||
|
||||
request(Request.OpenModeGlobal)
|
||||
binding.modeSelectionbutton2.backgroundTintList = checkedColor // 选中状态颜色
|
||||
//binding.modeSelectionbutton2.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#2b9a45"))
|
||||
}
|
||||
|
||||
R.id.modeSelectionbutton3 -> {
|
||||
request(Request.OpenModeRule)
|
||||
binding.modeSelectionbutton3.backgroundTintList = checkedColor // 选中状态颜色
|
||||
// binding.modeSelectionbutton3.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#2b9a45"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import com.github.kr328.clash.core.model.ConfigurationOverride
|
||||
import com.github.kr328.clash.design.databinding.DesignSettingsMetaFeatureBinding
|
||||
import com.github.kr328.clash.design.preference.*
|
||||
import com.github.kr328.clash.design.util.*
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class MetaFeatureSettingsDesign(
|
||||
context: Context,
|
||||
configuration: ConfigurationOverride
|
||||
) : Design<MetaFeatureSettingsDesign.Request>(context) {
|
||||
enum class Request {
|
||||
ResetOverride, ImportGeoIp, ImportGeoSite, ImportCountry, ImportASN
|
||||
}
|
||||
|
||||
private val binding = DesignSettingsMetaFeatureBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
suspend fun requestResetConfirm(): Boolean {
|
||||
return suspendCancellableCoroutine { ctx ->
|
||||
val dialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.reset_override_settings)
|
||||
.setMessage(R.string.reset_override_settings_message)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> ctx.resume(true) }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
if (!ctx.isCompleted)
|
||||
ctx.resume(false)
|
||||
}
|
||||
|
||||
ctx.invokeOnCancellation {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.scrollRoot.bindAppBarElevation(binding.activityBarLayout)
|
||||
|
||||
val booleanValues: Array<Boolean?> = arrayOf(
|
||||
null,
|
||||
true,
|
||||
false
|
||||
)
|
||||
val booleanValuesText: Array<Int> = arrayOf(
|
||||
R.string.dont_modify,
|
||||
R.string.enabled,
|
||||
R.string.disabled
|
||||
)
|
||||
|
||||
val screen = preferenceScreen(context) {
|
||||
category(R.string.settings)
|
||||
|
||||
selectableList(
|
||||
value = configuration::unifiedDelay,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.unified_delay,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration::geodataMode,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.geodata_mode,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration::tcpConcurrent,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.tcp_concurrent,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration::findProcessMode,
|
||||
values = arrayOf(
|
||||
null,
|
||||
ConfigurationOverride.FindProcessMode.Off,
|
||||
ConfigurationOverride.FindProcessMode.Strict,
|
||||
ConfigurationOverride.FindProcessMode.Always
|
||||
),
|
||||
valuesText = arrayOf(
|
||||
R.string.dont_modify,
|
||||
R.string.off,
|
||||
R.string.strict,
|
||||
R.string.always,
|
||||
),
|
||||
title = R.string.find_process_mode,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
category(R.string.sniffer_setting)
|
||||
|
||||
val snifferDependencies: MutableList<Preference> = mutableListOf()
|
||||
|
||||
val sniffer = selectableList(
|
||||
value = configuration.sniffer::enable,
|
||||
values = arrayOf(
|
||||
null,
|
||||
true,
|
||||
false
|
||||
),
|
||||
valuesText = arrayOf(
|
||||
R.string.dont_modify,
|
||||
R.string.enabled,
|
||||
R.string.disabled
|
||||
),
|
||||
title = R.string.strategy
|
||||
) {
|
||||
listener = OnChangedListener {
|
||||
if (configuration.sniffer.enable == false) {
|
||||
snifferDependencies.forEach {
|
||||
it.enabled = false
|
||||
}
|
||||
} else {
|
||||
snifferDependencies.forEach {
|
||||
it.enabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editableTextList(
|
||||
value = configuration.sniffer::sniffing,
|
||||
adapter = TextAdapter.String,
|
||||
title = R.string.sniffing,
|
||||
placeholder = R.string.dont_modify,
|
||||
configure = snifferDependencies::add,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration.sniffer::forceDnsMapping,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.force_dns_mapping,
|
||||
configure = snifferDependencies::add,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration.sniffer::parsePureIp,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.parse_pure_ip,
|
||||
configure = snifferDependencies::add,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration.sniffer::overrideDestination,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.override_destination,
|
||||
configure = snifferDependencies::add,
|
||||
)
|
||||
|
||||
editableTextList(
|
||||
value = configuration.sniffer::forceDomain,
|
||||
adapter = TextAdapter.String,
|
||||
title = R.string.force_domain,
|
||||
placeholder = R.string.dont_modify,
|
||||
configure = snifferDependencies::add,
|
||||
)
|
||||
|
||||
editableTextList(
|
||||
value = configuration.sniffer::skipDomain,
|
||||
adapter = TextAdapter.String,
|
||||
title = R.string.skip_domain,
|
||||
placeholder = R.string.dont_modify,
|
||||
configure = snifferDependencies::add,
|
||||
)
|
||||
|
||||
editableTextList(
|
||||
value = configuration.sniffer::portWhitelist,
|
||||
adapter = TextAdapter.String,
|
||||
title = R.string.port_whitelist,
|
||||
placeholder = R.string.dont_modify,
|
||||
configure = snifferDependencies::add,
|
||||
)
|
||||
|
||||
sniffer.listener?.onChanged()
|
||||
|
||||
/*
|
||||
category(R.string.geox_url_setting)
|
||||
|
||||
val geoxUrlDependencies: MutableList<Preference> = mutableListOf()
|
||||
|
||||
editableText(
|
||||
value = configuration.geoxurl::geoip,
|
||||
adapter = NullableTextAdapter.String,
|
||||
title = R.string.geox_geoip,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.geoip_url,
|
||||
configure = geoxUrlDependencies::add,
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration.geoxurl::mmdb,
|
||||
adapter = NullableTextAdapter.String,
|
||||
title = R.string.geox_mmdb,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.mmdb_url,
|
||||
configure = geoxUrlDependencies::add,
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration.geoxurl::geosite,
|
||||
adapter = NullableTextAdapter.String,
|
||||
title = R.string.geox_geosite,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.geosite_url,
|
||||
configure = geoxUrlDependencies::add,
|
||||
)
|
||||
*/
|
||||
|
||||
category(R.string.geox_files)
|
||||
|
||||
clickable (
|
||||
title = R.string.import_geoip_file,
|
||||
summary = R.string.press_to_import,
|
||||
){
|
||||
clicked {
|
||||
requests.trySend(Request.ImportGeoIp)
|
||||
}
|
||||
}
|
||||
|
||||
clickable (
|
||||
title = R.string.import_geosite_file,
|
||||
summary = R.string.press_to_import,
|
||||
){
|
||||
clicked {
|
||||
requests.trySend(Request.ImportGeoSite)
|
||||
}
|
||||
}
|
||||
|
||||
clickable (
|
||||
title = R.string.import_country_file,
|
||||
summary = R.string.press_to_import,
|
||||
){
|
||||
clicked {
|
||||
requests.trySend(Request.ImportCountry)
|
||||
}
|
||||
}
|
||||
|
||||
clickable (
|
||||
title = R.string.import_asn_file,
|
||||
summary = R.string.press_to_import,
|
||||
){
|
||||
clicked {
|
||||
requests.trySend(Request.ImportASN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.content.addView(screen.root)
|
||||
}
|
||||
|
||||
fun requestClear() {
|
||||
requests.trySend(Request.ResetOverride)
|
||||
}
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import com.github.kr328.clash.common.util.componentName
|
||||
import com.github.kr328.clash.design.databinding.DesignSettingsCommonBinding
|
||||
import com.github.kr328.clash.design.model.Behavior
|
||||
import com.github.kr328.clash.design.preference.*
|
||||
import com.github.kr328.clash.design.store.UiStore
|
||||
import com.github.kr328.clash.design.ui.ToastDuration
|
||||
import com.github.kr328.clash.design.util.applyFrom
|
||||
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.view.drividerline
|
||||
import com.github.kr328.clash.service.model.AccessControlMode
|
||||
import com.github.kr328.clash.service.store.ServiceStore
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NetworkSettingsDesign(
|
||||
context: Context,
|
||||
uiStore: UiStore,
|
||||
behavior: Behavior,
|
||||
srvStore: ServiceStore,
|
||||
running: Boolean,
|
||||
) : Design<NetworkSettingsDesign.Request>(context) {
|
||||
enum class Request {
|
||||
StartAccessControlList
|
||||
}
|
||||
|
||||
private val binding = DesignSettingsCommonBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
|
||||
|
||||
init {
|
||||
binding.surface = surface
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.scrollRoot.bindAppBarElevation(binding.activityBarLayout)
|
||||
|
||||
val screen = preferenceScreen(context) {
|
||||
|
||||
|
||||
val vpnDependencies: MutableList<Preference> = mutableListOf()
|
||||
|
||||
|
||||
category(R.string.default_)
|
||||
|
||||
|
||||
// switch(
|
||||
// value = behavior::autoRestart,
|
||||
// icon = R.drawable.ic_baseline_restore,
|
||||
// title = R.string.auto_restart,
|
||||
// summary = R.string.allow_clash_auto_restart,
|
||||
// )
|
||||
//
|
||||
//
|
||||
// drividerline()
|
||||
//
|
||||
switch(
|
||||
value = srvStore::dynamicNotification,
|
||||
icon = R.drawable.ic_baseline_domain,
|
||||
title = R.string.show_traffic,
|
||||
summary = R.string.show_traffic_summary
|
||||
) {
|
||||
// enabled = !running
|
||||
}
|
||||
|
||||
|
||||
drividerline()
|
||||
selectableList(
|
||||
value = srvStore::accessControlMode,
|
||||
values = AccessControlMode.values(),
|
||||
valuesText = arrayOf(
|
||||
R.string.allow_all_apps,
|
||||
R.string.allow_selected_apps,
|
||||
R.string.deny_selected_apps
|
||||
),
|
||||
title = R.string.access_control_mode,
|
||||
configure = vpnDependencies::add,
|
||||
)
|
||||
|
||||
drividerline()
|
||||
clickable(
|
||||
title = R.string.access_control_packages,
|
||||
summary = R.string.access_control_packages_summary,
|
||||
) {
|
||||
clicked {
|
||||
requests.trySend(Request.StartAccessControlList)
|
||||
}
|
||||
|
||||
vpnDependencies.add(this)
|
||||
}
|
||||
|
||||
drividerline()
|
||||
|
||||
|
||||
category(R.string.vpn_service_options)
|
||||
|
||||
val vpn = switch(
|
||||
value = uiStore::enableVpn,
|
||||
icon = R.drawable.ic_baseline_vpn_lock,
|
||||
title = R.string.route_system_traffic,
|
||||
summary = R.string.routing_via_vpn_service
|
||||
) {
|
||||
listener = OnChangedListener {
|
||||
vpnDependencies.forEach {
|
||||
it.enabled = uiStore.enableVpn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drividerline()
|
||||
|
||||
|
||||
switch(
|
||||
value = srvStore::bypassPrivateNetwork,
|
||||
title = R.string.bypass_private_network,
|
||||
summary = R.string.bypass_private_network_summary,
|
||||
configure = vpnDependencies::add,
|
||||
)
|
||||
|
||||
drividerline()
|
||||
|
||||
switch(
|
||||
value = srvStore::dnsHijacking,
|
||||
title = R.string.dns_hijacking,
|
||||
summary = R.string.dns_hijacking_summary,
|
||||
configure = vpnDependencies::add,
|
||||
)
|
||||
|
||||
drividerline()
|
||||
|
||||
switch(
|
||||
value = srvStore::allowBypass,
|
||||
title = R.string.allow_bypass,
|
||||
summary = R.string.allow_bypass_summary,
|
||||
configure = vpnDependencies::add,
|
||||
)
|
||||
|
||||
drividerline()
|
||||
|
||||
switch(
|
||||
value = srvStore::allowIpv6,
|
||||
title = R.string.allow_ipv6,
|
||||
summary = R.string.allow_ipv6_summary,
|
||||
configure = vpnDependencies::add,
|
||||
)
|
||||
|
||||
drividerline()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
switch(
|
||||
value = srvStore::systemProxy,
|
||||
title = R.string.system_proxy,
|
||||
summary = R.string.system_proxy_summary,
|
||||
configure = vpnDependencies::add,
|
||||
)
|
||||
}
|
||||
|
||||
drividerline()
|
||||
|
||||
selectableList(
|
||||
value = srvStore::tunStackMode,
|
||||
values = arrayOf(
|
||||
"system",
|
||||
"gvisor",
|
||||
"mixed"
|
||||
),
|
||||
valuesText = arrayOf(
|
||||
R.string.tun_stack_system,
|
||||
R.string.tun_stack_gvisor,
|
||||
R.string.tun_stack_mixed
|
||||
),
|
||||
title = R.string.tun_stack_mode,
|
||||
configure = vpnDependencies::add,
|
||||
)
|
||||
|
||||
drividerline()
|
||||
|
||||
|
||||
|
||||
/*
|
||||
if (running) {
|
||||
vpn.enabled = false
|
||||
|
||||
vpnDependencies.forEach {
|
||||
it.enabled = false
|
||||
}
|
||||
} else {
|
||||
vpn.listener?.onChanged()
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
binding.content.addView(screen.root)
|
||||
|
||||
// if (running) {
|
||||
// launch {
|
||||
// showToast(R.string.options_unavailable, ToastDuration.Indefinite)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import com.github.kr328.clash.design.adapter.ProfileProviderAdapter
|
||||
import com.github.kr328.clash.design.databinding.DesignNewProfileBinding
|
||||
import com.github.kr328.clash.design.model.ProfileProvider
|
||||
import com.github.kr328.clash.design.util.*
|
||||
|
||||
class NewProfileDesign(context: Context) : Design<NewProfileDesign.Request>(context) {
|
||||
sealed class Request {
|
||||
data class Create(val provider: ProfileProvider) : Request()
|
||||
data class OpenDetail(val provider: ProfileProvider.External) : Request()
|
||||
}
|
||||
|
||||
private val binding = DesignNewProfileBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
private val adapter = ProfileProviderAdapter(context, this::requestCreate, this::requestDetail)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
suspend fun patchProviders(providers: List<ProfileProvider>) {
|
||||
adapter.apply {
|
||||
patchDataSet(this::providers, providers)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.mainList.recyclerList.also {
|
||||
it.bindAppBarElevation(binding.activityBarLayout)
|
||||
it.applyLinearAdapter(context, adapter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestCreate(provider: ProfileProvider) {
|
||||
requests.trySend(Request.Create(provider))
|
||||
}
|
||||
|
||||
private fun requestDetail(provider: ProfileProvider): Boolean {
|
||||
if (provider !is ProfileProvider.External) return false
|
||||
|
||||
requests.trySend(Request.OpenDetail(provider))
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
+417
@@ -0,0 +1,417 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import com.github.kr328.clash.core.model.ConfigurationOverride
|
||||
import com.github.kr328.clash.core.model.LogMessage
|
||||
import com.github.kr328.clash.core.model.TunnelState
|
||||
import com.github.kr328.clash.design.databinding.DesignSettingsOverideBinding
|
||||
import com.github.kr328.clash.design.databinding.DialogPreferenceListBinding
|
||||
import com.github.kr328.clash.design.dialog.FullScreenDialog
|
||||
import com.github.kr328.clash.design.model.AppInfo
|
||||
import com.github.kr328.clash.design.preference.*
|
||||
import com.github.kr328.clash.design.util.*
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class OverrideSettingsDesign(
|
||||
context: Context,
|
||||
configuration: ConfigurationOverride
|
||||
) : Design<OverrideSettingsDesign.Request>(context) {
|
||||
enum class Request {
|
||||
ResetOverride
|
||||
}
|
||||
|
||||
private val binding = DesignSettingsOverideBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
suspend fun requestResetConfirm(): Boolean {
|
||||
return suspendCancellableCoroutine { ctx ->
|
||||
val dialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.reset_override_settings)
|
||||
.setMessage(R.string.reset_override_settings_message)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> ctx.resume(true) }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
if (!ctx.isCompleted)
|
||||
ctx.resume(false)
|
||||
}
|
||||
|
||||
ctx.invokeOnCancellation {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.scrollRoot.bindAppBarElevation(binding.activityBarLayout)
|
||||
|
||||
val booleanValues: Array<Boolean?> = arrayOf(
|
||||
null,
|
||||
true,
|
||||
false
|
||||
)
|
||||
val booleanValuesText: Array<Int> = arrayOf(
|
||||
R.string.dont_modify,
|
||||
R.string.enabled,
|
||||
R.string.disabled
|
||||
)
|
||||
|
||||
val screen = preferenceScreen(context) {
|
||||
category(R.string.general)
|
||||
|
||||
editableText(
|
||||
value = configuration::httpPort,
|
||||
adapter = NullableTextAdapter.Port,
|
||||
title = R.string.http_port,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.disabled,
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration::socksPort,
|
||||
adapter = NullableTextAdapter.Port,
|
||||
title = R.string.socks_port,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.disabled,
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration::redirectPort,
|
||||
adapter = NullableTextAdapter.Port,
|
||||
title = R.string.redirect_port,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.disabled,
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration::tproxyPort,
|
||||
adapter = NullableTextAdapter.Port,
|
||||
title = R.string.tproxy_port,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.disabled,
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration::mixedPort,
|
||||
adapter = NullableTextAdapter.Port,
|
||||
title = R.string.mixed_port,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.disabled,
|
||||
)
|
||||
|
||||
editableTextList(
|
||||
value = configuration::authentication,
|
||||
adapter = TextAdapter.String,
|
||||
title = R.string.authentication,
|
||||
placeholder = R.string.dont_modify,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration::allowLan,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.allow_lan,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration::ipv6,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.ipv6,
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration::bindAddress,
|
||||
adapter = NullableTextAdapter.String,
|
||||
title = R.string.bind_address,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.default_
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration::externalController,
|
||||
adapter = NullableTextAdapter.String,
|
||||
title = R.string.external_controller,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.default_
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration::externalControllerTLS,
|
||||
adapter = NullableTextAdapter.String,
|
||||
title = R.string.external_controller_tls,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.default_
|
||||
)
|
||||
|
||||
editableTextList(
|
||||
value = configuration.externalControllerCors::allowOrigins,
|
||||
adapter = TextAdapter.String,
|
||||
title = R.string.allow_origins,
|
||||
placeholder = R.string.dont_modify,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration.externalControllerCors::allowPrivateNetwork,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.allow_private_network,
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration::secret,
|
||||
adapter = NullableTextAdapter.String,
|
||||
title = R.string.secret,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.default_
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration::mode,
|
||||
values = arrayOf(
|
||||
null,
|
||||
TunnelState.Mode.Direct,
|
||||
TunnelState.Mode.Global,
|
||||
TunnelState.Mode.Rule
|
||||
),
|
||||
valuesText = arrayOf(
|
||||
R.string.dont_modify,
|
||||
R.string.direct_mode,
|
||||
R.string.global_mode,
|
||||
R.string.rule_mode
|
||||
),
|
||||
title = R.string.mode
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration::logLevel,
|
||||
values = arrayOf(
|
||||
null,
|
||||
LogMessage.Level.Info,
|
||||
LogMessage.Level.Warning,
|
||||
LogMessage.Level.Error,
|
||||
LogMessage.Level.Debug,
|
||||
LogMessage.Level.Silent,
|
||||
),
|
||||
valuesText = arrayOf(
|
||||
R.string.dont_modify,
|
||||
R.string.info,
|
||||
R.string.warning,
|
||||
R.string.error,
|
||||
R.string.debug,
|
||||
R.string.silent,
|
||||
),
|
||||
title = R.string.log_level,
|
||||
)
|
||||
|
||||
editableTextMap(
|
||||
value = configuration::hosts,
|
||||
keyAdapter = TextAdapter.String,
|
||||
valueAdapter = TextAdapter.String,
|
||||
title = R.string.hosts,
|
||||
placeholder = R.string.dont_modify,
|
||||
)
|
||||
|
||||
category(R.string.dns)
|
||||
|
||||
val dnsDependencies: MutableList<Preference> = mutableListOf()
|
||||
|
||||
val dns = selectableList(
|
||||
value = configuration.dns::enable,
|
||||
values = arrayOf(
|
||||
null,
|
||||
true,
|
||||
false
|
||||
),
|
||||
valuesText = arrayOf(
|
||||
R.string.dont_modify,
|
||||
R.string.force_enable,
|
||||
R.string.use_built_in,
|
||||
),
|
||||
title = R.string.strategy
|
||||
) {
|
||||
listener = OnChangedListener {
|
||||
if (configuration.dns.enable == false) {
|
||||
dnsDependencies.forEach {
|
||||
it.enabled = false
|
||||
}
|
||||
} else {
|
||||
dnsDependencies.forEach {
|
||||
it.enabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectableList(
|
||||
value = configuration.dns::preferH3,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.prefer_h3,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration.dns::listen,
|
||||
adapter = NullableTextAdapter.String,
|
||||
title = R.string.listen,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.disabled,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration.app::appendSystemDns,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.append_system_dns,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration.dns::ipv6,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.ipv6,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration.dns::useHosts,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.use_hosts,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration.dns::enhancedMode,
|
||||
values = arrayOf(
|
||||
null,
|
||||
ConfigurationOverride.DnsEnhancedMode.None,
|
||||
ConfigurationOverride.DnsEnhancedMode.FakeIp,
|
||||
ConfigurationOverride.DnsEnhancedMode.Mapping
|
||||
),
|
||||
valuesText = arrayOf(
|
||||
R.string.dont_modify,
|
||||
R.string.disabled,
|
||||
R.string.fakeip,
|
||||
R.string.mapping
|
||||
),
|
||||
title = R.string.enhanced_mode,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
editableTextList(
|
||||
value = configuration.dns::nameServer,
|
||||
adapter = TextAdapter.String,
|
||||
title = R.string.name_server,
|
||||
placeholder = R.string.dont_modify,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
editableTextList(
|
||||
value = configuration.dns::fallback,
|
||||
adapter = TextAdapter.String,
|
||||
title = R.string.fallback,
|
||||
placeholder = R.string.dont_modify,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
editableTextList(
|
||||
value = configuration.dns::defaultServer,
|
||||
adapter = TextAdapter.String,
|
||||
title = R.string.default_name_server,
|
||||
placeholder = R.string.dont_modify,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
editableTextList(
|
||||
value = configuration.dns::fakeIpFilter,
|
||||
adapter = TextAdapter.String,
|
||||
title = R.string.fakeip_filter,
|
||||
placeholder = R.string.dont_modify,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration.dns::fakeIPFilterMode,
|
||||
values = arrayOf(
|
||||
null,
|
||||
ConfigurationOverride.FilterMode.BlackList,
|
||||
ConfigurationOverride.FilterMode.WhiteList
|
||||
),
|
||||
valuesText = arrayOf(
|
||||
R.string.dont_modify,
|
||||
R.string.blacklist,
|
||||
R.string.whitelist
|
||||
),
|
||||
title = R.string.fakeip_filter_mode,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
selectableList(
|
||||
value = configuration.dns.fallbackFilter::geoIp,
|
||||
values = booleanValues,
|
||||
valuesText = booleanValuesText,
|
||||
title = R.string.geoip_fallback,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
editableText(
|
||||
value = configuration.dns.fallbackFilter::geoIpCode,
|
||||
adapter = NullableTextAdapter.String,
|
||||
title = R.string.geoip_fallback_code,
|
||||
placeholder = R.string.dont_modify,
|
||||
empty = R.string.raw_cn,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
editableTextList(
|
||||
value = configuration.dns.fallbackFilter::domain,
|
||||
adapter = TextAdapter.String,
|
||||
title = R.string.domain_fallback,
|
||||
placeholder = R.string.dont_modify,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
editableTextList(
|
||||
value = configuration.dns.fallbackFilter::ipcidr,
|
||||
adapter = TextAdapter.String,
|
||||
title = R.string.ipcidr_fallback,
|
||||
placeholder = R.string.dont_modify,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
editableTextMap(
|
||||
value = configuration.dns::nameserverPolicy,
|
||||
keyAdapter = TextAdapter.String,
|
||||
valueAdapter = TextAdapter.String,
|
||||
title = R.string.name_server_policy,
|
||||
placeholder = R.string.dont_modify,
|
||||
configure = dnsDependencies::add,
|
||||
)
|
||||
|
||||
dns.listener?.onChanged()
|
||||
}
|
||||
|
||||
binding.content.addView(screen.root)
|
||||
}
|
||||
|
||||
fun requestClear() {
|
||||
requests.trySend(Request.ResetOverride)
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.github.kr328.clash.network.ConfigResponse
|
||||
|
||||
object PreferenceManager {
|
||||
private const val PREF_NAME = "app_preferences"
|
||||
|
||||
private lateinit var prefs: SharedPreferences
|
||||
|
||||
fun init(context: Context) {
|
||||
prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE or Context.MODE_MULTI_PROCESS)
|
||||
}
|
||||
|
||||
fun clearData(){
|
||||
prefs.edit().clear().apply()
|
||||
}
|
||||
|
||||
var cached_userSubscritedata: String
|
||||
get() = prefs.getString("cached_userSubscritedata", "") ?: ""
|
||||
set(value) {
|
||||
prefs.edit().putString("cached_userSubscritedata", value).apply()
|
||||
}
|
||||
|
||||
var cache_timestamp: Long
|
||||
get() = prefs.getLong("cache_timestamp", 0) ?: 0
|
||||
set(value) {
|
||||
prefs.edit().putLong("cache_timestamp", value).apply()
|
||||
}
|
||||
var cached_data: String
|
||||
get() = prefs.getString("cached_data", "") ?: ""
|
||||
set(value) {
|
||||
prefs.edit().putString("cached_data", value).apply()
|
||||
}
|
||||
|
||||
var selectnodeName: String
|
||||
get() = prefs.getString("selectnodeName", "自动选择") ?: "自动选择"
|
||||
set(value) {
|
||||
prefs.edit().putString("selectnodeName", value).apply()
|
||||
}
|
||||
|
||||
var modeName: String
|
||||
get() = prefs.getString("modeName", "智能模式") ?: "智能模式"
|
||||
set(value) {
|
||||
prefs.edit().putString("modeName", value).apply()
|
||||
}
|
||||
|
||||
var loginemail: String
|
||||
get() = prefs.getString("loginemail", "") ?: ""
|
||||
set(value) {
|
||||
prefs.edit().putString("loginemail", value).apply()
|
||||
}
|
||||
|
||||
|
||||
var loginToken: String
|
||||
get() = prefs.getString("loginToken", "") ?: ""
|
||||
set(value) {
|
||||
prefs.edit().putString("loginToken", value).apply()
|
||||
}
|
||||
|
||||
var loginauthData: String
|
||||
get() = prefs.getString("loginauthData", "") ?: ""
|
||||
set(value) {
|
||||
prefs.edit().putString("loginauthData", value).apply()
|
||||
}
|
||||
|
||||
var isLoginin: Boolean
|
||||
get() = prefs.getBoolean("isLoginin", false) ?: false
|
||||
set(value) {
|
||||
prefs.edit().putBoolean("isLoginin", value).apply()
|
||||
}
|
||||
|
||||
var baseURL: String
|
||||
get() = prefs.getString("baseURL", "") ?: ""
|
||||
set(value) {
|
||||
prefs.edit().putString("baseURL", value).apply()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Save data to prefserences
|
||||
fun saveConfigToPreferences( configResponse: ConfigResponse) {
|
||||
|
||||
val editor = prefs.edit()
|
||||
|
||||
// Store each field individually
|
||||
editor.putString("baseURL", configResponse.baseURL)
|
||||
editor.putString("baseDYURL", configResponse.baseDYURL)
|
||||
editor.putString("mainregisterURL", configResponse.mainregisterURL)
|
||||
editor.putString("paymentURL", configResponse.paymentURL)
|
||||
editor.putString("telegramurl", configResponse.telegramurl)
|
||||
editor.putString("kefuurl", configResponse.kefuurl)
|
||||
editor.putString("websiteURL", configResponse.websiteURL)
|
||||
editor.putString("crisptoken", configResponse.crisptoken)
|
||||
editor.putString("banners", configResponse.banners.joinToString(","))
|
||||
|
||||
// Apply changes
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
// Retrieve data from SharedPreferences
|
||||
fun getConfigFromPreferences(context: Context): ConfigResponse? {
|
||||
|
||||
|
||||
// Retrieve each field individually
|
||||
val baseURL = prefs.getString("baseURL", null) ?: ""
|
||||
val baseDYURL = prefs.getString("baseDYURL", null) ?: ""
|
||||
val mainregisterURL = prefs.getString("mainregisterURL", null) ?: ""
|
||||
val paymentURL = prefs.getString("paymentURL", null) ?: ""
|
||||
val telegramurl = prefs.getString("telegramurl", null) ?: ""
|
||||
val kefuurl = prefs.getString("kefuurl", null) ?: ""
|
||||
val websiteURL = prefs.getString("websiteURL", null) ?: ""
|
||||
val crisptoken = prefs.getString("crisptoken", null) ?: ""
|
||||
val bannersString = prefs.getString("banners", null) ?: ""
|
||||
// If any field is null, return null (you can add your own null-check handling logic)
|
||||
// Convert the banners string back into a List
|
||||
val banners = bannersString.split(",").map { it.trim() } ?: emptyList()
|
||||
|
||||
return ConfigResponse(
|
||||
baseURL = baseURL,
|
||||
baseDYURL = baseDYURL,
|
||||
mainregisterURL = mainregisterURL,
|
||||
paymentURL = paymentURL,
|
||||
telegramurl = telegramurl,
|
||||
kefuurl = kefuurl,
|
||||
websiteURL = websiteURL,
|
||||
crisptoken = crisptoken,
|
||||
banners = banners, message = "", code = 1
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
//自动通知监听
|
||||
class PreferenceLiveData(private val context: Context,) : LiveData<String>() {
|
||||
private var pref: SharedPreferences? = null
|
||||
private val key: String = "selectnodeName"
|
||||
private val defValue: String = "自动选择"
|
||||
private var listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == this.key) {
|
||||
value = pref?.getString(key, defValue)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
pref = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
|
||||
pref?.registerOnSharedPreferenceChangeListener(listener)
|
||||
value = pref?.getString(key, defValue)
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
pref?.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.view.View
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
import android.os.Build
|
||||
import com.github.kr328.clash.core.bridge.Bridge
|
||||
import com.github.kr328.clash.design.MainDesign.Request
|
||||
import com.github.kr328.clash.design.databinding.ActivityProfileBinding
|
||||
|
||||
import com.github.kr328.clash.design.databinding.DesignProfilesBinding
|
||||
import com.github.kr328.clash.design.network.APIGlobalObject
|
||||
import com.github.kr328.clash.design.util.applyFrom
|
||||
import com.github.kr328.clash.design.util.bindAppBarElevation
|
||||
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.github.kr328.clash.network.ConfigResponse
|
||||
import com.github.kr328.clash.network.SubscribeData
|
||||
import com.github.kr328.clash.network.SubscribeResponse
|
||||
import com.github.kr328.clash.service.model.Profile
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class ProfileDesign (context: Context) : Design<ProfileDesign.Request>(context) {
|
||||
|
||||
enum class Request {
|
||||
OpenBuyPlan,
|
||||
OpenMyOrders,
|
||||
OpenMyBlance,
|
||||
OpenMyInvites,
|
||||
OpenCustomView,
|
||||
OpenMyGongdan,
|
||||
OpenLogout,
|
||||
OpenGSettings,
|
||||
OpenCustomViewIPtest,
|
||||
OpenCustomViewSpeed
|
||||
}
|
||||
|
||||
private var subData: SubscribeData? = null
|
||||
|
||||
private val binding = ActivityProfileBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
// binding.viewAccountinfo.setBackgroundColor(Color.parseColor("#FFFFFF")) // Semi-transparent white color
|
||||
// binding.viewAccountinfo2.setBackgroundColor(Color.parseColor("#FFFFFF")) // Semi-transparent white color
|
||||
// binding.viewAccountinfo3.setBackgroundColor(Color.parseColor("#FFFFFF")) // Semi-transparent white color
|
||||
// binding.viewAccountinfo4.setBackgroundColor(Color.parseColor("#FFFFFF")) // Semi-transparent white color
|
||||
|
||||
}
|
||||
|
||||
fun request(request: Request) {
|
||||
requests.trySend(request)
|
||||
}
|
||||
fun formatTimestamp(timestamp: Long): String {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) // 定义格式
|
||||
val date = Date(timestamp) // 将时间戳转为 Date 对象
|
||||
return dateFormat.format(date) // 格式化为字符串
|
||||
}
|
||||
|
||||
fun queryVersion(version: String){
|
||||
binding.textVersion.text = version
|
||||
}
|
||||
|
||||
suspend fun requestdataDisible() {
|
||||
|
||||
if ( APIGlobalObject.subData == null){
|
||||
val gson = Gson()
|
||||
val cached_userSubscritedata = gson.fromJson(PreferenceManager.cached_userSubscritedata, SubscribeResponse::class.java)
|
||||
APIGlobalObject.subData = cached_userSubscritedata.data
|
||||
}
|
||||
binding.accountEmail = PreferenceManager.loginemail
|
||||
|
||||
|
||||
if ((APIGlobalObject.subData?.plan?.id ?:0) > 0){
|
||||
|
||||
binding.accountPlanname = APIGlobalObject.subData?.plan?.name ?: ""
|
||||
if ( APIGlobalObject.subData?.expired_at == null){
|
||||
binding.accountPlantime = "该订阅长期有效"
|
||||
}else{
|
||||
binding.accountPlantime = "到期时间:${ formatTimestamp((APIGlobalObject.subData?.expired_at ?: 0) * 1000L)}"
|
||||
}
|
||||
val total = APIGlobalObject.subData?.plan?.transfer_enable ?: 0
|
||||
val upmb = (APIGlobalObject.subData?.d ?: 0.00).toDouble().div(1024).div(1024).div(1024)
|
||||
val downmb = (APIGlobalObject.subData?.u ?: 0.00).toDouble().div(1024).div(1024).div(1024)
|
||||
val usd = upmb + downmb
|
||||
binding.accountPlanuseinfo = "已用 ${String.format("%.2f", usd)}GB/总计 ${total}GB"
|
||||
|
||||
if (total>0 && usd>0) {
|
||||
binding.accountProgressValue = (usd /total.toDouble() * 100).toInt()
|
||||
if(usd > total){
|
||||
binding.useedprogressBar.progressTintList = ColorStateList.valueOf(
|
||||
// context.resolveThemedColor(R.attr.colorOnPrimary)
|
||||
Color.parseColor("#ce665e")
|
||||
)
|
||||
}else{
|
||||
|
||||
}
|
||||
}else{
|
||||
binding.accountProgressValue = 0
|
||||
}
|
||||
binding.accountBlance = "${ APIGlobalObject.subData?.transfer_enable }"
|
||||
}else{
|
||||
binding.accountPlanname = "未订阅任何套餐"
|
||||
binding.accountPlantime = "已过期"
|
||||
binding.accountPlanuseinfo = "已用 0GB/总计 0GB"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import com.github.kr328.clash.design.adapter.ProfileAdapter
|
||||
import com.github.kr328.clash.design.databinding.DesignProfilesBinding
|
||||
import com.github.kr328.clash.design.databinding.DialogProfilesMenuBinding
|
||||
import com.github.kr328.clash.design.dialog.AppBottomSheetDialog
|
||||
import com.github.kr328.clash.design.ui.ToastDuration
|
||||
import com.github.kr328.clash.design.util.*
|
||||
import com.github.kr328.clash.service.model.Profile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ProfilesDesign(context: Context) : Design<ProfilesDesign.Request>(context) {
|
||||
sealed class Request {
|
||||
object UpdateAll : Request()
|
||||
object Create : Request()
|
||||
data class Active(val profile: Profile) : Request()
|
||||
data class Update(val profile: Profile) : Request()
|
||||
data class Edit(val profile: Profile) : Request()
|
||||
data class Duplicate(val profile: Profile) : Request()
|
||||
data class Delete(val profile: Profile) : Request()
|
||||
}
|
||||
|
||||
private val binding = DesignProfilesBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
private val adapter = ProfileAdapter(context, this::requestActive, this::showMenu)
|
||||
|
||||
private var allUpdating: Boolean
|
||||
get() = adapter.states.allUpdating;
|
||||
set(value) {
|
||||
adapter.states.allUpdating = value
|
||||
}
|
||||
private val rotateAnimation : Animation = AnimationUtils.loadAnimation(context, R.anim.rotate_infinite)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
suspend fun patchProfiles(profiles: List<Profile>) {
|
||||
adapter.apply {
|
||||
patchDataSet(this::profiles, profiles, id = { it.uuid })
|
||||
}
|
||||
|
||||
val updatable = withContext(Dispatchers.Default) {
|
||||
profiles.any { it.imported && it.type != Profile.Type.File }
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.updateView.visibility = if (updatable) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requestSave(profile: Profile) {
|
||||
showToast(R.string.active_unsaved_tips, ToastDuration.Long) {
|
||||
setAction(R.string.edit) {
|
||||
requests.trySend(Request.Edit(profile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateElapsed() {
|
||||
adapter.updateElapsed()
|
||||
}
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.mainList.recyclerList.also {
|
||||
it.bindAppBarElevation(binding.activityBarLayout)
|
||||
it.applyLinearAdapter(context, adapter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMenu(profile: Profile) {
|
||||
val dialog = AppBottomSheetDialog(context)
|
||||
|
||||
val binding = DialogProfilesMenuBinding
|
||||
.inflate(context.layoutInflater, dialog.window?.decorView as ViewGroup?, false)
|
||||
|
||||
binding.master = this
|
||||
binding.self = dialog
|
||||
binding.profile = profile
|
||||
|
||||
dialog.setContentView(binding.root)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
fun requestUpdateAll() {
|
||||
allUpdating = true;
|
||||
changeUpdateAllButtonStatus()
|
||||
requests.trySend(Request.UpdateAll)
|
||||
}
|
||||
|
||||
fun finishUpdateAll() {
|
||||
allUpdating = false;
|
||||
changeUpdateAllButtonStatus()
|
||||
}
|
||||
|
||||
fun requestCreate() {
|
||||
requests.trySend(Request.Create)
|
||||
}
|
||||
|
||||
private fun requestActive(profile: Profile) {
|
||||
requests.trySend(Request.Active(profile))
|
||||
}
|
||||
|
||||
fun requestUpdate(dialog: Dialog, profile: Profile) {
|
||||
requests.trySend(Request.Update(profile))
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
fun requestEdit(dialog: Dialog, profile: Profile) {
|
||||
requests.trySend(Request.Edit(profile))
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
fun requestDuplicate(dialog: Dialog, profile: Profile) {
|
||||
requests.trySend(Request.Duplicate(profile))
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
fun requestDelete(dialog: Dialog, profile: Profile) {
|
||||
requests.trySend(Request.Delete(profile))
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
private fun changeUpdateAllButtonStatus() {
|
||||
if (allUpdating) {
|
||||
binding.updateView.startAnimation(rotateAnimation)
|
||||
} else {
|
||||
binding.updateView.clearAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import com.github.kr328.clash.core.model.FetchStatus
|
||||
import com.github.kr328.clash.design.databinding.DesignPropertiesBinding
|
||||
import com.github.kr328.clash.design.dialog.ModelProgressBarConfigure
|
||||
import com.github.kr328.clash.design.dialog.requestModelTextInput
|
||||
import com.github.kr328.clash.design.dialog.withModelProgressBar
|
||||
import com.github.kr328.clash.design.util.*
|
||||
import com.github.kr328.clash.service.model.Profile
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class PropertiesDesign(context: Context) : Design<PropertiesDesign.Request>(context) {
|
||||
sealed class Request {
|
||||
object Commit : Request()
|
||||
object BrowseFiles : Request()
|
||||
}
|
||||
|
||||
private val binding = DesignPropertiesBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
var profile: Profile
|
||||
get() = binding.profile!!
|
||||
set(value) {
|
||||
binding.profile = value
|
||||
}
|
||||
|
||||
val progressing: Boolean
|
||||
get() = binding.processing
|
||||
|
||||
suspend fun withProcessing(executeTask: suspend (suspend (FetchStatus) -> Unit) -> Unit) {
|
||||
try {
|
||||
binding.processing = true
|
||||
|
||||
context.withModelProgressBar {
|
||||
configure {
|
||||
isIndeterminate = true
|
||||
text = context.getString(R.string.initializing)
|
||||
}
|
||||
|
||||
executeTask {
|
||||
configure {
|
||||
applyFrom(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
binding.processing = false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requestExitWithoutSaving(): Boolean {
|
||||
return withContext(Dispatchers.Main) {
|
||||
suspendCancellableCoroutine { ctx ->
|
||||
val dialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.exit_without_save)
|
||||
.setMessage(R.string.exit_without_save_warning)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> ctx.resume(true) }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setOnDismissListener { if (!ctx.isCompleted) ctx.resume(false) }
|
||||
.show()
|
||||
|
||||
ctx.invokeOnCancellation { dialog.dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.tips.text = context.getHtml(R.string.tips_properties)
|
||||
|
||||
binding.scrollRoot.bindAppBarElevation(binding.activityBarLayout)
|
||||
}
|
||||
|
||||
fun inputName() {
|
||||
launch {
|
||||
val name = context.requestModelTextInput(
|
||||
initial = profile.name,
|
||||
title = context.getText(R.string.name),
|
||||
hint = context.getText(R.string.properties),
|
||||
error = context.getText(R.string.should_not_be_blank),
|
||||
validator = ValidatorNotBlank
|
||||
)
|
||||
|
||||
if (name != profile.name) {
|
||||
profile = profile.copy(name = name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun inputUrl() {
|
||||
if (profile.type == Profile.Type.External)
|
||||
return
|
||||
|
||||
launch {
|
||||
val url = context.requestModelTextInput(
|
||||
initial = profile.source,
|
||||
title = context.getText(R.string.url),
|
||||
hint = context.getText(R.string.profile_url),
|
||||
error = context.getText(R.string.accept_http_content),
|
||||
validator = ValidatorHttpUrl
|
||||
)
|
||||
|
||||
if (url != profile.source) {
|
||||
profile = profile.copy(source = url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun inputInterval() {
|
||||
launch {
|
||||
var minutes = TimeUnit.MILLISECONDS.toMinutes(profile.interval)
|
||||
|
||||
minutes = context.requestModelTextInput(
|
||||
initial = if (minutes == 0L) "" else minutes.toString(),
|
||||
title = context.getText(R.string.auto_update),
|
||||
hint = context.getText(R.string.auto_update_minutes),
|
||||
error = context.getText(R.string.at_least_15_minutes),
|
||||
validator = ValidatorAutoUpdateInterval
|
||||
).toLongOrNull() ?: 0
|
||||
|
||||
val interval = TimeUnit.MINUTES.toMillis(minutes)
|
||||
|
||||
if (interval != profile.interval) {
|
||||
profile = profile.copy(interval = interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestCommit() {
|
||||
requests.trySend(Request.Commit)
|
||||
}
|
||||
|
||||
fun requestBrowseFiles() {
|
||||
requests.trySend(Request.BrowseFiles)
|
||||
}
|
||||
|
||||
private fun ModelProgressBarConfigure.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
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.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ProvidersDesign(
|
||||
context: Context,
|
||||
providers: List<Provider>,
|
||||
) : Design<ProvidersDesign.Request>(context) {
|
||||
sealed class Request {
|
||||
data class Update(val index: Int, val provider: Provider) : Request()
|
||||
}
|
||||
|
||||
private val binding = DesignProvidersBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
private val adapter = ProviderAdapter(context, providers) { index, provider ->
|
||||
requests.trySend(Request.Update(index, provider))
|
||||
}
|
||||
|
||||
fun updateElapsed() {
|
||||
adapter.updateElapsed()
|
||||
}
|
||||
|
||||
suspend fun notifyUpdated(index: Int) {
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.notifyUpdated(index)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun notifyChanged(index: Int) {
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.notifyChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.mainList.recyclerList.bindAppBarElevation(binding.activityBarLayout)
|
||||
binding.mainList.recyclerList.applyLinearAdapter(context, adapter)
|
||||
}
|
||||
|
||||
fun requestUpdateAll() {
|
||||
adapter.states.filter { !it.updating }.forEachIndexed { index, state ->
|
||||
state.updating = true
|
||||
|
||||
requests.trySend(Request.Update(index, state.provider))
|
||||
}
|
||||
}
|
||||
}
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.github.kr328.clash.core.model.Proxy
|
||||
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.adapter.ServerListAdapter
|
||||
import com.github.kr328.clash.design.component.ProxyMenu
|
||||
import com.github.kr328.clash.design.component.ProxyViewConfig
|
||||
import com.github.kr328.clash.design.databinding.DesignProxyBinding
|
||||
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.github.kr328.clash.design.view.CustomDividerItemDecoration
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ProxyDesign(
|
||||
context: Context,
|
||||
overrideMode: TunnelState.Mode?,
|
||||
groupNames: List<String>,
|
||||
uiStore: UiStore,
|
||||
) : Design<ProxyDesign.Request>(context) {
|
||||
sealed class Request {
|
||||
object ReloadAll : Request()
|
||||
object ReLaunch : Request()
|
||||
|
||||
data class PatchMode(val mode: TunnelState.Mode?) : Request()
|
||||
data class Reload(val index: Int) : Request()
|
||||
data class Select(val index: Int, val name: String) : Request()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
override val root: View = binding.root
|
||||
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var adapter: ServerListAdapter
|
||||
|
||||
|
||||
var urlTesting: Boolean = false
|
||||
|
||||
/*
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
suspend fun updateGroup(
|
||||
position: Int,
|
||||
proxies: List<Proxy>,
|
||||
selectable: Boolean,
|
||||
parent: ProxyState,
|
||||
links: Map<String, ProxyState>
|
||||
) {
|
||||
adapter.updateAdapter(position, proxies, selectable, parent, links)
|
||||
|
||||
adapter.states[position].urlTesting = false
|
||||
|
||||
updateUrlTestButtonStatus()
|
||||
}
|
||||
|
||||
suspend fun requestRedrawVisible() {
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.requestRedrawVisible()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
suspend fun updateGroup(
|
||||
position: Int,
|
||||
proxies: List<Proxy>,
|
||||
selectable: Boolean,
|
||||
parent: ProxyState,
|
||||
links: Map<String, ProxyState>
|
||||
) {
|
||||
adapter.updateAdapter(position, proxies, selectable, parent, links)
|
||||
|
||||
adapter.urlTesting = false
|
||||
|
||||
updateUrlTestButtonStatus()
|
||||
}
|
||||
|
||||
suspend fun requestRedrawVisible() {
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.requestRedrawVisible()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.menuView.setOnClickListener {
|
||||
menu.show()
|
||||
}
|
||||
|
||||
if (groupNames.isEmpty() ) {
|
||||
binding.emptyView.visibility = View.VISIBLE
|
||||
binding.urlTestView.visibility = View.GONE
|
||||
//binding.pagesView.visibility = View.GONE
|
||||
binding.urlTestFloatView.visibility = View.GONE
|
||||
}else{
|
||||
|
||||
binding.urlTestView.visibility = View.VISIBLE
|
||||
binding.urlTestProgressView.visibility = View.GONE
|
||||
|
||||
|
||||
binding.urlTestFloatView.supportImageTintList = ColorStateList.valueOf(
|
||||
context.resolveThemedColor(R.attr.colorOnPrimary)
|
||||
)
|
||||
|
||||
// 初始化 RecyclerView
|
||||
recyclerView =binding.recyclerView
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
adapter = ServerListAdapter( surface,
|
||||
config){ name ->
|
||||
requests.trySend(Request.Select(0, name))
|
||||
}
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
|
||||
binding.baseSwipeRefreshLayout.setOnRefreshListener {
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.baseSwipeRefreshLayout.isRefreshing = true
|
||||
}
|
||||
requests.trySend(Request.Reload(0))
|
||||
}
|
||||
}
|
||||
|
||||
// 设置自定义分割线
|
||||
val customDivider = CustomDividerItemDecoration(1, Color.LTGRAY) // 4px高的灰色分割线
|
||||
recyclerView.addItemDecoration(customDivider)
|
||||
|
||||
// val firstObj = groupNames.first()
|
||||
// val newgroupNames = listOf(firstObj)
|
||||
// println(groupNames)
|
||||
}
|
||||
/*
|
||||
if (groupNames.isEmpty() ) {
|
||||
binding.emptyView.visibility = View.VISIBLE
|
||||
|
||||
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(R.attr.colorOnPrimary)
|
||||
)
|
||||
|
||||
val firstObj = groupNames.first()
|
||||
val newgroupNames = listOf(firstObj)
|
||||
|
||||
binding.pagesView.apply {
|
||||
adapter = ProxyPageAdapter(
|
||||
surface,
|
||||
config,
|
||||
List(groupNames.size) { index ->
|
||||
ProxyAdapter(config) { name ->
|
||||
requests.trySend(Request.Select(index, name))
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
if (it == currentItem)
|
||||
updateUrlTestButtonStatus()
|
||||
}
|
||||
|
||||
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
horizontalScrolling = state != ViewPager2.SCROLL_STATE_IDLE
|
||||
|
||||
updateUrlTestButtonStatus()
|
||||
}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
uiStore.proxyLastGroup = groupNames[position]
|
||||
}
|
||||
})
|
||||
}
|
||||
TabLayoutMediator(binding.tabLayoutView, binding.pagesView) { tab, index ->
|
||||
tab.text = groupNames[index]
|
||||
}.attach()
|
||||
|
||||
val initialPosition = groupNames.indexOf(uiStore.proxyLastGroup)
|
||||
|
||||
binding.pagesView.post {
|
||||
if (initialPosition > 0)
|
||||
binding.pagesView.setCurrentItem(initialPosition, false)
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
/*
|
||||
fun requestUrlTesting() {
|
||||
urlTesting = true
|
||||
|
||||
requests.trySend(Request.UrlTest(binding.pagesView.currentItem))
|
||||
|
||||
updateUrlTestButtonStatus()
|
||||
}
|
||||
|
||||
private fun updateUrlTestButtonStatus() {
|
||||
if (verticalBottomScrolled || horizontalScrolling || urlTesting) {
|
||||
binding.urlTestFloatView.hide()
|
||||
} else {
|
||||
binding.urlTestFloatView.show()
|
||||
}
|
||||
|
||||
if (urlTesting) {
|
||||
binding.urlTestView.visibility = View.GONE
|
||||
binding.urlTestProgressView.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.urlTestView.visibility = View.VISIBLE
|
||||
binding.urlTestProgressView.visibility = View.GONE
|
||||
}
|
||||
} */
|
||||
|
||||
fun requestUrlTesting() {
|
||||
urlTesting = true
|
||||
|
||||
requests.trySend(Request.UrlTest(0))
|
||||
|
||||
updateUrlTestButtonStatus()
|
||||
}
|
||||
private fun updateUrlTestButtonStatus() {
|
||||
|
||||
if (urlTesting) {
|
||||
binding.urlTestView.visibility = View.GONE
|
||||
binding.urlTestProgressView.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.urlTestView.visibility = View.VISIBLE
|
||||
binding.urlTestProgressView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun finishreferesh() {
|
||||
binding.baseSwipeRefreshLayout.isRefreshing = false
|
||||
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import com.github.kr328.clash.design.databinding.DesignSettingsBinding
|
||||
import com.github.kr328.clash.design.util.applyFrom
|
||||
import com.github.kr328.clash.design.util.bindAppBarElevation
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.root
|
||||
|
||||
class SettingsDesign(context: Context) : Design<SettingsDesign.Request>(context) {
|
||||
enum class Request {
|
||||
StartApp, StartNetwork, StartOverride, StartMetaFeature,
|
||||
}
|
||||
|
||||
private val binding = DesignSettingsBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
init {
|
||||
binding.self = this
|
||||
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
binding.scrollRoot.bindAppBarElevation(binding.activityBarLayout)
|
||||
}
|
||||
|
||||
fun request(request: Request) {
|
||||
requests.trySend(request)
|
||||
}
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.transition.Visibility
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.design.ConfigOrderDesign.Request
|
||||
import com.github.kr328.clash.design.databinding.ActivityPaymentItemBinding
|
||||
import com.github.kr328.clash.design.databinding.ActivityPlanItemBinding
|
||||
import com.github.kr328.clash.design.databinding.ActivitySubmitorderBinding
|
||||
import com.github.kr328.clash.design.network.APIGlobalObject
|
||||
import com.github.kr328.clash.design.ui.ToastDuration
|
||||
import com.github.kr328.clash.design.util.applyFrom
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.root
|
||||
import com.github.kr328.clash.design.util.showCustomDialog
|
||||
import com.github.kr328.clash.network.ApiClient
|
||||
import com.github.kr328.clash.network.ApiService
|
||||
import com.github.kr328.clash.network.CheckoutrderRequest
|
||||
import com.github.kr328.clash.network.PaymentData
|
||||
import com.github.kr328.clash.network.PublicResponse
|
||||
import com.github.kr328.clash.network.QueryOrderData
|
||||
import com.github.kr328.clash.network.QueryOrderRequest
|
||||
import com.github.kr328.clash.network.SaveOrderRequest
|
||||
import com.github.kr328.clash.network.SubmitOrderResponse
|
||||
import com.github.kr328.clash.network.safeApiRequestCall
|
||||
import com.github.kr328.clash.utity.LoadingDialog
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
class SubmitOrderViewDesign (context: Context) : Design<SubmitOrderViewDesign.Request>(context) {
|
||||
|
||||
enum class Request {
|
||||
SubmitPay,
|
||||
}
|
||||
|
||||
|
||||
private var currentChoose: PaymentData? = null
|
||||
|
||||
private var tradeNo: String = ""
|
||||
|
||||
|
||||
private val binding = ActivitySubmitorderBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
override val root: View
|
||||
get() = binding.root
|
||||
|
||||
|
||||
fun request(request: Request) {
|
||||
if (request == Request.SubmitPay) {
|
||||
//提交订单
|
||||
LoadingDialog.show(context, "正在结账中...")
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val apiService = ApiClient.retrofit.create(ApiService::class.java)
|
||||
safeApiRequestCall { apiService.checkoutOrder(PreferenceManager.loginauthData, CheckoutrderRequest(tradeNo,currentChoose?.id ?: 0 ))}.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
LoadingDialog.hide()
|
||||
}
|
||||
|
||||
if (it != null && it.isSuccessful){
|
||||
if (it.body()?.data != null){
|
||||
val url = it.body()?.data ?: ""
|
||||
if (url.length > 3){
|
||||
|
||||
try {
|
||||
withContext(Dispatchers.Main) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
intent.setData(Uri.parse(url))
|
||||
context.startActivity(intent)
|
||||
|
||||
//弹出支付框
|
||||
context.showCustomDialog(
|
||||
title = "温馨提示",
|
||||
message = "完成支付后若未到账,请重启一下客户端即可同步订阅套餐时长。",
|
||||
positiveButtonText = "确定",
|
||||
negativeButtonText = "取消",
|
||||
onPositiveClick = {
|
||||
fetchTradeInfo(tradeNo)
|
||||
|
||||
launch {
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
onNegativeClick = {
|
||||
// 执行取消操作
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("当前手机未安装浏览器")
|
||||
}
|
||||
}
|
||||
}
|
||||
println(it.body())
|
||||
}else{
|
||||
val errorinfo = (it?.errorBody()?.string())
|
||||
if (errorinfo != null){
|
||||
//解析错错误信息
|
||||
|
||||
val gson = Gson()
|
||||
val submitResponse = gson.fromJson(errorinfo, PublicResponse::class.java)
|
||||
|
||||
if (submitResponse?.message != null){
|
||||
withContext(Dispatchers.Main) {
|
||||
showToast("结算请求失败:${submitResponse.message}", ToastDuration.Long)
|
||||
}
|
||||
}
|
||||
}else{
|
||||
withContext(Dispatchers.Main) {
|
||||
showToast("结算失败:请求数据失败", ToastDuration.Long)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
init {
|
||||
binding.self = this
|
||||
binding.activityBarLayout.applyFrom(context)
|
||||
|
||||
}
|
||||
|
||||
fun fetchTradeInfo(trade_no: String?){
|
||||
tradeNo = trade_no ?: ""
|
||||
LoadingDialog.show(context, "请求订单数据...")
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val apiService = ApiClient.retrofit.create(ApiService::class.java)
|
||||
safeApiRequestCall { apiService.getOrderdetail(PreferenceManager.loginauthData,trade_no?: "")}.let {
|
||||
|
||||
if(it != null && it.isSuccessful){
|
||||
if (it.body()?.data != null){
|
||||
withContext(Dispatchers.Main) {
|
||||
fillData(it.body()?.data)
|
||||
LoadingDialog.hide()
|
||||
// 清空 binding.container 的所有子视图
|
||||
binding.container.removeAllViews()
|
||||
|
||||
}
|
||||
var selectedIndex = 0
|
||||
|
||||
//继续获取支付方式
|
||||
safeApiRequestCall { apiService.getPaymentList(PreferenceManager.loginauthData)}.let {
|
||||
if(it != null && it.isSuccessful){
|
||||
println("${it.body()}")
|
||||
withContext(Dispatchers.Main) {
|
||||
it.body().let {
|
||||
it?.data.let {
|
||||
if (it != null) {
|
||||
for (i in it.indices) {
|
||||
// 设置点击事件和单选逻辑
|
||||
val frameLayout =
|
||||
ActivityPaymentItemBinding.inflate(
|
||||
context.layoutInflater,
|
||||
context.root,
|
||||
false
|
||||
)
|
||||
val item = it[i]
|
||||
frameLayout.typeTextView.text = item.name
|
||||
frameLayout.amountTextView.text = "手续费:${item.handling_fee_percent ?: "0.00"}%"
|
||||
|
||||
|
||||
frameLayout.planitemFrameLayout.setOnClickListener {
|
||||
selectedIndex = i
|
||||
currentChoose = item
|
||||
|
||||
for (j in 0 until binding.container.childCount) {
|
||||
val child =
|
||||
binding.container.getChildAt(j)
|
||||
println("selectedIndex: ${selectedIndex} ${child.background} ")
|
||||
child.background =
|
||||
ContextCompat.getDrawable(
|
||||
context,
|
||||
if (j == selectedIndex) R.drawable.card_border_selected else R.drawable.card_border
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.container.addView(frameLayout.root)
|
||||
|
||||
|
||||
|
||||
if (i == 0){
|
||||
//默认选中第一个
|
||||
currentChoose = it[0]
|
||||
//设置手续费显示:
|
||||
|
||||
//binding.configorderPlanFeeAmount = currentChoose.handling_fee_percent
|
||||
frameLayout.planitemFrameLayout.background = ContextCompat.getDrawable(
|
||||
context, R.drawable.card_border_selected
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}else{
|
||||
val errorinfo = (it?.errorBody()?.string())
|
||||
if (errorinfo != null){
|
||||
//解析错错误信息
|
||||
|
||||
val gson = Gson()
|
||||
val submitResponse = gson.fromJson(errorinfo, PublicResponse::class.java)
|
||||
|
||||
if (submitResponse?.message != null){
|
||||
withContext(Dispatchers.Main) {
|
||||
showToast("订单请求失败:${submitResponse.message}", ToastDuration.Long)
|
||||
}
|
||||
}
|
||||
}else{
|
||||
withContext(Dispatchers.Main) {
|
||||
showToast("下单失败:请求数据失败", ToastDuration.Long)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
LoadingDialog.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun formatTimestamp(timestamp: Long): String {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) // 定义格式
|
||||
val date = Date(timestamp) // 将时间戳转为 Date 对象
|
||||
return dateFormat.format(date) // 格式化为字符串
|
||||
}
|
||||
|
||||
private fun fillData(data: QueryOrderData?) {
|
||||
data.let {
|
||||
binding.configorderPlanname = "商品名称:${data?.plan?.name}"
|
||||
binding.configorderTradeno = "订单号:${data?.trade_no}"
|
||||
binding.configorderPlanname2 = data?.plan?.name
|
||||
val amount = (data?.total_amount?.toDouble() ?: 0.0)/100
|
||||
binding.configorderPlanamount = "¥ ${amount}0"
|
||||
binding.configorderPlantype = "类型/周期:${data?.periodZh}"
|
||||
binding.configorderPlantransfer = "商品流量:${data?.plan?.transfer_enable} GB"
|
||||
binding.configorderTimer = "创建时间:${ formatTimestamp((data?.created_at ?: 0) * 1000L)}"
|
||||
binding.orderDetailsStatus.text = data?.statusZh
|
||||
if (data?.status == 0){
|
||||
binding.confirmPaymentButton.visibility = View.VISIBLE
|
||||
}else{
|
||||
binding.confirmPaymentButton.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.databinding.AdapterAppBinding
|
||||
import com.github.kr328.clash.design.model.AppInfo
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.root
|
||||
|
||||
class AppAdapter(
|
||||
private val context: Context,
|
||||
private val selected: MutableSet<String>,
|
||||
) : RecyclerView.Adapter<AppAdapter.Holder>() {
|
||||
class Holder(val binding: AdapterAppBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
var apps: List<AppInfo> = emptyList()
|
||||
|
||||
fun rebindAll() {
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
return Holder(
|
||||
AdapterAppBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
val current = apps[position]
|
||||
|
||||
holder.binding.app = current
|
||||
holder.binding.selected = current.packageName in selected
|
||||
holder.binding.root.setOnClickListener {
|
||||
if (holder.binding.selected) {
|
||||
selected.remove(current.packageName)
|
||||
holder.binding.selected = false
|
||||
} else {
|
||||
selected.add(current.packageName)
|
||||
holder.binding.selected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return apps.size
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.databinding.AdapterEditableTextListBinding
|
||||
import com.github.kr328.clash.design.preference.TextAdapter
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
|
||||
class EditableTextListAdapter<T>(
|
||||
private val context: Context,
|
||||
val values: MutableList<T>,
|
||||
private val adapter: TextAdapter<T>,
|
||||
) : RecyclerView.Adapter<EditableTextListAdapter.Holder>() {
|
||||
class Holder(val binding: AdapterEditableTextListBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
fun addElement(text: String) {
|
||||
val value = adapter.to(text)
|
||||
|
||||
notifyItemInserted(values.size)
|
||||
values.add(value)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
return Holder(
|
||||
AdapterEditableTextListBinding
|
||||
.inflate(context.layoutInflater, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
val current = values[position]
|
||||
|
||||
holder.binding.textView.text = adapter.from(current)
|
||||
holder.binding.deleteView.setOnClickListener {
|
||||
val index = values.indexOf(current)
|
||||
|
||||
if (index >= 0) {
|
||||
values.removeAt(index)
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return values.size
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.databinding.AdapterEditableTextMapBinding
|
||||
import com.github.kr328.clash.design.preference.TextAdapter
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
|
||||
class EditableTextMapAdapter<K, V>(
|
||||
private val context: Context,
|
||||
val values: MutableList<Pair<K, V>>,
|
||||
private val keyAdapter: TextAdapter<K>,
|
||||
private val valueAdapter: TextAdapter<V>,
|
||||
) : RecyclerView.Adapter<EditableTextMapAdapter.Holder>() {
|
||||
class Holder(val binding: AdapterEditableTextMapBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
fun addElement(key: String, value: String) {
|
||||
val keyValue = keyAdapter.to(key)
|
||||
val valueValue = valueAdapter.to(value)
|
||||
|
||||
notifyItemInserted(values.size)
|
||||
values.add(keyValue to valueValue)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
return Holder(
|
||||
AdapterEditableTextMapBinding
|
||||
.inflate(context.layoutInflater, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
val current = values[position]
|
||||
|
||||
holder.binding.keyView.text = keyAdapter.from(current.first)
|
||||
holder.binding.valueView.text = valueAdapter.from(current.second)
|
||||
holder.binding.deleteView.setOnClickListener {
|
||||
val index = values.indexOf(current)
|
||||
|
||||
if (index >= 0) {
|
||||
values.removeAt(index)
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return values.size
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.databinding.AdapterFileBinding
|
||||
import com.github.kr328.clash.design.model.File
|
||||
import com.github.kr328.clash.design.ui.ObservableCurrentTime
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
|
||||
class FileAdapter(
|
||||
private val context: Context,
|
||||
private val open: (File) -> Unit,
|
||||
private val more: (File) -> Unit,
|
||||
) : RecyclerView.Adapter<FileAdapter.Holder>() {
|
||||
class Holder(val binding: AdapterFileBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
private val currentTime = ObservableCurrentTime()
|
||||
|
||||
var files: List<File> = emptyList()
|
||||
|
||||
fun updateElapsed() {
|
||||
currentTime.update()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
return Holder(
|
||||
AdapterFileBinding
|
||||
.inflate(context.layoutInflater, parent, false)
|
||||
.also { it.currentTime = currentTime }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
val current = files[position]
|
||||
|
||||
holder.binding.apply {
|
||||
file = current
|
||||
|
||||
setOpen {
|
||||
open(current)
|
||||
}
|
||||
|
||||
setMore {
|
||||
more(current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return files.size
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.github.kr328.clash.design
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class ImageSliderAdapter(private val imageList: List<Int>) : RecyclerView.Adapter<ImageSliderAdapter.ImageViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.image_slider_item, parent, false)
|
||||
return ImageViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||
holder.imageView.setImageResource(imageList[position])
|
||||
|
||||
with(holder) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return imageList.size
|
||||
}
|
||||
|
||||
class ImageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val imageView: ImageView = view.findViewById(R.id.slider_image)
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.CircleCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.github.kr328.clash.design.R
|
||||
|
||||
class ImageSliderImagesAdapter(private val imageUrls: List<String>,
|
||||
private val onItemClick: (Int) -> Unit // 接收点击事件
|
||||
) :
|
||||
RecyclerView.Adapter<ImageSliderImagesAdapter.ImageSliderViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageSliderViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_image_slider, parent, false)
|
||||
|
||||
return ImageSliderViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ImageSliderViewHolder, position: Int) {
|
||||
val imageUrl = imageUrls[position]
|
||||
Glide.with(holder.imageView.context)
|
||||
.load(imageUrl) // 网络图片的 URL
|
||||
|
||||
.placeholder(R.drawable.placeholder) // 占位图
|
||||
.error(R.drawable.placeholder) // 加载失败时的图片
|
||||
.into(holder.imageView)
|
||||
// 设置点击事件
|
||||
// holder.imageView.apply { RequestOptions().transform(RoundedCorners(30)) }// 圆角处理
|
||||
holder.imageView.setOnClickListener {
|
||||
onItemClick(position) // 传递当前图片的 URL 或其他数据
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = imageUrls.size
|
||||
|
||||
class ImageSliderViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val imageView: ImageView = view.findViewById(R.id.imageView)
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.model.LogFile
|
||||
import com.github.kr328.clash.design.util.format
|
||||
import com.github.kr328.clash.design.view.ActionLabel
|
||||
|
||||
class LogFileAdapter(
|
||||
private val context: Context,
|
||||
private val open: (LogFile) -> Unit,
|
||||
) : RecyclerView.Adapter<LogFileAdapter.Holder>() {
|
||||
class Holder(val label: ActionLabel) : RecyclerView.ViewHolder(label)
|
||||
|
||||
var logs: List<LogFile> = emptyList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
return Holder(ActionLabel(context).apply {
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
val current = logs[position]
|
||||
|
||||
holder.label.text = current.fileName
|
||||
holder.label.subtext = current.date.format(context)
|
||||
holder.label.setOnClickListener {
|
||||
open(current)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return logs.size
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.core.model.LogMessage
|
||||
import com.github.kr328.clash.design.databinding.AdapterLogMessageBinding
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
|
||||
class LogMessageAdapter(
|
||||
private val context: Context,
|
||||
private val copy: (LogMessage) -> Unit,
|
||||
) :
|
||||
RecyclerView.Adapter<LogMessageAdapter.Holder>() {
|
||||
class Holder(val binding: AdapterLogMessageBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
var messages: List<LogMessage> = emptyList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
return Holder(
|
||||
AdapterLogMessageBinding
|
||||
.inflate(context.layoutInflater, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
val current = messages[position]
|
||||
|
||||
holder.binding.message = current
|
||||
holder.binding.root.setOnLongClickListener {
|
||||
copy(current)
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return messages.size
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.HorizontalScrollView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.network.OrderData
|
||||
import com.github.kr328.clash.network.PlanData
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class OrdersDataAdapter(private val onItemClick: (plan: OrderData) -> Unit ) : RecyclerView.Adapter<OrdersDataAdapterViewHolder>() {
|
||||
|
||||
private val subscriptions = mutableListOf<OrderData>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OrdersDataAdapterViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.activity_item_subscription, parent, false)
|
||||
return OrdersDataAdapterViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: OrdersDataAdapterViewHolder, position: Int) {
|
||||
val subscription = subscriptions[position]
|
||||
holder.bind(subscription)
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClick(subscription) // 将点击的 position 传递给回调函数
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return subscriptions.size
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setData(newData: List<OrderData>) {
|
||||
subscriptions.clear()
|
||||
subscriptions.addAll(newData)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun addData(newData: List<OrderData>) {
|
||||
subscriptions.addAll(newData)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class OrdersDataAdapterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
private val planName: TextView = itemView.findViewById(R.id.subscriptionName)
|
||||
private val planDescription: TextView = itemView.findViewById(R.id.subscriptionDescription)
|
||||
private val plantime: TextView = itemView.findViewById(R.id.subscriptionTime)
|
||||
|
||||
private val subscriptionAmount: TextView = itemView.findViewById(R.id.subscriptionAmount)
|
||||
private val subscriptionStatus: TextView = itemView.findViewById(R.id.subscriptionStatus)
|
||||
|
||||
private val groups_Scrollview: HorizontalScrollView = itemView.findViewById(R.id.groups_Scrollview)
|
||||
|
||||
|
||||
fun formatTimestamp(timestamp: Long): String {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) // 定义格式
|
||||
val date = Date(timestamp) // 将时间戳转为 Date 对象
|
||||
return dateFormat.format(date) // 格式化为字符串
|
||||
}
|
||||
|
||||
fun bind(data: OrderData) {
|
||||
// 绑定数据
|
||||
|
||||
planName.text = data.plan?.name + "(" + data.periodZh + ")"
|
||||
planDescription.text = "${data.plan?.transfer_enable ?: 0}GB"
|
||||
plantime.text = formatTimestamp((data.created_at ?: 0 ) * 1000L)
|
||||
|
||||
subscriptionStatus.text = data.statusZh
|
||||
val formattedAmount = data.total_amount?.div(100)
|
||||
subscriptionAmount.text = " ¥${formattedAmount}"
|
||||
|
||||
groups_Scrollview.visibility = View.GONE
|
||||
|
||||
|
||||
//// 如果需要加载图片,可以使用图像加载库,如 Glide 或 Picasso
|
||||
// Glide.with(itemView.context)
|
||||
// .load(subscription.planImageUrl)
|
||||
// .into(planImage)
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.network.PlanData
|
||||
|
||||
class PlanDataAdapter( private val onItemClick: (plan: PlanData) -> Unit ) : RecyclerView.Adapter<SubscriptionViewHolder>() {
|
||||
|
||||
private val subscriptions = mutableListOf<PlanData>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.activity_item_subscription, parent, false)
|
||||
return SubscriptionViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
|
||||
val subscription = subscriptions[position]
|
||||
holder.bind(subscription)
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClick(subscription) // 将点击的 position 传递给回调函数
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return subscriptions.size
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setData(newData: List<PlanData>) {
|
||||
subscriptions.clear()
|
||||
subscriptions.addAll(newData)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun addData(newData: List<PlanData>) {
|
||||
subscriptions.addAll(newData)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SubscriptionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
private val planName: TextView = itemView.findViewById(R.id.subscriptionName)
|
||||
private val planPrice: TextView = itemView.findViewById(R.id.subscriptionTime)
|
||||
private val planDescription: TextView = itemView.findViewById(R.id.subscriptionDescription)
|
||||
|
||||
private val grouponemonth: RelativeLayout = itemView.findViewById(R.id.grouponemohtthtest)
|
||||
private val groupquitytthtest: RelativeLayout = itemView.findViewById(R.id.groupquitytthtest)
|
||||
private val grouphelfyear: RelativeLayout = itemView.findViewById(R.id.grouphelfyear)
|
||||
private val grouponeyear: RelativeLayout = itemView.findViewById(R.id.grouponeyear)
|
||||
|
||||
private val onemohtthtest: TextView = itemView.findViewById(R.id.onemohtthtest)
|
||||
private val onequietythtest: TextView = itemView.findViewById(R.id.onequietythtest)
|
||||
private val helfyearthtest: TextView = itemView.findViewById(R.id.helfyearthtest)
|
||||
private val oneyearthtest: TextView = itemView.findViewById(R.id.oneyearthtest)
|
||||
|
||||
|
||||
private val onemonthprice: TextView = itemView.findViewById(R.id.onemonthprice)
|
||||
private val onequietyprice: TextView = itemView.findViewById(R.id.onequietyprice)
|
||||
private val helfyearprice: TextView = itemView.findViewById(R.id.helfyearprice)
|
||||
private val oneyearprice: TextView = itemView.findViewById(R.id.oneyearprice)
|
||||
|
||||
|
||||
private val onemonthpriceday: TextView = itemView.findViewById(R.id.onemonthpriceday)
|
||||
private val onequietypriceday: TextView = itemView.findViewById(R.id.onequietypriceday)
|
||||
private val helfyearpriceday: TextView = itemView.findViewById(R.id.helfyearpriceday)
|
||||
private val oneyearpriceday: TextView = itemView.findViewById(R.id.oneyearpriceday)
|
||||
|
||||
|
||||
fun bind(data: PlanData) {
|
||||
// 绑定数据
|
||||
planName.text = data.name
|
||||
|
||||
if (data.onetime_price == null) {
|
||||
planPrice.text = "流量:${data.transfer_enable ?: 0}GB"
|
||||
planDescription.text = "¥${(data.month_price ?: 0.00).toDouble()/100}"
|
||||
|
||||
val month_price = (data.month_price ?: 0.00).toDouble()/100
|
||||
val quarter_price = (data.quarter_price ?: 0.00).toDouble()/100
|
||||
val half_year_price = (data.half_year_price ?: 0.00).toDouble()/100
|
||||
val year_price = (data.year_price ?: 0.00).toDouble()/100
|
||||
|
||||
if (month_price > 0.0) {
|
||||
onemonthprice.text = "${month_price}"
|
||||
onemonthpriceday.text = "${String.format ("%.2f",month_price/30)}元/天"
|
||||
}else{
|
||||
grouponemonth.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (quarter_price > 0.0) {
|
||||
onequietyprice.text = "${quarter_price}"
|
||||
onequietypriceday.text = "${String.format ("%.2f",quarter_price/90)}元/天"
|
||||
}else{
|
||||
groupquitytthtest.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (half_year_price > 0.0) {
|
||||
helfyearprice.text = "${half_year_price}"
|
||||
helfyearpriceday.text = "${String.format ("%.2f",half_year_price/180)}元/天"
|
||||
}else{
|
||||
grouphelfyear.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (year_price > 0.0) {
|
||||
oneyearprice.text = "${year_price}"
|
||||
oneyearpriceday.text = "${String.format ("%.2f",year_price/360)}元/天"
|
||||
}else{
|
||||
grouponeyear.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
||||
}else{
|
||||
onemohtthtest.text = "一次性"
|
||||
planPrice.text = "流量:${data.transfer_enable ?: 0}GB 一次性"
|
||||
planDescription.text = "¥${(data.onetime_price ?: 0.00).toDouble()/100}"
|
||||
val month_price = (data.onetime_price ?: 0.00).toDouble()/100
|
||||
val transform = data.transfer_enable ?: 0
|
||||
onemonthprice.text = "${month_price}"
|
||||
onemonthpriceday.text = "${String.format ("%.2f",month_price/transform)}元/GB"
|
||||
|
||||
groupquitytthtest.visibility = View.GONE
|
||||
grouphelfyear.visibility = View.GONE
|
||||
grouponeyear.visibility = View.GONE
|
||||
}
|
||||
|
||||
//// 如果需要加载图片,可以使用图像加载库,如 Glide 或 Picasso
|
||||
// Glide.with(itemView.context)
|
||||
// .load(subscription.planImageUrl)
|
||||
// .into(planImage)
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.TextView
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.resolveThemedColor
|
||||
|
||||
class PopupListAdapter(
|
||||
private val context: Context,
|
||||
private val texts: List<CharSequence>,
|
||||
private val selected: Int,
|
||||
) : BaseAdapter() {
|
||||
private val colorPrimary = context.resolveThemedColor(R.attr.colorPrimary)
|
||||
private val colorOnPrimary = context.resolveThemedColor(R.attr.colorOnPrimary)
|
||||
private val colorControlNormal = context.resolveThemedColor(R.attr.colorControlNormal)
|
||||
|
||||
override fun getCount(): Int {
|
||||
return texts.size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Any {
|
||||
return texts[position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return texts[position].hashCode().toLong()
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
val view = convertView ?: context.layoutInflater
|
||||
.inflate(android.R.layout.simple_list_item_1, parent, false)
|
||||
|
||||
val text: TextView = view.findViewById(android.R.id.text1)
|
||||
|
||||
text.text = texts[position]
|
||||
|
||||
if (position == selected) {
|
||||
text.setBackgroundColor(
|
||||
Color.argb(
|
||||
200,
|
||||
Color.red(colorPrimary),
|
||||
Color.green(colorPrimary),
|
||||
Color.blue(colorPrimary)
|
||||
)
|
||||
)
|
||||
text.setTextColor(colorOnPrimary)
|
||||
} else {
|
||||
text.setBackgroundColor(Color.TRANSPARENT)
|
||||
text.setTextColor(colorControlNormal)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.databinding.AdapterProfileBinding
|
||||
import com.github.kr328.clash.design.model.ProfilePageState
|
||||
import com.github.kr328.clash.design.model.ProxyPageState
|
||||
import com.github.kr328.clash.design.ui.ObservableCurrentTime
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.service.model.Profile
|
||||
|
||||
class ProfileAdapter(
|
||||
private val context: Context,
|
||||
private val onClicked: (Profile) -> Unit,
|
||||
private val onMenuClicked: (Profile) -> Unit,
|
||||
) : RecyclerView.Adapter<ProfileAdapter.Holder>() {
|
||||
class Holder(val binding: AdapterProfileBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
private val currentTime = ObservableCurrentTime()
|
||||
|
||||
var profiles: List<Profile> = emptyList()
|
||||
val states = ProfilePageState()
|
||||
|
||||
fun updateElapsed() {
|
||||
currentTime.update()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
return Holder(
|
||||
AdapterProfileBinding
|
||||
.inflate(context.layoutInflater, parent, false)
|
||||
.also { it.currentTime = currentTime }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
val current = profiles[position]
|
||||
val binding = holder.binding
|
||||
|
||||
if (current === binding.profile)
|
||||
return
|
||||
|
||||
binding.profile = current
|
||||
binding.setClicked {
|
||||
onClicked(current)
|
||||
}
|
||||
binding.setMenu {
|
||||
onMenuClicked(current)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return profiles.size
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.databinding.AdapterProfileProviderBinding
|
||||
import com.github.kr328.clash.design.model.ProfileProvider
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
|
||||
class ProfileProviderAdapter(
|
||||
private val context: Context,
|
||||
private val select: (ProfileProvider) -> Unit,
|
||||
private val detail: (ProfileProvider) -> Boolean,
|
||||
) : RecyclerView.Adapter<ProfileProviderAdapter.Holder>() {
|
||||
class Holder(val binding: AdapterProfileProviderBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
var providers: List<ProfileProvider> = emptyList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
return Holder(
|
||||
AdapterProfileProviderBinding.inflate(
|
||||
context.layoutInflater,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
val current = providers[position]
|
||||
val binding = holder.binding
|
||||
|
||||
binding.provider = current
|
||||
|
||||
binding.root.apply {
|
||||
setOnClickListener {
|
||||
select(current)
|
||||
}
|
||||
setOnLongClickListener {
|
||||
detail(current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return providers.size
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
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<Provider>,
|
||||
private val requestUpdate: (Int, Provider) -> Unit,
|
||||
) : RecyclerView.Adapter<ProviderAdapter.Holder>() {
|
||||
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
|
||||
holder.binding.update = View.OnClickListener {
|
||||
state.updating = true
|
||||
|
||||
requestUpdate(position, state.provider)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return states.size
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import com.github.kr328.clash.design.component.ProxyView
|
||||
import com.github.kr328.clash.design.component.ProxyViewConfig
|
||||
/*
|
||||
class ProxyAdapter(
|
||||
private val config: ProxyViewConfig,
|
||||
private val clicked: (String) -> Unit,
|
||||
) : RecyclerView.Adapter<ProxyAdapter.Holder>() {
|
||||
class Holder(val view: ProxyView) : RecyclerView.ViewHolder(view)
|
||||
|
||||
var selectable: Boolean = false
|
||||
var states: List<ProxyViewState> = 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
|
||||
}
|
||||
}
|
||||
*/
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.component.ProxyViewState
|
||||
import com.github.kr328.clash.design.databinding.DesignProxyBinding
|
||||
|
||||
class ProxyAdapter(
|
||||
private val config: ProxyViewConfig,
|
||||
private val clicked: (String) -> Unit,
|
||||
) : RecyclerView.Adapter<ProxyAdapter.Holder>() {
|
||||
|
||||
// class Holder(val binding: DesignProxyBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
class Holder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val vpnnodemainid:View = view.findViewById(R.id.vpnnodemainid)
|
||||
val proxyName: TextView = view.findViewById(R.id.proxy_name)
|
||||
val proxyLatency: TextView = view.findViewById(R.id.proxy_latency)
|
||||
val proxySubName:TextView = view.findViewById(R.id.proxy_subtitle)
|
||||
val selectnodeselectImage:ImageView = view.findViewById(R.id.nodeselectImage)
|
||||
val viewsss:View = view.findViewById(R.id.view_iteminfo)
|
||||
|
||||
var drawableStartGreen: Drawable? = null // 用于存储 drawableStart 图标
|
||||
var drawableStartGray: Drawable? = null // 用于存储 drawableStart 图标
|
||||
}
|
||||
|
||||
var selectable: Boolean = false
|
||||
var states: List<ProxyViewState> = emptyList()
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProxyAdapter.Holder {
|
||||
|
||||
val drawableStartGreen = ContextCompat.getDrawable(parent.context, R.drawable.check_24px)?.let {
|
||||
// 包装 drawable 以支持 Tint
|
||||
val wrappedDrawable = DrawableCompat.wrap(it)
|
||||
// 设置 Tint 颜色
|
||||
DrawableCompat.setTint(wrappedDrawable, Color.parseColor("#2a9843"))
|
||||
wrappedDrawable
|
||||
}
|
||||
|
||||
val drawableStartGray = ContextCompat.getDrawable(parent.context, R.drawable.ic_baseline_info)?.let {
|
||||
// 包装 drawable 以支持 Tint
|
||||
val wrappedDrawable = DrawableCompat.wrap(it)
|
||||
// 设置 Tint 颜色
|
||||
DrawableCompat.setTint(wrappedDrawable, Color.parseColor("#6b6d6a"))
|
||||
wrappedDrawable
|
||||
}
|
||||
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.proxy_view_item, parent, false)
|
||||
return Holder(view).apply {
|
||||
// 设置 drawable 到 ViewHolder 的属性,供后续使用
|
||||
this.drawableStartGreen = drawableStartGreen
|
||||
this.drawableStartGray = drawableStartGray
|
||||
}
|
||||
}
|
||||
|
||||
var selectableView: Holder? = null
|
||||
var selectableData: ProxyViewState? = null
|
||||
|
||||
var state: ProxyViewState? = null
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
val current = states[position]
|
||||
|
||||
|
||||
holder.apply {
|
||||
state = current
|
||||
|
||||
viewsss.setBackgroundColor(Color.parseColor("#10FFFFFF")) // Semi-transparent white color
|
||||
proxyName.text = current.proxy.name
|
||||
//proxySubName.setColorFilter(ContextCompat.getColor(context, R.color.yourColor), PorterDuff.Mode.SRC_IN)
|
||||
|
||||
if (current.proxy.delay > 10000){
|
||||
proxySubName.text = "超时" //current.proxy.subtitle
|
||||
proxySubName.setTextColor(Color.parseColor("#6b6d6a"))
|
||||
proxySubName.setCompoundDrawablesWithIntrinsicBounds(drawableStartGray, null, null, null)
|
||||
proxyLatency.setTextColor(Color.parseColor("#6b6d6a"))
|
||||
proxyLatency.text = "..."
|
||||
}else if (current.proxy.delay > 500 && current.proxy.delay < 10000){
|
||||
proxyLatency.text = "${current.proxy.delay}ms"
|
||||
proxyLatency.setTextColor(Color.YELLOW)
|
||||
proxySubName.text = "在线可用" //current.proxy.subtitle
|
||||
proxySubName.setTextColor(Color.parseColor("#2a9843"))
|
||||
proxySubName.setCompoundDrawablesWithIntrinsicBounds(drawableStartGreen, null, null, null)
|
||||
}else if (current.proxy.delay < 500 && current.proxy.delay > 300){
|
||||
proxyLatency.text = "${current.proxy.delay}ms"
|
||||
proxyLatency.setTextColor(Color.parseColor("#fab610"))
|
||||
proxySubName.text = "在线可用" //current.proxy.subtitle
|
||||
proxySubName.setTextColor(Color.parseColor("#2a9843"))
|
||||
proxySubName.setCompoundDrawablesWithIntrinsicBounds(drawableStartGreen, null, null, null)
|
||||
}else{
|
||||
proxyLatency.text = "${current.proxy.delay}ms"
|
||||
proxyLatency.setTextColor(Color.parseColor("#2a9843"))
|
||||
proxySubName.text = "在线可用" //current.proxy.subtitle
|
||||
proxySubName.setTextColor(Color.parseColor("#2a9843"))
|
||||
proxySubName.setCompoundDrawablesWithIntrinsicBounds(drawableStartGreen, null, null, null)
|
||||
}
|
||||
// 示例:延迟时间
|
||||
|
||||
|
||||
// current.update(true)
|
||||
|
||||
vpnnodemainid.setOnClickListener {
|
||||
|
||||
if (selectableView != null) {
|
||||
if(selectableView == holder){
|
||||
//do nothing
|
||||
|
||||
}else{
|
||||
//之前的设置为未选择
|
||||
selectableData?.selected = false
|
||||
selectableView?.selectnodeselectImage?.setImageResource( android.R.color.transparent)
|
||||
|
||||
current.selected = true
|
||||
|
||||
selectableView = holder
|
||||
selectableData = current
|
||||
clicked(current.proxy.name)
|
||||
current.update(true)
|
||||
selectnodeselectImage.setImageResource((R.drawable.checkmarkcirclefill))
|
||||
|
||||
|
||||
}
|
||||
}else
|
||||
{
|
||||
|
||||
selectableView?.selectnodeselectImage?.setImageResource( android.R.color.transparent)
|
||||
|
||||
current.selected = true
|
||||
|
||||
selectableView = holder
|
||||
selectableData = current
|
||||
clicked(current.proxy.name)
|
||||
current.update(true)
|
||||
selectnodeselectImage.setImageResource((R.drawable.checkmarkcirclefill))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
current.update(true)
|
||||
|
||||
if (current.selected){
|
||||
selectableView = holder
|
||||
selectableData = current
|
||||
}
|
||||
|
||||
holder.selectnodeselectImage.setImageResource(if (current.selected) R.drawable.checkmarkcirclefill else android.R.color.transparent)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return states.size
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
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.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ProxyPageAdapter(
|
||||
private val surface: Surface,
|
||||
private val config: ProxyViewConfig,
|
||||
private val adapters: List<ProxyAdapter>,
|
||||
private val stateChanged: (Int) -> Unit,
|
||||
) : RecyclerView.Adapter<ProxyPageFactory.Holder>() { //<ProxyPageAdapter.VPNViewHolder>(){ //
|
||||
private val factory = ProxyPageFactory(config)
|
||||
private var parent: RecyclerView? = null
|
||||
|
||||
val states = List(adapters.size) { ProxyPageState() }
|
||||
|
||||
suspend fun updateAdapter(
|
||||
position: Int,
|
||||
proxies: List<Proxy>,
|
||||
selectable: Boolean,
|
||||
parent: ProxyState,
|
||||
links: Map<String, ProxyState>
|
||||
) {
|
||||
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()
|
||||
}
|
||||
/*
|
||||
// ViewHolder class to hold and recycle views for RecyclerView items
|
||||
inner class VPNViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val flagImageView: ImageView = itemView.findViewById(R.id.flagImageView)
|
||||
val vpnInfoTextView: TextView = itemView.findViewById(R.id.vpnInfoTextView)
|
||||
val speedTextView: TextView = itemView.findViewById(R.id.speedTextView)
|
||||
}
|
||||
|
||||
// This function inflates the item layout and returns the ViewHolder
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VPNViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_vpn, parent, false)
|
||||
return VPNViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: VPNViewHolder, position: Int) {
|
||||
val adapter = adapters[position]
|
||||
|
||||
states[position].bottom = false
|
||||
|
||||
Log.i("onBindViewHolder"," ${ adapter.states.size}")
|
||||
|
||||
// holder.flagImageView.setImageResource(vpn.countryFlag)
|
||||
holder.vpnInfoTextView.text = "xxx"
|
||||
holder.speedTextView.text = "xx"
|
||||
}
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
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.adapter.ProxyAdapter.Holder
|
||||
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.firstVisibleView
|
||||
import com.github.kr328.clash.design.util.invalidateChildren
|
||||
import com.github.kr328.clash.design.util.swapDataSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.internal.filterList
|
||||
|
||||
//private val stateChanged: (Int) -> Unit,
|
||||
class ServerListAdapter(private val surface: Surface,
|
||||
private val config: ProxyViewConfig,
|
||||
private val clicked: (String) -> Unit,) :
|
||||
RecyclerView.Adapter<ServerListAdapter.ViewHolder>() {
|
||||
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val flag: ImageView = view.findViewById(R.id.country_flag)
|
||||
val vpnnodemainid:View = view.findViewById(R.id.vpnnodemainid)
|
||||
val serverInfo: TextView = view.findViewById(R.id.server_info)
|
||||
val serverLimit: TextView = view.findViewById(R.id.server_limit)
|
||||
val signal: ImageView = view.findViewById(R.id.signal_strength)
|
||||
val latency: TextView = view.findViewById(R.id.latency)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val HEADER_VIEW_TYPE = 0
|
||||
private const val ITEM_VIEW_TYPE = 1
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
// 判断是否是第一行,如果是,返回 HEADER_VIEW_TYPE,否则返回 ITEM_VIEW_TYPE
|
||||
return if (position == 0) HEADER_VIEW_TYPE else ITEM_VIEW_TYPE
|
||||
}
|
||||
|
||||
var urlTesting: Boolean = true
|
||||
|
||||
var servers: List<ProxyViewState> = emptyList()
|
||||
|
||||
var selectable: Boolean = false
|
||||
|
||||
suspend fun updateAdapter(
|
||||
position: Int,
|
||||
proxies: List<Proxy>,
|
||||
selectable: Boolean,
|
||||
parent: ProxyState,
|
||||
links: Map<String, ProxyState>
|
||||
) {
|
||||
|
||||
var newlistProxy = mutableListOf<Proxy>()
|
||||
// newlistProxy.add(Proxy("智能连接","智能连接","自动切换速度最快的节点",Proxy.Type.Unknown,0))
|
||||
newlistProxy.addAll(proxies)
|
||||
|
||||
val states = withContext(Dispatchers.Default) {
|
||||
newlistProxy.map {
|
||||
val link = if (it.type.group) links[it.name] else null
|
||||
ProxyViewState(config, it, parent, link)
|
||||
}
|
||||
}
|
||||
|
||||
/* states.forEach {
|
||||
println(">>>>> ${it.proxy.title} ${it.proxy.subtitle} ${it.proxy.type.name} ${it.proxy.delay}")
|
||||
}*/
|
||||
val newstates = states.filterNot {
|
||||
it.proxy.title.uppercase() == "DIRECT"
|
||||
|| it.proxy.title.uppercase() == "REJECT"
|
||||
|| it.proxy.subtitle == "Selector"
|
||||
|| it.proxy.title.contains("流量")
|
||||
|| it.proxy.title.contains("故障转移")
|
||||
|| it.proxy.title.contains("套餐到期")
|
||||
|| it.proxy.title.contains("节点异常")
|
||||
|| it.proxy.title.contains("续费")
|
||||
|| it.proxy.title.contains("充值")
|
||||
|| it.proxy.title.contains("官网")
|
||||
|| it.proxy.title.contains("剩余")
|
||||
}
|
||||
|
||||
|
||||
this.swapDataSet(this::servers, newstates, false)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
requestRedrawVisible()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun requestRedrawVisible() {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.server_item_layout, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
val countryMap = mapOf(
|
||||
"美国" to "us",
|
||||
"United States" to "us",
|
||||
"中国" to "zh",
|
||||
"China" to "zh",
|
||||
"香港" to "hk",
|
||||
"Hong" to "hk",
|
||||
"日本" to "jp",
|
||||
"Japan" to "jp",
|
||||
"新加坡" to "sg",
|
||||
"Singapore" to "sg",
|
||||
"越南" to "vn",
|
||||
"Vietnam" to "vn",
|
||||
"马来西亚" to "my",
|
||||
"Malaysia" to "my",
|
||||
"泰国" to "th",
|
||||
"Thailand" to "th",
|
||||
"韩国" to "kr",
|
||||
"South Korea" to "kr",
|
||||
"巴西" to "br",
|
||||
"Brazil" to "br",
|
||||
"印度" to "in",
|
||||
"India" to "in",
|
||||
"智利" to "cl",
|
||||
"Chile" to "cl",
|
||||
"迪拜" to "ae",
|
||||
"United Arab" to "ae",
|
||||
"德国" to "de",
|
||||
"Germany" to "de",
|
||||
"法国" to "fr",
|
||||
"France" to "fr",
|
||||
"英国" to "gb",
|
||||
"Great Britain" to "gb",
|
||||
"意大利" to "it",
|
||||
"Italy" to "it",
|
||||
"澳大利亚" to "au",
|
||||
"Australia" to "au",
|
||||
"土耳其" to "tr",
|
||||
"台湾" to "tw",
|
||||
"Turkey" to "tr"
|
||||
// 添加其他国家映射
|
||||
)
|
||||
|
||||
fun getCountryCodeFromLog(logLine: String): String? {
|
||||
// 遍历 countryMap,找到匹配的国家名称
|
||||
for ((countryName, countryCode) in countryMap) {
|
||||
if (logLine.contains(countryName)) {
|
||||
return countryCode
|
||||
}
|
||||
}
|
||||
return "" // 没有匹配的国家名称
|
||||
}
|
||||
|
||||
var state: ProxyViewState? = null
|
||||
|
||||
var selectableView: ViewHolder? = null
|
||||
var selectableData: ProxyViewState? = null
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
|
||||
val server = servers[position]
|
||||
server.update(true)
|
||||
|
||||
holder.apply {
|
||||
state = server
|
||||
|
||||
}
|
||||
|
||||
if (server.selected){
|
||||
selectableView = holder
|
||||
selectableData = server
|
||||
}
|
||||
val flog_name = getCountryCodeFromLog(server.title) ?: ""
|
||||
val resourceId = holder.itemView.context.resources.getIdentifier("country_flag_"+flog_name, "drawable", holder.itemView.context.packageName)
|
||||
|
||||
// 检查资源 ID 是否有效
|
||||
if (resourceId != 0) {
|
||||
holder.flag.setImageResource(resourceId)
|
||||
}else{
|
||||
val aiicon = holder.itemView.context.resources.getIdentifier("icon_ai", "drawable", holder.itemView.context.packageName)
|
||||
holder.flag.setImageResource(aiicon)
|
||||
}
|
||||
|
||||
holder.serverInfo.text = server.title
|
||||
|
||||
holder.serverInfo.text = server.title
|
||||
|
||||
if (server.title == "自动选择"){
|
||||
holder.serverLimit.text = "智能匹配最优路线"
|
||||
}else{
|
||||
holder.serverLimit.text = server.subtitle
|
||||
}
|
||||
|
||||
//holder.signal.setImageResource(server.signalRes)
|
||||
|
||||
if ( server.proxy.delay > 5000){
|
||||
val resourcelow = holder.itemView.context.resources.getIdentifier("ic_signal_no", "drawable", holder.itemView.context.packageName)
|
||||
holder.signal.setImageResource(resourcelow)
|
||||
}else if (server.proxy.delay >= 1000 && server.proxy.delay < 5000){
|
||||
val resourcelow = holder.itemView.context.resources.getIdentifier("ic_signal_low", "drawable", holder.itemView.context.packageName)
|
||||
holder.signal.setImageResource(resourcelow)
|
||||
}else if (server.proxy.delay > 500 && server.proxy.delay < 1000){
|
||||
val resourcelow = holder.itemView.context.resources.getIdentifier("ic_signal_two", "drawable", holder.itemView.context.packageName)
|
||||
holder.signal.setImageResource(resourcelow)
|
||||
}else if (server.proxy.delay < 500 && server.proxy.delay > 300){
|
||||
val resourcelow = holder.itemView.context.resources.getIdentifier("ic_signal_three", "drawable", holder.itemView.context.packageName)
|
||||
holder.signal.setImageResource(resourcelow)
|
||||
}else{
|
||||
val resourcelow = holder.itemView.context.resources.getIdentifier("ic_signal_center", "drawable", holder.itemView.context.packageName)
|
||||
holder.signal.setImageResource(resourcelow)
|
||||
}
|
||||
|
||||
if (server.delayText.length > 0){
|
||||
holder.latency.text = " ${server.delayText}ms"
|
||||
}else{
|
||||
holder.latency.text = " 0ms"
|
||||
}
|
||||
|
||||
if (server.selected)
|
||||
{
|
||||
holder.serverInfo.setTextColor(Color.parseColor("#2a9843"))
|
||||
}else{
|
||||
holder.serverInfo.setTextColor(Color.parseColor("#888888"))
|
||||
}
|
||||
|
||||
val current = server
|
||||
holder.vpnnodemainid.setOnClickListener {
|
||||
// selectable = true
|
||||
// server.selected = true
|
||||
|
||||
if (selectableView != null) {
|
||||
if(selectableView == holder){
|
||||
//do nothing
|
||||
clicked(current.proxy.name)
|
||||
}else{
|
||||
//之前的设置为未选择
|
||||
selectableData?.selected = false
|
||||
|
||||
current.selected = true
|
||||
|
||||
selectableView = holder
|
||||
selectableData = current
|
||||
clicked(current.proxy.name)
|
||||
current.update(true)
|
||||
|
||||
}
|
||||
}else
|
||||
{
|
||||
|
||||
current.selected = true
|
||||
|
||||
selectableView = holder
|
||||
selectableData = current
|
||||
clicked(current.proxy.name)
|
||||
current.update(true)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = servers.size
|
||||
}
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.network.TicketMessage
|
||||
import com.github.kr328.clash.network.TicketsData
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
class TicketDetailAdapter( private val onItemClick: (plan: TicketMessage) -> Unit ) : RecyclerView.Adapter<RecyclerView.ViewHolder >() {
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE_LEFT = 0
|
||||
const val VIEW_TYPE_RIGHT = 1
|
||||
}
|
||||
|
||||
|
||||
private val subscriptions = mutableListOf<TicketMessage>()
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (subscriptions[position].is_me ?: false) VIEW_TYPE_RIGHT else VIEW_TYPE_LEFT
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
||||
return if (viewType == VIEW_TYPE_RIGHT) {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.activity_item_message_right, parent, false)
|
||||
RightTicketDetailViewHolder(view)
|
||||
} else {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.activity_item_message_left, parent, false)
|
||||
LeftTicketDetailViewHolder(view)
|
||||
}
|
||||
|
||||
// val viewlef = LayoutInflater.from(parent.context).inflate(R.layout.activity_item_message_left, parent, false)
|
||||
// return TicketDetailViewHolder(viewlef)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val message = subscriptions[position]
|
||||
if (holder is RightTicketDetailViewHolder) {
|
||||
holder.bind(message)
|
||||
} else if (holder is LeftTicketDetailViewHolder) {
|
||||
holder.bind(message)
|
||||
}
|
||||
}
|
||||
|
||||
// override fun onBindViewHolder(holder: TicketDetailViewHolder, position: Int) {
|
||||
// val subscription = subscriptions[position]
|
||||
// holder.bind(subscription)
|
||||
// holder.itemView.setOnClickListener {
|
||||
// onItemClick(subscription) // 将点击的 position 传递给回调函数
|
||||
// }
|
||||
// }
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return subscriptions.size
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setData(newData: List<TicketMessage>) {
|
||||
subscriptions.clear()
|
||||
subscriptions.addAll(newData)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun addData(newData: List<TicketMessage>) {
|
||||
subscriptions.addAll(newData)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
class LeftTicketDetailViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
private val titleTextView: TextView = itemView.findViewById(R.id.messageTextView)
|
||||
private val creationTimeTextView: TextView = itemView.findViewById(R.id.timeTextView)
|
||||
fun formatTimestamp(timestamp: Long): String {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) // 定义格式
|
||||
val date = Date(timestamp*1000L) // 将时间戳转为 Date 对象
|
||||
return dateFormat.format(date) // 格式化为字符串
|
||||
}
|
||||
fun bind(data: TicketMessage) {
|
||||
// 绑定数据
|
||||
titleTextView.text = "${data.message ?: ""}"
|
||||
|
||||
creationTimeTextView.text = "${formatTimestamp(data.created_at ?: 0)}"
|
||||
}
|
||||
}
|
||||
|
||||
class RightTicketDetailViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
private val titleTextView: TextView = itemView.findViewById(R.id.messageTextView)
|
||||
private val creationTimeTextView: TextView = itemView.findViewById(R.id.timeTextView)
|
||||
fun formatTimestamp(timestamp: Long): String {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) // 定义格式
|
||||
val date = Date(timestamp*1000L) // 将时间戳转为 Date 对象
|
||||
return dateFormat.format(date) // 格式化为字符串
|
||||
}
|
||||
fun bind(data: TicketMessage) {
|
||||
// 绑定数据
|
||||
titleTextView.text = "${data.message ?: ""}"
|
||||
|
||||
creationTimeTextView.text = "${formatTimestamp(data.created_at ?: 0)}"
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package com.github.kr328.clash.design.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.util.showCustomDialog
|
||||
import com.github.kr328.clash.network.TicketsData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
//class
|
||||
|
||||
class TicketsDataAdapter( private val onItemClick: (plan: TicketsData) -> Unit , private val onCloseItemClick: (plan: TicketsData) -> Unit ) : RecyclerView.Adapter<TicketViewHolder>() {
|
||||
|
||||
private val subscriptions = mutableListOf<TicketsData>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TicketViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.activity_gondan_item, parent, false)
|
||||
return TicketViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TicketViewHolder, position: Int) {
|
||||
val subscription = subscriptions[position]
|
||||
holder.bind(subscription)
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClick(subscription) // 将点击的 position 传递给回调函数
|
||||
}
|
||||
holder.viewButton.setOnClickListener {
|
||||
onItemClick(subscription) // 将点击的 position 传递给回调函数
|
||||
}
|
||||
holder.closeButton.setOnClickListener {
|
||||
onCloseItemClick(subscription)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return subscriptions.size
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setData(newData: List<TicketsData>) {
|
||||
subscriptions.clear()
|
||||
subscriptions.addAll(newData)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun addData(newData: List<TicketsData>) {
|
||||
subscriptions.addAll(newData)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
class TicketViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
private val titleTextView: TextView = itemView.findViewById(R.id.titleTextView)
|
||||
private val creationTimeTextView: TextView = itemView.findViewById(R.id.creationTimeTextView)
|
||||
private val statusTextView: TextView = itemView.findViewById(R.id.statusTextView)
|
||||
val closeButton: Button = itemView.findViewById(R.id.closeButton)
|
||||
val viewButton: TextView = itemView.findViewById(R.id.viewButton)
|
||||
|
||||
fun formatTimestamp(timestamp: Long): String {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) // 定义格式
|
||||
val date = Date(timestamp*1000L) // 将时间戳转为 Date 对象
|
||||
return dateFormat.format(date) // 格式化为字符串
|
||||
}
|
||||
fun bind(data: TicketsData) {
|
||||
// 绑定数据
|
||||
titleTextView.text = "#${data.id ?: 0} - ${data.subject ?: ""}"
|
||||
|
||||
creationTimeTextView.text = "创建时间: ${formatTimestamp(data.created_at ?: 0)}"
|
||||
if (data.status == 0){
|
||||
statusTextView.text = "当前状态: 待回复"
|
||||
closeButton.visibility = View.VISIBLE
|
||||
|
||||
}else{
|
||||
statusTextView.text = "当前状态: 已关闭"
|
||||
closeButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
//// 如果需要加载图片,可以使用图像加载库,如 Glide 或 Picasso
|
||||
// Glide.with(itemView.context)
|
||||
// .load(subscription.planImageUrl)
|
||||
// .into(planImage)
|
||||
}
|
||||
}
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
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.design.AccessControlDesign.Request
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.model.AppInfoSort
|
||||
import com.github.kr328.clash.design.store.UiStore
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
||||
class AccessControlMenu(
|
||||
context: Context,
|
||||
menuView: View,
|
||||
private val uiStore: UiStore,
|
||||
private val requests: Channel<Request>,
|
||||
) : PopupMenu.OnMenuItemClickListener {
|
||||
private val menu = PopupMenu(context, menuView)
|
||||
|
||||
fun show() {
|
||||
menu.show()
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
if (item.isCheckable)
|
||||
item.isChecked = !item.isChecked
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.select_all ->
|
||||
requests.trySend(Request.SelectAll)
|
||||
R.id.select_none ->
|
||||
requests.trySend(Request.SelectNone)
|
||||
R.id.select_invert ->
|
||||
requests.trySend(Request.SelectInvert)
|
||||
R.id.system_apps -> {
|
||||
uiStore.accessControlSystemApp = !item.isChecked
|
||||
|
||||
requests.trySend(Request.ReloadApps)
|
||||
}
|
||||
R.id.name -> {
|
||||
uiStore.accessControlSort = AppInfoSort.Label
|
||||
|
||||
requests.trySend(Request.ReloadApps)
|
||||
}
|
||||
R.id.package_name -> {
|
||||
uiStore.accessControlSort = AppInfoSort.PackageName
|
||||
|
||||
requests.trySend(Request.ReloadApps)
|
||||
}
|
||||
R.id.install_time -> {
|
||||
uiStore.accessControlSort = AppInfoSort.InstallTime
|
||||
|
||||
requests.trySend(Request.ReloadApps)
|
||||
}
|
||||
R.id.update_time -> {
|
||||
uiStore.accessControlSort = AppInfoSort.UpdateTime
|
||||
|
||||
requests.trySend(Request.ReloadApps)
|
||||
}
|
||||
R.id.reverse -> {
|
||||
uiStore.accessControlReverse = item.isChecked
|
||||
|
||||
requests.trySend(Request.ReloadApps)
|
||||
}
|
||||
R.id.import_from_clipboard -> {
|
||||
requests.trySend(Request.Import)
|
||||
}
|
||||
R.id.export_to_clipboard -> {
|
||||
requests.trySend(Request.Export)
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
init {
|
||||
menu.menuInflater.inflate(R.menu.menu_access_control, menu.menu)
|
||||
|
||||
when (uiStore.accessControlSort) {
|
||||
AppInfoSort.Label ->
|
||||
menu.menu.findItem(R.id.name).isChecked = true
|
||||
AppInfoSort.PackageName ->
|
||||
menu.menu.findItem(R.id.package_name).isChecked = true
|
||||
AppInfoSort.InstallTime ->
|
||||
menu.menu.findItem(R.id.install_time).isChecked = true
|
||||
AppInfoSort.UpdateTime ->
|
||||
menu.menu.findItem(R.id.update_time).isChecked = true
|
||||
}
|
||||
|
||||
menu.menu.findItem(R.id.system_apps).isChecked = !uiStore.accessControlSystemApp
|
||||
menu.menu.findItem(R.id.reverse).isChecked = uiStore.accessControlReverse
|
||||
|
||||
menu.setOnMenuItemClickListener(this)
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
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<ProxyDesign.Request>,
|
||||
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 = 1
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
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:Int = 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
|
||||
}
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
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
|
||||
import com.github.kr328.clash.design.store.UiStore
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.github.kr328.clash.design.component
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.util.getPixels
|
||||
import com.github.kr328.clash.design.util.resolveThemedColor
|
||||
import com.github.kr328.clash.design.util.resolveThemedResourceId
|
||||
|
||||
class ProxyViewConfig(val context: Context, var proxyLine: Int) {
|
||||
private val colorSurface = context.resolveThemedColor(R.attr.colorSurface)
|
||||
|
||||
val clickableBackground =
|
||||
context.resolveThemedResourceId(android.R.attr.selectableItemBackground)
|
||||
|
||||
val selectedControl = context.resolveThemedColor(R.attr.colorOnPrimary)
|
||||
val selectedBackground = context.resolveThemedColor(R.attr.colorPrimary)
|
||||
|
||||
val unselectedControl = context.resolveThemedColor(R.attr.colorOnSurface)
|
||||
val unselectedBackground: Int
|
||||
get() = if (proxyLine==1) Color.TRANSPARENT else colorSurface
|
||||
|
||||
val layoutPadding = context.getPixels(R.dimen.proxy_layout_padding).toFloat()
|
||||
val contentPadding
|
||||
get() = if (proxyLine==2) context.getPixels(R.dimen.proxy_content_padding).toFloat() else context.getPixels(R.dimen.proxy_content_padding_grid3).toFloat()
|
||||
val textMargin
|
||||
get() = if (proxyLine==2) context.getPixels(R.dimen.proxy_text_margin).toFloat() else context.getPixels(R.dimen.proxy_text_margin_grid3).toFloat()
|
||||
val textSize
|
||||
get() = if (proxyLine==2) context.getPixels(R.dimen.proxy_text_size).toFloat() else context.getPixels(R.dimen.proxy_text_size_grid3).toFloat()
|
||||
|
||||
val shadow = Color.argb(
|
||||
0x15,
|
||||
Color.red(Color.DKGRAY),
|
||||
Color.green(Color.DKGRAY),
|
||||
Color.blue(Color.DKGRAY),
|
||||
)
|
||||
|
||||
val cardRadius = context.getPixels(R.dimen.proxy_card_radius).toFloat()
|
||||
var cardOffset = context.getPixels(R.dimen.proxy_card_offset).toFloat()
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package com.github.kr328.clash.design.component
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import com.github.kr328.clash.core.model.Proxy
|
||||
import com.github.kr328.clash.design.model.ProxyState
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.max
|
||||
|
||||
class ProxyViewState(
|
||||
val config: ProxyViewConfig,
|
||||
val proxy: Proxy,
|
||||
private val parent: ProxyState,
|
||||
private val link: ProxyState?
|
||||
) {
|
||||
val paint = Paint()
|
||||
val rect = Rect()
|
||||
val path = Path()
|
||||
|
||||
var title: String = ""
|
||||
var subtitle: String = ""
|
||||
var delayText: String = ""
|
||||
var background: Int = config.unselectedBackground
|
||||
var controls: Int = config.unselectedControl
|
||||
|
||||
private var delay: Int = 0
|
||||
var selected: Boolean = false
|
||||
private var parentNow: String = ""
|
||||
private var linkNow: String? = null
|
||||
|
||||
private var lastFrameTime = System.currentTimeMillis()
|
||||
|
||||
fun update(snap: Boolean): Boolean {
|
||||
val frameTime = System.currentTimeMillis()
|
||||
var invalidate = false
|
||||
|
||||
if (proxy.type.group) {
|
||||
title = proxy.name
|
||||
|
||||
if (link == null) {
|
||||
subtitle = proxy.type.name
|
||||
} else {
|
||||
if (linkNow !== link.now) {
|
||||
linkNow = link.now
|
||||
|
||||
subtitle = "%s(%s)".format(
|
||||
proxy.type.name,
|
||||
link.now.ifEmpty { "*" }
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
title = proxy.title
|
||||
subtitle = proxy.subtitle
|
||||
}
|
||||
|
||||
if (delay != proxy.delay) {
|
||||
delay = proxy.delay
|
||||
delayText = if (proxy.delay in 0..Short.MAX_VALUE) proxy.delay.toString() else ""
|
||||
}
|
||||
|
||||
if (parentNow !== parent.now) {
|
||||
parentNow = parent.now
|
||||
selected = proxy.name == parent.now
|
||||
}
|
||||
|
||||
controls = if (selected) config.selectedControl else config.unselectedControl
|
||||
|
||||
if (snap) {
|
||||
background = if (selected) config.selectedBackground else config.unselectedBackground
|
||||
} else {
|
||||
val target = if (selected) config.selectedBackground else config.unselectedBackground
|
||||
|
||||
if (background != target) {
|
||||
val sa = Color.alpha(background)
|
||||
val sr = Color.red(background)
|
||||
val sg = Color.green(background)
|
||||
val sb = Color.blue(background)
|
||||
|
||||
val ta = Color.alpha(target)
|
||||
val tr = Color.red(target)
|
||||
val tg = Color.green(target)
|
||||
val tb = Color.blue(target)
|
||||
|
||||
val da = ta - sa
|
||||
val dr = tr - sr
|
||||
val dg = tg - sg
|
||||
val db = tb - sb
|
||||
|
||||
val max = max(
|
||||
da.absoluteValue,
|
||||
max(
|
||||
dr.absoluteValue,
|
||||
max(
|
||||
dg.absoluteValue,
|
||||
db.absoluteValue
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val frameOffset = frameTime - lastFrameTime
|
||||
|
||||
val colorOffset = (frameOffset / max.toFloat().coerceAtLeast(0.001f))
|
||||
.coerceIn(0.0f, 1.0f)
|
||||
|
||||
background = if (colorOffset > 0.999f) {
|
||||
target
|
||||
} else {
|
||||
Color.argb(
|
||||
(sa + da * colorOffset).toInt(),
|
||||
(sr + dr * colorOffset).toInt(),
|
||||
(sg + dg * colorOffset).toInt(),
|
||||
(sb + db * colorOffset).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
invalidate = true
|
||||
}
|
||||
}
|
||||
|
||||
lastFrameTime = frameTime
|
||||
|
||||
return invalidate
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package com.github.kr328.clash.design.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.github.kr328.clash.common.compat.isAllowForceDarkCompat
|
||||
import com.github.kr328.clash.common.compat.isSystemBarsTranslucentCompat
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.ui.Insets
|
||||
import com.github.kr328.clash.design.ui.Surface
|
||||
import com.github.kr328.clash.design.util.getPixels
|
||||
import com.github.kr328.clash.design.util.resolveThemedResourceId
|
||||
import com.github.kr328.clash.design.util.setOnInsertsChangedListener
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
|
||||
class AppBottomSheetDialog(context: Context) : BottomSheetDialog(context) {
|
||||
private var insets: Insets = Insets.EMPTY
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setCancelable(true)
|
||||
|
||||
window!!.apply {
|
||||
isSystemBarsTranslucentCompat = true
|
||||
isAllowForceDarkCompat = false
|
||||
}
|
||||
|
||||
findViewById<ViewGroup>(com.google.android.material.R.id.container)?.apply {
|
||||
fitsSystemWindows = false
|
||||
}
|
||||
|
||||
findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)?.apply {
|
||||
setOnInsertsChangedListener {
|
||||
if (insets != it) {
|
||||
insets = it
|
||||
|
||||
(layoutParams as CoordinatorLayout.LayoutParams).also { params ->
|
||||
if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
||||
params.setMargins(it.start, 0, it.end, 0)
|
||||
} else {
|
||||
params.setMargins(it.end, 0, it.start, 0)
|
||||
}
|
||||
|
||||
val top = context.getPixels(R.dimen.bottom_sheet_background_padding_top)
|
||||
val height = context.getPixels(R.dimen.bottom_sheet_header_height)
|
||||
|
||||
setPaddingRelative(
|
||||
0,
|
||||
top * 2 + height,
|
||||
0,
|
||||
it.bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setOnShowListener {
|
||||
behavior.halfExpandedRatio = 0.99f
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FullScreenDialog(
|
||||
context: Context
|
||||
) : Dialog(context, context.resolveThemedResourceId(R.attr.fullScreenDialogTheme)) {
|
||||
val surface = Surface()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
window!!.apply {
|
||||
isSystemBarsTranslucentCompat = true
|
||||
|
||||
setLayout(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
decorView.setOnInsertsChangedListener {
|
||||
if (surface.insets != it)
|
||||
surface.insets = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package com.github.kr328.clash.design.dialog
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.databinding.DialogTextFieldBinding
|
||||
import com.github.kr328.clash.design.util.*
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
suspend fun Context.requestModelTextInput(
|
||||
initial: String,
|
||||
title: CharSequence,
|
||||
hint: CharSequence? = null,
|
||||
error: CharSequence? = null,
|
||||
validator: Validator = ValidatorAcceptAll,
|
||||
): String {
|
||||
return this.requestModelTextInput(initial, title, null, hint, error, validator)!!
|
||||
}
|
||||
|
||||
suspend fun Context.requestModelTextInput(
|
||||
initial: String?,
|
||||
title: CharSequence,
|
||||
reset: CharSequence?,
|
||||
hint: CharSequence? = null,
|
||||
error: CharSequence? = null,
|
||||
validator: Validator = ValidatorAcceptAll,
|
||||
): String? {
|
||||
return suspendCancellableCoroutine {
|
||||
val binding = DialogTextFieldBinding
|
||||
.inflate(layoutInflater, this.root, false)
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(title)
|
||||
.setView(binding.root)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val text = binding.textField.text?.toString() ?: ""
|
||||
|
||||
if (validator(text))
|
||||
it.resume(text)
|
||||
else
|
||||
it.resume(initial)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setOnDismissListener { _ ->
|
||||
if (!it.isCompleted)
|
||||
it.resume(initial)
|
||||
}
|
||||
|
||||
if (reset != null) {
|
||||
builder.setNeutralButton(reset) { _, _ ->
|
||||
it.resume(null)
|
||||
}
|
||||
}
|
||||
|
||||
val dialog = builder.create()
|
||||
|
||||
it.invokeOnCancellation {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.setOnShowListener {
|
||||
if (hint != null)
|
||||
binding.textLayout.hint = hint
|
||||
|
||||
binding.textField.apply {
|
||||
binding.textLayout.isErrorEnabled = error != null
|
||||
|
||||
doOnTextChanged { text, _, _, _ ->
|
||||
if (!validator(text?.toString() ?: "")) {
|
||||
if (error != null)
|
||||
binding.textLayout.error = error
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
||||
} else {
|
||||
if (error != null)
|
||||
binding.textLayout.error = null
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
setText(initial)
|
||||
|
||||
setSelection(0, initial?.length ?: 0)
|
||||
|
||||
requestTextInput()
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.github.kr328.clash.design.dialog
|
||||
|
||||
import android.content.Context
|
||||
import com.github.kr328.clash.design.databinding.DialogFetchStatusBinding
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
interface ModelProgressBarConfigure {
|
||||
var isIndeterminate: Boolean
|
||||
var text: String?
|
||||
var progress: Int
|
||||
var max: Int
|
||||
}
|
||||
|
||||
interface ModelProgressBarScope {
|
||||
suspend fun configure(block: suspend ModelProgressBarConfigure.() -> Unit)
|
||||
}
|
||||
|
||||
suspend fun Context.withModelProgressBar(block: suspend ModelProgressBarScope.() -> Unit) {
|
||||
val view = DialogFetchStatusBinding.inflate(this.layoutInflater)
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setCancelable(false)
|
||||
.setView(view.root)
|
||||
.show()
|
||||
|
||||
val configureImpl = object : ModelProgressBarConfigure {
|
||||
override var isIndeterminate: Boolean
|
||||
get() = view.progressIndicator.isIndeterminate
|
||||
set(value) {
|
||||
view.progressIndicator.isIndeterminate = value
|
||||
}
|
||||
override var text: String?
|
||||
get() = view.text.text?.toString()
|
||||
set(value) {
|
||||
view.text.text = value
|
||||
}
|
||||
override var progress: Int
|
||||
get() = view.progressIndicator.progress
|
||||
set(value) {
|
||||
view.progressIndicator.setProgressCompat(value, true)
|
||||
}
|
||||
override var max: Int
|
||||
get() = view.progressIndicator.max
|
||||
set(value) {
|
||||
view.progressIndicator.max = value
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val scopeImpl = object : ModelProgressBarScope {
|
||||
override suspend fun configure(block: suspend ModelProgressBarConfigure.() -> Unit) {
|
||||
withContext(Dispatchers.Main) {
|
||||
configureImpl.block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
scopeImpl.block()
|
||||
} finally {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.github.kr328.clash.design.model
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
data class AppInfo(
|
||||
val packageName: String,
|
||||
val label: String,
|
||||
val icon: Drawable,
|
||||
val installTime: Long,
|
||||
val updateDate: Long,
|
||||
)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.github.kr328.clash.design.model
|
||||
|
||||
enum class AppInfoSort(comparator: Comparator<AppInfo>) : Comparator<AppInfo> by comparator {
|
||||
Label(compareBy(AppInfo::label)),
|
||||
PackageName(compareBy(AppInfo::packageName)),
|
||||
InstallTime(compareBy(AppInfo::installTime)),
|
||||
UpdateTime(compareBy(AppInfo::updateDate)),
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.github.kr328.clash.design.model
|
||||
|
||||
interface Behavior {
|
||||
var autoRestart: Boolean
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.github.kr328.clash.design.model
|
||||
|
||||
enum class DarkMode {
|
||||
Auto, ForceLight, ForceDark
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.github.kr328.clash.design.model
|
||||
|
||||
data class File(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val size: Long,
|
||||
val lastModified: Long,
|
||||
val isDirectory: Boolean
|
||||
)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.github.kr328.clash.design.model
|
||||
|
||||
import java.util.*
|
||||
|
||||
data class LogFile(val fileName: String, val date: Date) {
|
||||
companion object {
|
||||
private val REGEX_FILE = Regex("clash-(\\d+).log")
|
||||
private const val FORMAT_FILE_NAME = "clash-%d.log"
|
||||
|
||||
fun parseFromFileName(fileName: String): LogFile? {
|
||||
return REGEX_FILE.matchEntire(fileName)?.run {
|
||||
LogFile(fileName, Date(groupValues[1].toLong()))
|
||||
}
|
||||
}
|
||||
|
||||
fun generate(): LogFile {
|
||||
val current = Date()
|
||||
val fileName = FORMAT_FILE_NAME.format(current.time)
|
||||
|
||||
return LogFile(fileName, current)
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.github.kr328.clash.design.model
|
||||
|
||||
class ProfilePageState {
|
||||
var allUpdating = false
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.github.kr328.clash.design.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.github.kr328.clash.common.compat.getDrawableCompat
|
||||
import com.github.kr328.clash.design.R
|
||||
|
||||
sealed class ProfileProvider {
|
||||
class File(private val context: Context) : ProfileProvider() {
|
||||
override val name: String
|
||||
get() = context.getString(R.string.file)
|
||||
override val summary: String
|
||||
get() = context.getString(R.string.import_from_file)
|
||||
override val icon: Drawable?
|
||||
get() = context.getDrawableCompat(R.drawable.ic_baseline_attach_file)
|
||||
}
|
||||
|
||||
class Url(private val context: Context) : ProfileProvider() {
|
||||
override val name: String
|
||||
get() = context.getString(R.string.url)
|
||||
override val summary: String
|
||||
get() = context.getString(R.string.import_from_url)
|
||||
override val icon: Drawable?
|
||||
get() = context.getDrawableCompat(R.drawable.ic_baseline_cloud_download)
|
||||
}
|
||||
|
||||
class External(
|
||||
override val name: String,
|
||||
override val summary: String,
|
||||
override val icon: Drawable?,
|
||||
val intent: Intent,
|
||||
) : ProfileProvider()
|
||||
|
||||
abstract val name: String
|
||||
abstract val summary: String
|
||||
abstract val icon: Drawable?
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.github.kr328.clash.design.model
|
||||
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
import com.github.kr328.clash.core.model.Provider
|
||||
import com.github.kr328.clash.design.BR
|
||||
|
||||
class ProviderState(
|
||||
val provider: Provider,
|
||||
updatedAt: Long,
|
||||
updating: Boolean,
|
||||
) : BaseObservable() {
|
||||
var updatedAt: Long = updatedAt
|
||||
@Bindable get
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
notifyPropertyChanged(BR.updatedAt)
|
||||
}
|
||||
|
||||
var updating: Boolean = updating
|
||||
@Bindable get
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
notifyPropertyChanged(BR.updating)
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.github.kr328.clash.design.model
|
||||
|
||||
class ProxyPageState {
|
||||
var bottom = false
|
||||
var urlTesting = false
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.github.kr328.clash.design.model
|
||||
|
||||
data class ProxyState(var now: String)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.github.kr328.clash.design.network
|
||||
|
||||
import com.github.kr328.clash.network.SubscribeData
|
||||
|
||||
object APIGlobalObject {
|
||||
var subData: SubscribeData? = null
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.github.kr328.clash.network
|
||||
|
||||
import android.content.Context
|
||||
import com.github.kr328.clash.design.PreferenceManager
|
||||
import com.google.gson.GsonBuilder
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
object ApiClient {
|
||||
private val okHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
})
|
||||
.build()
|
||||
|
||||
// Lazy initialization of configURL
|
||||
private val configURL: String
|
||||
get() {
|
||||
val url = PreferenceManager.baseURL
|
||||
return if (url.isNullOrEmpty()) {
|
||||
"https://vungles.com/" // Default fallback URL to prevent crash
|
||||
} else {
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
"https://$url"
|
||||
} else {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Retrofit initialization
|
||||
val retrofit: Retrofit by lazy {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(configURL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.client(okHttpClient)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
object ApiClientConfig {
|
||||
private const val ConfigURL = "https://vungles.com/api/test/" // BASE 请求配置文件地址 见说明文档
|
||||
// private const val ConfigURL = "https://uuvpn.oss-cn-hongkong.aliyuncs.com/api/test/" // BASE 请求配置文件地址 见说明文档
|
||||
//这个链接主要是为了配置转化,一般有的订阅地址被墙之后国内无法访问,这里就是了防止被墙,使用我们的香港服务器进行顶级域名防护
|
||||
//const val ConfigNodeURL = "https://api.xxxx.com/api/parseyamlclash.php?target=clashmeta&url=" //通过香港服务器转接一次Clash转化的订阅地址
|
||||
const val ConfigNodeURL = ""
|
||||
|
||||
private val okHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
})
|
||||
.build()
|
||||
|
||||
val retrofit: Retrofit = Retrofit.Builder()
|
||||
.baseUrl(ConfigURL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.client(okHttpClient)
|
||||
.build()
|
||||
}
|
||||
+476
@@ -0,0 +1,476 @@
|
||||
package com.github.kr328.clash.network
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.coroutines.time.withTimeout
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
import java.io.IOException
|
||||
import java.io.Serializable
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
/// 所有网络接口
|
||||
interface ApiService {
|
||||
|
||||
@GET("config")
|
||||
suspend fun getConfig(): Response<ConfigResponse>
|
||||
|
||||
@POST("passport/auth/login")
|
||||
suspend fun loginUser(@Body request: LoginRequest): Response<LoginResponse>
|
||||
|
||||
@POST("passport/auth/register")
|
||||
suspend fun registerUser(@Body request: LoginRequest): Response<LoginResponse>
|
||||
|
||||
@GET("user/info")
|
||||
suspend fun getUserData(@Header("Authorization") auth_data: String): Response<UserInfoResponse>
|
||||
|
||||
@GET("user/getSubscribe")
|
||||
suspend fun getSubscribe(@Header("Authorization") auth_data: String): Response<SubscribeResponse>
|
||||
|
||||
@GET("user/plan/fetch")
|
||||
suspend fun getplans(@Header("Authorization") auth_data: String): Response<PlansResponse>
|
||||
|
||||
@GET("user/order/fetch")
|
||||
suspend fun getOrders(@Header("Authorization") auth_data: String): Response<OrdersResponse>
|
||||
|
||||
@POST("user/order/save") //period=half_year_price&plan_id=2&coupon_code=
|
||||
suspend fun saveOrder(@Header("Authorization") auth_data: String,@Body request: SaveOrderRequest): Response<SubmitOrderResponse>
|
||||
|
||||
@GET("user/order/detail") //period=half_year_price&plan_id=2&coupon_code=
|
||||
suspend fun getOrderdetail(@Header("Authorization") auth_data: String, @Query("trade_no") trade_no: String): Response<QueryOrderResponse>
|
||||
|
||||
@GET("user/order/getPaymentMethod")
|
||||
suspend fun getPaymentList(@Header("Authorization") auth_data: String): Response<PaymentResponse>
|
||||
|
||||
@POST("user/order/checkout")
|
||||
suspend fun checkoutOrder(@Header("Authorization") auth_data: String,@Body request: CheckoutrderRequest): Response<PublicResponse>
|
||||
|
||||
@GET("user/invite/fetch")
|
||||
suspend fun getinviteList(@Header("Authorization") auth_data: String) :Response<InviteResponse>
|
||||
|
||||
@GET("user/invite/save")
|
||||
suspend fun saveinvite(@Header("Authorization") auth_data: String) :Response<SaveInviteCodeResponse>
|
||||
|
||||
@GET("user/ticket/fetch")
|
||||
suspend fun getTickets(@Header("Authorization") auth_data: String): Response<TicketsResponse>
|
||||
|
||||
@GET("user/ticket/fetch")
|
||||
suspend fun getTicketsByTID(@Header("Authorization") auth_data: String, @Query("id") ticketID: Int): Response<TicketDetailResponse>
|
||||
|
||||
@POST("user/ticket/close")
|
||||
suspend fun closeTicket(@Header("Authorization") auth_data: String,@Body request: Map<String, Int>): Response<PublicResponse>
|
||||
|
||||
@POST("user/ticket/save")
|
||||
suspend fun saveTicket(@Header("Authorization") auth_data: String,@Body request: MutableMap<String, String>): Response<PublicResponse>
|
||||
|
||||
@POST("user/ticket/reply")
|
||||
suspend fun replyTicket(@Header("Authorization") auth_data: String,@Body request: MutableMap<String, String>): Response<PublicResponse>
|
||||
|
||||
}
|
||||
|
||||
|
||||
data class SaveInviteCodeResponse(
|
||||
val `data`: Any?,
|
||||
|
||||
val message: String?,
|
||||
val status: String?
|
||||
)
|
||||
|
||||
data class TicketDetailResponse(
|
||||
val `data`: TicketDetailData?,
|
||||
val message: String?,
|
||||
val status: String?
|
||||
)
|
||||
|
||||
data class TicketDetailData(
|
||||
val created_at: Int?,
|
||||
val id: Int?,
|
||||
val level: Int?,
|
||||
val message: List<TicketMessage>?,
|
||||
val reply_status: Int?,
|
||||
val status: Int?,
|
||||
val subject: String?,
|
||||
val updated_at: Int?,
|
||||
val user_id: Int?
|
||||
)
|
||||
|
||||
data class TicketMessage(
|
||||
val created_at: Long?,
|
||||
val id: Int?,
|
||||
val is_me: Boolean?,
|
||||
val message: String?,
|
||||
val ticket_id: Int?,
|
||||
val updated_at: Long?
|
||||
)
|
||||
|
||||
data class TicketsResponse(
|
||||
val `data`: List<TicketsData>?,
|
||||
val message: String?,
|
||||
val status: String?
|
||||
)
|
||||
|
||||
data class TicketsData(
|
||||
val created_at: Long?,
|
||||
val id: Int?,
|
||||
val level: Int?,
|
||||
val message: Any?,
|
||||
val reply_status: Int?,
|
||||
val status: Int?,
|
||||
val subject: String?,
|
||||
val updated_at: Long?,
|
||||
val user_id: Int?
|
||||
)
|
||||
|
||||
|
||||
suspend fun <T> safeApiCall(
|
||||
apiCall: suspend () -> Response<T>
|
||||
): T? {
|
||||
return try {
|
||||
val response = withTimeout(13000) {
|
||||
apiCall()
|
||||
}
|
||||
|
||||
if (response.isSuccessful) {
|
||||
response.body()
|
||||
} else {
|
||||
println("请求失败,错误码: ${response.code()}")
|
||||
null
|
||||
}
|
||||
} catch (e: TimeoutException) {
|
||||
println("请求超时,请检查网络连接: ${e.message}")
|
||||
null
|
||||
} catch (e: IOException) {
|
||||
println("网络错误,请检查网络连接: ${e.message}")
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
println("未知错误: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
suspend fun <T> safeApiRequestCall(
|
||||
apiCall: suspend () -> Response<T>
|
||||
): Response<T>? {
|
||||
return try {
|
||||
val response = withTimeout(13000) {
|
||||
apiCall()
|
||||
}
|
||||
if (response.isSuccessful) {
|
||||
response
|
||||
} else {
|
||||
println("请求失败,错误码: ${response.code()}")
|
||||
response
|
||||
}
|
||||
} catch (e: TimeoutException) {
|
||||
println("请求超时,请检查网络连接: ${e.message}")
|
||||
null
|
||||
} catch (e: IOException) {
|
||||
println("网络错误,请检查网络连接: ${e.message}")
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
println("未知错误: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
data class InviteResponse(
|
||||
val `data`: InviteData?,
|
||||
val error: Any?,
|
||||
val message: String?,
|
||||
val status: String?
|
||||
)
|
||||
|
||||
data class InviteData(
|
||||
val codes: List<InviteCode>?,
|
||||
val stat: List<Int>?
|
||||
)
|
||||
|
||||
data class InviteCode(
|
||||
val code: String?,
|
||||
val created_at: Int?,
|
||||
val pv: Int?,
|
||||
val status: Int?,
|
||||
val updated_at: Int?,
|
||||
val user_id: Int?
|
||||
)
|
||||
data class CheckoutrderRequest(
|
||||
val trade_no: String,
|
||||
val method: Int
|
||||
)
|
||||
|
||||
data class PaymentResponse(
|
||||
val `data`: List<PaymentData>?,
|
||||
val message: String?,
|
||||
val status: String?
|
||||
)
|
||||
|
||||
data class PaymentData(
|
||||
val handling_fee_fixed: Any?,
|
||||
val handling_fee_percent: String?,
|
||||
val icon: Any?,
|
||||
val id: Int?,
|
||||
val name: String?,
|
||||
val payment: String?
|
||||
)
|
||||
//
|
||||
data class QueryOrderRequest(
|
||||
val trade_no: String
|
||||
)
|
||||
|
||||
|
||||
data class QueryOrderResponse(
|
||||
val `data`: QueryOrderData?,
|
||||
val message: String?,
|
||||
val status: String?
|
||||
)
|
||||
|
||||
data class QueryOrderData(
|
||||
val actual_commission_balance: Any?,
|
||||
val balance_amount: Any?,
|
||||
val callback_no: Any?,
|
||||
val commission_balance: Int?,
|
||||
val commission_status: Int?,
|
||||
val coupon_code: Any?,
|
||||
val coupon_id: Any?,
|
||||
val created_at: Int?,
|
||||
val discount_amount: Any?,
|
||||
val handling_amount: Int?,
|
||||
val id: Int?,
|
||||
val invite_user_id: Any?,
|
||||
val paid_at: Any?,
|
||||
val payment_id: Int?,
|
||||
val period: String?,
|
||||
val plan: PlanData?,
|
||||
val plan_id: Int?,
|
||||
val refund_amount: Any?,
|
||||
val site_id: Any?,
|
||||
val status: Int?,
|
||||
val surplus_amount: Any?,
|
||||
val surplus_order_ids: Any?,
|
||||
val tixianstatus: Any?,
|
||||
val total_amount: Int?,
|
||||
val trade_no: String?,
|
||||
val try_out_plan_id: Int?,
|
||||
val type: Int?,
|
||||
val updated_at: Int?,
|
||||
val user_id: Int?
|
||||
){
|
||||
// status 对应的中文描述
|
||||
val statusZh: String
|
||||
get() = when (status) {
|
||||
0 -> "待支付"
|
||||
1 -> "已支付"
|
||||
2 -> "已取消"
|
||||
3 -> "已支付"
|
||||
else -> "未知"
|
||||
}
|
||||
// period 对应的中文描述
|
||||
val periodZh: String
|
||||
get() = when (period) {
|
||||
"month_price" -> "月付"
|
||||
"quarter_price" -> "季付"
|
||||
"half_year_price" -> "半年付"
|
||||
"year_price" -> "年付"
|
||||
"two_year_price" -> "两年付"
|
||||
"three_year_price" -> "三年付"
|
||||
"onetime_price" -> "一次性付"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
data class SaveOrderRequest(
|
||||
// period=half_year_price&plan_id=2&coupon_code=
|
||||
|
||||
val period: String,
|
||||
val plan_id: Int,
|
||||
val coupon_code: String?
|
||||
|
||||
)
|
||||
|
||||
|
||||
data class PublicResponse(
|
||||
val data: String?,
|
||||
val message: String?,
|
||||
val status: String?
|
||||
)
|
||||
|
||||
data class SubmitOrderResponse(
|
||||
val data: String?,
|
||||
val message: String?,
|
||||
val status: String?
|
||||
)
|
||||
|
||||
|
||||
data class ConfigResponse (
|
||||
val baseURL: String,
|
||||
val baseDYURL: String,
|
||||
val mainregisterURL: String,
|
||||
val paymentURL: String,
|
||||
val telegramurl: String,
|
||||
val kefuurl: String,
|
||||
val websiteURL: String,
|
||||
val crisptoken: String,
|
||||
val banners: List<String>,
|
||||
val message: String,
|
||||
val code: Int
|
||||
)
|
||||
|
||||
data class RegisterRequest(val username: String, val password: String)
|
||||
data class RegisterResponse(val success: Boolean, val message: String)
|
||||
|
||||
data class LoginRequest(val email: String, val password: String ,val captchaData: String = "")
|
||||
|
||||
data class LoginResponse(val message: String?, val data: LoginData?)
|
||||
|
||||
|
||||
data class LoginData (
|
||||
val token: String?,
|
||||
val isAdmin: Int?,
|
||||
val auth_data: String?
|
||||
)
|
||||
|
||||
|
||||
|
||||
data class SubscribeResponse (
|
||||
val data: SubscribeData
|
||||
)
|
||||
|
||||
|
||||
data class SubscribeData (
|
||||
|
||||
@SerializedName("plan_id") val plan_id : Int?,
|
||||
@SerializedName("token") val token : String,
|
||||
@SerializedName("expired_at") val expired_at : Long?,
|
||||
@SerializedName("u") val u : Long,
|
||||
@SerializedName("d") val d : Long,
|
||||
@SerializedName("transfer_enable") val transfer_enable : Long?,
|
||||
@SerializedName("email") val email : String,
|
||||
@SerializedName("uuid") val uuid : String,
|
||||
@SerializedName("plan") val plan : PlanData?,
|
||||
@SerializedName("subscribe_url") val subscribe_url : String,
|
||||
@SerializedName("reset_day") val reset_day : String?
|
||||
|
||||
)
|
||||
|
||||
data class PlanData (
|
||||
@SerializedName("id") val id : Int,
|
||||
@SerializedName("group_id") val group_id : Int?,
|
||||
@SerializedName("transfer_enable") val transfer_enable : Long?,
|
||||
@SerializedName("name") val name : String,
|
||||
@SerializedName("speed_limit") val speed_limit : Long?,
|
||||
@SerializedName("show") val show : Int?,
|
||||
@SerializedName("sort") val sort : Int?,
|
||||
@SerializedName("renew") val renew : Int?,
|
||||
@SerializedName("content") val content : String,
|
||||
@SerializedName("month_price") val month_price : Long?,
|
||||
@SerializedName("quarter_price") val quarter_price : Long?,
|
||||
@SerializedName("half_year_price") val half_year_price : Long?,
|
||||
@SerializedName("year_price") val year_price : Long?,
|
||||
@SerializedName("two_year_price") val two_year_price : Long?,
|
||||
@SerializedName("three_year_price") val three_year_price : Long?,
|
||||
@SerializedName("onetime_price") val onetime_price : Int?,
|
||||
@SerializedName("reset_price") val reset_price : Long?,
|
||||
@SerializedName("reset_traffic_method") val reset_traffic_method : String?,
|
||||
@SerializedName("capacity_limit") val capacity_limit : Long?,
|
||||
@SerializedName("created_at") val created_at : Long?,
|
||||
@SerializedName("updated_at") val updated_at : Long?
|
||||
) : Serializable // 添加 Serializable 接口
|
||||
|
||||
|
||||
|
||||
data class UserInfoResponse (
|
||||
val data: UserInfoData
|
||||
)
|
||||
|
||||
data class UserInfoData (
|
||||
val email: String,
|
||||
val transferEnable: Long?,
|
||||
val lastLoginAt: Long,
|
||||
val createdAt: Long?,
|
||||
val banned: Long?,
|
||||
val remindExpire: Long?,
|
||||
val remindTraffic: Long?,
|
||||
val expiredAt: Long?,
|
||||
val balance: Long?,
|
||||
val commissionBalance: Long?,
|
||||
val uuid: String,
|
||||
val avatarURL: String?
|
||||
)
|
||||
|
||||
data class PlansResponse(
|
||||
val data: List<PlanData>?,
|
||||
|
||||
val message: String?,
|
||||
val status: String?
|
||||
)
|
||||
|
||||
|
||||
data class OrdersResponse(
|
||||
val `data`: List<OrderData>?,
|
||||
|
||||
val message: String?,
|
||||
val status: String?
|
||||
)
|
||||
|
||||
data class OrderData(
|
||||
val actual_commission_balance: Any?,
|
||||
val balance_amount: Any?,
|
||||
val callback_no: Any?,
|
||||
val commission_balance: Int?,
|
||||
val commission_status: Int?,
|
||||
val coupon_code: Any?,
|
||||
val coupon_id: Any?,
|
||||
val created_at: Long?,
|
||||
val discount_amount: Any?,
|
||||
val handling_amount: Int?,
|
||||
val invite_user_id: Any?,
|
||||
val paid_at: Any?,
|
||||
val payment_id: Int?,
|
||||
val period: String?,
|
||||
val plan: PlanData?,
|
||||
val plan_id: Int?,
|
||||
val refund_amount: Any?,
|
||||
val site_id: Any?,
|
||||
val status: Int?,
|
||||
val surplus_amount: Any?,
|
||||
val surplus_order_ids: Any?,
|
||||
val tixianstatus: Any?,
|
||||
val total_amount: Int?,
|
||||
val trade_no: String?,
|
||||
val type: Int?,
|
||||
val updated_at: Long?
|
||||
){
|
||||
// status 对应的中文描述
|
||||
val statusZh: String
|
||||
get() = when (status) {
|
||||
0 -> "待支付"
|
||||
1 -> "已支付"
|
||||
2 -> "已取消"
|
||||
3 -> "已支付"
|
||||
else -> "未知"
|
||||
}
|
||||
|
||||
// period 对应的中文描述
|
||||
val periodZh: String
|
||||
get() = when (period) {
|
||||
"month_price" -> "月付"
|
||||
"quarter_price" -> "季付"
|
||||
"half_year_price" -> "半年付"
|
||||
"year_price" -> "年付"
|
||||
"two_year_price" -> "两年付"
|
||||
"three_year_price" -> "三年付"
|
||||
"onetime_price" -> "一次性付"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.github.kr328.clash.design.preference
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import com.github.kr328.clash.design.databinding.PreferenceCategoryBinding
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
|
||||
fun PreferenceScreen.category(
|
||||
@StringRes text: Int,
|
||||
) {
|
||||
val binding = PreferenceCategoryBinding
|
||||
.inflate(context.layoutInflater, root, false)
|
||||
|
||||
binding.textView.text = context.getString(text)
|
||||
|
||||
addElement(object : Preference {
|
||||
override val view: View
|
||||
get() = binding.root
|
||||
override var enabled: Boolean
|
||||
get() = binding.root.isEnabled
|
||||
set(value) {
|
||||
binding.root.isEnabled = value
|
||||
}
|
||||
})
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.github.kr328.clash.design.preference
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.github.kr328.clash.common.compat.getDrawableCompat
|
||||
import com.github.kr328.clash.design.databinding.PreferenceClickableBinding
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
|
||||
interface ClickablePreference : Preference {
|
||||
var title: CharSequence
|
||||
|
||||
var icon: Drawable?
|
||||
var summary: CharSequence?
|
||||
|
||||
fun clicked(clicked: () -> Unit)
|
||||
}
|
||||
|
||||
fun PreferenceScreen.clickable(
|
||||
@StringRes title: Int,
|
||||
@DrawableRes icon: Int? = null,
|
||||
@StringRes summary: Int? = null,
|
||||
configure: ClickablePreference.() -> Unit = {}
|
||||
): ClickablePreference {
|
||||
val binding = PreferenceClickableBinding
|
||||
.inflate(context.layoutInflater, root, false)
|
||||
|
||||
val impl = object : ClickablePreference {
|
||||
override var icon: Drawable?
|
||||
get() = binding.iconView.background
|
||||
set(value) {
|
||||
binding.iconView.background = value
|
||||
}
|
||||
override var title: CharSequence
|
||||
get() = binding.titleView.text
|
||||
set(value) {
|
||||
binding.titleView.text = value
|
||||
}
|
||||
override var summary: CharSequence?
|
||||
get() = binding.summaryView.text
|
||||
set(value) {
|
||||
binding.summaryView.text = value
|
||||
binding.summaryView.visibility = if (value == null) View.GONE else View.VISIBLE
|
||||
}
|
||||
override val view: View
|
||||
get() = binding.root
|
||||
|
||||
override fun clicked(clicked: () -> Unit) {
|
||||
binding.root.setOnClickListener {
|
||||
clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl.title = context.getText(title)
|
||||
|
||||
if (icon != null) {
|
||||
impl.icon = context.getDrawableCompat(icon)
|
||||
}
|
||||
|
||||
if (summary != null) {
|
||||
impl.summary = context.getText(summary)
|
||||
} else {
|
||||
impl.summary = null
|
||||
}
|
||||
|
||||
impl.configure()
|
||||
|
||||
addElement(impl)
|
||||
|
||||
return impl
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package com.github.kr328.clash.design.preference
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.dialog.requestModelTextInput
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
interface EditableTextPreference : ClickablePreference {
|
||||
var placeholder: CharSequence?
|
||||
var empty: CharSequence?
|
||||
var text: String?
|
||||
}
|
||||
|
||||
fun <T> PreferenceScreen.editableText(
|
||||
value: KMutableProperty0<T>,
|
||||
adapter: NullableTextAdapter<T>,
|
||||
@StringRes title: Int,
|
||||
@DrawableRes icon: Int? = null,
|
||||
@StringRes placeholder: Int? = null,
|
||||
@StringRes empty: Int? = null,
|
||||
configure: EditableTextPreference.() -> Unit = {},
|
||||
): EditableTextPreference {
|
||||
val impl = object : EditableTextPreference, ClickablePreference by clickable(title, icon) {
|
||||
override var placeholder: CharSequence? = null
|
||||
override var empty: CharSequence? = null
|
||||
override var text: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
when {
|
||||
value == null -> {
|
||||
this.summary = this.placeholder
|
||||
}
|
||||
value.isEmpty() -> {
|
||||
this.summary = this.empty
|
||||
}
|
||||
else -> {
|
||||
this.summary = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (placeholder != null) {
|
||||
impl.placeholder = context.getText(placeholder)
|
||||
}
|
||||
|
||||
if (empty != null) {
|
||||
impl.empty = context.getText(empty)
|
||||
}
|
||||
|
||||
impl.configure()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
impl.text = withContext(Dispatchers.IO) {
|
||||
adapter.from(value.get())
|
||||
}
|
||||
|
||||
impl.clicked {
|
||||
this@editableText.launch(Dispatchers.Main) {
|
||||
val text = context.requestModelTextInput(
|
||||
initial = impl.text,
|
||||
title = impl.title,
|
||||
reset = context.getText(R.string.reset),
|
||||
hint = impl.title,
|
||||
)
|
||||
|
||||
val newValue = withContext(Dispatchers.IO) {
|
||||
adapter.to(text).apply(value::set)
|
||||
}
|
||||
|
||||
impl.text = adapter.from(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return impl
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package com.github.kr328.clash.design.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.adapter.EditableTextListAdapter
|
||||
import com.github.kr328.clash.design.dialog.requestModelTextInput
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
interface EditableTextListPreference<T> : ClickablePreference {
|
||||
var placeholder: CharSequence?
|
||||
|
||||
var list: List<T>?
|
||||
}
|
||||
|
||||
fun <T> PreferenceScreen.editableTextList(
|
||||
value: KMutableProperty0<List<T>?>,
|
||||
adapter: TextAdapter<T>,
|
||||
@StringRes title: Int,
|
||||
@DrawableRes icon: Int? = null,
|
||||
@StringRes placeholder: Int? = null,
|
||||
configure: EditableTextListPreference<T>.() -> Unit = {},
|
||||
): EditableTextListPreference<T> {
|
||||
val impl =
|
||||
object : EditableTextListPreference<T>, ClickablePreference by clickable(title, icon) {
|
||||
override var list: List<T>? = null
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
when {
|
||||
value == null -> {
|
||||
this.summary = this.placeholder
|
||||
}
|
||||
value.isEmpty() -> {
|
||||
this.summary = context.getString(R.string.empty)
|
||||
}
|
||||
else -> {
|
||||
this.summary = context.getString(R.string.format_elements, value.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
override var placeholder: CharSequence? = null
|
||||
}
|
||||
|
||||
if (placeholder != null) {
|
||||
impl.placeholder = context.getText(placeholder)
|
||||
}
|
||||
|
||||
impl.configure()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
val v = withContext(Dispatchers.IO) {
|
||||
value.get()
|
||||
}
|
||||
|
||||
impl.list = v
|
||||
|
||||
impl.clicked {
|
||||
this@editableTextList.launch(Dispatchers.Main) {
|
||||
val newList = requestEditTextList(
|
||||
impl.list,
|
||||
context,
|
||||
adapter,
|
||||
impl.title
|
||||
)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
value.set(newList)
|
||||
}
|
||||
|
||||
impl.list = newList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return impl
|
||||
}
|
||||
|
||||
private suspend fun <T> requestEditTextList(
|
||||
initialValue: List<T>?,
|
||||
context: Context,
|
||||
adapter: TextAdapter<T>,
|
||||
title: CharSequence,
|
||||
): List<T>? {
|
||||
val recyclerAdapter = EditableTextListAdapter(
|
||||
context,
|
||||
initialValue?.toMutableList() ?: mutableListOf(),
|
||||
adapter
|
||||
)
|
||||
|
||||
val result = requestEditableListOverlay(context, recyclerAdapter, title) {
|
||||
val text = context.requestModelTextInput(
|
||||
initial = "",
|
||||
title = title,
|
||||
hint = title
|
||||
)
|
||||
|
||||
if (text.isNotBlank()) {
|
||||
recyclerAdapter.addElement(text)
|
||||
}
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
EditableListOverlayResult.Cancel -> initialValue
|
||||
EditableListOverlayResult.Apply -> recyclerAdapter.values
|
||||
EditableListOverlayResult.Reset -> null
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package com.github.kr328.clash.design.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.adapter.EditableTextMapAdapter
|
||||
import com.github.kr328.clash.design.databinding.DialogEditableMapTextFieldBinding
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.root
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
interface EditableTextMapPreference<K, V> : ClickablePreference {
|
||||
var placeholder: CharSequence?
|
||||
var map: Map<K, V>?
|
||||
}
|
||||
|
||||
fun <K, V> PreferenceScreen.editableTextMap(
|
||||
value: KMutableProperty0<Map<K, V>?>,
|
||||
keyAdapter: TextAdapter<K>,
|
||||
valueAdapter: TextAdapter<V>,
|
||||
@StringRes title: Int,
|
||||
@DrawableRes icon: Int? = null,
|
||||
@StringRes placeholder: Int? = null,
|
||||
configure: EditableTextMapPreference<K, V>.() -> Unit = {}
|
||||
): EditableTextMapPreference<K, V> {
|
||||
val impl =
|
||||
object : EditableTextMapPreference<K, V>, ClickablePreference by clickable(title, icon) {
|
||||
override var placeholder: CharSequence? = null
|
||||
override var map: Map<K, V>? = null
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
when {
|
||||
value == null -> {
|
||||
this.summary = this.placeholder
|
||||
}
|
||||
value.isEmpty() -> {
|
||||
this.summary = context.getString(R.string.empty)
|
||||
}
|
||||
else -> {
|
||||
this.summary = context.getString(R.string.format_elements, value.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (placeholder != null) {
|
||||
impl.placeholder = context.getText(placeholder)
|
||||
}
|
||||
|
||||
impl.configure()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
val v = withContext(Dispatchers.IO) {
|
||||
value.get()
|
||||
}
|
||||
|
||||
impl.map = v
|
||||
|
||||
impl.clicked {
|
||||
this@editableTextMap.launch(Dispatchers.Main) {
|
||||
val newMap = requestEditTextMap(
|
||||
impl.map,
|
||||
context,
|
||||
keyAdapter,
|
||||
valueAdapter,
|
||||
impl.title
|
||||
)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
value.set(newMap)
|
||||
}
|
||||
|
||||
impl.map = newMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return impl
|
||||
}
|
||||
|
||||
private suspend fun <K, V> requestEditTextMap(
|
||||
initialValue: Map<K, V>?,
|
||||
context: Context,
|
||||
keyAdapter: TextAdapter<K>,
|
||||
valueAdapter: TextAdapter<V>,
|
||||
title: CharSequence
|
||||
): Map<K, V>? {
|
||||
val editableValue = withContext(Dispatchers.Default) {
|
||||
initialValue?.map { it.key to it.value }?.toMutableList() ?: mutableListOf()
|
||||
}
|
||||
|
||||
val recyclerAdapter = EditableTextMapAdapter(
|
||||
context,
|
||||
editableValue,
|
||||
keyAdapter,
|
||||
valueAdapter,
|
||||
)
|
||||
|
||||
val result = requestEditableListOverlay(context, recyclerAdapter, title) {
|
||||
val newItem = requestModelInputEntry(context, title)
|
||||
|
||||
if (newItem != null) {
|
||||
recyclerAdapter.addElement(newItem.first, newItem.second)
|
||||
}
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
EditableListOverlayResult.Cancel -> initialValue
|
||||
EditableListOverlayResult.Apply -> recyclerAdapter.values.toMap()
|
||||
EditableListOverlayResult.Reset -> null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestModelInputEntry(
|
||||
context: Context,
|
||||
title: CharSequence
|
||||
): Pair<String, String>? {
|
||||
return suspendCancellableCoroutine { ctx ->
|
||||
val binding = DialogEditableMapTextFieldBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val k = binding.keyView.text?.toString()?.trim() ?: ""
|
||||
val v = binding.valueView.text?.toString()?.trim() ?: ""
|
||||
|
||||
if (k.isNotEmpty() && v.isNotEmpty()) {
|
||||
ctx.resume(k to v)
|
||||
}
|
||||
}
|
||||
.setView(binding.root)
|
||||
.create()
|
||||
|
||||
dialog.setOnCancelListener {
|
||||
if (!ctx.isCompleted) {
|
||||
ctx.resume(null)
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.github.kr328.clash.design.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.databinding.DialogPreferenceListBinding
|
||||
import com.github.kr328.clash.design.dialog.FullScreenDialog
|
||||
import com.github.kr328.clash.design.util.applyLinearAdapter
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.root
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
internal enum class EditableListOverlayResult {
|
||||
Cancel, Apply, Reset
|
||||
}
|
||||
|
||||
internal suspend fun requestEditableListOverlay(
|
||||
context: Context,
|
||||
adapter: RecyclerView.Adapter<*>,
|
||||
title: CharSequence,
|
||||
addNewItem: suspend () -> Unit
|
||||
): EditableListOverlayResult {
|
||||
return coroutineScope {
|
||||
suspendCancellableCoroutine { ctx ->
|
||||
val dialog = FullScreenDialog(context)
|
||||
val binding = DialogPreferenceListBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
|
||||
binding.surface = dialog.surface
|
||||
binding.mainList.applyLinearAdapter(context, adapter)
|
||||
binding.titleView.text = title
|
||||
|
||||
binding.newView.setOnClickListener {
|
||||
launch {
|
||||
addNewItem()
|
||||
}
|
||||
}
|
||||
|
||||
binding.resetView.setOnClickListener {
|
||||
ctx.resume(EditableListOverlayResult.Reset)
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
binding.cancelView.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
binding.okView.setOnClickListener {
|
||||
ctx.resume(EditableListOverlayResult.Apply)
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.setContentView(binding.root)
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
if (!ctx.isCompleted) {
|
||||
ctx.resume(EditableListOverlayResult.Cancel)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.invokeOnCancellation {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.github.kr328.clash.design.preference
|
||||
|
||||
import android.view.View
|
||||
|
||||
fun interface OnChangedListener {
|
||||
fun onChanged()
|
||||
}
|
||||
|
||||
interface Preference {
|
||||
val view: View
|
||||
|
||||
var enabled: Boolean
|
||||
get() = view.isEnabled
|
||||
set(value) {
|
||||
view.isEnabled = value
|
||||
view.isClickable = value
|
||||
view.isFocusable = value
|
||||
view.alpha = if (value) 1.0f else 0.33f
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.github.kr328.clash.design.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.LinearLayout.LayoutParams
|
||||
import android.widget.LinearLayout.LayoutParams.MATCH_PARENT
|
||||
import android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface PreferenceScreen : CoroutineScope {
|
||||
val context: Context
|
||||
val root: ViewGroup
|
||||
}
|
||||
|
||||
fun CoroutineScope.preferenceScreen(
|
||||
context: Context,
|
||||
configure: PreferenceScreen.() -> Unit
|
||||
): PreferenceScreen {
|
||||
val root = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
}
|
||||
|
||||
val impl = object : PreferenceScreen, CoroutineScope by this {
|
||||
override val context: Context
|
||||
get() = context
|
||||
override val root: ViewGroup
|
||||
get() = root
|
||||
}
|
||||
|
||||
impl.configure()
|
||||
|
||||
return impl
|
||||
}
|
||||
|
||||
fun PreferenceScreen.addElement(preference: Preference) {
|
||||
root.addView(preference.view, LayoutParams(MATCH_PARENT, WRAP_CONTENT))
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package com.github.kr328.clash.design.preference
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.ListPopupWindow
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.adapter.PopupListAdapter
|
||||
import com.github.kr328.clash.design.util.getPixels
|
||||
import com.github.kr328.clash.design.util.measureWidth
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
interface SelectableListPreference<T> : ClickablePreference {
|
||||
var selected: Int
|
||||
|
||||
var listener: OnChangedListener?
|
||||
}
|
||||
|
||||
fun <T> PreferenceScreen.selectableList(
|
||||
value: KMutableProperty0<T>,
|
||||
values: Array<T>,
|
||||
valuesText: Array<Int>,
|
||||
@StringRes title: Int,
|
||||
@DrawableRes icon: Int? = null,
|
||||
configure: SelectableListPreference<T>.() -> Unit = {},
|
||||
): SelectableListPreference<T> {
|
||||
val impl = object : SelectableListPreference<T>, ClickablePreference by clickable(title, icon) {
|
||||
override var selected: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
this.summary = context.getText(valuesText[value])
|
||||
}
|
||||
override var listener: OnChangedListener? = null
|
||||
}
|
||||
|
||||
impl.configure()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
val initial = withContext(Dispatchers.IO) {
|
||||
value.get()
|
||||
}
|
||||
|
||||
impl.selected = values.indexOf(initial)
|
||||
|
||||
impl.clicked {
|
||||
popupSelectMenu(impl, value, valuesText.map { context.getText(it) }, values)
|
||||
}
|
||||
}
|
||||
|
||||
return impl
|
||||
}
|
||||
|
||||
private fun <T> PreferenceScreen.popupSelectMenu(
|
||||
impl: SelectableListPreference<T>,
|
||||
value: KMutableProperty0<T>,
|
||||
valuesText: List<CharSequence>,
|
||||
values: Array<T>,
|
||||
) {
|
||||
ListPopupWindow(context).apply {
|
||||
val adapter = PopupListAdapter(
|
||||
context,
|
||||
valuesText,
|
||||
impl.selected,
|
||||
)
|
||||
|
||||
setAdapter(adapter)
|
||||
|
||||
anchorView = impl.view
|
||||
|
||||
width = adapter.measureWidth(context)
|
||||
.coerceAtLeast(context.getPixels(R.dimen.dialog_menu_min_width))
|
||||
|
||||
isModal = true
|
||||
|
||||
horizontalOffset = context.getPixels(R.dimen.item_header_component_size) +
|
||||
context.getPixels(R.dimen.item_header_margin) * 2
|
||||
|
||||
setOnItemClickListener { _, _, position, _ ->
|
||||
dismiss()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
withContext(Dispatchers.IO) {
|
||||
value.set(values[position])
|
||||
}
|
||||
|
||||
impl.selected = position
|
||||
impl.listener?.onChanged()
|
||||
}
|
||||
}
|
||||
|
||||
show()
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package com.github.kr328.clash.design.preference
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.github.kr328.clash.common.compat.getDrawableCompat
|
||||
import com.github.kr328.clash.design.databinding.PreferenceSwitchBinding
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
interface SwitchPreference : Preference {
|
||||
var icon: Drawable?
|
||||
var title: CharSequence?
|
||||
var summary: CharSequence?
|
||||
var listener: OnChangedListener?
|
||||
}
|
||||
|
||||
fun PreferenceScreen.switch(
|
||||
value: KMutableProperty0<Boolean>,
|
||||
@DrawableRes icon: Int? = null,
|
||||
@StringRes title: Int? = null,
|
||||
@StringRes summary: Int? = null,
|
||||
configure: SwitchPreference.() -> Unit = {},
|
||||
): SwitchPreference {
|
||||
val binding = PreferenceSwitchBinding
|
||||
.inflate(context.layoutInflater, root, false)
|
||||
|
||||
val impl = object : SwitchPreference {
|
||||
override val view: View
|
||||
get() = binding.root
|
||||
override var icon: Drawable?
|
||||
get() = binding.iconView.background
|
||||
set(value) {
|
||||
binding.iconView.background = value
|
||||
}
|
||||
override var title: CharSequence?
|
||||
get() = binding.titleView.text
|
||||
set(value) {
|
||||
binding.titleView.text = value
|
||||
}
|
||||
override var summary: CharSequence?
|
||||
get() = binding.summaryView.text
|
||||
set(value) {
|
||||
binding.summaryView.text = value
|
||||
}
|
||||
override var listener: OnChangedListener? = null
|
||||
override var enabled: Boolean
|
||||
get() = binding.root.isEnabled
|
||||
set(value) {
|
||||
binding.root.isEnabled = value
|
||||
binding.root.isFocusable = value
|
||||
binding.root.isClickable = value
|
||||
binding.root.alpha = if (value) 1.0f else 0.33f
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
impl.icon = context.getDrawableCompat(icon)
|
||||
}
|
||||
|
||||
if (title != null) {
|
||||
impl.title = context.getString(title)
|
||||
}
|
||||
|
||||
if (summary != null) {
|
||||
impl.summary = context.getString(summary)
|
||||
}
|
||||
|
||||
impl.configure()
|
||||
|
||||
addElement(impl)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
val initialValue = withContext(Dispatchers.IO) {
|
||||
value.get()
|
||||
}
|
||||
|
||||
binding.switchView.apply {
|
||||
isChecked = initialValue
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
isChecked = !isChecked
|
||||
|
||||
this@switch.launch(Dispatchers.Main) {
|
||||
withContext(Dispatchers.IO) {
|
||||
value.set(isChecked)
|
||||
}
|
||||
|
||||
impl.listener?.onChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return impl
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.github.kr328.clash.design.preference
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import com.github.kr328.clash.design.databinding.PreferenceTipsBinding
|
||||
import com.github.kr328.clash.design.util.getHtml
|
||||
import com.github.kr328.clash.design.util.layoutInflater
|
||||
import com.github.kr328.clash.design.util.root
|
||||
|
||||
interface TipsPreference : Preference {
|
||||
var text: CharSequence?
|
||||
}
|
||||
|
||||
fun PreferenceScreen.tips(
|
||||
@StringRes text: Int,
|
||||
configure: TipsPreference.() -> Unit = {},
|
||||
): TipsPreference {
|
||||
val binding = PreferenceTipsBinding
|
||||
.inflate(context.layoutInflater, context.root, false)
|
||||
val impl = object : TipsPreference {
|
||||
override var text: CharSequence?
|
||||
get() = binding.tips.text
|
||||
set(value) {
|
||||
binding.tips.text = value
|
||||
}
|
||||
override val view: View
|
||||
get() = binding.root
|
||||
override var enabled: Boolean
|
||||
get() = binding.root.isEnabled
|
||||
set(value) {
|
||||
binding.root.isEnabled = value
|
||||
}
|
||||
}
|
||||
|
||||
binding.tips.text = context.getHtml(text)
|
||||
|
||||
impl.configure()
|
||||
|
||||
addElement(impl)
|
||||
|
||||
return impl
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.github.kr328.clash.design.preference
|
||||
|
||||
interface NullableTextAdapter<T> {
|
||||
fun from(value: T): String?
|
||||
fun to(text: String?): T
|
||||
|
||||
companion object {
|
||||
val Port = object : NullableTextAdapter<Int?> {
|
||||
override fun from(value: Int?): String? {
|
||||
if (value == null) return null
|
||||
|
||||
return if (value > 0) value.toString() else ""
|
||||
}
|
||||
|
||||
override fun to(text: String?): Int? {
|
||||
if (text == null) return null
|
||||
|
||||
return text.toIntOrNull() ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
val String = object : NullableTextAdapter<String?> {
|
||||
override fun from(value: String?): String? {
|
||||
return value
|
||||
}
|
||||
|
||||
override fun to(text: String?): String? {
|
||||
return text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TextAdapter<T> {
|
||||
fun from(value: T): String
|
||||
fun to(text: String): T
|
||||
|
||||
companion object {
|
||||
val String = object : TextAdapter<String> {
|
||||
override fun from(value: String): String {
|
||||
return value
|
||||
}
|
||||
|
||||
override fun to(text: String): String {
|
||||
return text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.github.kr328.clash.design.store
|
||||
|
||||
import android.content.Context
|
||||
import com.github.kr328.clash.common.store.Store
|
||||
import com.github.kr328.clash.common.store.asStoreProvider
|
||||
import com.github.kr328.clash.core.model.ProxySort
|
||||
import com.github.kr328.clash.design.model.AppInfoSort
|
||||
import com.github.kr328.clash.design.model.DarkMode
|
||||
|
||||
class UiStore(context: Context) {
|
||||
private val store = Store(
|
||||
context
|
||||
.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||
.asStoreProvider()
|
||||
)
|
||||
|
||||
var enableVpn: Boolean by store.boolean(
|
||||
key = "enable_vpn",
|
||||
defaultValue = true
|
||||
)
|
||||
|
||||
var darkMode: DarkMode by store.enum(
|
||||
key = "dark_mode",
|
||||
defaultValue = DarkMode.Auto,
|
||||
values = DarkMode.values()
|
||||
)
|
||||
|
||||
var proxyExcludeNotSelectable by store.boolean(
|
||||
key = "proxy_exclude_not_selectable",
|
||||
defaultValue = false,
|
||||
)
|
||||
|
||||
var proxyLine: Int by store.int(
|
||||
key = "proxy_line",
|
||||
defaultValue = 1
|
||||
)
|
||||
|
||||
var proxySort: ProxySort by store.enum(
|
||||
key = "proxy_sort",
|
||||
defaultValue = ProxySort.Default,
|
||||
values = ProxySort.values()
|
||||
)
|
||||
|
||||
var proxyLastGroup: String by store.string(
|
||||
key = "proxy_last_group",
|
||||
defaultValue = ""
|
||||
)
|
||||
|
||||
var accessControlSort: AppInfoSort by store.enum(
|
||||
key = "access_control_sort",
|
||||
defaultValue = AppInfoSort.Label,
|
||||
values = AppInfoSort.values(),
|
||||
)
|
||||
|
||||
var accessControlReverse: Boolean by store.boolean(
|
||||
key = "access_control_reverse",
|
||||
defaultValue = false
|
||||
)
|
||||
|
||||
var accessControlSystemApp: Boolean by store.boolean(
|
||||
key = "access_control_system_app",
|
||||
defaultValue = false,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val PREFERENCE_NAME = "ui"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.github.kr328.clash.design.ui
|
||||
|
||||
enum class DayNight {
|
||||
Day, Night
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.github.kr328.clash.design.ui
|
||||
|
||||
data class Insets(val start: Int, val top: Int, val end: Int, val bottom: Int) {
|
||||
companion object {
|
||||
val EMPTY = Insets(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.github.kr328.clash.design.ui
|
||||
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.library.baseAdapters.BR
|
||||
|
||||
class ObservableCurrentTime : BaseObservable() {
|
||||
var value: Long = System.currentTimeMillis()
|
||||
@Bindable get
|
||||
private set(value) {
|
||||
field = value
|
||||
|
||||
notifyPropertyChanged(BR.value)
|
||||
}
|
||||
|
||||
fun update() {
|
||||
value = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.github.kr328.clash.design.ui
|
||||
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
import com.github.kr328.clash.design.BR
|
||||
|
||||
class Surface : BaseObservable() {
|
||||
var insets: Insets = Insets.EMPTY
|
||||
@Bindable get
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
notifyPropertyChanged(BR.insets)
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.github.kr328.clash.design.ui
|
||||
|
||||
enum class ToastDuration {
|
||||
Short, Long, Indefinite
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.github.kr328.clash.design.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.view.ActivityBarLayout
|
||||
|
||||
fun ActivityBarLayout.applyFrom(context: Context) {
|
||||
if (context is Activity) {
|
||||
findViewById<ImageView>(R.id.activity_bar_close_view)?.apply {
|
||||
setOnClickListener {
|
||||
context.onBackPressed()
|
||||
}
|
||||
}
|
||||
findViewById<TextView>(R.id.activity_bar_title_view)?.apply {
|
||||
text = context.title
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.github.kr328.clash.design.util
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.github.kr328.clash.common.compat.foreground
|
||||
import com.github.kr328.clash.design.model.AppInfo
|
||||
|
||||
fun PackageInfo.toAppInfo(pm: PackageManager): AppInfo {
|
||||
return AppInfo(
|
||||
packageName = packageName,
|
||||
icon = applicationInfo.loadIcon(pm).foreground(),
|
||||
label = applicationInfo.loadLabel(pm).toString(),
|
||||
installTime = firstInstallTime,
|
||||
updateDate = lastUpdateTime,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.github.kr328.clash.design.util
|
||||
|
||||
import android.view.View
|
||||
import androidx.databinding.BindingAdapter
|
||||
|
||||
@BindingAdapter("android:minHeight")
|
||||
fun bindMinHeight(view: View, value: Float) {
|
||||
view.minimumHeight = value.toInt()
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.github.kr328.clash.design.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.text.Spanned
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.DimenRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.github.kr328.clash.common.compat.fromHtmlCompat
|
||||
|
||||
val Context.layoutInflater: LayoutInflater
|
||||
get() = LayoutInflater.from(this)
|
||||
|
||||
val Context.root: ViewGroup?
|
||||
get() {
|
||||
return when (this) {
|
||||
is Activity -> {
|
||||
findViewById(android.R.id.content)
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.getPixels(@DimenRes resId: Int): Int {
|
||||
return resources.getDimensionPixelSize(resId)
|
||||
}
|
||||
|
||||
fun Context.getHtml(@StringRes resId: Int): Spanned {
|
||||
return fromHtmlCompat(getString(resId))
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.github.kr328.clash.design.util
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.github.kr328.clash.design.databinding.CustomDialogBinding
|
||||
|
||||
fun Context.showCustomDialog(
|
||||
title: String,
|
||||
message: String,
|
||||
positiveButtonText: String = "确定",
|
||||
negativeButtonText: String = "取消",
|
||||
onPositiveClick: (() -> Unit)? = null,
|
||||
onNegativeClick: (() -> Unit)? = null
|
||||
) {
|
||||
// 使用 ViewBinding 加载自定义布局
|
||||
|
||||
val binding = CustomDialogBinding.inflate(LayoutInflater.from(this))
|
||||
|
||||
// 设置标题和消息
|
||||
binding.dialogTitle.text = title
|
||||
binding.dialogMessage.text = message
|
||||
binding.positiveButton.text = positiveButtonText
|
||||
binding.negativeButton.text = negativeButtonText
|
||||
|
||||
// 创建 AlertDialog
|
||||
val dialog = AlertDialog.Builder(this).setView(binding.root).create()
|
||||
|
||||
// 去除默认背景
|
||||
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
|
||||
// 设置按钮点击事件
|
||||
binding.positiveButton.setOnClickListener {
|
||||
onPositiveClick?.invoke()
|
||||
dialog.dismiss()
|
||||
}
|
||||
binding.negativeButton.setOnClickListener {
|
||||
onNegativeClick?.invoke()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
dialog.show()
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.github.kr328.clash.design.util
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
|
||||
fun <T> List<T>.diffWith(
|
||||
newList: List<T>,
|
||||
detectMove: Boolean = false,
|
||||
id: (T) -> Any? = { it }
|
||||
): DiffUtil.DiffResult {
|
||||
val oldList = this
|
||||
|
||||
return DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return id(oldList[oldItemPosition]) == id(newList[newItemPosition])
|
||||
}
|
||||
|
||||
override fun getOldListSize(): Int {
|
||||
return oldList.size
|
||||
}
|
||||
|
||||
override fun getNewListSize(): Int {
|
||||
return newList.size
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldList[oldItemPosition] == newList[newItemPosition]
|
||||
}
|
||||
}, detectMove)
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.github.kr328.clash.design.util
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.view.ActivityBarLayout
|
||||
import com.github.kr328.clash.design.view.ObservableScrollView
|
||||
|
||||
private class AppBarElevationController(
|
||||
private val activityBar: ActivityBarLayout
|
||||
) {
|
||||
private var animator: ValueAnimator? = null
|
||||
|
||||
var elevated: Boolean = false
|
||||
set(value) {
|
||||
if (field == value)
|
||||
return
|
||||
|
||||
field = value
|
||||
|
||||
animator?.end()
|
||||
|
||||
animator = if (value) {
|
||||
ValueAnimator.ofFloat(
|
||||
activityBar.elevation,
|
||||
activityBar.context.getPixels(R.dimen.toolbar_elevation).toFloat()
|
||||
)
|
||||
} else {
|
||||
ValueAnimator.ofFloat(
|
||||
activityBar.elevation,
|
||||
0f
|
||||
)
|
||||
}.apply {
|
||||
addUpdateListener {
|
||||
activityBar.elevation = it.animatedValue as Float
|
||||
}
|
||||
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun RecyclerView.bindAppBarElevation(activityBar: ActivityBarLayout) {
|
||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
private val controller = AppBarElevationController(activityBar)
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
controller.elevated = !recyclerView.isTop
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun ObservableScrollView.bindAppBarElevation(activityBar: ActivityBarLayout) {
|
||||
val controller = AppBarElevationController(activityBar)
|
||||
|
||||
addOnScrollChangedListener { view, _, _, _, _ ->
|
||||
controller.elevated = !view.isTop
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.github.kr328.clash.design.util
|
||||
|
||||
import android.content.Context
|
||||
import com.github.kr328.clash.common.compat.preferredLocale
|
||||
import com.github.kr328.clash.core.model.Provider
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.service.model.Profile
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
private const val DATE_DATE_ONLY = "yyyy-MM-dd"
|
||||
private const val DATE_TIME_ONLY = "HH:mm:ss.SSS"
|
||||
private const val DATE_ALL = "$DATE_DATE_ONLY $DATE_TIME_ONLY"
|
||||
|
||||
fun Profile.Type.toString(context: Context): String {
|
||||
return when (this) {
|
||||
Profile.Type.File -> context.getString(R.string.file)
|
||||
Profile.Type.Url -> context.getString(R.string.url)
|
||||
Profile.Type.External -> context.getString(R.string.external)
|
||||
}
|
||||
}
|
||||
|
||||
fun Provider.type(context: Context): String {
|
||||
val type = when (type) {
|
||||
Provider.Type.Proxy -> context.getString(R.string.proxy)
|
||||
Provider.Type.Rule -> context.getString(R.string.rule)
|
||||
}
|
||||
|
||||
val vehicle = when (vehicleType) {
|
||||
Provider.VehicleType.HTTP -> context.getString(R.string.http)
|
||||
Provider.VehicleType.File -> context.getString(R.string.file)
|
||||
Provider.VehicleType.Compatible -> context.getString(R.string.compatible)
|
||||
}
|
||||
|
||||
return context.getString(R.string.format_provider_type, type, vehicle)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun Date.format(
|
||||
context: Context,
|
||||
includeDate: Boolean = true,
|
||||
includeTime: Boolean = true,
|
||||
): String {
|
||||
val locale = context.resources.configuration.preferredLocale
|
||||
|
||||
return when {
|
||||
includeDate && includeTime ->
|
||||
SimpleDateFormat(DATE_ALL, locale).format(this)
|
||||
includeDate ->
|
||||
SimpleDateFormat(DATE_DATE_ONLY, locale).format(this)
|
||||
includeTime ->
|
||||
SimpleDateFormat(DATE_TIME_ONLY, locale).format(this)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
fun Long.toBytesString(): String {
|
||||
return when {
|
||||
this > 1024.0 * 1024 * 1024 * 1024 * 1024 * 1024 ->
|
||||
String.format("%.2f EiB", (this.toDouble() / 1024 / 1024 / 1024 / 1024 / 1024 / 1024))
|
||||
this > 1024.0 * 1024 * 1024 * 1024 * 1024 ->
|
||||
String.format("%.2f PiB", (this.toDouble() / 1024 / 1024 / 1024 / 1024 / 1024))
|
||||
this > 1024.0 * 1024 * 1024 * 1024 ->
|
||||
String.format("%.2f TiB", (this.toDouble() / 1024 / 1024 / 1024 / 1024))
|
||||
this > 1024 * 1024 * 1024 ->
|
||||
String.format("%.2f GiB", (this.toDouble() / 1024 / 1024 / 1024))
|
||||
this > 1024 * 1024 ->
|
||||
String.format("%.2f MiB", (this.toDouble() / 1024 / 1024))
|
||||
this > 1024 ->
|
||||
String.format("%.2f KiB", (this.toDouble() / 1024))
|
||||
else ->
|
||||
"$this Bytes"
|
||||
}
|
||||
}
|
||||
|
||||
fun Double.toProgress(): Int {
|
||||
return this.toInt()
|
||||
}
|
||||
fun Long.toDateStr(): String {
|
||||
val simpleDateFormat =SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||
return simpleDateFormat.format(Date(this))
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.github.kr328.clash.design.util
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.github.kr328.clash.design.ui.Insets
|
||||
|
||||
fun View.setOnInsertsChangedListener(adaptLandscape: Boolean = true, listener: (Insets) -> Unit) {
|
||||
setOnApplyWindowInsetsListener { v, ins ->
|
||||
val compat = WindowInsetsCompat.toWindowInsetsCompat(ins)
|
||||
val insets = compat.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
|
||||
val rInsets = if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
||||
Insets(
|
||||
insets.left,
|
||||
insets.top,
|
||||
insets.right,
|
||||
insets.bottom,
|
||||
)
|
||||
} else {
|
||||
Insets(
|
||||
insets.right,
|
||||
insets.top,
|
||||
insets.left,
|
||||
insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
listener(if (adaptLandscape) rInsets.landscape(v.context) else rInsets)
|
||||
|
||||
compat.toWindowInsets()!!
|
||||
|
||||
|
||||
}
|
||||
|
||||
requestApplyInsets()
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.github.kr328.clash.design.util
|
||||
|
||||
import android.content.Context
|
||||
import com.github.kr328.clash.design.R
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
fun Long.elapsedIntervalString(context: Context): String {
|
||||
val day = TimeUnit.MILLISECONDS.toDays(this)
|
||||
val hour = TimeUnit.MILLISECONDS.toHours(this)
|
||||
val minute = TimeUnit.MILLISECONDS.toMinutes(this)
|
||||
|
||||
return when {
|
||||
day > 0 -> context.getString(R.string.format_days_ago, day)
|
||||
hour > 0 -> context.getString(R.string.format_hours_ago, hour)
|
||||
minute > 0 -> context.getString(R.string.format_minutes_ago, minute)
|
||||
else -> context.getString(R.string.recently)
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.github.kr328.clash.design.util
|
||||
|
||||
import android.content.Context
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.ui.Insets
|
||||
|
||||
fun Insets.landscape(context: Context): Insets {
|
||||
val displayMetrics = context.resources.displayMetrics
|
||||
val minWidth = context.getPixels(R.dimen.surface_landscape_min_width)
|
||||
|
||||
val width = displayMetrics.widthPixels
|
||||
val height = displayMetrics.heightPixels
|
||||
|
||||
return if (width > height && width > minWidth) {
|
||||
val expectedWidth = width.coerceAtMost(height.coerceAtLeast(minWidth))
|
||||
|
||||
val padding = (width - expectedWidth).coerceAtLeast(start + end) / 2
|
||||
|
||||
copy(start = padding.coerceAtLeast(start), end = padding.coerceAtLeast(end))
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.github.kr328.clash.design.util
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.View.MeasureSpec
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ListAdapter
|
||||
|
||||
fun ListAdapter.measureWidth(context: Context): Int {
|
||||
val parent = FrameLayout(context)
|
||||
|
||||
val widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
||||
val heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
||||
|
||||
var itemView: View? = null
|
||||
var maxWidth = 0
|
||||
var itemType = 0
|
||||
|
||||
for (i in 0 until count) {
|
||||
val positionType = getItemViewType(i)
|
||||
if (positionType != itemType) {
|
||||
itemType = positionType
|
||||
itemView = null
|
||||
}
|
||||
|
||||
itemView = getView(i, itemView, parent)
|
||||
itemView.measure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
val itemWidth: Int = itemView.measuredWidth
|
||||
|
||||
if (itemWidth > maxWidth) {
|
||||
maxWidth = itemWidth
|
||||
}
|
||||
}
|
||||
|
||||
return maxWidth
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.github.kr328.clash.utity
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.github.kr328.clash.design.R
|
||||
|
||||
|
||||
object LoadingDialog {
|
||||
private var dialog: AlertDialog? = null
|
||||
|
||||
fun show(context: Context, message: String = "请稍等...") {
|
||||
if (dialog == null) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view: View = inflater.inflate(R.layout.loading_dialog, null)
|
||||
val progressBar: ProgressBar = view.findViewById(R.id.progressBar)
|
||||
val messageTextView: TextView = view.findViewById(R.id.loadingMessage)
|
||||
|
||||
messageTextView.text = message
|
||||
|
||||
dialog = AlertDialog.Builder(context)
|
||||
.setCancelable(false)
|
||||
.setView(view)
|
||||
.create()
|
||||
|
||||
dialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
dialog?.dismiss()
|
||||
dialog = null
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user