mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-29 22:24:29 +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
|
||||
|
||||
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")
|
||||
|
@ -1201,9 +1201,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) {
|
||||
@ -1264,57 +1264,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) {
|
||||
@ -1361,28 +1358,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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1954,7 +1953,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")
|
||||
@ -2216,9 +2217,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;
|
||||
}
|
||||
@ -2301,7 +2304,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) {
|
||||
|
@ -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 86cd96c41f15f7f73c091f61800a49f376a38150
|
||||
Subproject commit 7695198eeaeaaea4726712c460081c411ef67866
|
Loading…
x
Reference in New Issue
Block a user