Uh oh!
There was an error while loading. Please reload this page.
Fix/version week - #533
Conversation
🔍 OpenCodeReview found 13 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, | |
| constSizedBox(width:AppSpacing.sm), | |
| Flexible( | |
| child:Text( | |
| l10n.sponsorTitle, | |
| maxLines:1, | |
| overflow:TextOverflow.ellipsis, | |
| style: theme.textTheme.titleMedium?.copyWith( | |
| fontWeight:FontWeight.w700, | |
| color: gold.ink, | |
| ), | |
| ), | |
| ), | |
| constSizedBox(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), | |
| ), | |
| constSizedBox(width:AppSpacing.sm), | |
| Expanded( | |
| child:Text( | |
| l10n.sponsorTitle, | |
| maxLines:1, | |
| overflow:TextOverflow.ellipsis, | |
| style: theme.textTheme.titleMedium?.copyWith( | |
| fontWeight:FontWeight.w700, | |
| color: gold.ink, | |
| ), | |
| ), | |
| ), | |
| constSizedBox(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
Optimization(zh-Hant): 贊助卡片改為橫式排版,與右欄其他卡片對齊更好看 Optimization(en-US): the sponsor card now row-aligns with the right column
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
Fix(zh-Hant): 修正「更多」頁上方三張卡片的圖示與文字起點對齊 Fix(en-US): align the icons and text of the three hero cards on the More page
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
68ddee6Compare| 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
New(zh-Hant): 版本卡片徽章加上 build 日期 New(en-US): The More page badge shows the day the build was cut
New(zh-Hant): 版本徽章右側顯示 build 日期 New(en-US): The version badge now carries the day this build was cut
| 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:
| staticFuture<void> ensureLoaded() async { | |
| if (_label !=null) return; | |
| String platformVersion =''; | |
| staticbool _isLoaded =false; | |
| staticFuture<void> ensureLoaded() async { | |
| if (_isLoaded) return; | |
| String platformVersion =''; | |
| try { | |
| final info =awaitPackageInfo.fromPlatform(); | |
| platformVersion = info.version; | |
| } onObject { | |
| // 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 | |
| Stringget moreTagline =>'Platform Integrasi Informasi Bencana'; | |
| @override | |
| Stringget 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: (_) {}), | |
| ); |
New(zh-Hant): 首頁地區列下方浮一條金色支持橫條,點擊前往贊助頁 New(en-US): Home floats a gold support pill under the region bar, opening the sponsor page
New(zh-Hant): 繁體贊助相關文案改用「支援」 New(en-US): Traditional-Chinese support copy now reads 支援, not 支持
New(zh-Hant): README 新增 Android 測試版與 iOS TestFlight 連結 New(en-US): README now links the Android testing track and the iOS TestFlight beta
Fix(zh-Hant): 修正公測使用者可能被當成正式版使用者,收到比手上還舊的更新提示 Fix(en-US): fix a beta tester being treated as a stable user and offered an older build
| @override | ||
| String get moreTagline => 'Platform Integrasi Informasi Bencana'; |
There was a problem hiding this comment.
印尼文翻譯建議確認語意一致性。目前的翻譯為 "災害資訊整合平台" (Platform Integrasi Informasi Bencana),而英文版本為 "Disaster Prevention Information Platform",中文版本為 "防災資訊整合平台",兩者皆強調了「預防/防災」(Prevention) 的含義。建議確認是否應加入預防含義(例如使用 "Pencegahan Bencana"),以保持各語言間語意的一致性。
Suggestion:
| @override | |
| Stringget moreTagline =>'Platform Integrasi Informasi Bencana'; | |
| @override | |
| Stringget moreTagline =>'Platform Integrasi Informasi Pencegahan Bencana'; |
| @override | ||
| String get moreTagline => '防灾信息整合平台'; |
Fix(zh-Hant): 修正在「更多」點開 App 日誌後畫面狂刷並卡死 Fix(en-US): fix the app freezing after opening the log screen from More
Optimization(zh-Hant): App 日誌改用內建畫面,多了搜尋、等級篩選與分享 Optimization(en-US): the log screen gains search, level filtering and sharing
Fix(zh-Hant): 修正日誌頁把同一個錯誤不斷印到主控台,回放的紀錄也不再遺失等級 Fix(en-US): stop the log screen reprinting one fault forever, and keep replayed levels
7875cde to
bebb1e4CompareFix(zh-Hant): 修正開啟日誌頁會把整份紀錄重印到主控台,並重複寫回資料庫 Fix(en-US): opening the log no longer reprints the whole table or writes it back
Fix(zh-Hant): 修正日誌頁的清除只清畫面,重開又整份回來 Fix(en-US): clearing the log no longer leaves everything to come back on reopen
Fix(zh-Hant): 修正日誌頁上方等級篩選器把回放的紀錄全算成 undefined Fix(en-US): the level filter counts replayed lines under their level again
Fix(zh-Hant): 修正開啟日誌頁會把本次啟動的紀錄擠掉,順序也錯亂 Fix(en-US): opening the log no longer pushes out the running session's lines
Fix(zh-Hant): 修正啟動早期的紀錄從來沒被存下來,當機時最該看的那幾行都不見 Fix(en-US): fix the launch's own log lines never being stored, the ones a crash needs
Optimization(zh-Hant): 日誌的等級標籤改成大寫,一眼看得出哪一行不是 INFO Optimization(en-US): log lines are tagged INFO / WARN / ERROR, readable at a glance
Optimization(zh-Hant): 主控台的日誌改成一則一行,不再有框線與跑掉的顏色碼 Optimization(en-US): console logs are one plain line each, without borders or stray colour codes
Optimization(zh-Hant): 主控台的等級標籤可以上色,用 --dart-define=DPIP_LOG_COLOR=true 開啟 Optimization(en-US): the console level tag can be coloured with --dart-define=DPIP_LOG_COLOR=true
Fix(zh-Hant): 修正 iOS 上開啟主控台顏色只會多出跳脫字元,看不到顏色 Fix(en-US): enabling console colour on iOS added escape characters instead of colour
Optimization(zh-Hant): 新增 tool/colorize_logs.sh,在終端機端替日誌上色 Optimization(en-US): add tool/colorize_logs.sh, which colours the log in the terminal
Optimization(zh-Hant): 新增 tool/run.sh,一個指令跑起來就有上色的日誌 Optimization(en-US): add tool/run.sh so one command runs the app with a coloured log
Fix(zh-Hant): 修正結束執行時出現 Broken pipe 的未處理例外 Fix(en-US): fix the unhandled Broken pipe exception when a run is stopped
New(zh-Hant): 修復伺服器狀態頁依賴的網路層支援 New(en-US): restore the ApiClient hooks the status page relies on
New(zh-Hant): 除錯版偵測到非腳本啟動將直接拒絕執行並提示正確指令,新增 Windows 啟動腳本 New(en-US): the debug build refuses non-script launches and there is now a Windows run script
New(zh-Hant): 取得 App 下方新增 測試版 與 合作夥伴 區塊 New(en-US): beta channels and partner tiles now sit under Get the app
New(zh-Hant): 伺服器狀態查詢在離線時會改用上次快取結果 New(en-US): status POSTs cache under their URL and serve offline
New(zh-Hant): 更新日誌的貢獻者改為頭像加名稱的名牌,可點開 GitHub 個人頁 New(en-US): changelog contributors are avatar + name badges that open GitHub
New(zh-Hant): 地震報告圖快取到本機,重複查看不再每次重新下載 New(en-US): the report image now round-trips the ETag cache
New(zh-Hant): 無背景定位支援的平台不再回報麵包屑傾倒錯誤 New(en-US): breadcrumb draining no longer trips on missing plugins
4b5a16f to
6508e17Compare`bool.fromEnvironment` reads only the exact string `true` and answers `false` to everything else, including the `1` the script passed — so the guard fired on the very launch that had obeyed it. That is the worst way for a guard to fail: it punishes the correct behaviour and teaches people to ignore it. The scripts send `true` now, and the marker is read with `String.fromEnvironment != ''` so any value counts — the next person to edit that line will not remember this either. The colouriser also missed the prefix on a blank line, which Flutter writes as `flutter:` with no trailing space, so a multi-line message kept it.
Fix(zh-Hant): 更新日誌改為標示真正寫這項變更的人,並附上可點擊的 commit 連結 Fix(en-US): each entry credits who wrote it and links the commit it came from GitHub squashes a pull request into one commit and sets its author to whoever pressed the button, demoting everyone who wrote it to a `Co-authored-by:` trailer. `41a3c1e8 Fix eew (#534)` is authored by the maintainer who merged it and was written, every commit of it, by somebody else — so 26w34b credited the wrong person, which is worse than crediting nobody. A summary carrying `(#N)` now takes its authors from that pull request's own commits, which is the only place they survive a squash intact; trailers are merged in for a merge without a number, and the commit's own author is the fallback. Every entry also links its commit, which for a squash is also the link to the request. The gate had made this worse by banning `Co-authored-by:` outright. It was aimed at a tool crediting itself, and it destroyed the one record of human attribution a squash leaves behind. It now judges the trailer by who it names.
New(zh-Hant): 除錯資訊頁新增「傾印除錯資訊及日誌」,上傳後複製連結 New(en-US): the debug page can upload the diagnostics and log, and copies the link back The two halves answer different questions and a report needs both: the diagnostics say what this build and this device are, the log says what they just did. Pasting 4000 characters into a chat buries the conversation it is part of, so they go to a paste and only the link comes back. The diagnostics are never trimmed — a partial one reads as a complete one and gets answered as if it were — so the log takes whatever is left of the 4000, filled from the newest backwards and written oldest first. A line goes in whole or not at all, because half a line is a lie about what was logged. The log is in the same shape the terminal uses, so a line pasted from one and a line pasted from the other are the same line.
Change(zh-Hant): 「傾印除錯資訊及日誌」移到更多 → 除錯資訊下方,並翻譯成十種語言 Change(en-US): the debug dump moved to More, under Debug info, and is now translated It was a row at the bottom of the Developer page, which is the page a person reaches only after being told where it is. The dump exists for the people who have not been told, so it now sits in the menu they are already in, one row under the page it dumps. Two screens asking for the diagnostics meant the collection could no longer live inside one of them, and the layer rules would not let More reach into Settings for it — so it moved to core/diagnostics, which is where every source it reads already lives. The Developer page renders from the same report it uploads, and the redaction list that keeps the push token and the device identifier out of a paste is now one list instead of two. Two would have drifted, and the copy that drifts is the one that leaks. The row is user-facing, so unlike the Developer page it is translated.
這個 PR 做了什麼
相關 issue
怎麼驗
檢查清單
tool/check_commits.sh origin/main..HEAD通過—— commit 訊息就是更新日誌,格式見 commit.md
mise exec -- flutter analyze與mise exec -- flutter test通過AppLocalizations,沒有寫死AppSpacing/AppRadius/AppMotion,深色模式看過,文字對比度可接受