diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 102e7d09..0a62e28f 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -864,11 +864,13 @@ class Settings : FragmentedStorageFileJson() { class Other { @FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2) var playlistDeleteConfirmation: Boolean = true; + @FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3) + var playlistAllowDups: Boolean = true; - @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3) + @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4) var polycentricEnabled: Boolean = true; - @FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4) + @FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5) var polycentricLocalCache: Boolean = true; } diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index daf50455..582a74f7 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -895,9 +895,9 @@ class UISlideOverlays { "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), tag = "", call = { - StatePlaylists.instance.addToPlaylist(lastUpdated.id, video); + if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video)) + UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false); StateDownloads.instance.checkForOutdatedPlaylists(); - UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false); })) ); } @@ -992,9 +992,9 @@ class UISlideOverlays { "${playlist.videos.size} " + container.context.getString(R.string.videos), tag = "", call = { - StatePlaylists.instance.addToPlaylist(playlist.id, video); + if(StatePlaylists.instance.addToPlaylist(playlist.id, video)) + UIDialogs.appToast("Added to playlist [${playlist.name}]", false); StateDownloads.instance.checkForOutdatedPlaylists(); - UIDialogs.appToast("Added to playlist [${playlist.name}]", false); })); } @@ -1020,9 +1020,9 @@ class UISlideOverlays { "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), tag = "", call = { - StatePlaylists.instance.addToPlaylist(lastUpdated.id, video); + if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video)) + UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false); StateDownloads.instance.checkForOutdatedPlaylists(); - UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false); })) ); } @@ -1072,9 +1072,9 @@ class UISlideOverlays { "${playlist.videos.size} " + container.context.getString(R.string.videos), tag = "", call = { - StatePlaylists.instance.addToPlaylist(playlist.id, video); + if(StatePlaylists.instance.addToPlaylist(playlist.id, video)) + UIDialogs.appToast("Added to playlist [${playlist.name}]", false); StateDownloads.instance.checkForOutdatedPlaylists(); - UIDialogs.appToast("Added to playlist [${playlist.name}]", false); })); } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt index e4290830..a637e89d 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt @@ -33,6 +33,7 @@ class SourcePluginConfig( override val allowEval: Boolean = false, override val allowUrls: List = listOf(), override val packages: List = listOf(), + override val packagesOptional: List = listOf(), val settings: List = listOf(), @@ -102,6 +103,10 @@ class SourcePluginConfig( if(!packages.contains(pack)) return false; } + for(pack in newConfig.packagesOptional) { + if(!packagesOptional.contains(pack)) + return false; + } //Developer Submit Url should be same or empty if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl) return false; diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index 05adb55b..8ba2814f 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -1092,7 +1092,7 @@ class VideoDownload { StateDownloads.instance.updateCachedVideo(existing); } else { - val newVideo = VideoLocal(videoDetails!!); + val newVideo = VideoLocal(videoDetails!!, OffsetDateTime.now()); if(localVideoSource != null) newVideo.videoSource.add(localVideoSource); if(localAudioSource != null) diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoLocal.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoLocal.kt index c58491e6..578a5812 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoLocal.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoLocal.kt @@ -23,6 +23,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails import com.futo.platformplayer.api.media.structures.IPager +import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer import com.futo.platformplayer.stores.v2.IStoreItem import java.io.File import java.time.OffsetDateTime @@ -75,11 +76,16 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem { //TODO: Offline subtitles override val subtitles: List = listOf(); - constructor(video: SerializedPlatformVideoDetails) { + @kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class) + var downloadDate: OffsetDateTime? = null; + + constructor(video: SerializedPlatformVideoDetails, downloadDate: OffsetDateTime? = null) { this.videoSerialized = video; + this.downloadDate = downloadDate; } constructor(video: IPlatformVideoDetails, subtitleSources: List) { this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources); + downloadDate = OffsetDateTime.now(); } override fun getComments(client: IPlatformClient): IPager? = null; diff --git a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt index 9c12eb93..2a491a28 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -94,7 +94,11 @@ class V8Plugin { withDependency(PackageBridge(this, config)); for(pack in config.packages) - withDependency(getPackage(pack)); + withDependency(getPackage(pack)!!); + for(pack in config.packagesOptional) + getPackage(pack, true)?.let { + withDependency(it); + } } fun changeAllowDevSubmit(isAllowed: Boolean) { @@ -254,13 +258,13 @@ class V8Plugin { } } - private fun getPackage(packageName: String): V8Package { + private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? { //TODO: Auto get all package types? return when(packageName) { "DOMParser" -> PackageDOMParser(this) "Http" -> PackageHttp(this, config) "Utilities" -> PackageUtilities(this, config) - else -> throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}"); + else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}"); }; } diff --git a/app/src/main/java/com/futo/platformplayer/engine/V8PluginConfig.kt b/app/src/main/java/com/futo/platformplayer/engine/V8PluginConfig.kt index df6ad4bc..fbb1c113 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8PluginConfig.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8PluginConfig.kt @@ -5,6 +5,7 @@ interface IV8PluginConfig { val allowEval: Boolean; val allowUrls: List; val packages: List; + val packagesOptional: List; } @kotlinx.serialization.Serializable @@ -13,17 +14,20 @@ class V8PluginConfig : IV8PluginConfig { override val allowEval: Boolean; override val allowUrls: List; override val packages: List; + override val packagesOptional: List; constructor() { name = "Unknown"; allowEval = false; allowUrls = listOf(); packages = listOf(); + packagesOptional = listOf(); } - constructor(name: String, allowEval: Boolean, allowUrls: List, packages: List = listOf()) { + constructor(name: String, allowEval: Boolean, allowUrls: List, packages: List = listOf(), packagesOptional: List = listOf()) { this.name = name; this.allowEval = allowEval; this.allowUrls = allowUrls; this.packages = packages; + this.packagesOptional = packagesOptional; } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt index 93b5e7e9..b2bfc10f 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt @@ -4,8 +4,13 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.EditText import android.widget.LinearLayout +import android.widget.Spinner import android.widget.TextView +import androidx.core.widget.addTextChangedListener import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.futo.platformplayer.R @@ -25,6 +30,7 @@ import com.futo.platformplayer.views.items.PlaylistDownloadItem import com.futo.platformplayer.views.others.ProgressBar import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.time.OffsetDateTime class DownloadsFragment : MainFragment() { private val TAG = "DownloadsFragment"; @@ -92,8 +98,12 @@ class DownloadsFragment : MainFragment() { private val _listDownloadedHeader: LinearLayout; private val _listDownloadedMeta: TextView; + private val _listDownloadSearch: EditText; private val _listDownloaded: AnyInsertedAdapterView; + private var lastDownloads: List? = null; + private var ordering: String? = "nameAsc"; + constructor(frag: DownloadsFragment, inflater: LayoutInflater): super(frag.requireContext()) { inflater.inflate(R.layout.fragment_downloads, this); _frag = frag; @@ -104,6 +114,7 @@ class DownloadsFragment : MainFragment() { _listActiveDownloadsContainer = findViewById(R.id.downloads_active_downloads_container); _listActiveDownloadsMeta = findViewById(R.id.downloads_active_downloads_meta); + _listDownloadSearch = findViewById(R.id.downloads_search); _listActiveDownloads = findViewById(R.id.downloads_active_downloads_list); _listPlaylistsContainer = findViewById(R.id.downloads_playlist_container); @@ -113,6 +124,30 @@ class DownloadsFragment : MainFragment() { _listDownloadedHeader = findViewById(R.id.downloads_videos_header); _listDownloadedMeta = findViewById(R.id.downloads_videos_meta); + _listDownloadSearch.addTextChangedListener { + updateContentFilters(); + } + val spinnerSortBy: Spinner = findViewById(R.id.spinner_sortby); + spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also { + it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple); + }; + spinnerSortBy.setSelection(0); + spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { + when(pos) { + 0 -> ordering = "nameAsc" + 1 -> ordering = "nameDesc" + 2 -> ordering = "downloadDateAsc" + 3 -> ordering = "downloadDateDesc" + 4 -> ordering = "releasedAsc" + 5 -> ordering = "releasedDesc" + else -> ordering = null + } + updateContentFilters() + } + override fun onNothingSelected(parent: AdapterView<*>?) = Unit + }; + _listDownloaded = findViewById(R.id.list_downloaded) .asAnyWithTop(findViewById(R.id.downloads_top)) { it.onClick.subscribe { @@ -125,7 +160,6 @@ class DownloadsFragment : MainFragment() { reloadUI(); } - fun reloadUI() { val usage = StateDownloads.instance.getTotalUsage(true); _usageUsed.text = "${usage.usage.toHumanBytesSize()} " + context.getString(R.string.used); @@ -184,7 +218,29 @@ class DownloadsFragment : MainFragment() { _listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})"; } - _listDownloaded.setData(downloaded); + lastDownloads = downloaded; + _listDownloaded.setData(filterDownloads(downloaded)); + } + fun updateContentFilters(){ + val toFilter = lastDownloads ?: return; + _listDownloaded.setData(filterDownloads(toFilter)); + } + fun filterDownloads(vids: List): List{ + var vidsToReturn = vids; + if(!_listDownloadSearch.text.isNullOrEmpty()) + vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) }; + if(!ordering.isNullOrEmpty()) { + vidsToReturn = when(ordering){ + "downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX }; + "downloadDateDesc" -> vidsToReturn.sortedByDescending { it.downloadDate ?: OffsetDateTime.MIN }; + "nameAsc" -> vidsToReturn.sortedBy { it.name } + "nameDesc" -> vidsToReturn.sortedByDescending { it.name } + "releasedAsc" -> vidsToReturn.sortedBy { it.datetime ?: OffsetDateTime.MAX } + "releasedDesc" -> vidsToReturn.sortedByDescending { it.datetime ?: OffsetDateTime.MIN } + else -> vidsToReturn + } + } + return vidsToReturn; } } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt index 29eba44c..9b2e5904 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt @@ -5,6 +5,7 @@ import android.net.Uri import androidx.core.content.FileProvider import androidx.fragment.app.Fragment import com.futo.platformplayer.R +import com.futo.platformplayer.Settings import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.contents.IPlatformContent @@ -306,14 +307,19 @@ class StatePlaylists { broadcastSyncPlaylist(playlist); } } - fun addToPlaylist(id: String, video: IPlatformVideo) { + fun addToPlaylist(id: String, video: IPlatformVideo): Boolean { synchronized(playlistStore) { - val playlist = getPlaylist(id) ?: return; + val playlist = getPlaylist(id) ?: return false; + if(!Settings.instance.other.playlistAllowDups && playlist.videos.any { it.url == video.url }) + return false; + + playlist.videos.add(SerializedPlatformVideo.fromVideo(video)); playlist.dateUpdate = OffsetDateTime.now(); playlistStore.saveAsync(playlist, true); broadcastSyncPlaylist(playlist); + return true; } } diff --git a/app/src/main/res/layout/fragment_downloads.xml b/app/src/main/res/layout/fragment_downloads.xml index 397c0076..2b971a0b 100644 --- a/app/src/main/res/layout/fragment_downloads.xml +++ b/app/src/main/res/layout/fragment_downloads.xml @@ -99,7 +99,6 @@ - + + + + + + + + Bypass Rotation Prevention Playlist Delete Confirmation Show confirmation dialog when deleting media from a playlist + Allow duplicate playlist videos + Allow adding duplicate videos to playlists Enable Polycentric Enable Polycentric Local Caching Caches polycentric results on-device to reduce load times, changing requires app reboot @@ -956,6 +958,14 @@ Watchtime Ascending Watchtime Descending + + Name Ascending + Name Descending + Downloaded Ascending + Downloaded Descending + Released Ascending + Released Descending + Preview List