Playlist dup prevention, download search and ordering, optional package support

This commit is contained in:
Kelvin 2025-02-06 21:36:33 +01:00
parent 0b529ae94d
commit 330aa495c8
11 changed files with 144 additions and 21 deletions

View File

@ -864,11 +864,13 @@ class Settings : FragmentedStorageFileJson() {
class Other { class Other {
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2) @FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
var playlistDeleteConfirmation: Boolean = true; 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; 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; var polycentricLocalCache: Boolean = true;
} }

View File

@ -895,9 +895,9 @@ class UISlideOverlays {
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
tag = "", tag = "",
call = { 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(); 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), "${playlist.videos.size} " + container.context.getString(R.string.videos),
tag = "", tag = "",
call = { 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(); 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), "${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
tag = "", tag = "",
call = { 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(); 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), "${playlist.videos.size} " + container.context.getString(R.string.videos),
tag = "", tag = "",
call = { 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(); StateDownloads.instance.checkForOutdatedPlaylists();
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
})); }));
} }

View File

@ -33,6 +33,7 @@ class SourcePluginConfig(
override val allowEval: Boolean = false, override val allowEval: Boolean = false,
override val allowUrls: List<String> = listOf(), override val allowUrls: List<String> = listOf(),
override val packages: List<String> = listOf(), override val packages: List<String> = listOf(),
override val packagesOptional: List<String> = listOf(),
val settings: List<Setting> = listOf(), val settings: List<Setting> = listOf(),
@ -102,6 +103,10 @@ class SourcePluginConfig(
if(!packages.contains(pack)) if(!packages.contains(pack))
return false; return false;
} }
for(pack in newConfig.packagesOptional) {
if(!packagesOptional.contains(pack))
return false;
}
//Developer Submit Url should be same or empty //Developer Submit Url should be same or empty
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl) if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
return false; return false;

View File

@ -1092,7 +1092,7 @@ class VideoDownload {
StateDownloads.instance.updateCachedVideo(existing); StateDownloads.instance.updateCachedVideo(existing);
} }
else { else {
val newVideo = VideoLocal(videoDetails!!); val newVideo = VideoLocal(videoDetails!!, OffsetDateTime.now());
if(localVideoSource != null) if(localVideoSource != null)
newVideo.videoSource.add(localVideoSource); newVideo.videoSource.add(localVideoSource);
if(localAudioSource != null) if(localAudioSource != null)

View File

@ -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.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import com.futo.platformplayer.stores.v2.IStoreItem import com.futo.platformplayer.stores.v2.IStoreItem
import java.io.File import java.io.File
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -75,11 +76,16 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
//TODO: Offline subtitles //TODO: Offline subtitles
override val subtitles: List<ISubtitleSource> = listOf(); override val subtitles: List<ISubtitleSource> = listOf();
constructor(video: SerializedPlatformVideoDetails) { @kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
var downloadDate: OffsetDateTime? = null;
constructor(video: SerializedPlatformVideoDetails, downloadDate: OffsetDateTime? = null) {
this.videoSerialized = video; this.videoSerialized = video;
this.downloadDate = downloadDate;
} }
constructor(video: IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) { constructor(video: IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) {
this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources); this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources);
downloadDate = OffsetDateTime.now();
} }
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null; override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;

View File

@ -94,7 +94,11 @@ class V8Plugin {
withDependency(PackageBridge(this, config)); withDependency(PackageBridge(this, config));
for(pack in config.packages) 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) { 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? //TODO: Auto get all package types?
return when(packageName) { return when(packageName) {
"DOMParser" -> PackageDOMParser(this) "DOMParser" -> PackageDOMParser(this)
"Http" -> PackageHttp(this, config) "Http" -> PackageHttp(this, config)
"Utilities" -> PackageUtilities(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}");
}; };
} }

View File

@ -5,6 +5,7 @@ interface IV8PluginConfig {
val allowEval: Boolean; val allowEval: Boolean;
val allowUrls: List<String>; val allowUrls: List<String>;
val packages: List<String>; val packages: List<String>;
val packagesOptional: List<String>;
} }
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@ -13,17 +14,20 @@ class V8PluginConfig : IV8PluginConfig {
override val allowEval: Boolean; override val allowEval: Boolean;
override val allowUrls: List<String>; override val allowUrls: List<String>;
override val packages: List<String>; override val packages: List<String>;
override val packagesOptional: List<String>;
constructor() { constructor() {
name = "Unknown"; name = "Unknown";
allowEval = false; allowEval = false;
allowUrls = listOf(); allowUrls = listOf();
packages = listOf(); packages = listOf();
packagesOptional = listOf();
} }
constructor(name: String, allowEval: Boolean, allowUrls: List<String>, packages: List<String> = listOf()) { constructor(name: String, allowEval: Boolean, allowUrls: List<String>, packages: List<String> = listOf(), packagesOptional: List<String> = listOf()) {
this.name = name; this.name = name;
this.allowEval = allowEval; this.allowEval = allowEval;
this.allowUrls = allowUrls; this.allowUrls = allowUrls;
this.packages = packages; this.packages = packages;
this.packagesOptional = packagesOptional;
} }
} }

View File

@ -4,8 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R import com.futo.platformplayer.R
@ -25,6 +30,7 @@ import com.futo.platformplayer.views.items.PlaylistDownloadItem
import com.futo.platformplayer.views.others.ProgressBar import com.futo.platformplayer.views.others.ProgressBar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.OffsetDateTime
class DownloadsFragment : MainFragment() { class DownloadsFragment : MainFragment() {
private val TAG = "DownloadsFragment"; private val TAG = "DownloadsFragment";
@ -92,8 +98,12 @@ class DownloadsFragment : MainFragment() {
private val _listDownloadedHeader: LinearLayout; private val _listDownloadedHeader: LinearLayout;
private val _listDownloadedMeta: TextView; private val _listDownloadedMeta: TextView;
private val _listDownloadSearch: EditText;
private val _listDownloaded: AnyInsertedAdapterView<VideoLocal, VideoDownloadViewHolder>; private val _listDownloaded: AnyInsertedAdapterView<VideoLocal, VideoDownloadViewHolder>;
private var lastDownloads: List<VideoLocal>? = null;
private var ordering: String? = "nameAsc";
constructor(frag: DownloadsFragment, inflater: LayoutInflater): super(frag.requireContext()) { constructor(frag: DownloadsFragment, inflater: LayoutInflater): super(frag.requireContext()) {
inflater.inflate(R.layout.fragment_downloads, this); inflater.inflate(R.layout.fragment_downloads, this);
_frag = frag; _frag = frag;
@ -104,6 +114,7 @@ class DownloadsFragment : MainFragment() {
_listActiveDownloadsContainer = findViewById(R.id.downloads_active_downloads_container); _listActiveDownloadsContainer = findViewById(R.id.downloads_active_downloads_container);
_listActiveDownloadsMeta = findViewById(R.id.downloads_active_downloads_meta); _listActiveDownloadsMeta = findViewById(R.id.downloads_active_downloads_meta);
_listDownloadSearch = findViewById(R.id.downloads_search);
_listActiveDownloads = findViewById(R.id.downloads_active_downloads_list); _listActiveDownloads = findViewById(R.id.downloads_active_downloads_list);
_listPlaylistsContainer = findViewById(R.id.downloads_playlist_container); _listPlaylistsContainer = findViewById(R.id.downloads_playlist_container);
@ -113,6 +124,30 @@ class DownloadsFragment : MainFragment() {
_listDownloadedHeader = findViewById(R.id.downloads_videos_header); _listDownloadedHeader = findViewById(R.id.downloads_videos_header);
_listDownloadedMeta = findViewById(R.id.downloads_videos_meta); _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<RecyclerView>(R.id.list_downloaded) _listDownloaded = findViewById<RecyclerView>(R.id.list_downloaded)
.asAnyWithTop(findViewById(R.id.downloads_top)) { .asAnyWithTop(findViewById(R.id.downloads_top)) {
it.onClick.subscribe { it.onClick.subscribe {
@ -125,7 +160,6 @@ class DownloadsFragment : MainFragment() {
reloadUI(); reloadUI();
} }
fun reloadUI() { fun reloadUI() {
val usage = StateDownloads.instance.getTotalUsage(true); val usage = StateDownloads.instance.getTotalUsage(true);
_usageUsed.text = "${usage.usage.toHumanBytesSize()} " + context.getString(R.string.used); _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()})"; _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<VideoLocal>): List<VideoLocal>{
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;
} }
} }
} }

View File

@ -5,6 +5,7 @@ import android.net.Uri
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
@ -306,14 +307,19 @@ class StatePlaylists {
broadcastSyncPlaylist(playlist); broadcastSyncPlaylist(playlist);
} }
} }
fun addToPlaylist(id: String, video: IPlatformVideo) { fun addToPlaylist(id: String, video: IPlatformVideo): Boolean {
synchronized(playlistStore) { 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.videos.add(SerializedPlatformVideo.fromVideo(video));
playlist.dateUpdate = OffsetDateTime.now(); playlist.dateUpdate = OffsetDateTime.now();
playlistStore.saveAsync(playlist, true); playlistStore.saveAsync(playlist, true);
broadcastSyncPlaylist(playlist); broadcastSyncPlaylist(playlist);
return true;
} }
} }

View File

@ -99,7 +99,6 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<!--Playlists--> <!--Playlists-->
<LinearLayout <LinearLayout
android:id="@+id/downloads_playlist_container" android:id="@+id/downloads_playlist_container"
@ -163,6 +162,37 @@
android:textColor="#ACACAC" android:textColor="#ACACAC"
tools:text="(12 videos)" /> tools:text="(12 videos)" />
</LinearLayout> </LinearLayout>
<EditText
android:id="@+id/downloads_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/background_button_round"
android:hint="Seach.." />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14dp"
android:textColor="@color/gray_ac"
android:fontFamily="@font/inter_light"
android:text="@string/sort_by"
android:paddingStart="20dp" />
<Spinner
android:id="@+id/spinner_sortby"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="20dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView

View File

@ -427,6 +427,8 @@
<string name="bypass_rotation_prevention">Bypass Rotation Prevention</string> <string name="bypass_rotation_prevention">Bypass Rotation Prevention</string>
<string name="playlist_delete_confirmation">Playlist Delete Confirmation</string> <string name="playlist_delete_confirmation">Playlist Delete Confirmation</string>
<string name="playlist_delete_confirmation_description">Show confirmation dialog when deleting media from a playlist</string> <string name="playlist_delete_confirmation_description">Show confirmation dialog when deleting media from a playlist</string>
<string name="playlist_allow_dups">Allow duplicate playlist videos</string>
<string name="playlist_allow_dups_description">Allow adding duplicate videos to playlists</string>
<string name="enable_polycentric">Enable Polycentric</string> <string name="enable_polycentric">Enable Polycentric</string>
<string name="polycentric_local_cache">Enable Polycentric Local Caching</string> <string name="polycentric_local_cache">Enable Polycentric Local Caching</string>
<string name="polycentric_local_cache_description">Caches polycentric results on-device to reduce load times, changing requires app reboot</string> <string name="polycentric_local_cache_description">Caches polycentric results on-device to reduce load times, changing requires app reboot</string>
@ -956,6 +958,14 @@
<item>Watchtime Ascending</item> <item>Watchtime Ascending</item>
<item>Watchtime Descending</item> <item>Watchtime Descending</item>
</string-array> </string-array>
<string-array name="downloads_sortby_array">
<item>Name Ascending</item>
<item>Name Descending</item>
<item>Downloaded Ascending</item>
<item>Downloaded Descending</item>
<item>Released Ascending</item>
<item>Released Descending</item>
</string-array>
<string-array name="feed_style"> <string-array name="feed_style">
<item>Preview</item> <item>Preview</item>
<item>List</item> <item>List</item>