Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions assets/documentations/release_note/archive/0.0.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## 更新内容

这是一个预览版本。仅用于早期技术预览。
1. 修复了步长控制
2. 修复了分数过长时显示不全的问题

## 我应该下载哪个文件?

| 文件 | 描述 |
|-------------------------------------------------|-------------------------------------------------------------------------------------------------|
| **安卓** | |
| UtopiaScoreboard-x.x.x-Android-v8a.apk | 64位包,适用于常见主流的安卓手机 |
| UtopiaScoreboard-x.x.x-Android-v7a.apk | 32位包,适用于非常老旧的安卓手机 |
| UtopiaScoreboard-x.x.x-Android-x86_64.apk | 桌面模拟器或x86架构的设备如Chrome Book等 |
| **iPhone** | |
| UtopiaScoreboard-x.x.x-iOS-universal.ipa | 请使用[AltStore](https://altstore.io/)进行侧载安装。具体教程请参考[官方文档](https://faq.altstore.io/)。 |
| **macOS** | |
| UtopiaScoreboard-x.x.x-macOS-universal.dmg | 请开启信任任何来源,请参考: [打开来自未知开发者的 Mac App](https://support.apple.com/zh-cn/guide/mac-help/mh40616/mac) |
| **Windows** | |
| UtopiaScoreboard-x.x.x-Windows-x64-setup.exe | 推荐。需要安装。 |
| UtopiaScoreboard-x.x.x-Windows-x64-setup.msi | 暂无,无法打包。 |
| UtopiaScoreboard-x.x.x-Windows-x64-portable.zip | 便携版。如不希望安装可以使用本包。 |
| **Linux** | |
| UtopiaScoreboard-x.x.x-Linux-x64.AppImage | 推荐,通用不限平台 |
| UtopiaScoreboard-x.x.x-Linux-x64.rpm | RHEL、CentOS、Fedora等 (RedHat系) |
| UtopiaScoreboard-x.x.x-Linux-x64.deb | Debian、Ubuntu等 (Debian系) |
| **Web** | |
| UtopiaScoreboard-x.x.x-Web.zip | 静态前端APP |

26 changes: 26 additions & 0 deletions lib/providers/settings_provider.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class SettingsProvider extends ChangeNotifier {
double _uiScale = 1.0;
bool _initialized = false;
List<int> _globalScoreSteps = [1, 2, 4, 8, 16, 32, 64, 128];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The default score steps [1, 2, 4, 8, 16, 32, 64, 128] are hardcoded here, in reloadGlobalScoreSteps, and also in ScoreStepsEditor. To improve maintainability and avoid potential inconsistencies, consider defining this list as a static const in a shared location (e.g., SettingsProvider) and referencing it from all these places.


double get uiScale => _uiScale;
bool get initialized => _initialized;
List<int> get globalScoreSteps => _globalScoreSteps;

SettingsProvider() {
_loadSettings();
Expand All @@ -21,6 +24,10 @@ class SettingsProvider extends ChangeNotifier {
_uiScale = 1.0;
_initialized = false;
}
final stepsJson = prefs.getString('global_score_steps');
if (stepsJson != null) {
_globalScoreSteps = (jsonDecode(stepsJson) as List<dynamic>).cast<int>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The jsonDecode call can throw a FormatException if the data in SharedPreferences is corrupted. This could crash the app on startup. It's crucial to wrap this in a try-catch block to handle potential parsing errors gracefully.

try {
  _globalScoreSteps = (jsonDecode(stepsJson) as List<dynamic>).cast<int>();
} catch (e) {
  // Log the error and fall back to the initial default.
  print('Failed to parse global score steps: $e');
}

}
notifyListeners();
}

Expand All @@ -40,4 +47,23 @@ class SettingsProvider extends ChangeNotifier {
await prefs.setDouble('uiScale', _uiScale);
notifyListeners();
}

Future<void> setGlobalScoreSteps(List<int> steps) async {
_globalScoreSteps = steps;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('global_score_steps', jsonEncode(_globalScoreSteps));
notifyListeners();
}

/// Reload global score steps from SharedPreferences (call after ScoreStepsEditor saves)
Future<void> reloadGlobalScoreSteps() async {
final prefs = await SharedPreferences.getInstance();
final stepsJson = prefs.getString('global_score_steps');
if (stepsJson != null) {
_globalScoreSteps = (jsonDecode(stepsJson) as List<dynamic>).cast<int>();
} else {
_globalScoreSteps = [1, 2, 4, 8, 16, 32, 64, 128];
}
Comment on lines +62 to +66
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Similar to _loadSettings, this jsonDecode call should be wrapped in a try-catch block to prevent crashes from malformed JSON. If parsing fails, you should fall back to the default score steps.

if (stepsJson != null) {
  try {
    _globalScoreSteps = (jsonDecode(stepsJson) as List<dynamic>).cast<int>();
  } catch (e) {
    print('Failed to parse global score steps: $e');
    _globalScoreSteps = [1, 2, 4, 8, 16, 32, 64, 128];
  }
} else {
  _globalScoreSteps = [1, 2, 4, 8, 16, 32, 64, 128];
}

notifyListeners();
}
}
6 changes: 6 additions & 0 deletions lib/screens/score_steps_editor.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../providers/settings_provider.dart';

/// Editor for customizing the score step values.
class ScoreStepsEditor extends StatefulWidget {
Expand Down Expand Up @@ -33,6 +35,10 @@ class _ScoreStepsEditorState extends State<ScoreStepsEditor> {
Future<void> _saveSteps() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('global_score_steps', jsonEncode(_steps));
if (mounted) {
Provider.of<SettingsProvider>(context, listen: false)
.reloadGlobalScoreSteps();
}
}
Comment on lines 35 to 42
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The ScoreStepsEditor is directly manipulating SharedPreferences. It's better to centralize state management within the SettingsProvider. Instead of writing to SharedPreferences here and then telling the provider to reload, you should call the setGlobalScoreSteps method on SettingsProvider to update the steps. The provider is then responsible for persisting the change and notifying its listeners. This improves encapsulation and makes the code easier to maintain.

  Future<void> _saveSteps() async {
    if (mounted) {
      await Provider.of<SettingsProvider>(context, listen: false)
          .setGlobalScoreSteps(_steps);
    }
  }


@override
Expand Down
4 changes: 3 additions & 1 deletion lib/widgets/game/score_row.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import '../../models/player.dart';
import '../../providers/game_provider.dart';
import '../../providers/settings_provider.dart';

/// Inline score adjustment row shown when a player is selected.
class ScoreRow extends StatelessWidget {
Expand Down Expand Up @@ -49,7 +51,7 @@ class ScoreRow extends StatelessWidget {
const SizedBox(width: 8),
_SignToggle(game: game),
const SizedBox(width: 6),
...game.scoreSteps.map((step) => _ScoreStepButton(
...Provider.of<SettingsProvider>(context).globalScoreSteps.map((step) => _ScoreStepButton(
game: game,
step: step,
)),
Comment on lines +54 to 57
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 检查 game.scoreSteps 在其他地方的使用情况,以及是否有单局步长自定义的入口

# 查找 updateScoreSteps 的调用点
echo "=== updateScoreSteps 调用点 ==="
rg -n "updateScoreSteps" --type dart

# 查找 scoreSteps 的其他引用
echo -e "\n=== scoreSteps 在 GameProvider 中的使用 ==="
rg -n "scoreSteps" lib/providers/game_provider.dart

# 检查是否有 UI 入口允许用户设置单局步长
echo -e "\n=== 可能的单局步长设置 UI ==="
rg -n -C2 "scoreSteps" lib/screens/ lib/widgets/

Repository: UtopiaXC/UtopiaScoreboard

Length of output: 2009


🏁 Script executed:

# Mencari pemanggilan actual ke updateScoreSteps (bukan hanya definisi)
echo "=== Pemanggilan updateScoreSteps (actual calls) ==="
rg -n "updateScoreSteps\(" --type dart

# Melihat konteks penuh dari game_provider.dart untuk updateScoreSteps
echo -e "\n=== Konteks updateScoreSteps method ==="
sed -n '495,505p' lib/providers/game_provider.dart

# Melihat score_row.dart secara lengkap untuk konteks
echo -e "\n=== Isi score_row.dart ==="
wc -l lib/widgets/game/score_row.dart

Repository: UtopiaXC/UtopiaScoreboard

Length of output: 508


🏁 Script executed:

# Lihat full score_row.dart
echo "=== Full score_row.dart ==="
cat -n lib/widgets/game/score_row.dart

# Cek apakah ada UI untuk mengatur scoreSteps per game
echo -e "\n=== Cari settings atau dialog untuk scoreSteps ==="
rg -n "scoreSteps" lib/screens/ -A 2 -B 2

# Lihat full context dari transfer_dialog.dart untuk scoreSteps usage
echo -e "\n=== transfer_dialog.dart context (lines 70-80) ==="
sed -n '70,80p' lib/widgets/game/transfer_dialog.dart

Repository: UtopiaXC/UtopiaScoreboard

Length of output: 5179


注意:scoreSteps 迁移不完整,存在使用源不一致的问题。

虽然 game.scoreSteps 是在 GameProvider 中定义的单局级设置,但进一步的分析表明:

  1. per-game 自定义功能实际不可用updateScoreSteps() 方法已定义但从未被调用,且不存在用户界面允许自定义单局步长。
  2. 迁移不完整:此更改仅将 score_row.dart 切换到 globalScoreSteps,但 transfer_dialog.dart 仍然使用 game.scoreSteps,导致两个相似功能的数据源不一致。

应统一这两个文件的实现方式,确保整个应用中步长配置来源保持一致。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/widgets/game/score_row.dart` around lines 54 - 57, The score-steps
migration is inconsistent: score_row.dart now uses
Provider.of<SettingsProvider>(context).globalScoreSteps (rendering
_ScoreStepButton with step and game) while transfer_dialog.dart still reads
game.scoreSteps and updateScoreSteps is never invoked; unify the source of
truth. Choose between per-game (game.scoreSteps + wire up updateScoreSteps and
UI to edit it) or global (SettingsProvider.globalScoreSteps) and update both
files to use the chosen API: if keeping global, replace game.scoreSteps reads in
transfer_dialog.dart with SettingsProvider.globalScoreSteps and remove/disable
updateScoreSteps; if keeping per-game, change score_row.dart to use
game.scoreSteps and add UI hooks to call GameProvider.updateScoreSteps where
_ScoreStepButton changes steps. Ensure references to SettingsProvider,
globalScoreSteps, game.scoreSteps, updateScoreSteps, and _ScoreStepButton are
updated accordingly so both score_row.dart and transfer_dialog.dart use the same
data source.

Expand Down
40 changes: 23 additions & 17 deletions lib/widgets/player_avatar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,34 @@ class PlayerAvatar extends StatelessWidget {
Row(
children: [
Flexible(
child: Text(
'积分:${player.totalScore}',
style: GoogleFonts.notoSansSc(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 15,
fontWeight: FontWeight.w500,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
'积分:${player.totalScore}',
style: GoogleFonts.notoSansSc(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
overflow: TextOverflow.ellipsis,
),
),
if (player.currentRoundChange != 0) ...[
const SizedBox(width: 8),
Text(
player.currentRoundChange > 0
? '+${player.currentRoundChange}'
: '${player.currentRoundChange}',
style: GoogleFonts.outfit(
color: player.currentRoundChange > 0
? Colors.greenAccent
: Colors.redAccent,
fontSize: 13,
fontWeight: FontWeight.bold,
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
player.currentRoundChange > 0
? '+${player.currentRoundChange}'
: '${player.currentRoundChange}',
style: GoogleFonts.outfit(
color: player.currentRoundChange > 0
? Colors.greenAccent
: Colors.redAccent,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
Comment on lines +91 to 105
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

增量积分的 FittedBox 缺少宽度约束,大数值时可能无法正确缩放。

与主积分不同,这里的 FittedBox 没有被 Flexible 包裹。在 Row 布局中,无约束的子组件会优先获取其固有尺寸,导致 FittedBox 没有最大宽度限制。当 currentRoundChange 是较大数值时(如 +999999),文本不会缩小,可能挤压主积分显示或导致溢出。

🔧 建议添加 Flexible 包裹
                              const SizedBox(width: 8),
-                               FittedBox(
-                                 fit: BoxFit.scaleDown,
-                                 child: Text(
+                               Flexible(
+                                 child: FittedBox(
+                                   fit: BoxFit.scaleDown,
+                                   alignment: Alignment.centerLeft,
+                                   child: Text(
                                     player.currentRoundChange > 0
                                         ? '+${player.currentRoundChange}'
                                         : '${player.currentRoundChange}',
                                     style: GoogleFonts.outfit(
                                       color: player.currentRoundChange > 0
                                           ? Colors.greenAccent
                                           : Colors.redAccent,
                                       fontSize: 13,
                                       fontWeight: FontWeight.bold,
                                     ),
+                                   ),
                                  ),
                                ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
player.currentRoundChange > 0
? '+${player.currentRoundChange}'
: '${player.currentRoundChange}',
style: GoogleFonts.outfit(
color: player.currentRoundChange > 0
? Colors.greenAccent
: Colors.redAccent,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
player.currentRoundChange > 0
? '+${player.currentRoundChange}'
: '${player.currentRoundChange}',
style: GoogleFonts.outfit(
color: player.currentRoundChange > 0
? Colors.greenAccent
: Colors.redAccent,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/widgets/player_avatar.dart` around lines 91 - 105, The FittedBox showing
player.currentRoundChange inside the Row has no width constraint so large values
can prevent proper scaling; wrap that FittedBox with a Flexible (or
ConstrainedBox with a maxWidth) so it receives a max width and the Text
(player.currentRoundChange) can shrink correctly; update the widget containing
the FittedBox (reference the FittedBox/Text using player.currentRoundChange in
player_avatar.dart) to be Flexible(child: FittedBox(...)) or apply a maxWidth
constraint to ensure it doesn't push/overflow the main score.

],
Expand Down
9 changes: 7 additions & 2 deletions lib/widgets/score_panel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import '../providers/game_provider.dart';
import '../providers/settings_provider.dart';
import 'control_panel.dart';

/// Floating score panel that appears above/below the [ControlPanel]
Expand Down Expand Up @@ -32,6 +33,7 @@ class ScorePanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
final game = Provider.of<GameProvider>(context);
final settings = Provider.of<SettingsProvider>(context);
final selected = game.selectedPlayer;
if (selected == null || game.isZeroSum) {
return const SizedBox.shrink();
Expand Down Expand Up @@ -67,7 +69,7 @@ class ScorePanel extends StatelessWidget {
final double controlCenterX = barPos.dx + ControlPanel.expandedWidth / 2;
final double maxWidth = screenSize.width - _hMargin * 2;
final double estimatedW = _estimatePanelWidth(
game.scoreSteps.length,
settings.globalScoreSteps.length,
selected.name,
).clamp(0.0, maxWidth);

Expand All @@ -82,6 +84,7 @@ class ScorePanel extends StatelessWidget {
onTap: () {}, // absorb taps
child: _ScorePanelContent(
game: game,
scoreSteps: settings.globalScoreSteps,
maxWidth: maxWidth,
),
),
Expand All @@ -91,17 +94,19 @@ class ScorePanel extends StatelessWidget {

class _ScorePanelContent extends StatelessWidget {
final GameProvider game;
final List<int> scoreSteps;
final double maxWidth;

const _ScorePanelContent({
required this.game,
required this.scoreSteps,
required this.maxWidth,
});

@override
Widget build(BuildContext context) {
final selected = game.selectedPlayer!;
final steps = game.scoreSteps;
final steps = scoreSteps;

return Container(
constraints: BoxConstraints(maxWidth: maxWidth),
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: utopia_scoreboard
description: "A cross-platform scoreboard for Mahjong, Card Game or Table Game."
publish_to: 'none'
version: 0.0.2
version: 0.0.3

environment:
sdk: '>=3.2.3 <4.0.0'
Expand Down
Loading