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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,46 @@
#### 2.0.5.8

* 底栏左侧区域
* 1. 单击:快速跳转至配置列表主页面,并高亮定位当前自动选择器 / 活动配置。
* 2. 长按:一键清空所有节点的历史测试数据(重置 Ping 延迟、测速结果与报错状态)。
* 底栏右侧区域
* 1. 单击:测试当前活动出站链路的连通性(保持原有逻辑)。
* 2. 长按:批量测试当前主页面订阅/分组中全部节点的 Ping 延迟,并同步刷新出站 IP 与归属地国旗信息。

#### 2.0.5.7
* 配置列表(紧凑模式)信息覆盖显示规则
* 更改右侧数据展示优先级规则(由高到低):
* 测试中(实时进行中显示「测试中...」)
* 排队中(任务等待队列中半透明显示「排队中」)
* 连接异常 / 特殊状态(连通显示「连接成功」,超时/失败显示「不可用」)
* 测速结果(存在下载/上传速率时优先展示,覆盖延迟与 IP)
* 国旗与延迟(无测速数据时优先展示「国旗 + 延迟 ms」,覆盖出网 IP)
* 出网外部 IP(无测速且无延迟/国旗时显示「🌐 IP」)
* 历史流量统计(无任何测试结果时回退显示流量;流量为 0 时空白不占位)
* 涉及文件:
* app/src/main/java/io/nekohasekai/sagernet/ui/profiles/ProfileRowHolder.kt


#### 2.0.5.6
* 1. 删除底栏测试是显示的http https。app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt
* 2. 更新紧凑模式ip与国家测试,显示国旗和延时。app/src/main/java/io/nekohasekai/sagernet/ui/profiles/ProfileRowHolder.kt
#### 2.0.5.3

* 更新紧凑模式右侧数据显示规则。app/src/main/java/io/nekohasekai/sagernet/ui/profiles/ProfileRowHolder.kt
* 1. 如果没有测试结果,或者测试结果情况,则显示流量统计信息,如果流量统计信息为0,则显示空。
* 2. IP与国家、速度、延迟 等测试结果根据时间戳显示最新数据。

#### 2.0.5.2

**底栏修改**
* 1. 底栏形成“左侧双行网速、右侧国旗加状态”的对称排布。app/src/main/res/layout/layout_stats_bar_content.xml
* 2. 调低底栏高度。 app/src/main/res/layout/layout_stats_bar_content.xml
* 3. 一旦底层核心服务状态切换为 `Connected`(已连接),系统会**立即自动调用 `testConnection()` 发起测速**,实现了连接后全自动测速动作。app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt

#### 2.0.5.1

* 第一个编译版本,只生成 v8a 文件

#### 2.0.5

* Add per-group routing profiles **1**
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,6 @@ class MainActivity : ThemedActivity(),
return true
}


@SuppressLint("CommitTransaction")
fun displayFragment(fragment: ToolbarFragment) {
currentMainFragment = fragment
Expand Down Expand Up @@ -442,6 +441,9 @@ class MainActivity : ThemedActivity(),
showWhenConnected = state == BaseService.State.Connected,
animate = animateControls,
)
if (state == BaseService.State.Connected) {
binding.stats.testConnection()
}
if (state == BaseService.State.Connecting) errorBar?.dismiss()
if (msg == null) return
if (msg == DataStore.serviceError) showServiceError(msg) else snackbar(getString(R.string.vpn_error, msg)).show()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.database.SettingsRegistry
import io.nekohasekai.sagernet.database.TestShowItems
import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener
import io.nekohasekai.sagernet.ktx.alert
import io.nekohasekai.sagernet.ktx.getColorAttr
Expand Down Expand Up @@ -63,16 +62,12 @@ internal class RowContent(
private const val LRO = "\u202D"
private const val NBSP = "\u00A0"

/** Between items, where the line may break. */
private const val GAP = "\u2002 "

/** A core rate: "34.13Mbps". */
private val RATE = Regex("""(\d+(?:\.\d+)?)\s*([KMG]bps)""", RegexOption.IGNORE_CASE)

fun of(context: Context, profile: ProxyEntity, adapter: ProfileListAdapter): RowContent {
val secondary = context.getColorAttr(android.R.attr.textColorSecondary)
val brief = adapter.isCompactRow
val showTraffic = !brief && profile.rx + profile.tx != 0L
val up = Formatter.formatShortFileSize(context, profile.tx)
val down = Formatter.formatShortFileSize(context, profile.rx)
val address = if (!brief && adapter.host?.alwaysShowAddress == true && profile.outbound.name.isNotBlank()) {
Expand All @@ -84,9 +79,10 @@ internal class RowContent(
val color = profile.latencyColor().takeIf { it != 0 } ?: secondary
val result = SpannableStringBuilder()
var description: String? = null
// Profile::DisplayTestResult: a failed or connect-only test shows its verdict alone

when {
phase == RowPhase.TESTING -> result.append(context.getString(R.string.connection_test_testing))
phase == RowPhase.QUEUED -> result.append(context.getString(R.string.profiles_test_queued))
latency == ProxyEntity.LATENCY_CONNECT_ONLY || latency < 0 -> result.append(
context.getString(
if (latency == ProxyEntity.LATENCY_CONNECT_ONLY) R.string.test_connect_ok else R.string.unavailable
Expand All @@ -96,47 +92,51 @@ internal class RowContent(
)

else -> {
val items = testResult(context, profile, TestShowItems.of(adapter.testItemsToShow), color, secondary)
.let { if (brief) it.take(1) else it }
if (items.isNotEmpty()) {
// 获取覆盖互斥的单一最新测试结果
val singleItem = singleTestResult(context, profile, color, secondary)
if (singleItem != null) {
result.append(LRO)
items.forEachIndexed { index, item ->
if (index > 0) result.append(GAP)
result.append(item.text, ForegroundColorSpan(item.color), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
description = items.joinToString(", ") { it.spoken }
} else if (phase == RowPhase.QUEUED) {
result.append(context.getString(R.string.profiles_test_queued))
result.append(singleItem.text, ForegroundColorSpan(singleItem.color), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
description = singleItem.spoken
} else if (profile.rx + profile.tx > 0L) {
// 无测试结果,但产生了流量,回退显示流量统计
val trafficText = context.getString(R.string.traffic, up, down)
result.append(trafficText, ForegroundColorSpan(secondary), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
description = context.getString(R.string.profile_row_traffic, up, down)
}
// 流量为 0 且无测试结果时,result 保持为空,不会在卡片右侧留有任何占位
}
}

val error = if (phase == null) profile.testError?.takeIf { it.isNotBlank() } else null
return RowContent(
result = result,
resultDescription = description,
resultDimmed = phase == RowPhase.QUEUED,
testing = phase == RowPhase.TESTING,
traffic = if (showTraffic) context.getString(R.string.traffic, up, down) else "",
trafficDescription = if (showTraffic) context.getString(R.string.profile_row_traffic, up, down) else null,
traffic = "",
trafficDescription = null,
address = address,
error = error,
brief = brief,
)
}

/**
* The items of Profile::DisplayTestResult (Profile.cpp:36-56): flag and latency in the latency colour, then
* the speeds and the egress IP as the group's test_items_to_show allows.
* 互斥单选展示:测速 > 延迟与国旗 > IP
* 保证只显示一个结果,新测试触发后自动覆盖
*/
private fun testResult(
private fun singleTestResult(
context: Context,
profile: ProxyEntity,
show: TestShowItems,
@ColorInt color: Int,
@ColorInt secondary: Int,
): List<Item> {
): Item? {
// 1. 优先展示上下行速率(如果存在测速结果)
speeds(context, profile.dlSpeed, profile.ulSpeed, secondary)?.let { return it }

// 2. 其次展示基础:延迟与国家代码
val latency = profile.latency
val items = ArrayList<Item>()
val head = ArrayList<String>()
val headSpoken = ArrayList<String>()
profile.testCountry?.takeIf { it.isNotEmpty() }?.let { country ->
Expand All @@ -148,13 +148,17 @@ internal class RowContent(
head += "$latency${NBSP}ms"
headSpoken += "$latency ms"
}
if (head.isNotEmpty()) items += Item(head.joinToString(NBSP), color, headSpoken.joinToString(", "))
if (show.showSpeed) speeds(context, profile.dlSpeed, profile.ulSpeed, secondary)?.let { items += it }
if (head.isNotEmpty()) {
return Item(head.joinToString(NBSP), color, headSpoken.joinToString(", "))
}

// 3. 兜底展示出口 IP
val ip = profile.ipOut.orEmpty()
if (show.showIp && ip.isNotEmpty()) {
items += Item("🌐$NBSP$ip", secondary, context.getString(R.string.profile_row_ip, ip))
if (ip.isNotEmpty()) {
return Item("🌐$NBSP$ip", secondary, context.getString(R.string.profile_row_ip, ip))
}
return items

return null
}

/** "↓34.1 ↑11.6 Mbps": three significant digits, one unit when both rates share it; "N/A" is left out. */
Expand Down
10 changes: 2 additions & 8 deletions app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -415,19 +415,13 @@ class StatsBar @JvmOverloads constructor(
runOnDefaultDispatcher {
try {
val elapsed = activity.urlTest()
onMainDispatcher {
onMainDispatcher {
isEnabled = true
setStatus(
if (elapsed == ProxyEntity.LATENCY_CONNECT_ONLY) {
app.getText(R.string.test_connect_ok)
} else {
app.getString(
if (DataStore.testUrl.startsWith("https://")) {
R.string.connection_test_available
} else {
R.string.connection_test_available_http
}, elapsed
)
"$elapsed ms"
}
)
}
Expand Down
56 changes: 36 additions & 20 deletions app/src/main/res/layout/layout_stats_bar_content.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="4dp">

<!-- The speeds left of the FAB, the exit right of it; equal halves keep the gap under the centred FAB. -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">

<!-- 左侧:上行/下行速率 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
Expand All @@ -38,34 +42,46 @@

</LinearLayout>

<!-- 中间:预留给正中心悬浮 FAB 按钮的间距 -->
<Space
android:layout_width="72dp"
android:layout_height="0dp" />

<TextView
android:id="@+id/exit_country"
<!-- 右侧:国旗与测试状态(垂直排列靠右对齐) -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="end"
android:singleLine="true"
android:textColor="?whiteOrTextPrimary"
android:textSize="14sp"
android:visibility="gone"
tools:text="Germany"
tools:visibility="visible" />
android:orientation="vertical">

</LinearLayout>
<TextView
android:id="@+id/exit_country"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="end"
android:singleLine="true"
android:textColor="?whiteOrTextPrimary"
android:textSize="14sp"
android:visibility="gone"
tools:text="Germany"
tools:visibility="visible" />

<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?whiteOrTextPrimary"
android:textSize="16sp"
tools:text="@string/connection_test_available" />
<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:textColor="?whiteOrTextPrimary"
android:textSize="13sp"
tools:text="@string/connection_test_available" />

</LinearLayout>

</LinearLayout>

</LinearLayout>