mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Alias strings R to Res (#3619)
This commit is contained in:
parent
a687328f08
commit
0833a6767e
153 changed files with 1403 additions and 1350 deletions
|
|
@ -100,13 +100,13 @@ import org.meshtastic.core.database.model.Message
|
|||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.getChannel
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.NodeKeyStatusIcon
|
||||
import org.meshtastic.core.ui.component.SecurityIcon
|
||||
import org.meshtastic.core.ui.component.SharedContactDialog
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.proto.AppOnlyProtos
|
||||
import java.nio.charset.StandardCharsets
|
||||
import org.meshtastic.core.strings.R as Res
|
||||
|
||||
private const val MESSAGE_CHARACTER_LIMIT_BYTES = 200
|
||||
private const val SNIPPET_CHARACTER_LIMIT = 50
|
||||
|
|
@ -160,7 +160,7 @@ fun MessageScreen(
|
|||
Triple(index, id, name)
|
||||
}
|
||||
val (channelIndex, nodeId, rawChannelName) = channelInfo
|
||||
val unknownChannelText = stringResource(R.string.unknown_channel)
|
||||
val unknownChannelText = stringResource(Res.string.unknown_channel)
|
||||
val channelName = rawChannelName ?: unknownChannelText
|
||||
|
||||
val title =
|
||||
|
|
@ -350,7 +350,10 @@ private fun BoxScope.ScrollToBottomFab(coroutineScope: CoroutineScope, listState
|
|||
}
|
||||
},
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.ArrowDownward, contentDescription = stringResource(R.string.scroll_to_bottom))
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDownward,
|
||||
contentDescription = stringResource(Res.string.scroll_to_bottom),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -367,7 +370,7 @@ private fun ReplySnippet(originalMessage: Message?, onClearReply: () -> Unit, ou
|
|||
originalMessage?.let { message ->
|
||||
val isFromLocalUser = message.node.user.id == DataPacket.ID_LOCAL
|
||||
val replyingToNodeUser = if (isFromLocalUser) ourNode?.user else message.node.user
|
||||
val unknownUserText = stringResource(R.string.unknown)
|
||||
val unknownUserText = stringResource(Res.string.unknown)
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
|
|
@ -380,11 +383,11 @@ private fun ReplySnippet(originalMessage: Message?, onClearReply: () -> Unit, ou
|
|||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.Reply,
|
||||
contentDescription = stringResource(R.string.reply), // Decorative
|
||||
contentDescription = stringResource(Res.string.reply), // Decorative
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.replying_to, replyingToNodeUser?.shortName ?: unknownUserText),
|
||||
text = stringResource(Res.string.replying_to, replyingToNodeUser?.shortName ?: unknownUserText),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
Text(
|
||||
|
|
@ -397,7 +400,7 @@ private fun ReplySnippet(originalMessage: Message?, onClearReply: () -> Unit, ou
|
|||
IconButton(onClick = onClearReply) {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
contentDescription = stringResource(R.string.cancel_reply), // Specific action
|
||||
contentDescription = stringResource(Res.string.cancel_reply), // Specific action
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -489,15 +492,15 @@ private fun String.limitBytes(maxBytes: Int): String {
|
|||
*/
|
||||
@Composable
|
||||
private fun DeleteMessageDialog(count: Int, onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||
val deleteMessagesString = pluralStringResource(R.plurals.delete_messages, count, count)
|
||||
val deleteMessagesString = pluralStringResource(Res.plurals.delete_messages, count, count)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
title = { Text(stringResource(R.string.delete_messages_title)) },
|
||||
title = { Text(stringResource(Res.string.delete_messages_title)) },
|
||||
text = { Text(text = deleteMessagesString) },
|
||||
confirmButton = { TextButton(onClick = onConfirm) { Text(stringResource(R.string.delete)) } },
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } },
|
||||
confirmButton = { TextButton(onClick = onConfirm) { Text(stringResource(Res.string.delete)) } },
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(Res.string.cancel)) } },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -526,19 +529,19 @@ private fun ActionModeTopBar(selectedCount: Int, onAction: (MessageMenuAction) -
|
|||
IconButton(onClick = { onAction(MessageMenuAction.Dismiss) }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.clear_selection),
|
||||
contentDescription = stringResource(Res.string.clear_selection),
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { onAction(MessageMenuAction.ClipboardCopy) }) {
|
||||
Icon(imageVector = Icons.Default.ContentCopy, contentDescription = stringResource(R.string.copy))
|
||||
Icon(imageVector = Icons.Default.ContentCopy, contentDescription = stringResource(Res.string.copy))
|
||||
}
|
||||
IconButton(onClick = { onAction(MessageMenuAction.Delete) }) {
|
||||
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(R.string.delete))
|
||||
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(Res.string.delete))
|
||||
}
|
||||
IconButton(onClick = { onAction(MessageMenuAction.SelectAll) }) {
|
||||
Icon(imageVector = Icons.Default.SelectAll, contentDescription = stringResource(R.string.select_all))
|
||||
Icon(imageVector = Icons.Default.SelectAll, contentDescription = stringResource(Res.string.select_all))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -580,7 +583,7 @@ private fun MessageTopBar(
|
|||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.navigate_back),
|
||||
contentDescription = stringResource(Res.string.navigate_back),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -609,7 +612,7 @@ private fun MessageTopBarActions(
|
|||
var expanded by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
IconButton(onClick = { expanded = true }, enabled = true) {
|
||||
Icon(imageVector = Icons.Default.MoreVert, contentDescription = stringResource(R.string.overflow_menu))
|
||||
Icon(imageVector = Icons.Default.MoreVert, contentDescription = stringResource(Res.string.overflow_menu))
|
||||
}
|
||||
OverFlowMenu(
|
||||
expanded = expanded,
|
||||
|
|
@ -633,9 +636,9 @@ private fun OverFlowMenu(
|
|||
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
|
||||
val quickChatToggleTitle =
|
||||
if (showQuickChat) {
|
||||
stringResource(R.string.quick_chat_hide)
|
||||
stringResource(Res.string.quick_chat_hide)
|
||||
} else {
|
||||
stringResource(R.string.quick_chat_show)
|
||||
stringResource(Res.string.quick_chat_show)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(quickChatToggleTitle) },
|
||||
|
|
@ -656,7 +659,7 @@ private fun OverFlowMenu(
|
|||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.quick_chat)) },
|
||||
text = { Text(stringResource(Res.string.quick_chat)) },
|
||||
onClick = {
|
||||
onDismiss()
|
||||
onNavigateToQuickChatOptions()
|
||||
|
|
@ -664,7 +667,7 @@ private fun OverFlowMenu(
|
|||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChatBubbleOutline,
|
||||
contentDescription = stringResource(R.string.quick_chat),
|
||||
contentDescription = stringResource(Res.string.quick_chat),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -686,7 +689,7 @@ private fun QuickChatRow(
|
|||
actions: List<QuickChatAction>,
|
||||
onClick: (QuickChatAction) -> Unit,
|
||||
) {
|
||||
val alertActionMessage = stringResource(R.string.alert_bell_text)
|
||||
val alertActionMessage = stringResource(Res.string.alert_bell_text)
|
||||
val alertAction =
|
||||
remember(alertActionMessage) {
|
||||
// Memoize if content is static
|
||||
|
|
@ -741,11 +744,11 @@ private fun MessageInput(
|
|||
modifier = modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
state = textFieldState,
|
||||
lineLimits = TextFieldLineLimits.MultiLine(1, MAX_LINES),
|
||||
label = { Text(stringResource(R.string.message_input_label)) },
|
||||
label = { Text(stringResource(Res.string.message_input_label)) },
|
||||
enabled = isEnabled,
|
||||
shape = RoundedCornerShape(ROUNDED_CORNER_PERCENT.toFloat()),
|
||||
isError = isOverLimit,
|
||||
placeholder = { Text(stringResource(R.string.type_a_message)) },
|
||||
placeholder = { Text(stringResource(Res.string.type_a_message)) },
|
||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
|
||||
supportingText = {
|
||||
if (isEnabled) { // Only show supporting text if input is enabled
|
||||
|
|
@ -769,7 +772,10 @@ private fun MessageInput(
|
|||
// cursor position and multi-byte characters, likely outside simple inputTransformation.
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { if (canSend) onSendMessage() }, enabled = canSend) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Default.Send, contentDescription = stringResource(R.string.send))
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.Send,
|
||||
contentDescription = stringResource(Res.string.send),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -57,9 +57,9 @@ import org.meshtastic.core.database.entity.Reaction
|
|||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.feature.messaging.component.MessageItem
|
||||
import org.meshtastic.feature.messaging.component.ReactionDialog
|
||||
import org.meshtastic.core.strings.R as Res
|
||||
|
||||
@Composable
|
||||
fun DeliveryInfo(
|
||||
|
|
@ -73,13 +73,13 @@ fun DeliveryInfo(
|
|||
onDismissRequest = onDismiss,
|
||||
dismissButton = {
|
||||
FilledTonalButton(onClick = onDismiss, modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||
Text(text = stringResource(R.string.close))
|
||||
Text(text = stringResource(Res.string.close))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
if (resendOption) {
|
||||
FilledTonalButton(onClick = onConfirm, modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||
Text(text = stringResource(R.string.resend))
|
||||
Text(text = stringResource(Res.string.resend))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -102,7 +102,7 @@ fun DeliveryInfo(
|
|||
}
|
||||
relayNodeName?.let {
|
||||
Text(
|
||||
text = stringResource(R.string.relayed_by, it),
|
||||
text = stringResource(Res.string.relayed_by, it),
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
|
|
|
|||
|
|
@ -72,12 +72,12 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.meshtastic.core.database.entity.QuickChatAction
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.dragContainer
|
||||
import org.meshtastic.core.ui.component.dragDropItemsIndexed
|
||||
import org.meshtastic.core.ui.component.rememberDragDropState
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.strings.R as Res
|
||||
|
||||
@Composable
|
||||
fun QuickChatScreen(
|
||||
|
|
@ -98,7 +98,7 @@ fun QuickChatScreen(
|
|||
Scaffold(
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = stringResource(R.string.quick_chat),
|
||||
title = stringResource(Res.string.quick_chat),
|
||||
ourNode = null,
|
||||
showNodeChip = false,
|
||||
canNavigateUp = true,
|
||||
|
|
@ -137,7 +137,7 @@ fun QuickChatScreen(
|
|||
onClick = { showActionDialog = QuickChatAction(position = actions.size) },
|
||||
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(R.string.add))
|
||||
Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(Res.string.add))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -166,7 +166,7 @@ private fun EditQuickChatDialog(
|
|||
var actionInput by remember { mutableStateOf(action) }
|
||||
val newQuickChat = remember { action.uuid == 0L }
|
||||
val isInstant = actionInput.mode == QuickChatAction.Mode.Instant
|
||||
val title = if (newQuickChat) R.string.quick_chat_new else R.string.quick_chat_edit
|
||||
val title = if (newQuickChat) Res.string.quick_chat_new else Res.string.quick_chat_edit
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
@ -192,7 +192,7 @@ private fun EditQuickChatDialog(
|
|||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextFieldWithCounter(
|
||||
label = stringResource(R.string.name),
|
||||
label = stringResource(Res.string.name),
|
||||
value = actionInput.name,
|
||||
maxSize = 5,
|
||||
singleLine = true,
|
||||
|
|
@ -204,7 +204,7 @@ private fun EditQuickChatDialog(
|
|||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextFieldWithCounter(
|
||||
label = stringResource(R.string.message),
|
||||
label = stringResource(Res.string.message),
|
||||
value = actionInput.message,
|
||||
maxSize = 200,
|
||||
getSize = { it.toByteArray().size + 1 },
|
||||
|
|
@ -220,9 +220,9 @@ private fun EditQuickChatDialog(
|
|||
|
||||
val (text, icon) =
|
||||
if (isInstant) {
|
||||
R.string.quick_chat_instant to Icons.Default.FastForward
|
||||
Res.string.quick_chat_instant to Icons.Default.FastForward
|
||||
} else {
|
||||
R.string.quick_chat_append to Icons.Default.Add
|
||||
Res.string.quick_chat_append to Icons.Default.Add
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
|
@ -255,7 +255,7 @@ private fun EditQuickChatDialog(
|
|||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
TextButton(modifier = Modifier.weight(1f), onClick = onDismiss) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
Text(stringResource(Res.string.cancel))
|
||||
}
|
||||
|
||||
if (!newQuickChat) {
|
||||
|
|
@ -266,7 +266,7 @@ private fun EditQuickChatDialog(
|
|||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.delete))
|
||||
Text(text = stringResource(Res.string.delete))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -278,7 +278,7 @@ private fun EditQuickChatDialog(
|
|||
},
|
||||
enabled = actionInput.name.isNotEmpty() && actionInput.message.isNotEmpty(),
|
||||
) {
|
||||
Text(text = stringResource(R.string.save))
|
||||
Text(text = stringResource(Res.string.save))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -328,7 +328,7 @@ private fun QuickChatItem(
|
|||
if (action.mode == QuickChatAction.Mode.Instant) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FastForward,
|
||||
contentDescription = stringResource(R.string.quick_chat_instant),
|
||||
contentDescription = stringResource(Res.string.quick_chat_instant),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -339,12 +339,12 @@ private fun QuickChatItem(
|
|||
IconButton(onClick = { onEdit(action) }, modifier = Modifier.size(48.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = stringResource(R.string.quick_chat_edit),
|
||||
contentDescription = stringResource(Res.string.quick_chat_edit),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.DragHandle,
|
||||
contentDescription = stringResource(R.string.quick_chat),
|
||||
contentDescription = stringResource(Res.string.quick_chat),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
|
||||
import org.meshtastic.core.strings.R as Res
|
||||
|
||||
@Composable
|
||||
internal fun ReactionButton(onSendReaction: (String) -> Unit = {}) {
|
||||
|
|
@ -56,7 +56,7 @@ internal fun ReactionButton(onSendReaction: (String) -> Unit = {}) {
|
|||
)
|
||||
}
|
||||
IconButton(onClick = { showEmojiPickerDialog = true }) {
|
||||
Icon(imageVector = Icons.Default.EmojiEmotions, contentDescription = stringResource(R.string.react))
|
||||
Icon(imageVector = Icons.Default.EmojiEmotions, contentDescription = stringResource(Res.string.react))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ internal fun ReactionButton(onSendReaction: (String) -> Unit = {}) {
|
|||
private fun ReplyButton(onClick: () -> Unit = {}) = IconButton(
|
||||
onClick = onClick,
|
||||
content = {
|
||||
Icon(imageVector = Icons.AutoMirrored.Filled.Reply, contentDescription = stringResource(R.string.reply))
|
||||
Icon(imageVector = Icons.AutoMirrored.Filled.Reply, contentDescription = stringResource(Res.string.reply))
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ private fun MessageStatusButton(onStatusClick: () -> Unit = {}, status: MessageS
|
|||
MessageStatus.ERROR -> Icons.TwoTone.CloudOff
|
||||
else -> Icons.TwoTone.Warning
|
||||
},
|
||||
contentDescription = stringResource(R.string.message_delivery_status),
|
||||
contentDescription = stringResource(Res.string.message_delivery_status),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ import org.meshtastic.core.database.entity.Reaction
|
|||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.AutoLinkText
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.core.ui.component.Rssi
|
||||
|
|
@ -61,6 +60,7 @@ import org.meshtastic.core.ui.component.Snr
|
|||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.MessageItemColors
|
||||
import org.meshtastic.core.strings.R as Res
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
|
|
@ -144,7 +144,7 @@ internal fun MessageItem(
|
|||
if (message.viaMqtt) {
|
||||
Icon(
|
||||
Icons.Default.Cloud,
|
||||
contentDescription = stringResource(R.string.via_mqtt),
|
||||
contentDescription = stringResource(Res.string.via_mqtt),
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
|
|
@ -179,7 +179,7 @@ internal fun MessageItem(
|
|||
}
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.hops_away_template, message.hopsAway),
|
||||
text = stringResource(Res.string.hops_away_template, message.hopsAway),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
|
|
@ -228,7 +228,7 @@ private fun OriginalMessageSnippet(
|
|||
) {
|
||||
Icon(
|
||||
Icons.Default.FormatQuote,
|
||||
contentDescription = stringResource(R.string.reply), // Add to strings.xml
|
||||
contentDescription = stringResource(Res.string.reply), // Add to strings.xml
|
||||
)
|
||||
Text(
|
||||
text = originalMessageNode.user.shortName,
|
||||
|
|
@ -254,7 +254,7 @@ private fun OriginalMessageSnippet(
|
|||
private fun MessageItemPreview() {
|
||||
val sent =
|
||||
Message(
|
||||
text = stringResource(R.string.sample_message),
|
||||
text = stringResource(Res.string.sample_message),
|
||||
time = "10:00",
|
||||
fromLocal = true,
|
||||
status = MessageStatus.DELIVERED,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue