mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
feat(settings): align relay and storage controls
This commit is contained in:
@@ -175,7 +175,7 @@
|
||||
<string name="relay_apply_active_transfers">Beenden Sie alle aktiven Übertragungen und Freigaben, bevor Sie die Netzwerkeinstellungen anwenden.</string>
|
||||
<string name="relay_apply_failed">Diese Einstellungen konnten nicht angewendet werden. Die vorherigen Netzwerkeinstellungen wurden wiederhergestellt.</string>
|
||||
<string name="relay_apply_restart_description">Beim Anwenden wird die Netzwerkverbindung von VniDrop neu gestartet. Beenden Sie zuerst aktive Übertragungen und Freigaben. Vorhandene Einladungen müssen eventuell erneut geteilt werden.</string>
|
||||
<string name="relay_custom_urls_help">Geben Sie pro Zeile eine HTTPS-Relay-URL ein. Anmeldedaten in URLs werden nicht unterstützt. Das TLS-Zertifikat muss von einer öffentlich vertrauenswürdigen Zertifizierungsstelle ausgestellt sein.</string>
|
||||
<string name="relay_custom_urls_help">Fügen Sie jede HTTPS-Relay-URL separat hinzu. Anmeldedaten in URLs werden nicht unterstützt. Das TLS-Zertifikat muss von einer öffentlich vertrauenswürdigen Zertifizierungsstelle ausgestellt sein.</string>
|
||||
<string name="relay_custom_urls_label">Relay-URLs</string>
|
||||
<string name="relay_mode_automatic">Automatisch (empfohlen)</string>
|
||||
<string name="relay_mode_automatic_description">Verwendet die öffentliche Standard-Relay-Infrastruktur von VniDrop, wenn keine direkte Verbindung möglich ist.</string>
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
<string name="relay_apply_active_transfers">Detenga todas las transferencias y elementos compartidos activos antes de aplicar los ajustes de red.</string>
|
||||
<string name="relay_apply_failed">No se han podido aplicar estos ajustes. Se han restaurado los ajustes de red anteriores.</string>
|
||||
<string name="relay_apply_restart_description">Al aplicar los ajustes, se reinicia la conexión de red de VniDrop. Detenga primero las transferencias y los elementos compartidos activos. Es posible que tenga que volver a compartir las invitaciones existentes.</string>
|
||||
<string name="relay_custom_urls_help">Introduzca una URL HTTPS de relé por línea. No se admiten credenciales en las URL. El certificado TLS debe ser emitido por una autoridad de certificación de confianza pública.</string>
|
||||
<string name="relay_custom_urls_help">Añada cada URL HTTPS de relé por separado. No se admiten credenciales en las URL. El certificado TLS debe ser emitido por una autoridad de certificación de confianza pública.</string>
|
||||
<string name="relay_custom_urls_label">URL de relés</string>
|
||||
<string name="relay_mode_automatic">Automático (recomendado)</string>
|
||||
<string name="relay_mode_automatic_description">Usa la infraestructura pública de relés predeterminada de VniDrop cuando no haya una conexión directa disponible.</string>
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
<string name="relay_apply_active_transfers">Arrêtez tous les transferts et partages actifs avant d’appliquer les réglages réseau.</string>
|
||||
<string name="relay_apply_failed">Impossible d’appliquer ces réglages. Les réglages réseau précédents ont été restaurés.</string>
|
||||
<string name="relay_apply_restart_description">L’application de ces réglages redémarre la connexion réseau de VniDrop. Arrêtez d’abord les transferts et partages actifs. Il peut être nécessaire de partager à nouveau les invitations existantes.</string>
|
||||
<string name="relay_custom_urls_help">Saisissez une URL de relais HTTPS par ligne. Les identifiants dans les URL ne sont pas pris en charge. Le certificat TLS doit être émis par une autorité de certification reconnue publiquement.</string>
|
||||
<string name="relay_custom_urls_help">Ajoutez chaque URL de relais HTTPS séparément. Les identifiants dans les URL ne sont pas pris en charge. Le certificat TLS doit être émis par une autorité de certification reconnue publiquement.</string>
|
||||
<string name="relay_custom_urls_label">URL des relais</string>
|
||||
<string name="relay_mode_automatic">Automatique (recommandé)</string>
|
||||
<string name="relay_mode_automatic_description">Utiliser l’infrastructure de relais publique par défaut de VniDrop lorsqu’une connexion directe est indisponible.</string>
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
<string name="relay_apply_active_transfers">Interrompa tutti i trasferimenti e le condivisioni attivi prima di applicare le impostazioni di rete.</string>
|
||||
<string name="relay_apply_failed">Impossibile applicare queste impostazioni. Sono state ripristinate le impostazioni di rete precedenti.</string>
|
||||
<string name="relay_apply_restart_description">L’applicazione riavvia la connessione di rete di VniDrop. Interrompa prima i trasferimenti e le condivisioni attivi. Potrebbe essere necessario condividere di nuovo gli inviti esistenti.</string>
|
||||
<string name="relay_custom_urls_help">Inserisca un URL relay HTTPS per riga. Le credenziali negli URL non sono supportate. Il certificato TLS deve essere emesso da un’autorità di certificazione pubblicamente attendibile.</string>
|
||||
<string name="relay_custom_urls_help">Aggiungi separatamente ogni URL relay HTTPS. Le credenziali negli URL non sono supportate. Il certificato TLS deve essere emesso da un’autorità di certificazione pubblicamente attendibile.</string>
|
||||
<string name="relay_custom_urls_label">URL relay</string>
|
||||
<string name="relay_mode_automatic">Automatica (consigliata)</string>
|
||||
<string name="relay_mode_automatic_description">Usa l’infrastruttura relay pubblica predefinita di VniDrop quando non è disponibile una connessione diretta.</string>
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
<string name="relay_apply_active_transfers">Stop alle actieve overdrachten en gedeelde items voordat u de netwerkinstellingen toepast.</string>
|
||||
<string name="relay_apply_failed">Deze instellingen konden niet worden toegepast. De vorige netwerkinstellingen zijn hersteld.</string>
|
||||
<string name="relay_apply_restart_description">Bij het toepassen wordt de netwerkverbinding van VniDrop opnieuw gestart. Stop eerst actieve overdrachten en gedeelde items. Bestaande uitnodigingen moeten mogelijk opnieuw worden gedeeld.</string>
|
||||
<string name="relay_custom_urls_help">Voer per regel één HTTPS-relay-URL in. Aanmeldgegevens in URL\'s worden niet ondersteund. Het TLS-certificaat moet zijn uitgegeven door een openbaar vertrouwde certificeringsinstantie.</string>
|
||||
<string name="relay_custom_urls_help">Voeg elke HTTPS-relay-URL afzonderlijk toe. Aanmeldgegevens in URL\'s worden niet ondersteund. Het TLS-certificaat moet zijn uitgegeven door een openbaar vertrouwde certificeringsinstantie.</string>
|
||||
<string name="relay_custom_urls_label">Relay-URL\'s</string>
|
||||
<string name="relay_mode_automatic">Automatisch (aanbevolen)</string>
|
||||
<string name="relay_mode_automatic_description">Gebruikt de standaard openbare relay-infrastructuur van VniDrop wanneer geen directe verbinding beschikbaar is.</string>
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
<string name="relay_apply_active_transfers">Zatrzymaj wszystkie aktywne transfery i udostępnienia przed zastosowaniem ustawień sieci.</string>
|
||||
<string name="relay_apply_failed">Nie udało się zastosować tych ustawień. Przywrócono poprzednie ustawienia sieci.</string>
|
||||
<string name="relay_apply_restart_description">Zastosowanie ustawień ponownie uruchamia połączenie sieciowe VniDrop. Najpierw zatrzymaj aktywne transfery i udostępnienia. Istniejące zaproszenia mogą wymagać ponownego udostępnienia.</string>
|
||||
<string name="relay_custom_urls_help">Wprowadź po jednym adresie URL HTTPS przekaźnika w każdym wierszu. Dane logowania w adresach URL nie są obsługiwane. Certyfikat TLS musi być wystawiony przez publicznie zaufany urząd certyfikacji.</string>
|
||||
<string name="relay_custom_urls_help">Dodaj każdy adres URL HTTPS serwera przekaźnikowego osobno. Dane logowania w adresach URL nie są obsługiwane. Certyfikat TLS musi być wystawiony przez publicznie zaufany urząd certyfikacji.</string>
|
||||
<string name="relay_custom_urls_label">Adresy URL przekaźników</string>
|
||||
<string name="relay_mode_automatic">Automatyczny (zalecany)</string>
|
||||
<string name="relay_mode_automatic_description">Używa domyślnej publicznej infrastruktury przekaźników VniDrop, gdy połączenie bezpośrednie jest niedostępne.</string>
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
<string name="relay_apply_active_transfers">Pare todas as transferências e partilhas ativas antes de aplicar as definições de rede.</string>
|
||||
<string name="relay_apply_failed">Não foi possível aplicar estas definições. As definições de rede anteriores foram restauradas.</string>
|
||||
<string name="relay_apply_restart_description">A aplicação reinicia a ligação de rede do VniDrop. Pare primeiro as transferências e partilhas ativas. Poderá ser necessário voltar a partilhar os convites existentes.</string>
|
||||
<string name="relay_custom_urls_help">Introduza um URL HTTPS de retransmissor por linha. Não são suportadas credenciais nos URLs. O certificado TLS tem de ser emitido por uma autoridade de certificação publicamente reconhecida.</string>
|
||||
<string name="relay_custom_urls_help">Adicione cada URL HTTPS de retransmissor separadamente. Não são suportadas credenciais nos URLs. O certificado TLS tem de ser emitido por uma autoridade de certificação publicamente reconhecida.</string>
|
||||
<string name="relay_custom_urls_label">URLs dos retransmissores</string>
|
||||
<string name="relay_mode_automatic">Automático (recomendado)</string>
|
||||
<string name="relay_mode_automatic_description">Utiliza a infraestrutura pública de retransmissores predefinida do VniDrop quando não está disponível uma ligação direta.</string>
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
<string name="relay_apply_active_transfers">Остановите все активные передачи и раздачи перед применением настроек сети.</string>
|
||||
<string name="relay_apply_failed">Не удалось применить эти настройки. Предыдущие настройки сети восстановлены.</string>
|
||||
<string name="relay_apply_restart_description">При применении сетевое соединение VniDrop перезапускается. Сначала остановите активные передачи и раздачи. Возможно, существующие приглашения потребуется отправить повторно.</string>
|
||||
<string name="relay_custom_urls_help">Введите по одному HTTPS-адресу ретранслятора в строке. Учётные данные в URL-адресах не поддерживаются. Сертификат TLS должен быть выдан общедоступным доверенным центром сертификации.</string>
|
||||
<string name="relay_custom_urls_help">Добавляйте каждый HTTPS-адрес ретранслятора отдельно. Учётные данные в URL-адресах не поддерживаются. Сертификат TLS должен быть выдан общедоступным доверенным центром сертификации.</string>
|
||||
<string name="relay_custom_urls_label">URL-адреса ретрансляторов</string>
|
||||
<string name="relay_mode_automatic">Автоматически (рекомендуется)</string>
|
||||
<string name="relay_mode_automatic_description">Использовать стандартную публичную инфраструктуру ретрансляторов VniDrop, если прямое соединение недоступно.</string>
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
<string name="relay_apply_active_transfers">Stop all active transfers and shares before applying network settings.</string>
|
||||
<string name="relay_apply_failed">Could not apply these settings. The previous network settings were restored.</string>
|
||||
<string name="relay_apply_restart_description">Applying restarts VniDrop’s network connection. Stop active transfers and shares first. Existing invitations may need to be shared again.</string>
|
||||
<string name="relay_custom_urls_help">Enter one HTTPS relay URL per line. URL credentials are not supported. The TLS certificate must be issued by a publicly trusted certificate authority.</string>
|
||||
<string name="relay_custom_urls_help">Add each HTTPS relay URL separately. URL credentials are not supported. The TLS certificate must be issued by a publicly trusted certificate authority.</string>
|
||||
<string name="relay_custom_urls_label">Relay URLs</string>
|
||||
<string name="relay_mode_automatic">Automatic (recommended)</string>
|
||||
<string name="relay_mode_automatic_description">Use VniDrop’s default public relay infrastructure when a direct connection is unavailable.</string>
|
||||
|
||||
@@ -2,18 +2,26 @@ package com.vnidrop.app.feature.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vnidrop.app.core.RelayMode
|
||||
import com.vnidrop.app.core.usesCustomRelayUrls
|
||||
import com.vnidrop.app.ui.components.Field
|
||||
import com.vnidrop.app.ui.components.PrimaryButton
|
||||
import com.vnidrop.app.ui.icons.AppIcon
|
||||
import com.vnidrop.app.ui.icons.PlatformIcon
|
||||
@@ -21,6 +29,7 @@ import com.vnidrop.app.ui.theme.LocalVniDropColors
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import vnidrop.shared.generated.resources.Res
|
||||
import vnidrop.shared.generated.resources.approval_endpoint_id
|
||||
import vnidrop.shared.generated.resources.relay_add_url
|
||||
import vnidrop.shared.generated.resources.relay_apply
|
||||
import vnidrop.shared.generated.resources.relay_apply_active_transfers
|
||||
import vnidrop.shared.generated.resources.relay_apply_failed
|
||||
@@ -31,12 +40,13 @@ import vnidrop.shared.generated.resources.relay_custom_urls_label
|
||||
import vnidrop.shared.generated.resources.relay_mode_automatic
|
||||
import vnidrop.shared.generated.resources.relay_mode_automatic_description
|
||||
import vnidrop.shared.generated.resources.relay_mode_custom
|
||||
import vnidrop.shared.generated.resources.relay_mode_custom_description
|
||||
import vnidrop.shared.generated.resources.relay_mode_custom_direct_fallback
|
||||
import vnidrop.shared.generated.resources.relay_mode_custom_direct_fallback_description
|
||||
import vnidrop.shared.generated.resources.relay_mode_custom_description
|
||||
import vnidrop.shared.generated.resources.relay_mode_local_only
|
||||
import vnidrop.shared.generated.resources.relay_mode_local_only_description
|
||||
import vnidrop.shared.generated.resources.relay_privacy_description
|
||||
import vnidrop.shared.generated.resources.relay_remove_url
|
||||
import vnidrop.shared.generated.resources.relay_restore_failed
|
||||
import vnidrop.shared.generated.resources.relay_strict_warning
|
||||
import vnidrop.shared.generated.resources.relay_validation_duplicate_url
|
||||
@@ -50,7 +60,9 @@ import vnidrop.shared.generated.resources.settings_network_title
|
||||
internal fun NetworkSettings(
|
||||
state: SettingsState,
|
||||
onModeChanged: (RelayMode) -> Unit,
|
||||
onUrlsChanged: (String) -> Unit,
|
||||
onUrlChanged: (Int, String) -> Unit,
|
||||
onAddUrl: () -> Unit,
|
||||
onRemoveUrl: (Int) -> Unit,
|
||||
onApply: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
showBack: Boolean,
|
||||
@@ -58,15 +70,6 @@ internal fun NetworkSettings(
|
||||
val colors = LocalVniDropColors.current
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
SettingsTopBar(stringResource(Res.string.settings_network_title), onBack, showBack)
|
||||
state.endpointId?.takeIf(String::isNotBlank)?.let { endpointId ->
|
||||
SelectionContainer {
|
||||
Text(
|
||||
stringResource(Res.string.approval_endpoint_id, endpointId),
|
||||
color = colors.foregroundLighter,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
SettingsGroup {
|
||||
RelayModeRow(
|
||||
icon = AppIcon.Globe,
|
||||
@@ -104,14 +107,52 @@ internal fun NetworkSettings(
|
||||
onClick = { onModeChanged(RelayMode.LocalOnly) },
|
||||
)
|
||||
}
|
||||
Text(
|
||||
stringResource(Res.string.relay_privacy_description),
|
||||
color = colors.foregroundLighter,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
state.endpointId?.takeIf(String::isNotBlank)?.let { endpointId ->
|
||||
SelectionContainer {
|
||||
Text(
|
||||
stringResource(Res.string.approval_endpoint_id, endpointId),
|
||||
color = colors.foregroundLighter,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (state.relayMode.usesCustomRelayUrls) {
|
||||
Field(
|
||||
value = state.relayUrlsText,
|
||||
onValueChange = onUrlsChanged,
|
||||
label = stringResource(Res.string.relay_custom_urls_label),
|
||||
minLines = 3,
|
||||
enabled = !state.isApplyingRelaySettings,
|
||||
Text(
|
||||
stringResource(Res.string.relay_custom_urls_label),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
SettingsGroup {
|
||||
state.relayUrls.forEachIndexed { index, url ->
|
||||
RelayUrlRow(
|
||||
value = url,
|
||||
error = relayUrlErrorText(state.relayInputError, index),
|
||||
enabled = !state.isApplyingRelaySettings,
|
||||
onValueChange = { onUrlChanged(index, it) },
|
||||
onRemove = { onRemoveUrl(index) },
|
||||
)
|
||||
if (index < state.relayUrls.lastIndex) {
|
||||
SettingsDivider(startPadding = 14.dp)
|
||||
}
|
||||
}
|
||||
if (state.relayUrls.isNotEmpty()) {
|
||||
SettingsDivider(startPadding = 14.dp)
|
||||
}
|
||||
TextButton(
|
||||
onClick = onAddUrl,
|
||||
enabled = state.relayUrls.size < MaximumRelayUrls && !state.isApplyingRelaySettings,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
) {
|
||||
PlatformIcon(AppIcon.Add, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(Res.string.relay_add_url))
|
||||
}
|
||||
}
|
||||
Text(
|
||||
stringResource(Res.string.relay_custom_urls_help),
|
||||
color = colors.foregroundLighter,
|
||||
@@ -125,13 +166,8 @@ internal fun NetworkSettings(
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
stringResource(Res.string.relay_privacy_description),
|
||||
color = colors.foregroundLighter,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
relayErrorText(state)?.let { error ->
|
||||
relayGlobalErrorText(state)?.let { error ->
|
||||
Text(
|
||||
error,
|
||||
color = colors.destructiveDefault,
|
||||
@@ -189,7 +225,70 @@ private fun RelayModeRow(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun relayErrorText(state: SettingsState): String? {
|
||||
private fun RelayUrlRow(
|
||||
value: String,
|
||||
error: String?,
|
||||
enabled: Boolean,
|
||||
onValueChange: (String) -> Unit,
|
||||
onRemove: () -> Unit,
|
||||
) {
|
||||
val colors = LocalVniDropColors.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 14.dp, end = 6.dp, top = 10.dp, bottom = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = enabled,
|
||||
singleLine = true,
|
||||
isError = error != null,
|
||||
placeholder = { Text("https://relay.example.com") },
|
||||
supportingText = error?.let {
|
||||
{
|
||||
Text(it, color = colors.destructiveDefault)
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
IconButton(onClick = onRemove, enabled = enabled) {
|
||||
PlatformIcon(
|
||||
AppIcon.Delete,
|
||||
contentDescription = stringResource(Res.string.relay_remove_url),
|
||||
tint = colors.destructiveDefault,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun relayUrlErrorText(error: RelaySettingsInputError?, index: Int): String? = when (error) {
|
||||
is RelaySettingsInputError.HttpsRequired -> if (error.line == index + 1) {
|
||||
stringResource(Res.string.relay_validation_https_required, error.line)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
is RelaySettingsInputError.InvalidUrl -> if (error.line == index + 1) {
|
||||
stringResource(Res.string.relay_validation_invalid_url, error.line)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
is RelaySettingsInputError.DuplicateUrl -> if (error.line == index + 1) {
|
||||
stringResource(Res.string.relay_validation_duplicate_url, error.line)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
RelaySettingsInputError.MissingUrl,
|
||||
is RelaySettingsInputError.TooManyUrls,
|
||||
null,
|
||||
-> null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun relayGlobalErrorText(state: SettingsState): String? {
|
||||
if (state.hasActiveNetworkWork || state.relayApplyError == RelaySettingsApplyError.ActiveTransfers) {
|
||||
return stringResource(Res.string.relay_apply_active_transfers)
|
||||
}
|
||||
@@ -200,18 +299,10 @@ private fun relayErrorText(state: SettingsState): String? {
|
||||
Res.string.relay_validation_too_many_urls,
|
||||
error.maximum,
|
||||
)
|
||||
is RelaySettingsInputError.HttpsRequired -> stringResource(
|
||||
Res.string.relay_validation_https_required,
|
||||
error.line,
|
||||
)
|
||||
is RelaySettingsInputError.InvalidUrl -> stringResource(
|
||||
Res.string.relay_validation_invalid_url,
|
||||
error.line,
|
||||
)
|
||||
is RelaySettingsInputError.DuplicateUrl -> stringResource(
|
||||
Res.string.relay_validation_duplicate_url,
|
||||
error.line,
|
||||
)
|
||||
is RelaySettingsInputError.HttpsRequired,
|
||||
is RelaySettingsInputError.InvalidUrl,
|
||||
is RelaySettingsInputError.DuplicateUrl,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
return when (state.relayApplyError) {
|
||||
|
||||
@@ -19,13 +19,13 @@ data class RelaySettingsValidation(
|
||||
|
||||
fun validateRelaySettings(
|
||||
mode: RelayMode,
|
||||
urlsText: String,
|
||||
relayUrls: List<String>,
|
||||
retainedUrls: List<String> = emptyList(),
|
||||
): RelaySettingsValidation {
|
||||
if (!mode.usesCustomRelayUrls) {
|
||||
return RelaySettingsValidation(RelaySettings(mode, retainedUrls))
|
||||
}
|
||||
val lines = urlsText.lineSequence()
|
||||
val lines = relayUrls
|
||||
.mapIndexedNotNull { index, raw -> raw.trim().takeIf(String::isNotEmpty)?.let { index + 1 to it } }
|
||||
.toList()
|
||||
if (lines.isEmpty()) return RelaySettingsValidation(error = RelaySettingsInputError.MissingUrl)
|
||||
@@ -144,5 +144,5 @@ private fun isValidPortSuffix(suffix: String): Boolean {
|
||||
}
|
||||
|
||||
private const val HttpsPrefix = "https://"
|
||||
private const val MaximumRelayUrls = 8
|
||||
internal const val MaximumRelayUrls = 8
|
||||
private const val MaximumRelayUrlLength = 2_048
|
||||
|
||||
@@ -40,14 +40,6 @@ internal fun SettingsOverview(
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
SettingsGroup {
|
||||
SettingsRow(
|
||||
icon = AppIcon.Globe,
|
||||
title = stringResource(Res.string.settings_network_title),
|
||||
value = relayModeLabel(state.savedRelaySettings.mode),
|
||||
selected = state.selectedSection == SettingsSection.Network,
|
||||
onClick = { onSectionSelected(SettingsSection.Network) },
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsRow(
|
||||
icon = AppIcon.User,
|
||||
title = stringResource(Res.string.preferences_title),
|
||||
@@ -78,7 +70,17 @@ internal fun SettingsOverview(
|
||||
selected = state.selectedSection == SettingsSection.Storage,
|
||||
onClick = { onSectionSelected(SettingsSection.Storage) },
|
||||
)
|
||||
SettingsDivider()
|
||||
}
|
||||
SettingsGroup {
|
||||
SettingsRow(
|
||||
icon = AppIcon.Globe,
|
||||
title = stringResource(Res.string.settings_network_title),
|
||||
value = relayModeLabel(state.savedRelaySettings.mode),
|
||||
selected = state.selectedSection == SettingsSection.Network,
|
||||
onClick = { onSectionSelected(SettingsSection.Network) },
|
||||
)
|
||||
}
|
||||
SettingsGroup {
|
||||
SettingsRow(
|
||||
icon = AppIcon.Info,
|
||||
title = stringResource(Res.string.about_title),
|
||||
|
||||
@@ -25,7 +25,9 @@ fun SettingsRoute(viewModel: SettingsViewModel, windowClass: WindowClass) {
|
||||
onUsernameChanged = viewModel::setUsername,
|
||||
onThemeModeChanged = viewModel::setThemeMode,
|
||||
onRelayModeChanged = viewModel::setRelayMode,
|
||||
onRelayUrlsChanged = viewModel::setRelayUrlsText,
|
||||
onRelayUrlChanged = viewModel::setRelayUrl,
|
||||
onAddRelayUrl = viewModel::addRelayUrl,
|
||||
onRemoveRelayUrl = viewModel::removeRelayUrl,
|
||||
onApplyRelaySettings = viewModel::applyRelaySettings,
|
||||
onChooseFolder = viewModel::chooseReceiveFolder,
|
||||
onResetFolder = viewModel::resetReceiveFolder,
|
||||
|
||||
@@ -32,7 +32,9 @@ fun SettingsScreen(
|
||||
onSubmitBugReport: () -> Unit,
|
||||
onDeleteAllTransfers: () -> Unit = {},
|
||||
onRelayModeChanged: (RelayMode) -> Unit = {},
|
||||
onRelayUrlsChanged: (String) -> Unit = {},
|
||||
onRelayUrlChanged: (Int, String) -> Unit = { _, _ -> },
|
||||
onAddRelayUrl: () -> Unit = {},
|
||||
onRemoveRelayUrl: (Int) -> Unit = {},
|
||||
onApplyRelaySettings: () -> Unit = {},
|
||||
) {
|
||||
if (windowClass == WindowClass.Desktop) {
|
||||
@@ -65,7 +67,9 @@ fun SettingsScreen(
|
||||
onSubmitBugReport = onSubmitBugReport,
|
||||
onDeleteAllTransfers = onDeleteAllTransfers,
|
||||
onRelayModeChanged = onRelayModeChanged,
|
||||
onRelayUrlsChanged = onRelayUrlsChanged,
|
||||
onRelayUrlChanged = onRelayUrlChanged,
|
||||
onAddRelayUrl = onAddRelayUrl,
|
||||
onRemoveRelayUrl = onRemoveRelayUrl,
|
||||
onApplyRelaySettings = onApplyRelaySettings,
|
||||
)
|
||||
}
|
||||
@@ -102,7 +106,9 @@ fun SettingsScreen(
|
||||
onSubmitBugReport = onSubmitBugReport,
|
||||
onDeleteAllTransfers = onDeleteAllTransfers,
|
||||
onRelayModeChanged = onRelayModeChanged,
|
||||
onRelayUrlsChanged = onRelayUrlsChanged,
|
||||
onRelayUrlChanged = onRelayUrlChanged,
|
||||
onAddRelayUrl = onAddRelayUrl,
|
||||
onRemoveRelayUrl = onRemoveRelayUrl,
|
||||
onApplyRelaySettings = onApplyRelaySettings,
|
||||
)
|
||||
}
|
||||
@@ -131,7 +137,9 @@ private fun SettingsSectionContent(
|
||||
onSubmitBugReport: () -> Unit,
|
||||
onDeleteAllTransfers: () -> Unit,
|
||||
onRelayModeChanged: (RelayMode) -> Unit,
|
||||
onRelayUrlsChanged: (String) -> Unit,
|
||||
onRelayUrlChanged: (Int, String) -> Unit,
|
||||
onAddRelayUrl: () -> Unit,
|
||||
onRemoveRelayUrl: (Int) -> Unit,
|
||||
onApplyRelaySettings: () -> Unit,
|
||||
) {
|
||||
when (section) {
|
||||
@@ -141,7 +149,9 @@ private fun SettingsSectionContent(
|
||||
SettingsSection.Network -> NetworkSettings(
|
||||
state = state,
|
||||
onModeChanged = onRelayModeChanged,
|
||||
onUrlsChanged = onRelayUrlsChanged,
|
||||
onUrlChanged = onRelayUrlChanged,
|
||||
onAddUrl = onAddRelayUrl,
|
||||
onRemoveUrl = onRemoveRelayUrl,
|
||||
onApply = onApplyRelaySettings,
|
||||
onBack = onBack,
|
||||
showBack = showBack,
|
||||
|
||||
@@ -89,7 +89,7 @@ data class SettingsState(
|
||||
val themeMode: ThemeMode = ThemeMode.System,
|
||||
val savedRelaySettings: RelaySettings = RelaySettings(),
|
||||
val relayMode: RelayMode = RelayMode.Automatic,
|
||||
val relayUrlsText: String = "",
|
||||
val relayUrls: List<String> = emptyList(),
|
||||
val relayInputError: RelaySettingsInputError? = null,
|
||||
val relayApplyError: RelaySettingsApplyError? = null,
|
||||
val isApplyingRelaySettings: Boolean = false,
|
||||
@@ -114,7 +114,7 @@ data class SettingsState(
|
||||
) {
|
||||
val hasRelaySettingsChanges: Boolean
|
||||
get() = relayMode != savedRelaySettings.mode ||
|
||||
(relayMode.usesCustomRelayUrls && relayUrlsText != savedRelaySettings.relayUrls.joinToString("\n"))
|
||||
(relayMode.usesCustomRelayUrls && relayUrls != savedRelaySettings.relayUrls)
|
||||
}
|
||||
|
||||
sealed interface SettingsEffect {
|
||||
@@ -162,10 +162,10 @@ class SettingsViewModel(
|
||||
diagnosticsEnabled = preferences.diagnosticsEnabled,
|
||||
savedRelaySettings = preferences.relaySettings,
|
||||
relayMode = if (hasLocalRelayDraft) current.relayMode else preferences.relaySettings.mode,
|
||||
relayUrlsText = if (hasLocalRelayDraft) {
|
||||
current.relayUrlsText
|
||||
relayUrls = if (hasLocalRelayDraft) {
|
||||
current.relayUrls
|
||||
} else {
|
||||
preferences.relaySettings.relayUrls.joinToString("\n")
|
||||
preferences.relaySettings.relayUrls
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -269,6 +269,7 @@ class SettingsViewModel(
|
||||
_state.update {
|
||||
it.copy(
|
||||
relayMode = mode,
|
||||
relayUrls = if (mode.usesCustomRelayUrls && it.relayUrls.isEmpty()) listOf("") else it.relayUrls,
|
||||
relayInputError = null,
|
||||
relayApplyError = null,
|
||||
)
|
||||
@@ -276,11 +277,39 @@ class SettingsViewModel(
|
||||
hasLocalRelayDraft = _state.value.hasRelaySettingsChanges
|
||||
}
|
||||
|
||||
fun setRelayUrlsText(value: String) {
|
||||
fun setRelayUrl(index: Int, value: String) {
|
||||
if (index !in _state.value.relayUrls.indices) return
|
||||
hasLocalRelayDraft = true
|
||||
_state.update {
|
||||
it.copy(
|
||||
relayUrlsText = value,
|
||||
relayUrls = it.relayUrls.toMutableList().apply { this[index] = value },
|
||||
relayInputError = null,
|
||||
relayApplyError = null,
|
||||
)
|
||||
}
|
||||
hasLocalRelayDraft = _state.value.hasRelaySettingsChanges
|
||||
}
|
||||
|
||||
fun addRelayUrl() {
|
||||
if (_state.value.relayUrls.size >= MaximumRelayUrls) return
|
||||
hasLocalRelayDraft = true
|
||||
_state.update {
|
||||
it.copy(
|
||||
relayUrls = it.relayUrls + "",
|
||||
relayInputError = null,
|
||||
relayApplyError = null,
|
||||
)
|
||||
}
|
||||
hasLocalRelayDraft = _state.value.hasRelaySettingsChanges
|
||||
}
|
||||
|
||||
fun removeRelayUrl(index: Int) {
|
||||
if (index !in _state.value.relayUrls.indices) return
|
||||
hasLocalRelayDraft = true
|
||||
_state.update {
|
||||
val remaining = it.relayUrls.toMutableList().apply { removeAt(index) }
|
||||
it.copy(
|
||||
relayUrls = remaining.ifEmpty { listOf("") },
|
||||
relayInputError = null,
|
||||
relayApplyError = null,
|
||||
)
|
||||
@@ -293,7 +322,7 @@ class SettingsViewModel(
|
||||
if (snapshot.isApplyingRelaySettings || !snapshot.hasRelaySettingsChanges) return
|
||||
val validation = validateRelaySettings(
|
||||
mode = snapshot.relayMode,
|
||||
urlsText = snapshot.relayUrlsText,
|
||||
relayUrls = snapshot.relayUrls,
|
||||
retainedUrls = snapshot.savedRelaySettings.relayUrls,
|
||||
)
|
||||
val desired = validation.settings
|
||||
@@ -349,7 +378,7 @@ class SettingsViewModel(
|
||||
it.copy(
|
||||
savedRelaySettings = desired,
|
||||
relayMode = desired.mode,
|
||||
relayUrlsText = desired.relayUrls.joinToString("\n"),
|
||||
relayUrls = desired.relayUrls,
|
||||
isApplyingRelaySettings = false,
|
||||
relayInputError = null,
|
||||
relayApplyError = null,
|
||||
|
||||
@@ -2,14 +2,21 @@ package com.vnidrop.app.feature.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vnidrop.app.ui.icons.AppIcon
|
||||
import com.vnidrop.app.ui.state.formatBytes
|
||||
import com.vnidrop.app.ui.theme.LocalVniDropColors
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -37,27 +44,26 @@ internal fun StorageSettings(
|
||||
val storage = state.storage
|
||||
if (storage == null || state.isCalculatingStorage) {
|
||||
SettingsGroup {
|
||||
SettingsRow(
|
||||
icon = AppIcon.Storage,
|
||||
StorageRow(
|
||||
title = stringResource(Res.string.storage_calculating),
|
||||
trailing = { CircularProgressIndicator() },
|
||||
trailing = { CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
SettingsGroup {
|
||||
StorageRow(AppIcon.TransferData, stringResource(Res.string.storage_transfer_data), storage.transferCacheBytes)
|
||||
SettingsDivider()
|
||||
StorageRow(AppIcon.Storage, stringResource(Res.string.storage_app_data), storage.appDataBytes)
|
||||
SettingsDivider()
|
||||
StorageRow(AppIcon.Temporary, stringResource(Res.string.storage_temporary), storage.temporaryBytes)
|
||||
SettingsDivider()
|
||||
SettingsRow(
|
||||
icon = AppIcon.Folder,
|
||||
title = stringResource(Res.string.storage_received_files),
|
||||
value = formatBytes(storage.receivedBytes),
|
||||
StorageRow(stringResource(Res.string.storage_received_files), storage.receivedBytes)
|
||||
SettingsDivider(startPadding = 16.dp)
|
||||
StorageRow(stringResource(Res.string.storage_transfer_data), storage.transferCacheBytes)
|
||||
SettingsDivider(startPadding = 16.dp)
|
||||
StorageRow(stringResource(Res.string.storage_app_data), storage.appDataBytes)
|
||||
SettingsDivider(startPadding = 16.dp)
|
||||
StorageRow(stringResource(Res.string.storage_temporary), storage.temporaryBytes)
|
||||
SettingsDivider(startPadding = 16.dp)
|
||||
StorageRow(
|
||||
title = stringResource(Res.string.storage_total),
|
||||
bytes = storage.deviceImpactBytes,
|
||||
emphasized = true,
|
||||
)
|
||||
SettingsDivider()
|
||||
StorageRow(AppIcon.TotalStorage, stringResource(Res.string.storage_total), storage.deviceImpactBytes)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
@@ -76,6 +82,33 @@ internal fun StorageSettings(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StorageRow(icon: AppIcon, title: String, bytes: ULong) {
|
||||
SettingsRow(icon = icon, title = title, value = formatBytes(bytes))
|
||||
private fun StorageRow(
|
||||
title: String,
|
||||
bytes: ULong? = null,
|
||||
emphasized: Boolean = false,
|
||||
trailing: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 52.dp)
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = if (emphasized) FontWeight.SemiBold else FontWeight.Normal,
|
||||
)
|
||||
if (bytes != null) {
|
||||
Text(
|
||||
formatBytes(bytes),
|
||||
color = LocalVniDropColors.current.foregroundLighter,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = if (emphasized) FontWeight.SemiBold else FontWeight.Normal,
|
||||
)
|
||||
}
|
||||
trailing?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user