WIP library support, albums, artists, videos

This commit is contained in:
Kelvin
2025-10-15 01:03:47 +02:00
parent b6676e7763
commit 87d93c2ed8
28 changed files with 2264 additions and 17 deletions
+3
View File
@@ -16,6 +16,9 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -57,6 +57,12 @@ import com.futo.platformplayer.fragment.mainactivity.main.HistoryFragment
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
import com.futo.platformplayer.fragment.mainactivity.main.ImportPlaylistsFragment
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumsFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryArtistFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryArtistsFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryVideosFragment
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistFragment
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsFragment
@@ -179,6 +185,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragBuy: BuyFragment;
lateinit var _fragSubGroup: SubscriptionGroupFragment;
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
lateinit var _fragLibrary: LibraryFragment;
lateinit var _fragLibraryAlbums: LibraryAlbumsFragment;
lateinit var _fragLibraryAlbum: LibraryAlbumFragment;
lateinit var _fragLibraryArtists: LibraryArtistsFragment;
lateinit var _fragLibraryArtist: LibraryArtistFragment;
lateinit var _fragLibraryVideos: LibraryVideosFragment;
lateinit var _fragBrowser: BrowserFragment;
@@ -349,6 +361,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragBuy = BuyFragment.newInstance();
_fragSubGroup = SubscriptionGroupFragment.newInstance();
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
_fragLibrary = LibraryFragment.newInstance();
_fragLibraryAlbums = LibraryAlbumsFragment.newInstance();
_fragLibraryAlbum = LibraryAlbumFragment.newInstance();
_fragLibraryArtists = LibraryArtistsFragment.newInstance();
_fragLibraryArtist = LibraryArtistFragment.newInstance();
_fragLibraryVideos = LibraryVideosFragment.newInstance();
_fragBrowser = BrowserFragment.newInstance();
@@ -475,6 +493,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragImportSubscriptions.topBar = _fragTopBarImport;
_fragImportPlaylists.topBar = _fragTopBarImport;
_fragSubGroupList.topBar = _fragTopBarAdd;
_fragLibrary.topBar = _fragTopBarGeneral;
_fragLibraryAlbums.topBar = _fragTopBarNavigation;
_fragLibraryAlbum.topBar = _fragTopBarNavigation;
_fragLibraryArtists.topBar = _fragTopBarNavigation;
_fragLibraryArtist.topBar = _fragTopBarNavigation;
_fragLibraryVideos.topBar = _fragTopBarNavigation;
_fragBrowser.topBar = _fragTopBarNavigation;
@@ -1273,6 +1297,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
BuyFragment::class -> _fragBuy as T;
SubscriptionGroupFragment::class -> _fragSubGroup as T;
SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
LibraryFragment::class -> _fragLibrary as T;
LibraryAlbumsFragment::class -> _fragLibraryAlbums as T;
LibraryAlbumFragment::class -> _fragLibraryAlbum as T;
LibraryArtistsFragment::class -> _fragLibraryArtists as T;
LibraryArtistFragment::class -> _fragLibraryArtist as T;
LibraryVideosFragment::class -> _fragLibraryVideos as T;
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
}
}
@@ -1,5 +1,149 @@
package com.futo.platformplayer.api.media.platforms.local
class LocalClient {
//TODO
import android.content.ContentResolver
import android.net.Uri
import android.provider.MediaStore
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformClientCapabilities
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.states.StateLibrary
import java.net.MalformedURLException
class LocalClient: IPlatformClient {
override val id: String = "LOCAL"
override val name: String = "Local"
override val icon: ImageVariable? = ImageVariable.fromResource(R.drawable.ic_library)
override val capabilities: PlatformClientCapabilities = PlatformClientCapabilities()
override fun initialize() {}
override fun disable() {
}
override fun getHome(): IPager<IPlatformContent>
= EmptyPager();
override fun isContentDetailsUrl(url: String): Boolean {
try {
val uri = Uri.parse(url);
return ContentResolver.SCHEME_CONTENT == uri.scheme
&& MediaStore.AUTHORITY == uri.authority;
}
catch(ex: MalformedURLException) {
return false;
}
}
override fun getContentDetails(url: String): IPlatformContentDetails {
val uri = Uri.parse(url);
if("audio" in uri.pathSegments) {
return StateLibrary.getAudioTrack(url) ?: throw Exception("Failed to find ${url}");
}
else if("video" in uri.pathSegments) {
return StateLibrary.getVideoTrack(url) ?: throw Exception("Failed to find ${url}");
}
else
throw Exception("Unknown content url [${url}]");
}
override fun getSearchCapabilities(): ResultCapabilities
= ResultCapabilities();
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> {
return EmptyPager(); //TODO
}
override fun getSearchChannelContentsCapabilities(): ResultCapabilities
= ResultCapabilities();
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> {
return EmptyPager(); //TODO
}
override fun searchChannels(query: String): IPager<PlatformAuthorLink> {
return EmptyPager(); //TODO
}
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> {
return EmptyPager(); //TODO
}
override fun isChannelUrl(url: String): Boolean {
return false //TODO
}
override fun getChannel(channelUrl: String): IPlatformChannel {
throw NotImplementedError();
}
override fun getChannelCapabilities(): ResultCapabilities
= ResultCapabilities();
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> {
return EmptyPager();
}
override fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist> {
return EmptyPager();
}
override fun getPeekChannelTypes(): List<String> = listOf();
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent>
= listOf();
override fun getShorts(): IPager<IPlatformVideo> = EmptyPager();
override fun searchSuggestions(query: String): Array<String> = arrayOf();
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String?
= null;
override fun getContentChapters(url: String): List<IChapter>
= listOf();
override fun getPlaybackTracker(url: String): IPlaybackTracker?
= null;
override fun getContentRecommendations(url: String): IPager<IPlatformContent>?
= null;
override fun getComments(url: String): IPager<IPlatformComment>
= EmptyPager();
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment>
= EmptyPager();
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor?
= null;
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>?
= null;
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent>
= throw NotImplementedError();
override fun isPlaylistUrl(url: String): Boolean = false;
override fun getPlaylist(url: String): IPlatformPlaylistDetails
= throw NotImplementedError();
override fun getUserPlaylists(): Array<String> = throw NotImplementedError();
override fun getUserSubscriptions(): Array<String> = throw NotImplementedError();
override fun getUserHistory(): IPager<IPlatformContent> = throw NotImplementedError();
override fun isClaimTypeSupported(claimType: Int): Boolean = false;
}
@@ -55,7 +55,7 @@ class PackageDOMParser : V8Package {
}
@V8Property
fun lastChild(): DOMNode? {
val result = _element.firstElementChild()?.let { DOMNode(_package, it) };
val result = _element.lastElementChild()?.let { DOMNode(_package, it) };
if(result != null)
_children.add(result);
return result;
@@ -389,6 +389,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
}
}),
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>(withHistory = false) }),
ButtonDefinition(12, R.drawable.ic_library, R.drawable.ic_library, R.string.library, canToggle = false, { it.currentMain is LibraryFragment }, { it.navigate<LibraryFragment>(withHistory = false) }),
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>(withHistory = false) }),
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>(withHistory = false) }),
ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>(withHistory = false) }),
@@ -64,7 +64,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
_exoPlayer = player;
return PreviewContentListAdapter(fragment.lifecycleScope, context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
return PreviewContentListAdapter(fragment.lifecycleScope, context, feedStyle ?: FeedStyle.THUMBNAIL, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
attachAdapterEvents(this);
}
}
@@ -0,0 +1,123 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.provider.MediaStore
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.Album
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateLibrary
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
class LibraryAlbumFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var view: FragView? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): android.view.View {
val newView = FragView(this, inflater);
view = newView;
return newView;
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
view?.onShown(parameter);
}
override fun onDestroyMainView() {
view = null;
super.onDestroyMainView();
}
companion object {
fun newInstance() = LibraryAlbumFragment().apply {}
}
class FragView: VideoListEditorView {
val fragment: LibraryAlbumFragment;
private var _album: Album? = null;
private var _tracks: List<IPlatformVideo>? = null;
private var _url: String? = null;
constructor(fragment: LibraryAlbumFragment, inflater: LayoutInflater) : super(inflater) {
this.fragment = fragment;
}
fun onShown(parameter: Any? = null) {
val album = if(parameter is String)
StateLibrary.instance.getAlbum(parameter);
else if(parameter is Long)
StateLibrary.instance.getAlbum(parameter);
else if(parameter is Album)
parameter;
else null;
if(album == null) {
_album = null;
_tracks = null;
setVideos(listOf(), false);
return;
}
setName(album.name);
val tracks = album.getTracks();
_album = album;
_tracks = tracks;
setMetadata(tracks.size, if(tracks.size > 0) tracks.sumOf { it.duration } else -1);
setVideos(tracks, false, album.thumbnail);
}
override fun onPlayAllClick() {
val playlist = _album?.toPlaylist(_tracks);
if (playlist != null) {
StatePlayer.instance.setPlaylist(playlist, focus = true);
}
}
override fun onShuffleClick() {
val playlist = _album?.toPlaylist(_tracks);
if (playlist != null) {
StatePlayer.instance.setPlaylist(playlist, focus = true, shuffle = true);
}
}
override fun onVideoOptions(video: IPlatformVideo) {
UISlideOverlays.showVideoOptionsOverlay(video, overlayContainer);
}
override fun onVideoClicked(video: IPlatformVideo) {
val playlist = _album?.toPlaylist(_tracks);
if (playlist != null) {
val index = playlist.videos.indexOf(video);
if (index == -1)
return;
StatePlayer.instance.setPlaylist(playlist, index, true);
}
}
}
}
@@ -0,0 +1,160 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Context
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout.GONE
import android.widget.LinearLayout.VISIBLE
import android.widget.Spinner
import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.structures.AdhocPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.Album
import com.futo.platformplayer.states.StateLibrary
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.platform.PlatformIndicator
class LibraryAlbumsFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
var view: FragView? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = FragView(this, inflater);
this.view = view;
return view;
}
override fun onShown(parameter: Any?, isBack: Boolean) {
super.onShown(parameter, isBack)
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
view?.onShown();
}
override fun onDestroyMainView() {
view = null;
super.onDestroyMainView();
}
companion object {
fun newInstance() = LibraryAlbumsFragment().apply {}
}
class FragView : FeedView<LibraryAlbumsFragment, Album, Album, IPager<Album>, AlbumViewHolder> {
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
constructor(fragment: LibraryAlbumsFragment, inflater: LayoutInflater) : super(fragment, inflater)
fun onShown() {
val initialAlbums = StateLibrary.instance.getAlbums();
Logger.i(TAG, "Initial album count: " + initialAlbums.size);
setPager(AdhocPager<Album>({ listOf(); }, initialAlbums));
}
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<Album>): InsertedViewAdapterWithLoader<AlbumViewHolder> {
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
childCountGetter = { dataset.size },
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
childViewHolderFactory = { viewGroup, _ ->
val holder = AlbumViewHolder(viewGroup);
holder.onClick.subscribe { c -> fragment.navigate<LibraryAlbumFragment>(c) };
return@InsertedViewAdapterWithLoader holder;
}
);
}
override fun updateSpanCount(){ }
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
val glmResults = GridLayoutManager(context, 1)
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
rightMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8.0f,
context.resources.displayMetrics
).toInt()
}
return glmResults
}
companion object {
private const val TAG = "LibraryAlbumsFragmentsView";
}
}
class AlbumViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<Album>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_album,
_viewGroup, false)) {
val onClick = Event1<Album?>();
protected var _album: Album? = null;
protected val _imageThumbnail: ImageView
protected val _textName: TextView
protected val _textMetadata: TextView
init {
_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
_textName = _view.findViewById(R.id.text_name);
_textMetadata = _view.findViewById(R.id.text_metadata);
_view.setOnClickListener { onClick.emit(_album) };
}
override fun bind(album: Album) {
_album = album;
_imageThumbnail?.let {
if (album.thumbnail != null)
Glide.with(it)
.load(album.thumbnail)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(it)
else
Glide.with(it).load(R.drawable.placeholder_channel_thumbnail).into(it);
};
_textName.text = album.name;
_textMetadata.text = album.artist ?: "";
}
}
}
@@ -0,0 +1,576 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Context
import android.graphics.drawable.Animatable
import android.net.Uri
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.LinearLayout.GONE
import android.widget.LinearLayout.VISIBLE
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.allViews
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
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.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.local.LocalClient
import com.futo.platformplayer.api.media.structures.AdhocPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.assume
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.Event3
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelMonetizationFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment
import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment.Companion
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.selectHighestResolutionImage
import com.futo.platformplayer.states.Album
import com.futo.platformplayer.states.Artist
import com.futo.platformplayer.states.StateLibrary
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.ToggleBar
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.ChannelTab
import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.platform.PlatformIndicator
import com.futo.platformplayer.views.subscriptions.SubscribeButton
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class LibraryArtistFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var _textMeta: TextView? = null;
var view: FragView? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = FragView(this, inflater);
this.view = view;
return view;
}
override fun onShown(parameter: Any?, isBack: Boolean) {
super.onShown(parameter, isBack)
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
view?.onShown(parameter, isBack);
}
override fun onDestroyMainView() {
view = null;
super.onDestroyMainView();
}
companion object {
fun newInstance() = LibraryArtistFragment().apply {}
}
class FragView(fragment: LibraryArtistFragment, inflater: LayoutInflater) : LinearLayout(inflater.context) {
private val _fragment: LibraryArtistFragment = fragment
private var _textChannel: TextView
private var _textChannelSub: TextView
private var _creatorThumbnail: CreatorThumbnail
private var _imageBanner: AppCompatImageView
private var _tabs: TabLayout
private var _viewPager: ViewPager2
// private var _adapter: ChannelViewPagerAdapter;
private var _tabLayoutMediator: TabLayoutMediator
private var _buttonSubscribe: SubscribeButton
private var _buttonSubscriptionSettings: ImageButton
private var _overlayContainer: FrameLayout
private var _overlayLoading: LinearLayout
private var _overlayLoadingSpinner: ImageView
private var _slideUpOverlay: SlideUpMenuOverlay? = null
private var _isLoading: Boolean = false
private var _selectedTabIndex: Int = -1
var channel: Artist? = null
private set
private var _url: String? = null
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
init {
inflater.inflate(R.layout.fragment_channel, this)
val tabs: TabLayout = findViewById(R.id.tabs)
val viewPager: ViewPager2 = findViewById(R.id.view_pager)
_textChannel = findViewById(R.id.text_channel_name)
_textChannelSub = findViewById(R.id.text_metadata)
_creatorThumbnail = findViewById(R.id.creator_thumbnail)
_imageBanner = findViewById(R.id.image_channel_banner)
_buttonSubscribe = findViewById(R.id.button_subscribe)
_buttonSubscriptionSettings = findViewById(R.id.button_sub_settings)
_overlayLoading = findViewById(R.id.channel_loading_overlay)
_overlayLoadingSpinner = findViewById(R.id.channel_loader_frag)
_overlayContainer = findViewById(R.id.overlay_container)
_buttonSubscribe.onSubscribed.subscribe {
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer)
_buttonSubscriptionSettings.visibility =
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
}
_buttonSubscribe.onUnSubscribed.subscribe {
_buttonSubscriptionSettings.visibility =
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
}
_buttonSubscriptionSettings.setOnClickListener {
val url = channel?.contentUrl ?: _url ?: return@setOnClickListener
val sub =
StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener
UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer)
}
//TODO: Determine if this is really the only solution (isSaveEnabled=false)
viewPager.isSaveEnabled = false
viewPager.registerOnPageChangeCallback(_onPageChangeCallback)
val adapter = ArtistViewPagerAdapter(fragment, fragment.childFragmentManager, fragment.lifecycle)
adapter.onChannelClicked.subscribe { c -> fragment.navigate<ChannelFragment>(c) }
adapter.onContentClicked.subscribe { v, _ ->
when (v) {
is IPlatformVideo -> {
StatePlayer.instance.clearQueue()
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail()
}
is IPlatformPlaylist -> {
fragment.navigate<RemotePlaylistFragment>(v)
}
is IPlatformPost -> {
fragment.navigate<PostDetailFragment>(v)
}
}
}
adapter.onShortClicked.subscribe { v, _, pagerPair ->
when (v) {
is IPlatformVideo -> {
StatePlayer.instance.clearQueue()
fragment.navigate<ShortsFragment>(Triple(v, pagerPair!!.first, pagerPair.second))
}
}
}
adapter.onAddToClicked.subscribe { content ->
_overlayContainer.let {
if (content is IPlatformVideo) _slideUpOverlay =
UISlideOverlays.showVideoOptionsOverlay(content, it)
}
}
adapter.onAddToQueueClicked.subscribe { content ->
if (content is IPlatformVideo) {
StatePlayer.instance.addToQueue(content)
}
}
adapter.onAddToWatchLaterClicked.subscribe { content ->
if (content is IPlatformVideo) {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
UIDialogs.toast("Added to watch later\n[${content.name}]")
else
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
}
}
adapter.onUrlClicked.subscribe { url ->
fragment.navigate<BrowserFragment>(url)
}
adapter.onContentUrlClicked.subscribe { url, contentType ->
when (contentType) {
ContentType.MEDIA -> {
StatePlayer.instance.clearQueue()
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
}
ContentType.URL -> fragment.navigate<BrowserFragment>(url)
else -> {}
}
}
adapter.onLongPress.subscribe { content ->
_overlayContainer.let {
if (content is IPlatformVideo) _slideUpOverlay =
UISlideOverlays.showVideoOptionsOverlay(content, it)
}
}
viewPager.adapter = adapter
val tabLayoutMediator = TabLayoutMediator(
tabs, viewPager, (viewPager.adapter as ArtistViewPagerAdapter)::getTabNames
)
tabLayoutMediator.attach()
_tabLayoutMediator = tabLayoutMediator
_tabs = tabs
_viewPager = viewPager
if (_selectedTabIndex != -1) {
selectTab(_selectedTabIndex)
}
setLoading(true)
}
fun selectTab(tab: ArtistTab) {
(_viewPager.adapter as ArtistViewPagerAdapter).getTabPosition(tab)
}
fun cleanup() {
_tabLayoutMediator.detach()
_viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback)
hideSlideUpOverlay()
(_overlayLoadingSpinner.drawable as Animatable?)?.stop()
}
fun onShown(parameter: Any?, isBack: Boolean) {
hideSlideUpOverlay()
_selectedTabIndex = -1
if (!isBack || _url == null) {
_imageBanner.setImageDrawable(null)
when (parameter) {
is String -> {
_buttonSubscribe.setSubscribeChannel(parameter)
_buttonSubscriptionSettings.visibility =
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
_url = parameter
val parsed = Uri.parse(parameter);
val idLong = parsed.lastPathSegment?.toLongOrNull();
if(idLong != null) {
val artist = StateLibrary.instance.getArtist(idLong) ?: return;
showArtist(artist);
}
}
is Artist -> {
showArtist(parameter)
_url = parameter.contentUrl
}
}
}
}
private fun selectTab(selectedTabIndex: Int) {
_selectedTabIndex = selectedTabIndex
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
}
private fun setLoading(isLoading: Boolean) {
if (_isLoading == isLoading) {
return
}
_isLoading = isLoading
if (isLoading) {
_overlayLoading.visibility = View.VISIBLE
(_overlayLoadingSpinner.drawable as Animatable?)?.start()
} else {
(_overlayLoadingSpinner.drawable as Animatable?)?.stop()
_overlayLoading.visibility = View.GONE
}
}
fun onBackPressed(): Boolean {
if (_slideUpOverlay != null) {
hideSlideUpOverlay()
return true
}
return false
}
private fun hideSlideUpOverlay() {
_slideUpOverlay?.hide(false)
_slideUpOverlay = null
}
private fun showArtist(channel: Artist) {
setLoading(false)
_fragment.topBar?.onShown(channel)
val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) {
})
_fragment.lifecycleScope.launch(Dispatchers.IO) {
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.contentUrl ?: return@launch)
withContext(Dispatchers.Main) {
buttons.add(Pair(R.drawable.ic_search) {
_fragment.navigate<SuggestionsFragment>(
SuggestionsFragmentData(
"", SearchType.VIDEO
)
)
})
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons)
}
}
_buttonSubscribe.visibility = GONE;
_buttonSubscriptionSettings.visibility =
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
_textChannel.text = channel.name
_textChannelSub.text = ""
var supportsPlaylists = false;
val playlistPosition = 1
if (supportsPlaylists && !(_viewPager.adapter as ArtistViewPagerAdapter).containsItem(
ArtistTab.PLAYLISTS.ordinal.toLong()
)
) {
// keep the current tab selected
if (_viewPager.currentItem >= playlistPosition) {
_viewPager.setCurrentItem(_viewPager.currentItem + 1, false)
}
(_viewPager.adapter as ArtistViewPagerAdapter).insert(
playlistPosition,
ArtistTab.PLAYLISTS
)
}
if (!supportsPlaylists && (_viewPager.adapter as ArtistViewPagerAdapter).containsItem(
ArtistTab.PLAYLISTS.ordinal.toLong()
)
) {
// keep the current tab selected
if (_viewPager.currentItem >= playlistPosition) {
_viewPager.setCurrentItem(_viewPager.currentItem - 1, false)
}
(_viewPager.adapter as ArtistViewPagerAdapter).remove(playlistPosition)
}
// sets the channel for each tab
for (fragment in _fragment.childFragmentManager.fragments) {
(fragment as IArtistTabFragment).setArtist(channel)
}
(_viewPager.adapter as ArtistViewPagerAdapter).artist = channel
_viewPager.adapter!!.notifyDataSetChanged()
this.channel = channel
}
companion object {
private const val TAG = "LibraryArtistFragmentsView";
}
}
enum class ArtistTab {
VIDEOS, PLAYLISTS
}
class ArtistViewPagerAdapter(private val fragment: LibraryArtistFragment, fragmentManager: FragmentManager, lifecycle: Lifecycle) :
FragmentStateAdapter(fragmentManager, lifecycle) {
private val _supportedFragments = mutableMapOf(
ArtistTab.VIDEOS.ordinal to ArtistTab.VIDEOS
)
private val _tabs = arrayListOf(ArtistTab.VIDEOS)
var artist: Artist? = null
val onContentUrlClicked = Event2<String, ContentType>()
val onUrlClicked = Event1<String>()
val onContentClicked = Event2<IPlatformContent, Long>()
val onShortClicked = Event3<IPlatformContent, Long, Pair<IPager<IPlatformContent>, ArrayList<IPlatformContent>>?>()
val onChannelClicked = Event1<PlatformAuthorLink>()
val onAddToClicked = Event1<IPlatformContent>()
val onAddToQueueClicked = Event1<IPlatformContent>()
val onAddToWatchLaterClicked = Event1<IPlatformContent>()
val onLongPress = Event1<IPlatformContent>()
override fun getItemId(position: Int): Long {
return _tabs[position].ordinal.toLong()
}
override fun containsItem(itemId: Long): Boolean {
return _supportedFragments.containsKey(itemId.toInt())
}
override fun getItemCount(): Int {
return _supportedFragments.size
}
fun getTabPosition(tab: ArtistTab): Int {
return _tabs.indexOf(tab)
}
fun getTabNames(tab: TabLayout.Tab, position: Int) {
tab.text = _tabs[position].name
}
fun insert(position: Int, tab: ArtistTab) {
_supportedFragments[tab.ordinal] = tab
_tabs.add(position, tab)
notifyItemInserted(position)
}
fun remove(position: Int) {
_supportedFragments.remove(_tabs[position].ordinal)
_tabs.removeAt(position)
notifyItemRemoved(position)
}
override fun createFragment(position: Int): Fragment {
val fragment: Fragment
when (_tabs[position]) {
ArtistTab.VIDEOS -> {
fragment = ChannelContentsFragment(this.fragment).apply {
/*
onContentClicked.subscribe { video, num, _ ->
this@ArtistViewPagerAdapter.onContentClicked.emit(video, num)
}
onContentUrlClicked.subscribe(this@ArtistViewPagerAdapter.onContentUrlClicked::emit)
onUrlClicked.subscribe(this@ArtistViewPagerAdapter.onUrlClicked::emit)
onChannelClicked.subscribe(this@ArtistViewPagerAdapter.onChannelClicked::emit)
onAddToClicked.subscribe(this@ArtistViewPagerAdapter.onAddToClicked::emit)
onAddToQueueClicked.subscribe(this@ArtistViewPagerAdapter.onAddToQueueClicked::emit)
onAddToWatchLaterClicked.subscribe(this@ArtistViewPagerAdapter.onAddToWatchLaterClicked::emit)
onLongPress.subscribe(this@ArtistViewPagerAdapter.onLongPress::emit)
*/
}
}
ArtistTab.PLAYLISTS -> {
fragment = ChannelPlaylistsFragment.newInstance().apply {
/*
onContentClicked.subscribe(this@ArtistViewPagerAdapter.onContentClicked::emit)
onContentUrlClicked.subscribe(this@ArtistViewPagerAdapter.onContentUrlClicked::emit)
onUrlClicked.subscribe(this@ArtistViewPagerAdapter.onUrlClicked::emit)
onChannelClicked.subscribe(this@ArtistViewPagerAdapter.onChannelClicked::emit)
onAddToClicked.subscribe(this@ArtistViewPagerAdapter.onAddToClicked::emit)
onAddToQueueClicked.subscribe(this@ArtistViewPagerAdapter.onAddToQueueClicked::emit)
onAddToWatchLaterClicked.subscribe(this@ArtistViewPagerAdapter.onAddToWatchLaterClicked::emit)
onLongPress.subscribe(this@ArtistViewPagerAdapter.onLongPress::emit)
*/
}
}
}
artist?.let { (fragment as IArtistTabFragment).setArtist(it) }
return fragment
}
}
interface IArtistTabFragment {
fun setArtist(artist: Artist);
}
class ChannelContentsFragment(private val frag: LibraryArtistFragment) : Fragment(), IArtistTabFragment {
var view: ArtistContentView? = null;
private var _lastArtist: Artist? = null;
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
view = ArtistContentView(frag, inflater);
_lastArtist?.let {
view?.setArtist(it);
}
return view;
}
override fun onDestroyView() {
view = null;
super.onDestroyView()
}
override fun setArtist(artist: Artist) {
view?.setArtist(artist);
_lastArtist = artist;
}
}
class ArtistContentView : ContentFeedView<LibraryArtistFragment> {
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
constructor(fragment: LibraryArtistFragment, inflater: LayoutInflater) : super(fragment, inflater) {
}
fun setArtist(artist: Artist) {
val tracks = artist.getAudioTracks();
if(tracks.getResults().isEmpty())
UIDialogs.appToast("No tracks found");
setPager(tracks);
}
override fun updateSpanCount(){ }
companion object {
private const val TAG = "LibraryAlbumsFragmentsView";
}
}
}
@@ -0,0 +1,161 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Context
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout.GONE
import android.widget.LinearLayout.VISIBLE
import android.widget.Spinner
import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.structures.AdhocPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.Album
import com.futo.platformplayer.states.Artist
import com.futo.platformplayer.states.StateLibrary
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.platform.PlatformIndicator
class LibraryArtistsFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var _textMeta: TextView? = null;
var view: FragView? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = FragView(this, inflater);
this.view = view;
return view;
}
override fun onShown(parameter: Any?, isBack: Boolean) {
super.onShown(parameter, isBack)
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
view?.onShown();
}
override fun onDestroyMainView() {
view = null;
super.onDestroyMainView();
}
companion object {
fun newInstance() = LibraryArtistsFragment().apply {}
}
class FragView : FeedView<LibraryArtistsFragment, Artist, Artist, IPager<Artist>, ArtistViewHolder> {
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
constructor(fragment: LibraryArtistsFragment, inflater: LayoutInflater) : super(fragment, inflater)
fun onShown() {
val intialArtists = StateLibrary.instance.getArtists();
Logger.i(TAG, "Initial album count: " + intialArtists.size);
setPager(AdhocPager<Artist>({ listOf(); }, intialArtists));
}
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<Artist>): InsertedViewAdapterWithLoader<ArtistViewHolder> {
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
childCountGetter = { dataset.size },
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
childViewHolderFactory = { viewGroup, _ ->
val holder = ArtistViewHolder(viewGroup);
holder.onClick.subscribe { c ->
fragment.navigate<LibraryArtistFragment>(c)
};
return@InsertedViewAdapterWithLoader holder;
}
);
}
override fun updateSpanCount(){ }
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
val glmResults = GridLayoutManager(context, 1)
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
rightMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8.0f,
context.resources.displayMetrics
).toInt()
}
return glmResults
}
companion object {
private const val TAG = "LibraryArtistsFragmentsView";
}
}
class ArtistViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<Artist>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_album,
_viewGroup, false)) {
val onClick = Event1<Artist>();
protected var _artist: Artist? = null;
protected val _imageThumbnail: ImageView
protected val _textName: TextView
protected val _textMetadata: TextView
init {
_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
_textName = _view.findViewById(R.id.text_name);
_textMetadata = _view.findViewById(R.id.text_metadata);
_view.setOnClickListener { _artist?.let { onClick.emit(it) } };
}
override fun bind(artist: Artist) {
_artist = artist;
_imageThumbnail?.let {
if (artist.thumbnail != null)
Glide.with(it)
.load(artist.thumbnail)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(it)
else
Glide.with(it).load(R.drawable.placeholder_channel_thumbnail).into(it);
};
_textName.text = artist.name;
_textMetadata.text = artist.countTracks?.let { "${it} tracks" } ?: "";
}
}
}
@@ -0,0 +1,79 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.provider.MediaStore
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.futo.platformplayer.R
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.buttons.BigButton
class LibraryFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var view: FragView? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): android.view.View {
val newView = FragView(this);
view = newView;
return newView;
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
view?.onShown();
}
override fun onDestroyMainView() {
view = null;
super.onDestroyMainView();
}
companion object {
fun newInstance() = LibraryFragment().apply {}
}
class FragView: ConstraintLayout {
val fragment: LibraryFragment;
constructor(fragment: LibraryFragment, attrs : AttributeSet? = null) : super(fragment.requireContext(), attrs) {
inflate(context, R.layout.fragview_library, this);
this.fragment = fragment;
val buttonArtists = findViewById<BigButton>(R.id.button_artists);
val buttonAlbums = findViewById<BigButton>(R.id.button_albums);
val buttonVideos = findViewById<BigButton>(R.id.button_videos);
val buttonPlaylists = findViewById<BigButton>(R.id.button_playlists);
val buttonFiles = findViewById<BigButton>(R.id.button_files);
buttonArtists.onClick.subscribe {
fragment.navigate<LibraryArtistsFragment>();
}
buttonAlbums.onClick.subscribe {
fragment.navigate<LibraryAlbumsFragment>();
}
buttonVideos.onClick.subscribe {
fragment.navigate<LibraryVideosFragment>();
}
buttonPlaylists.onClick.subscribe {
fragment.navigate<PlaylistsFragment>();
}
buttonFiles.onClick.subscribe {
}
}
fun onShown() {
}
}
}
@@ -0,0 +1,169 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Context
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout.GONE
import android.widget.LinearLayout.VISIBLE
import android.widget.Spinner
import android.widget.TextView
import androidx.core.view.allViews
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.UISlideOverlays.Companion.showOrderOverlay
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.AdhocPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.Album
import com.futo.platformplayer.states.StateLibrary
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.ToggleBar
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.platform.PlatformIndicator
class LibraryVideosFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var _toggleBuckets = StateLibrary.instance.getVideoBucketNames().map { it.name }.toMutableList();
var view: FragView? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = FragView(this, inflater);
this.view = view;
return view;
}
override fun onShown(parameter: Any?, isBack: Boolean) {
super.onShown(parameter, isBack)
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
view?.onShown();
}
override fun onDestroyMainView() {
view = null;
super.onDestroyMainView();
}
companion object {
fun newInstance() = LibraryVideosFragment().apply {}
}
class FragView : ContentFeedView<LibraryVideosFragment> {
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
private var _toggleBar: ToggleBar? = null;
constructor(fragment: LibraryVideosFragment, inflater: LayoutInflater) : super(fragment, inflater) {
initializeToolbarContent();
}
fun onShown() {
val initialAlbums = StateLibrary.instance.getAlbums();
Logger.i(TAG, "Initial album count: " + initialAlbums.size);
val buckets = StateLibrary.instance.getVideoBucketNames();
setPager(StateLibrary.instance.getVideos(fragment._toggleBuckets));
}
private val _filterLock = Object();
fun initializeToolbarContent() {
if(_toolbarContentView.allViews.any { it is ToggleBar })
_toolbarContentView.removeView(_toolbarContentView.allViews.find { it is ToggleBar });
_toggleBar = ToggleBar(context).apply {
layoutParams =
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
synchronized(_filterLock) {
var buttonsPlugins: List<ToggleBar.Toggle> = listOf()
buttonsPlugins =
(StateLibrary.instance.getVideoBucketNames()
.map { bucket ->
ToggleBar.Toggle(bucket.name, null, fragment._toggleBuckets.contains(bucket.name), { view, active ->
var dontSwap = false;
if (!active) {
if (fragment._toggleBuckets.contains(bucket.name))
fragment._toggleBuckets.remove(bucket.name);
} else {
if (!fragment._toggleBuckets.contains(bucket.name)) {
val enabledClients = StatePlatform.instance.getEnabledClients();
val availableAfterDisable = enabledClients.count { !fragment._toggleBuckets.contains(it.id) && it.id != bucket.name };
if(availableAfterDisable > 0)
fragment._toggleBuckets.add(bucket.name);
else {
UIDialogs.appToast("Select atleast 1 bucket");
dontSwap = true;
}
}
}
if(!dontSwap)
reloadForFilters();
else {
view.setToggle(active);
}
}, { view, views, enabled ->
val toDisable = views.filter { it != view && it.tag == "plugins" };
if(!view.isActive)
view.handleClick();
for(tag in toDisable) {
if(tag.isActive)
tag.handleClick();
}
}).withTag("plugins")
})
val buttons = (buttonsPlugins)
.sortedBy { it.name }.toTypedArray()
_toggleBar?.setToggles(*buttons);
}
_toolbarContentView.addView(_toggleBar, 0);
}
fun reloadForFilters() {
setPager(StateLibrary.instance.getVideos(fragment._toggleBuckets));
}
override fun updateSpanCount(){ }
companion object {
private const val TAG = "LibraryAlbumsFragmentsView";
}
}
}
@@ -0,0 +1,52 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.Spinner
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import com.futo.platformplayer.R
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
class RecyclerFragment : MainFragment(){
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var view: View? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): android.view.View {
val newView = RecyclerFragment.View(inflater.context);
view = newView;
return newView;
}
override fun onDestroyMainView() {
view = null;
super.onDestroyMainView();
}
companion object {
fun newInstance() = RecyclerFragment().apply {}
}
class View: ConstraintLayout {
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.fragview_filter_recycler, this);
}
}
}
@@ -20,6 +20,7 @@ import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.assume
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.images.GlideHelper
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlaylists
@@ -194,22 +195,35 @@ abstract class VideoListEditorView : LinearLayout {
_textMetadata.text = parts.joinToString("");
}
protected fun setVideos(videos: List<IPlatformVideo>?, canEdit: Boolean) {
if (videos != null && videos.isNotEmpty()) {
val video = videos.first();
protected fun setVideos(videos: List<IPlatformVideo>?, canEdit: Boolean, thumbnail: String? = null) {
if(thumbnail != null) {
_imagePlaylistThumbnail.let {
Glide.with(it)
.load(video.thumbnails.getHQThumbnail())
.load(thumbnail)
.placeholder(R.drawable.placeholder_video_thumbnail)
.crossfade()
.into(it);
};
} else {
_textMetadata.text = "0 " + context.getString(R.string.videos);
Glide.with(_imagePlaylistThumbnail)
.load(R.drawable.placeholder_video_thumbnail)
.into(_imagePlaylistThumbnail)
}
}
else {
if (videos != null && videos.isNotEmpty()) {
val video = videos.first();
_imagePlaylistThumbnail.let {
Glide.with(it)
.load(video.thumbnails.getHQThumbnail())
.placeholder(R.drawable.placeholder_video_thumbnail)
.crossfade()
.into(it);
};
} else {
Glide.with(_imagePlaylistThumbnail)
.load(R.drawable.placeholder_video_thumbnail)
.into(_imagePlaylistThumbnail)
}
}
if(videos == null || videos.isEmpty())
_textMetadata.text = "0 " + context.getString(R.string.videos);
_loadedVideos = videos;
_loadedVideosCanEdit = canEdit;
_videoListEditorView.setVideos(videos, canEdit);
@@ -29,7 +29,6 @@ class GlideHelper {
req.into(this);
}
fun RequestBuilder<Drawable>.crossfade(): RequestBuilder<Drawable> {
return this.transition(DrawableTransitionOptions.withCrossFade());
}
@@ -1,11 +1,14 @@
package com.futo.platformplayer.images;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
import java.io.InputStream;
import java.nio.ByteBuffer;
@GlideModule
@@ -14,5 +17,8 @@ public class GrayjayAppGlideModule extends AppGlideModule {
public void registerComponents(Context context, Glide glide, Registry registry) {
Log.i("GrayjayAppGlideModule", "registerComponents called");
registry.prepend(String.class, ByteBuffer.class, new PolycentricModelLoader.Factory());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
registry.prepend(String.class, InputStream.class, new MediaStoreThumbnailLoader.InputStreamFactory());
}
}
}
@@ -0,0 +1,74 @@
package com.futo.platformplayer.images
import android.content.ContentResolver
import android.graphics.Point
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.LocalUriFetcher
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.signature.ObjectKey
import com.futo.platformplayer.states.StateApp
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.net.MalformedURLException
@RequiresApi(Build.VERSION_CODES.Q)
class MediaStoreThumbnailLoader private constructor() : ModelLoader<String, InputStream> {
override fun handles(model: String): Boolean = isMediaStoreAudioUri(model)
private fun isMediaStoreAudioUri(uri: String): Boolean {
try {
val parsed = Uri.parse(uri);
return ContentResolver.SCHEME_CONTENT == parsed.scheme
&& MediaStore.AUTHORITY == parsed.authority
&& "audio" in parsed.pathSegments
}
catch(ex: MalformedURLException) {
return false;
}
}
override fun buildLoadData(model: String, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
val diskCacheKey = ObjectKey(model)
val resolver = StateApp.instance.contextOrNull?.contentResolver ?: return null;
val fetcher = InputStreamFetcher(resolver, Uri.parse(model), width, height)
return ModelLoader.LoadData(diskCacheKey, fetcher)
}
class InputStreamFactory() : ModelLoaderFactory<String, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<String, InputStream> = MediaStoreThumbnailLoader()
override fun teardown() {
// Do nothing.
}
}
private class InputStreamFetcher(resolver: ContentResolver, uri: Uri, private val width: Int, private val height: Int) : LocalUriFetcher<InputStream>(resolver, uri) {
override fun getDataClass(): Class<InputStream> = InputStream::class.java
@Throws(FileNotFoundException::class)
override fun loadResource(uri: Uri, contentResolver: ContentResolver): InputStream {
val optimalSizeOptions = Bundle(1)
optimalSizeOptions.putParcelable(ContentResolver.EXTRA_SIZE, Point(width, height))
return contentResolver.openTypedAssetFile(uri, "image/*", optimalSizeOptions, null)
?.createInputStream()
?: throw FileNotFoundException("FileDescriptor is null for: $uri")
}
@Throws(IOException::class)
override fun close(data: InputStream) {
data.close()
}
}
}
@@ -0,0 +1,442 @@
package com.futo.platformplayer.states
import android.content.ContentUris
import android.database.Cursor
import android.net.Uri
import android.provider.MediaStore
import android.provider.MediaStore.Audio.Artists
import android.provider.MediaStore.Images.ImageColumns
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnail
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
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.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.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.Album.Companion
import com.futo.platformplayer.states.Album.Companion.TAG
import com.futo.platformplayer.states.StateLibrary.Companion.getAudioTrack
import com.futo.platformplayer.states.StateLibrary.Companion.videoFromCursor
import com.google.protobuf.Empty
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
class StateLibrary {
fun getAlbums(): List<Album> {
return Album.getAlbums();
}
fun getAlbum(str: String): Album? {
val idLong = str.toLongOrNull();
if(idLong != null)
return getAlbum(idLong);
return null;
}
fun getAlbum(id: Long): Album? {
return Album.getAlbum(id);
}
fun getArtists(): List<Artist> {
return Artist.getArtists();
}
fun getArtist(str: String): Artist? {
val idLong = str.toLongOrNull();
if(idLong != null)
return getArtist(idLong);
return null;
}
fun getArtist(id: Long): Artist? {
return Artist.getArtist(id);
}
fun getVideos(buckets: List<String>? = null): IPager<IPlatformContent> {
val cursor = StateApp.instance.contextOrNull?.contentResolver?.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, PROJECTION_VIDEO,
if(buckets != null) "${MediaStore.Video.Media.BUCKET_DISPLAY_NAME} IN " + "(" + buckets.map { "'${it}'" }.joinToString(",") + ")" else null,
null,
MediaStore.Video.Media.DATE_ADDED + " DESC") ?: return EmptyPager();
cursor.moveToFirst();
val list = mutableListOf<IPlatformVideo>()
while(!cursor.isAfterLast && list.size < 10) {
list.add(videoFromCursor(cursor));
cursor.moveToNext();
}
return AdhocPager<IPlatformContent>({
val list = mutableListOf<IPlatformContent>()
while(!cursor.isAfterLast && list.size < 10) {
list.add(videoFromCursor(cursor));
cursor.moveToNext();
}
return@AdhocPager list;
}, list);
}
private var _cacheBucketNames: List<Bucket>? = null;
fun getVideoBucketNames(): List<Bucket> {
if(_cacheBucketNames != null)
return _cacheBucketNames ?: listOf();
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();
val buckets = mutableListOf<Bucket>();
val list = HashSet<Long>();
if (cur.moveToFirst()) {
var id: Long;
var bucket: String
do {
id = cur.getLong(0);
bucket = cur.getString(1)
if(!list.contains(id)) {
list.add(id);
buckets.add(Bucket(id, bucket));
}
} while (cur.moveToNext())
}
_cacheBucketNames = buckets.toList()
return _cacheBucketNames ?: listOf();
}
companion object {
val PROJECTION_VIDEO = arrayOf(
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.AUTHOR,
MediaStore.Video.Media.DATE_ADDED,
MediaStore.Video.Media.MIME_TYPE,
MediaStore.Video.Media.BUCKET_DISPLAY_NAME
);
val PROJECTION_MEDIA = arrayOf(
MediaStore.Audio.Media._ID, //0
MediaStore.Audio.Media.DISPLAY_NAME, //1
MediaStore.Audio.Media.ARTIST, //2
MediaStore.Audio.Media.ALBUM_ID, //3
MediaStore.Audio.Media.DURATION, //4
MediaStore.Audio.Media.DATE_ADDED, //5
MediaStore.Audio.Media.MIME_TYPE, //6
MediaStore.Audio.Media.BUCKET_DISPLAY_NAME //7
);
fun getAudioTrack(url: String): IPlatformContentDetails? {
val uri = Uri.parse(url);
val id = uri.lastPathSegment?.toLongOrNull();
if(id == null)
return null;
val resolver = StateApp.instance.contextOrNull?.contentResolver;
if(resolver == null) {
Logger.w(TAG, "Album contentResolver not found");
return null;
}
val cursor = resolver?.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_MEDIA, "${MediaStore.Audio.Media._ID} = ?", arrayOf(id.toString()),
null) ?: return null;
cursor.moveToFirst();
if(cursor.isAfterLast)
return null;
return audioFromCursor(cursor);
}
fun getVideoTrack(url: String): IPlatformContentDetails? {
val uri = Uri.parse(url);
val id = uri.lastPathSegment?.toLongOrNull();
if(id == null)
return null;
val resolver = StateApp.instance.contextOrNull?.contentResolver;
if(resolver == null) {
Logger.w(TAG, "Album contentResolver not found");
return null;
}
val cursor = resolver?.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_VIDEO, "${MediaStore.Video.Media._ID} = ?", arrayOf(id.toString()),
null) ?: return null;
cursor.moveToFirst();
if(cursor.isAfterLast)
return null;
return videoFromCursor(cursor);
}
fun audioFromCursor(cursor: Cursor): IPlatformVideoDetails {
val id = cursor.getString(0);
val displayName = cursor.getString(1);
val author = cursor.getString(2);
val albumId = cursor.getLong(3);
val duration = cursor.getLong(4).let { if(it > 0) it / 1000 else 0 };
val date = cursor.getLong(5);
val contentType = cursor.getString(6);
val category = cursor.getString(7);
val idLong = id.toLongOrNull();
val contentUrl = if(idLong != null )
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, idLong).toString();
else
"";
val albumContentUrl = if(albumId > 0)
ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, albumId)?.toString()
else null;
val dateObj = if(date > 0)
OffsetDateTime.ofInstant(Instant.ofEpochSecond(date), ZoneOffset.UTC)
else null;
val authorObj = if(!author.isNullOrBlank())
PlatformAuthorLink(PlatformID.NONE, author, "", null, null)
else PlatformAuthorLink.UNKNOWN;
return LocalVideoDetails(
PlatformID("FILE", contentUrl, null, 0, -1),
displayName, Thumbnails(arrayOf(
Thumbnail(albumContentUrl ?: contentUrl, 0)
)), authorObj, contentUrl, duration, contentType, dateObj);
}
fun videoFromCursor(cursor: Cursor): IPlatformVideoDetails {
val id = cursor.getString(0);
val displayName = cursor.getString(1);
val author = cursor.getString(2);
val date = cursor.getLong(3);
val contentType = cursor.getString(4);
val category = cursor.getString(5);
val idLong = id.toLongOrNull();
val contentUrl = if(idLong != null )
ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, idLong).toString();
else
"";
val dateObj = if(date > 0)
OffsetDateTime.ofInstant(Instant.ofEpochSecond(date), ZoneOffset.UTC)
else null;
val authorObj = if(!author.isNullOrBlank())
PlatformAuthorLink(PlatformID.NONE, author, "", null, null)
else PlatformAuthorLink.UNKNOWN;
return LocalVideoDetails(
PlatformID("FILE", contentUrl, null, 0, -1),
displayName, Thumbnails(arrayOf(
Thumbnail(contentUrl, 0)
)), authorObj, contentUrl, -1, contentType, dateObj);
}
private var _instance : StateLibrary? = null;
val instance : StateLibrary
get(){
if(_instance == null)
_instance = StateLibrary();
return _instance!!;
};
fun finish() {
_instance?.let {
_instance = null;
}
}
}
}
class Bucket(val id: Long, val name: String);
class Artist {
val id: String;
val name: String;
val countTracks: Int;
val countAlbums: Int;
val thumbnail: String?;
val contentUrl: String?;
constructor(name: String, countTracks: Int = -1, countAlbums: Int = -1, thumbnail: String? = null, id: String? = null, contentUrl: String? = null) {
this.id = id ?: ID_UNKNOWN;
this.name = name;
this.thumbnail = thumbnail;
this.countTracks = countTracks;
this.countAlbums = countAlbums;
this.contentUrl = contentUrl;
}
fun getAlbums(): List<Album> {
return listOf();
}
fun getAudioTracks(): IPager<IPlatformContent> {
val idLong = id.toLongOrNull() ?: return EmptyPager();
return AdhocPager({ listOf() }, getTracksPager(idLong));
}
companion object {
val ID_UNKNOWN = "UNKNOWN";
val PROJECTION: Array<String> = arrayOf(Artists._ID,
Artists.ARTIST,
Artists.NUMBER_OF_TRACKS,
Artists.NUMBER_OF_ALBUMS);
fun fromCursor(cursor: Cursor): Artist {
val id = cursor.getString(0);
val artist = cursor.getString(1);
val numTracks = cursor.getInt(2);
val numAlbums = cursor.getInt(3);
val idLong = id.toLongOrNull();
val uri = if(idLong != null) ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, idLong) else null;
return Artist(artist, numTracks, numAlbums, null, id, uri?.toString());
}
fun getArtist(id: Long): Artist? {
val resolver = StateApp.instance.contextOrNull?.contentResolver;
if(resolver == null) {
Logger.w(TAG, "Artist contentResolver not found");
return null
}
val cursor = resolver.query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
Artist.PROJECTION,
"${MediaStore.Audio.Artists._ID} = ?",
arrayOf(id.toString()), null) ?:
return null;
cursor.moveToFirst();
if(cursor.isAfterLast)
return null;
return Artist.fromCursor(cursor);
}
fun getArtists(): List<Artist> {
val cursor = StateApp.instance.contextOrNull?.contentResolver?.query(Artists.EXTERNAL_CONTENT_URI, PROJECTION, null, null,
Artists.ARTIST + " ASC") ?: return listOf();
cursor.moveToFirst();
val list = mutableListOf<Artist>()
while(!cursor.isAfterLast) {
list.add(fromCursor(cursor));
cursor.moveToNext();
}
return list;
}
fun getTracksPager(artistId: Long): List<IPlatformVideo> {
val resolver = StateApp.instance.contextOrNull?.contentResolver;
if(resolver == null) {
Logger.w(TAG, "Album contentResolver not found");
return listOf();
}
val cursor = resolver?.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_MEDIA, "${MediaStore.Audio.Media.ARTIST_ID} = ?", arrayOf(artistId.toString()),
null) ?: return listOf();
cursor.moveToFirst();
val list = mutableListOf<IPlatformVideo>()
while(!cursor.isAfterLast) {
list.add(StateLibrary.audioFromCursor(cursor));
cursor.moveToNext();
}
return list;
}
}
}
class Album {
val id: String;
val name: String;
val artist: String?;
val countTracks: Int;
val thumbnail: String?;
constructor(name: String, countTracks: Int = -1, artist: String? = null, id: String? = null, thumbnail: String? = null) {
this.id = id ?: ID_UNKNOWN;
this.name = name;
this.artist = artist;
this.countTracks = countTracks;
this.thumbnail = thumbnail;
}
fun getTracks(): List<IPlatformVideo> {
return getAlbumTracks(id.toLongOrNull() ?: return listOf())
}
fun toPlaylist(tracks: List<IPlatformVideo>? = null): Playlist {
return Playlist(name, tracks?.map { SerializedPlatformVideo.fromVideo(it) } ?: getTracks().map { SerializedPlatformVideo.fromVideo(it) })
}
companion object {
val TAG = "StateLibrary";
val ID_UNKNOWN = "UNKNOWN";
val PROJECTION = arrayOf(MediaStore.Audio.Albums.ALBUM_ID,
MediaStore.Audio.Albums.ALBUM,
MediaStore.Audio.Albums.NUMBER_OF_SONGS,
MediaStore.Audio.Albums.ARTIST);
fun fromCursor(cursor: Cursor): Album {
val id = cursor.getString(0);
val album = cursor.getString(1);
val numTracks = cursor.getInt(2);
val artist = cursor.getString(3);
val idLong = id.toLongOrNull();
val uri = if(idLong != null) ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, idLong) else null;
return Album(album, numTracks, artist, id, uri?.toString());
}
fun getAlbumTracks(albumId: Long): List<IPlatformVideo> {
val resolver = StateApp.instance.contextOrNull?.contentResolver;
if(resolver == null) {
Logger.w(TAG, "Album contentResolver not found");
return listOf();
}
val cursor = resolver?.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_MEDIA, "${MediaStore.Audio.Media.ALBUM_ID} = ?", arrayOf(albumId.toString()),
null) ?: return listOf();
cursor.moveToFirst();
val list = mutableListOf<IPlatformVideo>()
while(!cursor.isAfterLast) {
list.add(StateLibrary.audioFromCursor(cursor));
cursor.moveToNext();
}
return list;
}
fun getAlbum(id: Long): Album? {
val resolver = StateApp.instance.contextOrNull?.contentResolver;
if(resolver == null) {
Logger.w(TAG, "Album contentResolver not found");
return null
}
val cursor = resolver.query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
PROJECTION,
"${MediaStore.Audio.Albums.ALBUM_ID} = ?",
arrayOf(id.toString()), null) ?:
return null;
cursor.moveToFirst();
if(cursor.isAfterLast)
return null;
return fromCursor(cursor);
}
fun getAlbums(): List<Album> {
val resolver = StateApp.instance.contextOrNull?.contentResolver;
if(resolver == null) {
Logger.w(TAG, "Album contentResolver not found");
return listOf();
}
val cursor = resolver?.query(
MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, PROJECTION, null, null,
MediaStore.Audio.Albums.ALBUM + " ASC") ?: return listOf();
cursor.moveToFirst();
val list = mutableListOf<Album>()
while(!cursor.isAfterLast) {
list.add(fromCursor(cursor));
cursor.moveToNext();
}
return list;
}
}
}
@@ -28,6 +28,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
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.local.LocalClient
import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
@@ -75,6 +76,7 @@ class StatePlatform {
private val _cache : LruCache<String, CachedPlatformContent> = LruCache<String, CachedPlatformContent>(VIDEO_CACHE);
//Clients
private val _localClient = LocalClient();
private val _enabledClientsPersistent = FragmentedStorage.get<StringArrayStorage>("enabledClients");
private val _platformOrderPersistent = FragmentedStorage.get<StringArrayStorage>("platformOrder");
private val _clientsLock = Object();
@@ -117,6 +119,7 @@ class StatePlatform {
_enabledClients.find { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) }?.let {
_mainClientPool.getClientPooled(it).getContentDetails(url)
}
?: (if(_localClient.isContentDetailsUrl(url)) _localClient.getContentDetails(url) else null)
?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
}
else {
@@ -124,6 +127,7 @@ class StatePlatform {
_enabledClients.find { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) }?.let {
_privateClientPool.getClientPooled(it).getContentDetails(url)
}
?: (if(_localClient.isContentDetailsUrl(url)) _localClient.getContentDetails(url) else null)
?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
}
},
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,640Q546.92,640 593.46,593.46Q640,546.92 640,480Q640,413.08 593.46,366.54Q546.92,320 480,320Q413.08,320 366.54,366.54Q320,413.08 320,480Q320,546.92 366.54,593.46Q413.08,640 480,640ZM480,520Q463,520 451.5,508.5Q440,497 440,480Q440,463 451.5,451.5Q463,440 480,440Q497,440 508.5,451.5Q520,463 520,480Q520,497 508.5,508.5Q497,520 480,520ZM480.07,860Q401.23,860 331.86,830.08Q262.49,800.16 211.18,748.87Q159.87,697.58 129.93,628.24Q100,558.9 100,480.07Q100,401.23 129.92,331.86Q159.84,262.49 211.13,211.18Q262.42,159.87 331.76,129.93Q401.1,100 479.93,100Q558.77,100 628.14,129.92Q697.51,159.84 748.82,211.13Q800.13,262.42 830.07,331.76Q860,401.1 860,479.93Q860,558.77 830.08,628.14Q800.16,697.51 748.87,748.82Q697.58,800.13 628.24,830.07Q558.9,860 480.07,860ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M493.85,593.85Q531.61,593.85 557.73,567.73Q583.84,541.62 583.84,503.85L583.84,279.23L698.46,279.23L698.46,208.46L548.46,208.46L548.46,434.62Q537,424.23 523.35,419.04Q509.69,413.85 493.85,413.85Q456.08,413.85 429.96,439.96Q403.85,466.08 403.85,503.85Q403.85,541.62 429.96,567.73Q456.08,593.85 493.85,593.85ZM322.31,700Q292,700 271,679Q250,658 250,627.69L250,172.31Q250,142 271,121Q292,100 322.31,100L777.69,100Q808,100 829,121Q850,142 850,172.31L850,627.69Q850,658 829,679Q808,700 777.69,700L322.31,700ZM322.31,640L777.69,640Q782.31,640 786.15,636.15Q790,632.31 790,627.69L790,172.31Q790,167.69 786.15,163.85Q782.31,160 777.69,160L322.31,160Q317.69,160 313.85,163.85Q310,167.69 310,172.31L310,627.69Q310,632.31 313.85,636.15Q317.69,640 322.31,640ZM182.31,840Q152,840 131,819Q110,798 110,767.69L110,252.31L170,252.31L170,767.69Q170,772.31 173.85,776.15Q177.69,780 182.31,780L697.69,780L697.69,840L182.31,840ZM310,160L310,160Q310,160 310,163.46Q310,166.92 310,172.31L310,627.69Q310,633.08 310,636.54Q310,640 310,640L310,640Q310,640 310,636.54Q310,633.08 310,627.69L310,172.31Q310,166.92 310,163.46Q310,160 310,160Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M460,561.54L711.54,400L460,238.46L460,561.54ZM322.31,700Q292,700 271,679Q250,658 250,627.69L250,172.31Q250,142 271,121Q292,100 322.31,100L777.69,100Q808,100 829,121Q850,142 850,172.31L850,627.69Q850,658 829,679Q808,700 777.69,700L322.31,700ZM322.31,640L777.69,640Q782.31,640 786.15,636.15Q790,632.31 790,627.69L790,172.31Q790,167.69 786.15,163.85Q782.31,160 777.69,160L322.31,160Q317.69,160 313.85,163.85Q310,167.69 310,172.31L310,627.69Q310,632.31 313.85,636.15Q317.69,640 322.31,640ZM182.31,840Q152,840 131,819Q110,798 110,767.69L110,252.31L170,252.31L170,767.69Q170,772.31 173.85,776.15Q177.69,780 182.31,780L697.69,780L697.69,840L182.31,840ZM310,160L310,160Q310,160 310,163.46Q310,166.92 310,172.31L310,627.69Q310,633.08 310,636.54Q310,640 310,640L310,640Q310,640 310,636.54Q310,633.08 310,627.69L310,172.31Q310,166.92 310,163.46Q310,160 310,160Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M181.92,780Q151.62,780 130.62,759Q109.62,738 109.62,707.69L109.62,252.31Q109.62,222 130.62,201Q151.62,180 181.92,180L637.31,180Q667.61,180 688.61,201Q709.61,222 709.61,252.31L709.61,435.39L850.38,294.62L850.38,665.38L709.61,524.61L709.61,707.69Q709.61,738 688.61,759Q667.61,780 637.31,780L181.92,780ZM181.92,720L637.31,720Q642.69,720 646.15,716.54Q649.62,713.08 649.62,707.69L649.62,252.31Q649.62,246.92 646.15,243.46Q642.69,240 637.31,240L181.92,240Q176.54,240 173.08,243.46Q169.62,246.92 169.62,252.31L169.62,707.69Q169.62,713.08 173.08,716.54Q176.54,720 181.92,720ZM169.62,720Q169.62,720 169.62,716.54Q169.62,713.08 169.62,707.69L169.62,252.31Q169.62,246.92 169.62,243.46Q169.62,240 169.62,240L169.62,240Q169.62,240 169.62,243.46Q169.62,246.92 169.62,252.31L169.62,707.69Q169.62,713.08 169.62,716.54Q169.62,720 169.62,720Z"/>
</vector>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:orientation="vertical"
android:fitsSystemWindows="false"
android:background="@drawable/bottom_menu_border"
android:id="@+id/root"
android:clickable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/filter_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:orientation="vertical">
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/filter_top"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:orientation="vertical"
android:fitsSystemWindows="false"
android:background="@drawable/bottom_menu_border"
android:id="@+id/root"
android:clickable="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_artists"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonIcon="@drawable/ic_creators"
app:buttonSubText="All artists known on this phone"
app:buttonText="Artists"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="8dp"/>
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_albums"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonIcon="@drawable/ic_album"
app:buttonSubText="All albums known on this phone"
app:buttonText="Albums"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="8dp" />
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_playlists"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonIcon="@drawable/ic_playlist"
app:buttonText="Playlists"
app:buttonSubText="All playlists in Grayjay"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="8dp" />
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_videos"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonIcon="@drawable/ic_videocam"
app:buttonText="Videos"
app:buttonSubText="All artists known on this phone"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="8dp" />
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_files"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonIcon="@drawable/ic_gallery"
app:buttonText="Files"
app:buttonSubText="Browse files on your phone"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="8dp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
+70
View File
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:id="@+id/root"
android:clickable="true">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image_thumbnail"
android:layout_height="50dp"
android:layout_width="50dp"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/roundedCorners_4dp"
app:srcCompat="@drawable/placeholder_video_thumbnail"
android:background="@drawable/video_thumbnail_outline"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
<TextView
android:id="@+id/text_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="13dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_light"
tools:text="Legendary grant recipient: Marvin Wißfeld Very Long Title That is Long"
android:maxLines="2"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/button_trash"
app:layout_constraintBottom_toTopOf="@id/text_metadata"
android:layout_marginStart="10dp" />
<TextView
android:id="@+id/text_metadata"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="10dp"
android:textColor="@color/gray_e0"
android:fontFamily="@font/inter_extra_light"
tools:text="3 videos"
android:maxLines="1"
app:layout_constraintTop_toBottomOf="@id/text_name"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintRight_toLeftOf="@id/button_trash"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="10dp" />
<!--
<ImageButton
android:id="@+id/button_play"
android:layout_width="34dp"
android:layout_height="34dp"
app:srcCompat="@drawable/ic_play_white_nopad"
android:scaleType="fitCenter"
android:padding="8dp"
app:layout_constraintRight_toLeftOf="@id/button_trash"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginEnd="10dp"/> -->
</androidx.constraintlayout.widget.ConstraintLayout>
+3 -2
View File
@@ -35,7 +35,7 @@
android:maxLines="2"
app:layout_constraintLeft_toRightOf="@id/image_video_thumbnail"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/button_trash"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@id/text_metadata"
android:layout_marginStart="10dp" />
@@ -51,7 +51,7 @@
android:maxLines="1"
app:layout_constraintTop_toBottomOf="@id/text_name"
app:layout_constraintLeft_toRightOf="@id/image_video_thumbnail"
app:layout_constraintRight_toLeftOf="@id/button_trash"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="10dp" />
@@ -68,6 +68,7 @@
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginEnd="10dp"/> -->
<ImageButton
android:id="@+id/button_trash"
android:layout_width="50dp"
+1
View File
@@ -73,6 +73,7 @@
<string name="share">Share</string>
<string name="view_all">View all</string>
<string name="creators">Creators</string>
<string name="library">Library</string>
<string name="enabled">Enabled</string>
<string name="keep_screen_on">Keep screen on</string>
<string name="keep_screen_on_while_casting">Keep screen on while casting</string>