feat(android): add 5-minute countdown timer UI for keygen/sign sessions
Displays remaining time during the 5-minute polling timeout: - Shows countdown in CreateWalletScreen (SessionScreen) - Shows countdown in JoinKeygenScreen (JoiningScreen, KeygenProgressScreen) - Shows countdown in CoSignJoinScreen (JoiningScreen, SigningProgressScreen) - Format: mm:ss with Timer icon in tertiary container card - Countdown starts on all_joined event and stops on session start/cancel 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b30017f3a7
commit
3576af0f25
|
|
@ -242,6 +242,7 @@ fun TssPartyApp(
|
||||||
currentRound = currentRound,
|
currentRound = currentRound,
|
||||||
totalRounds = 9,
|
totalRounds = 9,
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
|
countdownSeconds = uiState.countdownSeconds,
|
||||||
onCreateSession = { name, t, n, participantName ->
|
onCreateSession = { name, t, n, participantName ->
|
||||||
viewModel.createKeygenSession(name, t, n, participantName)
|
viewModel.createKeygenSession(name, t, n, participantName)
|
||||||
},
|
},
|
||||||
|
|
@ -292,6 +293,7 @@ fun TssPartyApp(
|
||||||
currentRound = joinKeygenRound,
|
currentRound = joinKeygenRound,
|
||||||
totalRounds = 9,
|
totalRounds = 9,
|
||||||
publicKey = joinKeygenPublicKey,
|
publicKey = joinKeygenPublicKey,
|
||||||
|
countdownSeconds = uiState.countdownSeconds,
|
||||||
onValidateInviteCode = { inviteCode ->
|
onValidateInviteCode = { inviteCode ->
|
||||||
viewModel.validateInviteCode(inviteCode)
|
viewModel.validateInviteCode(inviteCode)
|
||||||
},
|
},
|
||||||
|
|
@ -347,6 +349,7 @@ fun TssPartyApp(
|
||||||
currentRound = coSignRound,
|
currentRound = coSignRound,
|
||||||
totalRounds = 9,
|
totalRounds = 9,
|
||||||
signature = coSignSignature,
|
signature = coSignSignature,
|
||||||
|
countdownSeconds = uiState.countdownSeconds,
|
||||||
onValidateInviteCode = { inviteCode ->
|
onValidateInviteCode = { inviteCode ->
|
||||||
viewModel.validateSignInviteCode(inviteCode)
|
viewModel.validateSignInviteCode(inviteCode)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,10 @@ class TssRepository @Inject constructor(
|
||||||
// Called when 5-minute polling timeout is reached without session starting
|
// Called when 5-minute polling timeout is reached without session starting
|
||||||
private var keygenTimeoutCallback: ((String) -> Unit)? = null
|
private var keygenTimeoutCallback: ((String) -> Unit)? = null
|
||||||
|
|
||||||
|
// Countdown tick callback (set by ViewModel)
|
||||||
|
// Called every second with remaining seconds for UI countdown display
|
||||||
|
private var countdownTickCallback: ((Long) -> Unit)? = null
|
||||||
|
|
||||||
// Repository-level CoroutineScope for background tasks
|
// Repository-level CoroutineScope for background tasks
|
||||||
// Uses SupervisorJob so individual task failures don't cancel other tasks
|
// Uses SupervisorJob so individual task failures don't cancel other tasks
|
||||||
private val repositoryScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val repositoryScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
@ -281,6 +285,15 @@ class TssRepository @Inject constructor(
|
||||||
keygenTimeoutCallback = callback
|
keygenTimeoutCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set countdown tick callback (called by ViewModel)
|
||||||
|
* This callback is invoked every second with the remaining seconds
|
||||||
|
* for displaying countdown in the UI
|
||||||
|
*/
|
||||||
|
fun setCountdownTickCallback(callback: (Long) -> Unit) {
|
||||||
|
countdownTickCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start polling session status as fallback mechanism
|
* Start polling session status as fallback mechanism
|
||||||
* This matches Electron's checkAndTriggerKeygen() polling behavior:
|
* This matches Electron's checkAndTriggerKeygen() polling behavior:
|
||||||
|
|
@ -301,14 +314,28 @@ class TssRepository @Inject constructor(
|
||||||
|
|
||||||
android.util.Log.d("TssRepository", "[POLLING] Starting session status polling for $sessionType session: $sessionId")
|
android.util.Log.d("TssRepository", "[POLLING] Starting session status polling for $sessionType session: $sessionId")
|
||||||
|
|
||||||
|
// Notify UI that countdown has started (5 minutes = 300 seconds)
|
||||||
|
countdownTickCallback?.invoke(MAX_WAIT_MS / 1000)
|
||||||
|
|
||||||
sessionStatusPollingJob = repositoryScope.launch {
|
sessionStatusPollingJob = repositoryScope.launch {
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
var pollCount = 0
|
var pollCount = 0
|
||||||
|
var lastTickSecond = MAX_WAIT_MS / 1000
|
||||||
|
|
||||||
while (isActive && (System.currentTimeMillis() - startTime) < MAX_WAIT_MS) {
|
while (isActive && (System.currentTimeMillis() - startTime) < MAX_WAIT_MS) {
|
||||||
pollCount++
|
pollCount++
|
||||||
android.util.Log.d("TssRepository", "[POLLING] Poll #$pollCount for session: $sessionId")
|
android.util.Log.d("TssRepository", "[POLLING] Poll #$pollCount for session: $sessionId")
|
||||||
|
|
||||||
|
// Calculate and emit remaining seconds for countdown display
|
||||||
|
val elapsedMs = System.currentTimeMillis() - startTime
|
||||||
|
val remainingSeconds = (MAX_WAIT_MS - elapsedMs) / 1000
|
||||||
|
if (remainingSeconds != lastTickSecond) {
|
||||||
|
lastTickSecond = remainingSeconds
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
countdownTickCallback?.invoke(remainingSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val statusResult = getSessionStatus(sessionId)
|
val statusResult = getSessionStatus(sessionId)
|
||||||
|
|
||||||
|
|
@ -320,6 +347,11 @@ class TssRepository @Inject constructor(
|
||||||
if (status.status == "in_progress") {
|
if (status.status == "in_progress") {
|
||||||
android.util.Log.d("TssRepository", "[POLLING] Session is in_progress, triggering $sessionType via callback")
|
android.util.Log.d("TssRepository", "[POLLING] Session is in_progress, triggering $sessionType via callback")
|
||||||
|
|
||||||
|
// Clear countdown when session starts
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
countdownTickCallback?.invoke(-1L) // -1 indicates countdown stopped (success)
|
||||||
|
}
|
||||||
|
|
||||||
// Create synthetic session_started event to trigger keygen/sign
|
// Create synthetic session_started event to trigger keygen/sign
|
||||||
// This matches Electron's behavior of checking status and triggering manually
|
// This matches Electron's behavior of checking status and triggering manually
|
||||||
val eventData = SessionEventData(
|
val eventData = SessionEventData(
|
||||||
|
|
@ -346,12 +378,18 @@ class TssRepository @Inject constructor(
|
||||||
// Check if session failed or was cancelled
|
// Check if session failed or was cancelled
|
||||||
if (status.status == "failed" || status.status == "cancelled") {
|
if (status.status == "failed" || status.status == "cancelled") {
|
||||||
android.util.Log.d("TssRepository", "[POLLING] Session ${status.status}, stopping polling")
|
android.util.Log.d("TssRepository", "[POLLING] Session ${status.status}, stopping polling")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
countdownTickCallback?.invoke(-1L) // Clear countdown
|
||||||
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if session already completed (keygen finished)
|
// Check if session already completed (keygen finished)
|
||||||
if (status.status == "completed") {
|
if (status.status == "completed") {
|
||||||
android.util.Log.d("TssRepository", "[POLLING] Session completed, stopping polling")
|
android.util.Log.d("TssRepository", "[POLLING] Session completed, stopping polling")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
countdownTickCallback?.invoke(-1L) // Clear countdown
|
||||||
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -374,6 +412,7 @@ class TssRepository @Inject constructor(
|
||||||
android.util.Log.e("TssRepository", "[POLLING] Timeout after ${elapsedSeconds}s waiting for $sessionType to start")
|
android.util.Log.e("TssRepository", "[POLLING] Timeout after ${elapsedSeconds}s waiting for $sessionType to start")
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
countdownTickCallback?.invoke(0L) // Countdown reached zero
|
||||||
keygenTimeoutCallback?.invoke("等待 $sessionType 启动超时(5分钟)")
|
keygenTimeoutCallback?.invoke("等待 $sessionType 启动超时(5分钟)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -390,6 +429,8 @@ class TssRepository @Inject constructor(
|
||||||
fun stopSessionStatusPolling() {
|
fun stopSessionStatusPolling() {
|
||||||
sessionStatusPollingJob?.cancel()
|
sessionStatusPollingJob?.cancel()
|
||||||
sessionStatusPollingJob = null
|
sessionStatusPollingJob = null
|
||||||
|
// Clear countdown display when polling is stopped
|
||||||
|
countdownTickCallback?.invoke(-1L)
|
||||||
android.util.Log.d("TssRepository", "[POLLING] Session status polling stopped")
|
android.util.Log.d("TssRepository", "[POLLING] Session status polling stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,16 @@ data class SignSessionInfo(
|
||||||
val currentParticipants: Int
|
val currentParticipants: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format countdown seconds to mm:ss display
|
||||||
|
*/
|
||||||
|
private fun formatCountdown(seconds: Long): String {
|
||||||
|
if (seconds < 0) return ""
|
||||||
|
val minutes = seconds / 60
|
||||||
|
val secs = seconds % 60
|
||||||
|
return "%d:%02d".format(minutes, secs)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CoSign Join screen matching service-party-app/src/renderer/src/pages/Sign.tsx
|
* CoSign Join screen matching service-party-app/src/renderer/src/pages/Sign.tsx
|
||||||
* Flow: input → select_share → joining → signing → completed
|
* Flow: input → select_share → joining → signing → completed
|
||||||
|
|
@ -52,6 +62,7 @@ fun CoSignJoinScreen(
|
||||||
currentRound: Int = 0,
|
currentRound: Int = 0,
|
||||||
totalRounds: Int = 9,
|
totalRounds: Int = 9,
|
||||||
signature: String? = null,
|
signature: String? = null,
|
||||||
|
countdownSeconds: Long = -1L, // 5-minute countdown: -1 = not counting, >0 = remaining seconds
|
||||||
onValidateInviteCode: (inviteCode: String) -> Unit,
|
onValidateInviteCode: (inviteCode: String) -> Unit,
|
||||||
onJoinSign: (inviteCode: String, shareId: Long, password: String) -> Unit,
|
onJoinSign: (inviteCode: String, shareId: Long, password: String) -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
|
|
@ -205,12 +216,16 @@ fun CoSignJoinScreen(
|
||||||
},
|
},
|
||||||
onCancel = resetToInput
|
onCancel = resetToInput
|
||||||
)
|
)
|
||||||
"joining" -> JoiningScreen(onCancel = resetToInput) // Reset to input state, stay on page
|
"joining" -> JoiningScreen(
|
||||||
|
countdownSeconds = countdownSeconds,
|
||||||
|
onCancel = resetToInput
|
||||||
|
) // Reset to input state, stay on page
|
||||||
"signing" -> SigningProgressScreen(
|
"signing" -> SigningProgressScreen(
|
||||||
sessionStatus = sessionStatus,
|
sessionStatus = sessionStatus,
|
||||||
participants = participants,
|
participants = participants,
|
||||||
currentRound = currentRound,
|
currentRound = currentRound,
|
||||||
totalRounds = totalRounds,
|
totalRounds = totalRounds,
|
||||||
|
countdownSeconds = countdownSeconds,
|
||||||
onCancel = resetToInput // Reset to input state, stay on page
|
onCancel = resetToInput // Reset to input state, stay on page
|
||||||
)
|
)
|
||||||
"completed" -> SigningCompletedScreen(
|
"completed" -> SigningCompletedScreen(
|
||||||
|
|
@ -699,6 +714,7 @@ private fun SelectShareScreen(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoiningScreen(
|
private fun JoiningScreen(
|
||||||
|
countdownSeconds: Long = -1L,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -729,6 +745,35 @@ private fun JoiningScreen(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Countdown timer (if counting down)
|
||||||
|
if (countdownSeconds > 0) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Timer,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "等待启动: ${formatCountdown(countdownSeconds)}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
OutlinedButton(onClick = onCancel) {
|
OutlinedButton(onClick = onCancel) {
|
||||||
|
|
@ -745,6 +790,7 @@ private fun SigningProgressScreen(
|
||||||
participants: List<String>,
|
participants: List<String>,
|
||||||
currentRound: Int,
|
currentRound: Int,
|
||||||
totalRounds: Int,
|
totalRounds: Int,
|
||||||
|
countdownSeconds: Long = -1L,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -804,6 +850,46 @@ private fun SigningProgressScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Countdown timer (if counting down - waiting for sign to start)
|
||||||
|
if (countdownSeconds > 0) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Timer,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "等待签名启动",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = formatCountdown(countdownSeconds),
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Participants card
|
// Participants card
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ fun CreateWalletScreen(
|
||||||
currentRound: Int = 0,
|
currentRound: Int = 0,
|
||||||
totalRounds: Int = 9,
|
totalRounds: Int = 9,
|
||||||
publicKey: String? = null,
|
publicKey: String? = null,
|
||||||
|
countdownSeconds: Long = -1L, // 5-minute countdown: -1 = not counting, >0 = remaining seconds
|
||||||
onCreateSession: (walletName: String, thresholdT: Int, thresholdN: Int, participantName: String) -> Unit,
|
onCreateSession: (walletName: String, thresholdT: Int, thresholdN: Int, participantName: String) -> Unit,
|
||||||
onCopyInviteCode: () -> Unit,
|
onCopyInviteCode: () -> Unit,
|
||||||
onEnterSession: () -> Unit,
|
onEnterSession: () -> Unit,
|
||||||
|
|
@ -115,6 +116,7 @@ fun CreateWalletScreen(
|
||||||
totalRounds = totalRounds,
|
totalRounds = totalRounds,
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
inviteCode = inviteCode,
|
inviteCode = inviteCode,
|
||||||
|
countdownSeconds = countdownSeconds,
|
||||||
onCopyInviteCode = onCopyInviteCode,
|
onCopyInviteCode = onCopyInviteCode,
|
||||||
onBackToHome = onBackToHome
|
onBackToHome = onBackToHome
|
||||||
)
|
)
|
||||||
|
|
@ -610,6 +612,16 @@ private fun generateQRCodeBitmap(content: String, size: Int): Bitmap? {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format countdown seconds to mm:ss display
|
||||||
|
*/
|
||||||
|
private fun formatCountdown(seconds: Long): String {
|
||||||
|
if (seconds < 0) return ""
|
||||||
|
val minutes = seconds / 60
|
||||||
|
val secs = seconds % 60
|
||||||
|
return "%d:%02d".format(minutes, secs)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SessionScreen(
|
private fun SessionScreen(
|
||||||
walletName: String,
|
walletName: String,
|
||||||
|
|
@ -622,6 +634,7 @@ private fun SessionScreen(
|
||||||
totalRounds: Int,
|
totalRounds: Int,
|
||||||
publicKey: String?,
|
publicKey: String?,
|
||||||
inviteCode: String?,
|
inviteCode: String?,
|
||||||
|
countdownSeconds: Long = -1L,
|
||||||
onCopyInviteCode: () -> Unit,
|
onCopyInviteCode: () -> Unit,
|
||||||
onBackToHome: () -> Unit
|
onBackToHome: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
@ -763,6 +776,46 @@ private fun SessionScreen(
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Countdown timer (if counting down - waiting for keygen to start after all joined)
|
||||||
|
if (countdownSeconds > 0) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Timer,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "等待密钥生成启动",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = formatCountdown(countdownSeconds),
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
// Progress Bar (if in progress)
|
// Progress Bar (if in progress)
|
||||||
if (sessionStatus == SessionStatus.IN_PROGRESS) {
|
if (sessionStatus == SessionStatus.IN_PROGRESS) {
|
||||||
Card(
|
Card(
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,16 @@ data class JoinSessionInfo(
|
||||||
val totalParticipants: Int
|
val totalParticipants: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format countdown seconds to mm:ss display
|
||||||
|
*/
|
||||||
|
private fun formatCountdown(seconds: Long): String {
|
||||||
|
if (seconds < 0) return ""
|
||||||
|
val minutes = seconds / 60
|
||||||
|
val secs = seconds % 60
|
||||||
|
return "%d:%02d".format(minutes, secs)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JoinKeygen screen matching service-party-app/src/renderer/src/pages/Join.tsx
|
* JoinKeygen screen matching service-party-app/src/renderer/src/pages/Join.tsx
|
||||||
* Simplified flow without password: input → confirm → joining
|
* Simplified flow without password: input → confirm → joining
|
||||||
|
|
@ -47,6 +57,7 @@ fun JoinKeygenScreen(
|
||||||
currentRound: Int = 0,
|
currentRound: Int = 0,
|
||||||
totalRounds: Int = 9,
|
totalRounds: Int = 9,
|
||||||
publicKey: String? = null,
|
publicKey: String? = null,
|
||||||
|
countdownSeconds: Long = -1L, // 5-minute countdown: -1 = not counting, >0 = remaining seconds
|
||||||
onValidateInviteCode: (inviteCode: String) -> Unit,
|
onValidateInviteCode: (inviteCode: String) -> Unit,
|
||||||
onJoinKeygen: (inviteCode: String, password: String) -> Unit,
|
onJoinKeygen: (inviteCode: String, password: String) -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
|
|
@ -143,12 +154,16 @@ fun JoinKeygenScreen(
|
||||||
},
|
},
|
||||||
onCancel = resetToInput // Reset to input state, stay on page
|
onCancel = resetToInput // Reset to input state, stay on page
|
||||||
)
|
)
|
||||||
"joining" -> JoiningScreen(onCancel = resetToInput) // Reset to input state, stay on page
|
"joining" -> JoiningScreen(
|
||||||
|
countdownSeconds = countdownSeconds,
|
||||||
|
onCancel = resetToInput
|
||||||
|
) // Reset to input state, stay on page
|
||||||
"progress" -> KeygenProgressScreen(
|
"progress" -> KeygenProgressScreen(
|
||||||
sessionStatus = sessionStatus,
|
sessionStatus = sessionStatus,
|
||||||
participants = participants,
|
participants = participants,
|
||||||
currentRound = currentRound,
|
currentRound = currentRound,
|
||||||
totalRounds = totalRounds,
|
totalRounds = totalRounds,
|
||||||
|
countdownSeconds = countdownSeconds,
|
||||||
onCancel = resetToInput // Reset to input state, stay on page
|
onCancel = resetToInput // Reset to input state, stay on page
|
||||||
)
|
)
|
||||||
"completed" -> KeygenCompletedScreen(
|
"completed" -> KeygenCompletedScreen(
|
||||||
|
|
@ -512,7 +527,10 @@ private fun ConfirmScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoiningScreen(onCancel: () -> Unit) {
|
private fun JoiningScreen(
|
||||||
|
countdownSeconds: Long = -1L,
|
||||||
|
onCancel: () -> Unit
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -541,6 +559,35 @@ private fun JoiningScreen(onCancel: () -> Unit) {
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Countdown timer (if counting down)
|
||||||
|
if (countdownSeconds > 0) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Timer,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "等待启动: ${formatCountdown(countdownSeconds)}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
// Cancel button
|
// Cancel button
|
||||||
|
|
@ -558,6 +605,7 @@ private fun KeygenProgressScreen(
|
||||||
participants: List<String>,
|
participants: List<String>,
|
||||||
currentRound: Int,
|
currentRound: Int,
|
||||||
totalRounds: Int,
|
totalRounds: Int,
|
||||||
|
countdownSeconds: Long = -1L,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -617,6 +665,46 @@ private fun KeygenProgressScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Countdown timer (if counting down - waiting for keygen to start)
|
||||||
|
if (countdownSeconds > 0) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Timer,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "等待密钥生成启动",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = formatCountdown(countdownSeconds),
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Participants card
|
// Participants card
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,13 @@ class MainViewModel @Inject constructor(
|
||||||
// Setup keygen timeout callback (matching Electron's 5-minute timeout in checkAndTriggerKeygen)
|
// Setup keygen timeout callback (matching Electron's 5-minute timeout in checkAndTriggerKeygen)
|
||||||
repository.setKeygenTimeoutCallback { errorMessage ->
|
repository.setKeygenTimeoutCallback { errorMessage ->
|
||||||
android.util.Log.e("MainViewModel", "Keygen timeout: $errorMessage")
|
android.util.Log.e("MainViewModel", "Keygen timeout: $errorMessage")
|
||||||
_uiState.update { it.copy(isLoading = false, error = errorMessage) }
|
_uiState.update { it.copy(isLoading = false, error = errorMessage, countdownSeconds = -1L) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup countdown tick callback for UI countdown display
|
||||||
|
repository.setCountdownTickCallback { remainingSeconds ->
|
||||||
|
android.util.Log.d("MainViewModel", "Countdown tick: $remainingSeconds seconds remaining")
|
||||||
|
_uiState.update { it.copy(countdownSeconds = remainingSeconds) }
|
||||||
}
|
}
|
||||||
|
|
||||||
repository.setSessionEventCallback { event ->
|
repository.setSessionEventCallback { event ->
|
||||||
|
|
@ -1217,7 +1223,10 @@ data class MainUiState(
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
val successMessage: String? = null,
|
val successMessage: String? = null,
|
||||||
val lastCreatedAddress: String? = null,
|
val lastCreatedAddress: String? = null,
|
||||||
val lastSignature: String? = null
|
val lastSignature: String? = null,
|
||||||
|
// 5-minute countdown for keygen/sign startup timeout
|
||||||
|
// -1 = not counting, 0 = timeout, >0 = remaining seconds
|
||||||
|
val countdownSeconds: Long = -1L
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue