Playlists sort and search support, Playlist search support, wip local playback, other fixes

This commit is contained in:
Kelvin K 2025-04-02 22:53:54 +02:00
parent b545545712
commit dd6bde97a9
18 changed files with 481 additions and 61 deletions

View file

@ -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();

View file

@ -0,0 +1,5 @@
package com.futo.platformplayer.api.media.platforms.local
class LocalClient {
//TODO
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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 };

View file

@ -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;
};
}

View file

@ -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;

View file

@ -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;
}
}
}
}

View file

@ -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()) {

View file

@ -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>()

View file

@ -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();
};
};

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="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>

View file

@ -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"

View file

@ -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>

View file

@ -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>