diff --git a/lib/router.dart b/lib/router.dart index 04f4901..552a011 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'view/home.dart'; import 'view/screens/search/kanji_result_page.dart'; +import 'view/screens/search/search_mechanisms/drawing.dart'; +import 'view/screens/search/search_mechanisms/radical_list.dart'; import 'view/screens/search/search_results_page.dart'; Route generateRoute(RouteSettings settings) { @@ -26,6 +28,9 @@ Route generateRoute(RouteSettings settings) { case '/kanjiSearch/draw': return MaterialPageRoute(builder: (_) => const KanjiDrawingSearch()); + case '/kanjiSearch/radicals': + return MaterialPageRoute(builder: (_) => const KanjiRadicalSearch()); + default: return MaterialPageRoute( builder: (_) => const Text('ERROR: this route does not exist'), diff --git a/lib/services/jisho_api/radicals.dart b/lib/services/jisho_api/radicals.dart new file mode 100644 index 0000000..523d6df --- /dev/null +++ b/lib/services/jisho_api/radicals.dart @@ -0,0 +1,17 @@ +const Map> radicals = { + 1: ['一', '|', '丶', 'ノ', '乙', '亅'], + 2: ['二', '亠', '人', '⺅', '𠆢', '儿', '入', 'ハ', '丷', '冂', '冖', '冫', '几', '凵', '刀', '⺉', '力', '勹', '匕', '匚', '十', '卜', '卩', '厂', '厶', '又', 'マ', '九', 'ユ', '乃', '𠂉'], + 3: ['⻌', '口', '囗', '土', '士', '夂', '夕', '大', '女', '子', '宀', '寸', '小', '⺌', '尢', '尸', '屮', '山', '川', '巛', '工', '已', '巾', '干', '幺', '广', '廴', '廾', '弋', '弓', 'ヨ', '彑', '彡', '彳', '⺖', '⺘', '⺡', '⺨', '⺾', '⻏', '⻖', '也', '亡', '及', '久'], + 4: ['⺹', '心', '戈', '戸', '手', '支', '攵', '文', '斗', '斤', '方', '无', '日', '曰', '月', '木', '欠', '止', '歹', '殳', '比', '毛', '氏', '气', '水', '火', '⺣', '爪', '父', '爻', '爿', '片', '牛', '犬', '⺭', '王', '元', '井', '勿', '尤', '五', '屯', '巴', '毋'], + 5: ['玄', '瓦', '甘', '生', '用', '田', '疋', '疒', '癶', '白', '皮', '皿', '目', '矛', '矢', '石', '示', '禸', '禾', '穴', '立', '⻂', '世', '巨', '冊', '母', '⺲', '牙'], + 6: ['瓜', '竹', '米', '糸', '缶', '羊', '羽', '而', '耒', '耳', '聿', '肉', '自', '至', '臼', '舌', '舟', '艮', '色', '虍', '虫', '血', '行', '衣', '西'], + 7: ['臣', '見', '角', '言', '谷', '豆', '豕', '豸', '貝', '赤', '走', '足', '身', '車', '辛', '辰', '酉', '釆', '里', '舛', '麦'], + 8: ['金', '長', '門', '隶', '隹', '雨', '青', '非', '奄', '岡', '免', '斉'], + 9: ['面', '革', '韭', '音', '頁', '風', '飛', '食', '首', '香', '品'], + 10: ['馬', '骨', '高', '髟', '鬥', '鬯', '鬲', '鬼', '竜', '韋'], + 11: ['魚', '鳥', '鹵', '鹿', '麻', '亀', '啇', '黄', '黒'], + 12: ['黍', '黹', '無', '歯'], + 13: ['黽', '鼎', '鼓', '鼠'], + 14: ['鼻', '齊'], + 17: ['龠'], +}; diff --git a/lib/services/jisho_api/radicals_search.dart b/lib/services/jisho_api/radicals_search.dart new file mode 100644 index 0000000..e03dc1e --- /dev/null +++ b/lib/services/jisho_api/radicals_search.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +class RadicalsSearchKanji { + final String kanji; + final int strokes; + final int grade; + final int gradeSort; + + RadicalsSearchKanji({ + required this.kanji, + required this.strokes, + required this.grade, + required this.gradeSort, + }); + + factory RadicalsSearchKanji.fromJson(Map json) => + RadicalsSearchKanji( + kanji: json['kanji'], + strokes: json['strokes'], + grade: json['grade'], + gradeSort: json['grade_sort'], + ); +} + +class RadicalsSearchResult { + final List kanji; + final List validRadicals; + + RadicalsSearchResult({ + required this.kanji, + required this.validRadicals, + }); + + factory RadicalsSearchResult.fromJson(Map json) => + RadicalsSearchResult( + kanji: (json['kanji'] as List) + .map((k) => RadicalsSearchKanji.fromJson(k)) + .toList(), + validRadicals: + (json['is_valid_radical'] as Map).keys.toList(), + ); +} + +Future searchKanjiByRadicals( + List radicals, +) async { + final response = await http + .get(Uri.parse('https://jisho.org/radicals/${radicals.join(',')}')); + return RadicalsSearchResult.fromJson(jsonDecode(response.body)); +} diff --git a/lib/view/components/kanji/kanji_search_body/kanji_search_options_bar.dart b/lib/view/components/kanji/kanji_search_body/kanji_search_options_bar.dart index 6014d35..8f3cdb6 100644 --- a/lib/view/components/kanji/kanji_search_body/kanji_search_options_bar.dart +++ b/lib/view/components/kanji/kanji_search_body/kanji_search_options_bar.dart @@ -19,7 +19,7 @@ class KanjiSearchOptionsBar extends StatelessWidget { fontSize: 18, ), ), - onPressed: () {}, + onPressed: () => Navigator.pushNamed(context, '/kanjiSearch/radicals'), ), _IconButton( icon: const Icon(Icons.category), diff --git a/lib/view/screens/search/search_mechanisms/radical_list.dart b/lib/view/screens/search/search_mechanisms/radical_list.dart index a6ed8a2..651b34a 100644 --- a/lib/view/screens/search/search_mechanisms/radical_list.dart +++ b/lib/view/screens/search/search_mechanisms/radical_list.dart @@ -1 +1,200 @@ -const List> radicals = [[]]; +import 'package:flutter/material.dart'; + +import '../../../../bloc/theme/theme_bloc.dart'; +import '../../../../services/jisho_api/radicals.dart'; +import '../../../../services/jisho_api/radicals_search.dart'; + +class KanjiRadicalSearch extends StatefulWidget { + const KanjiRadicalSearch({Key? key}) : super(key: key); + + @override + _KanjiRadicalSearchState createState() => _KanjiRadicalSearchState(); +} + +class _KanjiRadicalSearchState extends State { + static const double fontSize = 25; + + List suggestions = []; + + Map radicalToggles = { + for (final String r in radicals.values.expand((l) => l)) r: false + }; + + Map allowedToggles = { + for (final String r in radicals.values.expand((l) => l)) r: true + }; + + void resetRadicalToggles() => radicalToggles.forEach((k, _) { + radicalToggles[k] = false; + }); + + void resetAllowedToggles() => allowedToggles.forEach((k, _) { + allowedToggles[k] = true; + }); + + Future updateSuggestions() async { + final toggledRadicals = + radicalToggles.keys.where((r) => radicalToggles[r] ?? false).toList(); + + if (toggledRadicals.isEmpty) { + suggestions.clear(); + resetAllowedToggles(); + return; + } + + final newSuggestions = await searchKanjiByRadicals(toggledRadicals); + + setState(() { + allowedToggles.forEach((key, value) { + allowedToggles[key] = false; + }); + for (final r in newSuggestions.validRadicals) { + allowedToggles[r] = true; + } + suggestions = (newSuggestions.kanji + ..sort((a, b) => a.strokes.compareTo(b.strokes))) + .map((k) => k.kanji) + .toList(); + }); + } + + Widget radicalGridElement(String radical, {bool isNumber = false}) { + // final theme = BlocProvider.of(context).state.theme; + final theme = LightTheme(); + + final color = isNumber + ? theme.menuGreyDark + : radicalToggles[radical]! + ? AppTheme.jishoGreen + : theme.menuGreyNormal; + + return InkWell( + onTap: isNumber + ? () {} + : () => setState(() { + // TODO: Don't let the user toggle on another kanji before the last one is updated + radicalToggles[radical] = !radicalToggles[radical]!; + updateSuggestions(); + }), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(5)), + color: color.background, + ), + child: Text( + radical, + style: TextStyle( + color: color.foreground, + fontSize: fontSize, + ), + ), + ), + ); + } + + List get radicalGridElements => + [ + IconButton( + onPressed: () => setState(() { + suggestions.clear(); + resetRadicalToggles(); + resetAllowedToggles(); + }), + icon: const Icon(Icons.restore), + color: AppTheme.jishoGreen.background, + iconSize: fontSize * 1.3, + ), + ] + + radicals + .map( + (key, value) => MapEntry( + key, + value + .where((r) => allowedToggles[r]!) + .map((r) => radicalGridElement(r)) + .toList() + ..insert(0, radicalGridElement(key.toString(), isNumber: true)), + ), + ) + .values + .where((element) => element.length != 1) + .expand((l) => l) + .toList(); + + Widget kanjiGridElement(String kanji) { + final color = LightTheme().menuGreyNormal; + return InkWell( + onTap: () => Navigator.popAndPushNamed( + context, + '/kanjiSearch', + arguments: kanji, + ), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(5)), + color: color.background, + ), + alignment: Alignment.center, + child: Text( + kanji, + style: TextStyle( + color: color.foreground, + fontSize: fontSize, + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Choose by radicals')), + body: Column( + children: [ + Expanded( + child: (suggestions.isEmpty) + ? Center( + child: Text( + 'Toggle a radical to start', + style: TextStyle( + fontSize: fontSize * 0.8, + color: BlocProvider.of(context) + .state + .theme + .menuGreyNormal + .background, + ), + ), + ) + : GridView.count( + crossAxisCount: 6, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + padding: const EdgeInsets.all(10), + children: + suggestions.map((s) => kanjiGridElement(s)).toList(), + ), + ), + Divider( + color: AppTheme.jishoGreen.background, + thickness: 3, + height: 30, + indent: 5, + endIndent: 5, + ), + Expanded( + child: GridView.count( + crossAxisCount: 6, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + padding: const EdgeInsets.all(10), + children: radicalGridElements, + ), + ), + ], + ), + ); + } +}