@@ -3,6 +3,8 @@ import 'package:shared_preferences/shared_preferences.dart';
3
3
import '../models/problem_difficulty.dart' ;
4
4
import '../services/atcoder_service.dart' ;
5
5
import 'problem_detail_screen.dart' ;
6
+ import '../utils/atcoder_colors.dart' ;
7
+ import '../utils/rating_utils.dart' ;
6
8
7
9
class RecommendScreen extends StatefulWidget {
8
10
const RecommendScreen ({super .key});
@@ -20,7 +22,7 @@ class _RecommendScreenState extends State<RecommendScreen> {
20
22
bool _isLoading = false ;
21
23
String ? _errorMessage;
22
24
String ? _savedUsername; // 設定済みユーザー名
23
- int ? _currentRating; // 取得したレート
25
+ int ? _currentRating; // 取得したレート(表示用: 最新レート)
24
26
25
27
// AtCoder カラー判定
26
28
Color _ratingColor (int rating) {
@@ -62,6 +64,43 @@ class _RecommendScreenState extends State<RecommendScreen> {
62
64
);
63
65
}
64
66
67
+ // Difficulty 表示用バッジ(補正後diffで表示。色も補正後ベース)
68
+ Widget _difficultyBadge (int ? difficulty) {
69
+ int ? mappedInt;
70
+ if (difficulty != null ) {
71
+ final mapped = difficulty <= 400
72
+ ? RatingUtils .mapRating (difficulty)
73
+ : difficulty.toDouble ();
74
+ mappedInt = mapped.round ();
75
+ }
76
+ final color = (mappedInt != null )
77
+ ? atcoderRatingToColor (mappedInt)
78
+ : const Color (0xFF808080 );
79
+ return Container (
80
+ padding: const EdgeInsets .symmetric (horizontal: 10 , vertical: 4 ),
81
+ decoration: BoxDecoration (
82
+ color: color.withOpacity (0.12 ),
83
+ border: Border .all (color: color, width: 1 ),
84
+ borderRadius: BorderRadius .circular (999 ),
85
+ ),
86
+ child: Row (
87
+ mainAxisSize: MainAxisSize .min,
88
+ children: [
89
+ Icon (Icons .bolt, size: 14 , color: color),
90
+ const SizedBox (width: 6 ),
91
+ Text (
92
+ mappedInt? .toString () ?? 'N/A' ,
93
+ style: TextStyle (
94
+ color: color,
95
+ fontSize: 14 ,
96
+ fontWeight: FontWeight .w700,
97
+ ),
98
+ ),
99
+ ],
100
+ ),
101
+ );
102
+ }
103
+
65
104
@override
66
105
void initState () {
67
106
super .initState ();
@@ -131,31 +170,44 @@ class _RecommendScreenState extends State<RecommendScreen> {
131
170
throw Exception ('ユーザー名を入力してください' );
132
171
}
133
172
134
- final rating = await _atcoderService.fetchAtCoderRate (username);
135
- if (rating == null ) {
173
+ final ratingInfo = await _atcoderService.fetchAtcoderRatingInfo (username);
174
+ if (ratingInfo == null ) {
136
175
throw Exception ('ユーザーが見つからないか、レーティングがありません' );
137
176
}
138
177
139
178
// レートを保存してUIに表示
140
179
if (mounted) {
141
180
setState (() {
142
- _currentRating = rating ;
181
+ _currentRating = ratingInfo.latestRating ;
143
182
});
144
183
}
145
184
146
185
final allProblems = await _atcoderService.fetchProblemDifficulties ();
186
+ // TrueRating を計算(数式(10))
187
+ final trueRating = RatingUtils .trueRating (
188
+ rating: ratingInfo.latestRating,
189
+ contests: ratingInfo.contestCount,
190
+ );
147
191
148
192
final recommended = allProblems.entries.where ((entry) {
149
193
final difficulty = entry.value.difficulty;
150
- return difficulty != null &&
151
- difficulty >= rating + lowerDelta &&
152
- difficulty <= rating + upperDelta;
194
+ if (difficulty == null ) return false ;
195
+ // 400 以下の diff は mapRating で補正(比較用のみ)
196
+ final mappedDiff = difficulty <= 400
197
+ ? RatingUtils .mapRating (difficulty)
198
+ : difficulty.toDouble ();
199
+ return mappedDiff >= trueRating + lowerDelta &&
200
+ mappedDiff <= trueRating + upperDelta;
153
201
}).toList ();
154
202
155
203
// 自分のレートに近い順に並べ替え(差の絶対値の昇順)
156
204
recommended.sort ((a, b) {
157
- final da = (a.value.difficulty! - rating).abs ();
158
- final db = (b.value.difficulty! - rating).abs ();
205
+ final ad = a.value.difficulty! ;
206
+ final bd = b.value.difficulty! ;
207
+ final mad = ad <= 400 ? RatingUtils .mapRating (ad) : ad.toDouble ();
208
+ final mbd = bd <= 400 ? RatingUtils .mapRating (bd) : bd.toDouble ();
209
+ final da = (mad - trueRating).abs ();
210
+ final db = (mbd - trueRating).abs ();
159
211
final cmp = da.compareTo (db);
160
212
if (cmp != 0 ) return cmp;
161
213
// 差が同じ場合は難易度の昇順で安定化
@@ -275,13 +327,18 @@ class _RecommendScreenState extends State<RecommendScreen> {
275
327
itemCount: _recommendedProblems.length,
276
328
itemBuilder: (context, index) {
277
329
final problem = _recommendedProblems[index];
330
+ final diff = problem.value.difficulty;
278
331
return ListTile (
279
332
title: Text (problem.key),
280
- subtitle: Text (
281
- 'Difficulty: ${problem .value .difficulty ?? "なし" }' ),
282
- trailing: const Icon (Icons .open_in_new),
333
+ trailing: Row (
334
+ mainAxisSize: MainAxisSize .min,
335
+ children: [
336
+ _difficultyBadge (diff),
337
+ const SizedBox (width: 8 ),
338
+ const Icon (Icons .open_in_new),
339
+ ],
340
+ ),
283
341
onTap: () {
284
- // 問題詳細へ遷移(WebView連携と同様にIDからページを開く)
285
342
Navigator .push (
286
343
context,
287
344
MaterialPageRoute (
0 commit comments