Feed filter loading improved, home filters support, various peripheral stuff

This commit is contained in:
Kelvin K 2025-04-04 00:37:26 +02:00
parent 0a59e04f19
commit 7d64003d1c
20 changed files with 411 additions and 62 deletions

View File

@ -205,7 +205,7 @@ class Settings : FragmentedStorageFileJson() {
var home = HomeSettings(); var home = HomeSettings();
@Serializable @Serializable
class HomeSettings { class HomeSettings {
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5) @FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 3)
@DropdownFieldOptionsId(R.array.feed_style) @DropdownFieldOptionsId(R.array.feed_style)
var homeFeedStyle: Int = 1; var homeFeedStyle: Int = 1;
@ -216,6 +216,9 @@ class Settings : FragmentedStorageFileJson() {
return FeedStyle.THUMBNAIL; return FeedStyle.THUMBNAIL;
} }
@FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4)
var showHomeFilters: Boolean = true;
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;

View File

@ -1148,7 +1148,7 @@ class UISlideOverlays {
container.context.getString(R.string.decide_which_buttons_should_be_pinned), container.context.getString(R.string.decide_which_buttons_should_be_pinned),
tag = "", tag = "",
call = { call = {
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) { showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }, {
val selected = it val selected = it
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } } .map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
.filter { it != null } .filter { it != null }
@ -1156,7 +1156,7 @@ class UISlideOverlays {
.toList(); .toList();
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) }); onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
} });
}, },
invokeParent = false invokeParent = false
)) ))
@ -1164,29 +1164,40 @@ class UISlideOverlays {
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() }; return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
} }
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit, description: String? = null) {
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
val selection: MutableList<Any> = mutableListOf(); val selection: MutableList<Any> = mutableListOf();
var overlay: SlideUpMenuOverlay? = null; var overlay: SlideUpMenuOverlay? = null;
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true, overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
options.map { SlideUpMenuItem( listOf(
if(!description.isNullOrEmpty()) SlideUpMenuGroup(container.context, "", description, "", listOf()) else null,
).filterNotNull() +
(options.map { SlideUpMenuItem(
container.context, container.context,
R.drawable.ic_move_up, R.drawable.ic_move_up,
it.first, it.first,
"", "",
tag = it.second, tag = it.second,
call = { call = {
val overlayItem = overlay?.getSlideUpItemByTag(it.second);
if(overlay!!.selectOption(null, it.second, true, true)) { if(overlay!!.selectOption(null, it.second, true, true)) {
if(!selection.contains(it.second)) if(!selection.contains(it.second)) {
selection.add(it.second); selection.add(it.second);
} else if(overlayItem != null) {
overlayItem.setSubText(selection.indexOf(it.second).toString());
}
}
} else {
selection.remove(it.second); selection.remove(it.second);
if(overlayItem != null) {
overlayItem.setSubText("");
}
}
}, },
invokeParent = false invokeParent = false
) )
}); }));
overlay.onOK.subscribe { overlay.onOK.subscribe {
onOrdered.invoke(selection); onOrdered.invoke(selection);
overlay.hide(); overlay.hide();

View File

@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.contents
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.time.OffsetDateTime import java.time.OffsetDateTime
interface IPlatformContent { interface IPlatformContent {

View File

@ -10,6 +10,7 @@ import com.futo.polycentric.core.combineHashCodes
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNames
import java.time.OffsetDateTime import java.time.OffsetDateTime
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@ -20,6 +21,7 @@ open class SerializedPlatformVideo(
override val thumbnails: Thumbnails, override val thumbnails: Thumbnails,
override val author: PlatformAuthorLink, override val author: PlatformAuthorLink,
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class) @kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
@JsonNames("datetime", "dateTime")
override val datetime: OffsetDateTime? = null, override val datetime: OffsetDateTime? = null,
override val url: String, override val url: String,
override val shareUrl: String = "", override val shareUrl: String = "",

View File

@ -6,7 +6,7 @@ import com.futo.platformplayer.constructs.Event1
* A RefreshPager represents a pager that can be modified overtime (eg. By getting more results later, by recreating the pager) * A RefreshPager represents a pager that can be modified overtime (eg. By getting more results later, by recreating the pager)
* When the onPagerChanged event is emitted, a new pager instance is passed, or requested via getCurrentPager * When the onPagerChanged event is emitted, a new pager instance is passed, or requested via getCurrentPager
*/ */
interface IRefreshPager<T> { interface IRefreshPager<T>: IPager<T> {
val onPagerChanged: Event1<IPager<T>>; val onPagerChanged: Event1<IPager<T>>;
val onPagerError: Event1<Throwable>; val onPagerError: Event1<Throwable>;

View File

@ -1,5 +1,7 @@
package com.futo.platformplayer.api.media.structures package com.futo.platformplayer.api.media.structures
import com.futo.platformplayer.api.media.structures.ReusablePager.Window
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
/** /**
@ -9,8 +11,8 @@ import com.futo.platformplayer.logging.Logger
* A "Window" is effectively a pager that just reads previous results from the shared results, but when the end is reached, it will call nextPage on the parent if possible for new results. * A "Window" is effectively a pager that just reads previous results from the shared results, but when the end is reached, it will call nextPage on the parent if possible for new results.
* This allows multiple Windows to exist of the same pager, without messing with position, or duplicate requests * This allows multiple Windows to exist of the same pager, without messing with position, or duplicate requests
*/ */
class ReusablePager<T>: INestedPager<T>, IPager<T> { open class ReusablePager<T>: INestedPager<T>, IReusablePager<T> {
private val _pager: IPager<T>; protected var _pager: IPager<T>;
val previousResults = arrayListOf<T>(); val previousResults = arrayListOf<T>();
constructor(subPager: IPager<T>) { constructor(subPager: IPager<T>) {
@ -44,7 +46,7 @@ class ReusablePager<T>: INestedPager<T>, IPager<T> {
return previousResults; return previousResults;
} }
fun getWindow(): Window<T> { override fun getWindow(): Window<T> {
return Window(this); return Window(this);
} }
@ -95,4 +97,118 @@ class ReusablePager<T>: INestedPager<T>, IPager<T> {
return ReusablePager(this); return ReusablePager(this);
} }
} }
}
public class ReusableRefreshPager<T>: INestedPager<T>, IReusablePager<T> {
protected var _pager: IRefreshPager<T>;
val previousResults = arrayListOf<T>();
private var _currentPage: IPager<T>;
val onPagerChanged = Event1<IPager<T>>()
val onPagerError = Event1<Throwable>()
constructor(subPager: IRefreshPager<T>) {
this._pager = subPager;
_currentPage = this;
synchronized(previousResults) {
previousResults.addAll(subPager.getResults());
}
_pager.onPagerError.subscribe(onPagerError::emit);
_pager.onPagerChanged.subscribe {
_currentPage = it;
synchronized(previousResults) {
previousResults.clear();
previousResults.addAll(it.getResults());
}
onPagerChanged.emit(_currentPage);
};
}
override fun findPager(query: (IPager<T>) -> Boolean): IPager<T>? {
if(query(_pager))
return _pager;
else if(_pager is INestedPager<*>)
return (_pager as INestedPager<T>).findPager(query);
return null;
}
override fun hasMorePages(): Boolean {
return _pager.hasMorePages();
}
override fun nextPage() {
_pager.nextPage();
}
override fun getResults(): List<T> {
val results = _pager.getResults();
synchronized(previousResults) {
previousResults.addAll(results);
}
return previousResults;
}
override fun getWindow(): RefreshWindow<T> {
return RefreshWindow(this);
}
class RefreshWindow<T>: IPager<T>, INestedPager<T>, IRefreshPager<T> {
private val _parent: ReusableRefreshPager<T>;
private var _position: Int = 0;
private var _read: Int = 0;
private var _currentResults: List<T>;
override val onPagerChanged = Event1<IPager<T>>();
override val onPagerError = Event1<Throwable>();
override fun getCurrentPager(): IPager<T> {
return _parent.getWindow();
}
constructor(parent: ReusableRefreshPager<T>) {
_parent = parent;
synchronized(_parent.previousResults) {
_currentResults = _parent.previousResults.toList();
_read += _currentResults.size;
}
parent.onPagerChanged.subscribe(onPagerChanged::emit);
parent.onPagerError.subscribe(onPagerError::emit);
}
override fun hasMorePages(): Boolean {
return _parent.previousResults.size > _read || _parent.hasMorePages();
}
override fun nextPage() {
synchronized(_parent.previousResults) {
if (_parent.previousResults.size <= _read) {
_parent.nextPage();
_parent.getResults();
}
_currentResults = _parent.previousResults.drop(_read).toList();
_read += _currentResults.size;
}
}
override fun getResults(): List<T> {
return _currentResults;
}
override fun findPager(query: (IPager<T>) -> Boolean): IPager<T>? {
return _parent.findPager(query);
}
}
}
interface IReusablePager<T>: IPager<T> {
fun getWindow(): IPager<T>;
} }

View File

@ -3,12 +3,15 @@ package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.util.DisplayMetrics
import android.view.Display
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.* import android.widget.*
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager import androidx.recyclerview.widget.RecyclerView.LayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@ -20,6 +23,7 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.others.ProgressBar import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.others.TagsView import com.futo.platformplayer.views.others.TagsView
@ -28,7 +32,9 @@ import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.announcements.AnnouncementView import com.futo.platformplayer.views.announcements.AnnouncementView
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.OffsetDateTime import java.time.OffsetDateTime
import kotlin.math.max import kotlin.math.max
@ -68,6 +74,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
private val _scrollListener: RecyclerView.OnScrollListener; private val _scrollListener: RecyclerView.OnScrollListener;
private var _automaticNextPageCounter = 0; private var _automaticNextPageCounter = 0;
private val _automaticBackoff = arrayOf(0, 500, 1000, 1000, 2000, 5000, 5000, 5000);
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) { constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
this.fragment = fragment; this.fragment = fragment;
@ -129,6 +136,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
_toolbarContentView = findViewById(R.id.container_toolbar_content); _toolbarContentView = findViewById(R.id.container_toolbar_content);
_nextPageHandler = TaskHandler<TPager, List<TResult>>({fragment.lifecycleScope}, { _nextPageHandler = TaskHandler<TPager, List<TResult>>({fragment.lifecycleScope}, {
if (it is IAsyncPager<*>) if (it is IAsyncPager<*>)
it.nextPageAsync(); it.nextPageAsync();
else else
@ -182,26 +190,53 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) { private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
val canScroll = if (recyclerData.results.isEmpty()) false else { val canScroll = if (recyclerData.results.isEmpty()) false else {
val height = resources.displayMetrics.heightPixels;
val layoutManager = recyclerData.layoutManager val layoutManager = recyclerData.layoutManager
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
val firstVisibleItemView = if(firstVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(firstVisibleItemPosition) else null;
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) { val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition();
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition) val lastVisibleItemView = if(lastVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(lastVisibleItemPosition) else null;
val itemHeight = firstVisibleView?.height ?: 0 if(lastVisibleItemView != null && lastVisibleItemPosition == (recyclerData.results.size - 1)) {
val occupiedSpace = recyclerData.results.size / recyclerData.layoutManager.spanCount * itemHeight false;
val recyclerViewHeight = _recyclerResults.height }
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight") else if (firstVisibleItemView != null && height != null && firstVisibleItemView.height * recyclerData.results.size < height) {
occupiedSpace >= recyclerViewHeight false;
} else { } else {
false true;
} }
} }
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter") Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
if (!canScroll || filteredResults.isEmpty()) { if (!canScroll || filteredResults.isEmpty()) {
_automaticNextPageCounter++ _automaticNextPageCounter++
if(_automaticNextPageCounter <= 4) if(_automaticNextPageCounter < _automaticBackoff.size) {
loadNextPage() if(_automaticNextPageCounter > 0) {
val automaticNextPageCounterSaved = _automaticNextPageCounter;
fragment.lifecycleScope.launch(Dispatchers.Default) {
val backoff = _automaticBackoff[Math.min(_automaticBackoff.size - 1, _automaticNextPageCounter)];
withContext(Dispatchers.Main) {
setLoading(true);
}
delay(backoff.toLong());
if(automaticNextPageCounterSaved == _automaticNextPageCounter) {
withContext(Dispatchers.Main) {
loadNextPage();
}
}
else {
withContext(Dispatchers.Main) {
setLoading(false);
}
}
}
}
else
loadNextPage();
}
} else { } else {
Logger.i(TAG, "ensureEnoughContentVisible automaticNextPageCounter reset");
_automaticNextPageCounter = 0; _automaticNextPageCounter = 0;
} }
} }

View File

@ -5,22 +5,32 @@ 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 androidx.core.view.allViews
import androidx.core.view.contains
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.UISlideOverlays.Companion.showOrderOverlay
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.IRefreshPager
import com.futo.platformplayer.api.media.structures.IReusablePager
import com.futo.platformplayer.api.media.structures.ReusablePager
import com.futo.platformplayer.api.media.structures.ReusableRefreshPager
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.NoResultsView import com.futo.platformplayer.views.NoResultsView
import com.futo.platformplayer.views.ToggleBar import com.futo.platformplayer.views.ToggleBar
@ -91,6 +101,7 @@ class HomeFragment : MainFragment() {
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.home.previewFeedItems); _view?.setPreviewsEnabled(previewsEnabled && Settings.instance.home.previewFeedItems);
} }
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
class HomeView : ContentFeedView<HomeFragment> { class HomeView : ContentFeedView<HomeFragment> {
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle(); override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
@ -100,11 +111,20 @@ class HomeFragment : MainFragment() {
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>; private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
private 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) : super(fragment, inflater, cachedRecyclerData) {
_taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, { _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, {
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope) StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
}) })
.success { loadedResult(it); } .success {
val wrappedPager = if(it is IRefreshPager)
ReusableRefreshPager(it);
else
ReusablePager(it);
_lastPager = wrappedPager;
loadedResult(wrappedPager.getWindow());
}
.exception<ScriptCaptchaRequiredException> { } .exception<ScriptCaptchaRequiredException> { }
.exception<ScriptExecutionException> { .exception<ScriptExecutionException> {
Logger.w(ChannelFragment.TAG, "Plugin failure.", it); Logger.w(ChannelFragment.TAG, "Plugin failure.", it);
@ -208,21 +228,81 @@ class HomeFragment : MainFragment() {
private val _filterLock = Object(); private val _filterLock = Object();
private var _toggleRecent = false; private var _toggleRecent = false;
private var _toggleWatched = false;
private var _togglePluginsDisabled = mutableListOf<String>();
private var _togglesConfig = FragmentedStorage.get<StringArrayStorage>("home_toggles");
fun initializeToolbarContent() { fun initializeToolbarContent() {
//Not stable enough with current viewport paging, doesn't work with less results, and reloads content instead of just re-filtering existing if(_toolbarContentView.allViews.any { it is ToggleBar })
/* _toolbarContentView.removeView(_toolbarContentView.allViews.find { it is ToggleBar });
_toggleBar = ToggleBar(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
synchronized(_filterLock) {
_toggleBar?.setToggles(
//TODO: loadResults needs to be replaced with an internal reload of the current content
ToggleBar.Toggle("Recent", _toggleRecent) { _toggleRecent = it; loadResults(false) }
)
}
_toolbarContentView.addView(_toggleBar, 0); if(Settings.instance.home.showHomeFilters) {
*/
if (!_togglesConfig.any()) {
_togglesConfig.set("today", "watched", "plugins");
_togglesConfig.save();
}
_toggleBar = ToggleBar(context).apply {
layoutParams =
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
_togglePluginsDisabled.clear();
synchronized(_filterLock) {
val buttonsPlugins = (if (_togglesConfig.contains("plugins"))
(StatePlatform.instance.getEnabledClients()
.map { plugin ->
ToggleBar.Toggle(plugin.name, plugin.icon, true, {
if (it) {
if (_togglePluginsDisabled.contains(plugin.id))
_togglePluginsDisabled.remove(plugin.id);
} else {
if (!_togglePluginsDisabled.contains(plugin.id))
_togglePluginsDisabled.add(plugin.id);
}
reloadForFilters();
}).withTag("plugins")
})
else listOf())
val buttons = (listOf<ToggleBar.Toggle?>(
(if (_togglesConfig.contains("today"))
ToggleBar.Toggle("Today", _toggleRecent) {
_toggleRecent = it; reloadForFilters()
}
.withTag("today") else null),
(if (_togglesConfig.contains("watched"))
ToggleBar.Toggle("Unwatched", _toggleWatched) {
_toggleWatched = it; reloadForFilters()
}
.withTag("watched") else null),
).filterNotNull() + buttonsPlugins)
.sortedBy { _togglesConfig.indexOf(it.tag ?: "") } ?: listOf()
val buttonSettings = ToggleBar.Toggle("", R.drawable.ic_settings, true, {
showOrderOverlay(_overlayContainer,
"Visible home filters",
listOf(
Pair("Plugins", "plugins"),
Pair("Today", "today"),
Pair("Watched", "watched")
),
{
val newArray = it.map { it.toString() }.toTypedArray();
_togglesConfig.set(*(if (newArray.any()) newArray else arrayOf("none")));
_togglesConfig.save();
initializeToolbarContent();
},
"Select which toggles you want to see in order. You can also choose to hide filters in the Grayjay Settings"
);
}).asButton();
val buttonsOrder = (buttons + listOf(buttonSettings)).toTypedArray();
_toggleBar?.setToggles(*buttonsOrder);
}
_toolbarContentView.addView(_toggleBar, 0);
}
}
fun reloadForFilters() {
_lastPager?.let { loadedResult(it.getWindow()) };
} }
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> { override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
@ -232,7 +312,11 @@ class HomeFragment : MainFragment() {
if(StateMeta.instance.isCreatorHidden(it.author.url)) if(StateMeta.instance.isCreatorHidden(it.author.url))
return@filter false; return@filter false;
if(_toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 23) { if(_toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 25)
return@filter false;
if(_toggleWatched && StateHistory.instance.isHistoryWatched(it.url, 0))
return@filter false;
if(_togglePluginsDisabled.any() && it.id.pluginId != null && _togglePluginsDisabled.contains(it.id.pluginId)) {
return@filter false; return@filter false;
} }

View File

@ -19,10 +19,10 @@ import kotlinx.serialization.json.jsonPrimitive
class PlatformContentSerializer : JsonContentPolymorphicSerializer<SerializedPlatformContent>(SerializedPlatformContent::class) { class PlatformContentSerializer : JsonContentPolymorphicSerializer<SerializedPlatformContent>(SerializedPlatformContent::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<SerializedPlatformContent> { override fun selectDeserializer(element: JsonElement): DeserializationStrategy<SerializedPlatformContent> {
val obj = element.jsonObject["contentType"]; val obj = element.jsonObject["contentType"] ?: element.jsonObject["ContentType"];
//TODO: Remove this temporary fallback..at some point //TODO: Remove this temporary fallback..at some point
if(obj == null && element.jsonObject["isLive"]?.jsonPrimitive?.booleanOrNull != null) if(obj == null && (element.jsonObject["isLive"]?.jsonPrimitive?.booleanOrNull ?: element.jsonObject["IsLive"]?.jsonPrimitive?.booleanOrNull) != null)
return SerializedPlatformVideo.serializer(); return SerializedPlatformVideo.serializer();
if(obj?.jsonPrimitive?.isString != false) { if(obj?.jsonPrimitive?.isString != false) {

View File

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

View File

@ -41,4 +41,19 @@ class StringArrayStorage : FragmentedStorageFileJson() {
return values.toList(); return values.toList();
} }
} }
fun any(): Boolean {
synchronized(values) {
return values.any();
}
}
fun contains(v: String): Boolean {
synchronized(values) {
return values.contains(v);
}
}
fun indexOf(v: String): Int {
synchronized(values){
return values.indexOf(v);
}
}
} }

View File

@ -153,6 +153,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
*resolves *resolves
); );
if (resolve != null) { if (resolve != null) {
val invalids = resolve.filter { it.content.any { it.datetime == null } };
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size})") UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size})")
for(result in resolve){ for(result in resolve){
val task = providedTasks?.find { it.url == result.channelUrl }; val task = providedTasks?.find { it.url == result.channelUrl };

View File

@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import com.futo.platformplayer.serializers.OffsetDateTimeStringSerializer
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -12,12 +13,12 @@ import java.time.OffsetDateTime
@Serializable @Serializable
class ChannelResult( class ChannelResult(
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class) @kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
@SerialName("DateTime") @SerialName("dateTime")
var dateTime: OffsetDateTime, var dateTime: OffsetDateTime,
@SerialName("ChannelUrl") @SerialName("channelUrl")
var channelUrl: String, var channelUrl: String,
@SerialName("Content") @SerialName("content")
var content: List<SerializedPlatformContent>, var content: List<SerializedPlatformContent>,
@SerialName("Channel") @SerialName("channel")
var channel: IPlatformChannel? = null var channel: IPlatformChannel? = null
) )

View File

@ -52,6 +52,7 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
fun resolveContract(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> { fun resolveContract(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
val contractResolve = convertResolves(*resolves) val contractResolve = convertResolves(*resolves)
val result = post("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json") val result = post("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json")
Logger.v("SubsExchangeClient", "Resolve:" + result);
return Serializer.json.decodeFromString(result) return Serializer.json.decodeFromString(result)
} }
suspend fun resolveContractAsync(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> { suspend fun resolveContractAsync(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {

View File

@ -12,6 +12,7 @@ import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.channels.SerializedChannel import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.states.StateSubscriptionGroups import com.futo.platformplayer.states.StateSubscriptionGroups
@ -46,7 +47,12 @@ class ToggleBar : LinearLayout {
_tagsContainer.removeAllViews(); _tagsContainer.removeAllViews();
for(button in buttons) { for(button in buttons) {
_tagsContainer.addView(ToggleTagView(context).apply { _tagsContainer.addView(ToggleTagView(context).apply {
this.setInfo(button.name, button.isActive); if(button.icon > 0)
this.setInfo(button.icon, button.name, button.isActive, button.isButton);
else if(button.iconVariable != null)
this.setInfo(button.iconVariable, button.name, button.isActive, button.isButton);
else
this.setInfo(button.name, button.isActive, button.isButton);
this.onClick.subscribe { button.action(it); }; this.onClick.subscribe { button.action(it); };
}); });
} }
@ -55,20 +61,42 @@ class ToggleBar : LinearLayout {
class Toggle { class Toggle {
val name: String; val name: String;
val icon: Int; val icon: Int;
val iconVariable: ImageVariable?;
val action: (Boolean)->Unit; val action: (Boolean)->Unit;
val isActive: Boolean; val isActive: Boolean;
var isButton: Boolean = false
private set;
var tag: String? = null;
constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (Boolean)->Unit) {
this.name = name;
this.icon = 0;
this.iconVariable = icon;
this.action = action;
this.isActive = isActive;
}
constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) { constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) {
this.name = name; this.name = name;
this.icon = icon; this.icon = icon;
this.iconVariable = null;
this.action = action; this.action = action;
this.isActive = isActive; this.isActive = isActive;
} }
constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) { constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) {
this.name = name; this.name = name;
this.icon = 0; this.icon = 0;
this.iconVariable = null;
this.action = action; this.action = action;
this.isActive = isActive; this.isActive = isActive;
} }
fun asButton(): Toggle{
isButton = true;
return this;
}
fun withTag(str: String): Toggle {
tag = str;
return this;
}
} }
} }

View File

@ -4,19 +4,27 @@ import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.images.GlideHelper
import com.futo.platformplayer.models.ImageVariable
class ToggleTagView : LinearLayout { class ToggleTagView : LinearLayout {
private val _root: FrameLayout; private val _root: FrameLayout;
private val _textTag: TextView; private val _textTag: TextView;
private var _text: String = ""; private var _text: String = "";
private var _image: ImageView;
var isActive: Boolean = false var isActive: Boolean = false
private set; private set;
var isButton: Boolean = false
private set;
var onClick = Event1<Boolean>(); var onClick = Event1<Boolean>();
@ -24,7 +32,12 @@ class ToggleTagView : LinearLayout {
LayoutInflater.from(context).inflate(R.layout.view_toggle_tag, this, true); LayoutInflater.from(context).inflate(R.layout.view_toggle_tag, this, true);
_root = findViewById(R.id.root); _root = findViewById(R.id.root);
_textTag = findViewById(R.id.text_tag); _textTag = findViewById(R.id.text_tag);
_root.setOnClickListener { setToggle(!isActive); onClick.emit(isActive); } _image = findViewById(R.id.image_tag);
_root.setOnClickListener {
if(!isButton)
setToggle(!isActive);
onClick.emit(isActive);
}
} }
fun setToggle(isActive: Boolean) { fun setToggle(isActive: Boolean) {
@ -39,9 +52,27 @@ class ToggleTagView : LinearLayout {
} }
} }
fun setInfo(text: String, isActive: Boolean) { fun setInfo(imageResource: Int, text: String, isActive: Boolean, isButton: Boolean = false) {
_text = text; _text = text;
_textTag.text = text; _textTag.text = text;
setToggle(isActive); setToggle(isActive);
_image.setImageResource(imageResource);
_image.visibility = View.VISIBLE;
this.isButton = isButton;
}
fun setInfo(image: ImageVariable, text: String, isActive: Boolean, isButton: Boolean = false) {
_text = text;
_textTag.text = text;
setToggle(isActive);
image.setImageView(_image, R.drawable.ic_error_pred);
_image.visibility = View.VISIBLE;
this.isButton = isButton;
}
fun setInfo(text: String, isActive: Boolean, isButton: Boolean = false) {
_image.visibility = View.GONE;
_text = text;
_textTag.text = text;
setToggle(isActive);
this.isButton = isButton;
} }
} }

View File

@ -113,6 +113,13 @@ class SlideUpMenuOverlay : RelativeLayout {
_textOK.visibility = View.VISIBLE; _textOK.visibility = View.VISIBLE;
} }
} }
fun getSlideUpItemByTag(itemTag: Any?): SlideUpMenuItem? {
for(view in groupItems){
if(view is SlideUpMenuItem && view.itemTag == itemTag)
return view;
}
return null;
}
fun selectOption(groupTag: Any?, itemTag: Any?, multiSelect: Boolean = false, toggle: Boolean = false): Boolean { fun selectOption(groupTag: Any?, itemTag: Any?, multiSelect: Boolean = false, toggle: Boolean = false): Boolean {
var didSelect = false; var didSelect = false;

View File

@ -3,14 +3,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<ScrollView <HorizontalScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="horizontal"> android:scrollbars="horizontal">
<LinearLayout <LinearLayout
android:id="@+id/container_tags" android:id="@+id/container_tags"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" /> android:orientation="horizontal" />
</ScrollView> </HorizontalScrollView>
</LinearLayout> </LinearLayout>

View File

@ -10,16 +10,27 @@
android:layout_marginTop="17dp" android:layout_marginTop="17dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:id="@+id/root"> android:id="@+id/root">
<LinearLayout
<TextView
android:id="@+id/text_tag"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:textColor="@color/white" android:orientation="horizontal">
android:layout_gravity="center" <ImageView
android:gravity="center" android:id="@+id/image_tag"
android:textSize="11dp" android:visibility="gone"
android:fontFamily="@font/inter_light" android:layout_width="24dp"
tools:text="Tag text" /> android:layout_height="24dp"
android:layout_marginRight="5dp"
android:layout_marginTop="4dp" />
<TextView
android:id="@+id/text_tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:layout_gravity="center"
android:gravity="center"
android:textSize="11dp"
android:fontFamily="@font/inter_light"
tools:text="Tag text" />
</LinearLayout>
</FrameLayout> </FrameLayout>

View File

@ -417,6 +417,8 @@
<string name="show_subscription_group_description">If subscription groups should be shown above your subscriptions to filter</string> <string name="show_subscription_group_description">If subscription groups should be shown above your subscriptions to filter</string>
<string name="preview_feed_items">Preview Feed Items</string> <string name="preview_feed_items">Preview Feed Items</string>
<string name="preview_feed_items_description">When the preview feedstyle is used, if items should auto-preview when scrolling over them</string> <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="log_level">Log Level</string> <string name="log_level">Log Level</string>
<string name="logging">Logging</string> <string name="logging">Logging</string>
<string name="sync_grayjay">Sync Grayjay</string> <string name="sync_grayjay">Sync Grayjay</string>