Home filter fixes, persistent sorts, subs exchange fixes, playlist video options

This commit is contained in:
Kelvin K 2025-04-05 01:02:50 +02:00
parent 7d64003d1c
commit 70e6397c6c
20 changed files with 218 additions and 114 deletions

View file

@ -218,6 +218,8 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4)
var showHomeFilters: Boolean = true;
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
var showHomeFiltersPluginNames: Boolean = false;
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;

View file

@ -21,6 +21,8 @@ import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.toHumanBytesSize
import com.futo.platformplayer.toHumanDuration
import com.futo.platformplayer.views.AnyInsertedAdapterView
@ -103,12 +105,15 @@ class DownloadsFragment : MainFragment() {
private val _listDownloaded: AnyInsertedAdapterView<VideoLocal, VideoDownloadViewHolder>;
private var lastDownloads: List<VideoLocal>? = null;
private var ordering: String? = "nameAsc";
private var ordering = FragmentedStorage.get<StringStorage>("downloads_ordering")
constructor(frag: DownloadsFragment, inflater: LayoutInflater): super(frag.requireContext()) {
inflater.inflate(R.layout.fragment_downloads, this);
_frag = frag;
if(ordering.value.isNullOrBlank())
ordering.value = "nameAsc";
_usageUsed = findViewById(R.id.downloads_usage_used);
_usageAvailable = findViewById(R.id.downloads_usage_available);
_usageProgress = findViewById(R.id.downloads_usage_progress);
@ -132,22 +137,23 @@ class DownloadsFragment : MainFragment() {
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);
val options = listOf("nameAsc", "nameDesc", "downloadDateAsc", "downloadDateDesc", "releasedAsc", "releasedDesc");
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
0 -> ordering.setAndSave("nameAsc")
1 -> ordering.setAndSave("nameDesc")
2 -> ordering.setAndSave("downloadDateAsc")
3 -> ordering.setAndSave("downloadDateDesc")
4 -> ordering.setAndSave("releasedAsc")
5 -> ordering.setAndSave("releasedDesc")
else -> ordering.setAndSave("")
}
updateContentFilters()
}
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
};
spinnerSortBy.setSelection(Math.max(0, options.indexOf(ordering.value)));
_listDownloaded = findViewById<RecyclerView>(R.id.list_downloaded)
.asAnyWithTop(findViewById(R.id.downloads_top)) {
@ -230,8 +236,8 @@ class DownloadsFragment : MainFragment() {
var vidsToReturn = vids;
if(!_listDownloadSearch.text.isNullOrEmpty())
vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) || it.author.name.contains(_listDownloadSearch.text, true) };
if(!ordering.isNullOrEmpty()) {
vidsToReturn = when(ordering){
if(!ordering.value.isNullOrEmpty()) {
vidsToReturn = when(ordering.value){
"downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX };
"downloadDateDesc" -> vidsToReturn.sortedByDescending { it.downloadDate ?: OffsetDateTime.MIN };
"nameAsc" -> vidsToReturn.sortedBy { it.name.lowercase() }

View file

@ -240,6 +240,9 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
_automaticNextPageCounter = 0;
}
}
fun resetAutomaticNextPageCounter(){
_automaticNextPageCounter = 0;
}
protected fun setTextCentered(text: String?) {
_textCentered.text = text;

View file

@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.allViews
import androidx.core.view.contains
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.futo.platformplayer.*
@ -29,6 +28,7 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.views.FeedStyle
@ -37,7 +37,6 @@ import com.futo.platformplayer.views.ToggleBar
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.announcements.AnnouncementView
import com.futo.platformplayer.views.buttons.BigButton
import kotlinx.coroutines.runBlocking
import java.time.OffsetDateTime
@ -49,6 +48,12 @@ class HomeFragment : MainFragment() {
private var _view: HomeView? = null;
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
private var _cachedLastPager: IReusablePager<IPlatformContent>? = null
private var _toggleRecent = false;
private var _toggleWatched = false;
private var _togglePluginsDisabled = mutableListOf<String>();
fun reloadFeed() {
_view?.reloadFeed()
@ -74,7 +79,7 @@ class HomeFragment : MainFragment() {
}
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = HomeView(this, inflater, _cachedRecyclerData);
val view = HomeView(this, inflater, _cachedRecyclerData, _cachedLastPager);
_view = view;
return view;
}
@ -92,6 +97,7 @@ class HomeFragment : MainFragment() {
val view = _view;
if (view != null) {
_cachedRecyclerData = view.recyclerData;
_cachedLastPager = view.lastPager;
view.cleanup();
_view = null;
}
@ -111,9 +117,10 @@ class HomeFragment : MainFragment() {
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
private var _lastPager: IReusablePager<IPlatformContent>? = null;
var lastPager: IReusablePager<IPlatformContent>? = null;
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null, cachedLastPager: IReusablePager<IPlatformContent>? = null) : super(fragment, inflater, cachedRecyclerData) {
lastPager = cachedLastPager
_taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, {
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
})
@ -122,7 +129,8 @@ class HomeFragment : MainFragment() {
ReusableRefreshPager(it);
else
ReusablePager(it);
_lastPager = wrappedPager;
lastPager = wrappedPager;
resetAutomaticNextPageCounter();
loadedResult(wrappedPager.getWindow());
}
.exception<ScriptCaptchaRequiredException> { }
@ -227,9 +235,6 @@ class HomeFragment : MainFragment() {
}
private val _filterLock = Object();
private var _toggleRecent = false;
private var _toggleWatched = false;
private var _togglePluginsDisabled = mutableListOf<String>();
private var _togglesConfig = FragmentedStorage.get<StringArrayStorage>("home_toggles");
fun initializeToolbarContent() {
if(_toolbarContentView.allViews.any { it is ToggleBar })
@ -245,18 +250,19 @@ class HomeFragment : MainFragment() {
layoutParams =
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
_togglePluginsDisabled.clear();
fragment._togglePluginsDisabled.clear();
synchronized(_filterLock) {
val buttonsPlugins = (if (_togglesConfig.contains("plugins"))
(StatePlatform.instance.getEnabledClients()
.filter { it is JSClient && it.enableInHome }
.map { plugin ->
ToggleBar.Toggle(plugin.name, plugin.icon, true, {
ToggleBar.Toggle(if(Settings.instance.home.showHomeFiltersPluginNames) plugin.name else "", plugin.icon, !fragment._togglePluginsDisabled.contains(plugin.id), {
if (it) {
if (_togglePluginsDisabled.contains(plugin.id))
_togglePluginsDisabled.remove(plugin.id);
if (fragment._togglePluginsDisabled.contains(plugin.id))
fragment._togglePluginsDisabled.remove(plugin.id);
} else {
if (!_togglePluginsDisabled.contains(plugin.id))
_togglePluginsDisabled.add(plugin.id);
if (!fragment._togglePluginsDisabled.contains(plugin.id))
fragment._togglePluginsDisabled.add(plugin.id);
}
reloadForFilters();
}).withTag("plugins")
@ -264,13 +270,13 @@ class HomeFragment : MainFragment() {
else listOf())
val buttons = (listOf<ToggleBar.Toggle?>(
(if (_togglesConfig.contains("today"))
ToggleBar.Toggle("Today", _toggleRecent) {
_toggleRecent = it; reloadForFilters()
ToggleBar.Toggle("Today", fragment._toggleRecent) {
fragment._toggleRecent = it; reloadForFilters()
}
.withTag("today") else null),
(if (_togglesConfig.contains("watched"))
ToggleBar.Toggle("Unwatched", _toggleWatched) {
_toggleWatched = it; reloadForFilters()
ToggleBar.Toggle("Unwatched", fragment._toggleWatched) {
fragment._toggleWatched = it; reloadForFilters()
}
.withTag("watched") else null),
).filterNotNull() + buttonsPlugins)
@ -302,7 +308,7 @@ class HomeFragment : MainFragment() {
}
}
fun reloadForFilters() {
_lastPager?.let { loadedResult(it.getWindow()) };
lastPager?.let { loadedResult(it.getWindow()) };
}
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
@ -312,11 +318,11 @@ class HomeFragment : MainFragment() {
if(StateMeta.instance.isCreatorHidden(it.author.url))
return@filter false;
if(_toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 25)
if(fragment._toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 25)
return@filter false;
if(_toggleWatched && StateHistory.instance.isHistoryWatched(it.url, 0))
if(fragment._toggleWatched && StateHistory.instance.isHistoryWatched(it.url, 0))
return@filter false;
if(_togglePluginsDisabled.any() && it.id.pluginId != null && _togglePluginsDisabled.contains(it.id.pluginId)) {
if(fragment._togglePluginsDisabled.any() && it.id.pluginId != null && fragment._togglePluginsDisabled.contains(it.id.pluginId)) {
return@filter false;
}

View file

@ -326,6 +326,10 @@ class PlaylistFragment : MainFragment() {
playlist.videos = ArrayList(playlist.videos.filter { it != video });
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
}
override fun onVideoOptions(video: IPlatformVideo) {
UISlideOverlays.showVideoOptionsOverlay(video, overlayContainer);
}
override fun onVideoClicked(video: IPlatformVideo) {
val playlist = _playlist;
if (playlist != null) {

View file

@ -26,6 +26,8 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.views.SearchView
import com.futo.platformplayer.views.adapters.*
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
@ -82,7 +84,7 @@ class PlaylistsFragment : MainFragment() {
private var _listPlaylistsSearch: EditText;
private var _ordering: String? = null;
private var _ordering = FragmentedStorage.get<StringStorage>("playlists_ordering")
constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
@ -145,24 +147,25 @@ class PlaylistsFragment : MainFragment() {
spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.playlists_sortby_array)).also {
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
};
spinnerSortBy.setSelection(0);
val options = listOf("nameAsc", "nameDesc", "dateEditAsc", "dateEditDesc", "dateCreateAsc", "dateCreateDesc", "datePlayAsc", "datePlayDesc");
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 = "dateEditAsc"
3 -> _ordering = "dateEditDesc"
4 -> _ordering = "dateCreateAsc"
5 -> _ordering = "dateCreateDesc"
6 -> _ordering = "datePlayAsc"
7 -> _ordering = "datePlayDesc"
else -> _ordering = null
0 -> _ordering.setAndSave("nameAsc")
1 -> _ordering.setAndSave("nameDesc")
2 -> _ordering.setAndSave("dateEditAsc")
3 -> _ordering.setAndSave("dateEditDesc")
4 -> _ordering.setAndSave("dateCreateAsc")
5 -> _ordering.setAndSave("dateCreateDesc")
6 -> _ordering.setAndSave("datePlayAsc")
7 -> _ordering.setAndSave("datePlayDesc")
else -> _ordering.setAndSave("")
}
updatePlaylistsFiltering()
}
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
};
spinnerSortBy.setSelection(Math.max(0, options.indexOf(_ordering.value)));
findViewById<TextView>(R.id.text_view_all).setOnClickListener { _fragment.navigate<WatchLaterFragment>(context.getString(R.string.watch_later)); };
@ -214,8 +217,8 @@ class PlaylistsFragment : MainFragment() {
var playlistsToReturn = pls;
if(!_listPlaylistsSearch.text.isNullOrEmpty())
playlistsToReturn = playlistsToReturn.filter { it.name.contains(_listPlaylistsSearch.text, true) };
if(!_ordering.isNullOrEmpty()){
playlistsToReturn = when(_ordering){
if(!_ordering.value.isNullOrEmpty()){
playlistsToReturn = when(_ordering.value){
"nameAsc" -> playlistsToReturn.sortedBy { it.name.lowercase() }
"nameDesc" -> playlistsToReturn.sortedByDescending { it.name.lowercase() };
"dateEditAsc" -> playlistsToReturn.sortedBy { it.dateUpdate ?: OffsetDateTime.MAX };

View file

@ -693,6 +693,9 @@ class VideoDetailView : ConstraintLayout {
_container_content_description.onClose.subscribe { switchContentView(_container_content_main); };
_container_content_liveChat.onClose.subscribe { switchContentView(_container_content_main); };
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
_container_content_queue.onOptions.subscribe {
UISlideOverlays.showVideoOptionsOverlay(it, _overlayContainer);
}
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };

View file

@ -104,6 +104,7 @@ abstract class VideoListEditorView : LinearLayout {
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
videoListEditorView.onVideoRemoved.subscribe(::onVideoRemoved);
videoListEditorView.onVideoOptions.subscribe(::onVideoOptions);
videoListEditorView.onVideoClicked.subscribe(::onVideoClicked);
_videoListEditorView = videoListEditorView;
@ -122,6 +123,7 @@ abstract class VideoListEditorView : LinearLayout {
open fun onShuffleClick() { }
open fun onEditClick() { }
open fun onVideoRemoved(video: IPlatformVideo) {}
open fun onVideoOptions(video: IPlatformVideo) {}
open fun onVideoOrderChanged(videos : List<IPlatformVideo>) {}
open fun onVideoClicked(video: IPlatformVideo) {

View file

@ -103,6 +103,9 @@ class WatchLaterFragment : MainFragment() {
StatePlaylists.instance.removeFromWatchLater(video, true);
}
}
override fun onVideoOptions(video: IPlatformVideo) {
UISlideOverlays.showVideoOptionsOverlay(video, overlayContainer);
}
override fun onVideoClicked(video: IPlatformVideo) {
val watchLater = StatePlaylists.instance.getWatchLater();

View file

@ -69,7 +69,7 @@ class StateSubscriptions {
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
private val _subsExchangeServer = "http://10.10.15.159"//"https://exchange.grayjay.app/";
private val _subsExchangeServer = "https://exchange.grayjay.app/";
private val _subscriptionKey = FragmentedStorage.get<StringStorage>("sub_exchange_key");
init {

View file

@ -82,23 +82,30 @@ abstract class SubscriptionsTaskFetchAlgorithm(
var providedTasks: MutableList<SubscriptionTask>? = null;
try {
val contractableTasks =
tasks.filter { !it.fromPeek && !it.fromCache && (it.type == ResultCapabilities.TYPE_VIDEOS || it.type == ResultCapabilities.TYPE_MIXED) };
contract =
if (contractableTasks.size > 10) subsExchangeClient?.requestContract(*contractableTasks.map {
ChannelRequest(it.url)
}.toTypedArray()) else null;
if (contract?.provided?.isNotEmpty() == true)
Logger.i(TAG, "Received subscription exchange contract (Requires ${contract?.required?.size}, Provides ${contract?.provided?.size}), ID: ${contract?.id}");
if (contract != null && contract.required.isNotEmpty()) {
providedTasks = mutableListOf()
for (task in tasks.toList()) {
if (!task.fromCache && !task.fromPeek && contract.provided.contains(task.url)) {
providedTasks.add(task);
tasks.remove(task);
val contractingTime = measureTimeMillis {
val contractableTasks =
tasks.filter { !it.fromPeek && !it.fromCache && (it.type == ResultCapabilities.TYPE_VIDEOS || it.type == ResultCapabilities.TYPE_MIXED) };
contract =
if (contractableTasks.size > 10) subsExchangeClient?.requestContract(*contractableTasks.map {
ChannelRequest(it.url)
}.toTypedArray()) else null;
if (contract?.provided?.isNotEmpty() == true)
Logger.i(TAG, "Received subscription exchange contract (Requires ${contract?.required?.size}, Provides ${contract?.provided?.size}), ID: ${contract?.id}");
if (contract != null && contract!!.required.isNotEmpty()) {
providedTasks = mutableListOf()
for (task in tasks.toList()) {
if (!task.fromCache && !task.fromPeek && contract!!.provided.contains(task.url)) {
providedTasks!!.add(task);
tasks.remove(task);
}
}
}
}
if(contract != null)
Logger.i(TAG, "Subscription Exchange contract received in ${contractingTime}ms");
else if(contractingTime > 100)
Logger.i(TAG, "Subscription Exchange contract failed to received in${contractingTime}ms");
}
catch(ex: Throwable){
Logger.e("SubscriptionsTaskFetchAlgorithm", "Failed to retrieve SubsExchange contract due to: " + ex.message, ex);
@ -109,6 +116,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
val taskResults = arrayListOf<SubscriptionTaskResult>();
var resolveCount = 0;
var resolveTime = 0L;
val timeTotal = measureTimeMillis {
for(task in forkTasks) {
try {
@ -137,51 +146,68 @@ abstract class SubscriptionsTaskFetchAlgorithm(
}
};
}
}
//Resolve Subscription Exchange
if(contract != null) {
try {
val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract.required.contains(it.task.url) }.map {
ChannelResolve(
it.task.url,
it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) }
)
}.toTypedArray()
val resolve = subsExchangeClient?.resolveContract(
contract,
*resolves
);
if (resolve != null) {
val invalids = resolve.filter { it.content.any { it.datetime == null } };
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size})")
for(result in resolve){
val task = providedTasks?.find { it.url == result.channelUrl };
if(task != null) {
taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null));
providedTasks?.remove(task);
//Resolve Subscription Exchange
if(contract != null) {
try {
resolveTime = measureTimeMillis {
val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract!!.required.contains(it.task.url) }.map {
ChannelResolve(
it.task.url,
it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) }
)
}.toTypedArray()
val resolve = subsExchangeClient?.resolveContract(
contract!!,
*resolves
);
if (resolve != null) {
resolveCount = resolves.size;
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}")
for(result in resolve){
val task = providedTasks?.find { it.url == result.channelUrl };
if(task != null) {
taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null));
providedTasks?.remove(task);
}
}
}
if (providedTasks != null) {
for(task in providedTasks!!) {
taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange")));
}
}
}
Logger.i(TAG, "Subscription Exchange contract resolved in ${resolveTime}ms");
}
if (providedTasks != null) {
for(task in providedTasks) {
taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange")));
}
catch(ex: Throwable) {
//TODO: fetch remainder after all?
Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex);
}
}
catch(ex: Throwable) {
//TODO: fetch remainder after all?
Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex);
}
}
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms")
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms");
if(resolveCount > 0) {
val selfFetchTime = timeTotal - resolveTime;
val selfFetchCount = tasks.count { !it.fromPeek && !it.fromCache };
if(selfFetchCount > 0) {
val selfResolvePercentage = resolveCount.toDouble() / selfFetchCount;
val estimateSelfFetchTime = selfFetchTime + selfFetchTime * selfResolvePercentage;
val selfFetchDelta = timeTotal - estimateSelfFetchTime;
if(selfFetchDelta > 0)
UIDialogs.appToast("Subscription Exchange lost ${selfFetchDelta}ms (out of ${timeTotal}ms)", true);
else
UIDialogs.appToast("Subscription Exchange saved ${(selfFetchDelta * -1).toInt()}ms (out of ${timeTotal}ms)", true);
}
}
//Cache pagers grouped by channel
val groupedPagers = taskResults.groupBy { it.task.sub.channel.url }
.map { entry ->
val sub = if(!entry.value.isEmpty()) entry.value[0].task.sub else null;
val liveTasks = entry.value.filter { !it.task.fromCache };
val liveTasks = entry.value.filter { !it.task.fromCache && it.pager != null };
val cachedTasks = entry.value.filter { it.task.fromCache };
val livePager = if(liveTasks.isNotEmpty()) StateCache.cachePagerResults(scope, MultiChronoContentPager(liveTasks.map { it.pager!! }, true).apply { this.initialize() }) {
onNewCacheHit.emit(sub!!, it);

View file

@ -28,7 +28,7 @@ import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.RSAPublicKeySpec
class SubsExchangeClient(private val server: String, private val privateKey: String) {
class SubsExchangeClient(private val server: String, private val privateKey: String, private val contractTimeout: Int = 1000) {
private val json = Json {
ignoreUnknownKeys = true
@ -40,7 +40,7 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
// Endpoint: Contract
fun requestContract(vararg channels: ChannelRequest): ExchangeContract {
val data = post("/api/Channel/Contract", Json.encodeToString(channels), "application/json")
val data = post("/api/Channel/Contract", Json.encodeToString(channels), "application/json", contractTimeout)
return Json.decodeFromString(data)
}
suspend fun requestContractAsync(vararg channels: ChannelRequest): ExchangeContract {
@ -74,9 +74,11 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
}
// IO methods
private fun post(query: String, body: String, contentType: String): String {
private fun post(query: String, body: String, contentType: String, timeout: Int = 0): String {
val url = URL("${server.trim('/')}$query")
with(url.openConnection() as HttpURLConnection) {
if(timeout > 0)
this.connectTimeout = timeout
requestMethod = "POST"
setRequestProperty("Content-Type", contentType)
doOutput = true

View file

@ -14,6 +14,7 @@ class VideoListEditorAdapter : RecyclerView.Adapter<VideoListEditorViewHolder> {
val onClick = Event1<IPlatformVideo>();
val onRemove = Event1<IPlatformVideo>();
val onOptions = Event1<IPlatformVideo>();
var canEdit = false
private set;
@ -28,6 +29,7 @@ class VideoListEditorAdapter : RecyclerView.Adapter<VideoListEditorViewHolder> {
val holder = VideoListEditorViewHolder(view, _touchHelper);
holder.onRemove.subscribe { v -> onRemove.emit(v); };
holder.onOptions.subscribe { v -> onOptions.emit(v); };
holder.onClick.subscribe { v -> onClick.emit(v); };
return holder;

View file

@ -32,6 +32,7 @@ class VideoListEditorViewHolder : ViewHolder {
private val _containerDuration: LinearLayout;
private val _containerLive: LinearLayout;
private val _imageRemove: ImageButton;
private val _imageOptions: ImageButton;
private val _imageDragDrop: ImageButton;
private val _platformIndicator: PlatformIndicator;
private val _layoutDownloaded: FrameLayout;
@ -41,6 +42,7 @@ class VideoListEditorViewHolder : ViewHolder {
val onClick = Event1<IPlatformVideo>();
val onRemove = Event1<IPlatformVideo>();
val onOptions = Event1<IPlatformVideo>();
@SuppressLint("ClickableViewAccessibility")
constructor(view: View, touchHelper: ItemTouchHelper? = null) : super(view) {
@ -54,6 +56,7 @@ class VideoListEditorViewHolder : ViewHolder {
_containerDuration = view.findViewById(R.id.thumbnail_duration_container);
_containerLive = view.findViewById(R.id.thumbnail_live_container);
_imageRemove = view.findViewById(R.id.image_trash);
_imageOptions = view.findViewById(R.id.image_settings);
_imageDragDrop = view.findViewById<ImageButton>(R.id.image_drag_drop);
_platformIndicator = view.findViewById(R.id.thumbnail_platform);
_layoutDownloaded = view.findViewById(R.id.layout_downloaded);
@ -74,6 +77,10 @@ class VideoListEditorViewHolder : ViewHolder {
val v = video ?: return@setOnClickListener;
onRemove.emit(v);
};
_imageOptions?.setOnClickListener {
val v = video ?: return@setOnClickListener;
onOptions.emit(v);
}
}
fun bind(v: IPlatformVideo, canEdit: Boolean) {

View file

@ -8,6 +8,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
@ -22,6 +23,7 @@ class VideoListEditorView : FrameLayout {
val onVideoOrderChanged = Event1<List<IPlatformVideo>>()
val onVideoRemoved = Event1<IPlatformVideo>();
val onVideoOptions = Event1<IPlatformVideo>();
val onVideoClicked = Event1<IPlatformVideo>();
val isEmpty get() = _videos.isEmpty();
@ -54,6 +56,9 @@ class VideoListEditorView : FrameLayout {
}
};
adapterVideos.onOptions.subscribe { v ->
onVideoOptions?.emit(v);
}
adapterVideos.onRemove.subscribe { v ->
val executeDelete = {
synchronized(_videos) {

View file

@ -58,6 +58,7 @@ class ToggleTagView : LinearLayout {
setToggle(isActive);
_image.setImageResource(imageResource);
_image.visibility = View.VISIBLE;
_textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE;
this.isButton = isButton;
}
fun setInfo(image: ImageVariable, text: String, isActive: Boolean, isButton: Boolean = false) {
@ -66,12 +67,14 @@ class ToggleTagView : LinearLayout {
setToggle(isActive);
image.setImageView(_image, R.drawable.ic_error_pred);
_image.visibility = View.VISIBLE;
_textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE;
this.isButton = isButton;
}
fun setInfo(text: String, isActive: Boolean, isButton: Boolean = false) {
_image.visibility = View.GONE;
_text = text;
_textTag.text = text;
_textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE;
setToggle(isActive);
this.isButton = isButton;
}

View file

@ -8,7 +8,9 @@ import android.widget.LinearLayout
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.views.lists.VideoListEditorView
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
@ -23,6 +25,7 @@ class QueueEditorOverlay : LinearLayout {
private val _overlayContainer: FrameLayout;
val onOptions = Event1<IPlatformVideo>();
val onClose = Event0();
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
@ -35,6 +38,9 @@ class QueueEditorOverlay : LinearLayout {
_topbar.onClose.subscribe(this, onClose::emit);
_editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) }
_editor.onVideoOptions.subscribe { v ->
onOptions?.emit(v);
}
_editor.onVideoRemoved.subscribe { v ->
StatePlayer.instance.removeFromQueue(v);
_topbar.setInfo(context.getString(R.string.queue), "${StatePlayer.instance.queueSize} " + context.getString(R.string.videos));

View file

@ -135,7 +135,7 @@
android:ellipsize="end"
app:layout_constraintLeft_toRightOf="@id/layout_video_thumbnail"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/image_trash"
app:layout_constraintRight_toLeftOf="@id/buttons"
app:layout_constraintBottom_toTopOf="@id/text_author"
android:layout_marginStart="10dp" />
@ -152,7 +152,7 @@
android:ellipsize="end"
app:layout_constraintLeft_toRightOf="@id/layout_video_thumbnail"
app:layout_constraintTop_toBottomOf="@id/text_video_name"
app:layout_constraintRight_toLeftOf="@id/image_trash"
app:layout_constraintRight_toLeftOf="@id/buttons"
app:layout_constraintBottom_toTopOf="@id/text_video_metadata"
android:layout_marginStart="10dp" />
@ -169,19 +169,35 @@
android:ellipsize="end"
app:layout_constraintLeft_toRightOf="@id/layout_video_thumbnail"
app:layout_constraintTop_toBottomOf="@id/text_author"
app:layout_constraintRight_toLeftOf="@id/image_trash"
app:layout_constraintRight_toLeftOf="@id/buttons"
android:layout_marginStart="10dp" />
<ImageButton
android:id="@+id/image_trash"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@string/cd_button_delete"
app:srcCompat="@drawable/ic_trash_18dp"
android:scaleType="fitCenter"
android:paddingTop="10dp"
android:paddingBottom="10dp"
<LinearLayout
android:id="@+id/buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/layout_video_thumbnail"
app:layout_constraintBottom_toBottomOf="@id/layout_video_thumbnail" />
app:layout_constraintBottom_toBottomOf="@id/layout_video_thumbnail" >
<ImageButton
android:id="@+id/image_trash"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@string/cd_button_delete"
app:srcCompat="@drawable/ic_trash_18dp"
android:scaleType="fitCenter"
android:paddingTop="10dp"
android:paddingBottom="10dp"/>
<ImageButton
android:id="@+id/image_settings"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@string/cd_button_settings"
app:srcCompat="@drawable/ic_settings"
android:scaleType="fitCenter"
android:paddingTop="10dp"
android:paddingBottom="10dp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -3,8 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:background="@drawable/background_pill"
android:layout_marginEnd="6dp"
android:layout_marginTop="17dp"
@ -19,12 +19,15 @@
android:visibility="gone"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginRight="5dp"
android:layout_marginTop="4dp" />
android:layout_gravity="center"
android:layout_marginLeft="2.5dp"
android:layout_marginRight="2.5dp" />
<TextView
android:id="@+id/text_tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="2.5dp"
android:layout_marginRight="2.5dp"
android:textColor="@color/white"
android:layout_gravity="center"
android:gravity="center"

View file

@ -419,6 +419,8 @@
<string name="preview_feed_items_description">When the preview feedstyle is used, if items should auto-preview when scrolling over them</string>
<string name="show_home_filters">Show Home Filters</string>
<string name="show_home_filters_description">If the home filters should be shown above home</string>
<string name="show_home_filters_plugin_names">Home filter Plugin Names</string>
<string name="show_home_filters_plugin_names_description">If home filters should show full plugin names or just icons</string>
<string name="log_level">Log Level</string>
<string name="logging">Logging</string>
<string name="sync_grayjay">Sync Grayjay</string>