Skip to content
This repository was archived by the owner on Jul 18, 2026. It is now read-only.

Commit 2f79f03

Browse files
authored
Merge pull request #388 from misaka10032w/baseline/compose-without-player
[Added] 支持自定义 CloudFlare 边缘节点 HOSTS 、优化 SAF 下载续传与错误处理
2 parents bf0d1a8 + 5175984 commit 2f79f03

15 files changed

Lines changed: 580 additions & 141 deletions

File tree

app/src/main/java/com/yenaly/han1meviewer/Preferences.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ object Preferences {
189189
val useBuiltInHosts: Boolean
190190
get() = preferenceSp.getBoolean(SettingsPreferenceKeys.USE_BUILT_IN_HOSTS, false)
191191

192+
val customHostsData: String
193+
get() = preferenceSp.getString(SettingsPreferenceKeys.CUSTOM_HOSTS_DATA, EMPTY_STRING).orEmpty()
194+
192195
val useDoH: Boolean
193196
get() = preferenceSp.getBoolean(SettingsPreferenceKeys.USE_DOH, false)
194197

app/src/main/java/com/yenaly/han1meviewer/logic/network/HDns.kt

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ class HDns : Dns {
3232
companion object {
3333

3434
private val cloudFlareIps = listOf(
35-
"172.64.229.154", "104.25.254.167", "172.67.75.184", "104.21.7.20", "172.67.187.141",
36-
"2606:4700:8dd1::2a46:47f8", "2606:4700:3031::ac43:bb8d", "2606:4700:3030::6815:746"
35+
"172.64.229.154", "162.159.0.1", "108.162.192.1", "172.64.33.1", "104.19.0.1",
36+
"2606:4700:3035::ac43:bb8d", "2606:4700:3030::6815:746", "2606:4700:3030::6815:714"
3737
)
3838

3939
/**
@@ -46,10 +46,53 @@ class HDns : Dns {
4646
InetAddress.getByAddress(host, InetAddress.getByName(it).address)
4747
}
4848
}
49+
50+
/**
51+
* 解析自定义 IP 列表,逗号分隔
52+
*/
53+
fun parseCustomIps(raw: String): List<String> {
54+
return raw.split(",")
55+
.map { it.trim() }
56+
.filter { it.isNotEmpty() }
57+
}
58+
59+
/**
60+
* 验证自定义 IP 列表格式是否有效
61+
* @return 无效 IP 的错误信息列表,为空表示全部有效
62+
*/
63+
fun validateCustomHosts(raw: String): List<String> {
64+
val errors = mutableListOf<String>()
65+
if (raw.isBlank()) return errors
66+
val ips = parseCustomIps(raw)
67+
if (ips.isEmpty()) {
68+
errors.add("No IP addresses entered")
69+
return errors
70+
}
71+
ips.forEach { ip ->
72+
if (!isValidIpAddress(ip)) {
73+
errors.add("Invalid IP address: \"$ip\"")
74+
}
75+
}
76+
return errors
77+
}
78+
79+
private fun isValidIpAddress(ip: String): Boolean {
80+
return runCatching {
81+
val addr = InetAddress.getByName(ip)
82+
addr.hostAddress == ip || addr.hostAddress == ip.removePrefix("[")
83+
.removeSuffix("]")
84+
}.getOrDefault(false)
85+
}
4986
}
5087

5188
override fun lookup(hostname: String): List<InetAddress> {
5289
if (Preferences.useBuiltInHosts && HANIME_HOSTNAME.contains(hostname)) {
90+
val customIps = resolveCustomIps()
91+
if (!customIps.isNullOrEmpty()) {
92+
return customIps.map {
93+
InetAddress.getByAddress(hostname, InetAddress.getByName(it).address)
94+
}
95+
}
5396
return cloudFlareIps.map {
5497
InetAddress.getByAddress(hostname, InetAddress.getByName(it).address)
5598
}
@@ -119,6 +162,10 @@ class HDns : Dns {
119162

120163
fun getCDNList(host: String): List<String> {
121164
if (Preferences.useBuiltInHosts && HANIME_HOSTNAME.contains(host)) {
165+
val customIps = resolveCustomIps()
166+
if (!customIps.isNullOrEmpty()) {
167+
return customIps.distinct()
168+
}
122169
return cloudFlareIps.distinct()
123170
}
124171

@@ -130,4 +177,22 @@ class HDns : Dns {
130177
}
131178
}
132179

180+
@Volatile
181+
private var cachedCustomIps: List<String>? = null
182+
183+
@Volatile
184+
private var cachedCustomIpsRaw: String? = null
185+
186+
private fun resolveCustomIps(): List<String>? {
187+
val raw = Preferences.customHostsData
188+
if (raw.isBlank()) return null
189+
if (raw == cachedCustomIpsRaw && cachedCustomIps != null) {
190+
return cachedCustomIps
191+
}
192+
val result = parseCustomIps(raw)
193+
cachedCustomIpsRaw = raw
194+
cachedCustomIps = result
195+
return result
196+
}
197+
133198
}

app/src/main/java/com/yenaly/han1meviewer/logic/network/ServiceCreator.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.yenaly.yenaly_libs.utils.unsafeLazy
1414
import okhttp3.Cache
1515
import okhttp3.MediaType.Companion.toMediaType
1616
import okhttp3.OkHttpClient
17+
import okhttp3.Protocol
1718
import retrofit2.Retrofit
1819
import java.io.File
1920
import java.util.concurrent.TimeUnit
@@ -71,6 +72,7 @@ object ServiceCreator {
7172
private fun buildDownloadClient(): OkHttpClient {
7273
return OkHttpClient.Builder()
7374
.connectTimeout(5, TimeUnit.SECONDS)
75+
.protocols(listOf(Protocol.HTTP_1_1))
7476
.addInterceptor(UserAgentInterceptor)
7577
.addInterceptor(downloadSpeedLimitInterceptor)
7678
.dns(dns)

app/src/main/java/com/yenaly/han1meviewer/logic/network/interceptor/SpeedLimitResponseBody.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,9 @@ class SpeedLimitResponseBody(
2121

2222
override fun contentType() = responseBody.contentType()
2323

24-
override fun source() = throttler.source(responseBody.source()).buffer()
25-
}
24+
override fun source() = if (maxSpeed > 0) {
25+
throttler.source(responseBody.source()).buffer()
26+
} else {
27+
responseBody.source()
28+
}
29+
}

app/src/main/java/com/yenaly/han1meviewer/ui/component/TripleButtonDialog.kt

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
package com.yenaly.han1meviewer.ui.component
22

33
import androidx.compose.foundation.layout.Arrangement
4-
import androidx.compose.foundation.layout.Row
5-
import androidx.compose.foundation.layout.Spacer
4+
import androidx.compose.foundation.layout.FlowRow
65
import androidx.compose.foundation.layout.fillMaxWidth
7-
import androidx.compose.foundation.layout.width
86
import androidx.compose.material3.AlertDialog
97
import androidx.compose.material3.Text
108
import androidx.compose.material3.TextButton
119
import androidx.compose.runtime.Composable
12-
import androidx.compose.ui.Alignment
1310
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.text.style.TextOverflow
1412
import androidx.compose.ui.tooling.preview.Preview
1513
import androidx.compose.ui.unit.dp
1614
import com.yenaly.han1meviewer.ui.preview.ComponentPreview
@@ -35,21 +33,28 @@ fun TripleButtonDialog(
3533
title = { Text(title) },
3634
text = message?.let { { Text(it) } },
3735
confirmButton = {
38-
Row(
36+
FlowRow(
3937
modifier = Modifier.fillMaxWidth(),
4038
horizontalArrangement = Arrangement.End,
41-
verticalAlignment = Alignment.CenterVertically,
39+
verticalArrangement = Arrangement.spacedBy(4.dp),
4240
) {
43-
TextButton(onClick = onNegative) { Text(negativeText) }
44-
Spacer(Modifier.width(4.dp))
45-
TextButton(onClick = onNeutral) { Text(neutralText) }
46-
Spacer(Modifier.width(4.dp))
47-
TextButton(onClick = onPositive) { Text(positiveText) }
41+
TextButton(onClick = onNegative) { DialogButtonText(negativeText) }
42+
TextButton(onClick = onNeutral) { DialogButtonText(neutralText) }
43+
TextButton(onClick = onPositive) { DialogButtonText(positiveText) }
4844
}
4945
},
5046
)
5147
}
5248

49+
@Composable
50+
private fun DialogButtonText(text: String) {
51+
Text(
52+
text = text,
53+
maxLines = 2,
54+
overflow = TextOverflow.Ellipsis,
55+
)
56+
}
57+
5358
@Preview(showBackground = true, showSystemUi = true)
5459
@Composable
5560
private fun TripleButtonDialogPreview(){
@@ -58,12 +63,12 @@ private fun TripleButtonDialogPreview(){
5863
visible = true,
5964
title = "这是标题",
6065
message = "这是信息",
61-
negativeText = "反向按钮",
62-
neutralText = "中性按钮",
63-
positiveText = "正向按钮",
66+
negativeText = "取消并关闭",
67+
neutralText = "恢复默认下载目录",
68+
positiveText = "选择自定义下载文件夹",
6469
onNegative = { },
6570
onNeutral = { },
6671
onPositive = { },
6772
) { }
6873
}
69-
}
74+
}

app/src/main/java/com/yenaly/han1meviewer/ui/navigation/settings/NetworkSettingsRoute.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import android.content.Context
44
import android.os.Handler
55
import android.os.Looper
66
import android.util.Log
7+
import androidx.compose.material3.AlertDialog
8+
import androidx.compose.material3.Text
9+
import androidx.compose.material3.TextButton
710
import androidx.compose.runtime.Composable
811
import androidx.compose.runtime.DisposableEffect
912
import androidx.compose.runtime.getValue
@@ -42,6 +45,7 @@ private const val NETWORK_PROXY_PORT = "proxy_port"
4245
private const val NETWORK_DOMAIN_NAME = "domain_name"
4346
private const val NETWORK_SELECTED_BASE_URL = "selectedBaseUrl"
4447
private const val NETWORK_USE_BUILT_IN_HOSTS = "use_built_in_hosts"
48+
private const val NETWORK_CUSTOM_HOSTS_DATA = "custom_hosts_data"
4549
private const val NETWORK_USE_DOH = "use_doh"
4650
private const val NETWORK_DOH_PRESET = "doh_preset"
4751
private const val NETWORK_DOH_CUSTOM_URL = "doh_custom_url"
@@ -62,6 +66,7 @@ fun NetworkSettingsRouteScreen() {
6266
var isDohTesting by remember { mutableStateOf(false) }
6367
var showDomainRestartConfirm by remember { mutableStateOf(false) }
6468
var showHostsRestartConfirm by remember { mutableStateOf(false) }
69+
var showCustomHostsValidationError by remember { mutableStateOf<List<String>?>(null) }
6570
var showDohConflictConfirm by remember { mutableStateOf(false) }
6671
var pendingDomainValue by remember { mutableStateOf("") }
6772
var pendingDohConflictTarget by remember { mutableStateOf(DohConflictTarget.EnableDoH) }
@@ -195,6 +200,21 @@ fun NetworkSettingsRouteScreen() {
195200
refreshKey++
196201
showHostsRestartConfirm = true
197202
},
203+
onSaveCustomHosts = { data ->
204+
val errors = HDns.validateCustomHosts(data)
205+
if (errors.isNotEmpty()) {
206+
showCustomHostsValidationError = errors
207+
return@NetworkSettingsScreen
208+
}
209+
Preferences.preferenceSp.edit(commit = true) {
210+
putString(NETWORK_CUSTOM_HOSTS_DATA, data)
211+
}
212+
refreshKey++
213+
if (Preferences.useBuiltInHosts) {
214+
HanimeNetwork.rebuildNetwork()
215+
}
216+
},
217+
customHostsData = Preferences.customHostsData,
198218
onSaveDohSettings = { enabled, preset, url, bootstrapIps, timeoutSeconds ->
199219
pendingDohEnabled = enabled
200220
pendingDohPreset = preset
@@ -296,6 +316,20 @@ fun NetworkSettingsRouteScreen() {
296316
onDismiss = { showHostsRestartConfirm = false },
297317
)
298318

319+
val validationErrors = showCustomHostsValidationError
320+
if (validationErrors != null) {
321+
AlertDialog(
322+
onDismissRequest = { showCustomHostsValidationError = null },
323+
title = { Text(stringResource(R.string.attention)) },
324+
text = { Text(validationErrors.joinToString("\n")) },
325+
confirmButton = {
326+
TextButton(onClick = { showCustomHostsValidationError = null }) {
327+
Text(stringResource(R.string.confirm))
328+
}
329+
},
330+
)
331+
}
332+
299333
ConfirmDialog(
300334
visible = showDohConflictConfirm,
301335
title = stringResource(R.string.attention),

app/src/main/java/com/yenaly/han1meviewer/ui/navigation/settings/SettingsPreferenceKeys.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ object SettingsPreferenceKeys {
3333
const val DOMAIN_NAME = "domain_name"
3434
const val SELECTED_BASE_URL = "selectedBaseUrl"
3535
const val USE_BUILT_IN_HOSTS = "use_built_in_hosts"
36+
const val CUSTOM_HOSTS_DATA = "custom_hosts_data"
3637
const val USE_DOH = "use_doh"
3738
const val DOH_PRESET = "doh_preset"
3839
const val DOH_CUSTOM_URL = "doh_custom_url"

app/src/main/java/com/yenaly/han1meviewer/ui/screen/home/download/DownloadUtils.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ fun downloadStateText(state: DownloadState, progress: Int): String = when (state
8080
DownloadState.Queued -> stringResource(R.string.already_in_queue)
8181
DownloadState.Downloading -> stringResource(R.string.download_progress_percent, progress)
8282
DownloadState.Paused -> stringResource(R.string.paused)
83-
DownloadState.Failed -> stringResource(R.string.retry)
83+
DownloadState.Failed -> stringResource(R.string.download_failed_tap_retry)
8484
DownloadState.Finished -> stringResource(R.string.download_complete)
8585
DownloadState.Unknown -> stringResource(R.string.loading)
8686
}

0 commit comments

Comments
 (0)