mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-29 22:24:29 +02:00
Merge branch 'subs-exchange' into 'master'
Experimental Subs Exchange See merge request videostreaming/grayjay!91
This commit is contained in:
commit
b57abb646f
@ -294,6 +294,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
||||||
var showSubscriptionGroups: Boolean = true;
|
var showSubscriptionGroups: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
|
||||||
|
var useSubscriptionExchange: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.contents
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
interface IPlatformContent {
|
interface IPlatformContent {
|
||||||
|
@ -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();
|
@ -14,6 +14,7 @@ import java.time.OffsetDateTime
|
|||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
open class SerializedPlatformVideo(
|
open class SerializedPlatformVideo(
|
||||||
|
override val contentType: ContentType = ContentType.MEDIA,
|
||||||
override val id: PlatformID,
|
override val id: PlatformID,
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val thumbnails: Thumbnails,
|
override val thumbnails: Thumbnails,
|
||||||
@ -27,7 +28,6 @@ open class SerializedPlatformVideo(
|
|||||||
override val viewCount: Long,
|
override val viewCount: Long,
|
||||||
override val isShort: Boolean = false
|
override val isShort: Boolean = false
|
||||||
) : IPlatformVideo, SerializedPlatformContent {
|
) : IPlatformVideo, SerializedPlatformContent {
|
||||||
override val contentType: ContentType = ContentType.MEDIA;
|
|
||||||
|
|
||||||
override val isLive: Boolean = false;
|
override val isLive: Boolean = false;
|
||||||
|
|
||||||
@ -44,6 +44,7 @@ open class SerializedPlatformVideo(
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
|
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
|
||||||
return SerializedPlatformVideo(
|
return SerializedPlatformVideo(
|
||||||
|
ContentType.MEDIA,
|
||||||
video.id,
|
video.id,
|
||||||
video.name,
|
video.name,
|
||||||
video.thumbnails,
|
video.thumbnails,
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -132,6 +132,7 @@ import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
|||||||
import com.futo.platformplayer.views.casting.CastView
|
import com.futo.platformplayer.views.casting.CastView
|
||||||
import com.futo.platformplayer.views.comments.AddCommentView
|
import com.futo.platformplayer.views.comments.AddCommentView
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.overlays.ChaptersOverlay
|
||||||
import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
||||||
import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
||||||
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||||
@ -147,6 +148,7 @@ import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
|||||||
import com.futo.platformplayer.views.pills.RoundButton
|
import com.futo.platformplayer.views.pills.RoundButton
|
||||||
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
||||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
import com.futo.platformplayer.views.segments.ChaptersList
|
||||||
import com.futo.platformplayer.views.segments.CommentsList
|
import com.futo.platformplayer.views.segments.CommentsList
|
||||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
import com.futo.platformplayer.views.video.FutoVideoPlayer
|
import com.futo.platformplayer.views.video.FutoVideoPlayer
|
||||||
@ -195,6 +197,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private var _liveChat: LiveChatManager? = null;
|
private var _liveChat: LiveChatManager? = null;
|
||||||
private var _videoResumePositionMilliseconds : Long = 0L;
|
private var _videoResumePositionMilliseconds : Long = 0L;
|
||||||
|
|
||||||
|
private var _chapters: List<IChapter>? = null;
|
||||||
|
|
||||||
private val _player: FutoVideoPlayer;
|
private val _player: FutoVideoPlayer;
|
||||||
private val _cast: CastView;
|
private val _cast: CastView;
|
||||||
private val _playerProgress: PlayerControlView;
|
private val _playerProgress: PlayerControlView;
|
||||||
@ -263,6 +267,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _container_content_liveChat: LiveChatOverlay;
|
private val _container_content_liveChat: LiveChatOverlay;
|
||||||
private val _container_content_browser: WebviewOverlay;
|
private val _container_content_browser: WebviewOverlay;
|
||||||
private val _container_content_support: SupportOverlay;
|
private val _container_content_support: SupportOverlay;
|
||||||
|
private val _container_content_chapters: ChaptersOverlay;
|
||||||
|
|
||||||
private var _container_content_current: View;
|
private var _container_content_current: View;
|
||||||
|
|
||||||
@ -374,6 +379,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
||||||
_container_content_support = findViewById(R.id.videodetail_container_support);
|
_container_content_support = findViewById(R.id.videodetail_container_support);
|
||||||
_container_content_browser = findViewById(R.id.videodetail_container_webview)
|
_container_content_browser = findViewById(R.id.videodetail_container_webview)
|
||||||
|
_container_content_chapters = findViewById(R.id.videodetail_container_chapters);
|
||||||
|
|
||||||
_addCommentView = findViewById(R.id.add_comment_view);
|
_addCommentView = findViewById(R.id.add_comment_view);
|
||||||
_commentsList = findViewById(R.id.comments_list);
|
_commentsList = findViewById(R.id.comments_list);
|
||||||
@ -398,6 +404,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_monetization = findViewById(R.id.monetization);
|
_monetization = findViewById(R.id.monetization);
|
||||||
_player.attachPlayer();
|
_player.attachPlayer();
|
||||||
|
|
||||||
|
_player.onChapterClicked.subscribe {
|
||||||
|
showChaptersUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
_buttonSubscribe.onSubscribed.subscribe {
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||||
@ -686,6 +696,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
|
_container_content_chapters.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
|
|
||||||
|
_container_content_chapters.onClick.subscribe {
|
||||||
|
handleSeek(it.timeStart.toLong() * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
_description_viewMore.setOnClickListener {
|
_description_viewMore.setOnClickListener {
|
||||||
switchContentView(_container_content_description);
|
switchContentView(_container_content_description);
|
||||||
@ -852,6 +867,22 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_cast.stopAllGestures();
|
_cast.stopAllGestures();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showChaptersUI(){
|
||||||
|
video?.let {
|
||||||
|
try {
|
||||||
|
_chapters?.let {
|
||||||
|
if(it.size == 0)
|
||||||
|
return@let;
|
||||||
|
_container_content_chapters.setChapters(_chapters);
|
||||||
|
switchContentView(_container_content_chapters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateMoreButtons() {
|
fun updateMoreButtons() {
|
||||||
val isLimitedVersion = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
val isLimitedVersion = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
||||||
if (it is JSClient)
|
if (it is JSClient)
|
||||||
@ -865,6 +896,13 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
_chapters?.let {
|
||||||
|
if(it != null && it.size > 0)
|
||||||
|
RoundButton(context, R.drawable.ic_list, "Chapters", TAG_CHAPTERS) {
|
||||||
|
showChaptersUI();
|
||||||
|
}
|
||||||
|
else null
|
||||||
|
},
|
||||||
if(video?.isLive ?: false)
|
if(video?.isLive ?: false)
|
||||||
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.live_chat), TAG_LIVECHAT) {
|
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.live_chat), TAG_LIVECHAT) {
|
||||||
video?.let {
|
video?.let {
|
||||||
@ -1340,10 +1378,12 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val chapters = null ?: StatePlatform.instance.getContentChapters(video.url);
|
val chapters = null ?: StatePlatform.instance.getContentChapters(video.url);
|
||||||
_player.setChapters(chapters);
|
_player.setChapters(chapters);
|
||||||
_cast.setChapters(chapters);
|
_cast.setChapters(chapters);
|
||||||
|
_chapters = _player.getChapters();
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
Logger.e(TAG, "Failed to get chapters", ex);
|
Logger.e(TAG, "Failed to get chapters", ex);
|
||||||
_player.setChapters(null);
|
_player.setChapters(null);
|
||||||
_cast.setChapters(null);
|
_cast.setChapters(null);
|
||||||
|
_chapters = null;
|
||||||
|
|
||||||
/*withContext(Dispatchers.Main) {
|
/*withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
||||||
@ -1382,6 +1422,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
updateMoreButtons();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1863,7 +1907,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
else null;
|
else null;
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
video = newDetails;
|
video = newDetails;
|
||||||
_player.setSource(newVideoSource, newAudioSource, true, true);
|
_player.setSource(newVideoSource, newAudioSource, true, true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@ -2601,7 +2645,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChannelClicked.subscribe {
|
onChannelClicked.subscribe {
|
||||||
fragment.navigate<ChannelFragment>(it)
|
if(it.url.isNotBlank())
|
||||||
|
fragment.navigate<ChannelFragment>(it)
|
||||||
|
else
|
||||||
|
UIDialogs.appToast("No author url present");
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddToWatchLaterClicked.subscribe(this) {
|
onAddToWatchLaterClicked.subscribe(this) {
|
||||||
@ -3077,6 +3124,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
const val TAG_SHARE = "share";
|
const val TAG_SHARE = "share";
|
||||||
const val TAG_OVERLAY = "overlay";
|
const val TAG_OVERLAY = "overlay";
|
||||||
const val TAG_LIVECHAT = "livechat";
|
const val TAG_LIVECHAT = "livechat";
|
||||||
|
const val TAG_CHAPTERS = "chapters";
|
||||||
const val TAG_OPEN = "open";
|
const val TAG_OPEN = "open";
|
||||||
const val TAG_SEND_TO_DEVICE = "send_to_device";
|
const val TAG_SEND_TO_DEVICE = "send_to_device";
|
||||||
const val TAG_MORE = "MORE";
|
const val TAG_MORE = "MORE";
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package com.futo.platformplayer.models
|
|||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@ -46,6 +47,7 @@ class HistoryVideo {
|
|||||||
val name = str.substring(indexNext + 3);
|
val name = str.substring(indexNext + 3);
|
||||||
|
|
||||||
val video = resolve?.invoke(url) ?: SerializedPlatformVideo(
|
val video = resolve?.invoke(url) ?: SerializedPlatformVideo(
|
||||||
|
ContentType.MEDIA,
|
||||||
id = PlatformID.asUrlID(url),
|
id = PlatformID.asUrlID(url),
|
||||||
name = name,
|
name = name,
|
||||||
thumbnails = Thumbnails(),
|
thumbnails = Thumbnails(),
|
||||||
|
@ -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()) {
|
||||||
|
@ -39,4 +39,16 @@ class OffsetDateTimeSerializer : KSerializer<OffsetDateTime> {
|
|||||||
return OffsetDateTime.MIN;
|
return OffsetDateTime.MIN;
|
||||||
return OffsetDateTime.of(LocalDateTime.ofEpochSecond(epochSecond, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
return OffsetDateTime.of(LocalDateTime.ofEpochSecond(epochSecond, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
class OffsetDateTimeStringSerializer : KSerializer<OffsetDateTime> {
|
||||||
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("OffsetDateTime", PrimitiveKind.STRING)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: OffsetDateTime) {
|
||||||
|
encoder.encodeString(value.toString());
|
||||||
|
}
|
||||||
|
override fun deserialize(decoder: Decoder): OffsetDateTime {
|
||||||
|
val str = decoder.decodeString();
|
||||||
|
|
||||||
|
return OffsetDateTime.parse(str);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
|
import SubsExchangeClient
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
@ -18,6 +19,7 @@ import com.futo.platformplayer.models.SubscriptionGroup
|
|||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringDateMapStorage
|
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
import com.futo.platformplayer.stores.StringStringMapStorage
|
import com.futo.platformplayer.stores.StringStringMapStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||||
@ -67,10 +69,24 @@ class StateSubscriptions {
|
|||||||
|
|
||||||
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
||||||
|
|
||||||
|
private val _subsExchangeServer = "https://exchange.grayjay.app/";
|
||||||
|
private val _subscriptionKey = FragmentedStorage.get<StringStorage>("sub_exchange_key");
|
||||||
|
|
||||||
init {
|
init {
|
||||||
global.onUpdateProgress.subscribe { progress, total ->
|
global.onUpdateProgress.subscribe { progress, total ->
|
||||||
onFeedProgress.emit(null, progress, total);
|
onFeedProgress.emit(null, progress, total);
|
||||||
}
|
}
|
||||||
|
if(_subscriptionKey.value.isNullOrBlank())
|
||||||
|
generateNewSubsExchangeKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateNewSubsExchangeKey(){
|
||||||
|
_subscriptionKey.setAndSave(SubsExchangeClient.createPrivateKey());
|
||||||
|
}
|
||||||
|
fun getSubsExchangeClient(): SubsExchangeClient {
|
||||||
|
if(_subscriptionKey.value.isNullOrBlank())
|
||||||
|
throw IllegalStateException("No valid subscription exchange key set");
|
||||||
|
return SubsExchangeClient(_subsExchangeServer, _subscriptionKey.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOldestUpdateTime(): OffsetDateTime {
|
fun getOldestUpdateTime(): OffsetDateTime {
|
||||||
@ -359,7 +375,17 @@ class StateSubscriptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null, subGroup: SubscriptionGroup? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null, subGroup: SubscriptionGroup? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
||||||
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool);
|
var exchangeClient: SubsExchangeClient? = null;
|
||||||
|
if(Settings.instance.subscriptions.useSubscriptionExchange) {
|
||||||
|
try {
|
||||||
|
exchangeClient = getSubsExchangeClient();
|
||||||
|
}
|
||||||
|
catch(ex: Throwable){
|
||||||
|
Logger.e(TAG, "Failed to get subs exchange client: ${ex.message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool, exchangeClient);
|
||||||
if(onNewCacheHit != null)
|
if(onNewCacheHit != null)
|
||||||
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
|
import SubsExchangeClient
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
@ -15,8 +16,9 @@ class SmartSubscriptionAlgorithm(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
allowFailure: Boolean = false,
|
allowFailure: Boolean = false,
|
||||||
withCacheFallback: Boolean = true,
|
withCacheFallback: Boolean = true,
|
||||||
threadPool: ForkJoinPool? = null
|
threadPool: ForkJoinPool? = null,
|
||||||
): SubscriptionsTaskFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool) {
|
subsExchangeClient: SubsExchangeClient? = null
|
||||||
|
): SubscriptionsTaskFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool, subsExchangeClient) {
|
||||||
override fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask> {
|
override fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask> {
|
||||||
val allTasks: List<SubscriptionTask> = subs.flatMap { entry ->
|
val allTasks: List<SubscriptionTask> = subs.flatMap { entry ->
|
||||||
val sub = entry.key;
|
val sub = entry.key;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
|
import SubsExchangeClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@ -33,11 +34,11 @@ abstract class SubscriptionFetchAlgorithm(
|
|||||||
companion object {
|
companion object {
|
||||||
public val TAG = "SubscriptionAlgorithm";
|
public val TAG = "SubscriptionAlgorithm";
|
||||||
|
|
||||||
fun getAlgorithm(algo: SubscriptionFetchAlgorithms, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = false, pool: ForkJoinPool? = null): SubscriptionFetchAlgorithm {
|
fun getAlgorithm(algo: SubscriptionFetchAlgorithms, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = false, pool: ForkJoinPool? = null, withExchangeClient: SubsExchangeClient? = null): SubscriptionFetchAlgorithm {
|
||||||
return when(algo) {
|
return when(algo) {
|
||||||
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, 50);
|
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, 50);
|
||||||
SubscriptionFetchAlgorithms.SIMPLE -> SimpleSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
SubscriptionFetchAlgorithms.SIMPLE -> SimpleSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
||||||
SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, withExchangeClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
|
import SubsExchangeClient
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.PlatformContentPager
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.findNonRuntimeException
|
import com.futo.platformplayer.findNonRuntimeException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
||||||
@ -24,6 +28,9 @@ import com.futo.platformplayer.states.StateCache
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
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.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
|
||||||
@ -35,7 +42,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
allowFailure: Boolean = false,
|
allowFailure: Boolean = false,
|
||||||
withCacheFallback: Boolean = true,
|
withCacheFallback: Boolean = true,
|
||||||
_threadPool: ForkJoinPool? = null
|
_threadPool: ForkJoinPool? = null,
|
||||||
|
private val subsExchangeClient: SubsExchangeClient? = null
|
||||||
) : SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, _threadPool) {
|
) : SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, _threadPool) {
|
||||||
|
|
||||||
|
|
||||||
@ -45,7 +53,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
||||||
val tasks = getSubscriptionTasks(subs);
|
var tasks = getSubscriptionTasks(subs).toMutableList()
|
||||||
|
|
||||||
val tasksGrouped = tasks.groupBy { it.client }
|
val tasksGrouped = tasks.groupBy { it.client }
|
||||||
|
|
||||||
@ -70,6 +78,32 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
|
|
||||||
val exs: ArrayList<Throwable> = arrayListOf();
|
val exs: ArrayList<Throwable> = arrayListOf();
|
||||||
|
|
||||||
|
var contract: ExchangeContract? = null;
|
||||||
|
var providedTasks: MutableList<SubscriptionTask>? = null;
|
||||||
|
|
||||||
|
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 failedPlugins = mutableListOf<String>();
|
||||||
val cachedChannels = mutableListOf<String>()
|
val cachedChannels = mutableListOf<String>()
|
||||||
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
||||||
@ -104,6 +138,42 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Resolve Subscription Exchange
|
||||||
|
if(contract != null) {
|
||||||
|
try {
|
||||||
|
val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract.required.contains(it.task.url) }.map {
|
||||||
|
ChannelResolve(
|
||||||
|
it.task.url,
|
||||||
|
it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) }
|
||||||
|
)
|
||||||
|
}.toTypedArray()
|
||||||
|
val resolve = subsExchangeClient?.resolveContract(
|
||||||
|
contract,
|
||||||
|
*resolves
|
||||||
|
);
|
||||||
|
if (resolve != null) {
|
||||||
|
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size})")
|
||||||
|
for(result in resolve){
|
||||||
|
val task = providedTasks?.find { it.url == result.channelUrl };
|
||||||
|
if(task != null) {
|
||||||
|
taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null));
|
||||||
|
providedTasks?.remove(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (providedTasks != null) {
|
||||||
|
for(task in providedTasks) {
|
||||||
|
taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
//TODO: fetch remainder after all?
|
||||||
|
Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms")
|
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms")
|
||||||
|
|
||||||
//Cache pagers grouped by channel
|
//Cache pagers grouped by channel
|
||||||
@ -173,6 +243,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Intercepts task.fromCache & task.fromPeek
|
||||||
synchronized(cachedChannels) {
|
synchronized(cachedChannels) {
|
||||||
if(task.fromCache || task.fromPeek) {
|
if(task.fromCache || task.fromPeek) {
|
||||||
finished++;
|
finished++;
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.futo.platformplayer.subsexchange
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChannelRequest(
|
||||||
|
@SerialName("ChannelUrl")
|
||||||
|
var channelUrl: String
|
||||||
|
);
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.futo.platformplayer.subsexchange
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChannelResolve(
|
||||||
|
@SerialName("ChannelUrl")
|
||||||
|
var channelUrl: String,
|
||||||
|
@SerialName("Content")
|
||||||
|
var content: List<SerializedPlatformContent>,
|
||||||
|
@SerialName("Channel")
|
||||||
|
var channel: IPlatformChannel? = null
|
||||||
|
)
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.futo.platformplayer.subsexchange
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChannelResult(
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
|
@SerialName("DateTime")
|
||||||
|
var dateTime: OffsetDateTime,
|
||||||
|
@SerialName("ChannelUrl")
|
||||||
|
var channelUrl: String,
|
||||||
|
@SerialName("Content")
|
||||||
|
var content: List<SerializedPlatformContent>,
|
||||||
|
@SerialName("Channel")
|
||||||
|
var channel: IPlatformChannel? = null
|
||||||
|
)
|
@ -0,0 +1,27 @@
|
|||||||
|
package com.futo.platformplayer.subsexchange
|
||||||
|
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeStringSerializer
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Serializer
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ExchangeContract(
|
||||||
|
@SerialName("ID")
|
||||||
|
var id: String,
|
||||||
|
@SerialName("Requests")
|
||||||
|
var requests: List<ChannelRequest>,
|
||||||
|
@SerialName("Provided")
|
||||||
|
var provided: List<String> = listOf(),
|
||||||
|
@SerialName("Required")
|
||||||
|
var required: List<String> = listOf(),
|
||||||
|
@SerialName("Expire")
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeStringSerializer::class)
|
||||||
|
var expired: OffsetDateTime = OffsetDateTime.MIN,
|
||||||
|
@SerialName("ContractVersion")
|
||||||
|
var contractVersion: Int = 1
|
||||||
|
)
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.futo.platformplayer.subsexchange
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ExchangeContractResolve(
|
||||||
|
@SerialName("PublicKey")
|
||||||
|
val publicKey: String,
|
||||||
|
@SerialName("Signature")
|
||||||
|
val signature: String,
|
||||||
|
@SerialName("Data")
|
||||||
|
val data: String
|
||||||
|
)
|
@ -0,0 +1,149 @@
|
|||||||
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.subsexchange.ChannelRequest
|
||||||
|
import com.futo.platformplayer.subsexchange.ChannelResolve
|
||||||
|
import com.futo.platformplayer.subsexchange.ChannelResult
|
||||||
|
import com.futo.platformplayer.subsexchange.ExchangeContract
|
||||||
|
import com.futo.platformplayer.subsexchange.ExchangeContractResolve
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.security.Signature
|
||||||
|
import java.security.interfaces.RSAPrivateKey
|
||||||
|
import java.security.interfaces.RSAPublicKey
|
||||||
|
import java.util.Base64
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.io.OutputStreamWriter
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import java.security.spec.RSAPublicKeySpec
|
||||||
|
|
||||||
|
|
||||||
|
class SubsExchangeClient(private val server: String, private val privateKey: String) {
|
||||||
|
|
||||||
|
private val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val publicKey: String = extractPublicKey(privateKey)
|
||||||
|
|
||||||
|
// Endpoints
|
||||||
|
|
||||||
|
// Endpoint: Contract
|
||||||
|
fun requestContract(vararg channels: ChannelRequest): ExchangeContract {
|
||||||
|
val data = post("/api/Channel/Contract", Json.encodeToString(channels), "application/json")
|
||||||
|
return Json.decodeFromString(data)
|
||||||
|
}
|
||||||
|
suspend fun requestContractAsync(vararg channels: ChannelRequest): ExchangeContract {
|
||||||
|
val data = postAsync("/api/Channel/Contract", Json.encodeToString(channels), "application/json")
|
||||||
|
return Json.decodeFromString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint: Resolve
|
||||||
|
fun resolveContract(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
|
||||||
|
val contractResolve = convertResolves(*resolves)
|
||||||
|
val result = post("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json")
|
||||||
|
return Serializer.json.decodeFromString(result)
|
||||||
|
}
|
||||||
|
suspend fun resolveContractAsync(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
|
||||||
|
val contractResolve = convertResolves(*resolves)
|
||||||
|
val result = postAsync("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json")
|
||||||
|
return Serializer.json.decodeFromString(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun convertResolves(vararg resolves: ChannelResolve): ExchangeContractResolve {
|
||||||
|
val data = Serializer.json.encodeToString(resolves)
|
||||||
|
val signature = createSignature(data, privateKey)
|
||||||
|
|
||||||
|
return ExchangeContractResolve(
|
||||||
|
publicKey = publicKey,
|
||||||
|
signature = signature,
|
||||||
|
data = data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IO methods
|
||||||
|
private fun post(query: String, body: String, contentType: String): String {
|
||||||
|
val url = URL("${server.trim('/')}$query")
|
||||||
|
with(url.openConnection() as HttpURLConnection) {
|
||||||
|
requestMethod = "POST"
|
||||||
|
setRequestProperty("Content-Type", contentType)
|
||||||
|
doOutput = true
|
||||||
|
OutputStreamWriter(outputStream, StandardCharsets.UTF_8).use { it.write(body); it.flush() }
|
||||||
|
|
||||||
|
val status = responseCode;
|
||||||
|
Logger.i("SubsExchangeClient", "POST [${url}]: ${status}");
|
||||||
|
|
||||||
|
if(status == 200)
|
||||||
|
InputStreamReader(inputStream, StandardCharsets.UTF_8).use {
|
||||||
|
return it.readText()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var errorStr = "";
|
||||||
|
try {
|
||||||
|
errorStr = InputStreamReader(errorStream, StandardCharsets.UTF_8).use {
|
||||||
|
return@use it.readText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable){}
|
||||||
|
|
||||||
|
throw Exception("Exchange server resulted in code ${status}:\n" + errorStr);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private suspend fun postAsync(query: String, body: String, contentType: String): String {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
post(query, body, contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crypto methods
|
||||||
|
companion object {
|
||||||
|
fun createPrivateKey(): String {
|
||||||
|
val rsa = KeyFactory.getInstance("RSA")
|
||||||
|
val keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||||
|
keyPairGenerator.initialize(2048);
|
||||||
|
val keyPair = keyPairGenerator.generateKeyPair();
|
||||||
|
return Base64.getEncoder().encodeToString(keyPair.private.encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractPublicKey(privateKey: String): String {
|
||||||
|
val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))
|
||||||
|
val keyFactory = KeyFactory.getInstance("RSA")
|
||||||
|
val privateKeyObj = keyFactory.generatePrivate(keySpec) as RSAPrivateKey
|
||||||
|
val publicKeyObj: PublicKey? = keyFactory.generatePublic(RSAPublicKeySpec(privateKeyObj.modulus, BigInteger.valueOf(65537)));
|
||||||
|
var publicKeyBase64 = Base64.getEncoder().encodeToString(publicKeyObj?.encoded);
|
||||||
|
var pem = "-----BEGIN PUBLIC KEY-----"
|
||||||
|
while(publicKeyBase64.length > 0) {
|
||||||
|
val length = Math.min(publicKeyBase64.length, 64);
|
||||||
|
pem += "\n" + publicKeyBase64.substring(0, length);
|
||||||
|
publicKeyBase64 = publicKeyBase64.substring(length);
|
||||||
|
}
|
||||||
|
return pem + "\n-----END PUBLIC KEY-----";
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createSignature(data: String, privateKey: String): String {
|
||||||
|
val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))
|
||||||
|
val keyFactory = KeyFactory.getInstance("RSA")
|
||||||
|
val rsaPrivateKey = keyFactory.generatePrivate(keySpec) as RSAPrivateKey
|
||||||
|
|
||||||
|
val signature = Signature.getInstance("SHA256withRSA")
|
||||||
|
signature.initSign(rsaPrivateKey)
|
||||||
|
signature.update(data.toByteArray(Charsets.UTF_8))
|
||||||
|
|
||||||
|
val signatureBytes = signature.sign()
|
||||||
|
return Base64.getEncoder().encodeToString(signatureBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package com.futo.platformplayer.views.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.LazyComment
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
|
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.toHumanDuration
|
||||||
|
import com.futo.platformplayer.toHumanNowDiffString
|
||||||
|
import com.futo.platformplayer.toHumanNumber
|
||||||
|
import com.futo.platformplayer.toHumanTime
|
||||||
|
import com.futo.platformplayer.views.LoaderView
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.pills.PillButton
|
||||||
|
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
|
import com.futo.polycentric.core.Opinion
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class ChapterViewHolder : ViewHolder {
|
||||||
|
|
||||||
|
private val _layoutChapter: ConstraintLayout;
|
||||||
|
|
||||||
|
private val _containerChapter: ConstraintLayout;
|
||||||
|
|
||||||
|
private val _textTitle: TextView;
|
||||||
|
private val _textTimestamp: TextView;
|
||||||
|
private val _textMeta: TextView;
|
||||||
|
|
||||||
|
var onClick = Event1<IChapter>();
|
||||||
|
var chapter: IChapter? = null
|
||||||
|
private set;
|
||||||
|
|
||||||
|
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_chapter, viewGroup, false)) {
|
||||||
|
_layoutChapter = itemView.findViewById(R.id.layout_chapter);
|
||||||
|
_containerChapter = itemView.findViewById(R.id.chapter_container);
|
||||||
|
|
||||||
|
_containerChapter.setOnClickListener {
|
||||||
|
chapter?.let {
|
||||||
|
onClick.emit(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_textTitle = itemView.findViewById(R.id.text_title);
|
||||||
|
_textTimestamp = itemView.findViewById(R.id.text_timestamp);
|
||||||
|
_textMeta = itemView.findViewById(R.id.text_meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(chapter: IChapter) {
|
||||||
|
_textTitle.text = chapter.name;
|
||||||
|
_textTimestamp.text = chapter.timeStart.toLong().toHumanTime(false);
|
||||||
|
|
||||||
|
if(chapter.type == ChapterType.NORMAL) {
|
||||||
|
_textMeta.isVisible = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_textMeta.isVisible = true;
|
||||||
|
when(chapter.type) {
|
||||||
|
ChapterType.SKIP -> _textMeta.text = "(Skip)";
|
||||||
|
ChapterType.SKIPPABLE -> _textMeta.text = "(Manual Skip)"
|
||||||
|
ChapterType.SKIPONCE -> _textMeta.text = "(Skip Once)"
|
||||||
|
else -> _textMeta.isVisible = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.chapter = chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "CommentViewHolder";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
package com.futo.platformplayer.views.overlays
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.toHumanNowDiffString
|
||||||
|
import com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||||
|
import com.futo.platformplayer.views.comments.AddCommentView
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.segments.ChaptersList
|
||||||
|
import com.futo.platformplayer.views.segments.CommentsList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import userpackage.Protocol
|
||||||
|
|
||||||
|
class ChaptersOverlay : LinearLayout {
|
||||||
|
val onClose = Event0();
|
||||||
|
val onClick = Event1<IChapter>();
|
||||||
|
|
||||||
|
private val _topbar: OverlayTopbar;
|
||||||
|
private val _chaptersList: ChaptersList;
|
||||||
|
private var _onChapterClicked: ((chapter: IChapter) -> Unit)? = null;
|
||||||
|
private val _layoutItems: LinearLayout
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
|
inflate(context, R.layout.overlay_chapters, this)
|
||||||
|
_layoutItems = findViewById(R.id.layout_items)
|
||||||
|
_topbar = findViewById(R.id.topbar);
|
||||||
|
_chaptersList = findViewById(R.id.chapters_list);
|
||||||
|
_chaptersList.onChapterClick.subscribe(onClick::emit);
|
||||||
|
_topbar.onClose.subscribe(this, onClose::emit);
|
||||||
|
_topbar.setInfo(context.getString(R.string.chapters), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setChapters(chapters: List<IChapter>?) {
|
||||||
|
_chaptersList?.setChapters(chapters ?: listOf());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun cleanup() {
|
||||||
|
_topbar.onClose.remove(this);
|
||||||
|
_onChapterClicked = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ChaptersOverlay"
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,103 @@
|
|||||||
|
package com.futo.platformplayer.views.segments
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.LazyComment
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.views.adapters.ChapterViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.CommentViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
|
class ChaptersList : ConstraintLayout {
|
||||||
|
private val _llmReplies: LinearLayoutManager;
|
||||||
|
|
||||||
|
private val _adapterChapters: InsertedViewAdapterWithLoader<ChapterViewHolder>;
|
||||||
|
private val _recyclerChapters: RecyclerView;
|
||||||
|
private val _chapters: ArrayList<IChapter> = arrayListOf();
|
||||||
|
private val _prependedView: FrameLayout;
|
||||||
|
private var _readonly: Boolean = false;
|
||||||
|
private val _layoutScrollToTop: FrameLayout;
|
||||||
|
|
||||||
|
var onChapterClick = Event1<IChapter>();
|
||||||
|
var onCommentsLoaded = Event1<Int>();
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||||
|
LayoutInflater.from(context).inflate(R.layout.view_chapters_list, this, true);
|
||||||
|
|
||||||
|
_recyclerChapters = findViewById(R.id.recycler_chapters);
|
||||||
|
|
||||||
|
_layoutScrollToTop = findViewById(R.id.layout_scroll_to_top);
|
||||||
|
_layoutScrollToTop.setOnClickListener {
|
||||||
|
_recyclerChapters.smoothScrollToPosition(0)
|
||||||
|
}
|
||||||
|
_layoutScrollToTop.visibility = View.GONE
|
||||||
|
|
||||||
|
_prependedView = FrameLayout(context);
|
||||||
|
_prependedView.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
|
||||||
|
|
||||||
|
_adapterChapters = InsertedViewAdapterWithLoader(context, arrayListOf(_prependedView), arrayListOf(),
|
||||||
|
childCountGetter = { _chapters.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(_chapters[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = ChapterViewHolder(viewGroup);
|
||||||
|
holder.onClick.subscribe { c -> onChapterClick.emit(c) };
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
_llmReplies = LinearLayoutManager(context);
|
||||||
|
_recyclerChapters.layoutManager = _llmReplies;
|
||||||
|
_recyclerChapters.adapter = _adapterChapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addChapter(chapter: IChapter) {
|
||||||
|
_chapters.add(0, chapter);
|
||||||
|
_adapterChapters.notifyItemRangeInserted(_adapterChapters.childToParentPosition(0), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPrependedView(view: View) {
|
||||||
|
_prependedView.removeAllViews();
|
||||||
|
_prependedView.addView(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setChapters(chapters: List<IChapter>) {
|
||||||
|
_chapters.clear();
|
||||||
|
_chapters.addAll(chapters);
|
||||||
|
_adapterChapters.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
_chapters.clear();
|
||||||
|
_adapterChapters.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "CommentsList";
|
||||||
|
}
|
||||||
|
}
|
@ -145,6 +145,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
val onVideoClicked = Event0();
|
val onVideoClicked = Event0();
|
||||||
val onTimeBarChanged = Event2<Long, Long>();
|
val onTimeBarChanged = Event2<Long, Long>();
|
||||||
|
|
||||||
|
val onChapterClicked = Event1<IChapter>();
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
|
constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
|
||||||
LayoutInflater.from(context).inflate(R.layout.video_view, this, true);
|
LayoutInflater.from(context).inflate(R.layout.video_view, this, true);
|
||||||
@ -185,6 +187,12 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
_control_duration_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_duration);
|
_control_duration_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_duration);
|
||||||
_control_pause_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_pause);
|
_control_pause_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_pause);
|
||||||
|
|
||||||
|
_control_chapter.setOnClickListener {
|
||||||
|
_currentChapter?.let {
|
||||||
|
onChapterClicked.emit(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val castVisibility = if (Settings.instance.casting.enabled) View.VISIBLE else View.GONE
|
val castVisibility = if (Settings.instance.casting.enabled) View.VISIBLE else View.GONE
|
||||||
_control_cast.visibility = castVisibility
|
_control_cast.visibility = castVisibility
|
||||||
_control_cast_fullscreen.visibility = castVisibility
|
_control_cast_fullscreen.visibility = castVisibility
|
||||||
|
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>
|
@ -168,7 +168,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:background="@drawable/background_button_round"
|
android:background="@drawable/background_button_round"
|
||||||
android:hint="Seach.." />
|
android:hint="Search.." />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -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>
|
||||||
|
@ -579,6 +579,12 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.overlays.ChaptersOverlay
|
||||||
|
android:id="@+id/videodetail_container_chapters"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<com.futo.platformplayer.views.overlays.SupportOverlay
|
<com.futo.platformplayer.views.overlays.SupportOverlay
|
||||||
android:id="@+id/videodetail_container_support"
|
android:id="@+id/videodetail_container_support"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
81
app/src/main/res/layout/list_chapter.xml
Normal file
81
app/src/main/res/layout/list_chapter.xml
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/layout_chapter"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
|
android:layout_marginStart="14dp"
|
||||||
|
android:layout_marginEnd="14dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/chapter_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="2dp"
|
||||||
|
android:padding="15dp"
|
||||||
|
android:background="@drawable/background_1b_round_6dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="Some chapter text" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_meta"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textColor="@color/text_color_tinted"
|
||||||
|
android:textSize="11sp"
|
||||||
|
tools:text="test" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_timestamp"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:background="@drawable/background_thumbnail_duration"
|
||||||
|
android:paddingLeft="10dp"
|
||||||
|
android:paddingRight="10dp"
|
||||||
|
android:paddingTop="5dp"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:textColor="@color/gray_ac"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="1:23" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
34
app/src/main/res/layout/overlay_chapters.xml
Normal file
34
app/src/main/res/layout/overlay_chapters.xml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:id="@+id/layout_items">
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.overlays.OverlayTopbar
|
||||||
|
android:id="@+id/topbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:paddingTop="5dp"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
|
app:title="Chapters"
|
||||||
|
app:metadata="" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.segments.ChaptersList
|
||||||
|
android:id="@+id/chapters_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginTop="12dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
31
app/src/main/res/layout/view_chapters_list.xml
Normal file
31
app/src/main/res/layout/view_chapters_list.xml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_chapters"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|center_horizontal"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:paddingBottom="7dp"
|
||||||
|
android:paddingEnd="14dp"
|
||||||
|
android:paddingTop="7dp"
|
||||||
|
android:paddingStart="14dp"
|
||||||
|
android:background="@drawable/background_pill"
|
||||||
|
android:id="@+id/layout_scroll_to_top">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/scroll_to_top"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textSize="14dp"/>
|
||||||
|
</FrameLayout>
|
||||||
|
</FrameLayout>
|
@ -412,6 +412,8 @@
|
|||||||
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
|
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
|
||||||
<string name="subscription_group_menu">Groups</string>
|
<string name="subscription_group_menu">Groups</string>
|
||||||
<string name="show_subscription_group">Show Subscription Groups</string>
|
<string name="show_subscription_group">Show Subscription Groups</string>
|
||||||
|
<string name="use_subscription_exchange">Use Subscription Exchange (Experimental)</string>
|
||||||
|
<string name="use_subscription_exchange_description">Uses a centralized crowd-sourced server to significantly reduce the required requests for subscriptions, in exchange you submit your subscriptions to the server.</string>
|
||||||
<string name="show_subscription_group_description">If subscription groups should be shown above your subscriptions to filter</string>
|
<string name="show_subscription_group_description">If subscription groups should be shown above your subscriptions to filter</string>
|
||||||
<string name="preview_feed_items">Preview Feed Items</string>
|
<string name="preview_feed_items">Preview Feed Items</string>
|
||||||
<string name="preview_feed_items_description">When the preview feedstyle is used, if items should auto-preview when scrolling over them</string>
|
<string name="preview_feed_items_description">When the preview feedstyle is used, if items should auto-preview when scrolling over them</string>
|
||||||
@ -663,6 +665,7 @@
|
|||||||
<string name="failed_to_load_post">Failed to load post.</string>
|
<string name="failed_to_load_post">Failed to load post.</string>
|
||||||
<string name="replies">replies</string>
|
<string name="replies">replies</string>
|
||||||
<string name="Replies">Replies</string>
|
<string name="Replies">Replies</string>
|
||||||
|
<string name="chapters">Chapters</string>
|
||||||
<string name="plugin_settings_saved">Plugin settings saved</string>
|
<string name="plugin_settings_saved">Plugin settings saved</string>
|
||||||
<string name="plugin_settings">Plugin settings</string>
|
<string name="plugin_settings">Plugin settings</string>
|
||||||
<string name="these_settings_are_defined_by_the_plugin">These settings are defined by the plugin</string>
|
<string name="these_settings_are_defined_by_the_plugin">These settings are defined by the plugin</string>
|
||||||
@ -970,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>
|
||||||
|
@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.Serializer
|
|||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnail
|
import com.futo.platformplayer.api.media.models.Thumbnail
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||||
@ -39,6 +40,7 @@ class RequireMigrationTests {
|
|||||||
val viewCount = 1000L
|
val viewCount = 1000L
|
||||||
|
|
||||||
return SerializedPlatformVideo(
|
return SerializedPlatformVideo(
|
||||||
|
ContentType.MEDIA,
|
||||||
platformId,
|
platformId,
|
||||||
name,
|
name,
|
||||||
thumbnails,
|
thumbnails,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user