mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-09-07 18:19:03 +00:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
commit
56248bf4b0
33 changed files with 522 additions and 213 deletions
|
@ -1,11 +1,13 @@
|
|||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.views.adapters.CommentViewHolder
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import userpackage.Protocol
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
@ -47,6 +49,15 @@ fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
|||
}
|
||||
|
||||
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()
|
||||
for (pair in exceptions) {
|
||||
val server = pair.key
|
||||
|
|
|
@ -8,12 +8,15 @@ import android.widget.ImageButton
|
|||
import android.widget.LinearLayout
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||
import com.futo.polycentric.core.KeyPair
|
||||
import com.futo.polycentric.core.Process
|
||||
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.base64UrlToByteArray
|
||||
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.ExportBundle
|
||||
|
||||
|
@ -29,6 +35,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||
private lateinit var _buttonScanProfile: LinearLayout;
|
||||
private lateinit var _buttonImportProfile: LinearLayout;
|
||||
private lateinit var _editProfile: EditText;
|
||||
private lateinit var _loaderOverlay: LoaderOverlay;
|
||||
|
||||
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||
|
@ -52,6 +59,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||
_buttonHelp = findViewById(R.id.button_help);
|
||||
_buttonScanProfile = findViewById(R.id.button_scan_profile);
|
||||
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
||||
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||
_editProfile = findViewById(R.id.edit_profile);
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
finish();
|
||||
|
@ -94,42 +102,57 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||
return;
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
_loaderOverlay.show()
|
||||
|
||||
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
||||
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
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);
|
||||
if (existingProcessSecret != null) {
|
||||
UIDialogs.toast(this, getString(R.string.this_profile_is_already_imported));
|
||||
return;
|
||||
}
|
||||
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
||||
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
||||
|
||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||
Store.instance.addProcessSecret(processSecret);
|
||||
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
||||
if (existingProcessSecret != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
|
||||
}
|
||||
return@launch;
|
||||
}
|
||||
|
||||
val processHandle = processSecret.toProcessHandle();
|
||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||
Store.instance.addProcessSecret(processSecret);
|
||||
|
||||
for (e in exportBundle.events.eventsList) {
|
||||
try {
|
||||
val se = SignedEvent.fromProto(e);
|
||||
Store.instance.putSignedEvent(se);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Ignored invalid event", e);
|
||||
val processHandle = processSecret.toProcessHandle();
|
||||
|
||||
for (e in exportBundle.events.eventsList) {
|
||||
try {
|
||||
val se = SignedEvent.fromProto(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
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
@ -12,6 +14,7 @@ import android.webkit.MimeTypeMap
|
|||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumptech.glide.Glide
|
||||
|
@ -21,14 +24,16 @@ import com.futo.platformplayer.dp
|
|||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
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.Synchronization
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.toBase64Url
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -46,6 +51,8 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||
private lateinit var _buttonDelete: BigButton;
|
||||
private lateinit var _username: String;
|
||||
private lateinit var _imagePolycentric: ImageView;
|
||||
private lateinit var _loaderOverlay: LoaderOverlay;
|
||||
private lateinit var _textSystem: TextView;
|
||||
private var _avatarUri: Uri? = null;
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
|
@ -63,28 +70,13 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||
_buttonExport = findViewById(R.id.button_export);
|
||||
_buttonLogout = findViewById(R.id.button_logout);
|
||||
_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 {
|
||||
saveIfRequired();
|
||||
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 {
|
||||
ImagePicker.with(this)
|
||||
.cropSquare()
|
||||
|
@ -120,6 +112,37 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||
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() {
|
||||
|
@ -128,13 +151,17 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||
var hasChanges = false;
|
||||
val username = _editName.text.toString();
|
||||
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;
|
||||
}
|
||||
|
||||
val processHandle = StatePolycentric.instance.processHandle;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -219,6 +246,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||
private fun updateUI() {
|
||||
val processHandle = StatePolycentric.instance.processHandle!!;
|
||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
|
||||
_textSystem.text = processHandle.system.key.toBase64Url()
|
||||
_username = systemState.username;
|
||||
_editName.text.clear();
|
||||
_editName.text.append(_username);
|
||||
|
|
|
@ -6,11 +6,12 @@ import android.graphics.Color
|
|||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
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.UIDialogs
|
||||
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.states.StateApp
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -93,7 +98,7 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||
|
||||
val comment = _editComment.text.toString();
|
||||
val processHandle = StatePolycentric.instance.processHandle!!
|
||||
val eventPointer = processHandle.post(comment, null, ref)
|
||||
val eventPointer = processHandle.post(comment, ref)
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
|
|
|
@ -465,7 +465,7 @@ class ChannelFragment : MainFragment() {
|
|||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
|
||||
|
|
|
@ -314,8 +314,8 @@ class PostDetailFragment : MainFragment {
|
|||
private fun updatePolycentricRating() {
|
||||
_rating.visibility = View.GONE;
|
||||
|
||||
val value = _post?.id?.value ?: _postOverview?.id?.value ?: return;
|
||||
val ref = Models.referenceFromBuffer(value.toByteArray());
|
||||
val ref = Models.referenceFromBuffer((_post?.url ?: _postOverview?.url)?.toByteArray() ?: return)
|
||||
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.toByteArray()
|
||||
val version = _version;
|
||||
|
||||
_rating.onLikeDislikeUpdated.remove(this);
|
||||
|
@ -333,7 +333,8 @@ class PostDetailFragment : MainFragment {
|
|||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
||||
ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.dislike.data)).build()
|
||||
)
|
||||
),
|
||||
extraByteReferences = listOfNotNull(extraBytesRef)
|
||||
);
|
||||
|
||||
if (version != _version) {
|
||||
|
@ -342,8 +343,8 @@ class PostDetailFragment : MainFragment {
|
|||
|
||||
val likes = queryReferencesResponse.countsList[0];
|
||||
val dislikes = queryReferencesResponse.countsList[1];
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(ref);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref);
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (version != _version) {
|
||||
|
@ -468,9 +469,7 @@ class PostDetailFragment : MainFragment {
|
|||
if (_postOverview == null) {
|
||||
fetchPolycentricProfile();
|
||||
updatePolycentricRating();
|
||||
|
||||
val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); };
|
||||
_addCommentView.setContext(value.url, ref);
|
||||
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||
}
|
||||
|
||||
updateCommentType(true);
|
||||
|
@ -489,9 +488,7 @@ class PostDetailFragment : MainFragment {
|
|||
_textMeta.text = value.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "" //TODO: Include view count?
|
||||
_textContent.text = value.description.fixHtmlWhitespace();
|
||||
_platformIndicator.setPlatformFromClientID(value.id.pluginId);
|
||||
|
||||
val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); };
|
||||
_addCommentView.setContext(value.url, ref);
|
||||
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||
|
||||
updatePolycentricRating();
|
||||
fetchPolycentricProfile();
|
||||
|
@ -636,12 +633,12 @@ class PostDetailFragment : MainFragment {
|
|||
|
||||
if (cachedPolycentricProfile?.profile == null) {
|
||||
_layoutMonetization.visibility = View.GONE;
|
||||
_creatorThumbnail.setHarborAvailable(false, animate);
|
||||
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
||||
return;
|
||||
}
|
||||
|
||||
_layoutMonetization.visibility = View.VISIBLE;
|
||||
_creatorThumbnail.setHarborAvailable(true, animate);
|
||||
_creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto());
|
||||
}
|
||||
|
||||
private fun fetchPost() {
|
||||
|
@ -665,14 +662,16 @@ class PostDetailFragment : MainFragment {
|
|||
private fun fetchPolycentricComments() {
|
||||
Logger.i(TAG, "fetchPolycentricComments")
|
||||
val post = _post;
|
||||
val idValue = post?.id?.value
|
||||
if (idValue == null) {
|
||||
Logger.w(TAG, "Failed to fetch polycentric comments because id was null")
|
||||
val ref = (_post?.url ?: _postOverview?.url)?.toByteArray()?.let { Models.referenceFromBuffer(it) }
|
||||
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.toByteArray()
|
||||
|
||||
if (ref == null) {
|
||||
Logger.w(TAG, "Failed to fetch polycentric comments because url was not set null")
|
||||
_commentsList.clear();
|
||||
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) {
|
||||
|
|
|
@ -129,8 +129,8 @@ class TutorialFragment : MainFragment() {
|
|||
override val dash: IDashManifestSource? = null
|
||||
override val hls: IHLSManifestSource? = null
|
||||
override val subtitles: List<ISubtitleSource> = emptyList()
|
||||
override val shareUrl: String = ""
|
||||
override val url: String = ""
|
||||
override val shareUrl: String = videoUrl
|
||||
override val url: String = videoUrl
|
||||
override val datetime: OffsetDateTime? = OffsetDateTime.parse("2023-12-18T00:00:00Z")
|
||||
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")
|
||||
|
|
|
@ -1207,9 +1207,9 @@ class VideoDetailView : ConstraintLayout {
|
|||
};
|
||||
}
|
||||
|
||||
val ref = video.id.value?.let { Models.referenceFromBuffer(it.toByteArray()) };
|
||||
_addCommentView.setContext(video.url, ref);
|
||||
|
||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||
val extraBytesRef = video.id.value?.toByteArray()
|
||||
_addCommentView.setContext(video.url, ref)
|
||||
_player.setMetadata(video.name, video.author.name);
|
||||
|
||||
if (video !is TutorialFragment.TutorialVideo) {
|
||||
|
@ -1270,57 +1270,54 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
_rating.onLikeDislikeUpdated.remove(this);
|
||||
|
||||
if (ref != null) {
|
||||
_rating.visibility = View.GONE;
|
||||
_rating.visibility = View.GONE;
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
||||
arrayListOf(
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.like.data)).build(),
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.dislike.data)).build()
|
||||
)
|
||||
);
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
||||
arrayListOf(
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.like.data)).build(),
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.dislike.data)).build()
|
||||
),
|
||||
extraByteReferences = listOfNotNull(extraBytesRef)
|
||||
);
|
||||
|
||||
val likes = queryReferencesResponse.countsList[0];
|
||||
val dislikes = queryReferencesResponse.countsList[1];
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(ref);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref);
|
||||
val likes = queryReferencesResponse.countsList[0];
|
||||
val dislikes = queryReferencesResponse.countsList[1];
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
_rating.visibility = View.VISIBLE;
|
||||
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
||||
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
||||
if (args.hasLiked) {
|
||||
args.processHandle.opinion(ref, Opinion.like);
|
||||
} else if (args.hasDisliked) {
|
||||
args.processHandle.opinion(ref, Opinion.dislike);
|
||||
} else {
|
||||
args.processHandle.opinion(ref, Opinion.neutral);
|
||||
withContext(Dispatchers.Main) {
|
||||
_rating.visibility = View.VISIBLE;
|
||||
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
||||
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
||||
if (args.hasLiked) {
|
||||
args.processHandle.opinion(ref, Opinion.like);
|
||||
} else if (args.hasDisliked) {
|
||||
args.processHandle.opinion(ref, Opinion.dislike);
|
||||
} else {
|
||||
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) {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
_rating.visibility = View.GONE;
|
||||
}
|
||||
|
||||
when (video.rating) {
|
||||
|
@ -1367,28 +1364,30 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
updateQueueState();
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val historyItem = getHistoryIndex(videoDetail);
|
||||
if (video !is TutorialFragment.TutorialVideo) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val historyItem = getHistoryIndex(videoDetail);
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
||||
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
|
||||
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
|
||||
_layoutResume.visibility = View.VISIBLE;
|
||||
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
|
||||
withContext(Dispatchers.Main) {
|
||||
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
||||
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
|
||||
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
|
||||
_layoutResume.visibility = View.VISIBLE;
|
||||
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
|
||||
|
||||
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
delay(8000);
|
||||
_layoutResume.visibility = View.GONE;
|
||||
_textResume.text = "";
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to set resume changes.", e);
|
||||
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
delay(8000);
|
||||
_layoutResume.visibility = View.GONE;
|
||||
_textResume.text = "";
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to set resume changes.", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_layoutResume.visibility = View.GONE;
|
||||
_textResume.text = "";
|
||||
}
|
||||
} else {
|
||||
_layoutResume.visibility = View.GONE;
|
||||
_textResume.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1960,7 +1959,9 @@ class VideoDetailView : ConstraintLayout {
|
|||
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() {
|
||||
Logger.i(TAG, "fetchVideo")
|
||||
|
@ -2222,9 +2223,11 @@ class VideoDetailView : ConstraintLayout {
|
|||
val v = video ?: return;
|
||||
val currentTime = System.currentTimeMillis();
|
||||
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val history = getHistoryIndex(v);
|
||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
||||
if (v !is TutorialFragment.TutorialVideo) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val history = getHistoryIndex(v);
|
||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
||||
}
|
||||
}
|
||||
_lastPositionSaveTime = currentTime;
|
||||
}
|
||||
|
@ -2307,7 +2310,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_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
|
||||
|
|
|
@ -56,7 +56,7 @@ class PolycentricCache {
|
|||
|
||||
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
|
||||
{ system ->
|
||||
val signedProfileEvents = ApiMethods.getQueryLatest(
|
||||
val signedEventsList = ApiMethods.getQueryLatest(
|
||||
SERVER,
|
||||
system.toProto(),
|
||||
listOf(
|
||||
|
@ -72,8 +72,9 @@ class PolycentricCache {
|
|||
ContentType.MEMBERSHIP_URLS.value,
|
||||
ContentType.DONATION_DESTINATIONS.value
|
||||
)
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) }
|
||||
.groupBy { e -> e.event.contentType }
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||
|
||||
val signedProfileEvents = signedEventsList.groupBy { e -> e.event.contentType }
|
||||
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
|
||||
|
||||
val storageSystemState = StorageTypeSystemState.create()
|
||||
|
@ -151,17 +152,7 @@ class PolycentricCache {
|
|||
|
||||
private val _batchTaskGetData = BatchedTaskHandler<String, ByteBuffer>(_scope,
|
||||
{
|
||||
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) {
|
||||
throw Exception("Only URLInfoDataLink is supported");
|
||||
}
|
||||
|
||||
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
||||
val dataLink = getDataLinkFromUrl(it) ?: throw Exception("Only URLInfoDataLink is supported");
|
||||
return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink);
|
||||
},
|
||||
{ return@BatchedTaskHandler null },
|
||||
|
@ -325,9 +316,10 @@ class PolycentricCache {
|
|||
.build();
|
||||
|
||||
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 val CACHE_EXPIRATION_SECONDS = 60 * 60 * 3;
|
||||
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
|
||||
|
||||
@JvmStatic
|
||||
val instance: PolycentricCache
|
||||
|
@ -343,5 +335,20 @@ class PolycentricCache {
|
|||
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.stores.FragmentedStorage
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
@ -38,7 +51,6 @@ import userpackage.Protocol
|
|||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.Exception
|
||||
|
||||
class StatePolycentric {
|
||||
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);
|
||||
}
|
||||
|
||||
fun hasDisliked(ref: Protocol.Reference): Boolean {
|
||||
fun hasDisliked(data: ByteArray): Boolean {
|
||||
if (!enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
|
||||
val entry = _likeDislikeMap[data.toBase64()] ?: return false;
|
||||
return entry.hasDisliked;
|
||||
}
|
||||
|
||||
fun hasLiked(ref: Protocol.Reference): Boolean {
|
||||
fun hasLiked(data: ByteArray): Boolean {
|
||||
if (!enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
|
||||
val entry = _likeDislikeMap[data.toBase64()] ?: return false;
|
||||
return entry.hasLiked;
|
||||
}
|
||||
|
||||
|
@ -316,7 +328,7 @@ class StatePolycentric {
|
|||
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) {
|
||||
return EmptyPager()
|
||||
}
|
||||
|
@ -338,7 +350,8 @@ class StatePolycentric {
|
|||
Protocol.QueryReferencesRequestCountReferences.newBuilder()
|
||||
.setFromType(ContentType.POST.value)
|
||||
.build())
|
||||
.build()
|
||||
.build(),
|
||||
extraByteReferences = extraByteReferences
|
||||
);
|
||||
|
||||
val results = mapQueryReferences(contextUrl, response);
|
||||
|
@ -407,7 +420,8 @@ class StatePolycentric {
|
|||
ContentType.AVATAR.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 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 androidx.constraintlayout.widget.ConstraintLayout
|
||||
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.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
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.setPlatformPlayerLinkMovementMethod
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
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.pills.PillButton
|
||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
|
@ -104,7 +110,8 @@ class CommentViewHolder : ViewHolder {
|
|||
|
||||
fun bind(comment: IPlatformComment, readonly: Boolean) {
|
||||
_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;
|
||||
|
||||
val date = comment.date;
|
||||
|
@ -161,8 +168,8 @@ class CommentViewHolder : ViewHolder {
|
|||
_pillRatingLikesDislikes.visibility = View.VISIBLE;
|
||||
|
||||
if (comment is PolycentricPlatformComment) {
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(comment.reference);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference);
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(comment.reference.toByteArray());
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference.toByteArray());
|
||||
_pillRatingLikesDislikes.setRating(comment.rating, hasLiked, hasDisliked);
|
||||
} else {
|
||||
_pillRatingLikesDislikes.setRating(comment.rating);
|
||||
|
|
|
@ -126,7 +126,8 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
|||
_taskGetLiveComment.cancel()
|
||||
|
||||
_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;
|
||||
|
||||
val date = comment.date;
|
||||
|
@ -168,8 +169,8 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
|||
if (likesDislikesReplies != null) {
|
||||
Log.i(TAG, "updateLikesDislikesReplies set")
|
||||
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(c.reference);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference);
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(c.reference.toByteArray());
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference.toByteArray());
|
||||
_pillRatingLikesDislikes.setRating(RatingLikeDislikes(likesDislikesReplies.likes, likesDislikesReplies.dislikes), hasLiked, hasDisliked);
|
||||
|
||||
_buttonReplies.setLoading(false)
|
||||
|
|
|
@ -7,7 +7,7 @@ import android.widget.ImageView
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
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.models.PlatformAuthorLink
|
||||
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.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
|
||||
|
||||
|
@ -149,7 +149,8 @@ open class PlaylistView : LinearLayout {
|
|||
_neopassAnimator?.cancel();
|
||||
_neopassAnimator = null;
|
||||
|
||||
val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty();
|
||||
val firstClaim = claims?.ownedClaims?.firstOrNull();
|
||||
val harborAvailable = firstClaim != null
|
||||
if (harborAvailable) {
|
||||
_imageNeopassChannel?.visibility = View.VISIBLE
|
||||
if (animate) {
|
||||
|
@ -160,7 +161,7 @@ open class PlaylistView : LinearLayout {
|
|||
_imageNeopassChannel?.visibility = View.GONE
|
||||
}
|
||||
|
||||
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate)
|
||||
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate, firstClaim?.system?.toProto())
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -6,21 +6,18 @@ import android.widget.ImageButton
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
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.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
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.states.StateSubscriptions
|
||||
import com.futo.platformplayer.toHumanBytesSpeed
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.toHumanTimeIndicator
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
|
@ -107,7 +104,7 @@ class SubscriptionViewHolder : ViewHolder {
|
|||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
|
|
@ -334,7 +334,7 @@ open class PreviewVideoView : LinearLayout {
|
|||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
} else if (_imageChannel != null) {
|
||||
val dp_28 = 28.dp(context.resources);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.futo.platformplayer.views.adapters.viewholders
|
||||
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
|
@ -8,12 +7,10 @@ import android.widget.TextView
|
|||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
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.TaskHandler
|
||||
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.states.StateApp
|
||||
|
@ -76,7 +73,7 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
|
|||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
@ -148,7 +145,7 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
|||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
|
|
@ -98,7 +98,7 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
|||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_authorLink?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
|
|
@ -77,7 +77,7 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
|
|||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
|
|
|
@ -18,12 +18,20 @@ import android.widget.TextView
|
|||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.animation.doOnStart
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.views.others.CircularProgressBar
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class GestureControlView : LinearLayout {
|
||||
private val _scope = CoroutineScope(Dispatchers.Main);
|
||||
|
@ -95,22 +103,23 @@ class GestureControlView : LinearLayout {
|
|||
if(p0 == null)
|
||||
return false;
|
||||
|
||||
val minDistance = Math.min(width, height)
|
||||
if (_isFullScreen && _adjustingBrightness) {
|
||||
val adjustAmount = (distanceY * 2) / height;
|
||||
val adjustAmount = (distanceY * 2) / minDistance;
|
||||
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_progressBrightness.progress = _brightnessFactor;
|
||||
onBrightnessAdjusted.emit(_brightnessFactor);
|
||||
} else if (_isFullScreen && _adjustingSound) {
|
||||
val adjustAmount = (distanceY * 2) / height;
|
||||
val adjustAmount = (distanceY * 2) / minDistance;
|
||||
_soundFactor = (_soundFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_progressSound.progress = _soundFactor;
|
||||
onSoundAdjusted.emit(_soundFactor);
|
||||
} else if (_adjustingFullscreenUp) {
|
||||
val adjustAmount = (distanceY * 2) / height;
|
||||
val adjustAmount = (distanceY * 2) / minDistance;
|
||||
_fullScreenFactorUp = (_fullScreenFactorUp + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_layoutControlsFullscreen.alpha = _fullScreenFactorUp;
|
||||
} else if (_adjustingFullscreenDown) {
|
||||
val adjustAmount = (-distanceY * 2) / height;
|
||||
val adjustAmount = (-distanceY * 2) / minDistance;
|
||||
_fullScreenFactorDown = (_fullScreenFactorDown + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_layoutControlsFullscreen.alpha = _fullScreenFactorDown;
|
||||
} else {
|
||||
|
|
|
@ -12,12 +12,16 @@ import com.bumptech.glide.Glide
|
|||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
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 {
|
||||
private val _root: ConstraintLayout;
|
||||
private val _imageChannelThumbnail: ImageView;
|
||||
private val _imageNewActivity: ImageView;
|
||||
private val _imageNeoPass: ImageView;
|
||||
private val _identicon: IdenticonView;
|
||||
private var _harborAnimator: ObjectAnimator? = null;
|
||||
private var _imageAnimator: ObjectAnimator? = null;
|
||||
|
||||
|
@ -28,19 +32,22 @@ class CreatorThumbnail : ConstraintLayout {
|
|||
|
||||
_root = findViewById(R.id.root);
|
||||
_imageChannelThumbnail = findViewById(R.id.image_channel_thumbnail);
|
||||
_identicon = findViewById(R.id.identicon);
|
||||
_imageChannelThumbnail.clipToOutline = true;
|
||||
_imageChannelThumbnail.visibility = View.GONE
|
||||
_imageNewActivity = findViewById(R.id.image_new_activity);
|
||||
_imageNeoPass = findViewById(R.id.image_neopass);
|
||||
|
||||
if (!isInEditMode) {
|
||||
setHarborAvailable(false, animate = false);
|
||||
setHarborAvailable(false, animate = false, system = null);
|
||||
setNewActivity(false);
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_imageChannelThumbnail.visibility = View.GONE;
|
||||
_imageChannelThumbnail.setImageResource(R.drawable.placeholder_channel_thumbnail);
|
||||
setHarborAvailable(false, animate = false);
|
||||
setHarborAvailable(false, animate = false, system = null);
|
||||
setNewActivity(false);
|
||||
}
|
||||
|
||||
|
@ -50,13 +57,24 @@ class CreatorThumbnail : ConstraintLayout {
|
|||
return;
|
||||
}
|
||||
|
||||
_imageChannelThumbnail.visibility = View.VISIBLE;
|
||||
|
||||
_harborAnimator?.cancel();
|
||||
_harborAnimator = null;
|
||||
|
||||
_imageAnimator?.cancel();
|
||||
_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) {
|
||||
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 = null;
|
||||
|
||||
|
@ -85,6 +103,13 @@ class CreatorThumbnail : ConstraintLayout {
|
|||
} else {
|
||||
_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) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.futo.platformplayer.views.overlays
|
|||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
|
@ -16,6 +17,21 @@ class LoaderOverlay(context: Context, attrs: AttributeSet?) : FrameLayout(contex
|
|||
inflate(context, R.layout.overlay_loader, this);
|
||||
_container = findViewById(R.id.container);
|
||||
_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() {
|
||||
|
|
|
@ -6,8 +6,8 @@ import android.view.View
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
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.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
|
@ -102,7 +102,8 @@ class RepliesOverlay : LinearLayout {
|
|||
}
|
||||
|
||||
_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);
|
||||
|
|
|
@ -94,4 +94,11 @@
|
|||
android:text="@string/import_profile" />
|
||||
</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>
|
|
@ -51,6 +51,21 @@
|
|||
app:layout_constraintLeft_toLeftOf="@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
|
||||
android:id="@+id/layout_buttons"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -91,4 +106,11 @@
|
|||
android:layout_marginTop="8dp"
|
||||
app:buttonBackground="@drawable/background_big_button_red"/>
|
||||
</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>
|
|
@ -17,5 +17,6 @@
|
|||
android:layout_gravity="top|center_horizontal"
|
||||
android:alpha="0.7"
|
||||
android:layout_marginTop="80dp"
|
||||
android:layout_marginBottom="80dp"
|
||||
android:contentDescription="@string/loading" />
|
||||
</FrameLayout>
|
|
@ -7,6 +7,18 @@
|
|||
android:layout_height="wrap_content"
|
||||
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
|
||||
android:id="@+id/image_channel_thumbnail"
|
||||
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 863d0be1322660c99e4d0cdae0b45d0a5918542d
|
||||
Subproject commit 01270edbb4b6b4fb004e22fc529bf787c7f5be81
|
|
@ -1 +1 @@
|
|||
Subproject commit b0e35a9b6631fb3279fb38619ecc3eba812e5ed6
|
||||
Subproject commit 7d4303c82a1ec06871f0717a606f5c6cb87e78be
|
|
@ -1 +1 @@
|
|||
Subproject commit d41cc8e848891ef8e949e6d49384b754e7c305c7
|
||||
Subproject commit 13551ab67fc8fb1899b5bcbfdec750855b154790
|
|
@ -1 +1 @@
|
|||
Subproject commit b0e35a9b6631fb3279fb38619ecc3eba812e5ed6
|
||||
Subproject commit 7d4303c82a1ec06871f0717a606f5c6cb87e78be
|
|
@ -1 +1 @@
|
|||
Subproject commit 86cd96c41f15f7f73c091f61800a49f376a38150
|
||||
Subproject commit 7695198eeaeaaea4726712c460081c411ef67866
|
Loading…
Add table
Add a link
Reference in a new issue