* 'main' of https://github.com/vitorpamplona/amethyst:
  code review fixes:   1. options.isNotEmpty().also → if guard   2. Hardcoded English strings in FeedGroup enum   3. Hardcoded accessibility label   4. Raw 14.sp literals (×6) → replaced with Font14SP theme constant   5. Raw 12.sp literal → replaced with Font12SP theme constant   6. Modifier.size(20.dp) → replaced with existing Size20Modifier theme constant
  update gitignore
  add grouped feed filter dialog with Material 3 styling add icons, reduce text size, center group headers in filter dialog
  modernize SpinnerSelectionDialog with Material 3 styling
  feature switch chess icon on android: visible in debug/benchmark client only
  New Crowdin translations by GitHub Action
  Intentionality: check the return value and log a warning via Log.w() when deletion fails
  Update CS, DE, SV, PT
This commit is contained in:
Vitor Pamplona
2026-03-20 17:05:29 -04:00
12 changed files with 440 additions and 111 deletions
+4
View File
@@ -33,6 +33,10 @@
/captures
.cxx
# superpowers skill
.superpowers
docs/brainstorms
docs/superpowers
# Built application files
*.apk
@@ -171,7 +171,9 @@ object MetadataStripper {
if (muxerStarted) runCatching { muxer?.stop() }
muxer?.release()
extractor.release()
if (!succeeded) outputFile.delete()
if (!succeeded && !outputFile.delete()) {
Log.w("MetadataStripper", "Failed to delete temp file: ${outputFile.absolutePath}")
}
}
return succeeded
}
@@ -199,7 +201,9 @@ object MetadataStripper {
val inputStream =
context.contentResolver.openInputStream(uri)
?: run {
tempFile.delete()
if (!tempFile.delete()) {
Log.w("MetadataStripper", "Failed to delete temp file: ${tempFile.absolutePath}")
}
return StrippingResult(uri, false)
}
inputStream.use { input ->
@@ -218,7 +222,9 @@ object MetadataStripper {
StrippingResult(tempFile.toUri(), true)
} catch (e: Exception) {
if (e is CancellationException) throw e
tempFile?.delete()
if (tempFile?.delete() == false) {
Log.w("MetadataStripper", "Failed to delete temp file: ${tempFile.absolutePath}")
}
Log.d("MetadataStripper", "Failed to strip image metadata: ${e.message}")
StrippingResult(uri, false)
}
@@ -304,7 +310,9 @@ object MetadataStripper {
input.copyTo(output)
}
} ?: run {
tempInputFile.delete()
if (!tempInputFile.delete()) {
Log.w("MetadataStripper", "Failed to delete temp file: ${tempInputFile.absolutePath}")
}
return StrippingResult(uri, false)
}
@@ -345,7 +353,9 @@ object MetadataStripper {
}
if (startOffset == 0L && endOffset == fileSize) {
tempInputFile.delete()
if (!tempInputFile.delete()) {
Log.w("MetadataStripper", "Failed to delete temp file: ${tempInputFile.absolutePath}")
}
tempInputFile = null
return StrippingResult(uri, true) // no tags found, already clean
}
@@ -365,14 +375,18 @@ object MetadataStripper {
}
}
}
tempInputFile.delete()
if (!tempInputFile.delete()) {
Log.w("MetadataStripper", "Failed to delete temp file: ${tempInputFile.absolutePath}")
}
tempInputFile = null
Log.d("MetadataStripper", "Stripped ID3 tags from MP3")
StrippingResult(tempOutputFile.toUri(), true)
} catch (e: Exception) {
if (e is CancellationException) throw e
tempInputFile?.delete()
if (tempInputFile?.delete() == false) {
Log.w("MetadataStripper", "Failed to delete temp file: ${tempInputFile.absolutePath}")
}
Log.d("MetadataStripper", "Failed to strip MP3 metadata: ${e.message}")
StrippingResult(uri, false)
}
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@@ -34,7 +33,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
@@ -61,7 +59,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import kotlinx.collections.immutable.ImmutableList
@@ -159,13 +156,11 @@ private fun BaseTextSpinner(
)
}
if (optionsShowing) {
options.isNotEmpty().also {
SpinnerSelectionDialog(options = options, onDismiss = { optionsShowing = false }) {
currentText = options[it].title
optionsShowing = false
onSelect(it)
}
if (optionsShowing && options.isNotEmpty()) {
SpinnerSelectionDialog(options = options, onDismiss = { optionsShowing = false }) {
currentText = options[it].title
optionsShowing = false
onSelect(it)
}
}
}
@@ -211,14 +206,14 @@ fun <T> SpinnerSelectionDialog(
) {
Dialog(onDismissRequest = onDismiss) {
Surface(
border = BorderStroke(0.25.dp, Color.LightGray),
shape = RoundedCornerShape(5.dp),
shape = RoundedCornerShape(28.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
LazyColumn {
title?.let {
item {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp, 16.dp),
modifier = Modifier.fillMaxWidth().padding(20.dp),
horizontalArrangement = Arrangement.Center,
) {
Text(
@@ -227,7 +222,6 @@ fun <T> SpinnerSelectionDialog(
fontWeight = FontWeight.Bold,
)
}
HorizontalDivider(color = Color.LightGray, thickness = DividerThickness)
}
}
itemsIndexed(options) { index, item ->
@@ -237,7 +231,7 @@ fun <T> SpinnerSelectionDialog(
Modifier
.fillMaxWidth()
.clickable { onSelect(index) }
.padding(16.dp, 16.dp)
.padding(horizontal = 16.dp, vertical = 12.dp)
.semantics {
role = Role.Button
contentDescription = optionsOfLabel
@@ -245,9 +239,6 @@ fun <T> SpinnerSelectionDialog(
) {
Column { onRenderItem(item) }
}
if (index < options.lastIndex) {
HorizontalDivider(color = Color.LightGray, thickness = DividerThickness)
}
}
}
}
@@ -89,6 +89,7 @@ import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
@@ -473,14 +474,16 @@ fun ListContent(
route = Route.Wallet,
)
NavigationRow(
title = R.string.route_chess,
icon = R.drawable.ic_chess,
iconReference = 1,
tint = MaterialTheme.colorScheme.onBackground,
nav = nav,
route = Route.Chess,
)
if (isDebug) {
NavigationRow(
title = R.string.route_chess,
icon = R.drawable.ic_chess,
iconReference = 1,
tint = MaterialTheme.colorScheme.onBackground,
nav = nav,
route = Route.Chess,
)
}
NavigationRow(
title = R.string.event_sync_title,
@@ -21,20 +21,36 @@
package com.vitorpamplona.amethyst.ui.navigation.topbars
import android.Manifest
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ViewList
import androidx.compose.material.icons.automirrored.outlined.VolumeOff
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.outlined.Groups
import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.SensorDoor
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -43,14 +59,17 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
@@ -62,7 +81,6 @@ import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.ui.components.LoadingAnimation
import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog
import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName
import com.vitorpamplona.amethyst.ui.screen.CommunityName
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
@@ -74,6 +92,8 @@ import com.vitorpamplona.amethyst.ui.screen.RelayName
import com.vitorpamplona.amethyst.ui.screen.ResourceName
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Font12SP
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -120,6 +140,8 @@ fun FeedFilterSpinner(
stringRes(R.string.feed_filter_select_an_option, selectAnOption)
}
val openDropdownLabel = stringRes(R.string.open_dropdown_menu)
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
@@ -137,7 +159,7 @@ fun FeedFilterSpinner(
Text(
text = stringRes(R.string.lack_location_permissions),
fontSize = 12.sp,
fontSize = Font12SP,
lineHeight = 12.sp,
)
} else {
@@ -152,7 +174,7 @@ fun FeedFilterSpinner(
Row {
Text(
text = "(${myLocation.geoHash})",
fontSize = 12.sp,
fontSize = Font12SP,
lineHeight = 12.sp,
)
Spacer(modifier = StdHorzSpacer)
@@ -162,7 +184,7 @@ fun FeedFilterSpinner(
) { cityName ->
Text(
text = "($cityName)",
fontSize = 12.sp,
fontSize = Font12SP,
lineHeight = 12.sp,
)
}
@@ -171,7 +193,7 @@ fun FeedFilterSpinner(
LocationState.LocationResult.LackPermission -> {
Text(
text = stringRes(R.string.lack_location_permissions),
fontSize = 12.sp,
fontSize = Font12SP,
lineHeight = 12.sp,
)
}
@@ -179,7 +201,7 @@ fun FeedFilterSpinner(
LocationState.LocationResult.Loading -> {
Text(
text = stringRes(R.string.loading_location),
fontSize = 12.sp,
fontSize = Font12SP,
lineHeight = 12.sp,
)
}
@@ -207,7 +229,7 @@ fun FeedFilterSpinner(
}.semantics {
role = Role.DropdownList
stateDescription = accessibilityDescription
onClick(label = "Open feed filter menu") {
onClick(label = openDropdownLabel) {
optionsShowing = true
return@onClick true
}
@@ -215,20 +237,18 @@ fun FeedFilterSpinner(
)
}
if (optionsShowing) {
options.isNotEmpty().also {
SpinnerSelectionDialog(
title = explainer,
options = options,
onDismiss = { optionsShowing = false },
onSelect = {
selected = options[it]
optionsShowing = false
onSelect(it)
},
) {
RenderOption(it.name, accountViewModel)
}
if (optionsShowing && options.isNotEmpty()) {
GroupedFeedFilterDialog(
title = explainer,
options = options,
onDismiss = { optionsShowing = false },
onSelect = {
selected = options[it]
optionsShowing = false
onSelect(it)
},
) {
RenderOption(it.name, accountViewModel)
}
}
}
@@ -241,84 +261,258 @@ fun RenderOption(
when (option) {
is GeoHashName -> {
LoadCityName(option.geoHashTag) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(),
) {
Text(text = "/g/$it", color = MaterialTheme.colorScheme.onSurface)
}
Text(text = "/g/$it", fontSize = Font14SP, color = MaterialTheme.colorScheme.onSurface)
}
}
is HashtagName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(),
) {
Text(text = option.name(), color = MaterialTheme.colorScheme.onSurface)
}
Text(text = option.name(), fontSize = Font14SP, color = MaterialTheme.colorScheme.onSurface)
}
is ResourceName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringRes(id = option.resourceId),
color = MaterialTheme.colorScheme.onSurface,
)
}
Text(
text = stringRes(id = option.resourceId),
fontSize = Font14SP,
color = MaterialTheme.colorScheme.onSurface,
)
}
is PeopleListName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(),
) {
val noteState by observeNote(option.note, accountViewModel)
val noteState by observeNote(option.note, accountViewModel)
val noteEvent = noteState.note.event
val name =
when (noteEvent) {
is PeopleListEvent -> {
noteEvent.titleOrName() ?: option.note.dTag()
}
is FollowListEvent -> {
noteEvent.title() ?: option.note.dTag()
}
else -> {
option.note.dTag()
}
val noteEvent = noteState.note.event
val name =
when (noteEvent) {
is PeopleListEvent -> {
noteEvent.titleOrName() ?: option.note.dTag()
}
Text(text = name, color = MaterialTheme.colorScheme.onSurface)
}
is FollowListEvent -> {
noteEvent.title() ?: option.note.dTag()
}
else -> {
option.note.dTag()
}
}
Text(text = name, fontSize = Font14SP, color = MaterialTheme.colorScheme.onSurface)
}
is CommunityName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(),
) {
val it by observeNote(option.note, accountViewModel)
val it by observeNote(option.note, accountViewModel)
Text(text = "/n/${((it.note as? AddressableNote)?.dTag() ?: "")}", color = MaterialTheme.colorScheme.onSurface)
}
Text(text = "/n/${((it.note as? AddressableNote)?.dTag() ?: "")}", fontSize = Font14SP, color = MaterialTheme.colorScheme.onSurface)
}
is RelayName -> {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(),
Text(
text = option.name(),
fontSize = Font14SP,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
@Immutable
private data class IndexedFeedDefinition(
val originalIndex: Int,
val item: FeedDefinition,
)
private enum class FeedGroup(
@param:androidx.annotation.StringRes val labelRes: Int,
) {
FEEDS(R.string.feed_group_feeds),
HASHTAGS(R.string.feed_group_hashtags),
COMMUNITIES(R.string.feed_group_communities),
LISTS(R.string.feed_group_lists),
}
private fun groupFeedDefinitions(options: ImmutableList<FeedDefinition>): Map<FeedGroup, List<IndexedFeedDefinition>> {
val indexed = options.mapIndexed { index, item -> IndexedFeedDefinition(index, item) }
return indexed.groupBy { entry ->
when (entry.item.name) {
is HashtagName -> FeedGroup.HASHTAGS
is CommunityName -> FeedGroup.COMMUNITIES
is PeopleListName -> FeedGroup.LISTS
else -> FeedGroup.FEEDS
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun GroupedFeedFilterDialog(
title: String,
options: ImmutableList<FeedDefinition>,
onSelect: (Int) -> Unit,
onDismiss: () -> Unit,
onRenderItem: @Composable (FeedDefinition) -> Unit,
) {
val grouped = remember(options) { groupFeedDefinitions(options) }
Dialog(onDismissRequest = onDismiss) {
Surface(
shape = RoundedCornerShape(28.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
LazyColumn(
modifier = Modifier.padding(vertical = 20.dp),
) {
Text(
text = option.name(),
color = MaterialTheme.colorScheme.onSurface,
)
item {
Text(
text = title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 8.dp),
)
}
FeedGroup.entries.forEach { group ->
val items = grouped[group]
if (!items.isNullOrEmpty()) {
item {
GroupSection(
label = stringRes(group.labelRes),
items = items,
isChipLayout = group == FeedGroup.HASHTAGS,
onSelect = onSelect,
onRenderItem = onRenderItem,
)
}
}
}
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun GroupSection(
label: String,
items: List<IndexedFeedDefinition>,
isChipLayout: Boolean,
onSelect: (Int) -> Unit,
onRenderItem: @Composable (FeedDefinition) -> Unit,
) {
Surface(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
shape = RoundedCornerShape(16.dp),
color = MaterialTheme.colorScheme.surfaceContainerLow,
) {
Column {
Text(
text = label.uppercase(),
fontSize = Font12SP,
letterSpacing = 0.8.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 6.dp),
)
if (isChipLayout) {
FlowRow(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
items.forEach { entry ->
Surface(
modifier = Modifier.clickable { onSelect(entry.originalIndex) },
shape = RoundedCornerShape(18.dp),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
color = Color.Transparent,
) {
Text(
text = entry.item.name.name(),
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 7.dp),
)
}
}
}
Spacer(modifier = Modifier.height(4.dp))
} else {
items.forEach { entry ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.fillMaxWidth()
.clickable { onSelect(entry.originalIndex) }
.padding(horizontal = 16.dp, vertical = 6.dp),
) {
FeedIcon(
item = entry.item,
modifier = Size20Modifier,
)
Spacer(modifier = Modifier.padding(start = 12.dp))
Column(modifier = Modifier.weight(1f)) { onRenderItem(entry.item) }
}
}
}
}
}
}
@Composable
private fun FeedIcon(
item: FeedDefinition,
modifier: Modifier = Modifier,
) {
val icon =
when (item.code) {
is TopFilter.Global -> {
Icons.Outlined.Public
}
is TopFilter.AroundMe -> {
Icons.Outlined.LocationOn
}
is TopFilter.AllFollows -> {
Icons.Outlined.Groups
}
is TopFilter.AllUserFollows -> {
Icons.Outlined.Person
}
is TopFilter.DefaultFollows -> {
Icons.Outlined.Groups
}
is TopFilter.MuteList -> {
Icons.AutoMirrored.Outlined.VolumeOff
}
is TopFilter.Chess -> {
Icons.Outlined.Groups
}
is TopFilter.PeopleList -> {
Icons.AutoMirrored.Outlined.ViewList
}
else -> {
when (item.name) {
is GeoHashName -> Icons.Outlined.LocationOn
is RelayName -> Icons.Outlined.SensorDoor
is CommunityName -> Icons.Outlined.Groups
is PeopleListName -> Icons.AutoMirrored.Outlined.ViewList
else -> Icons.Outlined.Person
}
}
}
Icon(
imageVector = icon,
contentDescription = null,
modifier = modifier,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
@@ -634,6 +634,10 @@
<string name="app_notification_zaps_channel_message">%1$s satů</string>
<string name="app_notification_zaps_channel_message_from">Od %1$s</string>
<string name="app_notification_zaps_channel_message_for">pro %1$s</string>
<string name="app_notification_reply_label">Odpovědět</string>
<string name="app_notification_mark_read_label">Označit jako přečtené</string>
<string name="app_notification_dms_summary">Nové zprávy</string>
<string name="app_notification_zaps_summary">Nové zapsy</string>
<string name="reply_notify">Upozornit: </string>
<string name="channel_list_join_conversation">Připojit se ke konverzaci</string>
<string name="channel_list_user_or_group_id">ID uživatele nebo skupiny</string>
@@ -1043,6 +1047,13 @@
<string name="media_compression_quality_uncompressed">Bez komprese</string>
<string name="video_codec_h265_label">Použít kodek H.265/HEVC</string>
<string name="video_codec_h265_description">Lepší kvalita při menší velikosti souboru, ale ne všechna zařízení podporují přehrávání H.265.</string>
<string name="strip_metadata_label">Odstranit soukromá metadata</string>
<string name="strip_metadata_description">Pokusí se odstranit soukromá metadata z podporovaných mediálních souborů před nahráním</string>
<string name="metadata_strip_failed_title">Metadata nelze odstranit</string>
<string name="metadata_strip_failed_body">Tento formát souboru nepodporuje odstranění metadat. Mohou být obsaženy soukromé informace, jako je poloha a informace o zařízení. Přesto nahrát?</string>
<string name="metadata_strip_failed_upload">Přesto nahrát</string>
<string name="metadata_strip_failed_upload_cancelled">Nepodařilo se odstranit soukromá metadata z média. Nahrávání zrušeno.</string>
<string name="upload_cancelled">Nahrávání zrušeno</string>
<string name="edit_draft">Upravit koncept</string>
<string name="login_with_qr_code">Přihlášení pomocí QR kódu</string>
<string name="route">Trasa</string>
@@ -639,6 +639,10 @@ anz der Bedingungen ist erforderlich</string>
<string name="app_notification_zaps_channel_message">%1$s Sats</string>
<string name="app_notification_zaps_channel_message_from">Von %1$s</string>
<string name="app_notification_zaps_channel_message_for">für %1$s</string>
<string name="app_notification_reply_label">Antworten</string>
<string name="app_notification_mark_read_label">Als gelesen markieren</string>
<string name="app_notification_dms_summary">Neue Nachrichten</string>
<string name="app_notification_zaps_summary">Neue Zaps</string>
<string name="reply_notify">Benachrichtigen: </string>
<string name="channel_list_join_conversation">Unterhaltung beitreten</string>
<string name="channel_list_user_or_group_id">Benutzer- oder Gruppen-ID</string>
@@ -1048,6 +1052,13 @@ anz der Bedingungen ist erforderlich</string>
<string name="media_compression_quality_uncompressed">Ohne Kompression</string>
<string name="video_codec_h265_label">H.265/HEVC-Codec verwenden</string>
<string name="video_codec_h265_description">Bessere Qualität bei kleinerer Dateigröße, aber nicht alle Geräte unterstützen die H.265-Wiedergabe.</string>
<string name="strip_metadata_label">Private Metadaten entfernen</string>
<string name="strip_metadata_description">Versucht, private Metadaten aus unterstützten Mediendateien vor dem Hochladen zu entfernen</string>
<string name="metadata_strip_failed_title">Metadaten konnten nicht entfernt werden</string>
<string name="metadata_strip_failed_body">Dieses Dateiformat unterstützt das Entfernen von Metadaten nicht. Private Informationen wie Standort und Geräteinformationen können enthalten sein. Trotzdem hochladen?</string>
<string name="metadata_strip_failed_upload">Trotzdem hochladen</string>
<string name="metadata_strip_failed_upload_cancelled">Private Metadaten konnten nicht aus der Mediendatei entfernt werden. Hochladen abgebrochen.</string>
<string name="upload_cancelled">Hochladen abgebrochen</string>
<string name="edit_draft">Entwurf bearbeiten</string>
<string name="login_with_qr_code">Einloggen mit QR-Code</string>
<string name="route">Route</string>
@@ -638,6 +638,10 @@
<string name="app_notification_zaps_channel_message">%1$s satoshi</string>
<string name="app_notification_zaps_channel_message_from">Tőle: %1$s</string>
<string name="app_notification_zaps_channel_message_for">neki: %1$s</string>
<string name="app_notification_reply_label">Válasz</string>
<string name="app_notification_mark_read_label">Megjelölés olvasottként</string>
<string name="app_notification_dms_summary">Új üzenetek</string>
<string name="app_notification_zaps_summary">Új zap-ek</string>
<string name="reply_notify">Értesítés: </string>
<string name="channel_list_join_conversation">Csatlakozás a beszélgetéshez</string>
<string name="channel_list_user_or_group_id">Felhasználó- vagy csoport-azonosító</string>
@@ -1048,6 +1052,13 @@
<string name="media_compression_quality_uncompressed">Tömörítetlen</string>
<string name="video_codec_h265_label">H.265/HEVC-kodek használata</string>
<string name="video_codec_h265_description">Jobb minőség kisebb fájlméret mellett, de nem minden eszköz támogatja a H.265 lejátszást.</string>
<string name="strip_metadata_label">Privát metaadatok törlése</string>
<string name="strip_metadata_description">Kísérletek a támogatott médiafájlokból a privát metaadatok eltávolítására a feltöltés előtt</string>
<string name="metadata_strip_failed_title">A metaadatok nem távolíthatók el</string>
<string name="metadata_strip_failed_body">Ez a fájlformátum nem támogatja a metaadatok eltávolítását. Lehet, hogy a fájl tartalmaz személyes adatokat, például hely- és eszközadatokat. Mindenképp fel akarja tölteni?</string>
<string name="metadata_strip_failed_upload">Feltöltés mindenképp</string>
<string name="metadata_strip_failed_upload_cancelled">Nem sikerült eltávolítani a médiafájlokból a privát metaadatokat. Feltöltés megszakítva.</string>
<string name="upload_cancelled">Feltölrés megszakítva</string>
<string name="edit_draft">Piszkozat szerkesztése</string>
<string name="login_with_qr_code">Bejelentkezés QR-kóddal</string>
<string name="route">Útvonal</string>
@@ -1152,6 +1163,7 @@
<string name="dm_relays_not_found_examples2">Jó választási lehetőségek:\n - auth.nostr1.com (ingyenes)\n - inbox.nostr.wine (fizetős)\n - relay.0xchat.com (ingyenes)</string>
<string name="dm_relays_not_found_editing">Adjon hozzá 13 átjátszót, hogy privát postafiókként szolgáljanak. A bejövő privát üzenetek átjátszóinak el kell fogadniuk bármely üzenetet bárkitől, de azok letöltését csak Ön engedélyezheti.</string>
<string name="dm_relays_not_found_create_now">Beállítás most</string>
<string name="recipient_missing_dm_relays">Nem találhatók a beérkező közvetlen üzenetek átjátszói. Az üzenetek kézbesítése addig nem lehetséges, amíg be nem állítja az átjátszók listáját.</string>
<string name="search_relays_title">Keresési átjátszók</string>
<string name="search_relays_not_found">Keresési átjátszók beállítása</string>
<string name="search_relays_not_found_description">A kifejezetten a kereséshez és a felhasználói címkézéshez tervezett átjátszólista létrehozása javítani fogja ezeket az eredményeket.</string>
@@ -634,6 +634,10 @@
<string name="app_notification_zaps_channel_message">%1$s sats</string>
<string name="app_notification_zaps_channel_message_from">De %1$s</string>
<string name="app_notification_zaps_channel_message_for">por %1$s</string>
<string name="app_notification_reply_label">Responder</string>
<string name="app_notification_mark_read_label">Marcar como lida</string>
<string name="app_notification_dms_summary">Novas mensagens</string>
<string name="app_notification_zaps_summary">Novos zaps</string>
<string name="reply_notify">Notificar: </string>
<string name="channel_list_join_conversation">Entrar na conversa</string>
<string name="channel_list_user_or_group_id">ID do usuário ou grupo</string>
@@ -1043,6 +1047,13 @@
<string name="media_compression_quality_uncompressed">Sem compressão</string>
<string name="video_codec_h265_label">Usar codec H.265/HEVC</string>
<string name="video_codec_h265_description">Melhor qualidade em arquivos menores, mas nem todos os dispositivos suportam reprodução em H.265.</string>
<string name="strip_metadata_label">Remover metadados privados</string>
<string name="strip_metadata_description">Tenta remover metadados privados de arquivos de mídia compatíveis antes do envio</string>
<string name="metadata_strip_failed_title">Não foi possível remover os metadados</string>
<string name="metadata_strip_failed_body">Este formato de arquivo não suporta a remoção de metadados. Informações privadas como localização e dados do dispositivo podem estar incluídas. Enviar mesmo assim?</string>
<string name="metadata_strip_failed_upload">Enviar mesmo assim</string>
<string name="metadata_strip_failed_upload_cancelled">Não foi possível remover metadados privados da mídia. Envio cancelado.</string>
<string name="upload_cancelled">Envio cancelado</string>
<string name="edit_draft">Editar rascunho</string>
<string name="login_with_qr_code">Entrar com Código QR</string>
<string name="route">Rota</string>
@@ -633,6 +633,10 @@
<string name="app_notification_zaps_channel_message">%1$s sats</string>
<string name="app_notification_zaps_channel_message_from">Från %1$s</string>
<string name="app_notification_zaps_channel_message_for">till %1$s</string>
<string name="app_notification_reply_label">Svara</string>
<string name="app_notification_mark_read_label">Markera som läst</string>
<string name="app_notification_dms_summary">Nya meddelanden</string>
<string name="app_notification_zaps_summary">Nya zaps</string>
<string name="reply_notify">Meddela: </string>
<string name="channel_list_join_conversation">Gå med i konversation</string>
<string name="channel_list_user_or_group_id">Användare eller grupp ID</string>
@@ -1042,6 +1046,13 @@
<string name="media_compression_quality_uncompressed">Okomprimerad</string>
<string name="video_codec_h265_label">Använd H.265/HEVC-codec</string>
<string name="video_codec_h265_description">Bättre kvalitet med mindre filstorlek, men inte alla enheter stöder H.265-uppspelning.</string>
<string name="strip_metadata_label">Ta bort privat metadata</string>
<string name="strip_metadata_description">Försöker ta bort privat metadata från mediefiler som stöds innan uppladdning</string>
<string name="metadata_strip_failed_title">Metadata kunde inte tas bort</string>
<string name="metadata_strip_failed_body">Detta filformat stöder inte borttagning av metadata. Privat information som plats och enhetsinformation kan ingå. Ladda upp ändå?</string>
<string name="metadata_strip_failed_upload">Ladda upp ändå</string>
<string name="metadata_strip_failed_upload_cancelled">Kunde inte ta bort privat metadata från media. Uppladdning avbruten.</string>
<string name="upload_cancelled">Uppladdning avbruten</string>
<string name="edit_draft">Redigera utkast</string>
<string name="login_with_qr_code">Logga in med QR-kod</string>
<string name="route">Rutt</string>
@@ -638,6 +638,10 @@
<string name="app_notification_zaps_channel_message">%1$s聪</string>
<string name="app_notification_zaps_channel_message_from">来自 %1$s</string>
<string name="app_notification_zaps_channel_message_for">为 %1$s</string>
<string name="app_notification_reply_label">回复</string>
<string name="app_notification_mark_read_label">标记为已读</string>
<string name="app_notification_dms_summary">新信息</string>
<string name="app_notification_zaps_summary">新打闪</string>
<string name="reply_notify">通知:</string>
<string name="channel_list_join_conversation">加入对话</string>
<string name="channel_list_user_or_group_id">用户或群组 ID</string>
@@ -1034,6 +1038,12 @@
<string name="could_not_prepare_header">无法准备头部信息:%1$s</string>
<string name="compression_cancelled">压缩已取消</string>
<string name="compression_returned_null">压缩返回的文件失败</string>
<string name="encrypt_files_label">加密文件</string>
<string name="encrypt_files_description">上传前为了隐私加密文件。有些服务器可能不接受免费账户的加密文件。</string>
<string name="failed_to_upload_encrypted_media_title">加密上传失败</string>
<string name="failed_to_upload_encrypted_media_message">许多服务器不接受免费账户的加密文件。您可以不加密重试。</string>
<string name="retry_without_encryption">不加密重试</string>
<string name="upload_without_encryption_warning">警告:没有加密,任何有文件链接的人都可以看到内容。</string>
<string name="media_compression_quality_label">媒体质量</string>
<string name="media_compression_quality_explainer">选择「低质量」来将你的媒体文件压缩到较小体积,或者选择「高质量」来将你的媒体文件压缩到较大体积。</string>
<string name="media_compression_quality_low">低质量</string>
@@ -1042,6 +1052,13 @@
<string name="media_compression_quality_uncompressed">未压缩</string>
<string name="video_codec_h265_label">使用 H.265/HEVC 编解码器</string>
<string name="video_codec_h265_description">文件较小而质量更佳,但不是所有设备都支持 H.265 播放。</string>
<string name="strip_metadata_label">删除私密元数据</string>
<string name="strip_metadata_description">尝试在上传之前从支持的媒体文件中删除私密元数据</string>
<string name="metadata_strip_failed_title">无法删除元数据</string>
<string name="metadata_strip_failed_body">此文件格式不支持删除元数据。可以包含位置和设备信息等私人信息。仍然要上传吗?</string>
<string name="metadata_strip_failed_upload">仍然上传</string>
<string name="metadata_strip_failed_upload_cancelled">从媒体删除私密元数据失败。上传取消。</string>
<string name="upload_cancelled">上传已取消</string>
<string name="edit_draft">编辑草稿</string>
<string name="login_with_qr_code">使用二维码登录</string>
<string name="route">路径</string>
@@ -1076,6 +1093,9 @@
<string name="wallet_incoming">已收到</string>
<string name="wallet_outgoing">已发送</string>
<string name="wallet_refresh">刷新</string>
<string name="wallet_filter_all">全部</string>
<string name="wallet_filter_zaps">打闪</string>
<string name="wallet_filter_non_zaps">非打闪</string>
<string name="route_security_filters">安全滤镜</string>
<string name="route_import_follows">导入关注</string>
<string name="new_post">新帖子</string>
@@ -1143,6 +1163,7 @@
<string name="dm_relays_not_found_examples2">示例:\n - auth.nostr1.com (免费)\n - inbox.nostr.wine (付费)\n - revisy.0xchat.com (免费)</string>
<string name="dm_relays_not_found_editing">设置 1 ~ 3 个私人收件箱中继。需要确保这些收件箱中继能够接受来自任何人的任何私信消息,但只允许您读取这些消息。</string>
<string name="dm_relays_not_found_create_now">立即设置</string>
<string name="recipient_missing_dm_relays">未找到私信收件箱中继。配置中继列表前无法传送消息。</string>
<string name="search_relays_title">搜索中继</string>
<string name="search_relays_not_found">设置你的搜索中继</string>
<string name="search_relays_not_found_description">通过创建专用于关键词和标签检索的中继列表能够改善搜索结果。</string>
@@ -1558,9 +1579,51 @@
<string name="select_all">全选</string>
<string name="uptime">%1$d%% 运行时间</string>
<string name="namecoin_settings">Namecoin 设置</string>
<string name="event_sync_title">中继同步</string>
<string name="event_sync_section">中继同步</string>
<string name="event_sync_section_explainer">在所有已知的中继重新发布您的事件,以保持您的发件箱、收件箱和私信中继是最新的。 需要 Wi-Fi - 这可能使用大量数据。</string>
<string name="event_sync_open_button">打开中继同步…</string>
<string name="event_sync_what_happens_title">这是做什么</string>
<string name="event_sync_what_happens_body">此工具扫描您的应用已看到的每一个中继并将您的事件重新分发到正确的目的地: </string>
<string name="event_sync_step1">下载您编写的所有事件并发送到您的发件箱中继中。</string>
<string name="event_sync_step2">下载所有提到您的事件并发送到您的收件箱中继中。</string>
<string name="event_sync_step3">下载发送给您的所有私信并发送到您的私信中继。</string>
<string name="event_sync_wifi_warning">⚠ 您似乎在按流量计费或移动网络连接上。此操作可能传输大量数据。启动前请连接到 Wi-Fi。</string>
<string name="event_sync_mobile_data_dialog_title">使用移动数据?</string>
<string name="event_sync_start">开始同步</string>
<string name="event_sync_start_anyway">仍要启动(移动数据)</string>
<string name="event_sync_pause">暂停</string>
<string name="event_sync_resume">恢复</string>
<string name="event_sync_start_over">重来</string>
<string name="event_sync_cancel">取消</string>
<string name="event_sync_relays_progress">中继: %1$d / %2$d</string>
<string name="event_sync_events_sent">已重新分发事件:%1$d 个新事件,共 %2$d 个已发送事件和 %3$d 个已接收事件</string>
<string name="event_sync_paused_title">同步已暂停</string>
<string name="event_sync_paused_body">完成了 %2$d 个中继中的 %1$d 个中继 — 迄今为止重新分发了 %3$d 个事件。轻按“恢复“继续。</string>
<string name="event_sync_done_title">同步完成</string>
<string name="event_sync_done_sent">转发了 %1$d 个事件到目标中继,共接收到 %2$d 个事件。</string>
<string name="event_sync_done_accepted">%1$d 个事件被目标中继接受为新事件。</string>
<string name="event_sync_done_duration">在 %1$d 秒内完成。</string>
<string name="event_sync_error_title">同步错误</string>
<string name="event_sync_sending_to">发送到</string>
<string name="event_sync_outbox_relays">发件箱</string>
<string name="event_sync_inbox_relays">收件箱</string>
<string name="event_sync_dm_relays">私信</string>
<string name="event_sync_activity_log">当前正在检查 (%1$d 个中继)</string>
<string name="event_sync_activity_log_finished">已完成 (%1$d 个中继)</string>
<string name="event_sync_log_sent">已发送 %1$s</string>
<string name="event_sync_log_recv">已收到 %1$s</string>
<string name="event_sync_log_new">新增 %1$s</string>
<string name="event_sync_no_events">没有事件</string>
<string name="ots_explorer_settings">比特币资源管理器 (OTS)</string>
<string name="events">事件</string>
<string name="dms">私信</string>
<string name="profiles">个人资料</string>
<string name="relay_settings_lower">中继设置</string>
<string name="last_seen">上次看见在 %1$s 秒前</string>
<string name="event_sync_less_than_until">&lt;%1$s</string>
<string name="event_sync_status_connecting">连接中</string>
<string name="event_sync_status_downloading">下载中</string>
<string name="event_sync_status_error">错误</string>
<string name="event_sync_status_completed">已完成</string>
</resources>
+5 -1
View File
@@ -1545,7 +1545,11 @@
<string name="my_lists_and_sets">My Lists/Sets</string>
<string name="my_lists">My Lists</string>
<string name="people_list_label">Users</string>
<string name="select_list_to_filter">Select a list to filter the feed</string>
<string name="select_list_to_filter">Select an option to filter the feed</string>
<string name="feed_group_feeds">Feeds</string>
<string name="feed_group_hashtags">Hashtags</string>
<string name="feed_group_communities">Communities</string>
<string name="feed_group_lists">Lists</string>
<string name="temporary_account">Log off on device lock</string>
<string name="private_message">Private Message</string>