This commit is contained in:
252
lib/data/services/grading_service.dart
Normal file
252
lib/data/services/grading_service.dart
Normal file
@@ -0,0 +1,252 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' show Color;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
/// One belt level with its ITF grading requirements.
|
||||
class BeltLevel {
|
||||
const BeltLevel({
|
||||
required this.id,
|
||||
required this.gup,
|
||||
required this.name,
|
||||
required this.baseColor,
|
||||
this.stripeColor,
|
||||
required this.requirements,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String gup;
|
||||
final String name;
|
||||
final Color baseColor;
|
||||
final Color? stripeColor;
|
||||
final List<String> requirements;
|
||||
}
|
||||
|
||||
/// Tracks grading preparation: which requirements are checked off per belt,
|
||||
/// and which belt the user is currently preparing for. Persisted as JSON.
|
||||
class GradingService extends ChangeNotifier {
|
||||
static const _fileName = 'trainhub_grading.json';
|
||||
|
||||
String? _currentBeltId;
|
||||
Map<String, Set<int>> _checked = {};
|
||||
bool _loaded = false;
|
||||
|
||||
String? get currentBeltId => _currentBeltId;
|
||||
|
||||
bool isChecked(String beltId, int index) =>
|
||||
_checked[beltId]?.contains(index) ?? false;
|
||||
|
||||
int checkedCount(String beltId) => _checked[beltId]?.length ?? 0;
|
||||
|
||||
double progress(BeltLevel belt) => belt.requirements.isEmpty
|
||||
? 0
|
||||
: checkedCount(belt.id) / belt.requirements.length;
|
||||
|
||||
Future<File> _file() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
return File(p.join(dir.path, _fileName));
|
||||
}
|
||||
|
||||
Future<void> ensureLoaded() async {
|
||||
if (_loaded) return;
|
||||
_loaded = true;
|
||||
try {
|
||||
final file = await _file();
|
||||
if (!file.existsSync()) return;
|
||||
final json = jsonDecode(await file.readAsString());
|
||||
_currentBeltId = json['current'] as String?;
|
||||
_checked = {
|
||||
for (final entry in (json['checked'] as Map? ?? {}).entries)
|
||||
entry.key as String: {
|
||||
for (final i in entry.value as List) (i as num).toInt(),
|
||||
},
|
||||
};
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint('Failed to load grading state: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void toggle(String beltId, int index) {
|
||||
final set = _checked.putIfAbsent(beltId, () => {});
|
||||
set.contains(index) ? set.remove(index) : set.add(index);
|
||||
notifyListeners();
|
||||
_persist();
|
||||
}
|
||||
|
||||
void setCurrentBelt(String? beltId) {
|
||||
_currentBeltId = beltId;
|
||||
notifyListeners();
|
||||
_persist();
|
||||
}
|
||||
|
||||
Future<void> _persist() async {
|
||||
try {
|
||||
final file = await _file();
|
||||
await file.writeAsString(
|
||||
jsonEncode({
|
||||
'current': _currentBeltId,
|
||||
'checked': {
|
||||
for (final entry in _checked.entries)
|
||||
entry.key: entry.value.toList(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint('Failed to save grading state: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ITF colored-belt syllabus (10 gup → 1 gup)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const _white = Color(0xFFF4F4F5);
|
||||
const _yellow = Color(0xFFFACC15);
|
||||
const _green = Color(0xFF22C55E);
|
||||
const _blue = Color(0xFF3B82F6);
|
||||
const _red = Color(0xFFFF2B2B);
|
||||
const _black = Color(0xFF18181B);
|
||||
|
||||
const kBeltSyllabus = <BeltLevel>[
|
||||
BeltLevel(
|
||||
id: '10gup',
|
||||
gup: '10 GUP',
|
||||
name: 'White → Yellow Stripe',
|
||||
baseColor: _white,
|
||||
stripeColor: _yellow,
|
||||
requirements: [
|
||||
'Saju Jirugi (4-direction punch)',
|
||||
'Saju Makgi (4-direction block)',
|
||||
'Stances: charyot, narani, annun, gunnun sogi',
|
||||
'Ap Chagi (front snap kick) — both legs',
|
||||
'Theory: meaning of Taekwon-Do, belt colors, dojang etiquette',
|
||||
],
|
||||
),
|
||||
BeltLevel(
|
||||
id: '9gup',
|
||||
gup: '9 GUP',
|
||||
name: 'Yellow Stripe → Yellow',
|
||||
baseColor: _white,
|
||||
stripeColor: _yellow,
|
||||
requirements: [
|
||||
'Tul: Chon-Ji (19 movements)',
|
||||
'Niunja sogi (L-stance) + palmok daebi makgi',
|
||||
'Ap Chagi and Yop Chagi from walking stance',
|
||||
'3-step sparring (Sambo Matsogi) #1–2',
|
||||
'Theory: meaning of Chon-Ji, counting in Korean 1–10',
|
||||
],
|
||||
),
|
||||
BeltLevel(
|
||||
id: '8gup',
|
||||
gup: '8 GUP',
|
||||
name: 'Yellow',
|
||||
baseColor: _yellow,
|
||||
requirements: [
|
||||
'Tul: Dan-Gun (21 movements)',
|
||||
'Sonkal daebi makgi (knife-hand guarding block)',
|
||||
'Dollyo Chagi (turning kick) — both legs',
|
||||
'3-step sparring #1–4',
|
||||
'Theory: meaning of Dan-Gun, yellow belt symbolism',
|
||||
],
|
||||
),
|
||||
BeltLevel(
|
||||
id: '7gup',
|
||||
gup: '7 GUP',
|
||||
name: 'Yellow → Green Stripe',
|
||||
baseColor: _yellow,
|
||||
stripeColor: _green,
|
||||
requirements: [
|
||||
'Tul: Do-San (24 movements)',
|
||||
'Bakat palmok makgi + sonkal taerigi combinations',
|
||||
'Yopcha Jirugi (side piercing kick) — height improvement',
|
||||
'3-step sparring full set',
|
||||
'Theory: meaning of Do-San (Ahn Chang-Ho)',
|
||||
],
|
||||
),
|
||||
BeltLevel(
|
||||
id: '6gup',
|
||||
gup: '6 GUP',
|
||||
name: 'Green',
|
||||
baseColor: _green,
|
||||
requirements: [
|
||||
'Tul: Won-Hyo (28 movements)',
|
||||
'Gojung sogi (fixed stance) + punch',
|
||||
'Goro Chagi (hooking kick), Naeryo Chagi (downward kick)',
|
||||
'2-step sparring (Ibo Matsogi) #1–3',
|
||||
'Theory: meaning of Won-Hyo, green belt symbolism',
|
||||
],
|
||||
),
|
||||
BeltLevel(
|
||||
id: '5gup',
|
||||
gup: '5 GUP',
|
||||
name: 'Green → Blue Stripe',
|
||||
baseColor: _green,
|
||||
stripeColor: _blue,
|
||||
requirements: [
|
||||
'Tul: Yul-Gok (38 movements)',
|
||||
'Palkup taerigi (elbow strike), dwijibo jirugi',
|
||||
'Dwit Chagi (back piercing kick) — both legs',
|
||||
'2-step sparring full set + free sparring intro',
|
||||
'Theory: meaning of Yul-Gok (Yi I, "Confucius of Korea")',
|
||||
],
|
||||
),
|
||||
BeltLevel(
|
||||
id: '4gup',
|
||||
gup: '4 GUP',
|
||||
name: 'Blue',
|
||||
baseColor: _blue,
|
||||
requirements: [
|
||||
'Tul: Joong-Gun (32 movements)',
|
||||
'Kyocha sogi (X-stance), sang palmok makgi',
|
||||
'Bandae Dollyo Chagi (reverse turning kick)',
|
||||
'1-step sparring (Ilbo Matsogi) #1–3',
|
||||
'Theory: meaning of Joong-Gun (Ahn Joong-Gun)',
|
||||
],
|
||||
),
|
||||
BeltLevel(
|
||||
id: '3gup',
|
||||
gup: '3 GUP',
|
||||
name: 'Blue → Red Stripe',
|
||||
baseColor: _blue,
|
||||
stripeColor: _red,
|
||||
requirements: [
|
||||
'Tul: Toi-Gye (37 movements)',
|
||||
'W-shape block (san makgi) in annun sogi',
|
||||
'Twimyo Yop Chagi (flying side kick)',
|
||||
'1-step sparring full set + free sparring 2×2 min',
|
||||
'Theory: meaning of Toi-Gye (Yi Hwang)',
|
||||
],
|
||||
),
|
||||
BeltLevel(
|
||||
id: '2gup',
|
||||
gup: '2 GUP',
|
||||
name: 'Red',
|
||||
baseColor: _red,
|
||||
requirements: [
|
||||
'Tul: Hwa-Rang (29 movements)',
|
||||
'Combination kicking: dollyo + bandae dollyo chagi',
|
||||
'Twimyo Dollyo Chagi (flying turning kick)',
|
||||
'Free sparring 2×2 min + self-defence (hosinsul) basics',
|
||||
'Theory: meaning of Hwa-Rang, red belt symbolism',
|
||||
],
|
||||
),
|
||||
BeltLevel(
|
||||
id: '1gup',
|
||||
gup: '1 GUP',
|
||||
name: 'Red → Black Stripe',
|
||||
baseColor: _red,
|
||||
stripeColor: _black,
|
||||
requirements: [
|
||||
'Tul: Choong-Moo (30 movements)',
|
||||
'All previous patterns on demand',
|
||||
'Twimyo Bandae Dollyo Chagi (flying reverse turning kick)',
|
||||
'Free sparring vs two opponents + breaking (kyokpa)',
|
||||
'Theory: meaning of Choong-Moo (Admiral Yi Sun-Sin), full terminology',
|
||||
],
|
||||
),
|
||||
];
|
||||
Reference in New Issue
Block a user