mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-19 19:14:51 +00:00
Playlists sort and search support, Playlist search support, wip local playback, other fixes
This commit is contained in:
parent
b545545712
commit
dd6bde97a9
18 changed files with 481 additions and 61 deletions
|
@ -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.downloads.VideoLocal
|
||||
|
||||
class LocalVideoMuxedSourceDescriptor(
|
||||
class DownloadedVideoMuxedSourceDescriptor(
|
||||
private val video: VideoLocal
|
||||
) : VideoMuxedSourceDescriptor() {
|
||||
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.ratings.IRating
|
||||
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.sources.IDashManifestSource
|
||||
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())
|
||||
LocalVideoUnMuxedSourceDescriptor(this)
|
||||
else
|
||||
LocalVideoMuxedSourceDescriptor(this);
|
||||
DownloadedVideoMuxedSourceDescriptor(this);
|
||||
override val preview: IVideoSourceDescriptor? get() = videoSerialized.preview;
|
||||
|
||||
override val live: IVideoSource? get() = videoSerialized.live;
|
||||
|
|
|
@ -229,7 +229,7 @@ class DownloadsFragment : MainFragment() {
|
|||
fun filterDownloads(vids: List<VideoLocal>): List<VideoLocal>{
|
||||
var vidsToReturn = vids;
|
||||
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()) {
|
||||
vidsToReturn = when(ordering){
|
||||
"downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX };
|
||||
|
|
|
@ -6,12 +6,17 @@ 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.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.views.SearchView
|
||||
import com.futo.platformplayer.views.adapters.*
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
|
||||
class PlaylistsFragment : MainFragment() {
|
||||
|
@ -65,6 +72,7 @@ class PlaylistsFragment : MainFragment() {
|
|||
private val _fragment: PlaylistsFragment;
|
||||
|
||||
var watchLater: ArrayList<IPlatformVideo> = arrayListOf();
|
||||
var allPlaylists: ArrayList<Playlist> = arrayListOf();
|
||||
var playlists: ArrayList<Playlist> = arrayListOf();
|
||||
private var _appBar: AppBarLayout;
|
||||
private var _adapterWatchLater: VideoListHorizontalAdapter;
|
||||
|
@ -72,12 +80,20 @@ class PlaylistsFragment : MainFragment() {
|
|||
private var _layoutWatchlist: ConstraintLayout;
|
||||
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
||||
|
||||
private var _listPlaylistsSearch: EditText;
|
||||
|
||||
private var _ordering: String? = null;
|
||||
|
||||
|
||||
constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||
_fragment = fragment;
|
||||
inflater.inflate(R.layout.fragment_playlists, this);
|
||||
|
||||
_listPlaylistsSearch = findViewById(R.id.playlists_search);
|
||||
|
||||
watchLater = ArrayList();
|
||||
playlists = ArrayList();
|
||||
allPlaylists = ArrayList();
|
||||
|
||||
val recyclerWatchLater = findViewById<RecyclerView>(R.id.recycler_watch_later);
|
||||
|
||||
|
@ -105,6 +121,7 @@ class PlaylistsFragment : MainFragment() {
|
|||
buttonCreatePlaylist.setOnClickListener {
|
||||
_slideUpOverlay = UISlideOverlays.showCreatePlaylistOverlay(findViewById<FrameLayout>(R.id.overlay_create_playlist)) {
|
||||
val playlist = Playlist(it, arrayListOf());
|
||||
allPlaylists.add(0, playlist);
|
||||
playlists.add(0, playlist);
|
||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||
|
||||
|
@ -120,6 +137,34 @@ class PlaylistsFragment : MainFragment() {
|
|||
_appBar = findViewById(R.id.app_bar);
|
||||
_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)); };
|
||||
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
|
@ -134,10 +179,12 @@ class PlaylistsFragment : MainFragment() {
|
|||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun onShown() {
|
||||
allPlaylists.clear();
|
||||
playlists.clear()
|
||||
playlists.addAll(
|
||||
allPlaylists.addAll(
|
||||
StatePlaylists.instance.getPlaylists().sortedByDescending { maxOf(it.datePlayed, it.dateUpdate, it.dateCreation) }
|
||||
);
|
||||
playlists.addAll(filterPlaylists(allPlaylists));
|
||||
_adapterPlaylist.notifyDataSetChanged();
|
||||
|
||||
updateWatchLater();
|
||||
|
@ -157,6 +204,32 @@ class PlaylistsFragment : MainFragment() {
|
|||
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() {
|
||||
val watchList = StatePlaylists.instance.getWatchLater();
|
||||
if (watchList.isNotEmpty()) {
|
||||
|
@ -164,7 +237,7 @@ class PlaylistsFragment : MainFragment() {
|
|||
|
||||
_appBar.let { appBar ->
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
|
@ -172,7 +245,7 @@ class PlaylistsFragment : MainFragment() {
|
|||
|
||||
_appBar.let { appBar ->
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.widget.ImageButton
|
|||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.setPadding
|
||||
import com.bumptech.glide.Glide
|
||||
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.toHumanDuration
|
||||
import com.futo.platformplayer.toHumanTime
|
||||
import com.futo.platformplayer.views.SearchView
|
||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||
|
||||
abstract class VideoListEditorView : LinearLayout {
|
||||
|
@ -37,9 +39,15 @@ abstract class VideoListEditorView : LinearLayout {
|
|||
protected var _buttonExport: ImageButton;
|
||||
private var _buttonShare: ImageButton;
|
||||
private var _buttonEdit: ImageButton;
|
||||
private var _buttonSearch: ImageButton;
|
||||
|
||||
private var _search: SearchView;
|
||||
|
||||
private var _onShare: (()->Unit)? = null;
|
||||
|
||||
private var _loadedVideos: List<IPlatformVideo>? = null;
|
||||
private var _loadedVideosCanEdit: Boolean = false;
|
||||
|
||||
constructor(inflater: LayoutInflater) : super(inflater.context) {
|
||||
inflater.inflate(R.layout.fragment_video_list_editor, this);
|
||||
|
||||
|
@ -57,6 +65,26 @@ abstract class VideoListEditorView : LinearLayout {
|
|||
_buttonDownload.visibility = View.GONE;
|
||||
_buttonExport = findViewById(R.id.button_export);
|
||||
_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);
|
||||
val onShare = _onShare;
|
||||
|
@ -171,9 +199,22 @@ abstract class VideoListEditorView : LinearLayout {
|
|||
.load(R.drawable.placeholder_video_thumbnail)
|
||||
.into(_imagePlaylistThumbnail)
|
||||
}
|
||||
|
||||
_loadedVideos = videos;
|
||||
_loadedVideosCanEdit = 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) {
|
||||
_buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||
|
|
|
@ -214,5 +214,38 @@ class VideoHelper {
|
|||
}
|
||||
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.R
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Transient
|
||||
import java.io.File
|
||||
|
@ -35,6 +36,12 @@ data class ImageVariable(
|
|||
} else if(!url.isNullOrEmpty()) {
|
||||
Glide.with(imageView)
|
||||
.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)
|
||||
.into(imageView);
|
||||
} else if(!presetName.isNullOrEmpty()) {
|
||||
|
|
|
@ -30,6 +30,7 @@ import com.futo.platformplayer.states.StatePlugins
|
|||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.subsexchange.ChannelRequest
|
||||
import com.futo.platformplayer.subsexchange.ChannelResolve
|
||||
import com.futo.platformplayer.subsexchange.ExchangeContract
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
@ -77,22 +78,31 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||
|
||||
val exs: ArrayList<Throwable> = arrayListOf();
|
||||
|
||||
|
||||
|
||||
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 contract: ExchangeContract? = null;
|
||||
var providedTasks: MutableList<SubscriptionTask>? = null;
|
||||
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);
|
||||
|
||||
try {
|
||||
val contractableTasks =
|
||||
tasks.filter { !it.fromPeek && !it.fromCache && (it.type == ResultCapabilities.TYPE_VIDEOS || it.type == ResultCapabilities.TYPE_MIXED) };
|
||||
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}");
|
||||
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 cachedChannels = mutableListOf<String>()
|
||||
|
|
|
@ -98,7 +98,11 @@ class ImageVariableOverlay: ConstraintLayout {
|
|||
UIDialogs.toast(context, "No thumbnail found");
|
||||
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();
|
||||
};
|
||||
};
|
||||
|
|
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
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="230dp"
|
||||
android:layout_height="315dp"
|
||||
android:background="@color/transparent"
|
||||
app:elevation="0dp">
|
||||
|
||||
|
@ -87,7 +87,7 @@
|
|||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="25dp"
|
||||
android:layout_height="110dp"
|
||||
android:minHeight="0dp"
|
||||
app:contentInsetStart="0dp"
|
||||
app:contentInsetEnd="0dp"
|
||||
|
@ -96,35 +96,82 @@
|
|||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_playlists"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="110dp"
|
||||
android:orientation="vertical">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
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" />
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
<TextView
|
||||
android:id="@+id/text_playlists"
|
||||
android:layout_width="wrap_content"
|
||||
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>
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
@ -136,7 +183,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
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_constraintRight_toRightOf="parent"
|
||||
android:paddingTop="10dp"
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
android:orientation="vertical">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="220dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_playlist_thumbnail"
|
||||
|
@ -53,6 +53,22 @@
|
|||
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
|
||||
android:id="@+id/button_export"
|
||||
|
@ -89,7 +105,7 @@
|
|||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-90dp"
|
||||
android:layout_marginStart="20dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
@ -116,6 +132,8 @@
|
|||
app:layout_constraintLeft_toLeftOf="@id/container_buttons"
|
||||
app:layout_constraintBottom_toTopOf="@id/container_buttons" />
|
||||
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_buttons"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -176,20 +194,18 @@
|
|||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_edit"
|
||||
android:id="@+id/button_search"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="@string/cd_button_edit"
|
||||
android:contentDescription="@string/cd_search_icon"
|
||||
android:background="@drawable/background_button_round"
|
||||
android:gravity="center"
|
||||
android:layout_marginStart="5dp"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_shuffle"
|
||||
app:layout_constraintBottom_toBottomOf="@id/button_play_all"
|
||||
android:layout_marginStart="10dp"
|
||||
android:orientation="horizontal"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_edit"
|
||||
android:padding="10dp"
|
||||
app:tint="@color/white" />
|
||||
app:srcCompat="@drawable/ic_search"
|
||||
app:tint="@color/white"
|
||||
android:padding="5dp"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_download"
|
||||
|
@ -207,6 +223,16 @@
|
|||
</LinearLayout>
|
||||
|
||||
</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>
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
|
|
@ -973,6 +973,16 @@
|
|||
<item>Release Date (Oldest)</item>
|
||||
<item>Release Date (Newest)</item>
|
||||
</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">
|
||||
<item>Preview</item>
|
||||
<item>List</item>
|
||||
|
|
Loading…
Add table
Reference in a new issue