Compare commits

...

36 Commits

Author SHA1 Message Date
Kelvin 6141c36855 Refs 2023-12-21 20:04:53 +01:00
Kelvin 4084ab3ed0 refs 2023-12-21 20:01:51 +01:00
Kelvin 34e733823a Refs 2023-12-21 19:27:13 +01:00
Kelvin f1d01642cd Refs, ui fixes 2023-12-21 19:24:30 +01:00
Kelvin d5551d7118 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 17:24:18 +01:00
Kelvin d079a1e8e4 Add missing channel name setter 2023-12-21 17:24:11 +01:00
Koen c06c00ee9b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 17:19:17 +01:00
Koen 1d8eababc2 Minor casting dialog fixes. 2023-12-21 17:19:05 +01:00
Kelvin 75cf1ffbdd Offline playback fix with remote sources 2023-12-21 17:13:43 +01:00
Kelvin 5499706a9b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 16:14:01 +01:00
Kelvin ba57e32920 Minor queue styling, refs 2023-12-21 16:13:53 +01:00
Koen df96c5b51c Fixed live video previews in preview layout. Fixed time bar spacing at bottom. 2023-12-21 16:07:03 +01:00
Koen 75f81d20db Fixed buttons in subscription groups and made select button click only work when there are things selected. 2023-12-21 13:10:38 +01:00
Koen 3fc92e4065 Fixed rounding of subscription groups. 2023-12-21 12:56:35 +01:00
Koen 8ffd5f411f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 11:21:34 +01:00
Koen 918161a299 Fixed margins for subscription groups and menu bar icons now show filled variant if existing. 2023-12-21 11:20:53 +01:00
Kelvin 9f50f72eaa Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-21 11:12:45 +01:00
Kelvin 2f66f124aa Toast on cancel creation, default subgroup creator image, fix subgroup cache load 2023-12-21 11:12:34 +01:00
Koen 9a11717cf4 Fixed Rumble channel contents having empty author. Changed TutorialVideo so author is not clickable. 2023-12-21 10:20:52 +01:00
Kelvin 0d80424799 Optimized selection creator overlay, global subscription progress listener 2023-12-20 21:54:39 +01:00
Kelvin ed9a65b2f0 Filter cache results by enabled clients 2023-12-20 18:35:13 +01:00
Kelvin 8a53297be2 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-20 18:08:52 +01:00
Kelvin 20862a27c8 Search creator overlay, Fix crash Add topbar 2023-12-20 18:08:40 +01:00
Koen 95785e6c78 Added something more similar to Jdenticons. 2023-12-20 17:35:57 +01:00
Koen e88c649578 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-20 17:09:19 +01:00
Koen 09f91e64fb Fixed Kick subscription imports. 2023-12-20 17:04:10 +01:00
Koen b8923e59a1 Fixed bottom bar new tabs not showing up for people who changed tabs. 2023-12-20 17:03:48 +01:00
Kelvin e722c0ce9a Explore subscription groups button 2023-12-20 17:02:40 +01:00
Kelvin 56248bf4b0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-20 12:36:12 +01:00
Kelvin 5af4787c45 Fix nested overlays in more buttons 2023-12-20 12:35:40 +01:00
Koen 0990247322 Fixed Rumble channel content fetching, channel image and show comments when sign in is not required. 2023-12-20 12:06:21 +01:00
Koen 0154525578 Revert "Fixed download button overlay not working in more button."
This reverts commit 1dc6eee242.
2023-12-20 12:02:35 +01:00
Koen 1dc6eee242 Fixed download button overlay not working in more button. 2023-12-20 11:51:28 +01:00
Koen c63a63cb33 Fixed Gesture control distances for portrait full screen. 2023-12-20 11:06:18 +01:00
Koen c1967556ac Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-20 10:56:28 +01:00
Koen 309a57f5a1 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. 2023-12-20 10:56:16 +01:00
82 changed files with 1489 additions and 435 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
@@ -739,7 +739,7 @@ class UISlideOverlays {
}
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), invokeParents: Boolean = true, onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
val visible = buttonGroup.getVisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
val hidden = buttonGroup.getInvisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
@@ -747,7 +747,7 @@ class UISlideOverlays {
hidden
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
btn.handler?.invoke(btn);
}, true) as View }.toTypedArray(),
}, invokeParents) as View }.toTypedArray(),
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
val selected = it
@@ -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 {
@@ -7,6 +7,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
@@ -26,8 +27,8 @@ import kotlinx.coroutines.launch
class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
private lateinit var _imageLoader: ImageView;
private lateinit var _buttonClose: Button;
private lateinit var _buttonAdd: Button;
private lateinit var _buttonScanQR: Button;
private lateinit var _buttonAdd: ImageButton;
private lateinit var _buttonScanQR: ImageButton;
private lateinit var _textNoDevicesFound: TextView;
private lateinit var _textNoDevicesRemembered: TextView;
private lateinit var _recyclerDevices: RecyclerView;
@@ -289,10 +289,17 @@ class MenuBottomBarFragment : MainActivityFragment() {
buttonDefinitions.find { d -> d.id == it.id }
}.toMutableList()
if (!StatePayment.instance.hasPaid) {
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
//Add unconfigured tabs with default values
buttonDefinitions.forEach { buttonDefinition ->
if (!Settings.instance.tabs.any { it.id == buttonDefinition.id }) {
newCurrentButtonDefinitions.add(buttonDefinition)
}
}
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.faq, canToggle = false, { false }, {
if (!StatePayment.instance.hasPaid) {
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
}
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
it.navigate<BrowserFragment>(Settings.URL_FAQ);
}))
@@ -349,8 +356,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }),
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>() }),
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>() }),
ButtonDefinition(10, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>() }),
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, {
ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>() }),
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
val c = it.context ?: return@ButtonDefinition;
Logger.i(TAG, "settings preventPictureInPicture()");
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
@@ -418,6 +418,7 @@ class ChannelFragment : MainFragment() {
_buttonSubscribe.setSubscribeChannel(channel);
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
_textChannel.text = channel.name;
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else "";
//TODO: Find a better way to access the adapter fragments..
@@ -465,7 +466,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) {
@@ -39,10 +39,12 @@ class SourcesFragment : MainFragment() {
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack)
if(topBar is AddTopBarFragment)
if(topBar is AddTopBarFragment) {
(topBar as AddTopBarFragment).onAdd.clear();
(topBar as AddTopBarFragment).onAdd.subscribe {
startActivity(Intent(requireContext(), AddSourceOptionsActivity::class.java));
};
}
_view?.reloadSources();
}
@@ -4,19 +4,15 @@ import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.getSystemService
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -25,14 +21,13 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.dp
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.SearchView
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
import com.futo.platformplayer.views.overlays.CreatorSelectOverlay
import com.futo.platformplayer.views.overlays.ImageVariableOverlay
@@ -64,6 +59,11 @@ class SubscriptionGroupFragment : MainFragment() {
return view;
}
override fun onHide() {
super.onHide();
_view?.onHide();
}
companion object {
private const val TAG = "SourcesFragment";
fun newInstance() = SubscriptionGroupFragment().apply {}
@@ -86,7 +86,7 @@ class SubscriptionGroupFragment : MainFragment() {
private val _buttonSettings: ImageButton;
private val _buttonDelete: ImageButton;
private val _buttonAddCreator: Button;
private val _buttonAddCreator: FrameLayout;
private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
@@ -97,6 +97,8 @@ class SubscriptionGroupFragment : MainFragment() {
private var _group: SubscriptionGroup? = null;
private var _didDelete: Boolean = false;
constructor(context: Context, fragment: SubscriptionGroupFragment): super(context) {
inflate(context, R.layout.fragment_subscriptions_group, this);
_fragment = fragment;
@@ -137,6 +139,7 @@ class SubscriptionGroupFragment : MainFragment() {
UIDialogs.Action("Delete", {
_group?.let {
it.urls.remove(channel.url);
save();
reloadCreators(it);
}
}, UIDialogs.ActionStyle.DANGEROUS))
@@ -178,6 +181,7 @@ class SubscriptionGroupFragment : MainFragment() {
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Delete", {
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id);
_didDelete = true;
fragment.close(true);
}, UIDialogs.ActionStyle.DANGEROUS))
};
@@ -188,6 +192,12 @@ class SubscriptionGroupFragment : MainFragment() {
filterCreators();
}
_topbar.setButtons(
Pair(R.drawable.ic_share) {
UIDialogs.toast(context, "Coming soon");
}
);
setGroup(null);
}
@@ -240,6 +250,18 @@ class SubscriptionGroupFragment : MainFragment() {
_overlay.animate().alpha(1f).setDuration(300).start();
overlay.onSelected.subscribe {
_group?.let { g ->
if(g.urls.isEmpty() && g.image == null) {
//Obtain image
for(sub in it) {
val sub = StateSubscriptions.instance.getSubscription(sub);
if(sub != null && sub.channel.thumbnail != null) {
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
g.image?.setImageView(_imageGroup);
g.image?.setImageView(_imageGroupBackground);
break;
}
}
}
for(url in it) {
if(!g.urls.contains(url))
g.urls.add(url);
@@ -256,6 +278,7 @@ class SubscriptionGroupFragment : MainFragment() {
fun setGroup(group: SubscriptionGroup?) {
_didDelete = false;
_group = group;
_textGroupTitle.text = group?.name;
@@ -272,6 +295,12 @@ class SubscriptionGroupFragment : MainFragment() {
reloadCreators(group);
}
fun onHide() {
if(!_didDelete && _group != null && StateSubscriptionGroups.instance.getSubscriptionGroup(_group!!.id) === null) {
UIDialogs.toast(context, "Group creation cancelled");
}
}
@SuppressLint("NotifyDataSetChanged")
private fun reloadCreators(group: SubscriptionGroup?) {
_enabledCreators.clear();
@@ -107,12 +107,14 @@ class SubscriptionGroupListFragment : MainFragment() {
updateGroups();
}
if(topBar is AddTopBarFragment)
if(topBar is AddTopBarFragment) {
(topBar as AddTopBarFragment).onAdd.clear();
(topBar as AddTopBarFragment).onAdd.subscribe {
_overlay?.let {
UISlideOverlays.showCreateSubscriptionGroup(it)
}
};
}
}
private fun updateGroups() {
@@ -52,6 +52,7 @@ class SubscriptionsFeedFragment : MainFragment() {
override val hasBottomBar: Boolean get() = true;
private var _view: SubscriptionsFeedView? = null;
private var _group: SubscriptionGroup? = null;
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
@@ -72,6 +73,8 @@ class SubscriptionsFeedFragment : MainFragment() {
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = SubscriptionsFeedView(this, inflater, _cachedRecyclerData);
_view = view;
if(_group != null)
view.selectSubgroup(_group);
return view;
}
@@ -80,6 +83,7 @@ class SubscriptionsFeedFragment : MainFragment() {
val view = _view;
if (view != null) {
_cachedRecyclerData = view.recyclerData;
_group = view.subGroup;
view.cleanup();
_view = null;
}
@@ -100,12 +104,12 @@ class SubscriptionsFeedFragment : MainFragment() {
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar
private var _subGroup: SubscriptionGroup? = null;
var subGroup: SubscriptionGroup? = null;
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
StateSubscriptions.instance.onFeedProgress.subscribe(this) { id, progress, total ->
if(_subGroup?.id == id)
if(subGroup?.id == id)
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
setProgress(progress, total);
@@ -140,8 +144,9 @@ class SubscriptionsFeedFragment : MainFragment() {
recyclerData.lastLoad.getNowDiffSeconds() > 60 ) {
recyclerData.lastLoad = OffsetDateTime.now();
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen)
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen) {
loadResults(false);
}
else if(recyclerData.results.size == 0) {
loadCache();
setLoading(false);
@@ -197,7 +202,7 @@ class SubscriptionsFeedFragment : MainFragment() {
private var _bypassRateLimit = false;
private val _lastExceptions: List<Throwable>? = null;
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
val group = _subGroup;
val group = subGroup;
if(!_bypassRateLimit) {
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
@@ -257,6 +262,11 @@ class SubscriptionsFeedFragment : MainFragment() {
}
};
fun selectSubgroup(g: SubscriptionGroup?) {
if(g != null)
_subscriptionBar?.selectGroup(g);
}
private fun initializeToolbarContent() {
_subscriptionBar = SubscriptionBar(context).apply {
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
@@ -266,11 +276,17 @@ class SubscriptionsFeedFragment : MainFragment() {
if(g is SubscriptionGroup.Add)
UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer);
else {
_subGroup = g;
subGroup = g;
setProgress(0, 0);
if(Settings.instance.subscriptions.fetchOnTabOpen)
if(Settings.instance.subscriptions.fetchOnTabOpen) {
loadCache();
loadResults(false);
else loadCache();
}
else if(g != null && StateSubscriptions.instance.getFeed(g.id) != null) {
loadResults(false);
}
else
loadCache();
}
};
_subscriptionBar?.onHoldGroup?.subscribe { g ->
@@ -311,7 +327,7 @@ class SubscriptionsFeedFragment : MainFragment() {
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
val nowSoon = OffsetDateTime.now().plusMinutes(5);
val filterGroup = _subGroup;
val filterGroup = subGroup;
return results.filter {
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
@@ -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")
@@ -437,16 +437,20 @@ class VideoDetailView : ConstraintLayout {
var buttonMore: RoundButton? = null;
buttonMore = RoundButton(context, R.drawable.ic_menu, context.getString(R.string.more), TAG_MORE) {
_slideUpOverlay = UISlideOverlays.showMoreButtonOverlay(_overlayContainer, _buttonPins, listOf(TAG_MORE)) {selected ->
_slideUpOverlay = UISlideOverlays.showMoreButtonOverlay(_overlayContainer, _buttonPins, listOf(TAG_MORE), false) {selected ->
_buttonPins.setButtons(*(selected + listOf(buttonMore!!)).toTypedArray());
_buttonPinStore.set(*selected.filter { it.tagRef is String }.map{ it.tagRef as String }.toTypedArray())
_buttonPinStore.save();
}
};
};
_buttonMore = buttonMore;
updateMoreButtons();
_channelButton.setOnClickListener {
if (video is TutorialFragment.TutorialVideo) {
return@setOnClickListener
}
(video?.author ?: _searchVideo?.author)?.let {
fragment.navigate<ChannelFragment>(it);
fragment.lifecycleScope.launch {
@@ -769,6 +773,7 @@ class VideoDetailView : ConstraintLayout {
Logger.e(TAG, "Failed to reopen live chat", ex);
}
}
_slideUpOverlay?.hide();
} else null,
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
if(!allowBackground) {
@@ -781,6 +786,7 @@ class VideoDetailView : ConstraintLayout {
allowBackground = false;
it.text.text = resources.getString(R.string.background);
}
_slideUpOverlay?.hide();
},
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
video?.let {
@@ -793,11 +799,13 @@ class VideoDetailView : ConstraintLayout {
preventPictureInPicture = true;
shareVideo();
};
_slideUpOverlay?.hide();
},
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
this.startPictureInPicture();
fragment.forcePictureInPicture();
//PiPActivity.startPiP(context);
_slideUpOverlay?.hide();
},
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
video?.let {
@@ -805,9 +813,11 @@ class VideoDetailView : ConstraintLayout {
fragment.navigate<BrowserFragment>(url);
fragment.minimizeVideoDetail();
};
_slideUpOverlay?.hide();
},
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
reloadVideo();
_slideUpOverlay?.hide();
}).filterNotNull();
if(!_buttonPinStore.getAllValues().any())
_buttonPins.setButtons(*(buttons + listOf(_buttonMore)).toTypedArray());
@@ -1134,6 +1144,7 @@ class VideoDetailView : ConstraintLayout {
if(videoDetail is VideoLocal) {
videoLocal = videoDetail;
video = videoDetail;
this.video = video;
val videoTask = StatePlatform.instance.getContentDetails(videoDetail.url);
videoTask.invokeOnCompletion { ex ->
if(ex != null) {
@@ -1201,9 +1212,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 +1275,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 +1369,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 +1964,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 +2228,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 +2315,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
}
}
}
@@ -49,13 +49,17 @@ class StateCache {
Logger.i(TAG, "Subscriptions CachePager get subscriptions");
val subs = StateSubscriptions.instance.getSubscriptions();
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
val allUrls = subs.map {
val allUrls = subs
.map {
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
if(!otherUrls.contains(it.channel.url))
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
else
return@map otherUrls;
}.flatten().distinct();
}
.flatten()
.distinct()
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
Logger.i(TAG, "Subscriptions CachePager get pagers");
val pagers: List<IPager<IPlatformContent>>;
@@ -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 };
@@ -51,10 +51,16 @@ class StateSubscriptions {
val global: CentralizedFeed = CentralizedFeed();
val feeds: HashMap<String, CentralizedFeed> = hashMapOf();
val onFeedProgress = Event3<String, Int, Int>();
val onFeedProgress = Event3<String?, Int, Int>();
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
init {
global.onUpdateProgress.subscribe { progress, total ->
onFeedProgress.emit(null, progress, total);
}
}
fun getOldestUpdateTime(): OffsetDateTime {
val subs = getSubscriptions();
if(subs.size == 0)
@@ -0,0 +1,456 @@
package com.futo.platformplayer.views
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PointF
import android.util.AttributeSet
import android.view.View
import java.security.MessageDigest
import kotlin.math.max
import kotlin.math.min
class IdenticonView(context: Context, attrs: AttributeSet) : View(context, attrs) {
var hashString: String = "default"
set(value) {
field = value
hash = md5(value)
iconGenerator = null
invalidate()
}
private var hash = ByteArray(16)
private var iconGenerator: IconGenerator? = null
private val path = Path()
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)
if (iconGenerator == null) {
iconGenerator = IconGenerator(min(height, width).toFloat(), hash)
}
iconGenerator?.render(canvas)
}
private fun md5(input: String): ByteArray {
val md = MessageDigest.getInstance("MD5")
return md.digest(input.toByteArray(Charsets.UTF_8))
}
interface Shape {
fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint)
}
class CutCorner : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val k = size * 0.42f
val path = Path().apply {
moveTo(0f, 0f)
lineTo(size, 0f)
lineTo(size, size - k * 2)
lineTo(size - k, size)
lineTo(0f, size)
close()
}
canvas.drawPath(path, paint)
}
}
class SideTriangle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val w = size / 2
val h = size * 0.8f
val path = Path().apply {
moveTo(size - w, 0f)
lineTo(size, h)
lineTo(size - w, h)
close()
}
canvas.drawPath(path, paint)
}
}
class MiddleSquare : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val s = size / 3
canvas.drawRect(s, s, size - s, size - s, paint)
}
}
class CornerSquare : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val inner = size * 0.1f
val outer = max(1f, size * 0.25f)
canvas.drawRect(outer, outer, size - inner - outer, size - inner - outer, paint)
}
}
class OffCenterCircle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val m = size * 0.15f
val s = size * 0.5f
canvas.drawCircle(size - s - m, size - s - m, s / 2, paint)
}
}
class NegativeTriangle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val inner = size * 0.1f
val outer = inner * 4
val path = Path().apply {
addRect(0f, 0f, size, size, Path.Direction.CW)
moveTo(outer, outer)
lineTo(size - inner, outer)
lineTo(outer + (size - outer - inner) / 2, size - inner)
close()
}
canvas.drawPath(path, paint)
}
}
class CutSquare : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val path = Path().apply {
moveTo(0f, 0f)
lineTo(size, 0f)
lineTo(size, size * 0.7f)
lineTo(size * 0.4f, size * 0.4f)
lineTo(size * 0.7f, size)
lineTo(0f, size)
close()
}
canvas.drawPath(path, paint)
}
}
class CornerPlusTriangle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val halfSize = size / 2
canvas.drawRect(0f, 0f, size, halfSize, paint)
canvas.drawRect(0f, halfSize, halfSize, size, paint)
val path = Path().apply {
moveTo(halfSize, halfSize)
lineTo(size, halfSize)
lineTo(halfSize, size)
close()
}
canvas.drawPath(path, paint)
}
}
class NegativeSquare : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val inner = size * 0.14f
val outer = size * 0.35f
val path = Path().apply {
addRect(0f, 0f, size, size, Path.Direction.CW)
addRect(outer, outer, size - outer - inner, size - outer - inner, Path.Direction.CCW)
}
canvas.drawPath(path, paint)
}
}
class NegativeCircle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val inner = size * 0.12f
val outer = inner * 3
val path = Path().apply {
addRect(0f, 0f, size, size, Path.Direction.CW)
addCircle(outer, outer, (size - inner - outer) / 2, Path.Direction.CCW)
}
canvas.drawPath(path, paint)
}
}
class NegativeRhombus : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val m = size * 0.25f
val path = Path().apply {
addRect(0f, 0f, size, size, Path.Direction.CW)
moveTo(m, size / 2)
lineTo(size / 2, m)
lineTo(size - m, size / 2)
lineTo(size / 2, size - m)
close()
}
canvas.drawPath(path, paint)
}
}
class ConditionalCircle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
if (index == 0) {
val m = size * 0.4f
val s = size * 1.2f
canvas.drawCircle(m, m, s / 2, paint)
}
}
}
class HalfTriangle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val path = Path().apply {
moveTo(size / 2, size / 2)
lineTo(size, size / 2)
lineTo(size / 2, size)
close()
}
canvas.drawPath(path, paint)
}
}
class Triangle(val corner: Int = 0) : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val path = Path().apply {
when (corner) {
0 -> {
moveTo(0f, 0f)
lineTo(size, 0f)
lineTo(0f, size)
}
1 -> {
moveTo(size, 0f)
lineTo(size, size)
lineTo(0f, size)
}
2 -> {
moveTo(0f, 0f)
lineTo(size, 0f)
lineTo(size, size)
}
3 -> {
moveTo(0f, 0f)
lineTo(0f, size)
lineTo(size, size)
}
}
close()
}
canvas.drawPath(path, paint)
}
}
class BottomHalfTriangle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val path = Path().apply {
moveTo(0f, size / 2)
lineTo(size, size / 2)
lineTo(size / 2, size)
close()
}
canvas.drawPath(path, paint)
}
}
class Rhombus : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val path = Path().apply {
moveTo(size / 2, 0f)
lineTo(size, size / 2)
lineTo(size / 2, size)
lineTo(0f, size / 2)
close()
}
canvas.drawPath(path, paint)
}
}
class Circle : Shape {
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
val m = size / 6
canvas.drawCircle(m, m, size / 2 - m, paint)
}
}
class IconGenerator(private val size: Float, private val hash: ByteArray) {
private val digits: ByteArray
private var selectedColors = arrayOf<Paint>()
init {
digits = ByteArray(max(12, hash.size * 2))
var index = 0
for (byte in hash) {
if (index >= digits.size) {
break
}
digits[index] = ((byte.toInt() shr 4) and 0x0f).toByte()
digits[index + 1] = (byte.toInt() and 0x0f).toByte()
index += 2
}
selectColors()
}
private fun selectColors() {
val value = hash.copyOfRange(hash.size - 4, hash.size).fold(0) { acc, byte ->
(acc shl 8) or (byte.toInt() and 0xFF)
} and 0x0FFFFFFF
val colorTheme = ColorTheme(hue = value.toFloat() / 0x0FFFFFFF)
val selectedColorIndices = mutableListOf<Int>()
for (i in 0 until 3) {
val index = (digits[8 + i].toInt() % colorTheme.colors.size)
selectedColorIndices.add(colorTheme.validateIndex(index, selectedColorIndices))
}
selectedColors = selectedColorIndices.map { index ->
Paint().apply {
color = colorTheme.colors[index]
style = Paint.Style.FILL
}
}.toTypedArray()
}
fun renderBitmap(): Bitmap {
val bitmap = Bitmap.createBitmap(size.toInt(), size.toInt(), Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
render(canvas)
return bitmap
}
fun render(canvas: Canvas) {
canvas.drawColor(Color.WHITE)
renderShape(canvas, 0, outerShapes, 2, 3, arrayOf(
PointF(1f, 0f),
PointF(2f, 0f),
PointF(2f, 3f),
PointF(1f, 3f),
PointF(0f, 1f),
PointF(3f, 1f),
PointF(3f, 2f),
PointF(0f, 2f),
))
renderShape(canvas, 1, outerShapes, 4, 5, arrayOf(
PointF(0f, 0f),
PointF(3f, 0f),
PointF(3f, 3f),
PointF(0f, 3f),
))
renderShape(canvas, 2, centerShapes, 1, null, arrayOf(
PointF(1f, 1f),
PointF(2f, 1f),
PointF(2f, 2f),
PointF(1f, 2f),
))
}
private fun renderShape(
canvas: Canvas,
colorIndex: Int,
shapes: Array<Shape>,
index: Int,
rotationIndex: Int?,
positions: Array<PointF>
) {
val cellSize = size / 4
var r = rotationIndex?.let { digits[it].toInt() } ?: 0
val shape = shapes[digits[index].toInt() % shapes.size]
val paint = Paint().apply {
color = selectedColors[colorIndex % selectedColors.size].color
style = Paint.Style.FILL
}
for ((idx, position) in positions.withIndex()) {
canvas.save()
canvas.translate(position.x * cellSize, position.y * cellSize)
canvas.translate(cellSize / 2, cellSize / 2)
canvas.rotate((r % 4) * 90f)
canvas.translate(-cellSize / 2, -cellSize / 2)
shape.draw(canvas, cellSize, idx, paint)
canvas.restore()
r++
}
}
class ColorTheme(val hue: Float, val saturation: Float = 0.5f) {
val colors: List<Int>
init {
colors = listOf(
// Dark gray
grayscaleColor(0f),
// Mid color
hslColor(hue, saturation, colorLightness(0.5f)),
// Light gray
grayscaleColor(1f),
// Light color
hslColor(hue, saturation, colorLightness(1f)),
// Dark color
hslColor(hue, saturation, colorLightness(0f))
)
}
fun validateIndex(index: Int, selected: List<Int>): Int {
return if (isDuplicate(index, listOf(0, 4), selected) || isDuplicate(index, listOf(2, 3), selected)) {
1
} else {
index
}
}
private fun isDuplicate(index: Int, values: List<Int>, selected: List<Int>): Boolean {
if (!values.contains(index)) return false
return values.any { selected.contains(it) }
}
private fun colorLightness(value: Float): Float = lightness(value, 0.4f, 0.8f)
private fun grayscaleLightness(value: Float): Float = lightness(value, 0.3f, 0.9f)
private fun lightness(value: Float, min: Float, max: Float): Float {
val lightness = min + value * (max - min)
return minOf(1f, maxOf(0f, lightness))
}
private fun grayscaleColor(lightness: Float): Int {
return Color.HSVToColor(floatArrayOf(0f, 0f, lightness))
}
private fun hslColor(hue: Float, saturation: Float, lightness: Float): Int {
return Color.HSVToColor(floatArrayOf(hue, saturation, lightness))
}
}
}
companion object {
val centerShapes = arrayOf(
CutCorner(),
SideTriangle(),
MiddleSquare(),
CornerSquare(),
OffCenterCircle(),
NegativeTriangle(),
CutSquare(),
HalfTriangle(),
CornerPlusTriangle(),
CutSquare(),
NegativeCircle(),
HalfTriangle(),
NegativeRhombus(),
ConditionalCircle()
)
val outerShapes = arrayOf(
Triangle(),
BottomHalfTriangle(),
Rhombus(),
Circle(),
)
private const val TAG = "IdenticonView"
}
}
@@ -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) {
@@ -15,6 +15,7 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.debug.Stopwatch
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.video.PlayerManager
@@ -46,7 +47,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
val contentDetails = StatePlatform.instance.getContentDetails(video.url).await();
stopwatch.logAndNext(TAG, "Retrieving video detail (IO thread)")
return@TaskHandler Pair(viewHolder, contentDetails)
}).success { previewContentDetails(it.first, it.second) }
}).exception<Throwable> { Logger.e(TAG, "Failed to retrieve preview content.", it) }.success { previewContentDetails(it.first, it.second) }
constructor(context: Context, feedStyle : FeedStyle, dataSet: ArrayList<IPlatformContent>, exoPlayer: PlayerManager? = null,
initialPlay: Boolean = false, viewsToPrepend: ArrayList<View> = arrayListOf(),
@@ -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) {
@@ -3,45 +3,31 @@ package com.futo.platformplayer.views.adapters.viewholders
import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.FrameLayout
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.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SubscriptionGroup>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_group_bar, _viewGroup, false)) {
private var _group: SubscriptionGroup? = null;
private val _root: FrameLayout;
private val _image: ShapeableImageView;
private val _textSubGroup: TextView;
val onClick = Event1<SubscriptionGroup>();
val onClickLong = Event1<SubscriptionGroup>();
init {
_root = _view.findViewById(R.id.root);
_image = _view.findViewById(R.id.image);
_textSubGroup = _view.findViewById(R.id.text_sub_group);
val dp6 = 6.dp(_view.resources);
_image.shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
.build()
_view.setOnClickListener {
_group?.let {
onClick.emit(it);
@@ -58,9 +44,9 @@ class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
override fun bind(value: SubscriptionGroup) {
_group = value;
val img = value.image;
if(img != null)
if(img != null) {
img.setImageView(_image)
else {
} else {
_image.setImageResource(0);
if(value is SubscriptionGroup.Add)
@@ -68,10 +54,11 @@ class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
}
_textSubGroup.text = value.name;
if(value is SubscriptionGroup.Selectable && value.selected)
_view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimary, null));
else
_view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null));
if (value is SubscriptionGroup.Selectable && value.selected) {
_root.setBackgroundResource(R.drawable.background_primary_round_6dp)
} else {
_root.background = null
}
}
companion object {
@@ -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,23 @@ class CreatorThumbnail : ConstraintLayout {
_root = findViewById(R.id.root);
_imageChannelThumbnail = findViewById(R.id.image_channel_thumbnail);
_identicon = findViewById(R.id.identicon);
_imageChannelThumbnail.clipToOutline = true;
_identicon.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 +58,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 +91,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 +104,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) {
@@ -1,55 +1,32 @@
package com.futo.platformplayer.views.overlays
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.shapes.Shape
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.activity.result.contract.ActivityResultContracts
import android.widget.FrameLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.futo.platformplayer.PresetImages
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.dp
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
import com.futo.platformplayer.views.SearchView
import com.futo.platformplayer.views.adapters.viewholders.SelectableCreatorBarViewHolder
import com.futo.platformplayer.views.buttons.BigButton
import com.github.dhaval2404.imagepicker.ImagePicker
import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
import java.io.File
class CreatorSelectOverlay: ConstraintLayout {
private val _buttonSelect: Button;
private val _buttonSelect: FrameLayout;
private val _topbar: OverlayTopbar;
private val _searchBar: SearchView;
private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
private val _creators: ArrayList<SelectableCreatorBarViewHolder.Selectable> = arrayListOf();
private val _creatorsFiltered: ArrayList<SelectableCreatorBarViewHolder.Selectable> = arrayListOf();
private var _selected: MutableList<String> = mutableListOf();
@@ -66,7 +43,7 @@ class CreatorSelectOverlay: ConstraintLayout {
else
_creators.addAll(subs
.map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) });
_recyclerCreators.notifyContentChanged();
filterCreators();
}
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { }
init {
@@ -74,7 +51,8 @@ class CreatorSelectOverlay: ConstraintLayout {
_topbar = findViewById(R.id.topbar);
_buttonSelect = findViewById(R.id.button_select);
val dp6 = 6.dp(resources);
_recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creators, RecyclerView.HORIZONTAL) { creatorView ->
_searchBar = findViewById(R.id.search_bar);
_recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creatorsFiltered, RecyclerView.HORIZONTAL) { creatorView ->
creatorView.itemView.setPadding(0, dp6, 0, dp6);
creatorView.onClick.subscribe {
if(it.channel.thumbnail == null) {
@@ -92,19 +70,33 @@ class CreatorSelectOverlay: ConstraintLayout {
this.orientation = LinearLayoutManager.VERTICAL;
};
_buttonSelect.setOnClickListener {
_selected?.let {
if (_selected.isNotEmpty()) {
select();
}
};
_topbar.onClose.subscribe {
onClose.emit();
}
_searchBar.onSearchChanged.subscribe {
filterCreators();
};
updateSelected();
filterCreators();
}
fun updateSelected() {
_creators.forEach { p -> p.active = _selected.contains(p.channel.url) };
_recyclerCreators.notifyContentChanged();
val changed = arrayListOf<SelectableCreatorBarViewHolder.Selectable>()
for(creator in _creators) {
val act = _selected.contains(creator.channel.url);
if(creator.active != act) {
creator.active = act;
changed.add(creator);
}
}
for(change in changed) {
val index = _creatorsFiltered.indexOf(change);
_recyclerCreators.notifyContentChanged(index);
}
if(_selected.isNotEmpty())
_buttonSelect.alpha = 1f;
@@ -113,6 +105,17 @@ class CreatorSelectOverlay: ConstraintLayout {
}
private fun filterCreators(withUpdate: Boolean = true) {
val query = _searchBar.textSearch.text.toString().lowercase();
val filteredEnabled = _creators.filter { query.isEmpty() || it.channel.name.lowercase().contains(query) };
//Optimize
_creatorsFiltered.clear();
_creatorsFiltered.addAll(filteredEnabled);
if(withUpdate)
_recyclerCreators.notifyContentChanged();
}
fun select() {
if(_creators.isEmpty())
return;
@@ -11,6 +11,7 @@ import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.net.toFile
@@ -48,7 +49,7 @@ class ImageVariableOverlay: ConstraintLayout {
private val _buttonGallery: BigButton;
private val _imageGallerySelected: ImageView;
private val _imageGallerySelectedContainer: LinearLayout;
private val _buttonSelect: Button;
private val _buttonSelect: TextView;
private val _topbar: OverlayTopbar;
private val _recyclerPresets: AnyAdapterView<PresetImage, PresetViewHolder>;
private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
@@ -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() {
@@ -3,10 +3,13 @@ package com.futo.platformplayer.views.overlays
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.marginRight
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.dp
import com.futo.platformplayer.views.lists.VideoListEditorView
class OverlayTopbar : ConstraintLayout {
@@ -16,6 +19,8 @@ class OverlayTopbar : ConstraintLayout {
private val _button_close: ImageView;
private val _button_list: LinearLayout;
val onClose = Event0();
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
@@ -24,6 +29,7 @@ class OverlayTopbar : ConstraintLayout {
_name = findViewById(R.id.text_name);
_meta = findViewById(R.id.text_meta);
_button_close = findViewById(R.id.button_close);
_button_list = findViewById(R.id.button_list);
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.OverlayTopbar, 0, 0);
val attrText = attrArr.getText(R.styleable.OverlayTopbar_title) ?: "";
@@ -42,4 +48,20 @@ class OverlayTopbar : ConstraintLayout {
_name.text = name;
_meta.text = meta;
}
fun setButtons(vararg buttons: Pair<Int, ()->Unit>) {
_button_list.removeAllViews();
val dp40 = 40.dp(resources);
val dp5 = 5.dp(resources);
for(button in buttons) {
_button_list.addView(ImageView(context).apply {
layoutParams = LinearLayout.LayoutParams(dp40, dp40)
setPadding(dp5, dp5, dp5 * 2, dp5);
setImageResource(button.first);
setOnClickListener {
button.second();
}
});
}
}
}
@@ -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);
@@ -2,12 +2,14 @@ package com.futo.platformplayer.views.subscriptions
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.Subscription
@@ -24,7 +26,8 @@ import kotlinx.coroutines.launch
class SubscriptionBar : LinearLayout {
private var _adapterView: AnyAdapterView<Subscription, SubscriptionBarViewHolder>? = null;
private var _subGroups: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>
private var _subGroups: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>;
private var _subGroupsExplore: SubscriptionExploreButton;
private val _tagsContainer: LinearLayout;
private val _groups: ArrayList<SubscriptionGroup>;
@@ -64,7 +67,32 @@ class SubscriptionBar : LinearLayout {
onHoldGroup.emit(g);
}
}
_subGroupsExplore = findViewById(R.id.subgroup_explore);
_tagsContainer = findViewById(R.id.container_tags);
_subGroupsExplore.onClick.subscribe {
UIDialogs.showDialog(context, R.drawable.ic_subscriptions, "Subscription Groups",
"Subscription groups are an easy way to navigate your subscriptions.\n\nDefine your own subsets, and in the near future share them with others.", null, 0,
UIDialogs.Action("Hide Bar", {
Settings.instance.subscriptions.showSubscriptionGroups = false;
Settings.instance.save();
reloadGroups();
UIDialogs.showDialogOk(context, R.drawable.ic_quiz, "Subscription groups can be re-enabled in settings")
}),
UIDialogs.Action("Create", {
onToggleGroup.emit(SubscriptionGroup.Add()); //Shortcut..
}, UIDialogs.ActionStyle.PRIMARY))
};
updateExplore();
}
fun selectGroup(group: SubscriptionGroup) {
val relevantGroup = _groups.find { it.id == group.id };
if(relevantGroup != null && _group != relevantGroup) {
groupClicked(relevantGroup);
}
}
private fun groupClicked(g: SubscriptionGroup) {
@@ -100,6 +128,8 @@ class SubscriptionBar : LinearLayout {
_groups.clear();
_groups.addAll(results);
_subGroups.notifyContentChanged();
updateExplore();
}
private fun getGroups(): List<SubscriptionGroup> {
return if(Settings.instance.subscriptions.showSubscriptionGroups)
@@ -110,6 +140,18 @@ class SubscriptionBar : LinearLayout {
else listOf();
}
fun updateExplore() {
val show = Settings.instance.subscriptions.showSubscriptionGroups &&
_groups.all { it is SubscriptionGroup.Add };
if(show) {
_subGroupsExplore.visibility = View.VISIBLE;
_subGroups.view.visibility = View.GONE;
}
else {
_subGroupsExplore.visibility = View.GONE;
_subGroups.view.visibility = View.VISIBLE;
}
}
fun setToggles(vararg buttons: Toggle) {
_tagsContainer.removeAllViews();
@@ -0,0 +1,45 @@
package com.futo.platformplayer.views.subscriptions
import android.content.Context
import android.graphics.drawable.Animatable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
class SubscriptionExploreButton : ConstraintLayout {
val onClick = Event0();
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
inflate(context, R.layout.view_subscription_group_bar_explore, this);
val dp10 = 10.dp(resources);
findViewById<ShapeableImageView>(R.id.image)
.apply {
adjustViewBounds = true
scaleType = ImageView.ScaleType.CENTER_CROP;
shapeAppearanceModel = ShapeAppearanceModel.builder().setAllCorners(CornerFamily.ROUNDED, dp10.toFloat()).build()
}
findViewById<ConstraintLayout>(R.id.root).setOnClickListener {
onClick.emit();
}
}
}
@@ -1,6 +1,9 @@
package com.futo.platformplayer.views.video
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
@@ -11,6 +14,9 @@ import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.PlayerView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@@ -39,6 +45,14 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
//Events
private val _evMuteChanged = mutableListOf<(FutoThumbnailPlayer, Boolean)->Unit>();
private val _loadArtwork = object: CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
setArtwork(BitmapDrawable(resources, resource));
}
override fun onLoadCleared(placeholder: Drawable?) {
setArtwork(null);
}
}
@OptIn(UnstableApi::class)
@@ -113,11 +127,38 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
}
fun setPreview(video: IPlatformVideoDetails) {
val videoSource = VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS);
val audioSource = VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context));
setSource(videoSource, audioSource,true, false);
if (video.live != null) {
setSource(video.live, null,true, false);
} else {
val videoSource = VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS);
val audioSource = VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context));
if (videoSource == null && audioSource != null) {
val thumbnail = video.thumbnails.getHQThumbnail();
if (!thumbnail.isNullOrBlank()) {
Glide.with(videoView).asBitmap().load(thumbnail).into(_loadArtwork);
} else {
Glide.with(videoView).clear(_loadArtwork);
setArtwork(null);
}
} else {
Glide.with(videoView).clear(_loadArtwork);
}
setSource(videoSource, audioSource,true, false);
}
}
override fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean) {
}
@OptIn(UnstableApi::class)
fun setArtwork(drawable: Drawable?) {
if (drawable != null) {
videoView.defaultArtwork = drawable;
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_FILL;
} else {
videoView.defaultArtwork = null;
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_OFF;
}
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:radius="10dp"
android:topRightRadius="10dp"
android:bottomRightRadius="10dp"
android:bottomLeftRadius="10dp" />
<stroke
android:width="1dp"
android:color="#6F6F6F" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:radius="10dp"
android:topRightRadius="10dp"
android:bottomRightRadius="10dp"
android:bottomLeftRadius="10dp" />
<stroke
android:width="1dp"
android:color="#6F6F6F" />
<solid android:color="#99000000" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#99000000" />
<corners android:radius="6dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#2D63ED" />
<corners android:radius="6dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M481.54,685.39Q494.85,685.39 503.96,676.27Q513.08,667.15 513.08,653.85Q513.08,640.54 503.96,631.42Q494.85,622.31 481.54,622.31Q468.23,622.31 459.11,631.42Q450,640.54 450,653.85Q450,667.15 459.11,676.27Q468.23,685.39 481.54,685.39ZM460.92,552.92L499.54,552.92Q501.08,526.15 509.46,510.31Q517.85,494.46 541.54,470.77Q570.39,441.92 583.35,420.73Q596.31,399.54 596.31,372.61Q596.31,325.77 564.15,297.11Q532,268.46 485.61,268.46Q442.92,268.46 412.11,291.23Q381.31,314 367.08,345.85L403.85,361.08Q413.92,337.15 432.61,321.42Q451.31,305.69 483.15,305.69Q520.92,305.69 539.31,326.35Q557.69,347 557.69,373.31Q557.69,393.39 546.69,410.08Q535.69,426.77 515.85,445.15Q483.61,474.92 472.27,498.73Q460.92,522.54 460.92,552.92ZM224.62,800Q197,800 178.5,781.5Q160,763 160,735.39L160,224.61Q160,197 178.5,178.5Q197,160 224.62,160L735.39,160Q763,160 781.5,178.5Q800,197 800,224.61L800,735.39Q800,763 781.5,781.5Q763,800 735.39,800L224.62,800ZM224.62,760L735.39,760Q744.61,760 752.31,752.31Q760,744.61 760,735.39L760,224.61Q760,215.39 752.31,207.69Q744.61,200 735.39,200L224.62,200Q215.38,200 207.69,207.69Q200,215.39 200,224.61L200,735.39Q200,744.61 207.69,752.31Q215.38,760 224.62,760ZM200,200L200,200Q200,200 200,206.92Q200,213.85 200,224.61L200,735.39Q200,746.15 200,753.08Q200,760 200,760L200,760Q200,760 200,753.08Q200,746.15 200,735.39L200,224.61Q200,213.85 200,206.92Q200,200 200,200Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M481.54,685.39Q494.85,685.39 503.96,676.27Q513.08,667.15 513.08,653.85Q513.08,640.54 503.96,631.42Q494.85,622.31 481.54,622.31Q468.23,622.31 459.11,631.42Q450,640.54 450,653.85Q450,667.15 459.11,676.27Q468.23,685.39 481.54,685.39ZM460.92,552.92L499.54,552.92Q501.08,526.15 509.46,510.31Q517.85,494.46 541.54,470.77Q570.39,441.92 583.35,420.73Q596.31,399.54 596.31,372.61Q596.31,325.77 564.15,297.11Q532,268.46 485.61,268.46Q442.92,268.46 412.11,291.23Q381.31,314 367.08,345.85L403.85,361.08Q413.92,337.15 432.61,321.42Q451.31,305.69 483.15,305.69Q520.92,305.69 539.31,326.35Q557.69,347 557.69,373.31Q557.69,393.39 546.69,410.08Q535.69,426.77 515.85,445.15Q483.61,474.92 472.27,498.73Q460.92,522.54 460.92,552.92ZM224.62,800Q197,800 178.5,781.5Q160,763 160,735.39L160,224.61Q160,197 178.5,178.5Q197,160 224.62,160L735.39,160Q763,160 781.5,178.5Q800,197 800,224.61L800,735.39Q800,763 781.5,781.5Q763,800 735.39,800L224.62,800Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M459.39,744.61L498.61,744.61L498.61,697.69Q541.69,694.08 581.15,666.77Q620.61,639.46 620.61,582Q620.61,540 595.08,510.39Q569.54,480.77 497.54,454.77Q431.39,431.69 413,415.15Q394.61,398.61 394.61,368Q394.61,337.39 418.5,317Q442.39,296.61 482,296.61Q512.46,296.61 532.77,310.58Q553.08,324.54 565.69,346L600.46,332.31Q586.39,303.46 559.19,284Q532,264.54 500.61,262.31L500.61,215.39L461.39,215.39L461.39,262.31Q409.08,271 382.23,301.31Q355.39,331.61 355.39,368Q355.39,411.15 382.5,437.08Q409.61,463 474,486.31Q538.54,510.08 560.73,529.23Q582.92,548.39 582.92,582Q582.92,624.23 552.11,642.81Q521.31,661.39 486,661.39Q451.46,661.39 423.65,641.27Q395.85,621.15 379.23,584L344,599.23Q361.08,640.31 389.81,663.27Q418.54,686.23 459.39,695.69L459.39,744.61ZM480,840Q405.46,840 339.77,811.58Q274.08,783.15 225.46,734.54Q176.85,685.92 148.42,620.23Q120,554.54 120,480Q120,405.46 148.42,339.77Q176.85,274.08 225.46,225.46Q274.08,176.85 339.77,148.42Q405.46,120 480,120Q554.54,120 620.23,148.42Q685.92,176.85 734.54,225.46Q783.15,274.08 811.58,339.77Q840,405.46 840,480Q840,554.54 811.58,620.23Q783.15,685.92 734.54,734.54Q685.92,783.15 620.23,811.58Q554.54,840 480,840Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M120,640L120,560L440,560L440,640L120,640ZM120,480L120,400L600,400L600,480L120,480ZM120,320L120,240L600,240L600,320L120,320ZM640,840L640,520L880,680L640,840Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M540,581.54Q552.39,581.54 561.81,572.11Q571.23,562.69 571.23,550.31Q571.23,537.92 561.81,528.5Q552.39,519.08 540,519.08Q527.61,519.08 518.19,528.5Q508.77,537.92 508.77,550.31Q508.77,562.69 518.19,572.11Q527.61,581.54 540,581.54ZM522.31,468.92L557.69,468.92Q559.23,443.77 565.62,431.04Q572,418.31 596.31,395.54Q621.69,372.46 631.69,354.35Q641.69,336.23 641.69,312.77Q641.69,272.39 612.89,245.42Q584.08,218.46 540,218.46Q506.69,218.46 480.81,236.46Q454.92,254.46 441.39,285.54L473.85,299.85Q485.15,276.39 501.42,264.65Q517.69,252.92 540,252.92Q568.61,252.92 587.46,269.89Q606.31,286.85 606.31,313.69Q606.31,330 597.15,344.04Q588,358.08 565.69,377.85Q540.39,399.92 531.35,418.35Q522.31,436.77 522.31,468.92ZM324.61,680Q297,680 278.5,661.5Q260,643 260,615.39L260,184.61Q260,157 278.5,138.5Q297,120 324.61,120L755.39,120Q783,120 801.5,138.5Q820,157 820,184.61L820,615.39Q820,643 801.5,661.5Q783,680 755.39,680L324.61,680ZM204.62,800Q177,800 158.5,781.5Q140,763 140,735.39L140,264.61L180,264.61L180,735.39Q180,744.62 187.69,752.31Q195.38,760 204.62,760L675.39,760L675.39,800L204.62,800Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M405.38,840L390.92,724.31Q371.77,718.54 349.5,706.15Q327.23,693.77 311.61,679.62L204.92,725L130.31,595L222.54,525.46Q220.77,514.61 219.62,503.11Q218.46,491.61 218.46,480.77Q218.46,470.69 219.62,459.58Q220.77,448.46 222.54,434.54L130.31,365L204.92,236.54L310.85,281.15Q328.77,266.23 349.61,254.23Q370.46,242.23 390.15,235.69L405.38,120L554.62,120L569.08,236.46Q592.08,244.54 609.73,255Q627.39,265.46 646.08,281.15L755.08,236.54L829.69,365L734.39,436.85Q737.69,449.23 738.08,459.58Q738.46,469.92 738.46,480Q738.46,489.31 737.69,499.65Q736.92,510 734.15,524.69L827.92,595L753.31,725L646.08,678.85Q627.39,694.54 608.46,705.77Q589.54,717 569.08,723.54L554.62,840L405.38,840ZM478.92,580Q520.77,580 549.85,550.92Q578.92,521.85 578.92,480Q578.92,438.15 549.85,409.08Q520.77,380 478.92,380Q436.85,380 407.88,409.08Q378.92,438.15 378.92,480Q378.92,521.85 407.88,550.92Q436.85,580 478.92,580Z"/>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

@@ -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>
@@ -81,43 +81,38 @@
<TextView
android:id="@+id/text_remembered_devices"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_height="wrap_content"
android:text="@string/remembered_devices"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:textSize="14dp"
android:ellipsize="end"
android:textColor="@color/white"
android:maxLines="1"
android:fontFamily="@font/inter_regular" />
<Button
<ImageButton
android:id="@+id/button_scan_qr"
android:layout_width="0dp"
android:layout_weight="1.7"
android:layout_height="wrap_content"
android:text="@string/scan_qr"
android:textSize="14dp"
android:textAlignment="center"
android:layout_marginEnd="2dp"
android:ellipsize="end"
android:maxLines="1"
android:fontFamily="@font/inter_regular"
android:textColor="@color/colorPrimary"
android:background="@color/transparent" />
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="centerCrop"
app:srcCompat="@drawable/ic_qr"
app:tint="@color/primary" />
<Button
<Space android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<ImageButton
android:id="@+id/button_add"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="@string/add"
android:textSize="14dp"
android:textAlignment="textEnd"
android:layout_marginEnd="2dp"
android:ellipsize="end"
android:maxLines="1"
android:fontFamily="@font/inter_regular"
android:textColor="@color/colorPrimary"
android:background="@color/transparent" />
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="centerCrop"
app:srcCompat="@drawable/ic_add"
app:tint="@color/primary"
android:layout_marginEnd="20dp"/>
</LinearLayout>
<LinearLayout
@@ -27,7 +27,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/failed_to_retrieve_data_are_you_connected"
android:textSize="14dp"
android:textSize="15dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"
android:textAlignment="center"
@@ -43,7 +43,7 @@
android:textAlignment="center"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:textSize="9dp"
android:textSize="11dp"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/dialog_text_code"
@@ -180,20 +180,28 @@
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/container_top"
app:layout_constraintBottom_toTopOf="@id/button_creator_add"
android:layout_marginTop="10dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:paddingTop="10dp"
android:paddingBottom="10dp" />
<Button
<FrameLayout
android:id="@+id/button_creator_add"
android:layout_width="match_parent"
android:background="@drawable/background_button_primary"
android:layout_height="50dp"
android:layout_margin="10dp"
app:layout_constraintBottom_toBottomOf="parent"
android:text="Add Creator" />
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fontFamily="@font/inter_regular"
android:text="@string/add_creator"
android:textSize="16dp"
android:layout_gravity="center"
android:gravity="center" />
</FrameLayout>
<FrameLayout
android:id="@+id/overlay"
@@ -128,7 +128,7 @@
android:layout_marginTop="2dp"
android:gravity="top"
android:orientation="horizontal"
android:paddingBottom="5dp">
android:paddingBottom="0dp">
<com.futo.platformplayer.views.others.CreatorThumbnail
android:id="@+id/creator_thumbnail"
@@ -179,6 +179,7 @@
android:gravity="center_vertical"
android:maxLines="1"
android:textColor="@color/gray_e0"
android:layout_marginBottom="5dp"
android:textSize="12dp"
tools:text="57K views • 1 day ago" />
</LinearLayout>
@@ -140,37 +140,22 @@
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp">
<ImageButton
android:id="@+id/button_add_to_queue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="1dp"
android:paddingTop="7dp"
android:paddingStart="6dp"
android:paddingEnd="5dp"
android:paddingBottom="3dp"
app:srcCompat="@drawable/ic_queue_16dp"
android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<LinearLayout
android:id="@+id/button_add_to"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="27dp"
android:orientation="horizontal"
android:background="@drawable/edit_text_background"
android:layout_marginStart="4dp"
android:gravity="center_vertical"
android:padding="4dp"
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
android:padding="5dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
app:srcCompat="@drawable/ic_add_white_8dp"
android:background="@color/transparent"
android:layout_marginStart="4dp"
android:contentDescription="@string/options" />
@@ -185,6 +170,23 @@
android:layout_marginEnd="4dp"/>
</LinearLayout>
<ImageButton
android:id="@+id/button_add_to_queue"
android:layout_width="wrap_content"
android:layout_height="27dp"
android:src="@drawable/ic_queue"
android:paddingTop="4dp"
android:paddingBottom="2dp"
android:paddingLeft="10dp"
android:paddingRight="7dp"
android:layout_marginLeft="7dp"
android:scaleType="fitCenter"
android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue"
app:layout_constraintLeft_toRightOf="@id/button_add_to"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/text_video_name"
android:layout_width="fill_parent"
@@ -195,8 +197,9 @@
android:textSize="13dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_light"
tools:text="Legendary grant recipient: Marvin Wißfeld of MicroG Very loong title"
tools:text="Legendary grant recipient: Marvin Wißfeld of MicroG Very loong title fff"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent" />
@@ -174,37 +174,23 @@
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp">
<ImageButton
android:id="@+id/button_add_to_queue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="1dp"
android:paddingTop="7dp"
android:paddingStart="6dp"
android:paddingEnd="5dp"
android:paddingBottom="3dp"
app:srcCompat="@drawable/ic_queue_16dp"
android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<LinearLayout
android:id="@+id/button_add_to"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="27dp"
android:orientation="horizontal"
android:background="@drawable/edit_text_background"
android:layout_marginStart="4dp"
android:gravity="center_vertical"
android:padding="4dp"
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
android:padding="5dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
app:srcCompat="@drawable/ic_add_white_8dp"
android:background="@color/transparent"
android:layout_marginStart="4dp"
android:contentDescription="@string/options" />
@@ -219,6 +205,23 @@
android:layout_marginEnd="4dp"/>
</LinearLayout>
<ImageButton
android:id="@+id/button_add_to_queue"
android:layout_width="wrap_content"
android:layout_height="27dp"
android:src="@drawable/ic_queue"
android:paddingTop="4dp"
android:paddingBottom="2dp"
android:paddingLeft="10dp"
android:paddingRight="7dp"
android:layout_marginLeft="7dp"
android:scaleType="fitCenter"
android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue"
app:layout_constraintLeft_toRightOf="@id/button_add_to"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/text_video_name"
android:layout_width="fill_parent"
@@ -16,29 +16,44 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<com.futo.platformplayer.views.SearchView
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/topbar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_creators"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/topbar"
app:layout_constraintBottom_toTopOf="@id/container_select"
app:layout_constraintTop_toBottomOf="@id/search_bar"
app:layout_constraintBottom_toTopOf="@id/button_select"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
</androidx.recyclerview.widget.RecyclerView>
<LinearLayout
android:id="@+id/container_select"
<FrameLayout
android:id="@+id/button_select"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="50dp"
android:background="@drawable/background_button_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<Button
android:id="@+id/button_select"
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="Select" />
</LinearLayout>
android:layout_height="match_parent"
android:fontFamily="@font/inter_regular"
android:text="@string/select"
android:textSize="16dp"
android:gravity="center"
android:layout_gravity="center" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -116,20 +116,29 @@
</LinearLayout>
</ScrollView>
<LinearLayout
<FrameLayout
android:id="@+id/container_select"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="50dp"
android:background="@drawable/background_button_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<Button
<TextView
android:id="@+id/button_select"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/background_button_primary"
android:text="Select" />
</LinearLayout>
android:layout_height="match_parent"
android:fontFamily="@font/inter_regular"
android:text="@string/select"
android:textSize="16dp"
android:gravity="center"
android:layout_gravity="center" />
</FrameLayout>
</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>
@@ -40,6 +40,10 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/button_list"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/button_close"
android:layout_width="40dp"
@@ -107,7 +107,7 @@
android:id="@id/exo_progress"
android:layout_width="match_parent"
android:layout_height="16dp"
android:layout_marginBottom="-1dp"
android:layout_marginBottom="-2dp"
android:paddingStart="0dp"
app:scrubber_drawable="@drawable/player_thumb"
app:bar_height="2dp"
@@ -7,12 +7,23 @@
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"
android:background="@drawable/rounded_outline"
android:clipToOutline="true"
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"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:srcCompat="@drawable/ic_futo_logo"
android:background="@drawable/rounded_outline"
android:clipToOutline="true"
android:scaleType="centerCrop"
+4 -1
View File
@@ -4,7 +4,10 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="10dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:orientation="vertical"
android:id="@+id/root">
@@ -28,4 +28,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" />
<com.futo.platformplayer.views.subscriptions.SubscriptionExploreButton
android:id="@+id/subgroup_explore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
</LinearLayout>
@@ -1,36 +1,38 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="78dp"
android:layout_height="54dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="2dp"
android:layout_margin="2dp"
android:clickable="true"
android:id="@+id/root">
android:id="@+id/root"
android:background="@drawable/background_primary_round_6dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/xp_book" />
<LinearLayout
android:src="@drawable/xp_book"
app:shapeAppearanceOverlay="@style/roundedCorners_6dp"
android:layout_margin="2dp" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#99000000"
android:gravity="center">
<TextView
android:id="@+id/text_sub_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:maxLines="2"
android:ellipsize="end"
android:textSize="12dp"
android:textAlignment="center"
android:text="News" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
android:background="@drawable/background_dark_round_6dp"
android:gravity="center"
android:layout_margin="2dp" />
<TextView
android:id="@+id/text_sub_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:maxLines="2"
android:ellipsize="end"
android:textSize="12dp"
android:textAlignment="center"
android:layout_gravity="center"
tools:text="News" />
</FrameLayout>
@@ -0,0 +1,36 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="54dp"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="1dp"
android:clickable="true"
android:id="@+id/root">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/sub_group_demo" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_button_explore_inner"
android:gravity="center">
<TextView
android:id="@+id/text_sub_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:maxLines="2"
android:ellipsize="end"
android:fontFamily="@font/inter_medium"
android:textSize="13dp"
android:textAlignment="center"
android:text="Explore Subscription Groups" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LoaderOverlay">
<attr name="centerLoader" format="boolean" />
</declare-styleable>
</resources>
+3 -1
View File
@@ -569,7 +569,7 @@
<string name="playlist_copied_as_local_playlist">Playlist copied as local playlist</string>
<string name="are_you_sure_you_want_to_delete_the_downloaded_videos">Are you sure you want to delete the downloaded videos?</string>
<string name="create_new_playlist">Create new playlist</string>
<string name="create_new_subgroup">Create new subscription group</string>
<string name="create_new_subgroup">Create new group</string>
<string name="expected_media_content_found">Expected media content, found</string>
<string name="failed_to_load_post">Failed to load post.</string>
<string name="replies">replies</string>
@@ -724,6 +724,8 @@
<string name="position">Position</string>
<string name="tutorials">Tutorials</string>
<string name="do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button">Do you want to see the tutorials? You can find them at any time through the more button.</string>
<string name="add_creator">Add Creators</string>
<string name="select">Select</string>
<string-array name="home_screen_array">
<item>Recommendations</item>
<item>Subscriptions</item>
+4
View File
@@ -11,6 +11,10 @@
<item name="cornerFamily">rounded</item>
<item name="cornerSize">4dp</item>
</style>
<style name="roundedCorners_6dp" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">6dp</item>
</style>
<style name="roundedCorners_10dp" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">10dp</item>