mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-06 00:00:10 +00:00
Subscriptions fetching support for subgroups
This commit is contained in:
parent
76f5962232
commit
b8acd0b5b2
10 changed files with 437 additions and 225 deletions
|
@ -321,7 +321,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
_fragDownloads.topBar = _fragTopBarGeneral;
|
_fragDownloads.topBar = _fragTopBarGeneral;
|
||||||
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
||||||
_fragImportPlaylists.topBar = _fragTopBarImport;
|
_fragImportPlaylists.topBar = _fragTopBarImport;
|
||||||
_fragSubGroup.topBar = _fragTopBarNavigation;
|
|
||||||
_fragSubGroupList.topBar = _fragTopBarAdd;
|
_fragSubGroupList.topBar = _fragTopBarAdd;
|
||||||
|
|
||||||
_fragBrowser.topBar = _fragTopBarNavigation;
|
_fragBrowser.topBar = _fragTopBarNavigation;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.Button
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
@ -20,6 +21,7 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.UISlideOverlays
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
|
@ -32,7 +34,9 @@ import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
import com.futo.platformplayer.views.SearchView
|
import com.futo.platformplayer.views.SearchView
|
||||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
|
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
|
||||||
|
import com.futo.platformplayer.views.overlays.CreatorSelectOverlay
|
||||||
import com.futo.platformplayer.views.overlays.ImageVariableOverlay
|
import com.futo.platformplayer.views.overlays.ImageVariableOverlay
|
||||||
|
import com.futo.platformplayer.views.overlays.OverlayTopbar
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.shape.CornerFamily
|
import com.google.android.material.shape.CornerFamily
|
||||||
|
@ -69,6 +73,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||||
private class SubscriptionGroupView: ConstraintLayout {
|
private class SubscriptionGroupView: ConstraintLayout {
|
||||||
private val _fragment: SubscriptionGroupFragment;
|
private val _fragment: SubscriptionGroupFragment;
|
||||||
|
|
||||||
|
private val _topbar: OverlayTopbar;
|
||||||
private val _textGroupTitleContainer: LinearLayout;
|
private val _textGroupTitleContainer: LinearLayout;
|
||||||
private val _textGroupTitle: TextView;
|
private val _textGroupTitle: TextView;
|
||||||
private val _imageGroup: ShapeableImageView;
|
private val _imageGroup: ShapeableImageView;
|
||||||
|
@ -81,16 +86,12 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||||
private val _buttonSettings: ImageButton;
|
private val _buttonSettings: ImageButton;
|
||||||
private val _buttonDelete: ImageButton;
|
private val _buttonDelete: ImageButton;
|
||||||
|
|
||||||
private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
|
private val _buttonAddCreator: Button;
|
||||||
private val _disabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
|
|
||||||
private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
|
|
||||||
private val _disabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
|
|
||||||
|
|
||||||
private val _containerEnabled: LinearLayout;
|
private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
|
||||||
private val _containerDisabled: LinearLayout;
|
private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
|
||||||
|
|
||||||
private val _recyclerCreatorsEnabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
|
private val _recyclerCreatorsEnabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
|
||||||
private val _recyclerCreatorsDisabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
|
|
||||||
|
|
||||||
private val _overlay: FrameLayout;
|
private val _overlay: FrameLayout;
|
||||||
|
|
||||||
|
@ -101,6 +102,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||||
_fragment = fragment;
|
_fragment = fragment;
|
||||||
|
|
||||||
_overlay = findViewById(R.id.overlay);
|
_overlay = findViewById(R.id.overlay);
|
||||||
|
_topbar = findViewById(R.id.topbar);
|
||||||
_searchBar = findViewById(R.id.search_bar);
|
_searchBar = findViewById(R.id.search_bar);
|
||||||
_textGroupTitleContainer = findViewById(R.id.text_group_title_container);
|
_textGroupTitleContainer = findViewById(R.id.text_group_title_container);
|
||||||
_textGroupTitle = findViewById(R.id.text_group_title);
|
_textGroupTitle = findViewById(R.id.text_group_title);
|
||||||
|
@ -110,33 +112,50 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||||
_textGroupMeta = findViewById(R.id.text_group_meta);
|
_textGroupMeta = findViewById(R.id.text_group_meta);
|
||||||
_buttonSettings = findViewById(R.id.button_settings);
|
_buttonSettings = findViewById(R.id.button_settings);
|
||||||
_buttonDelete = findViewById(R.id.button_delete);
|
_buttonDelete = findViewById(R.id.button_delete);
|
||||||
|
_buttonAddCreator = findViewById(R.id.button_creator_add);
|
||||||
_imageGroup.setBackgroundColor(Color.GRAY);
|
_imageGroup.setBackgroundColor(Color.GRAY);
|
||||||
|
|
||||||
|
_topbar.onClose.subscribe {
|
||||||
|
fragment.close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_buttonAddCreator.setOnClickListener {
|
||||||
|
addCreators();
|
||||||
|
}
|
||||||
|
|
||||||
val dp6 = 6.dp(resources);
|
val dp6 = 6.dp(resources);
|
||||||
_imageGroup.shapeAppearanceModel = ShapeAppearanceModel.builder()
|
_imageGroup.shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||||
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
|
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
_containerEnabled = findViewById(R.id.container_enabled);
|
|
||||||
_containerDisabled = findViewById(R.id.container_disabled);
|
|
||||||
_recyclerCreatorsEnabled = findViewById<RecyclerView>(R.id.recycler_creators_enabled).asAny(_enabledCreatorsFiltered) {
|
_recyclerCreatorsEnabled = findViewById<RecyclerView>(R.id.recycler_creators_enabled).asAny(_enabledCreatorsFiltered) {
|
||||||
it.itemView.setPadding(0, dp6, 0, dp6);
|
it.itemView.setPadding(0, dp6, 0, dp6);
|
||||||
it.onClick.subscribe { channel ->
|
it.onClick.subscribe { channel ->
|
||||||
disableCreator(channel);
|
//disableCreator(channel);
|
||||||
|
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete", "Are you sure you want to delete\n[${channel.name}]?", null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Delete", {
|
||||||
|
_group?.let {
|
||||||
|
it.urls.remove(channel.url);
|
||||||
|
reloadCreators(it);
|
||||||
|
}
|
||||||
|
}, UIDialogs.ActionStyle.DANGEROUS))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
_recyclerCreatorsDisabled = findViewById<RecyclerView>(R.id.recycler_creators_disabled).asAny(_disabledCreatorsFiltered) {
|
_recyclerCreatorsDisabled = findViewById<RecyclerView>(R.id.recycler_creators_disabled).asAny(_disabledCreatorsFiltered) {
|
||||||
it.itemView.setPadding(0, dp6, 0, dp6);
|
it.itemView.setPadding(0, dp6, 0, dp6);
|
||||||
it.onClick.subscribe { channel ->
|
it.onClick.subscribe { channel ->
|
||||||
enableCreator(channel);
|
enableCreator(channel);
|
||||||
};
|
};
|
||||||
}
|
}*/
|
||||||
_recyclerCreatorsEnabled.view.layoutManager = GridLayoutManager(context, 5).apply {
|
_recyclerCreatorsEnabled.view.layoutManager = GridLayoutManager(context, 5).apply {
|
||||||
this.orientation = LinearLayoutManager.VERTICAL;
|
this.orientation = LinearLayoutManager.VERTICAL;
|
||||||
};
|
};
|
||||||
|
/*
|
||||||
_recyclerCreatorsDisabled.view.layoutManager = GridLayoutManager(context, 5).apply {
|
_recyclerCreatorsDisabled.view.layoutManager = GridLayoutManager(context, 5).apply {
|
||||||
this.orientation = LinearLayoutManager.VERTICAL;
|
this.orientation = LinearLayoutManager.VERTICAL;
|
||||||
};
|
};*/
|
||||||
|
|
||||||
_textGroupTitleContainer.setOnClickListener {
|
_textGroupTitleContainer.setOnClickListener {
|
||||||
_group?.let { editName(it) };
|
_group?.let { editName(it) };
|
||||||
|
@ -154,10 +173,14 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||||
|
|
||||||
}
|
}
|
||||||
_buttonDelete.setOnClickListener {
|
_buttonDelete.setOnClickListener {
|
||||||
_group?.let {
|
_group?.let { g ->
|
||||||
StateSubscriptionGroups.instance.deleteSubscriptionGroup(it.id);
|
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${g.name}]?", null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Delete", {
|
||||||
|
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id);
|
||||||
|
fragment.close(true);
|
||||||
|
}, UIDialogs.ActionStyle.DANGEROUS))
|
||||||
};
|
};
|
||||||
fragment.close(true);
|
|
||||||
}
|
}
|
||||||
_buttonSettings.visibility = View.GONE;
|
_buttonSettings.visibility = View.GONE;
|
||||||
|
|
||||||
|
@ -208,6 +231,28 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||||
overlay.removeAllViews();
|
overlay.removeAllViews();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun addCreators() {
|
||||||
|
val overlay = CreatorSelectOverlay(context, _enabledCreators.map { it.url });
|
||||||
|
_overlay.removeAllViews();
|
||||||
|
_overlay.addView(overlay);
|
||||||
|
_overlay.alpha = 0f
|
||||||
|
_overlay.visibility = View.VISIBLE;
|
||||||
|
_overlay.animate().alpha(1f).setDuration(300).start();
|
||||||
|
overlay.onSelected.subscribe {
|
||||||
|
_group?.let { g ->
|
||||||
|
for(url in it) {
|
||||||
|
if(!g.urls.contains(url))
|
||||||
|
g.urls.add(url);
|
||||||
|
}
|
||||||
|
save();
|
||||||
|
reloadCreators(g);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
overlay.onClose.subscribe {
|
||||||
|
_overlay.visibility = View.GONE;
|
||||||
|
overlay.removeAllViews();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun setGroup(group: SubscriptionGroup?) {
|
fun setGroup(group: SubscriptionGroup?) {
|
||||||
|
@ -230,73 +275,30 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun reloadCreators(group: SubscriptionGroup?) {
|
private fun reloadCreators(group: SubscriptionGroup?) {
|
||||||
_enabledCreators.clear();
|
_enabledCreators.clear();
|
||||||
_disabledCreators.clear();
|
//_disabledCreators.clear();
|
||||||
|
|
||||||
if(group != null) {
|
if(group != null) {
|
||||||
val urls = group.urls.toList();
|
val urls = group.urls.toList();
|
||||||
val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel }
|
val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel }
|
||||||
_enabledCreators.addAll(subs.filter { urls.contains(it.url) });
|
_enabledCreators.addAll(subs.filter { urls.contains(it.url) });
|
||||||
_disabledCreators.addAll(subs.filter { !urls.contains(it.url) });
|
|
||||||
}
|
}
|
||||||
|
updateMeta();
|
||||||
filterCreators();
|
filterCreators();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterCreators() {
|
private fun filterCreators() {
|
||||||
val query = _searchBar.textSearch.text.toString().lowercase();
|
val query = _searchBar.textSearch.text.toString().lowercase();
|
||||||
val filteredEnabled = _enabledCreators.filter { it.name.lowercase().contains(query) };
|
val filteredEnabled = _enabledCreators.filter { it.name.lowercase().contains(query) };
|
||||||
val filteredDisabled = _disabledCreators.filter { it.name.lowercase().contains(query) };
|
|
||||||
|
|
||||||
//Optimize
|
//Optimize
|
||||||
_enabledCreatorsFiltered.clear();
|
_enabledCreatorsFiltered.clear();
|
||||||
_enabledCreatorsFiltered.addAll(filteredEnabled);
|
_enabledCreatorsFiltered.addAll(filteredEnabled);
|
||||||
_disabledCreatorsFiltered.clear();
|
|
||||||
_disabledCreatorsFiltered.addAll(filteredDisabled);
|
|
||||||
|
|
||||||
_recyclerCreatorsEnabled.notifyContentChanged();
|
_recyclerCreatorsEnabled.notifyContentChanged();
|
||||||
_recyclerCreatorsDisabled.notifyContentChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enableCreator(channel: IPlatformChannel) {
|
|
||||||
val index = _disabledCreatorsFiltered.indexOf(channel);
|
|
||||||
if (index >= 0) {
|
|
||||||
_disabledCreators.remove(channel)
|
|
||||||
_disabledCreatorsFiltered.remove(channel);
|
|
||||||
_recyclerCreatorsDisabled.adapter.notifyItemRangeRemoved(index);
|
|
||||||
|
|
||||||
_enabledCreators.add(channel);
|
|
||||||
_enabledCreatorsFiltered.add(channel);
|
|
||||||
_recyclerCreatorsEnabled.adapter.notifyItemInserted(_enabledCreatorsFiltered.size - 1);
|
|
||||||
|
|
||||||
_group?.let {
|
|
||||||
if(!it.urls.contains(channel.url)) {
|
|
||||||
it.urls.add(channel.url);
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateMeta();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun disableCreator(channel: IPlatformChannel) {
|
|
||||||
val index = _enabledCreatorsFiltered.indexOf(channel);
|
|
||||||
if (index >= 0) {
|
|
||||||
_enabledCreators.remove(channel)
|
|
||||||
_enabledCreatorsFiltered.removeAt(index);
|
|
||||||
_recyclerCreatorsEnabled.adapter.notifyItemRangeRemoved(index);
|
|
||||||
|
|
||||||
_disabledCreators.add(channel);
|
|
||||||
_disabledCreatorsFiltered.add(channel);
|
|
||||||
_recyclerCreatorsDisabled.adapter.notifyItemInserted(_disabledCreatorsFiltered.size - 1);
|
|
||||||
|
|
||||||
_group?.let {
|
|
||||||
it.urls.remove(channel.url);
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
updateMeta();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMeta() {
|
private fun updateMeta() {
|
||||||
_textGroupMeta.text = "${_enabledCreators.size} creators";
|
_textGroupMeta.text = "${_group?.urls?.size} creators";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -104,7 +104,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
|
|
||||||
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||||
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
||||||
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->
|
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total ->
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
setProgress(progress, total);
|
setProgress(progress, total);
|
||||||
|
@ -162,14 +162,14 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StateSubscriptions.instance.isGlobalUpdating) {
|
if (!StateSubscriptions.instance.global.isGlobalUpdating) {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanup() {
|
override fun cleanup() {
|
||||||
super.cleanup()
|
super.cleanup()
|
||||||
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.remove(this);
|
StateSubscriptions.instance.global.onUpdateProgress.remove(this);
|
||||||
StateSubscriptions.instance.onSubscriptionsChanged.remove(this);
|
StateSubscriptions.instance.onSubscriptionsChanged.remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,8 +194,9 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
private var _bypassRateLimit = false;
|
private var _bypassRateLimit = false;
|
||||||
private val _lastExceptions: List<Throwable>? = null;
|
private val _lastExceptions: List<Throwable>? = null;
|
||||||
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
|
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
|
||||||
|
val group = _subGroup;
|
||||||
if(!_bypassRateLimit) {
|
if(!_bypassRateLimit) {
|
||||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
|
||||||
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
|
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
|
||||||
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true }
|
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true }
|
||||||
Logger.w(TAG, "Trying to refreshing subscriptions with requests:\n" + reqCountStr);
|
Logger.w(TAG, "Trying to refreshing subscriptions with requests:\n" + reqCountStr);
|
||||||
|
@ -203,9 +204,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
||||||
}
|
}
|
||||||
_bypassRateLimit = false;
|
_bypassRateLimit = false;
|
||||||
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh);
|
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh, group);
|
||||||
|
val feed = StateSubscriptions.instance.getFeed(group?.id);
|
||||||
|
|
||||||
val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions;
|
val currentExs = feed?.exceptions ?: listOf();
|
||||||
if(currentExs != _lastExceptions && currentExs.any())
|
if(currentExs != _lastExceptions && currentExs.any())
|
||||||
handleExceptions(currentExs);
|
handleExceptions(currentExs);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.futo.platformplayer.functional
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.structures.ReusablePager
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
|
||||||
|
//TODO: Integrate this better?
|
||||||
|
class CentralizedFeed {
|
||||||
|
var lock = Object();
|
||||||
|
var feed: ReusablePager<IPlatformContent>? = null;
|
||||||
|
var isGlobalUpdating: Boolean = false;
|
||||||
|
var exceptions: List<Throwable> = listOf();
|
||||||
|
|
||||||
|
|
||||||
|
var lastProgress: Int = 0;
|
||||||
|
var lastTotal: Int = 0;
|
||||||
|
val onUpdateProgress = Event2<Int, Int>();
|
||||||
|
val onUpdated = Event0();
|
||||||
|
val onUpdatedOnce = Event1<Throwable?>();
|
||||||
|
val onException = Event1<List<Throwable>>();
|
||||||
|
}
|
|
@ -48,11 +48,16 @@ class Subscription {
|
||||||
var playbackSeconds: Int = 0;
|
var playbackSeconds: Int = 0;
|
||||||
var playbackViews: Int = 0;
|
var playbackViews: Int = 0;
|
||||||
|
|
||||||
|
var isOther = false;
|
||||||
|
|
||||||
constructor(channel : SerializedChannel) {
|
constructor(channel : SerializedChannel) {
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isChannel(url: String): Boolean {
|
||||||
|
return channel.url == url || channel.urlAlternatives.contains(url);
|
||||||
|
}
|
||||||
|
|
||||||
fun shouldFetchVideos() = doFetchVideos &&
|
fun shouldFetchVideos() = doFetchVideos &&
|
||||||
(lastVideo.getNowDiffDays() < 30 || lastVideoUpdate.getNowDiffDays() >= 1) &&
|
(lastVideo.getNowDiffDays() < 30 || lastVideoUpdate.getNowDiffDays() >= 1) &&
|
||||||
(lastVideo.getNowDiffDays() < 180 || lastVideoUpdate.getNowDiffDays() >= 3);
|
(lastVideo.getNowDiffDays() < 180 || lastVideoUpdate.getNowDiffDays() >= 3);
|
||||||
|
@ -63,10 +68,16 @@ class Subscription {
|
||||||
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
||||||
|
|
||||||
fun save() {
|
fun save() {
|
||||||
StateSubscriptions.instance.saveSubscription(this);
|
if(isOther)
|
||||||
|
StateSubscriptions.instance.saveSubscriptionOther(this);
|
||||||
|
else
|
||||||
|
StateSubscriptions.instance.saveSubscription(this);
|
||||||
}
|
}
|
||||||
fun saveAsync() {
|
fun saveAsync() {
|
||||||
StateSubscriptions.instance.saveSubscription(this);
|
if(isOther)
|
||||||
|
StateSubscriptions.instance.saveSubscriptionOtherAsync(this);
|
||||||
|
else
|
||||||
|
StateSubscriptions.instance.saveSubscriptionAsync(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChannel(channel: IPlatformChannel) {
|
fun updateChannel(channel: IPlatformChannel) {
|
||||||
|
|
|
@ -1,27 +1,18 @@
|
||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
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.platforms.js.SourcePluginConfig
|
|
||||||
import com.futo.platformplayer.api.media.structures.*
|
import com.futo.platformplayer.api.media.structures.*
|
||||||
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
||||||
import com.futo.platformplayer.constructs.Event0
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.functional.CentralizedFeed
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
|
||||||
import com.futo.platformplayer.findNonRuntimeException
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.getNowDiffDays
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
@ -32,15 +23,10 @@ import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
|
||||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ExecutionException
|
|
||||||
import java.util.concurrent.ForkJoinPool
|
import java.util.concurrent.ForkJoinPool
|
||||||
import java.util.concurrent.ForkJoinTask
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import kotlin.streams.asSequence
|
import kotlin.streams.asSequence
|
||||||
import kotlin.streams.toList
|
|
||||||
import kotlin.system.measureTimeMillis
|
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Used to maintain subscriptions
|
* Used to maintain subscriptions
|
||||||
|
@ -54,25 +40,17 @@ class StateSubscriptions {
|
||||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Subscription =
|
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Subscription =
|
||||||
Subscription(SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false)));
|
Subscription(SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false)));
|
||||||
}).load();
|
}).load();
|
||||||
|
private val _subscriptionOthers = FragmentedStorage.storeJson<Subscription>("subscriptions_others")
|
||||||
|
.withUnique { it.channel.url }
|
||||||
|
.load();
|
||||||
private val _subscriptionsPool = ForkJoinPool(Settings.instance.subscriptions.getSubscriptionsConcurrency());
|
private val _subscriptionsPool = ForkJoinPool(Settings.instance.subscriptions.getSubscriptionsConcurrency());
|
||||||
private val _legacySubscriptions = FragmentedStorage.get<SubscriptionStorage>();
|
private val _legacySubscriptions = FragmentedStorage.get<SubscriptionStorage>();
|
||||||
|
|
||||||
|
|
||||||
private var _globalSubscriptionsLock = Object();
|
|
||||||
private var _globalSubscriptionFeed: ReusablePager<IPlatformContent>? = null;
|
|
||||||
var isGlobalUpdating: Boolean = false
|
|
||||||
private set;
|
|
||||||
var globalSubscriptionExceptions: List<Throwable> = listOf()
|
|
||||||
private set;
|
|
||||||
|
|
||||||
private val _algorithmSubscriptions = SubscriptionFetchAlgorithms.SMART;
|
private val _algorithmSubscriptions = SubscriptionFetchAlgorithms.SMART;
|
||||||
|
|
||||||
private var _lastGlobalSubscriptionProgress: Int = 0;
|
val global: CentralizedFeed = CentralizedFeed();
|
||||||
private var _lastGlobalSubscriptionTotal: Int = 0;
|
val feeds: HashMap<String, CentralizedFeed> = hashMapOf();
|
||||||
val onGlobalSubscriptionsUpdateProgress = Event2<Int, Int>();
|
|
||||||
val onGlobalSubscriptionsUpdated = Event0();
|
|
||||||
val onGlobalSubscriptionsUpdatedOnce = Event1<Throwable?>();
|
|
||||||
val onGlobalSubscriptionsException = Event1<List<Throwable>>();
|
|
||||||
|
|
||||||
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
||||||
|
|
||||||
|
@ -83,75 +61,95 @@ class StateSubscriptions {
|
||||||
else
|
else
|
||||||
return subs.minOf { it.lastVideoUpdate };
|
return subs.minOf { it.lastVideoUpdate };
|
||||||
}
|
}
|
||||||
fun getGlobalSubscriptionProgress(): Pair<Int, Int> {
|
|
||||||
return Pair(_lastGlobalSubscriptionProgress, _lastGlobalSubscriptionTotal);
|
fun getFeed(id: String? = null, createIfNew: Boolean = false): CentralizedFeed? {
|
||||||
|
if(id == null)
|
||||||
|
return global;
|
||||||
|
else {
|
||||||
|
return synchronized(feeds) {
|
||||||
|
var f = feeds[id];
|
||||||
|
if(f == null && createIfNew) {
|
||||||
|
f = CentralizedFeed();
|
||||||
|
feeds[id] = f;
|
||||||
|
}
|
||||||
|
return@synchronized f;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun updateSubscriptionFeed(scope: CoroutineScope, onlyIfNull: Boolean = false, onProgress: ((Int, Int)->Unit)? = null) {
|
|
||||||
|
fun getGlobalSubscriptionProgress(id: String? = null): Pair<Int, Int> {
|
||||||
|
val feed = getFeed(id, false) ?: return Pair(0, 0);
|
||||||
|
return Pair(feed.lastProgress, feed.lastTotal);
|
||||||
|
}
|
||||||
|
fun updateSubscriptionFeed(scope: CoroutineScope, onlyIfNull: Boolean = false, onProgress: ((Int, Int)->Unit)? = null, group: SubscriptionGroup? = null) {
|
||||||
|
val feed = getFeed(group?.id, true) ?: return;
|
||||||
Logger.v(TAG, "updateSubscriptionFeed");
|
Logger.v(TAG, "updateSubscriptionFeed");
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
synchronized(_globalSubscriptionsLock) {
|
synchronized(feed.lock) {
|
||||||
if (isGlobalUpdating || (onlyIfNull && _globalSubscriptionFeed != null)) {
|
if (feed.isGlobalUpdating || (onlyIfNull && feed.feed != null)) {
|
||||||
Logger.i(TAG, "Already updating subscriptions or not required")
|
Logger.i(TAG, "Already updating subscriptions or not required")
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
isGlobalUpdating = true;
|
feed.isGlobalUpdating = true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val subsResult = getSubscriptionsFeedWithExceptions(true, true, scope, { progress, total ->
|
val subsResult = getSubscriptionsFeedWithExceptions(true, true, scope, { progress, total ->
|
||||||
_lastGlobalSubscriptionProgress = progress;
|
feed.lastProgress = progress;
|
||||||
_lastGlobalSubscriptionTotal = total;
|
feed.lastTotal = total;
|
||||||
onGlobalSubscriptionsUpdateProgress.emit(progress, total);
|
feed.onUpdateProgress.emit(progress, total);
|
||||||
onProgress?.invoke(progress, total);
|
onProgress?.invoke(progress, total);
|
||||||
});
|
}, null, group);
|
||||||
if (subsResult.second.any()) {
|
if (subsResult.second.any()) {
|
||||||
globalSubscriptionExceptions = subsResult.second;
|
feed.exceptions = subsResult.second;
|
||||||
onGlobalSubscriptionsException.emit(subsResult.second);
|
feed.onException.emit(subsResult.second);
|
||||||
}
|
}
|
||||||
_globalSubscriptionFeed = subsResult.first.asReusable();
|
feed.feed = subsResult.first.asReusable();
|
||||||
synchronized(_globalSubscriptionsLock) {
|
synchronized(feed.lock) {
|
||||||
onGlobalSubscriptionsUpdated.emit();
|
feed.onUpdated.emit();
|
||||||
onGlobalSubscriptionsUpdatedOnce.emit(null);
|
feed.onUpdatedOnce.emit(null);
|
||||||
onGlobalSubscriptionsUpdatedOnce.clear();
|
feed.onUpdatedOnce.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e: Throwable) {
|
catch (e: Throwable) {
|
||||||
synchronized(_globalSubscriptionsLock) {
|
synchronized(feed.lock) {
|
||||||
onGlobalSubscriptionsUpdatedOnce.emit(e);
|
feed.onUpdatedOnce.emit(e);
|
||||||
onGlobalSubscriptionsUpdatedOnce.clear();
|
feed.onUpdatedOnce.clear();
|
||||||
}
|
}
|
||||||
Logger.e(TAG, "Failed to update subscription feed.", e);
|
Logger.e(TAG, "Failed to update subscription feed.", e);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
isGlobalUpdating = false;
|
feed.isGlobalUpdating = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
fun clearSubscriptionFeed() {
|
fun clearSubscriptionFeed(id: String? = null) {
|
||||||
synchronized(_globalSubscriptionsLock) {
|
val feed = getFeed(id) ?: return;
|
||||||
_globalSubscriptionFeed = null;
|
synchronized(feed.lock) {
|
||||||
|
feed.feed = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadIndex = 0;
|
private var loadIndex = 0;
|
||||||
suspend fun getGlobalSubscriptionFeed(scope: CoroutineScope, updated: Boolean): IPager<IPlatformContent> {
|
suspend fun getGlobalSubscriptionFeed(scope: CoroutineScope, updated: Boolean, group: SubscriptionGroup? = null): IPager<IPlatformContent> {
|
||||||
|
val feed = getFeed(group?.id, true) ?: return EmptyPager();
|
||||||
//Get Subscriptions only if null
|
//Get Subscriptions only if null
|
||||||
updateSubscriptionFeed(scope, !updated);
|
updateSubscriptionFeed(scope, !updated, null, group);
|
||||||
|
|
||||||
val evRef = Object();
|
val evRef = Object();
|
||||||
val result = suspendCoroutine {
|
val result = suspendCoroutine {
|
||||||
synchronized(_globalSubscriptionsLock) {
|
synchronized(feed.lock) {
|
||||||
if (_globalSubscriptionFeed != null && !updated) {
|
if (feed.feed != null && !updated) {
|
||||||
Logger.i(TAG, "Subscriptions got feed preloaded");
|
Logger.i(TAG, "Subscriptions got feed preloaded");
|
||||||
it.resumeWith(Result.success(_globalSubscriptionFeed!!.getWindow()));
|
it.resumeWith(Result.success(feed.feed!!.getWindow()));
|
||||||
} else {
|
} else {
|
||||||
val loadIndex = loadIndex++;
|
val loadIndex = loadIndex++;
|
||||||
Logger.i(TAG, "[${loadIndex}] Starting await update");
|
Logger.i(TAG, "[${loadIndex}] Starting await update");
|
||||||
onGlobalSubscriptionsUpdatedOnce.subscribe(evRef) {ex ->
|
feed.onUpdatedOnce.subscribe(evRef) { ex ->
|
||||||
Logger.i(TAG, "[${loadIndex}] Subscriptions got feed after update");
|
Logger.i(TAG, "[${loadIndex}] Subscriptions got feed after update");
|
||||||
if(ex != null)
|
if(ex != null)
|
||||||
it.resumeWithException(ex);
|
it.resumeWithException(ex);
|
||||||
else if (_globalSubscriptionFeed != null)
|
else if (feed.feed != null)
|
||||||
it.resumeWith(Result.success(_globalSubscriptionFeed!!.getWindow()));
|
it.resumeWith(Result.success(feed.feed!!.getWindow()));
|
||||||
else
|
else
|
||||||
it.resumeWithException(IllegalStateException("No subscription pager after change? Illegal null set on global subscriptions"))
|
it.resumeWithException(IllegalStateException("No subscription pager after change? Illegal null set on global subscriptions"))
|
||||||
}
|
}
|
||||||
|
@ -176,12 +174,35 @@ class StateSubscriptions {
|
||||||
return _subscriptions.findItem { it.channel.url == url || it.channel.urlAlternatives.contains(url) };
|
return _subscriptions.findItem { it.channel.url == url || it.channel.urlAlternatives.contains(url) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun getSubscriptionOther(url: String) : Subscription? {
|
||||||
|
synchronized(_subscriptionOthers) {
|
||||||
|
return _subscriptionOthers.findItem { it.isChannel(url)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getSubscriptionOtherOrCreate(url: String) : Subscription {
|
||||||
|
synchronized(_subscriptionOthers) {
|
||||||
|
val sub = getSubscriptionOther(url);
|
||||||
|
if(sub == null) {
|
||||||
|
val newSub = Subscription(SerializedChannel(PlatformID.NONE, url, null, null, 0, null, url, mapOf()));
|
||||||
|
newSub.isOther = true;
|
||||||
|
_subscriptions.save(newSub);
|
||||||
|
return newSub;
|
||||||
|
}
|
||||||
|
else return sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
fun saveSubscription(sub: Subscription) {
|
fun saveSubscription(sub: Subscription) {
|
||||||
_subscriptions.save(sub, false, true);
|
_subscriptions.save(sub, false, true);
|
||||||
}
|
}
|
||||||
fun saveSubscriptionAsync(sub: Subscription) {
|
fun saveSubscriptionAsync(sub: Subscription) {
|
||||||
_subscriptions.saveAsync(sub, false, true);
|
_subscriptions.saveAsync(sub, false, true);
|
||||||
}
|
}
|
||||||
|
fun saveSubscriptionOther(sub: Subscription) {
|
||||||
|
_subscriptionOthers.save(sub, false, true);
|
||||||
|
}
|
||||||
|
fun saveSubscriptionOtherAsync(sub: Subscription) {
|
||||||
|
_subscriptionOthers.saveAsync(sub, false, true);
|
||||||
|
}
|
||||||
fun getSubscriptionCount(): Int {
|
fun getSubscriptionCount(): Int {
|
||||||
synchronized(_subscriptions) {
|
synchronized(_subscriptions) {
|
||||||
return _subscriptions.getItems().size;
|
return _subscriptions.getItems().size;
|
||||||
|
@ -239,12 +260,19 @@ class StateSubscriptions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriptionRequestCount(): Map<JSClient, Int> {
|
fun getSubscriptionRequestCount(subGroup: SubscriptionGroup? = null): Map<JSClient, Int> {
|
||||||
|
val subs = getSubscriptions();
|
||||||
|
val emulatedSubs = subGroup?.let {
|
||||||
|
it.urls.map {url ->
|
||||||
|
subs.find { it.channel.url == url }
|
||||||
|
?: getSubscriptionOtherOrCreate(url);
|
||||||
|
};
|
||||||
|
} ?: subs;
|
||||||
return SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, StateApp.instance.scope)
|
return SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, StateApp.instance.scope)
|
||||||
.countRequests(getSubscriptions().associateWith { StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id, true) });
|
.countRequests(emulatedSubs.associateWith { StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id, true) });
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null, subGroup: SubscriptionGroup? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
||||||
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool);
|
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool);
|
||||||
if(onNewCacheHit != null)
|
if(onNewCacheHit != null)
|
||||||
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
||||||
|
@ -253,10 +281,19 @@ class StateSubscriptions {
|
||||||
onProgress?.invoke(progress, total);
|
onProgress?.invoke(progress, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val subs = getSubscriptions();
|
||||||
|
val emulatedSubs = subGroup?.let {
|
||||||
|
it.urls.map {url ->
|
||||||
|
subs.find { it.channel.url == url }
|
||||||
|
?: getSubscriptionOtherOrCreate(url);
|
||||||
|
};
|
||||||
|
} ?: subs;
|
||||||
|
|
||||||
|
|
||||||
val usePolycentric = true;
|
val usePolycentric = true;
|
||||||
val lock = Object();
|
val lock = Object();
|
||||||
var polycentricBudget: Int = 10;
|
var polycentricBudget: Int = 10;
|
||||||
val subUrls = getSubscriptions().parallelStream().map {
|
val subUrls = emulatedSubs.parallelStream().map {
|
||||||
if(usePolycentric) {
|
if(usePolycentric) {
|
||||||
val result = StatePolycentric.instance.getChannelUrlsWithUpdateResult(it.channel.url, it.channel.id, polycentricBudget <= 0, true);
|
val result = StatePolycentric.instance.getChannelUrlsWithUpdateResult(it.channel.url, it.channel.id, polycentricBudget <= 0, true);
|
||||||
if(result.first) {
|
if(result.first) {
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
package com.futo.platformplayer.views.overlays
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.shapes.Shape
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.net.toFile
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.PresetImages
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SelectableCreatorBarViewHolder
|
||||||
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||||
|
import com.google.android.flexbox.FlexboxLayout
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
import com.google.android.material.shape.CornerFamily
|
||||||
|
import com.google.android.material.shape.ShapeAppearanceModel
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class CreatorSelectOverlay: ConstraintLayout {
|
||||||
|
private val _buttonSelect: Button;
|
||||||
|
private val _topbar: OverlayTopbar;
|
||||||
|
private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
|
||||||
|
|
||||||
|
private val _creators: ArrayList<SelectableCreatorBarViewHolder.Selectable> = arrayListOf();
|
||||||
|
|
||||||
|
private var _selected: MutableList<String> = mutableListOf();
|
||||||
|
|
||||||
|
val onSelected = Event1<List<String>>();
|
||||||
|
val onClose = Event0();
|
||||||
|
|
||||||
|
constructor(context: Context, hideSubscriptions: List<String>? = null): super(context) {
|
||||||
|
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||||
|
if(hideSubscriptions != null) {
|
||||||
|
_creators.addAll(subs
|
||||||
|
.filter { !hideSubscriptions.contains(it.channel.url) }
|
||||||
|
.map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_creators.addAll(subs
|
||||||
|
.map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) });
|
||||||
|
_recyclerCreators.notifyContentChanged();
|
||||||
|
}
|
||||||
|
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { }
|
||||||
|
init {
|
||||||
|
inflate(context, R.layout.overlay_creator_select, this);
|
||||||
|
_topbar = findViewById(R.id.topbar);
|
||||||
|
_buttonSelect = findViewById(R.id.button_select);
|
||||||
|
val dp6 = 6.dp(resources);
|
||||||
|
_recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creators, RecyclerView.HORIZONTAL) { creatorView ->
|
||||||
|
creatorView.itemView.setPadding(0, dp6, 0, dp6);
|
||||||
|
creatorView.onClick.subscribe {
|
||||||
|
if(it.channel.thumbnail == null) {
|
||||||
|
UIDialogs.toast(context, "No thumbnail found");
|
||||||
|
return@subscribe;
|
||||||
|
}
|
||||||
|
if(_selected.contains(it.channel.url))
|
||||||
|
_selected.remove(it.channel.url);
|
||||||
|
else
|
||||||
|
_selected.add(it.channel.url);
|
||||||
|
updateSelected();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
_recyclerCreators.view.layoutManager = GridLayoutManager(context, 5).apply {
|
||||||
|
this.orientation = LinearLayoutManager.VERTICAL;
|
||||||
|
};
|
||||||
|
_buttonSelect.setOnClickListener {
|
||||||
|
_selected?.let {
|
||||||
|
select();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_topbar.onClose.subscribe {
|
||||||
|
onClose.emit();
|
||||||
|
}
|
||||||
|
updateSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSelected() {
|
||||||
|
_creators.forEach { p -> p.active = _selected.contains(p.channel.url) };
|
||||||
|
_recyclerCreators.notifyContentChanged();
|
||||||
|
|
||||||
|
if(_selected.isNotEmpty())
|
||||||
|
_buttonSelect.alpha = 1f;
|
||||||
|
else
|
||||||
|
_buttonSelect.alpha = 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun select() {
|
||||||
|
if(_creators.isEmpty())
|
||||||
|
return;
|
||||||
|
onSelected.emit(_selected.toList());
|
||||||
|
onClose.emit();
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,9 +8,16 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:animateLayoutChanges="true">
|
android:animateLayoutChanges="true">
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/container_top"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
<com.futo.platformplayer.views.overlays.OverlayTopbar
|
||||||
|
android:id="@+id/topbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/black"
|
||||||
|
app:title="Group" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -137,99 +144,63 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<ScrollView
|
<!--
|
||||||
android:layout_width="match_parent"
|
<TextView
|
||||||
android:layout_height="match_parent">
|
android:layout_width="wrap_content"
|
||||||
<LinearLayout
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="match_parent"
|
android:textSize="16dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_marginLeft="20dp"
|
||||||
android:orientation="vertical"
|
android:layout_marginRight="20dp"
|
||||||
android:paddingStart="0dp"
|
android:textColor="@color/white"
|
||||||
android:paddingEnd="0dp">
|
android:fontFamily="@font/inter_light"
|
||||||
<LinearLayout
|
android:text="@string/enabled" />
|
||||||
android:id="@+id/container_enabled"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:layout_marginBottom="10dp">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="16dp"
|
android:textSize="12dp"
|
||||||
android:layout_marginLeft="20dp"
|
android:layout_marginLeft="20dp"
|
||||||
android:layout_marginRight="20dp"
|
android:layout_marginRight="20dp"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/gray_ac"
|
||||||
android:fontFamily="@font/inter_light"
|
android:fontFamily="@font/inter_extra_light"
|
||||||
android:text="@string/enabled" />
|
android:text="@string/these_creators_in_group" /> -->
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="12dp"
|
|
||||||
android:layout_marginLeft="20dp"
|
|
||||||
android:layout_marginRight="20dp"
|
|
||||||
android:textColor="@color/gray_ac"
|
|
||||||
android:fontFamily="@font/inter_extra_light"
|
|
||||||
android:text="@string/these_creators_in_group" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recycler_creators_enabled"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:layout_marginLeft="5dp"
|
|
||||||
android:layout_marginRight="5dp"
|
|
||||||
android:paddingTop="10dp"
|
|
||||||
android:paddingBottom="10dp" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/container_disabled"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_marginTop="10dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="16dp"
|
|
||||||
android:layout_marginLeft="20dp"
|
|
||||||
android:layout_marginRight="20dp"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:fontFamily="@font/inter_light"
|
|
||||||
android:text="@string/disabled" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="12dp"
|
|
||||||
android:layout_marginLeft="20dp"
|
|
||||||
android:layout_marginRight="20dp"
|
|
||||||
android:textColor="@color/gray_ac"
|
|
||||||
android:fontFamily="@font/inter_extra_light"
|
|
||||||
android:text="@string/these_creators_not_in_group" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recycler_creators_disabled"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:paddingTop="10dp"
|
|
||||||
android:paddingBottom="10dp" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/container_top"
|
||||||
|
|
||||||
|
android:orientation="vertical">
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_creators_enabled"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/container_top"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/button_creator_add"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginLeft="5dp"
|
||||||
|
android:layout_marginRight="5dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_creator_add"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:background="@drawable/background_button_primary"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:text="Add Creator" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/overlay"
|
android:id="@+id/overlay"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:elevation="10dp"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
44
app/src/main/res/layout/overlay_creator_select.xml
Normal file
44
app/src/main/res/layout/overlay_creator_select.xml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:background="@color/black"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.overlays.OverlayTopbar
|
||||||
|
android:id="@+id/topbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
app:title="Select creators"
|
||||||
|
app:metadata=""
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_creators"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/container_select"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent">
|
||||||
|
</androidx.recyclerview.widget.RecyclerView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/container_select"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent">
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_select"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
android:text="Select" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -127,7 +127,8 @@
|
||||||
android:id="@+id/button_select"
|
android:id="@+id/button_select"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@color/colorPrimary"
|
android:layout_margin="10dp"
|
||||||
|
android:background="@drawable/background_button_primary"
|
||||||
android:text="Select" />
|
android:text="Select" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue