mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-05-16 14:27:20 +02:00
Added icon based on the system key when a polycentric user has not set a profile picture. Made managing Polycentric profile most robust. Fixed an issue where latest events were not always being shown in relation to Polycentric profiles. Added copyable (on long press) system key in Polycentric profile activity. Added tutorial videos tab and flow.
This commit is contained in:
parent
65174ffc97
commit
309a57f5a1
@ -1,11 +1,13 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.adapters.CommentViewHolder
|
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.SystemState
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@ -47,6 +49,15 @@ fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
||||||
|
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
||||||
|
if (!systemState.servers.contains(PolycentricCache.STAGING_SERVER)) {
|
||||||
|
removeServer(PolycentricCache.STAGING_SERVER)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
||||||
|
removeServer(PolycentricCache.SERVER)
|
||||||
|
}
|
||||||
|
|
||||||
val exceptions = fullyBackfillServers()
|
val exceptions = fullyBackfillServers()
|
||||||
for (pair in exceptions) {
|
for (pair in exceptions) {
|
||||||
val server = pair.key
|
val server = pair.key
|
||||||
|
@ -8,12 +8,15 @@ import android.widget.ImageButton
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
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.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
import com.futo.polycentric.core.KeyPair
|
import com.futo.polycentric.core.KeyPair
|
||||||
import com.futo.polycentric.core.Process
|
import com.futo.polycentric.core.Process
|
||||||
import com.futo.polycentric.core.ProcessSecret
|
import com.futo.polycentric.core.ProcessSecret
|
||||||
@ -21,6 +24,9 @@ import com.futo.polycentric.core.SignedEvent
|
|||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.base64UrlToByteArray
|
import com.futo.polycentric.core.base64UrlToByteArray
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import userpackage.Protocol.ExportBundle
|
import userpackage.Protocol.ExportBundle
|
||||||
|
|
||||||
@ -29,6 +35,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonScanProfile: LinearLayout;
|
private lateinit var _buttonScanProfile: LinearLayout;
|
||||||
private lateinit var _buttonImportProfile: LinearLayout;
|
private lateinit var _buttonImportProfile: LinearLayout;
|
||||||
private lateinit var _editProfile: EditText;
|
private lateinit var _editProfile: EditText;
|
||||||
|
private lateinit var _loaderOverlay: LoaderOverlay;
|
||||||
|
|
||||||
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
@ -52,6 +59,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
_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);
|
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
||||||
|
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||||
_editProfile = findViewById(R.id.edit_profile);
|
_editProfile = findViewById(R.id.edit_profile);
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
@ -94,42 +102,57 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
_loaderOverlay.show()
|
||||||
val data = url.substring("polycentric://".length).base64UrlToByteArray();
|
|
||||||
val urlInfo = Protocol.URLInfo.parseFrom(data);
|
|
||||||
if (urlInfo.urlType != 3L) {
|
|
||||||
throw Exception("Expected urlInfo struct of type ExportBundle")
|
|
||||||
}
|
|
||||||
|
|
||||||
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
try {
|
||||||
|
val data = url.substring("polycentric://".length).base64UrlToByteArray();
|
||||||
|
val urlInfo = Protocol.URLInfo.parseFrom(data);
|
||||||
|
if (urlInfo.urlType != 3L) {
|
||||||
|
throw Exception("Expected urlInfo struct of type ExportBundle")
|
||||||
|
}
|
||||||
|
|
||||||
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
||||||
if (existingProcessSecret != null) {
|
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
||||||
UIDialogs.toast(this, getString(R.string.this_profile_is_already_imported));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
||||||
Store.instance.addProcessSecret(processSecret);
|
if (existingProcessSecret != null) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
|
||||||
|
}
|
||||||
|
return@launch;
|
||||||
|
}
|
||||||
|
|
||||||
val processHandle = processSecret.toProcessHandle();
|
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||||
|
Store.instance.addProcessSecret(processSecret);
|
||||||
|
|
||||||
for (e in exportBundle.events.eventsList) {
|
val processHandle = processSecret.toProcessHandle();
|
||||||
try {
|
|
||||||
val se = SignedEvent.fromProto(e);
|
for (e in exportBundle.events.eventsList) {
|
||||||
Store.instance.putSignedEvent(se);
|
try {
|
||||||
} catch (e: Throwable) {
|
val se = SignedEvent.fromProto(e);
|
||||||
Logger.w(TAG, "Ignored invalid event", e);
|
Store.instance.putSignedEvent(se);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Ignored invalid event", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
|
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to import profile", e);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_loaderOverlay.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
|
||||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
|
||||||
finish();
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to import profile", e);
|
|
||||||
UIDialogs.toast(this, getString(R.string.failed_to_import_profile) + " '${e.message}'");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@ -12,6 +14,7 @@ import android.webkit.MimeTypeMap
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
@ -21,14 +24,16 @@ import com.futo.platformplayer.dp
|
|||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
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.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.views.overlays.LoaderOverlay
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.Synchronization
|
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.toBase64Url
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -46,6 +51,8 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonDelete: BigButton;
|
private lateinit var _buttonDelete: BigButton;
|
||||||
private lateinit var _username: String;
|
private lateinit var _username: String;
|
||||||
private lateinit var _imagePolycentric: ImageView;
|
private lateinit var _imagePolycentric: ImageView;
|
||||||
|
private lateinit var _loaderOverlay: LoaderOverlay;
|
||||||
|
private lateinit var _textSystem: TextView;
|
||||||
private var _avatarUri: Uri? = null;
|
private var _avatarUri: Uri? = null;
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
@ -63,28 +70,13 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
_buttonExport = findViewById(R.id.button_export);
|
_buttonExport = findViewById(R.id.button_export);
|
||||||
_buttonLogout = findViewById(R.id.button_logout);
|
_buttonLogout = findViewById(R.id.button_logout);
|
||||||
_buttonDelete = findViewById(R.id.button_delete);
|
_buttonDelete = findViewById(R.id.button_delete);
|
||||||
|
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||||
|
_textSystem = findViewById(R.id.text_system)
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
saveIfRequired();
|
saveIfRequired();
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val processHandle = StatePolycentric.instance.processHandle!!;
|
|
||||||
Synchronization.fullyBackFillClient(processHandle, processHandle.system, "https://srv1-stg.polycentric.io");
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
|
|
||||||
_imagePolycentric.setOnClickListener {
|
_imagePolycentric.setOnClickListener {
|
||||||
ImagePicker.with(this)
|
ImagePicker.with(this)
|
||||||
.cropSquare()
|
.cropSquare()
|
||||||
@ -120,6 +112,37 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_textSystem.setOnLongClickListener {
|
||||||
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip: ClipData = ClipData.newPlainText("system", _textSystem.text)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
return@setOnLongClickListener true
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI()
|
||||||
|
|
||||||
|
StatePolycentric.instance.processHandle?.let { processHandle ->
|
||||||
|
_loaderOverlay.show()
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_loaderOverlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveIfRequired() {
|
private fun saveIfRequired() {
|
||||||
@ -128,13 +151,17 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
var hasChanges = false;
|
var hasChanges = false;
|
||||||
val username = _editName.text.toString();
|
val username = _editName.text.toString();
|
||||||
if (username.length < 3) {
|
if (username.length < 3) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
|
||||||
|
}
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
|
|
||||||
val processHandle = StatePolycentric.instance.processHandle;
|
val processHandle = StatePolycentric.instance.processHandle;
|
||||||
if (processHandle == null) {
|
if (processHandle == null) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
|
||||||
|
}
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,6 +246,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
private fun updateUI() {
|
private fun updateUI() {
|
||||||
val processHandle = StatePolycentric.instance.processHandle!!;
|
val processHandle = StatePolycentric.instance.processHandle!!;
|
||||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
|
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
|
||||||
|
_textSystem.text = processHandle.system.key.toBase64Url()
|
||||||
_username = systemState.username;
|
_username = systemState.username;
|
||||||
_editName.text.clear();
|
_editName.text.clear();
|
||||||
_editName.text.append(_username);
|
_editName.text.append(_username);
|
||||||
|
@ -6,11 +6,12 @@ import android.graphics.Color
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.*
|
import android.widget.EditText
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
@ -25,7 +26,11 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.polycentric.core.*
|
import com.futo.polycentric.core.ClaimType
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -93,7 +98,7 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
|
|
||||||
val comment = _editComment.text.toString();
|
val comment = _editComment.text.toString();
|
||||||
val processHandle = StatePolycentric.instance.processHandle!!
|
val processHandle = StatePolycentric.instance.processHandle!!
|
||||||
val eventPointer = processHandle.post(comment, null, ref)
|
val eventPointer = processHandle.post(comment, ref)
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
@ -465,7 +465,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
} else {
|
} else {
|
||||||
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
|
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
|
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
|
||||||
|
@ -314,8 +314,8 @@ class PostDetailFragment : MainFragment {
|
|||||||
private fun updatePolycentricRating() {
|
private fun updatePolycentricRating() {
|
||||||
_rating.visibility = View.GONE;
|
_rating.visibility = View.GONE;
|
||||||
|
|
||||||
val value = _post?.id?.value ?: _postOverview?.id?.value ?: return;
|
val ref = Models.referenceFromBuffer((_post?.url ?: _postOverview?.url)?.toByteArray() ?: return)
|
||||||
val ref = Models.referenceFromBuffer(value.toByteArray());
|
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.toByteArray()
|
||||||
val version = _version;
|
val version = _version;
|
||||||
|
|
||||||
_rating.onLikeDislikeUpdated.remove(this);
|
_rating.onLikeDislikeUpdated.remove(this);
|
||||||
@ -333,7 +333,8 @@ class PostDetailFragment : MainFragment {
|
|||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
||||||
ContentType.OPINION.value).setValue(
|
ContentType.OPINION.value).setValue(
|
||||||
ByteString.copyFrom(Opinion.dislike.data)).build()
|
ByteString.copyFrom(Opinion.dislike.data)).build()
|
||||||
)
|
),
|
||||||
|
extraByteReferences = listOfNotNull(extraBytesRef)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (version != _version) {
|
if (version != _version) {
|
||||||
@ -342,8 +343,8 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
val likes = queryReferencesResponse.countsList[0];
|
val likes = queryReferencesResponse.countsList[0];
|
||||||
val dislikes = queryReferencesResponse.countsList[1];
|
val dislikes = queryReferencesResponse.countsList[1];
|
||||||
val hasLiked = StatePolycentric.instance.hasLiked(ref);
|
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref);
|
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (version != _version) {
|
if (version != _version) {
|
||||||
@ -468,9 +469,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
if (_postOverview == null) {
|
if (_postOverview == null) {
|
||||||
fetchPolycentricProfile();
|
fetchPolycentricProfile();
|
||||||
updatePolycentricRating();
|
updatePolycentricRating();
|
||||||
|
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||||
val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); };
|
|
||||||
_addCommentView.setContext(value.url, ref);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCommentType(true);
|
updateCommentType(true);
|
||||||
@ -489,9 +488,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
_textMeta.text = value.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "" //TODO: Include view count?
|
_textMeta.text = value.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "" //TODO: Include view count?
|
||||||
_textContent.text = value.description.fixHtmlWhitespace();
|
_textContent.text = value.description.fixHtmlWhitespace();
|
||||||
_platformIndicator.setPlatformFromClientID(value.id.pluginId);
|
_platformIndicator.setPlatformFromClientID(value.id.pluginId);
|
||||||
|
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||||
val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); };
|
|
||||||
_addCommentView.setContext(value.url, ref);
|
|
||||||
|
|
||||||
updatePolycentricRating();
|
updatePolycentricRating();
|
||||||
fetchPolycentricProfile();
|
fetchPolycentricProfile();
|
||||||
@ -636,12 +633,12 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
if (cachedPolycentricProfile?.profile == null) {
|
if (cachedPolycentricProfile?.profile == null) {
|
||||||
_layoutMonetization.visibility = View.GONE;
|
_layoutMonetization.visibility = View.GONE;
|
||||||
_creatorThumbnail.setHarborAvailable(false, animate);
|
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_layoutMonetization.visibility = View.VISIBLE;
|
_layoutMonetization.visibility = View.VISIBLE;
|
||||||
_creatorThumbnail.setHarborAvailable(true, animate);
|
_creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchPost() {
|
private fun fetchPost() {
|
||||||
@ -665,14 +662,16 @@ class PostDetailFragment : MainFragment {
|
|||||||
private fun fetchPolycentricComments() {
|
private fun fetchPolycentricComments() {
|
||||||
Logger.i(TAG, "fetchPolycentricComments")
|
Logger.i(TAG, "fetchPolycentricComments")
|
||||||
val post = _post;
|
val post = _post;
|
||||||
val idValue = post?.id?.value
|
val ref = (_post?.url ?: _postOverview?.url)?.toByteArray()?.let { Models.referenceFromBuffer(it) }
|
||||||
if (idValue == null) {
|
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.toByteArray()
|
||||||
Logger.w(TAG, "Failed to fetch polycentric comments because id was null")
|
|
||||||
|
if (ref == null) {
|
||||||
|
Logger.w(TAG, "Failed to fetch polycentric comments because url was not set null")
|
||||||
_commentsList.clear();
|
_commentsList.clear();
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post.url, Models.referenceFromBuffer(idValue.toByteArray())); };
|
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); };
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCommentType(reloadComments: Boolean) {
|
private fun updateCommentType(reloadComments: Boolean) {
|
||||||
|
@ -129,8 +129,8 @@ class TutorialFragment : MainFragment() {
|
|||||||
override val dash: IDashManifestSource? = null
|
override val dash: IDashManifestSource? = null
|
||||||
override val hls: IHLSManifestSource? = null
|
override val hls: IHLSManifestSource? = null
|
||||||
override val subtitles: List<ISubtitleSource> = emptyList()
|
override val subtitles: List<ISubtitleSource> = emptyList()
|
||||||
override val shareUrl: String = ""
|
override val shareUrl: String = videoUrl
|
||||||
override val url: String = ""
|
override val url: String = videoUrl
|
||||||
override val datetime: OffsetDateTime? = OffsetDateTime.parse("2023-12-18T00:00:00Z")
|
override val datetime: OffsetDateTime? = OffsetDateTime.parse("2023-12-18T00:00:00Z")
|
||||||
override val thumbnails: Thumbnails = Thumbnails(arrayOf(Thumbnail(thumbnailUrl)))
|
override val thumbnails: Thumbnails = Thumbnails(arrayOf(Thumbnail(thumbnailUrl)))
|
||||||
override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("tutorial", "f422ced6-b551-4b62-818e-27a4f5f4918a"), "Grayjay", "", "https://releases.grayjay.app/tutorials/author.jpeg")
|
override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("tutorial", "f422ced6-b551-4b62-818e-27a4f5f4918a"), "Grayjay", "", "https://releases.grayjay.app/tutorials/author.jpeg")
|
||||||
|
@ -1201,9 +1201,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
val ref = video.id.value?.let { Models.referenceFromBuffer(it.toByteArray()) };
|
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||||
_addCommentView.setContext(video.url, ref);
|
val extraBytesRef = video.id.value?.toByteArray()
|
||||||
|
_addCommentView.setContext(video.url, ref)
|
||||||
_player.setMetadata(video.name, video.author.name);
|
_player.setMetadata(video.name, video.author.name);
|
||||||
|
|
||||||
if (video !is TutorialFragment.TutorialVideo) {
|
if (video !is TutorialFragment.TutorialVideo) {
|
||||||
@ -1264,57 +1264,54 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_rating.onLikeDislikeUpdated.remove(this);
|
_rating.onLikeDislikeUpdated.remove(this);
|
||||||
|
|
||||||
if (ref != null) {
|
_rating.visibility = View.GONE;
|
||||||
_rating.visibility = View.GONE;
|
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
||||||
arrayListOf(
|
arrayListOf(
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
||||||
ByteString.copyFrom(Opinion.like.data)).build(),
|
ByteString.copyFrom(Opinion.like.data)).build(),
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
||||||
ByteString.copyFrom(Opinion.dislike.data)).build()
|
ByteString.copyFrom(Opinion.dislike.data)).build()
|
||||||
)
|
),
|
||||||
);
|
extraByteReferences = listOfNotNull(extraBytesRef)
|
||||||
|
);
|
||||||
|
|
||||||
val likes = queryReferencesResponse.countsList[0];
|
val likes = queryReferencesResponse.countsList[0];
|
||||||
val dislikes = queryReferencesResponse.countsList[1];
|
val dislikes = queryReferencesResponse.countsList[1];
|
||||||
val hasLiked = StatePolycentric.instance.hasLiked(ref);
|
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref);
|
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_rating.visibility = View.VISIBLE;
|
_rating.visibility = View.VISIBLE;
|
||||||
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
||||||
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
||||||
if (args.hasLiked) {
|
if (args.hasLiked) {
|
||||||
args.processHandle.opinion(ref, Opinion.like);
|
args.processHandle.opinion(ref, Opinion.like);
|
||||||
} else if (args.hasDisliked) {
|
} else if (args.hasDisliked) {
|
||||||
args.processHandle.opinion(ref, Opinion.dislike);
|
args.processHandle.opinion(ref, Opinion.dislike);
|
||||||
} else {
|
} else {
|
||||||
args.processHandle.opinion(ref, Opinion.neutral);
|
args.processHandle.opinion(ref, Opinion.neutral);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
Logger.i(TAG, "Started backfill");
|
||||||
|
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
|
Logger.i(TAG, "Finished backfill");
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to backfill servers", e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
|
||||||
try {
|
};
|
||||||
Logger.i(TAG, "Started backfill");
|
|
||||||
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
|
||||||
Logger.i(TAG, "Finished backfill");
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to backfill servers", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e);
|
|
||||||
_rating.visibility = View.GONE;
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e);
|
||||||
|
_rating.visibility = View.GONE;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
_rating.visibility = View.GONE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
when (video.rating) {
|
when (video.rating) {
|
||||||
@ -1361,28 +1358,30 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
updateQueueState();
|
updateQueueState();
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
if (video !is TutorialFragment.TutorialVideo) {
|
||||||
val historyItem = getHistoryIndex(videoDetail);
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val historyItem = getHistoryIndex(videoDetail);
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
||||||
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
|
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
|
||||||
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
|
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
|
||||||
_layoutResume.visibility = View.VISIBLE;
|
_layoutResume.visibility = View.VISIBLE;
|
||||||
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
|
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
|
||||||
|
|
||||||
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
delay(8000);
|
delay(8000);
|
||||||
_layoutResume.visibility = View.GONE;
|
_layoutResume.visibility = View.GONE;
|
||||||
_textResume.text = "";
|
_textResume.text = "";
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to set resume changes.", e);
|
Logger.e(TAG, "Failed to set resume changes.", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
_layoutResume.visibility = View.GONE;
|
||||||
|
_textResume.text = "";
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
_layoutResume.visibility = View.GONE;
|
|
||||||
_textResume.text = "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1954,7 +1953,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, Models.referenceFromBuffer(idValue.toByteArray())); };
|
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||||
|
val extraBytesRef = video.id.value?.toByteArray()
|
||||||
|
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); };
|
||||||
}
|
}
|
||||||
private fun fetchVideo() {
|
private fun fetchVideo() {
|
||||||
Logger.i(TAG, "fetchVideo")
|
Logger.i(TAG, "fetchVideo")
|
||||||
@ -2216,9 +2217,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val v = video ?: return;
|
val v = video ?: return;
|
||||||
val currentTime = System.currentTimeMillis();
|
val currentTime = System.currentTimeMillis();
|
||||||
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
if (v !is TutorialFragment.TutorialVideo) {
|
||||||
val history = getHistoryIndex(v);
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
val history = getHistoryIndex(v);
|
||||||
|
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_lastPositionSaveTime = currentTime;
|
_lastPositionSaveTime = currentTime;
|
||||||
}
|
}
|
||||||
@ -2301,7 +2304,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
} else {
|
} else {
|
||||||
_creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate);
|
_creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate);
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
val username = cachedPolycentricProfile?.profile?.systemState?.username
|
val username = cachedPolycentricProfile?.profile?.systemState?.username
|
||||||
|
@ -56,7 +56,7 @@ class PolycentricCache {
|
|||||||
|
|
||||||
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
|
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
|
||||||
{ system ->
|
{ system ->
|
||||||
val signedProfileEvents = ApiMethods.getQueryLatest(
|
val signedEventsList = ApiMethods.getQueryLatest(
|
||||||
SERVER,
|
SERVER,
|
||||||
system.toProto(),
|
system.toProto(),
|
||||||
listOf(
|
listOf(
|
||||||
@ -72,8 +72,9 @@ class PolycentricCache {
|
|||||||
ContentType.MEMBERSHIP_URLS.value,
|
ContentType.MEMBERSHIP_URLS.value,
|
||||||
ContentType.DONATION_DESTINATIONS.value
|
ContentType.DONATION_DESTINATIONS.value
|
||||||
)
|
)
|
||||||
).eventsList.map { e -> SignedEvent.fromProto(e) }
|
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||||
.groupBy { e -> e.event.contentType }
|
|
||||||
|
val signedProfileEvents = signedEventsList.groupBy { e -> e.event.contentType }
|
||||||
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
|
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
|
||||||
|
|
||||||
val storageSystemState = StorageTypeSystemState.create()
|
val storageSystemState = StorageTypeSystemState.create()
|
||||||
@ -151,17 +152,7 @@ class PolycentricCache {
|
|||||||
|
|
||||||
private val _batchTaskGetData = BatchedTaskHandler<String, ByteBuffer>(_scope,
|
private val _batchTaskGetData = BatchedTaskHandler<String, ByteBuffer>(_scope,
|
||||||
{
|
{
|
||||||
val urlData = if (it.startsWith("polycentric://")) {
|
val dataLink = getDataLinkFromUrl(it) ?: throw Exception("Only URLInfoDataLink is supported");
|
||||||
it.substring("polycentric://".length)
|
|
||||||
} else it;
|
|
||||||
|
|
||||||
val urlBytes = urlData.base64UrlToByteArray();
|
|
||||||
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
|
||||||
if (urlInfo.urlType != 4L) {
|
|
||||||
throw Exception("Only URLInfoDataLink is supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
|
||||||
return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink);
|
return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink);
|
||||||
},
|
},
|
||||||
{ return@BatchedTaskHandler null },
|
{ return@BatchedTaskHandler null },
|
||||||
@ -325,9 +316,10 @@ class PolycentricCache {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
private const val TAG = "PolycentricCache"
|
private const val TAG = "PolycentricCache"
|
||||||
const val SERVER = "https://srv1-stg.polycentric.io"
|
const val STAGING_SERVER = "https://srv1-stg.polycentric.io"
|
||||||
|
const val SERVER = "https://srv1-prod.polycentric.io"
|
||||||
private var _instance: PolycentricCache? = null;
|
private var _instance: PolycentricCache? = null;
|
||||||
private val CACHE_EXPIRATION_SECONDS = 60 * 60 * 3;
|
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val instance: PolycentricCache
|
val instance: PolycentricCache
|
||||||
@ -343,5 +335,20 @@ class PolycentricCache {
|
|||||||
it._scope.cancel("PolycentricCache finished");
|
it._scope.cancel("PolycentricCache finished");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDataLinkFromUrl(it: String): Protocol.URLInfoDataLink? {
|
||||||
|
val urlData = if (it.startsWith("polycentric://")) {
|
||||||
|
it.substring("polycentric://".length)
|
||||||
|
} else it;
|
||||||
|
|
||||||
|
val urlBytes = urlData.base64UrlToByteArray();
|
||||||
|
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
||||||
|
if (urlInfo.urlType != 4L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
||||||
|
return dataLink
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -27,7 +27,20 @@ import com.futo.platformplayer.resolveChannelUrl
|
|||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringStorage
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
import com.futo.polycentric.core.*
|
import com.futo.polycentric.core.ApiMethods
|
||||||
|
import com.futo.polycentric.core.ClaimType
|
||||||
|
import com.futo.polycentric.core.ContentType
|
||||||
|
import com.futo.polycentric.core.Opinion
|
||||||
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
|
import com.futo.polycentric.core.PublicKey
|
||||||
|
import com.futo.polycentric.core.SignedEvent
|
||||||
|
import com.futo.polycentric.core.SqlLiteDbHelper
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.base64ToByteArray
|
||||||
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
|
import com.futo.polycentric.core.toBase64
|
||||||
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
@ -38,7 +51,6 @@ import userpackage.Protocol
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import kotlin.Exception
|
|
||||||
|
|
||||||
class StatePolycentric {
|
class StatePolycentric {
|
||||||
private data class LikeDislikeEntry(val unixMilliseconds: Long, val hasLiked: Boolean, val hasDisliked: Boolean);
|
private data class LikeDislikeEntry(val unixMilliseconds: Long, val hasLiked: Boolean, val hasDisliked: Boolean);
|
||||||
@ -128,21 +140,21 @@ class StatePolycentric {
|
|||||||
_likeDislikeMap[ref.toByteArray().toBase64()] = LikeDislikeEntry(System.currentTimeMillis(), hasLiked, hasDisliked);
|
_likeDislikeMap[ref.toByteArray().toBase64()] = LikeDislikeEntry(System.currentTimeMillis(), hasLiked, hasDisliked);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasDisliked(ref: Protocol.Reference): Boolean {
|
fun hasDisliked(data: ByteArray): Boolean {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
|
val entry = _likeDislikeMap[data.toBase64()] ?: return false;
|
||||||
return entry.hasDisliked;
|
return entry.hasDisliked;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasLiked(ref: Protocol.Reference): Boolean {
|
fun hasLiked(data: ByteArray): Boolean {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
|
val entry = _likeDislikeMap[data.toBase64()] ?: return false;
|
||||||
return entry.hasLiked;
|
return entry.hasLiked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,7 +328,7 @@ class StatePolycentric {
|
|||||||
return LikesDislikesReplies(likes, dislikes, replyCount)
|
return LikesDislikesReplies(likes, dislikes, replyCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference): IPager<IPlatformComment> {
|
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return EmptyPager()
|
return EmptyPager()
|
||||||
}
|
}
|
||||||
@ -338,7 +350,8 @@ class StatePolycentric {
|
|||||||
Protocol.QueryReferencesRequestCountReferences.newBuilder()
|
Protocol.QueryReferencesRequestCountReferences.newBuilder()
|
||||||
.setFromType(ContentType.POST.value)
|
.setFromType(ContentType.POST.value)
|
||||||
.build())
|
.build())
|
||||||
.build()
|
.build(),
|
||||||
|
extraByteReferences = extraByteReferences
|
||||||
);
|
);
|
||||||
|
|
||||||
val results = mapQueryReferences(contextUrl, response);
|
val results = mapQueryReferences(contextUrl, response);
|
||||||
@ -407,7 +420,8 @@ class StatePolycentric {
|
|||||||
ContentType.AVATAR.value,
|
ContentType.AVATAR.value,
|
||||||
ContentType.USERNAME.value
|
ContentType.USERNAME.value
|
||||||
)
|
)
|
||||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType }
|
||||||
|
.map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } };
|
||||||
|
|
||||||
val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value };
|
val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value };
|
||||||
val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value };
|
val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value };
|
||||||
|
117
app/src/main/java/com/futo/platformplayer/views/IdenticonView.kt
Normal file
117
app/src/main/java/com/futo/platformplayer/views/IdenticonView.kt
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package com.futo.platformplayer.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
class IdenticonView(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||||
|
var hashString: String = "default"
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
hash = md5(value)
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hash = ByteArray(16)
|
||||||
|
|
||||||
|
private val path = Path()
|
||||||
|
private val paint = Paint().apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
hashString = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
|
||||||
|
val radius = (width.coerceAtMost(height) / 2).toFloat()
|
||||||
|
val clipPath = path.apply {
|
||||||
|
reset()
|
||||||
|
addCircle(width / 2f, height / 2f, radius, Path.Direction.CW)
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.clipPath(clipPath)
|
||||||
|
|
||||||
|
val size = width.coerceAtMost(height) / 5
|
||||||
|
val colors = generateColorsFromHash(hash)
|
||||||
|
|
||||||
|
for (x in 0 until 5) {
|
||||||
|
for (y in 0 until 5) {
|
||||||
|
val shapeIndex = getShapeIndex(x, y, hash)
|
||||||
|
paint.color = colors[shapeIndex % colors.size]
|
||||||
|
drawShape(canvas, x, y, size, shapeIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun md5(input: String): ByteArray {
|
||||||
|
val md = MessageDigest.getInstance("MD5")
|
||||||
|
return md.digest(input.toByteArray(Charsets.UTF_8))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateColorsFromHash(hash: ByteArray): List<Int> {
|
||||||
|
val hue = hash[0].toFloat() / 255f
|
||||||
|
return listOf(
|
||||||
|
adjustColor(hue, 0.5f, 0.4f),
|
||||||
|
adjustColor(hue, 0.5f, 0.8f),
|
||||||
|
adjustColor(hue, 0.5f, 0.3f, 0.9f),
|
||||||
|
adjustColor(hue, 0.5f, 0.4f, 0.7f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getShapeIndex(x: Int, y: Int, hash: ByteArray): Int {
|
||||||
|
val index = if (x < 3) y else 4 - y
|
||||||
|
return hash[index].toInt() shr x * 2 and 0x03
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawShape(canvas: Canvas, x: Int, y: Int, size: Int, shapeIndex: Int) {
|
||||||
|
val left = x * size.toFloat()
|
||||||
|
val top = y * size.toFloat()
|
||||||
|
val path = Path()
|
||||||
|
|
||||||
|
when (shapeIndex) {
|
||||||
|
0 -> {
|
||||||
|
// Square
|
||||||
|
path.addRect(left, top, left + size, top + size, Path.Direction.CW)
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
// Circle
|
||||||
|
val radius = size / 2f
|
||||||
|
path.addCircle(left + radius, top + radius, radius, Path.Direction.CW)
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
// Diamond
|
||||||
|
val halfSize = size / 2f
|
||||||
|
path.moveTo(left + halfSize, top)
|
||||||
|
path.lineTo(left + size, top + halfSize)
|
||||||
|
path.lineTo(left + halfSize, top + size)
|
||||||
|
path.lineTo(left, top + halfSize)
|
||||||
|
path.close()
|
||||||
|
}
|
||||||
|
3 -> {
|
||||||
|
// Triangle
|
||||||
|
path.moveTo(left + size / 2f, top)
|
||||||
|
path.lineTo(left + size, top + size)
|
||||||
|
path.lineTo(left, top + size)
|
||||||
|
path.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjustColor(hue: Float, saturation: Float, lightness: Float, alpha: Float = 1.0f): Int {
|
||||||
|
val color = Color.HSVToColor(floatArrayOf(hue * 360, saturation, lightness))
|
||||||
|
val red = Color.red(color)
|
||||||
|
val green = Color.green(color)
|
||||||
|
val blue = Color.blue(color)
|
||||||
|
return Color.argb((alpha * 255).toInt(), red, green, blue)
|
||||||
|
}
|
||||||
|
}
|
@ -9,15 +9,21 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
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.ratings.RatingLikeDislikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.toHumanNowDiffString
|
||||||
|
import com.futo.platformplayer.toHumanNumber
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.pills.PillButton
|
import com.futo.platformplayer.views.pills.PillButton
|
||||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||||
@ -104,7 +110,8 @@ class CommentViewHolder : ViewHolder {
|
|||||||
|
|
||||||
fun bind(comment: IPlatformComment, readonly: Boolean) {
|
fun bind(comment: IPlatformComment, readonly: Boolean) {
|
||||||
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
||||||
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
|
val polycentricComment = if (comment is PolycentricPlatformComment) comment else null
|
||||||
|
_creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto());
|
||||||
_textAuthor.text = comment.author.name;
|
_textAuthor.text = comment.author.name;
|
||||||
|
|
||||||
val date = comment.date;
|
val date = comment.date;
|
||||||
@ -161,8 +168,8 @@ class CommentViewHolder : ViewHolder {
|
|||||||
_pillRatingLikesDislikes.visibility = View.VISIBLE;
|
_pillRatingLikesDislikes.visibility = View.VISIBLE;
|
||||||
|
|
||||||
if (comment is PolycentricPlatformComment) {
|
if (comment is PolycentricPlatformComment) {
|
||||||
val hasLiked = StatePolycentric.instance.hasLiked(comment.reference);
|
val hasLiked = StatePolycentric.instance.hasLiked(comment.reference.toByteArray());
|
||||||
val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference);
|
val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference.toByteArray());
|
||||||
_pillRatingLikesDislikes.setRating(comment.rating, hasLiked, hasDisliked);
|
_pillRatingLikesDislikes.setRating(comment.rating, hasLiked, hasDisliked);
|
||||||
} else {
|
} else {
|
||||||
_pillRatingLikesDislikes.setRating(comment.rating);
|
_pillRatingLikesDislikes.setRating(comment.rating);
|
||||||
|
@ -126,7 +126,8 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
|||||||
_taskGetLiveComment.cancel()
|
_taskGetLiveComment.cancel()
|
||||||
|
|
||||||
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
||||||
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
|
val polycentricComment = if (comment is PolycentricPlatformComment) comment else null
|
||||||
|
_creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto());
|
||||||
_textAuthor.text = comment.author.name;
|
_textAuthor.text = comment.author.name;
|
||||||
|
|
||||||
val date = comment.date;
|
val date = comment.date;
|
||||||
@ -168,8 +169,8 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
|||||||
if (likesDislikesReplies != null) {
|
if (likesDislikesReplies != null) {
|
||||||
Log.i(TAG, "updateLikesDislikesReplies set")
|
Log.i(TAG, "updateLikesDislikesReplies set")
|
||||||
|
|
||||||
val hasLiked = StatePolycentric.instance.hasLiked(c.reference);
|
val hasLiked = StatePolycentric.instance.hasLiked(c.reference.toByteArray());
|
||||||
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference);
|
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference.toByteArray());
|
||||||
_pillRatingLikesDislikes.setRating(RatingLikeDislikes(likesDislikesReplies.likes, likesDislikesReplies.dislikes), hasLiked, hasDisliked);
|
_pillRatingLikesDislikes.setRating(RatingLikeDislikes(likesDislikesReplies.likes, likesDislikesReplies.dislikes), hasLiked, hasDisliked);
|
||||||
|
|
||||||
_buttonReplies.setLoading(false)
|
_buttonReplies.setLoading(false)
|
||||||
|
@ -7,7 +7,7 @@ import android.widget.ImageView
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
@ -18,8 +18,8 @@ import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
|
||||||
|
|
||||||
@ -149,7 +149,8 @@ open class PlaylistView : LinearLayout {
|
|||||||
_neopassAnimator?.cancel();
|
_neopassAnimator?.cancel();
|
||||||
_neopassAnimator = null;
|
_neopassAnimator = null;
|
||||||
|
|
||||||
val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty();
|
val firstClaim = claims?.ownedClaims?.firstOrNull();
|
||||||
|
val harborAvailable = firstClaim != null
|
||||||
if (harborAvailable) {
|
if (harborAvailable) {
|
||||||
_imageNeopassChannel?.visibility = View.VISIBLE
|
_imageNeopassChannel?.visibility = View.VISIBLE
|
||||||
if (animate) {
|
if (animate) {
|
||||||
@ -160,7 +161,7 @@ open class PlaylistView : LinearLayout {
|
|||||||
_imageNeopassChannel?.visibility = View.GONE
|
_imageNeopassChannel?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate)
|
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate, firstClaim?.system?.toProto())
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -6,21 +6,18 @@ import android.widget.ImageButton
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.models.Subscription
|
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.Subscription
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.toHumanBytesSpeed
|
|
||||||
import com.futo.platformplayer.toHumanTimeIndicator
|
import com.futo.platformplayer.toHumanTimeIndicator
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
@ -107,7 +104,7 @@ class SubscriptionViewHolder : ViewHolder {
|
|||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
} else {
|
} else {
|
||||||
_creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate);
|
_creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate);
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
|
@ -334,7 +334,7 @@ open class PreviewVideoView : LinearLayout {
|
|||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
} else {
|
} else {
|
||||||
_creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate);
|
_creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate);
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
} else if (_imageChannel != null) {
|
} else if (_imageChannel != null) {
|
||||||
val dp_28 = 28.dp(context.resources);
|
val dp_28 = 28.dp(context.resources);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package com.futo.platformplayer.views.adapters.viewholders
|
package com.futo.platformplayer.views.adapters.viewholders
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
@ -8,12 +7,10 @@ import android.widget.TextView
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
@ -76,7 +73,7 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
|
|||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
} else {
|
} else {
|
||||||
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
@ -148,7 +145,7 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
|||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
} else {
|
} else {
|
||||||
_creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate);
|
_creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate);
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
|
@ -98,7 +98,7 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
|||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
} else {
|
} else {
|
||||||
_creatorThumbnail.setThumbnail(_authorLink?.thumbnail, animate);
|
_creatorThumbnail.setThumbnail(_authorLink?.thumbnail, animate);
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
|
@ -77,7 +77,7 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
|
|||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
} else {
|
} else {
|
||||||
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
|
@ -12,12 +12,16 @@ import com.bumptech.glide.Glide
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.views.IdenticonView
|
||||||
|
import userpackage.Protocol
|
||||||
|
|
||||||
class CreatorThumbnail : ConstraintLayout {
|
class CreatorThumbnail : ConstraintLayout {
|
||||||
private val _root: ConstraintLayout;
|
private val _root: ConstraintLayout;
|
||||||
private val _imageChannelThumbnail: ImageView;
|
private val _imageChannelThumbnail: ImageView;
|
||||||
private val _imageNewActivity: ImageView;
|
private val _imageNewActivity: ImageView;
|
||||||
private val _imageNeoPass: ImageView;
|
private val _imageNeoPass: ImageView;
|
||||||
|
private val _identicon: IdenticonView;
|
||||||
private var _harborAnimator: ObjectAnimator? = null;
|
private var _harborAnimator: ObjectAnimator? = null;
|
||||||
private var _imageAnimator: ObjectAnimator? = null;
|
private var _imageAnimator: ObjectAnimator? = null;
|
||||||
|
|
||||||
@ -28,19 +32,22 @@ class CreatorThumbnail : ConstraintLayout {
|
|||||||
|
|
||||||
_root = findViewById(R.id.root);
|
_root = findViewById(R.id.root);
|
||||||
_imageChannelThumbnail = findViewById(R.id.image_channel_thumbnail);
|
_imageChannelThumbnail = findViewById(R.id.image_channel_thumbnail);
|
||||||
|
_identicon = findViewById(R.id.identicon);
|
||||||
_imageChannelThumbnail.clipToOutline = true;
|
_imageChannelThumbnail.clipToOutline = true;
|
||||||
|
_imageChannelThumbnail.visibility = View.GONE
|
||||||
_imageNewActivity = findViewById(R.id.image_new_activity);
|
_imageNewActivity = findViewById(R.id.image_new_activity);
|
||||||
_imageNeoPass = findViewById(R.id.image_neopass);
|
_imageNeoPass = findViewById(R.id.image_neopass);
|
||||||
|
|
||||||
if (!isInEditMode) {
|
if (!isInEditMode) {
|
||||||
setHarborAvailable(false, animate = false);
|
setHarborAvailable(false, animate = false, system = null);
|
||||||
setNewActivity(false);
|
setNewActivity(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
_imageChannelThumbnail.visibility = View.GONE;
|
||||||
_imageChannelThumbnail.setImageResource(R.drawable.placeholder_channel_thumbnail);
|
_imageChannelThumbnail.setImageResource(R.drawable.placeholder_channel_thumbnail);
|
||||||
setHarborAvailable(false, animate = false);
|
setHarborAvailable(false, animate = false, system = null);
|
||||||
setNewActivity(false);
|
setNewActivity(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,13 +57,24 @@ class CreatorThumbnail : ConstraintLayout {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_imageChannelThumbnail.visibility = View.VISIBLE;
|
||||||
|
|
||||||
_harborAnimator?.cancel();
|
_harborAnimator?.cancel();
|
||||||
_harborAnimator = null;
|
_harborAnimator = null;
|
||||||
|
|
||||||
_imageAnimator?.cancel();
|
_imageAnimator?.cancel();
|
||||||
_imageAnimator = null;
|
_imageAnimator = null;
|
||||||
|
|
||||||
setHarborAvailable(url.startsWith("polycentric://"), animate);
|
if (url.startsWith("polycentric://")) {
|
||||||
|
try {
|
||||||
|
val dataLink = PolycentricCache.getDataLinkFromUrl(url)
|
||||||
|
setHarborAvailable(true, animate, dataLink?.system);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
setHarborAvailable(false, animate, null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHarborAvailable(false, animate, null);
|
||||||
|
}
|
||||||
|
|
||||||
if (animate) {
|
if (animate) {
|
||||||
Glide.with(_imageChannelThumbnail)
|
Glide.with(_imageChannelThumbnail)
|
||||||
@ -72,7 +90,7 @@ class CreatorThumbnail : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setHarborAvailable(available: Boolean, animate: Boolean) {
|
fun setHarborAvailable(available: Boolean, animate: Boolean, system: Protocol.PublicKey?) {
|
||||||
_harborAnimator?.cancel();
|
_harborAnimator?.cancel();
|
||||||
_harborAnimator = null;
|
_harborAnimator = null;
|
||||||
|
|
||||||
@ -85,6 +103,13 @@ class CreatorThumbnail : ConstraintLayout {
|
|||||||
} else {
|
} else {
|
||||||
_imageNeoPass.visibility = View.GONE;
|
_imageNeoPass.visibility = View.GONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (system != null) {
|
||||||
|
_identicon.hashString = system.toString()
|
||||||
|
_identicon.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
_identicon.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setChannelImageResource(resource: Int?, animate: Boolean) {
|
fun setChannelImageResource(resource: Int?, animate: Boolean) {
|
||||||
|
@ -3,6 +3,7 @@ package com.futo.platformplayer.views.overlays
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@ -16,6 +17,21 @@ class LoaderOverlay(context: Context, attrs: AttributeSet?) : FrameLayout(contex
|
|||||||
inflate(context, R.layout.overlay_loader, this);
|
inflate(context, R.layout.overlay_loader, this);
|
||||||
_container = findViewById(R.id.container);
|
_container = findViewById(R.id.container);
|
||||||
_loader = findViewById(R.id.loader);
|
_loader = findViewById(R.id.loader);
|
||||||
|
|
||||||
|
val centerLoader: Boolean;
|
||||||
|
if (attrs != null) {
|
||||||
|
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.LoaderOverlay, 0, 0);
|
||||||
|
centerLoader = attrArr.getBoolean(R.styleable.LoaderOverlay_centerLoader, false);
|
||||||
|
attrArr.recycle();
|
||||||
|
} else {
|
||||||
|
centerLoader = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (centerLoader) {
|
||||||
|
(_loader.layoutParams as LayoutParams).apply {
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show() {
|
fun show() {
|
||||||
|
@ -6,8 +6,8 @@ import android.view.View
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import com.futo.platformplayer.UIDialogs
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@ -102,7 +102,8 @@ class RepliesOverlay : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_creatorThumbnail.setThumbnail(parentComment.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(parentComment.author.thumbnail, false);
|
||||||
_creatorThumbnail.setHarborAvailable(parentComment is PolycentricPlatformComment,false);
|
val polycentricPlatformComment = if (parentComment is PolycentricPlatformComment) parentComment else null
|
||||||
|
_creatorThumbnail.setHarborAvailable(polycentricPlatformComment != null,false, polycentricPlatformComment?.eventPointer?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
_topbar.setInfo(context.getString(R.string.Replies), metadata);
|
_topbar.setInfo(context.getString(R.string.Replies), metadata);
|
||||||
|
@ -94,4 +94,11 @@
|
|||||||
android:text="@string/import_profile" />
|
android:text="@string/import_profile" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
|
android:id="@+id/loader_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:centerLoader="true"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -51,6 +51,21 @@
|
|||||||
app:layout_constraintLeft_toLeftOf="@id/image_polycentric"
|
app:layout_constraintLeft_toLeftOf="@id/image_polycentric"
|
||||||
app:layout_constraintRight_toRightOf="@id/image_polycentric" />
|
app:layout_constraintRight_toRightOf="@id/image_polycentric" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_system"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="gX0eCWctTm6WHVGot4sMAh7NDAIwWsIM5tRsOz9dX04="
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textSize="10dp"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="middle"
|
||||||
|
android:textColor="@color/gray_67"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/edit_profile_name"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/layout_buttons"
|
android:id="@+id/layout_buttons"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -91,4 +106,11 @@
|
|||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
app:buttonBackground="@drawable/background_big_button_red"/>
|
app:buttonBackground="@drawable/background_big_button_red"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
|
android:id="@+id/loader_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:centerLoader="true"
|
||||||
|
android:visibility="gone" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -17,5 +17,6 @@
|
|||||||
android:layout_gravity="top|center_horizontal"
|
android:layout_gravity="top|center_horizontal"
|
||||||
android:alpha="0.7"
|
android:alpha="0.7"
|
||||||
android:layout_marginTop="80dp"
|
android:layout_marginTop="80dp"
|
||||||
|
android:layout_marginBottom="80dp"
|
||||||
android:contentDescription="@string/loading" />
|
android:contentDescription="@string/loading" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
@ -7,6 +7,18 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:id="@+id/root">
|
android:id="@+id/root">
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.IdenticonView
|
||||||
|
android:id="@+id/identicon"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintDimensionRatio="1:1"
|
||||||
|
app:srcCompat="@drawable/ic_futo_logo"
|
||||||
|
android:background="@drawable/rounded_outline"
|
||||||
|
android:contentDescription="@string/channel_image"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_channel_thumbnail"
|
android:id="@+id/image_channel_thumbnail"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
6
app/src/main/res/values/loader_overlay_attrs.xml
Normal file
6
app/src/main/res/values/loader_overlay_attrs.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<declare-styleable name="LoaderOverlay">
|
||||||
|
<attr name="centerLoader" format="boolean" />
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
@ -1 +1 @@
|
|||||||
Subproject commit 86cd96c41f15f7f73c091f61800a49f376a38150
|
Subproject commit 7695198eeaeaaea4726712c460081c411ef67866
|
Loading…
x
Reference in New Issue
Block a user