summaryrefslogtreecommitdiff
path: root/Source/Android/app
diff options
context:
space:
mode:
authorTom Pratt <tom.pratt@outlook.com>2026-04-30 10:16:20 +0200
committerTom Pratt <tompratt@squareup.com>2026-05-19 12:02:58 +0200
commitacbf9e155f8e0397e6bfa2f3d00bad0e5f5ecbcf (patch)
treec581744bba8bd33e37c9173f4cdd92be41b24823 /Source/Android/app
parent117d1d71acc6b85b6c67396bd2222fbee7f0ff05 (diff)
Game digest progress dialog
We just about get away with using a StateFlow in NetplaySession since the host sends AbortGameDigest when closing their own dialog. Without that it would be harder for the UI to distinguish between subsequent dialogs. If that wasn't the case then NetplaySession might need to expose the individual progress and result updates and have the view model assemble it into the overall GameDigestProgress.
Diffstat (limited to 'Source/Android/app')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt58
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/GameDigestProgress.kt14
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt2
-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.kt97
-rw-r--r--Source/Android/app/src/main/res/values/strings.xml3
6 files changed, 173 insertions, 2 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt
index fbeba1510c..e0531a6477 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt
@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
+import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
import org.dolphinemu.dolphinemu.features.netplay.model.Player
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
@@ -104,6 +105,9 @@ class NetplaySession(
private val _saveTransferProgress = MutableStateFlow<SaveTransferProgress?>(null)
val saveTransferProgress = _saveTransferProgress.asStateFlow()
+ private val _gameDigestProgress = MutableStateFlow<GameDigestProgress?>(null)
+ val gameDigestProgress = _gameDigestProgress.asStateFlow()
+
suspend fun join(): Boolean = withContext(Dispatchers.IO) {
if (isClosed) throw IllegalStateException("Cannot join a closed session")
@@ -282,6 +286,60 @@ class NetplaySession(
fun onHideChunkedProgressDialog() {
_saveTransferProgress.value = null
}
+
+ @Keep
+ fun onShowGameDigestDialog(title: String) {
+ val players = _players.replayCache.firstOrNull()
+ _gameDigestProgress.value = GameDigestProgress(
+ title = title,
+ playerProgresses = players?.map { player ->
+ GameDigestProgress.PlayerProgress(
+ playerId = player.pid,
+ name = player.name,
+ progress = 0,
+ result = null,
+ )
+ } ?: emptyList(),
+ matches = null,
+ )
+ }
+
+ @Keep
+ fun onSetGameDigestProgress(playerId: Int, progress: Int) {
+ val current = _gameDigestProgress.value ?: return
+ _gameDigestProgress.value = current.copy(
+ playerProgresses = current.playerProgresses.map {
+ if (it.playerId == playerId) it.copy(progress = progress) else it
+ }
+ )
+ }
+
+ @Keep
+ fun onSetGameDigestResult(playerId: Int, result: String) {
+ val current = _gameDigestProgress.value ?: return
+ val updated = current.copy(
+ playerProgresses = current.playerProgresses.map {
+ if (it.playerId == playerId) it.copy(result = result) else it
+ }
+ )
+ val finished = updated.playerProgresses.all { it.result != null }
+ _gameDigestProgress.value = if (finished) {
+ val results = updated.playerProgresses.map { it.result }
+ updated.copy(matches = results.distinct().size == 1)
+ } else {
+ updated
+ }
+ }
+
+ /**
+ * Hosts send this when they dismiss their dialog even in a successful scenario. Ensuring
+ * that the value is cleared before a new game digest is started. Without this, StateFlow
+ * would not be a good choice.
+ */
+ @Keep
+ fun onAbortGameDigest() {
+ _gameDigestProgress.value = null
+ }
}
private fun <T> Channel<T>.flush() {
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/GameDigestProgress.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/GameDigestProgress.kt
new file mode 100644
index 0000000000..57a9cae78b
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/GameDigestProgress.kt
@@ -0,0 +1,14 @@
+package org.dolphinemu.dolphinemu.features.netplay.model
+
+data class GameDigestProgress(
+ val title: String,
+ val playerProgresses: List<PlayerProgress>,
+ val matches: Boolean?,
+) {
+ data class PlayerProgress(
+ val playerId: Int,
+ val name: String,
+ val progress: Int,
+ val result: String?,
+ )
+}
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 96f9d7328b..d0f86ed3f6 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
@@ -41,6 +41,8 @@ class NetplayViewModel(
val saveTransferProgress = netplaySession.saveTransferProgress
+ val gameDigestProgress = netplaySession.gameDigestProgress
+
fun sendMessage(message: String) {
val trimmedMessage = message.trim()
if (trimmedMessage.isEmpty()) {
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 f2acf50107..d99c4da57a 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
@@ -56,6 +56,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
maxBuffer = viewModel.maxBuffer.collectAsState().value,
onMaxBufferChanged = viewModel::setMaxBuffer,
saveTransferProgress = viewModel.saveTransferProgress.collectAsState().value,
+ gameDigestProgress = viewModel.gameDigestProgress.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 40fe1fe5ec..71595ddb43 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
@@ -50,6 +50,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -69,6 +70,7 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import org.dolphinemu.dolphinemu.R
+import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
import org.dolphinemu.dolphinemu.features.netplay.model.Player
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
@@ -91,6 +93,7 @@ fun NetplayScreen(
onMaxBufferChanged: (Int) -> Unit,
players: List<Player>,
saveTransferProgress: SaveTransferProgress?,
+ gameDigestProgress: GameDigestProgress?,
) {
Scaffold(
topBar = {
@@ -136,16 +139,21 @@ fun NetplayScreen(
)
}
- var showConnectionLostDialog by remember { mutableStateOf(false) }
+ var showConnectionLostDialog by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
connectionLost.collect { showConnectionLostDialog = true }
}
- var dismissSaveTransferProgressDialog by remember { mutableStateOf(false) }
+ var dismissSaveTransferProgressDialog by rememberSaveable { mutableStateOf(false) }
if (saveTransferProgress == null) {
dismissSaveTransferProgressDialog = false
}
+ var dismissGameDigestDialog by rememberSaveable { mutableStateOf(false) }
+ if (gameDigestProgress == null) {
+ dismissGameDigestDialog = false
+ }
+
when {
showConnectionLostDialog -> {
AlertDialog(
@@ -165,6 +173,13 @@ fun NetplayScreen(
onDismiss = { dismissSaveTransferProgressDialog = true },
)
}
+
+ gameDigestProgress != null && !dismissGameDigestDialog -> {
+ GameDigestProgressDialog(
+ gameDigestProgress = gameDigestProgress,
+ onDismiss = { dismissGameDigestDialog = true },
+ )
+ }
}
}
}
@@ -548,6 +563,7 @@ private fun SaveTransferProgressDialog(
text = {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.verticalScroll(rememberScrollState()),
) {
saveTransferProgress.playerProgresses.forEachIndexed { index, playerProgress ->
SaveTransferProgressRow(
@@ -600,6 +616,82 @@ private fun SaveTransferProgressRow(
}
}
+@Composable
+private fun GameDigestProgressDialog(
+ gameDigestProgress: GameDigestProgress,
+ onDismiss: () -> Unit,
+) {
+ AlertDialog(
+ title = { Text(gameDigestProgress.title) },
+ text = {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.verticalScroll(rememberScrollState()),
+ ) {
+ gameDigestProgress.playerProgresses.forEachIndexed { index, playerProgress ->
+ GameDigestPlayerRow(playerProgress)
+ if (index < gameDigestProgress.playerProgresses.lastIndex) {
+ HorizontalDivider()
+ }
+ }
+ if (gameDigestProgress.matches != null) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = stringResource(
+ if (gameDigestProgress.matches) {
+ R.string.netplay_game_digest_match
+ } else {
+ R.string.netplay_game_digest_mismatch
+ }
+ ),
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Medium,
+ )
+ }
+ }
+ },
+ confirmButton = {
+ if (gameDigestProgress.matches != null) {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.netplay_game_digest_close))
+ }
+ }
+ },
+ onDismissRequest = { onDismiss() },
+ )
+}
+
+@Composable
+private fun GameDigestPlayerRow(
+ playerProgress: GameDigestProgress.PlayerProgress,
+) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ LinearProgressIndicator(
+ progress = { playerProgress.progress / 100f },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ if (playerProgress.result == null) {
+ Text(
+ text = playerProgress.name,
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ text = "${playerProgress.progress}%",
+ )
+ } else {
+ Text(
+ text = "${playerProgress.name}:\u00A0${playerProgress.result}",
+ )
+ }
+ }
+ }
+}
+
@Preview
@Composable
private fun NetplayScreenPreview() {
@@ -671,6 +763,7 @@ private fun PreviewNetplayScreen() {
maxBuffer = 10,
onMaxBufferChanged = {},
saveTransferProgress = null,
+ gameDigestProgress = null,
// saveTransferProgress = SaveTransferProgress(
// title = "Title",
// totalSize = 1024L,
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index 4e9c977faa..ef103432aa 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -1011,4 +1011,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<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>
+ <string name="netplay_game_digest_match">The hashes match</string>
+ <string name="netplay_game_digest_mismatch">The hashes do not match</string>
+ <string name="netplay_game_digest_close">Close</string>
</resources>