Compare commits

...

12 Commits

44 changed files with 559 additions and 204 deletions
@@ -35,4 +35,8 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
fun Protocol.Claim.resolveChannelUrl(): String? {
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
}
fun Protocol.Claim.resolveChannelUrls(): List<String> {
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
}
@@ -22,6 +22,7 @@ import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormFieldButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -45,8 +46,9 @@ class Settings : FragmentedStorageFileJson() {
@FormField(
"Manage Polycentric identity", FieldForm.BUTTON,
"Manage your Polycentric identity", -3
"Manage your Polycentric identity", -4
)
@FormFieldButton(R.drawable.ic_person)
fun managePolycentricIdentity() {
SettingsActivity.getActivity()?.let {
if (StatePolycentric.instance.processHandle != null) {
@@ -58,12 +60,26 @@ class Settings : FragmentedStorageFileJson() {
}
@FormField(
"Open FAQ", FieldForm.BUTTON,
"Get answers to common questions", -2
"Show FAQ", FieldForm.BUTTON,
"Get answers to common questions", -3
)
@FormFieldButton(R.drawable.ic_quiz)
fun openFAQ() {
try {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://grayjay.app/faq.html"))
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ))
SettingsActivity.getActivity()?.startActivity(browserIntent);
} catch (e: Throwable) {
//Ignored
}
}
@FormField(
"Show Issues", FieldForm.BUTTON,
"A list of user-reported and self-reported issues", -2
)
@FormFieldButton(R.drawable.ic_data_alert)
fun openIssues() {
try {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues"))
SettingsActivity.getActivity()?.startActivity(browserIntent);
} catch (e: Throwable) {
//Ignored
@@ -74,6 +90,7 @@ class Settings : FragmentedStorageFileJson() {
"Submit feedback", FieldForm.BUTTON,
"Give feedback on the application", -1
)
@FormFieldButton(R.drawable.ic_bug)
fun submitFeedback() {
try {
val i = Intent(Intent.ACTION_VIEW);
@@ -93,6 +110,7 @@ class Settings : FragmentedStorageFileJson() {
"Manage Tabs", FieldForm.BUTTON,
"Change tabs visible on the home screen", -1
)
@FormFieldButton(R.drawable.ic_tabs)
fun manageTabs() {
try {
SettingsActivity.getActivity()?.let {
@@ -615,6 +633,7 @@ class Settings : FragmentedStorageFileJson() {
companion object {
private const val TAG = "Settings";
const val URL_FAQ = "https://grayjay.app/faq.html";
private var _isFirst = true;
@@ -17,6 +17,7 @@ import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.fields.FieldForm
@@ -272,6 +273,15 @@ class SettingsDev : FragmentedStorageFileJson() {
@FormField("Other", FieldForm.GROUP, "Others...", 5)
val otherTests: OtherTests = OtherTests();
class OtherTests {
@FormField("Unsubscribe all", FieldForm.BUTTON, "Removes all subscriptions", -1)
fun unsubscribeAll() {
val toUnsub = StateSubscriptions.instance.getSubscriptions();
UIDialogs.toast("Started unsubbing.. (${toUnsub.size})")
toUnsub.forEach {
StateSubscriptions.instance.removeSubscription(it.channel.url);
};
UIDialogs.toast("Finished unsubbing.. (${toUnsub.size})")
}
@FormField("Clear Downloads", FieldForm.BUTTON, "Deletes all ongoing downloads", 1)
fun clearDownloads() {
StateDownloads.instance.getDownloading().forEach {
@@ -304,7 +304,7 @@ class UISlideOverlays {
return overlay;
}
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, onVideoHidden: (()->Unit)? = null): SlideUpMenuOverlay {
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, vararg actions: SlideUpMenuItem): SlideUpMenuOverlay {
val items = arrayListOf<View>();
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
@@ -323,11 +323,11 @@ class UISlideOverlays {
val queue = StatePlayer.instance.getQueue();
val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, "Actions", "actions",
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, "Hide", "Hide from Home", "hide",
{ StateMeta.instance.addHiddenVideo(video.url); onVideoHidden?.invoke() }),
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
{ showDownloadVideoOverlay(video, container, true); }, false)
))
(listOf(
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
{ showDownloadVideoOverlay(video, container, true); }, false))
+ actions)
));
items.add(
SlideUpMenuGroup(container.context, "Add To", "addto",
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, "Add to Queue", "${queue.size} videos", "queue",
@@ -568,6 +568,23 @@ open class JSClient : IPlatformClient {
};
}
fun resolveChannelUrlsByClaimTemplates(claimType: Int, values: Map<Int, String>): List<String> {
val urls = arrayListOf<String>();
channelClaimTemplates?.let {
if(it.containsKey(claimType)) {
val templates = it[claimType];
if(templates != null)
for(value in values.keys.sortedBy { it }) {
if(templates.containsKey(value)) {
urls.add(templates[value]!!.replace("{{CLAIMVALUE}}", values[value]!!));
}
}
}
};
return urls;
}
private fun <T> isBusyWith(handle: ()->T): T {
try {
@@ -75,7 +75,12 @@ class DedupContentPager : IPager<IPlatformContent>, IAsyncPager<IPlatformContent
return toReturn;
}
private fun isSameItem(item: IPlatformContent, item2: IPlatformContent): Boolean {
return item.name == item2.name && (item.datetime == null || item2.datetime == null || abs(item.datetime!!.getDiffDays(item2.datetime!!)) < 2);
//return item == item2;
val daysAgo = Math.abs(item.datetime?.getNowDiffDays() ?: return false);
val maxDelta = Math.max(2, (daysAgo / 1.5).toInt()); //TODO: Better scaling delta
val isSame = item.name.equals(item2.name, true) && (item.datetime == null || item2.datetime == null || abs(item.datetime!!.getDiffDays(item2.datetime!!)) < maxDelta);
return isSame;
}
private fun calculateHash(item: IPlatformContent): Int {
return combineHashCodes(listOf(item.name.hashCode(), item.datetime?.hashCode()));
@@ -8,6 +8,7 @@ import java.util.stream.IntStream
*/
class MultiChronoContentPager : MultiPager<IPlatformContent> {
constructor(pagers : Array<IPager<IPlatformContent>>, allowFailure: Boolean = false, pageSize: Int = 9) : super(pagers.map { it }.toList(), allowFailure, pageSize) {}
constructor(pagers : List<IPager<IPlatformContent>>, allowFailure: Boolean = false, pageSize: Int = 9) : super(pagers, allowFailure, pageSize) {}
@Synchronized
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
@@ -0,0 +1,33 @@
package com.futo.platformplayer.api.media.structures
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import kotlinx.coroutines.runBlocking
import java.util.stream.IntStream
/**
* A Content AsyncMultiPager that returns results based on a specified distribution
* Unlike its non-async counterpart, this one uses parallel nextPage requests
*/
class MultiChronoContentParallelPager : MultiParallelPager<IPlatformContent> {
constructor(pagers: List<IPager<IPlatformContent>>) : super(pagers)
@Synchronized
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
if(options.size == 0)
return -1;
var bestIndex = 0;
val allResults = runBlocking { options.map { Pair(it, it.item?.await()) } };
for(i in IntStream.range(1, options.size)) {
val best = allResults[bestIndex].second;
val cur = allResults[i].second ?: continue;
if(best?.datetime == null || (cur.datetime != null && cur.datetime!! > best.datetime!!))
bestIndex = i;
}
return bestIndex;
}
}
@@ -66,25 +66,25 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
override fun getResults(): List<T> = synchronized(_pagersReusable){ _currentPager.getResults() };
private fun updatePager(pagerToAdd: IPager<T>?, toReplacePager: IPager<T>? = null, error: Throwable? = null) {
if(pagerToAdd == null) {
if(toReplacePager != null && toReplacePager is PlaceholderPager && error != null) {
val pluginId = toReplacePager.placeholderFactory.invoke().id?.pluginId ?: "";
_pagersReusable.add((PlaceholderPager(5) {
return@PlaceholderPager PlatformContentPlaceholder(pluginId, error)
} as IPager<T>).asReusable());
_currentPager = recreatePager(getCurrentSubPagers());
if(_currentPager is MultiParallelPager<*>)
runBlocking { (_currentPager as MultiParallelPager).initialize(); };
else if(_currentPager is MultiPager<*>)
(_currentPager as MultiPager).initialize()
onPagerChanged.emit(_currentPager);
}
return;
}
synchronized(_pagersReusable) {
if(pagerToAdd == null) {
if(toReplacePager != null && toReplacePager is PlaceholderPager && error != null) {
val pluginId = toReplacePager.placeholderFactory.invoke().id?.pluginId ?: "";
_pagersReusable.add((PlaceholderPager(5) {
return@PlaceholderPager PlatformContentPlaceholder(pluginId, error)
} as IPager<T>).asReusable());
_currentPager = recreatePager(getCurrentSubPagers());
if(_currentPager is MultiParallelPager<*>)
runBlocking { (_currentPager as MultiParallelPager).initialize(); };
else if(_currentPager is MultiPager<*>)
(_currentPager as MultiPager).initialize()
onPagerChanged.emit(_currentPager);
}
return;
}
Logger.i("RefreshMultiDistributionContentPager", "Received new pager for RefreshPager")
_pagersReusable.add(pagerToAdd.asReusable());
@@ -0,0 +1,19 @@
package com.futo.platformplayer.api.media.structures
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import kotlinx.coroutines.Deferred
/**
* A RefreshMultiPager that simply returns all respective pagers in equal distribution, optionally inserting PlaceholderPager results as provided for their respective promised pagers
* (Eg. Pager A is completed, Pager [B,C,D] are promised/deferred. placeholderPagers [1,2,3] will map B=>1, C=>2, D=>3 until promised pagers are completed)
* Uses wrapped MultiDistributionContentAsyncPager for inidivual pagers.
*/
class RefreshChronoContentPager(pagers: List<IPager<IPlatformContent>>, pendingPagers: List<Deferred<IPager<IPlatformContent>?>>, placeholderPagers: List<IPager<IPlatformContent>>? = null)
: MultiRefreshPager<IPlatformContent>(pagers, pendingPagers, placeholderPagers) {
override fun recreatePager(pagers: List<IPager<IPlatformContent>>): IPager<IPlatformContent> {
return MultiChronoContentPager(pagers);
//return MultiChronoContentParallelPager(pagers);
//return MultiDistributionContentPager(pagers.associateWith { 1f });
}
}
@@ -43,6 +43,7 @@ class SingleAsyncItemPager<T> {
if (_currentResultPos >= _requestedPageItems.size) {
val startPos = fillDeferredUntil(_currentResultPos);
if(!_pager.hasMorePages()) {
Logger.i("SingleAsyncItemPager", "end of async page reached");
completeRemainder { it?.complete(null) };
}
if(_isRequesting)
@@ -12,6 +12,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.builders.DashBuilder
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.CastingDeviceInfo
import com.futo.platformplayer.states.StateApp
@@ -352,16 +353,25 @@ class StateCasting {
}
}
} else {
if (videoSource is IVideoUrlSource) {
if (videoSource is IVideoUrlSource)
ad.loadVideo("BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble());
} else if (audioSource is IAudioUrlSource) {
else if(videoSource is IHLSManifestSource)
ad.loadVideo("BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble());
else if (audioSource is IAudioUrlSource)
ad.loadVideo("BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble());
} else if (videoSource is LocalVideoSource) {
else if(audioSource is IHLSManifestAudioSource)
ad.loadVideo("BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble());
else if (videoSource is LocalVideoSource)
castLocalVideo(video, videoSource, resumePosition);
} else if (audioSource is LocalAudioSource) {
else if (audioSource is LocalAudioSource)
castLocalAudio(video, audioSource, resumePosition);
} else {
throw Exception("Unhandled source type videoSource=$videoSource audioSource=$audioSource subtitleSource=$subtitleSource");
else {
var str = listOf(
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null,
if(subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null
).filterNotNull().joinToString(", ");
throw UnsupportedCastException(str);
}
}
@@ -0,0 +1,6 @@
package com.futo.platformplayer.exceptions
import java.lang.Exception
class UnsupportedCastException(msg: String) : Exception(msg) {
}
@@ -248,7 +248,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
if(_pager is IReplacerPager<*>)
(_pager as IReplacerPager<*>).onReplaced.remove(this);
if(pager is IReplacerPager<*>) {
pager.onReplaced.subscribe(this) { oldItem, newItem ->
if(_pager != pager)
@@ -257,11 +256,14 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
if(_pager !is IPager<IPlatformContent>)
return@subscribe;
val toReplaceIndex = _results.indexOfFirst { it == newItem };
if(toReplaceIndex >= 0) {
_results[toReplaceIndex] = newItem as IPlatformContent;
_adapterResults?.let {
it.notifyItemChanged(it.childToParentPosition(toReplaceIndex));
lifecycleScope.launch(Dispatchers.Main) {
val toReplaceIndex = _results.indexOfFirst { it == oldItem };
if (toReplaceIndex >= 0) {
_results[toReplaceIndex] = newItem as IPlatformContent;
_adapterResults?.let {
it.notifyItemChanged(it.childToParentPosition(toReplaceIndex));
}
}
}
}
@@ -220,6 +220,13 @@ class MenuBottomBarFragment : MainActivityFragment() {
buttons.removeAt(buyIndex)
buttons.add(0, button)
}
//Force faq to be second
val faqIndex = buttons.indexOfFirst { b -> b.id == 97 };
if (faqIndex != -1) {
val button = buttons[faqIndex]
buttons.removeAt(faqIndex)
buttons.add(1, button)
}
for (data in buttons) {
val button = MenuButton(context, data, _fragment, true);
@@ -289,6 +296,9 @@ class MenuBottomBarFragment : MainActivityFragment() {
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>() }))
}
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.faq, canToggle = false, { false }, {
it.navigate<BrowserFragment>(Settings.URL_FAQ);
}))
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
@@ -246,28 +246,45 @@ class ChannelFragment : MainFragment() {
if (parameter is String) {
_buttonSubscribe.setSubscribeChannel(parameter);
_textChannel.text = "";
_textChannelSub.text = "";
setPolycentricProfileOr(parameter) {
_textChannel.text = "";
_textChannelSub.text = "";
_creatorThumbnail.setThumbnail(null, true);
Glide.with(_imageBanner)
.clear(_imageBanner);
};
_url = parameter;
loadChannel();
} else if (parameter is SerializedChannel) {
showChannel(parameter);
_url = parameter.url;
_creatorThumbnail.setThumbnail(parameter.url, false);
loadChannel();
} else if (parameter is IPlatformChannel)
showChannel(parameter);
else if (parameter is PlatformAuthorLink) {
_textChannel.text = parameter.name;
_textChannelSub.text = "";
_creatorThumbnail.setThumbnail(parameter.url, false);
setPolycentricProfileOr(parameter.url) {
_textChannel.text = parameter.name;
_textChannelSub.text = "";
_creatorThumbnail.setThumbnail(parameter.thumbnail, true);
Glide.with(_imageBanner)
.clear(_imageBanner);
_taskLoadPolycentricProfile.run(parameter.id);
};
_url = parameter.url;
loadChannel();
} else if (parameter is Subscription) {
_textChannel.text = parameter.channel.name;
_textChannelSub.text = "";
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, false);
setPolycentricProfileOr(parameter.channel.url) {
_textChannel.text = parameter.channel.name;
_textChannelSub.text = "";
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true);
Glide.with(_imageBanner)
.clear(_imageBanner);
_taskLoadPolycentricProfile.run(parameter.channel.id);
};
_url = parameter.channel.url;
loadChannel();
@@ -360,15 +377,8 @@ class ChannelFragment : MainFragment() {
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons);
_buttonSubscribe.setSubscribeChannel(channel);
_textChannel.text = channel.name;
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else "";
_creatorThumbnail.setThumbnail(channel.thumbnail, true);
Glide.with(_imageBanner)
.load(channel.banner)
.crossfade()
.into(_imageBanner)
//TODO: Find a better way to access the adapter fragments..
(_viewPager.adapter as ChannelViewPagerAdapter?)?.let {
@@ -381,51 +391,68 @@ class ChannelFragment : MainFragment() {
this.channel = channel;
val cachedProfile = PolycentricCache.instance.getCachedProfile(channel.url);
setPolycentricProfileOr(channel.url) {
_textChannel.text = channel.name;
_creatorThumbnail.setThumbnail(channel.thumbnail, true);
Glide.with(_imageBanner)
.load(channel.banner)
.crossfade()
.into(_imageBanner);
_taskLoadPolycentricProfile.run(channel.id);
};
}
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(it.url) };
if (cachedProfile != null) {
setPolycentricProfile(cachedProfile, animate = false);
} else {
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(channel.id);
or();
}
}
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
Log.i(TAG, "setPolycentricProfile(cachedPolycentricProfile = $cachedPolycentricProfile, animate = $animate)")
val polycentricProfile = cachedPolycentricProfile?.profile;
if (polycentricProfile != null) {
_fragment.topBar?.onShown(polycentricProfile);
val dp_35 = 35.dp(resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (polycentricProfile.systemState.username.isNotBlank())
_textChannel.text = polycentricProfile.systemState.username;
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
val dp_35 = 35.dp(resources)
val avatar = polycentricProfile.systemState.avatar?.selectBestImage(dp_35 * dp_35)
?.let { it.toURLInfoSystemLinkUrl(polycentricProfile.system.toProto(), it.process, polycentricProfile.systemState.servers.toList()) };
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, true);
} else {
_creatorThumbnail.setHarborAvailable(true, true);
}
if (banner != null) {
Glide.with(_imageBanner)
.load(banner)
.crossfade()
.into(_imageBanner);
} else {
Glide.with(_imageBanner)
.load(channel?.banner)
.crossfade()
.into(_imageBanner);
}
val banner = polycentricProfile.systemState.banner?.selectHighestResolutionImage()
?.let { it.toURLInfoSystemLinkUrl(polycentricProfile.system.toProto(), it.process, polycentricProfile.systemState.servers.toList()) };
if (banner != null) {
Glide.with(_imageBanner)
.load(banner)
.crossfade()
.into(_imageBanner);
}
if (profile != null) {
_fragment.topBar?.onShown(profile);
_textChannel.text = profile.systemState.username;
}
(_viewPager.adapter as ChannelViewPagerAdapter?)?.let {
it.getFragment<ChannelAboutFragment>().setPolycentricProfile(polycentricProfile, animate);
it.getFragment<ChannelMonetizationFragment>().setPolycentricProfile(polycentricProfile, animate);
it.getFragment<ChannelListFragment>().setPolycentricProfile(polycentricProfile, animate);
it.getFragment<ChannelContentsFragment>().setPolycentricProfile(polycentricProfile, animate);
it.getFragment<ChannelAboutFragment>().setPolycentricProfile(profile, animate);
it.getFragment<ChannelMonetizationFragment>().setPolycentricProfile(profile, animate);
it.getFragment<ChannelListFragment>().setPolycentricProfile(profile, animate);
it.getFragment<ChannelContentsFragment>().setPolycentricProfile(profile, animate);
//TODO: Call on other tabs as needed
}
}
@@ -15,7 +15,9 @@ import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.PreviewContentListAdapter
@@ -24,6 +26,7 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.adapters.PreviewNestedVideoViewHolder
import com.futo.platformplayer.views.adapters.PreviewVideoViewHolder
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import kotlin.math.floor
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
@@ -69,15 +72,24 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
//TODO: Reconstruct search video from detail if search is null
_overlayContainer.let {
if(content is IPlatformVideo)
UISlideOverlays.showVideoOptionsOverlay(content, it) {
if (fragment is HomeFragment) {
val removeIndex = recyclerData.results.indexOf(content);
if (removeIndex >= 0) {
recyclerData.results.removeAt(removeIndex);
recyclerData.adapter.notifyItemRemoved(recyclerData.adapter.childToParentPosition(removeIndex));
UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(context, R.drawable.ic_visibility_off, "Hide", "Hide from Home", "hide",
{ StateMeta.instance.addHiddenVideo(content.url);
if (fragment is HomeFragment) {
val removeIndex = recyclerData.results.indexOf(content);
if (removeIndex >= 0) {
recyclerData.results.removeAt(removeIndex);
recyclerData.adapter.notifyItemRemoved(recyclerData.adapter.childToParentPosition(removeIndex));
}
}
}
};
}),
SlideUpMenuItem(context, R.drawable.ic_playlist, "Play Feed as Queue", "Play entire feed", "playFeed",
{
val newQueue = listOf(content) + recyclerData.results
.filterIsInstance<IPlatformVideo>()
.filter { it != content };
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue", true, false);
})
);
}
};
adapter.onAddToQueueClicked.subscribe(this) {
@@ -101,14 +101,12 @@ class ContentSearchResultsFragment : MainFragment() {
fun onShown(parameter: Any?, isBack: Boolean) {
if(parameter is SuggestionsFragmentData) {
if(!isBack) {
setQuery(parameter.query, false);
setChannelUrl(parameter.channelUrl, false);
setQuery(parameter.query, false);
setChannelUrl(parameter.channelUrl, false);
fragment.topBar?.apply {
if (this is SearchTopBarFragment) {
this.setText(parameter.query);
}
fragment.topBar?.apply {
if (this is SearchTopBarFragment) {
this.setText(parameter.query);
}
}
}
@@ -71,16 +71,14 @@ class CreatorSearchResultsFragment : MainFragment() {
fun onShown(parameter: Any?, isBack: Boolean) {
if(parameter is String) {
if(!isBack) {
setQuery(parameter);
setQuery(parameter);
fragment.topBar?.apply {
if (this is SearchTopBarFragment) {
setText(parameter);
onSearch.subscribe(this) {
setQuery(it);
};
}
fragment.topBar?.apply {
if (this is SearchTopBarFragment) {
setText(parameter);
onSearch.subscribe(this) {
setQuery(it);
};
}
}
}
@@ -73,16 +73,14 @@ class PlaylistSearchResultsFragment : MainFragment() {
fun onShown(parameter: Any?, isBack: Boolean) {
if(parameter is String) {
if(!isBack) {
setQuery(parameter);
setQuery(parameter);
fragment.topBar?.apply {
if (this is SearchTopBarFragment) {
setText(parameter);
onSearch.subscribe(this) {
setQuery(it);
};
}
fragment.topBar?.apply {
if (this is SearchTopBarFragment) {
setText(parameter);
onSearch.subscribe(this) {
setQuery(it);
};
}
}
}
@@ -62,6 +62,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptAgeException
import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
@@ -870,7 +871,6 @@ class VideoDetailView : ConstraintLayout {
_commentsList.clear();
_platform.setPlatformFromClientID(video.id.pluginId);
_subTitle.text = subTitleSegments.joinToString("");
_channelName.text = video.author.name;
_playWhenReady = true;
if(video.author.subscribers != null) {
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " subscribers" else "";
@@ -897,6 +897,7 @@ class VideoDetailView : ConstraintLayout {
} else {
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(video.author.id);
_channelName.text = video.author.name;
}
_player.clear();
@@ -1254,6 +1255,10 @@ class VideoDetailView : ConstraintLayout {
_lastVideoSource = videoSource;
_lastAudioSource = audioSource;
}
catch(ex: UnsupportedCastException) {
Logger.e(TAG, "Failed to load cast media", ex);
UIDialogs.showGeneralErrorDialog(context, "Unsupported Cast format", ex);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to load media", ex);
UIDialogs.showGeneralErrorDialog(context, "Failed to load media", ex);
@@ -1971,14 +1976,24 @@ class VideoDetailView : ConstraintLayout {
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
_polycentricProfile = cachedPolycentricProfile;
if (cachedPolycentricProfile?.profile == null) {
_layoutMonetization.visibility = View.GONE;
_creatorThumbnail.setHarborAvailable(false, animate);
return;
val dp_35 = 35.dp(context.resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
_layoutMonetization.visibility = View.VISIBLE;
_creatorThumbnail.setHarborAvailable(true, animate);
if (profile != null) {
_channelName.text = cachedPolycentricProfile.profile.systemState.username;
_layoutMonetization.visibility = View.VISIBLE;
} else {
_layoutMonetization.visibility = View.GONE;
}
}
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
@@ -64,12 +64,11 @@ class Logging {
val client = OkHttpClient()
val response: Response = client.newCall(request).execute()
if (response.isSuccessful) {
val body = response.body?.string();
return if (body != null) Json.decodeFromString<String>(body) else null;
return if (response.isSuccessful) {
response.body?.string();
} else {
Logger.e("Failed to submit log.") { "Failed to submit logs (${response.code}): ${response.body?.string()}" };
return null;
null;
}
}
}
@@ -11,5 +11,6 @@ data class Telemetry(
val isUnstableBuild: Boolean,
val brand: String,
val manufacturer: String,
val model: String
val model: String,
val sdkVersion: Int
) { }
@@ -8,6 +8,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.getNowDiffSeconds
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.resolveChannelUrls
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import com.futo.platformplayer.stores.CachedPolycentricProfileStorage
import com.futo.platformplayer.stores.FragmentedStorage
@@ -37,7 +38,8 @@ class PolycentricCache {
ContentType.AVATAR.value,
ContentType.USERNAME.value,
ContentType.DESCRIPTION.value,
ContentType.STORE.value
ContentType.STORE.value,
ContentType.SERVER.value
)
).eventsList.map { e -> SignedEvent.fromProto(e) };
@@ -88,8 +90,9 @@ class PolycentricCache {
if (result.profile != null) {
for (claim in result.profile.ownedClaims) {
val url = claim.claim.resolveChannelUrl() ?: continue;
_profileUrlCache.map[url] = result;
val urls = claim.claim.resolveChannelUrls();
for (url in urls)
_profileUrlCache.map[url] = result;
}
}
@@ -784,6 +784,15 @@ class StatePlatform {
return null;
}
fun resolveChannelUrlsByClaimTemplates(claimType: Int, claimValues: Map<Int, String>): List<String> {
val urls = arrayListOf<String>();
for(client in getClientsByClaimType(claimType).filter { it is JSClient }) {
val res = (client as JSClient).resolveChannelUrlsByClaimTemplates(claimType, claimValues);
urls.addAll(res);
}
return urls;
}
fun getPlaylistClientOrNull(url: String): IPlatformClient? = getEnabledClients().find { it.isPlaylistUrl(url) }
fun getPlaylistClient(url: String): IPlatformClient = getEnabledClients().find { it.isPlaylistUrl(url) }
?: throw NoPlatformClientException("No client enabled that supports this playlist url (${url})");
@@ -116,8 +116,9 @@ class StatePlugins {
else if(embeddedConfig != null) {
val existing = getPlugin(embedded.key);
if(existing != null && existing.config.version < embeddedConfig.version ) {
Logger.i(TAG, "Found outdated embedded plugin [${existing.config.id}] ${existing.config.name}, deleting and reinstalling");
deletePlugin(embedded.key);
Logger.i(TAG, "Outdated Embedded plugin [${existing.config.id}] ${existing.config.name} (${existing.config.version} < ${embeddedConfig?.version}), reinstalling");
//deletePlugin(embedded.key);
installEmbeddedPlugin(context, embedded.value)
}
else if(existing != null && _isFirstEmbedUpdate)
Logger.i(TAG, "Embedded plugin [${existing.config.id}] ${existing.config.name}, up to date (${existing.config.version} >= ${embeddedConfig?.version})");
@@ -360,6 +361,8 @@ class StatePlugins {
}
val existing = getPlugin(config.id)
val existingAuth = existing?.getAuth();
val existingCaptcha = existing?.getCaptchaData();
if (existing != null) {
if(!reinstall)
throw IllegalStateException("Plugin with id ${config.id} already exists");
@@ -373,7 +376,7 @@ class StatePlugins {
if(icon != null)
iconsDir.saveIconBinary(config.id, icon);
_plugins.save(SourcePluginDescriptor(config, null, null, flags));
_plugins.save(SourcePluginDescriptor(config, existingAuth?.toEncrypted(), existingCaptcha?.toEncrypted(), flags));
return null;
}
catch(ex: Throwable) {
@@ -19,6 +19,7 @@ import com.futo.platformplayer.api.media.structures.IAsyncPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
import com.futo.platformplayer.api.media.structures.PlaceholderPager
import com.futo.platformplayer.api.media.structures.RefreshChronoContentPager
import com.futo.platformplayer.api.media.structures.RefreshDedupContentPager
import com.futo.platformplayer.api.media.structures.RefreshDistributionContentPager
import com.futo.platformplayer.awaitFirstDeferred
@@ -130,10 +131,7 @@ class StatePolycentric {
//TODO: Currently abusing subscription concurrency for parallelism
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
val pagers = profile.ownedClaims.groupBy { it.claim.claimType }.mapNotNull {
//TODO: Deduplicate once multiple urls in single claim is supported
return@mapNotNull it.value.firstOrNull();
}.mapNotNull {
val url = it.claim.resolveChannelUrl() ?: return@mapNotNull null;
val url = it.value.firstOrNull()?.claim?.resolveChannelUrl() ?: return@mapNotNull null;
if (!StatePlatform.instance.hasEnabledChannelClient(url)) {
return@mapNotNull null;
}
@@ -151,10 +149,7 @@ class StatePolycentric {
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
val deferred = profile.ownedClaims.groupBy { it.claim.claimType }
.mapNotNull {
//TODO: Deduplicate once multiple urls in single claim is supported
return@mapNotNull it.value.firstOrNull();
}.mapNotNull {
val url = it.claim.resolveChannelUrl() ?: return@mapNotNull null;
val url = it.value.firstOrNull()?.claim?.resolveChannelUrl() ?: return@mapNotNull null;
val client = StatePlatform.instance.getChannelClientOrNull(url) ?: return@mapNotNull null;
return@mapNotNull Pair(client, scope.async(Dispatchers.IO) {
@@ -173,12 +168,21 @@ class StatePolycentric {
}) ?: return null;
val toAwait = deferred.filter { it.second != finishedPager.first };
//TODO: Get a Parallel pager to work here.
val innerPager = MultiChronoContentPager(listOf(finishedPager.second!!) + toAwait.mapNotNull { runBlocking { it.second.await(); } });
innerPager.initialize();
//return RefreshChronoContentPager(listOf(finishedPager.second!!), toAwait.map { it.second }, listOf());
//return RefreshDedupContentPager(RefreshChronoContentPager(listOf(finishedPager.second!!), toAwait.map { it.second }, listOf()), StatePlatform.instance.getEnabledClients().map { it.id });
return DedupContentPager(innerPager, StatePlatform.instance.getEnabledClients().map { it.id });
/* //Gives out-of-order results
return RefreshDedupContentPager(RefreshDistributionContentPager(
listOf(finishedPager.second!!),
toAwait.map { it.second },
toAwait.map { PlaceholderPager(5) { PlatformContentPlaceholder(it.first.id) } }),
StatePlatform.instance.getEnabledClients().map { it.id }
);
);*/
}
suspend fun getChannelContent(profile: PolycentricProfile): IPager<IPlatformContent> {
return withContext(Dispatchers.IO) {
@@ -39,7 +39,8 @@ class StateTelemetry {
BuildConfig.IS_UNSTABLE_BUILD,
Build.BRAND,
Build.MANUFACTURER,
Build.MODEL
Build.MODEL,
Build.VERSION.SDK_INT
);
val headers = hashMapOf(
@@ -70,7 +70,7 @@ class CommentViewHolder : ViewHolder {
args.processHandle.opinion(c.reference, Opinion.neutral);
}
_layoutComment.alpha = if (args.dislikes > 2 && args.dislikes / (args.likes + args.dislikes) >= 0.7) 0.5f else 1.0f;
_layoutComment.alpha = if (args.dislikes > 2 && args.dislikes.toFloat() / (args.likes + args.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
@@ -93,6 +93,7 @@ class CommentViewHolder : ViewHolder {
fun bind(comment: IPlatformComment, readonly: Boolean) {
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
_textAuthor.text = comment.author.name;
val date = comment.date;
@@ -105,7 +106,7 @@ class CommentViewHolder : ViewHolder {
val rating = comment.rating;
if (rating is RatingLikeDislikes) {
_layoutComment.alpha = if (rating.dislikes > 0 && rating.dislikes / (rating.likes + rating.dislikes) >= 0.7) 0.5f else 1.0f;
_layoutComment.alpha = if (rating.dislikes > 2 && rating.dislikes.toFloat() / (rating.likes + rating.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
} else {
_layoutComment.alpha = 1.0f;
}
@@ -32,6 +32,7 @@ import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.platform.PlatformIndicator
import com.futo.platformplayer.views.video.FutoThumbnailPlayer
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
open class PreviewVideoView : LinearLayout {
@@ -58,11 +59,12 @@ open class PreviewVideoView : LinearLayout {
protected val _exoPlayer: PlayerManager?;
private val _taskLoadValidClaims = TaskHandler<PlatformID, PolycentricCache.CachedOwnedClaims>(StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getValidClaimsAsync(it).await() })
.success { it -> updateClaimsLayout(it, animate = true) }
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getProfileAsync(it) })
.success { it -> onProfileLoaded(it, true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load claims.", it);
Logger.w(TAG, "Failed to load profile.", it);
};
val onVideoClicked = Event2<IPlatformVideo, Long>();
@@ -145,15 +147,7 @@ open class PreviewVideoView : LinearLayout {
open fun bind(content: IPlatformContent) {
_taskLoadValidClaims.cancel();
val cachedClaims = PolycentricCache.instance.getCachedValidClaims(content.author.id);
if (cachedClaims != null) {
updateClaimsLayout(cachedClaims, animate = false);
} else {
updateClaimsLayout(null, animate = false);
_taskLoadValidClaims.run(content.author.id);
}
_taskLoadProfile.cancel();
isClickable = true;
@@ -161,16 +155,25 @@ open class PreviewVideoView : LinearLayout {
stopPreview();
if(_imageChannel != null)
Glide.with(_imageChannel)
.load(content.author.thumbnail)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(_imageChannel);
val cachedProfile = PolycentricCache.instance.getCachedProfile(content.author.url, true);
if (cachedProfile != null) {
onProfileLoaded(cachedProfile, false);
} else {
_imageNeopassChannel?.visibility = View.GONE;
_creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
_imageChannel?.let {
Glide.with(_imageChannel)
.load(content.author.thumbnail)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(_imageChannel);
}
_taskLoadProfile.run(content.author.id);
_textChannelName.text = content.author.name
}
_imageChannel?.clipToOutline = true;
_textVideoName.text = content.name;
_textChannelName.text = content.author.name
_layoutDownloaded.visibility = if (StateDownloads.instance.isDownloaded(content.id)) VISIBLE else GONE;
_platformIndicator.setPlatformFromClientID(content.id.pluginId);
@@ -296,22 +299,50 @@ open class PreviewVideoView : LinearLayout {
_playerVideoThumbnail?.setMuteChangedListener(callback);
}
private fun updateClaimsLayout(claims: PolycentricCache.CachedOwnedClaims?, animate: Boolean) {
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
_neopassAnimator?.cancel();
_neopassAnimator = null;
val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty();
if (harborAvailable) {
_imageNeopassChannel?.visibility = View.VISIBLE
if (animate) {
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
_neopassAnimator?.start()
val profile = cachedPolycentricProfile?.profile;
if (_creatorThumbnail != null) {
val dp_32 = 32.dp(context.resources);
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_32 * dp_32)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
} else if (_imageChannel != null) {
val dp_28 = 28.dp(context.resources);
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_28 * dp_28)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_imageChannel.let {
Glide.with(_imageChannel)
.load(avatar)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(_imageChannel);
}
_imageNeopassChannel?.visibility = View.VISIBLE
if (animate) {
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
_neopassAnimator?.start()
} else {
_imageNeopassChannel?.alpha = 1.0f;
}
} else {
_imageNeopassChannel?.visibility = View.GONE
}
} else {
_imageNeopassChannel?.visibility = View.GONE
}
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate)
if (profile != null) {
_textChannelName.text = profile.systemState.username
}
}
companion object {
@@ -72,22 +72,27 @@ class SubscriptionViewHolder : ViewHolder {
} else {
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
_taskLoadProfile.run(sub.channel.id);
_textName.text = sub.channel.name;
}
_textName.text = sub.channel.name;
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
}
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
val dp_46 = 46.dp(itemView.context.resources);
val avatar = cachedPolycentricProfile?.profile?.systemState?.avatar?.selectBestImage(dp_46 * dp_46)
?.let { it.toURLInfoSystemLinkUrl(cachedPolycentricProfile.profile.system.toProto(), it.process, cachedPolycentricProfile.profile.systemState.servers.toList()) };
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_46 * dp_46)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(cachedPolycentricProfile?.profile != null, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
if (profile != null) {
_textName.text = profile.systemState.username;
}
}
@@ -6,13 +6,23 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
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.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.SubscriptionViewHolder
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.platform.PlatformIndicator
import com.futo.platformplayer.views.subscriptions.SubscribeButton
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Boolean) : AnyAdapter.AnyViewHolder<PlatformAuthorLink>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_creator, _viewGroup, false)) {
@@ -25,7 +35,15 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
private var _authorLink: PlatformAuthorLink? = null;
val onClick = Event1<PlatformAuthorLink>();
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getProfileAsync(it) })
.success { it -> onProfileLoaded(it, true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load profile.", it);
};
init {
_textName = _view.findViewById(R.id.text_channel_name);
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
@@ -45,12 +63,21 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
}
override fun bind(authorLink: PlatformAuthorLink) {
_textName.text = authorLink.name;
_creatorThumbnail.setThumbnail(authorLink.thumbnail, false);
_taskLoadProfile.cancel();
val cachedProfile = PolycentricCache.instance.getCachedProfile(authorLink.url, true);
if (cachedProfile != null) {
onProfileLoaded(cachedProfile, false);
} else {
_creatorThumbnail.setThumbnail(authorLink.thumbnail, false);
_taskLoadProfile.run(authorLink.id);
_textName.text = authorLink.name;
}
if(authorLink.subscribers == null || (authorLink.subscribers ?: 0) <= 0L)
_textMetadata.visibility = View.GONE;
else {
_textMetadata.text = if(authorLink?.subscribers ?: 0 > 0) authorLink.subscribers!!.toHumanNumber() + " subscribers" else "";
_textMetadata.text = if((authorLink.subscribers ?: 0) > 0) authorLink.subscribers!!.toHumanNumber() + " subscribers" else "";
_textMetadata.visibility = View.VISIBLE;
}
_buttonSubscribe.setSubscribeChannel(authorLink.url);
@@ -58,6 +85,25 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
_authorLink = authorLink;
}
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
val dp_61 = 61.dp(itemView.context.resources);
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_61 * dp_61)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_authorLink?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
if (profile != null) {
_textName.text = profile.systemState.username;
}
}
companion object {
private const val TAG = "CreatorViewHolder";
}
@@ -57,22 +57,27 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
} else {
_creatorThumbnail.setThumbnail(subscription.channel.thumbnail, false);
_taskLoadProfile.run(subscription.channel.id);
_name.text = subscription.channel.name;
}
_name.text = subscription.channel.name;
_subscription = subscription;
}
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
val dp_55 = 55.dp(itemView.context.resources)
val avatar = cachedPolycentricProfile?.profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
?.let { it.toURLInfoSystemLinkUrl(cachedPolycentricProfile.profile.system.toProto(), it.process, cachedPolycentricProfile.profile.systemState.servers.toList()) };
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(cachedPolycentricProfile?.profile != null, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
if (profile != null) {
_name.text = profile.systemState.username;
}
}
@@ -13,7 +13,7 @@ import com.futo.platformplayer.constructs.Event0
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.ShapeAppearanceModel
class BigButton : LinearLayout {
open class BigButton : LinearLayout {
private val _root: LinearLayout;
private val _icon: ShapeableImageView;
private val _textPrimary: TextView;
@@ -78,6 +78,10 @@ class BigButton : LinearLayout {
_textSecondary.text = text;
return this;
}
fun withSecondaryTextMaxLines(lines: Int): BigButton {
_textSecondary.maxLines = lines;
return this;
}
fun withIcon(resourceId: Int, rounded: Boolean = false): BigButton {
if (resourceId != -1) {
@@ -4,13 +4,21 @@ import android.content.Context
import android.util.AttributeSet
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.dp
import com.futo.platformplayer.views.buttons.BigButton
import java.lang.reflect.Field
import java.lang.reflect.Method
class ButtonField : LinearLayout, IField {
@Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class FormFieldButton(val drawable: Int = 0)
class ButtonField : BigButton, IField {
override var descriptor: FormField? = null;
private var _obj : Any? = null;
private var _method : Method? = null;
@@ -26,17 +34,22 @@ class ButtonField : LinearLayout, IField {
return null;
};
private val _title : TextView;
private val _subtitle : TextView;
//private val _title : TextView;
//private val _subtitle : TextView;
override val onChanged = Event2<IField, Any>();
constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs){
inflate(context, R.layout.field_button, this);
_title = findViewById(R.id.field_title);
_subtitle = findViewById(R.id.field_subtitle);
//inflate(context, R.layout.field_button, this);
//_title = findViewById(R.id.field_title);
//_subtitle = findViewById(R.id.field_subtitle);
setOnClickListener {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
val dp5 = 5.dp(context.resources);
setMargins(0, dp5, 0, dp5)
};
super.onClick.subscribe {
if(_method?.parameterCount == 1)
_method?.invoke(_obj, context);
else if(_method?.parameterCount == 2)
@@ -51,13 +64,17 @@ class ButtonField : LinearLayout, IField {
this._obj = obj;
val attrField = method.getAnnotation(FormField::class.java);
val attrButtonField = method.getAnnotation(FormFieldButton::class.java);
if(attrField != null) {
_title.text = attrField.title;
_subtitle.text = attrField.subtitle;
super.withPrimaryText(attrField.title)
.withSecondaryText(attrField.subtitle)
.withSecondaryTextMaxLines(2);
descriptor = attrField;
}
else
_title.text = method.name;
super.withPrimaryText(method.name);
if(attrButtonField != null)
super.withIcon(attrButtonField.drawable, false);
return this;
}
+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="M480,760Q546,760 593,713Q640,666 640,600L640,440Q640,374 593,327Q546,280 480,280Q414,280 367,327Q320,374 320,440L320,600Q320,666 367,713Q414,760 480,760ZM400,640L560,640L560,560L400,560L400,640ZM400,480L560,480L560,400L400,400L400,480ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520ZM480,840Q415,840 359.5,808Q304,776 272,720L160,720L160,640L244,640Q241,620 240.5,600Q240,580 240,560L160,560L160,480L240,480Q240,460 240.5,440Q241,420 244,400L160,400L160,320L272,320Q286,297 303.5,277Q321,257 344,242L280,176L336,120L422,206Q450,197 479,197Q508,197 536,206L624,120L680,176L614,242Q637,257 655.5,276.5Q674,296 688,320L800,320L800,400L716,400Q719,420 719.5,440Q720,460 720,480L800,480L800,560L720,560Q720,580 719.5,600Q719,620 716,640L800,640L800,720L688,720Q656,776 600.5,808Q545,840 480,840Z"/>
</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:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M120,800L120,720L600,720L600,800L120,800ZM640,520Q557,520 498.5,461.5Q440,403 440,320Q440,237 498.5,178.5Q557,120 640,120Q723,120 781.5,178.5Q840,237 840,320Q840,403 781.5,461.5Q723,520 640,520ZM120,480L120,400L372,400Q379,422 388,442Q397,462 410,480L120,480ZM120,640L120,560L496,560Q519,574 545,583.5Q571,593 600,597L600,640L120,640ZM620,360L660,360L660,200L620,200L620,360ZM640,440Q648,440 654,434Q660,428 660,420Q660,412 654,406Q648,400 640,400Q632,400 626,406Q620,412 620,420Q620,428 626,434Q632,440 640,440Z"/>
</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="M560,600Q577,600 589.5,587.5Q602,575 602,558Q602,541 589.5,528.5Q577,516 560,516Q543,516 530.5,528.5Q518,541 518,558Q518,575 530.5,587.5Q543,600 560,600ZM530,472L590,472Q590,443 596,429.5Q602,416 624,394Q654,364 664,345.5Q674,327 674,302Q674,257 642.5,228.5Q611,200 560,200Q519,200 488.5,223Q458,246 446,284L500,306Q509,281 524.5,268.5Q540,256 560,256Q584,256 599,269.5Q614,283 614,306Q614,320 606,332.5Q598,345 578,364Q545,393 537.5,409.5Q530,426 530,472ZM320,720Q287,720 263.5,696.5Q240,673 240,640L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L320,720ZM320,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640ZM160,880Q127,880 103.5,856.5Q80,833 80,800L80,240L160,240L160,800Q160,800 160,800Q160,800 160,800L720,800L720,880L160,880ZM320,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640L320,640Q320,640 320,640Q320,640 320,640L320,160Q320,160 320,160Q320,160 320,160Z"/>
</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="M160,720L800,720Q800,720 800,720Q800,720 800,720L800,400L520,400L520,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720L160,720Z"/>
</vector>
+1
View File
@@ -20,6 +20,7 @@
<string name="history">History</string>
<string name="sources">Sources</string>
<string name="buy">Buy</string>
<string name="faq">FAQ</string>
<string name="the_top_source_will_be_considered_primary">The top source will be considered primary</string>
<string name="defaults">Defaults</string>
<string name="home_screen">Home Screen</string>