feat(ui): copy button on error states and chat error snackbar (#1420)
This commit is contained in:
6 files changed
+50
-1
No files matched your search
@@ -1,17 +1,23 @@
|
|||||||
package com.m57.hermescontrol.ui.chat.components
|
package com.m57.hermescontrol.ui.chat.components
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.ClipData
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@@ -19,6 +25,7 @@ import androidx.lifecycle.LifecycleEventObserver
|
|||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.m57.hermescontrol.NavigationController
|
import com.m57.hermescontrol.NavigationController
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.data.ws.ConnectionStatus
|
import com.m57.hermescontrol.data.ws.ConnectionStatus
|
||||||
import com.m57.hermescontrol.notification.ReplyNotificationTracker
|
import com.m57.hermescontrol.notification.ReplyNotificationTracker
|
||||||
import com.m57.hermescontrol.notification.correlationScopeId
|
import com.m57.hermescontrol.notification.correlationScopeId
|
||||||
@@ -28,6 +35,7 @@ import com.m57.hermescontrol.ui.chat.ClarifyUi
|
|||||||
import com.m57.hermescontrol.ui.chat.SecretPromptUi
|
import com.m57.hermescontrol.ui.chat.SecretPromptUi
|
||||||
import com.m57.hermescontrol.ui.chat.SudoPromptUi
|
import com.m57.hermescontrol.ui.chat.SudoPromptUi
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatLifecycleEffects(
|
fun ChatLifecycleEffects(
|
||||||
@@ -137,10 +145,24 @@ fun ChatLifecycleEffects(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show error as snackbar
|
// Show error as snackbar
|
||||||
|
val clipboard = LocalClipboard.current
|
||||||
|
val copyLabel = context.getString(R.string.action_copy_error)
|
||||||
|
// Own scope + immediate clear: a later `errorMessage = null` elsewhere must not cancel a visible popup.
|
||||||
|
val snackbarScope = rememberCoroutineScope()
|
||||||
LaunchedEffect(errorMessage) {
|
LaunchedEffect(errorMessage) {
|
||||||
errorMessage?.let { error ->
|
errorMessage?.let { error ->
|
||||||
snackbarHostState.showSnackbar(error)
|
|
||||||
viewModel.clearError()
|
viewModel.clearError()
|
||||||
|
snackbarScope.launch {
|
||||||
|
val result =
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
error,
|
||||||
|
actionLabel = copyLabel,
|
||||||
|
duration = SnackbarDuration.Long,
|
||||||
|
)
|
||||||
|
if (result == SnackbarResult.ActionPerformed) {
|
||||||
|
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(null, error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.m57.hermescontrol.ui.common
|
package com.m57.hermescontrol.ui.common
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.RepeatMode
|
import androidx.compose.animation.core.RepeatMode
|
||||||
import androidx.compose.animation.core.animateFloat
|
import androidx.compose.animation.core.animateFloat
|
||||||
@@ -22,6 +23,7 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
import androidx.compose.material.icons.filled.Inbox
|
import androidx.compose.material.icons.filled.Inbox
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
@@ -30,8 +32,10 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -39,6 +43,8 @@ import androidx.compose.ui.geometry.Offset
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
@@ -48,6 +54,7 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.theme.LocalSpacing
|
import com.m57.hermescontrol.theme.LocalSpacing
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
// Unified loading / error / empty / skeleton placeholders so every screen
|
// Unified loading / error / empty / skeleton placeholders so every screen
|
||||||
// shares the same visual language.
|
// shares the same visual language.
|
||||||
@@ -96,6 +103,8 @@ fun ErrorState(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val spacing = LocalSpacing.current
|
val spacing = LocalSpacing.current
|
||||||
|
val clipboard = LocalClipboard.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
@@ -117,6 +126,20 @@ fun ErrorState(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch { clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(null, message))) }
|
||||||
|
},
|
||||||
|
modifier = Modifier.testTag("error_copy_button"),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ContentCopy,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(spacing.xs))
|
||||||
|
Text(stringResource(R.string.action_copy_error))
|
||||||
|
}
|
||||||
if (onRetry != null) {
|
if (onRetry != null) {
|
||||||
Spacer(modifier = Modifier.height(spacing.md))
|
Spacer(modifier = Modifier.height(spacing.md))
|
||||||
Button(
|
Button(
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<string name="chat_next_match_desc">النتيجة التالية</string>
|
<string name="chat_next_match_desc">النتيجة التالية</string>
|
||||||
<string name="chat_close_search_desc">إغلاق البحث</string>
|
<string name="chat_close_search_desc">إغلاق البحث</string>
|
||||||
<string name="action_retry">إعادة المحاولة</string>
|
<string name="action_retry">إعادة المحاولة</string>
|
||||||
|
<string name="action_copy_error">نسخ الخطأ</string>
|
||||||
<string name="action_cancel">إلغاء</string>
|
<string name="action_cancel">إلغاء</string>
|
||||||
<string name="action_confirm">تأكيد</string>
|
<string name="action_confirm">تأكيد</string>
|
||||||
<string name="action_clear">مسح</string>
|
<string name="action_clear">مسح</string>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<string name="action_retry">재시도</string>
|
<string name="action_retry">재시도</string>
|
||||||
|
<string name="action_copy_error">오류 복사</string>
|
||||||
<string name="action_cancel">취소</string>
|
<string name="action_cancel">취소</string>
|
||||||
<string name="action_clear">지우기</string>
|
<string name="action_clear">지우기</string>
|
||||||
<string name="action_ok">확인</string>
|
<string name="action_ok">확인</string>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<string name="chat_next_match_desc">下一个匹配</string>
|
<string name="chat_next_match_desc">下一个匹配</string>
|
||||||
<string name="chat_close_search_desc">关闭搜索</string>
|
<string name="chat_close_search_desc">关闭搜索</string>
|
||||||
<string name="action_retry">重试</string>
|
<string name="action_retry">重试</string>
|
||||||
|
<string name="action_copy_error">复制错误</string>
|
||||||
<string name="action_cancel">取消</string>
|
<string name="action_cancel">取消</string>
|
||||||
<string name="action_confirm">确认</string>
|
<string name="action_confirm">确认</string>
|
||||||
<string name="action_clear">清除</string>
|
<string name="action_clear">清除</string>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<string name="action_retry">Retry</string>
|
<string name="action_retry">Retry</string>
|
||||||
|
<string name="action_copy_error">Copy error</string>
|
||||||
<string name="action_cancel">Cancel</string>
|
<string name="action_cancel">Cancel</string>
|
||||||
<string name="action_confirm">Confirm</string>
|
<string name="action_confirm">Confirm</string>
|
||||||
<string name="action_clear">Clear</string>
|
<string name="action_clear">Clear</string>
|
||||||
|
|||||||
Reference in new issue
Block a user