Skip to content

feat: 番剧详情页新增集数列表弹窗和集成按钮 - #2396

Open
BrainWangs wants to merge 2 commits into
Predidit:mainfrom
BrainWangs:main
Open

BrainWangs wants to merge 2 commits into
Predidit:mainfrom
BrainWangs:main

Conversation

@BrainWangs

Copy link
Copy Markdown

改动说明

新增文件

lib/pages/info/episode_list_sheet.dart — 集数列表弹窗组件,支持分组展示(本篇/特别篇/OP/ED)及骨架屏加载状态

修改文件

lib/pages/info/info_controller.dart — 新增集数数据加载逻辑,管理请求状态与错误处理
lib/pages/info/info_page.dart — 详情页集成集数列表入口按钮
lib/bean/card/bangumi_info_card.dart — 调整布局以适配集数列表入口

关联issue

#1169 请求在番剧界面添加已播出集数
#2117 可考虑在点选动漫进入的动漫详情页上,加个目前集数详情

效果展示

Snipaste_2026-07-27_16-27-12 Snipaste_2026-07-27_16-27-16 20260727163158_14_84

验证

flutter test --no-pub +125: All tests passed
flutter analyze --no-pub --no-fatal-infos --fatal-warnings passed

@BrainWangs BrainWangs changed the title feat: add episode list sheet and integration into anime detail page feat: 番剧详情页新增集数列表弹窗和集成按钮 Jul 27, 2026
void showEpisodeListSheet() {
showAdaptiveBottomSheet<void>(
context: context,
builder: (context) => EpisodeListSheet(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: The bottom sheet never reflects updated episode state

EpisodeListSheet is a StatelessWidget fed one-shot snapshots (episodeList, isLoading, queryTimeout) and is not wrapped in Observer. showAdaptiveBottomSheet -> showModalBottomSheet builds this widget inside its own route, so setState(() {}) in retryLoadEpisodes (line 240) rebuilds only _InfoPageState, not the sheet.

Consequences:

  • Tapping 重试 refetches the data but the failure UI stays on screen until the user closes and reopens the sheet.
  • Opening the sheet while the initState preload is still in flight leaves the skeleton frozen forever.
  • queryBangumiEpisodesByID replaces the list instance (episodeList = ObservableList<EpisodeInfo>.of(list), info_controller.dart:237), so even the reference captured at line 252 can never see the new items.

Wrap the sheet body in Observer (the pattern used in source_sheet.dart:497 and episode_comments_sheet.dart:112) or make EpisodeListSheet stateful and own the fetch.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


// 分集列表:进入番剧详情页自动预加载,仅用于展示。
@observable
var episodeList = ObservableList<EpisodeInfo>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: MobX codegen is stale, so these new @observable fields are not reactive

lib/pages/info/info_controller.g.dart is tracked in the repo but was not regenerated in this PR: _$InfoController still stops at staffList (info_controller.g.dart:93-118) and contains no atoms for episodeList, episodesIsLoading, episodesQueryTimeout or episodesIsEmpty.

Without generated getter/setter overrides these are ordinary fields: reads are never tracked and writes never notify, so no Observer (including the ones at info_page.dart:447/498) can react to episode state. This compiles and flutter analyze stays clean, which makes the breakage silent.

Run dart run build_runner build --delete-conflicting-outputs and commit the regenerated info_controller.g.dart.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

episodesQueryTimeout = false;
episodesIsEmpty = false;
try {
final list = await BangumiApi.getBangumiEpisodesByID(id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: A partially failed pagination is reported as success

BangumiApi.getBangumiEpisodesByID wraps the whole do/while pagination loop in a single try/catch and returns whatever it accumulated (bangumi_api.dart:391-422). If page 1 succeeds but page 2 fails (network drop, rate limit, jsonData['total'] as int? cast error on an unexpected payload), this method receives a non-empty but silently truncated list — e.g. only the first 100 episodes of a 300-episode subject — and treats it as a successful load with no error state and no retry affordance.

Have the API surface the failure (rethrow, or return a result object with a partial/failed flag) so the UI can distinguish complete data from truncated data.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

// BangumiApi.getBangumiEpisodesByID 内部吞掉异常后返回空列表,
// 因此空列表既可能是失败也可能是真空。统一走「重试」入口,
// 让用户决定是再拉一次还是放弃。
if (episodeList.isEmpty) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Legitimately empty results are shown as a failure, and episodesIsEmpty is dead state

Nothing ever sets episodesIsEmpty = true (it is only set to false at lines 69 and 234), so the empty-state branch in episode_list_sheet.dart:181-188 is unreachable dead code, and the isEmpty prop threaded through info_page.dart:255 is always false.

More importantly, a subject that genuinely has zero episode records (unaired series, some movies) is classified as episodesQueryTimeout = true and rendered as 集数获取失败 with a retry button that can never succeed. episode_comments_sheet.dart:239 treats the same empty response as a not-found case (未找到分集列表) instead of an error — please align, or make the API distinguish failure from emptiness.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


void clearEpisodes() {
episodeList.clear();
episodesIsLoading = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Clearing the loading flag breaks the re-entrancy guard and allows cross-subject data leakage

clearEpisodes() is called from initState (info_page.dart:210) and dispose (info_page.dart:325). Resetting episodesIsLoading = false while a request is still in flight defeats the guard at line 229, so a second concurrent fetch can start and the two responses resolve last-writer-wins.

There is also no request/subject token: an in-flight fetch that completes after the page is disposed still writes episodeList and episodesQueryTimeout, so the next subject's detail page can display the previous subject's episode list (the controller instance is provided per route, but the pending future outlives the widget).

Capture the requested id and discard the result when it no longer matches bangumiItem.id, and don't reset the in-flight flag in clearEpisodes().


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

child: CollectButton.extend(
bangumiItem: widget.bangumiItem,
),
Row(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Fixed 168px button row will overflow on compact phone widths

The card's outer Row (line 141) has a single inflexible child (SizedBox(width: 16), line 166) plus two loose Flexible children (the poster and this info column; the vote chart is skipped below 600dp, lines 275-279). Flex splits the remaining space by flex factor regardless of the poster being height-bound, so this column gets about (screenWidth - 32 - 16) / 2 (the card is padded with fromLTRB(16, ..., 16, 0) at info_page.dart:470).

This Row is MainAxisSize.min with hard-coded children of 120 + 8 + 40 = 168px, so it cannot shrink and only fits when screen width is roughly >= 384dp: 360dp (very common on Android) overflows by ~12px and 320dp by ~32px. Previously the slot needed only 120px, so this is a regression. Wrap the collect button in Flexible/Expanded, or use Wrap, instead of fixed widths.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread lib/pages/info/info_page.dart Outdated
);
}
// 分集列表仅做展示,与播放页解耦,进入详情页即自动预加载。
loadEpisodes();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Full episode list is prefetched on every detail-page open

This runs unconditionally in initState, and getBangumiEpisodesByID pages 100 records at a time until total (bangumi_api.dart:391-422). Opening any subject therefore fires 1..N sequential HTTP requests and parses potentially thousands of models even when the user never taps the new button, competing with the info/comments/image loads during the page-entry animation, and with no caching or cancellation.

Load lazily on first sheet open (as episode_comments_sheet.dart:229 does for an explicit gesture) and keep the result for the lifetime of the page.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

return SizedBox(
width: 40,
height: 40,
child: IconButton(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Icon-only button has no tooltip or semantics label

_EpisodeListButton renders only Icons.video_collection_rounded, so hover users and screen readers get no indication of what it does. The adjacent collect button sets tooltip: (collect_button.dart:115) — do the same here, e.g. tooltip: '集数详情'.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread lib/pages/info/episode_list_sheet.dart Outdated
required this.onRetry,
});

final BangumiItem bangumiItem;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: bangumiItem is a required parameter that is never read

Nothing in this widget uses bangumiItem, yet it is required in the constructor (line 14) and forces info_page.dart:251 to pass it. The analyzer does not flag unused public fields, so this dead coupling to BangumiItem will silently persist. Remove it, or use it (e.g. show the subject title in the sheet header).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread lib/pages/info/info_page.dart Outdated
}
}

Future<void> retryLoadEpisodes() async {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: retryLoadEpisodes is byte-identical to loadEpisodes

Lines 233-238 and 240-245 are the same code; keep one method. Note also that the setState(() {}) in both is pure churn: build() reads no episode state, so it rebuilds the entire page (including the blurred header image subtree) without changing any output — the sheet, which is what needs updating, is on a separate route.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 13 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 6
SUGGESTION 7
Issue Details (click to expand)

WARNING

File Line Issue
lib/modules/bangumi/episode_item.dart 46 Bangumi EpType map shifted by one: type 5 (MAD) labelled 广告, type 6 (其他) labelled MAD
lib/modules/bangumi/episode_item.dart 80 episode > 0 cannot distinguish missing sort from sort == 0; 第0話 rows render as a bare EP: and become indistinguishable
lib/pages/info/episode_list_sheet.dart 155 CustomScrollView without shrinkWrap always takes 75%/90% sheet height while the skeleton branch shrink-wraps: height jumps on load, near-empty sheet for short subjects
lib/pages/info/info_controller.dart 245 Partial pagination failure inside getBangumiEpisodesByID yields a truncated list reported as success; with eager preload removed it is now cached for the whole page session with no retry affordance
lib/pages/info/info_controller.dart 255 Genuinely empty results are set to episodesQueryTimeout, so they render as 集数获取失败 with an unusable 重试; episodesIsEmpty stays dead state and the sheet's empty branch is unreachable
lib/pages/info/info_controller.dart 76 resetEpisodesState() still clears the in-flight flag and there is no request/subject token, so a late response can overwrite episodeList with another subject's episodes

SUGGESTION

File Line Issue
lib/modules/bangumi/episode_item.dart 64 readType() fallback changed from '' to 'other', changing existing player strings (episode_comments_sheet.dart:175/182/183/266) to OTHER.12
lib/modules/bangumi/episode_item.dart 91 #$type puts a type id in the episode-number slot and discards a present sort
lib/pages/info/episode_list_sheet.dart 41 Two different unmapped types produce duplicate adjacent 其它 (n) sections
lib/pages/info/info_controller.dart 66 Doc comment says "sheet 关闭后" but the only caller is dispose(); wiping data while keeping the flags leaves the controller self-contradictory
lib/pages/info/info_controller.dart 249 clear() + addAll() outside a MobX action publishes a transient empty list and multiple reaction passes
lib/bean/card/bangumi_info_card.dart 259 minWidth: 80 is clamped away by enforce() and maxWidth: 160 is never reached; label still squeezed at 320dp or ~1.2x text scale
lib/pages/info/info_page.dart 235 mounted guard sits before the only await so it protects nothing, but can silently no-op a 重试 tap
Resolved since the previous review
  • Sheet reactivity: EpisodeListSheet now reads InfoController inside an Observer (loading/retry/success transitions rebuild).
  • MobX codegen: info_controller.g.dart now contains all four episode atoms and the updated toString().
  • Lazy list: section headers + SliverList.builder replace the effectively non-lazy ListView.builder.
  • Eager prefetch removed; episodes load on first sheet open.
  • Fixed 168px button row overflow fixed via Flexible + ConstrainedBox (residual squeeze noted above).
  • Icon button now has a tooltip; unused bangumiItem param and duplicated retryLoadEpisodes removed.
Files Reviewed (6 files)
  • lib/modules/bangumi/episode_item.dart - 4 issues
  • lib/pages/info/episode_list_sheet.dart - 2 issues
  • lib/pages/info/info_controller.dart - 5 issues
  • lib/pages/info/info_page.dart - 1 issue
  • lib/bean/card/bangumi_info_card.dart - 1 issue
  • lib/pages/info/info_controller.g.dart - generated, verified consistent with declared observables
Notes & assumptions
  • Incremental review of 475bbf6..5864d45; unchanged files were not re-reviewed. All findings were re-verified against the current file contents at 5864d45.
  • Static review only: no build, flutter analyze, tests, or device run were executed. Layout findings are derived from the flex/constraint math (adaptiveBottomSheetConstraints, LayoutBreakpoint.compact, FilledButton.icon intrinsic width), not from a device.
  • Type-enum finding is based on Bangumi's documented EpType (0 本篇 / 1 特别篇 / 2 OP / 3 ED / 4 预告·宣传·广告 / 5 MAD / 6 其他).

Fix these issues in Kilo Cloud

Previous Review Summary (commit 475bbf6)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 475bbf6)

Status: 12 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 2
WARNING 7
SUGGESTION 3
Issue Details (click to expand)

CRITICAL

File Line Issue
lib/pages/info/info_page.dart 250 Sheet is a non-observing StatelessWidget on a separate route: 重试 and in-flight loading never update the visible sheet
lib/pages/info/info_controller.dart 45 info_controller.g.dart was not regenerated, so the four new @observable fields have no MobX atoms and are silently non-reactive

WARNING

File Line Issue
lib/pages/info/info_controller.dart 236 Partial pagination failure returns a truncated list that is reported as success
lib/pages/info/info_controller.dart 241 Genuinely empty results are rendered as 集数获取失败; episodesIsEmpty is dead state and the sheet's empty branch is unreachable
lib/pages/info/info_controller.dart 67 clearEpisodes() resets the in-flight flag, breaking the re-entrancy guard; no request/subject token, so a late response can leak another subject's episodes
lib/pages/info/episode_list_sheet.dart 191 ListView.builder is effectively non-lazy (one item per type section + shrinkWrap), building thousands of SelectableText widgets for long-running subjects
lib/pages/info/episode_list_sheet.dart 51 type > 3 (PV/CM, MAD, 其他) renders labels like 12:/0: with no category, and grouping by raw type creates duplicate 其它 sections
lib/bean/card/bangumi_info_card.dart 253 Fixed 120 + 8 + 40 = 168px non-shrinkable row likely overflows below ~384dp width (~12px at 360dp, ~32px at 320dp)
lib/pages/info/info_page.dart 222 Full paginated episode list is prefetched on every detail-page open even if the sheet is never opened

SUGGESTION

File Line Issue
lib/bean/card/bangumi_info_card.dart 303 Icon-only button lacks tooltip/semantics label (neighbouring collect button has one)
lib/pages/info/episode_list_sheet.dart 22 bangumiItem is required but never used
lib/pages/info/info_page.dart 240 retryLoadEpisodes duplicates loadEpisodes; the setState(() {}) in both is churn since build() reads no episode state
Files Reviewed (4 files)
  • lib/pages/info/episode_list_sheet.dart - 3 issues
  • lib/pages/info/info_controller.dart - 4 issues
  • lib/pages/info/info_page.dart - 3 issues
  • lib/bean/card/bangumi_info_card.dart - 2 issues
Notes & assumptions
  • Review is static only: no build, tests, or app run were executed. The overflow finding is derived from the flex/width math in bangumi_info_card.dart and the page padding in info_page.dart:470, not from a device run.
  • The stale-codegen finding was verified by reading the committed lib/pages/info/info_controller.g.dart (atoms stop at staffList).

Fix these issues in Kilo Cloud


Reviewed by claude-opus-5 · Input: 54 · Output: 29.7K · Cached: 1.5M

@Predidit

Copy link
Copy Markdown
Owner

代码审查机器人报告的问题全部需要解决,我对使用 AI 辅助编程没有任何意见,但至少使用较强的 AI 以减少返工和交流成本

此外此功能的意义是什么,仅展示还是你希望后期直接在选中时跳转到对应集数

@BrainWangs

Copy link
Copy Markdown
Author

我的错, 我只基于自己的环境做了功能测试, 没有审查代码;

此功能仅作展示, 目前的痛点是用户必须选中一个播放源才能知道这个番剧更新到多少集, 如果能在详情页直接查看会方便很多

3: 'ed',
4: 'pv',
5: 'cm',
6: 'mad',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Episode type map is shifted by one for types 5 and 6

Bangumi's EpType enum is 0 = 本篇, 1 = 特别篇, 2 = OP, 3 = ED, 4 = 预告/宣传/广告 (PV/CM), 5 = MAD, 6 = 其他 — there is no distinct "CM" type. This map assigns 5 -> 'cm' / '广告' and 6 -> 'mad' / 'MAD', so real type-5 MAD episodes are labelled 广告 and type-6 其他 episodes are labelled MAD. The mislabeling shows up both in the new sheet section headers (typeSectionTitles) and in the player UI via readType() (lib/pages/player/episode_comments_sheet.dart:175).

Map 5 to MAD and let 6 fall through to otherSectionTitle (or map it explicitly to 其他).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

final typeStr = readType().toUpperCase();
final isOther = typeStr == otherAbbreviation.toUpperCase();
// 已知类型且有 sort:显示前缀+编号
if (!isOther && episode > 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: episode > 0 silently drops the episode number for sort == 0

EpisodeInfo.fromJson defaults a missing sort to 0 (line 19), and Bangumi legitimately returns sort == 0 for prologue / 第0話 entries, so this branch cannot distinguish "no sort" from "sort is zero". Those rows now render as a bare EP: instead of EP0:, and if a section contains more than one of them every row shows an identical label with no way to tell the episodes apart.

Track sort presence explicitly (nullable sort, or a hasSort flag set in fromJson) instead of overloading 0.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

return 'ed';
default:
return '';
return typeAbbreviations[type] ?? otherAbbreviation;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Changing the readType() fallback leaks into the player UI

readType() previously returned '' for unmapped types; it now returns 'other'. The pre-existing consumer formats '${readType()}.${episode}' (lib/pages/player/episode_comments_sheet.dart:175, :182, :183, :266), so the player header and episode picker change from .12 to OTHER.12/other.12 for unknown types — a side effect of a model-level change that this PR does not otherwise touch.

Keep the raw abbreviation empty for unknown types and apply the other fallback in the new sheet only, or update the player call sites deliberately.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

return typeStr;
}
// 未知类型:用 #type 编号标识,不显示空前缀
return '#$type';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: #$type puts a type id where an episode number is expected

Every other prefix in this slot ends with an episode number, so an unknown type 9 renders #9: which reads as "episode 9". It also discards a present sort, so all unknown-type rows inside one section get the identical label.

Including the sort (e.g. #<sort> when available) keeps rows distinguishable and avoids implying the value is a type id.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

// 用 CustomScrollView + SliverList 实现真正的懒加载:
// 每个 section 一个 SliverToBoxAdapter(header) + SliverList(episodes),
// 长番剧只构建可见区域内的 tile。
return CustomScrollView(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Non-shrink-wrapping CustomScrollView makes the sheet height jump and over-tall

CustomScrollView defaults to shrinkWrap: false, and a viewport under Flexible (loose constraints, bounded by adaptiveBottomSheetConstraints' maxHeight = 0.75 * screenHeight, 0.9 in compact landscape) always takes the full available height. The skeleton branch at line 108 uses SingleChildScrollView, which does size to its child, so the sheet opens short and then snaps to ~75% of the screen when data arrives; a movie with one or two records renders a mostly empty sheet. The previous implementation used ListView(shrinkWrap: true) and sized to content.

Use CustomScrollView(shrinkWrap: true) (lazy sliver building is preserved) or give all four states a consistent height.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

}
return [
for (final type in order)
(title: EpisodeInfo.sectionTitleForType(type), episodes: byType[type]!),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Two unmapped types produce two adjacent sections both titled 其它

Grouping is keyed by the raw type value while the title falls back to the shared EpisodeInfo.otherSectionTitle, so a subject that returns two different unmapped types (e.g. 7 and 8) renders duplicate 其它 (n) headers with no way to tell them apart.

Bucket all unmapped types into a single group, or append the raw type to the fallback title.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

}

/// 仅清空数据,保留 in-flight 标志 [episodesIsLoading]。
/// 用于 sheet 关闭后重置展示数据,避免破坏 [queryBangumiEpisodesByID]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Doc comment contradicts the only call site, and this leaves the flags inconsistent

The comment says the method is for resetting display data "sheet 关闭后", but nothing runs on sheet close — the sole caller is InfoPage.dispose() (lib/pages/info/info_page.dart:319). At dispose this wipes episodeList while leaving episodesIsLoading / episodesQueryTimeout set, so the controller is left in a self-contradictory state (flags say "loading"/"failed" with an empty list). It only self-corrects because initState calls resetEpisodesState(); if this controller instance is ever reached without a fresh initState, the gate at info_page.dart:241-243 will permanently skip loading and the sheet shows the skeleton forever.

Align the comment with the actual call site, and prefer a request/subject token over flag juggling to protect against late responses.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

// 用 mutation 方式更新 ObservableList(clear + addAll),
// 而非替换整个 list 引用。符合 MobX 官方文档推荐用法:
// Observer 会跟踪 list 内部 mutation,无需替换引用。
episodeList

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: clear() + addAll() outside a MobX action publishes an intermediate empty list

These are two independent observable mutations performed outside any action (this store declares no @action, unlike video_controller.dart / download_controller.dart), so each load produces up to four reaction passes and briefly exposes an empty episodeList to the new Observer in episode_list_sheet.dart:222.

Wrap the update in runInAction (or annotate an @action and regenerate codegen) so the list swap and the flag updates are atomic.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Flexible(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 80,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: These constraints are mostly inert, and the label can still be squeezed on narrow screens

Flexible passes loose constraints, so minWidth: 80 is clamped away by BoxConstraints.enforce whenever free space is smaller (which is exactly the overflow case this is meant to guard), and maxWidth: 160 is never reached because FilledButton.icon takes its ~94dp intrinsic width — the collect button therefore no longer has the stable 120dp width it used to have. With the outer padding of 16/16 (info_page.dart:470) and the vote chart suppressed below 600dp, the right-hand column gets ~136dp at a 320dp screen, leaving ~88dp after the 40dp icon button and 8dp gap — below the button's intrinsic width, so the label is compressed and wraps inside the fixed height: 40 (also reachable at ~1.2x text scale on 360dp).

The RenderFlex overflow is fixed, but consider FittedBox, a maxLines: 1 + ellipsis label, or dropping the fixed height so the squeeze degrades gracefully.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


/// 加载集数详情。仅在首次打开 sheet 或重试时调用。
Future<void> loadEpisodes() async {
if (!mounted) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: This mounted check cannot prevent post-dispose work

The guard runs before the only await and no state is touched after it, so it protects nothing; what it does do is silently no-op a 重试 tap in an edge case (the sheet is a separate route, so it can outlive the page State), leaving the user with an error view and an unresponsive button.

Drop the guard, or replace it with a post-await staleness check that compares the requested subject id against infoController.bangumiItem.id before the result is applied.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants