mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-19 19:14:51 +00:00
Playlist dup prevention, download search and ordering, optional package support
This commit is contained in:
parent
0b529ae94d
commit
330aa495c8
11 changed files with 144 additions and 21 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ class SourcePluginConfig(
|
|||
override val allowEval: Boolean = false,
|
||||
override val allowUrls: List<String> = listOf(),
|
||||
override val packages: List<String> = listOf(),
|
||||
override val packagesOptional: List<String> = listOf(),
|
||||
|
||||
val settings: List<Setting> = 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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<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.downloadDate = downloadDate;
|
||||
}
|
||||
constructor(video: IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) {
|
||||
this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources);
|
||||
downloadDate = OffsetDateTime.now();
|
||||
}
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||
|
|
|
@ -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}");
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ interface IV8PluginConfig {
|
|||
val allowEval: Boolean;
|
||||
val allowUrls: List<String>;
|
||||
val packages: List<String>;
|
||||
val packagesOptional: List<String>;
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
|
@ -13,17 +14,20 @@ class V8PluginConfig : IV8PluginConfig {
|
|||
override val allowEval: Boolean;
|
||||
override val allowUrls: List<String>;
|
||||
override val packages: List<String>;
|
||||
override val packagesOptional: List<String>;
|
||||
|
||||
constructor() {
|
||||
name = "Unknown";
|
||||
allowEval = false;
|
||||
allowUrls = 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.allowEval = allowEval;
|
||||
this.allowUrls = allowUrls;
|
||||
this.packages = packages;
|
||||
this.packagesOptional = packagesOptional;
|
||||
}
|
||||
}
|
|
@ -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<VideoLocal, VideoDownloadViewHolder>;
|
||||
|
||||
private var lastDownloads: List<VideoLocal>? = 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<RecyclerView>(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<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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -99,7 +99,6 @@
|
|||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<!--Playlists-->
|
||||
<LinearLayout
|
||||
android:id="@+id/downloads_playlist_container"
|
||||
|
@ -163,6 +162,37 @@
|
|||
android:textColor="#ACACAC"
|
||||
tools:text="(12 videos)" />
|
||||
</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>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
|
|
|
@ -427,6 +427,8 @@
|
|||
<string name="bypass_rotation_prevention">Bypass Rotation Prevention</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_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="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>
|
||||
|
@ -956,6 +958,14 @@
|
|||
<item>Watchtime Ascending</item>
|
||||
<item>Watchtime Descending</item>
|
||||
</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">
|
||||
<item>Preview</item>
|
||||
<item>List</item>
|
||||
|
|
Loading…
Add table
Reference in a new issue