Fix/version week - #533
Conversation
|
🔍 OpenCodeReview found 6 issue(s) in this PR.
📄
|
| child: Row( | ||
| mainAxisAlignment: MainAxisAlignment.center, | ||
| children: [ | ||
| // Filled, not outlined: the one active affordance on a page | ||
| // whose every other row is an outlined icon. | ||
| Container( | ||
| width: 44, | ||
| height: 44, | ||
| width: 34, | ||
| height: 34, | ||
| decoration: BoxDecoration( | ||
| shape: BoxShape.circle, | ||
| color: gold.badge, | ||
| ), | ||
| child: Icon(Icons.favorite, color: gold.onBadge, size: 24), | ||
| child: Icon(Icons.favorite, color: gold.onBadge, size: 19), | ||
| ), | ||
| Text( | ||
| l10n.sponsorTitle, | ||
| textAlign: TextAlign.center, | ||
| style: theme.textTheme.titleMedium?.copyWith( | ||
| fontWeight: FontWeight.w700, | ||
| color: gold.ink, | ||
| const SizedBox(width: AppSpacing.sm), | ||
| Flexible( | ||
| child: Text( | ||
| l10n.sponsorTitle, | ||
| maxLines: 1, | ||
| overflow: TextOverflow.ellipsis, | ||
| style: theme.textTheme.titleMedium?.copyWith( | ||
| fontWeight: FontWeight.w700, | ||
| color: gold.ink, | ||
| ), | ||
| ), | ||
| ), | ||
| const SizedBox(width: AppSpacing.xs), | ||
| Icon( | ||
| Icons.chevron_right, | ||
| size: 14, | ||
| color: gold.ink.withValues(alpha: 0.7), | ||
| ), | ||
| ], | ||
| ), |
There was a problem hiding this comment.
根據程式碼註解的設計意圖,「右側欄位應呈現為一個對齊的堆疊(the right column reads as one aligned stack)」,意指後方的箭頭(trailing arrow)應該在垂直方向上對齊。
然而,目前的實作使用了 MainAxisAlignment.center 搭配 Flexible,這會導致:
- 當文字較短時,整個
Row的內容會集中在中間,箭頭的位置會隨文字長度而左右移動,無法形成右側對齊的「列」。 Flexible不會強制佔用剩餘空間,只有在文字過長需要截斷時才會縮減。
建議改為:
- 將
MainAxisAlignment.center改為MainAxisAlignment.start(或移除,因為預設即為 start)。 - 將
Flexible改為Expanded。這樣文字會填滿中間的剩餘空間,將箭頭強制推至 Row 的最右側,從而達成註解所述的「右側對齊堆疊」效果。
Suggestion:
| child: Row( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| // Filled, not outlined: the one active affordance on a page | |
| // whose every other row is an outlined icon. | |
| Container( | |
| width: 44, | |
| height: 44, | |
| width: 34, | |
| height: 34, | |
| decoration: BoxDecoration( | |
| shape: BoxShape.circle, | |
| color: gold.badge, | |
| ), | |
| child: Icon(Icons.favorite, color: gold.onBadge, size: 24), | |
| child: Icon(Icons.favorite, color: gold.onBadge, size: 19), | |
| ), | |
| Text( | |
| l10n.sponsorTitle, | |
| textAlign: TextAlign.center, | |
| style: theme.textTheme.titleMedium?.copyWith( | |
| fontWeight: FontWeight.w700, | |
| color: gold.ink, | |
| const SizedBox(width: AppSpacing.sm), | |
| Flexible( | |
| child: Text( | |
| l10n.sponsorTitle, | |
| maxLines: 1, | |
| overflow: TextOverflow.ellipsis, | |
| style: theme.textTheme.titleMedium?.copyWith( | |
| fontWeight: FontWeight.w700, | |
| color: gold.ink, | |
| ), | |
| ), | |
| ), | |
| const SizedBox(width: AppSpacing.xs), | |
| Icon( | |
| Icons.chevron_right, | |
| size: 14, | |
| color: gold.ink.withValues(alpha: 0.7), | |
| ), | |
| ], | |
| ), | |
| child: Row( | |
| mainAxisAlignment: MainAxisAlignment.start, | |
| children: [ | |
| // Filled, not outlined: the one active affordance on a page | |
| // whose every other row is an outlined icon. | |
| Container( | |
| width: 34, | |
| height: 34, | |
| decoration: BoxDecoration( | |
| shape: BoxShape.circle, | |
| color: gold.badge, | |
| ), | |
| child: Icon(Icons.favorite, color: gold.onBadge, size: 19), | |
| ), | |
| const SizedBox(width: AppSpacing.sm), | |
| Expanded( | |
| child: Text( | |
| l10n.sponsorTitle, | |
| maxLines: 1, | |
| overflow: TextOverflow.ellipsis, | |
| style: theme.textTheme.titleMedium?.copyWith( | |
| fontWeight: FontWeight.w700, | |
| color: gold.ink, | |
| ), | |
| ), | |
| ), | |
| const SizedBox(width: AppSpacing.xs), | |
| Icon( | |
| Icons.chevron_right, | |
| size: 14, | |
| color: gold.ink.withValues(alpha: 0.7), | |
| ), | |
| ], | |
| ), |
Fix(zh-Hant): 修正週一上午建置的版本號會標成上一週 Fix(en-US): fix a Monday-morning build being named for the previous week The week came from UTC, and the people reading the label are eight hours ahead — so every Monday between 00:00 and 08:00 Taipei fell in the previous ISO week. `26w33e` was built at 07:47 that Monday and the build an hour later became `26w34a`: two consecutive snapshots, a week apart by name. Report dates already use Asia/Taipei (api.md); the label now does too.
Optimization(zh-Hant): 贊助卡片改為橫式排版,與右欄其他卡片對齊更好看 Optimization(en-US): the sponsor card now row-aligns with the right column
The More page will lead the version card with a stable anchor number: a release names itself and a snapshot names the release it builds toward. That value — the newest v* tag, v stripped — now rides every build the same way the label does, through version.sh, the generated build info, and CI's dart-define.
New(zh-Hant): 更多頁上方卡片重新設計,版本以漸層數字顯示,pre-release 顯示上一版主版本號 New(en-US): the More hero cards go flat, and the version card leads with a gradient major.minor named after the last release
Fix(zh-Hant): 修正圖層時間軸在某些時區會顯示成 UTC 時間 Fix(en-US): map timelines now show frame times in the device's local time instead of UTC
The version card's big number is the train (26.1) — the release a snapshot is cut toward — not the previous release tag. version.sh already derives it; this replaces the last-release pipeline with a train pipeline end to end: script output, generated build_info, the AppBuild surface, and the dart-defines CI stamps. No user-visible change yet; the card itself lands in the next commit.
The support, Discord and announcement cards each centred their row, so every icon and label started where its own row's text happened to end. Now they all start at the same left margin and the trailing arrows sit at the same right margin. Fix(zh-Hant): 修正「更多」頁上方三張卡片的圖示與文字起點對齊 Fix(en-US): align the icons and text of the three hero cards on the More page
The version card now reads 26.1 first — the train every build rides — and a snapshot names itself beside it in smaller print (26w34a). Both are larger than before so the number is the thing the eye lands on. New(zh-Hant): 「更多」頁版本卡改以版號 26.1 為主,測試版同時標示完整版號 New(en-US): the version card on the More page leads with 26.1 and shows the full snapshot name beneath it
New(zh-Hant): 更多頁英雄區加入伺服器狀態卡,並縮小版本卡 New(en-US): Add the server-status card to the More hero block and slim the version card
fee2df0 to
68ddee6
Compare
| static const double _versionHeight = 176; | ||
|
|
||
| /// Height of a small card (Discord, announcement, status) and, matching it, | ||
| /// the full-width support card below. | ||
| static const double _smallCardHeight = 56; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return SizedBox( | ||
| height: _height, | ||
| child: Padding( | ||
| padding: const EdgeInsets.fromLTRB( | ||
| AppSpacing.lg, | ||
| 0, | ||
| AppSpacing.lg, | ||
| AppSpacing.md, | ||
| ), | ||
| child: Row( | ||
| crossAxisAlignment: CrossAxisAlignment.stretch, | ||
| children: [ | ||
| const Expanded(flex: 1, child: _VersionCard()), | ||
| const SizedBox(width: AppSpacing.md), | ||
| Expanded( | ||
| flex: 1, | ||
| child: Column( | ||
| children: const [ | ||
| Expanded(flex: 2, child: _SupportCallout()), | ||
| SizedBox(height: AppSpacing.xs), | ||
| Expanded(flex: 1, child: _DiscordCallout()), | ||
| SizedBox(height: AppSpacing.xs), | ||
| Expanded(flex: 1, child: _AnnouncementCard()), | ||
| ], | ||
| return Padding( | ||
| padding: const EdgeInsets.fromLTRB( | ||
| AppSpacing.lg, | ||
| 0, | ||
| AppSpacing.lg, | ||
| AppSpacing.md, | ||
| ), | ||
| child: Column( | ||
| crossAxisAlignment: CrossAxisAlignment.stretch, | ||
| children: [ | ||
| Row( | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| children: [ | ||
| Expanded( | ||
| child: SizedBox( | ||
| height: _versionHeight, | ||
| child: const _VersionCard(), | ||
| ), | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| const SizedBox(width: AppSpacing.md), | ||
| Expanded( | ||
| child: Column( | ||
| children: const [ | ||
| SizedBox( | ||
| height: _smallCardHeight, | ||
| child: _DiscordCallout(), | ||
| ), | ||
| SizedBox(height: AppSpacing.xs), | ||
| SizedBox( | ||
| height: _smallCardHeight, | ||
| child: _AnnouncementCard(), | ||
| ), | ||
| SizedBox(height: AppSpacing.xs), | ||
| SizedBox(height: _smallCardHeight, child: _StatusCard()), | ||
| ], | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| const SizedBox(height: AppSpacing.md), | ||
| SizedBox(height: _smallCardHeight, child: const _SupportCallout()), | ||
| ], | ||
| ), | ||
| ); | ||
| } |
| static List<Color> _hashGradient(String seed, Brightness brightness) { | ||
| var h = 7; | ||
| for (final rune in seed.runes) { | ||
| h = (h * 31 + rune) & 0x7fffffff; | ||
| } | ||
| final base = h % 360; | ||
| const saturation = 0.62; | ||
| final light = brightness == Brightness.dark ? 0.70 : 0.46; | ||
| return [ | ||
| HSLColor.fromAHSL(1, base.toDouble(), saturation, light).toColor(), | ||
| HSLColor.fromAHSL( | ||
| 1, | ||
| (base + 137.508) % 360, | ||
| saturation, | ||
| light - 0.10, | ||
| ).toColor(), | ||
| ]; | ||
| } |
| final end = _time.format( | ||
| widget.frames[_liveIndex].time.toLocal().add(period), | ||
| ); |
There was a problem hiding this comment.
當 widget.timeFormat 與預設的 _time 不同時,start 會使用 widget.timeFormat 格式化,而 end 會使用 _time 格式化,導致顯示的範圍標籤格式不一致。建議在計算 end 時也使用 widget.timeFormat ?? _time。
Suggestion:
| final end = _time.format( | |
| widget.frames[_liveIndex].time.toLocal().add(period), | |
| ); | |
| final format = widget.timeFormat ?? _time; | |
| final end = format.format( | |
| widget.frames[_liveIndex].time.toLocal().add(period), | |
| ); |
New(zh-Hant): 版本卡片正式版也會顯示完整版本號 New(en-US): The hero version card now shows a version line for releases too
New(zh-Hant): 新版號 26.2.1 的版型只顯示 26.2 New(en-US): A three-part release (26.2.1) advertises as 26.2; the patch stays in the label
| static Future<void> ensureLoaded() async { | ||
| if (_label != null) return; | ||
| String platformVersion = ''; |
There was a problem hiding this comment.
ensureLoaded 方法目前的冪等性檢查依賴於 _label != null。如果 _bestLabel 為空或 _bestCode 不大於 0(例如在某些開發環境下),_label 將保持為 null,導致每次呼叫 ensureLoaded 都會重新執行非同步的 PackageInfo.fromPlatform()。建議使用一個獨立的布林值(如 _isLoaded)來標記載入狀態,以避免重複的 IO 操作。
Suggestion:
| static Future<void> ensureLoaded() async { | |
| if (_label != null) return; | |
| String platformVersion = ''; | |
| static bool _isLoaded = false; | |
| static Future<void> ensureLoaded() async { | |
| if (_isLoaded) return; | |
| String platformVersion = ''; | |
| try { | |
| final info = await PackageInfo.fromPlatform(); | |
| platformVersion = info.version; | |
| } on Object { | |
| // A version readout is never worth failing a launch over. The platform | |
| // version line simply stays empty for that build. | |
| } | |
| _platformVersion = platformVersion; | |
| if (_bestLabel.isNotEmpty && _bestCode > 0) { | |
| _label = _bestLabel; | |
| _code = _bestCode; | |
| } | |
| _isLoaded = true; | |
| } |
| child: Column( | ||
| children: const [ | ||
| SizedBox( | ||
| height: _smallCardHeight, | ||
| child: _DiscordCallout(), | ||
| ), | ||
| SizedBox(height: AppSpacing.xs), | ||
| SizedBox( | ||
| height: _smallCardHeight, | ||
| child: _AnnouncementCard(), | ||
| ), | ||
| SizedBox(height: AppSpacing.xs), | ||
| SizedBox(height: _smallCardHeight, child: _StatusCard()), | ||
| ], | ||
| ), |
There was a problem hiding this comment.
_HeroCards 的佈局使用了固定的高度(如 _versionHeight 和 _smallCardHeight)來包裹包含文字內容的組件(例如 _DiscordCallout、_AnnouncementCard、_StatusCard 和 _VersionCard)。這在 Flutter 中存在嚴重的佈局溢出(Overflow)風險,特別是當使用者在系統設定中啟用了「大字體模式」(Font Scaling),或者在不同語言的本地化過程中,文字內容長度增加導致高度超過預設值時。建議考慮使用 IntrinsicHeight 來讓 Row 中的元素高度同步,或者改用 BoxConstraints 的 minHeight 而非固定 height,以允許組件根據內容自動增長。
| static List<Color> _hashGradient(String seed, Brightness brightness) { | ||
| var h = 7; | ||
| for (final rune in seed.runes) { | ||
| h = (h * 31 + rune) & 0x7fffffff; | ||
| } | ||
| final base = h % 360; | ||
| const saturation = 0.62; | ||
| final light = brightness == Brightness.dark ? 0.70 : 0.46; | ||
| return [ | ||
| HSLColor.fromAHSL(1, base.toDouble(), saturation, light).toColor(), | ||
| HSLColor.fromAHSL( | ||
| 1, | ||
| (base + 137.508) % 360, | ||
| saturation, | ||
| light - 0.10, | ||
| ).toColor(), | ||
| ]; | ||
| } |
| @override | ||
| String get moreTagline => 'Platform Integrasi Informasi Bencana'; |
There was a problem hiding this comment.
印尼文翻譯與英文原文語意不一致。英文定義為 'Disaster Prevention Information Platform' (災害預防資訊平台),但目前的印尼文翻譯為 'Platform Integrasi Informasi Bencana' (災害資訊整合平台),其中 'Integrasi' 意為 '整合' 而非 '預防' (Pencegahan)。建議修正以符合原文語意。
Suggestion:
| @override | |
| String get moreTagline => 'Platform Integrasi Informasi Bencana'; | |
| @override | |
| String get moreTagline => 'Platform Informasi Pencegahan Bencana'; |
| final utc = DateTime.utc(2026, 7, 13, 14, 30); // 22:30 in UTC+8 | ||
| final frames = [MapFrame(id: '0', time: utc)]; | ||
| await tester.pumpWidget( | ||
| _wrap(frames: frames, selectedIndex: 0, onSelected: (_) {}), | ||
| ); |
There was a problem hiding this comment.
該測試案例高度依賴於執行環境的時區。若在 CI 環境(通常時區設定為 UTC)中執行,utc.toLocal() 的結果會與 utc 完全相同。這意味著即使程式碼錯誤地直接顯示了 UTC 時間,測試依然會通過,從而無法達到攔截 Bug 的目的。建議選擇一個在轉換為本地時間後會明顯改變小時或日期的時間點(例如接近 UTC 當天結尾的時間),以增加測試在不同環境下的魯棒性。
Suggestion:
| final utc = DateTime.utc(2026, 7, 13, 14, 30); // 22:30 in UTC+8 | |
| final frames = [MapFrame(id: '0', time: utc)]; | |
| await tester.pumpWidget( | |
| _wrap(frames: frames, selectedIndex: 0, onSelected: (_) {}), | |
| ); | |
| // 建議選擇一個轉換後容易產生差異的時間點,例如 UTC 23:30 | |
| final utc = DateTime.utc(2026, 7, 13, 23, 30); | |
| final frames = [MapFrame(id: '0', time: utc)]; | |
| await tester.pumpWidget( | |
| _wrap(frames: frames, selectedIndex: 0, onSelected: (_) {}), | |
| ); |
這個 PR 做了什麼
相關 issue
怎麼驗
檢查清單
tool/check_commits.sh origin/main..HEAD通過—— commit 訊息就是更新日誌,格式見 commit.md
mise exec -- flutter analyze與mise exec -- flutter test通過AppLocalizations,沒有寫死AppSpacing/AppRadius/AppMotion,深色模式看過,文字對比度可接受