diff --git a/app/src/main/java/com/futo/platformplayer/api/media/IPlatformClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/IPlatformClient.kt index 590ecc32..010fd3c1 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/IPlatformClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/IPlatformClient.kt @@ -1,5 +1,6 @@ package com.futo.platformplayer.api.media +import com.futo.platformplayer.api.media.models.IPlatformChannelContent import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.channels.IPlatformChannel @@ -66,6 +67,11 @@ interface IPlatformClient { */ fun searchChannels(query: String): IPager; + /** + * Searches for channels and returns a content pager + */ + fun searchChannelsAsContent(query: String): IPager; + //Video Pages /** diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorLink.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorLink.kt index e0acb91e..330597a4 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorLink.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorLink.kt @@ -2,7 +2,10 @@ package com.futo.platformplayer.api.media.models import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.media.PlatformID +import com.futo.platformplayer.api.media.models.contents.ContentType +import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.api.media.platforms.js.models.JSContent import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow @@ -42,4 +45,21 @@ open class PlatformAuthorLink { ); } } +} + +interface IPlatformChannelContent : IPlatformContent { + val thumbnail: String? + val subscribers: Long? +} + +open class JSChannelContent : JSContent, IPlatformChannelContent { + override val contentType: ContentType get() = ContentType.CHANNEL + override val thumbnail: String? + override val subscribers: Long? + + constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) { + val contextName = "Channel"; + thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null) + subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/contents/ContentType.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/contents/ContentType.kt index a310e089..d181c6da 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/contents/ContentType.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/contents/ContentType.kt @@ -12,6 +12,7 @@ enum class ContentType(val value: Int) { URL(9), NESTED_VIDEO(11), + CHANNEL(60), LOCKED(70), diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt index 2b6deaf8..371d846b 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt @@ -10,6 +10,7 @@ import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.PlatformClientCapabilities +import com.futo.platformplayer.api.media.models.IPlatformChannelContent import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.channels.IPlatformChannel @@ -31,6 +32,7 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs import com.futo.platformplayer.api.media.platforms.js.models.IJSContent import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails import com.futo.platformplayer.api.media.platforms.js.models.JSChannel +import com.futo.platformplayer.api.media.platforms.js.models.JSChannelContentPager import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager import com.futo.platformplayer.api.media.platforms.js.models.JSChapter import com.futo.platformplayer.api.media.platforms.js.models.JSComment @@ -361,6 +363,10 @@ open class JSClient : IPlatformClient { return@isBusyWith JSChannelPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})")); } + override fun searchChannelsAsContent(query: String): IPager = isBusyWith("searchChannels") { + ensureEnabled(); + return@isBusyWith JSChannelContentPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"), ); + } @JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform") @JSDocsParameter("url", "A channel url (May not be your platform)") diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt index a6a15fb6..fd1f0894 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt @@ -1,6 +1,7 @@ package com.futo.platformplayer.api.media.platforms.js.models import com.caoccao.javet.values.reference.V8ValueObject +import com.futo.platformplayer.api.media.models.JSChannelContent import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.platforms.js.JSClient @@ -26,6 +27,7 @@ interface IJSContent: IPlatformContent { ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj); ContentType.PLAYLIST -> JSPlaylist(config, obj); ContentType.LOCKED -> JSLockedContent(config, obj); + ContentType.CHANNEL -> JSChannelContent(config, obj) else -> throw NotImplementedError("Unknown content type ${type}"); } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChannelPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChannelPager.kt index 683c64af..3a0331d4 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChannelPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChannelPager.kt @@ -5,7 +5,6 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink 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.IPager -import com.futo.platformplayer.engine.V8Plugin class JSChannelPager : JSPager, IPager { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContent.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContent.kt index ab3b6f10..1e74bd0d 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContent.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContent.kt @@ -49,8 +49,8 @@ open class JSContent : IPlatformContent, IPluginSourced { else author = PlatformAuthorLink.UNKNOWN; - val datetimeInt = _content.getOrThrow(config, "datetime", contextName).toLong(); - if(datetimeInt == 0.toLong()) + val datetimeInt = _content.getOrDefault(config, "datetime", contextName, null)?.toLong(); + if(datetimeInt == null || datetimeInt == 0.toLong()) datetime = null; else datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContentPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContentPager.kt index 490fa7c4..256e8a5a 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContentPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContentPager.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.platforms.js.models import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.media.IPluginSourced +import com.futo.platformplayer.api.media.models.JSChannelContent 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 @@ -15,4 +16,14 @@ class JSContentPager : JSPager, IPluginSourced { override fun convertResult(obj: V8ValueObject): IPlatformContent { return IJSContent.fromV8(plugin, obj); } +} + +class JSChannelContentPager : JSPager, IPluginSourced { + override val sourceConfig: SourcePluginConfig get() = config; + + constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {} + + override fun convertResult(obj: V8ValueObject): IPlatformContent { + return JSChannelContent(config, obj); + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt index 4390a80c..76103d4a 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt @@ -201,11 +201,12 @@ abstract class ContentFeedView : FeedView { - StatePlayer.instance.clearQueue(); - fragment.navigate(url).maximizeVideoDetail(); - }; - ContentType.PLAYLIST -> fragment.navigate(url); - ContentType.URL -> fragment.navigate(url); + StatePlayer.instance.clearQueue() + fragment.navigate(url).maximizeVideoDetail() + } + ContentType.PLAYLIST -> fragment.navigate(url) + ContentType.URL -> fragment.navigate(url) + ContentType.CHANNEL -> fragment.navigate(url) else -> {}; } } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt index b8b0b567..f5d518f8 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt @@ -11,6 +11,7 @@ import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UISlideOverlays import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.api.media.models.ResultCapabilities +import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.constructs.TaskHandler @@ -18,6 +19,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment import com.futo.platformplayer.isHttpUrl import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.models.SearchType import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.views.FeedStyle @@ -84,6 +86,7 @@ class ContentSearchResultsFragment : MainFragment() { private var _filterValues: HashMap> = hashMapOf(); private var _enabledClientIds: List? = null; private var _channelUrl: String? = null; + private var _searchType: SearchType? = null; private val _taskSearch: TaskHandler>; override val shouldShowTimeBar: Boolean get() = Settings.instance.search.progressBar @@ -95,7 +98,13 @@ class ContentSearchResultsFragment : MainFragment() { if (channelUrl != null) { StatePlatform.instance.searchChannel(channelUrl, query, null, _sortBy, _filterValues, _enabledClientIds) } else { - StatePlatform.instance.searchRefresh(fragment.lifecycleScope, query, null, _sortBy, _filterValues, _enabledClientIds) + when (_searchType) + { + SearchType.VIDEO -> StatePlatform.instance.searchRefresh(fragment.lifecycleScope, query, null, _sortBy, _filterValues, _enabledClientIds) + SearchType.CREATOR -> StatePlatform.instance.searchChannelsAsContent(query) + SearchType.PLAYLIST -> StatePlatform.instance.searchPlaylist(query) + else -> throw Exception("Search type must be specified") + } } }) .success { loadedResult(it); }.exception { } @@ -116,6 +125,7 @@ class ContentSearchResultsFragment : MainFragment() { if(parameter is SuggestionsFragmentData) { setQuery(parameter.query, false); setChannelUrl(parameter.channelUrl, false); + setSearchType(parameter.searchType, false) fragment.topBar?.apply { if (this is SearchTopBarFragment) { @@ -258,6 +268,15 @@ class ContentSearchResultsFragment : MainFragment() { } } + private fun setSearchType(searchType: SearchType, updateResults: Boolean = true) { + _searchType = searchType + + if (updateResults) { + clearResults(); + loadResults(); + } + } + private fun setSortBy(sortBy: String?, updateResults: Boolean = true) { _sortBy = sortBy; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt index 3c915ebe..a9ea33b2 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt @@ -136,7 +136,6 @@ abstract class FeedView : L _toolbarContentView = findViewById(R.id.container_toolbar_content); _nextPageHandler = TaskHandler>({fragment.lifecycleScope}, { - if (it is IAsyncPager<*>) it.nextPageAsync(); else diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt index a07de94e..9cc4e6a7 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt @@ -49,14 +49,7 @@ class SuggestionsFragment : MainFragment { _adapterSuggestions.onClicked.subscribe { suggestion -> val storage = FragmentedStorage.get(); storage.add(suggestion); - - if (_searchType == SearchType.CREATOR) { - navigate(suggestion); - } else if (_searchType == SearchType.PLAYLIST) { - navigate(suggestion); - } else { - navigate(SuggestionsFragmentData(suggestion, SearchType.VIDEO, _channelUrl)); - } + navigate(SuggestionsFragmentData(suggestion, _searchType, _channelUrl)); } _adapterSuggestions.onRemove.subscribe { suggestion -> val index = _suggestions.indexOf(suggestion); diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt index dfddd51f..389d8a5e 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -632,6 +632,27 @@ class StatePlatform { return pager; } + fun searchChannelsAsContent(query: String): IPager { + Logger.i(TAG, "Platform - searchChannels"); + val pagers = mutableMapOf, Float>(); + getSortedEnabledClient().parallelStream().forEach { + try { + if (it.capabilities.hasChannelSearch) + pagers.put(it.searchChannelsAsContent(query), 1f); + } + catch(ex: Throwable) { + Logger.e(TAG, "Failed search channels", ex) + UIDialogs.toast("Failed search channels on [${it.name}]\n(${ex.message})"); + } + }; + if(pagers.isEmpty()) + return EmptyPager(); + + val pager = MultiDistributionContentPager(pagers); + pager.initialize(); + return pager; + } + //Video fun hasEnabledVideoClient(url: String) : Boolean = getEnabledClients().any { it.isContentDetailsUrl(url) }; diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelView.kt new file mode 100644 index 00000000..28225da9 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelView.kt @@ -0,0 +1,88 @@ +package com.futo.platformplayer.views.adapters + +import android.content.Context +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import com.futo.platformplayer.R +import com.futo.platformplayer.api.media.models.IPlatformChannelContent +import com.futo.platformplayer.api.media.models.PlatformAuthorLink +import com.futo.platformplayer.api.media.models.contents.IPlatformContent +import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.toHumanNumber +import com.futo.platformplayer.views.FeedStyle +import com.futo.platformplayer.views.others.CreatorThumbnail +import com.futo.platformplayer.views.platform.PlatformIndicator +import com.futo.platformplayer.views.subscriptions.SubscribeButton + + +open class ChannelView : LinearLayout { + protected val _feedStyle : FeedStyle; + protected val _tiny: Boolean + + private val _textName: TextView; + private val _creatorThumbnail: CreatorThumbnail; + private val _textMetadata: TextView; + private val _buttonSubscribe: SubscribeButton; + private val _platformIndicator: PlatformIndicator; + + val onClick = Event1(); + + var currentChannel: IPlatformChannelContent? = null + private set + + val content: IPlatformContent? get() = currentChannel; + + constructor(context: Context, feedStyle: FeedStyle, tiny: Boolean) : super(context) { + inflate(feedStyle); + _feedStyle = feedStyle; + _tiny = tiny + + _textName = findViewById(R.id.text_channel_name); + _creatorThumbnail = findViewById(R.id.creator_thumbnail); + _textMetadata = findViewById(R.id.text_channel_metadata); + _buttonSubscribe = findViewById(R.id.button_subscribe); + _platformIndicator = findViewById(R.id.platform_indicator); + + if (_tiny) { + _buttonSubscribe.visibility = View.GONE; + _textMetadata.visibility = View.GONE; + } + + findViewById(R.id.root).setOnClickListener { + val s = currentChannel ?: return@setOnClickListener; + onClick.emit(s); + } + } + + protected open fun inflate(feedStyle: FeedStyle) { + inflate(context, when(feedStyle) { + FeedStyle.PREVIEW -> R.layout.list_creator + else -> R.layout.list_creator + }, this) + } + + open fun bind(content: IPlatformContent) { + isClickable = true; + + if(content !is IPlatformChannelContent) + return + + _creatorThumbnail.setThumbnail(content.thumbnail, false); + _textName.text = content.name; + + if(content.subscribers == null || (content.subscribers ?: 0) <= 0L) + _textMetadata.visibility = View.GONE; + else { + _textMetadata.text = if((content.subscribers ?: 0) > 0) content.subscribers!!.toHumanNumber() + " " + context.getString(R.string.subscribers) else ""; + _textMetadata.visibility = View.VISIBLE; + } + _buttonSubscribe.setSubscribeChannel(content.url); + _platformIndicator.setPlatformFromClientID(content.id.pluginId); + } + + companion object { + private val TAG = "ChannelView" + } +} diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewChannelViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewChannelViewHolder.kt new file mode 100644 index 00000000..17754984 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewChannelViewHolder.kt @@ -0,0 +1,40 @@ +package com.futo.platformplayer.views.adapters.feedtypes + +import android.view.ViewGroup +import com.futo.platformplayer.api.media.models.IPlatformChannelContent +import com.futo.platformplayer.api.media.models.PlatformAuthorLink +import com.futo.platformplayer.api.media.models.contents.IPlatformContent +import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails +import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist +import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.fragment.mainactivity.main.CreatorFeedView +import com.futo.platformplayer.views.FeedStyle +import com.futo.platformplayer.views.adapters.ChannelView +import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder +import com.futo.platformplayer.views.adapters.PlaylistView + + +class PreviewChannelViewHolder : ContentPreviewViewHolder { + val onClick = Event1(); + + val currentChannel: IPlatformChannelContent? get() = view.currentChannel; + + override val content: IPlatformContent? get() = currentChannel; + + private val view: ChannelView get() = itemView as ChannelView; + + constructor(viewGroup: ViewGroup, feedStyle: FeedStyle, tiny: Boolean): super(ChannelView(viewGroup.context, feedStyle, tiny)) { + view.onClick.subscribe(onClick::emit); + } + + override fun bind(content: IPlatformContent) = view.bind(content); + + override fun preview(details: IPlatformContentDetails?, paused: Boolean) = Unit; + override fun stopPreview() = Unit; + override fun pausePreview() = Unit; + override fun resumePreview() = Unit; + + companion object { + private val TAG = "PreviewChannelViewHolder" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewContentListAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewContentListAdapter.kt index 225cf6d7..35dd2d7b 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewContentListAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewContentListAdapter.kt @@ -23,6 +23,7 @@ import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder import com.futo.platformplayer.views.adapters.EmptyPreviewViewHolder import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader +import okhttp3.internal.platform.Platform class PreviewContentListAdapter : InsertedViewAdapterWithLoader { private var _initialPlay = true; @@ -82,6 +83,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader createPlaylistViewHolder(viewGroup); ContentType.NESTED_VIDEO -> createNestedViewHolder(viewGroup); ContentType.LOCKED -> createLockedViewHolder(viewGroup); + ContentType.CHANNEL -> createChannelViewHolder(viewGroup) else -> EmptyPreviewViewHolder(viewGroup) } } @@ -115,6 +117,10 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader