mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-21 09:49:29 +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: {
|
Text: {
|
||||||
RAW: 0,
|
RAW: 0,
|
||||||
HTML: 1,
|
HTML: 1,
|
||||||
MARKUP: 2
|
MARKUP: 2,
|
||||||
|
CODE: 3
|
||||||
},
|
},
|
||||||
Chapter: {
|
Chapter: {
|
||||||
NORMAL: 0,
|
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) {
|
constructor(obj) {
|
||||||
super(obj, 3);
|
super(obj, 3);
|
||||||
obj = obj ?? {};
|
obj = obj ?? {};
|
||||||
this.plugin_type = "PlatformArticleDetails";
|
this.plugin_type = "PlatformArticleDetails";
|
||||||
this.rating = obj.rating ?? new RatingLikes(-1);
|
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||||
this.summary = obj.summary ?? "";
|
|
||||||
this.segments = obj.segments ?? [];
|
this.segments = obj.segments ?? [];
|
||||||
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ArticleSegment {
|
class ArticleSegment {
|
||||||
|
@ -315,9 +340,10 @@ class ArticleTextSegment extends ArticleSegment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ArticleImagesSegment extends ArticleSegment {
|
class ArticleImagesSegment extends ArticleSegment {
|
||||||
constructor(images) {
|
constructor(images, caption) {
|
||||||
super(2);
|
super(2);
|
||||||
this.images = images;
|
this.images = images;
|
||||||
|
this.caption = caption;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ArticleNestedSegment extends ArticleSegment {
|
class ArticleNestedSegment extends ArticleSegment {
|
||||||
|
|
|
@ -25,7 +25,6 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
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.constructs.Event1
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
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.BrowserFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
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
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment.State
|
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment.State
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
|
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.AddTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
||||||
|
@ -152,6 +153,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
//Frags Main
|
//Frags Main
|
||||||
lateinit var _fragMainHome: HomeFragment;
|
lateinit var _fragMainHome: HomeFragment;
|
||||||
lateinit var _fragPostDetail: PostDetailFragment;
|
lateinit var _fragPostDetail: PostDetailFragment;
|
||||||
|
lateinit var _fragArticleDetail: ArticleDetailFragment;
|
||||||
|
lateinit var _fragWebDetail: WebDetailFragment;
|
||||||
lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment;
|
lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment;
|
||||||
lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment;
|
lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment;
|
||||||
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
|
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
|
||||||
|
@ -330,6 +333,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
_fragMainPlaylist = PlaylistFragment.newInstance();
|
_fragMainPlaylist = PlaylistFragment.newInstance();
|
||||||
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
|
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
|
||||||
_fragPostDetail = PostDetailFragment.newInstance();
|
_fragPostDetail = PostDetailFragment.newInstance();
|
||||||
|
_fragArticleDetail = ArticleDetailFragment.newInstance();
|
||||||
|
_fragWebDetail = WebDetailFragment.newInstance();
|
||||||
_fragWatchlist = WatchLaterFragment.newInstance();
|
_fragWatchlist = WatchLaterFragment.newInstance();
|
||||||
_fragHistory = HistoryFragment.newInstance();
|
_fragHistory = HistoryFragment.newInstance();
|
||||||
_fragSourceDetail = SourceDetailFragment.newInstance();
|
_fragSourceDetail = SourceDetailFragment.newInstance();
|
||||||
|
@ -456,6 +461,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
_fragMainPlaylist.topBar = _fragTopBarNavigation;
|
_fragMainPlaylist.topBar = _fragTopBarNavigation;
|
||||||
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
|
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
|
||||||
_fragPostDetail.topBar = _fragTopBarNavigation;
|
_fragPostDetail.topBar = _fragTopBarNavigation;
|
||||||
|
_fragArticleDetail.topBar = _fragTopBarNavigation;
|
||||||
|
_fragWebDetail.topBar = _fragTopBarNavigation;
|
||||||
_fragWatchlist.topBar = _fragTopBarNavigation;
|
_fragWatchlist.topBar = _fragTopBarNavigation;
|
||||||
_fragHistory.topBar = _fragTopBarNavigation;
|
_fragHistory.topBar = _fragTopBarNavigation;
|
||||||
_fragSourceDetail.topBar = _fragTopBarNavigation;
|
_fragSourceDetail.topBar = _fragTopBarNavigation;
|
||||||
|
@ -855,7 +862,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) on 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");
|
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (position > 0)
|
if (position > 0)
|
||||||
|
@ -1240,6 +1247,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
PlaylistFragment::class -> _fragMainPlaylist as T;
|
PlaylistFragment::class -> _fragMainPlaylist as T;
|
||||||
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
|
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
|
||||||
PostDetailFragment::class -> _fragPostDetail as T;
|
PostDetailFragment::class -> _fragPostDetail as T;
|
||||||
|
ArticleDetailFragment::class -> _fragArticleDetail as T;
|
||||||
|
WebDetailFragment::class -> _fragWebDetail as T;
|
||||||
WatchLaterFragment::class -> _fragWatchlist as T;
|
WatchLaterFragment::class -> _fragWatchlist as T;
|
||||||
HistoryFragment::class -> _fragHistory as T;
|
HistoryFragment::class -> _fragHistory as T;
|
||||||
SourceDetailFragment::class -> _fragSourceDetail 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),
|
POST(2),
|
||||||
ARTICLE(3),
|
ARTICLE(3),
|
||||||
PLAYLIST(4),
|
PLAYLIST(4),
|
||||||
|
WEB(7),
|
||||||
|
|
||||||
URL(9),
|
URL(9),
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
|
||||||
enum class TextType(val value: Int) {
|
enum class TextType(val value: Int) {
|
||||||
RAW(0),
|
RAW(0),
|
||||||
HTML(1),
|
HTML(1),
|
||||||
MARKUP(2);
|
MARKUP(2),
|
||||||
|
CODE(3);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): TextType
|
fun fromInt(value: Int): TextType
|
||||||
|
|
|
@ -27,7 +27,9 @@ interface IJSContent: IPlatformContent {
|
||||||
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
|
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
|
||||||
ContentType.PLAYLIST -> JSPlaylist(config, obj);
|
ContentType.PLAYLIST -> JSPlaylist(config, obj);
|
||||||
ContentType.LOCKED -> JSLockedContent(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}");
|
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ interface IJSContentDetails: IPlatformContent {
|
||||||
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
||||||
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
||||||
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
|
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
|
||||||
|
ContentType.WEB -> JSWebDetails(plugin, obj);
|
||||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
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.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
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.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
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.IPlatformContent
|
||||||
|
@ -21,20 +23,20 @@ import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.getOrThrowNullableList
|
import com.futo.platformplayer.getOrThrowNullableList
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
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;
|
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
||||||
|
|
||||||
private val _hasGetComments: Boolean;
|
private val _hasGetComments: Boolean;
|
||||||
private val _hasGetContentRecommendations: Boolean;
|
private val _hasGetContentRecommendations: Boolean;
|
||||||
|
|
||||||
val rating: IRating;
|
override val rating: IRating;
|
||||||
|
|
||||||
val summary: String;
|
override val summary: String;
|
||||||
val thumbnails: Thumbnails?;
|
override val thumbnails: Thumbnails?;
|
||||||
val segments: List<IJSArticleSegment>;
|
override val segments: List<IJSArticleSegment>;
|
||||||
|
|
||||||
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
|
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);
|
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);
|
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.states.StateDeveloper
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
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.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClientConstants
|
import com.futo.platformplayer.api.media.platforms.js.JSClientConstants
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
@ -77,6 +78,22 @@ class PackageBridge : V8Package {
|
||||||
return "android";
|
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
|
@V8Function
|
||||||
fun dispose(value: V8Value) {
|
fun dispose(value: V8Value) {
|
||||||
Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
|
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.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.UISlideOverlays
|
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.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
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.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
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.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
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.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
|
@ -196,7 +198,14 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||||
fragment.navigate<RemotePlaylistFragment>(content);
|
fragment.navigate<RemotePlaylistFragment>(content);
|
||||||
} else if (content is IPlatformPost) {
|
} else if (content is IPlatformPost) {
|
||||||
fragment.navigate<PostDetailFragment>(content);
|
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) {
|
protected open fun onContentUrlClicked(url: String, contentType: ContentType) {
|
||||||
when(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.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
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.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
import com.futo.platformplayer.api.media.PlatformMultiClientPool
|
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.models.ImageVariable
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
import com.futo.platformplayer.stores.StringStorage
|
|
||||||
import com.futo.platformplayer.views.ToastView
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
|
@ -56,7 +54,6 @@ import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.internal.concat
|
|
||||||
import java.lang.Thread.sleep
|
import java.lang.Thread.sleep
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import kotlin.streams.asSequence
|
import kotlin.streams.asSequence
|
||||||
|
@ -669,7 +666,7 @@ class StatePlatform {
|
||||||
|
|
||||||
|
|
||||||
//Video
|
//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)
|
fun getContentClient(url: String) : IPlatformClient = getContentClientOrNull(url)
|
||||||
?: throw NoPlatformClientException("No client enabled that supports this content url (${url})");
|
?: throw NoPlatformClientException("No client enabled that supports this content url (${url})");
|
||||||
fun getContentClientOrNull(url: String) : IPlatformClient? = getEnabledClients().find { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) };
|
fun getContentClientOrNull(url: String) : IPlatformClient? = getEnabledClients().find { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) };
|
||||||
|
|
|
@ -79,6 +79,8 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||||
return when(contentType) {
|
return when(contentType) {
|
||||||
ContentType.PLACEHOLDER -> createPlaceholderViewHolder(viewGroup);
|
ContentType.PLACEHOLDER -> createPlaceholderViewHolder(viewGroup);
|
||||||
ContentType.MEDIA -> createVideoPreviewViewHolder(viewGroup);
|
ContentType.MEDIA -> createVideoPreviewViewHolder(viewGroup);
|
||||||
|
ContentType.ARTICLE -> createPostViewHolder(viewGroup);
|
||||||
|
ContentType.WEB -> createPostViewHolder(viewGroup);
|
||||||
ContentType.POST -> createPostViewHolder(viewGroup);
|
ContentType.POST -> createPostViewHolder(viewGroup);
|
||||||
ContentType.PLAYLIST -> createPlaylistViewHolder(viewGroup);
|
ContentType.PLAYLIST -> createPlaylistViewHolder(viewGroup);
|
||||||
ContentType.NESTED_VIDEO -> createNestedViewHolder(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.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
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.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
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.IPlatformPostDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSWeb
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
|
@ -141,6 +143,16 @@ class PreviewPostView : LinearLayout {
|
||||||
content.content
|
content.content
|
||||||
else
|
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 "";
|
} else "";
|
||||||
|
|
||||||
if (content.name.isNullOrEmpty()) {
|
if (content.name.isNullOrEmpty()) {
|
||||||
|
@ -154,7 +166,14 @@ class PreviewPostView : LinearLayout {
|
||||||
|
|
||||||
if (content is IPlatformPost) {
|
if (content is IPlatformPost) {
|
||||||
setImages(content.thumbnails.filterNotNull());
|
setImages(content.thumbnails.filterNotNull());
|
||||||
} else {
|
}
|
||||||
|
else if(content is IPlatformArticle) {
|
||||||
|
if(content.thumbnails != null)
|
||||||
|
setImages(listOf(content.thumbnails!!));
|
||||||
|
else
|
||||||
|
setImages(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
setImages(null);
|
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