Add radical kanji search feature

This commit is contained in:
Oystein Kristoffer Tveit 2022-01-14 17:43:39 +01:00
parent 37ee031693
commit 32740dd7dd
5 changed files with 275 additions and 2 deletions

View File

@ -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<Widget> generateRoute(RouteSettings settings) {
@ -26,6 +28,9 @@ Route<Widget> 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'),

View File

@ -0,0 +1,17 @@
const Map<int, List<String>> radicals = {
1: ['', '', '', '', '', ''],
2: ['', '', '', '', '𠆢', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '𠂉'],
3: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '广', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
4: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
5: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
6: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '西'],
7: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
8: ['', '', '', '', '', '', '', '', '', '', '', ''],
9: ['', '', '', '', '', '', '', '', '', '', ''],
10: ['', '', '', '', '', '', '', '', '', ''],
11: ['', '', '', '鹿', '', '', '', '', ''],
12: ['', '', '', ''],
13: ['', '', '', ''],
14: ['', ''],
17: [''],
};

View File

@ -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<String, dynamic> json) =>
RadicalsSearchKanji(
kanji: json['kanji'],
strokes: json['strokes'],
grade: json['grade'],
gradeSort: json['grade_sort'],
);
}
class RadicalsSearchResult {
final List<RadicalsSearchKanji> kanji;
final List<String> validRadicals;
RadicalsSearchResult({
required this.kanji,
required this.validRadicals,
});
factory RadicalsSearchResult.fromJson(Map<String, dynamic> json) =>
RadicalsSearchResult(
kanji: (json['kanji'] as List<dynamic>)
.map((k) => RadicalsSearchKanji.fromJson(k))
.toList(),
validRadicals:
(json['is_valid_radical'] as Map<String, dynamic>).keys.toList(),
);
}
Future<RadicalsSearchResult> searchKanjiByRadicals(
List<String> radicals,
) async {
final response = await http
.get(Uri.parse('https://jisho.org/radicals/${radicals.join(',')}'));
return RadicalsSearchResult.fromJson(jsonDecode(response.body));
}

View File

@ -19,7 +19,7 @@ class KanjiSearchOptionsBar extends StatelessWidget {
fontSize: 18,
),
),
onPressed: () {},
onPressed: () => Navigator.pushNamed(context, '/kanjiSearch/radicals'),
),
_IconButton(
icon: const Icon(Icons.category),

View File

@ -1 +1,200 @@
const List<List<String>> 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<KanjiRadicalSearch> {
static const double fontSize = 25;
List<String> suggestions = [];
Map<String, bool> radicalToggles = {
for (final String r in radicals.values.expand((l) => l)) r: false
};
Map<String, bool> 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<void> 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<ThemeBloc>(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<Widget> get radicalGridElements =>
<Widget>[
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<ThemeBloc>(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,
),
),
],
),
);
}
}