Skip to content

Commit 1f10fbd

Browse files
anonvectorclaude
andcommitted
Scanner/UI improvements, N-tier shuffle, setup guide relocation
- Scanner: BackHandler stops scan on system back gesture - Scanner: N-tier SHUFFLE_BELOW support for resolver list - Scanner: filled Button for scan resolvers in edit profile - UI: move server setup guides to bottom of edit profile form - Data: remove 62 duplicate IPs from resolver list Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d7a3158 commit 1f10fbd

6 files changed

Lines changed: 94 additions & 125 deletions

File tree

app/src/main/java/app/slipnet/data/repository/ResolverScannerRepositoryImpl.kt

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -46,32 +46,23 @@ class ResolverScannerRepositoryImpl @Inject constructor(
4646
) : ResolverScannerRepository {
4747

4848
private var cachedResolvers: List<String>? = null
49-
private var cachedPriorityCount: Int = 0
50-
private var cachedSecondaryCount: Int = 0
49+
private var cachedTierBoundaries: List<Int> = emptyList()
5150

5251
override fun getDefaultResolvers(): List<String> {
53-
// Return cached list if available
5452
cachedResolvers?.let { return it }
5553

56-
// Load from raw resource file; supports two "# SHUFFLE_BELOW" markers:
54+
// Load from raw resource file; supports N "# SHUFFLE_BELOW" markers:
5755
// - Before first marker: top priority resolvers (not shuffled, scanned first)
58-
// - Between first and second marker: secondary resolvers (shuffled, scanned second)
59-
// - After second marker: remaining resolvers (shuffled, scanned last)
56+
// - Between consecutive markers: independent tiers (each shuffled separately)
57+
// - After last marker: remaining resolvers (shuffled, scanned last)
6058
val resolvers = mutableListOf<String>()
61-
var markerCount = 0
62-
var firstMarkerIndex = 0
63-
var secondMarkerIndex = 0
59+
val boundaries = mutableListOf<Int>()
6460
try {
6561
context.resources.openRawResource(R.raw.resolvers).bufferedReader().useLines { lines ->
6662
for (line in lines) {
6763
val trimmed = line.trim()
6864
if (trimmed == "# SHUFFLE_BELOW") {
69-
markerCount++
70-
if (markerCount == 1) {
71-
firstMarkerIndex = resolvers.size
72-
} else if (markerCount == 2) {
73-
secondMarkerIndex = resolvers.size
74-
}
65+
boundaries.add(resolvers.size)
7566
continue
7667
}
7768
if (trimmed.isNotBlank() && !trimmed.startsWith("#") && isValidIpAddress(trimmed)) {
@@ -80,32 +71,18 @@ class ResolverScannerRepositoryImpl @Inject constructor(
8071
}
8172
}
8273
} catch (e: Exception) {
83-
// Fallback to basic public DNS if resource loading fails
8474
return listOf("8.8.8.8", "8.8.4.4", "1.1.1.1", "1.0.0.1")
8575
}
8676

87-
if (markerCount == 0) {
88-
firstMarkerIndex = 0
89-
secondMarkerIndex = 0
90-
} else if (markerCount == 1) {
91-
// Single marker: treat as priority count, no secondary section
92-
secondMarkerIndex = resolvers.size
93-
}
94-
cachedPriorityCount = firstMarkerIndex
95-
cachedSecondaryCount = secondMarkerIndex - firstMarkerIndex
77+
cachedTierBoundaries = boundaries
9678
cachedResolvers = resolvers
97-
Log.d("ResolverScanner", "Parsed ${resolvers.size} resolvers, priorityCount=$firstMarkerIndex, secondaryCount=${cachedSecondaryCount}, markers=$markerCount, first5=${resolvers.take(5)}")
79+
Log.d("ResolverScanner", "Parsed ${resolvers.size} resolvers, tiers=${boundaries.size + 1}, boundaries=$boundaries, first5=${resolvers.take(5)}")
9880
return resolvers
9981
}
10082

101-
override fun getDefaultResolverPriorityCount(): Int {
102-
if (cachedResolvers == null) getDefaultResolvers()
103-
return cachedPriorityCount
104-
}
105-
106-
override fun getDefaultResolverSecondaryCount(): Int {
83+
override fun getDefaultResolverTierBoundaries(): List<Int> {
10784
if (cachedResolvers == null) getDefaultResolvers()
108-
return cachedSecondaryCount
85+
return cachedTierBoundaries
10986
}
11087

11188
override fun parseResolverList(content: String): List<String> {

app/src/main/java/app/slipnet/domain/repository/ResolverScannerRepository.kt

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,12 @@ interface ResolverScannerRepository {
1616
fun getDefaultResolvers(): List<String>
1717

1818
/**
19-
* Number of priority (famous) resolvers at the top of the default list that should not be shuffled.
19+
* Tier boundaries from `# SHUFFLE_BELOW` markers in the resolver list.
20+
* Returns indices where each marker appeared. Items before the first marker are
21+
* never shuffled; items between consecutive markers form independent shuffle tiers.
22+
* Example: markers at [10, 50] → tier 0 (0..9, unshuffled), tier 1 (10..49, shuffled), tier 2 (50..end, shuffled)
2023
*/
21-
fun getDefaultResolverPriorityCount(): Int
22-
23-
/**
24-
* Number of secondary resolvers (between first and second SHUFFLE_BELOW markers) that are
25-
* shuffled independently and scanned before the remaining resolvers.
26-
*/
27-
fun getDefaultResolverSecondaryCount(): Int
24+
fun getDefaultResolverTierBoundaries(): List<Int>
2825

2926
/**
3027
* Parse a text file content into a list of IP addresses

app/src/main/java/app/slipnet/presentation/profiles/EditProfileScreen.kt

Lines changed: 58 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ fun EditProfileScreen(
395395
)
396396

397397
if (onNavigateToScanner != null) {
398-
OutlinedButton(
398+
Button(
399399
onClick = { viewModel.saveForScanner() },
400400
modifier = Modifier.fillMaxWidth(),
401401
shape = RoundedCornerShape(10.dp)
@@ -621,38 +621,6 @@ fun EditProfileScreen(
621621
visualTransformation = PasswordVisualTransformation(),
622622
modifier = Modifier.fillMaxWidth()
623623
)
624-
Surface(
625-
onClick = {
626-
val intent = android.content.Intent(
627-
android.content.Intent.ACTION_VIEW,
628-
android.net.Uri.parse("https://github.com/anonvector/slipgate")
629-
)
630-
context.startActivity(intent)
631-
},
632-
shape = MaterialTheme.shapes.small,
633-
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
634-
modifier = Modifier.fillMaxWidth()
635-
) {
636-
Row(
637-
modifier = Modifier
638-
.fillMaxWidth()
639-
.padding(vertical = 12.dp, horizontal = 12.dp),
640-
verticalAlignment = Alignment.CenterVertically
641-
) {
642-
Icon(
643-
Icons.Default.OpenInNew,
644-
contentDescription = null,
645-
modifier = Modifier.size(18.dp),
646-
tint = MaterialTheme.colorScheme.primary
647-
)
648-
Spacer(Modifier.width(8.dp))
649-
Text(
650-
"Server setup guide",
651-
style = MaterialTheme.typography.bodyMedium,
652-
color = MaterialTheme.colorScheme.primary
653-
)
654-
}
655-
}
656624
}
657625

658626
// SSH Port (shown only for SSH-only, near domain)
@@ -753,42 +721,6 @@ fun EditProfileScreen(
753721
}
754722
}
755723

756-
// NoizDNS server setup guide
757-
if (uiState.isNoizdnsBased) {
758-
Surface(
759-
onClick = {
760-
val intent = android.content.Intent(
761-
android.content.Intent.ACTION_VIEW,
762-
android.net.Uri.parse("https://github.com/anonvector/noizdns-deploy")
763-
)
764-
context.startActivity(intent)
765-
},
766-
shape = MaterialTheme.shapes.small,
767-
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
768-
modifier = Modifier.fillMaxWidth()
769-
) {
770-
Row(
771-
modifier = Modifier
772-
.fillMaxWidth()
773-
.padding(vertical = 12.dp, horizontal = 12.dp),
774-
verticalAlignment = Alignment.CenterVertically
775-
) {
776-
Icon(
777-
Icons.Default.OpenInNew,
778-
contentDescription = null,
779-
modifier = Modifier.size(18.dp),
780-
tint = MaterialTheme.colorScheme.primary
781-
)
782-
Spacer(Modifier.width(8.dp))
783-
Text(
784-
"Server setup guide",
785-
style = MaterialTheme.typography.bodyMedium,
786-
color = MaterialTheme.colorScheme.primary
787-
)
788-
}
789-
}
790-
}
791-
792724
// NoizDNS stealth mode toggle
793725
if (uiState.isNoizdnsBased) {
794726
Row(
@@ -876,9 +808,10 @@ fun EditProfileScreen(
876808

877809
// Scan Resolvers button
878810
if (onNavigateToScanner != null) {
879-
OutlinedButton(
811+
Button(
880812
onClick = { viewModel.saveForScanner() },
881-
modifier = Modifier.fillMaxWidth()
813+
modifier = Modifier.fillMaxWidth(),
814+
shape = RoundedCornerShape(10.dp)
882815
) {
883816
Icon(Icons.Default.Search, contentDescription = null)
884817
Spacer(Modifier.width(8.dp))
@@ -1435,6 +1368,60 @@ fun EditProfileScreen(
14351368
}
14361369
}
14371370

1371+
// Server setup guide
1372+
if (uiState.isNoizdnsBased || uiState.isNaiveBased || uiState.isSlipstreamBased) {
1373+
Spacer(modifier = Modifier.height(8.dp))
1374+
HorizontalDivider()
1375+
Spacer(modifier = Modifier.height(12.dp))
1376+
1377+
val guideUrl = when {
1378+
uiState.isNoizdnsBased -> "https://github.com/anonvector/noizdns-deploy"
1379+
uiState.isNaiveBased -> "https://github.com/anonvector/slipgate"
1380+
uiState.isSlipstreamBased -> "https://github.com/anonvector/slipgate"
1381+
else -> null
1382+
}
1383+
val guideLabel = when {
1384+
uiState.isNoizdnsBased -> "NoizDNS Server Setup Guide"
1385+
uiState.isNaiveBased -> "NaiveProxy Server Setup Guide"
1386+
uiState.isSlipstreamBased -> "Slipstream Server Setup Guide"
1387+
else -> null
1388+
}
1389+
if (guideUrl != null && guideLabel != null) {
1390+
Surface(
1391+
onClick = {
1392+
val intent = android.content.Intent(
1393+
android.content.Intent.ACTION_VIEW,
1394+
android.net.Uri.parse(guideUrl)
1395+
)
1396+
context.startActivity(intent)
1397+
},
1398+
shape = RoundedCornerShape(12.dp),
1399+
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f),
1400+
modifier = Modifier.fillMaxWidth()
1401+
) {
1402+
Row(
1403+
modifier = Modifier
1404+
.fillMaxWidth()
1405+
.padding(vertical = 14.dp, horizontal = 16.dp),
1406+
verticalAlignment = Alignment.CenterVertically
1407+
) {
1408+
Icon(
1409+
Icons.Default.OpenInNew,
1410+
contentDescription = null,
1411+
modifier = Modifier.size(20.dp),
1412+
tint = MaterialTheme.colorScheme.primary
1413+
)
1414+
Spacer(Modifier.width(12.dp))
1415+
Text(
1416+
guideLabel,
1417+
style = MaterialTheme.typography.titleSmall,
1418+
color = MaterialTheme.colorScheme.primary
1419+
)
1420+
}
1421+
}
1422+
}
1423+
}
1424+
14381425
Spacer(modifier = Modifier.height(32.dp))
14391426
}
14401427
}

app/src/main/java/app/slipnet/presentation/scanner/DnsScannerViewModel.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -868,12 +868,14 @@ class DnsScannerViewModel @Inject constructor(
868868
// For the default list, always fetch original order so famous resolvers stay at the top.
869869
val resolvers = if (state.shuffleList && state.listSource == ListSource.DEFAULT) {
870870
val defaultResolvers = scannerRepository.getDefaultResolvers()
871-
val priorityCount = scannerRepository.getDefaultResolverPriorityCount()
872-
val secondaryCount = scannerRepository.getDefaultResolverSecondaryCount()
873-
Log.d("DnsScanner", "Shuffle default: priorityCount=$priorityCount, secondaryCount=$secondaryCount, total=${defaultResolvers.size}, first5=${defaultResolvers.take(5)}")
874-
defaultResolvers.take(priorityCount) +
875-
defaultResolvers.drop(priorityCount).take(secondaryCount).shuffled() +
876-
defaultResolvers.drop(priorityCount + secondaryCount).shuffled()
871+
val boundaries = scannerRepository.getDefaultResolverTierBoundaries()
872+
Log.d("DnsScanner", "Shuffle default: tiers=${boundaries.size + 1}, boundaries=$boundaries, total=${defaultResolvers.size}")
873+
// First tier (before first marker) stays in order; all others are shuffled
874+
val indices = listOf(0) + boundaries + listOf(defaultResolvers.size)
875+
indices.zipWithNext().flatMapIndexed { i, (from, to) ->
876+
val tier = defaultResolvers.subList(from, to)
877+
if (i == 0) tier else tier.shuffled()
878+
}
877879
} else if (state.shuffleList) {
878880
state.resolverList.shuffled()
879881
} else {

app/src/main/java/app/slipnet/presentation/scanner/ScanResultsScreen.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import androidx.compose.foundation.lazy.items
3434
import androidx.compose.foundation.shape.CircleShape
3535
import androidx.compose.foundation.shape.RoundedCornerShape
3636
import androidx.compose.material.icons.Icons
37+
import androidx.activity.compose.BackHandler
3738
import androidx.compose.material.icons.automirrored.filled.ArrowBack
3839
import androidx.compose.material.icons.filled.CheckCircle
3940
import androidx.compose.material.icons.filled.ContentCopy
@@ -152,6 +153,11 @@ fun ScanResultsScreen(
152153
var pendingAction by remember { mutableStateOf<String?>(null) }
153154
val scope = rememberCoroutineScope()
154155

156+
BackHandler {
157+
viewModel.stopScan()
158+
onNavigateBack()
159+
}
160+
155161
LaunchedEffect(uiState.error) {
156162
uiState.error?.let { error ->
157163
snackbarHostState.showSnackbar(error)

app/src/main/res/raw/resolvers.txt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,12 @@
3838
76.76.20.0
3939
8.26.56.26
4040
8.20.247.20
41-
109.69.8.51
4241
185.228.168.9
4342
185.228.169.9
4443
185.228.168.168
4544
185.228.169.168
4645
185.228.168.10
4746
185.228.169.11
48-
185.181.182.209
4947
77.88.8.8
5048
77.88.8.1
5149
223.5.5.5
@@ -54,10 +52,17 @@
5452
119.29.29.29
5553
101.101.101.101
5654
101.102.103.104
55+
64.6.64.6
56+
64.6.65.6
57+
84.200.69.80
58+
84.200.70.40
59+
109.69.8.51
5760
194.255.152.10
5861
194.255.62.80
5962
195.46.39.39
6063
195.46.39.40
64+
# SHUFFLE_BELOW
65+
185.181.182.209
6166
178.22.122.100
6267
185.51.200.2
6368
10.202.10.202
@@ -72,10 +77,6 @@
7277
94.103.125.158
7378
185.55.226.26
7479
185.55.225.25
75-
64.6.64.6
76-
64.6.65.6
77-
84.200.69.80
78-
84.200.70.40
7980
163.172.141.219
8081
207.246.121.77
8182
185.49.84.2
@@ -1463,7 +1464,6 @@
14631464
178.131.190.233
14641465
37.148.0.167
14651466
188.121.147.14
1466-
# Iranian Resolvers
14671467
# SHUFFLE_BELOW
14681468
2.144.6.75
14691469
2.144.198.225

0 commit comments

Comments
 (0)