Skip to content
Snippets Groups Projects
UISlideOverlays.kt 61.5 KiB
Newer Older
Koen's avatar
Koen committed
package com.futo.platformplayer

import android.app.NotificationManager
Koen's avatar
Koen committed
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
Koen's avatar
Koen committed
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
Kelvin's avatar
Kelvin committed
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
Koen's avatar
Koen committed
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.ResultCapabilities
Kelvin's avatar
Kelvin committed
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
Koen's avatar
Koen committed
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
Koen's avatar
Koen committed
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
Koen's avatar
Koen committed
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
Koen's avatar
Koen committed
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
Koen's avatar
Koen committed
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
Kelvin's avatar
Kelvin committed
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
Koen's avatar
Koen committed
import com.futo.platformplayer.downloads.VideoLocal
Kelvin's avatar
Kelvin committed
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
Koen's avatar
Koen committed
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger
Kelvin's avatar
Kelvin committed
import com.futo.platformplayer.models.ImageVariable
Koen's avatar
Koen committed
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.Subscription
Kelvin's avatar
Kelvin committed
import com.futo.platformplayer.models.SubscriptionGroup
Koen's avatar
Koen committed
import com.futo.platformplayer.parsers.HLS
Koen's avatar
Koen committed
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
Kelvin's avatar
Kelvin committed
import com.futo.platformplayer.states.StateHistory
Koen's avatar
Koen committed
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
Koen's avatar
Koen committed
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
Koen's avatar
Koen committed
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
Koen's avatar
Koen committed
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
Koen's avatar
Koen committed
import com.futo.platformplayer.views.pills.RoundButton
import com.futo.platformplayer.views.pills.RoundButtonGroup
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
Kelvin's avatar
Kelvin committed
import isDownloadable
Koen's avatar
Koen committed
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class UISlideOverlays {
    companion object {
        private const val TAG = "UISlideOverlays";

Kelvin's avatar
Kelvin committed
        fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit,  vararg views: View): SlideUpMenuOverlay {
Koen's avatar
Koen committed
            var menu = SlideUpMenuOverlay(container.context, container, title, okButton, true, *views);

            menu.onOK.subscribe {
                menu.hide();
                onOk.invoke();
            };
            menu.show();
Kelvin's avatar
Kelvin committed
            return menu;
Koen's avatar
Koen committed
        }

        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){
                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);
Kelvin's avatar
Kelvin committed

                                            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/*,,
Kelvin's avatar
Kelvin committed

                        SlideUpMenuGroup(container.context, "Actions",
                            "Various things you can do with this subscription",
Kelvin's avatar
Kelvin committed
                        SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
                            showCreateSubscriptionGroup(container, subscription.channel);
Kelvin's avatar
Kelvin committed
                        ).filterNotNull());

                    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", {
                                            val intent = Intent(mainContext, SettingsActivity::class.java);
                                            intent.putExtra("query", mainContext.getString(R.string.background_update));
                                            mainContext.startActivity(intent);
                                        }, 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();
                }
            }
Kelvin's avatar
Kelvin committed
        fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {

        }

Koen's avatar
Koen committed
        fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
            val items = arrayListOf<View>(LoaderView(container.context))
Koen's avatar
Koen committed
            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 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 {
                    masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)

                    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
                        ))
Koen's avatar
Koen committed
                    }

                    /*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))
                                }
Koen's avatar
Koen committed
                    }

                    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, sourceUrl), 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, sourceUrl), 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? {
Koen's avatar
Koen committed
            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;
Koen's avatar
Koen committed
            var selectedSubtitle: ISubtitleSource? = null;

            val videoSources = descriptor.videoSources;
            val audioSources = if(descriptor is VideoUnMuxedSourceDescriptor) descriptor.audioSources else null;
            val subtitleSources = video.subtitles;

Koen's avatar
Koen committed
            if(videoSources.isEmpty() && (audioSources?.size ?: 0) == 0) {
                UIDialogs.toast(container.context.getString(R.string.no_downloads_available), false);
Koen's avatar
Koen committed
                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));
            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
Koen's avatar
Koen committed
                videoSources
Koen's avatar
Koen committed
                .map {
Koen's avatar
Koen committed
                    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
                            )
                        }

Koen's avatar
Koen committed
                        is IHLSManifestSource -> {
                            SlideUpMenuItem(
                                container.context,
                                R.drawable.ic_movie,
                                it.name,
                                "HLS",
                                tag = it,
                                call = {
                                    showHlsPicker(video, it, it.url, container)
                                },
                                invokeParent = false
                            )
Koen's avatar
Koen committed
                        }

                        else -> {
                            Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
                            null;//throw Exception("Unhandled source type")
Koen's avatar
Koen committed
                        }
Koen's avatar
Koen committed
                    }
                }.filterNotNull()).flatten().toList()
Koen's avatar
Koen committed
            ));

Koen's avatar
Koen committed
            if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
Koen's avatar
Koen committed
                //TODO: Add HLS support here
                selectedVideo = VideoHelper.selectBestVideoSource(
                    videoSources.filter { it is IVideoSource && it.isDownloadable() }.asIterable(),
Koen's avatar
Koen committed
                    Settings.instance.downloads.getDefaultVideoQualityPixels(),
Koen's avatar
Koen committed
                    FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
Koen's avatar
Koen committed
            }
Koen's avatar
Koen committed

Koen's avatar
Koen committed
            if (audioSources != null) {
                items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioSources, audioSources
Koen's avatar
Koen committed
                    .map {
Koen's avatar
Koen committed
                        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
                                );
                            }

Koen's avatar
Koen committed
                            is IHLSManifestAudioSource -> {
                                SlideUpMenuItem(
                                    container.context,
                                    R.drawable.ic_movie,
                                    it.name,
                                    "HLS Audio",
                                    tag = it,
                                    call = {
                                        showHlsPicker(video, it, it.url, container)
                                    },
                                    invokeParent = false
                                )
Koen's avatar
Koen committed
                            }

                            else -> {
                                Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
                                null;//throw Exception("Unhandled source type")
Koen's avatar
Koen committed
                            }
Koen's avatar
Koen committed
                        }
Koen's avatar
Koen committed

Koen's avatar
Koen committed
                //TODO: Add HLS support here
                selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioSource && it.isDownloadable() }.asIterable(),
Koen's avatar
Koen committed
                    FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
                    Settings.instance.playback.getPrimaryLanguage(container.context),
                    if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioSource?;
Koen's avatar
Koen committed
            }

Koen's avatar
Koen committed
            if(contentResolver != null && subtitleSources.isNotEmpty()) {
Koen's avatar
Koen committed
                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
                        );
Koen's avatar
Koen committed
                    })
                );
Koen's avatar
Koen committed

            menu = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items);
Koen's avatar
Koen committed

            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));
Koen's avatar
Koen committed
            }

            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
                }

Koen's avatar
Koen committed
                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);
Koen's avatar
Koen committed

                                    withContext(Dispatchers.Main) {
                                        StateDownloads.instance.download(video, selectedVideo, selectedAudio, subtitlesRaw);
Koen's avatar
Koen committed
                                    }
                                } else {
                                    withContext(Dispatchers.Main) {
                                        StateDownloads.instance.download(video, selectedVideo, selectedAudio, null);
                                    }
                                }
                            } catch (e: Throwable) {
                                Logger.e(TAG, "Failed download subtitles.", e);
                            }
                        }
                    }
                }
            };
            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)
                };
Koen's avatar
Koen committed
            };
            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);
                                UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download) + "\n" + ex.message);
                                handleUnknownDownload();
                                loader.hide(true);
                            }
                        }
                    }
                }
                else handleUnknownDownload();
            }
Koen's avatar
Koen committed
        }
        fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
            showUnknownVideoDownload(container.context.getString(R.string.playlist), container) { px, bitrate ->
Koen's avatar
Koen committed
                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);
            })
        }
Koen's avatar
Koen committed
        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),
Koen's avatar
Koen committed
                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
                )
Koen's avatar
Koen committed
            }));

            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
                )
Koen's avatar
Koen committed
            )));


            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));
Koen's avatar
Koen committed
            }
            else {
                targetBitrate = 1;
                menu.selectOption("Bitrate", 1);
                menu.setOk(container.context.getString(R.string.download));
Koen's avatar
Koen committed
            }

            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 {
Kelvin's avatar
Kelvin committed
            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 {
Koen's avatar
Koen committed
            val items = arrayListOf<View>();
            val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();

Kelvin's avatar
Kelvin committed
            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;

Koen's avatar
Koen committed
            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);
Koen's avatar
Koen committed
                                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",
Kelvin's avatar
Kelvin committed
                    if(!isLimited && !video.isLive)
Kelvin's avatar
Kelvin committed
                        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");
                        }))
Kelvin's avatar
Kelvin committed
                        + actions).filterNotNull()
Koen's avatar
Koen committed
            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",
Kelvin's avatar
Kelvin committed
                        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",