mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
@@ -245,5 +245,9 @@
|
|||||||
android:name=".activities.PolycentricModerationActivity"
|
android:name=".activities.PolycentricModerationActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.QRCodeFullscreenActivity"
|
||||||
|
android:screenOrientation="sensorPortrait"
|
||||||
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -725,7 +725,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@AdvancedField
|
@AdvancedField
|
||||||
@FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6)
|
@FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var experimentalCasting: Boolean = false
|
var experimentalCasting: Boolean = true
|
||||||
|
|
||||||
/*TODO: Should we have a different casting quality?
|
/*TODO: Should we have a different casting quality?
|
||||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||||
|
|||||||
@@ -252,6 +252,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
UIDialogs.toast(this, "Notification permission denied");
|
UIDialogs.toast(this, "Notification permission denied");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun requestNotificationPermissions() {
|
fun requestNotificationPermissions() {
|
||||||
_notificationPermissionLauncher?.launch(_notifPermission);
|
_notificationPermissionLauncher?.launch(_notifPermission);
|
||||||
}
|
}
|
||||||
@@ -1379,6 +1381,23 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _callbackPermissionAudio: ((Boolean)->Unit)? = null;
|
||||||
|
var _callbackPermissionVideo: ((Boolean)->Unit)? = null;
|
||||||
|
val permissionReqAudio = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
|
||||||
|
_callbackPermissionAudio?.invoke(isGranted);
|
||||||
|
});
|
||||||
|
val permissionReqVideo = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
|
||||||
|
_callbackPermissionVideo?.invoke(isGranted);
|
||||||
|
});
|
||||||
|
fun requestPermissionAudio(cb: ((Boolean)->Unit)? = null) {
|
||||||
|
_callbackPermissionAudio = cb;
|
||||||
|
permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO);
|
||||||
|
}
|
||||||
|
fun requestPermissionVideo(cb: ((Boolean)->Unit)? = null) {
|
||||||
|
_callbackPermissionVideo = cb;
|
||||||
|
permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||||
|
|||||||
@@ -13,15 +13,18 @@ import android.view.View
|
|||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateApp.Companion.withContext
|
import com.futo.platformplayer.states.StateApp.Companion.withContext
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import com.futo.platformplayer.activities.QRCodeFullscreenActivity
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.SignedEvent
|
import com.futo.polycentric.core.SignedEvent
|
||||||
import com.futo.polycentric.core.StorageTypeCRDTItem
|
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.Store
|
||||||
import com.futo.polycentric.core.toBase64Url
|
import com.futo.polycentric.core.toBase64Url
|
||||||
import com.google.zxing.BarcodeFormat
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.google.zxing.EncodeHintType
|
||||||
import com.google.zxing.MultiFormatWriter
|
import com.google.zxing.MultiFormatWriter
|
||||||
import com.google.zxing.common.BitMatrix
|
import com.google.zxing.common.BitMatrix
|
||||||
|
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -41,11 +46,27 @@ import userpackage.Protocol.URLInfo
|
|||||||
class PolycentricBackupActivity : AppCompatActivity() {
|
class PolycentricBackupActivity : AppCompatActivity() {
|
||||||
private lateinit var _buttonShare: BigButton;
|
private lateinit var _buttonShare: BigButton;
|
||||||
private lateinit var _buttonCopy: BigButton;
|
private lateinit var _buttonCopy: BigButton;
|
||||||
|
private lateinit var _buttonExportFile: BigButton;
|
||||||
private lateinit var _imageQR: ImageView;
|
private lateinit var _imageQR: ImageView;
|
||||||
private lateinit var _exportBundle: String;
|
private lateinit var _exportBundle: String;
|
||||||
private lateinit var _textQR: TextView;
|
private lateinit var _textQR: TextView;
|
||||||
|
private lateinit var _textQRHint: TextView;
|
||||||
private lateinit var _loader: View
|
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?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
}
|
}
|
||||||
@@ -57,8 +78,10 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_buttonShare = findViewById(R.id.button_share)
|
_buttonShare = findViewById(R.id.button_share)
|
||||||
_buttonCopy = findViewById(R.id.button_copy)
|
_buttonCopy = findViewById(R.id.button_copy)
|
||||||
|
_buttonExportFile = findViewById(R.id.button_export_file)
|
||||||
_imageQR = findViewById(R.id.image_qr)
|
_imageQR = findViewById(R.id.image_qr)
|
||||||
_textQR = findViewById(R.id.text_qr)
|
_textQR = findViewById(R.id.text_qr)
|
||||||
|
_textQRHint = findViewById(R.id.text_qr_hint)
|
||||||
_loader = findViewById(R.id.progress_loader)
|
_loader = findViewById(R.id.progress_loader)
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
@@ -66,14 +89,23 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_imageQR.visibility = View.INVISIBLE
|
_imageQR.visibility = View.INVISIBLE
|
||||||
_textQR.visibility = View.INVISIBLE
|
_textQR.visibility = View.INVISIBLE
|
||||||
|
_textQRHint.visibility = View.INVISIBLE
|
||||||
_loader.visibility = View.VISIBLE
|
_loader.visibility = View.VISIBLE
|
||||||
_buttonShare.visibility = View.INVISIBLE
|
_buttonShare.visibility = View.INVISIBLE
|
||||||
_buttonCopy.visibility = View.INVISIBLE
|
_buttonCopy.visibility = View.INVISIBLE
|
||||||
|
_buttonExportFile.visibility = View.INVISIBLE
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
val bundle = withContext(Dispatchers.IO) { createExportBundle() }
|
||||||
|
_exportBundle = bundle
|
||||||
|
Logger.i(TAG, "Export bundle created, length: ${bundle.length}")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val pair = withContext(Dispatchers.IO) {
|
val pair = withContext(Dispatchers.IO) {
|
||||||
val bundle = createExportBundle()
|
if (!isContentSuitableForQRCode(bundle)) {
|
||||||
|
throw Exception("Data too big for QR code generation")
|
||||||
|
}
|
||||||
|
|
||||||
val dimension = TypedValue.applyDimension(
|
val dimension = TypedValue.applyDimension(
|
||||||
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
|
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
|
||||||
).toInt()
|
).toInt()
|
||||||
@@ -81,18 +113,35 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
Pair(bundle, qr)
|
Pair(bundle, qr)
|
||||||
}
|
}
|
||||||
|
|
||||||
_exportBundle = pair.first
|
|
||||||
_imageQR.setImageBitmap(pair.second)
|
_imageQR.setImageBitmap(pair.second)
|
||||||
_imageQR.visibility = View.VISIBLE
|
_imageQR.visibility = View.VISIBLE
|
||||||
_textQR.visibility = View.VISIBLE
|
_textQR.visibility = View.VISIBLE
|
||||||
|
_textQRHint.visibility = View.VISIBLE
|
||||||
_buttonShare.visibility = View.VISIBLE
|
_buttonShare.visibility = View.VISIBLE
|
||||||
_buttonCopy.visibility = View.VISIBLE
|
_buttonCopy.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
_imageQR.setOnClickListener {
|
||||||
|
val intent = QRCodeFullscreenActivity.createIntent(this@PolycentricBackupActivity, _exportBundle)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} 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
|
_imageQR.visibility = View.INVISIBLE
|
||||||
_textQR.visibility = View.INVISIBLE
|
|
||||||
_buttonShare.visibility = View.INVISIBLE
|
|
||||||
_buttonCopy.visibility = View.INVISIBLE
|
|
||||||
} finally {
|
} finally {
|
||||||
_loader.visibility = View.GONE
|
_loader.visibility = View.GONE
|
||||||
}
|
}
|
||||||
@@ -108,11 +157,29 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
|
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
|
||||||
clipboard.setPrimaryClip(clip);
|
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 {
|
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
||||||
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
|
if (!isContentSuitableForQRCode(content)) {
|
||||||
return bitMatrixToBitmap(bitMatrix);
|
throw Exception("Data too big for QR code generation")
|
||||||
|
}
|
||||||
|
|
||||||
|
val hints = java.util.EnumMap<EncodeHintType, Any>(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 {
|
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
||||||
@@ -203,7 +270,8 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
.setBody(exportBundle.toByteString())
|
.setBody(exportBundle.toByteString())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return "polycentric://" + urlInfo.toByteArray().toBase64Url()
|
val data = urlInfo.toByteArray()
|
||||||
|
return "polycentric://" + data.toBase64Url()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+133
-61
@@ -32,100 +32,166 @@ import userpackage.Protocol
|
|||||||
import userpackage.Protocol.ExportBundle
|
import userpackage.Protocol.ExportBundle
|
||||||
|
|
||||||
class PolycentricImportProfileActivity : AppCompatActivity() {
|
class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||||
private lateinit var _buttonHelp: ImageButton;
|
private lateinit var _buttonHelp: ImageButton
|
||||||
private lateinit var _buttonScanProfile: LinearLayout;
|
private lateinit var _buttonScanProfile: LinearLayout
|
||||||
private lateinit var _buttonImportProfile: LinearLayout;
|
private lateinit var _buttonImportFile: LinearLayout
|
||||||
private lateinit var _editProfile: EditText;
|
private lateinit var _buttonImportProfile: LinearLayout
|
||||||
private lateinit var _loaderOverlay: LoaderOverlay;
|
private lateinit var _editProfile: EditText
|
||||||
|
private lateinit var _loaderOverlay: LoaderOverlay
|
||||||
|
|
||||||
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val _qrCodeResultLauncher =
|
||||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
scanResult?.let {
|
val scanResult =
|
||||||
if (it.contents != null) {
|
IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
val scannedUrl = it.contents
|
scanResult?.let {
|
||||||
import(scannedUrl)
|
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?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_polycentric_import_profile);
|
setContentView(R.layout.activity_polycentric_import_profile)
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons()
|
||||||
|
|
||||||
_buttonHelp = findViewById(R.id.button_help);
|
_buttonHelp = findViewById(R.id.button_help)
|
||||||
_buttonScanProfile = findViewById(R.id.button_scan_profile);
|
_buttonScanProfile = findViewById(R.id.button_scan_profile)
|
||||||
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
_buttonImportFile = findViewById(R.id.button_import_file)
|
||||||
_loaderOverlay = findViewById(R.id.loader_overlay);
|
_buttonImportProfile = findViewById(R.id.button_import_profile)
|
||||||
_editProfile = findViewById(R.id.edit_profile);
|
_loaderOverlay = findViewById(R.id.loader_overlay)
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
_editProfile = findViewById(R.id.edit_profile)
|
||||||
finish();
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener { finish() }
|
||||||
};
|
|
||||||
|
|
||||||
_buttonHelp.setOnClickListener {
|
_buttonHelp.setOnClickListener {
|
||||||
startActivity(Intent(this, PolycentricWhyActivity::class.java));
|
startActivity(Intent(this, PolycentricWhyActivity::class.java))
|
||||||
};
|
}
|
||||||
|
|
||||||
_buttonScanProfile.setOnClickListener {
|
_buttonScanProfile.setOnClickListener {
|
||||||
val integrator = IntentIntegrator(this)
|
val integrator = IntentIntegrator(this)
|
||||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||||
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
||||||
integrator.setOrientationLocked(true);
|
integrator.setOrientationLocked(true)
|
||||||
integrator.setCameraId(0)
|
integrator.setCameraId(0)
|
||||||
integrator.setBeepEnabled(false)
|
integrator.setBeepEnabled(false)
|
||||||
integrator.setBarcodeImageEnabled(true)
|
integrator.setBarcodeImageEnabled(true)
|
||||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
integrator.setCaptureActivity(QRCaptureActivity::class.java)
|
||||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
};
|
}
|
||||||
|
|
||||||
|
_buttonImportFile.setOnClickListener { _filePickerLauncher.launch("text/plain") }
|
||||||
|
|
||||||
_buttonImportProfile.setOnClickListener {
|
_buttonImportProfile.setOnClickListener {
|
||||||
if (_editProfile.text.isEmpty()) {
|
if (_editProfile.text.isEmpty()) {
|
||||||
UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data));
|
UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data))
|
||||||
return@setOnClickListener;
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
|
||||||
import(_editProfile.text.toString());
|
import(_editProfile.text.toString())
|
||||||
};
|
}
|
||||||
|
|
||||||
val url = intent.getStringExtra("url");
|
val url = intent.getStringExtra("url")
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
import(url);
|
import(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun import(url: String) {
|
private fun import(url: String) {
|
||||||
if (!url.startsWith("polycentric://")) {
|
if (!url.startsWith("polycentric://")) {
|
||||||
UIDialogs.toast(this, getString(R.string.not_a_valid_url));
|
UIDialogs.toast(this, getString(R.string.not_a_valid_url))
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_loaderOverlay.show()
|
_loaderOverlay.show()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val data = url.substring("polycentric://".length).base64UrlToByteArray();
|
val data = url.substring("polycentric://".length).base64UrlToByteArray()
|
||||||
val urlInfo = Protocol.URLInfo.parseFrom(data);
|
val urlInfo = Protocol.URLInfo.parseFrom(data)
|
||||||
|
|
||||||
if (urlInfo.urlType != 3L) {
|
if (urlInfo.urlType != 3L) {
|
||||||
throw Exception("Expected urlInfo struct of type ExportBundle")
|
throw Exception("Expected urlInfo struct of type ExportBundle")
|
||||||
}
|
}
|
||||||
|
|
||||||
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
val exportBundle = ExportBundle.parseFrom(urlInfo.body)
|
||||||
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
val keyPair = KeyPair.fromProto(exportBundle.keyPair)
|
||||||
|
|
||||||
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey)
|
||||||
if (existingProcessSecret != null) {
|
if (existingProcessSecret != null) {
|
||||||
withContext(Dispatchers.Main) {
|
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());
|
val processSecret = ProcessSecret(keyPair, Process.random())
|
||||||
Store.instance.addProcessSecret(processSecret);
|
Store.instance.addProcessSecret(processSecret)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PolycentricStorage.instance.addProcessSecret(processSecret)
|
PolycentricStorage.instance.addProcessSecret(processSecret)
|
||||||
@@ -133,37 +199,43 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
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) {
|
for (e in exportBundle.events.eventsList) {
|
||||||
try {
|
try {
|
||||||
val se = SignedEvent.fromProto(e);
|
val se = SignedEvent.fromProto(e)
|
||||||
Store.instance.putSignedEvent(se);
|
Store.instance.putSignedEvent(se)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Ignored invalid event", e);
|
Logger.w(TAG, "Ignored invalid event", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle)
|
||||||
processHandle.fullyBackfillClient(ApiMethods.SERVER);
|
processHandle.fullyBackfillClient(ApiMethods.SERVER)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
startActivity(
|
||||||
finish();
|
Intent(
|
||||||
|
this@PolycentricImportProfileActivity,
|
||||||
|
PolycentricProfileActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to import profile", e);
|
Logger.w(TAG, "Failed to import profile", e)
|
||||||
withContext(Dispatchers.Main) {
|
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 {
|
} finally {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) { _loaderOverlay.hide() }
|
||||||
_loaderOverlay.hide();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PolycentricImportProfileActivity";
|
private const val TAG = "PolycentricImportProfileActivity"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<ImageView>(R.id.image_qr_fullscreen)
|
||||||
|
val buttonBack = findViewById<ImageButton>(R.id.button_back_fullscreen)
|
||||||
|
val buttonClose = findViewById<ImageButton>(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, Any>(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -39,7 +40,7 @@ import java.time.OffsetDateTime
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
||||||
protected val _feedRoot: FrameLayout;
|
protected val _feedRoot: ConstraintLayout;
|
||||||
protected val _recyclerResults: RecyclerView;
|
protected val _recyclerResults: RecyclerView;
|
||||||
protected val _overlayContainer: FrameLayout;
|
protected val _overlayContainer: FrameLayout;
|
||||||
protected val _swipeRefresh: SwipeRefreshLayout;
|
protected val _swipeRefresh: SwipeRefreshLayout;
|
||||||
@@ -52,6 +53,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
private val _emptyPagerContainer: FrameLayout;
|
private val _emptyPagerContainer: FrameLayout;
|
||||||
|
|
||||||
protected val _toolbarContentView: LinearLayout;
|
protected val _toolbarContentView: LinearLayout;
|
||||||
|
protected val _bottomContentView: LinearLayout;
|
||||||
|
|
||||||
private var _loading: Boolean = true;
|
private var _loading: Boolean = true;
|
||||||
|
|
||||||
@@ -136,6 +138,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
setActiveTags(null);
|
setActiveTags(null);
|
||||||
|
|
||||||
_toolbarContentView = findViewById(R.id.container_toolbar_content);
|
_toolbarContentView = findViewById(R.id.container_toolbar_content);
|
||||||
|
_bottomContentView = findViewById(R.id.container_bottom);
|
||||||
|
|
||||||
_nextPageHandler = TaskHandler<TPager, Pair<TPager, List<TResult>>>({fragment.lifecycleScope}, {
|
_nextPageHandler = TaskHandler<TPager, Pair<TPager, List<TResult>>>({fragment.lifecycleScope}, {
|
||||||
if (it is IAsyncPager<*>)
|
if (it is IAsyncPager<*>)
|
||||||
|
|||||||
+6
-5
@@ -319,8 +319,7 @@ class LibraryArtistFragment : MainFragment() {
|
|||||||
|
|
||||||
_fragment.topBar?.onShown(channel)
|
_fragment.topBar?.onShown(channel)
|
||||||
|
|
||||||
val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) {
|
val buttons = arrayListOf<Pair<Int, ()->Unit>>();
|
||||||
})
|
|
||||||
|
|
||||||
_fragment.lifecycleScope.launch(Dispatchers.IO) {
|
_fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.contentUrl ?: return@launch)
|
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.contentUrl ?: return@launch)
|
||||||
@@ -337,8 +336,7 @@ class LibraryArtistFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buttonSubscribe.visibility = GONE;
|
_buttonSubscribe.visibility = GONE;
|
||||||
_buttonSubscriptionSettings.visibility =
|
_buttonSubscriptionSettings.visibility = View.GONE
|
||||||
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
|
|
||||||
_textChannel.text = channel.name
|
_textChannel.text = channel.name
|
||||||
_textChannelSub.text = "${channel.countTracks} songs, ${channel.countAlbums} albums";
|
_textChannelSub.text = "${channel.countTracks} songs, ${channel.countAlbums} albums";
|
||||||
|
|
||||||
@@ -506,7 +504,10 @@ class LibraryArtistFragment : MainFragment() {
|
|||||||
|
|
||||||
val playlist = _artist?.toPlaylist();
|
val playlist = _artist?.toPlaylist();
|
||||||
if (playlist != null) {
|
if (playlist != null) {
|
||||||
val index = playlist.videos.indexOf(c);
|
val sameVideo = playlist.videos.find { it.name == c.name };
|
||||||
|
val index = sameVideo?.let {
|
||||||
|
playlist.videos.indexOf(sameVideo)
|
||||||
|
} ?: -1;
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
|
|
||||||
|
|||||||
+29
@@ -8,25 +8,32 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.api.media.models.video.LocalVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.structures.AdhocPager
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.FilesTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.FilesTopBarFragment
|
||||||
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.states.FileEntry
|
import com.futo.platformplayer.states.FileEntry
|
||||||
import com.futo.platformplayer.states.StateLibrary
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.NoResultsView
|
import com.futo.platformplayer.views.NoResultsView
|
||||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder
|
import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import com.futo.platformplayer.views.buttons.ButtonsContainer
|
||||||
|
|
||||||
class LibraryFilesFragment : MainFragment() {
|
class LibraryFilesFragment : MainFragment() {
|
||||||
override val isMainView : Boolean = true;
|
override val isMainView : Boolean = true;
|
||||||
@@ -70,6 +77,7 @@ class LibraryFilesFragment : MainFragment() {
|
|||||||
private var root: FileEntry? = null;
|
private var root: FileEntry? = null;
|
||||||
|
|
||||||
constructor(fragment: LibraryFilesFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
constructor(fragment: LibraryFilesFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
disableRefreshLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onShown(parameter: Any? = null) {
|
fun onShown(parameter: Any? = null) {
|
||||||
@@ -139,6 +147,27 @@ class LibraryFilesFragment : MainFragment() {
|
|||||||
setPager(AdhocPager<FileEntry>({ listOf(); }, stack.files));
|
setPager(AdhocPager<FileEntry>({ listOf(); }, stack.files));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
|
val allSongs = stack.files.filter { !it.isDirectory };
|
||||||
|
if(allSongs.any()) {
|
||||||
|
_bottomContentView.addView(ButtonsContainer(context,
|
||||||
|
listOf(
|
||||||
|
ButtonsContainer.Button("Play All", R.drawable.background_button_primary) {
|
||||||
|
StatePlayer.instance.setPlaylist(Playlist(stack.path.toUri().lastPathSegment ?: "", allSongs.map {
|
||||||
|
SerializedPlatformVideo.fromVideo(LocalVideoDetails.fromContent(it.path))
|
||||||
|
}), focus = true, shuffle = false)
|
||||||
|
},
|
||||||
|
ButtonsContainer.Button("Shuffle", R.drawable.background_button_accent) {
|
||||||
|
StatePlayer.instance.setPlaylist(Playlist(stack.path.toUri().lastPathSegment ?: "", allSongs.map {
|
||||||
|
SerializedPlatformVideo.fromVideo(LocalVideoDetails.fromContent(it.path))
|
||||||
|
}), focus = true, shuffle = true)
|
||||||
|
}
|
||||||
|
)).apply {
|
||||||
|
this.layoutParams = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_bottomContentView.removeAllViews();
|
||||||
|
|
||||||
fragment.topBar?.let {
|
fragment.topBar?.let {
|
||||||
if(it is FilesTopBarFragment) {
|
if(it is FilesTopBarFragment) {
|
||||||
if(navStack.size > 1)
|
if(navStack.size > 1)
|
||||||
|
|||||||
+13
-10
@@ -93,14 +93,18 @@ class LibraryFragment : MainFragment() {
|
|||||||
UIDialogs.showDialog(requireContext(), R.drawable.ic_library,
|
UIDialogs.showDialog(requireContext(), R.drawable.ic_library,
|
||||||
"Music permissions", "We require permissions to see your on-device music, denying this will hide the option to see local music.", null, 1,
|
"Music permissions", "We require permissions to see your on-device music, denying this will hide the option to see local music.", null, 1,
|
||||||
UIDialogs.Action("Ok", {
|
UIDialogs.Action("Ok", {
|
||||||
permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO);
|
StateApp?.instance?.activity?.requestPermissionAudio {
|
||||||
|
setPermissionResultAudio(it);
|
||||||
|
}
|
||||||
}, UIDialogs.ActionStyle.PRIMARY),
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
UIDialogs.Action("Cancel", {
|
UIDialogs.Action("Cancel", {
|
||||||
|
|
||||||
}, UIDialogs.ActionStyle.NONE));
|
}, UIDialogs.ActionStyle.NONE));
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO);
|
StateApp?.instance?.activity?.requestPermissionAudio {
|
||||||
|
setPermissionResultAudio(it);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,24 +117,22 @@ class LibraryFragment : MainFragment() {
|
|||||||
UIDialogs.showDialog(requireContext(), R.drawable.ic_library, false,
|
UIDialogs.showDialog(requireContext(), R.drawable.ic_library, false,
|
||||||
"Videos permissions", "We require permissions to see your on-device videos, denying this will hide the option to see local videos.", null, 1,
|
"Videos permissions", "We require permissions to see your on-device videos, denying this will hide the option to see local videos.", null, 1,
|
||||||
UIDialogs.Action("Ok", {
|
UIDialogs.Action("Ok", {
|
||||||
permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO);
|
StateApp?.instance?.activity?.requestPermissionVideo {
|
||||||
|
setPermissionResultVideo(it);
|
||||||
|
}
|
||||||
}, UIDialogs.ActionStyle.PRIMARY),
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
UIDialogs.Action("Cancel", {
|
UIDialogs.Action("Cancel", {
|
||||||
|
|
||||||
}, UIDialogs.ActionStyle.NONE));
|
}, UIDialogs.ActionStyle.NONE));
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO);
|
StateApp?.instance?.activity?.requestPermissionVideo {
|
||||||
|
setPermissionResultVideo(it);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val permissionReqAudio = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
|
|
||||||
setPermissionResultAudio(isGranted);
|
|
||||||
});
|
|
||||||
val permissionReqVideo = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
|
|
||||||
setPermissionResultVideo(isGranted);
|
|
||||||
});
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance() = LibraryFragment().apply {}
|
fun newInstance() = LibraryFragment().apply {}
|
||||||
@@ -292,6 +294,7 @@ class LibraryFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onShown() {
|
fun onShown() {
|
||||||
|
UIDialogs.appToast("Library is in alpha\nImprovements are coming to local media playback.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+38
-12
@@ -55,6 +55,7 @@ import com.futo.platformplayer.api.media.LiveChatManager
|
|||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
||||||
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
||||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
@@ -77,6 +78,7 @@ import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
|||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.video.LocalVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
@@ -175,6 +177,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -563,6 +566,18 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if (video is TutorialFragment.TutorialVideo) {
|
if (video is TutorialFragment.TutorialVideo) {
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
if(video is LocalVideoDetails) {
|
||||||
|
video?.author?.let {
|
||||||
|
if(it.url.startsWith("content://media/external/audio/artists")) {
|
||||||
|
fragment.navigate<LibraryArtistFragment>(it.url);
|
||||||
|
fragment.lifecycleScope.launch {
|
||||||
|
delay(100);
|
||||||
|
fragment.minimizeVideoDetail();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@setOnClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
(video?.author ?: _searchVideo?.author)?.let {
|
(video?.author ?: _searchVideo?.author)?.let {
|
||||||
fragment.navigate<ChannelFragment>(it);
|
fragment.navigate<ChannelFragment>(it);
|
||||||
@@ -1035,7 +1050,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
}
|
}
|
||||||
else null,
|
else null,
|
||||||
if(!isLimitedVersion && !(video?.isLive ?: false))
|
if(!isLimitedVersion && !(video?.isLive ?: false) && !(video is LocalVideoDetails))
|
||||||
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||||
video?.let {
|
video?.let {
|
||||||
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
||||||
@@ -1058,15 +1073,16 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
}
|
}
|
||||||
else null,
|
else null,
|
||||||
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
|
if(!(video is LocalVideoDetails))
|
||||||
|
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
|
||||||
video?.let {
|
video?.let {
|
||||||
val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url;
|
val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url;
|
||||||
fragment.navigate<BrowserFragment>(url);
|
fragment.navigate<BrowserFragment>(url);
|
||||||
fragment.minimizeVideoDetail();
|
fragment.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
},
|
} else null,
|
||||||
if (StateSync.instance.hasAuthorizedDevice()) {
|
if (StateSync.instance.hasAuthorizedDevice() && !(video is LocalVideoDetails)) {
|
||||||
RoundButton(context, R.drawable.ic_device, context.getString(R.string.send_to_device), TAG_SEND_TO_DEVICE) {
|
RoundButton(context, R.drawable.ic_device, context.getString(R.string.send_to_device), TAG_SEND_TO_DEVICE) {
|
||||||
val devices = StateSync.instance.getAuthorizedSessions();
|
val devices = StateSync.instance.getAuthorizedSessions();
|
||||||
val videoToSend = video ?: return@RoundButton;
|
val videoToSend = video ?: return@RoundButton;
|
||||||
@@ -1089,10 +1105,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}} else null,
|
}} else null,
|
||||||
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
|
if(!(video is LocalVideoDetails))
|
||||||
|
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
|
||||||
reloadVideo();
|
reloadVideo();
|
||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
}).filterNotNull();
|
} else null).filterNotNull();
|
||||||
if(!_buttonPinStore.getAllValues().any())
|
if(!_buttonPinStore.getAllValues().any())
|
||||||
_buttonPins.setButtons(*(buttons + listOf(_buttonMore)).toTypedArray());
|
_buttonPins.setButtons(*(buttons + listOf(_buttonMore)).toTypedArray());
|
||||||
else {
|
else {
|
||||||
@@ -1624,7 +1641,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_buttonSubscribe.setSubscribeChannel(video.author.url);
|
_buttonSubscribe.setSubscribeChannel(video.author.url);
|
||||||
setDescription(video.description.fixHtmlLinks());
|
setDescription(video.description.fixHtmlLinks());
|
||||||
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(video.author.thumbnail, false,
|
||||||
|
video is LocalVideoDetails
|
||||||
|
);
|
||||||
setPolycentricProfile(null, animate = false);
|
setPolycentricProfile(null, animate = false);
|
||||||
_taskLoadPolycentricProfile.run(video.author.id);
|
_taskLoadPolycentricProfile.run(video.author.id);
|
||||||
|
|
||||||
@@ -1652,7 +1671,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_rating.visibility = View.GONE;
|
_rating.visibility = View.GONE;
|
||||||
|
|
||||||
if (StatePolycentric.instance.enabled) {
|
if (StatePolycentric.instance.enabled && !(video is LocalVideoDetails)) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val queryReferencesResponse = ApiMethods.getQueryReferences(
|
val queryReferencesResponse = ApiMethods.getQueryReferences(
|
||||||
@@ -1811,17 +1830,19 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_player.updateNextPrevious();
|
_player.updateNextPrevious();
|
||||||
updateMoreButtons();
|
updateMoreButtons();
|
||||||
|
|
||||||
if (videoDetail is TutorialFragment.TutorialVideo) {
|
if (videoDetail is TutorialFragment.TutorialVideo || videoDetail is LocalVideoDetails) {
|
||||||
_buttonSubscribe.visibility = View.GONE
|
_buttonSubscribe.visibility = View.GONE
|
||||||
_buttonMore.visibility = View.GONE
|
_buttonMore.visibility = if(videoDetail is LocalVideoDetails) View.VISIBLE else View.GONE;
|
||||||
_buttonPins.visibility = View.GONE
|
_buttonPins.visibility = if(videoDetail is LocalVideoDetails) View.VISIBLE else View.GONE;
|
||||||
_layoutRating.visibility = View.GONE
|
_layoutRating.visibility = View.GONE
|
||||||
|
_rating.visibility = View.GONE;
|
||||||
_layoutChangeBottomSection.visibility = View.GONE
|
_layoutChangeBottomSection.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
_buttonSubscribe.visibility = View.VISIBLE
|
_buttonSubscribe.visibility = View.VISIBLE
|
||||||
_buttonMore.visibility = View.VISIBLE
|
_buttonMore.visibility = View.VISIBLE
|
||||||
_buttonPins.visibility = View.VISIBLE
|
_buttonPins.visibility = View.VISIBLE
|
||||||
_layoutRating.visibility = View.VISIBLE
|
_layoutRating.visibility = View.VISIBLE
|
||||||
|
_rating.visibility = View.VISIBLE;
|
||||||
_layoutChangeBottomSection.visibility = View.VISIBLE
|
_layoutChangeBottomSection.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2685,7 +2706,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private fun fetchComments() {
|
private fun fetchComments() {
|
||||||
Logger.i(TAG, "fetchComments")
|
Logger.i(TAG, "fetchComments")
|
||||||
video?.let {
|
video?.let {
|
||||||
_commentsList.load(true) { StatePlatform.instance.getComments(it); };
|
if(video is LocalVideoDetails) {
|
||||||
|
_commentsList.clearComments();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_commentsList.load(true) { StatePlatform.instance.getComments(it); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun fetchPolycentricComments() {
|
private fun fetchPolycentricComments() {
|
||||||
@@ -2972,6 +2997,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChannelClicked.subscribe {
|
onChannelClicked.subscribe {
|
||||||
|
Logger.i(TAG, "Opening channel url: ${it.url}");
|
||||||
if(it.url.isNotBlank()) {
|
if(it.url.isNotBlank()) {
|
||||||
fragment.minimizeVideoDetail()
|
fragment.minimizeVideoDetail()
|
||||||
fragment.navigate<ChannelFragment>(it)
|
fragment.navigate<ChannelFragment>(it)
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ data class Telemetry(
|
|||||||
val brand: String,
|
val brand: String,
|
||||||
val manufacturer: String,
|
val manufacturer: String,
|
||||||
val model: String,
|
val model: String,
|
||||||
val sdkVersion: Int
|
val sdkVersion: Int,
|
||||||
|
val plugins: List<String>? = null
|
||||||
) { }
|
) { }
|
||||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
|||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.Audio.Artists
|
import android.provider.MediaStore.Audio.Artists
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.collection.emptyLongSet
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@@ -173,6 +174,7 @@ class StateLibrary {
|
|||||||
list.add(videoFromCursor(cursor));
|
list.add(videoFromCursor(cursor));
|
||||||
cursor.moveToNext();
|
cursor.moveToNext();
|
||||||
}
|
}
|
||||||
|
Logger.i(TAG, "Videos nextPage: ${list.size}")
|
||||||
return@AdhocPager list;
|
return@AdhocPager list;
|
||||||
}, list);
|
}, list);
|
||||||
//}
|
//}
|
||||||
@@ -243,11 +245,12 @@ class StateLibrary {
|
|||||||
MediaStore.Audio.Media._ID, //0
|
MediaStore.Audio.Media._ID, //0
|
||||||
MediaStore.Audio.Media.DISPLAY_NAME, //1
|
MediaStore.Audio.Media.DISPLAY_NAME, //1
|
||||||
MediaStore.Audio.Media.ARTIST, //2
|
MediaStore.Audio.Media.ARTIST, //2
|
||||||
MediaStore.Audio.Media.ALBUM_ID, //3
|
MediaStore.Audio.Media.ARTIST_ID, //3
|
||||||
MediaStore.Audio.Media.DURATION, //4
|
MediaStore.Audio.Media.ALBUM_ID, //4
|
||||||
MediaStore.Audio.Media.DATE_ADDED, //5
|
MediaStore.Audio.Media.DURATION, //5
|
||||||
MediaStore.Audio.Media.MIME_TYPE, //6
|
MediaStore.Audio.Media.DATE_ADDED, //6
|
||||||
MediaStore.Audio.Media.BUCKET_DISPLAY_NAME //7
|
MediaStore.Audio.Media.MIME_TYPE, //7
|
||||||
|
MediaStore.Audio.Media.BUCKET_DISPLAY_NAME //8
|
||||||
);
|
);
|
||||||
|
|
||||||
fun getDocumentTrack(url: String): IPlatformContentDetails? {
|
fun getDocumentTrack(url: String): IPlatformContentDetails? {
|
||||||
@@ -359,11 +362,12 @@ class StateLibrary {
|
|||||||
val id = cursor.getString(0);
|
val id = cursor.getString(0);
|
||||||
val displayName = cursor.getString(1);
|
val displayName = cursor.getString(1);
|
||||||
val author = cursor.getString(2);
|
val author = cursor.getString(2);
|
||||||
val albumId = cursor.getLong(3);
|
val authorId = cursor.getStringOrNull(3);
|
||||||
val duration = cursor.getLong(4).let { if(it > 0) it / 1000 else 0 };
|
val albumId = cursor.getLong(4);
|
||||||
val date = cursor.getLong(5);
|
val duration = cursor.getLong(5).let { if(it > 0) it / 1000 else 0 };
|
||||||
val contentType = cursor.getString(6);
|
val date = cursor.getLong(6);
|
||||||
val category = cursor.getString(7);
|
val contentType = cursor.getString(7);
|
||||||
|
val category = cursor.getString(8);
|
||||||
|
|
||||||
val idLong = id.toLongOrNull();
|
val idLong = id.toLongOrNull();
|
||||||
val contentUrl = if(idLong != null )
|
val contentUrl = if(idLong != null )
|
||||||
@@ -371,6 +375,13 @@ class StateLibrary {
|
|||||||
else
|
else
|
||||||
"";
|
"";
|
||||||
|
|
||||||
|
val authorIdLong = authorId?.toLongOrNull();
|
||||||
|
val authorUrl = if(authorIdLong != null)
|
||||||
|
ContentUris.withAppendedId(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, authorIdLong).toString();
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
|
||||||
|
|
||||||
val albumContentUrl = if(albumId > 0)
|
val albumContentUrl = if(albumId > 0)
|
||||||
ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, albumId)?.toString()
|
ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, albumId)?.toString()
|
||||||
else null;
|
else null;
|
||||||
@@ -380,7 +391,10 @@ class StateLibrary {
|
|||||||
else null;
|
else null;
|
||||||
|
|
||||||
val authorObj = if(!author.isNullOrBlank())
|
val authorObj = if(!author.isNullOrBlank())
|
||||||
PlatformAuthorLink(PlatformID.NONE, author, "", null, null)
|
PlatformAuthorLink(
|
||||||
|
if(authorId != null) PlatformID("LOCAL", authorId) else PlatformID.NONE,
|
||||||
|
author,
|
||||||
|
authorUrl, null, null)
|
||||||
else PlatformAuthorLink.UNKNOWN;
|
else PlatformAuthorLink.UNKNOWN;
|
||||||
|
|
||||||
return LocalVideoDetails(
|
return LocalVideoDetails(
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ class StateTelemetry {
|
|||||||
Build.BRAND,
|
Build.BRAND,
|
||||||
Build.MANUFACTURER,
|
Build.MANUFACTURER,
|
||||||
Build.MODEL,
|
Build.MODEL,
|
||||||
Build.VERSION.SDK_INT
|
Build.VERSION.SDK_INT,
|
||||||
|
StatePlatform.instance.getEnabledClients().map { it.id }.toList()
|
||||||
);
|
);
|
||||||
|
|
||||||
val headers = hashMapOf(
|
val headers = hashMapOf(
|
||||||
|
|||||||
+2
-2
@@ -40,10 +40,10 @@ class ArtistTileViewHolder(val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder
|
|||||||
if (artist.thumbnail != null)
|
if (artist.thumbnail != null)
|
||||||
Glide.with(it)
|
Glide.with(it)
|
||||||
.load(artist.thumbnail)
|
.load(artist.thumbnail)
|
||||||
.placeholder(R.drawable.unknown_music)
|
.placeholder(R.drawable.ic_artist)
|
||||||
.into(it)
|
.into(it)
|
||||||
else
|
else
|
||||||
Glide.with(it).load(R.drawable.unknown_music).into(it);
|
Glide.with(it).load(R.drawable.ic_artist).into(it);
|
||||||
};
|
};
|
||||||
|
|
||||||
_textName.text = artist.name;
|
_textName.text = artist.name;
|
||||||
|
|||||||
+2
-2
@@ -42,11 +42,11 @@ class FileViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHold
|
|||||||
_file = file;
|
_file = file;
|
||||||
_imageThumbnail?.let {
|
_imageThumbnail?.let {
|
||||||
if(file.isDirectory)
|
if(file.isDirectory)
|
||||||
it.setImageResource(R.drawable.ic_library);
|
it.setImageResource(R.drawable.ic_folder);
|
||||||
else {
|
else {
|
||||||
Glide.with(it)
|
Glide.with(it)
|
||||||
.load(file.thumbnail)
|
.load(file.thumbnail)
|
||||||
.placeholder(R.drawable.ic_music)
|
.placeholder(R.drawable.ic_song)
|
||||||
.into(it)
|
.into(it)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.futo.platformplayer.views.buttons
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.collection.emptyLongSet
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.views.pills.PillButton
|
||||||
|
|
||||||
|
class ButtonsContainer : LinearLayout {
|
||||||
|
|
||||||
|
val container_buttons: LinearLayout
|
||||||
|
|
||||||
|
var currentButtons: List<Button> = listOf();
|
||||||
|
|
||||||
|
constructor(context: Context, buttons: List<Button>) : super(context) {
|
||||||
|
inflate(context, R.layout.view_buttons, this)
|
||||||
|
container_buttons = findViewById(R.id.container_buttons);
|
||||||
|
setButtons(buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setButtons(buttons: List<Button>) {
|
||||||
|
this.currentButtons = buttons;
|
||||||
|
container_buttons.removeAllViews();
|
||||||
|
for(button in buttons) {
|
||||||
|
container_buttons.addView(StandardButton(context, button.name) {
|
||||||
|
button?.handler?.invoke();
|
||||||
|
}.apply {
|
||||||
|
this.layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
this.weight = 1f;
|
||||||
|
};
|
||||||
|
if(button.background != null)
|
||||||
|
this.withBackground(button.background);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Button(
|
||||||
|
val name: String,
|
||||||
|
val background: Int?,
|
||||||
|
val handler: (()->Unit),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.futo.platformplayer.views.buttons
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
|
||||||
|
class StandardButton : LinearLayout {
|
||||||
|
private val _root: LinearLayout;
|
||||||
|
private val _text: TextView;
|
||||||
|
|
||||||
|
constructor(context: Context, text: String, onClick: ()->Unit) : super(context) {
|
||||||
|
inflate(context, R.layout.view_button_standard, this);
|
||||||
|
_root = findViewById(R.id.root);
|
||||||
|
_text = findViewById(R.id.text_button);
|
||||||
|
_text.text = text;
|
||||||
|
_root.setOnClickListener {
|
||||||
|
onClick.invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withPrimaryBackground(): StandardButton {
|
||||||
|
_root.setBackgroundResource(R.drawable.background_button_primary)
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
fun withAccentBackground(): StandardButton {
|
||||||
|
_root.setBackgroundResource(R.drawable.background_button_accent)
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
fun withBackground(id: Int): StandardButton {
|
||||||
|
_root.setBackgroundResource(id);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,9 +54,14 @@ class CreatorThumbnail : ConstraintLayout {
|
|||||||
setNewActivity(false);
|
setNewActivity(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setThumbnail(url: String?, animate: Boolean) {
|
fun setThumbnail(url: String?, animate: Boolean, isArtist: Boolean = false) {
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
clear();
|
if(isArtist) {
|
||||||
|
_imageChannelThumbnail.setImageResource(R.drawable.ic_artist);
|
||||||
|
_imageChannelThumbnail.visibility = View.VISIBLE;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,18 +83,21 @@ class CreatorThumbnail : ConstraintLayout {
|
|||||||
} else {
|
} else {
|
||||||
setHarborAvailable(false, animate, null);
|
setHarborAvailable(false, animate, null);
|
||||||
}
|
}
|
||||||
|
var placeholder = R.drawable.placeholder_channel_thumbnail;
|
||||||
|
if(url.startsWith("content://") || isArtist)
|
||||||
|
placeholder = R.drawable.ic_artist;
|
||||||
|
|
||||||
if (animate) {
|
if (animate) {
|
||||||
Glide.with(_imageChannelThumbnail)
|
Glide.with(_imageChannelThumbnail)
|
||||||
.load(url)
|
.load(url)
|
||||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
.placeholder(placeholder)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
||||||
.crossfade()
|
.crossfade()
|
||||||
.into(_imageChannelThumbnail)
|
.into(_imageChannelThumbnail)
|
||||||
} else {
|
} else {
|
||||||
Glide.with(_imageChannelThumbnail)
|
Glide.with(_imageChannelThumbnail)
|
||||||
.load(url)
|
.load(url)
|
||||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
.placeholder(placeholder)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
||||||
.into(_imageChannelThumbnail);
|
.into(_imageChannelThumbnail);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class PillButton : LinearLayout {
|
|||||||
val onClick = Event0();
|
val onClick = Event0();
|
||||||
private var _isLoading = false;
|
private var _isLoading = false;
|
||||||
|
|
||||||
constructor(context : Context, attrs : AttributeSet) : super(context, attrs) {
|
constructor(context : Context, attrs : AttributeSet?) : super(context, attrs) {
|
||||||
LayoutInflater.from(context).inflate(R.layout.pill_button, this, true);
|
LayoutInflater.from(context).inflate(R.layout.pill_button, this, true);
|
||||||
icon = findViewById(R.id.pill_icon);
|
icon = findViewById(R.id.pill_icon);
|
||||||
text = findViewById(R.id.pill_text);
|
text = findViewById(R.id.pill_text);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
|||||||
import com.futo.platformplayer.api.media.models.comments.LazyComment
|
import com.futo.platformplayer.api.media.models.comments.LazyComment
|
||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
@@ -224,6 +225,12 @@ class CommentsList : ConstraintLayout {
|
|||||||
_commentsPager = pager;
|
_commentsPager = pager;
|
||||||
onCommentsLoaded.emit(_comments.size);
|
onCommentsLoaded.emit(_comments.size);
|
||||||
}
|
}
|
||||||
|
fun clearComments() {
|
||||||
|
_comments.clear();
|
||||||
|
_adapterComments.notifyDataSetChanged();
|
||||||
|
_commentsPager = EmptyPager();
|
||||||
|
onCommentsLoaded.emit(0);
|
||||||
|
}
|
||||||
|
|
||||||
fun load(readonly: Boolean, loader: suspend () -> IPager<IPlatformComment>) {
|
fun load(readonly: Boolean, loader: suspend () -> IPager<IPlatformComment>) {
|
||||||
cancel();
|
cancel();
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960"
|
android:viewportHeight="960">
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M450,780L450,294.92L222.15,522.77L180,480L480,180L780,480L737.85,522.77L510,294.92L510,780L450,780Z"/>
|
android:pathData="M450,780L450,294.92L222.15,522.77L180,480L480,180L780,480L737.85,522.77L510,294.92L510,780L450,780Z"/>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 462 B |
@@ -2,9 +2,8 @@
|
|||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960"
|
android:viewportHeight="960">
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M493.85,593.85Q531.61,593.85 557.73,567.73Q583.84,541.62 583.84,503.85L583.84,279.23L698.46,279.23L698.46,208.46L548.46,208.46L548.46,434.62Q537,424.23 523.35,419.04Q509.69,413.85 493.85,413.85Q456.08,413.85 429.96,439.96Q403.85,466.08 403.85,503.85Q403.85,541.62 429.96,567.73Q456.08,593.85 493.85,593.85ZM322.31,700Q292,700 271,679Q250,658 250,627.69L250,172.31Q250,142 271,121Q292,100 322.31,100L777.69,100Q808,100 829,121Q850,142 850,172.31L850,627.69Q850,658 829,679Q808,700 777.69,700L322.31,700ZM322.31,640L777.69,640Q782.31,640 786.15,636.15Q790,632.31 790,627.69L790,172.31Q790,167.69 786.15,163.85Q782.31,160 777.69,160L322.31,160Q317.69,160 313.85,163.85Q310,167.69 310,172.31L310,627.69Q310,632.31 313.85,636.15Q317.69,640 322.31,640ZM182.31,840Q152,840 131,819Q110,798 110,767.69L110,252.31L170,252.31L170,767.69Q170,772.31 173.85,776.15Q177.69,780 182.31,780L697.69,780L697.69,840L182.31,840ZM310,160L310,160Q310,160 310,163.46Q310,166.92 310,172.31L310,627.69Q310,633.08 310,636.54Q310,640 310,640L310,640Q310,640 310,636.54Q310,633.08 310,627.69L310,172.31Q310,166.92 310,163.46Q310,160 310,160Z"/>
|
android:pathData="M493.85,593.85Q531.61,593.85 557.73,567.73Q583.84,541.62 583.84,503.85L583.84,279.23L698.46,279.23L698.46,208.46L548.46,208.46L548.46,434.62Q537,424.23 523.35,419.04Q509.69,413.85 493.85,413.85Q456.08,413.85 429.96,439.96Q403.85,466.08 403.85,503.85Q403.85,541.62 429.96,567.73Q456.08,593.85 493.85,593.85ZM322.31,700Q292,700 271,679Q250,658 250,627.69L250,172.31Q250,142 271,121Q292,100 322.31,100L777.69,100Q808,100 829,121Q850,142 850,172.31L850,627.69Q850,658 829,679Q808,700 777.69,700L322.31,700ZM182.31,840Q152,840 131,819Q110,798 110,767.69L110,252.31L170,252.31L170,767.69Q170,772.31 173.85,776.15Q177.69,780 182.31,780L697.69,780L697.69,840L182.31,840Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 826 B |
@@ -29,6 +29,8 @@
|
|||||||
android:id="@+id/image_qr"
|
android:id="@+id/image_qr"
|
||||||
android:layout_width="200dp"
|
android:layout_width="200dp"
|
||||||
android:layout_height="200dp"
|
android:layout_height="200dp"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:padding="8dp"
|
||||||
app:srcCompat="@drawable/ic_qr"
|
app:srcCompat="@drawable/ic_qr"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
@@ -37,15 +39,31 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_qr"
|
android:id="@+id/text_qr"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/scan_to_import"
|
android:text="@string/scan_to_import"
|
||||||
android:fontFamily="@font/inter_light"
|
android:fontFamily="@font/inter_light"
|
||||||
android:textSize="32dp"
|
android:textSize="32dp"
|
||||||
|
android:textAlignment="center"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/image_qr"
|
app:layout_constraintTop_toBottomOf="@id/image_qr"
|
||||||
app:layout_constraintLeft_toLeftOf="@id/image_qr"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="@id/image_qr" />
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_qr_hint"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/tap_qr_code_for_fullscreen"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:textColor="@color/gray_400"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_qr"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@id/text_qr"
|
||||||
|
app:layout_constraintRight_toRightOf="@id/text_qr" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/layout_buttons"
|
android:id="@+id/layout_buttons"
|
||||||
@@ -55,7 +73,7 @@
|
|||||||
android:layout_marginStart="20dp"
|
android:layout_marginStart="20dp"
|
||||||
android:layout_marginEnd="20dp"
|
android:layout_marginEnd="20dp"
|
||||||
android:layout_marginTop="30dp"
|
android:layout_marginTop="30dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_qr"
|
app:layout_constraintTop_toBottomOf="@id/text_qr_hint"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent">
|
app:layout_constraintRight_toRightOf="parent">
|
||||||
|
|
||||||
@@ -75,6 +93,15 @@
|
|||||||
app:buttonSubText="@string/copy_your_identity_to_clipboard"
|
app:buttonSubText="@string/copy_your_identity_to_clipboard"
|
||||||
app:buttonIcon="@drawable/ic_copy"
|
app:buttonIcon="@drawable/ic_copy"
|
||||||
android:layout_marginTop="8dp" />
|
android:layout_marginTop="8dp" />
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
android:id="@+id/button_export_file"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:buttonText="@string/export_to_file"
|
||||||
|
app:buttonSubText="@string/save_profile_to_file_for_sharing"
|
||||||
|
app:buttonIcon="@drawable/ic_download"
|
||||||
|
android:layout_marginTop="8dp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
|||||||
@@ -47,6 +47,28 @@
|
|||||||
android:text="@string/scan_qr" />
|
android:text="@string/scan_qr" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/button_import_file"
|
||||||
|
android:layout_width="140dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="@drawable/background_button_primary_round"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/button_scan_profile">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="16dp"
|
||||||
|
android:text="@string/import_from_file" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_or"
|
android:id="@+id/text_or"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -55,7 +77,7 @@
|
|||||||
android:fontFamily="@font/inter_light"
|
android:fontFamily="@font/inter_light"
|
||||||
android:textSize="28dp"
|
android:textSize="28dp"
|
||||||
android:layout_marginTop="30dp"
|
android:layout_marginTop="30dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/button_scan_profile"
|
app:layout_constraintTop_toBottomOf="@id/button_import_file"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent" />
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black">
|
||||||
|
|
||||||
|
<!-- Top navigation bar -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_back_fullscreen"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_back"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginStart="16dp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_close_fullscreen"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:contentDescription="@string/cd_button_close"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
app:srcCompat="@drawable/ic_close"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="16dp" />
|
||||||
|
|
||||||
|
<!-- Full screen QR code -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_qr_fullscreen"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:padding="40dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/button_back_fullscreen"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginEnd="20dp" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout
|
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/feed_root"
|
||||||
android:id="@+id/feed_root"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/container_bottom"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
tools:context=".fragment.mainactivity.main.FeedFragment">
|
tools:context=".fragment.mainactivity.main.FeedFragment">
|
||||||
|
|
||||||
@@ -124,6 +126,12 @@
|
|||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/container_bottom"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/overlay_container"
|
android:id="@+id/overlay_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -131,4 +139,4 @@
|
|||||||
android:elevation="100dp"
|
android:elevation="100dp"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
</FrameLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
android:layout_marginBottom="5dp"
|
android:layout_marginBottom="5dp"
|
||||||
android:layout_marginLeft="10dp"
|
android:layout_marginLeft="10dp"
|
||||||
android:layout_marginRight="10dp"
|
android:layout_marginRight="10dp"
|
||||||
android:background="@drawable/background_16_round_4dp"
|
android:background="@drawable/background_1b_round_6dp"
|
||||||
android:id="@+id/root"
|
android:id="@+id/root"
|
||||||
android:clickable="true">
|
android:clickable="true">
|
||||||
|
|
||||||
@@ -23,12 +23,10 @@
|
|||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent">
|
||||||
android:background="@drawable/background_1b_round_6dp">
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_thumbnail"
|
android:id="@+id/image_thumbnail"
|
||||||
android:alpha="0.4"
|
|
||||||
android:layout_height="34dp"
|
android:layout_height="34dp"
|
||||||
android:layout_width="34dp"
|
android:layout_width="34dp"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@drawable/background_button_accent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:id="@+id/root">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:autoSizeTextType="uniform"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:text="Play all" />
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:id="@+id/root">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/container_buttons"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="14dp"
|
||||||
|
android:layout_marginEnd="14dp"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:layout_marginBottom="14dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:showDividers="middle"
|
||||||
|
android:divider="@drawable/divider_transparent_4dp">
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -388,6 +388,7 @@
|
|||||||
<string name="unhandled_exception_in_vs">استثناء غير معالج في VS</string>
|
<string name="unhandled_exception_in_vs">استثناء غير معالج في VS</string>
|
||||||
<string name="send_exception_to_developers">إرسال الاستثناء للمطورين…</string>
|
<string name="send_exception_to_developers">إرسال الاستثناء للمطورين…</string>
|
||||||
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">تم تعيين مفتاح الترخيص الخاص بك!\nقد يكون هناك حاجة لإعادة تشغيل التطبيق.</string>
|
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">تم تعيين مفتاح الترخيص الخاص بك!\nقد يكون هناك حاجة لإعادة تشغيل التطبيق.</string>
|
||||||
|
<string name="qr_code_too_large_use_text_below">رمز الاستجابة السريعة كبير جدًا. استخدم النص أدناه لمشاركة ملفك الشخصي.</string>
|
||||||
<string name="invalid_license_format">تنسيق الترخيص غير صالح</string>
|
<string name="invalid_license_format">تنسيق الترخيص غير صالح</string>
|
||||||
<string name="unknown_content_format">تنسيق المحتوى غير معروف</string>
|
<string name="unknown_content_format">تنسيق المحتوى غير معروف</string>
|
||||||
<string name="unknown_file_format">تنسيق الملف غير معروف</string>
|
<string name="unknown_file_format">تنسيق الملف غير معروف</string>
|
||||||
|
|||||||
@@ -395,6 +395,7 @@
|
|||||||
<string name="unhandled_exception_in_vs">Unbehandelte Ausnahme in VS</string>
|
<string name="unhandled_exception_in_vs">Unbehandelte Ausnahme in VS</string>
|
||||||
<string name="send_exception_to_developers">Ausnahme an Entwickler senden…</string>
|
<string name="send_exception_to_developers">Ausnahme an Entwickler senden…</string>
|
||||||
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">Ihr Lizenzschlüssel wurde festgelegt!\nEin Neustart der App könnte erforderlich sein.</string>
|
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">Ihr Lizenzschlüssel wurde festgelegt!\nEin Neustart der App könnte erforderlich sein.</string>
|
||||||
|
<string name="qr_code_too_large_use_text_below">QR-Code zu groß. Verwenden Sie den Text unten, um Ihr Profil zu teilen.</string>
|
||||||
<string name="invalid_license_format">Ungültiges Lizenzformat</string>
|
<string name="invalid_license_format">Ungültiges Lizenzformat</string>
|
||||||
<string name="unknown_content_format">Unbekanntes Inhaltsformat</string>
|
<string name="unknown_content_format">Unbekanntes Inhaltsformat</string>
|
||||||
<string name="unknown_file_format">Unbekanntes Dateiformat</string>
|
<string name="unknown_file_format">Unbekanntes Dateiformat</string>
|
||||||
|
|||||||
@@ -372,6 +372,7 @@
|
|||||||
<string name="unhandled_exception_in_vs">Excepción no manejada en VS</string>
|
<string name="unhandled_exception_in_vs">Excepción no manejada en VS</string>
|
||||||
<string name="send_exception_to_developers">Enviar excepción a los desarrolladores...</string>
|
<string name="send_exception_to_developers">Enviar excepción a los desarrolladores...</string>
|
||||||
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">¡Se ha configurado tu clave de licencia!\nPuede ser necesario reiniciar la aplicación.</string>
|
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">¡Se ha configurado tu clave de licencia!\nPuede ser necesario reiniciar la aplicación.</string>
|
||||||
|
<string name="qr_code_too_large_use_text_below">Código QR demasiado grande. Use el texto de abajo para compartir su perfil.</string>
|
||||||
<string name="invalid_license_format">Formato de licencia no válido</string>
|
<string name="invalid_license_format">Formato de licencia no válido</string>
|
||||||
<string name="unknown_content_format">Formato de contenido desconocido</string>
|
<string name="unknown_content_format">Formato de contenido desconocido</string>
|
||||||
<string name="unknown_file_format">Formato de archivo desconocido</string>
|
<string name="unknown_file_format">Formato de archivo desconocido</string>
|
||||||
|
|||||||
@@ -411,6 +411,7 @@
|
|||||||
<string name="unhandled_exception_in_vs">Exception non gérée dans VS</string>
|
<string name="unhandled_exception_in_vs">Exception non gérée dans VS</string>
|
||||||
<string name="send_exception_to_developers">Envoyer l\'exception aux développeurs…</string>
|
<string name="send_exception_to_developers">Envoyer l\'exception aux développeurs…</string>
|
||||||
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">Votre clé de licence a été définie !\nUn redémarrage de l\'application peut être nécessaire.</string>
|
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">Votre clé de licence a été définie !\nUn redémarrage de l\'application peut être nécessaire.</string>
|
||||||
|
<string name="qr_code_too_large_use_text_below">Code QR trop volumineux. Utilisez le texte ci-dessous pour partager votre profil.</string>
|
||||||
<string name="invalid_license_format">Format de licence invalide</string>
|
<string name="invalid_license_format">Format de licence invalide</string>
|
||||||
<string name="unknown_content_format">Format de contenu inconnu</string>
|
<string name="unknown_content_format">Format de contenu inconnu</string>
|
||||||
<string name="unknown_file_format">Format de fichier inconnu</string>
|
<string name="unknown_file_format">Format de fichier inconnu</string>
|
||||||
|
|||||||
@@ -617,6 +617,7 @@
|
|||||||
<string name="unhandled_exception_in_vs">Eccezione non gestita in VS</string>
|
<string name="unhandled_exception_in_vs">Eccezione non gestita in VS</string>
|
||||||
<string name="send_exception_to_developers">Invio eccezione agli sviluppatori…</string>
|
<string name="send_exception_to_developers">Invio eccezione agli sviluppatori…</string>
|
||||||
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">La tua chiave di licenza è stata impostata!\nIl riavvio dell\'app potrebbe essere richiesto.</string>
|
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">La tua chiave di licenza è stata impostata!\nIl riavvio dell\'app potrebbe essere richiesto.</string>
|
||||||
|
<string name="qr_code_too_large_use_text_below">Codice QR troppo grande. Usa il testo qui sotto per condividere il tuo profilo.</string>
|
||||||
<string name="invalid_license_format">Formato licenza non valido</string>
|
<string name="invalid_license_format">Formato licenza non valido</string>
|
||||||
<string name="unknown_content_format">Formato contenuto sconosciuto</string>
|
<string name="unknown_content_format">Formato contenuto sconosciuto</string>
|
||||||
<string name="unknown_file_format">Formato file sconosciuto</string>
|
<string name="unknown_file_format">Formato file sconosciuto</string>
|
||||||
|
|||||||
@@ -374,6 +374,7 @@
|
|||||||
<string name="unhandled_exception_in_vs">VSで未処理の例外</string>
|
<string name="unhandled_exception_in_vs">VSで未処理の例外</string>
|
||||||
<string name="send_exception_to_developers">開発者に例外を送信…</string>
|
<string name="send_exception_to_developers">開発者に例外を送信…</string>
|
||||||
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">ライセンスキーが設定されました!\nアプリを再起動する可能性があります。</string>
|
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">ライセンスキーが設定されました!\nアプリを再起動する可能性があります。</string>
|
||||||
|
<string name="qr_code_too_large_use_text_below">QRコードが大きすぎます。下のテキストを使用してプロフィールを共有してください。</string>
|
||||||
<string name="invalid_license_format">無効なライセンス形式</string>
|
<string name="invalid_license_format">無効なライセンス形式</string>
|
||||||
<string name="unknown_content_format">不明なコンテンツ形式</string>
|
<string name="unknown_content_format">不明なコンテンツ形式</string>
|
||||||
<string name="unknown_file_format">不明なファイル形式</string>
|
<string name="unknown_file_format">不明なファイル形式</string>
|
||||||
|
|||||||
@@ -410,6 +410,7 @@
|
|||||||
<string name="unhandled_exception_in_vs">VS에서 처리되지 않은 예외</string>
|
<string name="unhandled_exception_in_vs">VS에서 처리되지 않은 예외</string>
|
||||||
<string name="send_exception_to_developers">개발자에게 예외를 보냅니다…</string>
|
<string name="send_exception_to_developers">개발자에게 예외를 보냅니다…</string>
|
||||||
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">라이선스 키가 설정되었습니다!\n앱을 다시 시작해야 할 수 있습니다.</string>
|
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">라이선스 키가 설정되었습니다!\n앱을 다시 시작해야 할 수 있습니다.</string>
|
||||||
|
<string name="qr_code_too_large_use_text_below">QR 코드가 너무 큽니다. 아래 텍스트를 사용하여 프로필을 공유하세요.</string>
|
||||||
<string name="invalid_license_format">잘못된 라이선스 형식</string>
|
<string name="invalid_license_format">잘못된 라이선스 형식</string>
|
||||||
<string name="unknown_content_format">알 수 없는 콘텐츠 형식</string>
|
<string name="unknown_content_format">알 수 없는 콘텐츠 형식</string>
|
||||||
<string name="unknown_file_format">알 수 없는 파일 형식</string>
|
<string name="unknown_file_format">알 수 없는 파일 형식</string>
|
||||||
|
|||||||
@@ -407,6 +407,7 @@
|
|||||||
<string name="unhandled_exception_in_vs">Exceção não tratada no VS</string>
|
<string name="unhandled_exception_in_vs">Exceção não tratada no VS</string>
|
||||||
<string name="send_exception_to_developers">Enviar exceção aos desenvolvedores…</string>
|
<string name="send_exception_to_developers">Enviar exceção aos desenvolvedores…</string>
|
||||||
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">Sua chave de licença foi definida!\nUma reinicialização do aplicativo pode ser necessária.</string>
|
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">Sua chave de licença foi definida!\nUma reinicialização do aplicativo pode ser necessária.</string>
|
||||||
|
<string name="qr_code_too_large_use_text_below">Código QR muito grande. Use o texto abaixo para compartilhar seu perfil.</string>
|
||||||
<string name="invalid_license_format">Formato de licença inválido</string>
|
<string name="invalid_license_format">Formato de licença inválido</string>
|
||||||
<string name="unknown_content_format">Formato de conteúdo desconhecido</string>
|
<string name="unknown_content_format">Formato de conteúdo desconhecido</string>
|
||||||
<string name="unknown_file_format">Formato de arquivo desconhecido</string>
|
<string name="unknown_file_format">Formato de arquivo desconhecido</string>
|
||||||
|
|||||||
@@ -407,6 +407,7 @@
|
|||||||
<string name="unhandled_exception_in_vs">Необработанное исключение в VS</string>
|
<string name="unhandled_exception_in_vs">Необработанное исключение в VS</string>
|
||||||
<string name="send_exception_to_developers">Отправить исключение разработчикам…</string>
|
<string name="send_exception_to_developers">Отправить исключение разработчикам…</string>
|
||||||
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">Ваш лицензионный ключ установлен!\nМожет потребоваться перезагрузка приложения.</string>
|
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">Ваш лицензионный ключ установлен!\nМожет потребоваться перезагрузка приложения.</string>
|
||||||
|
<string name="qr_code_too_large_use_text_below">QR-код слишком большой. Используйте текст ниже, чтобы поделиться своим профилем.</string>
|
||||||
<string name="invalid_license_format">Неверный формат лицензии</string>
|
<string name="invalid_license_format">Неверный формат лицензии</string>
|
||||||
<string name="unknown_content_format">Неизвестный формат содержимого</string>
|
<string name="unknown_content_format">Неизвестный формат содержимого</string>
|
||||||
<string name="unknown_file_format">Неизвестный формат файла</string>
|
<string name="unknown_file_format">Неизвестный формат файла</string>
|
||||||
|
|||||||
@@ -581,6 +581,7 @@
|
|||||||
<string name="unhandled_exception_in_vs">VS\'de bilinmeyen hata (exception)</string>
|
<string name="unhandled_exception_in_vs">VS\'de bilinmeyen hata (exception)</string>
|
||||||
<string name="send_exception_to_developers">Geliştiricilere exception\'ı gönder…</string>
|
<string name="send_exception_to_developers">Geliştiricilere exception\'ı gönder…</string>
|
||||||
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">Lisans anahtarınız ayarlandı!\nUygulamayı yeniden başlatmanız gerekebilir.</string>
|
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">Lisans anahtarınız ayarlandı!\nUygulamayı yeniden başlatmanız gerekebilir.</string>
|
||||||
|
<string name="qr_code_too_large_use_text_below">QR kodu çok büyük. Profilinizi paylaşmak için aşağıdaki metni kullanın.</string>
|
||||||
<string name="invalid_license_format">Geçersiz lisans formatı</string>
|
<string name="invalid_license_format">Geçersiz lisans formatı</string>
|
||||||
<string name="unknown_content_format">Bilinmeyen içerik formatı</string>
|
<string name="unknown_content_format">Bilinmeyen içerik formatı</string>
|
||||||
<string name="unknown_file_format">Bilinmeyen dosya formatı</string>
|
<string name="unknown_file_format">Bilinmeyen dosya formatı</string>
|
||||||
|
|||||||
@@ -411,6 +411,7 @@
|
|||||||
<string name="unhandled_exception_in_vs">VS 中的未处理异常</string>
|
<string name="unhandled_exception_in_vs">VS 中的未处理异常</string>
|
||||||
<string name="send_exception_to_developers">向开发者发送异常…</string>
|
<string name="send_exception_to_developers">向开发者发送异常…</string>
|
||||||
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">您的许可证密钥已设置!\n可能需要重新启动应用程序。</string>
|
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">您的许可证密钥已设置!\n可能需要重新启动应用程序。</string>
|
||||||
|
<string name="qr_code_too_large_use_text_below">二维码太大。请使用下方文字分享您的个人资料。</string>
|
||||||
<string name="invalid_license_format">无效的许可证格式</string>
|
<string name="invalid_license_format">无效的许可证格式</string>
|
||||||
<string name="unknown_content_format">未知的内容格式</string>
|
<string name="unknown_content_format">未知的内容格式</string>
|
||||||
<string name="unknown_file_format">未知的文件格式</string>
|
<string name="unknown_file_format">未知的文件格式</string>
|
||||||
|
|||||||
@@ -646,6 +646,7 @@
|
|||||||
<string name="unhandled_exception_in_vs">Unhandled exception in VS</string>
|
<string name="unhandled_exception_in_vs">Unhandled exception in VS</string>
|
||||||
<string name="send_exception_to_developers">Send exception to developers…</string>
|
<string name="send_exception_to_developers">Send exception to developers…</string>
|
||||||
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">Your license key has been set!\nAn app restart might be required.</string>
|
<string name="your_license_key_has_been_set_an_app_restart_might_be_required">Your license key has been set!\nAn app restart might be required.</string>
|
||||||
|
<string name="qr_code_too_large_use_text_below">QR code too large. Use the text below to share your profile.</string>
|
||||||
<string name="invalid_license_format">Invalid license format</string>
|
<string name="invalid_license_format">Invalid license format</string>
|
||||||
<string name="unknown_content_format">Unknown content format</string>
|
<string name="unknown_content_format">Unknown content format</string>
|
||||||
<string name="unknown_file_format">Unknown file format</string>
|
<string name="unknown_file_format">Unknown file format</string>
|
||||||
@@ -1175,4 +1176,10 @@
|
|||||||
<item>1500</item>
|
<item>1500</item>
|
||||||
<item>2000</item>
|
<item>2000</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string name="qr_code_too_large_use_file_export">Export to file or copy backup code</string>
|
||||||
|
<string name="tap_qr_code_for_fullscreen">Tap QR code for fullscreen view</string>
|
||||||
|
<string name="export_to_file">Export to File</string>
|
||||||
|
<string name="import_from_file">Import from File</string>
|
||||||
|
<string name="save_profile_to_file_for_sharing">Save profile to file for sharing</string>
|
||||||
|
<string name="profile_saved_successfully">Profile saved successfully</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user