Various improvements to library and other fixes

This commit is contained in:
Kelvin
2025-11-18 23:35:34 +01:00
parent ee2af411aa
commit f89b074d28
23 changed files with 307 additions and 55 deletions
@@ -252,6 +252,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
UIDialogs.toast(this, "Notification permission denied"); UIDialogs.toast(this, "Notification permission denied");
}; };
fun requestNotificationPermissions() { fun requestNotificationPermissions() {
_notificationPermissionLauncher?.launch(_notifPermission); _notificationPermissionLauncher?.launch(_notifPermission);
} }
@@ -1379,6 +1381,23 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
); );
} }
var _callbackPermissionAudio: ((Boolean)->Unit)? = null;
var _callbackPermissionVideo: ((Boolean)->Unit)? = null;
val permissionReqAudio = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
_callbackPermissionAudio?.invoke(isGranted);
});
val permissionReqVideo = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
_callbackPermissionVideo?.invoke(isGranted);
});
fun requestPermissionAudio(cb: ((Boolean)->Unit)? = null) {
_callbackPermissionAudio = cb;
permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO);
}
fun requestPermissionVideo(cb: ((Boolean)->Unit)? = null) {
_callbackPermissionVideo = cb;
permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO);
}
val notifPermission = "android.permission.POST_NOTIFICATIONS"; val notifPermission = "android.permission.POST_NOTIFICATIONS";
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
@@ -9,6 +9,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -39,7 +40,7 @@ import java.time.OffsetDateTime
import kotlin.math.max import kotlin.math.max
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment { abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
protected val _feedRoot: FrameLayout; protected val _feedRoot: ConstraintLayout;
protected val _recyclerResults: RecyclerView; protected val _recyclerResults: RecyclerView;
protected val _overlayContainer: FrameLayout; protected val _overlayContainer: FrameLayout;
protected val _swipeRefresh: SwipeRefreshLayout; protected val _swipeRefresh: SwipeRefreshLayout;
@@ -52,6 +53,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
private val _emptyPagerContainer: FrameLayout; private val _emptyPagerContainer: FrameLayout;
protected val _toolbarContentView: LinearLayout; protected val _toolbarContentView: LinearLayout;
protected val _bottomContentView: LinearLayout;
private var _loading: Boolean = true; private var _loading: Boolean = true;
@@ -136,6 +138,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
setActiveTags(null); setActiveTags(null);
_toolbarContentView = findViewById(R.id.container_toolbar_content); _toolbarContentView = findViewById(R.id.container_toolbar_content);
_bottomContentView = findViewById(R.id.container_bottom);
_nextPageHandler = TaskHandler<TPager, Pair<TPager, List<TResult>>>({fragment.lifecycleScope}, { _nextPageHandler = TaskHandler<TPager, Pair<TPager, List<TResult>>>({fragment.lifecycleScope}, {
if (it is IAsyncPager<*>) if (it is IAsyncPager<*>)
@@ -506,7 +506,10 @@ class LibraryArtistFragment : MainFragment() {
val playlist = _artist?.toPlaylist(); val playlist = _artist?.toPlaylist();
if (playlist != null) { if (playlist != null) {
val index = playlist.videos.indexOf(c); val sameVideo = playlist.videos.find { it.name == c.name };
val index = sameVideo?.let {
playlist.videos.indexOf(sameVideo)
} ?: -1;
if (index == -1) if (index == -1)
return@subscribe; return@subscribe;
@@ -8,25 +8,32 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.video.LocalVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.structures.AdhocPager import com.futo.platformplayer.api.media.structures.AdhocPager
import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.fragment.mainactivity.topbar.FilesTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.FilesTopBarFragment
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.FileEntry import com.futo.platformplayer.states.FileEntry
import com.futo.platformplayer.states.StateLibrary import com.futo.platformplayer.states.StateLibrary
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.NoResultsView import com.futo.platformplayer.views.NoResultsView
import com.futo.platformplayer.views.adapters.AnyAdapter import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.buttons.ButtonsContainer
class LibraryFilesFragment : MainFragment() { class LibraryFilesFragment : MainFragment() {
override val isMainView : Boolean = true; override val isMainView : Boolean = true;
@@ -70,6 +77,7 @@ class LibraryFilesFragment : MainFragment() {
private var root: FileEntry? = null; private var root: FileEntry? = null;
constructor(fragment: LibraryFilesFragment, inflater: LayoutInflater) : super(fragment, inflater) { constructor(fragment: LibraryFilesFragment, inflater: LayoutInflater) : super(fragment, inflater) {
disableRefreshLayout();
} }
fun onShown(parameter: Any? = null) { fun onShown(parameter: Any? = null) {
@@ -139,6 +147,27 @@ class LibraryFilesFragment : MainFragment() {
setPager(AdhocPager<FileEntry>({ listOf(); }, stack.files)); setPager(AdhocPager<FileEntry>({ listOf(); }, stack.files));
setLoading(false); setLoading(false);
val allSongs = stack.files.filter { !it.isDirectory };
if(allSongs.any()) {
_bottomContentView.addView(ButtonsContainer(context,
listOf(
ButtonsContainer.Button("Play All", R.drawable.background_button_primary) {
StatePlayer.instance.setPlaylist(Playlist(stack.path.toUri().lastPathSegment ?: "", allSongs.map {
SerializedPlatformVideo.fromVideo(LocalVideoDetails.fromContent(it.path))
}), focus = true, shuffle = false)
},
ButtonsContainer.Button("Shuffle", R.drawable.background_button_accent) {
StatePlayer.instance.setPlaylist(Playlist(stack.path.toUri().lastPathSegment ?: "", allSongs.map {
SerializedPlatformVideo.fromVideo(LocalVideoDetails.fromContent(it.path))
}), focus = true, shuffle = true)
}
)).apply {
this.layoutParams = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
})
}
else
_bottomContentView.removeAllViews();
fragment.topBar?.let { fragment.topBar?.let {
if(it is FilesTopBarFragment) { if(it is FilesTopBarFragment) {
if(navStack.size > 1) if(navStack.size > 1)
@@ -93,14 +93,18 @@ class LibraryFragment : MainFragment() {
UIDialogs.showDialog(requireContext(), R.drawable.ic_library, UIDialogs.showDialog(requireContext(), R.drawable.ic_library,
"Music permissions", "We require permissions to see your on-device music, denying this will hide the option to see local music.", null, 1, "Music permissions", "We require permissions to see your on-device music, denying this will hide the option to see local music.", null, 1,
UIDialogs.Action("Ok", { UIDialogs.Action("Ok", {
permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO); StateApp?.instance?.activity?.requestPermissionAudio {
setPermissionResultAudio(it);
}
}, UIDialogs.ActionStyle.PRIMARY), }, UIDialogs.ActionStyle.PRIMARY),
UIDialogs.Action("Cancel", { UIDialogs.Action("Cancel", {
}, UIDialogs.ActionStyle.NONE)); }, UIDialogs.ActionStyle.NONE));
} }
else -> { else -> {
permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO); StateApp?.instance?.activity?.requestPermissionAudio {
setPermissionResultAudio(it);
}
} }
} }
} }
@@ -113,24 +117,22 @@ class LibraryFragment : MainFragment() {
UIDialogs.showDialog(requireContext(), R.drawable.ic_library, false, UIDialogs.showDialog(requireContext(), R.drawable.ic_library, false,
"Videos permissions", "We require permissions to see your on-device videos, denying this will hide the option to see local videos.", null, 1, "Videos permissions", "We require permissions to see your on-device videos, denying this will hide the option to see local videos.", null, 1,
UIDialogs.Action("Ok", { UIDialogs.Action("Ok", {
permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO); StateApp?.instance?.activity?.requestPermissionVideo {
setPermissionResultVideo(it);
}
}, UIDialogs.ActionStyle.PRIMARY), }, UIDialogs.ActionStyle.PRIMARY),
UIDialogs.Action("Cancel", { UIDialogs.Action("Cancel", {
}, UIDialogs.ActionStyle.NONE)); }, UIDialogs.ActionStyle.NONE));
} }
else -> { else -> {
permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO); StateApp?.instance?.activity?.requestPermissionVideo {
setPermissionResultVideo(it);
}
} }
} }
} }
val permissionReqAudio = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
setPermissionResultAudio(isGranted);
});
val permissionReqVideo = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
setPermissionResultVideo(isGranted);
});
companion object { companion object {
fun newInstance() = LibraryFragment().apply {} fun newInstance() = LibraryFragment().apply {}
@@ -292,6 +294,7 @@ class LibraryFragment : MainFragment() {
} }
fun onShown() { fun onShown() {
UIDialogs.appToast("Library is in alpha\nImprovements are coming to local media playback.")
} }
} }
} }
@@ -55,6 +55,7 @@ import com.futo.platformplayer.api.media.LiveChatManager
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
import com.futo.platformplayer.api.media.models.chapters.ChapterType import com.futo.platformplayer.api.media.models.chapters.ChapterType
import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.chapters.IChapter
@@ -77,6 +78,7 @@ import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.LocalVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
@@ -175,6 +177,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import userpackage.Protocol import userpackage.Protocol
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.Locale import java.util.Locale
@@ -563,6 +566,18 @@ class VideoDetailView : ConstraintLayout {
if (video is TutorialFragment.TutorialVideo) { if (video is TutorialFragment.TutorialVideo) {
return@setOnClickListener return@setOnClickListener
} }
if(video is LocalVideoDetails) {
video?.author?.let {
if(it.url.startsWith("content://media/external/audio/artists")) {
fragment.navigate<LibraryArtistFragment>(it.url);
fragment.lifecycleScope.launch {
delay(100);
fragment.minimizeVideoDetail();
};
}
}
return@setOnClickListener;
}
(video?.author ?: _searchVideo?.author)?.let { (video?.author ?: _searchVideo?.author)?.let {
fragment.navigate<ChannelFragment>(it); fragment.navigate<ChannelFragment>(it);
@@ -1035,7 +1050,7 @@ class VideoDetailView : ConstraintLayout {
_slideUpOverlay?.hide(); _slideUpOverlay?.hide();
} }
else null, else null,
if(!isLimitedVersion && !(video?.isLive ?: false)) if(!isLimitedVersion && !(video?.isLive ?: false) && !(video is LocalVideoDetails))
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) { RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
video?.let { video?.let {
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver); _slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
@@ -1058,15 +1073,16 @@ class VideoDetailView : ConstraintLayout {
_slideUpOverlay?.hide(); _slideUpOverlay?.hide();
} }
else null, else null,
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) { if(!(video is LocalVideoDetails))
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
video?.let { video?.let {
val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url; val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url;
fragment.navigate<BrowserFragment>(url); fragment.navigate<BrowserFragment>(url);
fragment.minimizeVideoDetail(); fragment.minimizeVideoDetail();
}; };
_slideUpOverlay?.hide(); _slideUpOverlay?.hide();
}, } else null,
if (StateSync.instance.hasAuthorizedDevice()) { if (StateSync.instance.hasAuthorizedDevice() && !(video is LocalVideoDetails)) {
RoundButton(context, R.drawable.ic_device, context.getString(R.string.send_to_device), TAG_SEND_TO_DEVICE) { RoundButton(context, R.drawable.ic_device, context.getString(R.string.send_to_device), TAG_SEND_TO_DEVICE) {
val devices = StateSync.instance.getAuthorizedSessions(); val devices = StateSync.instance.getAuthorizedSessions();
val videoToSend = video ?: return@RoundButton; val videoToSend = video ?: return@RoundButton;
@@ -1089,10 +1105,11 @@ class VideoDetailView : ConstraintLayout {
}) })
} }
}} else null, }} else null,
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") { if(!(video is LocalVideoDetails))
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
reloadVideo(); reloadVideo();
_slideUpOverlay?.hide(); _slideUpOverlay?.hide();
}).filterNotNull(); } else null).filterNotNull();
if(!_buttonPinStore.getAllValues().any()) if(!_buttonPinStore.getAllValues().any())
_buttonPins.setButtons(*(buttons + listOf(_buttonMore)).toTypedArray()); _buttonPins.setButtons(*(buttons + listOf(_buttonMore)).toTypedArray());
else { else {
@@ -1624,7 +1641,9 @@ class VideoDetailView : ConstraintLayout {
_buttonSubscribe.setSubscribeChannel(video.author.url); _buttonSubscribe.setSubscribeChannel(video.author.url);
setDescription(video.description.fixHtmlLinks()); setDescription(video.description.fixHtmlLinks());
_creatorThumbnail.setThumbnail(video.author.thumbnail, false); _creatorThumbnail.setThumbnail(video.author.thumbnail, false,
video is LocalVideoDetails
);
setPolycentricProfile(null, animate = false); setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(video.author.id); _taskLoadPolycentricProfile.run(video.author.id);
@@ -1652,7 +1671,7 @@ class VideoDetailView : ConstraintLayout {
_rating.visibility = View.GONE; _rating.visibility = View.GONE;
if (StatePolycentric.instance.enabled) { if (StatePolycentric.instance.enabled && !(video is LocalVideoDetails)) {
fragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
try { try {
val queryReferencesResponse = ApiMethods.getQueryReferences( val queryReferencesResponse = ApiMethods.getQueryReferences(
@@ -1811,17 +1830,19 @@ class VideoDetailView : ConstraintLayout {
_player.updateNextPrevious(); _player.updateNextPrevious();
updateMoreButtons(); updateMoreButtons();
if (videoDetail is TutorialFragment.TutorialVideo) { if (videoDetail is TutorialFragment.TutorialVideo || videoDetail is LocalVideoDetails) {
_buttonSubscribe.visibility = View.GONE _buttonSubscribe.visibility = View.GONE
_buttonMore.visibility = View.GONE _buttonMore.visibility = if(videoDetail is LocalVideoDetails) View.VISIBLE else View.GONE;
_buttonPins.visibility = View.GONE _buttonPins.visibility = if(videoDetail is LocalVideoDetails) View.VISIBLE else View.GONE;
_layoutRating.visibility = View.GONE _layoutRating.visibility = View.GONE
_rating.visibility = View.GONE;
_layoutChangeBottomSection.visibility = View.GONE _layoutChangeBottomSection.visibility = View.GONE
} else { } else {
_buttonSubscribe.visibility = View.VISIBLE _buttonSubscribe.visibility = View.VISIBLE
_buttonMore.visibility = View.VISIBLE _buttonMore.visibility = View.VISIBLE
_buttonPins.visibility = View.VISIBLE _buttonPins.visibility = View.VISIBLE
_layoutRating.visibility = View.VISIBLE _layoutRating.visibility = View.VISIBLE
_rating.visibility = View.VISIBLE;
_layoutChangeBottomSection.visibility = View.VISIBLE _layoutChangeBottomSection.visibility = View.VISIBLE
} }
@@ -2685,7 +2706,11 @@ class VideoDetailView : ConstraintLayout {
private fun fetchComments() { private fun fetchComments() {
Logger.i(TAG, "fetchComments") Logger.i(TAG, "fetchComments")
video?.let { video?.let {
_commentsList.load(true) { StatePlatform.instance.getComments(it); }; if(video is LocalVideoDetails) {
_commentsList.clearComments();
}
else
_commentsList.load(true) { StatePlatform.instance.getComments(it); };
} }
} }
private fun fetchPolycentricComments() { private fun fetchPolycentricComments() {
@@ -2972,6 +2997,7 @@ class VideoDetailView : ConstraintLayout {
} }
onChannelClicked.subscribe { onChannelClicked.subscribe {
Logger.i(TAG, "Opening channel url: ${it.url}");
if(it.url.isNotBlank()) { if(it.url.isNotBlank()) {
fragment.minimizeVideoDetail() fragment.minimizeVideoDetail()
fragment.navigate<ChannelFragment>(it) fragment.navigate<ChannelFragment>(it)
@@ -12,5 +12,6 @@ data class Telemetry(
val brand: String, val brand: String,
val manufacturer: String, val manufacturer: String,
val model: String, val model: String,
val sdkVersion: Int val sdkVersion: Int,
val plugins: List<String>? = null
) { } ) { }
@@ -7,6 +7,7 @@ import android.net.Uri
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
import androidx.collection.emptyLongSet
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
@@ -243,11 +244,12 @@ class StateLibrary {
MediaStore.Audio.Media._ID, //0 MediaStore.Audio.Media._ID, //0
MediaStore.Audio.Media.DISPLAY_NAME, //1 MediaStore.Audio.Media.DISPLAY_NAME, //1
MediaStore.Audio.Media.ARTIST, //2 MediaStore.Audio.Media.ARTIST, //2
MediaStore.Audio.Media.ALBUM_ID, //3 MediaStore.Audio.Media.ARTIST_ID, //3
MediaStore.Audio.Media.DURATION, //4 MediaStore.Audio.Media.ALBUM_ID, //4
MediaStore.Audio.Media.DATE_ADDED, //5 MediaStore.Audio.Media.DURATION, //5
MediaStore.Audio.Media.MIME_TYPE, //6 MediaStore.Audio.Media.DATE_ADDED, //6
MediaStore.Audio.Media.BUCKET_DISPLAY_NAME //7 MediaStore.Audio.Media.MIME_TYPE, //7
MediaStore.Audio.Media.BUCKET_DISPLAY_NAME //8
); );
fun getDocumentTrack(url: String): IPlatformContentDetails? { fun getDocumentTrack(url: String): IPlatformContentDetails? {
@@ -359,11 +361,12 @@ class StateLibrary {
val id = cursor.getString(0); val id = cursor.getString(0);
val displayName = cursor.getString(1); val displayName = cursor.getString(1);
val author = cursor.getString(2); val author = cursor.getString(2);
val albumId = cursor.getLong(3); val authorId = cursor.getStringOrNull(3);
val duration = cursor.getLong(4).let { if(it > 0) it / 1000 else 0 }; val albumId = cursor.getLong(4);
val date = cursor.getLong(5); val duration = cursor.getLong(5).let { if(it > 0) it / 1000 else 0 };
val contentType = cursor.getString(6); val date = cursor.getLong(6);
val category = cursor.getString(7); val contentType = cursor.getString(7);
val category = cursor.getString(8);
val idLong = id.toLongOrNull(); val idLong = id.toLongOrNull();
val contentUrl = if(idLong != null ) val contentUrl = if(idLong != null )
@@ -371,6 +374,13 @@ class StateLibrary {
else else
""; "";
val authorIdLong = authorId?.toLongOrNull();
val authorUrl = if(authorIdLong != null)
ContentUris.withAppendedId(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, authorIdLong).toString();
else
"";
val albumContentUrl = if(albumId > 0) val albumContentUrl = if(albumId > 0)
ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, albumId)?.toString() ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, albumId)?.toString()
else null; else null;
@@ -380,7 +390,10 @@ class StateLibrary {
else null; else null;
val authorObj = if(!author.isNullOrBlank()) val authorObj = if(!author.isNullOrBlank())
PlatformAuthorLink(PlatformID.NONE, author, "", null, null) PlatformAuthorLink(
if(authorId != null) PlatformID("LOCAL", authorId) else PlatformID.NONE,
author,
authorUrl, null, null)
else PlatformAuthorLink.UNKNOWN; else PlatformAuthorLink.UNKNOWN;
return LocalVideoDetails( return LocalVideoDetails(
@@ -39,7 +39,8 @@ class StateTelemetry {
Build.BRAND, Build.BRAND,
Build.MANUFACTURER, Build.MANUFACTURER,
Build.MODEL, Build.MODEL,
Build.VERSION.SDK_INT Build.VERSION.SDK_INT,
StatePlatform.instance.getEnabledClients().map { it.id }.toList()
); );
val headers = hashMapOf( val headers = hashMapOf(
@@ -40,10 +40,10 @@ class ArtistTileViewHolder(val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder
if (artist.thumbnail != null) if (artist.thumbnail != null)
Glide.with(it) Glide.with(it)
.load(artist.thumbnail) .load(artist.thumbnail)
.placeholder(R.drawable.unknown_music) .placeholder(R.drawable.ic_artist)
.into(it) .into(it)
else else
Glide.with(it).load(R.drawable.unknown_music).into(it); Glide.with(it).load(R.drawable.ic_artist).into(it);
}; };
_textName.text = artist.name; _textName.text = artist.name;
@@ -42,11 +42,11 @@ class FileViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHold
_file = file; _file = file;
_imageThumbnail?.let { _imageThumbnail?.let {
if(file.isDirectory) if(file.isDirectory)
it.setImageResource(R.drawable.ic_library); it.setImageResource(R.drawable.ic_folder);
else { else {
Glide.with(it) Glide.with(it)
.load(file.thumbnail) .load(file.thumbnail)
.placeholder(R.drawable.ic_music) .placeholder(R.drawable.ic_song)
.into(it) .into(it)
} }
}; };
@@ -0,0 +1,47 @@
package com.futo.platformplayer.views.buttons
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.collection.emptyLongSet
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.views.pills.PillButton
class ButtonsContainer : LinearLayout {
val container_buttons: LinearLayout
var currentButtons: List<Button> = listOf();
constructor(context: Context, buttons: List<Button>) : super(context) {
inflate(context, R.layout.view_buttons, this)
container_buttons = findViewById(R.id.container_buttons);
setButtons(buttons);
}
fun setButtons(buttons: List<Button>) {
this.currentButtons = buttons;
container_buttons.removeAllViews();
for(button in buttons) {
container_buttons.addView(StandardButton(context, button.name) {
button?.handler?.invoke();
}.apply {
this.layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
this.weight = 1f;
};
if(button.background != null)
this.withBackground(button.background);
})
}
}
class Button(
val name: String,
val background: Int?,
val handler: (()->Unit),
);
}
@@ -0,0 +1,34 @@
package com.futo.platformplayer.views.buttons
import android.content.Context
import android.widget.LinearLayout
import android.widget.TextView
import com.futo.platformplayer.R
class StandardButton : LinearLayout {
private val _root: LinearLayout;
private val _text: TextView;
constructor(context: Context, text: String, onClick: ()->Unit) : super(context) {
inflate(context, R.layout.view_button_standard, this);
_root = findViewById(R.id.root);
_text = findViewById(R.id.text_button);
_text.text = text;
_root.setOnClickListener {
onClick.invoke();
}
}
fun withPrimaryBackground(): StandardButton {
_root.setBackgroundResource(R.drawable.background_button_primary)
return this;
}
fun withAccentBackground(): StandardButton {
_root.setBackgroundResource(R.drawable.background_button_accent)
return this;
}
fun withBackground(id: Int): StandardButton {
_root.setBackgroundResource(id);
return this;
}
}
@@ -54,9 +54,14 @@ class CreatorThumbnail : ConstraintLayout {
setNewActivity(false); setNewActivity(false);
} }
fun setThumbnail(url: String?, animate: Boolean) { fun setThumbnail(url: String?, animate: Boolean, isArtist: Boolean = false) {
if (url == null) { if (url == null) {
clear(); if(isArtist) {
_imageChannelThumbnail.setImageResource(R.drawable.ic_artist);
_imageChannelThumbnail.visibility = View.VISIBLE;
}
else
clear();
return; return;
} }
@@ -78,18 +83,21 @@ class CreatorThumbnail : ConstraintLayout {
} else { } else {
setHarborAvailable(false, animate, null); setHarborAvailable(false, animate, null);
} }
var placeholder = R.drawable.placeholder_channel_thumbnail;
if(url.startsWith("content://") || isArtist)
placeholder = R.drawable.ic_artist;
if (animate) { if (animate) {
Glide.with(_imageChannelThumbnail) Glide.with(_imageChannelThumbnail)
.load(url) .load(url)
.placeholder(R.drawable.placeholder_channel_thumbnail) .placeholder(placeholder)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.crossfade() .crossfade()
.into(_imageChannelThumbnail) .into(_imageChannelThumbnail)
} else { } else {
Glide.with(_imageChannelThumbnail) Glide.with(_imageChannelThumbnail)
.load(url) .load(url)
.placeholder(R.drawable.placeholder_channel_thumbnail) .placeholder(placeholder)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.into(_imageChannelThumbnail); .into(_imageChannelThumbnail);
} }
@@ -18,7 +18,7 @@ class PillButton : LinearLayout {
val onClick = Event0(); val onClick = Event0();
private var _isLoading = false; private var _isLoading = false;
constructor(context : Context, attrs : AttributeSet) : super(context, attrs) { constructor(context : Context, attrs : AttributeSet?) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.pill_button, this, true); LayoutInflater.from(context).inflate(R.layout.pill_button, this, true);
icon = findViewById(R.id.pill_icon); icon = findViewById(R.id.pill_icon);
text = findViewById(R.id.pill_text); text = findViewById(R.id.pill_text);
@@ -17,6 +17,7 @@ import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.LazyComment import com.futo.platformplayer.api.media.models.comments.LazyComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IAsyncPager import com.futo.platformplayer.api.media.structures.IAsyncPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
@@ -224,6 +225,12 @@ class CommentsList : ConstraintLayout {
_commentsPager = pager; _commentsPager = pager;
onCommentsLoaded.emit(_comments.size); onCommentsLoaded.emit(_comments.size);
} }
fun clearComments() {
_comments.clear();
_adapterComments.notifyDataSetChanged();
_commentsPager = EmptyPager();
onCommentsLoaded.emit(0);
}
fun load(readonly: Boolean, loader: suspend () -> IPager<IPlatformComment>) { fun load(readonly: Boolean, loader: suspend () -> IPager<IPlatformComment>) {
cancel(); cancel();
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

+13 -5
View File
@@ -1,16 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/feed_root"
android:id="@+id/feed_root"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/container_bottom"
android:orientation="vertical" android:orientation="vertical"
tools:context=".fragment.mainactivity.main.FeedFragment"> tools:context=".fragment.mainactivity.main.FeedFragment">
@@ -124,6 +126,12 @@
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<LinearLayout
android:id="@+id/container_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" />
<FrameLayout <FrameLayout
android:id="@+id/overlay_container" android:id="@+id/overlay_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -131,4 +139,4 @@
android:elevation="100dp" android:elevation="100dp"
android:visibility="gone" /> android:visibility="gone" />
</FrameLayout> </androidx.constraintlayout.widget.ConstraintLayout>
+2 -4
View File
@@ -10,7 +10,7 @@
android:layout_marginBottom="5dp" android:layout_marginBottom="5dp"
android:layout_marginLeft="10dp" android:layout_marginLeft="10dp"
android:layout_marginRight="10dp" android:layout_marginRight="10dp"
android:background="@drawable/background_16_round_4dp" android:background="@drawable/background_1b_round_6dp"
android:id="@+id/root" android:id="@+id/root"
android:clickable="true"> android:clickable="true">
@@ -23,12 +23,10 @@
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent">
android:background="@drawable/background_1b_round_6dp">
<ImageView <ImageView
android:id="@+id/image_thumbnail" android:id="@+id/image_thumbnail"
android:alpha="0.4"
android:layout_height="34dp" android:layout_height="34dp"
android:layout_width="34dp" android:layout_width="34dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_gravity="bottom"
android:orientation="vertical"
android:background="@drawable/background_button_accent"
android:gravity="center"
android:id="@+id/root">
<TextView
android:id="@+id/text_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:autoSizeTextType="uniform"
android:padding="12dp"
android:text="Play all" />
</LinearLayout>
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:orientation="vertical"
android:id="@+id/root">
<LinearLayout
android:id="@+id/container_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginEnd="14dp"
android:layout_marginTop="14dp"
android:layout_marginBottom="14dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:showDividers="middle"
android:divider="@drawable/divider_transparent_4dp">
</LinearLayout>
</LinearLayout>