mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
WIP library support, albums, artists, videos
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+146
-2
@@ -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;
|
||||
|
||||
+1
@@ -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) }),
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+123
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+160
@@ -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 ?: "";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+576
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
+161
@@ -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" } ?: "";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+79
@@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
+169
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+24
-10
@@ -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)");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user