diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 6dfe8472..35e5debf 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -321,7 +321,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { _fragDownloads.topBar = _fragTopBarGeneral; _fragImportSubscriptions.topBar = _fragTopBarImport; _fragImportPlaylists.topBar = _fragTopBarImport; - _fragSubGroup.topBar = _fragTopBarNavigation; _fragSubGroupList.topBar = _fragTopBarAdd; _fragBrowser.topBar = _fragTopBarNavigation; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt index f77df2cd..52dc7e57 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt @@ -9,6 +9,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager +import android.widget.Button import android.widget.FrameLayout import android.widget.ImageButton import android.widget.ImageView @@ -20,6 +21,7 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.futo.platformplayer.R +import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UISlideOverlays import com.futo.platformplayer.api.media.models.channels.IPlatformChannel 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.adapters.AnyAdapter 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.OverlayTopbar import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.shape.CornerFamily @@ -69,6 +73,7 @@ class SubscriptionGroupFragment : MainFragment() { private class SubscriptionGroupView: ConstraintLayout { private val _fragment: SubscriptionGroupFragment; + private val _topbar: OverlayTopbar; private val _textGroupTitleContainer: LinearLayout; private val _textGroupTitle: TextView; private val _imageGroup: ShapeableImageView; @@ -81,16 +86,12 @@ class SubscriptionGroupFragment : MainFragment() { private val _buttonSettings: ImageButton; private val _buttonDelete: ImageButton; - private val _enabledCreators: ArrayList = arrayListOf(); - private val _disabledCreators: ArrayList = arrayListOf(); - private val _enabledCreatorsFiltered: ArrayList = arrayListOf(); - private val _disabledCreatorsFiltered: ArrayList = arrayListOf(); + private val _buttonAddCreator: Button; - private val _containerEnabled: LinearLayout; - private val _containerDisabled: LinearLayout; + private val _enabledCreators: ArrayList = arrayListOf(); + private val _enabledCreatorsFiltered: ArrayList = arrayListOf(); private val _recyclerCreatorsEnabled: AnyAdapterView; - private val _recyclerCreatorsDisabled: AnyAdapterView; private val _overlay: FrameLayout; @@ -101,6 +102,7 @@ class SubscriptionGroupFragment : MainFragment() { _fragment = fragment; _overlay = findViewById(R.id.overlay); + _topbar = findViewById(R.id.topbar); _searchBar = findViewById(R.id.search_bar); _textGroupTitleContainer = findViewById(R.id.text_group_title_container); _textGroupTitle = findViewById(R.id.text_group_title); @@ -110,33 +112,50 @@ class SubscriptionGroupFragment : MainFragment() { _textGroupMeta = findViewById(R.id.text_group_meta); _buttonSettings = findViewById(R.id.button_settings); _buttonDelete = findViewById(R.id.button_delete); + _buttonAddCreator = findViewById(R.id.button_creator_add); _imageGroup.setBackgroundColor(Color.GRAY); + _topbar.onClose.subscribe { + fragment.close(true); + } + + _buttonAddCreator.setOnClickListener { + addCreators(); + } + val dp6 = 6.dp(resources); _imageGroup.shapeAppearanceModel = ShapeAppearanceModel.builder() .setAllCorners(CornerFamily.ROUNDED, dp6.toFloat()) .build() - _containerEnabled = findViewById(R.id.container_enabled); - _containerDisabled = findViewById(R.id.container_disabled); _recyclerCreatorsEnabled = findViewById(R.id.recycler_creators_enabled).asAny(_enabledCreatorsFiltered) { it.itemView.setPadding(0, dp6, 0, dp6); 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(R.id.recycler_creators_disabled).asAny(_disabledCreatorsFiltered) { it.itemView.setPadding(0, dp6, 0, dp6); it.onClick.subscribe { channel -> enableCreator(channel); }; - } + }*/ _recyclerCreatorsEnabled.view.layoutManager = GridLayoutManager(context, 5).apply { this.orientation = LinearLayoutManager.VERTICAL; }; + /* _recyclerCreatorsDisabled.view.layoutManager = GridLayoutManager(context, 5).apply { this.orientation = LinearLayoutManager.VERTICAL; - }; + };*/ _textGroupTitleContainer.setOnClickListener { _group?.let { editName(it) }; @@ -154,10 +173,14 @@ class SubscriptionGroupFragment : MainFragment() { } _buttonDelete.setOnClickListener { - _group?.let { - StateSubscriptionGroups.instance.deleteSubscriptionGroup(it.id); + _group?.let { g -> + 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; @@ -208,6 +231,28 @@ class SubscriptionGroupFragment : MainFragment() { 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?) { @@ -230,73 +275,30 @@ class SubscriptionGroupFragment : MainFragment() { @SuppressLint("NotifyDataSetChanged") private fun reloadCreators(group: SubscriptionGroup?) { _enabledCreators.clear(); - _disabledCreators.clear(); + //_disabledCreators.clear(); if(group != null) { val urls = group.urls.toList(); val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel } _enabledCreators.addAll(subs.filter { urls.contains(it.url) }); - _disabledCreators.addAll(subs.filter { !urls.contains(it.url) }); } + updateMeta(); filterCreators(); } private fun filterCreators() { val query = _searchBar.textSearch.text.toString().lowercase(); val filteredEnabled = _enabledCreators.filter { it.name.lowercase().contains(query) }; - val filteredDisabled = _disabledCreators.filter { it.name.lowercase().contains(query) }; //Optimize _enabledCreatorsFiltered.clear(); _enabledCreatorsFiltered.addAll(filteredEnabled); - _disabledCreatorsFiltered.clear(); - _disabledCreatorsFiltered.addAll(filteredDisabled); _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() { - _textGroupMeta.text = "${_enabledCreators.size} creators"; + _textGroupMeta.text = "${_group?.urls?.size} creators"; } } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt index 41578d00..1d078590 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt @@ -104,7 +104,7 @@ class SubscriptionsFeedFragment : MainFragment() { constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null) : super(fragment, inflater, cachedRecyclerData) { 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) { try { setProgress(progress, total); @@ -162,14 +162,14 @@ class SubscriptionsFeedFragment : MainFragment() { } } - if (!StateSubscriptions.instance.isGlobalUpdating) { + if (!StateSubscriptions.instance.global.isGlobalUpdating) { finishRefreshLayoutLoader(); } } override fun cleanup() { super.cleanup() - StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.remove(this); + StateSubscriptions.instance.global.onUpdateProgress.remove(this); StateSubscriptions.instance.onSubscriptionsChanged.remove(this); } @@ -194,8 +194,9 @@ class SubscriptionsFeedFragment : MainFragment() { private var _bypassRateLimit = false; private val _lastExceptions: List? = null; private val _taskGetPager = TaskHandler>({StateApp.instance.scope}, { withRefresh -> + val group = _subGroup; 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 rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true } 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 }); } _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()) handleExceptions(currentExs); diff --git a/app/src/main/java/com/futo/platformplayer/functional/CentralizedFeed.kt b/app/src/main/java/com/futo/platformplayer/functional/CentralizedFeed.kt new file mode 100644 index 00000000..b033d432 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/functional/CentralizedFeed.kt @@ -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? = null; + var isGlobalUpdating: Boolean = false; + var exceptions: List = listOf(); + + + var lastProgress: Int = 0; + var lastTotal: Int = 0; + val onUpdateProgress = Event2(); + val onUpdated = Event0(); + val onUpdatedOnce = Event1(); + val onException = Event1>(); +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/models/Subscription.kt b/app/src/main/java/com/futo/platformplayer/models/Subscription.kt index 0d2c5382..00435ba2 100644 --- a/app/src/main/java/com/futo/platformplayer/models/Subscription.kt +++ b/app/src/main/java/com/futo/platformplayer/models/Subscription.kt @@ -48,11 +48,16 @@ class Subscription { var playbackSeconds: Int = 0; var playbackViews: Int = 0; + var isOther = false; constructor(channel : SerializedChannel) { this.channel = channel; } + fun isChannel(url: String): Boolean { + return channel.url == url || channel.urlAlternatives.contains(url); + } + fun shouldFetchVideos() = doFetchVideos && (lastVideo.getNowDiffDays() < 30 || lastVideoUpdate.getNowDiffDays() >= 1) && (lastVideo.getNowDiffDays() < 180 || lastVideoUpdate.getNowDiffDays() >= 3); @@ -63,10 +68,16 @@ class Subscription { fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url); fun save() { - StateSubscriptions.instance.saveSubscription(this); + if(isOther) + StateSubscriptions.instance.saveSubscriptionOther(this); + else + StateSubscriptions.instance.saveSubscription(this); } fun saveAsync() { - StateSubscriptions.instance.saveSubscription(this); + if(isOther) + StateSubscriptions.instance.saveSubscriptionOtherAsync(this); + else + StateSubscriptions.instance.saveSubscriptionAsync(this); } fun updateChannel(channel: IPlatformChannel) { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt index 2e4c5854..9af9d2b6 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt @@ -1,27 +1,18 @@ package com.futo.platformplayer.states import com.futo.platformplayer.Settings -import com.futo.platformplayer.UIDialogs -import com.futo.platformplayer.api.media.models.ResultCapabilities +import com.futo.platformplayer.api.media.PlatformID 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.contents.IPlatformContent 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.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.engine.exceptions.PluginException -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.functional.CentralizedFeed import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.Subscription +import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.stores.FragmentedStorage @@ -32,15 +23,10 @@ import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms import kotlinx.coroutines.* import java.time.OffsetDateTime -import java.util.concurrent.ExecutionException import java.util.concurrent.ForkJoinPool -import java.util.concurrent.ForkJoinTask -import kotlin.collections.ArrayList import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine import kotlin.streams.asSequence -import kotlin.streams.toList -import kotlin.system.measureTimeMillis /*** * Used to maintain subscriptions @@ -54,25 +40,17 @@ class StateSubscriptions { override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Subscription = Subscription(SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false))); }).load(); + private val _subscriptionOthers = FragmentedStorage.storeJson("subscriptions_others") + .withUnique { it.channel.url } + .load(); private val _subscriptionsPool = ForkJoinPool(Settings.instance.subscriptions.getSubscriptionsConcurrency()); private val _legacySubscriptions = FragmentedStorage.get(); - - private var _globalSubscriptionsLock = Object(); - private var _globalSubscriptionFeed: ReusablePager? = null; - var isGlobalUpdating: Boolean = false - private set; - var globalSubscriptionExceptions: List = listOf() - private set; - private val _algorithmSubscriptions = SubscriptionFetchAlgorithms.SMART; - private var _lastGlobalSubscriptionProgress: Int = 0; - private var _lastGlobalSubscriptionTotal: Int = 0; - val onGlobalSubscriptionsUpdateProgress = Event2(); - val onGlobalSubscriptionsUpdated = Event0(); - val onGlobalSubscriptionsUpdatedOnce = Event1(); - val onGlobalSubscriptionsException = Event1>(); + val global: CentralizedFeed = CentralizedFeed(); + val feeds: HashMap = hashMapOf(); + val onSubscriptionsChanged = Event2, Boolean>(); @@ -83,75 +61,95 @@ class StateSubscriptions { else return subs.minOf { it.lastVideoUpdate }; } - fun getGlobalSubscriptionProgress(): Pair { - 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 { + 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"); scope.launch(Dispatchers.IO) { - synchronized(_globalSubscriptionsLock) { - if (isGlobalUpdating || (onlyIfNull && _globalSubscriptionFeed != null)) { + synchronized(feed.lock) { + if (feed.isGlobalUpdating || (onlyIfNull && feed.feed != null)) { Logger.i(TAG, "Already updating subscriptions or not required") return@launch; } - isGlobalUpdating = true; + feed.isGlobalUpdating = true; } try { val subsResult = getSubscriptionsFeedWithExceptions(true, true, scope, { progress, total -> - _lastGlobalSubscriptionProgress = progress; - _lastGlobalSubscriptionTotal = total; - onGlobalSubscriptionsUpdateProgress.emit(progress, total); + feed.lastProgress = progress; + feed.lastTotal = total; + feed.onUpdateProgress.emit(progress, total); onProgress?.invoke(progress, total); - }); + }, null, group); if (subsResult.second.any()) { - globalSubscriptionExceptions = subsResult.second; - onGlobalSubscriptionsException.emit(subsResult.second); + feed.exceptions = subsResult.second; + feed.onException.emit(subsResult.second); } - _globalSubscriptionFeed = subsResult.first.asReusable(); - synchronized(_globalSubscriptionsLock) { - onGlobalSubscriptionsUpdated.emit(); - onGlobalSubscriptionsUpdatedOnce.emit(null); - onGlobalSubscriptionsUpdatedOnce.clear(); + feed.feed = subsResult.first.asReusable(); + synchronized(feed.lock) { + feed.onUpdated.emit(); + feed.onUpdatedOnce.emit(null); + feed.onUpdatedOnce.clear(); } } catch (e: Throwable) { - synchronized(_globalSubscriptionsLock) { - onGlobalSubscriptionsUpdatedOnce.emit(e); - onGlobalSubscriptionsUpdatedOnce.clear(); + synchronized(feed.lock) { + feed.onUpdatedOnce.emit(e); + feed.onUpdatedOnce.clear(); } Logger.e(TAG, "Failed to update subscription feed.", e); } finally { - isGlobalUpdating = false; + feed.isGlobalUpdating = false; } }; } - fun clearSubscriptionFeed() { - synchronized(_globalSubscriptionsLock) { - _globalSubscriptionFeed = null; + fun clearSubscriptionFeed(id: String? = null) { + val feed = getFeed(id) ?: return; + synchronized(feed.lock) { + feed.feed = null; } } private var loadIndex = 0; - suspend fun getGlobalSubscriptionFeed(scope: CoroutineScope, updated: Boolean): IPager { + suspend fun getGlobalSubscriptionFeed(scope: CoroutineScope, updated: Boolean, group: SubscriptionGroup? = null): IPager { + val feed = getFeed(group?.id, true) ?: return EmptyPager(); //Get Subscriptions only if null - updateSubscriptionFeed(scope, !updated); + updateSubscriptionFeed(scope, !updated, null, group); val evRef = Object(); val result = suspendCoroutine { - synchronized(_globalSubscriptionsLock) { - if (_globalSubscriptionFeed != null && !updated) { + synchronized(feed.lock) { + if (feed.feed != null && !updated) { Logger.i(TAG, "Subscriptions got feed preloaded"); - it.resumeWith(Result.success(_globalSubscriptionFeed!!.getWindow())); + it.resumeWith(Result.success(feed.feed!!.getWindow())); } else { val loadIndex = loadIndex++; 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"); if(ex != null) it.resumeWithException(ex); - else if (_globalSubscriptionFeed != null) - it.resumeWith(Result.success(_globalSubscriptionFeed!!.getWindow())); + else if (feed.feed != null) + it.resumeWith(Result.success(feed.feed!!.getWindow())); else 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) }; } } + 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) { _subscriptions.save(sub, false, true); } fun saveSubscriptionAsync(sub: Subscription) { _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 { synchronized(_subscriptions) { return _subscriptions.getItems().size; @@ -239,12 +260,19 @@ class StateSubscriptions { } } - fun getSubscriptionRequestCount(): Map { + fun getSubscriptionRequestCount(subGroup: SubscriptionGroup? = null): Map { + 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) - .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, List> { + fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null, subGroup: SubscriptionGroup? = null): Pair, List> { val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool); if(onNewCacheHit != null) algo.onNewCacheHit.subscribe(onNewCacheHit) @@ -253,10 +281,19 @@ class StateSubscriptions { 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 lock = Object(); var polycentricBudget: Int = 10; - val subUrls = getSubscriptions().parallelStream().map { + val subUrls = emulatedSubs.parallelStream().map { if(usePolycentric) { val result = StatePolycentric.instance.getChannelUrlsWithUpdateResult(it.channel.url, it.channel.id, polycentricBudget <= 0, true); if(result.first) { diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/CreatorSelectOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/CreatorSelectOverlay.kt new file mode 100644 index 00000000..1703442f --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/CreatorSelectOverlay.kt @@ -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; + + private val _creators: ArrayList = arrayListOf(); + + private var _selected: MutableList = mutableListOf(); + + val onSelected = Event1>(); + val onClose = Event0(); + + constructor(context: Context, hideSubscriptions: List? = 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(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(); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_subscriptions_group.xml b/app/src/main/res/layout/fragment_subscriptions_group.xml index cf728852..9e094d0d 100644 --- a/app/src/main/res/layout/fragment_subscriptions_group.xml +++ b/app/src/main/res/layout/fragment_subscriptions_group.xml @@ -8,9 +8,16 @@ android:orientation="vertical" android:animateLayoutChanges="true"> + - - - + + + + + + +