Poprawki
Some checks failed
Build Linux App / build (push) Failing after 1m12s

This commit is contained in:
Kazimierz Ciołek
2026-07-07 22:03:36 +02:00
parent 6dd7213eb0
commit ebb7426c67
14 changed files with 886 additions and 82 deletions

View File

@@ -7,7 +7,7 @@ import 'package:path_provider/path_provider.dart';
/// Which backend answers chat requests. Embeddings (knowledge base) always
/// stay on the local Nomic server regardless of this choice.
enum AiProvider { local, ollama, lmstudio, anthropic, openai, gemini }
enum AiProvider { local, ollama, lmstudio, anthropic, openai, gemini, openrouter }
extension AiProviderX on AiProvider {
String get label => switch (this) {
@@ -17,6 +17,7 @@ extension AiProviderX on AiProvider {
AiProvider.anthropic => 'Anthropic (Claude)',
AiProvider.openai => 'OpenAI (GPT)',
AiProvider.gemini => 'Google (Gemini)',
AiProvider.openrouter => 'OpenRouter',
};
String get defaultModel => switch (this) {
@@ -26,6 +27,9 @@ extension AiProviderX on AiProvider {
AiProvider.anthropic => 'claude-opus-4-8',
AiProvider.openai => 'gpt-5.1',
AiProvider.gemini => 'gemini-2.5-flash',
// OpenRouter routes to any model — IDs like "anthropic/claude-opus-4.8",
// "openai/gpt-4o", "meta-llama/llama-3.3-70b-instruct".
AiProvider.openrouter => 'openai/gpt-4o',
};
/// Self-hosted OpenAI-compatible servers reachable over a base URL.
@@ -39,7 +43,10 @@ extension AiProviderX on AiProvider {
};
bool get needsApiKey => switch (this) {
AiProvider.anthropic || AiProvider.openai || AiProvider.gemini => true,
AiProvider.anthropic ||
AiProvider.openai ||
AiProvider.gemini ||
AiProvider.openrouter => true,
_ => false,
};
}

View 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) #12',
'Theory: meaning of Chon-Ji, counting in Korean 110',
],
),
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 #14',
'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) #13',
'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) #13',
'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',
],
),
];

View File

@@ -72,6 +72,8 @@ class LlmClient {
AiProvider.openai => 'https://api.openai.com/v1/chat/completions',
AiProvider.gemini =>
'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions',
AiProvider.openrouter =>
'https://openrouter.ai/api/v1/chat/completions',
AiProvider.anthropic => throw StateError('handled separately'),
};