treewide: dart format

This commit is contained in:
2025-07-16 22:44:35 +02:00
parent 100c4ae6cc
commit d40ad09135
64 changed files with 1362 additions and 1626 deletions

View File

@@ -3,10 +3,7 @@ import 'package:flutter/material.dart';
class DenshiJishoBackground extends StatelessWidget {
final Widget child;
const DenshiJishoBackground({
super.key,
required this.child,
});
const DenshiJishoBackground({super.key, required this.child});
@override
Widget build(BuildContext context) {

View File

@@ -50,15 +50,15 @@ class KanjiBox extends StatelessWidget {
this.foreground,
this.background,
this.borderRadius = defaultBorderRadius,
}) : assert(
kanji.length == 1,
'KanjiBox can not show more than one character at a time',
),
assert(
contentPaddingRatio != null || (fontSize != null && padding != null),
'Either contentPaddingRatio or both the fontSize and padding need to be '
'explicitly defined in order for the box to be able to render correctly',
);
}) : assert(
kanji.length == 1,
'KanjiBox can not show more than one character at a time',
),
assert(
contentPaddingRatio != null || (fontSize != null && padding != null),
'Either contentPaddingRatio or both the fontSize and padding need to be '
'explicitly defined in order for the box to be able to render correctly',
);
const factory KanjiBox.withFontSizeAndPadding({
required String kanji,
@@ -76,15 +76,14 @@ class KanjiBox extends StatelessWidget {
Color? foreground,
Color? background,
double borderRadius = defaultBorderRadius,
}) =>
KanjiBox._(
kanji: kanji,
fontSize: fontSize,
padding: pow(ratio * (1 / fontSize), -1).toDouble(),
foreground: foreground,
background: background,
borderRadius: borderRadius,
);
}) => KanjiBox._(
kanji: kanji,
fontSize: fontSize,
padding: pow(ratio * (1 / fontSize), -1).toDouble(),
foreground: foreground,
background: background,
borderRadius: borderRadius,
);
factory KanjiBox.withPadding({
required String kanji,
@@ -93,15 +92,14 @@ class KanjiBox extends StatelessWidget {
Color? foreground,
Color? background,
double borderRadius = defaultBorderRadius,
}) =>
KanjiBox._(
kanji: kanji,
fontSize: ratio * padding,
padding: padding,
foreground: foreground,
background: background,
borderRadius: borderRadius,
);
}) => KanjiBox._(
kanji: kanji,
fontSize: ratio * padding,
padding: padding,
foreground: foreground,
background: background,
borderRadius: borderRadius,
);
factory KanjiBox.expanded({
required String kanji,
@@ -109,14 +107,13 @@ class KanjiBox extends StatelessWidget {
Color? foreground,
Color? background,
double borderRadius = defaultBorderRadius,
}) =>
KanjiBox._(
kanji: kanji,
contentPaddingRatio: ratio,
foreground: foreground,
background: background,
borderRadius: borderRadius,
);
}) => KanjiBox._(
kanji: kanji,
contentPaddingRatio: ratio,
foreground: foreground,
background: background,
borderRadius: borderRadius,
);
/// A shortcut
factory KanjiBox.headline4({
@@ -126,15 +123,14 @@ class KanjiBox extends StatelessWidget {
Color? foreground,
Color? background,
double borderRadius = defaultBorderRadius,
}) =>
KanjiBox.withFontSize(
kanji: kanji,
fontSize: Theme.of(context).textTheme.displaySmall!.fontSize!,
ratio: ratio,
foreground: foreground,
background: background,
borderRadius: borderRadius,
);
}) => KanjiBox.withFontSize(
kanji: kanji,
fontSize: Theme.of(context).textTheme.displaySmall!.fontSize!,
ratio: ratio,
foreground: foreground,
background: background,
borderRadius: borderRadius,
);
@override
Widget build(BuildContext context) {
@@ -146,8 +142,10 @@ class KanjiBox extends StatelessWidget {
background ?? state.theme.menuGreyLight.background;
return LayoutBuilder(
builder: (context, constraints) {
final sizeConstraint =
min(constraints.maxHeight, constraints.maxWidth);
final sizeConstraint = min(
constraints.maxHeight,
constraints.maxWidth,
);
final calculatedFontSize =
fontSize ?? sizeConstraint * fontSizeFactor;
final calculatedPadding =

View File

@@ -5,8 +5,6 @@ class LoadingScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
return const Center(child: CircularProgressIndicator());
}
}

View File

@@ -3,16 +3,14 @@ import 'package:flutter/material.dart';
class OpaqueBox extends StatelessWidget {
final Widget child;
const OpaqueBox({
required this.child,
super.key,
});
const OpaqueBox({required this.child, super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration:
BoxDecoration(color: Theme.of(context).scaffoldBackgroundColor),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: child,
);
}

View File

@@ -10,9 +10,7 @@ class SplashScreen extends StatelessWidget {
return Container(
decoration: BoxDecoration(color: AppTheme.mugitenWheat.background),
child: const Center(
child: Image(
image: AssetImage('assets/images/logo/mugi.png'),
),
child: Image(image: AssetImage('assets/images/logo/mugi.png')),
),
);
}

View File

@@ -44,10 +44,12 @@ class _DrawingBoardState extends State<DrawingBoard> {
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 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,
@@ -55,11 +57,13 @@ class _DrawingBoardState extends State<DrawingBoard> {
strokes.add([]);
undoQueue.clear();
},
onDrawMove: () => strokes.last.add(StrokePoint(
t: DateTime.now().millisecondsSinceEpoch,
x: controller.points.last.offset.dx,
y: controller.points.last.offset.dy,
)),
onDrawMove: () => strokes.last.add(
StrokePoint(
t: DateTime.now().millisecondsSinceEpoch,
x: controller.points.last.offset.dx,
y: controller.points.last.offset.dy,
),
),
onDrawEnd: () => updateSuggestions(),
);
@@ -92,9 +96,9 @@ class _DrawingBoardState extends State<DrawingBoard> {
const katakanaR = r'\p{Script=Katakana}';
final kanjiSuggestions = await GetIt.instance.get<Database>().filterKanji(
suggestions,
deduplicate: true,
);
suggestions,
deduplicate: true,
);
final hiraganaSuggestions = suggestions
.where((s) => RegExp(hiraganaR).hasMatch(s))
.toSet()
@@ -105,40 +109,40 @@ class _DrawingBoardState extends State<DrawingBoard> {
.toList();
return {
if (widget.allowKanji) ...kanjiSuggestions,
if (widget.allowHiragana) ...hiraganaSuggestions,
if (widget.allowKatakana) ...katakanaSuggestions,
}
if (widget.allowKanji) ...kanjiSuggestions,
if (widget.allowHiragana) ...hiraganaSuggestions,
if (widget.allowKatakana) ...katakanaSuggestions,
}
.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;
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),
),
),
);
},
),
);
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);
@@ -176,7 +180,8 @@ class _DrawingBoardState extends State<DrawingBoard> {
// TODO: calculate dynamically
constraints: BoxConstraints(
minHeight: 8 +
minHeight:
8 +
suggestionCirclePadding * 2 +
fontSize +
(2 * 4) +
@@ -194,50 +199,50 @@ class _DrawingBoardState extends State<DrawingBoard> {
}
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),
),
],
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() {
final board = AspectRatio(
@@ -273,12 +278,7 @@ class _DrawingBoardState extends State<DrawingBoard> {
panelColor = state.theme.menuGreyLight;
barColor = state.theme.menuGreyDark;
}),
child: Column(
children: [
suggestionBar(),
drawingPanel(),
],
),
child: Column(children: [suggestionBar(), drawingPanel()]),
);
}
}

View File

@@ -6,27 +6,21 @@ import '../../bloc/theme/theme_bloc.dart';
class TextDivider extends StatelessWidget {
final String text;
const TextDivider({
super.key,
required this.text,
});
const TextDivider({super.key, required this.text});
@override
Widget build(BuildContext context) => BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final colors = state.theme.menuGreyNormal;
builder: (context, state) {
final colors = state.theme.menuGreyNormal;
return Container(
decoration: BoxDecoration(color: colors.background),
padding: const EdgeInsets.symmetric(
vertical: 5,
horizontal: 10,
),
child: DefaultTextStyle.merge(
child: Text(text),
style: TextStyle(color: colors.foreground),
),
);
},
return Container(
decoration: BoxDecoration(color: colors.background),
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
child: DefaultTextStyle.merge(
child: Text(text),
style: TextStyle(color: colors.foreground),
),
);
},
);
}

View File

@@ -28,46 +28,43 @@ class HistoryEntryTile extends StatelessWidget {
void Function() _onTap(context) => entry.isKanji
? () => Navigator.pushNamed(
context,
Routes.kanjiSearch,
arguments: entry.kanji,
)
: () => Navigator.pushNamed(
context,
Routes.search,
arguments: entry.word,
);
context,
Routes.kanjiSearch,
arguments: entry.kanji,
)
: () =>
Navigator.pushNamed(context, Routes.search, arguments: entry.word);
MaterialPageRoute get timestamps => MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(title: const Text('Last searched')),
body: ListView(
children: entry.timestamps
.map(
(ts) => ListTile(
title: Text('${formatDate(ts)} ${formatTime(ts)}'),
),
)
.toList(),
),
),
);
builder: (context) => Scaffold(
appBar: AppBar(title: const Text('Last searched')),
body: ListView(
children: entry.timestamps
.map(
(ts) => ListTile(
title: Text('${formatDate(ts)} ${formatTime(ts)}'),
),
)
.toList(),
),
),
);
List<SlidableAction> _actions(context) => [
SlidableAction(
backgroundColor: Colors.blue,
icon: Icons.access_time,
onPressed: (_) => Navigator.push(context, timestamps),
),
SlidableAction(
backgroundColor: Colors.red,
icon: Icons.delete,
onPressed: (_) async {
await GetIt.instance.get<Database>().historyEntryDelete(entry.id);
onDelete?.call();
},
),
];
SlidableAction(
backgroundColor: Colors.blue,
icon: Icons.access_time,
onPressed: (_) => Navigator.push(context, timestamps),
),
SlidableAction(
backgroundColor: Colors.red,
icon: Icons.delete,
onPressed: (_) async {
await GetIt.instance.get<Database>().historyEntryDelete(entry.id);
onDelete?.call();
},
),
];
@override
Widget build(BuildContext context) {
@@ -88,13 +85,11 @@ class HistoryEntryTile extends StatelessWidget {
child: Text(formatTime(entry.lastTimestamp)),
),
DefaultTextStyle.merge(
style: japaneseFont.textStyle,
child: entry.isKanji
? KanjiBox.headline4(
context: context,
kanji: entry.kanji!,
)
: Expanded(child: Text(entry.word!))),
style: japaneseFont.textStyle,
child: entry.isKanji
? KanjiBox.headline4(context: context, kanji: entry.kanji!)
: Expanded(child: Text(entry.word!)),
),
if (entry.isKanji) Expanded(child: SizedBox.shrink()),
if (entry.timestampCount > 1)
Padding(

View File

@@ -27,21 +27,17 @@ class YomiExample {
// ignore: public_member_api_docs
Map<String, String> toJson() => {
'example': example,
'reading': reading,
'meaning': meaning,
};
'example': example,
'reading': reading,
'meaning': meaning,
};
}
class Examples extends StatelessWidget {
final List<YomiExample> onyomi;
final List<YomiExample> kunyomi;
const Examples({
super.key,
required this.onyomi,
required this.kunyomi,
});
const Examples({super.key, required this.onyomi, required this.kunyomi});
@override
Widget build(BuildContext context) {
@@ -49,7 +45,7 @@ class Examples extends StatelessWidget {
final yomiWidgets =
onyomi.map((onEx) => _Example(onEx, _KanaType.onyomi)).toList() +
kunyomi.map((kunEx) => _Example(kunEx, _KanaType.kunyomi)).toList();
kunyomi.map((kunEx) => _Example(kunEx, _KanaType.kunyomi)).toList();
const noExamplesWidget = Padding(
padding: EdgeInsets.symmetric(vertical: 10),
@@ -66,7 +62,7 @@ class Examples extends StatelessWidget {
if (onyomi.isEmpty && kunyomi.isEmpty)
noExamplesWidget
else
...yomiWidgets
...yomiWidgets,
],
);
}
@@ -82,50 +78,44 @@ class _Example extends StatelessWidget {
@override
Widget build(BuildContext context) => BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final theme = state.theme;
final menuColors = theme.menuGreyNormal;
final kanaColors = kanaType == _KanaType.kunyomi
? theme.kunyomiColor
: theme.onyomiColor;
builder: (context, state) {
final theme = state.theme;
final menuColors = theme.menuGreyNormal;
final kanaColors = kanaType == _KanaType.kunyomi
? theme.kunyomiColor
: theme.onyomiColor;
return Container(
margin: const EdgeInsets.symmetric(
vertical: 5.0,
horizontal: 10.0,
),
decoration: BoxDecoration(
color: menuColors.background,
borderRadius: BorderRadius.circular(10.0),
),
child: IntrinsicHeight(
child: Row(
children: [
InkWell(
onTap: () => Navigator.pushNamed(
context,
Routes.search,
arguments: yomiExample.example,
),
child: _Kana(colors: kanaColors, example: yomiExample),
),
_ExampleText(colors: menuColors, example: yomiExample)
],
return Container(
margin: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
decoration: BoxDecoration(
color: menuColors.background,
borderRadius: BorderRadius.circular(10.0),
),
child: IntrinsicHeight(
child: Row(
children: [
InkWell(
onTap: () => Navigator.pushNamed(
context,
Routes.search,
arguments: yomiExample.example,
),
child: _Kana(colors: kanaColors, example: yomiExample),
),
),
);
},
_ExampleText(colors: menuColors, example: yomiExample),
],
),
),
);
},
);
}
class _Kana extends StatelessWidget {
final ColorSet colors;
final YomiExample example;
const _Kana({
required this.colors,
required this.example,
});
const _Kana({required this.colors, required this.example});
@override
Widget build(BuildContext context) {
@@ -168,10 +158,7 @@ class _ExampleText extends StatelessWidget {
final ColorSet colors;
final YomiExample example;
const _ExampleText({
required this.colors,
required this.example,
});
const _ExampleText({required this.colors, required this.example});
@override
Widget build(BuildContext context) {
@@ -180,12 +167,7 @@ class _ExampleText extends StatelessWidget {
padding: const EdgeInsets.all(10),
child: Wrap(
children: [
Text(
example.meaning,
style: TextStyle(
color: colors.foreground,
),
)
Text(example.meaning, style: TextStyle(color: colors.foreground)),
],
),
),

View File

@@ -8,31 +8,27 @@ class Grade extends StatelessWidget {
final String? grade;
final String ifNullChar;
const Grade({
required this.grade,
this.ifNullChar = '',
super.key,
});
const Grade({required this.grade, this.ifNullChar = '', super.key});
@override
Widget build(BuildContext context) => BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final colors = state.theme.kanjiResultColor;
builder: (context, state) {
final colors = state.theme.kanjiResultColor;
return Container(
padding: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: colors.background,
shape: BoxShape.circle,
),
child: Text(
grade ?? ifNullChar,
style: TextStyle(
color: colors.foreground,
fontSize: 20.0,
).merge(japaneseFont.textStyle),
),
);
},
return Container(
padding: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: colors.background,
shape: BoxShape.circle,
),
child: Text(
grade ?? ifNullChar,
style: TextStyle(
color: colors.foreground,
fontSize: 20.0,
).merge(japaneseFont.textStyle),
),
);
},
);
}

View File

@@ -7,31 +7,30 @@ import '../../../settings.dart';
class Header extends StatelessWidget {
final String kanji;
const Header({
required this.kanji,
super.key,
});
const Header({required this.kanji, super.key});
@override
Widget build(BuildContext context) => AspectRatio(
aspectRatio: 1,
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final colors = state.theme.kanjiResultColor;
aspectRatio: 1,
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final colors = state.theme.kanjiResultColor;
return Container(
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
color: colors.background,
),
child: Text(
kanji,
style: TextStyle(fontSize: 70.0, color: colors.foreground)
.merge(japaneseFont.textStyle),
),
);
},
),
);
return Container(
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
color: colors.background,
),
child: Text(
kanji,
style: TextStyle(
fontSize: 70.0,
color: colors.foreground,
).merge(japaneseFont.textStyle),
),
);
},
),
);
}

View File

@@ -7,30 +7,23 @@ class JlptLevel extends StatelessWidget {
final String? jlptLevel;
final String ifNullChar;
const JlptLevel({
required this.jlptLevel,
this.ifNullChar = '',
super.key,
});
const JlptLevel({required this.jlptLevel, this.ifNullChar = '', super.key});
@override
Widget build(BuildContext context) => BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final colors = state.theme.kanjiResultColor;
return Container(
padding: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colors.background,
),
child: Text(
jlptLevel ?? ifNullChar,
style: TextStyle(
color: colors.foreground,
fontSize: 20.0,
),
),
);
},
builder: (context, state) {
final colors = state.theme.kanjiResultColor;
return Container(
padding: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colors.background,
),
child: Text(
jlptLevel ?? ifNullChar,
style: TextStyle(color: colors.foreground, fontSize: 20.0),
),
);
},
);
}

View File

@@ -8,37 +8,34 @@ import '../../../settings.dart';
class Radical extends StatelessWidget {
final String radical;
const Radical({
required this.radical,
super.key,
});
const Radical({required this.radical, super.key});
@override
Widget build(BuildContext context) => BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final colors = state.theme.kanjiResultColor;
builder: (context, state) {
final colors = state.theme.kanjiResultColor;
return InkWell(
onTap: () => Navigator.pushNamed(
context,
Routes.kanjiSearchRadicals,
arguments: radical,
),
child: Container(
padding: const EdgeInsets.all(15.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colors.background,
),
child: Text(
radical,
style: TextStyle(
color: colors.foreground,
fontSize: 40.0,
).merge(japaneseFont.textStyle),
),
),
);
},
return InkWell(
onTap: () => Navigator.pushNamed(
context,
Routes.kanjiSearchRadicals,
arguments: radical,
),
child: Container(
padding: const EdgeInsets.all(15.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colors.background,
),
child: Text(
radical,
style: TextStyle(
color: colors.foreground,
fontSize: 40.0,
).merge(japaneseFont.textStyle),
),
),
);
},
);
}

View File

@@ -7,32 +7,25 @@ class Rank extends StatelessWidget {
final int? rank;
final String ifNullChar;
const Rank({
required this.rank,
this.ifNullChar = '',
super.key,
});
const Rank({required this.rank, this.ifNullChar = '', super.key});
@override
Widget build(BuildContext context) => BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final colors = state.theme.kanjiResultColor;
builder: (context, state) {
final colors = state.theme.kanjiResultColor;
return Container(
padding: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
shape: (rank == null) ? BoxShape.circle : BoxShape.rectangle,
borderRadius: (rank == null) ? null : BorderRadius.circular(10.0),
color: colors.background,
),
child: Text(
rank != null ? '${rank.toString()} / 2500' : ifNullChar,
style: TextStyle(
color: colors.foreground,
fontSize: 20.0,
),
),
);
},
return Container(
padding: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
shape: (rank == null) ? BoxShape.circle : BoxShape.rectangle,
borderRadius: (rank == null) ? null : BorderRadius.circular(10.0),
color: colors.background,
),
child: Text(
rank != null ? '${rank.toString()} / 2500' : ifNullChar,
style: TextStyle(color: colors.foreground, fontSize: 20.0),
),
);
},
);
}

View File

@@ -6,26 +6,23 @@ import '../../../bloc/theme/theme_bloc.dart';
class StrokeOrderGif extends StatelessWidget {
final String uri;
const StrokeOrderGif({
required this.uri,
super.key,
});
const StrokeOrderGif({required this.uri, super.key});
@override
Widget build(BuildContext context) => BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 20.0),
padding: const EdgeInsets.all(5.0),
decoration: BoxDecoration(
color: state.theme.kanjiResultColor.background,
borderRadius: BorderRadius.circular(15.0),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: Image.network(uri),
),
);
},
builder: (context, state) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 20.0),
padding: const EdgeInsets.all(5.0),
decoration: BoxDecoration(
color: state.theme.kanjiResultColor.background,
borderRadius: BorderRadius.circular(15.0),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: Image.network(uri),
),
);
},
);
}

View File

@@ -7,11 +7,7 @@ import '../../../bloc/theme/theme_bloc.dart';
import '../../../routing/routes.dart';
import '../../../settings.dart';
enum YomiType {
onyomi,
kunyomi,
meaning,
}
enum YomiType { onyomi, kunyomi, meaning }
extension on YomiType {
String get title {
@@ -44,11 +40,7 @@ class YomiChips extends StatelessWidget {
final List<String> yomi;
final YomiType type;
const YomiChips({
required this.yomi,
required this.type,
super.key,
});
const YomiChips({required this.yomi, required this.type, super.key});
bool get isExpandable => yomi.length > 6;
@@ -58,30 +50,26 @@ class YomiChips extends StatelessWidget {
required ColorSet colors,
bool searchable = true,
TextStyle? extraTextStyle,
}) =>
InkWell(
onTap: searchable
? () => Navigator.pushNamed(context, Routes.search, arguments: yomi)
: null,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 5),
padding: const EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 10.0,
),
decoration: BoxDecoration(
color: colors.background,
borderRadius: BorderRadius.circular(10.0),
),
child: Text(
yomi,
style: TextStyle(
fontSize: 20.0,
color: colors.foreground,
).merge(extraTextStyle),
),
),
);
}) => InkWell(
onTap: searchable
? () => Navigator.pushNamed(context, Routes.search, arguments: yomi)
: null,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 5),
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
decoration: BoxDecoration(
color: colors.background,
borderRadius: BorderRadius.circular(10.0),
),
child: Text(
yomi,
style: TextStyle(
fontSize: 20.0,
color: colors.foreground,
).merge(extraTextStyle),
),
),
);
Widget yomiWrapper(BuildContext context) {
final yomiCards = yomi
@@ -109,7 +97,7 @@ class YomiChips extends StatelessWidget {
background: Colors.transparent,
),
),
...yomiCards
...yomiCards,
];
final wrap = Wrap(
@@ -141,10 +129,7 @@ class YomiChips extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 5.0,
),
margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
alignment: Alignment.centerLeft,
child: yomiWrapper(context),
);

View File

@@ -13,10 +13,7 @@ class KanjiGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: 20.0,
horizontal: 40.0,
),
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 40.0),
child: GridView.count(
shrinkWrap: true,
crossAxisCount: 3,

View File

@@ -57,12 +57,11 @@ class KanjiSearchBarState extends State<KanjiSearchBar> {
style: japaneseFont.textStyle,
decoration: InputDecoration(
hintText: 'Search',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.0)),
isDense: false,
suffixIcon:
(button == TextFieldButton.clear) ? clearButton : pasteButton,
suffixIcon: (button == TextFieldButton.clear)
? clearButton
: pasteButton,
),
);
}

View File

@@ -40,18 +40,15 @@ class _IconButton extends StatelessWidget {
final Widget icon;
final void Function()? onPressed;
const _IconButton({
required this.icon,
required this.onPressed,
});
const _IconButton({required this.icon, required this.onPressed});
@override
Widget build(BuildContext context) => BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) => IconButton(
onPressed: onPressed,
icon: icon,
iconSize: 30,
color: state.theme.menuGreyDark.background,
),
);
builder: (context, state) => IconButton(
onPressed: onPressed,
icon: icon,
iconSize: 30,
color: state.theme.menuGreyDark.background,
),
);
}

View File

@@ -11,15 +11,12 @@ Future<void> showAddToLibraryDialog({
required BuildContext context,
required int? jmdictEntryId,
required String? kanji,
}) =>
showDialog(
context: context,
barrierDismissible: true,
builder: (_) => AddToLibraryDialog(
jmdictEntryId: jmdictEntryId,
kanji: kanji,
),
);
}) => showDialog(
context: context,
barrierDismissible: true,
builder: (_) =>
AddToLibraryDialog(jmdictEntryId: jmdictEntryId, kanji: kanji),
);
class AddToLibraryDialog extends StatefulWidget {
final int? jmdictEntryId;
@@ -61,10 +58,10 @@ class _AddToLibraryDialogState extends State<AddToLibraryDialog> {
setState(() => toggleLock = true);
await GetIt.instance.get<Database>().libraryListToggleEntry(
libraryName,
jmdictEntryId: widget.jmdictEntryId,
kanji: widget.kanji,
);
libraryName,
jmdictEntryId: widget.jmdictEntryId,
kanji: widget.kanji,
);
setState(() {
toggleLock = false;
@@ -93,9 +90,9 @@ class _AddToLibraryDialogState extends State<AddToLibraryDialog> {
style: Theme.of(context).textTheme.displayMedium,
)
: FutureBuilder(
future: GetIt.instance
.get<Database>()
.jadbGetWordById(widget.jmdictEntryId!),
future: GetIt.instance.get<Database>().jadbGetWordById(
widget.jmdictEntryId!,
),
builder: (context, snapshot) {
if (snapshot.hasError) {
return ErrorWidget(snapshot.error!);
@@ -139,8 +136,9 @@ class _AddToLibraryDialogState extends State<AddToLibraryDialog> {
final checked = e.value;
return ListTile(
onTap: () => toggleEntry(libraryName),
contentPadding:
const EdgeInsets.symmetric(vertical: 5),
contentPadding: const EdgeInsets.symmetric(
vertical: 5,
),
title: Row(
children: [
Checkbox(

View File

@@ -37,10 +37,9 @@ class LibraryListEntryTile extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
(index + 1).toString(),
style: Theme.of(context)
.textTheme
.titleMedium!
.merge(japaneseFont.textStyle),
style: Theme.of(
context,
).textTheme.titleMedium!.merge(japaneseFont.textStyle),
),
);
}
@@ -51,10 +50,10 @@ class LibraryListEntryTile extends StatelessWidget {
icon: Icons.delete,
onPressed: (_) async {
await GetIt.instance.get<Database>().libraryListDeleteEntry(
library.name,
jmdictEntryId: entry.jmdictEntryId,
kanji: entry.kanji,
);
library.name,
jmdictEntryId: entry.jmdictEntryId,
kanji: entry.kanji,
);
onDelete?.call();
},
);
@@ -76,10 +75,12 @@ class LibraryListEntryTile extends StatelessWidget {
);
onUpdate?.call();
},
title: Row(children: [
SizedBox(width: 15),
KanjiBox.headline4(context: context, kanji: kanji),
]),
title: Row(
children: [
SizedBox(width: 15),
KanjiBox.headline4(context: context, kanji: kanji),
],
),
),
);
}

View File

@@ -43,9 +43,9 @@ class LibraryListTile extends StatelessWidget {
backgroundColor: Colors.red,
icon: Icons.delete,
onPressed: (_) async {
await GetIt.instance
.get<Database>()
.libraryListDeleteList(library.name);
await GetIt.instance.get<Database>().libraryListDeleteList(
library.name,
);
onDelete?.call();
},
),
@@ -53,11 +53,8 @@ class LibraryListTile extends StatelessWidget {
),
child: ListTile(
leading: leading,
onTap: () => Navigator.pushNamed(
context,
Routes.library,
arguments: library,
),
onTap: () =>
Navigator.pushNamed(context, Routes.library, arguments: library),
title: Row(
children: [
Expanded(child: Text(library.name)),

View File

@@ -4,16 +4,16 @@ import 'package:mugiten/models/library_list.dart';
import 'package:sqflite/sqlite_api.dart';
void Function() showNewLibraryDialog(context) => () async {
final String? listName = await showDialog<String>(
context: context,
barrierDismissible: true,
builder: (_) => const NewLibraryDialog(),
);
final String? listName = await showDialog<String>(
context: context,
barrierDismissible: true,
builder: (_) => const NewLibraryDialog(),
);
if (listName == null) return;
if (listName == null) return;
await GetIt.instance.get<Database>().libraryListInsertList(listName);
};
await GetIt.instance.get<Database>().libraryListInsertList(listName);
};
class NewLibraryDialog extends StatefulWidget {
const NewLibraryDialog({super.key});
@@ -22,13 +22,7 @@ class NewLibraryDialog extends StatefulWidget {
State<NewLibraryDialog> createState() => _NewLibraryDialogState();
}
enum _NameState {
initial,
currentlyChecking,
invalid,
alreadyExists,
valid,
}
enum _NameState { initial, currentlyChecking, invalid, alreadyExists, valid }
class _NewLibraryDialogState extends State<NewLibraryDialog> {
final controller = TextEditingController();
@@ -54,9 +48,9 @@ class _NewLibraryDialogState extends State<NewLibraryDialog> {
bool get errorStatus =>
nameState == _NameState.invalid || nameState == _NameState.alreadyExists;
String? get statusLabel => {
_NameState.invalid: 'Invalid Name',
_NameState.alreadyExists: 'Already Exists',
}[nameState];
_NameState.invalid: 'Invalid Name',
_NameState.alreadyExists: 'Already Exists',
}[nameState];
bool get confirmButtonActive => nameState == _NameState.valid;
@override

View File

@@ -12,11 +12,8 @@ class GlobalSearchBar extends StatelessWidget {
GlobalSearchBar({super.key});
void _search(BuildContext context, String text) => Navigator.pushNamed(
context,
Routes.search,
arguments: text,
);
void _search(BuildContext context, String text) =>
Navigator.pushNamed(context, Routes.search, arguments: text);
@override
Widget build(BuildContext context) {
@@ -48,10 +45,7 @@ class GlobalSearchBar extends StatelessWidget {
_search(context, text);
}
},
icon: const Icon(
Icons.search,
color: Colors.white,
),
icon: const Icon(Icons.search, color: Colors.white),
),
),
),
@@ -70,8 +64,8 @@ class GlobalSearchBar extends StatelessWidget {
final pos = textController.selection.baseOffset;
textController.text =
textController.text.substring(0, pos) +
result +
textController.text.substring(pos);
result +
textController.text.substring(pos);
textController.selection = TextSelection.fromPosition(
TextPosition(offset: pos + result.length),
);
@@ -84,9 +78,9 @@ class GlobalSearchBar extends StatelessWidget {
}
}
},
)
),
],
)
),
],
),
);
@@ -102,10 +96,8 @@ class GlobalSearchBar extends StatelessWidget {
const Expanded(child: Column()),
DrawingBoard(
onlyOneCharacterSuggestions: true,
onSuggestionChosen: (suggestion) => Navigator.pop(
context,
suggestion,
),
onSuggestionChosen: (suggestion) =>
Navigator.pop(context, suggestion),
),
],
),

View File

@@ -23,9 +23,9 @@ class _LanguageSelectorState extends State<LanguageSelector> {
}
Future<void> _updateSelectedStatus() async => prefs.setStringList(
'languageSelectorStatus',
isSelected.map((b) => b ? '1' : '0').toList(),
);
'languageSelectorStatus',
isSelected.map((b) => b ? '1' : '0').toList(),
);
List<bool>? _getSelectedStatus() => prefs
.getStringList('languageSelectorStatus')
@@ -33,13 +33,10 @@ class _LanguageSelectorState extends State<LanguageSelector> {
.toList();
Widget _languageOption(String language, {TextStyle? style}) => Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0),
child: Text(
language,
style: style,
),
);
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0),
child: Text(language, style: style),
);
@override
Widget build(BuildContext context) {
@@ -49,7 +46,7 @@ class _LanguageSelectorState extends State<LanguageSelector> {
children: [
_languageOption('Auto'),
_languageOption('日本語', style: japaneseFont.textStyle),
_languageOption('English')
_languageOption('English'),
],
onPressed: (buttonIndex) {
setState(() {

View File

@@ -4,11 +4,7 @@ class CircleBadge extends StatelessWidget {
final Widget? child;
final Color color;
const CircleBadge({
super.key,
this.child,
required this.color,
});
const CircleBadge({super.key, this.child, required this.color});
@override
Widget build(BuildContext context) {
@@ -17,10 +13,7 @@ class CircleBadge extends StatelessWidget {
width: 30,
height: 30,
margin: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
child: FittedBox(child: child),
);
}

View File

@@ -4,10 +4,7 @@ import 'circle_badge.dart';
class CommonBadge extends StatelessWidget {
final bool isCommon;
const CommonBadge({
required this.isCommon,
super.key,
});
const CommonBadge({required this.isCommon, super.key});
@override
Widget build(BuildContext context) {
@@ -15,9 +12,7 @@ class CommonBadge extends StatelessWidget {
color: isCommon ? Colors.green : Colors.transparent,
child: Text(
'C',
style: TextStyle(
color: isCommon ? Colors.white : Colors.transparent,
),
style: TextStyle(color: isCommon ? Colors.white : Colors.transparent),
),
);
}

View File

@@ -28,10 +28,7 @@ class JapaneseHeader extends StatelessWidget {
style: japaneseFont.textStyle,
)
: const Text(''),
Text(
baseWord,
style: japaneseFont.textStyle,
),
Text(baseWord, style: japaneseFont.textStyle),
],
),
);

View File

@@ -4,19 +4,13 @@ import 'circle_badge.dart';
class JLPTBadge extends StatelessWidget {
final String? jlptLevel;
const JLPTBadge({
required this.jlptLevel,
super.key,
});
const JLPTBadge({required this.jlptLevel, super.key});
@override
Widget build(BuildContext context) {
return CircleBadge(
color: jlptLevel != null ? Colors.blue : Colors.transparent,
child: Text(
jlptLevel ?? '',
style: const TextStyle(color: Colors.white),
),
child: Text(jlptLevel ?? '', style: const TextStyle(color: Colors.white)),
);
}
}

View File

@@ -8,51 +8,44 @@ import '../../../../settings.dart';
class KanjiRow extends StatelessWidget {
final List<String> kanji;
final double fontSize;
const KanjiRow({
super.key,
required this.kanji,
this.fontSize = 20,
});
const KanjiRow({super.key, required this.kanji, this.fontSize = 20});
Widget _kanjiBox(String kanji) => UnconstrainedBox(
child: IntrinsicHeight(
child: AspectRatio(
aspectRatio: 1,
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final colors = state.theme.menuGreyLight;
return Container(
padding: const EdgeInsets.all(10),
alignment: Alignment.center,
decoration: BoxDecoration(
color: colors.background,
borderRadius: BorderRadius.circular(10),
),
child: FittedBox(
child: Text(
kanji,
style: TextStyle(
color: colors.foreground,
fontSize: fontSize,
).merge(japaneseFont.textStyle),
),
),
);
},
),
),
child: IntrinsicHeight(
child: AspectRatio(
aspectRatio: 1,
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final colors = state.theme.menuGreyLight;
return Container(
padding: const EdgeInsets.all(10),
alignment: Alignment.center,
decoration: BoxDecoration(
color: colors.background,
borderRadius: BorderRadius.circular(10),
),
child: FittedBox(
child: Text(
kanji,
style: TextStyle(
color: colors.foreground,
fontSize: fontSize,
).merge(japaneseFont.textStyle),
),
),
);
},
),
);
),
),
);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Kanji:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const Text('Kanji:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 5),
Wrap(
spacing: 10,
@@ -66,7 +59,7 @@ class KanjiRow extends StatelessWidget {
arguments: k,
),
child: _kanjiBox(k),
)
),
],
),
],

View File

@@ -26,16 +26,14 @@ class KanjiKanaBox extends StatelessWidget {
this.centerFurigana = true,
this.furiganaFontsize,
this.kanjiFontsize,
this.margin = const EdgeInsets.symmetric(
horizontal: 5.0,
vertical: 5.0,
),
this.margin = const EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0),
this.padding = const EdgeInsets.all(5.0),
});
@override
Widget build(BuildContext context) {
final fFontsize = furiganaFontsize ??
final fFontsize =
furiganaFontsize ??
((kanjiFontsize != null) ? 0.8 * kanjiFontsize! : null);
return Container(
@@ -53,14 +51,15 @@ class KanjiKanaBox extends StatelessWidget {
romajiEnabled
? transliterateKanaToLatin(furigana!)
: furigana!,
style: TextStyle(
fontSize: fFontsize,
color: colors.foreground,
).merge(
romajiEnabled && autoTransliterateRomaji
? null
: japaneseFont.textStyle,
),
style:
TextStyle(
fontSize: fFontsize,
color: colors.foreground,
).merge(
romajiEnabled && autoTransliterateRomaji
? null
: japaneseFont.textStyle,
),
)
: Text(
'',
@@ -71,13 +70,12 @@ class KanjiKanaBox extends StatelessWidget {
),
DefaultTextStyle.merge(
child: Text(baseWord),
style: TextStyle(fontSize: kanjiFontsize)
.merge(japaneseFont.textStyle),
style: TextStyle(
fontSize: kanjiFontsize,
).merge(japaneseFont.textStyle),
),
if (romajiEnabled && showRomajiBelow)
Text(
transliterateKanaToLatin(furigana ?? baseWord),
)
Text(transliterateKanaToLatin(furigana ?? baseWord)),
],
),
style: TextStyle(color: colors.foreground),

View File

@@ -6,13 +6,10 @@ class Notes extends StatelessWidget {
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Notes:',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(notes.join(', ')),
],
);
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Notes:', style: TextStyle(fontWeight: FontWeight.bold)),
Text(notes.join(', ')),
],
);
}

View File

@@ -12,28 +12,28 @@ class OtherForms extends StatelessWidget {
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: forms.isNotEmpty
? [
const Text(
'Other Forms:',
style: TextStyle(fontWeight: FontWeight.bold),
),
Wrap(
children: [
for (final form in forms)
BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
return KanjiKanaBox(
baseWord: form.base,
furigana: form.furigana,
colors: state.theme.menuGreyLight,
);
},
),
],
),
]
: [],
);
crossAxisAlignment: CrossAxisAlignment.start,
children: forms.isNotEmpty
? [
const Text(
'Other Forms:',
style: TextStyle(fontWeight: FontWeight.bold),
),
Wrap(
children: [
for (final form in forms)
BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
return KanjiKanaBox(
baseWord: form.base,
furigana: form.furigana,
colors: state.theme.menuGreyLight,
);
},
),
],
),
]
: [],
);
}

View File

@@ -14,12 +14,12 @@ class EnglishDefinitions extends StatelessWidget {
@override
Widget build(BuildContext context) => Wrap(
runSpacing: 10.0,
spacing: 5,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (final def in englishDefinitions)
SearchChip(text: def, colors: colors)
],
);
runSpacing: 10.0,
spacing: 5,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (final def in englishDefinitions)
SearchChip(text: def, colors: colors),
],
);
}

View File

@@ -16,14 +16,14 @@ class SearchChip extends StatelessWidget {
@override
Widget build(BuildContext context) => Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: colors.background,
borderRadius: BorderRadius.circular(10.0),
),
child: Text(
text,
style: TextStyle(color: colors.foreground).merge(extraTextStyle),
),
);
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: colors.background,
borderRadius: BorderRadius.circular(10.0),
),
child: Text(
text,
style: TextStyle(color: colors.foreground).merge(extraTextStyle),
),
);
}

View File

@@ -6,22 +6,15 @@ import 'sense/sense.dart';
class Senses extends StatelessWidget {
final List<WordSearchSense> senses;
const Senses({
required this.senses,
super.key,
});
const Senses({required this.senses, super.key});
List<Widget> get _senseWidgets => [
for (int i = 0; i < senses.length; i++)
Sense(
index: i,
sense: senses[i],
),
];
for (int i = 0; i < senses.length; i++) Sense(index: i, sense: senses[i]),
];
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _senseWidgets,
);
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _senseWidgets,
);
}

View File

@@ -57,10 +57,7 @@ class DatabaseMigration {
final String path;
final String content;
const DatabaseMigration({
required this.path,
required this.content,
});
const DatabaseMigration({required this.path, required this.content});
int get version {
final String fileName = basenameWithoutExtension(path);
@@ -76,12 +73,12 @@ class DatabaseMigration {
Future<List<DatabaseMigration>> readMigrationsFromAssets() async {
log('Reading migrations from assets...');
final String assetManifest =
await rootBundle.loadString('AssetManifest.json');
final String assetManifest = await rootBundle.loadString(
'AssetManifest.json',
);
final List<String> migrations =
(jsonDecode(assetManifest) as Map<String, Object?>)
.keys
(jsonDecode(assetManifest) as Map<String, Object?>).keys
.where(
(assetPath) =>
RegExp(r'^migrations\/\d{4}.*\.sql$').hasMatch(assetPath),
@@ -96,12 +93,10 @@ Future<List<DatabaseMigration>> readMigrationsFromAssets() async {
}
return Future.wait(
migrations.map(
(migration) async {
final content = await rootBundle.loadString(migration, cache: false);
return DatabaseMigration(path: migration, content: content);
},
),
migrations.map((migration) async {
final content = await rootBundle.loadString(migration, cache: false);
return DatabaseMigration(path: migration, content: content);
}),
);
}
@@ -164,8 +159,11 @@ Future<Database> openAndMigrateDatabase(
onUpgrade: (db, oldVersion, newVersion) async {
log('Migrating database from v$oldVersion to v$newVersion...');
final migrationsToRun = migrations
.where((migration) =>
migration.version > oldVersion && migration.version <= newVersion)
.where(
(migration) =>
migration.version > oldVersion &&
migration.version <= newVersion,
)
.toList();
await migrate(db, migrationsToRun);
@@ -194,7 +192,9 @@ Future<void> setupDatabase() async {
final String dbPath = await databasePath();
assert(
await File(dbPath).exists(), 'Database file should exist at this point');
await File(dbPath).exists(),
'Database file should exist at this point',
);
final database = await openDatabaseWithoutMigrations(
dbPath,
@@ -202,8 +202,10 @@ Future<void> setupDatabase() async {
verifyTables: true,
);
assert(await database.getVersion() == expectedDatabaseVersion,
'Database version should be $expectedDatabaseVersion');
assert(
await database.getVersion() == expectedDatabaseVersion,
'Database version should be $expectedDatabaseVersion',
);
log('Registering database in GetIt...');
GetIt.instance.registerSingleton<Database>(database);
@@ -235,5 +237,6 @@ Future<void> extractJadbFromAssets(String path) async {
ByteData data = await rootBundle.load('assets/jadb.sqlite');
await jadbFile.writeAsBytes(
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes));
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes),
);
}

View File

@@ -2,10 +2,7 @@ abstract class DatabaseError implements ArgumentError {
final String? tableName;
final Map<String, dynamic>? illegalArguments;
const DatabaseError({
this.tableName,
this.illegalArguments,
});
const DatabaseError({this.tableName, this.illegalArguments});
@override
dynamic get invalidValue => illegalArguments;
@@ -15,10 +12,7 @@ abstract class DatabaseError implements ArgumentError {
}
class DataAlreadyExistsError extends DatabaseError {
const DataAlreadyExistsError({
super.tableName,
super.illegalArguments,
});
const DataAlreadyExistsError({super.tableName, super.illegalArguments});
@override
String? get name => illegalArguments?.keys.join(', ');
@@ -31,10 +25,7 @@ class DataAlreadyExistsError extends DatabaseError {
}
class DataNotFoundError extends DatabaseError {
const DataNotFoundError({
super.tableName,
super.illegalArguments,
});
const DataNotFoundError({super.tableName, super.illegalArguments});
@override
String? get name => illegalArguments?.keys.join(', ');
@@ -47,10 +38,7 @@ class DataNotFoundError extends DatabaseError {
}
class IllegalDeletionError extends DatabaseError {
const IllegalDeletionError({
super.tableName,
super.illegalArguments,
});
const IllegalDeletionError({super.tableName, super.illegalArguments});
@override
String? get name => illegalArguments?.keys.join(', ');

View File

@@ -33,10 +33,10 @@ abstract class HistoryTableNames {
'Mugiten_HistoryEntry_orderedByTimestamp';
static Set<String> get allTables => {
historyEntry,
historyEntryKanji,
historyEntryTimestamp,
historyEntryWord,
historyEntryOrderedByTimestamp,
};
historyEntry,
historyEntryKanji,
historyEntryTimestamp,
historyEntryWord,
historyEntryOrderedByTimestamp,
};
}

View File

@@ -22,8 +22,8 @@ abstract class LibraryListTableNames {
static const String libraryListOrdered = 'Mugiten_LibraryList_Ordered';
static Set<String> get allTables => {
libraryList,
libraryListEntry,
libraryListOrdered,
};
libraryList,
libraryListEntry,
libraryListOrdered,
};
}

View File

@@ -68,9 +68,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (context) => themeBloc),
],
providers: [BlocProvider(create: (context) => themeBloc)],
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, themeState) => MaterialApp(
title: '麦典',

View File

@@ -26,14 +26,18 @@ extension HistoryEntryExt on DatabaseExecutor {
final entryId = result.first['entryId']! as int;
final language = result.first['language'] as String?;
final List<DateTime> timestamps = (await query(
HistoryTableNames.historyEntryTimestamp,
where: 'entryId = ?',
whereArgs: [entryId],
orderBy: 'timestamp DESC',
))
.map((e) => DateTime.fromMillisecondsSinceEpoch(e['timestamp']! as int))
.toList();
final List<DateTime> timestamps =
(await query(
HistoryTableNames.historyEntryTimestamp,
where: 'entryId = ?',
whereArgs: [entryId],
orderBy: 'timestamp DESC',
))
.map(
(e) =>
DateTime.fromMillisecondsSinceEpoch(e['timestamp']! as int),
)
.toList();
// TODO: join with search result(s) if matching exactly one, or single search result
@@ -64,17 +68,22 @@ extension HistoryEntryExt on DatabaseExecutor {
final entryId = result.first['entryId']! as int;
final List<DateTime> timestamps = (await query(
HistoryTableNames.historyEntryTimestamp,
where: 'entryId = ?',
whereArgs: [entryId],
orderBy: 'timestamp DESC',
))
.map((e) => DateTime.fromMillisecondsSinceEpoch(e['timestamp']! as int))
.toList();
final List<DateTime> timestamps =
(await query(
HistoryTableNames.historyEntryTimestamp,
where: 'entryId = ?',
whereArgs: [entryId],
orderBy: 'timestamp DESC',
))
.map(
(e) =>
DateTime.fromMillisecondsSinceEpoch(e['timestamp']! as int),
)
.toList();
KanjiSearchResult? kanjiSearchResult =
includeSearchResult ? await jadbSearchKanji(kanji) : null;
KanjiSearchResult? kanjiSearchResult = includeSearchResult
? await jadbSearchKanji(kanji)
: null;
return HistoryEntry(
id: entryId,
@@ -109,10 +118,7 @@ extension HistoryEntryExt on DatabaseExecutor {
${pageSize != null ? 'LIMIT ?' : ''}
${page != null ? 'OFFSET ?' : ''}
''',
[
if (pageSize != null) pageSize,
if (page != null) page * pageSize!,
],
[if (pageSize != null) pageSize, if (page != null) page * pageSize!],
);
final List<HistoryEntry> entries = result.map((e) {
@@ -152,12 +158,10 @@ extension HistoryEntryExt on DatabaseExecutor {
);
count = result.firstOrNull?['count'] as int? ?? 0;
} else {
final result = await rawQuery(
'''
final result = await rawQuery('''
SELECT COUNT(*) AS count
FROM "${HistoryTableNames.historyEntryTimestamp}"
''',
);
''');
count = result.firstOrNull?['count'] as int? ?? 0;
}
@@ -185,21 +189,15 @@ extension HistoryEntryExt on DatabaseExecutor {
{},
nullColumnHack: 'id',
);
await insert(
HistoryTableNames.historyEntryKanji,
{
'entryId': id,
'kanji': kanji,
},
);
await insert(HistoryTableNames.historyEntryKanji, {
'entryId': id,
'kanji': kanji,
});
}
await insert(
HistoryTableNames.historyEntryTimestamp,
{
'entryId': id,
'timestamp': timestamp.millisecondsSinceEpoch,
},
{'entryId': id, 'timestamp': timestamp.millisecondsSinceEpoch},
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
@@ -223,27 +221,17 @@ extension HistoryEntryExt on DatabaseExecutor {
{},
nullColumnHack: 'id',
);
await insert(
HistoryTableNames.historyEntryWord,
{
'entryId': id,
'word': word,
// TODO: use an enum?
'language': {
null: null,
'japanese': 'j',
'english': 'e',
}[language]
},
);
await insert(HistoryTableNames.historyEntryWord, {
'entryId': id,
'word': word,
// TODO: use an enum?
'language': {null: null, 'japanese': 'j', 'english': 'e'}[language],
});
}
await insert(
HistoryTableNames.historyEntryTimestamp,
{
'entryId': id,
'timestamp': timestamp.millisecondsSinceEpoch,
},
{'entryId': id, 'timestamp': timestamp.millisecondsSinceEpoch},
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
@@ -294,10 +282,7 @@ extension HistoryEntryExt on DatabaseExecutor {
final result = await delete(
HistoryTableNames.historyEntryTimestamp,
where: 'entryId = ? AND timestamp = ?',
whereArgs: [
entryId,
timestamp.millisecondsSinceEpoch,
],
whereArgs: [entryId, timestamp.millisecondsSinceEpoch],
);
if (result == 0) {
@@ -351,15 +336,13 @@ extension HistoryEntryExt on DatabaseExecutor {
} else {
id = existingEntry.first['entryId']! as int;
}
final List<int> timestamps =
(jsonObject['timestamps']! as List).map((ts) => ts as int).toList();
final List<int> timestamps = (jsonObject['timestamps']! as List)
.map((ts) => ts as int)
.toList();
for (final timestamp in timestamps) {
b.insert(
HistoryTableNames.historyEntryTimestamp,
{
'entryId': id,
'timestamp': timestamp,
},
{'entryId': id, 'timestamp': timestamp},
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
@@ -388,36 +371,33 @@ class HistoryEntry {
this.wordSearchResult,
this.kanji,
this.kanjiSearchResult,
}) : assert(
(word != null && kanji == null) || (word == null && kanji != null),
'HistoryEntry must have either a word or a kanji, but not both',
),
assert(
(language == null || word != null),
'If language is provided, word must not be null',
),
assert(
(kanjiSearchResult == null || kanji != null),
'If kanjiSearchResult is provided, kanji must not be null',
),
assert(
(wordSearchResult == null || word != null),
'If wordSearchResult is provided, word must not be null',
),
assert(
kanji == null || kanji.runes.length == 1,
'Kanji must be a single character',
),
// TODO: This has not always been the case, so we should add a migration
// or something to clean up the data.
// assert(
// word == null || word == word.trim(),
// 'Word must not contain leading or trailing whitespace',
// ),
assert(
timestamps.isNotEmpty,
'Timestamps must not be empty',
);
}) : assert(
(word != null && kanji == null) || (word == null && kanji != null),
'HistoryEntry must have either a word or a kanji, but not both',
),
assert(
(language == null || word != null),
'If language is provided, word must not be null',
),
assert(
(kanjiSearchResult == null || kanji != null),
'If kanjiSearchResult is provided, kanji must not be null',
),
assert(
(wordSearchResult == null || word != null),
'If wordSearchResult is provided, word must not be null',
),
assert(
kanji == null || kanji.runes.length == 1,
'Kanji must be a single character',
),
// TODO: This has not always been the case, so we should add a migration
// or something to clean up the data.
// assert(
// word == null || word == word.trim(),
// 'Word must not contain leading or trailing whitespace',
// ),
assert(timestamps.isNotEmpty, 'Timestamps must not be empty');
bool get isKanji => word == null;
int get timestampCount => timestamps.length;

View File

@@ -25,20 +25,19 @@ extension LibraryListExt on DatabaseExecutor {
${pageSize != null ? 'LIMIT ?' : ''}
${page != null ? 'OFFSET ?' : ''}
''',
[
if (pageSize != null) pageSize,
if (page != null) page * pageSize!,
],
[if (pageSize != null) pageSize, if (page != null) page * pageSize!],
);
// COUNT(*) AS "count"
// LEFT JOIN "${LibraryListTableNames.libraryListEntry}"
return result
.map((row) => LibraryList(
name: row['name'] as String,
totalCount: row['count'] as int? ?? 0,
))
.map(
(row) => LibraryList(
name: row['name'] as String,
totalCount: row['count'] as int? ?? 0,
),
)
.toList();
}
@@ -207,10 +206,7 @@ extension LibraryListExt on DatabaseExecutor {
) AS "exists"
FROM "${LibraryListTableNames.libraryListOrdered}"
''',
[
jmdictEntryId,
kanji,
],
[jmdictEntryId, kanji],
);
return {
@@ -233,11 +229,7 @@ extension LibraryListExt on DatabaseExecutor {
AND ("jmdictEntryId" = ? OR "kanji" = ?)
) AS "exists"
''',
[
listName,
jmdictEntryId,
kanji,
],
[listName, jmdictEntryId, kanji],
);
return (result.firstOrNull?['exists'] as int? ?? 0) == 1;
@@ -282,13 +274,10 @@ extension LibraryListExt on DatabaseExecutor {
// // This is ok, because "favourites" should always exist.
final prevList = (await libraryListGetLists()).last;
await insert(
LibraryListTableNames.libraryList,
{
'name': listName,
'prevList': prevList.name,
},
);
await insert(LibraryListTableNames.libraryList, {
'name': listName,
'prevList': prevList.name,
});
return true;
}
@@ -393,10 +382,7 @@ extension LibraryListExt on DatabaseExecutor {
b.update(
LibraryListTableNames.libraryListEntry,
{
'prevEntryJmdictEntryId': jmdictEntryId,
'prevEntryKanji': kanji,
},
{'prevEntryJmdictEntryId': jmdictEntryId, 'prevEntryKanji': kanji},
where: '"listName" = ? AND ("jmdictEntryId" = ? OR "kanji" = ?)',
whereArgs: [listName, nextEntry.jmdictEntryId, nextEntry.kanji],
);
@@ -407,19 +393,17 @@ extension LibraryListExt on DatabaseExecutor {
}
}
final LibraryListEntry? prevEntry =
(await libraryListGetListEntries(listName))!.entries.lastOrNull;
final LibraryListEntry? prevEntry = (await libraryListGetListEntries(
listName,
))!.entries.lastOrNull;
await insert(
LibraryListTableNames.libraryListEntry,
{
'listName': listName,
'jmdictEntryId': jmdictEntryId,
'kanji': kanji,
'prevEntryJmdictEntryId': prevEntry?.jmdictEntryId,
'prevEntryKanji': prevEntry?.kanji,
},
);
await insert(LibraryListTableNames.libraryListEntry, {
'listName': listName,
'jmdictEntryId': jmdictEntryId,
'kanji': kanji,
'prevEntryJmdictEntryId': prevEntry?.jmdictEntryId,
'prevEntryKanji': prevEntry?.kanji,
});
return true;
}
@@ -470,8 +454,9 @@ extension LibraryListExt on DatabaseExecutor {
entryQuery.first['prevEntryJmdictEntryId'] as int?;
final prevEntryKanji = entryQuery.first['prevEntryKanji'] as String?;
final LibraryListEntry? nextEntry =
nextEntryQuery.map((e) => LibraryListEntry.fromDBMap(e)).firstOrNull;
final LibraryListEntry? nextEntry = nextEntryQuery
.map((e) => LibraryListEntry.fromDBMap(e))
.firstOrNull;
// TODO: use a transaction instead of a batch
final b = batch();
@@ -523,8 +508,7 @@ extension LibraryListExt on DatabaseExecutor {
listName,
page: 0,
pageSize: position + 1,
))
?.entries;
))?.entries;
if (entries == null || position >= entries.length) {
return false;
}
@@ -570,7 +554,8 @@ extension LibraryListExt on DatabaseExecutor {
);
}
final shouldToggleOn = overrideToggleOn ??
final shouldToggleOn =
overrideToggleOn ??
!(await libraryListListContains(
listName,
jmdictEntryId: jmdictEntryId,
@@ -583,20 +568,14 @@ extension LibraryListExt on DatabaseExecutor {
jmdictEntryId: jmdictEntryId,
kanji: kanji,
);
assert(
result,
'Failed to insert entry into library list "$listName".',
);
assert(result, 'Failed to insert entry into library list "$listName".');
} else {
final result = await libraryListDeleteEntry(
listName,
jmdictEntryId: jmdictEntryId,
kanji: kanji,
);
assert(
result,
'Failed to delete entry from library list "$listName".',
);
assert(result, 'Failed to delete entry from library list "$listName".');
}
return shouldToggleOn;
@@ -623,8 +602,9 @@ extension LibraryListExt on DatabaseExecutor {
String listName,
List<Map<String, Object?>> jsonEntries,
) async {
List<LibraryListEntry> entries =
jsonEntries.map((e) => LibraryListEntry.fromJson(e)).toList();
List<LibraryListEntry> entries = jsonEntries
.map((e) => LibraryListEntry.fromJson(e))
.toList();
// TODO: batch
for (final entry in entries) {
@@ -641,10 +621,7 @@ class LibraryList {
final String name;
final int totalCount;
const LibraryList({
required this.name,
required this.totalCount,
});
const LibraryList({required this.name, required this.totalCount});
}
class LibraryListPage {
@@ -674,45 +651,45 @@ class LibraryListEntry {
this.jmdictEntryId,
this.kanji,
this.kanjiSearchResult,
}) : lastModified = lastModified ?? DateTime.now(),
assert(
kanji != null || jmdictEntryId != null,
"Library entry can't be empty",
),
assert(
!(kanji != null && jmdictEntryId != null),
"Library entry can't have both kanji and jmdictEntryId",
),
assert(
kanjiSearchResult?.kanji == kanji,
"KanjiSearchResult's kanji must match the kanji in LibraryListEntry",
),
assert(
wordSearchResult?.entryId == jmdictEntryId,
"WordSearchResult's jmdictEntryId must match the jmdictEntryId in LibraryListEntry",
);
}) : lastModified = lastModified ?? DateTime.now(),
assert(
kanji != null || jmdictEntryId != null,
"Library entry can't be empty",
),
assert(
!(kanji != null && jmdictEntryId != null),
"Library entry can't have both kanji and jmdictEntryId",
),
assert(
kanjiSearchResult?.kanji == kanji,
"KanjiSearchResult's kanji must match the kanji in LibraryListEntry",
),
assert(
wordSearchResult?.entryId == jmdictEntryId,
"WordSearchResult's jmdictEntryId must match the jmdictEntryId in LibraryListEntry",
);
LibraryListEntry.fromJmdictId({
required int this.jmdictEntryId,
this.wordSearchResult,
DateTime? lastModified,
}) : lastModified = lastModified ?? DateTime.now(),
kanji = null,
kanjiSearchResult = null;
}) : lastModified = lastModified ?? DateTime.now(),
kanji = null,
kanjiSearchResult = null;
LibraryListEntry.fromKanji({
required String this.kanji,
this.kanjiSearchResult,
DateTime? lastModified,
}) : lastModified = lastModified ?? DateTime.now(),
jmdictEntryId = null,
wordSearchResult = null;
}) : lastModified = lastModified ?? DateTime.now(),
jmdictEntryId = null,
wordSearchResult = null;
Map<String, Object?> toJson() => {
'kanji': kanji,
'jmdictEntryId': jmdictEntryId,
'lastModified': lastModified.millisecondsSinceEpoch,
};
'kanji': kanji,
'jmdictEntryId': jmdictEntryId,
'lastModified': lastModified.millisecondsSinceEpoch,
};
factory LibraryListEntry.fromJson(Map<String, Object?> json) {
assert(

View File

@@ -43,10 +43,7 @@ class ColorSet {
final Color foreground;
final Color background;
const ColorSet({
required this.foreground,
required this.background,
});
const ColorSet({required this.foreground, required this.background});
}
/// Source: https://blog.usejournal.com/creating-a-custom-color-swatch-in-flutter-554bcdcb27f3

View File

@@ -5,14 +5,14 @@ import 'package:sqflite/sqflite.dart';
Future<void> verifyMugitenTablesWithDbConnection(DatabaseExecutor db) async {
final Set<String> tables = await db
.query(
'sqlite_master',
columns: ['name'],
where: 'type IN (?, ?)',
whereArgs: ['table', 'view'],
)
'sqlite_master',
columns: ['name'],
where: 'type IN (?, ?)',
whereArgs: ['table', 'view'],
)
.then((result) {
return result.map((row) => row['name'] as String).toSet();
});
return result.map((row) => row['name'] as String).toSet();
});
final Set<String> expectedTables = {
...HistoryTableNames.allTables,
@@ -22,14 +22,16 @@ Future<void> verifyMugitenTablesWithDbConnection(DatabaseExecutor db) async {
final missingTables = expectedTables.difference(tables);
if (missingTables.isNotEmpty) {
throw Exception([
'Missing tables:',
missingTables.map((table) => ' - $table').join('\n'),
'',
'Found tables:\n',
tables.map((table) => ' - $table').join('\n'),
'',
'Please ensure the database is correctly set up.',
].join('\n'));
throw Exception(
[
'Missing tables:',
missingTables.map((table) => ' - $table').join('\n'),
'',
'Found tables:\n',
tables.map((table) => ' - $table').join('\n'),
'',
'Please ensure the database is correctly set up.',
].join('\n'),
);
}
}

View File

@@ -28,9 +28,7 @@ Route<Widget> generateRoute(RouteSettings settings) {
case Routes.kanjiSearch:
final searchTerm = args! as String;
return MaterialPageRoute(
builder: (_) => KanjiSearchResultPage(
kanji: searchTerm,
),
builder: (_) => KanjiSearchResultPage(kanji: searchTerm),
);
case Routes.kanjiSearchDraw:
@@ -63,8 +61,10 @@ Route<Widget> generateRoute(RouteSettings settings) {
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
appBar:
AppBar(title: const Text('Error'), backgroundColor: Colors.red),
appBar: AppBar(
title: const Text('Error'),
backgroundColor: Colors.red,
),
body: Center(child: ErrorWidget('Some kind of error occured')),
),
);

View File

@@ -8,14 +8,12 @@ class DebugView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: GetIt.instance.get<Database>().rawQuery(
"""
future: GetIt.instance.get<Database>().rawQuery("""
SELECT name, type
FROM sqlite_master
WHERE name NOT LIKE 'sqlite_%'
ORDER BY name
""",
),
"""),
builder: (context, snapshot) {
if (snapshot.hasError) return ErrorWidget(snapshot.error!);
if (!snapshot.hasData) {
@@ -23,14 +21,13 @@ class DebugView extends StatelessWidget {
}
return Scaffold(
appBar: AppBar(
title: const Text('Debug View'),
),
appBar: AppBar(title: const Text('Debug View')),
body: ListView.builder(
itemCount: (snapshot.data as List<Map<String, dynamic>>).length,
itemBuilder: (context, index) {
final data = (snapshot.data as List<Map<String, dynamic>>)[index];
final tableName = (data['name'] as String) +
final tableName =
(data['name'] as String) +
(data['type'] == 'table' ? '' : ' (${data['type']})');
return ListTile(
title: Text(tableName),

View File

@@ -25,11 +25,9 @@ class _HistoryViewState extends State<HistoryView> {
getNextPageKey: (state) =>
state.lastPageIsEmpty ? null : state.nextIntPageKey,
fetchPage: (pageKey) async {
List<HistoryEntry?> result =
await GetIt.instance.get<Database>().historyEntryGetAll(
page: pageKey - 1,
pageSize: pageSize,
);
List<HistoryEntry?> result = await GetIt.instance
.get<Database>()
.historyEntryGetAll(page: pageKey - 1, pageSize: pageSize);
// Insert a null entry at the start in order to prepend a separator to the first actual entry.
if (pageKey == 1) {
@@ -65,10 +63,7 @@ class _HistoryViewState extends State<HistoryView> {
child: Center(
child: Text(
'$amountOfEntries distinct searches made',
style: const TextStyle(
fontSize: 10,
color: Colors.grey,
),
style: const TextStyle(fontSize: 10, color: Colors.grey),
),
),
),
@@ -77,48 +72,48 @@ class _HistoryViewState extends State<HistoryView> {
controller: _pagingController,
builder: (context, state, fetchNextPage) =>
PagedListView<int, HistoryEntry?>.separated(
state: state,
fetchNextPage: fetchNextPage,
separatorBuilder: (context, index) {
if (index == 0) {
final firstItemDate =
_pagingController.items![1]!.lastTimestamp;
return _dateDivider(firstItemDate);
}
state: state,
fetchNextPage: fetchNextPage,
separatorBuilder: (context, index) {
if (index == 0) {
final firstItemDate =
_pagingController.items![1]!.lastTimestamp;
return _dateDivider(firstItemDate);
}
final data = _pagingController.items!;
final data = _pagingController.items!;
final HistoryEntry search = data[index]!;
// Previous in the sense of time, but it is the next item in the list.
final HistoryEntry? previousSearch =
data.length >= index + 1 ? data[index + 1] : null;
final HistoryEntry search = data[index]!;
// Previous in the sense of time, but it is the next item in the list.
final HistoryEntry? previousSearch =
data.length >= index + 1 ? data[index + 1] : null;
if (previousSearch != null &&
!dateIsEqual(
search.lastTimestamp,
previousSearch.lastTimestamp,
)) {
return _dateDivider(previousSearch.lastTimestamp);
}
if (previousSearch != null &&
!dateIsEqual(
search.lastTimestamp,
previousSearch.lastTimestamp,
)) {
return _dateDivider(previousSearch.lastTimestamp);
}
return _divider();
},
builderDelegate: PagedChildBuilderDelegate<HistoryEntry?>(
invisibleItemsThreshold: invisibleItemsThreshold,
itemBuilder: (context, entry, index) => index == 0
? SizedBox.shrink()
: HistoryEntryTile(
entry: entry!,
objectKey: entry.id,
onDelete: () => _pagingController.refresh(),
return _divider();
},
builderDelegate: PagedChildBuilderDelegate<HistoryEntry?>(
invisibleItemsThreshold: invisibleItemsThreshold,
itemBuilder: (context, entry, index) => index == 0
? SizedBox.shrink()
: HistoryEntryTile(
entry: entry!,
objectKey: entry.id,
onDelete: () => _pagingController.refresh(),
),
noItemsFoundIndicatorBuilder: (context) => const Center(
child: Text(
'The history is empty.\nTry searching for something!',
),
noItemsFoundIndicatorBuilder: (context) => const Center(
child: Text(
'The history is empty.\nTry searching for something!',
),
),
),
),
),
),
),
],
@@ -131,9 +126,5 @@ class _HistoryViewState extends State<HistoryView> {
Widget _dateDivider(DateTime date) =>
TextDivider(text: formatDate(roundToDay(date)));
Widget _divider() => const Divider(
height: 0,
indent: 10,
endIndent: 10,
);
Widget _divider() => const Divider(height: 0, indent: 10, endIndent: 10);
}

View File

@@ -49,10 +49,8 @@ class _HomeState extends State<Home> {
}),
items: pages
.map(
(p) => BottomNavigationBarItem(
label: p.titleBar,
icon: p.icon,
),
(p) =>
BottomNavigationBarItem(label: p.titleBar, icon: p.icon),
)
.toList(),
showSelectedLabels: false,
@@ -65,59 +63,61 @@ class _HomeState extends State<Home> {
}
List<_Page> get pages => [
_Page(
content: WordSearchView(),
titleBar: 'Search',
icon: Icon(Icons.search),
actions: [
if (incognitoModeEnabled)
IconButton(
icon: const Icon(Mdi.incognito),
onPressed: () =>
showSnackbar(context, 'History tracking is disabled'),
),
]),
_Page(
content: KanjiSearchView(),
titleBar: 'Kanji Search',
icon: Icon(Mdi.ideogramCjk, size: 30),
actions: [
if (incognitoModeEnabled)
IconButton(
icon: const Icon(Mdi.incognito),
onPressed: () =>
showSnackbar(context, 'History tracking is disabled'),
),
]),
const _Page(
content: HistoryView(),
titleBar: 'History',
icon: Icon(Icons.history),
_Page(
content: WordSearchView(),
titleBar: 'Search',
icon: Icon(Icons.search),
actions: [
if (incognitoModeEnabled)
IconButton(
icon: const Icon(Mdi.incognito),
onPressed: () =>
showSnackbar(context, 'History tracking is disabled'),
),
],
),
_Page(
content: KanjiSearchView(),
titleBar: 'Kanji Search',
icon: Icon(Mdi.ideogramCjk, size: 30),
actions: [
if (incognitoModeEnabled)
IconButton(
icon: const Icon(Mdi.incognito),
onPressed: () =>
showSnackbar(context, 'History tracking is disabled'),
),
],
),
const _Page(
content: HistoryView(),
titleBar: 'History',
icon: Icon(Icons.history),
),
_Page(
content: const LibraryView(),
titleBar: 'Library',
icon: const Icon(Icons.bookmark),
actions: [
IconButton(
onPressed: showNewLibraryDialog(context),
icon: const Icon(Icons.add),
),
_Page(
content: const LibraryView(),
titleBar: 'Library',
icon: const Icon(Icons.bookmark),
actions: [
IconButton(
onPressed: showNewLibraryDialog(context),
icon: const Icon(Icons.add),
)
],
),
const _Page(
content: SettingsView(),
titleBar: 'Settings',
icon: Icon(Icons.settings),
),
if (kDebugMode) ...[
const _Page(
content: DebugView(),
titleBar: 'Debug Page',
icon: Icon(Icons.biotech),
)
],
];
],
),
const _Page(
content: SettingsView(),
titleBar: 'Settings',
icon: Icon(Icons.settings),
),
if (kDebugMode) ...[
const _Page(
content: DebugView(),
titleBar: 'Debug Page',
icon: Icon(Icons.biotech),
),
],
];
}
class _Page {

View File

@@ -11,9 +11,7 @@ class ChangelogView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Changelog'),
),
appBar: AppBar(title: const Text('Changelog')),
body: FutureBuilder<List<String>>(
future: _fetchChangelogs(),
builder: (context, snapshot) {
@@ -31,26 +29,34 @@ class ChangelogView extends StatelessWidget {
}
Future<List<String>> _fetchChangelogs() async {
final String assetManifest =
await rootBundle.loadString('AssetManifest.json');
final String assetManifest = await rootBundle.loadString(
'AssetManifest.json',
);
final List<String> changelogs =
(jsonDecode(assetManifest) as Map<String, Object?>)
.keys
(jsonDecode(assetManifest) as Map<String, Object?>).keys
.where(
(assetPath) =>
RegExp(r'^docs/changelog/v.*\.md$').hasMatch(assetPath),
)
.map((assetPath) => assetPath
.replaceFirst('docs/changelog/', '')
.replaceFirst('.md', ''))
.map(
(assetPath) => assetPath
.replaceFirst('docs/changelog/', '')
.replaceFirst('.md', ''),
)
.toList();
changelogs.sort((a, b) {
final aVersion =
a.replaceFirst(RegExp('^v'), '').split('.').map(int.parse).toList();
final bVersion =
b.replaceFirst(RegExp('^v'), '').split('.').map(int.parse).toList();
final aVersion = a
.replaceFirst(RegExp('^v'), '')
.split('.')
.map(int.parse)
.toList();
final bVersion = b
.replaceFirst(RegExp('^v'), '')
.split('.')
.map(int.parse)
.toList();
for (int i = 0; i < aVersion.length && i < bVersion.length; i++) {
if (aVersion[i] != bVersion[i]) {
return bVersion[i].compareTo(aVersion[i]);
@@ -70,10 +76,7 @@ class ChangelogView extends StatelessWidget {
return ListTile(
title: Text(version),
onTap: () {
Navigator.push(
context,
_buildChangelogDetailRoute(version),
);
Navigator.push(context, _buildChangelogDetailRoute(version));
},
);
},
@@ -89,9 +92,7 @@ class ChangelogView extends StatelessWidget {
MaterialPageRoute _buildChangelogDetailRoute(String version) {
return MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
title: Text(version),
),
appBar: AppBar(title: Text(version)),
body: FutureBuilder<String>(
future: rootBundle.loadString('docs/changelog/$version.md'),
builder: (context, snapshot) {
@@ -103,8 +104,10 @@ class ChangelogView extends StatelessWidget {
}
return SingleChildScrollView(
padding:
const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0),
padding: const EdgeInsets.symmetric(
horizontal: 20.0,
vertical: 20.0,
),
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 100),

View File

@@ -6,36 +6,32 @@ class LicensesView extends StatelessWidget {
@override
Widget build(BuildContext context) => FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final packageInfo = snapshot.data!;
return _buildLicensePage(packageInfo);
},
);
final packageInfo = snapshot.data!;
return _buildLicensePage(packageInfo);
},
);
Widget _buildLicensePage(PackageInfo packageInfo) => LicensePage(
applicationName: '麦典',
applicationVersion: 'Version: ${packageInfo.version}',
applicationIcon: Padding(
padding: const EdgeInsets.symmetric(vertical: 30),
child: Row(
children: [
const Expanded(child: SizedBox()),
Expanded(
child: Image.asset(
'assets/images/logo/mugi.png',
),
),
const Expanded(child: SizedBox()),
],
),
),
);
applicationName: '麦典',
applicationVersion: 'Version: ${packageInfo.version}',
applicationIcon: Padding(
padding: const EdgeInsets.symmetric(vertical: 30),
child: Row(
children: [
const Expanded(child: SizedBox()),
Expanded(child: Image.asset('assets/images/logo/mugi.png')),
const Expanded(child: SizedBox()),
],
),
),
);
}

View File

@@ -23,10 +23,7 @@ class InitializationView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 100),
Image.asset(
'assets/images/logo/mugi.png',
height: 100,
),
Image.asset('assets/images/logo/mugi.png', height: 100),
const SizedBox(height: 20),
BlocBuilder<InitializationCubit, InitializationStatus>(
bloc: cubit,

View File

@@ -12,10 +12,7 @@ const int invisibleItemsThreshold = 25;
class LibraryContentView extends StatefulWidget {
final LibraryList library;
const LibraryContentView({
super.key,
required this.library,
});
const LibraryContentView({super.key, required this.library});
@override
State<LibraryContentView> createState() => _LibraryContentViewState();
@@ -60,9 +57,9 @@ class _LibraryContentViewState extends State<LibraryContentView> {
);
if (!userIsSure) return;
await GetIt.instance
.get<Database>()
.libraryListDeleteAllEntries(widget.library.name);
await GetIt.instance.get<Database>().libraryListDeleteAllEntries(
widget.library.name,
);
_pagingController.refresh();
},
@@ -74,30 +71,25 @@ class _LibraryContentViewState extends State<LibraryContentView> {
controller: _pagingController,
builder: (context, state, fetchNextPage) =>
PagedListView<int, LibraryListEntry>.separated(
state: state,
fetchNextPage: fetchNextPage,
builderDelegate: PagedChildBuilderDelegate<LibraryListEntry>(
invisibleItemsThreshold: invisibleItemsThreshold,
itemBuilder: (context, entry, index) => LibraryListEntryTile(
index: index,
entry: entry,
library: widget.library,
onDelete: () => _pagingController.refresh(),
onUpdate: () => _pagingController.refresh(),
state: state,
fetchNextPage: fetchNextPage,
builderDelegate: PagedChildBuilderDelegate<LibraryListEntry>(
invisibleItemsThreshold: invisibleItemsThreshold,
itemBuilder: (context, entry, index) => LibraryListEntryTile(
index: index,
entry: entry,
library: widget.library,
onDelete: () => _pagingController.refresh(),
onUpdate: () => _pagingController.refresh(),
),
firstPageErrorIndicatorBuilder: (_) =>
ErrorWidget(_pagingController.error!),
noItemsFoundIndicatorBuilder: (_) =>
const Center(child: Text('List is empty')),
),
separatorBuilder: (_, __) =>
const Divider(height: 0, indent: 10, endIndent: 10),
),
firstPageErrorIndicatorBuilder: (_) => ErrorWidget(
_pagingController.error!,
),
noItemsFoundIndicatorBuilder: (_) => const Center(
child: Text('List is empty'),
),
),
separatorBuilder: (_, __) => const Divider(
height: 0,
indent: 10,
endIndent: 10,
),
),
),
);
}

View File

@@ -33,105 +33,106 @@ class _KanjiSearchResultPageState extends State<KanjiSearchResultPage> {
// TODO: add compart link
Widget _headerRow(KanjiSearchResult result) => Container(
margin: const EdgeInsets.fromLTRB(20.0, 20.0, 20.0, 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Flexible(
fit: FlexFit.tight,
child: SizedBox(),
),
Flexible(
fit: FlexFit.tight,
child: Center(child: Header(kanji: result.kanji)),
),
Flexible(
fit: FlexFit.tight,
child: Center(
child: (result.radical != null)
? Radical(radical: result.radical!.symbol)
: const SizedBox(),
),
),
],
margin: const EdgeInsets.fromLTRB(20.0, 20.0, 20.0, 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Flexible(fit: FlexFit.tight, child: SizedBox()),
Flexible(
fit: FlexFit.tight,
child: Center(child: Header(kanji: result.kanji)),
),
);
Flexible(
fit: FlexFit.tight,
child: Center(
child: (result.radical != null)
? Radical(radical: result.radical!.symbol)
: const SizedBox(),
),
),
],
),
);
Widget _rankingColumn(KanjiSearchResult result) => Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Row(
children: [
const Text('JLPT: ', style: TextStyle(fontSize: 20.0)),
JlptLevel(jlptLevel: result.jlptLevel ?? ''),
],
),
Row(
children: [
const Text('Grade: ', style: TextStyle(fontSize: 20.0)),
Grade(
grade: {
1: '1',
2: '2',
3: '3',
4: '4',
5: '小5',
6: '小6',
8: '',
9: '',
10: '',
null: null,
}[result.taughtIn]),
],
),
Row(
children: [
const Text('Rank: ', style: TextStyle(fontSize: 20.0)),
Rank(rank: result.newspaperFrequencyRank),
],
const Text('JLPT: ', style: TextStyle(fontSize: 20.0)),
JlptLevel(jlptLevel: result.jlptLevel ?? ''),
],
),
Row(
children: [
const Text('Grade: ', style: TextStyle(fontSize: 20.0)),
Grade(
grade: {
1: '小1',
2: '小2',
3: '3',
4: '4',
5: '5',
6: '6',
8: '',
9: '',
10: '',
null: null,
}[result.taughtIn],
),
],
);
),
Row(
children: [
const Text('Rank: ', style: TextStyle(fontSize: 20.0)),
Rank(rank: result.newspaperFrequencyRank),
],
),
],
);
String _gifUri(String kanji) {
final String charcode =
kanji.characters.first.codeUnits.map((c) => c.toRadixString(16)).join();
final String charcode = kanji.characters.first.codeUnits
.map((c) => c.toRadixString(16))
.join();
return "https://raw.githubusercontent.com/mistval/kanji_images/master/gifs/$charcode.gif";
}
Widget _body(KanjiSearchResult result) {
return Scaffold(
appBar: AppBar(actions: [
if (incognitoModeEnabled)
appBar: AppBar(
actions: [
if (incognitoModeEnabled)
IconButton(
icon: const Icon(Mdi.incognito),
onPressed: () =>
showSnackbar(context, 'History tracking is disabled'),
),
IconButton(
icon: const Icon(Mdi.incognito),
onPressed: () =>
showSnackbar(context, 'History tracking is disabled'),
icon: const Icon(Icons.star),
color: isFavourite ? Colors.yellow : null,
onPressed: () {
GetIt.instance
.get<Database>()
.libraryListToggleEntry(
"favourites",
jmdictEntryId: null,
kanji: result.kanji,
)
.then((state) => setState(() => isFavourite = state));
},
),
IconButton(
icon: const Icon(Icons.star),
color: isFavourite ? Colors.yellow : null,
onPressed: () {
GetIt.instance
.get<Database>()
.libraryListToggleEntry(
"favourites",
jmdictEntryId: null,
kanji: result.kanji,
)
.then((state) => setState(() => isFavourite = state));
},
),
IconButton(
icon: const Icon(Icons.bookmark),
onPressed: () => showAddToLibraryDialog(
context: context,
jmdictEntryId: null,
kanji: result.kanji,
IconButton(
icon: const Icon(Icons.bookmark),
onPressed: () => showAddToLibraryDialog(
context: context,
jmdictEntryId: null,
kanji: result.kanji,
),
),
),
]),
],
),
body: ListView(
children: [
_headerRow(result),
@@ -168,10 +169,7 @@ class _KanjiSearchResultPageState extends State<KanjiSearchResultPage> {
GetIt.instance
.get<Database>()
.libraryListListContains(
"favourites",
kanji: widget.kanji,
)
.libraryListListContains("favourites", kanji: widget.kanji)
.then((value) => setState(() => isFavourite = value));
if (!incognitoModeEnabled && !addedToDatabase) {

View File

@@ -27,14 +27,14 @@ class _GridItem extends StatelessWidget {
: LightTheme.defaultMenuGreyNormal;
final onTap = isNumber
? () => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(text)),
)
? () => ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(text)))
: () => Navigator.popAndPushNamed(
context,
Routes.kanjiSearch,
arguments: text,
);
context,
Routes.kanjiSearch,
arguments: text,
);
return InkWell(
onTap: onTap,
@@ -57,19 +57,19 @@ class _GridItem extends StatelessWidget {
}
class _KanjiGradeSearchState extends State<KanjiGradeSearch> {
Future<Map<int, Map<int, List<Widget>>>> get gradeWidgets async => compute<
Map<int, Map<int, List<String>>>, Map<int, Map<int, List<Widget>>>>(
Future<Map<int, Map<int, List<Widget>>>> get gradeWidgets async =>
compute<
Map<int, Map<int, List<String>>>,
Map<int, Map<int, List<Widget>>>
>(
(gs) => gs.map(
(grade, sortedByStrokes) => MapEntry(
grade,
sortedByStrokes.map<int, List<Widget>>(
(strokeCount, kanji) => MapEntry(
strokeCount,
[
_GridItem(text: strokeCount.toString(), isNumber: true),
...kanji.map((k) => _GridItem(text: k)),
],
),
(strokeCount, kanji) => MapEntry(strokeCount, [
_GridItem(text: strokeCount.toString(), isNumber: true),
...kanji.map((k) => _GridItem(text: k)),
]),
),
),
),
@@ -77,32 +77,30 @@ class _KanjiGradeSearchState extends State<KanjiGradeSearch> {
);
Future<Widget> get makeGrids async => SingleChildScrollView(
child: Column(
children: (await Future.wait(
JOUYOU_KANJI_BY_GRADE_AND_STROKE_COUNT.keys.map(
(grade) async => ExpansionTile(
title: Text(grade == 7 ? 'Junior Highschool' : 'Grade $grade'),
maintainState: true,
children: [
GridView.count(
crossAxisCount: 6,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
padding: const EdgeInsets.all(10),
children: (await gradeWidgets)[grade]!
.values
.expand((l) => l)
.toList(),
)
],
child: Column(
children: (await Future.wait(
JOUYOU_KANJI_BY_GRADE_AND_STROKE_COUNT.keys.map(
(grade) async => ExpansionTile(
title: Text(grade == 7 ? 'Junior Highschool' : 'Grade $grade'),
maintainState: true,
children: [
GridView.count(
crossAxisCount: 6,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
padding: const EdgeInsets.all(10),
children: (await gradeWidgets)[grade]!.values
.expand((l) => l)
.toList(),
),
),
))
.toList(),
],
),
),
);
)).toList(),
),
);
@override
Widget build(BuildContext context) {

View File

@@ -25,11 +25,11 @@ class _KanjiRadicalSearchState extends State<KanjiRadicalSearch> {
List<String> suggestions = [];
Map<String, bool> radicalToggles = {
for (final String r in RADICALS.values.expand((l) => l)) r: false
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
for (final String r in RADICALS.values.expand((l) => l)) r: true,
};
@override
@@ -43,16 +43,17 @@ class _KanjiRadicalSearchState extends State<KanjiRadicalSearch> {
}
void resetRadicalToggles() => radicalToggles.forEach((k, _) {
radicalToggles[k] = false;
});
radicalToggles[k] = false;
});
void resetAllowedToggles() => allowedToggles.forEach((k, _) {
allowedToggles[k] = true;
});
allowedToggles[k] = true;
});
Future<void> updateSuggestions() async {
final toggledRadicals =
radicalToggles.keys.where((r) => radicalToggles[r] ?? false).toList();
final toggledRadicals = radicalToggles.keys
.where((r) => radicalToggles[r] ?? false)
.toList();
if (toggledRadicals.isEmpty) {
suggestions.clear();
@@ -87,17 +88,17 @@ class _KanjiRadicalSearchState extends State<KanjiRadicalSearch> {
final color = isNumber
? LightTheme.defaultMenuGreyDark
: radicalToggles[radical]!
? AppTheme.mugitenWheat
: LightTheme.defaultMenuGreyNormal;
? AppTheme.mugitenWheat
: LightTheme.defaultMenuGreyNormal;
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();
}),
// 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(
@@ -106,44 +107,38 @@ class _KanjiRadicalSearchState extends State<KanjiRadicalSearch> {
),
child: Text(
radical,
style: TextStyle(
color: color.foreground,
fontSize: fontSize,
),
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.mugitenWheat.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)
];
IconButton(
onPressed: () => setState(() {
suggestions.clear();
resetRadicalToggles();
resetAllowedToggles();
}),
icon: const Icon(Icons.restore),
color: AppTheme.mugitenWheat.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),
];
Widget kanjiGridElement(String kanji) {
const color = LightTheme.defaultMenuGreyNormal;
@@ -161,10 +156,7 @@ class _KanjiRadicalSearchState extends State<KanjiRadicalSearch> {
alignment: Alignment.center,
child: Text(
kanji,
style: TextStyle(
color: color.foreground,
fontSize: fontSize,
),
style: TextStyle(color: color.foreground, fontSize: fontSize),
),
),
);
@@ -196,8 +188,9 @@ class _KanjiRadicalSearchState extends State<KanjiRadicalSearch> {
mainAxisSpacing: 10,
crossAxisSpacing: 10,
padding: const EdgeInsets.all(10),
children:
suggestions.map((s) => kanjiGridElement(s)).toList(),
children: suggestions
.map((s) => kanjiGridElement(s))
.toList(),
),
),
Divider(

View File

@@ -20,10 +20,7 @@ const int invisibleItemsThreshold = 25;
class WordSearchResultPage extends StatefulWidget {
final String searchTerm;
const WordSearchResultPage({
required this.searchTerm,
super.key,
});
const WordSearchResultPage({required this.searchTerm, super.key});
@override
State<WordSearchResultPage> createState() => _WordSearchResultPageState();
@@ -54,9 +51,11 @@ class _WordSearchResultPageState extends State<WordSearchResultPage> {
GetIt.instance
.get<Database>()
.historyEntryInsertWord(widget.searchTerm)
.then((_) => GetIt.instance
.get<Database>()
.historyEntryGetWord(widget.searchTerm))
.then(
(_) => GetIt.instance.get<Database>().historyEntryGetWord(
widget.searchTerm,
),
)
.then(
(entry) => setState(() {
addedToDatabase = true;
@@ -123,10 +122,7 @@ class _WordSearchResultPageState extends State<WordSearchResultPage> {
Center(
child: Text(
'Found $searchCount results for "${widget.searchTerm}"',
style: const TextStyle(
fontSize: 10,
color: Colors.grey,
),
style: const TextStyle(fontSize: 10, color: Colors.grey),
),
),
Expanded(
@@ -134,14 +130,14 @@ class _WordSearchResultPageState extends State<WordSearchResultPage> {
controller: _pagingController,
builder: (context, state, fetchNextPage) =>
PagedListView<int, WordSearchResult>(
state: state,
fetchNextPage: fetchNextPage,
builderDelegate: PagedChildBuilderDelegate(
invisibleItemsThreshold: invisibleItemsThreshold,
itemBuilder: (context, item, index) =>
SearchResultCard(result: item),
),
),
state: state,
fetchNextPage: fetchNextPage,
builderDelegate: PagedChildBuilderDelegate(
invisibleItemsThreshold: invisibleItemsThreshold,
itemBuilder: (context, item, index) =>
SearchResultCard(result: item),
),
),
),
),
],

View File

@@ -33,8 +33,9 @@ class _SettingsViewState extends State<SettingsView> {
bool dataImportIsLoading = false;
Future<void> clearHistory(context) async {
final historyCount =
await GetIt.instance.get<Database>().historyEntryAmount();
final historyCount = await GetIt.instance
.get<Database>()
.historyEntryAmount();
if (!context.mounted) return;
@@ -59,8 +60,9 @@ class _SettingsViewState extends State<SettingsView> {
? WidgetsBinding.instance.window.platformBrightness == Brightness.dark
: darkThemeEnabled;
BlocProvider.of<ThemeBloc>(context)
.add(SetTheme(themeIsDark: newThemeIsDark));
BlocProvider.of<ThemeBloc>(
context,
).add(SetTheme(themeIsDark: newThemeIsDark));
setState(() => autoThemeEnabled = b);
}
@@ -142,209 +144,209 @@ class _SettingsViewState extends State<SettingsView> {
String? title,
}) =>
(context) => Navigator.push<int>(
context,
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(title: title == null ? null : Text(title)),
body: DenshiJishoBackground(
child: ListView.builder(
itemBuilder: (context, i) => ListTile(
title: Text(list[i]),
trailing: (chosen != null && chosen == i)
? const Icon(Icons.check)
: null,
onTap: () => Navigator.pop(context, i),
),
itemCount: list.length,
),
context,
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(title: title == null ? null : Text(title)),
body: DenshiJishoBackground(
child: ListView.builder(
itemBuilder: (context, i) => ListTile(
title: Text(list[i]),
trailing: (chosen != null && chosen == i)
? const Icon(Icons.check)
: null,
onTap: () => Navigator.pop(context, i),
),
itemCount: list.length,
),
),
);
),
),
);
@override
Widget build(BuildContext context) => BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final TextStyle titleTextStyle = TextStyle(
color: state is DarkThemeState
? AppTheme.mugitenWheat.background
: null,
);
builder: (context, state) {
final TextStyle titleTextStyle = TextStyle(
color: state is DarkThemeState
? AppTheme.mugitenWheat.background
: null,
);
return SettingsList(
// backgroundColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(vertical: 10),
sections: <SettingsSection>[
SettingsSection(
title: Text('Dictionary', style: titleTextStyle),
// titleTextStyle: _titleTextStyle,
tiles: <SettingsTile>[
SettingsTile.switchTile(
title: const Text('Romaji mode'),
description: const Text(
'Display romaji instead of kana for word readings',
),
leading: const Icon(Mdi.alphabetical),
onToggle: (b) => setState(() => romajiEnabled = b),
initialValue: romajiEnabled,
// theme: theme,
activeSwitchColor: AppTheme.mugitenWheat.background,
),
SettingsTile(
title: const Text('Japanese font'),
leading: const Icon(Icons.format_size),
onPressed: changeFont,
// theme: theme,
trailing: Text(japaneseFont.name),
// subtitle:
// 'Which font to use for japanese text. This might be useful if your phone shows kanji with a Chinese font.',
// subtitleMaxLines: 3,
),
],
return SettingsList(
// backgroundColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(vertical: 10),
sections: <SettingsSection>[
SettingsSection(
title: Text('Dictionary', style: titleTextStyle),
// titleTextStyle: _titleTextStyle,
tiles: <SettingsTile>[
SettingsTile.switchTile(
title: const Text('Romaji mode'),
description: const Text(
'Display romaji instead of kana for word readings',
),
leading: const Icon(Mdi.alphabetical),
onToggle: (b) => setState(() => romajiEnabled = b),
initialValue: romajiEnabled,
// theme: theme,
activeSwitchColor: AppTheme.mugitenWheat.background,
),
SettingsSection(
title: Text('Theme', style: titleTextStyle),
tiles: <SettingsTile>[
SettingsTile.switchTile(
title: const Text('Automatic theme'),
description:
const Text('Let theme be determined by system'),
leading: const Icon(Icons.brightness_auto),
onToggle: toggleAutoTheme,
initialValue: autoThemeEnabled,
// theme: theme,
activeSwitchColor: AppTheme.mugitenWheat.background,
),
SettingsTile.switchTile(
title: const Text('Dark Theme'),
leading: const Icon(Icons.dark_mode),
onToggle: (b) {
BlocProvider.of<ThemeBloc>(context)
.add(SetTheme(themeIsDark: b));
setState(() => darkThemeEnabled = b);
},
initialValue: darkThemeEnabled,
enabled: !autoThemeEnabled,
// theme: theme,
activeSwitchColor: AppTheme.mugitenWheat.background,
),
],
),
SettingsSection(
title: Text('Data', style: titleTextStyle),
tiles: <SettingsTile>[
SettingsTile(
enabled: true,
leading: const Icon(Icons.file_upload),
title: const Text('Import Data'),
description: const Text('Import user data from a file'),
onPressed: importHandler,
value: dataImportIsLoading
? const LinearProgressIndicator()
: null,
),
SettingsTile(
enabled: true,
leading: const Icon(Icons.file_download),
title: const Text('Export Data'),
description: const Text('Export user data to a file'),
onPressed: exportHandler,
value: dataExportIsLoading
? const LinearProgressIndicator()
: null,
),
SettingsTile(
enabled: true,
leading: const Icon(Icons.delete),
title: const Text(
'Clear History',
style: TextStyle(color: Colors.red),
),
description: const Text('Delete all search history'),
onPressed: clearHistory,
),
],
),
SettingsSection(
title: Text('Misc', style: titleTextStyle),
tiles: <SettingsTile>[
SettingsTile.switchTile(
leading: const Icon(Mdi.incognito),
title: const Text('Disable history tracking'),
description: const Text(
'Useful for reviewing history for library lists without cluttering the order',
),
onToggle: (b) => setState(() => incognitoModeEnabled = b),
initialValue: incognitoModeEnabled,
activeSwitchColor: AppTheme.mugitenWheat.background,
),
SettingsTile.switchTile(
leading: const Icon(Icons.close_fullscreen),
title: const Text('Shrink kanji drawing board'),
description: const Text(
'Useful if you keep accidentally activating system gestures',
),
onToggle: (b) =>
setState(() => reduceKanjiDrawingBoardSize = b),
initialValue: reduceKanjiDrawingBoardSize,
activeSwitchColor: AppTheme.mugitenWheat.background,
),
SettingsTile(
enabled: true,
leading: const Icon(Icons.cached),
title: const Text(
'Reinitialize application',
style: TextStyle(color: Colors.red),
),
description: const Text(
'Reinstall dictionary data and set up internal workings anew',
),
onPressed: (_) async {
if (!await confirm(
context,
content: const Text(
'Are you sure you want to reinitialize the application?',
),
)) {
return;
}
GetIt.instance.get<Database>().close();
GetIt.instance.reset();
runInitializationScreen(true);
},
),
],
),
SettingsSection(
title: Text('Info', style: titleTextStyle),
tiles: <SettingsTile>[
SettingsTile(
leading: const Icon(Icons.copyright),
title: const Text('About'),
description: const Text(
'Information about Mugiten and licenses used'),
onPressed: (c) =>
Navigator.pushNamed(context, Routes.aboutLicenses),
),
SettingsTile(
leading: const Icon(Icons.notes),
title: const Text('Changelog'),
onPressed: (c) =>
Navigator.pushNamed(context, Routes.aboutChangelog),
),
SettingsTile(
leading: const Icon(Mdi.git),
title: const Text('Repository'),
description: const Text('https://git.pvv.ntnu.no/mugiten'),
onPressed: (c) => launchUrl(
Uri.parse('https://git.pvv.ntnu.no/mugiten'),
),
)
],
SettingsTile(
title: const Text('Japanese font'),
leading: const Icon(Icons.format_size),
onPressed: changeFont,
// theme: theme,
trailing: Text(japaneseFont.name),
// subtitle:
// 'Which font to use for japanese text. This might be useful if your phone shows kanji with a Chinese font.',
// subtitleMaxLines: 3,
),
],
);
},
),
SettingsSection(
title: Text('Theme', style: titleTextStyle),
tiles: <SettingsTile>[
SettingsTile.switchTile(
title: const Text('Automatic theme'),
description: const Text('Let theme be determined by system'),
leading: const Icon(Icons.brightness_auto),
onToggle: toggleAutoTheme,
initialValue: autoThemeEnabled,
// theme: theme,
activeSwitchColor: AppTheme.mugitenWheat.background,
),
SettingsTile.switchTile(
title: const Text('Dark Theme'),
leading: const Icon(Icons.dark_mode),
onToggle: (b) {
BlocProvider.of<ThemeBloc>(
context,
).add(SetTheme(themeIsDark: b));
setState(() => darkThemeEnabled = b);
},
initialValue: darkThemeEnabled,
enabled: !autoThemeEnabled,
// theme: theme,
activeSwitchColor: AppTheme.mugitenWheat.background,
),
],
),
SettingsSection(
title: Text('Data', style: titleTextStyle),
tiles: <SettingsTile>[
SettingsTile(
enabled: true,
leading: const Icon(Icons.file_upload),
title: const Text('Import Data'),
description: const Text('Import user data from a file'),
onPressed: importHandler,
value: dataImportIsLoading
? const LinearProgressIndicator()
: null,
),
SettingsTile(
enabled: true,
leading: const Icon(Icons.file_download),
title: const Text('Export Data'),
description: const Text('Export user data to a file'),
onPressed: exportHandler,
value: dataExportIsLoading
? const LinearProgressIndicator()
: null,
),
SettingsTile(
enabled: true,
leading: const Icon(Icons.delete),
title: const Text(
'Clear History',
style: TextStyle(color: Colors.red),
),
description: const Text('Delete all search history'),
onPressed: clearHistory,
),
],
),
SettingsSection(
title: Text('Misc', style: titleTextStyle),
tiles: <SettingsTile>[
SettingsTile.switchTile(
leading: const Icon(Mdi.incognito),
title: const Text('Disable history tracking'),
description: const Text(
'Useful for reviewing history for library lists without cluttering the order',
),
onToggle: (b) => setState(() => incognitoModeEnabled = b),
initialValue: incognitoModeEnabled,
activeSwitchColor: AppTheme.mugitenWheat.background,
),
SettingsTile.switchTile(
leading: const Icon(Icons.close_fullscreen),
title: const Text('Shrink kanji drawing board'),
description: const Text(
'Useful if you keep accidentally activating system gestures',
),
onToggle: (b) =>
setState(() => reduceKanjiDrawingBoardSize = b),
initialValue: reduceKanjiDrawingBoardSize,
activeSwitchColor: AppTheme.mugitenWheat.background,
),
SettingsTile(
enabled: true,
leading: const Icon(Icons.cached),
title: const Text(
'Reinitialize application',
style: TextStyle(color: Colors.red),
),
description: const Text(
'Reinstall dictionary data and set up internal workings anew',
),
onPressed: (_) async {
if (!await confirm(
context,
content: const Text(
'Are you sure you want to reinitialize the application?',
),
)) {
return;
}
GetIt.instance.get<Database>().close();
GetIt.instance.reset();
runInitializationScreen(true);
},
),
],
),
SettingsSection(
title: Text('Info', style: titleTextStyle),
tiles: <SettingsTile>[
SettingsTile(
leading: const Icon(Icons.copyright),
title: const Text('About'),
description: const Text(
'Information about Mugiten and licenses used',
),
onPressed: (c) =>
Navigator.pushNamed(context, Routes.aboutLicenses),
),
SettingsTile(
leading: const Icon(Icons.notes),
title: const Text('Changelog'),
onPressed: (c) =>
Navigator.pushNamed(context, Routes.aboutChangelog),
),
SettingsTile(
leading: const Icon(Mdi.git),
title: const Text('Repository'),
description: const Text('https://git.pvv.ntnu.no/mugiten'),
onPressed: (c) =>
launchUrl(Uri.parse('https://git.pvv.ntnu.no/mugiten')),
),
],
),
],
);
},
);
}

View File

@@ -25,41 +25,29 @@ Future<Directory> tmpdir() async =>
Future<Directory> unpackZipToTempDir(String zipFilePath) async {
final outputDir = await tmpdir();
await extractFileToDisk(
zipFilePath,
outputDir.path,
);
await extractFileToDisk(zipFilePath, outputDir.path);
return outputDir;
}
Future<File> packZip(
Directory dir, {
File? outputFile,
}) async {
Future<File> packZip(Directory dir, {File? outputFile}) async {
if (outputFile == null || !outputFile.existsSync()) {
final outputDir = await tmpdir();
outputFile = File(outputDir.uri.resolve('mugiten_data.zip').toFilePath());
outputFile.createSync();
}
final archive = createArchiveFromDirectory(
dir,
includeDirName: false,
);
final archive = createArchiveFromDirectory(dir, includeDirName: false);
final outputStream = OutputFileStream(outputFile.path);
ZipEncoder().encodeStream(
archive,
outputStream,
autoClose: true,
);
ZipEncoder().encodeStream(archive, outputStream, autoClose: true);
return outputFile;
}
String getExportFileNameNoSuffix() {
final DateTime today = DateTime.now();
final String formattedDate = '${today.year}'
final String formattedDate =
'${today.year}'
'.${today.month.toString().padLeft(2, '0')}'
'.${today.day.toString().padLeft(2, '0')}';
@@ -97,15 +85,13 @@ Future<void> importData(Database db, File zipFile) async {
// HISTORY //
/////////////
Future<void> exportHistoryTo(
DatabaseExecutor db,
Directory dir,
) async {
Future<void> exportHistoryTo(DatabaseExecutor db, Directory dir) async {
final file = dir.historyFile;
file.createSync();
final List<Map<String, Object?>> jsonEntries =
(await db.historyEntryGetAll()).map((e) => e.toJson()).toList();
final List<Map<String, Object?>> jsonEntries = (await db.historyEntryGetAll())
.map((e) => e.toJson())
.toList();
file.writeAsStringSync(jsonEncode(jsonEntries));
}
@@ -123,14 +109,10 @@ Future<void> importHistoryFrom(Database db, File file) async {
// LIBRARY LISTS //
///////////////////
Future<void> exportLibraryListsTo(
DatabaseExecutor db,
Directory dir,
) async {
final libraryNames = await db.query(
LibraryListTableNames.libraryList,
columns: ['name'],
).then((result) => result.map((row) => row['name'] as String).toList());
Future<void> exportLibraryListsTo(DatabaseExecutor db, Directory dir) async {
final libraryNames = await db
.query(LibraryListTableNames.libraryList, columns: ['name'])
.then((result) => result.map((row) => row['name'] as String).toList());
await Future.wait([
for (final libraryName in libraryNames)
@@ -147,33 +129,39 @@ Future<void> exportLibraryListTo(
await file.create();
// TODO: properly null check
final entries = (await db.libraryListGetListEntries(libraryName))!
.entries
.map((e) => e.toJson())
.toList();
final entries = (await db.libraryListGetListEntries(
libraryName,
))!.entries.map((e) => e.toJson()).toList();
await file.writeAsString(jsonEncode(entries));
}
// TODO: how do we handle lists that already exist? There seems to be no good way to merge them?
Future<void> importLibraryListsFrom(
DatabaseExecutor db, Directory libraryListsDir) async {
DatabaseExecutor db,
Directory libraryListsDir,
) async {
for (final file in libraryListsDir.listSync()) {
if (file is! File) continue;
assert(file.path.endsWith('.json'));
final libraryName =
file.uri.pathSegments.last.replaceFirst(RegExp(r'\.json$'), '');
final libraryName = file.uri.pathSegments.last.replaceFirst(
RegExp(r'\.json$'),
'',
);
if (await db.libraryListExists(libraryName)) {
if ((await db.libraryListGetList(libraryName))!.totalCount > 0) {
print(
'Library list "$libraryName" already exists and is not empty. Skipping import.');
'Library list "$libraryName" already exists and is not empty. Skipping import.',
);
continue;
} else {
print('Library list "$libraryName" already exists but is empty. '
'Importing entries from file ${file.path}.');
print(
'Library list "$libraryName" already exists but is empty. '
'Importing entries from file ${file.path}.',
);
}
} else {
await db.libraryListInsertList(libraryName);

View File

@@ -52,8 +52,9 @@ class InitializationCubit extends Cubit<InitializationStatus> {
await database.close();
tmpdirDataDump =
await dataDump.copy('${tempDir.path}/mugiten_data_backup.zip');
tmpdirDataDump = await dataDump.copy(
'${tempDir.path}/mugiten_data_backup.zip',
);
emit(BackupUserData(total: 2, progress: 2));
}

View File

@@ -45,15 +45,12 @@ Future<void> setupSharedPreferences() async {
GetIt.instance.registerSingleton<SharedPreferences>(prefs);
}
void registerExtraLicenses() => LicenseRegistry.addLicense(
() async* {
final jsonString = await rootBundle.loadString('assets/licenses.json');
final Map<String, dynamic> jsonData = jsonDecode(jsonString);
for (final license in jsonData.entries) {
yield LicenseEntryWithLineBreaks(
[license.key],
await rootBundle.loadString(license.value as String),
);
}
},
);
void registerExtraLicenses() => LicenseRegistry.addLicense(() async* {
final jsonString = await rootBundle.loadString('assets/licenses.json');
final Map<String, dynamic> jsonData = jsonDecode(jsonString);
for (final license in jsonData.entries) {
yield LicenseEntryWithLineBreaks([
license.key,
], await rootBundle.loadString(license.value as String));
}
});

View File

@@ -20,30 +20,21 @@ class BackupUserData extends InitializationStatus {
final int progress;
final int total;
BackupUserData({
required this.progress,
required this.total,
});
BackupUserData({required this.progress, required this.total});
}
class MigrateDatabase extends InitializationStatus {
final int progress;
final int total;
MigrateDatabase({
required this.progress,
required this.total,
});
MigrateDatabase({required this.progress, required this.total});
}
class RestoreUserData extends InitializationStatus {
final int progress;
final int total;
RestoreUserData({
required this.progress,
required this.total,
});
RestoreUserData({required this.progress, required this.total});
}
class DatabaseUpdateFinished extends InitializationStatus {}

View File

@@ -4,12 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
final SharedPreferences _prefs = GetIt.instance.get<SharedPreferences>();
enum JapaneseFont {
none,
droidSansJapanese,
notoSansCJK,
notoSerifCJK,
}
enum JapaneseFont { none, droidSansJapanese, notoSansCJK, notoSerifCJK }
extension Methods on JapaneseFont {
TextStyle get textStyle {

View File

@@ -21,10 +21,12 @@ Future<Database> createDatabaseCopy({
}
// Make a copy of jadbPath
final random_suffix =
Random().nextInt((pow(2, 32) - 1) as int).toRadixString(16);
final jadbCopyPath =
jadbFile.parent.uri.resolve("jadb_copy_$random_suffix.sqlite").path;
final random_suffix = Random()
.nextInt((pow(2, 32) - 1) as int)
.toRadixString(16);
final jadbCopyPath = jadbFile.parent.uri
.resolve("jadb_copy_$random_suffix.sqlite")
.path;
await jadbFile.copy(jadbCopyPath);
@@ -76,10 +78,12 @@ void main() {
throw Exception("JADB_PATH environment variable is not set.");
}
libsqlitePath = File(Platform.environment["LIBSQLITE_PATH"]!)
.resolveSymbolicLinksSync();
jadbPath =
File(Platform.environment["JADB_PATH"]!).resolveSymbolicLinksSync();
libsqlitePath = File(
Platform.environment["LIBSQLITE_PATH"]!,
).resolveSymbolicLinksSync();
jadbPath = File(
Platform.environment["JADB_PATH"]!,
).resolveSymbolicLinksSync();
});
// Setup sqflite_common_ffi for flutter test