Skip to content

Commit ca94399

Browse files
SoxiaLiSAclaude
andcommitted
feat: V3 小说详情页/系列页 UI 重构,统一视觉风格
- 小说详情页 header 增加发布日期与字数 meta 行,profile 卡片增加阅读数/收藏数统计 - 系列页 header 增加章节数/总字数 meta 行,替换 RedSectionHeaderHolder 为 V3SectionLabelHolder - 所有按钮/文字颜色统一为 v3_text_1,简介/标签区内联 section label - 新增 VERTICAL_NO_HORIZONTAL ListMode,小说页移除水平内边距 - 历史小说卡片移除冗余 NOVEL badge,阅读按钮适配 V3Palette Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 475df21 commit ca94399

21 files changed

Lines changed: 699 additions & 296 deletions

app/src/main/java/ceui/lisa/fragments/HistoryNovelHolder.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package ceui.lisa.fragments
22

33
import android.content.Intent
44
import ceui.lisa.R
5-
import ceui.lisa.activities.Shaft
65
import ceui.lisa.activities.TemplateActivity
76
import ceui.lisa.activities.UActivity
87
import ceui.lisa.annotations.ItemHolder
@@ -43,7 +42,6 @@ class HistoryNovelViewHolder(bd: CellHistoryNovelV3Binding) :
4342
val novel = holder.novel
4443
val entity = holder.entity
4544

46-
binding.pSize.visibility = android.view.View.GONE
4745
Glide.with(context).load(GlideUtil.getUrl(novel.image_urls?.medium))
4846
.placeholder(R.color.v3_surface_2).into(binding.illustImage)
4947
binding.title.text = novel.title

app/src/main/java/ceui/pixiv/ui/common/ListMode.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ object ListMode {
1010
const val STAGGERED_GRID = 3
1111
const val GRID = 4
1212
const val GRID_AND_SECTION_HEADER = 5
13+
14+
const val VERTICAL_NO_HORIZONTAL = 6
1315
}

app/src/main/java/ceui/pixiv/ui/common/PixivFragment.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,16 @@ import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
2222
import androidx.recyclerview.widget.LinearLayoutManager
2323
import androidx.recyclerview.widget.RecyclerView
2424
import androidx.recyclerview.widget.StaggeredGridLayoutManager
25-
import ceui.lisa.helper.StaggeredManager
2625
import ceui.lisa.R
2726
import ceui.lisa.activities.UActivity
2827
import ceui.lisa.databinding.FragmentPixivListBinding
2928
import ceui.lisa.databinding.LayoutToolbarBinding
29+
import ceui.lisa.helper.StaggeredManager
3030
import ceui.lisa.utils.Common
3131
import ceui.lisa.utils.Params
3232
import ceui.lisa.utils.ShareIllust
3333
import ceui.lisa.view.LinearItemDecoration
34+
import ceui.lisa.view.LinearItemDecorationNoLRTB
3435
import ceui.lisa.view.SpacesItemDecoration
3536
import ceui.loxia.Article
3637
import ceui.loxia.Client
@@ -46,20 +47,19 @@ import ceui.loxia.Tag
4647
import ceui.loxia.getHumanReadableMessage
4748
import ceui.loxia.launchSuspend
4849
import ceui.loxia.pushFragment
49-
import ceui.pixiv.widgets.RateAppManager
5050
import ceui.pixiv.ui.chats.RedSectionHeaderHolder
5151
import ceui.pixiv.ui.circles.CircleFragmentArgs
5252
import ceui.pixiv.ui.detail.ArtworkViewPagerFragmentArgs
5353
import ceui.pixiv.ui.detail.ArtworksMap
5454
import ceui.pixiv.ui.detail.IllustSeriesFragmentArgs
5555
import ceui.pixiv.ui.novel.NovelSeriesActionReceiver
56-
import ceui.pixiv.ui.novel.NovelSeriesFragmentArgs
5756
import ceui.pixiv.ui.user.UserActionReceiver
5857
import ceui.pixiv.ui.user.UserFragmentArgs
5958
import ceui.pixiv.ui.web.WebFragmentArgs
6059
import ceui.pixiv.utils.animateWiggle
6160
import ceui.pixiv.utils.ppppx
6261
import ceui.pixiv.utils.setOnClick
62+
import ceui.pixiv.widgets.RateAppManager
6363
import ceui.pixiv.widgets.TagsActionReceiver
6464
import com.scwang.smart.refresh.footer.ClassicsFooter
6565
import com.scwang.smart.refresh.header.FalsifyFooter
@@ -260,7 +260,10 @@ open class PixivFragment(layoutId: Int) : Fragment(layoutId),
260260
}
261261

262262
override fun onClickNovelSeries(sender: View, series: Series) {
263-
val intent = android.content.Intent(requireContext(), ceui.lisa.activities.TemplateActivity::class.java).apply {
263+
val intent = android.content.Intent(
264+
requireContext(),
265+
ceui.lisa.activities.TemplateActivity::class.java
266+
).apply {
264267
putExtra(ceui.lisa.activities.TemplateActivity.EXTRA_FRAGMENT, "小说系列")
265268
putExtra(ceui.pixiv.ui.novel.NovelSeriesFragment.ARG_SERIES_ID, series.id)
266269
}
@@ -415,6 +418,9 @@ fun Fragment.setUpLayoutManager(listView: RecyclerView, listMode: Int = ListMode
415418
} else if (listMode == ListMode.VERTICAL) {
416419
listView.layoutManager = LinearLayoutManager(ctx)
417420
listView.addItemDecoration(LinearItemDecoration(18.ppppx))
421+
} else if (listMode == ListMode.VERTICAL_NO_HORIZONTAL) {
422+
listView.layoutManager = LinearLayoutManager(ctx)
423+
listView.addItemDecoration(LinearItemDecorationNoLRTB(18.ppppx))
418424
} else if (listMode == ListMode.VERTICAL_COMMENT) {
419425
listView.layoutManager = LinearLayoutManager(requireContext())
420426
listView.addItemDecoration(
@@ -468,7 +474,8 @@ fun FragmentActivity.findCurrentFragmentOrNull(): Fragment? {
468474
.filterIsInstance<NavHostFragment>()
469475
.firstOrNull()
470476

471-
val currentFragment = navigationFragment?.childFragmentManager?.fragments?.firstOrNull { it.isVisible }
477+
val currentFragment =
478+
navigationFragment?.childFragmentManager?.fragments?.firstOrNull { it.isVisible }
472479

473480
currentFragment?.let {
474481
Timber.d("Current Fragment Instance: ${it.javaClass.simpleName}")
@@ -502,7 +509,6 @@ fun Fragment.shareIllust(illust: Illust) {
502509
}
503510

504511

505-
506512
const val NOVEL_URL_HEAD = "https://www.pixiv.net/novel/show.php?id="
507513

508514
fun Fragment.shareNovel(novel: Novel) {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package ceui.pixiv.ui.common
2+
3+
import ceui.lisa.annotations.ItemHolder
4+
import ceui.lisa.databinding.ItemV3SectionLabelBinding
5+
6+
/**
7+
* V3 风格的区域标题标签:大写、12sp、v3_text_3 色、letterSpacing 0.12。
8+
* 用于替代旧式 RedSectionHeaderHolder,视觉对齐 V3 详情页的 section header。
9+
*/
10+
class V3SectionLabelHolder(
11+
val title: String,
12+
) : ListItemHolder()
13+
14+
@ItemHolder(V3SectionLabelHolder::class)
15+
class V3SectionLabelViewHolder(bd: ItemV3SectionLabelBinding) :
16+
ListItemViewHolder<ItemV3SectionLabelBinding, V3SectionLabelHolder>(bd) {
17+
18+
override fun onBindViewHolder(holder: V3SectionLabelHolder, position: Int) {
19+
super.onBindViewHolder(holder, position)
20+
binding.label.text = holder.title
21+
}
22+
}

app/src/main/java/ceui/pixiv/ui/novel/NovelHeaderHolder.kt

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package ceui.pixiv.ui.novel
22

33
import android.content.Intent
44
import android.view.View
5+
import androidx.core.view.isVisible
6+
import ceui.lisa.R
57
import ceui.lisa.activities.TemplateActivity
68
import ceui.lisa.annotations.ItemHolder
79
import ceui.lisa.databinding.CellNovelHeaderBinding
@@ -15,6 +17,7 @@ import ceui.pixiv.ui.common.ListItemHolder
1517
import ceui.pixiv.ui.common.ListItemViewHolder
1618
import ceui.pixiv.ui.common.NovelActionReceiver
1719
import ceui.pixiv.utils.setOnClick
20+
import java.text.NumberFormat
1821

1922

2023
class NovelHeaderHolder(val novelId: Long) : ListItemHolder() {
@@ -34,20 +37,37 @@ class NovelHeaderViewHolder(bd: CellNovelHeaderBinding) : ListItemViewHolder<Cel
3437
it.findActionReceiverOrNull<NovelActionReceiver>()
3538
?.onClickBookmarkNovel(it, holder.novelId)
3639
}
37-
// 长按收藏按钮 → 打开「按标签收藏」以自定义标签/公开私密(issue #839)。
3840
binding.bookmark.setOnLongClickListener { sender ->
3941
val novel = liveNovel.value ?: return@setOnLongClickListener false
4042
openTagBookmarkForNovel(sender, novel)
4143
true
4244
}
43-
binding.seriesName.setOnClick { sender ->
45+
binding.seriesStrip.setOnClick { sender ->
4446
liveNovel.value?.series?.let { series ->
4547
sender.findActionReceiverOrNull<NovelSeriesActionReceiver>()?.onClickNovelSeries(sender, series)
4648
}
4749
}
4850
binding.title.setOnClick {
4951
Common.copy(context, liveNovel.value?.title)
5052
}
53+
liveNovel.observe(lifecycleOwner) { novel ->
54+
if (novel == null) return@observe
55+
// Meta line
56+
val date = novel.create_date?.replace('T', ' ')?.take(16).orEmpty()
57+
binding.metaDate.text = date
58+
val wordCount = novel.text_length
59+
if (wordCount != null && wordCount > 0) {
60+
binding.metaWordCount.text = context.getString(
61+
R.string.novel_meta_word_count,
62+
NumberFormat.getInstance().format(wordCount),
63+
)
64+
binding.metaWordCount.isVisible = true
65+
binding.metaDot2.isVisible = true
66+
} else {
67+
binding.metaWordCount.isVisible = false
68+
binding.metaDot2.isVisible = false
69+
}
70+
}
5171
}
5272
}
5373

@@ -60,7 +80,6 @@ internal fun openTagBookmarkForNovel(sender: View, novel: Novel) {
6080
val tagNames = novel.tags.orEmpty().mapNotNull { it.name }.toTypedArray()
6181
val intent = Intent(ctx, TemplateActivity::class.java).apply {
6282
putExtra(TemplateActivity.EXTRA_FRAGMENT, "按标签收藏")
63-
// FragmentSB 对 illust/novel 统一用 ILLUST_ID 作为 id key,type 区分。
6483
putExtra(Params.ILLUST_ID, novel.id.toInt())
6584
putExtra(Params.DATA_TYPE, Params.TYPE_NOVEL)
6685
putExtra(Params.TAG_NAMES, tagNames)

app/src/main/java/ceui/pixiv/ui/novel/NovelProfileHolder.kt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,9 @@ import ceui.pixiv.ui.common.ListItemHolder
1313
import ceui.pixiv.ui.common.ListItemViewHolder
1414
import ceui.pixiv.ui.common.NOVEL_URL_HEAD
1515
import ceui.pixiv.utils.setOnClick
16+
import java.text.NumberFormat
1617

1718

18-
/**
19-
* 作品档案信息单元(锚点卡片)。
20-
*
21-
* 任务 #3:把原本散落在 NovelCaptionHolder 里的 info chip 迁移到独立的
22-
* 卡片 holder 里,让它成为核心按钮区上方最后一个"稳定锚点"。
23-
* 档案字段长度基本固定,卡片位置不受标签/简介长度影响,用户可形成肌肉记忆。
24-
*/
2519
class NovelProfileHolder(val novelId: Long) : ListItemHolder() {
2620
override fun getItemId(): Long {
2721
return novelId
@@ -37,10 +31,17 @@ class NovelProfileViewHolder(bd: CellNovelProfileBinding) :
3731
val liveNovel = ObjectPool.get<Novel>(holder.novelId)
3832
liveNovel.observe(lifecycleOwner) { novel ->
3933
if (novel == null) return@observe
34+
bindStats(novel)
4035
bindInfoChips(novel)
4136
}
4237
}
4338

39+
private fun bindStats(novel: Novel) {
40+
val fmt = NumberFormat.getInstance()
41+
binding.statViews.text = fmt.format(novel.total_view ?: 0)
42+
binding.statBookmarks.text = fmt.format(novel.total_bookmarks ?: 0)
43+
}
44+
4445
private fun bindInfoChips(novel: Novel) {
4546
chip(binding.chipNovelId, R.string.novel_chip_id, novel.id.toString(), novel.id.toString())
4647
novel.text_length?.let {

app/src/main/java/ceui/pixiv/ui/novel/NovelSeriesFragment.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ import ceui.lisa.models.IllustsBean
3131
import ceui.lisa.utils.Params
3232
import ceui.lisa.utils.V3Palette
3333
import ceui.loxia.Client
34-
import ceui.loxia.ObjectPool
35-
import ceui.pixiv.ui.common.CommonAdapter
3634
import ceui.pixiv.ui.common.ListMode
3735
import ceui.pixiv.ui.common.NovelMultiSelectReceiver
3836
import ceui.pixiv.ui.common.PixivFragment
@@ -68,14 +66,15 @@ class NovelSeriesFragment : PixivFragment(R.layout.fragment_pixiv_list), NovelMu
6866

6967
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
7068
super.onViewCreated(view, savedInstanceState)
71-
setUpRefreshState(binding, viewModel, ListMode.VERTICAL)
69+
setUpRefreshState(binding, viewModel, ListMode.VERTICAL_NO_HORIZONTAL)
7270
val density = resources.displayMetrics.density
7371
binding.listView.clipToPadding = false
7472
// 用户反馈:模糊封面图作为背景反而干扰前景文字阅读。改为 v3_bg(白天/夜间自动适配)。
7573
binding.pageBackground.setBackgroundColor(
7674
androidx.core.content.ContextCompat.getColor(requireContext(), R.color.v3_bg),
7775
)
7876
binding.toolbarLayout.root.visibility = View.GONE
77+
binding.topShadow.isVisible = false
7978

8079
// 醒目的合集下载按钮——老版本放在 toolbarLayout 的 more 菜单里,但 toolbarLayout
8180
// 被整体 GONE 了,用户根本看不到入口。挂到 bottomCovered 里做成一个 fab-ish 的
@@ -162,9 +161,11 @@ class NovelSeriesFragment : PixivFragment(R.layout.fragment_pixiv_list), NovelMu
162161
// 进入多选模式,用户手动勾选章节后再走批量下载
163162
viewModel.setMultiSelectMode(true)
164163
}
164+
165165
SeriesDownloadOptionsSheet.Action.AllSeparate -> {
166166
launchDownloadAll()
167167
}
168+
168169
SeriesDownloadOptionsSheet.Action.MergeOne -> {
169170
launchMergeDownload()
170171
}

app/src/main/java/ceui/pixiv/ui/novel/NovelSeriesHeaderHolder.kt

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import ceui.loxia.NovelSeriesDetail
1212
import ceui.pixiv.ui.common.ListItemHolder
1313
import ceui.pixiv.ui.common.ListItemViewHolder
1414
import ceui.pixiv.utils.setOnClick
15+
import java.text.NumberFormat
1516

1617
class NovelSeriesHeaderHolder(val series: NovelSeriesDetail) : ListItemHolder() {
1718
override fun getItemId(): Long {
@@ -25,17 +26,29 @@ class NovelSeriesHeaderViewHolder(bd: CellNovelSeriesHeaderBinding) : ListItemVi
2526
override fun onBindViewHolder(holder: NovelSeriesHeaderHolder, position: Int) {
2627
super.onBindViewHolder(holder, position)
2728
binding.series = holder.series
28-
bindInfoChips(holder.series)
29+
val series = holder.series
30+
val fmt = NumberFormat.getInstance()
2931

30-
val rawCaption = holder.series.caption.orEmpty()
32+
// Meta line
33+
binding.metaContentCount.text = context.getString(R.string.novel_meta_chapter_count, series.content_count)
34+
if (series.total_character_count > 0) {
35+
binding.metaCharCount.text = context.getString(
36+
R.string.novel_meta_word_count,
37+
fmt.format(series.total_character_count),
38+
)
39+
binding.metaCharCount.isVisible = true
40+
binding.metaDot2.isVisible = true
41+
} else {
42+
binding.metaCharCount.isVisible = false
43+
binding.metaDot2.isVisible = false
44+
}
45+
46+
// Caption
47+
val rawCaption = series.caption.orEmpty()
3148
if (rawCaption.isNotEmpty()) {
3249
binding.caption.isVisible = true
33-
// Pixiv 的 series caption 经常混用裸 `\n` 和 `<br>`,HtmlCompat 只认后者,
34-
// 不做替换就会把几十段压成一整段(issue 里的排版投诉)。先把 `\n` 改成 `<br>`
35-
// 再交给 HtmlCompat 解析。
3650
val normalized = rawCaption.replace("\r\n", "\n").replace("\n", "<br/>")
3751
binding.caption.text = HtmlCompat.fromHtml(normalized, HtmlCompat.FROM_HTML_MODE_COMPACT)
38-
// 点简介 = 复制简介(纯文本)。
3952
binding.caption.setOnClick {
4053
val plain = HtmlCompat.fromHtml(normalized, HtmlCompat.FROM_HTML_MODE_COMPACT)
4154
.toString().trim()
@@ -44,9 +57,12 @@ class NovelSeriesHeaderViewHolder(bd: CellNovelSeriesHeaderBinding) : ListItemVi
4457
} else {
4558
binding.caption.isVisible = false
4659
}
60+
4761
binding.title.setOnClick {
48-
Common.copy(it.context, holder.series.title)
62+
Common.copy(it.context, series.title)
4963
}
64+
65+
bindInfoChips(series)
5066
}
5167

5268
private fun bindInfoChips(series: NovelSeriesDetail) {
@@ -73,7 +89,6 @@ class NovelSeriesHeaderViewHolder(bd: CellNovelSeriesHeaderBinding) : ListItemVi
7389
} else {
7490
binding.chipCharCount.isVisible = false
7591
}
76-
// 系列链接:与小说详情页 NOVEL_URL_HEAD 同源,但路径不同;这里沿用旧版 FragmentNovelSeriesDetail 的格式。
7792
linkChip(binding.chipSeriesLink, R.string.novel_chip_series_link,
7893
"https://www.pixiv.net/novel/series/${series.id}")
7994
}

app/src/main/java/ceui/pixiv/ui/novel/NovelSeriesViewModel.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import ceui.loxia.NovelSeriesResp
1313
import ceui.loxia.ObjectPool
1414
import ceui.loxia.RefreshHint
1515
import ceui.loxia.RefreshState
16-
import ceui.pixiv.ui.chats.RedSectionHeaderHolder
1716
import ceui.pixiv.ui.common.DataSource
1817
import ceui.pixiv.ui.common.HoldersContainer
1918
import ceui.pixiv.ui.common.HoldersViewModel
@@ -22,6 +21,7 @@ import ceui.pixiv.ui.common.LoadMoreOwner
2221
import ceui.pixiv.ui.common.LoadingHolder
2322
import ceui.pixiv.ui.common.NovelCardHolder
2423
import ceui.pixiv.ui.common.RefreshOwner
24+
import ceui.pixiv.ui.common.V3SectionLabelHolder
2525
import ceui.pixiv.ui.common.createResponseStore
2626
import ceui.pixiv.ui.detail.ArtworksMap
2727
import ceui.pixiv.ui.detail.UserInfoHolder
@@ -147,13 +147,10 @@ class NovelSeriesViewModel(
147147
resp.novel_series_detail?.let {
148148
result.add(NovelSeriesHeaderHolder(it))
149149
}
150-
result.add(RedSectionHeaderHolder(context.getString(R.string.string_432)))
151150
result.add(UserInfoHolder(resp.novel_series_detail?.user?.id ?: 0L))
152-
result.add(RedSectionHeaderHolder(
153-
context.getString(
154-
R.string.total_works_count,
155-
resp.novel_series_detail?.content_count
156-
)))
151+
result.add(V3SectionLabelHolder(
152+
context.getString(R.string.novel_series_section_works)
153+
))
157154
result.addAll(resp.displayList.map { novel -> NovelCardHolder(novel) })
158155
_lastOrder = resp.novels?.size
159156
_itemHolders.value = result

0 commit comments

Comments
 (0)