Fixed thumbnail to consider max size 1920 and fitcenter and implemented fixes for pagination of library.

This commit is contained in:
Koen J
2025-12-01 14:20:41 +01:00
parent 0abc65a9bd
commit 45f621763a
20 changed files with 242 additions and 129 deletions
@@ -43,6 +43,8 @@ import java.util.concurrent.ThreadLocalRandom
import java.util.zip.GZIPInputStream import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
import androidx.core.graphics.scale import androidx.core.graphics.scale
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz "; private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
fun getRandomString(sizeOfRandomString: Int): String { fun getRandomString(sizeOfRandomString: Int): String {
@@ -441,3 +443,16 @@ fun addressScore(addr: InetAddress): Int {
} }
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this) fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)
fun <T> RequestBuilder<T>.withMaxSizePx(maxSizePx: Int = 1920, useCenterCrop: Boolean = false): RequestBuilder<T> {
var builder = this
.downsample(DownsampleStrategy.AT_MOST)
.override(maxSizePx, maxSizePx)
builder = if (useCenterCrop) {
builder.centerCrop()
} else {
builder.fitCenter()
}
return builder
}
@@ -55,7 +55,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
protected val _toolbarContentView: LinearLayout; protected val _toolbarContentView: LinearLayout;
protected val _bottomContentView: LinearLayout; protected val _bottomContentView: LinearLayout;
private var _loading: Boolean = true; private var _loading: Boolean = false;
private val _pagerLock = Object(); private val _pagerLock = Object();
private var _cache: ItemCache<TResult>? = null; private var _cache: ItemCache<TResult>? = null;
@@ -180,10 +180,9 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
val visibleItemCount = _recyclerResults.childCount; val visibleItemCount = _recyclerResults.childCount;
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition() val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition()
//Logger.i(TAG, "onScrolled loadNextPage visibleItemCount=$visibleItemCount firstVisibleItem=$visibleItemCount") //Logger.i(TAG, "onScrolled loadNextPage(): firstVisibleItem=$firstVisibleItem visibleItemCount=$visibleItemCount visibleThreshold=$visibleThreshold recyclerData.results.size=${recyclerData.results.size}")
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size && firstVisibleItem > 0) { if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size) {
//Logger.i(TAG, "onScrolled loadNextPage(): firstVisibleItem=$firstVisibleItem visibleItemCount=$visibleItemCount visibleThreshold=$visibleThreshold recyclerData.results.size=${recyclerData.results.size}")
loadNextPage(); loadNextPage();
} }
} }
@@ -197,57 +196,44 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
} }
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) { private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
val canScroll = if (recyclerData.results.isEmpty()) false else { _recyclerResults.post {
val height = resources.displayMetrics.heightPixels; val canScroll = _recyclerResults.canScrollVertically(1)
Logger.i(
TAG,
"ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter"
)
if (!canScroll || filteredResults.isEmpty()) {
_automaticNextPageCounter++
if (_automaticNextPageCounter < _automaticBackoff.size) {
if (_automaticNextPageCounter > 0) {
val automaticNextPageCounterSaved = _automaticNextPageCounter;
fragment.lifecycleScope.launch(Dispatchers.Default) {
val backoff = _automaticBackoff[Math.min(
_automaticBackoff.size - 1,
_automaticNextPageCounter
)];
val layoutManager = recyclerData.layoutManager
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
val firstVisibleItemView = if(firstVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(firstVisibleItemPosition) else null;
val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition();
val lastVisibleItemView = if(lastVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(lastVisibleItemPosition) else null;
val rows = if(recyclerData.layoutManager is GridLayoutManager) Math.max(1, recyclerData.results.size / recyclerData.layoutManager.spanCount) else 1;
val rowsHeight = (firstVisibleItemView?.height ?: 0) * rows;
if(lastVisibleItemView != null && lastVisibleItemPosition == (recyclerData.results.size - 1)) {
false;
}
else if (firstVisibleItemView != null && height != null && rowsHeight < height) {
false;
} else {
true;
}
}
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
if (!canScroll || filteredResults.isEmpty()) {
_automaticNextPageCounter++
if(_automaticNextPageCounter < _automaticBackoff.size) {
if(_automaticNextPageCounter > 0) {
val automaticNextPageCounterSaved = _automaticNextPageCounter;
fragment.lifecycleScope.launch(Dispatchers.Default) {
val backoff = _automaticBackoff[Math.min(_automaticBackoff.size - 1, _automaticNextPageCounter)];
withContext(Dispatchers.Main) {
setLoading(true);
}
delay(backoff.toLong());
if(automaticNextPageCounterSaved == _automaticNextPageCounter) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
loadNextPage(); setLoading(true);
}
delay(backoff.toLong());
if (automaticNextPageCounterSaved == _automaticNextPageCounter) {
withContext(Dispatchers.Main) {
loadNextPage();
}
} else {
withContext(Dispatchers.Main) {
setLoading(false);
}
} }
} }
else { } else
withContext(Dispatchers.Main) { loadNextPage();
setLoading(false);
}
}
}
} }
else } else {
loadNextPage(); Logger.i(TAG, "ensureEnoughContentVisible automaticNextPageCounter reset");
_automaticNextPageCounter = 0;
} }
} else {
Logger.i(TAG, "ensureEnoughContentVisible automaticNextPageCounter reset");
_automaticNextPageCounter = 0;
} }
} }
fun resetAutomaticNextPageCounter(){ fun resetAutomaticNextPageCounter(){
@@ -484,6 +470,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
recyclerData.resultsUnfiltered.addAll(toAdd); recyclerData.resultsUnfiltered.addAll(toAdd);
recyclerData.adapter.notifyDataSetChanged(); recyclerData.adapter.notifyDataSetChanged();
recyclerData.loadedFeedStyle = feedStyle; recyclerData.loadedFeedStyle = feedStyle;
setLoading(false)
if(pager.hasMorePages()) if(pager.hasMorePages())
ensureEnoughContentVisible(filteredResults) ensureEnoughContentVisible(filteredResults)
} }
@@ -519,7 +506,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
synchronized(_pagerLock) { synchronized(_pagerLock) {
val pager: TPager = recyclerData.pager ?: return; val pager: TPager = recyclerData.pager ?: return;
val hasMorePages = pager.hasMorePages(); val hasMorePages = pager.hasMorePages();
Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages, page size=${pager.getResults().size}"); //Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages, page size=${pager.getResults().size}");
//loadCachedPage(); //loadCachedPage();
if (pager.hasMorePages()) { if (pager.hasMorePages()) {
@@ -96,7 +96,6 @@ class LibraryVideosFragment : MainFragment() {
fun onShown() { fun onShown() {
val initialAlbums = StateLibrary.instance.getAlbums(); val initialAlbums = StateLibrary.instance.getAlbums();
Logger.i(TAG, "Initial album count: " + initialAlbums.size); Logger.i(TAG, "Initial album count: " + initialAlbums.size);
val buckets = StateLibrary.instance.getVideoBucketNames();
setPager(StateLibrary.instance.getVideos(fragment._toggleBuckets)); setPager(StateLibrary.instance.getVideos(fragment._toggleBuckets));
} }
@@ -364,7 +364,7 @@ class RemotePlaylistFragment : MainFragment() {
_imagePlaylistThumbnail.let { _imagePlaylistThumbnail.let {
Glide.with(it) Glide.with(it)
.load(video.thumbnails.getHQThumbnail()) .load(video.thumbnails.getHQThumbnail())
.downsample(DownsampleStrategy.AT_MOST).override(1080, 1080) .withMaxSizePx()
.placeholder(R.drawable.placeholder_video_thumbnail) .placeholder(R.drawable.placeholder_video_thumbnail)
.crossfade() .crossfade()
.into(it); .into(it);
@@ -78,6 +78,7 @@ import com.futo.platformplayer.views.video.FutoShortPlayer
import com.futo.platformplayer.views.video.FutoVideoPlayerBase import com.futo.platformplayer.views.video.FutoVideoPlayerBase
import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_AUDIO_CONTAINERS import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_AUDIO_CONTAINERS
import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_VIDEO_CONTAINERS import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_VIDEO_CONTAINERS
import com.futo.platformplayer.withMaxSizePx
import com.futo.polycentric.core.ApiMethods import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ContentType import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Models import com.futo.polycentric.core.Models
@@ -859,7 +860,7 @@ class ShortView : FrameLayout {
val thumbnail = videoDetails.thumbnails.getHQThumbnail() val thumbnail = videoDetails.thumbnails.getHQThumbnail()
if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap() if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap()
.load(thumbnail).downsample(DownsampleStrategy.AT_MOST).override(1080, 1080).into(object : CustomTarget<Bitmap>() { .load(thumbnail).withMaxSizePx().into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) { override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
player.setArtwork(resource.toDrawable(resources)) player.setArtwork(resource.toDrawable(resources))
} }
@@ -162,6 +162,7 @@ import com.futo.platformplayer.views.subscriptions.SubscribeButton
import com.futo.platformplayer.views.video.FutoVideoPlayer import com.futo.platformplayer.views.video.FutoVideoPlayer
import com.futo.platformplayer.views.video.FutoVideoPlayerBase import com.futo.platformplayer.views.video.FutoVideoPlayerBase
import com.futo.platformplayer.views.videometa.UpNextView import com.futo.platformplayer.views.videometa.UpNextView
import com.futo.platformplayer.withMaxSizePx
import com.futo.polycentric.core.ApiMethods import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ContentType import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Models import com.futo.polycentric.core.Models
@@ -2050,7 +2051,7 @@ class VideoDetailView : ConstraintLayout {
} else { } else {
val thumbnail = video.thumbnails.getHQThumbnail(); val thumbnail = video.thumbnails.getHQThumbnail();
if ((videoSource == null) && !thumbnail.isNullOrBlank()) // || _player.isAudioMode if ((videoSource == null) && !thumbnail.isNullOrBlank()) // || _player.isAudioMode
Glide.with(context).asBitmap().load(thumbnail).downsample(DownsampleStrategy.AT_MOST).override(1080, 1080) Glide.with(context).asBitmap().load(thumbnail).withMaxSizePx()
.into(object: CustomTarget<Bitmap>() { .into(object: CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) { override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
_player.setArtwork(BitmapDrawable(resources, resource)); _player.setArtwork(BitmapDrawable(resources, resource));
@@ -29,6 +29,7 @@ import com.futo.platformplayer.toHumanDuration
import com.futo.platformplayer.toHumanTime import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.SearchView import com.futo.platformplayer.views.SearchView
import com.futo.platformplayer.views.lists.VideoListEditorView import com.futo.platformplayer.views.lists.VideoListEditorView
import com.futo.platformplayer.withMaxSizePx
abstract class VideoListEditorView : LinearLayout { abstract class VideoListEditorView : LinearLayout {
private var _videoListEditorView: VideoListEditorView; private var _videoListEditorView: VideoListEditorView;
@@ -212,7 +213,7 @@ abstract class VideoListEditorView : LinearLayout {
_imagePlaylistThumbnail.let { _imagePlaylistThumbnail.let {
Glide.with(it) Glide.with(it)
.load(video.thumbnails.getHQThumbnail()) .load(video.thumbnails.getHQThumbnail())
.downsample(DownsampleStrategy.AT_MOST).override(1080, 1080) .withMaxSizePx()
.placeholder(R.drawable.placeholder_video_thumbnail) .placeholder(R.drawable.placeholder_video_thumbnail)
.crossfade() .crossfade()
.into(it); .into(it);
@@ -7,6 +7,7 @@ import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.futo.platformplayer.api.media.models.Thumbnails import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.withMaxSizePx
class GlideHelper { class GlideHelper {
@@ -15,7 +16,7 @@ class GlideHelper {
fun ImageView.loadThumbnails(thumbnails: Thumbnails, isHQ: Boolean = true, continuation: ((RequestBuilder<Drawable>) -> Unit)? = null) { fun ImageView.loadThumbnails(thumbnails: Thumbnails, isHQ: Boolean = true, continuation: ((RequestBuilder<Drawable>) -> Unit)? = null) {
val url = if(isHQ) thumbnails.getHQThumbnail() ?: thumbnails.getLQThumbnail() else thumbnails.getLQThumbnail(); val url = if(isHQ) thumbnails.getHQThumbnail() ?: thumbnails.getLQThumbnail() else thumbnails.getLQThumbnail();
val req = Glide.with(this).load(url).downsample(DownsampleStrategy.AT_MOST).override(1080, 1080); val req = Glide.with(this).load(url).withMaxSizePx()
if (thumbnails.hasMultiple() && false) { //TODO: Resolve issue where fallback triggered on second loads? if (thumbnails.hasMultiple() && false) { //TODO: Resolve issue where fallback triggered on second loads?
val fallbackUrl = if (isHQ) thumbnails.getLQThumbnail() else thumbnails.getHQThumbnail(); val fallbackUrl = if (isHQ) thumbnails.getLQThumbnail() else thumbnails.getHQThumbnail();
@@ -39,6 +39,7 @@ import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.withMaxSizePx
class MediaPlaybackService : Service() { class MediaPlaybackService : Service() {
private val TAG = "MediaPlaybackService"; private val TAG = "MediaPlaybackService";
@@ -225,7 +226,7 @@ class MediaPlaybackService : Service() {
val tag = video; val tag = video;
Glide.with(this).asBitmap() Glide.with(this).asBitmap()
.load(thumbnail) .load(thumbnail)
.downsample(DownsampleStrategy.AT_MOST).override(1080, 1080) .withMaxSizePx()
.into(object: CustomTarget<Bitmap>() { .into(object: CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap,transition: Transition<in Bitmap>?) { override fun onResourceReady(resource: Bitmap,transition: Transition<in Bitmap>?) {
if (tag != _notif_last_video) return if (tag != _notif_last_video) return
@@ -1,10 +1,12 @@
package com.futo.platformplayer.states package com.futo.platformplayer.states
import android.content.ContentResolver
import android.content.ContentUris import android.content.ContentUris
import android.content.Intent import android.content.Intent
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.MediaStore.Audio.Artists import android.provider.MediaStore.Audio.Artists
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
@@ -154,34 +156,101 @@ class StateLibrary {
fun getArtist(id: Long): Artist? { fun getArtist(id: Long): Artist? {
return Artist.getArtist(id); return Artist.getArtist(id);
} }
fun getVideos(
buckets: List<String>? = null,
pageSize: Int = 20
): IPager<IPlatformContent> {
val resolver = StateApp.instance.contextOrNull?.contentResolver ?: return EmptyPager()
val selection: String?
val selectionArgs: Array<String>?
fun getVideos(buckets: List<String>? = null): IPager<IPlatformContent> { if (!buckets.isNullOrEmpty()) {
var query = if(buckets != null) "${MediaStore.Video.Media.BUCKET_DISPLAY_NAME} IN " + "(" + buckets.map { "'${it}'" }.joinToString(",") + ")" else null; val placeholders = buckets.joinToString(",") { "?" }
val cursor = StateApp.instance.contextOrNull?.contentResolver?.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, PROJECTION_VIDEO, selection = "${MediaStore.Video.Media.BUCKET_DISPLAY_NAME} IN ($placeholders)"
query, selectionArgs = buckets.toTypedArray()
null, } else {
MediaStore.Video.Media.DATE_ADDED + " DESC") ?: return EmptyPager(); selection = null
selectionArgs = null
}
//Ongoing usage of cursor..todo disposal val collectionUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//return cursor.use { MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
cursor.moveToFirst(); } else {
val list = mutableListOf<IPlatformVideo>() MediaStore.Video.Media.EXTERNAL_CONTENT_URI
while(!cursor.isAfterLast && list.size < 10) { }
list.add(videoFromCursor(cursor));
cursor.moveToNext(); var nextPageIndex = 0
fun loadPage(pageIndex: Int): List<IPlatformContent> {
Logger.i(TAG, "loadPage $pageIndex")
val offset = pageIndex * pageSize
val queryArgs = Bundle().apply {
selection?.let {
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, it)
}
selectionArgs?.let {
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, it)
}
putStringArray(
ContentResolver.QUERY_ARG_SORT_COLUMNS,
arrayOf(
MediaStore.Video.Media.DATE_ADDED,
MediaStore.Video.Media._ID
)
)
putInt(
ContentResolver.QUERY_ARG_SORT_DIRECTION,
ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
)
putInt(ContentResolver.QUERY_ARG_LIMIT, pageSize)
putInt(ContentResolver.QUERY_ARG_OFFSET, offset)
} }
return AdhocPager<IPlatformContent>({ val cursor = resolver.query(
val list = mutableListOf<IPlatformContent>() collectionUri,
while(!cursor.isAfterLast && list.size < 10) { PROJECTION_VIDEO,
list.add(videoFromCursor(cursor)); queryArgs,
cursor.moveToNext(); null
)
if (cursor == null) {
Logger.i(TAG, "loadPage $pageIndex null, returning empty list")
return emptyList()
}
cursor.use { c ->
if (!c.moveToFirst()) {
Logger.i(TAG, "loadPage $pageIndex moveToFirst failed, returning empty list")
return emptyList()
} }
Logger.i(TAG, "Videos nextPage: ${list.size}")
return@AdhocPager list; val list = ArrayList<IPlatformContent>(pageSize)
}, list); do {
//} list.add(videoFromCursor(c))
} while (c.moveToNext() && list.size < pageSize)
Logger.i(TAG, "loadPage $pageIndex found ${list.size} items")
return list
}
}
val firstPage = loadPage(0)
if (firstPage.isEmpty()) {
return EmptyPager()
}
nextPageIndex = 1
return AdhocPager<IPlatformContent>({
val page = loadPage(nextPageIndex)
nextPageIndex++
Logger.i(TAG, "loadPage nextPage: ${page.size}")
page
}, firstPage)
} }
fun getRecentVideos(buckets: List<String>? = null, count: Int = 20): List<IPlatformVideo> { fun getRecentVideos(buckets: List<String>? = null, count: Int = 20): List<IPlatformVideo> {
val videoPager = getVideos(buckets); val videoPager = getVideos(buckets);
val items = mutableListOf<IPlatformVideo>(); val items = mutableListOf<IPlatformVideo>();
@@ -193,48 +262,80 @@ class StateLibrary {
return items; return items;
} }
private var _cacheBucketNames: List<Bucket>? = null; @Volatile
fun getVideoBucketNames(): List<Bucket> { private var _cachedVideoBuckets: List<Bucket>? = null
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) private val _bucketCacheLock = Any()
return listOf();
if(_cacheBucketNames != null)
return _cacheBucketNames ?: listOf();
try {
val cur: Cursor = StateApp.instance.contextOrNull?.contentResolver?.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, arrayOf(
MediaStore.Video.Media.BUCKET_ID,
MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
), null, null, null
) ?: return listOf();
return cur.use { fun getVideoBucketNames(forceRefresh: Boolean = false): List<Bucket> {
val buckets = mutableListOf<Bucket>(); if (!forceRefresh) {
val list = HashSet<Long>(); _cachedVideoBuckets?.let { return it }
if (cur.moveToFirst()) { }
var id: Long;
var bucket: String val resolver = StateApp.instance.contextOrNull?.contentResolver
do { ?: return emptyList()
try {
id = cur.getLong(0); val projection = arrayOf(
bucket = cur.getStringOrNull(1) ?: continue; MediaStore.Video.VideoColumns.BUCKET_ID,
if (!list.contains(id)) { MediaStore.Video.VideoColumns.BUCKET_DISPLAY_NAME
list.add(id); )
buckets.add(Bucket(id, bucket));
} val sortOrder = "${MediaStore.Video.VideoColumns.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC"
} catch (ex: Throwable) { val loadedBuckets: List<Bucket> = try {
Logger.e(TAG, "Failed to parse bucket due to ${ex.message}", ex); resolver.query(
} MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
} while (cur.moveToNext()) projection,
null,
null,
sortOrder
)?.use { cursor ->
if (!cursor.moveToFirst()) {
return@use emptyList<Bucket>()
} }
_cacheBucketNames = buckets.toList()
return@use _cacheBucketNames ?: listOf(); val idxId = cursor.getColumnIndexOrThrow(MediaStore.Video.VideoColumns.BUCKET_ID)
val idxName = cursor.getColumnIndexOrThrow(MediaStore.Video.VideoColumns.BUCKET_DISPLAY_NAME)
val seenIds = HashSet<Long>()
val buckets = ArrayList<Bucket>()
do {
try {
val id = cursor.getLong(idxId)
if (!seenIds.add(id)) {
continue
}
val name = cursor.getStringOrNull(idxName) ?: continue
buckets.add(Bucket(id, name))
} catch (e: Exception) {
Logger.e(TAG, "Failed to parse video bucket row: ${e.message}", e)
}
} while (cursor.moveToNext())
buckets
} ?: emptyList()
} catch (e: Exception) {
Logger.e(TAG, "Buckets loading failed, returning empty: ${e.message}", e)
emptyList()
}
if (loadedBuckets.isEmpty()) {
if (!forceRefresh) {
_cachedVideoBuckets?.let { return it }
} }
return emptyList()
} }
catch(ex: Throwable) {
Logger.e(TAG, "Buckets loading failed, returning empty"); synchronized(_bucketCacheLock) {
return listOf(); if (!forceRefresh) {
_cachedVideoBuckets?.let { return it }
}
_cachedVideoBuckets = loadedBuckets
return loadedBuckets
} }
} }
fun invalidateVideoBucketNamesCache() {
_cachedVideoBuckets = null
}
companion object { companion object {
@@ -23,6 +23,7 @@ import com.futo.platformplayer.serializers.PlatformContentSerializer
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.toHumanNowDiffString import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNowDiffStringMinDay import com.futo.platformplayer.toHumanNowDiffStringMinDay
import com.futo.platformplayer.withMaxSizePx
import java.time.OffsetDateTime import java.time.OffsetDateTime
class StateNotifications { class StateNotifications {
@@ -97,7 +98,7 @@ class StateNotifications {
if(thumbnail != null) if(thumbnail != null)
Glide.with(context).asBitmap() Glide.with(context).asBitmap()
.load(thumbnail) .load(thumbnail)
.downsample(DownsampleStrategy.AT_MOST).override(1080, 1080) .withMaxSizePx()
.into(object: CustomTarget<Bitmap>() { .into(object: CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) { override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
notifyNewContent(context, manager, notificationChannel, id, content, resource); notifyNewContent(context, manager, notificationChannel, id, content, resource);
@@ -12,6 +12,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.withMaxSizePx
class PlaylistsViewHolder : ViewHolder { class PlaylistsViewHolder : ViewHolder {
private val _root: ConstraintLayout; private val _root: ConstraintLayout;
@@ -45,7 +46,7 @@ class PlaylistsViewHolder : ViewHolder {
if (p.videos.isNotEmpty()) { if (p.videos.isNotEmpty()) {
Glide.with(_imageThumbnail) Glide.with(_imageThumbnail)
.load(p.videos[0].thumbnails.getMinimumThumbnail(380)) .load(p.videos[0].thumbnails.getMinimumThumbnail(380))
.downsample(DownsampleStrategy.AT_MOST).override(1080, 1080) .withMaxSizePx()
.placeholder(R.drawable.placeholder_video_thumbnail) .placeholder(R.drawable.placeholder_video_thumbnail)
.crossfade() .crossfade()
.into(_imageThumbnail); .into(_imageThumbnail);
@@ -24,6 +24,7 @@ import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.toHumanTime import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.others.ProgressBar import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.platform.PlatformIndicator import com.futo.platformplayer.views.platform.PlatformIndicator
import com.futo.platformplayer.withMaxSizePx
class VideoListEditorViewHolder : ViewHolder { class VideoListEditorViewHolder : ViewHolder {
private val _root: ConstraintLayout; private val _root: ConstraintLayout;
@@ -90,7 +91,7 @@ class VideoListEditorViewHolder : ViewHolder {
fun bind(v: IPlatformVideo, canEdit: Boolean) { fun bind(v: IPlatformVideo, canEdit: Boolean) {
Glide.with(_imageThumbnail) Glide.with(_imageThumbnail)
.load(v.thumbnails.getHQThumbnail()) .load(v.thumbnails.getHQThumbnail())
.downsample(DownsampleStrategy.AT_MOST).override(1080, 1080) .withMaxSizePx()
.placeholder(R.drawable.placeholder_video_thumbnail) .placeholder(R.drawable.placeholder_video_thumbnail)
.crossfade() .crossfade()
.into(_imageThumbnail); .into(_imageThumbnail);
@@ -17,6 +17,7 @@ import com.futo.platformplayer.states.Artist
import com.futo.platformplayer.toHumanNowDiffString import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanTime import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.adapters.AnyAdapter import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.withMaxSizePx
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
@@ -50,7 +51,7 @@ class LocalVideoTileViewHolder(val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHo
Glide.with(it) Glide.with(it)
.load(content.thumbnails.getHQThumbnail()) .load(content.thumbnails.getHQThumbnail())
.placeholder(R.drawable.unknown_music) .placeholder(R.drawable.unknown_music)
.downsample(DownsampleStrategy.AT_MOST).override(1080, 1080) .withMaxSizePx()
.into(it) .into(it)
else else
Glide.with(it).load(R.drawable.unknown_music).into(it); Glide.with(it).load(R.drawable.unknown_music).into(it);
@@ -33,6 +33,7 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.TargetTapLoaderView import com.futo.platformplayer.views.TargetTapLoaderView
import com.futo.platformplayer.views.behavior.GestureControlView import com.futo.platformplayer.views.behavior.GestureControlView
import com.futo.platformplayer.withMaxSizePx
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -307,7 +308,7 @@ class CastView : ConstraintLayout {
Glide.with(_thumbnail) Glide.with(_thumbnail)
.load(video.thumbnails.getHQThumbnail()) .load(video.thumbnails.getHQThumbnail())
.placeholder(R.drawable.placeholder_video_thumbnail) .placeholder(R.drawable.placeholder_video_thumbnail)
.downsample(DownsampleStrategy.AT_MOST).override(1080, 1080) .withMaxSizePx()
.into(_thumbnail); .into(_thumbnail);
_textPosition.text = (position * 1000).formatDuration(); _textPosition.text = (position * 1000).formatDuration();
_textDuration.text = (video.duration * 1000).formatDuration(); _textDuration.text = (video.duration * 1000).formatDuration();
@@ -61,8 +61,8 @@ class ActiveDownloadItem: LinearLayout {
} }
Glide.with(_videoImage) Glide.with(_videoImage)
.downsample(DownsampleStrategy.AT_MOST).override(1080, 1080)
.load(download.thumbnail) .load(download.thumbnail)
.withMaxSizePx()
.crossfade() .crossfade()
.into(_videoImage); .into(_videoImage);
@@ -9,6 +9,7 @@ import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.models.PlaylistDownloaded import com.futo.platformplayer.models.PlaylistDownloaded
import com.futo.platformplayer.withMaxSizePx
class PlaylistDownloadItem(context: Context, playlistName: String, playlistThumbnail: String?, val obj: Any): LinearLayout(context) { class PlaylistDownloadItem(context: Context, playlistName: String, playlistThumbnail: String?, val obj: Any): LinearLayout(context) {
init { inflate(context, R.layout.list_downloaded_playlist, this) } init { inflate(context, R.layout.list_downloaded_playlist, this) }
@@ -20,7 +21,7 @@ class PlaylistDownloadItem(context: Context, playlistName: String, playlistThumb
imageText.text = playlistName; imageText.text = playlistName;
Glide.with(imageView) Glide.with(imageView)
.load(playlistThumbnail) .load(playlistThumbnail)
.downsample(DownsampleStrategy.AT_MOST).override(1080, 1080) .withMaxSizePx()
.crossfade() .crossfade()
.into(imageView); .into(imageView);
} }
@@ -26,6 +26,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.toHumanTime import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.withMaxSizePx
class FutoThumbnailPlayer : FutoVideoPlayerBase { class FutoThumbnailPlayer : FutoVideoPlayerBase {
@@ -136,7 +137,7 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
if (videoSource == null && audioSource != null) { if (videoSource == null && audioSource != null) {
val thumbnail = video.thumbnails.getHQThumbnail(); val thumbnail = video.thumbnails.getHQThumbnail();
if (!thumbnail.isNullOrBlank()) { if (!thumbnail.isNullOrBlank()) {
Glide.with(videoView).asBitmap().load(thumbnail).downsample(DownsampleStrategy.AT_MOST).override(1080, 1080).into(_loadArtwork); Glide.with(videoView).asBitmap().load(thumbnail).withMaxSizePx().into(_loadArtwork);
} else { } else {
Glide.with(videoView).clear(_loadArtwork); Glide.with(videoView).clear(_loadArtwork);
setArtwork(null); setArtwork(null);
@@ -54,6 +54,7 @@ import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.TargetTapLoaderView import com.futo.platformplayer.views.TargetTapLoaderView
import com.futo.platformplayer.views.behavior.GestureControlView import com.futo.platformplayer.views.behavior.GestureControlView
import com.futo.platformplayer.views.others.ProgressBar import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.withMaxSizePx
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -928,11 +929,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
override fun switchToAudioMode(video: IPlatformVideoDetails?) { override fun switchToAudioMode(video: IPlatformVideoDetails?) {
super.switchToAudioMode(video) super.switchToAudioMode(video)
//This causes issues, and is in general confusing, needs improvements
/*
val thumbnail = video?.thumbnails?.getHQThumbnail() val thumbnail = video?.thumbnails?.getHQThumbnail()
if (!thumbnail.isNullOrBlank()) { if (!thumbnail.isNullOrBlank()) {
Glide.with(context).asBitmap().load(thumbnail) Glide.with(context).asBitmap().load(thumbnail).withMaxSizePx()
.into(object : CustomTarget<Bitmap>() { .into(object : CustomTarget<Bitmap>() {
override fun onResourceReady( override fun onResourceReady(
resource: Bitmap, resource: Bitmap,
@@ -946,6 +945,5 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
} }
}) })
} }
*/
} }
} }
@@ -17,6 +17,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.toHumanNowDiffString import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNumber import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.withMaxSizePx
class UpNextView : LinearLayout { class UpNextView : LinearLayout {
private val _layoutContainer: LinearLayout; private val _layoutContainer: LinearLayout;
@@ -161,7 +162,7 @@ class UpNextView : LinearLayout {
_textChannelName.text = nextItem.author.name; _textChannelName.text = nextItem.author.name;
Glide.with(_imageThumbnail) Glide.with(_imageThumbnail)
.load(nextItem.thumbnails.getHQThumbnail()) .load(nextItem.thumbnails.getHQThumbnail())
.downsample(DownsampleStrategy.AT_MOST).override(1080, 1080) .withMaxSizePx()
.placeholder(R.drawable.placeholder_video_thumbnail) .placeholder(R.drawable.placeholder_video_thumbnail)
.into(_imageThumbnail); .into(_imageThumbnail);
Glide.with(_imageChannelThumbnail) Glide.with(_imageChannelThumbnail)