diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d271724b..2823496c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -245,5 +245,9 @@ android:name=".activities.PolycentricModerationActivity" android:exported="false" android:screenOrientation="portrait" /> + diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt index 9cf58134..963999ce 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt @@ -13,15 +13,18 @@ import android.view.View import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.R +import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp.Companion.withContext import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.views.buttons.BigButton +import com.futo.platformplayer.activities.QRCodeFullscreenActivity import com.futo.polycentric.core.ContentType import com.futo.polycentric.core.SignedEvent import com.futo.polycentric.core.StorageTypeCRDTItem @@ -29,8 +32,10 @@ import com.futo.polycentric.core.StorageTypeCRDTSetItem import com.futo.polycentric.core.Store import com.futo.polycentric.core.toBase64Url import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType import com.google.zxing.MultiFormatWriter import com.google.zxing.common.BitMatrix +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -41,11 +46,27 @@ import userpackage.Protocol.URLInfo class PolycentricBackupActivity : AppCompatActivity() { private lateinit var _buttonShare: BigButton; private lateinit var _buttonCopy: BigButton; + private lateinit var _buttonExportFile: BigButton; private lateinit var _imageQR: ImageView; private lateinit var _exportBundle: String; private lateinit var _textQR: TextView; + private lateinit var _textQRHint: TextView; private lateinit var _loader: View + private val _createDocumentLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri -> + uri?.let { fileUri -> + try { + contentResolver.openOutputStream(fileUri)?.use { outputStream -> + outputStream.write(_exportBundle.toByteArray()) + } + UIDialogs.toast(this, getString(R.string.profile_saved_successfully)) + } catch (e: Exception) { + Logger.e(TAG, "Failed to write to document", e) + UIDialogs.toast(this, "Failed to save profile: ${e.message}") + } + } + } + override fun attachBaseContext(newBase: Context?) { super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) } @@ -57,8 +78,10 @@ class PolycentricBackupActivity : AppCompatActivity() { _buttonShare = findViewById(R.id.button_share) _buttonCopy = findViewById(R.id.button_copy) + _buttonExportFile = findViewById(R.id.button_export_file) _imageQR = findViewById(R.id.image_qr) _textQR = findViewById(R.id.text_qr) + _textQRHint = findViewById(R.id.text_qr_hint) _loader = findViewById(R.id.progress_loader) findViewById(R.id.button_back).setOnClickListener { finish(); @@ -66,14 +89,23 @@ class PolycentricBackupActivity : AppCompatActivity() { _imageQR.visibility = View.INVISIBLE _textQR.visibility = View.INVISIBLE + _textQRHint.visibility = View.INVISIBLE _loader.visibility = View.VISIBLE _buttonShare.visibility = View.INVISIBLE _buttonCopy.visibility = View.INVISIBLE + _buttonExportFile.visibility = View.INVISIBLE lifecycleScope.launch { + val bundle = withContext(Dispatchers.IO) { createExportBundle() } + _exportBundle = bundle + Logger.i(TAG, "Export bundle created, length: ${bundle.length}") + try { val pair = withContext(Dispatchers.IO) { - val bundle = createExportBundle() + if (!isContentSuitableForQRCode(bundle)) { + throw Exception("Data too big for QR code generation") + } + val dimension = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics ).toInt() @@ -81,18 +113,35 @@ class PolycentricBackupActivity : AppCompatActivity() { Pair(bundle, qr) } - _exportBundle = pair.first _imageQR.setImageBitmap(pair.second) _imageQR.visibility = View.VISIBLE _textQR.visibility = View.VISIBLE + _textQRHint.visibility = View.VISIBLE _buttonShare.visibility = View.VISIBLE _buttonCopy.visibility = View.VISIBLE + + _imageQR.setOnClickListener { + val intent = QRCodeFullscreenActivity.createIntent(this@PolycentricBackupActivity, _exportBundle) + startActivity(intent) + } } catch (e: Exception) { - Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e) + val byteSize = bundle.toByteArray(Charsets.UTF_8).size + Logger.e(TAG, "QR code generation failed. Bundle length: ${bundle.length} chars, ${byteSize} bytes, Error: ${e.message}", e) + + if (e.message?.contains("Data too big") == true) { + _textQR.text = getString(R.string.qr_code_too_large_use_file_export) + _buttonExportFile.visibility = View.VISIBLE + } else { + _textQR.text = getString(R.string.failed_to_generate_qr_code) + } + + _textQR.visibility = View.VISIBLE + _textQRHint.visibility = View.INVISIBLE + _buttonShare.visibility = View.VISIBLE + _buttonCopy.visibility = View.VISIBLE + + // Hide QR image since generation failed _imageQR.visibility = View.INVISIBLE - _textQR.visibility = View.INVISIBLE - _buttonShare.visibility = View.INVISIBLE - _buttonCopy.visibility = View.INVISIBLE } finally { _loader.visibility = View.GONE } @@ -108,11 +157,29 @@ class PolycentricBackupActivity : AppCompatActivity() { val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle); clipboard.setPrimaryClip(clip); }; + + _buttonExportFile.onClick.subscribe { + val fileName = "polycentric_profile_${System.currentTimeMillis()}.txt" + _createDocumentLauncher.launch(fileName) + }; + } + + private fun isContentSuitableForQRCode(content: String): Boolean { + val bytes = content.toByteArray(Charsets.UTF_8) + return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes } private fun generateQRCode(content: String, width: Int, height: Int): Bitmap { - val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height); - return bitMatrixToBitmap(bitMatrix); + if (!isContentSuitableForQRCode(content)) { + throw Exception("Data too big for QR code generation") + } + + val hints = java.util.EnumMap(EncodeHintType::class.java) + hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M + hints[EncodeHintType.MARGIN] = 1 + + val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints) + return bitMatrixToBitmap(bitMatrix) } private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap { @@ -203,7 +270,8 @@ class PolycentricBackupActivity : AppCompatActivity() { .setBody(exportBundle.toByteString()) .build(); - return "polycentric://" + urlInfo.toByteArray().toBase64Url() + val data = urlInfo.toByteArray() + return "polycentric://" + data.toBase64Url() } companion object { diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt index ab6d70a3..91a0cb5c 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt @@ -32,100 +32,166 @@ import userpackage.Protocol import userpackage.Protocol.ExportBundle class PolycentricImportProfileActivity : AppCompatActivity() { - private lateinit var _buttonHelp: ImageButton; - private lateinit var _buttonScanProfile: LinearLayout; - private lateinit var _buttonImportProfile: LinearLayout; - private lateinit var _editProfile: EditText; - private lateinit var _loaderOverlay: LoaderOverlay; + private lateinit var _buttonHelp: ImageButton + private lateinit var _buttonScanProfile: LinearLayout + private lateinit var _buttonImportFile: LinearLayout + private lateinit var _buttonImportProfile: LinearLayout + private lateinit var _editProfile: EditText + private lateinit var _loaderOverlay: LoaderOverlay - private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data) - scanResult?.let { - if (it.contents != null) { - val scannedUrl = it.contents - import(scannedUrl) + private val _qrCodeResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val scanResult = + IntentIntegrator.parseActivityResult(result.resultCode, result.data) + scanResult?.let { + if (it.contents != null) { + val scannedUrl = it.contents + import(scannedUrl) + } + } + } + + private val _filePickerLauncher = + registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { fileUri -> + try { + // Check file size before reading + val fileSize = + contentResolver.openFileDescriptor(fileUri, "r")?.statSize ?: 0 + val maxFileSize = 10 * 1024 * 1024 // 10MB limit + + if (fileSize > maxFileSize) { + UIDialogs.toast(this, "File too large. Maximum size is 10MB.") + return@let + } + + if (fileSize == 0L) { + UIDialogs.toast(this, "Selected file is empty.") + return@let + } + + val content = + contentResolver + .openInputStream(fileUri) + ?.bufferedReader() + ?.readText() + content?.let { fileContent -> + val trimmedContent = fileContent.trim() + + // Check if content is empty after trimming + if (trimmedContent.isEmpty()) { + UIDialogs.toast(this, "Selected file contains no data.") + return@let + } + + // Check if content looks like a valid polycentric URL + if (!trimmedContent.startsWith("polycentric://")) { + UIDialogs.toast( + this, + "Selected file does not contain a valid polycentric profile URL." + ) + return@let + } + + import(trimmedContent) + } + ?: run { UIDialogs.toast(this, "Could not read file content.") } + } catch (e: SecurityException) { + Logger.e(TAG, "Security exception reading file", e) + UIDialogs.toast(this, "Permission denied to read file.") + } catch (e: OutOfMemoryError) { + Logger.e(TAG, "Out of memory reading file", e) + UIDialogs.toast(this, "File too large to process.") + } catch (e: Exception) { + Logger.e(TAG, "Failed to read file", e) + UIDialogs.toast(this, "Failed to read file: ${e.message}") + } + } } - } - } override fun attachBaseContext(newBase: Context?) { super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) } override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_polycentric_import_profile); - setNavigationBarColorAndIcons(); + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_polycentric_import_profile) + setNavigationBarColorAndIcons() - _buttonHelp = findViewById(R.id.button_help); - _buttonScanProfile = findViewById(R.id.button_scan_profile); - _buttonImportProfile = findViewById(R.id.button_import_profile); - _loaderOverlay = findViewById(R.id.loader_overlay); - _editProfile = findViewById(R.id.edit_profile); - findViewById(R.id.button_back).setOnClickListener { - finish(); - }; + _buttonHelp = findViewById(R.id.button_help) + _buttonScanProfile = findViewById(R.id.button_scan_profile) + _buttonImportFile = findViewById(R.id.button_import_file) + _buttonImportProfile = findViewById(R.id.button_import_profile) + _loaderOverlay = findViewById(R.id.loader_overlay) + _editProfile = findViewById(R.id.edit_profile) + findViewById(R.id.button_back).setOnClickListener { finish() } _buttonHelp.setOnClickListener { - startActivity(Intent(this, PolycentricWhyActivity::class.java)); - }; + startActivity(Intent(this, PolycentricWhyActivity::class.java)) + } _buttonScanProfile.setOnClickListener { val integrator = IntentIntegrator(this) integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE) integrator.setPrompt(getString(R.string.scan_a_qr_code)) - integrator.setOrientationLocked(true); + integrator.setOrientationLocked(true) integrator.setCameraId(0) integrator.setBeepEnabled(false) integrator.setBarcodeImageEnabled(true) - integrator.setCaptureActivity(QRCaptureActivity::class.java); + integrator.setCaptureActivity(QRCaptureActivity::class.java) _qrCodeResultLauncher.launch(integrator.createScanIntent()) - }; + } + + _buttonImportFile.setOnClickListener { _filePickerLauncher.launch("text/plain") } _buttonImportProfile.setOnClickListener { if (_editProfile.text.isEmpty()) { - UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data)); - return@setOnClickListener; + UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data)) + return@setOnClickListener } - import(_editProfile.text.toString()); - }; + import(_editProfile.text.toString()) + } - val url = intent.getStringExtra("url"); + val url = intent.getStringExtra("url") if (url != null) { - import(url); + import(url) } } private fun import(url: String) { if (!url.startsWith("polycentric://")) { - UIDialogs.toast(this, getString(R.string.not_a_valid_url)); - return; + UIDialogs.toast(this, getString(R.string.not_a_valid_url)) + return } _loaderOverlay.show() lifecycleScope.launch(Dispatchers.IO) { try { - val data = url.substring("polycentric://".length).base64UrlToByteArray(); - val urlInfo = Protocol.URLInfo.parseFrom(data); + val data = url.substring("polycentric://".length).base64UrlToByteArray() + val urlInfo = Protocol.URLInfo.parseFrom(data) + if (urlInfo.urlType != 3L) { throw Exception("Expected urlInfo struct of type ExportBundle") } - val exportBundle = ExportBundle.parseFrom(urlInfo.body); - val keyPair = KeyPair.fromProto(exportBundle.keyPair); + val exportBundle = ExportBundle.parseFrom(urlInfo.body) + val keyPair = KeyPair.fromProto(exportBundle.keyPair) - val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey); + val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey) if (existingProcessSecret != null) { withContext(Dispatchers.Main) { - UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported)); + UIDialogs.toast( + this@PolycentricImportProfileActivity, + getString(R.string.this_profile_is_already_imported) + ) } - return@launch; + return@launch } - val processSecret = ProcessSecret(keyPair, Process.random()); - Store.instance.addProcessSecret(processSecret); + val processSecret = ProcessSecret(keyPair, Process.random()) + Store.instance.addProcessSecret(processSecret) try { PolycentricStorage.instance.addProcessSecret(processSecret) @@ -133,37 +199,43 @@ class PolycentricImportProfileActivity : AppCompatActivity() { Logger.e(TAG, "Failed to save process secret to secret storage.", e) } - val processHandle = processSecret.toProcessHandle(); + val processHandle = processSecret.toProcessHandle() for (e in exportBundle.events.eventsList) { try { - val se = SignedEvent.fromProto(e); - Store.instance.putSignedEvent(se); + val se = SignedEvent.fromProto(e) + Store.instance.putSignedEvent(se) } catch (e: Throwable) { - Logger.w(TAG, "Ignored invalid event", e); + Logger.w(TAG, "Ignored invalid event", e) } } - StatePolycentric.instance.setProcessHandle(processHandle); - processHandle.fullyBackfillClient(ApiMethods.SERVER); + StatePolycentric.instance.setProcessHandle(processHandle) + processHandle.fullyBackfillClient(ApiMethods.SERVER) withContext(Dispatchers.Main) { - startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java)); - finish(); + startActivity( + Intent( + this@PolycentricImportProfileActivity, + PolycentricProfileActivity::class.java + ) + ) + finish() } } catch (e: Throwable) { - Logger.w(TAG, "Failed to import profile", e); + Logger.w(TAG, "Failed to import profile", e) withContext(Dispatchers.Main) { - UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'"); + UIDialogs.toast( + this@PolycentricImportProfileActivity, + getString(R.string.failed_to_import_profile) + " '${e.message}'" + ) } } finally { - withContext(Dispatchers.Main) { - _loaderOverlay.hide(); - } + withContext(Dispatchers.Main) { _loaderOverlay.hide() } } } } companion object { - private const val TAG = "PolycentricImportProfileActivity"; + private const val TAG = "PolycentricImportProfileActivity" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/activities/QRCodeFullscreenActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/QRCodeFullscreenActivity.kt new file mode 100644 index 00000000..9acefed2 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/activities/QRCodeFullscreenActivity.kt @@ -0,0 +1,109 @@ +package com.futo.platformplayer.activities + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color +import android.os.Bundle +import android.util.TypedValue +import android.view.View +import android.widget.ImageButton +import android.widget.ImageView +import androidx.appcompat.app.AppCompatActivity +import com.futo.platformplayer.R +import com.futo.platformplayer.setNavigationBarColorAndIcons +import com.futo.platformplayer.states.StateApp +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.MultiFormatWriter +import com.google.zxing.common.BitMatrix +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel + +class QRCodeFullscreenActivity : AppCompatActivity() { + companion object { + private const val EXTRA_QR_TEXT = "qr_text" + + fun createIntent(context: Context, qrText: String): android.content.Intent { + return android.content.Intent(context, QRCodeFullscreenActivity::class.java).apply { + putExtra(EXTRA_QR_TEXT, qrText) + } + } + } + + override fun attachBaseContext(newBase: Context?) { + super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_qr_code_fullscreen) + setNavigationBarColorAndIcons() + + val qrText = intent.getStringExtra(EXTRA_QR_TEXT) + + val imageQR = findViewById(R.id.image_qr_fullscreen) + val buttonBack = findViewById(R.id.button_back_fullscreen) + val buttonClose = findViewById(R.id.button_close_fullscreen) + + // Generate QR code bitmap from text + qrText?.let { text -> + try { + if (!isContentSuitableForQRCode(text)) { + throw Exception("Data too big for QR code generation") + } + + val dimension = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 300f, resources.displayMetrics + ).toInt() + val qrBitmap = generateQRCode(text, dimension, dimension) + imageQR.setImageBitmap(qrBitmap) + } catch (e: Exception) { + // If QR generation fails, show error or fallback + imageQR.setImageResource(R.drawable.ic_qr) + } + } + + buttonBack.setOnClickListener { + finish() + } + + buttonClose.setOnClickListener { + finish() + } + + imageQR.setOnClickListener { + finish() + } + } + + private fun isContentSuitableForQRCode(content: String): Boolean { + val bytes = content.toByteArray(Charsets.UTF_8) + return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes + } + + private fun generateQRCode(content: String, width: Int, height: Int): Bitmap { + if (!isContentSuitableForQRCode(content)) { + throw Exception("Data too big for QR code generation") + } + + val hints = java.util.EnumMap(EncodeHintType::class.java) + hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M + hints[EncodeHintType.MARGIN] = 1 + + val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints) + return bitMatrixToBitmap(bitMatrix) + } + + private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap { + val width = matrix.width + val height = matrix.height + val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565) + + for (x in 0 until width) { + for (y in 0 until height) { + bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE) + } + } + return bmp + } +} + diff --git a/app/src/main/res/layout/activity_polycentric_backup.xml b/app/src/main/res/layout/activity_polycentric_backup.xml index d6579dd7..130c4934 100644 --- a/app/src/main/res/layout/activity_polycentric_backup.xml +++ b/app/src/main/res/layout/activity_polycentric_backup.xml @@ -29,6 +29,8 @@ android:id="@+id/image_qr" android:layout_width="200dp" android:layout_height="200dp" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:padding="8dp" app:srcCompat="@drawable/ic_qr" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" @@ -37,15 +39,31 @@ + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" /> + + @@ -75,6 +93,15 @@ app:buttonSubText="@string/copy_your_identity_to_clipboard" app:buttonIcon="@drawable/ic_copy" android:layout_marginTop="8dp" /> + + + + + + + diff --git a/app/src/main/res/layout/activity_qr_code_fullscreen.xml b/app/src/main/res/layout/activity_qr_code_fullscreen.xml new file mode 100644 index 00000000..0dd128e2 --- /dev/null +++ b/app/src/main/res/layout/activity_qr_code_fullscreen.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 4724f6ce..269736f8 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -388,6 +388,7 @@ استثناء غير معالج في VS إرسال الاستثناء للمطورين… تم تعيين مفتاح الترخيص الخاص بك!\nقد يكون هناك حاجة لإعادة تشغيل التطبيق. + رمز الاستجابة السريعة كبير جدًا. استخدم النص أدناه لمشاركة ملفك الشخصي. تنسيق الترخيص غير صالح تنسيق المحتوى غير معروف تنسيق الملف غير معروف diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 234bb900..7cd81816 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -395,6 +395,7 @@ Unbehandelte Ausnahme in VS Ausnahme an Entwickler senden… Ihr Lizenzschlüssel wurde festgelegt!\nEin Neustart der App könnte erforderlich sein. + QR-Code zu groß. Verwenden Sie den Text unten, um Ihr Profil zu teilen. Ungültiges Lizenzformat Unbekanntes Inhaltsformat Unbekanntes Dateiformat diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 96b9449a..e9c04d20 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -372,6 +372,7 @@ Excepción no manejada en VS Enviar excepción a los desarrolladores... ¡Se ha configurado tu clave de licencia!\nPuede ser necesario reiniciar la aplicación. + Código QR demasiado grande. Use el texto de abajo para compartir su perfil. Formato de licencia no válido Formato de contenido desconocido Formato de archivo desconocido diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 557c485f..48ffdafe 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -411,6 +411,7 @@ Exception non gérée dans VS Envoyer l\'exception aux développeurs… Votre clé de licence a été définie !\nUn redémarrage de l\'application peut être nécessaire. + Code QR trop volumineux. Utilisez le texte ci-dessous pour partager votre profil. Format de licence invalide Format de contenu inconnu Format de fichier inconnu diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a5783903..d91c3b0b 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -617,6 +617,7 @@ Eccezione non gestita in VS Invio eccezione agli sviluppatori… La tua chiave di licenza è stata impostata!\nIl riavvio dell\'app potrebbe essere richiesto. + Codice QR troppo grande. Usa il testo qui sotto per condividere il tuo profilo. Formato licenza non valido Formato contenuto sconosciuto Formato file sconosciuto diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e3452959..9177ffac 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -374,6 +374,7 @@ VSで未処理の例外 開発者に例外を送信… ライセンスキーが設定されました!\nアプリを再起動する可能性があります。 + QRコードが大きすぎます。下のテキストを使用してプロフィールを共有してください。 無効なライセンス形式 不明なコンテンツ形式 不明なファイル形式 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 0312da2f..01a84b8d 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -410,6 +410,7 @@ VS에서 처리되지 않은 예외 개발자에게 예외를 보냅니다… 라이선스 키가 설정되었습니다!\n앱을 다시 시작해야 할 수 있습니다. + QR 코드가 너무 큽니다. 아래 텍스트를 사용하여 프로필을 공유하세요. 잘못된 라이선스 형식 알 수 없는 콘텐츠 형식 알 수 없는 파일 형식 diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index de46d305..9a0fcec9 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -407,6 +407,7 @@ Exceção não tratada no VS Enviar exceção aos desenvolvedores… Sua chave de licença foi definida!\nUma reinicialização do aplicativo pode ser necessária. + Código QR muito grande. Use o texto abaixo para compartilhar seu perfil. Formato de licença inválido Formato de conteúdo desconhecido Formato de arquivo desconhecido diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 82a479ad..cf66c21d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -407,6 +407,7 @@ Необработанное исключение в VS Отправить исключение разработчикам… Ваш лицензионный ключ установлен!\nМожет потребоваться перезагрузка приложения. + QR-код слишком большой. Используйте текст ниже, чтобы поделиться своим профилем. Неверный формат лицензии Неизвестный формат содержимого Неизвестный формат файла diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 5a17d7b9..8cc01b87 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -581,6 +581,7 @@ VS\'de bilinmeyen hata (exception) Geliştiricilere exception\'ı gönder… Lisans anahtarınız ayarlandı!\nUygulamayı yeniden başlatmanız gerekebilir. + QR kodu çok büyük. Profilinizi paylaşmak için aşağıdaki metni kullanın. Geçersiz lisans formatı Bilinmeyen içerik formatı Bilinmeyen dosya formatı diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 1e0e843f..74ab2f65 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -411,6 +411,7 @@ VS 中的未处理异常 向开发者发送异常… 您的许可证密钥已设置!\n可能需要重新启动应用程序。 + 二维码太大。请使用下方文字分享您的个人资料。 无效的许可证格式 未知的内容格式 未知的文件格式 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e0f03e8f..7e0c60da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -646,6 +646,7 @@ Unhandled exception in VS Send exception to developers… Your license key has been set!\nAn app restart might be required. + QR code too large. Use the text below to share your profile. Invalid license format Unknown content format Unknown file format @@ -1175,4 +1176,10 @@ 1500 2000 + Export to file or copy backup code + Tap QR code for fullscreen view + Export to File + Import from File + Save profile to file for sharing + Profile saved successfully