mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-29 22:24:29 +02:00
Playlists sort and search support, Playlist search support, wip local playback, other fixes
This commit is contained in:
parent
b545545712
commit
dd6bde97a9
@ -3,7 +3,7 @@ package com.futo.platformplayer.api.media.models.streams
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
class LocalVideoMuxedSourceDescriptor(
|
class DownloadedVideoMuxedSourceDescriptor(
|
||||||
private val video: VideoLocal
|
private val video: VideoLocal
|
||||||
) : VideoMuxedSourceDescriptor() {
|
) : VideoMuxedSourceDescriptor() {
|
||||||
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local
|
||||||
|
|
||||||
|
class LocalClient {
|
||||||
|
//TODO
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
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.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
import java.io.File
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
class LocalVideoDetails: IPlatformVideoDetails {
|
||||||
|
|
||||||
|
override val contentType: ContentType get() = ContentType.UNKNOWN;
|
||||||
|
|
||||||
|
override val id: PlatformID;
|
||||||
|
override val name: String;
|
||||||
|
override val author: PlatformAuthorLink;
|
||||||
|
|
||||||
|
override val datetime: OffsetDateTime?;
|
||||||
|
|
||||||
|
override val url: String;
|
||||||
|
override val shareUrl: String;
|
||||||
|
override val rating: IRating = RatingLikes(0);
|
||||||
|
override val description: String = "";
|
||||||
|
|
||||||
|
override val video: IVideoSourceDescriptor;
|
||||||
|
override val preview: IVideoSourceDescriptor? = null;
|
||||||
|
override val live: IVideoSource? = null;
|
||||||
|
override val dash: IDashManifestSource? = null;
|
||||||
|
override val hls: IHLSManifestSource? = null;
|
||||||
|
override val subtitles: List<ISubtitleSource> = listOf()
|
||||||
|
|
||||||
|
override val thumbnails: Thumbnails;
|
||||||
|
override val duration: Long;
|
||||||
|
override val viewCount: Long = 0;
|
||||||
|
override val isLive: Boolean = false;
|
||||||
|
override val isShort: Boolean = false;
|
||||||
|
|
||||||
|
constructor(file: File) {
|
||||||
|
id = PlatformID("Local", file.path, "LOCAL")
|
||||||
|
name = file.name;
|
||||||
|
author = PlatformAuthorLink.UNKNOWN;
|
||||||
|
|
||||||
|
url = file.canonicalPath;
|
||||||
|
shareUrl = "";
|
||||||
|
|
||||||
|
duration = 0;
|
||||||
|
thumbnails = Thumbnails(arrayOf());
|
||||||
|
|
||||||
|
datetime = OffsetDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(file.lastModified()),
|
||||||
|
ZoneId.systemDefault()
|
||||||
|
);
|
||||||
|
video = LocalVideoMuxedSourceDescriptor(LocalVideoFileSource(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPlaybackTracker(): IPlaybackTracker? {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
|
class LocalVideoMuxedSourceDescriptor(
|
||||||
|
private val video: LocalVideoFileSource
|
||||||
|
) : VideoMuxedSourceDescriptor() {
|
||||||
|
override val videoSources: Array<IVideoSource> get() = arrayOf(video);
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
|
||||||
|
class MediaStoreVideo {
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val URI = MediaStore.Files.getContentUri("external");
|
||||||
|
val PROJECTION = arrayOf(Video.Media._ID, Video.Media.TITLE, Video.Media.DURATION, Video.Media.HEIGHT, Video.Media.WIDTH, Video.Media.MIME_TYPE);
|
||||||
|
val ORDER = MediaStore.Video.Media.TITLE;
|
||||||
|
|
||||||
|
fun readMediaStoreVideo(cursor: Cursor) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun query(context: Context, selection: String, args: Array<String>, order: String? = null): Cursor? {
|
||||||
|
val cursor = context.contentResolver.query(URI, PROJECTION, selection, args, order ?: ORDER, null);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalVideoFileSource: IVideoSource {
|
||||||
|
|
||||||
|
|
||||||
|
override val name: String;
|
||||||
|
override val width: Int;
|
||||||
|
override val height: Int;
|
||||||
|
override val container: String;
|
||||||
|
override val codec: String = ""
|
||||||
|
override val bitrate: Int = 0
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean = false;
|
||||||
|
|
||||||
|
constructor(file: File) {
|
||||||
|
name = file.name;
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
container = VideoHelper.videoExtensionToMimetype(file.extension) ?: "";
|
||||||
|
duration = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -10,7 +10,7 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.LocalVideoMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
@ -57,7 +57,7 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
|||||||
override val video: IVideoSourceDescriptor get() = if(audioSource.isNotEmpty())
|
override val video: IVideoSourceDescriptor get() = if(audioSource.isNotEmpty())
|
||||||
LocalVideoUnMuxedSourceDescriptor(this)
|
LocalVideoUnMuxedSourceDescriptor(this)
|
||||||
else
|
else
|
||||||
LocalVideoMuxedSourceDescriptor(this);
|
DownloadedVideoMuxedSourceDescriptor(this);
|
||||||
override val preview: IVideoSourceDescriptor? get() = videoSerialized.preview;
|
override val preview: IVideoSourceDescriptor? get() = videoSerialized.preview;
|
||||||
|
|
||||||
override val live: IVideoSource? get() = videoSerialized.live;
|
override val live: IVideoSource? get() = videoSerialized.live;
|
||||||
|
@ -229,7 +229,7 @@ class DownloadsFragment : MainFragment() {
|
|||||||
fun filterDownloads(vids: List<VideoLocal>): List<VideoLocal>{
|
fun filterDownloads(vids: List<VideoLocal>): List<VideoLocal>{
|
||||||
var vidsToReturn = vids;
|
var vidsToReturn = vids;
|
||||||
if(!_listDownloadSearch.text.isNullOrEmpty())
|
if(!_listDownloadSearch.text.isNullOrEmpty())
|
||||||
vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) };
|
vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) || it.author.name.contains(_listDownloadSearch.text, true) };
|
||||||
if(!ordering.isNullOrEmpty()) {
|
if(!ordering.isNullOrEmpty()) {
|
||||||
vidsToReturn = when(ordering){
|
vidsToReturn = when(ordering){
|
||||||
"downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX };
|
"downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX };
|
||||||
|
@ -6,12 +6,17 @@ import android.util.TypedValue
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Spinner
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@ -21,11 +26,13 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import com.futo.platformplayer.views.SearchView
|
||||||
import com.futo.platformplayer.views.adapters.*
|
import com.futo.platformplayer.views.adapters.*
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
|
||||||
class PlaylistsFragment : MainFragment() {
|
class PlaylistsFragment : MainFragment() {
|
||||||
@ -65,6 +72,7 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
private val _fragment: PlaylistsFragment;
|
private val _fragment: PlaylistsFragment;
|
||||||
|
|
||||||
var watchLater: ArrayList<IPlatformVideo> = arrayListOf();
|
var watchLater: ArrayList<IPlatformVideo> = arrayListOf();
|
||||||
|
var allPlaylists: ArrayList<Playlist> = arrayListOf();
|
||||||
var playlists: ArrayList<Playlist> = arrayListOf();
|
var playlists: ArrayList<Playlist> = arrayListOf();
|
||||||
private var _appBar: AppBarLayout;
|
private var _appBar: AppBarLayout;
|
||||||
private var _adapterWatchLater: VideoListHorizontalAdapter;
|
private var _adapterWatchLater: VideoListHorizontalAdapter;
|
||||||
@ -72,12 +80,20 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
private var _layoutWatchlist: ConstraintLayout;
|
private var _layoutWatchlist: ConstraintLayout;
|
||||||
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
|
private var _listPlaylistsSearch: EditText;
|
||||||
|
|
||||||
|
private var _ordering: String? = null;
|
||||||
|
|
||||||
|
|
||||||
constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||||
_fragment = fragment;
|
_fragment = fragment;
|
||||||
inflater.inflate(R.layout.fragment_playlists, this);
|
inflater.inflate(R.layout.fragment_playlists, this);
|
||||||
|
|
||||||
|
_listPlaylistsSearch = findViewById(R.id.playlists_search);
|
||||||
|
|
||||||
watchLater = ArrayList();
|
watchLater = ArrayList();
|
||||||
playlists = ArrayList();
|
playlists = ArrayList();
|
||||||
|
allPlaylists = ArrayList();
|
||||||
|
|
||||||
val recyclerWatchLater = findViewById<RecyclerView>(R.id.recycler_watch_later);
|
val recyclerWatchLater = findViewById<RecyclerView>(R.id.recycler_watch_later);
|
||||||
|
|
||||||
@ -105,6 +121,7 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
buttonCreatePlaylist.setOnClickListener {
|
buttonCreatePlaylist.setOnClickListener {
|
||||||
_slideUpOverlay = UISlideOverlays.showCreatePlaylistOverlay(findViewById<FrameLayout>(R.id.overlay_create_playlist)) {
|
_slideUpOverlay = UISlideOverlays.showCreatePlaylistOverlay(findViewById<FrameLayout>(R.id.overlay_create_playlist)) {
|
||||||
val playlist = Playlist(it, arrayListOf());
|
val playlist = Playlist(it, arrayListOf());
|
||||||
|
allPlaylists.add(0, playlist);
|
||||||
playlists.add(0, playlist);
|
playlists.add(0, playlist);
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
|
||||||
@ -120,6 +137,34 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
_appBar = findViewById(R.id.app_bar);
|
_appBar = findViewById(R.id.app_bar);
|
||||||
_layoutWatchlist = findViewById(R.id.layout_watchlist);
|
_layoutWatchlist = findViewById(R.id.layout_watchlist);
|
||||||
|
|
||||||
|
|
||||||
|
_listPlaylistsSearch.addTextChangedListener {
|
||||||
|
updatePlaylistsFiltering();
|
||||||
|
}
|
||||||
|
val spinnerSortBy: Spinner = findViewById(R.id.spinner_sortby);
|
||||||
|
spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.playlists_sortby_array)).also {
|
||||||
|
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||||
|
};
|
||||||
|
spinnerSortBy.setSelection(0);
|
||||||
|
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
|
||||||
|
when(pos) {
|
||||||
|
0 -> _ordering = "nameAsc"
|
||||||
|
1 -> _ordering = "nameDesc"
|
||||||
|
2 -> _ordering = "dateEditAsc"
|
||||||
|
3 -> _ordering = "dateEditDesc"
|
||||||
|
4 -> _ordering = "dateCreateAsc"
|
||||||
|
5 -> _ordering = "dateCreateDesc"
|
||||||
|
6 -> _ordering = "datePlayAsc"
|
||||||
|
7 -> _ordering = "datePlayDesc"
|
||||||
|
else -> _ordering = null
|
||||||
|
}
|
||||||
|
updatePlaylistsFiltering()
|
||||||
|
}
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
findViewById<TextView>(R.id.text_view_all).setOnClickListener { _fragment.navigate<WatchLaterFragment>(context.getString(R.string.watch_later)); };
|
findViewById<TextView>(R.id.text_view_all).setOnClickListener { _fragment.navigate<WatchLaterFragment>(context.getString(R.string.watch_later)); };
|
||||||
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
|
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
@ -134,10 +179,12 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun onShown() {
|
fun onShown() {
|
||||||
|
allPlaylists.clear();
|
||||||
playlists.clear()
|
playlists.clear()
|
||||||
playlists.addAll(
|
allPlaylists.addAll(
|
||||||
StatePlaylists.instance.getPlaylists().sortedByDescending { maxOf(it.datePlayed, it.dateUpdate, it.dateCreation) }
|
StatePlaylists.instance.getPlaylists().sortedByDescending { maxOf(it.datePlayed, it.dateUpdate, it.dateCreation) }
|
||||||
);
|
);
|
||||||
|
playlists.addAll(filterPlaylists(allPlaylists));
|
||||||
_adapterPlaylist.notifyDataSetChanged();
|
_adapterPlaylist.notifyDataSetChanged();
|
||||||
|
|
||||||
updateWatchLater();
|
updateWatchLater();
|
||||||
@ -157,6 +204,32 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updatePlaylistsFiltering() {
|
||||||
|
val toFilter = allPlaylists ?: return;
|
||||||
|
playlists.clear();
|
||||||
|
playlists.addAll(filterPlaylists(toFilter));
|
||||||
|
_adapterPlaylist.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
private fun filterPlaylists(pls: List<Playlist>): List<Playlist> {
|
||||||
|
var playlistsToReturn = pls;
|
||||||
|
if(!_listPlaylistsSearch.text.isNullOrEmpty())
|
||||||
|
playlistsToReturn = playlistsToReturn.filter { it.name.contains(_listPlaylistsSearch.text, true) };
|
||||||
|
if(!_ordering.isNullOrEmpty()){
|
||||||
|
playlistsToReturn = when(_ordering){
|
||||||
|
"nameAsc" -> playlistsToReturn.sortedBy { it.name.lowercase() }
|
||||||
|
"nameDesc" -> playlistsToReturn.sortedByDescending { it.name.lowercase() };
|
||||||
|
"dateEditAsc" -> playlistsToReturn.sortedBy { it.dateUpdate ?: OffsetDateTime.MAX };
|
||||||
|
"dateEditDesc" -> playlistsToReturn.sortedByDescending { it.dateUpdate ?: OffsetDateTime.MIN }
|
||||||
|
"dateCreateAsc" -> playlistsToReturn.sortedBy { it.dateCreation ?: OffsetDateTime.MAX };
|
||||||
|
"dateCreateDesc" -> playlistsToReturn.sortedByDescending { it.dateCreation ?: OffsetDateTime.MIN }
|
||||||
|
"datePlayAsc" -> playlistsToReturn.sortedBy { it.datePlayed ?: OffsetDateTime.MAX };
|
||||||
|
"datePlayDesc" -> playlistsToReturn.sortedByDescending { it.datePlayed ?: OffsetDateTime.MIN }
|
||||||
|
else -> playlistsToReturn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return playlistsToReturn;
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateWatchLater() {
|
private fun updateWatchLater() {
|
||||||
val watchList = StatePlaylists.instance.getWatchLater();
|
val watchList = StatePlaylists.instance.getWatchLater();
|
||||||
if (watchList.isNotEmpty()) {
|
if (watchList.isNotEmpty()) {
|
||||||
@ -164,7 +237,7 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
|
|
||||||
_appBar.let { appBar ->
|
_appBar.let { appBar ->
|
||||||
val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams;
|
val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams;
|
||||||
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 230.0f, resources.displayMetrics).toInt();
|
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 315.0f, resources.displayMetrics).toInt();
|
||||||
appBar.layoutParams = layoutParams;
|
appBar.layoutParams = layoutParams;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -172,7 +245,7 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
|
|
||||||
_appBar.let { appBar ->
|
_appBar.let { appBar ->
|
||||||
val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams;
|
val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams;
|
||||||
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25.0f, resources.displayMetrics).toInt();
|
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 110.0f, resources.displayMetrics).toInt();
|
||||||
appBar.layoutParams = layoutParams;
|
appBar.layoutParams = layoutParams;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import android.widget.ImageButton
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
@ -22,6 +23,7 @@ import com.futo.platformplayer.states.StateDownloads
|
|||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.toHumanDuration
|
import com.futo.platformplayer.toHumanDuration
|
||||||
import com.futo.platformplayer.toHumanTime
|
import com.futo.platformplayer.toHumanTime
|
||||||
|
import com.futo.platformplayer.views.SearchView
|
||||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||||
|
|
||||||
abstract class VideoListEditorView : LinearLayout {
|
abstract class VideoListEditorView : LinearLayout {
|
||||||
@ -37,9 +39,15 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
protected var _buttonExport: ImageButton;
|
protected var _buttonExport: ImageButton;
|
||||||
private var _buttonShare: ImageButton;
|
private var _buttonShare: ImageButton;
|
||||||
private var _buttonEdit: ImageButton;
|
private var _buttonEdit: ImageButton;
|
||||||
|
private var _buttonSearch: ImageButton;
|
||||||
|
|
||||||
|
private var _search: SearchView;
|
||||||
|
|
||||||
private var _onShare: (()->Unit)? = null;
|
private var _onShare: (()->Unit)? = null;
|
||||||
|
|
||||||
|
private var _loadedVideos: List<IPlatformVideo>? = null;
|
||||||
|
private var _loadedVideosCanEdit: Boolean = false;
|
||||||
|
|
||||||
constructor(inflater: LayoutInflater) : super(inflater.context) {
|
constructor(inflater: LayoutInflater) : super(inflater.context) {
|
||||||
inflater.inflate(R.layout.fragment_video_list_editor, this);
|
inflater.inflate(R.layout.fragment_video_list_editor, this);
|
||||||
|
|
||||||
@ -57,6 +65,26 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
_buttonDownload.visibility = View.GONE;
|
_buttonDownload.visibility = View.GONE;
|
||||||
_buttonExport = findViewById(R.id.button_export);
|
_buttonExport = findViewById(R.id.button_export);
|
||||||
_buttonExport.visibility = View.GONE;
|
_buttonExport.visibility = View.GONE;
|
||||||
|
_buttonSearch = findViewById(R.id.button_search);
|
||||||
|
|
||||||
|
_search = findViewById(R.id.search_bar);
|
||||||
|
_search.visibility = View.GONE;
|
||||||
|
_search.onSearchChanged.subscribe {
|
||||||
|
updateVideoFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buttonSearch.setOnClickListener {
|
||||||
|
if(_search.isVisible) {
|
||||||
|
_search.visibility = View.GONE;
|
||||||
|
_search.textSearch.text = "";
|
||||||
|
updateVideoFilters();
|
||||||
|
_buttonSearch.setImageResource(R.drawable.ic_search);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_search.visibility = View.VISIBLE;
|
||||||
|
_buttonSearch.setImageResource(R.drawable.ic_search_off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_buttonShare = findViewById(R.id.button_share);
|
_buttonShare = findViewById(R.id.button_share);
|
||||||
val onShare = _onShare;
|
val onShare = _onShare;
|
||||||
@ -171,9 +199,22 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
.load(R.drawable.placeholder_video_thumbnail)
|
.load(R.drawable.placeholder_video_thumbnail)
|
||||||
.into(_imagePlaylistThumbnail)
|
.into(_imagePlaylistThumbnail)
|
||||||
}
|
}
|
||||||
|
_loadedVideos = videos;
|
||||||
|
_loadedVideosCanEdit = canEdit;
|
||||||
_videoListEditorView.setVideos(videos, canEdit);
|
_videoListEditorView.setVideos(videos, canEdit);
|
||||||
}
|
}
|
||||||
|
fun filterVideos(videos: List<IPlatformVideo>): List<IPlatformVideo> {
|
||||||
|
var toReturn = videos;
|
||||||
|
val searchStr = _search.textSearch.text
|
||||||
|
if(!searchStr.isNullOrBlank())
|
||||||
|
toReturn = toReturn.filter { it.name.contains(searchStr, true) || it.author.name.contains(searchStr, true) };
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateVideoFilters() {
|
||||||
|
val videos = _loadedVideos ?: return;
|
||||||
|
_videoListEditorView.setVideos(filterVideos(videos), _loadedVideosCanEdit);
|
||||||
|
}
|
||||||
|
|
||||||
protected fun setButtonDownloadVisible(isVisible: Boolean) {
|
protected fun setButtonDownloadVisible(isVisible: Boolean) {
|
||||||
_buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
_buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||||
|
@ -214,5 +214,38 @@ class VideoHelper {
|
|||||||
}
|
}
|
||||||
else return 0;
|
else return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun mediaExtensionToMimetype(extension: String): String? {
|
||||||
|
return videoExtensionToMimetype(extension) ?: audioExtensionToMimetype(extension);
|
||||||
|
}
|
||||||
|
fun videoExtensionToMimetype(extension: String): String? {
|
||||||
|
val extensionTrimmed = extension.trim('.').lowercase();
|
||||||
|
return when (extensionTrimmed) {
|
||||||
|
"mp4" -> return "video/mp4";
|
||||||
|
"webm" -> return "video/webm";
|
||||||
|
"m3u8" -> return "video/x-mpegURL";
|
||||||
|
"3gp" -> return "video/3gpp";
|
||||||
|
"mov" -> return "video/quicktime";
|
||||||
|
"mkv" -> return "video/x-matroska";
|
||||||
|
"mp4a" -> return "audio/vnd.apple.mpegurl";
|
||||||
|
"mpga" -> return "audio/mpga";
|
||||||
|
"mp3" -> return "audio/mp3";
|
||||||
|
"webm" -> return "audio/webm";
|
||||||
|
"3gp" -> return "audio/3gpp";
|
||||||
|
else -> null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun audioExtensionToMimetype(extension: String): String? {
|
||||||
|
val extensionTrimmed = extension.trim('.').lowercase();
|
||||||
|
return when (extensionTrimmed) {
|
||||||
|
"mkv" -> return "audio/x-matroska";
|
||||||
|
"mp4a" -> return "audio/vnd.apple.mpegurl";
|
||||||
|
"mpga" -> return "audio/mpga";
|
||||||
|
"mp3" -> return "audio/mp3";
|
||||||
|
"webm" -> return "audio/webm";
|
||||||
|
"3gp" -> return "audio/3gpp";
|
||||||
|
else -> null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import com.bumptech.glide.Glide
|
|||||||
import com.futo.platformplayer.PresetImages
|
import com.futo.platformplayer.PresetImages
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -35,6 +36,12 @@ data class ImageVariable(
|
|||||||
} else if(!url.isNullOrEmpty()) {
|
} else if(!url.isNullOrEmpty()) {
|
||||||
Glide.with(imageView)
|
Glide.with(imageView)
|
||||||
.load(url)
|
.load(url)
|
||||||
|
.error(if(!subscriptionUrl.isNullOrBlank()) StateSubscriptions.instance.getSubscription(subscriptionUrl!!)?.channel?.thumbnail else null)
|
||||||
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
|
.into(imageView);
|
||||||
|
} else if(!subscriptionUrl.isNullOrEmpty()) {
|
||||||
|
Glide.with(imageView)
|
||||||
|
.load(StateSubscriptions.instance.getSubscription(subscriptionUrl!!)?.channel?.thumbnail)
|
||||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
.into(imageView);
|
.into(imageView);
|
||||||
} else if(!presetName.isNullOrEmpty()) {
|
} else if(!presetName.isNullOrEmpty()) {
|
||||||
|
@ -30,6 +30,7 @@ import com.futo.platformplayer.states.StatePlugins
|
|||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.subsexchange.ChannelRequest
|
import com.futo.platformplayer.subsexchange.ChannelRequest
|
||||||
import com.futo.platformplayer.subsexchange.ChannelResolve
|
import com.futo.platformplayer.subsexchange.ChannelResolve
|
||||||
|
import com.futo.platformplayer.subsexchange.ExchangeContract
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
@ -77,22 +78,31 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
|
|
||||||
val exs: ArrayList<Throwable> = arrayListOf();
|
val exs: ArrayList<Throwable> = arrayListOf();
|
||||||
|
|
||||||
|
var contract: ExchangeContract? = null;
|
||||||
|
|
||||||
val contractableTasks = tasks.filter { !it.fromPeek && !it.fromCache && (it.type == ResultCapabilities.TYPE_VIDEOS || it.type == ResultCapabilities.TYPE_MIXED) };
|
|
||||||
val contract = if(contractableTasks.size > 10) subsExchangeClient?.requestContract(*contractableTasks.map { ChannelRequest(it.url) }.toTypedArray()) else null;
|
|
||||||
if(contract?.provided?.isNotEmpty() == true)
|
|
||||||
Logger.i(TAG, "Received subscription exchange contract (Requires ${contract?.required?.size}, Provides ${contract?.provided?.size}), ID: ${contract?.id}");
|
|
||||||
var providedTasks: MutableList<SubscriptionTask>? = null;
|
var providedTasks: MutableList<SubscriptionTask>? = null;
|
||||||
if(contract != null && contract.required.isNotEmpty()){
|
|
||||||
providedTasks = mutableListOf()
|
try {
|
||||||
for(task in tasks.toList()){
|
val contractableTasks =
|
||||||
if(!task.fromCache && !task.fromPeek && contract.provided.contains(task.url)) {
|
tasks.filter { !it.fromPeek && !it.fromCache && (it.type == ResultCapabilities.TYPE_VIDEOS || it.type == ResultCapabilities.TYPE_MIXED) };
|
||||||
providedTasks.add(task);
|
contract =
|
||||||
tasks.remove(task);
|
if (contractableTasks.size > 10) subsExchangeClient?.requestContract(*contractableTasks.map {
|
||||||
|
ChannelRequest(it.url)
|
||||||
|
}.toTypedArray()) else null;
|
||||||
|
if (contract?.provided?.isNotEmpty() == true)
|
||||||
|
Logger.i(TAG, "Received subscription exchange contract (Requires ${contract?.required?.size}, Provides ${contract?.provided?.size}), ID: ${contract?.id}");
|
||||||
|
if (contract != null && contract.required.isNotEmpty()) {
|
||||||
|
providedTasks = mutableListOf()
|
||||||
|
for (task in tasks.toList()) {
|
||||||
|
if (!task.fromCache && !task.fromPeek && contract.provided.contains(task.url)) {
|
||||||
|
providedTasks.add(task);
|
||||||
|
tasks.remove(task);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch(ex: Throwable){
|
||||||
|
Logger.e("SubscriptionsTaskFetchAlgorithm", "Failed to retrieve SubsExchange contract due to: " + ex.message, ex);
|
||||||
|
}
|
||||||
|
|
||||||
val failedPlugins = mutableListOf<String>();
|
val failedPlugins = mutableListOf<String>();
|
||||||
val cachedChannels = mutableListOf<String>()
|
val cachedChannels = mutableListOf<String>()
|
||||||
|
@ -98,7 +98,11 @@ class ImageVariableOverlay: ConstraintLayout {
|
|||||||
UIDialogs.toast(context, "No thumbnail found");
|
UIDialogs.toast(context, "No thumbnail found");
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
}
|
}
|
||||||
_selected = ImageVariable(it.channel.thumbnail);
|
val channelUrl = it.channel.url;
|
||||||
|
_selected = ImageVariable(it.channel.thumbnail).let {
|
||||||
|
it.subscriptionUrl = channelUrl;
|
||||||
|
return@let it;
|
||||||
|
}
|
||||||
updateSelected();
|
updateSelected();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
10
app/src/main/res/drawable/ic_search_off.xml
Normal file
10
app/src/main/res/drawable/ic_search_off.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M280,824.62Q213.15,824.62 166.58,778.04Q120,731.46 120,664.62Q120,597.77 166.58,551.19Q213.15,504.62 280,504.62Q346.85,504.62 393.42,551.19Q440,597.77 440,664.62Q440,731.46 393.42,778.04Q346.85,824.62 280,824.62ZM817.85,806.15L566.46,554.77Q555.69,564 540.69,572.46Q525.69,580.92 512.62,585.54Q508.31,577.15 503.27,569.04Q498.23,560.92 492.92,554.31Q543.23,533.38 576.23,487.62Q609.23,441.85 609.23,380Q609.23,301.15 554.04,245.96Q498.85,190.77 420,190.77Q341.15,190.77 285.96,245.96Q230.77,301.15 230.77,380Q230.77,392.15 232.81,404.58Q234.85,417 237.38,428.38Q228.62,428.85 217.88,432.15Q207.15,435.46 198.62,438.85Q195.08,426.31 192.92,411.08Q190.77,395.85 190.77,380Q190.77,284.08 257.42,217.42Q324.08,150.77 420,150.77Q515.92,150.77 582.58,217.42Q649.23,284.08 649.23,380Q649.23,423 634.19,461.12Q619.15,499.23 595.92,527.38L846.15,777.85L817.85,806.15ZM209.77,756.69L280,686.46L350,756.69L372.08,734.85L301.85,664.62L372.08,594.38L350.23,572.54L280,642.77L209.77,572.54L187.92,594.38L258.15,664.62L187.92,734.85L209.77,756.69Z"/>
|
||||||
|
</vector>
|
@ -13,7 +13,7 @@
|
|||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/app_bar"
|
android:id="@+id/app_bar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="230dp"
|
android:layout_height="315dp"
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
app:elevation="0dp">
|
app:elevation="0dp">
|
||||||
|
|
||||||
@ -87,7 +87,7 @@
|
|||||||
<androidx.appcompat.widget.Toolbar
|
<androidx.appcompat.widget.Toolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="25dp"
|
android:layout_height="110dp"
|
||||||
android:minHeight="0dp"
|
android:minHeight="0dp"
|
||||||
app:contentInsetStart="0dp"
|
app:contentInsetStart="0dp"
|
||||||
app:contentInsetEnd="0dp"
|
app:contentInsetEnd="0dp"
|
||||||
@ -96,35 +96,82 @@
|
|||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="110dp"
|
||||||
android:gravity="center_vertical">
|
android:orientation="vertical">
|
||||||
|
<LinearLayout
|
||||||
<TextView
|
android:layout_width="match_parent"
|
||||||
android:id="@+id/text_playlists"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="16dp"
|
android:gravity="center_vertical">
|
||||||
android:textColor="@color/white"
|
|
||||||
android:fontFamily="@font/inter_light"
|
|
||||||
android:text="@string/playlists"
|
|
||||||
android:paddingStart="15dp"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/recycler_watch_later" />
|
|
||||||
|
|
||||||
<Space
|
<TextView
|
||||||
android:layout_width="0dp"
|
android:id="@+id/text_playlists"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_weight="1" />
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:text="@string/playlists"
|
||||||
|
android:paddingStart="15dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/recycler_watch_later" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_create_playlist"
|
||||||
|
android:layout_width="35dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="@string/cd_button_create_playlist"
|
||||||
|
app:srcCompat="@drawable/ic_add_white_16dp"
|
||||||
|
android:paddingEnd="15dp"
|
||||||
|
android:paddingStart="15dp"
|
||||||
|
android:layout_marginEnd="12dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/playlists_filter_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/playlists_search"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:background="@drawable/background_button_round"
|
||||||
|
android:hint="Search.." />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:textColor="@color/gray_ac"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:text="@string/sort_by"
|
||||||
|
android:paddingStart="20dp" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sortby"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="20dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/button_create_playlist"
|
|
||||||
android:layout_width="35dp"
|
|
||||||
android:layout_height="20dp"
|
|
||||||
android:contentDescription="@string/cd_button_create_playlist"
|
|
||||||
app:srcCompat="@drawable/ic_add_white_16dp"
|
|
||||||
android:paddingEnd="15dp"
|
|
||||||
android:paddingStart="15dp"
|
|
||||||
android:layout_marginEnd="12dp" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.appcompat.widget.Toolbar>
|
</androidx.appcompat.widget.Toolbar>
|
||||||
@ -136,7 +183,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_view_all"
|
app:layout_constraintTop_toBottomOf="@id/playlists_filter_container"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
android:paddingTop="10dp"
|
android:paddingTop="10dp"
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="220dp">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_playlist_thumbnail"
|
android:id="@+id/image_playlist_thumbnail"
|
||||||
@ -53,6 +53,22 @@
|
|||||||
android:scaleType="fitXY" />
|
android:scaleType="fitXY" />
|
||||||
|
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_edit"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_button_edit"
|
||||||
|
android:background="@drawable/background_button_round"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_export"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/button_share"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:srcCompat="@drawable/ic_edit"
|
||||||
|
android:padding="10dp"
|
||||||
|
app:tint="@color/white" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/button_export"
|
android:id="@+id/button_export"
|
||||||
@ -89,7 +105,7 @@
|
|||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="120dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="-90dp"
|
android:layout_marginTop="-90dp"
|
||||||
android:layout_marginStart="20dp"
|
android:layout_marginStart="20dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent">
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
@ -116,6 +132,8 @@
|
|||||||
app:layout_constraintLeft_toLeftOf="@id/container_buttons"
|
app:layout_constraintLeft_toLeftOf="@id/container_buttons"
|
||||||
app:layout_constraintBottom_toTopOf="@id/container_buttons" />
|
app:layout_constraintBottom_toTopOf="@id/container_buttons" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/container_buttons"
|
android:id="@+id/container_buttons"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -176,20 +194,18 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/button_edit"
|
android:id="@+id/button_search"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:contentDescription="@string/cd_button_edit"
|
android:contentDescription="@string/cd_search_icon"
|
||||||
android:background="@drawable/background_button_round"
|
android:background="@drawable/background_button_round"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="10dp"
|
||||||
app:layout_constraintLeft_toRightOf="@id/button_shuffle"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/button_play_all"
|
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:scaleType="fitCenter"
|
app:srcCompat="@drawable/ic_search"
|
||||||
app:srcCompat="@drawable/ic_edit"
|
app:tint="@color/white"
|
||||||
android:padding="10dp"
|
android:padding="5dp"
|
||||||
app:tint="@color/white" />
|
android:scaleType="fitCenter" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/button_download"
|
android:id="@+id/button_download"
|
||||||
@ -207,6 +223,16 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
<com.futo.platformplayer.views.SearchView
|
||||||
|
android:id="@+id/search_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="-10dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/container_buttons"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.appcompat.widget.Toolbar>
|
</androidx.appcompat.widget.Toolbar>
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
@ -973,6 +973,16 @@
|
|||||||
<item>Release Date (Oldest)</item>
|
<item>Release Date (Oldest)</item>
|
||||||
<item>Release Date (Newest)</item>
|
<item>Release Date (Newest)</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="playlists_sortby_array">
|
||||||
|
<item>Name (Ascending)</item>
|
||||||
|
<item>Name (Descending)</item>
|
||||||
|
<item>Modified Date (Oldest)</item>
|
||||||
|
<item>Modified Date (Newest)</item>
|
||||||
|
<item>Creation Date (Oldest)</item>
|
||||||
|
<item>Creation Date (Newest)</item>
|
||||||
|
<item>Play Date (Oldest)</item>
|
||||||
|
<item>Play Date (Newest)</item>
|
||||||
|
</string-array>
|
||||||
<string-array name="feed_style">
|
<string-array name="feed_style">
|
||||||
<item>Preview</item>
|
<item>Preview</item>
|
||||||
<item>List</item>
|
<item>List</item>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user