Article support

This commit is contained in:
Kelvin 2025-05-29 16:51:06 +02:00
commit cbfd9ea559
7 changed files with 214 additions and 79 deletions

View file

@ -32,7 +32,8 @@ let Type = {
Text: {
RAW: 0,
HTML: 1,
MARKUP: 2
MARKUP: 2,
CODE: 3
},
Chapter: {
NORMAL: 0,
@ -323,9 +324,10 @@ class ArticleTextSegment extends ArticleSegment {
}
}
class ArticleImagesSegment extends ArticleSegment {
constructor(images) {
constructor(images, caption) {
super(2);
this.images = images;
this.caption = caption;
}
}
class ArticleNestedSegment extends ArticleSegment {

View file

@ -25,7 +25,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
@ -851,7 +850,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
return withContext(Dispatchers.IO) {
Logger.i(TAG, "handleUrl(url=$url) on IO");
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
if (StatePlatform.instance.hasEnabledContentClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found video client");
lifecycleScope.launch(Dispatchers.Main) {
if (position > 0)

View file

@ -5,7 +5,8 @@ import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
enum class TextType(val value: Int) {
RAW(0),
HTML(1),
MARKUP(2);
MARKUP(2),
CODE(3);
companion object {
fun fromInt(value: Int): TextType

View file

@ -2,8 +2,11 @@ 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
@ -16,23 +19,33 @@ 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
@ -43,10 +56,16 @@ 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
@ -126,6 +145,7 @@ class ArticleDetailFragment : MainFragment {
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;
@ -143,6 +163,7 @@ class ArticleDetailFragment : MainFragment {
private val _layoutLoadingOverlay: FrameLayout;
private val _imageLoader: ImageView;
private var _overlayContainer: FrameLayout
private val _repliesOverlay: RepliesOverlay;
private val _commentsList: CommentsList;
@ -187,10 +208,12 @@ class ArticleDetailFragment : MainFragment {
_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);
@ -449,6 +472,8 @@ class ArticleDetailFragment : MainFragment {
_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);
@ -457,12 +482,18 @@ class ArticleDetailFragment : MainFragment {
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 ->{}
@ -659,14 +690,86 @@ class ArticleDetailFragment : MainFragment {
}
}
class ArticleTextBlock : View {
constructor(context: Context?) : super(context){
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: View {
constructor(context: Context?) : super(context){
}
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);
}
}
}

View file

@ -5,7 +5,6 @@ import androidx.collection.LruCache
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.PlatformMultiClientPool
@ -46,7 +45,6 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.views.ToastView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
@ -56,7 +54,6 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.internal.concat
import java.lang.Thread.sleep
import java.time.OffsetDateTime
import kotlin.streams.asSequence
@ -669,7 +666,7 @@ class StatePlatform {
//Video
fun hasEnabledVideoClient(url: String) : Boolean = getEnabledClients().any { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) };
fun hasEnabledContentClient(url: String) : Boolean = getEnabledClients().any { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) };
fun getContentClient(url: String) : IPlatformClient = getContentClientOrNull(url)
?: throw NoPlatformClientException("No client enabled that supports this content url (${url})");
fun getContentClientOrNull(url: String) : IPlatformClient? = getEnabledClients().find { _instantClientPool.getClientPooled(it).isContentDetailsUrl(url) };

View file

@ -21,6 +21,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.post.IPlatformPostDetails
@ -141,6 +142,11 @@ class PreviewPostView : LinearLayout {
content.content
else
""
} else if(content is IPlatformArticle) {
if(!content.summary.isNullOrEmpty())
content.summary ?: ""
else
""
} else "";
if (content.name.isNullOrEmpty()) {
@ -154,7 +160,14 @@ class PreviewPostView : LinearLayout {
if (content is IPlatformPost) {
setImages(content.thumbnails.filterNotNull());
} else {
}
else if(content is IPlatformArticle) {
if(content.thumbnails != null)
setImages(listOf(content.thumbnails!!));
else
setImages(null);
}
else {
setImages(null);
}

View file

@ -16,6 +16,69 @@
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"
@ -26,7 +89,7 @@
android:textColor="@color/white"
android:textSize="17sp"
android:textIsSelectable="true"
android:layout_marginTop="8dp"
android:layout_marginTop="6dp"
android:layout_marginLeft="14dp"
android:layout_marginRight="14dp" />
@ -113,68 +176,17 @@
</LinearLayout>
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="14dp"
android:layout_marginRight="14dp"
android:layout_marginTop="10dp">
<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:id="@+id/text_summary"
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>
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"
@ -294,4 +306,12 @@
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>