229 lines
6.6 KiB
Dart
229 lines
6.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:signature/signature.dart';
|
|
|
|
import '../../bloc/theme/theme_bloc.dart';
|
|
import '../../services/handwriting.dart';
|
|
import '../../settings.dart';
|
|
|
|
class DrawingBoard extends StatefulWidget {
|
|
final Function(String)? onSuggestionChosen;
|
|
final bool onlyOneCharacterSuggestions;
|
|
final bool allowKanji;
|
|
final bool allowHiragana;
|
|
final bool allowKatakana;
|
|
final bool allowOther;
|
|
|
|
const DrawingBoard({
|
|
this.onSuggestionChosen,
|
|
this.onlyOneCharacterSuggestions = false,
|
|
this.allowKanji = true,
|
|
this.allowHiragana = false,
|
|
this.allowKatakana = false,
|
|
this.allowOther = false,
|
|
Key? key,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
_DrawingBoardState createState() => _DrawingBoardState();
|
|
}
|
|
|
|
class _DrawingBoardState extends State<DrawingBoard> {
|
|
List<String> suggestions = [];
|
|
|
|
final List<List<TimedPoint>> strokes = [];
|
|
final List<List<TimedPoint>> undoQueue = [];
|
|
|
|
GlobalKey signatureW = GlobalKey();
|
|
GlobalKey suggestionBarW = GlobalKey();
|
|
|
|
static const double fontSize = 30;
|
|
static const double suggestionCirclePadding = 13;
|
|
|
|
late ColorSet panelColor =
|
|
BlocProvider.of<ThemeBloc>(context).state.theme.menuGreyLight;
|
|
late ColorSet barColor =
|
|
BlocProvider.of<ThemeBloc>(context).state.theme.menuGreyNormal;
|
|
|
|
late final SignatureController controller = SignatureController(
|
|
penColor: panelColor.foreground,
|
|
onDrawStart: () {
|
|
strokes.add([]);
|
|
undoQueue.clear();
|
|
},
|
|
onDrawMove: () => strokes.last
|
|
.add(TimedPoint(time: DateTime.now(), point: controller.points.last)),
|
|
onDrawEnd: () => updateSuggestions(),
|
|
);
|
|
|
|
Future<void> updateSuggestions() async {
|
|
if (strokes.isEmpty) return setState(() => suggestions.clear());
|
|
|
|
final newSuggestions = await HandwritingRequest(
|
|
writingAreaHeight: signatureW.currentContext!.size!.width.toInt(),
|
|
writingAreaWidth: signatureW.currentContext!.size!.width.toInt(),
|
|
ink: strokes,
|
|
).fetch();
|
|
setState(() {
|
|
suggestions = newSuggestions;
|
|
});
|
|
}
|
|
|
|
List<String> get filteredSuggestions {
|
|
const kanjiR = r'\p{Script=Hani}';
|
|
const hiraganaR = r'\p{Script=Hiragana}';
|
|
const katakanaR = r'\p{Script=Katakana}';
|
|
const otherR = '[^$kanjiR$hiraganaR$katakanaR]';
|
|
|
|
final x = widget.allowKanji ? kanjiR : '';
|
|
final y = widget.allowHiragana ? hiraganaR : '';
|
|
final z = widget.allowKatakana ? katakanaR : '';
|
|
|
|
late final RegExp combinedRegex;
|
|
if ((widget.allowKanji || widget.allowHiragana || widget.allowKatakana) &&
|
|
widget.allowOther) {
|
|
combinedRegex = RegExp('^(?:[$x$y$z]|$otherR)+\$', unicode: true);
|
|
} else if (widget.allowOther) {
|
|
combinedRegex = RegExp('^$otherR+\$', unicode: true);
|
|
} else {
|
|
combinedRegex = RegExp('^[$x$y$z]+\$', unicode: true);
|
|
}
|
|
|
|
return suggestions
|
|
.where((s) => combinedRegex.hasMatch(s))
|
|
.where((s) => !widget.onlyOneCharacterSuggestions || s.length == 1)
|
|
.toList();
|
|
}
|
|
|
|
Widget kanjiChip(String kanji) => InkWell(
|
|
onTap: () => widget.onSuggestionChosen?.call(kanji),
|
|
child: BlocBuilder<ThemeBloc, ThemeState>(
|
|
builder: (context, state) {
|
|
final colors = state.theme.menuGreyLight;
|
|
|
|
return Container(
|
|
height: fontSize + 2 * suggestionCirclePadding,
|
|
width: fontSize + 2 * suggestionCirclePadding,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: colors.background,
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
kanji,
|
|
style: TextStyle(
|
|
fontSize: fontSize,
|
|
color: colors.foreground,
|
|
).merge(japaneseFont.textStyle),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
Widget suggestionBar() {
|
|
const padding = EdgeInsets.symmetric(horizontal: 10, vertical: 5);
|
|
|
|
return Container(
|
|
key: suggestionBarW,
|
|
color: barColor.background,
|
|
alignment: Alignment.center,
|
|
padding: padding,
|
|
|
|
// TODO: calculate dynamically
|
|
constraints: BoxConstraints(
|
|
minHeight: 8 +
|
|
suggestionCirclePadding * 2 +
|
|
fontSize +
|
|
(2 * 4) +
|
|
padding.vertical,
|
|
),
|
|
|
|
child: Wrap(
|
|
spacing: 20,
|
|
runSpacing: 5,
|
|
children: filteredSuggestions.map((s) => kanjiChip(s)).toList(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buttonRow() => Container(
|
|
color: panelColor.background,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
IconButton(
|
|
onPressed: () => setState(() {
|
|
controller.clear();
|
|
strokes.clear();
|
|
suggestions.clear();
|
|
}),
|
|
icon: const Icon(Icons.delete),
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
if (strokes.isNotEmpty) {
|
|
undoQueue.add(strokes.removeLast());
|
|
controller.undo();
|
|
updateSuggestions();
|
|
}
|
|
},
|
|
icon: const Icon(Icons.undo),
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
if (undoQueue.isNotEmpty) {
|
|
strokes.add(undoQueue.removeLast());
|
|
controller.redo();
|
|
updateSuggestions();
|
|
}
|
|
},
|
|
icon: const Icon(Icons.redo),
|
|
),
|
|
if (!widget.onlyOneCharacterSuggestions)
|
|
IconButton(
|
|
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('TODO: implement scrolling page feature!'),
|
|
),
|
|
),
|
|
icon: const Icon(Icons.arrow_forward),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
Widget drawingPanel() => AspectRatio(
|
|
aspectRatio: 1.2,
|
|
child: Stack(
|
|
alignment: Alignment.bottomRight,
|
|
children: [
|
|
ClipRect(
|
|
child: Signature(
|
|
key: signatureW,
|
|
controller: controller,
|
|
backgroundColor: panelColor.background,
|
|
),
|
|
),
|
|
buttonRow(),
|
|
],
|
|
),
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocListener<ThemeBloc, ThemeState>(
|
|
listener: (context, state) => setState(() {
|
|
panelColor = state.theme.menuGreyLight;
|
|
barColor = state.theme.menuGreyDark;
|
|
}),
|
|
child: Column(
|
|
children: [
|
|
suggestionBar(),
|
|
drawingPanel(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|