Skip to content

Commit 9f570ad

Browse files
committed
feat: 更精准的进度显示
1 parent 2f7f632 commit 9f570ad

3 files changed

Lines changed: 174 additions & 18 deletions

File tree

app/src/main/java/com/neoruaa/xhsdn/FileDownloader.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -200,17 +200,20 @@ private File saveToMediaStore(String fileName, ResponseBody body, String fileExt
200200
long totalBytesRead = 0;
201201
long contentLength = body.contentLength();
202202

203+
// Track the last progress update to enable more frequent updates
204+
long lastProgressUpdate = 0;
203205
InputStream inputStream = body.byteStream();
204206
while ((bytesRead = inputStream.read(buffer)) != -1) {
205207
outputStream.write(buffer, 0, bytesRead);
206208
totalBytesRead += bytesRead;
207-
208-
// Report progress updates less frequently to avoid UI thread contention
209+
210+
// Report progress updates more frequently for better user experience
209211
if (callback != null && contentLength > 0) {
210212
// Only report progress if we have a content length and it's not 0
211-
// Limit progress updates to once per 256KB to avoid excessive callbacks
212-
if (totalBytesRead % 262144 == 0 || totalBytesRead == contentLength) { // 256KB = 262144 bytes
213+
// Update progress every 64KB or when download completes to balance responsiveness and performance
214+
if (totalBytesRead - lastProgressUpdate >= 65536 || totalBytesRead == contentLength) { // 64KB = 65536 bytes
213215
callback.onDownloadProgressUpdate(totalBytesRead, contentLength);
216+
lastProgressUpdate = totalBytesRead;
214217
}
215218
}
216219
}
@@ -310,16 +313,19 @@ private File saveToFileSystem(String url, String fileName, ResponseBody body) th
310313
long totalBytesRead = 0;
311314
long contentLength = body.contentLength();
312315

316+
// Track the last progress update to enable more frequent updates
317+
long lastProgressUpdate = 0;
313318
while ((bytesRead = inputStream.read(buffer)) != -1) {
314319
outputStream.write(buffer, 0, bytesRead);
315320
totalBytesRead += bytesRead;
316-
317-
// Report progress updates less frequently to avoid UI thread contention
321+
322+
// Report progress updates more frequently for better user experience
318323
if (callback != null && contentLength > 0) {
319324
// Only report progress if we have a content length and it's not 0
320-
// Limit progress updates to once per 256KB to avoid excessive callbacks
321-
if (totalBytesRead % 262144 == 0 || totalBytesRead == contentLength) { // 256KB = 262144 bytes
325+
// Update progress every 64KB or when download completes to balance responsiveness and performance
326+
if (totalBytesRead - lastProgressUpdate >= 65536 || totalBytesRead == contentLength) { // 64KB = 65536 bytes
322327
callback.onDownloadProgressUpdate(totalBytesRead, contentLength);
328+
lastProgressUpdate = totalBytesRead;
323329
}
324330
}
325331
}

app/src/main/java/com/neoruaa/xhsdn/MainActivity.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -493,10 +493,16 @@ private fun LogPage(
493493
) {
494494
Row(
495495
modifier = Modifier.fillMaxWidth(),
496-
horizontalArrangement = Arrangement.SpaceBetween
496+
horizontalArrangement = Arrangement.SpaceBetween,
497+
verticalAlignment = Alignment.CenterVertically
497498
) {
498-
Text(text = "进度")
499-
Text(text = uiState.progressLabel.ifEmpty { "--" }, color = Color.Gray)
499+
Row(
500+
verticalAlignment = Alignment.CenterVertically
501+
) {
502+
Text(text = "进度 ")
503+
Text(text = uiState.progressLabel.ifEmpty { "--" }, color = Color.Gray)
504+
}
505+
Text(text = uiState.downloadProgressText, color = Color.Gray)
500506
}
501507
LinearProgressIndicator(progress = uiState.progress)
502508
}

app/src/main/java/com/neoruaa/xhsdn/MainViewModel.kt

Lines changed: 151 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import kotlinx.coroutines.flow.StateFlow
1212
import kotlinx.coroutines.flow.update
1313
import kotlinx.coroutines.launch
1414
import kotlinx.coroutines.withContext
15-
import java.io.File
1615
import java.util.Locale
1716

1817
data class MediaItem(val path: String, val type: MediaType)
@@ -28,6 +27,7 @@ data class MainUiState(
2827
val isDownloading: Boolean = false,
2928
val progressLabel: String = "",
3029
val progress: Float = 0f,
30+
val downloadProgressText: String = "0%|0kb/s", // Format: "XX%|XXXkb/s"
3131
val showWebCrawl: Boolean = false,
3232
val showVideoWarning: Boolean = false
3333
)
@@ -43,6 +43,48 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
4343
private var hasUserContinuedAfterVideoWarning = false
4444
private var downloadJob: kotlinx.coroutines.Job? = null
4545

46+
// Track individual file progress for more accurate overall progress
47+
private val fileProgressMap = mutableMapOf<String, Float>() // Maps file path to progress (0.0 to 1.0)
48+
private var currentFileProgress = 0f // Progress of the currently downloading file (0.0 to 1.0)
49+
50+
// Fields to track download progress and speed for the first callback
51+
private var currentDownloadStartTime: Long = 0
52+
private var currentDownloadStartBytes: Long = 0
53+
private var currentDownloadTotalBytes: Long = 0
54+
private var currentDownloadedBytes: Long = 0
55+
private var lastSpeedCalculationTime: Long = 0
56+
private var lastCalculatedSpeed = "0kb/s"
57+
58+
private fun formatSpeed(bytesPerSecond: Double): String {
59+
return when {
60+
bytesPerSecond >= 1024 * 1024 -> { // >= 1 MB/s
61+
val mbps = bytesPerSecond / (1024 * 1024)
62+
"${String.format("%.2f", mbps)}MB/s"
63+
}
64+
bytesPerSecond >= 1024 -> { // >= 1 KB/s
65+
val kbps = bytesPerSecond / 1024
66+
"${String.format("%.1f", kbps)}KB/s"
67+
}
68+
else -> {
69+
"${bytesPerSecond.toInt()}B/s"
70+
}
71+
}
72+
}
73+
74+
private fun resetDownloadTracking() {
75+
currentDownloadStartTime = System.currentTimeMillis()
76+
currentDownloadStartBytes = 0
77+
currentDownloadTotalBytes = 0
78+
currentDownloadedBytes = 0
79+
lastSpeedCalculationTime = 0
80+
lastCalculatedSpeed = "0KB/s"
81+
82+
// Update UI to show initial state
83+
_uiState.update { currentState ->
84+
currentState.copy(downloadProgressText = "0.0%|0KB/s")
85+
}
86+
}
87+
4688
fun updateUrl(value: String) {
4789
_uiState.update { it.copy(urlInput = value, showWebCrawl = false) }
4890
}
@@ -59,6 +101,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
59101
downloadedCount = 0
60102
totalMediaCount = 0
61103
displayedFiles.clear()
104+
105+
// Reset download tracking variables
106+
resetDownloadTracking()
107+
62108
_uiState.update {
63109
it.copy(
64110
isDownloading = true,
@@ -83,6 +129,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
83129
if (displayedFiles.add(filePath)) {
84130
addMedia(filePath)
85131
downloadedCount++
132+
currentFileProgress = 0f // Reset current file progress when file is completed
86133
updateProgress()
87134
}
88135
}
@@ -93,7 +140,48 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
93140
}
94141

95142
override fun onDownloadProgressUpdate(downloaded: Long, total: Long) {
96-
// 保留单个文件进度供后续扩展
143+
// Calculate progress percentage - always update this
144+
val progressPercent = if (total > 0) {
145+
(downloaded.toDouble() / total.toDouble() * 100).toFloat()
146+
} else 0f
147+
148+
// Calculate individual file progress (0.0 to 1.0)
149+
currentFileProgress = if (total > 0) {
150+
downloaded.toFloat() / total.toFloat()
151+
} else {
152+
0f
153+
}
154+
155+
// Calculate download speed more responsively
156+
val currentTime = System.currentTimeMillis()
157+
val deltaTime = currentTime - lastSpeedCalculationTime
158+
159+
// Update speed calculation more frequently for better responsiveness
160+
if (deltaTime > 500) { // Update every 0.5 seconds instead of 1 second
161+
val deltaBytes = downloaded - currentDownloadedBytes
162+
val deltaTimeSec = deltaTime.toDouble() / 1000.0 // Convert to seconds
163+
164+
val speedBps = if (deltaTimeSec > 0) {
165+
deltaBytes.toDouble() / deltaTimeSec
166+
} else 0.0
167+
168+
// Format speed with appropriate units (KB/s or MB/s)
169+
lastCalculatedSpeed = formatSpeed(speedBps)
170+
lastSpeedCalculationTime = currentTime
171+
}
172+
173+
// Update the current download stats
174+
currentDownloadedBytes = downloaded
175+
currentDownloadTotalBytes = total
176+
177+
// Update overall progress
178+
updateProgress()
179+
180+
// Always update UI state with current progress and speed
181+
val progressText = "${String.format("%.1f", progressPercent)}%|$lastCalculatedSpeed"
182+
_uiState.update { currentState ->
183+
currentState.copy(downloadProgressText = progressText)
184+
}
97185
}
98186

99187
override fun onDownloadError(status: String, originalUrl: String) {
@@ -140,6 +228,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
140228
withContext(Dispatchers.Main) {
141229
val jobStillActive = downloadJob?.isActive == true
142230
if (jobStillActive) {
231+
// Reset download tracking when download completes
232+
resetDownloadTracking()
143233
_uiState.update { it.copy(isDownloading = false) }
144234
if (!success) {
145235
appendStatus("下载失败,请检查链接或网络")
@@ -182,6 +272,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
182272
}
183273
downloadedCount = 0
184274
totalMediaCount = urls.size
275+
276+
// Reset download tracking variables for web crawl
277+
resetDownloadTracking()
278+
185279
_uiState.update {
186280
it.copy(
187281
isDownloading = true,
@@ -206,6 +300,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
206300
if (displayedFiles.add(filePath)) {
207301
addMedia(filePath)
208302
downloadedCount++
303+
currentFileProgress = 0f // Reset current file progress when file is completed
209304
updateProgress()
210305
}
211306
}
@@ -215,7 +310,50 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
215310
appendStatus(status)
216311
}
217312

218-
override fun onDownloadProgressUpdate(downloaded: Long, total: Long) {}
313+
override fun onDownloadProgressUpdate(downloaded: Long, total: Long) {
314+
// Calculate progress percentage - always update this
315+
val progressPercent = if (total > 0) {
316+
(downloaded.toDouble() / total.toDouble() * 100).toFloat()
317+
} else 0f
318+
319+
// Calculate individual file progress (0.0 to 1.0)
320+
currentFileProgress = if (total > 0) {
321+
downloaded.toFloat() / total.toFloat()
322+
} else {
323+
0f
324+
}
325+
326+
// Calculate download speed more responsively
327+
val currentTime = System.currentTimeMillis()
328+
val deltaTime = currentTime - lastSpeedCalculationTime
329+
330+
// Update speed calculation more frequently for better responsiveness
331+
if (deltaTime > 500) { // Update every 0.5 seconds instead of 1 second
332+
val deltaBytes = downloaded - currentDownloadedBytes
333+
val deltaTimeSec = deltaTime.toDouble() / 1000.0 // Convert to seconds
334+
335+
val speedBps = if (deltaTimeSec > 0) {
336+
deltaBytes.toDouble() / deltaTimeSec
337+
} else 0.0
338+
339+
// Format speed with appropriate units (KB/s or MB/s)
340+
lastCalculatedSpeed = formatSpeed(speedBps)
341+
lastSpeedCalculationTime = currentTime
342+
}
343+
344+
// Update the current download stats
345+
currentDownloadedBytes = downloaded
346+
currentDownloadTotalBytes = total
347+
348+
// Update overall progress
349+
updateProgress()
350+
351+
// Always update UI state with current progress and speed
352+
val progressText = "${String.format("%.1f", progressPercent)}%|$lastCalculatedSpeed"
353+
_uiState.update { currentState ->
354+
currentState.copy(downloadProgressText = progressText)
355+
}
356+
}
219357

220358
override fun onDownloadError(status: String, originalUrl: String) {
221359
appendStatus("错误:$status ($originalUrl)")
@@ -237,7 +375,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
237375
}
238376
// Reset the stop flag for new download
239377
downloader.resetStopDownload()
240-
val postId = currentUrl?.let { downloader.extractPostId(it) } ?: "webview"
378+
// Extract all valid XHS URLs from the input
379+
val url: List<String>? = downloader.extractLinks(currentUrl)
380+
val postIdTemp: String = url?.let { downloader.extractPostId(it.firstOrNull()) } ?: currentDownloadStartTime.toString()
381+
val postId = "webview_$postIdTemp"
241382
urls.forEachIndexed { index, rawUrl ->
242383
val transformed = downloader.transformXhsCdnUrl(rawUrl).takeUnless { it.isNullOrEmpty() } ?: rawUrl
243384
val extension = determineFileExtension(transformed)
@@ -247,6 +388,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
247388
withContext(Dispatchers.Main) {
248389
updateProgress()
249390
appendStatus("网页转存完成")
391+
// Reset download tracking when download completes
392+
resetDownloadTracking()
250393
_uiState.update { it.copy(showWebCrawl = false, isDownloading = false) }
251394
// Reset the flag after download completes (whether successful or not)
252395
hasUserContinuedAfterVideoWarning = false
@@ -350,12 +493,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
350493
} else {
351494
"$downloadedCount/?"
352495
}
353-
val fraction = if (totalMediaCount > 0) {
354-
downloadedCount.toFloat() / totalMediaCount
496+
val overallProgress = if (totalMediaCount > 0) {
497+
// Calculate progress as (completed files + current file progress) / total files
498+
(downloadedCount + currentFileProgress) / totalMediaCount.toFloat()
355499
} else {
356500
0f
357501
}
358-
_uiState.update { it.copy(progressLabel = label, progress = fraction) }
502+
_uiState.update { it.copy(progressLabel = label, progress = overallProgress) }
359503
}
360504

361505
private fun appendStatus(message: String) {

0 commit comments

Comments
 (0)