diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt index 35566c5..2fd5abc 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -5,8 +5,8 @@ import android.os.Build import android.util.Log import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.ReturnCode -import kotlinx.coroutines.delay import kotlinx.serialization.Serializable +import org.json.JSONObject import java.io.File import java.time.LocalDateTime import java.time.format.DateTimeFormatter.ISO_DATE_TIME @@ -40,8 +40,46 @@ data class AppSettings( DARK, } + fun toJSONObject(): JSONObject { + return JSONObject( + mapOf( + "audioRecorderSettings" to audioRecorderSettings.toJSONObject(), + "hasSeenOnboarding" to hasSeenOnboarding, + "showAdvancedSettings" to showAdvancedSettings, + "theme" to theme.name, + ) + ) + } + + fun exportToString(): String { + return JSONObject( + mapOf( + "_meta" to mapOf( + "version" to 1, + "date" to LocalDateTime.now().format(ISO_DATE_TIME), + "app" to "app.myzel394.alibi", + ), + "data" to toJSONObject(), + ) + ).toString(0) + } + companion object { fun getDefaultInstance(): AppSettings = AppSettings() + + fun fromJSONObject(data: JSONObject): AppSettings { + return AppSettings( + audioRecorderSettings = AudioRecorderSettings.fromJSONObject(data.getJSONObject("audioRecorderSettings")), + hasSeenOnboarding = data.getBoolean("hasSeenOnboarding"), + showAdvancedSettings = data.getBoolean("showAdvancedSettings"), + theme = Theme.valueOf(data.getString("theme")), + ) + } + + fun fromExportedString(data: String): AppSettings { + val json = JSONObject(data) + return fromJSONObject(json.getJSONObject("data")) + } } } @@ -292,6 +330,20 @@ data class AudioRecorderSettings( return supportedFormats.contains(outputFormat) } + fun toJSONObject(): JSONObject { + return JSONObject( + mapOf( + "maxDuration" to maxDuration, + "intervalDuration" to intervalDuration, + "forceExactMaxDuration" to forceExactMaxDuration, + "bitRate" to bitRate, + "samplingRate" to samplingRate, + "outputFormat" to outputFormat, + "encoder" to encoder, + ) + ) + } + companion object { fun getDefaultInstance(): AudioRecorderSettings = AudioRecorderSettings() val EXAMPLE_MAX_DURATIONS = listOf( @@ -398,5 +450,23 @@ data class AudioRecorderSettings( } } }).toMap() + + fun fromJSONObject(data: JSONObject): AudioRecorderSettings { + return AudioRecorderSettings( + maxDuration = data.getLong("maxDuration"), + intervalDuration = data.getLong("intervalDuration"), + forceExactMaxDuration = data.getBoolean("forceExactMaxDuration"), + bitRate = data.getInt("bitRate"), + samplingRate = data.optInt("samplingRate", -1).let { + if (it == -1) null else it + }, + outputFormat = data.optInt("outputFormat", -1).let { + if (it == -1) null else it + }, + encoder = data.optInt("encoder", -1).let { + if (it == -1) null else it + }, + ) + } } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/ImportExport.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/ImportExport.kt new file mode 100644 index 0000000..830d550 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/ImportExport.kt @@ -0,0 +1,165 @@ +package app.myzel394.alibi.ui.components.SettingsScreen.atoms + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.Upload +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarVisuals +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import app.myzel394.alibi.R +import app.myzel394.alibi.dataStore +import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.ui.utils.rememberFileSaverDialog +import app.myzel394.alibi.ui.utils.rememberFileSelectorDialog +import kotlinx.coroutines.launch +import java.io.File + +@Composable +fun ImportExport( + snackbarHostState: SnackbarHostState, +) { + val context = LocalContext.current + + val scope = rememberCoroutineScope() + val dataStore = LocalContext.current.dataStore + val settings = dataStore + .data + .collectAsState(initial = AppSettings.getDefaultInstance()) + .value + + var settingsToBeImported by remember { mutableStateOf(null) } + + val saveFile = rememberFileSaverDialog("application/json") + val openFile = rememberFileSelectorDialog { uri -> + val file = File.createTempFile("alibi_settings", ".json") + + context.contentResolver.openInputStream(uri)!!.use { + it.copyTo(file.outputStream()) + } + val rawContent = file.readText() + + settingsToBeImported = AppSettings.fromExportedString(rawContent) + } + + if (settingsToBeImported != null) { + val successMessage = stringResource(R.string.ui_settings_option_import_success) + + AlertDialog( + onDismissRequest = { + settingsToBeImported = null + }, + title = { + Text(stringResource(R.string.ui_settings_option_import_label)) + }, + text = { + Text(stringResource(R.string.ui_settings_option_import_dialog_text)) + }, + icon = { + Icon( + Icons.Default.Download, + contentDescription = null, + ) + }, + confirmButton = { + Button( + onClick = { + scope.launch { + dataStore.updateData { + settingsToBeImported!! + } + settingsToBeImported = null + + snackbarHostState.showSnackbar( + message = successMessage, + withDismissAction = true, + duration = SnackbarDuration.Short, + ) + } + + }, + ) { + Icon( + Icons.Default.CheckCircle, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.ui_settings_option_import_dialog_confirm)) + } + }, + dismissButton = { + Button( + onClick = { + settingsToBeImported = null + }, + colors = ButtonDefaults.textButtonColors(), + ) { + Text(stringResource(R.string.dialog_close_cancel_label)) + } + }, + ) + } + + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Button( + onClick = { + openFile("application/json") + }, + colors = ButtonDefaults.filledTonalButtonColors(), + ) { + Icon( + Icons.Default.Download, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.ui_settings_option_import_label)) + } + Button( + onClick = { + val rawContent = settings.exportToString() + + val tempFile = File.createTempFile("alibi_settings", ".json") + tempFile.writeText(rawContent) + + saveFile(tempFile, "alibi_settings.json") + }, + colors = ButtonDefaults.filledTonalButtonColors(), + ) { + Icon( + Icons.Default.Upload, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.ui_settings_option_export_label)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt index 0db7196..1e121bf 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt @@ -64,7 +64,7 @@ fun AudioRecorder( try { val file = audioRecorder.lastRecording!!.concatenateFiles() - saveFile(file) + saveFile(file, file.name) } catch (error: Exception) { Log.getStackTraceString(error) } finally { @@ -165,7 +165,7 @@ fun AudioRecorder( } ) }, - ) {padding -> + ) { padding -> Box( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt index 13294c5..420c117 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt @@ -1,6 +1,7 @@ package app.myzel394.alibi.ui.screens import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -15,7 +16,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text @@ -39,6 +42,7 @@ import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY import app.myzel394.alibi.ui.components.SettingsScreen.atoms.BitrateTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.EncoderTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ForceExactMaxDurationTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ImportExport import app.myzel394.alibi.ui.components.SettingsScreen.atoms.InAppLanguagePicker import app.myzel394.alibi.ui.components.SettingsScreen.atoms.IntervalDurationTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.MaxDurationTile @@ -63,7 +67,21 @@ fun SettingsScreen( ) Scaffold( - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + snackbarHost = { + SnackbarHost( + hostState = snackbarHostState, + snackbar = { + Snackbar( + snackbarData = it, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + actionColor = MaterialTheme.colorScheme.onPrimaryContainer, + actionContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + dismissActionContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + ) + }, topBar = { LargeTopAppBar( title = { @@ -128,16 +146,26 @@ fun SettingsScreen( ForceExactMaxDurationTile() InAppLanguagePicker() AnimatedVisibility(visible = settings.showAdvancedSettings) { - Column { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + Column { + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 32.dp) + ) + BitrateTile() + SamplingRateTile() + EncoderTile(snackbarHostState = snackbarHostState) + OutputFormatTile() + } Divider( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 32.dp) + .fillMaxWidth(0.5f) ) - BitrateTile() - SamplingRateTile() - EncoderTile(snackbarHostState = snackbarHostState) - OutputFormatTile() + ImportExport(snackbarHostState = snackbarHostState) } } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt index 8838d04..cd50b80 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/file.kt @@ -1,33 +1,53 @@ package app.myzel394.alibi.ui.utils +import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import java.io.File @Composable -fun rememberFileSaverDialog(mimeType: String): ((File) -> Unit) { +fun rememberFileSaverDialog(mimeType: String): ((File, String) -> Unit) { val context = LocalContext.current var file = remember { mutableStateOf(null) } - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(mimeType)) { - it?.let { - context.contentResolver.openOutputStream(it)?.use { outputStream -> - file.value!!.inputStream().use { inputStream -> - inputStream.copyTo(outputStream) + val launcher = + rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(mimeType)) { + it?.let { + context.contentResolver.openOutputStream(it)?.use { outputStream -> + file.value!!.inputStream().use { inputStream -> + inputStream.copyTo(outputStream) + } } } + + file.value = null + } + + return { it, name -> + file.value = it + launcher.launch(name ?: it.name) + } +} + +@Composable +fun rememberFileSelectorDialog( + callback: (Uri) -> Unit +): ((String) -> Unit) { + val launcher = + rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { + if (it != null) { + callback(it) + } } - file.value = null - } - - return { - file.value = it - launcher.launch(it.name) + return { mimeType -> + launcher.launch(arrayOf(mimeType)) } } diff --git a/app/src/main/res/values-b+de+DE/strings.xml b/app/src/main/res/values-b+de+DE/strings.xml deleted file mode 100644 index b40bab7..0000000 --- a/app/src/main/res/values-b+de+DE/strings.xml +++ /dev/null @@ -1,67 +0,0 @@ - - Alibi - - Abbrechen - OK - Fortsetzten - - %s KB/s - Bitte eine gültige Nummer eingeben - Bitte gib eine Nummer zwischen%s und %sein - Bitte gib eine größere Zahl als %s ein - - Recorder - Zeigt den aktuellen Status der Aufnahme an - - Berechtigung verweigert - Bitte erteile die Berechtigung, um fortzufahren - Du wirst zu den Einstellungen weitergeleitet, um dort die Berechtigung zu erteilen. - - Aufnahme starten - Aufnahme vom %s speichern - Löschen - Aufnahme Löschen? - Bist du sicher, dass du diese Aufnahme löschen möchtest? - Aufnahme pausieren - Aufnahme fortsetzten - Aufnahme speichern - Alibi wird im Hintergrund weiterhin aufnehmen und die letzten %s Minuten auf Wunsch speichern - Bearbeiten - Audio wird bearbeitet, Alibi nicht schließen! Du wirst automatisch aufgefordert, die Datei zu speichern, wenn diese fertig bearbeitet ist - Aufnahme läuft - Alibi nimmt im Hintergrund weiter auf - - Wilkommen zu Alibi! - Alibi funktioniert wird eine Dashcam für dein Handy. Es ermöglicht dir, Ton im Hintergrund kontinuierlich aufzunehmen und die letzten 30 Minuten auf Wunsch zu speichern. - Du bist für die Nutzung dieser App verantwortlich! - Alibi übernimmt keine Verantwortung für die Nutzung dieser App. Du trägst die alleinige Verantwortung. Die Nutzung erfolgt auf eigene Gefahr. - Alibi Starten - - Einstellungen - Erweiterte Einstellungen - Aufnahme läuft - Änderungen an den Einstellungen werden erst nach dem Neustart der Aufnahme wirksam - Maximale Aufnahmezeit - Setze die maximale Aufnahmezeit fest - Intervall-Länge - Dauer eines einzelnen Intervalls. Alibi nimmt in Intervallen auf und speichert diese einzeln ab. Die Intervalle werden automatisch zusammengefügt, wenn du eine Aufnahme speicherst. Die ältesten Intervalle werden automatisch gelöscht, wenn die maximale Aufnahmezeit erreicht ist. - Erzwinge exakte Länge - Erzwingt, dass die Ausgabedatei genau für die angegebene Dauer zugeschnitten wird. Wenn dies deaktiviert ist, kann die Ausgabedatei etwas länger sein, da mehrere Intervalle zusammengefasst werden. - Bitrate - Eine höhere Bitrate bedeutet bessere Qualität, aber auch eine größere Dateigröße - Leg die Bitrate fest - Ausgabeformat - Abtastrate - Leg fest, wie oft pro Sekunde der Ton abgetastet werden soll. Eine höhere Abtastrate bedeutet bessere Qualität, aber auch eine größere Dateigröße - Leg die Abtastrate fest - Encoder - Das Ausgabeformat wurde geändert, da das aktuelle mit diesem Encoder inkompatibel war - Auto - Aufnahme pausiert - Audio-Aufnahme wurde pausiert - Es ist ein Fehler aufgetreten - Alibi stieß bei der Aufnahme auf einen Fehler. Soll die Aufnahme gespeichert werden? - Sprache - Ändern - diff --git a/app/src/main/res/values-b+zh+CN/strings.xml b/app/src/main/res/values-b+zh+CN/strings.xml deleted file mode 100644 index 87a845c..0000000 --- a/app/src/main/res/values-b+zh+CN/strings.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - Alibi - 收起底部动作条 - 关闭底部动作条 - 拖动手柄 - 展开底部动作条 - 接听 - 视频通话 - 拒接 - 挂断 - 来电 - 正在通话 - 正在过滤来电 - 取消 - 关闭导航菜单 - 关闭工作表 - 已收起 - 继续 - 输入日期 - 输入日期:%1$s - 日期不符合预期格式:%1$s - 不允许的日期:%1$s - 日期超出预期年份范围 %1$s - %2$s - 日期 - - 选择日期 - 选定的日期 - 当前的选择:%1$s - 切换到年份:%1$s - - 滚动显示之前的年份 - 滚动显示之后的年份 - 切换到日历输入模式 - 滑动可选择年份,点按可切换回选择日期 - 切换到文本字段输入模式 - 转到下个月 - 转到上个月 - 切换以选择年份 - 选择日期 - 今天 - 年份选择器可见 - 输入的日期范围无效 - 输入日期 - 在范围内 - 结束日期 - 滚动显示下个月 - 滚动显示上个月 - 开始日期 - 选择日期 - 输入无效 - 弹出式窗口 - 对话框 - 取消 - OK - 下拉菜单 - 已展开 - 请输入有效数字 - 请输入一个大于 %s 的数字 - 请输入一个在 %s 和 %s 之间的数字 - %s KB/s - 进行中 - 部分选中 - 导航菜单 - 未选择 - 显示当前录制状态 - 录音机 - 已关闭 - Ok - 已开启 - 范围终点 - 范围起点 - 最小值 - 最大值 - 清除输入 - 删除最后一条输入 - h - 小时 - m - 分钟 - s - - 最多选择 %1$d 个选项 - 至少选择 %1$d 个选项 - 搜索 - 已选择 - 关闭 - 999+ - 以下是搜索建议 - 开关 - 标签页 - 百分之 %1$d。 - 上午 - - %1$d 小时 - 选择小时 - %1$d 点 - 表示小时 - - 选择分钟 - %1$d 分钟 - 表示分钟 - 选择上午或下午 - 下午 - 显示提示 - 提示 - 您确定要删除此录音吗? - 确定删除记录? - 删除 - 暂停录制 - 恢复录制 - 保存自 %s 起的录音 - 保存录音 - 正在处理音频,请勿关闭Alibi! 一旦文件准备好,您将自动收到保存文件的提示 - 处理中 - 根据您的要求,Alibi 将继续在后台录制并存储最近 %s 分钟的录音 - 开始录音 - 音频录制已暂停 - 录制暂停 - Alibi会在后台持续记录 - 录制中 - 您将被重定向到应用程序设置页面,在那里可以授予相关权限 - 请授予权限以继续 - 无法获取权限 - 高级设置 - 您的更改将在下次开始录音时应用 - 您正在录音... - 更高的比特率意味着更好的音频质量以及更大的储存空间占用 - 为音频录制设置比特率 - 比特率 - 编码器 - 强制将输出文件拆分为指定的时长。如果禁用此选项,由于批量音频样本被一同编码,输出文件可能会略长一些。 - 强制指定时长 - 按照指定的时长录制一个单独的批次。Alibi会记录多个批次并删除最旧的批次。在导出音频时,所有批次将被合并到一起。 - 批处理时间 - 设置录音的最大时长 - 最大持续时间 - 输出格式 - 定义每秒从音频信号中提取的采样数 - 设置采样率 - 采样率 - 设置 - 自动 - Alibi就像你手机上的行车记录仪一样。它会不断地录制音频,并在您需要时保存最近的30分钟录音。 - 欢迎使用Alibi! - Alibi不对该应用程序的使用负任何责任。您在使用时需自行承担使用风险。 - 您需要自行承担使用该应用程序所带来的风险和后果。 - 开始使用Alibi - \ No newline at end of file diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 3255468..b40bab7 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -1,18 +1,23 @@ - - + Alibi + Abbrechen OK Fortsetzten + %s KB/s Bitte eine gültige Nummer eingeben Bitte gib eine Nummer zwischen%s und %sein - Please enter a number greater than %s + Bitte gib eine größere Zahl als %s ein + Recorder Zeigt den aktuellen Status der Aufnahme an + Berechtigung verweigert Bitte erteile die Berechtigung, um fortzufahren Du wirst zu den Einstellungen weitergeleitet, um dort die Berechtigung zu erteilen. + Aufnahme starten Aufnahme vom %s speichern Löschen @@ -26,11 +31,13 @@ Audio wird bearbeitet, Alibi nicht schließen! Du wirst automatisch aufgefordert, die Datei zu speichern, wenn diese fertig bearbeitet ist Aufnahme läuft Alibi nimmt im Hintergrund weiter auf + Wilkommen zu Alibi! Alibi funktioniert wird eine Dashcam für dein Handy. Es ermöglicht dir, Ton im Hintergrund kontinuierlich aufzunehmen und die letzten 30 Minuten auf Wunsch zu speichern. Du bist für die Nutzung dieser App verantwortlich! Alibi übernimmt keine Verantwortung für die Nutzung dieser App. Du trägst die alleinige Verantwortung. Die Nutzung erfolgt auf eigene Gefahr. Alibi Starten + Einstellungen Erweiterte Einstellungen Aufnahme läuft @@ -57,9 +64,4 @@ Alibi stieß bei der Aufnahme auf einen Fehler. Soll die Aufnahme gespeichert werden? Sprache Ändern - Import Settings - Export Settings - Are you sure you want to import these settings? Your current settings will be overwritten! - Import settings - Settings have been imported successfully! diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 132b236..385cc83 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,65 +1,149 @@ - - + + Alibi + 收起底部动作条 + 关闭底部动作条 + 拖动手柄 + 展开底部动作条 + 接听 + 视频通话 + 拒接 + 挂断 + 来电 + 正在通话 + 正在过滤来电 + 取消 + 关闭导航菜单 + 关闭工作表 + 已收起 + 继续 + 输入日期 + 输入日期:%1$s + 日期不符合预期格式:%1$s + 不允许的日期:%1$s + 日期超出预期年份范围 %1$s - %2$s + 日期 + + 选择日期 + 选定的日期 + 当前的选择:%1$s + 切换到年份:%1$s + + 滚动显示之前的年份 + 滚动显示之后的年份 + 切换到日历输入模式 + 滑动可选择年份,点按可切换回选择日期 + 切换到文本字段输入模式 + 转到下个月 + 转到上个月 + 切换以选择年份 + 选择日期 + 今天 + 年份选择器可见 + 输入的日期范围无效 + 输入日期 + 在范围内 + 结束日期 + 滚动显示下个月 + 滚动显示上个月 + 开始日期 + 选择日期 + 输入无效 + 弹出式窗口 + 对话框 取消 OK - 继续 - %s KB/s + 下拉菜单 + 已展开 请输入有效数字 - Please enter a number between %s and %s - Please enter a number greater than %s - 录音机 + 请输入一个大于 %s 的数字 + 请输入一个在 %s 和 %s 之间的数字 + %s KB/s + 进行中 + 部分选中 + 导航菜单 + 未选择 显示当前录制状态 - 无法获取权限 - 请授予权限以继续 - 您将被重定向到应用程序设置页面,在那里可以授予相关权限 - 开始录音 - Save Recording from %s - 删除 - 确定删除记录? + 录音机 + 已关闭 + Ok + 已开启 + 范围终点 + 范围起点 + 最小值 + 最大值 + 清除输入 + 删除最后一条输入 + h + 小时 + m + 分钟 + s + + 最多选择 %1$d 个选项 + 至少选择 %1$d 个选项 + 搜索 + 已选择 + 关闭 + 999+ + 以下是搜索建议 + 开关 + 标签页 + 百分之 %1$d。 + 上午 + + %1$d 小时 + 选择小时 + %1$d 点 + 表示小时 + + 选择分钟 + %1$d 分钟 + 表示分钟 + 选择上午或下午 + 下午 + 显示提示 + 提示 您确定要删除此录音吗? + 确定删除记录? + 删除 暂停录制 恢复录制 + 保存自 %s 起的录音 保存录音 - Alibi will continue recording in the background and store the last %s minutes at your request - 处理中 正在处理音频,请勿关闭Alibi! 一旦文件准备好,您将自动收到保存文件的提示 - 录制中 + 处理中 + 根据您的要求,Alibi 将继续在后台录制并存储最近 %s 分钟的录音 + 开始录音 + 音频录制已暂停 + 录制暂停 Alibi会在后台持续记录 - 欢迎使用Alibi! - Alibi就像你手机上的行车记录仪一样。它会不断地录制音频,并在您需要时保存最近的30分钟录音。 - 您需要自行承担使用该应用程序所带来的风险和后果。 - Alibi不对该应用程序的使用负任何责任。您在使用时需自行承担使用风险。 - 开始使用Alibi - 设置 + 录制中 + 您将被重定向到应用程序设置页面,在那里可以授予相关权限 + 请授予权限以继续 + 无法获取权限 高级设置 - 您正在录音... 您的更改将在下次开始录音时应用 - 最大持续时间 - 设置录音的最大时长 - 批处理时间 - 按照指定的时长录制一个单独的批次。Alibi会记录多个批次并删除最旧的批次。在导出音频时,所有批次将被合并到一起。 - 强制指定时长 - 强制将输出文件拆分为指定的时长。如果禁用此选项,由于批量音频样本被一同编码,输出文件可能会略长一些。 - 比特率 + 您正在录音... 更高的比特率意味着更好的音频质量以及更大的储存空间占用 为音频录制设置比特率 + 比特率 + 编码器 + 强制将输出文件拆分为指定的时长。如果禁用此选项,由于批量音频样本被一同编码,输出文件可能会略长一些。 + 强制指定时长 + 按照指定的时长录制一个单独的批次。Alibi会记录多个批次并删除最旧的批次。在导出音频时,所有批次将被合并到一起。 + 批处理时间 + 设置录音的最大时长 + 最大持续时间 输出格式 - 采样率 定义每秒从音频信号中提取的采样数 设置采样率 - 编码器 - Output Format has been changed because the current one was incompatible with this encoder + 采样率 + 设置 自动 - 录制暂停 - 音频录制已暂停 - An error occured - Alibi encountered an error during recording. Would you like to try saving the recording? - Language - Change - Import Settings - Export Settings - Are you sure you want to import these settings? Your current settings will be overwritten! - Import settings - Settings have been imported successfully! + Alibi就像你手机上的行车记录仪一样。它会不断地录制音频,并在您需要时保存最近的30分钟录音。 + 欢迎使用Alibi! + Alibi不对该应用程序的使用负任何责任。您在使用时需自行承担使用风险。 + 您需要自行承担使用该应用程序所带来的风险和后果。 + 开始使用Alibi diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 15ab739..baa7240 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,4 @@ - + Alibi Cancel @@ -64,4 +63,9 @@ Alibi encountered an error during recording. Would you like to try saving the recording? Language Change + Import Settings + Export Settings + Are you sure you want to import these settings? Your current settings will be overwritten! + Import settings + Settings have been imported successfully! \ No newline at end of file diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000..2789da1 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,13 @@ +files: + - source: /app/src/main/res/values/strings.xml + translation: /app/src/main/res/values-%android_code%/strings.xml + translate_attributes: 0 + content_segmentation: 0 + type: xml + languages_mapping: + android_code: + de-rDE: de + tr-rTR: tr + zh-rCN: zh + - source: /fastlane/metadata/android/en-US/*.txt + translation: /fastlane/metadata/android/%locale%/%original_file_name%