diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 8d930838..db5c6060 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -132,6 +132,7 @@ import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout import com.futo.platformplayer.views.casting.CastView import com.futo.platformplayer.views.comments.AddCommentView import com.futo.platformplayer.views.others.CreatorThumbnail +import com.futo.platformplayer.views.overlays.ChaptersOverlay import com.futo.platformplayer.views.overlays.DescriptionOverlay import com.futo.platformplayer.views.overlays.LiveChatOverlay import com.futo.platformplayer.views.overlays.QueueEditorOverlay @@ -147,6 +148,7 @@ import com.futo.platformplayer.views.pills.PillRatingLikesDislikes import com.futo.platformplayer.views.pills.RoundButton import com.futo.platformplayer.views.pills.RoundButtonGroup import com.futo.platformplayer.views.platform.PlatformIndicator +import com.futo.platformplayer.views.segments.ChaptersList import com.futo.platformplayer.views.segments.CommentsList import com.futo.platformplayer.views.subscriptions.SubscribeButton import com.futo.platformplayer.views.video.FutoVideoPlayer @@ -195,6 +197,8 @@ class VideoDetailView : ConstraintLayout { private var _liveChat: LiveChatManager? = null; private var _videoResumePositionMilliseconds : Long = 0L; + private var _chapters: List? = null; + private val _player: FutoVideoPlayer; private val _cast: CastView; private val _playerProgress: PlayerControlView; @@ -263,6 +267,7 @@ class VideoDetailView : ConstraintLayout { private val _container_content_liveChat: LiveChatOverlay; private val _container_content_browser: WebviewOverlay; private val _container_content_support: SupportOverlay; + private val _container_content_chapters: ChaptersOverlay; private var _container_content_current: View; @@ -374,6 +379,7 @@ class VideoDetailView : ConstraintLayout { _container_content_liveChat = findViewById(R.id.videodetail_container_livechat); _container_content_support = findViewById(R.id.videodetail_container_support); _container_content_browser = findViewById(R.id.videodetail_container_webview) + _container_content_chapters = findViewById(R.id.videodetail_container_chapters); _addCommentView = findViewById(R.id.add_comment_view); _commentsList = findViewById(R.id.comments_list); @@ -686,6 +692,11 @@ class VideoDetailView : ConstraintLayout { _container_content_replies.onClose.subscribe { switchContentView(_container_content_main); }; _container_content_support.onClose.subscribe { switchContentView(_container_content_main); }; _container_content_browser.onClose.subscribe { switchContentView(_container_content_main); }; + _container_content_chapters.onClose.subscribe { switchContentView(_container_content_main); }; + + _container_content_chapters.onClick.subscribe { + handleSeek(it.timeStart.toLong() * 1000); + } _description_viewMore.setOnClickListener { switchContentView(_container_content_description); @@ -865,6 +876,21 @@ class VideoDetailView : ConstraintLayout { }; } }, + _chapters?.let { + if(it != null && it.size > 0) + RoundButton(context, R.drawable.ic_list, "Chapters", TAG_CHAPTERS) { + video?.let { + try { + _container_content_chapters.setChapters(_chapters); + switchContentView(_container_content_chapters); + } + catch(ex: Throwable) { + + } + } + } + else null + }, if(video?.isLive ?: false) RoundButton(context, R.drawable.ic_chat, context.getString(R.string.live_chat), TAG_LIVECHAT) { video?.let { @@ -1340,10 +1366,12 @@ class VideoDetailView : ConstraintLayout { val chapters = null ?: StatePlatform.instance.getContentChapters(video.url); _player.setChapters(chapters); _cast.setChapters(chapters); + _chapters = _player.getChapters(); } catch (ex: Throwable) { Logger.e(TAG, "Failed to get chapters", ex); _player.setChapters(null); _cast.setChapters(null); + _chapters = null; /*withContext(Dispatchers.Main) { UIDialogs.toast(context, "Failed to get chapters\n" + ex.message); @@ -1382,6 +1410,10 @@ class VideoDetailView : ConstraintLayout { ); } } + + fragment.lifecycleScope.launch(Dispatchers.Main) { + updateMoreButtons(); + } }; } @@ -3077,6 +3109,7 @@ class VideoDetailView : ConstraintLayout { const val TAG_SHARE = "share"; const val TAG_OVERLAY = "overlay"; const val TAG_LIVECHAT = "livechat"; + const val TAG_CHAPTERS = "chapters"; const val TAG_OPEN = "open"; const val TAG_SEND_TO_DEVICE = "send_to_device"; const val TAG_MORE = "MORE"; diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/ChapterViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/ChapterViewHolder.kt new file mode 100644 index 00000000..0b7efd4a --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/ChapterViewHolder.kt @@ -0,0 +1,91 @@ +package com.futo.platformplayer.views.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.futo.platformplayer.R +import com.futo.platformplayer.Settings +import com.futo.platformplayer.api.media.models.chapters.ChapterType +import com.futo.platformplayer.api.media.models.chapters.IChapter +import com.futo.platformplayer.api.media.models.comments.IPlatformComment +import com.futo.platformplayer.api.media.models.comments.LazyComment +import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment +import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes +import com.futo.platformplayer.api.media.models.ratings.RatingLikes +import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.fixHtmlLinks +import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod +import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StatePolycentric +import com.futo.platformplayer.toHumanDuration +import com.futo.platformplayer.toHumanNowDiffString +import com.futo.platformplayer.toHumanNumber +import com.futo.platformplayer.toHumanTime +import com.futo.platformplayer.views.LoaderView +import com.futo.platformplayer.views.others.CreatorThumbnail +import com.futo.platformplayer.views.pills.PillButton +import com.futo.platformplayer.views.pills.PillRatingLikesDislikes +import com.futo.polycentric.core.ApiMethods +import com.futo.polycentric.core.Opinion +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ChapterViewHolder : ViewHolder { + + private val _layoutChapter: ConstraintLayout; + + private val _containerChapter: ConstraintLayout; + + private val _textTitle: TextView; + private val _textTimestamp: TextView; + private val _textMeta: TextView; + + var onClick = Event1(); + var chapter: IChapter? = null + private set; + + constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_chapter, viewGroup, false)) { + _layoutChapter = itemView.findViewById(R.id.layout_chapter); + _containerChapter = itemView.findViewById(R.id.chapter_container); + + _containerChapter.setOnClickListener { + chapter?.let { + onClick.emit(it); + } + } + + _textTitle = itemView.findViewById(R.id.text_title); + _textTimestamp = itemView.findViewById(R.id.text_timestamp); + _textMeta = itemView.findViewById(R.id.text_meta); + } + + fun bind(chapter: IChapter) { + _textTitle.text = chapter.name; + _textTimestamp.text = chapter.timeStart.toLong().toHumanTime(false); + + if(chapter.type == ChapterType.NORMAL) { + _textMeta.isVisible = false; + } + else { + _textMeta.isVisible = true; + when(chapter.type) { + ChapterType.SKIP -> _textMeta.text = "(Skip)"; + ChapterType.SKIPPABLE -> _textMeta.text = "(Manual Skip)" + ChapterType.SKIPONCE -> _textMeta.text = "(Skip Once)" + else -> _textMeta.isVisible = false; + }; + } + this.chapter = chapter; + } + + companion object { + private const val TAG = "CommentViewHolder"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/ChaptersOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/ChaptersOverlay.kt new file mode 100644 index 00000000..becdb87b --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/ChaptersOverlay.kt @@ -0,0 +1,72 @@ +package com.futo.platformplayer.views.overlays + +import android.content.Context +import android.net.Uri +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import com.futo.platformplayer.R +import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.activities.MainActivity +import com.futo.platformplayer.api.http.ManagedHttpClient +import com.futo.platformplayer.api.media.models.chapters.IChapter +import com.futo.platformplayer.api.media.models.comments.IPlatformComment +import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment +import com.futo.platformplayer.api.media.structures.IPager +import com.futo.platformplayer.constructs.Event0 +import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.fixHtmlLinks +import com.futo.platformplayer.logging.Logger +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.views.behavior.NonScrollingTextView +import com.futo.platformplayer.views.comments.AddCommentView +import com.futo.platformplayer.views.others.CreatorThumbnail +import com.futo.platformplayer.views.segments.ChaptersList +import com.futo.platformplayer.views.segments.CommentsList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import userpackage.Protocol + +class ChaptersOverlay : LinearLayout { + val onClose = Event0(); + val onClick = Event1(); + + private val _topbar: OverlayTopbar; + private val _chaptersList: ChaptersList; + private var _onChapterClicked: ((chapter: IChapter) -> Unit)? = null; + private val _layoutItems: LinearLayout + + constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { + inflate(context, R.layout.overlay_chapters, this) + _layoutItems = findViewById(R.id.layout_items) + _topbar = findViewById(R.id.topbar); + _chaptersList = findViewById(R.id.chapters_list); + _chaptersList.onChapterClick.subscribe(onClick::emit); + _topbar.onClose.subscribe(this, onClose::emit); + _topbar.setInfo(context.getString(R.string.chapters), ""); + } + + fun setChapters(chapters: List?) { + _chaptersList?.setChapters(chapters ?: listOf()); + } + + + fun cleanup() { + _topbar.onClose.remove(this); + _onChapterClicked = null; + } + + companion object { + private const val TAG = "ChaptersOverlay" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/segments/ChaptersList.kt b/app/src/main/java/com/futo/platformplayer/views/segments/ChaptersList.kt new file mode 100644 index 00000000..50d7ba04 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/segments/ChaptersList.kt @@ -0,0 +1,103 @@ +package com.futo.platformplayer.views.segments + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.futo.platformplayer.R +import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.api.media.models.chapters.IChapter +import com.futo.platformplayer.api.media.models.comments.IPlatformComment +import com.futo.platformplayer.api.media.models.comments.LazyComment +import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment +import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails +import com.futo.platformplayer.api.media.structures.IAsyncPager +import com.futo.platformplayer.api.media.structures.IPager +import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.constructs.TaskHandler +import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StatePolycentric +import com.futo.platformplayer.views.adapters.ChapterViewHolder +import com.futo.platformplayer.views.adapters.CommentViewHolder +import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader +import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.net.UnknownHostException + +class ChaptersList : ConstraintLayout { + private val _llmReplies: LinearLayoutManager; + + private val _adapterChapters: InsertedViewAdapterWithLoader; + private val _recyclerChapters: RecyclerView; + private val _chapters: ArrayList = arrayListOf(); + private val _prependedView: FrameLayout; + private var _readonly: Boolean = false; + private val _layoutScrollToTop: FrameLayout; + + var onChapterClick = Event1(); + var onCommentsLoaded = Event1(); + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + LayoutInflater.from(context).inflate(R.layout.view_chapters_list, this, true); + + _recyclerChapters = findViewById(R.id.recycler_chapters); + + _layoutScrollToTop = findViewById(R.id.layout_scroll_to_top); + _layoutScrollToTop.setOnClickListener { + _recyclerChapters.smoothScrollToPosition(0) + } + _layoutScrollToTop.visibility = View.GONE + + _prependedView = FrameLayout(context); + _prependedView.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); + + _adapterChapters = InsertedViewAdapterWithLoader(context, arrayListOf(_prependedView), arrayListOf(), + childCountGetter = { _chapters.size }, + childViewHolderBinder = { viewHolder, position -> viewHolder.bind(_chapters[position]); }, + childViewHolderFactory = { viewGroup, _ -> + val holder = ChapterViewHolder(viewGroup); + holder.onClick.subscribe { c -> onChapterClick.emit(c) }; + return@InsertedViewAdapterWithLoader holder; + } + ); + + _llmReplies = LinearLayoutManager(context); + _recyclerChapters.layoutManager = _llmReplies; + _recyclerChapters.adapter = _adapterChapters; + } + + fun addChapter(chapter: IChapter) { + _chapters.add(0, chapter); + _adapterChapters.notifyItemRangeInserted(_adapterChapters.childToParentPosition(0), 1); + } + + fun setPrependedView(view: View) { + _prependedView.removeAllViews(); + _prependedView.addView(view); + } + + fun setChapters(chapters: List) { + _chapters.clear(); + _chapters.addAll(chapters); + _adapterChapters.notifyDataSetChanged(); + } + + fun clear() { + _chapters.clear(); + _adapterChapters.notifyDataSetChanged(); + } + + companion object { + private const val TAG = "CommentsList"; + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragview_video_detail.xml b/app/src/main/res/layout/fragview_video_detail.xml index d5062c06..7b8c1dc2 100644 --- a/app/src/main/res/layout/fragview_video_detail.xml +++ b/app/src/main/res/layout/fragview_video_detail.xml @@ -579,6 +579,12 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/overlay_chapters.xml b/app/src/main/res/layout/overlay_chapters.xml new file mode 100644 index 00000000..6664847c --- /dev/null +++ b/app/src/main/res/layout/overlay_chapters.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_chapters_list.xml b/app/src/main/res/layout/view_chapters_list.xml new file mode 100644 index 00000000..8fda13f3 --- /dev/null +++ b/app/src/main/res/layout/view_chapters_list.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5b1b6d9e..256fe1ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -665,6 +665,7 @@ Failed to load post. replies Replies + Chapters Plugin settings saved Plugin settings These settings are defined by the plugin