mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-20 01:11:34 +00:00
Merge branch 'article-web-support' into 'master'
Article/web support See merge request videostreaming/grayjay!106
This commit is contained in:
commit
43a373eceb
23 changed files with 1648 additions and 20 deletions
|
@ -32,7 +32,8 @@ let Type = {
|
|||
Text: {
|
||||
RAW: 0,
|
||||
HTML: 1,
|
||||
MARKUP: 2
|
||||
MARKUP: 2,
|
||||
CODE: 3
|
||||
},
|
||||
Chapter: {
|
||||
NORMAL: 0,
|
||||
|
@ -291,15 +292,39 @@ class PlatformPostDetails extends PlatformPost {
|
|||
}
|
||||
}
|
||||
|
||||
class PlatformArticleDetails extends PlatformContent {
|
||||
class PlatformWeb extends PlatformContent {
|
||||
constructor(obj) {
|
||||
super(obj, 7);
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "PlatformWeb";
|
||||
}
|
||||
}
|
||||
class PlatformWebDetails extends PlatformWeb {
|
||||
constructor(obj) {
|
||||
super(obj, 7);
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "PlatformWebDetails";
|
||||
this.html = obj.html;
|
||||
}
|
||||
}
|
||||
|
||||
class PlatformArticle extends PlatformContent {
|
||||
constructor(obj) {
|
||||
super(obj, 3);
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "PlatformArticle";
|
||||
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||
this.summary = obj.summary ?? "";
|
||||
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
|
||||
}
|
||||
}
|
||||
class PlatformArticleDetails extends PlatformArticle {
|
||||
constructor(obj) {
|
||||
super(obj, 3);
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "PlatformArticleDetails";
|
||||
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||
this.summary = obj.summary ?? "";
|
||||
this.segments = obj.segments ?? [];
|
||||
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
|
||||
}
|
||||
}
|
||||
class ArticleSegment {
|
||||
|
@ -315,9 +340,10 @@ class ArticleTextSegment extends ArticleSegment {
|
|||
}
|
||||
}
|
||||
class ArticleImagesSegment extends ArticleSegment {
|
||||
constructor(images) {
|
||||
constructor(images, caption) {
|
||||
super(2);
|
||||
this.images = images;
|
||||
this.caption = caption;
|
||||
}
|
||||
}
|
||||
class ArticleNestedSegment extends ArticleSegment {
|
||||
|
|
|
@ -25,7 +25,6 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
|
@ -45,6 +44,7 @@ import com.futo.platformplayer.casting.StateCasting
|
|||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ArticleDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||
|
@ -73,6 +73,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.TutorialFragment
|
|||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment.State
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
||||
|
@ -152,6 +153,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
//Frags Main
|
||||
lateinit var _fragMainHome: HomeFragment;
|
||||
lateinit var _fragPostDetail: PostDetailFragment;
|
||||
lateinit var _fragArticleDetail: ArticleDetailFragment;
|
||||
lateinit var _fragWebDetail: WebDetailFragment;
|
||||
lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment;
|
||||
lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment;
|
||||
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
|
||||
|
@ -330,6 +333,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
_fragMainPlaylist = PlaylistFragment.newInstance();
|
||||
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
|
||||
_fragPostDetail = PostDetailFragment.newInstance();
|
||||
_fragArticleDetail = ArticleDetailFragment.newInstance();
|
||||
_fragWebDetail = WebDetailFragment.newInstance();
|
||||
_fragWatchlist = WatchLaterFragment.newInstance();
|
||||
_fragHistory = HistoryFragment.newInstance();
|
||||
_fragSourceDetail = SourceDetailFragment.newInstance();
|
||||
|
@ -456,6 +461,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
_fragMainPlaylist.topBar = _fragTopBarNavigation;
|
||||
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
|
||||
_fragPostDetail.topBar = _fragTopBarNavigation;
|
||||
_fragArticleDetail.topBar = _fragTopBarNavigation;
|
||||
_fragWebDetail.topBar = _fragTopBarNavigation;
|
||||
_fragWatchlist.topBar = _fragTopBarNavigation;
|
||||
_fragHistory.topBar = _fragTopBarNavigation;
|
||||
_fragSourceDetail.topBar = _fragTopBarNavigation;
|
||||
|
@ -855,7 +862,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
|
||||
return withContext(Dispatchers.IO) {
|
||||
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||
if (StatePlatform.instance.hasEnabledContentClient(url)) {
|
||||
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||
withContext(Dispatchers.Main) {
|
||||
if (position > 0)
|
||||
|
@ -1240,6 +1247,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
PlaylistFragment::class -> _fragMainPlaylist as T;
|
||||
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
|
||||
PostDetailFragment::class -> _fragPostDetail as T;
|
||||
ArticleDetailFragment::class -> _fragArticleDetail as T;
|
||||
WebDetailFragment::class -> _fragWebDetail as T;
|
||||
WatchLaterFragment::class -> _fragWatchlist as T;
|
||||
HistoryFragment::class -> _fragHistory as T;
|
||||
SourceDetailFragment::class -> _fragSourceDetail as T;
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.futo.platformplayer.api.media.models.article
|
||||
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
|
||||
interface IPlatformArticle: IPlatformContent {
|
||||
val summary: String?;
|
||||
val thumbnails: Thumbnails?;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.futo.platformplayer.api.media.models.article
|
||||
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
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.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSArticleSegment
|
||||
|
||||
interface IPlatformArticleDetails: IPlatformContent, IPlatformArticle, IPlatformContentDetails {
|
||||
val segments: List<IJSArticleSegment>;
|
||||
val rating : IRating;
|
||||
}
|
|
@ -8,6 +8,7 @@ enum class ContentType(val value: Int) {
|
|||
POST(2),
|
||||
ARTICLE(3),
|
||||
PLAYLIST(4),
|
||||
WEB(7),
|
||||
|
||||
URL(9),
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@ import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
|
|||
enum class TextType(val value: Int) {
|
||||
RAW(0),
|
||||
HTML(1),
|
||||
MARKUP(2);
|
||||
MARKUP(2),
|
||||
CODE(3);
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int): TextType
|
||||
|
|
|
@ -27,7 +27,9 @@ interface IJSContent: IPlatformContent {
|
|||
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
|
||||
ContentType.PLAYLIST -> JSPlaylist(config, obj);
|
||||
ContentType.LOCKED -> JSLockedContent(config, obj);
|
||||
ContentType.CHANNEL -> JSChannelContent(config, obj)
|
||||
ContentType.CHANNEL -> JSChannelContent(config, obj);
|
||||
ContentType.ARTICLE -> JSArticle(config, obj);
|
||||
ContentType.WEB -> JSWeb(config, obj);
|
||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ interface IJSContentDetails: IPlatformContent {
|
|||
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
||||
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
||||
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
|
||||
ContentType.WEB -> JSWebDetails(plugin, obj);
|
||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package com.futo.platformplayer.api.media.platforms.js.models
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
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.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||
import com.futo.platformplayer.api.media.models.post.TextType
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
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.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullableList
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
open class JSArticle : JSContent, IPlatformArticle, IPluginSourced {
|
||||
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
||||
|
||||
override val summary: String;
|
||||
override val thumbnails: Thumbnails?;
|
||||
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||
val contextName = "PlatformArticle";
|
||||
|
||||
summary = _content.getOrDefault(config, "summary", contextName, "") ?: "";
|
||||
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
|
||||
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
|
||||
import com.futo.platformplayer.api.media.models.article.IPlatformArticleDetails
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
|
@ -21,20 +23,20 @@ import com.futo.platformplayer.getOrThrow
|
|||
import com.futo.platformplayer.getOrThrowNullableList
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
|
||||
open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
|
||||
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
||||
|
||||
private val _hasGetComments: Boolean;
|
||||
private val _hasGetContentRecommendations: Boolean;
|
||||
|
||||
val rating: IRating;
|
||||
override val rating: IRating;
|
||||
|
||||
val summary: String;
|
||||
val thumbnails: Thumbnails?;
|
||||
val segments: List<IJSArticleSegment>;
|
||||
override val summary: String;
|
||||
override val thumbnails: Thumbnails?;
|
||||
override val segments: List<IJSArticleSegment>;
|
||||
|
||||
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
|
||||
val contextName = "PlatformPost";
|
||||
val contextName = "PlatformArticle";
|
||||
|
||||
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
|
||||
summary = _content.getOrThrow(client.config, "summary", contextName);
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package com.futo.platformplayer.api.media.platforms.js.models
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
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.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||
import com.futo.platformplayer.api.media.models.post.TextType
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
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.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullableList
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
open class JSWeb : JSContent, IPluginSourced {
|
||||
final override val contentType: ContentType get() = ContentType.WEB;
|
||||
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||
val contextName = "PlatformWeb";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package com.futo.platformplayer.api.media.platforms.js.models
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
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.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.post.TextType
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
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.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullableList
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
open class JSWebDetails : JSContent, IPluginSourced, IPlatformContentDetails {
|
||||
final override val contentType: ContentType get() = ContentType.WEB;
|
||||
|
||||
val html: String?;
|
||||
//TODO: Options?
|
||||
|
||||
|
||||
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
|
||||
val contextName = "PlatformWeb";
|
||||
|
||||
html = obj.getOrDefault(client.config, "html", contextName, null);
|
||||
}
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
||||
|
||||
}
|
|
@ -12,6 +12,7 @@ import com.futo.platformplayer.logging.Logger
|
|||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClientConstants
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
|
@ -77,6 +78,22 @@ class PackageBridge : V8Package {
|
|||
return "android";
|
||||
}
|
||||
|
||||
@V8Property
|
||||
fun supportedContent(): Array<Int> {
|
||||
return arrayOf(
|
||||
ContentType.MEDIA.value,
|
||||
ContentType.POST.value,
|
||||
ContentType.PLAYLIST.value,
|
||||
ContentType.WEB.value,
|
||||
ContentType.URL.value,
|
||||
ContentType.NESTED_VIDEO.value,
|
||||
ContentType.CHANNEL.value,
|
||||
ContentType.LOCKED.value,
|
||||
ContentType.PLACEHOLDER.value,
|
||||
ContentType.DEFERRED.value
|
||||
)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun dispose(value: V8Value) {
|
||||
Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
|
||||
|
|
|
@ -0,0 +1,785 @@
|
|||
package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
|
||||
import com.futo.platformplayer.api.media.models.article.IPlatformArticleDetails
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.locked.IPlatformLockedContent
|
||||
import com.futo.platformplayer.api.media.models.nested.IPlatformNestedContent
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPostDetails
|
||||
import com.futo.platformplayer.api.media.models.post.TextType
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSArticleDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSImagesSegment
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSNestedSegment
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSTextSegment
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.SegmentType
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fixHtmlWhitespace
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewLockedView
|
||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewNestedVideoView
|
||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView
|
||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoView
|
||||
import com.futo.platformplayer.views.comments.AddCommentView
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
import com.futo.platformplayer.views.segments.CommentsList
|
||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.ContentType
|
||||
import com.futo.polycentric.core.Models
|
||||
import com.futo.polycentric.core.Opinion
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||
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 com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import userpackage.Protocol
|
||||
import java.lang.Integer.min
|
||||
|
||||
class ArticleDetailFragment : MainFragment {
|
||||
override val isMainView: Boolean = true;
|
||||
override val isTab: Boolean = true;
|
||||
override val hasBottomBar: Boolean get() = true;
|
||||
|
||||
private var _viewDetail: ArticleDetailView? = null;
|
||||
|
||||
constructor() : super() { }
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = ArticleDetailView(inflater.context).applyFragment(this);
|
||||
_viewDetail = view;
|
||||
return view;
|
||||
}
|
||||
|
||||
override fun onDestroyMainView() {
|
||||
super.onDestroyMainView();
|
||||
_viewDetail?.onDestroy();
|
||||
_viewDetail = null;
|
||||
}
|
||||
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
super.onShownWithView(parameter, isBack);
|
||||
|
||||
if (parameter is IPlatformArticleDetails) {
|
||||
_viewDetail?.clear();
|
||||
_viewDetail?.setArticleDetails(parameter);
|
||||
} else if (parameter is IPlatformArticle) {
|
||||
_viewDetail?.setArticleOverview(parameter);
|
||||
} else if(parameter is String) {
|
||||
_viewDetail?.setPostUrl(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
private class ArticleDetailView : ConstraintLayout {
|
||||
private lateinit var _fragment: ArticleDetailFragment;
|
||||
private var _url: String? = null;
|
||||
private var _isLoading = false;
|
||||
private var _article: IPlatformArticleDetails? = null;
|
||||
private var _articleOverview: IPlatformArticle? = null;
|
||||
private var _polycentricProfile: PolycentricProfile? = null;
|
||||
private var _version = 0;
|
||||
private var _isRepliesVisible: Boolean = false;
|
||||
private var _repliesAnimator: ViewPropertyAnimator? = null;
|
||||
|
||||
private val _creatorThumbnail: CreatorThumbnail;
|
||||
private val _buttonSubscribe: SubscribeButton;
|
||||
private val _channelName: TextView;
|
||||
private val _channelMeta: TextView;
|
||||
private val _textTitle: TextView;
|
||||
private val _textMeta: TextView;
|
||||
private val _textSummary: TextView;
|
||||
private val _containerSegments: LinearLayout;
|
||||
private val _platformIndicator: PlatformIndicator;
|
||||
private val _buttonShare: ImageButton;
|
||||
|
||||
private val _layoutRating: LinearLayout;
|
||||
private val _imageLikeIcon: ImageView;
|
||||
private val _textLikes: TextView;
|
||||
private val _imageDislikeIcon: ImageView;
|
||||
private val _textDislikes: TextView;
|
||||
|
||||
private val _addCommentView: AddCommentView;
|
||||
|
||||
private val _rating: PillRatingLikesDislikes;
|
||||
|
||||
private val _layoutLoadingOverlay: FrameLayout;
|
||||
private val _imageLoader: ImageView;
|
||||
|
||||
private var _overlayContainer: FrameLayout
|
||||
private val _repliesOverlay: RepliesOverlay;
|
||||
|
||||
private val _commentsList: CommentsList;
|
||||
|
||||
private var _commentType: Boolean? = null;
|
||||
private val _buttonPolycentric: Button
|
||||
private val _buttonPlatform: Button
|
||||
|
||||
private val _taskLoadPost = if(!isInEditMode) TaskHandler<String, IPlatformArticleDetails>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{
|
||||
val result = StatePlatform.instance.getContentDetails(it).await();
|
||||
if(result !is IPlatformArticleDetails)
|
||||
throw IllegalStateException(context.getString(R.string.expected_media_content_found) + " ${result.contentType}");
|
||||
return@TaskHandler result;
|
||||
})
|
||||
.success { setArticleDetails(it) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, context.getString(R.string.failed_to_load_post), it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
|
||||
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
||||
|
||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, {
|
||||
if (!StatePolycentric.instance.enabled)
|
||||
return@TaskHandler null
|
||||
|
||||
ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!)
|
||||
})
|
||||
.success { it -> setPolycentricProfile(it, animate = true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load claims.", it);
|
||||
};
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
inflate(context, R.layout.fragview_article_detail, this);
|
||||
|
||||
val root = findViewById<FrameLayout>(R.id.root);
|
||||
|
||||
_creatorThumbnail = findViewById(R.id.creator_thumbnail);
|
||||
_buttonSubscribe = findViewById(R.id.button_subscribe);
|
||||
_channelName = findViewById(R.id.text_channel_name);
|
||||
_channelMeta = findViewById(R.id.text_channel_meta);
|
||||
_textTitle = findViewById(R.id.text_title);
|
||||
_textMeta = findViewById(R.id.text_meta);
|
||||
_textSummary = findViewById(R.id.text_summary);
|
||||
_containerSegments = findViewById(R.id.container_segments);
|
||||
_platformIndicator = findViewById(R.id.platform_indicator);
|
||||
_buttonShare = findViewById(R.id.button_share);
|
||||
|
||||
_overlayContainer = findViewById(R.id.overlay_container);
|
||||
|
||||
_layoutRating = findViewById(R.id.layout_rating);
|
||||
_imageLikeIcon = findViewById(R.id.image_like_icon);
|
||||
_textLikes = findViewById(R.id.text_likes);
|
||||
_imageDislikeIcon = findViewById(R.id.image_dislike_icon);
|
||||
_textDislikes = findViewById(R.id.text_dislikes);
|
||||
|
||||
_commentsList = findViewById(R.id.comments_list);
|
||||
_addCommentView = findViewById(R.id.add_comment_view);
|
||||
|
||||
_rating = findViewById(R.id.rating);
|
||||
|
||||
_layoutLoadingOverlay = findViewById(R.id.layout_loading_overlay);
|
||||
_imageLoader = findViewById(R.id.image_loader);
|
||||
|
||||
|
||||
_repliesOverlay = findViewById(R.id.replies_overlay);
|
||||
|
||||
_buttonPolycentric = findViewById(R.id.button_polycentric)
|
||||
_buttonPlatform = findViewById(R.id.button_platform)
|
||||
|
||||
_buttonSubscribe.onSubscribed.subscribe {
|
||||
//TODO: add overlay to layout
|
||||
//UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||
};
|
||||
|
||||
val layoutTop: LinearLayout = findViewById(R.id.layout_top);
|
||||
root.removeView(layoutTop);
|
||||
_commentsList.setPrependedView(layoutTop);
|
||||
|
||||
/*TODO: Why is this here?
|
||||
_commentsList.onCommentsLoaded.subscribe {
|
||||
updateCommentType(false);
|
||||
};*/
|
||||
|
||||
_commentsList.onRepliesClick.subscribe { c ->
|
||||
val replyCount = c.replyCount ?: 0;
|
||||
var metadata = "";
|
||||
if (replyCount > 0) {
|
||||
metadata += "$replyCount " + context.getString(R.string.replies);
|
||||
}
|
||||
|
||||
if (c is PolycentricPlatformComment) {
|
||||
var parentComment: PolycentricPlatformComment = c;
|
||||
_repliesOverlay.load(_commentType!!, metadata, c.contextUrl, c.reference, c,
|
||||
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
|
||||
{
|
||||
val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
|
||||
_commentsList.replaceComment(parentComment, newComment);
|
||||
parentComment = newComment;
|
||||
});
|
||||
} else {
|
||||
_repliesOverlay.load(_commentType!!, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
||||
}
|
||||
|
||||
setRepliesOverlayVisible(isVisible = true, animate = true);
|
||||
};
|
||||
|
||||
if (StatePolycentric.instance.enabled) {
|
||||
_buttonPolycentric.setOnClickListener {
|
||||
updateCommentType(false)
|
||||
}
|
||||
} else {
|
||||
_buttonPolycentric.visibility = View.GONE
|
||||
}
|
||||
|
||||
_buttonPlatform.setOnClickListener {
|
||||
updateCommentType(true)
|
||||
}
|
||||
|
||||
_addCommentView.onCommentAdded.subscribe {
|
||||
_commentsList.addComment(it);
|
||||
};
|
||||
|
||||
_repliesOverlay.onClose.subscribe { setRepliesOverlayVisible(isVisible = false, animate = true); };
|
||||
|
||||
_buttonShare.setOnClickListener { share() };
|
||||
|
||||
_creatorThumbnail.onClick.subscribe { openChannel() };
|
||||
_channelName.setOnClickListener { openChannel() };
|
||||
_channelMeta.setOnClickListener { openChannel() };
|
||||
}
|
||||
|
||||
private fun openChannel() {
|
||||
val author = _article?.author ?: _articleOverview?.author ?: return;
|
||||
_fragment.navigate<ChannelFragment>(author);
|
||||
}
|
||||
|
||||
private fun share() {
|
||||
try {
|
||||
Logger.i(PreviewPostView.TAG, "sharePost")
|
||||
|
||||
val url = _article?.shareUrl ?: _articleOverview?.shareUrl ?: _url;
|
||||
_fragment.startActivity(Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND;
|
||||
putExtra(Intent.EXTRA_TEXT, url);
|
||||
type = "text/plain"; //TODO: Determine alt types?
|
||||
}, null));
|
||||
} catch (e: Throwable) {
|
||||
//Ignored
|
||||
Logger.e(PreviewPostView.TAG, "Failed to share.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePolycentricRating() {
|
||||
_rating.visibility = View.GONE;
|
||||
|
||||
val ref = Models.referenceFromBuffer((_article?.url ?: _articleOverview?.url)?.toByteArray() ?: return)
|
||||
val extraBytesRef = (_article?.id?.value ?: _articleOverview?.id?.value)?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||
val version = _version;
|
||||
|
||||
_rating.onLikeDislikeUpdated.remove(this);
|
||||
|
||||
if (!StatePolycentric.instance.enabled)
|
||||
return
|
||||
|
||||
_fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
if (version != _version) {
|
||||
return@launch;
|
||||
}
|
||||
|
||||
try {
|
||||
val queryReferencesResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, ref, null,null,
|
||||
arrayListOf(
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
||||
ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.like.data)).build(),
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
||||
ContentType.OPINION.value).setValue(
|
||||
ByteString.copyFrom(Opinion.dislike.data)).build()
|
||||
),
|
||||
extraByteReferences = listOfNotNull(extraBytesRef)
|
||||
);
|
||||
|
||||
if (version != _version) {
|
||||
return@launch;
|
||||
}
|
||||
|
||||
val likes = queryReferencesResponse.countsList[0];
|
||||
val dislikes = queryReferencesResponse.countsList[1];
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (version != _version) {
|
||||
return@withContext;
|
||||
}
|
||||
|
||||
_rating.visibility = VISIBLE;
|
||||
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
||||
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
||||
if (args.hasLiked) {
|
||||
args.processHandle.opinion(ref, Opinion.like);
|
||||
} else if (args.hasDisliked) {
|
||||
args.processHandle.opinion(ref, Opinion.dislike);
|
||||
} else {
|
||||
args.processHandle.opinion(ref, Opinion.neutral);
|
||||
}
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
Logger.i(TAG, "Started backfill");
|
||||
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
||||
Logger.i(TAG, "Finished backfill");
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to backfill servers", e)
|
||||
}
|
||||
}
|
||||
|
||||
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
|
||||
};
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e);
|
||||
_rating.visibility = View.GONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPlatformRating(rating: IRating?) {
|
||||
if (rating == null) {
|
||||
_layoutRating.visibility = View.GONE;
|
||||
return;
|
||||
}
|
||||
|
||||
_layoutRating.visibility = View.VISIBLE;
|
||||
|
||||
when (rating) {
|
||||
is RatingLikeDislikes -> {
|
||||
_textLikes.visibility = View.VISIBLE;
|
||||
_imageLikeIcon.visibility = View.VISIBLE;
|
||||
_textLikes.text = rating.likes.toHumanNumber();
|
||||
|
||||
_imageDislikeIcon.visibility = View.VISIBLE;
|
||||
_textDislikes.visibility = View.VISIBLE;
|
||||
_textDislikes.text = rating.dislikes.toHumanNumber();
|
||||
}
|
||||
is RatingLikes -> {
|
||||
_textLikes.visibility = View.VISIBLE;
|
||||
_imageLikeIcon.visibility = View.VISIBLE;
|
||||
_textLikes.text = rating.likes.toHumanNumber();
|
||||
|
||||
_imageDislikeIcon.visibility = View.GONE;
|
||||
_textDislikes.visibility = View.GONE;
|
||||
}
|
||||
else -> {
|
||||
_textLikes.visibility = View.GONE;
|
||||
_imageLikeIcon.visibility = View.GONE;
|
||||
_imageDislikeIcon.visibility = View.GONE;
|
||||
_textDislikes.visibility = View.GONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyFragment(frag: ArticleDetailFragment): ArticleDetailView {
|
||||
_fragment = frag;
|
||||
return this;
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_commentsList.cancel();
|
||||
_taskLoadPost.cancel();
|
||||
_taskLoadPolycentricProfile.cancel();
|
||||
_version++;
|
||||
|
||||
updateCommentType(null)
|
||||
_url = null;
|
||||
_article = null;
|
||||
_articleOverview = null;
|
||||
_creatorThumbnail.clear();
|
||||
//_buttonSubscribe.setSubscribeChannel(null); TODO: clear button
|
||||
_channelName.text = "";
|
||||
setChannelMeta(null);
|
||||
_textTitle.text = "";
|
||||
_textMeta.text = "";
|
||||
setPlatformRating(null);
|
||||
_polycentricProfile = null;
|
||||
_rating.visibility = View.GONE;
|
||||
updatePolycentricRating();
|
||||
setRepliesOverlayVisible(isVisible = false, animate = false);
|
||||
|
||||
_containerSegments.removeAllViews();
|
||||
|
||||
_addCommentView.setContext(null, null);
|
||||
_platformIndicator.clearPlatform();
|
||||
}
|
||||
|
||||
fun setArticleDetails(value: IPlatformArticleDetails) {
|
||||
_url = value.url;
|
||||
_article = value;
|
||||
|
||||
_creatorThumbnail.setThumbnail(value.author.thumbnail, false);
|
||||
_buttonSubscribe.setSubscribeChannel(value.author.url);
|
||||
_channelName.text = value.author.name;
|
||||
setChannelMeta(value);
|
||||
_textTitle.text = value.name;
|
||||
_textMeta.text = value.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "" //TODO: Include view count?
|
||||
|
||||
_textSummary.text = value.summary
|
||||
_textSummary.isVisible = !value.summary.isNullOrEmpty()
|
||||
|
||||
_platformIndicator.setPlatformFromClientID(value.id.pluginId);
|
||||
setPlatformRating(value.rating);
|
||||
|
||||
for(seg in value.segments) {
|
||||
when(seg.type) {
|
||||
SegmentType.TEXT -> {
|
||||
if(seg is JSTextSegment) {
|
||||
_containerSegments.addView(ArticleTextBlock(context, seg.content, seg.textType))
|
||||
}
|
||||
}
|
||||
SegmentType.IMAGES -> {
|
||||
if(seg is JSImagesSegment) {
|
||||
if(seg.images.size > 0)
|
||||
_containerSegments.addView(ArticleImageBlock(context, seg.images[0], seg.caption))
|
||||
}
|
||||
}
|
||||
SegmentType.NESTED -> {
|
||||
if(seg is JSNestedSegment) {
|
||||
_containerSegments.addView(ArticleContentBlock(context, seg.nested, _fragment, _overlayContainer));
|
||||
}
|
||||
}
|
||||
else ->{}
|
||||
}
|
||||
}
|
||||
|
||||
//Fetch only when not already called in setPostOverview
|
||||
if (_articleOverview == null) {
|
||||
fetchPolycentricProfile();
|
||||
updatePolycentricRating();
|
||||
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||
}
|
||||
|
||||
val commentType = !Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1
|
||||
updateCommentType(commentType, true);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
fun setArticleOverview(value: IPlatformArticle) {
|
||||
clear();
|
||||
_url = value.url;
|
||||
_articleOverview = value;
|
||||
|
||||
_creatorThumbnail.setThumbnail(value.author.thumbnail, false);
|
||||
_buttonSubscribe.setSubscribeChannel(value.author.url);
|
||||
_channelName.text = value.author.name;
|
||||
setChannelMeta(value);
|
||||
_textTitle.text = value.name;
|
||||
_textMeta.text = value.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "" //TODO: Include view count?
|
||||
|
||||
_platformIndicator.setPlatformFromClientID(value.id.pluginId);
|
||||
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||
|
||||
updatePolycentricRating();
|
||||
fetchPolycentricProfile();
|
||||
fetchPost();
|
||||
}
|
||||
|
||||
|
||||
private fun setRepliesOverlayVisible(isVisible: Boolean, animate: Boolean) {
|
||||
if (_isRepliesVisible == isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isRepliesVisible = isVisible;
|
||||
_repliesAnimator?.cancel();
|
||||
|
||||
if (isVisible) {
|
||||
_repliesOverlay.visibility = View.VISIBLE;
|
||||
|
||||
if (animate) {
|
||||
_repliesOverlay.translationY = _repliesOverlay.height.toFloat();
|
||||
|
||||
_repliesAnimator = _repliesOverlay.animate()
|
||||
.setDuration(300)
|
||||
.translationY(0f)
|
||||
.withEndAction {
|
||||
_repliesAnimator = null;
|
||||
}.apply { start() };
|
||||
}
|
||||
} else {
|
||||
if (animate) {
|
||||
_repliesOverlay.translationY = 0f;
|
||||
|
||||
_repliesAnimator = _repliesOverlay.animate()
|
||||
.setDuration(300)
|
||||
.translationY(_repliesOverlay.height.toFloat())
|
||||
.withEndAction {
|
||||
_repliesOverlay.visibility = GONE;
|
||||
_repliesAnimator = null;
|
||||
}.apply { start(); }
|
||||
} else {
|
||||
_repliesOverlay.visibility = View.GONE;
|
||||
_repliesOverlay.translationY = _repliesOverlay.height.toFloat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchPolycentricProfile() {
|
||||
val author = _article?.author ?: _articleOverview?.author ?: return;
|
||||
setPolycentricProfile(null, animate = false);
|
||||
_taskLoadPolycentricProfile.run(author.id);
|
||||
}
|
||||
|
||||
private fun setChannelMeta(value: IPlatformArticle?) {
|
||||
val subscribers = value?.author?.subscribers;
|
||||
if(subscribers != null && subscribers > 0) {
|
||||
_channelMeta.visibility = View.VISIBLE;
|
||||
_channelMeta.text = if((value.author.subscribers ?: 0) > 0) value.author.subscribers!!.toHumanNumber() + " " + context.getString(R.string.subscribers) else "";
|
||||
} else {
|
||||
_channelMeta.visibility = View.GONE;
|
||||
_channelMeta.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
fun setPostUrl(url: String) {
|
||||
clear();
|
||||
_url = url;
|
||||
fetchPost();
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
_commentsList.cancel();
|
||||
_taskLoadPost.cancel();
|
||||
_repliesOverlay.cleanup();
|
||||
}
|
||||
|
||||
private fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
|
||||
_polycentricProfile = polycentricProfile;
|
||||
|
||||
val pp = _polycentricProfile;
|
||||
if (pp == null) {
|
||||
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
||||
return;
|
||||
}
|
||||
|
||||
_creatorThumbnail.setHarborAvailable(true, animate, pp.system.toProto());
|
||||
}
|
||||
|
||||
private fun fetchPost() {
|
||||
Logger.i(TAG, "fetchVideo")
|
||||
_article = null;
|
||||
|
||||
val url = _url;
|
||||
if (!url.isNullOrBlank()) {
|
||||
setLoading(true);
|
||||
_taskLoadPost.run(url);
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchComments() {
|
||||
Logger.i(TAG, "fetchComments")
|
||||
_article?.let {
|
||||
_commentsList.load(true) { StatePlatform.instance.getComments(it); };
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchPolycentricComments() {
|
||||
Logger.i(TAG, "fetchPolycentricComments")
|
||||
val post = _article;
|
||||
val ref = (_article?.url ?: _articleOverview?.url)?.toByteArray()?.let { Models.referenceFromBuffer(it) }
|
||||
val extraBytesRef = (_article?.id?.value ?: _articleOverview?.id?.value)?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||
|
||||
if (ref == null) {
|
||||
Logger.w(TAG, "Failed to fetch polycentric comments because url was not set null")
|
||||
_commentsList.clear();
|
||||
return
|
||||
}
|
||||
|
||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); };
|
||||
}
|
||||
|
||||
private fun updateCommentType(commentType: Boolean?, forceReload: Boolean = false) {
|
||||
val changed = commentType != _commentType
|
||||
_commentType = commentType
|
||||
|
||||
if (commentType == null) {
|
||||
_buttonPlatform.setTextColor(resources.getColor(R.color.gray_ac))
|
||||
_buttonPolycentric.setTextColor(resources.getColor(R.color.gray_ac))
|
||||
} else {
|
||||
_buttonPlatform.setTextColor(resources.getColor(if (commentType) R.color.white else R.color.gray_ac))
|
||||
_buttonPolycentric.setTextColor(resources.getColor(if (!commentType) R.color.white else R.color.gray_ac))
|
||||
|
||||
if (commentType) {
|
||||
_addCommentView.visibility = View.GONE;
|
||||
|
||||
if (forceReload || changed) {
|
||||
fetchComments();
|
||||
}
|
||||
} else {
|
||||
_addCommentView.visibility = View.VISIBLE;
|
||||
|
||||
if (forceReload || changed) {
|
||||
fetchPolycentricComments()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading : Boolean) {
|
||||
if (_isLoading == isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = isLoading;
|
||||
|
||||
if(isLoading) {
|
||||
(_imageLoader.drawable as Animatable?)?.start()
|
||||
_layoutLoadingOverlay.visibility = View.VISIBLE;
|
||||
}
|
||||
else {
|
||||
_layoutLoadingOverlay.visibility = View.GONE;
|
||||
(_imageLoader.drawable as Animatable?)?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
class ArticleTextBlock : LinearLayout {
|
||||
constructor(context: Context?, content: String, textType: TextType) : super(context){
|
||||
inflate(context, R.layout.view_segment_text, this);
|
||||
|
||||
findViewById<TextView>(R.id.text_content)?.let {
|
||||
if(textType == TextType.HTML)
|
||||
it.text = Html.fromHtml(content, Html.FROM_HTML_MODE_COMPACT);
|
||||
else if(textType == TextType.CODE) {
|
||||
it.text = content;
|
||||
it.setPadding(15.dp(resources));
|
||||
it.setHorizontallyScrolling(true);
|
||||
it.movementMethod = ScrollingMovementMethod();
|
||||
it.setTypeface(Typeface.MONOSPACE);
|
||||
it.setBackgroundResource(R.drawable.background_videodetail_description)
|
||||
}
|
||||
else
|
||||
it.text = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
class ArticleImageBlock: LinearLayout {
|
||||
constructor(context: Context?, image: String, caption: String? = null) : super(context){
|
||||
inflate(context, R.layout.view_segment_image, this);
|
||||
|
||||
findViewById<ImageView>(R.id.image_content)?.let {
|
||||
Glide.with(it)
|
||||
.load(image)
|
||||
.crossfade()
|
||||
.into(it);
|
||||
}
|
||||
findViewById<TextView>(R.id.text_content)?.let {
|
||||
if(caption?.isNullOrEmpty() == true)
|
||||
it.isVisible = false;
|
||||
else
|
||||
it.text = caption;
|
||||
}
|
||||
}
|
||||
}
|
||||
class ArticleContentBlock: LinearLayout {
|
||||
constructor(context: Context, content: IPlatformContent?, fragment: ArticleDetailFragment? = null, overlayContainer: FrameLayout? = null): super(context) {
|
||||
if(content != null) {
|
||||
var view: View? = null;
|
||||
if(content is IPlatformNestedContent) {
|
||||
view = PreviewNestedVideoView(context, FeedStyle.THUMBNAIL, null);
|
||||
view.bind(content);
|
||||
view.onContentUrlClicked.subscribe { a,b -> }
|
||||
}
|
||||
else if(content is IPlatformVideo) {
|
||||
view = PreviewVideoView(context, FeedStyle.THUMBNAIL, null, true);
|
||||
view.bind(content);
|
||||
view.onVideoClicked.subscribe { a,b -> fragment?.navigate<VideoDetailFragment>(a) }
|
||||
view.onChannelClicked.subscribe { a -> fragment?.navigate<ChannelFragment>(a) }
|
||||
if(overlayContainer != null) {
|
||||
view.onAddToClicked.subscribe { a -> UISlideOverlays.showVideoOptionsOverlay(a, overlayContainer) };
|
||||
}
|
||||
view.onAddToQueueClicked.subscribe { a -> StatePlayer.instance.addToQueue(a) }
|
||||
view.onAddToWatchLaterClicked.subscribe { a ->
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
}
|
||||
}
|
||||
else if(content is IPlatformPost) {
|
||||
view = PreviewPostView(context, FeedStyle.THUMBNAIL);
|
||||
view.bind(content);
|
||||
view.onContentClicked.subscribe { a -> fragment?.navigate<PostDetailFragment>(a) }
|
||||
view.onChannelClicked.subscribe { a -> fragment?.navigate<ChannelFragment>(a) }
|
||||
}
|
||||
else if(content is IPlatformArticle) {
|
||||
view = PreviewPostView(context, FeedStyle.THUMBNAIL);
|
||||
view.bind(content);
|
||||
view.onContentClicked.subscribe { a -> fragment?.navigate<ArticleDetailFragment>(a) }
|
||||
view.onChannelClicked.subscribe { a -> fragment?.navigate<ChannelFragment>(a) }
|
||||
}
|
||||
else if(content is IPlatformLockedContent) {
|
||||
view = PreviewLockedView(context, FeedStyle.THUMBNAIL);
|
||||
view.bind(content);
|
||||
}
|
||||
if(view != null)
|
||||
addView(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
const val TAG = "PostDetailFragment"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = ArticleDetailFragment().apply {}
|
||||
}
|
||||
}
|
|
@ -10,12 +10,14 @@ import com.futo.platformplayer.R
|
|||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSWeb
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
|
@ -196,7 +198,14 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||
fragment.navigate<RemotePlaylistFragment>(content);
|
||||
} else if (content is IPlatformPost) {
|
||||
fragment.navigate<PostDetailFragment>(content);
|
||||
} else if(content is IPlatformArticle) {
|
||||
fragment.navigate<ArticleDetailFragment>(content);
|
||||
}
|
||||
else if(content is JSWeb) {
|
||||
fragment.navigate<WebDetailFragment>(content);
|
||||
}
|
||||
else
|
||||
UIDialogs.appToast("Unknown content type [" + content.contentType.name + "]");
|
||||
}
|
||||
protected open fun onContentUrlClicked(url: String, contentType: ContentType) {
|
||||
when(contentType) {
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.children
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.article.IPlatformArticleDetails
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPostDetails
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSWeb
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSWebDetails
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fixHtmlWhitespace
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.platformplayer.views.adapters.ChannelTab
|
||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView
|
||||
import com.futo.platformplayer.views.comments.AddCommentView
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
import com.futo.platformplayer.views.segments.CommentsList
|
||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.ContentType
|
||||
import com.futo.polycentric.core.Models
|
||||
import com.futo.polycentric.core.Opinion
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||
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 com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import userpackage.Protocol
|
||||
import java.lang.Integer.min
|
||||
|
||||
class WebDetailFragment : MainFragment {
|
||||
override val isMainView: Boolean = true;
|
||||
override val isTab: Boolean = true;
|
||||
override val hasBottomBar: Boolean get() = true;
|
||||
|
||||
private var _viewDetail: WebDetailView? = null;
|
||||
|
||||
constructor() : super() { }
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = WebDetailView(inflater.context).applyFragment(this);
|
||||
_viewDetail = view;
|
||||
return view;
|
||||
}
|
||||
|
||||
override fun onDestroyMainView() {
|
||||
super.onDestroyMainView();
|
||||
_viewDetail?.onDestroy();
|
||||
_viewDetail = null;
|
||||
}
|
||||
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
super.onShownWithView(parameter, isBack);
|
||||
|
||||
if (parameter is JSWeb) {
|
||||
_viewDetail?.clear();
|
||||
_viewDetail?.setWeb(parameter);
|
||||
}
|
||||
if (parameter is JSWebDetails) {
|
||||
_viewDetail?.clear();
|
||||
_viewDetail?.setWebDetails(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
private class WebDetailView : ConstraintLayout {
|
||||
private lateinit var _fragment: WebDetailFragment;
|
||||
private var _url: String? = null;
|
||||
private var _isLoading = false;
|
||||
private var _web: JSWebDetails? = null;
|
||||
|
||||
private val _layoutLoadingOverlay: FrameLayout;
|
||||
private val _imageLoader: ImageView;
|
||||
|
||||
private val _webview: WebView;
|
||||
|
||||
private val _taskLoadPost = if(!isInEditMode) TaskHandler<String, JSWebDetails>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{
|
||||
val result = StatePlatform.instance.getContentDetails(it).await();
|
||||
if(result !is JSWebDetails)
|
||||
throw IllegalStateException(context.getString(R.string.expected_media_content_found) + " ${result.contentType}");
|
||||
return@TaskHandler result;
|
||||
})
|
||||
.success { setWebDetails(it) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, context.getString(R.string.failed_to_load_post), it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
|
||||
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
||||
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
inflate(context, R.layout.fragview_web_detail, this);
|
||||
|
||||
val root = findViewById<FrameLayout>(R.id.root);
|
||||
|
||||
_layoutLoadingOverlay = findViewById(R.id.layout_loading_overlay);
|
||||
_imageLoader = findViewById(R.id.image_loader);
|
||||
|
||||
_webview = findViewById(R.id.webview);
|
||||
_webview.webViewClient = object: WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url);
|
||||
if(url != "about:blank")
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun applyFragment(frag: WebDetailFragment): WebDetailView {
|
||||
_fragment = frag;
|
||||
return this;
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_webview.loadUrl("about:blank");
|
||||
}
|
||||
|
||||
fun setWeb(value: JSWeb) {
|
||||
_url = value.url;
|
||||
setLoading(true);
|
||||
clear();
|
||||
fetchPost();
|
||||
}
|
||||
fun setWebDetails(value: JSWebDetails) {
|
||||
_web = value;
|
||||
setLoading(true);
|
||||
_webview.loadUrl("about:blank");
|
||||
if(!value.html.isNullOrEmpty())
|
||||
_webview.loadData(value.html, "text/html", null);
|
||||
else
|
||||
_webview.loadUrl(value.url ?: "about:blank");
|
||||
}
|
||||
|
||||
private fun fetchPost() {
|
||||
Logger.i(WebDetailView.TAG, "fetchWeb")
|
||||
_web = null;
|
||||
|
||||
val url = _url;
|
||||
if (!url.isNullOrBlank()) {
|
||||
setLoading(true);
|
||||
_taskLoadPost.run(url);
|
||||
}
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
_webview.loadUrl("about:blank");
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading : Boolean) {
|
||||
if (_isLoading == isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = isLoading;
|
||||
|
||||
if(isLoading) {
|
||||
(_imageLoader.drawable as Animatable?)?.start()
|
||||
_layoutLoadingOverlay.visibility = View.VISIBLE;
|
||||
}
|
||||
else {
|
||||
_layoutLoadingOverlay.visibility = View.GONE;
|
||||
(_imageLoader.drawable as Animatable?)?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "WebDetailFragment"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = WebDetailFragment().apply {}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ import androidx.collection.LruCache
|
|||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.PlatformMultiClientPool
|
||||
|
@ -46,7 +45,6 @@ import com.futo.platformplayer.logging.Logger
|
|||
import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringArrayStorage
|
||||
import com.futo.platformplayer.stores.StringStorage
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
@ -56,7 +54,6 @@ import kotlinx.coroutines.cancel
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.internal.concat
|
||||
import java.lang.Thread.sleep
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.streams.asSequence
|
||||
|
@ -669,7 +666,7 @@ class StatePlatform {
|
|||
|
||||
|
||||
//Video
|
||||
fun hasEnabledVideoClient(url: String) : Boolean = getEnabledClients().any { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) };
|
||||
fun hasEnabledContentClient(url: String) : Boolean = getEnabledClients().any { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) };
|
||||
fun getContentClient(url: String) : IPlatformClient = getContentClientOrNull(url)
|
||||
?: throw NoPlatformClientException("No client enabled that supports this content url (${url})");
|
||||
fun getContentClientOrNull(url: String) : IPlatformClient? = getEnabledClients().find { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) };
|
||||
|
|
|
@ -79,6 +79,8 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
|||
return when(contentType) {
|
||||
ContentType.PLACEHOLDER -> createPlaceholderViewHolder(viewGroup);
|
||||
ContentType.MEDIA -> createVideoPreviewViewHolder(viewGroup);
|
||||
ContentType.ARTICLE -> createPostViewHolder(viewGroup);
|
||||
ContentType.WEB -> createPostViewHolder(viewGroup);
|
||||
ContentType.POST -> createPostViewHolder(viewGroup);
|
||||
ContentType.PLAYLIST -> createPlaylistViewHolder(viewGroup);
|
||||
ContentType.NESTED_VIDEO -> createNestedViewHolder(viewGroup);
|
||||
|
|
|
@ -21,9 +21,11 @@ import com.futo.platformplayer.R
|
|||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPostDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSWeb
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
|
@ -141,6 +143,16 @@ class PreviewPostView : LinearLayout {
|
|||
content.content
|
||||
else
|
||||
""
|
||||
} else if(content is IPlatformArticle) {
|
||||
if(!content.summary.isNullOrEmpty())
|
||||
content.summary ?: ""
|
||||
else
|
||||
""
|
||||
} else if(content is JSWeb) {
|
||||
if(!content.url.isNullOrEmpty())
|
||||
"WEB:" + content.url
|
||||
else
|
||||
""
|
||||
} else "";
|
||||
|
||||
if (content.name.isNullOrEmpty()) {
|
||||
|
@ -154,7 +166,14 @@ class PreviewPostView : LinearLayout {
|
|||
|
||||
if (content is IPlatformPost) {
|
||||
setImages(content.thumbnails.filterNotNull());
|
||||
} else {
|
||||
}
|
||||
else if(content is IPlatformArticle) {
|
||||
if(content.thumbnails != null)
|
||||
setImages(listOf(content.thumbnails!!));
|
||||
else
|
||||
setImages(null);
|
||||
}
|
||||
else {
|
||||
setImages(null);
|
||||
}
|
||||
|
||||
|
|
317
app/src/main/res/layout/fragview_article_detail.xml
Normal file
317
app/src/main/res/layout/fragview_article_detail.xml
Normal file
|
@ -0,0 +1,317 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="false"
|
||||
android:background="@drawable/bottom_menu_border"
|
||||
android:id="@+id/root"
|
||||
android:clickable="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_top"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginLeft="14dp"
|
||||
android:layout_marginRight="14dp"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_channel_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/button_subscribe">
|
||||
|
||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
android:id="@+id/creator_thumbnail"
|
||||
android:layout_width="27dp"
|
||||
android:layout_height="27dp"
|
||||
android:contentDescription="@string/cd_creator_thumbnail" />
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="6dp"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:id="@+id/text_channel_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
tools:text="Channel Name" />
|
||||
<TextView
|
||||
android:id="@+id/text_channel_meta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#ACACAC"
|
||||
android:textSize="9sp"
|
||||
android:layout_gravity="center"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
tools:text="" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||
android:id="@+id/button_subscribe"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="24 Things I Wish I Had Done Sooner (or my biggest regrets)"
|
||||
android:fontFamily="@font/inter_medium"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="17sp"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginLeft="14dp"
|
||||
android:layout_marginRight="14dp" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="14dp"
|
||||
android:layout_marginRight="14dp"
|
||||
android:layout_marginTop="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_meta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="51K views • 3 years ago"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:textSize="10dp"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_rating"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_like_icon"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@string/cd_image_like_icon"
|
||||
app:srcCompat="@drawable/ic_thumb_up" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_likes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
tools:text="500K"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="10dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_dislike_icon"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@string/cd_image_dislike_icon"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
app:srcCompat="@drawable/ic_thumb_down" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_dislikes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
tools:text="500K"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="10dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
android:id="@+id/platform_indicator"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:scaleType="centerInside"
|
||||
tools:src="@drawable/ic_peertube"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintTop_toTopOf="@id/image_author_thumbnail"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/image_author_thumbnail" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_summary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:textFontWeight="400"
|
||||
tools:text="This is the summary of the article"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_segments"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="10dp">
|
||||
|
||||
<com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
android:id="@+id/rating"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="15dp" />
|
||||
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_share"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:contentDescription="@string/cd_button_share"
|
||||
android:background="@drawable/background_button_round"
|
||||
android:gravity="center"
|
||||
android:layout_marginStart="5dp"
|
||||
android:orientation="horizontal"
|
||||
app:srcCompat="@drawable/ic_share"
|
||||
app:tint="@color/white"
|
||||
android:padding="8dp"
|
||||
android:layout_marginEnd="15dp"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:scaleType="fitCenter" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_change_bottom_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/background_videodetail_description"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginLeft="14dp"
|
||||
android:layout_marginRight="14dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_polycentric"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:text="Polycentric"
|
||||
android:textColor="#fff"
|
||||
android:textSize="10dp"
|
||||
android:lines="1"
|
||||
android:ellipsize="marquee"
|
||||
android:padding="10dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_platform"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:text="Platform"
|
||||
android:textColor="#fff"
|
||||
android:textSize="10dp"
|
||||
android:lines="1"
|
||||
android:ellipsize="marquee"
|
||||
android:padding="10dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.comments.AddCommentView
|
||||
android:id="@+id/add_comment_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:layout_marginStart="28dp"
|
||||
android:layout_marginEnd="28dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.segments.CommentsList
|
||||
android:id="@+id/comments_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_loading_overlay"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#77000000"
|
||||
android:elevation="4dp">
|
||||
<ImageView
|
||||
android:id="@+id/image_loader"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
app:srcCompat="@drawable/ic_loader_animated"
|
||||
android:layout_gravity="top|center_horizontal"
|
||||
android:alpha="0.7"
|
||||
android:layout_marginTop="80dp"
|
||||
android:contentDescription="@string/loading" />
|
||||
</FrameLayout>
|
||||
|
||||
<com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||
android:id="@+id/replies_overlay"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/overlay_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
android:elevation="15dp">
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
37
app/src/main/res/layout/fragview_web_detail.xml
Normal file
37
app/src/main/res/layout/fragview_web_detail.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="false"
|
||||
android:background="@drawable/bottom_menu_border"
|
||||
android:id="@+id/root"
|
||||
android:clickable="true">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#111" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_loading_overlay"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#77000000"
|
||||
android:elevation="4dp">
|
||||
<ImageView
|
||||
android:id="@+id/image_loader"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
app:srcCompat="@drawable/ic_loader_animated"
|
||||
android:layout_gravity="top|center_horizontal"
|
||||
android:alpha="0.7"
|
||||
android:layout_marginTop="80dp"
|
||||
android:contentDescription="@string/loading" />
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
28
app/src/main/res/layout/view_segment_image.xml
Normal file
28
app/src/main/res/layout/view_segment_image.xml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="7dp"
|
||||
android:paddingEnd="7dp"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/root">
|
||||
<ImageView
|
||||
android:id="@+id/image_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp" />
|
||||
<TextView
|
||||
android:id="@+id/text_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:textSize="10sp"
|
||||
android:textColor="#999"
|
||||
android:layout_marginBottom="5dp" />
|
||||
</LinearLayout>
|
20
app/src/main/res/layout/view_segment_text.xml
Normal file
20
app/src/main/res/layout/view_segment_text.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:id="@+id/root">
|
||||
<TextView
|
||||
android:id="@+id/text_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:layout_weight="400"
|
||||
android:textColor="#BBB"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:layout_marginTop="5dp" />
|
||||
</LinearLayout>
|
Loading…
Add table
Add a link
Reference in a new issue