treewide: dart format
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -5,8 +5,6 @@ class LoadingScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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')),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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()]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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(() {
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,10 +28,7 @@ class JapaneseHeader extends StatelessWidget {
|
||||
style: japaneseFont.textStyle,
|
||||
)
|
||||
: const Text(''),
|
||||
Text(
|
||||
baseWord,
|
||||
style: japaneseFont.textStyle,
|
||||
),
|
||||
Text(baseWord, style: japaneseFont.textStyle),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(', ')),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(', ');
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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: '麦典',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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')),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user