mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-17 05:22:40 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48ab77eadc | |||
| f486513105 | |||
| f338adf033 | |||
| 74be667114 | |||
| b5a1fc92dc |
@@ -17,6 +17,7 @@ import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
@@ -272,6 +273,15 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||
@FormField("Other", FieldForm.GROUP, "Others...", 5)
|
||||
val otherTests: OtherTests = OtherTests();
|
||||
class OtherTests {
|
||||
@FormField("Unsubscribe all", FieldForm.BUTTON, "Removes all subscriptions", -1)
|
||||
fun unsubscribeAll() {
|
||||
val toUnsub = StateSubscriptions.instance.getSubscriptions();
|
||||
UIDialogs.toast("Started unsubbing.. (${toUnsub.size})")
|
||||
toUnsub.forEach {
|
||||
StateSubscriptions.instance.removeSubscription(it.channel.url);
|
||||
};
|
||||
UIDialogs.toast("Finished unsubbing.. (${toUnsub.size})")
|
||||
}
|
||||
@FormField("Clear Downloads", FieldForm.BUTTON, "Deletes all ongoing downloads", 1)
|
||||
fun clearDownloads() {
|
||||
StateDownloads.instance.getDownloading().forEach {
|
||||
|
||||
@@ -304,7 +304,7 @@ class UISlideOverlays {
|
||||
return overlay;
|
||||
}
|
||||
|
||||
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, onVideoHidden: (()->Unit)? = null): SlideUpMenuOverlay {
|
||||
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, vararg actions: SlideUpMenuItem): SlideUpMenuOverlay {
|
||||
val items = arrayListOf<View>();
|
||||
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
||||
|
||||
@@ -323,11 +323,11 @@ class UISlideOverlays {
|
||||
val queue = StatePlayer.instance.getQueue();
|
||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||
items.add(SlideUpMenuGroup(container.context, "Actions", "actions",
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, "Hide", "Hide from Home", "hide",
|
||||
{ StateMeta.instance.addHiddenVideo(video.url); onVideoHidden?.invoke() }),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
|
||||
{ showDownloadVideoOverlay(video, container, true); }, false)
|
||||
))
|
||||
(listOf(
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
|
||||
{ showDownloadVideoOverlay(video, container, true); }, false))
|
||||
+ actions)
|
||||
));
|
||||
items.add(
|
||||
SlideUpMenuGroup(container.context, "Add To", "addto",
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, "Add to Queue", "${queue.size} videos", "queue",
|
||||
|
||||
@@ -75,7 +75,12 @@ class DedupContentPager : IPager<IPlatformContent>, IAsyncPager<IPlatformContent
|
||||
return toReturn;
|
||||
}
|
||||
private fun isSameItem(item: IPlatformContent, item2: IPlatformContent): Boolean {
|
||||
return item.name == item2.name && (item.datetime == null || item2.datetime == null || abs(item.datetime!!.getDiffDays(item2.datetime!!)) < 2);
|
||||
//return item == item2;
|
||||
val daysAgo = Math.abs(item.datetime?.getNowDiffDays() ?: return false);
|
||||
val maxDelta = Math.max(2, (daysAgo / 1.5).toInt()); //TODO: Better scaling delta
|
||||
val isSame = item.name.equals(item2.name, true) && (item.datetime == null || item2.datetime == null || abs(item.datetime!!.getDiffDays(item2.datetime!!)) < maxDelta);
|
||||
|
||||
return isSame;
|
||||
}
|
||||
private fun calculateHash(item: IPlatformContent): Int {
|
||||
return combineHashCodes(listOf(item.name.hashCode(), item.datetime?.hashCode()));
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ import java.util.stream.IntStream
|
||||
*/
|
||||
class MultiChronoContentPager : MultiPager<IPlatformContent> {
|
||||
constructor(pagers : Array<IPager<IPlatformContent>>, allowFailure: Boolean = false, pageSize: Int = 9) : super(pagers.map { it }.toList(), allowFailure, pageSize) {}
|
||||
constructor(pagers : List<IPager<IPlatformContent>>, allowFailure: Boolean = false, pageSize: Int = 9) : super(pagers, allowFailure, pageSize) {}
|
||||
|
||||
@Synchronized
|
||||
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.futo.platformplayer.api.media.structures
|
||||
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.stream.IntStream
|
||||
|
||||
|
||||
/**
|
||||
* A Content AsyncMultiPager that returns results based on a specified distribution
|
||||
* Unlike its non-async counterpart, this one uses parallel nextPage requests
|
||||
*/
|
||||
class MultiChronoContentParallelPager : MultiParallelPager<IPlatformContent> {
|
||||
|
||||
constructor(pagers: List<IPager<IPlatformContent>>) : super(pagers)
|
||||
|
||||
@Synchronized
|
||||
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
|
||||
if(options.size == 0)
|
||||
return -1;
|
||||
var bestIndex = 0;
|
||||
|
||||
val allResults = runBlocking { options.map { Pair(it, it.item?.await()) } };
|
||||
for(i in IntStream.range(1, options.size)) {
|
||||
val best = allResults[bestIndex].second;
|
||||
val cur = allResults[i].second ?: continue;
|
||||
if(best?.datetime == null || (cur.datetime != null && cur.datetime!! > best.datetime!!))
|
||||
bestIndex = i;
|
||||
}
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+18
-18
@@ -66,25 +66,25 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
|
||||
override fun getResults(): List<T> = synchronized(_pagersReusable){ _currentPager.getResults() };
|
||||
|
||||
private fun updatePager(pagerToAdd: IPager<T>?, toReplacePager: IPager<T>? = null, error: Throwable? = null) {
|
||||
if(pagerToAdd == null) {
|
||||
if(toReplacePager != null && toReplacePager is PlaceholderPager && error != null) {
|
||||
val pluginId = toReplacePager.placeholderFactory.invoke().id?.pluginId ?: "";
|
||||
|
||||
_pagersReusable.add((PlaceholderPager(5) {
|
||||
return@PlaceholderPager PlatformContentPlaceholder(pluginId, error)
|
||||
} as IPager<T>).asReusable());
|
||||
_currentPager = recreatePager(getCurrentSubPagers());
|
||||
|
||||
if(_currentPager is MultiParallelPager<*>)
|
||||
runBlocking { (_currentPager as MultiParallelPager).initialize(); };
|
||||
else if(_currentPager is MultiPager<*>)
|
||||
(_currentPager as MultiPager).initialize()
|
||||
|
||||
onPagerChanged.emit(_currentPager);
|
||||
}
|
||||
return;
|
||||
}
|
||||
synchronized(_pagersReusable) {
|
||||
if(pagerToAdd == null) {
|
||||
if(toReplacePager != null && toReplacePager is PlaceholderPager && error != null) {
|
||||
val pluginId = toReplacePager.placeholderFactory.invoke().id?.pluginId ?: "";
|
||||
|
||||
_pagersReusable.add((PlaceholderPager(5) {
|
||||
return@PlaceholderPager PlatformContentPlaceholder(pluginId, error)
|
||||
} as IPager<T>).asReusable());
|
||||
_currentPager = recreatePager(getCurrentSubPagers());
|
||||
|
||||
if(_currentPager is MultiParallelPager<*>)
|
||||
runBlocking { (_currentPager as MultiParallelPager).initialize(); };
|
||||
else if(_currentPager is MultiPager<*>)
|
||||
(_currentPager as MultiPager).initialize()
|
||||
|
||||
onPagerChanged.emit(_currentPager);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Logger.i("RefreshMultiDistributionContentPager", "Received new pager for RefreshPager")
|
||||
_pagersReusable.add(pagerToAdd.asReusable());
|
||||
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.futo.platformplayer.api.media.structures
|
||||
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
||||
/**
|
||||
* A RefreshMultiPager that simply returns all respective pagers in equal distribution, optionally inserting PlaceholderPager results as provided for their respective promised pagers
|
||||
* (Eg. Pager A is completed, Pager [B,C,D] are promised/deferred. placeholderPagers [1,2,3] will map B=>1, C=>2, D=>3 until promised pagers are completed)
|
||||
* Uses wrapped MultiDistributionContentAsyncPager for inidivual pagers.
|
||||
*/
|
||||
class RefreshChronoContentPager(pagers: List<IPager<IPlatformContent>>, pendingPagers: List<Deferred<IPager<IPlatformContent>?>>, placeholderPagers: List<IPager<IPlatformContent>>? = null)
|
||||
: MultiRefreshPager<IPlatformContent>(pagers, pendingPagers, placeholderPagers) {
|
||||
|
||||
override fun recreatePager(pagers: List<IPager<IPlatformContent>>): IPager<IPlatformContent> {
|
||||
return MultiChronoContentPager(pagers);
|
||||
//return MultiChronoContentParallelPager(pagers);
|
||||
//return MultiDistributionContentPager(pagers.associateWith { 1f });
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ class SingleAsyncItemPager<T> {
|
||||
if (_currentResultPos >= _requestedPageItems.size) {
|
||||
val startPos = fillDeferredUntil(_currentResultPos);
|
||||
if(!_pager.hasMorePages()) {
|
||||
Logger.i("SingleAsyncItemPager", "end of async page reached");
|
||||
completeRemainder { it?.complete(null) };
|
||||
}
|
||||
if(_isRequesting)
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.builders.DashBuilder
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
@@ -352,16 +353,25 @@ class StateCasting {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (videoSource is IVideoUrlSource) {
|
||||
if (videoSource is IVideoUrlSource)
|
||||
ad.loadVideo("BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble());
|
||||
} else if (audioSource is IAudioUrlSource) {
|
||||
else if(videoSource is IHLSManifestSource)
|
||||
ad.loadVideo("BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble());
|
||||
else if (audioSource is IAudioUrlSource)
|
||||
ad.loadVideo("BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble());
|
||||
} else if (videoSource is LocalVideoSource) {
|
||||
else if(audioSource is IHLSManifestAudioSource)
|
||||
ad.loadVideo("BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble());
|
||||
else if (videoSource is LocalVideoSource)
|
||||
castLocalVideo(video, videoSource, resumePosition);
|
||||
} else if (audioSource is LocalAudioSource) {
|
||||
else if (audioSource is LocalAudioSource)
|
||||
castLocalAudio(video, audioSource, resumePosition);
|
||||
} else {
|
||||
throw Exception("Unhandled source type videoSource=$videoSource audioSource=$audioSource subtitleSource=$subtitleSource");
|
||||
else {
|
||||
var str = listOf(
|
||||
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
||||
if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null,
|
||||
if(subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null
|
||||
).filterNotNull().joinToString(", ");
|
||||
throw UnsupportedCastException(str);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.futo.platformplayer.exceptions
|
||||
|
||||
import java.lang.Exception
|
||||
|
||||
class UnsupportedCastException(msg: String) : Exception(msg) {
|
||||
}
|
||||
+8
-6
@@ -248,7 +248,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
|
||||
if(_pager is IReplacerPager<*>)
|
||||
(_pager as IReplacerPager<*>).onReplaced.remove(this);
|
||||
|
||||
if(pager is IReplacerPager<*>) {
|
||||
pager.onReplaced.subscribe(this) { oldItem, newItem ->
|
||||
if(_pager != pager)
|
||||
@@ -257,11 +256,14 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
if(_pager !is IPager<IPlatformContent>)
|
||||
return@subscribe;
|
||||
|
||||
val toReplaceIndex = _results.indexOfFirst { it == newItem };
|
||||
if(toReplaceIndex >= 0) {
|
||||
_results[toReplaceIndex] = newItem as IPlatformContent;
|
||||
_adapterResults?.let {
|
||||
it.notifyItemChanged(it.childToParentPosition(toReplaceIndex));
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val toReplaceIndex = _results.indexOfFirst { it == oldItem };
|
||||
if (toReplaceIndex >= 0) {
|
||||
_results[toReplaceIndex] = newItem as IPlatformContent;
|
||||
_adapterResults?.let {
|
||||
it.notifyItemChanged(it.childToParentPosition(toReplaceIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-8
@@ -15,7 +15,9 @@ import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.structures.*
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.adapters.PreviewContentListAdapter
|
||||
@@ -24,6 +26,7 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||
import com.futo.platformplayer.views.adapters.PreviewNestedVideoViewHolder
|
||||
import com.futo.platformplayer.views.adapters.PreviewVideoViewHolder
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
import kotlin.math.floor
|
||||
|
||||
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
||||
@@ -69,15 +72,24 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
//TODO: Reconstruct search video from detail if search is null
|
||||
_overlayContainer.let {
|
||||
if(content is IPlatformVideo)
|
||||
UISlideOverlays.showVideoOptionsOverlay(content, it) {
|
||||
if (fragment is HomeFragment) {
|
||||
val removeIndex = recyclerData.results.indexOf(content);
|
||||
if (removeIndex >= 0) {
|
||||
recyclerData.results.removeAt(removeIndex);
|
||||
recyclerData.adapter.notifyItemRemoved(recyclerData.adapter.childToParentPosition(removeIndex));
|
||||
UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(context, R.drawable.ic_visibility_off, "Hide", "Hide from Home", "hide",
|
||||
{ StateMeta.instance.addHiddenVideo(content.url);
|
||||
if (fragment is HomeFragment) {
|
||||
val removeIndex = recyclerData.results.indexOf(content);
|
||||
if (removeIndex >= 0) {
|
||||
recyclerData.results.removeAt(removeIndex);
|
||||
recyclerData.adapter.notifyItemRemoved(recyclerData.adapter.childToParentPosition(removeIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}),
|
||||
SlideUpMenuItem(context, R.drawable.ic_playlist, "Play Feed as Queue", "Play entire feed", "playFeed",
|
||||
{
|
||||
val newQueue = listOf(content) + recyclerData.results
|
||||
.filterIsInstance<IPlatformVideo>()
|
||||
.filter { it != content };
|
||||
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue", true, false);
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
adapter.onAddToQueueClicked.subscribe(this) {
|
||||
|
||||
+5
@@ -62,6 +62,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptAgeException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
@@ -1254,6 +1255,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
_lastVideoSource = videoSource;
|
||||
_lastAudioSource = audioSource;
|
||||
}
|
||||
catch(ex: UnsupportedCastException) {
|
||||
Logger.e(TAG, "Failed to load cast media", ex);
|
||||
UIDialogs.showGeneralErrorDialog(context, "Unsupported Cast format", ex);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to load media", ex);
|
||||
UIDialogs.showGeneralErrorDialog(context, "Failed to load media", ex);
|
||||
|
||||
@@ -116,8 +116,9 @@ class StatePlugins {
|
||||
else if(embeddedConfig != null) {
|
||||
val existing = getPlugin(embedded.key);
|
||||
if(existing != null && existing.config.version < embeddedConfig.version ) {
|
||||
Logger.i(TAG, "Found outdated embedded plugin [${existing.config.id}] ${existing.config.name}, deleting and reinstalling");
|
||||
deletePlugin(embedded.key);
|
||||
Logger.i(TAG, "Outdated Embedded plugin [${existing.config.id}] ${existing.config.name} (${existing.config.version} < ${embeddedConfig?.version}), reinstalling");
|
||||
//deletePlugin(embedded.key);
|
||||
installEmbeddedPlugin(context, embedded.value)
|
||||
}
|
||||
else if(existing != null && _isFirstEmbedUpdate)
|
||||
Logger.i(TAG, "Embedded plugin [${existing.config.id}] ${existing.config.name}, up to date (${existing.config.version} >= ${embeddedConfig?.version})");
|
||||
@@ -360,6 +361,8 @@ class StatePlugins {
|
||||
}
|
||||
|
||||
val existing = getPlugin(config.id)
|
||||
val existingAuth = existing?.getAuth();
|
||||
val existingCaptcha = existing?.getCaptchaData();
|
||||
if (existing != null) {
|
||||
if(!reinstall)
|
||||
throw IllegalStateException("Plugin with id ${config.id} already exists");
|
||||
@@ -373,7 +376,7 @@ class StatePlugins {
|
||||
if(icon != null)
|
||||
iconsDir.saveIconBinary(config.id, icon);
|
||||
|
||||
_plugins.save(SourcePluginDescriptor(config, null, null, flags));
|
||||
_plugins.save(SourcePluginDescriptor(config, existingAuth?.toEncrypted(), existingCaptcha?.toEncrypted(), flags));
|
||||
return null;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||
import com.futo.platformplayer.api.media.structures.PlaceholderPager
|
||||
import com.futo.platformplayer.api.media.structures.RefreshChronoContentPager
|
||||
import com.futo.platformplayer.api.media.structures.RefreshDedupContentPager
|
||||
import com.futo.platformplayer.api.media.structures.RefreshDistributionContentPager
|
||||
import com.futo.platformplayer.awaitFirstDeferred
|
||||
@@ -167,12 +168,21 @@ class StatePolycentric {
|
||||
}) ?: return null;
|
||||
|
||||
val toAwait = deferred.filter { it.second != finishedPager.first };
|
||||
|
||||
//TODO: Get a Parallel pager to work here.
|
||||
val innerPager = MultiChronoContentPager(listOf(finishedPager.second!!) + toAwait.mapNotNull { runBlocking { it.second.await(); } });
|
||||
innerPager.initialize();
|
||||
//return RefreshChronoContentPager(listOf(finishedPager.second!!), toAwait.map { it.second }, listOf());
|
||||
//return RefreshDedupContentPager(RefreshChronoContentPager(listOf(finishedPager.second!!), toAwait.map { it.second }, listOf()), StatePlatform.instance.getEnabledClients().map { it.id });
|
||||
return DedupContentPager(innerPager, StatePlatform.instance.getEnabledClients().map { it.id });
|
||||
|
||||
/* //Gives out-of-order results
|
||||
return RefreshDedupContentPager(RefreshDistributionContentPager(
|
||||
listOf(finishedPager.second!!),
|
||||
toAwait.map { it.second },
|
||||
toAwait.map { PlaceholderPager(5) { PlatformContentPlaceholder(it.first.id) } }),
|
||||
StatePlatform.instance.getEnabledClients().map { it.id }
|
||||
);
|
||||
);*/
|
||||
}
|
||||
suspend fun getChannelContent(profile: PolycentricProfile): IPager<IPlatformContent> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
|
||||
Submodule app/src/stable/assets/sources/patreon updated: 54a3cc4efe...6037691859
Submodule app/src/unstable/assets/sources/patreon updated: 54a3cc4efe...6037691859
Reference in New Issue
Block a user