summaryrefslogtreecommitdiff
path: root/Source/Android/app/src/main
diff options
context:
space:
mode:
authorTom Pratt <tom.pratt@outlook.com>2026-04-25 17:39:41 +0200
committerTom Pratt <tompratt@squareup.com>2026-05-19 12:02:57 +0200
commit8141a746713b87be595c94fe4598eb198395b8da (patch)
tree8cb64d65b671ffc3fedeed0e03491066f62442c4 /Source/Android/app/src/main
parent480968f05e03df0c860e84ac294593db91d74296 (diff)
Show save transfer progress
When transferring saves from the host. Equivalent of ChunkedProgressDialog in QT.
Diffstat (limited to 'Source/Android/app/src/main')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt45
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt4
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt13
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt1
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt118
-rw-r--r--Source/Android/app/src/main/res/values/strings.xml1
6 files changed, 170 insertions, 12 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt
index f4eb178cb4..882016dfa9 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt
@@ -13,7 +13,9 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -26,6 +28,7 @@ import kotlinx.coroutines.withContext
import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
import org.dolphinemu.dolphinemu.features.netplay.model.Player
+import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
object Netplay {
@Keep
@@ -87,6 +90,9 @@ object Netplay {
)
val padBuffer = _padBuffer.asSharedFlow()
+ private val _saveTransferProgress = MutableStateFlow<SaveTransferProgress?>(null)
+ val saveTransferProgress = _saveTransferProgress.asStateFlow()
+
suspend fun join(): Boolean = withContext(Dispatchers.IO) {
val scope = createSessionScope()
@@ -140,6 +146,7 @@ object Netplay {
_game.resetReplayCache()
_hostInputAuthorityEnabled.resetReplayCache()
_padBuffer.resetReplayCache()
+ _saveTransferProgress.value = null
}
private fun createSessionScope(): CoroutineScope {
@@ -234,6 +241,44 @@ object Netplay {
_padBuffer.tryEmit(buffer)
}
+ @Keep
+ @JvmStatic
+ fun onShowChunkedProgressDialog(title: String, dataSize: Long, playerIds: IntArray) {
+ val players = _players.replayCache.firstOrNull()
+ _saveTransferProgress.value = SaveTransferProgress(
+ title = title,
+ totalSize = dataSize,
+ playerProgresses = playerIds.map { playerId ->
+ SaveTransferProgress.PlayerProgress(
+ playerId = playerId,
+ name = players?.find { it.pid == playerId }?.name ?: "Invalid Player ID",
+ progress = 0,
+ )
+ },
+ )
+ }
+
+ @Keep
+ @JvmStatic
+ fun onSetChunkedProgress(playerId: Int, progress: Long) {
+ val current = _saveTransferProgress.value
+ _saveTransferProgress.value = current?.copy(
+ playerProgresses = current.playerProgresses.map {
+ if (it.playerId == playerId) {
+ it.copy(progress = progress)
+ } else {
+ it
+ }
+ }
+ )
+ }
+
+ @Keep
+ @JvmStatic
+ fun onHideChunkedProgressDialog() {
+ _saveTransferProgress.value = null
+ }
+
// Settings
object Settings {
@JvmStatic
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
index 955fb876dd..620a66ff5a 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
@@ -2,8 +2,6 @@
package org.dolphinemu.dolphinemu.features.netplay.model
-import android.app.Application
-import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -42,6 +40,8 @@ class NetplayViewModel : ViewModel() {
private val _maxBuffer = MutableStateFlow(Netplay.Settings.getClientBufferSize())
val maxBuffer = _maxBuffer.asStateFlow()
+ val saveTransferProgress = Netplay.saveTransferProgress
+
init {
if (!Netplay.isClientConnected()) {
_goBack.trySend(Unit)
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt
new file mode 100644
index 0000000000..e16cbab8ca
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/SaveTransferProgress.kt
@@ -0,0 +1,13 @@
+package org.dolphinemu.dolphinemu.features.netplay.model
+
+data class SaveTransferProgress(
+ val title: String,
+ val totalSize: Long,
+ val playerProgresses: List<PlayerProgress>
+) {
+ data class PlayerProgress(
+ val playerId: Int,
+ val name: String,
+ val progress: Long,
+ )
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
index 463ad31506..8a8f860825 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
@@ -52,6 +52,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value,
maxBuffer = viewModel.maxBuffer.collectAsState().value,
onMaxBufferChanged = viewModel::setMaxBuffer,
+ saveTransferProgress = viewModel.saveTransferProgress.collectAsState().value,
)
}
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
index 80914e952c..40fe1fe5ec 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
@@ -34,6 +34,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
@@ -70,10 +71,12 @@ import kotlinx.coroutines.flow.emptyFlow
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
import org.dolphinemu.dolphinemu.features.netplay.model.Player
+import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer
import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox
import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme
+import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -87,6 +90,7 @@ fun NetplayScreen(
maxBuffer: Int,
onMaxBufferChanged: (Int) -> Unit,
players: List<Player>,
+ saveTransferProgress: SaveTransferProgress?,
) {
Scaffold(
topBar = {
@@ -136,16 +140,31 @@ fun NetplayScreen(
LaunchedEffect(Unit) {
connectionLost.collect { showConnectionLostDialog = true }
}
- if (showConnectionLostDialog) {
- AlertDialog(
- text = { Text(stringResource(R.string.netplay_connection_lost)) },
- confirmButton = {
- TextButton(onClick = onBackClicked) {
- Text(stringResource(R.string.ok))
- }
- },
- onDismissRequest = onBackClicked,
- )
+
+ var dismissSaveTransferProgressDialog by remember { mutableStateOf(false) }
+ if (saveTransferProgress == null) {
+ dismissSaveTransferProgressDialog = false
+ }
+
+ when {
+ showConnectionLostDialog -> {
+ AlertDialog(
+ text = { Text(stringResource(R.string.netplay_connection_lost)) },
+ confirmButton = {
+ TextButton(onClick = onBackClicked) {
+ Text(stringResource(R.string.ok))
+ }
+ },
+ onDismissRequest = onBackClicked,
+ )
+ }
+
+ saveTransferProgress != null && !dismissSaveTransferProgressDialog -> {
+ SaveTransferProgressDialog(
+ saveTransferProgress = saveTransferProgress,
+ onDismiss = { dismissSaveTransferProgressDialog = true },
+ )
+ }
}
}
}
@@ -519,6 +538,68 @@ private fun BufferInput(
}
}
+@Composable
+private fun SaveTransferProgressDialog(
+ saveTransferProgress: SaveTransferProgress,
+ onDismiss: () -> Unit,
+) {
+ AlertDialog(
+ title = { Text(saveTransferProgress.title) },
+ text = {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ saveTransferProgress.playerProgresses.forEachIndexed { index, playerProgress ->
+ SaveTransferProgressRow(
+ playerProgress = playerProgress,
+ totalSize = saveTransferProgress.totalSize,
+ )
+
+ if (index < saveTransferProgress.playerProgresses.lastIndex) {
+ HorizontalDivider()
+ }
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.netplay_save_transfer_progress_close))
+ }
+ },
+ onDismissRequest = onDismiss,
+ )
+}
+
+@Composable
+private fun SaveTransferProgressRow(
+ playerProgress: SaveTransferProgress.PlayerProgress,
+ totalSize: Long,
+) {
+ fun formatMib(bytes: Long) = String.format(Locale.US, "%.2f", bytes / 1024f / 1024f)
+ val progressFraction = (playerProgress.progress.toFloat() / totalSize).coerceIn(0f, 1f)
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ LinearProgressIndicator(
+ progress = { progressFraction },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text(
+ text = playerProgress.name,
+ modifier = Modifier.weight(1f),
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(
+ text = "${formatMib(playerProgress.progress)}/${formatMib(totalSize)} MiB",
+ )
+ }
+ }
+}
+
@Preview
@Composable
private fun NetplayScreenPreview() {
@@ -589,5 +670,22 @@ private fun PreviewNetplayScreen() {
hostInputAuthorityEnabled = true,
maxBuffer = 10,
onMaxBufferChanged = {},
+ saveTransferProgress = null,
+// saveTransferProgress = SaveTransferProgress(
+// title = "Title",
+// totalSize = 1024L,
+// playerProgresses = listOf(
+// SaveTransferProgress.PlayerProgress(
+// playerId = 1,
+// name = "Player 1",
+// progress = 256,
+// ),
+// SaveTransferProgress.PlayerProgress(
+// playerId = 2,
+// name = "Player 2",
+// progress = 512,
+// ),
+// ),
+// ),
)
}
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index 7b4311c996..cc1866d473 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -1009,4 +1009,5 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="netplay_players_mapping">Mapping</string>
<string name="netplay_max_buffer">Max buffer</string>
<string name="netplay_connection_lost">Netplay connection lost</string>
+ <string name="netplay_save_transfer_progress_close">Close</string>
</resources>