diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Formatting.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Formatting.kt index f32c86d3..efaf0eba 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Formatting.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Formatting.kt @@ -185,6 +185,25 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String { return "${value} ${unit}"; }; +fun Int.toHumanTimeIndicator(abs: Boolean = false) : String { + var value = this; + + var unit = "s"; + + if(abs) value = abs(value); + if(value >= secondsInHour) { + value = (this / secondsInHour).toInt(); + if(abs) value = abs(value); + unit = "hr" + (if(value > 1) "s" else ""); + } + else if(value >= secondsInMinute) { + value = (this / secondsInMinute).toInt(); + if(abs) value = abs(value); + unit = "min"; + } + + return "${value}${unit}"; +} fun Long.toHumanTime(isMs: Boolean): String { var scaler = 1; diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index ab2aa90a..6cd51c33 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -86,6 +86,7 @@ class Settings : FragmentedStorageFileJson() { } } + /* @FormField( R.string.submit_feedback, FieldForm.BUTTON, R.string.give_feedback_on_the_application, -1 @@ -104,7 +105,7 @@ class Settings : FragmentedStorageFileJson() { } catch (e: Throwable) { //Ignored } - } + }*/ @FormField( R.string.manage_tabs, FieldForm.BUTTON, @@ -201,6 +202,12 @@ class Settings : FragmentedStorageFileJson() { fun getSubscriptionsConcurrency() : Int { return threadIndexToCount(subscriptionConcurrency); } + + @FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 9) + var showWatchMetrics: Boolean = false; + + @FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 10) + var allowPlaytimeTracking: Boolean = true; } @FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 4) diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index f0d52d39..7a641d68 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -92,7 +92,7 @@ class UISlideOverlays { menu.selectOption(null, "fetchPosts", true, true); menu.onOK.subscribe { - StateSubscriptions.instance.saveSubscription(subscription); + subscription.save(); menu.hide(true); }; menu.onCancel.subscribe { diff --git a/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt b/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt index 1464b035..f4ef7d2d 100644 --- a/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt +++ b/app/src/main/java/com/futo/platformplayer/cache/ChannelContentCache.kt @@ -41,7 +41,7 @@ class ChannelContentCache { val trimmed: Int; if(toTrim > 0) { val redundantContent = _channelContents.flatMap { it.value.getItems().filter { it.datetime != null && it.datetime!!.isBefore(minDays) }.drop(9) } - .sortedByDescending { it.datetime!! }.take(toTrim); + .sortedBy { it.datetime!! }.take(toTrim); for(content in redundantContent) uncacheContent(content); trimmed = redundantContent.size; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt index a956536d..0cbdf0b9 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt @@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IRefreshPager import com.futo.platformplayer.api.media.structures.IReplacerPager import com.futo.platformplayer.api.media.structures.MultiPager +import com.futo.platformplayer.cache.ChannelContentCache import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.TaskHandler @@ -33,6 +34,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.fragment.mainactivity.main.FeedView import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.states.StatePolycentric +import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.adapters.PreviewContentListAdapter import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder @@ -74,9 +76,14 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment { private val _taskLoadVideos = TaskHandler>({lifecycleScope}, { return@TaskHandler getContentPager(it); - }).success { + }).success { livePager -> setLoading(false); - setPager(it); + + val pager = if(_channel?.let { StateSubscriptions.instance.isSubscribed(it) } == true) + ChannelContentCache.cachePagerResults(lifecycleScope, livePager); + else livePager; + + setPager(pager); } .exception { } .exception { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt index e8bf35d8..3030c5fc 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt @@ -118,7 +118,11 @@ class SubscriptionsFeedFragment : MainFragment() { if(recyclerData.loadedFeedStyle != feedStyle || recyclerData.lastLoad.getNowDiffSeconds() > 60 ) { recyclerData.lastLoad = OffsetDateTime.now(); - loadResults(); + + if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5) + loadResults(false); + else if(recyclerData.results.size == 0) + loadCache(); } val announcementsView = _announcementsView; @@ -201,7 +205,7 @@ class SubscriptionsFeedFragment : MainFragment() { context.getString(R.string.rate_limit_warning), context.getString(R.string.this_is_a_temporary_measure_to_prevent_people_from_hitting_rate_limit_until_we_have_better_support_for_lots_of_subscriptions) + context.getString(R.string.you_have_too_many_subscriptions_for_the_following_plugins), subsByLimited.map { it.first.config.name + ": " + it.second.size + " " + context.getString(R.string.subscriptions) } .joinToString("\n"), 0, UIDialogs.Action("Refresh Anyway", { _bypassRateLimit = true; - loadResults(); + loadResults(true); }, UIDialogs.ActionStyle.DANGEROUS_TEXT), UIDialogs.Action("OK", { finishRefreshLayoutLoader(); @@ -213,7 +217,7 @@ class SubscriptionsFeedFragment : MainFragment() { .exception { Logger.w(ChannelFragment.TAG, "Failed to load channel.", it); if(it !is CancellationException) - UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }); + UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) }); else { finishRefreshLayoutLoader(); setLoading(false); @@ -278,16 +282,19 @@ class SubscriptionsFeedFragment : MainFragment() { loadResults(true); } + private fun loadCache() { + Logger.i(TAG, "Subscriptions load cache"); + val cachePager = ChannelContentCache.instance.getSubscriptionCachePager(); + val results = cachePager.getResults(); + Logger.i(TAG, "Subscriptions show cache (${results.size})"); + setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null); + setPager(cachePager); + } private fun loadResults(withRefetch: Boolean = false) { setLoading(true); Logger.i(TAG, "Subscriptions load"); if(recyclerData.results.size == 0) { - Logger.i(TAG, "Subscriptions load cache"); - val cachePager = ChannelContentCache.instance.getSubscriptionCachePager(); - val results = cachePager.getResults(); - Logger.i(TAG, "Subscriptions show cache (${results.size})"); - setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null); - setPager(cachePager); + loadCache(); } else { setTextCentered(null); } 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 94664802..778ccdd2 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 @@ -12,7 +12,6 @@ import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.net.Uri -import android.provider.Browser import android.support.v4.media.session.PlaybackStateCompat import android.text.Spanned import android.util.AttributeSet @@ -23,7 +22,6 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import android.view.WindowManager import android.widget.* import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.lifecycleScope @@ -66,6 +64,7 @@ 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.models.Subscription import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.states.* @@ -96,7 +95,6 @@ import com.google.android.exoplayer2.Format import com.google.android.exoplayer2.ui.PlayerControlView import com.google.android.exoplayer2.ui.TimeBar import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException -import com.google.common.base.Stopwatch import com.google.protobuf.ByteString import kotlinx.coroutines.* import userpackage.Protocol @@ -436,6 +434,7 @@ class VideoDetailView : ConstraintLayout { if (!_isCasting && !_didStop) { setLastPositionMilliseconds(position, true); } + updatePlaybackTracking(position); }; _player.onVideoClicked.subscribe { @@ -610,6 +609,61 @@ class VideoDetailView : ConstraintLayout { } } + val _trackingUpdateTimeLock = Object(); + val _trackingUpdateInterval = 3000; + var _trackingLastUpdateTime = System.currentTimeMillis(); + var _trackingLastPosition: Long = 0; + var _trackingLastVideo: IPlatformVideoDetails? = null; + var _trackingTotalWatched: Long = 0; + var _trackingDidCountView: Boolean = false; + var _trackingLastVideoSubscription: Subscription? = null; + fun updatePlaybackTracking(position: Long) { + if(!Settings.instance.subscriptions.allowPlaytimeTracking) + return; + val now = System.currentTimeMillis(); + val shouldUpdate = synchronized(_trackingUpdateTimeLock) { + val doUpdate = (now - _trackingLastUpdateTime) > _trackingUpdateInterval; + if(doUpdate) + _trackingLastUpdateTime = now; + return@synchronized doUpdate; + } + if(shouldUpdate) { + val currentVideo = video; + val delta = position - _trackingLastPosition; + _trackingLastPosition = position; + + if(currentVideo != null && currentVideo == _trackingLastVideo) { + if(delta > 500 && delta < _trackingUpdateInterval * 1.5) { + _trackingLastVideoSubscription?.let { + Logger.i(TAG, "Subscription [${it.channel.name}] watch time delta [${delta}]" + + "(${"%.2f".format((_trackingTotalWatched / 1000) / currentVideo.duration.toDouble().coerceAtLeast(1.0))})"); + it.updatePlayback(currentVideo, (delta / 1000).toInt()); + _trackingTotalWatched += delta; + if(!_trackingDidCountView && currentVideo.duration > 0) { + val percentage = (_trackingTotalWatched / 1000) / currentVideo.duration.toDouble(); + if(percentage > 0.4) { + Logger.i(TAG, "Subscription [${it.channel.name}] new view"); + _trackingDidCountView = true; + it.addPlaybackView(); + } + } + it.saveAsync(); + }; + } + } + else { + if(_trackingLastVideo == null && currentVideo == null) + return; + _trackingLastVideo = currentVideo; + _trackingTotalWatched = 0; + if(currentVideo?.author?.url != null) + _trackingLastVideoSubscription = StateSubscriptions.instance.getSubscription(currentVideo.author.url); + else + _trackingLastVideoSubscription = null; + } + } + } + fun updateMoreButtons() { val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) { (video ?: _searchVideo)?.let { diff --git a/app/src/main/java/com/futo/platformplayer/models/Subscription.kt b/app/src/main/java/com/futo/platformplayer/models/Subscription.kt index 9579dfcf..3506fe0c 100644 --- a/app/src/main/java/com/futo/platformplayer/models/Subscription.kt +++ b/app/src/main/java/com/futo/platformplayer/models/Subscription.kt @@ -4,9 +4,11 @@ import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.SerializedChannel import com.futo.platformplayer.api.media.models.contents.IPlatformContent +import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails import com.futo.platformplayer.getNowDiffDays import com.futo.platformplayer.serializers.OffsetDateTimeSerializer import com.futo.platformplayer.states.StatePlatform +import com.futo.platformplayer.states.StateSubscriptions import java.time.OffsetDateTime @kotlinx.serialization.Serializable @@ -43,6 +45,9 @@ class Subscription { var uploadStreamInterval : Int = 0; var uploadPostInterval : Int = 0; + var playbackSeconds: Int = 0; + var playbackViews: Int = 0; + constructor(channel : SerializedChannel) { this.channel = channel; @@ -55,10 +60,24 @@ class Subscription { fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url); + fun save() { + StateSubscriptions.instance.saveSubscription(this); + } + fun saveAsync() { + StateSubscriptions.instance.saveSubscription(this); + } + fun updateChannel(channel: IPlatformChannel) { this.channel = SerializedChannel.fromChannel(channel); } + fun updatePlayback(content: IPlatformContentDetails, seconds: Int) { + playbackSeconds += seconds; + } + fun addPlaybackView() { + playbackViews += 1; + } + fun updateSubscriptionState(type: String, initialPage: List) { val interval: Int; val mostRecent: OffsetDateTime?; diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index 371c15ff..6af44d7d 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -456,8 +456,9 @@ class StateApp { val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true }; if (isRateLimitReached) { Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}"); - delay(8000); - StateSubscriptions.instance.updateSubscriptionFeed(scope, false); + delay(5000); + if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5) + StateSubscriptions.instance.updateSubscriptionFeed(scope, false); } else Logger.w(TAG, "Too many subscription requests required:\n${reqCountStr}"); diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt index b5155c4a..855d720f 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -762,7 +762,7 @@ class StatePlatform { } if(hasChanges) - StateSubscriptions.instance.saveSubscription(sub); + sub.save(); } return pagerResult; diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt index b686e659..88a12ff5 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt @@ -32,6 +32,7 @@ import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms import kotlinx.coroutines.* +import java.time.OffsetDateTime import java.util.concurrent.ExecutionException import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinTask @@ -74,6 +75,9 @@ class StateSubscriptions { val onSubscriptionsChanged = Event2, Boolean>(); + fun getOldestUpdateTime(): OffsetDateTime { + return getSubscriptions().minOf { it.lastVideoUpdate }; + } fun getGlobalSubscriptionProgress(): Pair { return Pair(_lastGlobalSubscriptionProgress, _lastGlobalSubscriptionTotal); } @@ -170,6 +174,9 @@ class StateSubscriptions { fun saveSubscription(sub: Subscription) { _subscriptions.save(sub, false, true); } + fun saveSubscriptionAsync(sub: Subscription) { + _subscriptions.saveAsync(sub, false, true); + } fun getSubscriptionCount(): Int { synchronized(_subscriptions) { return _subscriptions.getItems().size; @@ -242,7 +249,7 @@ class StateSubscriptions { } - val usePolycentric = false; + val usePolycentric = true; val subUrls = getSubscriptions().associateWith { if(usePolycentric) StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id); diff --git a/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt b/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt index 596f3be2..5346d861 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt @@ -227,18 +227,18 @@ class ManagedStore{ } - fun saveAsync(obj: T, withReconstruction: Boolean = false) { + fun saveAsync(obj: T, withReconstruction: Boolean = false, onlyExisting: Boolean = false) { val scope = StateApp.instance.scopeOrNull; if(scope != null) scope.launch(Dispatchers.IO) { try { - save(obj, withReconstruction); + save(obj, withReconstruction, onlyExisting); } catch (e: Throwable) { Logger.e(TAG, "Failed to save.", e); } }; else - save(obj, withReconstruction); + save(obj, withReconstruction, onlyExisting); } fun saveAllAsync(objs: List, withReconstruction: Boolean = false) { val scope = StateApp.instance.scopeOrNull; diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt index 1b70d014..144032ce 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt @@ -54,7 +54,7 @@ abstract class SubscriptionsTaskFetchAlgorithm( val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size; val clientCacheCount = clientTasks.value.size - clientTaskCount; if(clientCacheCount > 0) { - UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels.") + UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels. (${clientCacheCount} cached)"); } } @@ -155,7 +155,7 @@ abstract class SubscriptionsTaskFetchAlgorithm( val initialPage = pager.getResults(); task.sub.updateSubscriptionState(task.type, initialPage); - StateSubscriptions.instance.saveSubscription(task.sub); + task.sub.save(); finished++; onProgress.emit(finished, forkTasks.size); diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt index 7b7358f3..8b3c0491 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt @@ -14,7 +14,7 @@ class SubscriptionAdapter : RecyclerView.Adapter { private val _confirmationMessage: String; var onClick = Event1(); - var sortBy: Int = 0 + var sortBy: Int = 3 set(value) { field = value; updateDataset(); @@ -51,6 +51,10 @@ class SubscriptionAdapter : RecyclerView.Adapter { _sortedDataset = when (sortBy) { 0 -> StateSubscriptions.instance.getSubscriptions().sortedBy({ u -> u.channel.name }) 1 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending({ u -> u.channel.name }) + 2 -> StateSubscriptions.instance.getSubscriptions().sortedBy { it.playbackViews } + 3 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackViews } + 4 -> StateSubscriptions.instance.getSubscriptions().sortedBy { it.playbackSeconds } + 5 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds } else -> throw IllegalStateException("Invalid sorting algorithm selected."); }.toList(); diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt index 85ad79b3..bda9301b 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt @@ -9,6 +9,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.R +import com.futo.platformplayer.Settings import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.models.Subscription @@ -17,6 +18,8 @@ import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.dp import com.futo.platformplayer.selectBestImage +import com.futo.platformplayer.toHumanBytesSpeed +import com.futo.platformplayer.toHumanTimeIndicator import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.platform.PlatformIndicator import com.futo.polycentric.core.toURLInfoSystemLinkUrl @@ -27,11 +30,12 @@ class SubscriptionViewHolder : ViewHolder { private val _creatorThumbnail: CreatorThumbnail; private val _buttonTrash: ImageButton; private val _platformIndicator : PlatformIndicator; + private val _textMeta: TextView; private val _taskLoadProfile = TaskHandler( StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) }) - .success { it -> onProfileLoaded(it, true) } + .success { it -> onProfileLoaded(null, it, true) } .exception { Logger.w(TAG, "Failed to load profile.", it); }; @@ -45,6 +49,7 @@ class SubscriptionViewHolder : ViewHolder { constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_subscription, viewGroup, false)) { _layoutSubscription = itemView.findViewById(R.id.layout_subscription); _textName = itemView.findViewById(R.id.text_name); + _textMeta = itemView.findViewById(R.id.text_meta); _creatorThumbnail = itemView.findViewById(R.id.creator_thumbnail); _buttonTrash = itemView.findViewById(R.id.button_trash); _platformIndicator = itemView.findViewById(R.id.platform); @@ -68,17 +73,18 @@ class SubscriptionViewHolder : ViewHolder { val cachedProfile = PolycentricCache.instance.getCachedProfile(sub.channel.url, true); if (cachedProfile != null) { - onProfileLoaded(cachedProfile, false); + onProfileLoaded(sub, cachedProfile, false); } else { _creatorThumbnail.setThumbnail(sub.channel.thumbnail, false); _taskLoadProfile.run(sub.channel.id); _textName.text = sub.channel.name; + bindViewMetrics(sub); } _platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId); } - private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { + private fun onProfileLoaded(sub: Subscription?, cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { val dp_46 = 46.dp(itemView.context.resources); val profile = cachedPolycentricProfile?.profile; val avatar = profile?.systemState?.avatar?.selectBestImage(dp_46 * dp_46) @@ -94,6 +100,19 @@ class SubscriptionViewHolder : ViewHolder { if (profile != null) { _textName.text = profile.systemState.username; } + + if(sub != null) + bindViewMetrics(sub) + } + + fun bindViewMetrics(sub: Subscription?) { + if(sub == null || !Settings.instance.subscriptions.showWatchMetrics) + _textMeta.text = ""; + else + _textMeta.text = listOf( + if(sub.playbackViews > 0) "${sub.playbackViews} view" + (if(sub.playbackViews > 1) "s" else "") else null, + if(sub.playbackSeconds > 0) sub.playbackSeconds.toHumanTimeIndicator() else null + ).filterNotNull().joinToString(" ยท "); } companion object { diff --git a/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt b/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt index 452f311f..3f5e2d30 100644 --- a/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt +++ b/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt @@ -25,7 +25,7 @@ class SubscriptionBar : LinearLayout { constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { inflate(context, R.layout.view_subscription_bar, this); - val subscriptions = StateSubscriptions.instance.getSubscriptions(); + val subscriptions = StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackViews }; _adapterView = findViewById(R.id.recycler_creators).asAny(subscriptions, orientation = RecyclerView.HORIZONTAL) { it.onClick.subscribe { c -> onClickChannel.emit(c.channel); diff --git a/app/src/main/res/layout/list_subscription.xml b/app/src/main/res/layout/list_subscription.xml index b4136954..cb373135 100644 --- a/app/src/main/res/layout/list_subscription.xml +++ b/app/src/main/res/layout/list_subscription.xml @@ -35,10 +35,25 @@ android:maxLines="1" android:ellipsize="end" /> - + + + + Submit logs Submit logs to help us narrow down issues Subscription Concurrency + Track Playtime Locally + Locally track playtime of subscriptions, used for subscriptions ordering and local creator recommendations. + Show Watch Metrics + Shows the watch time and views of each creator in the creators tab This prevents the device from rotating within the given amount of degrees Use the live chat web window when available over native implementation Version Code @@ -700,6 +704,10 @@ Name Ascending Name Descending + Views Ascending + Views Descending + Watchtime Ascending + Watchtime Descending Preview