diff --git a/app/src/main/java/com/futo/platformplayer/HorizontalSpaceItemDecoration.kt b/app/src/main/java/com/futo/platformplayer/HorizontalSpaceItemDecoration.kt new file mode 100644 index 00000000..0d57ce75 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/HorizontalSpaceItemDecoration.kt @@ -0,0 +1,20 @@ +package com.futo.platformplayer + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +class HorizontalSpaceItemDecoration(private val startSpace: Int, private val betweenSpace: Int, private val endSpace: Int) : RecyclerView.ItemDecoration() { + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + outRect.left = betweenSpace + + val position = parent.getChildAdapterPosition(view) + if (position == 0) { + outRect.left = startSpace + } + + else if (position == state.itemCount - 1) { + outRect.right = endSpace + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt index 83a37e9a..4fbdf607 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt @@ -11,11 +11,12 @@ import com.futo.platformplayer.R import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.views.SupportView import com.futo.platformplayer.views.buttons.BigButton class ChannelMonetizationFragment : Fragment, IChannelTabFragment { - private var _buttonStore: BigButton? = null; + private var _supportView: SupportView? = null private var _lastChannel: IPlatformChannel? = null; private var _lastPolycentricProfile: PolycentricProfile? = null; @@ -24,20 +25,7 @@ class ChannelMonetizationFragment : Fragment, IChannelTabFragment { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_channel_monetization, container, false); - _buttonStore = view.findViewById(R.id.button_store); - - _buttonStore?.onClick?.subscribe { - _lastPolycentricProfile?.systemState?.store?.let { - try { - val uri = Uri.parse(it); - val intent = Intent(Intent.ACTION_VIEW) - intent.data = uri - startActivity(intent) - } catch (e: Throwable) { - Logger.e(TAG, "Failed to open URI: '${it}'.", e); - } - } - }; + _supportView = view.findViewById(R.id.support); _lastChannel?.also { setChannel(it); @@ -52,24 +40,16 @@ class ChannelMonetizationFragment : Fragment, IChannelTabFragment { override fun onDestroyView() { super.onDestroyView(); - _buttonStore = null; + _supportView = null; } override fun setChannel(channel: IPlatformChannel) { _lastChannel = channel; - _buttonStore?.visibility = View.GONE; } fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) { - _lastPolycentricProfile = polycentricProfile; - - if (polycentricProfile == null) { - return; - } - - if (polycentricProfile.systemState.store.isNotEmpty()) { - _buttonStore?.visibility = View.VISIBLE; - } + _lastPolycentricProfile = polycentricProfile + _supportView?.setPolycentricProfile(polycentricProfile, animate) } companion object { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 308cd63c..6261b6e1 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -70,6 +70,7 @@ import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.states.* import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.StringArrayStorage +import com.futo.platformplayer.views.MonetizationView import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout import com.futo.platformplayer.views.casting.CastView import com.futo.platformplayer.views.comments.AddCommentView @@ -79,6 +80,7 @@ import com.futo.platformplayer.views.overlays.DescriptionOverlay import com.futo.platformplayer.views.overlays.LiveChatOverlay import com.futo.platformplayer.views.overlays.QueueEditorOverlay import com.futo.platformplayer.views.overlays.RepliesOverlay +import com.futo.platformplayer.views.overlays.SupportOverlay import com.futo.platformplayer.views.overlays.slideup.* import com.futo.platformplayer.views.pills.PillRatingLikesDislikes import com.futo.platformplayer.views.pills.RoundButton @@ -191,6 +193,7 @@ class VideoDetailView : ConstraintLayout { private val _container_content_replies: RepliesOverlay; private val _container_content_description: DescriptionOverlay; private val _container_content_liveChat: LiveChatOverlay; + private val _container_content_support: SupportOverlay; private var _container_content_current: View; @@ -200,9 +203,7 @@ class VideoDetailView : ConstraintLayout { private val _imageDislikeIcon: ImageView; private val _imageLikeIcon: ImageView; - private val _buttonSupport: LinearLayout; - private val _buttonStore: LinearLayout; - private val _layoutMonetization: LinearLayout; + private val _monetization: MonetizationView; private val _buttonMore: RoundButton; @@ -292,6 +293,7 @@ class VideoDetailView : ConstraintLayout { _container_content_replies = findViewById(R.id.videodetail_container_replies); _container_content_description = findViewById(R.id.videodetail_container_description); _container_content_liveChat = findViewById(R.id.videodetail_container_livechat); + _container_content_support = findViewById(R.id.videodetail_container_support) _textComments = findViewById(R.id.text_comments); _addCommentView = findViewById(R.id.add_comment_view); @@ -310,11 +312,7 @@ class VideoDetailView : ConstraintLayout { _imageLikeIcon = findViewById(R.id.image_like_icon); _imageDislikeIcon = findViewById(R.id.image_dislike_icon); - _buttonSupport = findViewById(R.id.button_support); - _buttonStore = findViewById(R.id.button_store); - _layoutMonetization = findViewById(R.id.layout_monetization); - - _layoutMonetization.visibility = View.GONE; + _monetization = findViewById(R.id.monetization); _player.attachPlayer(); @@ -327,16 +325,12 @@ class VideoDetailView : ConstraintLayout { fragment.navigate(it.targetUrl); }; - _buttonSupport.setOnClickListener { - val author = video?.author ?: _searchVideo?.author; - author?.let { fragment.navigate(it).selectTab(2); }; - fragment.lifecycleScope.launch { - delay(100); - fragment.minimizeVideoDetail(); - }; + _monetization.onSupportTap.subscribe { + _container_content_support.setPolycentricProfile(_polycentricProfile?.profile, false); + switchContentView(_container_content_support); }; - _buttonStore.setOnClickListener { + _monetization.onStoreTap.subscribe { _polycentricProfile?.profile?.systemState?.store?.let { try { val uri = Uri.parse(it); @@ -349,6 +343,13 @@ class VideoDetailView : ConstraintLayout { } }; + _player.attachPlayer(); + + _container_content_liveChat.onRaidNow.subscribe { + StatePlayer.instance.clearQueue(); + fragment.navigate(it.targetUrl); + }; + StateApp.instance.preventPictureInPicture.subscribe(this) { Logger.i(TAG, "StateApp.instance.preventPictureInPicture.subscribe preventPictureInPicture = true"); preventPictureInPicture = true; @@ -545,6 +546,7 @@ class VideoDetailView : ConstraintLayout { _container_content_liveChat.onClose.subscribe { switchContentView(_container_content_main); }; _container_content_queue.onClose.subscribe { switchContentView(_container_content_main); }; _container_content_replies.onClose.subscribe { switchContentView(_container_content_main); }; + _container_content_support.onClose.subscribe { switchContentView(_container_content_main); }; _description_viewMore.setOnClickListener { switchContentView(_container_content_description); @@ -847,6 +849,7 @@ class VideoDetailView : ConstraintLayout { _container_content_replies.cleanup(); _container_content_queue.cleanup(); _container_content_description.cleanup(); + _container_content_support.cleanup(); StateCasting.instance.onActiveDevicePlayChanged.remove(this); StateCasting.instance.onActiveDeviceTimeChanged.remove(this); StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this); @@ -2096,12 +2099,7 @@ class VideoDetailView : ConstraintLayout { _creatorThumbnail.setHarborAvailable(profile != null, animate); } - if (profile != null) { - _channelName.text = cachedPolycentricProfile.profile.systemState.username; - _layoutMonetization.visibility = View.VISIBLE; - } else { - _layoutMonetization.visibility = View.GONE; - } + _monetization.setPolycentricProfile(cachedPolycentricProfile, animate); } fun setProgressBarOverlayed(isOverlayed: Boolean?) { diff --git a/app/src/main/java/com/futo/platformplayer/images/PolycentricModelLoader.java b/app/src/main/java/com/futo/platformplayer/images/PolycentricModelLoader.java index a521a2fb..067d8fcf 100644 --- a/app/src/main/java/com/futo/platformplayer/images/PolycentricModelLoader.java +++ b/app/src/main/java/com/futo/platformplayer/images/PolycentricModelLoader.java @@ -1,5 +1,7 @@ package com.futo.platformplayer.images; +import android.util.Log; + import androidx.annotation.NonNull; import com.bumptech.glide.Priority; diff --git a/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt b/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt index 6e29cb3e..dc5ce5bb 100644 --- a/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt +++ b/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt @@ -39,7 +39,12 @@ class PolycentricCache { ContentType.USERNAME.value, ContentType.DESCRIPTION.value, ContentType.STORE.value, - ContentType.SERVER.value + ContentType.SERVER.value, + ContentType.STORE_DATA.value, + ContentType.PROMOTION_BANNER.value, + ContentType.PROMOTION.value, + ContentType.MEMBERSHIP_URLS.value, + ContentType.DONATION_DESTINATIONS.value ) ).eventsList.map { e -> SignedEvent.fromProto(e) }; diff --git a/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt b/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt new file mode 100644 index 00000000..b221817c --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt @@ -0,0 +1,145 @@ +package com.futo.platformplayer.views + +import android.content.Context +import android.net.Uri +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.futo.platformplayer.R +import com.futo.platformplayer.HorizontalSpaceItemDecoration +import com.futo.platformplayer.api.http.ManagedHttpClient +import com.futo.platformplayer.constructs.Event0 +import com.futo.platformplayer.constructs.TaskHandler +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.polycentric.PolycentricCache +import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny +import com.futo.platformplayer.views.adapters.viewholders.StoreItemViewHolder +import com.futo.platformplayer.views.platform.PlatformIndicator +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json + +@Serializable +data class StoreItem( + val url: String, + val name: String, + val image: String +); + +class MonetizationView : LinearLayout { + private val _buttonSupport: LinearLayout; + private val _buttonStore: LinearLayout; + private val _buttonMembership: LinearLayout; + private val _platformIndicator: PlatformIndicator; + + private val _textMerchandise: TextView; + private val _recyclerMerchandise: RecyclerView; + private val _loaderMerchandise: Loader; + private val _layoutMerchandise: FrameLayout; + private var _merchandiseAdapterView: AnyAdapterView? = null; + + private val _root: LinearLayout; + + private val _taskLoadMerchandise = TaskHandler>(StateApp.instance.scopeGetter, { url -> + val client = ManagedHttpClient(); + val result = client.get("https://storecache.grayjay.app/StoreData?url=$url") + if (!result.isOk) { + throw Exception("Failed to retrieve store data."); + } + + return@TaskHandler result.body?.let { Json.decodeFromString>(it.string()); } ?: listOf(); + }) + .success { setMerchandise(it) } + .exception { + Logger.w(TAG, "Failed to load merchandise profile.", it); + }; + + val onSupportTap = Event0(); + val onStoreTap = Event0(); + + constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { + inflate(context, R.layout.view_monetization, this); + _buttonSupport = findViewById(R.id.button_support); + _buttonStore = findViewById(R.id.button_store); + _buttonMembership = findViewById(R.id.button_membership); + _platformIndicator = findViewById(R.id.platform_indicator); + + _textMerchandise = findViewById(R.id.text_merchandise); + _recyclerMerchandise = findViewById(R.id.recycler_merchandise); + _loaderMerchandise = findViewById(R.id.loader_merchandise); + _layoutMerchandise = findViewById(R.id.layout_merchandise); + + _root = findViewById(R.id.root); + + _recyclerMerchandise.addItemDecoration(HorizontalSpaceItemDecoration(30, 16, 30)) + _merchandiseAdapterView = _recyclerMerchandise.asAny(orientation = RecyclerView.HORIZONTAL); + + _buttonSupport.setOnClickListener { onSupportTap.emit(); } + _buttonStore.setOnClickListener { onStoreTap.emit(); } + _buttonMembership.visibility = View.GONE; + setMerchandise(null); + } + + fun setPlatformMembership() { + //TODO: + } + + private fun setMerchandise(items: List?) { + _loaderMerchandise.stop(); + + if (items == null) { + _textMerchandise.visibility = View.GONE; + _recyclerMerchandise.visibility = View.GONE; + _layoutMerchandise.visibility = View.GONE; + } else { + _textMerchandise.visibility = View.VISIBLE; + _recyclerMerchandise.visibility = View.VISIBLE; + _layoutMerchandise.visibility = View.VISIBLE; + _merchandiseAdapterView?.adapter?.setData(items.shuffled()); + } + } + + fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { + val profile = cachedPolycentricProfile?.profile; + if (profile != null) { + if (profile.systemState.store.isNotEmpty()) { + _buttonStore.visibility = View.VISIBLE; + } else { + _buttonStore.visibility = View.GONE; + } + + _root.visibility = View.VISIBLE; + } else { + _root.visibility = View.GONE; + } + + setMerchandise(null); + val storeData = profile?.systemState?.storeData; + if (storeData != null) { + try { + val storeItems = Json.decodeFromString>(storeData); + setMerchandise(storeItems); + } catch (_: Throwable) { + try { + val uri = Uri.parse(storeData); + if (uri.isAbsolute) { + _taskLoadMerchandise.run(storeData); + _loaderMerchandise.start(); + } else { + Logger.i(TAG, "Merchandise not loaded, not URL nor JSON") + } + } catch (_: Throwable) { + + } + } + } + } + + companion object { + const val TAG = "MonetizationView"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/SupportView.kt b/app/src/main/java/com/futo/platformplayer/views/SupportView.kt new file mode 100644 index 00000000..0da86a64 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/SupportView.kt @@ -0,0 +1,243 @@ +package com.futo.platformplayer.views + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.net.Uri +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +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.dp +import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile +import com.futo.platformplayer.images.GlideHelper.Companion.crossfade +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.polycentric.PolycentricCache +import com.futo.platformplayer.views.buttons.BigButton +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 +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import userpackage.Protocol.ImageManifest + +class SupportView : LinearLayout { + private val _layoutStore: LinearLayout + private val _buttonPromotion: BigButton + private val _layoutMemberships: LinearLayout + private val _layoutMembershipEntries: LinearLayout + private val _layoutPromotions: LinearLayout + private val _layoutPromotionEntries: LinearLayout + private val _layoutDonation: LinearLayout + private val _layoutDonationEntries: LinearLayout + private val _buttonStore: BigButton + private val _imagePromotion: ShapeableImageView + private var _textNoSupportOptionsSet: TextView + private var _polycentricProfile: PolycentricProfile? = null + + constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { + inflate(context, R.layout.view_support, this); + + _layoutStore = findViewById(R.id.layout_store) + _buttonStore = findViewById(R.id.button_store) + _layoutMemberships = findViewById(R.id.layout_memberships) + _layoutMembershipEntries = findViewById(R.id.layout_membership_entries) + _layoutPromotions = findViewById(R.id.layout_promotions) + _layoutPromotionEntries = findViewById(R.id.layout_promotion_entries) + _layoutDonation = findViewById(R.id.layout_donation) + _layoutDonationEntries = findViewById(R.id.layout_donation_entries) + _buttonPromotion = findViewById(R.id.button_promotion) + _imagePromotion = findViewById(R.id.image_promotion) + _textNoSupportOptionsSet = findViewById(R.id.text_no_support_options_set) + + _buttonPromotion.onClick.subscribe { openPromotion() } + _imagePromotion.setOnClickListener { openPromotion() } + _buttonStore.onClick.subscribe { + val storeUrl = _polycentricProfile?.systemState?.store ?: return@subscribe + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(storeUrl)) + context.startActivity(browserIntent) + } + } + + private fun openPromotion() { + val promotionUrl = _polycentricProfile?.systemState?.promotion ?: return + val uri = Uri.parse(promotionUrl) + if (!uri.isAbsolute && (uri.scheme == "https" || uri.scheme == "http")) { + return + } + + val browserIntent = Intent(Intent.ACTION_VIEW, uri) + context.startActivity(browserIntent) + } + + private fun setMemberships(urls: List) { + _layoutMembershipEntries.removeAllViews() + for (url in urls) { + val button = createMembershipButton(url) + _layoutMembershipEntries.addView(button) + } + _layoutMemberships.visibility = if (urls.isEmpty()) View.GONE else View.VISIBLE + } + + private fun createMembershipButton(url: String): BigButton { + val uri = Uri.parse(url) + val name: String + val iconDrawableId: Int + + if (uri.host?.contains("patreon.com") == true) { + name = "Patreon" + iconDrawableId = R.drawable.patreon + } else { + name = uri.host.toString() + iconDrawableId = R.drawable.ic_web_white + } + + return BigButton(context, name, "Become a member on $name", iconDrawableId) { + val intent = Intent(Intent.ACTION_VIEW); + intent.data = uri; + context.startActivity(intent); + }.apply { + layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + }; + } + + private fun setDonations(destinations: List) { + _layoutDonationEntries.removeAllViews() + for (destination in destinations) { + val button = createDonationButton(destination) + _layoutDonationEntries.addView(button) + } + _layoutDonation.visibility = if (destinations.isEmpty()) View.GONE else View.VISIBLE + } + + private enum class CryptoType { + BITCOIN, ETHEREUM, LITECOIN, RIPPLE, UNKNOWN + } + + private fun getCryptoType(address: String): CryptoType { + val btcRegex = Regex("^(1|3)[1-9A-HJ-NP-Za-km-z]{25,34}$|^(bc1)[0-9a-zA-HJ-NP-Z]{39,59}$") + val ethRegex = Regex("^(0x)[0-9a-fA-F]{40}$") + val ltcRegex = Regex("^(L|M)[1-9A-HJ-NP-Za-km-z]{26,33}$|^(ltc1)[0-9a-zA-HJ-NP-Z]{39,59}$") + val xrpRegex = Regex("^r[1-9A-HJ-NP-Za-km-z]{24,34}$") + + return when { + ltcRegex.matches(address) -> CryptoType.LITECOIN + btcRegex.matches(address) -> CryptoType.BITCOIN + ethRegex.matches(address) -> CryptoType.ETHEREUM + xrpRegex.matches(address) -> CryptoType.RIPPLE + else -> CryptoType.UNKNOWN + } + } + + private fun createDonationButton(destination: String): BigButton { + val uri = Uri.parse(destination) + + var action: (() -> Unit)? = null + val (name, iconDrawableId, cryptoType) = if (uri.scheme == "http" || uri.scheme == "https") { + val hostName = uri.host ?: "" + + action = { + val intent = Intent(Intent.ACTION_VIEW); + intent.data = uri; + context.startActivity(intent); + } + + if (hostName.contains("paypal.com")) { + Triple("Paypal", R.drawable.paypal, null) // Replace with your actual PayPal drawable resource + } else { + Triple(hostName, R.drawable.ic_web_white, null) // Replace with your generic web drawable resource + } + } else { + action = { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Donation Address", destination) + clipboard.setPrimaryClip(clip) + Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show() + } + + when (getCryptoType(destination)) { + CryptoType.BITCOIN -> Triple("Bitcoin", R.drawable.bitcoin, CryptoType.BITCOIN) + CryptoType.ETHEREUM -> Triple("Ethereum", R.drawable.ethereum, CryptoType.ETHEREUM) + CryptoType.LITECOIN -> Triple("Litecoin", R.drawable.litecoin, CryptoType.LITECOIN) + CryptoType.RIPPLE -> Triple("Ripple", R.drawable.ripple, CryptoType.RIPPLE) + CryptoType.UNKNOWN -> Triple("Unknown", R.drawable.ic_paid, CryptoType.UNKNOWN) + } + } + + return BigButton(context, name, destination.takeIf { cryptoType != null } ?: "Donate on $name", iconDrawableId, action).apply { + layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + }; + } + + private fun setPromotions(url: String?, imageUrl: String?) { + Logger.i(TAG, "setPromotions($url, $imageUrl)") + + if (url != null) { + _layoutPromotions.visibility = View.VISIBLE + + if (imageUrl != null) { + _buttonPromotion.visibility = View.GONE + _imagePromotion.visibility = View.VISIBLE + + Glide.with(_imagePromotion) + .load(imageUrl) + .crossfade() + .into(_imagePromotion) + } else { + _buttonPromotion.setSecondaryText(url) + _buttonPromotion.visibility = View.VISIBLE + _imagePromotion.visibility = View.GONE + } + } else { + _layoutPromotions.visibility = View.GONE + } + } + + fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) { + if (_polycentricProfile == profile) { + return + } + + if (profile != null) { + setDonations(profile.systemState.donationDestinations); + setMemberships(profile.systemState.membershipUrls); + + val imageManifest = profile.systemState.promotionBanner?.imageManifestsList?.firstOrNull() + if (imageManifest != null) { + val imageUrl = imageManifest.toURLInfoSystemLinkUrl(profile.system.toProto(), imageManifest.process, profile.systemState.servers.toList()); + setPromotions(profile.systemState.promotion, imageUrl); + } else { + setPromotions(null, null); + } + + if (profile.systemState.store.isNotEmpty()) { + _layoutStore.visibility = View.VISIBLE + } else { + _layoutStore.visibility = View.GONE + } + + _textNoSupportOptionsSet.visibility = View.GONE + } else { + setDonations(listOf()); + setMemberships(listOf()); + setPromotions(null, null); + _layoutStore.visibility = View.GONE + _textNoSupportOptionsSet.visibility = View.VISIBLE + } + + _polycentricProfile = profile + } + + companion object { + const val TAG = "SupportView"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/StoreItemViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/StoreItemViewHolder.kt new file mode 100644 index 00000000..47c97acd --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/StoreItemViewHolder.kt @@ -0,0 +1,55 @@ +package com.futo.platformplayer.views.adapters.viewholders + +import android.content.Intent +import android.net.Uri +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +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.logging.Logger +import com.futo.platformplayer.views.StoreItem +import com.futo.platformplayer.views.adapters.AnyAdapter +import com.google.android.material.imageview.ShapeableImageView + +class StoreItemViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder( + LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_store_item, _viewGroup, false)) { + + private val _image: ShapeableImageView; + private val _name: TextView; + private var _storeItem: StoreItem? = null; + + init { + _image = _view.findViewById(R.id.image_item); + _name = _view.findViewById(R.id.text_item); + _view.findViewById(R.id.root).setOnClickListener { + val s = _storeItem ?: return@setOnClickListener; + + try { + val uri = Uri.parse(s.url); + val intent = Intent(Intent.ACTION_VIEW); + intent.data = uri; + _view.context.startActivity(intent); + } catch (e: Throwable) { + Logger.e(TAG, "Failed to open URI: '${it}'.", e); + } + } + } + + override fun bind(storeItem: StoreItem) { + Glide.with(_image) + .load(storeItem.image) + .crossfade() + .into(_image); + + _name.text = storeItem.name; + _storeItem = storeItem; + } + + companion object { + private const val TAG = "StoreItemViewHolder"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/buttons/BigButton.kt b/app/src/main/java/com/futo/platformplayer/views/buttons/BigButton.kt index 54307936..66d0b3bb 100644 --- a/app/src/main/java/com/futo/platformplayer/views/buttons/BigButton.kt +++ b/app/src/main/java/com/futo/platformplayer/views/buttons/BigButton.kt @@ -69,6 +69,10 @@ open class BigButton : LinearLayout { _textSecondary.text = attrTextSecondary; } + fun setSecondaryText(text: String?) { + _textSecondary.text = text + } + fun withPrimaryText(text: String): BigButton { _textPrimary.text = text; return this; diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/SupportOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/SupportOverlay.kt new file mode 100644 index 00000000..d43bddd4 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/SupportOverlay.kt @@ -0,0 +1,33 @@ +package com.futo.platformplayer.views.overlays + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import com.futo.platformplayer.R +import com.futo.platformplayer.constructs.Event0 +import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile +import com.futo.platformplayer.views.SupportView + +class SupportOverlay : LinearLayout { + val onClose = Event0(); + + private val _topbar: OverlayTopbar; + private val _support: SupportView; + + constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { + inflate(context, R.layout.overlay_support, this) + _topbar = findViewById(R.id.topbar); + _support = findViewById(R.id.support); + + _topbar.onClose.subscribe(this, onClose::emit); + } + + + fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) { + _support.setPolycentricProfile(profile, animate) + } + + fun cleanup() { + _topbar.onClose.remove(this); + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/background_membership.xml b/app/src/main/res/drawable/background_membership.xml new file mode 100644 index 00000000..6b33ecc3 --- /dev/null +++ b/app/src/main/res/drawable/background_membership.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_store.xml b/app/src/main/res/drawable/background_store.xml index d7cddcfc..3ef92416 100644 --- a/app/src/main/res/drawable/background_store.xml +++ b/app/src/main/res/drawable/background_store.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_support.xml b/app/src/main/res/drawable/background_support.xml index 5f5d3e63..4e39889b 100644 --- a/app/src/main/res/drawable/background_support.xml +++ b/app/src/main/res/drawable/background_support.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/bitcoin.xml b/app/src/main/res/drawable/bitcoin.xml new file mode 100644 index 00000000..c4472550 --- /dev/null +++ b/app/src/main/res/drawable/bitcoin.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/divider_transparent_4dp.xml b/app/src/main/res/drawable/divider_transparent_4dp.xml new file mode 100644 index 00000000..922d532c --- /dev/null +++ b/app/src/main/res/drawable/divider_transparent_4dp.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/divider_transparent_8dp.xml b/app/src/main/res/drawable/divider_transparent_8dp.xml new file mode 100644 index 00000000..2179f2a6 --- /dev/null +++ b/app/src/main/res/drawable/divider_transparent_8dp.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/divider_transparent_vertical_20dp.xml b/app/src/main/res/drawable/divider_transparent_vertical_20dp.xml new file mode 100644 index 00000000..16b9e007 --- /dev/null +++ b/app/src/main/res/drawable/divider_transparent_vertical_20dp.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/divider_transparent_vertical_8dp.xml b/app/src/main/res/drawable/divider_transparent_vertical_8dp.xml new file mode 100644 index 00000000..acbe718d --- /dev/null +++ b/app/src/main/res/drawable/divider_transparent_vertical_8dp.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ethereum.xml b/app/src/main/res/drawable/ethereum.xml new file mode 100644 index 00000000..bb3fbc4b --- /dev/null +++ b/app/src/main/res/drawable/ethereum.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bug.xml b/app/src/main/res/drawable/ic_bug.xml index 285030ed..bf600b76 100644 --- a/app/src/main/res/drawable/ic_bug.xml +++ b/app/src/main/res/drawable/ic_bug.xml @@ -2,8 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="960" - android:viewportHeight="960" - android:tint="?attr/colorControlNormal"> + android:viewportHeight="960"> diff --git a/app/src/main/res/drawable/ic_live_tv.xml b/app/src/main/res/drawable/ic_live_tv.xml index 560d1cb7..20523d23 100644 --- a/app/src/main/res/drawable/ic_live_tv.xml +++ b/app/src/main/res/drawable/ic_live_tv.xml @@ -2,8 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="960" - android:viewportHeight="960" - android:tint="?attr/colorControlNormal"> + android:viewportHeight="960"> diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml index 77461925..b5ac7872 100644 --- a/app/src/main/res/drawable/ic_notifications.xml +++ b/app/src/main/res/drawable/ic_notifications.xml @@ -2,8 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="960" - android:viewportHeight="960" - android:tint="?attr/colorControlNormal"> + android:viewportHeight="960"> diff --git a/app/src/main/res/drawable/ic_star.xml b/app/src/main/res/drawable/ic_star.xml new file mode 100644 index 00000000..e98e1d67 --- /dev/null +++ b/app/src/main/res/drawable/ic_star.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/litecoin.xml b/app/src/main/res/drawable/litecoin.xml new file mode 100644 index 00000000..7c010ea5 --- /dev/null +++ b/app/src/main/res/drawable/litecoin.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/unstable/res/drawable/logo_subscribestar.png b/app/src/main/res/drawable/logo_subscribestar.png similarity index 100% rename from app/src/unstable/res/drawable/logo_subscribestar.png rename to app/src/main/res/drawable/logo_subscribestar.png diff --git a/app/src/main/res/drawable/patreon.xml b/app/src/main/res/drawable/patreon.xml new file mode 100644 index 00000000..050325a7 --- /dev/null +++ b/app/src/main/res/drawable/patreon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/paypal.xml b/app/src/main/res/drawable/paypal.xml new file mode 100644 index 00000000..1695cd63 --- /dev/null +++ b/app/src/main/res/drawable/paypal.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ripple.xml b/app/src/main/res/drawable/ripple.xml new file mode 100644 index 00000000..5d7dc10a --- /dev/null +++ b/app/src/main/res/drawable/ripple.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_channel_monetization.xml b/app/src/main/res/layout/fragment_channel_monetization.xml index 8ada72cf..0394cc7d 100644 --- a/app/src/main/res/layout/fragment_channel_monetization.xml +++ b/app/src/main/res/layout/fragment_channel_monetization.xml @@ -1,92 +1,10 @@ - + android:layout_height="match_parent"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + android:layout_height="match_parent" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/fragview_video_detail.xml b/app/src/main/res/layout/fragview_video_detail.xml index be40a864..607de6ed 100644 --- a/app/src/main/res/layout/fragview_video_detail.xml +++ b/app/src/main/res/layout/fragview_video_detail.xml @@ -445,65 +445,10 @@ android:text="@string/click_to_read_more"/> - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" /> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_monetization.xml b/app/src/main/res/layout/view_monetization.xml new file mode 100644 index 00000000..76d9fd82 --- /dev/null +++ b/app/src/main/res/layout/view_monetization.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_store_item.xml b/app/src/main/res/layout/view_store_item.xml new file mode 100644 index 00000000..4db7f99a --- /dev/null +++ b/app/src/main/res/layout/view_store_item.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/app/src/main/res/layout/view_support.xml b/app/src/main/res/layout/view_support.xml new file mode 100644 index 00000000..392aefe9 --- /dev/null +++ b/app/src/main/res/layout/view_support.xml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dee4fc7d..adf199dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -79,6 +79,7 @@ Developer Remove historical suggestion Comments + Merchandise Reached the end of the playlist The playlist will restart after the video is finished Restart Now @@ -192,15 +193,19 @@ I Already Paid Memberships A monthly recurring payment with often - additional perks. + additional perks A one-time payment to support the creator + A store by the creator Donation + Promotions + Current promotions by this creator Downloading Videos Clear history Nothing to import Enabling lots of sources can reduce the loading speed of your application. Support + Membership Store Live Chat Remove @@ -625,6 +630,7 @@ Select your pins in order More Options Save + This creator has not set any support options on Harbor (Polycentric) Recommendations Subscriptions diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 37ccde21..d6dd1850 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -11,6 +11,10 @@ rounded 4dp +