mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
1347 lines
70 KiB
Kotlin
1347 lines
70 KiB
Kotlin
package com.futo.platformplayer
|
|
|
|
import android.app.NotificationManager
|
|
import android.content.ContentResolver
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.net.Uri
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import androidx.annotation.OptIn
|
|
import androidx.media3.common.util.UnstableApi
|
|
import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory
|
|
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist
|
|
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
|
|
import androidx.recyclerview.widget.RecyclerView
|
|
import com.futo.platformplayer.activities.MainActivity
|
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
|
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
|
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
|
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.streams.sources.IVideoUrlSource
|
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
|
import com.futo.platformplayer.downloads.VideoLocal
|
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
|
import com.futo.platformplayer.helpers.VideoHelper
|
|
import com.futo.platformplayer.logging.Logger
|
|
import com.futo.platformplayer.models.ImageVariable
|
|
import com.futo.platformplayer.models.Playlist
|
|
import com.futo.platformplayer.models.Subscription
|
|
import com.futo.platformplayer.models.SubscriptionGroup
|
|
import com.futo.platformplayer.parsers.HLS
|
|
import com.futo.platformplayer.parsers.HLS.MediaRendition
|
|
import com.futo.platformplayer.parsers.HLS.StreamInfo
|
|
import com.futo.platformplayer.parsers.HLS.VariantPlaylistReference
|
|
import com.futo.platformplayer.states.StateApp
|
|
import com.futo.platformplayer.states.StateDownloads
|
|
import com.futo.platformplayer.states.StateHistory
|
|
import com.futo.platformplayer.states.StateMeta
|
|
import com.futo.platformplayer.states.StatePlatform
|
|
import com.futo.platformplayer.states.StatePlayer
|
|
import com.futo.platformplayer.states.StatePlaylists
|
|
import com.futo.platformplayer.states.StateSubscriptionGroups
|
|
import com.futo.platformplayer.views.AnyAdapterView
|
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
|
import com.futo.platformplayer.views.LoaderView
|
|
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
|
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
|
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuRecycler
|
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
|
import com.futo.platformplayer.views.pills.RoundButton
|
|
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
|
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
|
|
import isDownloadable
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withContext
|
|
import java.io.ByteArrayInputStream
|
|
import androidx.core.net.toUri
|
|
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
|
|
|
class UISlideOverlays {
|
|
companion object {
|
|
private const val TAG = "UISlideOverlays";
|
|
|
|
fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit, vararg views: View): SlideUpMenuOverlay {
|
|
var menu = SlideUpMenuOverlay(container.context, container, title, okButton, true, *views);
|
|
|
|
menu.onOK.subscribe {
|
|
menu.hide();
|
|
onOk.invoke();
|
|
};
|
|
menu.show();
|
|
return menu;
|
|
}
|
|
|
|
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
|
|
UISlideOverlays.showOverlay(container, "Queue options", null, {
|
|
|
|
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
|
|
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
|
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
|
|
|
|
addPlaylistOverlay.onOK.subscribe {
|
|
val text = nameInput.text.trim()
|
|
if (text.isBlank()) {
|
|
return@subscribe;
|
|
}
|
|
|
|
addPlaylistOverlay.hide();
|
|
nameInput.deactivate();
|
|
nameInput.clear();
|
|
StatePlayer.instance.saveQueueAsPlaylist(text);
|
|
UIDialogs.appToast("Playlist [${text}] created");
|
|
};
|
|
|
|
addPlaylistOverlay.onCancel.subscribe {
|
|
nameInput.deactivate();
|
|
nameInput.clear();
|
|
};
|
|
|
|
addPlaylistOverlay.show();
|
|
nameInput.activate();
|
|
}, false));
|
|
}
|
|
|
|
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
|
val items = arrayListOf<View>();
|
|
|
|
val originalNotif = subscription.doNotifications;
|
|
val originalLive = subscription.doFetchLive;
|
|
val originalStream = subscription.doFetchStreams;
|
|
val originalVideo = subscription.doFetchVideos;
|
|
val originalPosts = subscription.doFetchPosts;
|
|
|
|
val menu = SlideUpMenuOverlay(
|
|
container.context,
|
|
container,
|
|
"Subscription Settings",
|
|
null,
|
|
true,
|
|
listOf()
|
|
);
|
|
|
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
|
try {
|
|
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
|
val capabilities = plugin.getChannelCapabilities();
|
|
|
|
withContext(Dispatchers.Main) {
|
|
items.addAll(
|
|
listOf(
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_notifications,
|
|
"Notifications",
|
|
"",
|
|
tag = "notifications",
|
|
call = {
|
|
subscription.doNotifications =
|
|
menu?.selectOption(null, "notifications", true, true)
|
|
?: subscription.doNotifications;
|
|
},
|
|
invokeParent = false
|
|
),
|
|
if (StateSubscriptionGroups.instance.getSubscriptionGroups()
|
|
.isNotEmpty()
|
|
)
|
|
SlideUpMenuGroup(
|
|
container.context, "Subscription Groups",
|
|
"You can select which groups this subscription is part of.",
|
|
-1, listOf()
|
|
) else null,
|
|
if (StateSubscriptionGroups.instance.getSubscriptionGroups()
|
|
.isNotEmpty()
|
|
)
|
|
SlideUpMenuRecycler(container.context, "as") {
|
|
val groups =
|
|
ArrayList<SubscriptionGroup>(
|
|
StateSubscriptionGroups.instance.getSubscriptionGroups()
|
|
.map {
|
|
SubscriptionGroup.Selectable(
|
|
it,
|
|
it.urls.contains(subscription.channel.url)
|
|
)
|
|
}
|
|
.sortedBy { !it.selected });
|
|
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? =
|
|
null;
|
|
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
|
|
it.onClick.subscribe {
|
|
if (it is SubscriptionGroup.Selectable) {
|
|
val actualGroup =
|
|
StateSubscriptionGroups.instance.getSubscriptionGroup(
|
|
it.id
|
|
)
|
|
?: return@subscribe;
|
|
groups.clear();
|
|
if (it.selected)
|
|
actualGroup.urls.remove(subscription.channel.url);
|
|
else
|
|
actualGroup.urls.add(subscription.channel.url);
|
|
|
|
StateSubscriptionGroups.instance.updateSubscriptionGroup(
|
|
actualGroup
|
|
);
|
|
groups.addAll(
|
|
StateSubscriptionGroups.instance.getSubscriptionGroups()
|
|
.map {
|
|
SubscriptionGroup.Selectable(
|
|
it,
|
|
it.urls.contains(subscription.channel.url)
|
|
)
|
|
}
|
|
.sortedBy { !it.selected });
|
|
adapter?.notifyContentChanged();
|
|
}
|
|
}
|
|
};
|
|
return@SlideUpMenuRecycler adapter;
|
|
} else null,
|
|
SlideUpMenuGroup(
|
|
container.context, "Fetch Settings",
|
|
"Depending on the platform you might not need to enable a type for it to be available.",
|
|
-1, listOf()
|
|
),
|
|
if (capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_live_tv,
|
|
"Livestreams",
|
|
"Check for livestreams",
|
|
tag = "fetchLive",
|
|
call = {
|
|
subscription.doFetchLive =
|
|
menu?.selectOption(null, "fetchLive", true, true)
|
|
?: subscription.doFetchLive;
|
|
},
|
|
invokeParent = false
|
|
) else null,
|
|
if (capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_play,
|
|
"Streams",
|
|
"Check for streams",
|
|
tag = "fetchStreams",
|
|
call = {
|
|
subscription.doFetchStreams =
|
|
menu?.selectOption(null, "fetchStreams", true, true)
|
|
?: subscription.doFetchStreams;
|
|
},
|
|
invokeParent = false
|
|
) else null,
|
|
if (capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_play,
|
|
"Videos",
|
|
"Check for videos",
|
|
tag = "fetchVideos",
|
|
call = {
|
|
subscription.doFetchVideos =
|
|
menu?.selectOption(null, "fetchVideos", true, true)
|
|
?: subscription.doFetchVideos;
|
|
},
|
|
invokeParent = false
|
|
) else if (capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_play,
|
|
"Content",
|
|
"Check for content",
|
|
tag = "fetchVideos",
|
|
call = {
|
|
subscription.doFetchVideos =
|
|
menu?.selectOption(null, "fetchVideos", true, true)
|
|
?: subscription.doFetchVideos;
|
|
},
|
|
invokeParent = false
|
|
) else null,
|
|
if (capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_chat,
|
|
"Posts",
|
|
"Check for posts",
|
|
tag = "fetchPosts",
|
|
call = {
|
|
subscription.doFetchPosts =
|
|
menu?.selectOption(null, "fetchPosts", true, true)
|
|
?: subscription.doFetchPosts;
|
|
},
|
|
invokeParent = false
|
|
) else null/*,,
|
|
|
|
SlideUpMenuGroup(container.context, "Actions",
|
|
"Various things you can do with this subscription",
|
|
-1, listOf())
|
|
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
|
|
showCreateSubscriptionGroup(container, subscription.channel);
|
|
}, false)*/
|
|
).filterNotNull()
|
|
);
|
|
|
|
menu.setItems(items);
|
|
|
|
if (subscription.doNotifications)
|
|
menu.selectOption(null, "notifications", true, true);
|
|
if (subscription.doFetchLive)
|
|
menu.selectOption(null, "fetchLive", true, true);
|
|
if (subscription.doFetchStreams)
|
|
menu.selectOption(null, "fetchStreams", true, true);
|
|
if (subscription.doFetchVideos)
|
|
menu.selectOption(null, "fetchVideos", true, true);
|
|
if (subscription.doFetchPosts)
|
|
menu.selectOption(null, "fetchPosts", true, true);
|
|
|
|
menu.onOK.subscribe {
|
|
subscription.save();
|
|
menu.hide(true);
|
|
|
|
if (subscription.doNotifications && !originalNotif) {
|
|
val mainContext = StateApp.instance.contextOrNull;
|
|
if (Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
|
|
UIDialogs.toast(
|
|
container.context,
|
|
"Enable 'Background Update' in settings for notifications to work"
|
|
);
|
|
|
|
if (mainContext is MainActivity) {
|
|
UIDialogs.showDialog(
|
|
mainContext,
|
|
R.drawable.ic_settings,
|
|
"Background Updating Required",
|
|
"You need to set a Background Updating interval for notifications",
|
|
null,
|
|
0,
|
|
UIDialogs.Action("Cancel", {}),
|
|
UIDialogs.Action("Configure", {
|
|
StateApp.instance.activity?.let {
|
|
it.navigate(it.getFragment<SettingsFragment>(), mainContext.getString(R.string.background_update))
|
|
}
|
|
}, UIDialogs.ActionStyle.PRIMARY)
|
|
);
|
|
}
|
|
return@subscribe;
|
|
} else if (!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
|
|
UIDialogs.toast(
|
|
container.context,
|
|
"Android notifications are disabled"
|
|
);
|
|
if (mainContext is MainActivity) {
|
|
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
|
|
}
|
|
}
|
|
}
|
|
};
|
|
menu.onCancel.subscribe {
|
|
subscription.doNotifications = originalNotif;
|
|
subscription.doFetchLive = originalLive;
|
|
subscription.doFetchStreams = originalStream;
|
|
subscription.doFetchVideos = originalVideo;
|
|
subscription.doFetchPosts = originalPosts;
|
|
};
|
|
|
|
menu.setOk("Save");
|
|
|
|
menu.show();
|
|
}
|
|
} catch (e: Throwable) {
|
|
Logger.e(TAG, "Failed to show subscription overlay.", e)
|
|
}
|
|
}
|
|
|
|
return menu;
|
|
}
|
|
|
|
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
|
|
|
|
}
|
|
|
|
@OptIn(UnstableApi::class)
|
|
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
|
val items = arrayListOf<View>(LoaderView(container.context))
|
|
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
|
|
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
|
val masterPlaylistResponse = ManagedHttpClient().get(sourceUrl)
|
|
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
|
|
|
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
|
?: throw Exception("Master playlist content is empty")
|
|
|
|
val resolvedPlaylistUrl = masterPlaylistResponse.url
|
|
|
|
val videoButtons = arrayListOf<SlideUpMenuItem>()
|
|
val audioButtons = arrayListOf<SlideUpMenuItem>()
|
|
//TODO: Implement subtitles
|
|
//val subtitleButtons = arrayListOf<SlideUpMenuItem>()
|
|
|
|
var selectedVideoVariant: HLSVariantVideoUrlSource? = null
|
|
var selectedAudioVariant: HLSVariantAudioUrlSource? = null
|
|
//TODO: Implement subtitles
|
|
//var selectedSubtitleVariant: HLSVariantSubtitleUrlSource? = null
|
|
|
|
val masterPlaylist: HLS.MasterPlaylist
|
|
try {
|
|
val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray())
|
|
val playlist = DefaultHlsPlaylistParserFactory().createPlaylistParser()
|
|
.parse(sourceUrl.toUri(), inputStream)
|
|
|
|
if (playlist is HlsMediaPlaylist) {
|
|
if (source is IHLSManifestAudioSource) {
|
|
val variant = HLS.mediaRenditionToVariant(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null))!!
|
|
|
|
val estSize = VideoHelper.estimateSourceSize(variant);
|
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
|
audioButtons.add(SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_music,
|
|
variant.name,
|
|
listOf(variant.language, variant.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
|
(prefix + variant.codec).trim(),
|
|
tag = variant,
|
|
call = {
|
|
selectedAudioVariant = variant
|
|
slideUpMenuOverlay.selectOption(audioButtons, variant)
|
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
|
},
|
|
invokeParent = false
|
|
))
|
|
} else {
|
|
val variant = HLS.variantReferenceToVariant(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null)))
|
|
|
|
val estSize = VideoHelper.estimateSourceSize(variant);
|
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
|
videoButtons.add(SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_movie,
|
|
variant.name,
|
|
"${variant.width}x${variant.height}",
|
|
(prefix + variant.codec).trim(),
|
|
tag = variant,
|
|
call = {
|
|
selectedVideoVariant = variant
|
|
slideUpMenuOverlay.selectOption(videoButtons, variant)
|
|
if (audioButtons.isEmpty()){
|
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
|
}
|
|
},
|
|
invokeParent = false
|
|
))
|
|
}
|
|
} else if (playlist is HlsMultivariantPlaylist) {
|
|
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, resolvedPlaylistUrl)
|
|
|
|
masterPlaylist.getAudioSources().forEach { it ->
|
|
|
|
val estSize = VideoHelper.estimateSourceSize(it);
|
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
|
audioButtons.add(SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_music,
|
|
it.name,
|
|
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
|
(prefix + it.codec).trim(),
|
|
tag = it,
|
|
call = {
|
|
selectedAudioVariant = it
|
|
slideUpMenuOverlay.selectOption(audioButtons, it)
|
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
|
},
|
|
invokeParent = false
|
|
))
|
|
}
|
|
|
|
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
|
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
|
selectedSubtitleVariant = it
|
|
slideUpMenuOverlay.selectOption(subtitleButtons, it)
|
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
|
}, false))
|
|
}*/
|
|
|
|
masterPlaylist.getVideoSources().forEach {
|
|
val estSize = VideoHelper.estimateSourceSize(it);
|
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
|
videoButtons.add(SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_movie,
|
|
it.name,
|
|
"${it.width}x${it.height}",
|
|
(prefix + it.codec).trim(),
|
|
tag = it,
|
|
call = {
|
|
selectedVideoVariant = it
|
|
slideUpMenuOverlay.selectOption(videoButtons, it)
|
|
if (audioButtons.isEmpty()){
|
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
|
}
|
|
},
|
|
invokeParent = false
|
|
))
|
|
}
|
|
}
|
|
|
|
val newItems = arrayListOf<View>()
|
|
if (videoButtons.isNotEmpty()) {
|
|
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoButtons, videoButtons))
|
|
}
|
|
if (audioButtons.isNotEmpty()) {
|
|
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioButtons, audioButtons))
|
|
}
|
|
//TODO: Implement subtitles
|
|
/*if (subtitleButtons.isNotEmpty()) {
|
|
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleButtons, subtitleButtons))
|
|
}*/
|
|
|
|
slideUpMenuOverlay.onOK.subscribe {
|
|
//TODO: Fix SubtitleRawSource issue
|
|
StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null);
|
|
slideUpMenuOverlay.hide()
|
|
}
|
|
|
|
withContext(Dispatchers.Main) {
|
|
slideUpMenuOverlay.setItems(newItems)
|
|
}
|
|
} catch (e: Throwable) {
|
|
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
|
withContext(Dispatchers.Main) {
|
|
if (source is IHLSManifestSource) {
|
|
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null)
|
|
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
|
slideUpMenuOverlay.hide()
|
|
} else if (source is IHLSManifestAudioSource) {
|
|
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null)
|
|
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
|
slideUpMenuOverlay.hide()
|
|
} else {
|
|
throw NotImplementedError()
|
|
}
|
|
}
|
|
} else {
|
|
throw e
|
|
}
|
|
}
|
|
}
|
|
|
|
return slideUpMenuOverlay.apply { show() }
|
|
|
|
}
|
|
|
|
fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
|
|
val items = arrayListOf<View>();
|
|
var menu: SlideUpMenuOverlay? = null;
|
|
|
|
var descriptor = video.video;
|
|
if(video is VideoLocal)
|
|
descriptor = video.videoSerialized.video;
|
|
|
|
|
|
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
|
|
var selectedVideo: IVideoSource? = null;
|
|
var selectedAudio: IAudioSource? = null;
|
|
var selectedSubtitle: ISubtitleSource? = null;
|
|
|
|
val videoSources = descriptor.videoSources;
|
|
val audioSources = if(descriptor is VideoUnMuxedSourceDescriptor) descriptor.audioSources else null;
|
|
val subtitleSources = video.subtitles;
|
|
|
|
if(videoSources.isEmpty() && (audioSources?.size ?: 0) == 0) {
|
|
UIDialogs.toast(container.context.getString(R.string.no_downloads_available), false);
|
|
return null;
|
|
}
|
|
|
|
if(!VideoHelper.isDownloadable(video)) {
|
|
Logger.i(TAG, "Attempted to open downloads without valid sources for [${video.name}]: ${video.url}");
|
|
UIDialogs.toast( container.context.getString(R.string.no_downloadable_sources_yet));
|
|
return null;
|
|
}
|
|
|
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
|
listOf((if (audioSources != null) listOf(SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_movie,
|
|
container.context.getString(R.string.none),
|
|
container.context.getString(R.string.audio_only),
|
|
tag = "none",
|
|
call = {
|
|
selectedVideo = null;
|
|
menu?.selectOption(videoSources, "none");
|
|
if(selectedAudio != null || !requiresAudio)
|
|
menu?.setOk(container.context.getString(R.string.download));
|
|
},
|
|
invokeParent = false
|
|
)) else listOf()) +
|
|
videoSources
|
|
.filter { it.isDownloadable() }
|
|
.map {
|
|
when (it) {
|
|
is IVideoUrlSource -> {
|
|
val estSize = VideoHelper.estimateSourceSize(it);
|
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_movie,
|
|
it.name,
|
|
"${it.width}x${it.height}",
|
|
(prefix + it.codec).trim(),
|
|
tag = it,
|
|
call = {
|
|
selectedVideo = it
|
|
menu?.selectOption(videoSources, it);
|
|
if(selectedAudio != null || !requiresAudio)
|
|
menu?.setOk(container.context.getString(R.string.download));
|
|
},
|
|
invokeParent = false
|
|
)
|
|
}
|
|
|
|
is JSDashManifestRawSource -> {
|
|
val estSize = VideoHelper.estimateSourceSize(it);
|
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_movie,
|
|
it.name,
|
|
"${it.width}x${it.height}",
|
|
(prefix + it.codec).trim(),
|
|
tag = it,
|
|
call = {
|
|
selectedVideo = it
|
|
menu?.selectOption(videoSources, it);
|
|
if(selectedAudio != null || !requiresAudio)
|
|
menu?.setOk(container.context.getString(R.string.download));
|
|
},
|
|
invokeParent = false
|
|
)
|
|
}
|
|
|
|
is IHLSManifestSource -> {
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_movie,
|
|
it.name,
|
|
"HLS",
|
|
tag = it,
|
|
call = {
|
|
showHlsPicker(video, it, it.url, container)
|
|
},
|
|
invokeParent = false
|
|
)
|
|
}
|
|
|
|
else -> {
|
|
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
|
null;//throw Exception("Unhandled source type")
|
|
}
|
|
}
|
|
}.filterNotNull()).flatten().toList()
|
|
));
|
|
|
|
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
|
|
//TODO: Add HLS support here
|
|
selectedVideo = VideoHelper.selectBestVideoSource(
|
|
videoSources.filter { it is IVideoSource && it.isDownloadable() }.asIterable(),
|
|
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
|
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
|
) as IVideoSource?;
|
|
}
|
|
|
|
if (audioSources != null) {
|
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioSources, audioSources
|
|
.filter { VideoHelper.isDownloadable(it) }
|
|
.map {
|
|
when (it) {
|
|
is IAudioUrlSource -> {
|
|
val estSize = VideoHelper.estimateSourceSize(it);
|
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_music,
|
|
it.name,
|
|
"${it.bitrate}",
|
|
(prefix + it.codec).trim(),
|
|
tag = it,
|
|
call = {
|
|
selectedAudio = it
|
|
menu?.selectOption(audioSources, it);
|
|
menu?.setOk(container.context.getString(R.string.download));
|
|
},
|
|
invokeParent = false
|
|
);
|
|
}
|
|
|
|
is JSDashManifestRawAudioSource -> {
|
|
val estSize = VideoHelper.estimateSourceSize(it);
|
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_music,
|
|
it.name,
|
|
"${it.bitrate}",
|
|
(prefix + it.codec).trim(),
|
|
tag = it,
|
|
call = {
|
|
selectedAudio = it
|
|
menu?.selectOption(audioSources, it);
|
|
menu?.setOk(container.context.getString(R.string.download));
|
|
},
|
|
invokeParent = false
|
|
);
|
|
}
|
|
|
|
is IHLSManifestAudioSource -> {
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_movie,
|
|
it.name,
|
|
"HLS Audio",
|
|
tag = it,
|
|
call = {
|
|
showHlsPicker(video, it, it.url, container)
|
|
},
|
|
invokeParent = false
|
|
)
|
|
}
|
|
|
|
else -> {
|
|
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
|
null;//throw Exception("Unhandled source type")
|
|
}
|
|
}
|
|
}.filterNotNull()));
|
|
|
|
//TODO: Add HLS support here
|
|
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioSource && it.isDownloadable() }.asIterable(),
|
|
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
|
Settings.instance.playback.getPrimaryLanguage(container.context),
|
|
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioSource?;
|
|
}
|
|
|
|
if(contentResolver != null && subtitleSources.isNotEmpty()) {
|
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map {
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_edit,
|
|
it.name,
|
|
"",
|
|
tag = it,
|
|
call = {
|
|
if (selectedSubtitle == it) {
|
|
selectedSubtitle = null;
|
|
menu?.selectOption(subtitleSources, null);
|
|
} else {
|
|
selectedSubtitle = it;
|
|
menu?.selectOption(subtitleSources, it);
|
|
}
|
|
},
|
|
invokeParent = false
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
menu = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items);
|
|
|
|
if(selectedVideo != null) {
|
|
menu.selectOption(videoSources, selectedVideo);
|
|
}
|
|
if(selectedAudio != null) {
|
|
audioSources?.let { audioSources -> menu.selectOption(audioSources, selectedAudio); };
|
|
}
|
|
if(selectedAudio != null || (!requiresAudio && selectedVideo != null)) {
|
|
menu.setOk(container.context.getString(R.string.download));
|
|
}
|
|
|
|
menu.onOK.subscribe {
|
|
val sv = selectedVideo
|
|
if (sv is IHLSManifestSource) {
|
|
showHlsPicker(video, sv, sv.url, container)
|
|
return@subscribe
|
|
}
|
|
|
|
val sa = selectedAudio
|
|
if (sa is IHLSManifestAudioSource) {
|
|
showHlsPicker(video, sa, sa.url, container)
|
|
return@subscribe
|
|
}
|
|
|
|
menu.hide();
|
|
val subtitleToDownload = selectedSubtitle;
|
|
if(selectedAudio != null || !requiresAudio) {
|
|
if (subtitleToDownload == null) {
|
|
StateDownloads.instance.download(video, selectedVideo, selectedAudio, null);
|
|
} else {
|
|
//TODO: Clean this up somewhere else, maybe pre-fetch instead of dup calls
|
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
|
try {
|
|
val subtitleUri = subtitleToDownload.getSubtitlesURI();
|
|
//TODO: Remove uri dependency, should be able to work with raw aswell?
|
|
if (subtitleUri != null && contentResolver != null) {
|
|
val subtitlesRaw = StateDownloads.instance.downloadSubtitles(subtitleToDownload, contentResolver);
|
|
|
|
withContext(Dispatchers.Main) {
|
|
StateDownloads.instance.download(video, selectedVideo, selectedAudio, subtitlesRaw);
|
|
}
|
|
} else {
|
|
withContext(Dispatchers.Main) {
|
|
StateDownloads.instance.download(video, selectedVideo, selectedAudio, null);
|
|
}
|
|
}
|
|
} catch (e: Throwable) {
|
|
Logger.e(TAG, "Failed download subtitles.", e);
|
|
}
|
|
}
|
|
}
|
|
if(!Settings.instance.downloads.shouldDownload()) {
|
|
UIDialogs.appToast("Download will start when you're back on wifi.\n" +
|
|
"(You can change this in settings)", true);
|
|
}
|
|
}
|
|
};
|
|
return menu.apply { show() };
|
|
}
|
|
fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup, useDetails: Boolean = false) {
|
|
val handleUnknownDownload: ()->Unit = {
|
|
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate ->
|
|
StateDownloads.instance.download(video, px, bitrate)
|
|
};
|
|
};
|
|
if(!useDetails)
|
|
handleUnknownDownload();
|
|
else {
|
|
val scope = StateApp.instance.scopeOrNull;
|
|
|
|
if(scope != null) {
|
|
val loader = showLoaderOverlay(container.context.getString(R.string.fetching_video_details), container);
|
|
scope.launch(Dispatchers.IO) {
|
|
try {
|
|
val videoDetails = StatePlatform.instance.getContentDetails(video.url, false).await();
|
|
if(videoDetails !is IPlatformVideoDetails)
|
|
throw IllegalStateException("Not a video details");
|
|
|
|
withContext(Dispatchers.Main) {
|
|
if(showDownloadVideoOverlay(videoDetails, container, StateApp.instance.contextOrNull?.contentResolver) == null)
|
|
loader.hide(true);
|
|
}
|
|
}
|
|
catch(ex: Throwable) {
|
|
Logger.e(TAG, "Fetching details for download failed due to: " + ex.message, ex);
|
|
withContext(Dispatchers.Main) {
|
|
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download) + "\n" + ex.message);
|
|
handleUnknownDownload();
|
|
loader.hide(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else handleUnknownDownload();
|
|
}
|
|
}
|
|
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
|
showUnknownVideoDownload(container.context.getString(R.string.playlist), container) { px, bitrate ->
|
|
StateDownloads.instance.download(playlist, px, bitrate);
|
|
};
|
|
}
|
|
fun showDownloadWatchlaterOverlay(container: ViewGroup) {
|
|
showUnknownVideoDownload(container.context.getString(R.string.watch_later), container, { px, bitrate ->
|
|
StateDownloads.instance.downloadWatchLater(px, bitrate);
|
|
})
|
|
}
|
|
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
|
|
val items = arrayListOf<View>();
|
|
var menu: SlideUpMenuOverlay? = null;
|
|
|
|
var targetPxSize: Long = 0;
|
|
var targetBitrate: Long = 0;
|
|
|
|
val resolutions = listOf(
|
|
Triple<String, String, Long>(container.context.getString(R.string.none), container.context.getString(R.string.none), -1),
|
|
Triple<String, String, Long>("480P", "720x480", 720*480),
|
|
Triple<String, String, Long>("720P", "1280x720", 1280*720),
|
|
Triple<String, String, Long>("1080P", "1920x1080", 1920*1080),
|
|
Triple<String, String, Long>("1440P", "2560x1440", 2560*1440),
|
|
Triple<String, String, Long>("2160P", "3840x2160", 3840*2160)
|
|
);
|
|
|
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map {
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_movie,
|
|
it.first,
|
|
it.second,
|
|
tag = it.third,
|
|
call = {
|
|
targetPxSize = it.third;
|
|
menu?.selectOption("Video", it.third);
|
|
},
|
|
invokeParent = false
|
|
)
|
|
}));
|
|
|
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf(
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_movie,
|
|
container.context.getString(R.string.low_bitrate),
|
|
"",
|
|
tag = 1,
|
|
call = {
|
|
targetBitrate = 1;
|
|
menu?.selectOption("Bitrate", 1);
|
|
menu?.setOk(container.context.getString(R.string.download));
|
|
},
|
|
invokeParent = false
|
|
),
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_movie,
|
|
container.context.getString(R.string.high_bitrate),
|
|
"",
|
|
tag = 9999999,
|
|
call = {
|
|
targetBitrate = 9999999;
|
|
menu?.selectOption("Bitrate", 9999999);
|
|
menu?.setOk(container.context.getString(R.string.download));
|
|
},
|
|
invokeParent = false
|
|
)
|
|
)));
|
|
|
|
|
|
menu = SlideUpMenuOverlay(container.context, container, "Download " + toDownload, null, true, items);
|
|
|
|
if(Settings.instance.downloads.getDefaultVideoQualityPixels() != 0) {
|
|
val defTarget = Settings.instance.downloads.getDefaultVideoQualityPixels();
|
|
if(defTarget == -1) {
|
|
targetPxSize = -1;
|
|
menu.selectOption("Video", (-1).toLong());
|
|
}
|
|
else {
|
|
targetPxSize = resolutions.drop(1).minBy { Math.abs(defTarget - it.third) }.third;
|
|
menu.selectOption("Video", targetPxSize);
|
|
}
|
|
}
|
|
if(Settings.instance.downloads.isHighBitrateDefault()) {
|
|
targetBitrate = 9999999;
|
|
menu.selectOption("Bitrate", 9999999);
|
|
menu.setOk(container.context.getString(R.string.download));
|
|
}
|
|
else {
|
|
targetBitrate = 1;
|
|
menu.selectOption("Bitrate", 1);
|
|
menu.setOk(container.context.getString(R.string.download));
|
|
}
|
|
|
|
menu.onOK.subscribe {
|
|
menu.hide();
|
|
cb(if(targetPxSize > 0) targetPxSize else null, if(targetBitrate > 0) targetBitrate else null);
|
|
};
|
|
menu.show();
|
|
}
|
|
|
|
fun showLoaderOverlay(text: String, container: ViewGroup): SlideUpMenuOverlay {
|
|
val dp70 = 70.dp(container.context.resources);
|
|
val dp15 = 15.dp(container.context.resources);
|
|
val overlay = SlideUpMenuOverlay(container.context, container, text, null, true, listOf(
|
|
LoaderView(container.context, true, dp70).apply {
|
|
this.setPadding(0, dp15, 0, dp15);
|
|
}
|
|
), true);
|
|
overlay.show();
|
|
return overlay;
|
|
}
|
|
|
|
fun showCreateSubscriptionGroup(container: ViewGroup, initialChannel: IPlatformChannel? = null, onCreate: ((String) -> Unit)? = null): SlideUpMenuOverlay {
|
|
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
|
val addSubGroupOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_subgroup), container.context.getString(R.string.ok), false, nameInput);
|
|
|
|
addSubGroupOverlay.onOK.subscribe {
|
|
val text = nameInput.text;
|
|
if (text.isBlank()) {
|
|
return@subscribe;
|
|
}
|
|
|
|
addSubGroupOverlay.hide();
|
|
nameInput.deactivate();
|
|
nameInput.clear();
|
|
if(onCreate == null)
|
|
{
|
|
//TODO: Do this better, temp
|
|
StateApp.instance.contextOrNull?.let {
|
|
if(it is MainActivity) {
|
|
val subGroup = SubscriptionGroup(text);
|
|
if(initialChannel != null) {
|
|
subGroup.urls.add(initialChannel.url);
|
|
if(initialChannel.thumbnail != null)
|
|
subGroup.image = ImageVariable(initialChannel.thumbnail);
|
|
}
|
|
it.navigate(it.getFragment<SubscriptionGroupFragment>(), subGroup);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
onCreate(text)
|
|
};
|
|
|
|
addSubGroupOverlay.onCancel.subscribe {
|
|
nameInput.deactivate();
|
|
nameInput.clear();
|
|
};
|
|
|
|
addSubGroupOverlay.show();
|
|
nameInput.activate();
|
|
|
|
return addSubGroupOverlay
|
|
}
|
|
fun showCreatePlaylistOverlay(container: ViewGroup, onCreate: (String) -> Unit): SlideUpMenuOverlay {
|
|
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
|
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
|
|
|
|
addPlaylistOverlay.onOK.subscribe {
|
|
val text = nameInput.text;
|
|
if (text.isBlank()) {
|
|
return@subscribe;
|
|
}
|
|
|
|
addPlaylistOverlay.hide();
|
|
nameInput.deactivate();
|
|
nameInput.clear();
|
|
onCreate(text)
|
|
};
|
|
|
|
addPlaylistOverlay.onCancel.subscribe {
|
|
nameInput.deactivate();
|
|
nameInput.clear();
|
|
};
|
|
|
|
addPlaylistOverlay.show();
|
|
nameInput.activate();
|
|
|
|
return addPlaylistOverlay
|
|
}
|
|
|
|
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, vararg actions: SlideUpMenuItem): SlideUpMenuOverlay {
|
|
val items = arrayListOf<View>();
|
|
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
|
|
|
val isLimited = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
|
if (it is JSClient)
|
|
return@let it.config.reduceFunctionsInLimitedVersion && BuildConfig.IS_PLAYSTORE_BUILD
|
|
else false;
|
|
} ?: false;
|
|
|
|
if (lastUpdated != null) {
|
|
items.add(
|
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
|
SlideUpMenuItem(container.context,
|
|
R.drawable.ic_playlist_add,
|
|
lastUpdated.name,
|
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
|
tag = "",
|
|
call = {
|
|
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
|
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
|
}))
|
|
);
|
|
}
|
|
|
|
val allPlaylists = StatePlaylists.instance.getPlaylists();
|
|
val queue = StatePlayer.instance.getQueue();
|
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
|
(listOf(
|
|
if(!isLimited && !video.isLive)
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_download,
|
|
container.context.getString(R.string.download),
|
|
container.context.getString(R.string.download_the_video),
|
|
tag = "download",
|
|
call = {
|
|
showDownloadVideoOverlay(video, container, true);
|
|
},
|
|
invokeParent = false
|
|
) else null,
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_share,
|
|
container.context.getString(R.string.share),
|
|
"Share the video",
|
|
tag = "share",
|
|
call = {
|
|
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
|
container.context.startActivity(Intent.createChooser(Intent().apply {
|
|
action = Intent.ACTION_SEND;
|
|
putExtra(Intent.EXTRA_TEXT, url);
|
|
type = "text/plain";
|
|
}, null));
|
|
},
|
|
invokeParent = false
|
|
),
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_visibility_off,
|
|
container.context.getString(R.string.hide_creator_from_home),
|
|
"",
|
|
tag = "hide_creator",
|
|
call = {
|
|
StateMeta.instance.addHiddenCreator(video.author.url);
|
|
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
|
}))
|
|
+ actions).filterNotNull()
|
|
));
|
|
items.add(
|
|
SlideUpMenuGroup(
|
|
container.context, container.context.getString(R.string.add_to), "addto",
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_queue_add,
|
|
container.context.getString(R.string.add_to_queue),
|
|
"${queue.size} " + container.context.getString(R.string.videos),
|
|
tag = "queue",
|
|
call = { StatePlayer.instance.addToQueue(video); }),
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_watchlist_add,
|
|
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
|
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
|
tag = "watch later",
|
|
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_history,
|
|
container.context.getString(R.string.add_to_history),
|
|
"Mark as watched",
|
|
tag = "history",
|
|
call = { StateHistory.instance.markAsWatched(video); }),
|
|
));
|
|
|
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
|
playlistItems.add(SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_playlist_add,
|
|
container.context.getString(R.string.new_playlist),
|
|
container.context.getString(R.string.add_to_new_playlist),
|
|
tag = "add_to_new_playlist",
|
|
call = {
|
|
showCreatePlaylistOverlay(container) {
|
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
|
};
|
|
},
|
|
invokeParent = false
|
|
))
|
|
|
|
for (playlist in allPlaylists) {
|
|
playlistItems.add(SlideUpMenuItem(container.context,
|
|
R.drawable.ic_playlist_add,
|
|
"${container.context.getString(R.string.add_to)} " + playlist.name + "",
|
|
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
|
tag = "",
|
|
call = {
|
|
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
|
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
|
}));
|
|
}
|
|
|
|
if(playlistItems.size > 0)
|
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.playlists), "", playlistItems));
|
|
|
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.video_options), null, true, items).apply { show() };
|
|
}
|
|
|
|
|
|
fun showAddToOverlay(video: IPlatformVideo, container: ViewGroup, slideUpMenuOverlayUpdated: (SlideUpMenuOverlay) -> Unit): SlideUpMenuOverlay {
|
|
|
|
val items = arrayListOf<View>();
|
|
|
|
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
|
|
|
if (lastUpdated != null) {
|
|
items.add(
|
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
|
SlideUpMenuItem(container.context,
|
|
R.drawable.ic_playlist_add,
|
|
lastUpdated.name,
|
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
|
tag = "",
|
|
call = {
|
|
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
|
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
|
}))
|
|
);
|
|
}
|
|
|
|
val allPlaylists = StatePlaylists.instance.getPlaylists().sortedByDescending { maxOf(it.datePlayed, it.dateUpdate, it.dateCreation) };
|
|
val queue = StatePlayer.instance.getQueue();
|
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
|
items.add(
|
|
SlideUpMenuGroup(
|
|
container.context, container.context.getString(R.string.other), "other",
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_queue_add,
|
|
container.context.getString(R.string.queue),
|
|
"${queue.size} " + container.context.getString(R.string.videos),
|
|
tag = "queue",
|
|
call = { StatePlayer.instance.addToQueue(video); }),
|
|
SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_watchlist_add,
|
|
StatePlayer.TYPE_WATCHLATER,
|
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
|
tag = "watch later",
|
|
call = {
|
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
|
UIDialogs.appToast("Added to watch later", false);
|
|
else
|
|
UIDialogs.toast(container.context.getString(R.string.already_in_watch_later))
|
|
}),
|
|
)
|
|
);
|
|
|
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
|
playlistItems.add(SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_playlist_add,
|
|
container.context.getString(R.string.new_playlist),
|
|
container.context.getString(R.string.add_to_new_playlist),
|
|
tag = "add_to_new_playlist",
|
|
call = {
|
|
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
|
});
|
|
},
|
|
invokeParent = false
|
|
))
|
|
|
|
for (playlist in allPlaylists) {
|
|
playlistItems.add(SlideUpMenuItem(container.context,
|
|
R.drawable.ic_playlist_add,
|
|
playlist.name,
|
|
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
|
tag = "",
|
|
call = {
|
|
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
|
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
|
}));
|
|
}
|
|
|
|
if(playlistItems.size > 0)
|
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.playlists), "", playlistItems));
|
|
|
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
|
}
|
|
|
|
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
|
|
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
|
|
overlay.show();
|
|
return overlay;
|
|
}
|
|
|
|
|
|
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), invokeParents: Boolean = true, onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
|
|
val visible = buttonGroup.getVisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
|
val hidden = buttonGroup.getInvisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
|
|
|
val views = arrayOf(
|
|
hidden
|
|
.map { btn -> SlideUpMenuItem(
|
|
container.context,
|
|
btn.iconResource,
|
|
btn.text.text.toString(),
|
|
"",
|
|
tag = "",
|
|
call = {
|
|
btn.handler?.invoke(btn);
|
|
},
|
|
invokeParent = invokeParents
|
|
) as View }.toTypedArray(),
|
|
arrayOf(SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_pin,
|
|
container.context.getString(R.string.change_pins),
|
|
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
|
|
tag = "",
|
|
call = {
|
|
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }, {
|
|
val selected = it
|
|
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
|
.filter { it != null }
|
|
.map { it!! }
|
|
.toList();
|
|
|
|
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
|
});
|
|
},
|
|
invokeParent = false
|
|
))
|
|
).flatten().toTypedArray();
|
|
|
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
|
}
|
|
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit, description: String? = null) {
|
|
val selection: MutableList<Any> = mutableListOf();
|
|
|
|
var overlay: SlideUpMenuOverlay? = null;
|
|
|
|
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
|
listOf(
|
|
if(!description.isNullOrEmpty()) SlideUpMenuGroup(container.context, "", description, "", listOf()) else null,
|
|
).filterNotNull() +
|
|
(options.map { SlideUpMenuItem(
|
|
container.context,
|
|
R.drawable.ic_move_up,
|
|
it.first,
|
|
"",
|
|
tag = it.second,
|
|
call = {
|
|
val overlayItem = overlay?.getSlideUpItemByTag(it.second);
|
|
if(overlay!!.selectOption(null, it.second, true, true)) {
|
|
if(!selection.contains(it.second)) {
|
|
selection.add(it.second);
|
|
if(overlayItem != null) {
|
|
overlayItem.setSubText(selection.indexOf(it.second).toString());
|
|
}
|
|
}
|
|
} else {
|
|
selection.remove(it.second);
|
|
if(overlayItem != null) {
|
|
overlayItem.setSubText("");
|
|
}
|
|
}
|
|
},
|
|
invokeParent = false
|
|
)
|
|
}));
|
|
overlay.onOK.subscribe {
|
|
onOrdered.invoke(selection);
|
|
overlay.hide();
|
|
};
|
|
|
|
overlay.show();
|
|
}
|
|
}
|
|
} |