Skip to content
This repository was archived by the owner on Jul 18, 2026. It is now read-only.

Commit de84dd2

Browse files
authored
Merge pull request #139 from misaka10032w/develop
[Add] 增加"系列影片"展开功能、评论排序功能等
2 parents 325cc88 + 692a4b0 commit de84dd2

19 files changed

Lines changed: 337 additions & 36 deletions

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ android {
3737
applicationId = "com.yenaly.han1meviewer"
3838
minSdk = property("min.sdk")?.toString()?.toIntOrNull()
3939
targetSdk = property("target.sdk")?.toString()?.toIntOrNull()
40-
val (code, name) = createVersion(major = 0, minor = 17, patch = 0)
40+
val (code, name) = createVersion(major = 0, minor = 17, patch = 2)
4141
versionCode = code
4242
versionName = name
4343

app/src/main/java/com/yenaly/han1meviewer/logic/Parser.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -761,14 +761,17 @@ object Parser {
761761
likeCommentStatus == "1",
762762
unlikeCommentStatus == "1",
763763
)
764+
val regex = """\d+""".toRegex()
765+
val replyCountText = child.select("div.load-replies-btn").text()
766+
val replyCount = regex.find(replyCountText)?.value?.toInt()
764767
val reportRedirectUrl = ""
765768
val reportableId = child.select("span.report-btn").first()?.attr("data-reportable-id")
766769
val reportableType = child.select("span.report-btn").first()?.attr("data-reportable-type")
767770

768771
commentList.add(
769772
VideoComments.VideoComment(
770773
avatar = avatarUrl, username = username, date = date,
771-
content = content, hasMoreReplies = hasMoreReplies,
774+
content = content, hasMoreReplies = hasMoreReplies, replyCount = replyCount,
772775
thumbUp = thumbUp.logIfParseNull(Parser::comments.name, "thumbUp"),
773776
id = id.logIfParseNull(Parser::comments.name, "id"),
774777
isChildComment = false, post = post,

app/src/main/java/com/yenaly/han1meviewer/logic/model/VideoComments.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ data class VideoComments(
2525
val isChildComment: Boolean,
2626
// 是否有更多回覆
2727
val hasMoreReplies: Boolean = false,
28+
//更多回复数量
29+
val replyCount: Int? = 0,
2830
// 評論id,登入前不能憑藉post獲取
2931
val id: String? = null,
3032
// post 相關

app/src/main/java/com/yenaly/han1meviewer/ui/adapter/VideoColumnTitleAdapter.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ class VideoColumnTitleAdapter : BaseSingleItemAdapter<Unit, QuickViewHolder> {
6969
viewType: Int,
7070
): QuickViewHolder {
7171
return QuickViewHolder(R.layout.item_video_column_title, parent).also { viewHolder ->
72-
viewHolder.setGone(R.id.more, context !is MainActivity)
73-
viewHolder.getView<Button>(R.id.more).setOnClickListener(onMoreHanimeListener)
72+
if (onMoreHanimeListener != null){
73+
viewHolder.setGone(R.id.more, context !is MainActivity)
74+
viewHolder.getView<Button>(R.id.more).setOnClickListener(onMoreHanimeListener)
75+
}
7476
}
7577
}
7678
}

app/src/main/java/com/yenaly/han1meviewer/ui/adapter/VideoCommentRvAdapter.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,12 @@ class VideoCommentRvAdapter(
110110
}
111111
holder.binding.tvDate.text = item.date
112112
holder.binding.tvUsername.text = item.username
113-
holder.binding.btnViewMoreReplies.isVisible = item.hasMoreReplies
113+
holder.binding.btnViewMoreReplies.apply {
114+
isVisible = item.hasMoreReplies
115+
if (isVisible) text = holder.itemView.context.getString(
116+
R.string.view_more_replies, item.replyCount ?: 0
117+
)
118+
}
114119
holder.binding.btnThumbUp.text = item.realLikesCount?.toString()
115120
holder.binding.btnThumbUp.setThumbUpIcon(item.post.likeCommentStatus)
116121
holder.binding.btnThumbDown.setThumbDownIcon(item.post.unlikeCommentStatus)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.yenaly.han1meviewer.ui.fragment
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import android.view.ViewTreeObserver
8+
import android.widget.TextView
9+
import androidx.fragment.app.viewModels
10+
import androidx.recyclerview.widget.GridLayoutManager
11+
import androidx.recyclerview.widget.RecyclerView
12+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
13+
import com.yenaly.han1meviewer.R
14+
import com.yenaly.han1meviewer.ui.adapter.HanimeVideoRvAdapter
15+
import com.yenaly.han1meviewer.ui.viewmodel.VideoViewModel
16+
import kotlin.math.max
17+
18+
class PlaylistBottomSheetFragment : BottomSheetDialogFragment() {
19+
companion object {
20+
const val TAG = "PlaylistBottomSheetFragment"
21+
}
22+
23+
private val viewModel: VideoViewModel by viewModels({ requireParentFragment() })
24+
private var videoCount = 0
25+
private lateinit var adapter: HanimeVideoRvAdapter
26+
27+
override fun onCreateView(
28+
inflater: LayoutInflater,
29+
container: ViewGroup?,
30+
savedInstanceState: Bundle?
31+
): View {
32+
return inflater.inflate(R.layout.fragment_bottom_sheet_list, container, false)
33+
}
34+
35+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
36+
adapter = HanimeVideoRvAdapter()
37+
val recyclerView = view.findViewById<RecyclerView>(R.id.rv_vertical_list)
38+
val countText = view.findViewById<TextView>(R.id.video_count)
39+
recyclerView.adapter = adapter
40+
recyclerView.viewTreeObserver.addOnGlobalLayoutListener(
41+
object : ViewTreeObserver.OnGlobalLayoutListener{
42+
override fun onGlobalLayout() {
43+
recyclerView.viewTreeObserver.removeOnGlobalLayoutListener(this)
44+
val spanCount = calculateSpanCount(recyclerView,185)
45+
recyclerView.layoutManager = GridLayoutManager(requireContext(),spanCount)
46+
}
47+
}
48+
)
49+
50+
viewModel.videoList.observe(viewLifecycleOwner) { list ->
51+
videoCount = list.size
52+
countText.text = getString(R.string.blank_brackets,videoCount)
53+
adapter.submitList(list)
54+
}
55+
56+
recyclerView.addOnLayoutChangeListener { _, left, _, right, _, _, _, _, _ ->
57+
val newWidth = right - left
58+
if (newWidth > 0) {
59+
val spanCount = calculateSpanCount(
60+
recyclerView,
61+
requireContext().resources
62+
.getDimension(R.dimen.video_cover_width)
63+
.toInt()
64+
)
65+
(recyclerView.layoutManager as? GridLayoutManager)?.spanCount = spanCount
66+
}
67+
}
68+
}
69+
private fun calculateSpanCount(recyclerView: RecyclerView, itemMinWidthDp: Int): Int {
70+
val density = recyclerView.resources.displayMetrics.density
71+
val itemMinWidthPx = (itemMinWidthDp * density).toInt()
72+
val totalSpace = recyclerView.measuredWidth - recyclerView.paddingLeft - recyclerView.paddingRight
73+
return max(2, totalSpace / itemMinWidthPx)
74+
}
75+
}

app/src/main/java/com/yenaly/han1meviewer/ui/fragment/search/SearchFragment.kt

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class SearchFragment : YenalyFragment<FragmentSearchBinding>(), StateLayoutMixin
7070
private val searchAdapter by unsafeLazy { HanimeVideoRvAdapter() }
7171
private val historyAdapter by unsafeLazy { HanimeSearchHistoryRvAdapter() }
7272
private var hasInitAdvancedSearch = false
73-
73+
private var observersBound = false
7474
private val optionsPopupFragment by unsafeLazy { SearchOptionsPopupFragment() }
7575
@Suppress ("DEPRECATION")
7676
private fun getAdvancedSearchMap(): Map<HAdvancedSearch, Any> {
@@ -128,7 +128,10 @@ class SearchFragment : YenalyFragment<FragmentSearchBinding>(), StateLayoutMixin
128128
})
129129
}
130130
binding.searchSrl.apply {
131-
setOnLoadMoreListener { getHanimeSearchResult() }
131+
setOnLoadMoreListener {
132+
viewModel.page++
133+
getHanimeSearchResult()
134+
}
132135
setOnRefreshListener { getNewHanimeSearchResult() }
133136
setDisableContentWhenRefresh(true)
134137
}
@@ -150,23 +153,20 @@ class SearchFragment : YenalyFragment<FragmentSearchBinding>(), StateLayoutMixin
150153
headerLp.topMargin = offset
151154
binding.searchHeader.layoutParams = headerLp
152155
}
156+
if (!observersBound) {
157+
sfBindDataObservers()
158+
observersBound = true
159+
}
153160
}
154161

155-
// override fun onDestroyView() {
156-
// binding.searchRv.apply {
157-
// adapter = null
158-
// layoutManager = null
159-
// clearOnScrollListeners()
160-
// }
161-
// super.onDestroyView()
162-
// _binding?.unbind()
163-
// }
164162

165163
@SuppressLint("SetTextI18n")
166-
override fun bindDataObservers() {
164+
private fun sfBindDataObservers() {
165+
Log.d("LifecycleDebug", "bindDataObservers called")
167166
lifecycleScope.launch {
168-
repeatOnLifecycle(Lifecycle.State.CREATED) {
167+
repeatOnLifecycle(Lifecycle.State.STARTED) {
169168
viewModel.searchStateFlow.collect { state ->
169+
Log.d("LifecycleDebug", "searchStateFlow collected: ${state.javaClass.simpleName}, page: ${viewModel.page}")
170170
binding.searchRv.isGone = state is PageLoadingState.Error
171171
when (state) {
172172
is PageLoadingState.Loading -> {
@@ -175,7 +175,6 @@ class SearchFragment : YenalyFragment<FragmentSearchBinding>(), StateLayoutMixin
175175
}
176176

177177
is PageLoadingState.Success -> {
178-
viewModel.page++
179178
binding.searchSrl.finishRefresh()
180179
binding.searchSrl.finishLoadMore(true)
181180
if (!hasAdapterLoaded) {
@@ -205,7 +204,7 @@ class SearchFragment : YenalyFragment<FragmentSearchBinding>(), StateLayoutMixin
205204
}
206205

207206
lifecycleScope.launch {
208-
repeatOnLifecycle(Lifecycle.State.CREATED) {
207+
repeatOnLifecycle(Lifecycle.State.STARTED) {
209208
viewModel.searchFlow.collectLatest {
210209
searchAdapter.submitList(it)
211210
}
@@ -215,7 +214,6 @@ class SearchFragment : YenalyFragment<FragmentSearchBinding>(), StateLayoutMixin
215214
repeatOnLifecycle(Lifecycle.State.STARTED) {
216215
viewModel.refreshTriggerFlow.collect {
217216
binding.searchSrl.autoRefresh()
218-
// getNewHanimeSearchResult()
219217
}
220218
}
221219
}

app/src/main/java/com/yenaly/han1meviewer/ui/fragment/video/ChildCommentPopupFragment.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import com.yenaly.han1meviewer.logic.model.ReportReason
3030
import com.yenaly.han1meviewer.logic.state.WebsiteState
3131
import com.yenaly.han1meviewer.ui.adapter.VideoCommentRvAdapter
3232
import com.yenaly.han1meviewer.ui.viewmodel.CommentViewModel
33+
import com.yenaly.han1meviewer.util.parseTimeStrToMinutes
34+
import com.yenaly.han1meviewer.util.safeSortedBy
3335
import com.yenaly.han1meviewer.util.setGravity
3436
import com.yenaly.yenaly_libs.base.YenalyBottomSheetDialogFragment
3537
import com.yenaly.yenaly_libs.utils.arguments
@@ -135,7 +137,8 @@ class ChildCommentPopupFragment :
135137

136138
lifecycleScope.launch {
137139
viewModel.videoReplyFlow.collectLatest { list ->
138-
replyAdapter.submitList(list)
140+
val sortedlist = list.safeSortedBy({ parseTimeStrToMinutes(it.date) }, descending = false)
141+
replyAdapter.submitList(sortedlist)
139142
attachRedDotCount(list.size)
140143
}
141144
}

app/src/main/java/com/yenaly/han1meviewer/ui/fragment/video/CommentFragment.kt

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package com.yenaly.han1meviewer.ui.fragment.video
33
import android.os.Bundle
44
import android.util.Log
55
import android.view.LayoutInflater
6+
import android.view.View.GONE
7+
import android.view.View.VISIBLE
68
import android.view.ViewGroup
9+
import android.widget.PopupMenu
710
import android.widget.TextView
811
import androidx.core.view.ViewCompat
912
import androidx.core.view.WindowInsetsCompat
@@ -15,6 +18,7 @@ import androidx.lifecycle.ViewModelProvider
1518
import androidx.lifecycle.lifecycleScope
1619
import androidx.lifecycle.repeatOnLifecycle
1720
import androidx.recyclerview.widget.LinearLayoutManager
21+
import androidx.recyclerview.widget.RecyclerView
1822
import com.google.android.material.dialog.MaterialAlertDialogBuilder
1923
import com.google.android.material.snackbar.Snackbar
2024
import com.lxj.xpopup.XPopup
@@ -34,6 +38,8 @@ import com.yenaly.han1meviewer.ui.adapter.VideoCommentRvAdapter
3438
import com.yenaly.han1meviewer.ui.popup.ReplyPopup
3539
import com.yenaly.han1meviewer.ui.viewmodel.CommentViewModel
3640
import com.yenaly.han1meviewer.ui.viewmodel.PreviewCommentPrefetcher
41+
import com.yenaly.han1meviewer.util.parseTimeStrToMinutes
42+
import com.yenaly.han1meviewer.util.safeSortedBy
3743
import com.yenaly.yenaly_libs.base.YenalyFragment
3844
import com.yenaly.yenaly_libs.utils.arguments
3945
import com.yenaly.yenaly_libs.utils.showShortToast
@@ -58,6 +64,7 @@ class CommentFragment : YenalyFragment<FragmentCommentBinding>(), StateLayoutMix
5864
}
5965
private var reportReason:List<ReportReason>? = null
6066
private val commentTypePrefix by arguments(COMMENT_TYPE, VIDEO_COMMENT_PREFIX)
67+
private lateinit var sortPopup: PopupMenu
6168
private val commentAdapter by unsafeLazy {
6269
VideoCommentRvAdapter(this){ item ->
6370
if (reportReason == null) {
@@ -126,12 +133,27 @@ class CommentFragment : YenalyFragment<FragmentCommentBinding>(), StateLayoutMix
126133
binding.rvComment.layoutManager = LinearLayoutManager(context)
127134
binding.rvComment.adapter = commentAdapter
128135
binding.rvComment.clipToPadding = false
129-
136+
sortPopup = PopupMenu(requireContext(), binding.btnSort)
137+
sortPopup.menuInflater.inflate(R.menu.menu_comment_sort, sortPopup.menu)
130138
if (context is PreviewCommentActivity) {
131139
val comments = PreviewCommentPrefetcher.here().commentFlow.value
132140
if (comments.isNotEmpty()) {
133141
isPreviewCommentPrefetched = true
134-
commentAdapter.submitList(comments)
142+
binding.btnSort.isVisible = comments.size >= 3
143+
binding.btnSort.setOnClickListener { sortPopup.show() }
144+
sortPopup.setOnMenuItemClickListener { item ->
145+
val sortedList = when (item.itemId) {
146+
R.id.sort_latest -> comments.safeSortedBy({ parseTimeStrToMinutes(it.date) }, descending = false)
147+
R.id.sort_earliest -> comments.safeSortedBy({ parseTimeStrToMinutes(it.date) }, descending = true)
148+
R.id.sort_most_reply -> comments.safeSortedBy({ it.replyCount ?: 0 }, descending = true)
149+
R.id.sort_most_likes -> comments.safeSortedBy({ it.realLikesCount ?: 0 }, descending = true)
150+
R.id.sort_most_dislikes -> comments.safeSortedBy({ it.realLikesCount ?: 0 }, descending = false)
151+
else -> comments
152+
}
153+
commentAdapter.submitList(sortedList) { binding.rvComment.scrollToPosition(0) }
154+
true
155+
}
156+
commentAdapter.submitList(comments.safeSortedBy({ parseTimeStrToMinutes(it.date) }, descending = false))
135157
}
136158
}
137159
ViewCompat.setOnApplyWindowInsetsListener(binding.rvComment) { v, insets ->
@@ -165,6 +187,27 @@ class CommentFragment : YenalyFragment<FragmentCommentBinding>(), StateLayoutMix
165187
}
166188
}).asCustom(replyPopup).show()
167189
}
190+
binding.rvComment.addOnScrollListener(object : RecyclerView.OnScrollListener(){
191+
private var isVisible = true
192+
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
193+
super.onScrolled(recyclerView, dx, dy)
194+
if (dy > 5 && isVisible) {
195+
binding.btnSort.animate()
196+
.alpha(0f)
197+
.setDuration(200)
198+
.withEndAction { binding.btnSort.visibility = GONE }
199+
.start()
200+
isVisible = false
201+
} else if (dy < -5 && !isVisible) {
202+
binding.btnSort.animate()
203+
.alpha(1f)
204+
.setDuration(200)
205+
.withEndAction { binding.btnSort.visibility = VISIBLE }
206+
.start()
207+
isVisible = true
208+
}
209+
}
210+
})
168211
}
169212

170213
override fun bindDataObservers() {
@@ -204,7 +247,21 @@ class CommentFragment : YenalyFragment<FragmentCommentBinding>(), StateLayoutMix
204247
repeatOnLifecycle(Lifecycle.State.CREATED) {
205248
viewModel.videoCommentFlow.collectLatest { list ->
206249
if (!isPreviewCommentPrefetched) {
207-
commentAdapter.submitList(list)
250+
binding.btnSort.isVisible = list.size >= 3
251+
binding.btnSort.setOnClickListener { sortPopup.show() }
252+
sortPopup.setOnMenuItemClickListener { item ->
253+
val sortedList = when (item.itemId) {
254+
R.id.sort_latest -> list.safeSortedBy({ parseTimeStrToMinutes(it.date) }, descending = false)
255+
R.id.sort_earliest -> list.safeSortedBy({ parseTimeStrToMinutes(it.date) }, descending = true)
256+
R.id.sort_most_reply -> list.safeSortedBy({ it.replyCount ?: 0 }, descending = true)
257+
R.id.sort_most_likes -> list.safeSortedBy({ it.realLikesCount ?: 0 }, descending = true)
258+
R.id.sort_most_dislikes -> list.safeSortedBy({ it.realLikesCount ?: 0 }, descending = false)
259+
else -> list
260+
}
261+
commentAdapter.submitList(sortedList) { binding.rvComment.scrollToPosition(0) }
262+
true
263+
}
264+
commentAdapter.submitList(list.safeSortedBy({ parseTimeStrToMinutes(it.date) }, descending = false))
208265
if (context is PreviewCommentActivity) {
209266
PreviewCommentPrefetcher.here().update(list)
210267
}

app/src/main/java/com/yenaly/han1meviewer/ui/fragment/video/VideoIntroductionFragment.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import com.yenaly.han1meviewer.ui.adapter.HanimeVideoRvAdapter
6565
import com.yenaly.han1meviewer.ui.adapter.RvWrapper.Companion.wrappedWith
6666
import com.yenaly.han1meviewer.ui.adapter.VideoColumnTitleAdapter
6767
import com.yenaly.han1meviewer.ui.fragment.PermissionRequester
68+
import com.yenaly.han1meviewer.ui.fragment.PlaylistBottomSheetFragment
6869
import com.yenaly.han1meviewer.ui.viewmodel.VideoViewModel
6970
import com.yenaly.han1meviewer.util.requestPostNotificationPermission
7071
import com.yenaly.han1meviewer.util.setDrawableTop
@@ -248,12 +249,21 @@ class VideoIntroductionFragment : YenalyFragment<FragmentVideoIntroductionBindin
248249
val cached = viewModel.videoIntroDataMap[code]
249250

250251
if (video.playlist != null && !viewModel.fromDownload) {
252+
val bottomSheet = PlaylistBottomSheetFragment()
251253
playlistTitleAdapter.subtitle = video.playlist.playlistName
252254
multi.addAdapter(playlistTitleAdapter)
253255
multi.addAdapter(playlistWrapper)
254256
if (cached?.playlist?.video != video.playlist.video) {
255257
playlistAdapter.submitList(video.playlist.video)
256258
}
259+
viewModel.setVideoList(video.playlist.video)
260+
261+
playlistTitleAdapter.apply {
262+
onMoreHanimeListener = {
263+
bottomSheet.show(parentFragmentManager,
264+
PlaylistBottomSheetFragment.TAG)
265+
}
266+
}
257267
}
258268

259269
if (!viewModel.fromDownload) {

0 commit comments

Comments
 (0)