mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-16 19:41:57 +00:00
android, desktop: servers summary (#4398)
* multiplatform: subscription icon (#4397) * multiplatform: added network setting to control display of subscription percentage * multiplatform: moved filter to search bar and scan to button * multiplatform: added types and calls for new apis * Update apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> * chore: minor changes in API connections and usage * fix: removed show sub setting from net cfg * multiplatform: added subscription status to chats screen * multiplatform: added reconnect all servers api * multiplatform: added basic view for single user messages * multiplatform: added basic xftp server info view * multiplatform: added detailed stats view for SMP server * multiplatform: added detailed stats view for XFTP server * multiplatform: added individual server view for XFTP servers * multiplatform: added individual server view for SMP servers * multiplatform: added custom coloring for connections * multiplatform: added all translation strings * multiplatform: added support for multi user in serve summary * multiplatform: added missing translations * multiplatform: added share button to servers info * better type safety for server summaries * multiplatform: fixed action arrow paddings in server summary views * multiplatform: serverSummaryView padding and icon fixes * multiplatform: reused shared section divider * move and rename * remove tab icons, text * colors * filter button * paddings * fix translation keys * text * fix buttons clickable area, alerts * stats view * remove chevrons * colors * remove id, fix open server button * don't log terminal items * desktop left modal * single timer, pass state variable * remove unused * fix no summary view * net cfg * ability to hide servers screen by clicking outside servers screen * addressed review feedback * move user/all users selector under tabs * disable horizontal scroll on desktop --------- Co-authored-by: Diogo Cunha <diogofncunha@gmail.com> Co-authored-by: Avently <7953703+avently@users.noreply.github.com> Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
co-authored by
Diogo Cunha
Avently
Evgeny Poberezkin
parent
fd90b47194
commit
3e623684bc
@@ -353,7 +353,7 @@ fun DesktopScreen(settingsState: SettingsViewState) {
|
||||
}
|
||||
val (userPickerState, scaffoldState ) = settingsState
|
||||
val scope = rememberCoroutineScope()
|
||||
if (scaffoldState.drawerState.isOpen) {
|
||||
if (scaffoldState.drawerState.isOpen || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
|
||||
+197
-8
@@ -127,6 +127,7 @@ class AppPreferences {
|
||||
val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false)
|
||||
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
|
||||
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
|
||||
val networkShowSubscriptionPercentage = mkBoolPreference(SHARED_PREFS_NETWORK_SHOW_SUBSCRIPTION_PERCENTAGE, false)
|
||||
val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050")
|
||||
private val _networkSessionMode = mkStrPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default.name)
|
||||
val networkSessionMode: SharedPreference<TransportSessionMode> = SharedPreference(
|
||||
@@ -338,6 +339,7 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls"
|
||||
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible"
|
||||
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
|
||||
private const val SHARED_PREFS_NETWORK_SHOW_SUBSCRIPTION_PERCENTAGE = "ShowSubscriptionPercentage"
|
||||
private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort"
|
||||
private const val SHARED_PREFS_NETWORK_SESSION_MODE = "NetworkSessionMode"
|
||||
private const val SHARED_PREFS_NETWORK_SMP_PROXY_MODE = "NetworkSMPProxyMode"
|
||||
@@ -411,6 +413,18 @@ object ChatController {
|
||||
|
||||
fun hasChatCtrl() = ctrl != -1L && ctrl != null
|
||||
|
||||
suspend fun getAgentServersSummary(rh: Long?): PresentedServersSummary? {
|
||||
val userId = currentUserId("getAgentServersSummary")
|
||||
|
||||
val r = sendCmd(rh, CC.GetAgentServersSummary(userId), log = false)
|
||||
|
||||
if (r is CR.AgentServersSummary) return r.serversSummary
|
||||
Log.e(TAG, "getAgentServersSummary bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun resetAgentServersStats(rh: Long?): Boolean = sendCommandOkResp(rh, CC.ResetAgentServersStats())
|
||||
|
||||
private suspend fun currentUserId(funcName: String): Long = changingActiveUserMutex.withLock {
|
||||
val userId = chatModel.currentUser.value?.userId
|
||||
if (userId == null) {
|
||||
@@ -569,20 +583,24 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null): CR {
|
||||
suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, log: Boolean = true): CR {
|
||||
val ctrl = otherCtrl ?: ctrl ?: throw Exception("Controller is not initialized")
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val c = cmd.cmdString
|
||||
chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated))
|
||||
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
|
||||
if (log) {
|
||||
chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated))
|
||||
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
|
||||
}
|
||||
val json = if (rhId == null) chatSendCmd(ctrl, c) else chatSendRemoteCmd(ctrl, rhId.toInt(), c)
|
||||
val r = APIResponse.decodeStr(json)
|
||||
Log.d(TAG, "sendCmd response type ${r.resp.responseType}")
|
||||
if (r.resp is CR.Response || r.resp is CR.Invalid) {
|
||||
Log.d(TAG, "sendCmd response json $json")
|
||||
if (log) {
|
||||
Log.d(TAG, "sendCmd response type ${r.resp.responseType}")
|
||||
if (r.resp is CR.Response || r.resp is CR.Invalid) {
|
||||
Log.d(TAG, "sendCmd response json $json")
|
||||
}
|
||||
chatModel.addTerminalItem(TerminalItem.resp(rhId, r.resp))
|
||||
}
|
||||
chatModel.addTerminalItem(TerminalItem.resp(rhId, r.resp))
|
||||
r.resp
|
||||
}
|
||||
}
|
||||
@@ -920,6 +938,14 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reconnectServer(rh: Long?, server: String): Boolean {
|
||||
val userId = currentUserId("reconnectServer")
|
||||
|
||||
return sendCommandOkResp(rh, CC.ReconnectServer(userId, server))
|
||||
}
|
||||
|
||||
suspend fun reconnectAllServers(rh: Long?): Boolean = sendCommandOkResp(rh, CC.ReconnectAllServers())
|
||||
|
||||
suspend fun apiSetSettings(rh: Long?, type: ChatType, id: Long, settings: ChatSettings): Boolean {
|
||||
val r = sendCmd(rh, CC.APISetChatSettings(type, id, settings))
|
||||
return when (r) {
|
||||
@@ -2577,6 +2603,8 @@ sealed class CC {
|
||||
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
|
||||
class APIGetNetworkConfig: CC()
|
||||
class APISetNetworkInfo(val networkInfo: UserNetworkInfo): CC()
|
||||
class ReconnectServer(val userId: Long, val server: String): CC()
|
||||
class ReconnectAllServers: CC()
|
||||
class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC()
|
||||
class ApiSetMemberSettings(val groupId: Long, val groupMemberId: Long, val memberSettings: GroupMemberSettings): CC()
|
||||
class APIContactInfo(val contactId: Long): CC()
|
||||
@@ -2648,6 +2676,8 @@ sealed class CC {
|
||||
class ApiStandaloneFileInfo(val url: String): CC()
|
||||
// misc
|
||||
class ShowVersion(): CC()
|
||||
class ResetAgentServersStats(): CC()
|
||||
class GetAgentServersSummary(val userId: Long): CC()
|
||||
|
||||
val cmdString: String get() = when (this) {
|
||||
is Console -> cmd
|
||||
@@ -2724,6 +2754,8 @@ sealed class CC {
|
||||
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
|
||||
is APIGetNetworkConfig -> "/network"
|
||||
is APISetNetworkInfo -> "/_network info ${json.encodeToString(networkInfo)}"
|
||||
is ReconnectServer -> "/reconnect $userId $server"
|
||||
is ReconnectAllServers -> "/reconnect"
|
||||
is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}"
|
||||
is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}"
|
||||
is APIContactInfo -> "/_info @$contactId"
|
||||
@@ -2804,6 +2836,8 @@ sealed class CC {
|
||||
is ApiDownloadStandaloneFile -> "/_download $userId $url ${file.filePath}"
|
||||
is ApiStandaloneFileInfo -> "/_download info $url"
|
||||
is ShowVersion -> "/version"
|
||||
is ResetAgentServersStats -> "/reset servers stats"
|
||||
is GetAgentServersSummary -> "/get servers summary $userId"
|
||||
}
|
||||
|
||||
val cmdType: String get() = when (this) {
|
||||
@@ -2864,6 +2898,8 @@ sealed class CC {
|
||||
is APISetNetworkConfig -> "apiSetNetworkConfig"
|
||||
is APIGetNetworkConfig -> "apiGetNetworkConfig"
|
||||
is APISetNetworkInfo -> "apiSetNetworkInfo"
|
||||
is ReconnectServer -> "reconnectServer"
|
||||
is ReconnectAllServers -> "reconnectAllServers"
|
||||
is APISetChatSettings -> "apiSetChatSettings"
|
||||
is ApiSetMemberSettings -> "apiSetMemberSettings"
|
||||
is APIContactInfo -> "apiContactInfo"
|
||||
@@ -2933,6 +2969,8 @@ sealed class CC {
|
||||
is ApiDownloadStandaloneFile -> "apiDownloadStandaloneFile"
|
||||
is ApiStandaloneFileInfo -> "apiStandaloneFileInfo"
|
||||
is ShowVersion -> "showVersion"
|
||||
is ResetAgentServersStats -> "resetAgentServersStats"
|
||||
is GetAgentServersSummary -> "getAgentServersSummary"
|
||||
}
|
||||
|
||||
class ItemRange(val from: Long, val to: Long)
|
||||
@@ -3203,7 +3241,7 @@ data class NetCfg(
|
||||
val tcpKeepAlive: KeepAliveOpts?,
|
||||
val smpPingInterval: Long, // microseconds
|
||||
val smpPingCount: Int,
|
||||
val logTLSErrors: Boolean = false
|
||||
val logTLSErrors: Boolean = false,
|
||||
) {
|
||||
val useSocksProxy: Boolean get() = socksProxy != null
|
||||
val enableKeepAlive: Boolean get() = tcpKeepAlive != null
|
||||
@@ -3428,6 +3466,154 @@ data class TimedMessagesPreference(
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PresentedServersSummary(
|
||||
val statsStartedAt: Instant,
|
||||
val allUsersSMP: SMPServersSummary,
|
||||
val allUsersXFTP: XFTPServersSummary,
|
||||
val currentUserSMP: SMPServersSummary,
|
||||
val currentUserXFTP: XFTPServersSummary
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMPServersSummary(
|
||||
val smpTotals: SMPTotals,
|
||||
val currentlyUsedSMPServers: List<SMPServerSummary>,
|
||||
val previouslyUsedSMPServers: List<SMPServerSummary>,
|
||||
val onlyProxiedSMPServers: List<SMPServerSummary>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMPTotals(
|
||||
val sessions: ServerSessions,
|
||||
val subs: SMPServerSubs,
|
||||
val stats: AgentSMPServerStatsData
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SMPServerSummary(
|
||||
val smpServer: String,
|
||||
val known: Boolean? = null,
|
||||
val sessions: ServerSessions? = null,
|
||||
val subs: SMPServerSubs? = null,
|
||||
val stats: AgentSMPServerStatsData? = null
|
||||
) {
|
||||
val hasSubs: Boolean
|
||||
get() = subs != null
|
||||
|
||||
val sessionsOrNew: ServerSessions
|
||||
get() = sessions ?: ServerSessions.newServerSessions
|
||||
|
||||
val subsOrNew: SMPServerSubs
|
||||
get() = subs ?: SMPServerSubs.newSMPServerSubs
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ServerSessions(
|
||||
val ssConnected: Int,
|
||||
val ssErrors: Int,
|
||||
val ssConnecting: Int
|
||||
) {
|
||||
companion object {
|
||||
val newServerSessions = ServerSessions(
|
||||
ssConnected = 0,
|
||||
ssErrors = 0,
|
||||
ssConnecting = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SMPServerSubs(
|
||||
val ssActive: Int,
|
||||
val ssPending: Int
|
||||
) {
|
||||
companion object {
|
||||
val newSMPServerSubs = SMPServerSubs(
|
||||
ssActive = 0,
|
||||
ssPending = 0
|
||||
)
|
||||
}
|
||||
|
||||
val total: Int
|
||||
get() = ssActive + ssPending
|
||||
|
||||
val shareOfActive: Float
|
||||
get() = if (total != 0) ssActive.toFloat() / total else 0f
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AgentSMPServerStatsData(
|
||||
val _sentDirect: Int,
|
||||
val _sentViaProxy: Int,
|
||||
val _sentProxied: Int,
|
||||
val _sentDirectAttempts: Int,
|
||||
val _sentViaProxyAttempts: Int,
|
||||
val _sentProxiedAttempts: Int,
|
||||
val _sentAuthErrs: Int,
|
||||
val _sentQuotaErrs: Int,
|
||||
val _sentExpiredErrs: Int,
|
||||
val _sentOtherErrs: Int,
|
||||
val _recvMsgs: Int,
|
||||
val _recvDuplicates: Int,
|
||||
val _recvCryptoErrs: Int,
|
||||
val _recvErrs: Int,
|
||||
val _ackMsgs: Int,
|
||||
val _ackAttempts: Int,
|
||||
val _ackNoMsgErrs: Int,
|
||||
val _ackOtherErrs: Int,
|
||||
val _connCreated: Int,
|
||||
val _connSecured: Int,
|
||||
val _connCompleted: Int,
|
||||
val _connDeleted: Int,
|
||||
val _connDelAttempts: Int,
|
||||
val _connDelErrs: Int,
|
||||
val _connSubscribed: Int,
|
||||
val _connSubAttempts: Int,
|
||||
val _connSubIgnored: Int,
|
||||
val _connSubErrs: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class XFTPServersSummary(
|
||||
val xftpTotals: XFTPTotals,
|
||||
val currentlyUsedXFTPServers: List<XFTPServerSummary>,
|
||||
val previouslyUsedXFTPServers: List<XFTPServerSummary>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class XFTPTotals(
|
||||
val sessions: ServerSessions,
|
||||
val stats: AgentXFTPServerStatsData
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class XFTPServerSummary(
|
||||
val xftpServer: String,
|
||||
val known: Boolean? = null,
|
||||
val sessions: ServerSessions? = null,
|
||||
val stats: AgentXFTPServerStatsData? = null,
|
||||
val rcvInProgress: Boolean,
|
||||
val sndInProgress: Boolean,
|
||||
val delInProgress: Boolean
|
||||
) {}
|
||||
|
||||
@Serializable
|
||||
data class AgentXFTPServerStatsData(
|
||||
val _uploads: Int,
|
||||
val _uploadsSize: Long,
|
||||
val _uploadAttempts: Int,
|
||||
val _uploadErrs: Int,
|
||||
val _downloads: Int,
|
||||
val _downloadsSize: Long,
|
||||
val _downloadAttempts: Int,
|
||||
val _downloadAuthErrs: Int,
|
||||
val _downloadErrs: Int,
|
||||
val _deletions: Int,
|
||||
val _deleteAttempts: Int,
|
||||
val _deleteErrs: Int
|
||||
)
|
||||
|
||||
sealed class CustomTimeUnit {
|
||||
object Second: CustomTimeUnit()
|
||||
object Minute: CustomTimeUnit()
|
||||
@@ -4439,6 +4625,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR()
|
||||
@Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List<ArchiveError>): CR()
|
||||
@Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): CR()
|
||||
@Serializable @SerialName("agentServersSummary") class AgentServersSummary(val user: UserRef, val serversSummary: PresentedServersSummary): CR()
|
||||
// general
|
||||
@Serializable class Response(val type: String, val json: String): CR()
|
||||
@Serializable class Invalid(val str: String): CR()
|
||||
@@ -4598,6 +4785,7 @@ sealed class CR {
|
||||
is ContactPQAllowed -> "contactPQAllowed"
|
||||
is ContactPQEnabled -> "contactPQEnabled"
|
||||
is VersionInfo -> "versionInfo"
|
||||
is AgentServersSummary -> "agentServersSummary"
|
||||
is CmdOk -> "cmdOk"
|
||||
is ChatCmdError -> "chatCmdError"
|
||||
is ChatRespError -> "chatError"
|
||||
@@ -4776,6 +4964,7 @@ sealed class CR {
|
||||
is RemoteCtrlStopped -> "rcsState: $rcsState\nrcsStopReason: $rcStopReason"
|
||||
is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption")
|
||||
is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled")
|
||||
is AgentServersSummary -> withUser(user, json.encodeToString(serversSummary))
|
||||
is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" +
|
||||
"chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" +
|
||||
"agent migrations: ${json.encodeToString(agentMigrations.map { it.upName })}"
|
||||
|
||||
+82
-54
@@ -35,7 +35,10 @@ import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.URI
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||
@@ -69,7 +72,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val (userPickerState, scaffoldState ) = settingsState
|
||||
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(searchText, scaffoldState.drawerState, userPickerState, stopped)} },
|
||||
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(scaffoldState.drawerState, userPickerState, stopped)} },
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = {
|
||||
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
|
||||
@@ -181,8 +184,10 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListToolbar(searchInList: State<TextFieldValue>, drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
|
||||
private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
|
||||
val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) }
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
|
||||
if (stopped) {
|
||||
barButtons.add {
|
||||
IconButton(onClick = {
|
||||
@@ -200,6 +205,7 @@ private fun ChatListToolbar(searchInList: State<TextFieldValue>, drawerState: Dr
|
||||
}
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
val clipboard = LocalClipboardManager.current
|
||||
DefaultTopAppBar(
|
||||
navigationButton = {
|
||||
if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
||||
@@ -219,20 +225,33 @@ private fun ChatListToolbar(searchInList: State<TextFieldValue>, drawerState: Dr
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) {
|
||||
Text(
|
||||
stringResource(MR.strings.your_chats),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
if (chatModel.chats.size > 0) {
|
||||
val enabled = remember { derivedStateOf { searchInList.value.text.isEmpty() } }
|
||||
if (enabled.value) {
|
||||
ToggleFilterEnabledButton()
|
||||
} else {
|
||||
ToggleFilterDisabledButton()
|
||||
SubscriptionStatusIndicator(
|
||||
serversSummary = serversSummary,
|
||||
click = {
|
||||
ModalManager.start.closeModals()
|
||||
ModalManager.start.showModalCloseable(
|
||||
endButtons = {
|
||||
val summary = serversSummary.value
|
||||
if (summary != null) {
|
||||
ShareButton {
|
||||
val json = Json {
|
||||
prettyPrint = true
|
||||
}
|
||||
|
||||
val text = json.encodeToString(PresentedServersSummary.serializer(), summary)
|
||||
clipboard.shareText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { ServersSummaryView(chatModel.currentRemoteHost.value, serversSummary) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
onTitleClick = null,
|
||||
@@ -243,6 +262,53 @@ private fun ChatListToolbar(searchInList: State<TextFieldValue>, drawerState: Dr
|
||||
Divider(Modifier.padding(top = AppBarHeight))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SubscriptionStatusIndicator(serversSummary: MutableState<PresentedServersSummary?>, click: (() -> Unit)) {
|
||||
var subs by remember { mutableStateOf(SMPServerSubs.newSMPServerSubs) }
|
||||
var sess by remember { mutableStateOf(ServerSessions.newServerSessions) }
|
||||
var timer: Job? by remember { mutableStateOf(null) }
|
||||
|
||||
val fetchInterval: Duration = 1.seconds
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
fun setServersSummary() {
|
||||
withBGApi {
|
||||
serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId())
|
||||
|
||||
serversSummary.value?.let {
|
||||
subs = it.allUsersSMP.smpTotals.subs
|
||||
sess = it.allUsersSMP.smpTotals.sessions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
setServersSummary()
|
||||
timer = timer ?: scope.launch {
|
||||
while (true) {
|
||||
delay(fetchInterval.inWholeMilliseconds)
|
||||
setServersSummary()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopTimer() {
|
||||
timer?.cancel()
|
||||
timer = null
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
stopTimer()
|
||||
}
|
||||
}
|
||||
|
||||
SimpleButtonFrame(click = click) {
|
||||
SubscriptionStatusIndicatorView(subs = subs, sess = sess)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -292,27 +358,11 @@ private fun ToggleFilterEnabledButton() {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_filter_list),
|
||||
null,
|
||||
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.primary,
|
||||
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.padding(3.dp)
|
||||
.background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||
.border(width = 1.dp, color = MaterialTheme.colors.primary, shape = RoundedCornerShape(50))
|
||||
.padding(3.dp)
|
||||
.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleFilterDisabledButton() {
|
||||
IconButton({}, enabled = false) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_filter_list),
|
||||
null,
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.padding(3.dp)
|
||||
.border(width = 1.dp, color = MaterialTheme.colors.secondary, shape = RoundedCornerShape(50))
|
||||
.border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||
.padding(3.dp)
|
||||
.size(16.dp)
|
||||
)
|
||||
@@ -357,33 +407,11 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
|
||||
hideSearchOnBack()
|
||||
}
|
||||
} else {
|
||||
Row {
|
||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val clipboardHasText = remember(focused) { chatModel.clipboardHasText }.value
|
||||
if (clipboardHasText) {
|
||||
IconButton(
|
||||
onClick = { searchText.value = searchText.value.copy(clipboard.getText()?.text ?: return@IconButton) },
|
||||
Modifier.size(30.dp).desktopPointerHoverIconHand()
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_article), null, tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
IconButton(
|
||||
onClick = {
|
||||
val fixedRhId = chatModel.currentRemoteHost.value
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close ->
|
||||
NewChatView(fixedRhId, selection = NewChatOption.CONNECT, showQRCodeScanner = true, close = close)
|
||||
}
|
||||
},
|
||||
Modifier.size(30.dp).desktopPointerHoverIconHand()
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_qr_code), null, tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||
if (chatModel.chats.size > 0) {
|
||||
ToggleFilterEnabledButton()
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
}
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardState = getKeyboardState()
|
||||
|
||||
+977
@@ -0,0 +1,977 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import InfoRow
|
||||
import InfoRowTwoValues
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.AgentSMPServerStatsData
|
||||
import chat.simplex.common.model.AgentXFTPServerStatsData
|
||||
import chat.simplex.common.model.ChatController.chatModel
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.PresentedServersSummary
|
||||
import chat.simplex.common.model.RemoteHostInfo
|
||||
import chat.simplex.common.model.SMPServerSubs
|
||||
import chat.simplex.common.model.SMPServerSummary
|
||||
import chat.simplex.common.model.SMPTotals
|
||||
import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.common.model.ServerProtocol
|
||||
import chat.simplex.common.model.ServerSessions
|
||||
import chat.simplex.common.model.XFTPServerSummary
|
||||
import chat.simplex.common.model.localTimestamp
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.ProtocolServersView
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Instant
|
||||
import numOrDash
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
enum class SubscriptionColorType {
|
||||
ACTIVE, ACTIVE_SOCKS_PROXY, DISCONNECTED, ACTIVE_DISCONNECTED
|
||||
}
|
||||
|
||||
data class SubscriptionStatus(
|
||||
val color: SubscriptionColorType,
|
||||
val variableValue: Float,
|
||||
val opacity: Float,
|
||||
val statusPercent: Float
|
||||
)
|
||||
|
||||
fun subscriptionStatusColorAndPercentage(
|
||||
online: Boolean,
|
||||
socksProxy: String?,
|
||||
subs: SMPServerSubs,
|
||||
sess: ServerSessions
|
||||
): SubscriptionStatus {
|
||||
|
||||
fun roundedToQuarter(n: Float): Float = when {
|
||||
n >= 1 -> 1f
|
||||
n <= 0 -> 0f
|
||||
else -> (n * 4).roundToInt() / 4f
|
||||
}
|
||||
|
||||
val activeColor: SubscriptionColorType = if (socksProxy != null) SubscriptionColorType.ACTIVE_SOCKS_PROXY else SubscriptionColorType.ACTIVE
|
||||
val noConnColorAndPercent = SubscriptionStatus(SubscriptionColorType.DISCONNECTED, 1f, 1f, 0f)
|
||||
val activeSubsRounded = roundedToQuarter(subs.shareOfActive)
|
||||
|
||||
return if (online && subs.total > 0) {
|
||||
if (subs.ssActive == 0) {
|
||||
if (sess.ssConnected == 0)
|
||||
noConnColorAndPercent
|
||||
else
|
||||
SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
|
||||
} else { // ssActive > 0
|
||||
if (sess.ssConnected == 0)
|
||||
// This would mean implementation error
|
||||
SubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
|
||||
else
|
||||
SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
|
||||
}
|
||||
} else noConnColorAndPercent
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubscriptionStatusIndicatorPercentage(percentageText: String) {
|
||||
Text(
|
||||
percentageText,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp,
|
||||
style = MaterialTheme.typography.caption
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SubscriptionStatusIndicatorView(subs: SMPServerSubs, sess: ServerSessions, leadingPercentage: Boolean = false) {
|
||||
val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg())
|
||||
val statusColorAndPercentage = subscriptionStatusColorAndPercentage(chatModel.networkInfo.value.online, netCfg.value.socksProxy, subs, sess)
|
||||
val pref = remember { chatModel.controller.appPrefs.networkShowSubscriptionPercentage }
|
||||
val percentageText = "${(floor(statusColorAndPercentage.statusPercent * 100)).toInt()}%"
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)
|
||||
) {
|
||||
if (pref.state.value && leadingPercentage) SubscriptionStatusIndicatorPercentage(percentageText)
|
||||
SubscriptionStatusIcon(
|
||||
color = when(statusColorAndPercentage.color) {
|
||||
SubscriptionColorType.ACTIVE -> MaterialTheme.colors.primary
|
||||
SubscriptionColorType.ACTIVE_SOCKS_PROXY -> Indigo
|
||||
SubscriptionColorType.ACTIVE_DISCONNECTED -> WarningOrange
|
||||
SubscriptionColorType.DISCONNECTED -> MaterialTheme.colors.secondary
|
||||
},
|
||||
modifier = Modifier.size(16.dp),
|
||||
variableValue = statusColorAndPercentage.variableValue)
|
||||
if (pref.state.value && !leadingPercentage) SubscriptionStatusIndicatorPercentage(percentageText)
|
||||
}
|
||||
}
|
||||
|
||||
enum class PresentedUserCategory {
|
||||
CURRENT_USER, ALL_USERS
|
||||
}
|
||||
|
||||
enum class PresentedServerType {
|
||||
SMP, XFTP
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServerSessionsView(sess: ServerSessions) {
|
||||
SectionView(generalGetString(MR.strings.servers_info_transport_sessions_section_header).uppercase()) {
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.servers_info_sessions_connected),
|
||||
numOrDash(sess.ssConnected)
|
||||
)
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.servers_info_sessions_errors),
|
||||
numOrDash(sess.ssErrors)
|
||||
)
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.servers_info_sessions_connecting),
|
||||
numOrDash(sess.ssConnecting)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun serverAddress(server: String): String {
|
||||
val address = parseServerAddress(server)
|
||||
|
||||
return address?.hostnames?.first() ?: server
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPServerView(srvSumm: SMPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) {
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
SMPServerSummaryView(
|
||||
rh = rh,
|
||||
close = close,
|
||||
summary = srvSumm,
|
||||
statsStartedAt = statsStartedAt
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
serverAddress(srvSumm.smpServer),
|
||||
modifier = Modifier.weight(10f, fill = true)
|
||||
)
|
||||
if (srvSumm.subs != null) {
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
SubscriptionStatusIndicatorView(subs = srvSumm.subs, sess = srvSumm.sessionsOrNew, leadingPercentage = true)
|
||||
} else if (srvSumm.sessions != null) {
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
Icon(painterResource(MR.images.ic_arrow_upward), contentDescription = null, tint = SessIconColor(srvSumm.sessions))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessIconColor(sess: ServerSessions): Color {
|
||||
val online = chatModel.networkInfo.value.online
|
||||
return if (online && sess.ssConnected > 0) SessionActiveColor() else MaterialTheme.colors.secondary
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessionActiveColor(): Color {
|
||||
val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg())
|
||||
return if (netCfg.value.socksProxy != null) Indigo else MaterialTheme.colors.primary
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPServersListView(servers: List<SMPServerSummary>, statsStartedAt: Instant, header: String? = null, footer: String? = null, rh: RemoteHostInfo?) {
|
||||
val sortedServers = servers.sortedWith(compareBy<SMPServerSummary> { !it.hasSubs }
|
||||
.thenBy { serverAddress(it.smpServer) })
|
||||
|
||||
SectionView(header) {
|
||||
sortedServers.map { svr -> SMPServerView(srvSumm = svr, statsStartedAt = statsStartedAt, rh = rh) }
|
||||
}
|
||||
if (footer != null) {
|
||||
SectionTextFooter(
|
||||
footer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun prettySize(sizeInKB: Long): String {
|
||||
if (sizeInKB == 0L) {
|
||||
return "-"
|
||||
}
|
||||
|
||||
val sizeInBytes = sizeInKB * 1024
|
||||
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
||||
var size = sizeInBytes.toDouble()
|
||||
var unitIndex = 0
|
||||
|
||||
while (size >= 1024 && unitIndex < units.size - 1) {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
val formatter = DecimalFormat("#,##0.#")
|
||||
return "${formatter.format(size)} ${units[unitIndex]}"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun XFTPServerView(srvSumm: XFTPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) {
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
XFTPServerSummaryView(
|
||||
rh = rh,
|
||||
close = close,
|
||||
summary = srvSumm,
|
||||
statsStartedAt = statsStartedAt
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
serverAddress(srvSumm.xftpServer),
|
||||
modifier = Modifier.weight(10f, fill = true)
|
||||
)
|
||||
if (srvSumm.rcvInProgress || srvSumm.sndInProgress || srvSumm.delInProgress) {
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
XFTPServerInProgressIcon(srvSumm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun XFTPServerInProgressIcon(srvSumm: XFTPServerSummary) {
|
||||
return when {
|
||||
srvSumm.rcvInProgress && !srvSumm.sndInProgress && !srvSumm.delInProgress -> Icon(painterResource(MR.images.ic_arrow_downward),"download", tint = SessionActiveColor())
|
||||
!srvSumm.rcvInProgress && srvSumm.sndInProgress && !srvSumm.delInProgress -> Icon(painterResource(MR.images.ic_arrow_upward), "upload", tint = SessionActiveColor())
|
||||
!srvSumm.rcvInProgress && !srvSumm.sndInProgress && srvSumm.delInProgress -> Icon(painterResource(MR.images.ic_delete), "deletion", tint = SessionActiveColor())
|
||||
else -> Icon(painterResource(MR.images.ic_expand_all), "upload and download", tint = SessionActiveColor())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun XFTPServersListView(servers: List<XFTPServerSummary>, statsStartedAt: Instant, header: String? = null, rh: RemoteHostInfo?) {
|
||||
val sortedServers = servers.sortedBy { serverAddress(it.xftpServer) }
|
||||
|
||||
SectionView(header) {
|
||||
sortedServers.map { svr -> XFTPServerView(svr, statsStartedAt, rh) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPStatsView(stats: AgentSMPServerStatsData, statsStartedAt: Instant, remoteHostInfo: RemoteHostInfo?) {
|
||||
SectionView(generalGetString(MR.strings.servers_info_statistics_section_header).uppercase()) {
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.servers_info_messages_sent),
|
||||
numOrDash(stats._sentDirect + stats._sentViaProxy)
|
||||
)
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.servers_info_messages_received),
|
||||
numOrDash(stats._recvMsgs)
|
||||
)
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { close -> DetailedSMPStatsView(
|
||||
rh = remoteHostInfo,
|
||||
close = close,
|
||||
stats = stats,
|
||||
statsStartedAt = statsStartedAt)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(text = generalGetString(MR.strings.servers_info_details), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(
|
||||
String.format(stringResource(MR.strings.servers_info_private_data_disclaimer), localTimestamp(statsStartedAt))
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPSubscriptionsSection(totals: SMPTotals) {
|
||||
Column {
|
||||
Row(
|
||||
Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON * 2)
|
||||
) {
|
||||
Text(
|
||||
generalGetString(MR.strings.servers_info_subscriptions_section_header).uppercase(),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
style = MaterialTheme.typography.body2,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
SubscriptionStatusIndicatorView(totals.subs, totals.sessions)
|
||||
}
|
||||
Column(Modifier.padding(PaddingValues()).fillMaxWidth()) {
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.servers_info_subscriptions_connections_subscribed),
|
||||
numOrDash(totals.subs.ssActive)
|
||||
)
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.servers_info_subscriptions_total),
|
||||
numOrDash(totals.subs.total)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPSubscriptionsSection(subs: SMPServerSubs, summary: SMPServerSummary, rh: RemoteHostInfo?) {
|
||||
Column {
|
||||
Row(
|
||||
Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON * 2)
|
||||
) {
|
||||
Text(
|
||||
generalGetString(MR.strings.servers_info_subscriptions_section_header).uppercase(),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
style = MaterialTheme.typography.body2,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
SubscriptionStatusIndicatorView(subs, summary.sessionsOrNew)
|
||||
}
|
||||
Column(Modifier.padding(PaddingValues()).fillMaxWidth()) {
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.servers_info_subscriptions_connections_subscribed),
|
||||
numOrDash(subs.ssActive)
|
||||
)
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.servers_info_subscriptions_connections_pending),
|
||||
numOrDash(subs.ssPending)
|
||||
)
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.servers_info_subscriptions_total),
|
||||
numOrDash(subs.total)
|
||||
)
|
||||
ReconnectServerButton(rh, summary.smpServer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReconnectServerButton(rh: RemoteHostInfo?, server: String) {
|
||||
SectionItemView(click = { reconnectServerAlert(rh, server) }) {
|
||||
Text(
|
||||
stringResource(MR.strings.reconnect),
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reconnectServerAlert(rh: RemoteHostInfo?, server: String) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.servers_info_reconnect_server_title),
|
||||
text = generalGetString(MR.strings.servers_info_reconnect_server_message),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
val success = controller.reconnectServer(rh?.remoteHostId, server)
|
||||
|
||||
if (!success) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.servers_info_modal_error_title),
|
||||
text = generalGetString(MR.strings.servers_info_reconnect_server_error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun XFTPStatsView(stats: AgentXFTPServerStatsData, statsStartedAt: Instant, rh: RemoteHostInfo?) {
|
||||
SectionView(generalGetString(MR.strings.servers_info_statistics_section_header).uppercase()) {
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.servers_info_uploaded),
|
||||
prettySize(stats._uploadsSize)
|
||||
)
|
||||
InfoRow(
|
||||
generalGetString(MR.strings.servers_info_downloaded),
|
||||
prettySize(stats._downloadsSize)
|
||||
)
|
||||
SectionItemView (
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { close -> DetailedXFTPStatsView(
|
||||
rh = rh,
|
||||
close = close,
|
||||
stats = stats,
|
||||
statsStartedAt = statsStartedAt)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(text = generalGetString(MR.strings.servers_info_details), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(
|
||||
String.format(stringResource(MR.strings.servers_info_private_data_disclaimer), localTimestamp(statsStartedAt))
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IndentedInfoRow(title: String, desc: String) {
|
||||
InfoRow(title, desc, padding = PaddingValues(start = 24.dp + DEFAULT_PADDING, end = DEFAULT_PADDING))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DetailedSMPStatsLayout(stats: AgentSMPServerStatsData, statsStartedAt: Instant) {
|
||||
SectionView(generalGetString(MR.strings.servers_info_detailed_statistics_sent_messages_header).uppercase()) {
|
||||
InfoRow(generalGetString(MR.strings.servers_info_detailed_statistics_sent_messages_total), numOrDash(stats._sentDirect + stats._sentViaProxy))
|
||||
InfoRowTwoValues(generalGetString(MR.strings.sent_directly), generalGetString(MR.strings.attempts_label), stats._sentDirect, stats._sentDirectAttempts)
|
||||
InfoRowTwoValues(generalGetString(MR.strings.sent_via_proxy), generalGetString(MR.strings.attempts_label), stats._sentViaProxy, stats._sentViaProxyAttempts)
|
||||
InfoRowTwoValues(generalGetString(MR.strings.proxied), generalGetString(MR.strings.attempts_label), stats._sentProxied, stats._sentProxiedAttempts)
|
||||
SectionItemView {
|
||||
Text(generalGetString(MR.strings.send_errors), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
IndentedInfoRow("AUTH", numOrDash(stats._sentAuthErrs))
|
||||
IndentedInfoRow("QUOTA", numOrDash(stats._sentQuotaErrs))
|
||||
IndentedInfoRow(generalGetString(MR.strings.expired_label), numOrDash(stats._sentExpiredErrs))
|
||||
IndentedInfoRow(generalGetString(MR.strings.other_label), numOrDash(stats._sentOtherErrs))
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(generalGetString(MR.strings.servers_info_detailed_statistics_received_messages_header).uppercase()) {
|
||||
InfoRow(generalGetString(MR.strings.servers_info_detailed_statistics_received_total), numOrDash(stats._recvMsgs))
|
||||
SectionItemView {
|
||||
Text(generalGetString(MR.strings.servers_info_detailed_statistics_receive_errors), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
IndentedInfoRow(generalGetString(MR.strings.duplicates_label), numOrDash(stats._recvDuplicates))
|
||||
IndentedInfoRow(generalGetString(MR.strings.decryption_errors), numOrDash(stats._recvCryptoErrs))
|
||||
IndentedInfoRow(generalGetString(MR.strings.other_errors), numOrDash(stats._recvErrs))
|
||||
InfoRowTwoValues(generalGetString(MR.strings.acknowledged), generalGetString(MR.strings.attempts_label), stats._ackMsgs, stats._ackAttempts)
|
||||
SectionItemView {
|
||||
Text(generalGetString(MR.strings.acknowledgement_errors), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
IndentedInfoRow("NO_MSG errors", numOrDash(stats._ackNoMsgErrs))
|
||||
IndentedInfoRow(generalGetString(MR.strings.other_errors), numOrDash(stats._ackOtherErrs))
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(generalGetString(MR.strings.connections).uppercase()) {
|
||||
InfoRow(generalGetString(MR.strings.created), numOrDash(stats._connCreated))
|
||||
InfoRow(generalGetString(MR.strings.secured), numOrDash(stats._connSecured))
|
||||
InfoRow(generalGetString(MR.strings.completed), numOrDash(stats._connCompleted))
|
||||
InfoRowTwoValues(generalGetString(MR.strings.deleted), generalGetString(MR.strings.attempts_label), stats._connDeleted, stats._connDelAttempts)
|
||||
InfoRow(generalGetString(MR.strings.deletion_errors), numOrDash(stats._connDelErrs))
|
||||
InfoRowTwoValues(generalGetString(MR.strings.subscribed), generalGetString(MR.strings.attempts_label), stats._connSubscribed, stats._connSubAttempts)
|
||||
InfoRow(generalGetString(MR.strings.subscription_results_ignored), numOrDash(stats._connSubIgnored))
|
||||
InfoRow(generalGetString(MR.strings.subscription_errors), numOrDash(stats._connSubErrs))
|
||||
}
|
||||
SectionTextFooter(
|
||||
String.format(stringResource(MR.strings.servers_info_starting_from), localTimestamp(statsStartedAt))
|
||||
)
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DetailedXFTPStatsLayout(stats: AgentXFTPServerStatsData, statsStartedAt: Instant) {
|
||||
SectionView(generalGetString(MR.strings.uploaded_files).uppercase()) {
|
||||
InfoRow(generalGetString(MR.strings.size), prettySize(stats._uploadsSize))
|
||||
InfoRowTwoValues(generalGetString(MR.strings.chunks_uploaded), generalGetString(MR.strings.attempts_label), stats._uploads, stats._uploadAttempts)
|
||||
InfoRow(generalGetString(MR.strings.upload_errors), numOrDash(stats._uploadErrs))
|
||||
InfoRowTwoValues(generalGetString(MR.strings.chunks_deleted), generalGetString(MR.strings.attempts_label), stats._deletions, stats._deleteAttempts)
|
||||
InfoRow(generalGetString(MR.strings.deletion_errors), numOrDash(stats._deleteErrs))
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
SectionView(generalGetString(MR.strings.downloaded_files).uppercase()) {
|
||||
InfoRow(generalGetString(MR.strings.size), prettySize(stats._downloadsSize))
|
||||
InfoRowTwoValues(generalGetString(MR.strings.chunks_downloaded), generalGetString(MR.strings.attempts_label), stats._downloads, stats._downloadAttempts)
|
||||
SectionItemView {
|
||||
Text(generalGetString(MR.strings.download_errors), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
IndentedInfoRow("AUTH", numOrDash(stats._downloadAuthErrs))
|
||||
IndentedInfoRow(generalGetString(MR.strings.other_label), numOrDash(stats._downloadErrs))
|
||||
}
|
||||
SectionTextFooter(
|
||||
String.format(stringResource(MR.strings.servers_info_starting_from), localTimestamp(statsStartedAt))
|
||||
)
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun XFTPServerSummaryLayout(summary: XFTPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) {
|
||||
SectionView(generalGetString(MR.strings.server_address).uppercase()) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
summary.xftpServer,
|
||||
Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp),
|
||||
style = TextStyle(
|
||||
fontFamily = FontFamily.Monospace, fontSize = 16.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
)
|
||||
}
|
||||
if (summary.known == true) {
|
||||
SectionItemView(click = {
|
||||
ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.XFTP, close) }
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.open_server_settings_button))
|
||||
}
|
||||
}
|
||||
|
||||
if (summary.stats != null) {
|
||||
SectionDividerSpaced()
|
||||
XFTPStatsView(stats = summary.stats, rh = rh, statsStartedAt = statsStartedAt)
|
||||
}
|
||||
|
||||
if (summary.sessions != null) {
|
||||
SectionDividerSpaced()
|
||||
ServerSessionsView(summary.sessions)
|
||||
}
|
||||
}
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SMPServerSummaryLayout(summary: SMPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) {
|
||||
SectionView(generalGetString(MR.strings.server_address).uppercase()) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
summary.smpServer,
|
||||
Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp),
|
||||
style = TextStyle(
|
||||
fontFamily = FontFamily.Monospace, fontSize = 16.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
)
|
||||
}
|
||||
if (summary.known == true) {
|
||||
SectionItemView(click = {
|
||||
ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.SMP, close) }
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.open_server_settings_button))
|
||||
}
|
||||
}
|
||||
|
||||
if (summary.stats != null) {
|
||||
SectionDividerSpaced()
|
||||
SMPStatsView(stats = summary.stats, remoteHostInfo = rh, statsStartedAt = statsStartedAt)
|
||||
}
|
||||
|
||||
if (summary.subs != null) {
|
||||
SectionDividerSpaced()
|
||||
SMPSubscriptionsSection(subs = summary.subs, summary = summary, rh = rh)
|
||||
}
|
||||
|
||||
if (summary.sessions != null) {
|
||||
SectionDividerSpaced()
|
||||
ServerSessionsView(summary.sessions)
|
||||
}
|
||||
}
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalData.SMPServerSummaryView(
|
||||
rh: RemoteHostInfo?,
|
||||
close: () -> Unit,
|
||||
summary: SMPServerSummary,
|
||||
statsStartedAt: Instant
|
||||
) {
|
||||
ModalView(
|
||||
close = close
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.smp_server),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
SMPServerSummaryLayout(summary, statsStartedAt, rh)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ModalData.DetailedXFTPStatsView(
|
||||
rh: RemoteHostInfo?,
|
||||
close: () -> Unit,
|
||||
stats: AgentXFTPServerStatsData,
|
||||
statsStartedAt: Instant
|
||||
) {
|
||||
ModalView(
|
||||
close = close
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.servers_info_detailed_statistics),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
DetailedXFTPStatsLayout(stats, statsStartedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalData.DetailedSMPStatsView(
|
||||
rh: RemoteHostInfo?,
|
||||
close: () -> Unit,
|
||||
stats: AgentSMPServerStatsData,
|
||||
statsStartedAt: Instant
|
||||
) {
|
||||
ModalView(
|
||||
close = close
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.servers_info_detailed_statistics),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
DetailedSMPStatsLayout(stats, statsStartedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalData.XFTPServerSummaryView(
|
||||
rh: RemoteHostInfo?,
|
||||
close: () -> Unit,
|
||||
summary: XFTPServerSummary,
|
||||
statsStartedAt: Instant
|
||||
) {
|
||||
ModalView(
|
||||
close = close
|
||||
) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.xftp_server),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
XFTPServerSummaryLayout(summary, statsStartedAt, rh)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableState<PresentedServersSummary?>) {
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
var showUserSelection by remember { mutableStateOf(false) }
|
||||
val selectedUserCategory =
|
||||
remember { stateGetOrPut("selectedUserCategory") { PresentedUserCategory.ALL_USERS } }
|
||||
val selectedServerType =
|
||||
remember { stateGetOrPut("serverTypeSelection") { PresentedServerType.SMP } }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (chatModel.users.count { u -> u.user.activeUser || !u.user.hidden } == 1
|
||||
) {
|
||||
selectedUserCategory.value = PresentedUserCategory.CURRENT_USER
|
||||
} else {
|
||||
showUserSelection = true
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.servers_info),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
if (serversSummary.value == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Text(generalGetString(MR.strings.servers_info_missing), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
} else {
|
||||
val userOptions by remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
PresentedUserCategory.ALL_USERS to generalGetString(MR.strings.all_users),
|
||||
PresentedUserCategory.CURRENT_USER to generalGetString(MR.strings.current_user),
|
||||
)
|
||||
)
|
||||
}
|
||||
val serverTypeTabTitles = PresentedServerType.entries.map {
|
||||
when (it) {
|
||||
PresentedServerType.SMP ->
|
||||
stringResource(MR.strings.messages_section_title)
|
||||
|
||||
PresentedServerType.XFTP ->
|
||||
stringResource(MR.strings.servers_info_files_tab)
|
||||
}
|
||||
}
|
||||
val serverTypePagerState = rememberPagerState(
|
||||
initialPage = selectedServerType.value.ordinal,
|
||||
initialPageOffsetFraction = 0f
|
||||
) { PresentedServerType.entries.size }
|
||||
|
||||
KeyChangeEffect(serverTypePagerState.currentPage) {
|
||||
selectedServerType.value = PresentedServerType.values()[serverTypePagerState.currentPage]
|
||||
}
|
||||
TabRow(
|
||||
selectedTabIndex = serverTypePagerState.currentPage,
|
||||
backgroundColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colors.primary,
|
||||
) {
|
||||
serverTypeTabTitles.forEachIndexed { index, it ->
|
||||
Tab(
|
||||
selected = serverTypePagerState.currentPage == index,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
serverTypePagerState.animateScrollToPage(index)
|
||||
}
|
||||
},
|
||||
text = { Text(it, fontSize = 13.sp) },
|
||||
selectedContentColor = MaterialTheme.colors.primary,
|
||||
unselectedContentColor = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
state = serverTypePagerState,
|
||||
Modifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.Top,
|
||||
userScrollEnabled = appPlatform.isAndroid
|
||||
) { index ->
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
if (showUserSelection) {
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.servers_info_target),
|
||||
userOptions,
|
||||
selectedUserCategory,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = {
|
||||
selectedUserCategory.value = it
|
||||
}
|
||||
)
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
when (index) {
|
||||
PresentedServerType.SMP.ordinal -> {
|
||||
serversSummary.value?.let {
|
||||
val smpSummary =
|
||||
if (selectedUserCategory.value == PresentedUserCategory.CURRENT_USER) it.currentUserSMP else it.allUsersSMP;
|
||||
val totals = smpSummary.smpTotals
|
||||
val currentlyUsedSMPServers = smpSummary.currentlyUsedSMPServers
|
||||
val previouslyUsedSMPServers = smpSummary.previouslyUsedSMPServers
|
||||
val proxySMPServers = smpSummary.onlyProxiedSMPServers
|
||||
val statsStartedAt = it.statsStartedAt
|
||||
|
||||
SMPStatsView(totals.stats, statsStartedAt, rh)
|
||||
SectionDividerSpaced()
|
||||
SMPSubscriptionsSection(totals)
|
||||
SectionDividerSpaced()
|
||||
|
||||
if (currentlyUsedSMPServers.isNotEmpty()) {
|
||||
SMPServersListView(
|
||||
servers = currentlyUsedSMPServers,
|
||||
statsStartedAt = statsStartedAt,
|
||||
header = generalGetString(MR.strings.servers_info_connected_servers_section_header).uppercase(),
|
||||
rh = rh
|
||||
)
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
if (previouslyUsedSMPServers.isNotEmpty()) {
|
||||
SMPServersListView(
|
||||
servers = previouslyUsedSMPServers,
|
||||
statsStartedAt = statsStartedAt,
|
||||
header = generalGetString(MR.strings.servers_info_previously_connected_servers_section_header).uppercase(),
|
||||
rh = rh
|
||||
)
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
if (proxySMPServers.isNotEmpty()) {
|
||||
SMPServersListView(
|
||||
servers = proxySMPServers,
|
||||
statsStartedAt = statsStartedAt,
|
||||
header = generalGetString(MR.strings.servers_info_proxied_servers_section_header).uppercase(),
|
||||
footer = generalGetString(MR.strings.servers_info_proxied_servers_section_footer),
|
||||
rh = rh
|
||||
)
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
ServerSessionsView(totals.sessions)
|
||||
}
|
||||
}
|
||||
|
||||
PresentedServerType.XFTP.ordinal -> {
|
||||
serversSummary.value?.let {
|
||||
val xftpSummary =
|
||||
if (selectedUserCategory.value == PresentedUserCategory.CURRENT_USER) it.currentUserXFTP else it.allUsersXFTP
|
||||
val totals = xftpSummary.xftpTotals
|
||||
val statsStartedAt = it.statsStartedAt
|
||||
val currentlyUsedXFTPServers = xftpSummary.currentlyUsedXFTPServers
|
||||
val previouslyUsedXFTPServers = xftpSummary.previouslyUsedXFTPServers
|
||||
|
||||
XFTPStatsView(totals.stats, statsStartedAt, rh)
|
||||
SectionDividerSpaced()
|
||||
|
||||
if (currentlyUsedXFTPServers.isNotEmpty()) {
|
||||
XFTPServersListView(
|
||||
currentlyUsedXFTPServers,
|
||||
statsStartedAt,
|
||||
generalGetString(MR.strings.servers_info_connected_servers_section_header).uppercase(),
|
||||
rh
|
||||
)
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
if (previouslyUsedXFTPServers.isNotEmpty()) {
|
||||
XFTPServersListView(
|
||||
previouslyUsedXFTPServers,
|
||||
statsStartedAt,
|
||||
generalGetString(MR.strings.servers_info_previously_connected_servers_section_header).uppercase(),
|
||||
rh
|
||||
)
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
ServerSessionsView(totals.sessions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView {
|
||||
ReconnectAllServersButton(rh)
|
||||
ResetStatisticsButton(rh)
|
||||
}
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReconnectAllServersButton(rh: RemoteHostInfo?) {
|
||||
SectionItemView(click = { reconnectAllServersAlert(rh) }) {
|
||||
Text(
|
||||
stringResource(MR.strings.servers_info_reconnect_all_servers_button),
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reconnectAllServersAlert(rh: RemoteHostInfo?) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.servers_info_reconnect_servers_title),
|
||||
text = generalGetString(MR.strings.servers_info_reconnect_servers_message),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
val success = controller.reconnectAllServers(rh?.remoteHostId)
|
||||
|
||||
if (!success) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.servers_info_modal_error_title),
|
||||
text = generalGetString(MR.strings.servers_info_reconnect_servers_error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResetStatisticsButton(rh: RemoteHostInfo?) {
|
||||
SectionItemView(click = { resetStatisticsAlert(rh) }) {
|
||||
Text(
|
||||
stringResource(MR.strings.servers_info_reset_stats),
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetStatisticsAlert(rh: RemoteHostInfo?) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.servers_info_reset_stats_alert_title),
|
||||
text = generalGetString(MR.strings.servers_info_reset_stats_alert_message),
|
||||
confirmText = generalGetString(MR.strings.servers_info_reset_stats_alert_confirm),
|
||||
destructive = true,
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
val success = controller.resetAgentServersStats(rh?.remoteHostId)
|
||||
|
||||
if (!success) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.servers_info_modal_error_title),
|
||||
text = generalGetString(MR.strings.servers_info_reset_stats_alert_error_title)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+3
@@ -100,6 +100,9 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
|
||||
fun hasModalsOpen() = modalCount.value > 0
|
||||
|
||||
val hasModalsOpen: Boolean
|
||||
@Composable get () = remember { modalCount }.value > 0
|
||||
|
||||
fun closeModal() {
|
||||
if (modalViews.isNotEmpty()) {
|
||||
if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex)
|
||||
|
||||
+57
-2
@@ -276,8 +276,8 @@ fun TextIconSpaced(extraPadding: Boolean = false) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null, textColor: Color = MaterialTheme.colors.onBackground) {
|
||||
SectionItemViewSpaceBetween {
|
||||
fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null, textColor: Color = MaterialTheme.colors.onBackground, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
SectionItemViewSpaceBetween(padding = padding) {
|
||||
Row {
|
||||
val iconSize = with(LocalDensity.current) { 21.sp.toDp() }
|
||||
if (icon != null) Icon(icon, title, Modifier.padding(end = 8.dp).size(iconSize), tint = iconTint ?: MaterialTheme.colors.secondary)
|
||||
@@ -287,6 +287,61 @@ fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color
|
||||
}
|
||||
}
|
||||
|
||||
fun numOrDash(n: Number): String = if (n.toLong() == 0L) "-" else n.toString()
|
||||
|
||||
@Composable
|
||||
fun InfoRowTwoValues(
|
||||
title: String,
|
||||
title2: String,
|
||||
value: Int,
|
||||
value2: Int,
|
||||
textColor: Color = MaterialTheme.colors.onBackground
|
||||
) {
|
||||
SectionItemViewSpaceBetween {
|
||||
Row(
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = textColor,
|
||||
)
|
||||
Text(
|
||||
text = " / ",
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
Text(
|
||||
text = title2,
|
||||
color = textColor,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.Bottom) {
|
||||
if (value == 0 && value2 == 0) {
|
||||
Text(
|
||||
text = "-",
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = numOrDash(value),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
)
|
||||
Text(
|
||||
text = " / ",
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
Text(
|
||||
text = numOrDash(value2),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun InfoRowEllipsis(title: String, value: String, onClick: () -> Unit) {
|
||||
SectionItemViewSpaceBetween(onClick) {
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
|
||||
@Composable
|
||||
fun SubscriptionStatusIcon(
|
||||
color: Color,
|
||||
variableValue: Float,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@Composable
|
||||
fun ZeroIcon() {
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color.copy(alpha = 0.33f), modifier = modifier)
|
||||
}
|
||||
|
||||
when {
|
||||
variableValue <= 0f -> ZeroIcon()
|
||||
variableValue > 0f && variableValue <= 0.25f -> Box {
|
||||
ZeroIcon()
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_1_bar), null, tint = color, modifier = modifier)
|
||||
}
|
||||
|
||||
variableValue > 0.25f && variableValue <= 0.5f -> Box {
|
||||
ZeroIcon()
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_2_bar), null, tint = color, modifier = modifier)
|
||||
}
|
||||
|
||||
variableValue > 0.5f && variableValue <= 0.75f -> Box {
|
||||
ZeroIcon()
|
||||
Icon(painterResource(MR.images.ic_radiowaves_up_forward_3_bar), null, tint = color, modifier = modifier)
|
||||
}
|
||||
|
||||
else -> Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color, modifier = modifier)
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -44,6 +44,11 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) }
|
||||
},
|
||||
scanPaste = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = true, close = close) }
|
||||
},
|
||||
createGroup = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
@@ -55,15 +60,17 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
|
||||
|
||||
private val titles = listOf(
|
||||
MR.strings.add_contact_tab,
|
||||
MR.strings.scan_paste_link,
|
||||
MR.strings.create_group_button
|
||||
)
|
||||
private val icons = listOf(MR.images.ic_add_link, MR.images.ic_group)
|
||||
private val icons = listOf(MR.images.ic_add_link, MR.images.ic_qr_code, MR.images.ic_group)
|
||||
|
||||
@Composable
|
||||
private fun NewChatSheetLayout(
|
||||
newChatSheetState: StateFlow<AnimatedViewState>,
|
||||
stopped: Boolean,
|
||||
addContact: () -> Unit,
|
||||
scanPaste: () -> Unit,
|
||||
createGroup: () -> Unit,
|
||||
closeNewChatSheet: (animated: Boolean) -> Unit,
|
||||
) {
|
||||
@@ -102,7 +109,7 @@ private fun NewChatSheetLayout(
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
val actions = remember { listOf(addContact, createGroup) }
|
||||
val actions = remember { listOf(addContact, scanPaste, createGroup) }
|
||||
val backgroundColor = if (isInDarkTheme())
|
||||
blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F)
|
||||
else
|
||||
@@ -264,6 +271,7 @@ private fun PreviewNewChatSheet() {
|
||||
MutableStateFlow(AnimatedViewState.VISIBLE),
|
||||
stopped = false,
|
||||
addContact = {},
|
||||
scanPaste = {},
|
||||
createGroup = {},
|
||||
closeNewChatSheet = {},
|
||||
)
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(state = pagerState, Modifier.fillMaxSize(), verticalAlignment = Alignment.Top) { index ->
|
||||
HorizontalPager(state = pagerState, Modifier.fillMaxSize(), verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index ->
|
||||
// LALAL SCROLLBAR DOESN'T WORK
|
||||
ColumnWithScrollBar(
|
||||
Modifier
|
||||
|
||||
+10
@@ -42,6 +42,7 @@ fun NetworkAndServersView() {
|
||||
// It's not a state, just a one-time value. Shouldn't be used in any state-related situations
|
||||
val netCfg = remember { chatModel.controller.getNetCfg() }
|
||||
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
|
||||
val networkShowSubscriptionPercentage: MutableState<Boolean> = remember { mutableStateOf(chatModel.controller.appPrefs.networkShowSubscriptionPercentage.get()) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
|
||||
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
|
||||
@@ -53,6 +54,7 @@ fun NetworkAndServersView() {
|
||||
currentRemoteHost = currentRemoteHost,
|
||||
developerTools = developerTools,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
networkShowSubscriptionPercentage = networkShowSubscriptionPercentage,
|
||||
onionHosts = onionHosts,
|
||||
sessionMode = sessionMode,
|
||||
smpProxyMode = smpProxyMode,
|
||||
@@ -117,6 +119,9 @@ fun NetworkAndServersView() {
|
||||
)
|
||||
}
|
||||
},
|
||||
toggleNetworkShowSubscriptionPercentage = { enable ->
|
||||
networkShowSubscriptionPercentage.value = enable
|
||||
},
|
||||
useOnion = {
|
||||
if (onionHosts.value == it) return@NetworkAndServersLayout
|
||||
val prevValue = onionHosts.value
|
||||
@@ -230,12 +235,14 @@ fun NetworkAndServersView() {
|
||||
currentRemoteHost: RemoteHostInfo?,
|
||||
developerTools: Boolean,
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
networkShowSubscriptionPercentage: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
smpProxyMode: MutableState<SMPProxyMode>,
|
||||
smpProxyFallback: MutableState<SMPProxyFallback>,
|
||||
proxyPort: State<Int>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
toggleNetworkShowSubscriptionPercentage: (Boolean) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
updateSMPProxyMode: (SMPProxyMode) -> Unit,
|
||||
@@ -256,6 +263,7 @@ fun NetworkAndServersView() {
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } })
|
||||
|
||||
if (currentRemoteHost == null) {
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_radiowaves_up_forward_4_bar),stringResource(MR.strings.subscription_percentage), chatModel.controller.appPrefs.networkShowSubscriptionPercentage)
|
||||
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false)
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
|
||||
if (developerTools) {
|
||||
@@ -677,8 +685,10 @@ fun PreviewNetworkAndServersLayout() {
|
||||
currentRemoteHost = null,
|
||||
developerTools = true,
|
||||
networkUseSocksProxy = remember { mutableStateOf(true) },
|
||||
networkShowSubscriptionPercentage = remember { mutableStateOf(false) },
|
||||
proxyPort = remember { mutableStateOf(9050) },
|
||||
toggleSocksProxy = {},
|
||||
toggleNetworkShowSubscriptionPercentage = {},
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
|
||||
smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) },
|
||||
|
||||
@@ -617,6 +617,7 @@
|
||||
<!-- NewChatView.kt -->
|
||||
<string name="new_chat">New chat</string>
|
||||
<string name="add_contact_tab">Add contact</string>
|
||||
<string name="scan_paste_link">Scan / Paste link</string>
|
||||
<string name="one_time_link">One-time invitation link</string>
|
||||
<string name="one_time_link_short">1-time link</string>
|
||||
<string name="simplex_address">SimpleX address</string>
|
||||
@@ -685,6 +686,7 @@
|
||||
<string name="smp_servers_per_user">The servers for new connections of your current chat profile</string>
|
||||
<string name="smp_save_servers_question">Save servers?</string>
|
||||
<string name="xftp_servers">XFTP servers</string>
|
||||
<string name="subscription_percentage">Subscription percentage</string>
|
||||
<string name="install_simplex_chat_for_terminal">Install SimpleX Chat for terminal</string>
|
||||
<string name="star_on_github">Star on GitHub</string>
|
||||
<string name="contribute">Contribute</string>
|
||||
@@ -2075,4 +2077,85 @@
|
||||
<string name="network_type_network_wifi">WiFi</string>
|
||||
<string name="network_type_ethernet">Wired ethernet</string>
|
||||
<string name="network_type_other">Other</string>
|
||||
|
||||
<!-- ServersSummaryView.kt -->
|
||||
<string name="servers_info">Servers info</string>
|
||||
<string name="servers_info_files_tab">Files</string>
|
||||
<string name="servers_info_missing">No info, try to reload</string>
|
||||
<string name="servers_info_target">Showing info for</string>
|
||||
<string name="all_users">All users</string>
|
||||
<string name="current_user">Current user</string>
|
||||
<string name="servers_info_transport_sessions_section_header">Transport sessions</string>
|
||||
<string name="servers_info_sessions_connected">Connected</string>
|
||||
<string name="servers_info_sessions_connecting">Connecting</string>
|
||||
<string name="servers_info_sessions_errors">Errors</string>
|
||||
<string name="servers_info_statistics_section_header">Statistics</string>
|
||||
<string name="servers_info_messages_sent">Messages sent</string>
|
||||
<string name="servers_info_messages_received">Messages received</string>
|
||||
<string name="servers_info_details">Details</string>
|
||||
<string name="servers_info_private_data_disclaimer">Starting from %s.\nAll data is private to your device.</string>
|
||||
<string name="servers_info_subscriptions_section_header">Messages subscriptions</string>
|
||||
<string name="servers_info_subscriptions_connections_subscribed">Connections subscribed</string>
|
||||
<string name="servers_info_subscriptions_connections_pending">Pending</string>
|
||||
<string name="servers_info_subscriptions_total">Total</string>
|
||||
<string name="servers_info_connected_servers_section_header">Connected servers</string>
|
||||
<string name="servers_info_previously_connected_servers_section_header">Previously connected servers</string>
|
||||
<string name="servers_info_proxied_servers_section_header">Proxied servers</string>
|
||||
<string name="servers_info_proxied_servers_section_footer">You are not connected to these servers. Private routing is used to deliver messages to them.</string>
|
||||
<string name="servers_info_reconnect_servers_title">Reconnect servers?</string>
|
||||
<string name="servers_info_reconnect_servers_message">Reconnect all connected servers to force message delivery. It uses additional traffic.</string>
|
||||
<string name="servers_info_reconnect_server_title">Reconnect server?</string>
|
||||
<string name="servers_info_reconnect_server_message">Reconnect server to force message delivery. It uses additional traffic.</string>
|
||||
<string name="servers_info_reconnect_servers_error">Error reconnecting servers</string>
|
||||
<string name="servers_info_reconnect_server_error">Error reconnecting server</string>
|
||||
<string name="servers_info_modal_error_title">Error</string>
|
||||
<string name="servers_info_reconnect_all_servers_button">Reconnect all servers</string>
|
||||
<string name="servers_info_reset_stats">Reset all statistics</string>
|
||||
<string name="servers_info_reset_stats_alert_title">Reset all statistics?</string>
|
||||
<string name="servers_info_reset_stats_alert_message">Servers statistics will be reset - this cannot be undone!</string>
|
||||
<string name="servers_info_reset_stats_alert_confirm">Reset</string>
|
||||
<string name="servers_info_reset_stats_alert_error_title">Error resetting statistics</string>
|
||||
<string name="servers_info_uploaded">Uploaded</string>
|
||||
<string name="servers_info_downloaded">Downloaded</string>
|
||||
<string name="servers_info_detailed_statistics">Detailed statistics</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_header">Sent messages</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_total">Sent total</string>
|
||||
<string name="servers_info_detailed_statistics_received_messages_header">Received messages</string>
|
||||
<string name="servers_info_detailed_statistics_received_total">Received total</string>
|
||||
<string name="servers_info_detailed_statistics_receive_errors">Receive errors</string>
|
||||
<string name="servers_info_starting_from">Starting from %s.</string>
|
||||
<string name="smp_server">SMP server</string>
|
||||
<string name="xftp_server">XFTP server</string>
|
||||
<string name="reconnect">Reconnect</string>
|
||||
<string name="attempts_label">attempts</string>
|
||||
<string name="sent_directly">Sent directly</string>
|
||||
<string name="sent_via_proxy">Sent via proxy</string>
|
||||
<string name="proxied">Proxied</string>
|
||||
<string name="send_errors">Send errors</string>
|
||||
<string name="expired_label">expired</string>
|
||||
<string name="other_label">other</string>
|
||||
<string name="duplicates_label">duplicates</string>
|
||||
<string name="decryption_errors">decryption errors</string>
|
||||
<string name="other_errors">other errors</string>
|
||||
<string name="acknowledged">Acknowledged</string>
|
||||
<string name="acknowledgement_errors">Acknowledgement errors</string>
|
||||
<string name="connections">Connections</string>
|
||||
<string name="created">Created</string>
|
||||
<string name="secured">Secured</string>
|
||||
<string name="completed">Completed</string>
|
||||
<string name="deleted">Deleted</string>
|
||||
<string name="deletion_errors">Deletion errors</string>
|
||||
<string name="subscribed">Subscribed</string>
|
||||
<string name="subscription_results_ignored">Subscriptions ignored</string>
|
||||
<string name="subscription_errors">Subscription errors</string>
|
||||
<string name="uploaded_files">Uploaded files</string>
|
||||
<string name="size">Size</string>
|
||||
<string name="chunks_uploaded">Chunks uploaded</string>
|
||||
<string name="upload_errors">Upload errors</string>
|
||||
<string name="chunks_deleted">Chunks deleted</string>
|
||||
<string name="chunks_downloaded">Chunks downloaded</string>
|
||||
<string name="downloaded_files">Downloaded files</string>
|
||||
<string name="download_errors">Download errors</string>
|
||||
<string name="server_address">Server address</string>
|
||||
<string name="open_server_settings_button">Open server settings</string>
|
||||
</resources>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg height="24" viewBox="-44.134 -43.207 588.823 585.294" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Light-S" transform="matrix(8.710668563842773, 0, 0, 8.710668563842773, -85.06543731689453, 558.4567260742189)">
|
||||
<path d="M 15.124 -6.711 C 18.033 -6.711 20.399 -9.129 20.399 -11.986 C 20.399 -14.96 18.082 -17.295 15.124 -17.295 C 12.232 -17.295 9.766 -14.862 9.766 -11.889 C 9.766 -9.129 12.281 -6.711 15.124 -6.711 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 492 B |
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg height="24" viewBox="-44.134 -43.207 588.823 585.294" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Light-S" transform="matrix(8.710668563842773, 0, 0, 8.710668563842773, -85.06543731689453, 558.4567260742188)">
|
||||
<path d="M 15.124 -6.711 C 18.033 -6.711 20.399 -9.129 20.399 -11.986 C 20.399 -14.96 18.082 -17.295 15.124 -17.295 C 12.232 -17.295 9.766 -14.862 9.766 -11.889 C 9.766 -9.129 12.281 -6.711 15.124 -6.711 Z M 10.714 -30.066 C 10.714 -28.585 11.962 -27.352 13.428 -27.352 C 22.787 -27.352 30.392 -19.732 30.392 -10.388 C 30.392 -8.908 31.64 -7.675 33.106 -7.675 C 34.571 -7.675 35.819 -8.908 35.819 -10.388 C 35.819 -22.746 25.718 -32.764 13.428 -32.764 C 11.962 -32.764 10.714 -31.565 10.714 -30.066 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 787 B |
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg height="24" viewBox="-44.134 -43.207 588.823 585.294" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Light-S" transform="matrix(8.710668563842773, 0, 0, 8.710668563842773, -85.06543731689453, 558.4567260742188)">
|
||||
<path d="M 15.124 -6.711 C 18.033 -6.711 20.399 -9.129 20.399 -11.986 C 20.399 -14.96 18.082 -17.295 15.124 -17.295 C 12.232 -17.295 9.766 -14.862 9.766 -11.889 C 9.766 -9.129 12.281 -6.711 15.124 -6.711 Z M 10.714 -30.066 C 10.714 -28.585 11.962 -27.352 13.428 -27.352 C 22.787 -27.352 30.392 -19.732 30.392 -10.388 C 30.392 -8.908 31.64 -7.675 33.106 -7.675 C 34.571 -7.675 35.819 -8.908 35.819 -10.388 C 35.819 -22.746 25.718 -32.764 13.428 -32.764 C 11.962 -32.764 10.714 -31.565 10.714 -30.066 Z M 10.714 -45.251 C 10.714 -43.771 11.962 -42.538 13.428 -42.538 C 31.216 -42.538 45.578 -28.161 45.578 -10.388 C 45.578 -8.908 46.826 -7.675 48.291 -7.675 C 49.756 -7.675 51.004 -8.908 51.004 -10.388 C 51.004 -31.141 34.147 -47.95 13.428 -47.95 C 11.962 -47.95 10.714 -46.75 10.714 -45.251 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg height="24" viewBox="-44.134 -43.207 588.823 585.294" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Light-S" transform="matrix(8.710668563842773, 0, 0, 8.710668563842773, -85.06543731689453, 558.4567260742188)">
|
||||
<path d="M15.1235-6.71093C18.0332-6.71093 20.3989-9.1289 20.3989-11.9863C20.3989-14.96 18.082-17.2954 15.1235-17.2954C12.2324-17.2954 9.76562-14.8623 9.76562-11.8887C9.76562-9.1289 12.2813-6.71093 15.1235-6.71093ZM10.7143-30.0659C10.7143-28.5854 11.9624-27.3525 13.4277-27.3525C22.7871-27.3525 30.3921-19.7324 30.3921-10.3882C30.3921-8.90771 31.6401-7.6748 33.1055-7.6748C34.5708-7.6748 35.8189-8.90771 35.8189-10.3882C35.8189-22.7456 25.7178-32.7642 13.4277-32.7642C11.9624-32.7642 10.7143-31.5649 10.7143-30.0659ZM10.7143-45.2515C10.7143-43.771 11.9624-42.5381 13.4277-42.5381C31.2158-42.5381 45.5776-28.1611 45.5776-10.3882C45.5776-8.90771 46.8257-7.6748 48.291-7.6748C49.7564-7.6748 51.0044-8.90771 51.0044-10.3882C51.0044-31.1406 34.1465-47.9497 13.4277-47.9497C11.9624-47.9497 10.7143-46.7505 10.7143-45.2515ZM10.7143-61.4136C10.7143-59.9331 11.9624-58.7002 13.4277-58.7002C40.0874-58.7002 61.7397-37.0327 61.7397-10.3882C61.7397-8.90771 62.9878-7.6748 64.4531-7.6748C65.9185-7.6748 67.1665-8.90771 67.1665-10.3882C67.1665-40.0273 43.0181-64.1118 13.4277-64.1118C11.9624-64.1118 10.7143-62.9126 10.7143-61.4136Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
Reference in New Issue
Block a user