Skip to content

Commit ecdabc2

Browse files
committed
feat: AtCoderレーティングに基づくテーマ色の適用機能を追加
1 parent 937ce4d commit ecdabc2

File tree

4 files changed

+132
-2
lines changed

4 files changed

+132
-2
lines changed

lib/main.dart

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,10 @@ class MyApp extends StatelessWidget {
103103

104104
return DynamicColorBuilder(
105105
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
106-
// Material Youを使用するかどうかでカラースキームを決定
106+
// Material You / Custom theme 基本スキーム決定
107107
ColorScheme lightColorScheme;
108-
ColorScheme darkColorScheme; if (themeProvider.useMaterialYou) {
108+
ColorScheme darkColorScheme;
109+
if (themeProvider.useMaterialYou) {
109110
lightColorScheme = lightDynamic ?? _defaultLightColorScheme;
110111
darkColorScheme = themeProvider.isPureBlack
111112
? _pureBlackColorScheme
@@ -117,6 +118,38 @@ class MyApp extends StatelessWidget {
117118
: _darkCustomTheme;
118119
}
119120

121+
// AtCoderレーティング色をそのままテーマの主色に適用(ハーモナイズなしで忠実に)
122+
if (themeProvider.useAtcoderRatingColor &&
123+
themeProvider.atcoderAccentColor != null) {
124+
final seed = themeProvider.atcoderAccentColor!;
125+
final onPrimary = seed.computeLuminance() > 0.5 ? Colors.black : Colors.white;
126+
127+
lightColorScheme = lightColorScheme.copyWith(
128+
primary: seed,
129+
onPrimary: onPrimary,
130+
primaryContainer: seed,
131+
onPrimaryContainer: onPrimary,
132+
surfaceTint: Colors.transparent,
133+
);
134+
135+
final baseDark = darkColorScheme;
136+
final darkAdjusted = baseDark.copyWith(
137+
primary: seed,
138+
onPrimary: onPrimary,
139+
primaryContainer: seed,
140+
onPrimaryContainer: onPrimary,
141+
surfaceTint: Colors.transparent,
142+
);
143+
darkColorScheme = themeProvider.isPureBlack
144+
? darkAdjusted.copyWith(
145+
surface: Colors.black,
146+
surfaceContainerHighest: Colors.black,
147+
onSurface: Colors.white,
148+
surfaceTint: Colors.transparent,
149+
)
150+
: darkAdjusted;
151+
}
152+
120153
// Noto Sans JPフォントをテキストテーマに適用
121154
final textTheme = TextTheme(
122155
displayLarge: GoogleFonts.notoSansJp(fontSize: 32, fontWeight: FontWeight.bold),

lib/providers/theme_provider.dart

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import 'package:flutter/material.dart';
22
import 'package:shared_preferences/shared_preferences.dart';
3+
import '../services/atcoder_service.dart';
4+
import '../utils/atcoder_colors.dart';
35

46
const String _navOpacityKey = 'nav_opacity'; // Key for bottom nav opacity
57
const String _useMaterialYouKey = 'use_material_you'; // Key for Material You setting
68
const String _codeFontFamilyKey = 'code_font_family'; // Key for code font family
79
const String _customCodeFontsKey = 'custom_code_fonts'; // Key for custom code fonts list
10+
const String _useAtcoderRatingColorKey = 'use_atcoder_rating_color'; // Key for AtCoder accent option
811

912
// Built-in supported Google fonts and generic fallback
1013
const List<String> defaultCodeFontFamilies = [
@@ -32,6 +35,8 @@ class ThemeProvider extends ChangeNotifier {
3235
String _codeFontFamily = defaultCodeFontFamilies.first; // Default font
3336
final List<String> _customCodeFonts = []; // User-added font family names (must be declared in pubspec)
3437
bool _isLoading = true;
38+
bool _useAtcoderRatingColor = false; // Default off
39+
Color? _atcoderAccentColor; // Resolved accent color based on rating
3540

3641
ThemeProvider() {
3742
_loadFromPrefs();
@@ -46,6 +51,12 @@ class ThemeProvider extends ChangeNotifier {
4651
// Material Youを使用するかどうか
4752
bool get useMaterialYou => _useMaterialYou;
4853

54+
// AtCoderレート色をアクセントとして使うか
55+
bool get useAtcoderRatingColor => _useAtcoderRatingColor;
56+
57+
// 解決済みのアクセントカラー(nullの場合は未設定/無効)
58+
Color? get atcoderAccentColor => _atcoderAccentColor;
59+
4960
// Code block font family
5061
String get codeFontFamily => _codeFontFamily;
5162

@@ -74,6 +85,20 @@ class ThemeProvider extends ChangeNotifier {
7485
notifyListeners();
7586
}
7687

88+
// AtCoderレート色の使用設定を変更
89+
Future<void> setUseAtcoderRatingColor(bool use) async {
90+
if (_useAtcoderRatingColor == use) return;
91+
92+
_useAtcoderRatingColor = use;
93+
await _saveToPrefs();
94+
if (use) {
95+
await refreshAtcoderAccentColor();
96+
} else {
97+
_atcoderAccentColor = null;
98+
notifyListeners();
99+
}
100+
}
101+
77102
// Material Youの使用設定を変更
78103
Future<void> setUseMaterialYou(bool use) async {
79104
if (_useMaterialYou == use) return;
@@ -131,6 +156,11 @@ class ThemeProvider extends ChangeNotifier {
131156
if (savedUseMaterialYou != null) {
132157
_useMaterialYou = savedUseMaterialYou;
133158
}
159+
// Load AtCoder accent option
160+
final savedUseAtcoder = prefs.getBool(_useAtcoderRatingColorKey);
161+
if (savedUseAtcoder != null) {
162+
_useAtcoderRatingColor = savedUseAtcoder;
163+
}
134164
// Load code font family if exists
135165
final savedFontFamily = prefs.getString(_codeFontFamilyKey);
136166
// Load custom fonts list
@@ -150,6 +180,11 @@ class ThemeProvider extends ChangeNotifier {
150180
_themeMode = ThemeModeOption.values[themeModeIndex];
151181
}
152182

183+
// If using AtCoder accent, resolve it now
184+
if (_useAtcoderRatingColor) {
185+
await refreshAtcoderAccentColor();
186+
}
187+
153188
_isLoading = false;
154189
notifyListeners();
155190
}
@@ -161,6 +196,7 @@ class ThemeProvider extends ChangeNotifier {
161196
await prefs.setBool(_useMaterialYouKey, _useMaterialYou); // Save Material You setting
162197
await prefs.setString(_codeFontFamilyKey, _codeFontFamily); // Save font family
163198
await prefs.setStringList(_customCodeFontsKey, _customCodeFonts); // Save custom fonts
199+
await prefs.setBool(_useAtcoderRatingColorKey, _useAtcoderRatingColor); // Save AtCoder accent option
164200
}
165201

166202
// Set bottom nav opacity and persist
@@ -170,6 +206,32 @@ class ThemeProvider extends ChangeNotifier {
170206
await prefs.setDouble(_navOpacityKey, opacity);
171207
notifyListeners();
172208
}
209+
210+
/// Fetch current user's AtCoder rating and resolve accent color.
211+
/// Uses 'atcoder_username' from SharedPreferences. If missing or fetch fails,
212+
/// falls back to null and leaves theme unchanged.
213+
Future<void> refreshAtcoderAccentColor() async {
214+
if (!_useAtcoderRatingColor) return;
215+
try {
216+
final prefs = await SharedPreferences.getInstance();
217+
final username = prefs.getString('atcoder_username');
218+
if (username == null || username.trim().isEmpty) {
219+
_atcoderAccentColor = null;
220+
notifyListeners();
221+
return;
222+
}
223+
final service = AtCoderService();
224+
final rating = await service.fetchAtCoderRate(username.trim());
225+
if (rating == null) {
226+
_atcoderAccentColor = null;
227+
} else {
228+
_atcoderAccentColor = atcoderRatingToColor(rating);
229+
}
230+
} catch (_) {
231+
_atcoderAccentColor = null;
232+
}
233+
notifyListeners();
234+
}
173235
}
174236

175237
// テーマモードの選択肢

lib/screens/settings_screen.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
8383
'atcoder_username',
8484
_atcoderUsernameController.text.trim(),
8585
);
86+
// If theme uses AtCoder accent, refresh it
87+
try {
88+
final themeProvider = Provider.of<ThemeProvider>(context, listen: false);
89+
if (themeProvider.useAtcoderRatingColor) {
90+
await themeProvider.refreshAtcoderAccentColor();
91+
}
92+
} catch (_) {
93+
// ignore UI refresh errors
94+
}
8695
if (!mounted) return;
8796
ScaffoldMessenger.of(context).showSnackBar(
8897
const SnackBar(content: Text('AtCoderユーザー名を保存しました')),
@@ -332,6 +341,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
332341
onChanged: themeProvider.setUseMaterialYou,
333342
icon: Icons.color_lens_outlined,
334343
),
344+
_HapticSwitchListTile(
345+
title: 'AtCoderの色をテーマに使う',
346+
subtitle: '保存したユーザーのレート色をアクセントに適用します',
347+
value: themeProvider.useAtcoderRatingColor,
348+
onChanged: (v) async {
349+
await themeProvider.setUseAtcoderRatingColor(v);
350+
if (v) {
351+
await themeProvider.refreshAtcoderAccentColor();
352+
}
353+
},
354+
icon: Icons.emoji_events_outlined,
355+
),
335356
const Divider(),
336357
// Font Family Selector
337358
Padding(

lib/utils/atcoder_colors.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'package:flutter/material.dart';
2+
3+
/// Map AtCoder rating to its representative color.
4+
/// Based on typical AtCoder color bands.
5+
Color atcoderRatingToColor(int rating) {
6+
if (rating >= 2800) return const Color(0xFFFF0000); // Red
7+
if (rating >= 2400) return const Color(0xFFFF8000); // Orange
8+
if (rating >= 2000) return const Color(0xFFC0C000); // Yellow
9+
if (rating >= 1600) return const Color(0xFF0000FF); // Blue
10+
if (rating >= 1200) return const Color(0xFF00C0C0); // Cyan
11+
if (rating >= 800) return const Color(0xFF008000); // Green
12+
if (rating >= 400) return const Color(0xFF804000); // Brown
13+
return const Color(0xFF808080); // Gray
14+
}

0 commit comments

Comments
 (0)