Compare commits

...

4 Commits

Author SHA1 Message Date
d2d8ea07a6 WIP 2023-02-24 09:55:55 +01:00
ea220e25f5 Rename some badly named pieces
- Saved and SavedLists are now referred to as Library and LibraryLists
- the sql searchword is now just called word
2022-06-05 22:07:30 +02:00
d2a3de4823 Migrate history to SQLite + more
- remove all sembast code
- setup database migration system
- setup data import export system
- remove sembast object tests
- make everything ready for implementing "saved lists" feature
2022-06-05 02:41:11 +02:00
cad62f2b8b Remove test database 2022-06-04 15:53:48 +02:00
81 changed files with 2596 additions and 80036 deletions

View File

@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
compileSdkVersion 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -42,7 +42,6 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "app.jishostudytool.jisho_study_tool"
// minSdkVersion flutter.minSdkVersion
minSdkVersion 19

View File

@@ -1,5 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.jishostudytool.jisho_study_tool">
<application
android:requestLegacyExternalStorage="true"
>
</application>
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->

View File

@@ -1,9 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.jishostudytool.jisho_study_tool">
<application
<application
android:label="Jisho Study Tool"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
android:icon="@mipmap/launcher_icon"
android:requestLegacyExternalStorage="true"
>
<activity
android:name=".MainActivity"
android:exported="true"

View File

@@ -1,16 +1,21 @@
// Generated file.
//
// If you wish to remove Flutter's multidex support, delete this entire file.
//
// Modifications to this file should be done in a copy under a different name
// as this file may be regenerated.
package io.flutter.app;
import android.app.Application;
import android.content.Context;
import androidx.annotation.CallSuper;
import androidx.multidex.MultiDex;
/**
* Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support.
* Extension of {@link android.app.Application}, adding multidex support.
*/
public class FlutterMultiDexApplication extends FlutterApplication {
public class FlutterMultiDexApplication extends Application {
@Override
@CallSuper
protected void attachBaseContext(Context base) {

View File

@@ -0,0 +1,89 @@
CREATE TABLE "JST_LibraryList" (
"name" TEXT PRIMARY KEY NOT NULL,
"prevList" TEXT
UNIQUE
REFERENCES "JST_LibraryList"("name"),
-- The list can't link to itself
CHECK("prevList" != "name"),
-- 'favourites' should always be the first list
CHECK (NOT (("name" = 'favourites') <> ("prevList" IS NULL)))
);
-- This entry should always exist
INSERT INTO "JST_LibraryList"("name") VALUES ('favourites');
-- Useful for the view below
CREATE INDEX "JST_LibraryList_byPrevList" ON "JST_LibraryList"("prevList");
-- A view that sorts the LibraryLists in their custom order.
CREATE VIEW "JST_LibraryListOrdered" AS
WITH RECURSIVE "RecursionTable"("name") AS (
SELECT "name"
FROM "JST_LibraryList" "I"
WHERE "I"."prevList" IS NULL
UNION ALL
SELECT "R"."name"
FROM "JST_LibraryList" "R"
JOIN "RecursionTable" ON
("R"."prevList" = "RecursionTable"."name")
)
SELECT * FROM "RecursionTable";
CREATE TABLE "JST_LibraryListEntry" (
"listName" TEXT NOT NULL REFERENCES "JST_LibraryList"("name") ON DELETE CASCADE,
"entryText" TEXT NOT NULL,
"isKanji" BOOLEAN NOT NULL DEFAULT 0,
-- Defaults to unix timestamp in milliseconds
"lastModified" INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
"prevEntryText" TEXT,
"prevEntryIsKanji" BOOLEAN NOT NULL DEFAULT 0,
PRIMARY KEY ("listName", "entryText", "isKanji"),
FOREIGN KEY ("listName", "prevEntryText", "prevEntryIsKanji")
REFERENCES "JST_LibraryListEntry"("listName", "entryText", "isKanji"),
-- Two entries can not appear directly after the same entry
UNIQUE("listName", "prevEntryText", "prevEntryIsKanji"),
-- The entry can't link to itself
CHECK(NOT ("prevEntryText" == "entryText" AND "prevEntryIsKanji" == "isKanji")),
-- Kanji entries should only have a length of 1
CHECK ((NOT "isKanji") OR ("isKanji" AND length("entryText") = 1))
);
-- Useful when doing the recursive ordering statement
CREATE INDEX "JST_LibraryListEntry_byListNameAndPrevEntry"
ON "JST_LibraryListEntry"("listName", "prevEntryText", "prevEntryIsKanji");
CREATE TABLE "JST_HistoryEntry" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
);
CREATE TABLE "JST_HistoryEntryKanji" (
"entryId" INTEGER NOT NULL REFERENCES "JST_HistoryEntry"("id") ON DELETE CASCADE,
"kanji" CHAR(1) NOT NULL,
PRIMARY KEY ("entryId", "kanji")
);
CREATE TABLE "JST_HistoryEntryWord" (
"entryId" INTEGER NOT NULL REFERENCES "JST_HistoryEntry"("id") ON DELETE CASCADE,
"word" TEXT NOT NULL,
"language" CHAR(1) CHECK ("language" IN ("e", "j")),
PRIMARY KEY ("entryId", "word")
);
CREATE TABLE "JST_HistoryEntryTimestamp" (
"entryId" INTEGER NOT NULL REFERENCES "JST_HistoryEntry"("id") ON DELETE CASCADE,
-- Defaults to unix timestamp in milliseconds
"timestamp" INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
PRIMARY KEY ("entryId", "timestamp")
);
-- Useful when ordering entries by the timestamps
CREATE INDEX "JST_HistoryEntryTimestamp_byTimestamp" ON "JST_HistoryEntryTimestamp"("timestamp");
CREATE VIEW "JST_HistoryEntry_orderedByTimestamp" AS
SELECT * FROM "JST_HistoryEntryTimestamp"
LEFT JOIN "JST_HistoryEntryWord" USING ("entryId")
LEFT JOIN "JST_HistoryEntryKanji" USING ("entryId")
GROUP BY "entryId"
ORDER BY MAX("timestamp") DESC;

87
flake.lock generated
View File

@@ -3,17 +3,17 @@
"android-nixpkgs": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils_2",
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1651782096,
"narHash": "sha256-rrj0HPwmDf6Q14sljnVf2hkMvc97rndgi4PJkFtpFPk=",
"lastModified": 1677183680,
"narHash": "sha256-xPg1gYyZ8UYNWcQYBtvmmbum3l1hx5cFpoWKrJA15DI=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "ccd2a8f58709ea3413fcb72769b2f62a98332215",
"rev": "61410f48b49495f38f835bd79f98c9a0528151dd",
"type": "github"
},
"original": {
@@ -24,15 +24,21 @@
},
"devshell": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
"flake-utils": [
"android-nixpkgs",
"nixpkgs"
],
"nixpkgs": [
"android-nixpkgs",
"nixpkgs"
]
},
"locked": {
"lastModified": 1650900878,
"narHash": "sha256-qhNncMBSa9STnhiLfELEQpYC1L4GrYHNIzyCZ/pilsI=",
"lastModified": 1676293499,
"narHash": "sha256-uIOTlTxvrXxpKeTvwBI1JGDGtCxMXE3BI0LFwoQMhiQ=",
"owner": "numtide",
"repo": "devshell",
"rev": "d97df53b5ddaa1cfbea7cddbd207eb2634304733",
"rev": "71e3022e3ab20bbf1342640547ef5bc14fb43bf4",
"type": "github"
},
"original": {
@@ -43,11 +49,11 @@
},
"flake-utils": {
"locked": {
"lastModified": 1642700792,
"narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=",
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "846b2ae0fc4cc943637d3d1def4454213e203cba",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"type": "github"
},
"original": {
@@ -58,11 +64,11 @@
},
"flake-utils_2": {
"locked": {
"lastModified": 1649676176,
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"type": "github"
},
"original": {
@@ -86,35 +92,20 @@
"type": "github"
}
},
"flake-utils_4": {
"locked": {
"lastModified": 1649676176,
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-dart": {
"inputs": {
"flake-utils": "flake-utils_4",
"flake-utils": "flake-utils_3",
"nixpkgs": [
"nixpkgs"
],
"pub2nix": "pub2nix"
},
"locked": {
"lastModified": 1651781526,
"narHash": "sha256-q01e+S69g4UDrMcEitaQOccr2aHeiJ+VEmPS94h/7WY=",
"lastModified": 1652213615,
"narHash": "sha256-+eehm2JlhoKgY+Ea4DTxDMei/x4Fgz7S+ZPqWpZysuI=",
"owner": "tadfisher",
"repo": "nix-dart",
"rev": "71d2fda0f9590d5de917fb736dee312d9fef7e27",
"rev": "6f686ddf984306d944e9b5adf9f35f3a0a0a70b7",
"type": "github"
},
"original": {
@@ -125,32 +116,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1643381941,
"narHash": "sha256-pHTwvnN4tTsEKkWlXQ8JMY423epos8wUOhthpwJjtpc=",
"lastModified": 1677075010,
"narHash": "sha256-X+UmR1AkdR//lPVcShmLy8p1n857IGf7y+cyCArp8bU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5efc8ca954272c4376ac929f4c5ffefcc20551d5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1651743098,
"narHash": "sha256-NuQNu6yHh54li0kZffM59FRC5bWCJusygL4Cy+3O0fY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d4191fe35cbe52f755ef73009d4d37b9e002efa2",
"rev": "c95bf18beba4290af25c60cbaaceea1110d0f727",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-21.11",
"ref": "nixos-22.11",
"type": "indirect"
}
},
@@ -173,9 +148,9 @@
"root": {
"inputs": {
"android-nixpkgs": "android-nixpkgs",
"flake-utils": "flake-utils_3",
"flake-utils": "flake-utils_2",
"nix-dart": "nix-dart",
"nixpkgs": "nixpkgs_2"
"nixpkgs": "nixpkgs"
}
}
},

View File

@@ -2,12 +2,8 @@
description = "A dictionary app for studying japanese";
inputs = {
nixpkgs.url = "nixpkgs/nixos-21.11";
flake-utils = {
url = "github:numtide/flake-utils";
inputs.nixpkgs.follows = "nixpkgs";
};
nixpkgs.url = "nixpkgs/nixos-22.11";
flake-utils.url = "github:numtide/flake-utils";
android-nixpkgs = {
url = "github:tadfisher/android-nixpkgs";
@@ -18,11 +14,6 @@
url = "github:tadfisher/nix-dart";
inputs.nixpkgs.follows = "nixpkgs";
};
# nix-flutter = {
# url = "path:/home/h7x4/git/flutter_linux_2.5.1-stable/flutter";
# inputs.nixpkgs.follows = "nixpkgs";
# };
};
outputs = { self, nixpkgs, flake-utils, android-nixpkgs, nix-dart }:
@@ -35,68 +26,37 @@
allowUnfree = true;
};
};
dartVersion = "2.14.2";
dartChannel = "stable";
flutterVersion = "2.5.1";
flutterChannel = "stable";
in {
packages.${system} = {
android-sdk = android-nixpkgs.sdk.${system} (sdkPkgs: with sdkPkgs; [
cmdline-tools-latest
build-tools-33-0-0
build-tools-32-0-0
build-tools-31-0-0
build-tools-30-0-2
build-tools-29-0-2
platform-tools
platforms-android-33
platforms-android-32
platforms-android-31
platforms-android-30
platforms-android-29
emulator
]);
# dart = nix-dart.packages.${system}.dart;
dart = (pkgs.callPackage ./nix/dart.nix {});
inherit (pkgs.callPackage ./nix/flutter.nix { inherit (self.packages.${system}) dart; }) flutter;
# pub2nix-lock = nix-dart.packages.${system}.pub2nix-lock;
};
# apps.${system} = {
# web-debug = {
# type = "app";
# program = "";
# };
# web-release = {
# type = "app";
# program = "";
# };
# apk-debug = {
# type = "app";
# program = "";
# };
# apk-release = {
# type = "app";
# program = "${self.packages.${system}.flutter}/bin/flutter run --release";
# };
# default = self.apps.${system}.apk-debug;
# };
devShell.${system} = let
inherit (pkgs) lcov google-chrome sqlite sqlite-web;
inherit (self.packages.${system}) android-sdk flutter dart;
inherit (pkgs) lcov google-chrome sqlite sqlite-web flutter dart;
jdk = pkgs.jdk11;
inherit (self.packages.${system}) android-sdk;
inherit (nix-dart.packages.${system}) pub2nix-lock;
java = pkgs.jdk8;
in pkgs.mkShell rec {
ANDROID_JAVA_HOME="${java.home}";
ANDROID_JAVA_HOME="${jdk.home}";
ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk";
CHROME_EXECUTABLE = "${google-chrome}/bin/google-chrome-stable";
FLUTTER_SDK="${flutter}";
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/32.0.0/aapt2";
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/33.0.0/aapt2";
JAVA_HOME="${ANDROID_JAVA_HOME}";
USE_CCACHE=0;
@@ -105,7 +65,7 @@
dart
flutter
google-chrome
java
jdk
lcov
pub2nix-lock
sqlite

View File

@@ -20,7 +20,7 @@ class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
);
final bool autoThemeIsDark =
SchedulerBinding.instance!.window.platformBrightness == Brightness.dark;
SchedulerBinding.instance?.window.platformBrightness == Brightness.dark;
add(
SetTheme(

View File

@@ -0,0 +1,184 @@
import 'dart:math';
import 'package:flutter/material.dart';
import '../../bloc/theme/theme_bloc.dart';
import '../../settings.dart';
// TODO: Check that it looks right in
// - saved
/// The ratio is defined as 'the amount of space the text should take'
/// divided by 'the amount of space the padding should take'.
///
/// So if the KanjiBox should span 50 pixels, and you wanted 10 of those pixels
/// to be used for padding (5 on each side), and 40 to be used for the text,
/// you could write:
///
/// ```dart
/// KanjiBox.withRatioAndFontSize({
/// kanji: '例',
/// ratio: 4 / 1,
/// fontSize: 40,
/// })
/// ```
///
class KanjiBox extends StatelessWidget {
final String kanji;
final double? fontSize;
final double? padding;
final Color? foreground;
final Color? background;
final double? contentPaddingRatio;
final double borderRadius;
static const double defaultRatio = 3 / 1;
static const double defaultBorderRadius = 10;
double get ratio => contentPaddingRatio ?? fontSize! / padding!;
double get fontSizeFactor => ratio / (ratio + 1);
double get paddingSizeFactor => 1 / (ratio + 1);
bool get isExpanded => contentPaddingRatio != null;
double? get size => isExpanded ? null : fontSize! + (2 * padding!);
double? get oneSidePadding => padding != null ? padding! / 2 : null;
const KanjiBox._({
Key? key,
required this.kanji,
this.fontSize,
this.padding,
this.contentPaddingRatio,
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',
),
super(key: key);
const factory KanjiBox.withFontSizeAndPadding({
required String kanji,
required double fontSize,
required double padding,
Color? foreground,
Color? background,
double borderRadius,
}) = KanjiBox._;
factory KanjiBox.withFontSize({
required String kanji,
required double fontSize,
double ratio = defaultRatio,
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,
);
factory KanjiBox.withPadding({
required String kanji,
double ratio = defaultRatio,
required double padding,
Color? foreground,
Color? background,
double borderRadius = defaultBorderRadius,
}) =>
KanjiBox._(
kanji: kanji,
fontSize: ratio * padding,
padding: padding,
foreground: foreground,
background: background,
borderRadius: borderRadius,
);
factory KanjiBox.expanded({
required String kanji,
double ratio = defaultRatio,
Color? foreground,
Color? background,
double borderRadius = defaultBorderRadius,
}) =>
KanjiBox._(
kanji: kanji,
contentPaddingRatio: ratio,
foreground: foreground,
background: background,
borderRadius: borderRadius,
);
/// A shortcut
factory KanjiBox.headline4({
required BuildContext context,
required String kanji,
double ratio = defaultRatio,
Color? foreground,
Color? background,
double borderRadius = defaultBorderRadius,
}) =>
KanjiBox.withFontSize(
kanji: kanji,
fontSize: Theme.of(context).textTheme.headline4!.fontSize!,
ratio: ratio,
foreground: foreground,
background: background,
borderRadius: borderRadius,
);
@override
Widget build(BuildContext context) {
return BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final calculatedForeground =
foreground ?? state.theme.menuGreyLight.foreground;
final calculatedBackground =
background ?? state.theme.menuGreyLight.background;
return LayoutBuilder(
builder: (context, constraints) {
final sizeConstraint =
min(constraints.maxHeight, constraints.maxWidth);
final calculatedFontSize =
fontSize ?? sizeConstraint * fontSizeFactor;
final calculatedPadding =
oneSidePadding ?? (sizeConstraint * paddingSizeFactor) / 2;
return Container(
padding: EdgeInsets.all(calculatedPadding),
alignment: Alignment.center,
width: size,
height: size,
decoration: BoxDecoration(
color: calculatedBackground,
borderRadius: BorderRadius.circular(borderRadius),
),
child: FittedBox(
child: Text(
kanji,
textScaleFactor: 1,
style: TextStyle(
color: calculatedForeground,
fontSize: calculatedFontSize,
).merge(japaneseFont.textStyle),
),
),
);
},
);
},
);
}
}

View File

@@ -3,6 +3,7 @@ import 'package:signature/signature.dart';
import '../../bloc/theme/theme_bloc.dart';
import '../../services/handwriting.dart';
import '../../services/snackbar.dart';
import '../../settings.dart';
class DrawingBoard extends StatefulWidget {
@@ -182,10 +183,9 @@ class _DrawingBoardState extends State<DrawingBoard> {
),
if (!widget.onlyOneCharacterSuggestions)
IconButton(
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('TODO: implement scrolling page feature!'),
),
onPressed: () => showSnackbar(
context,
'TODO: implement scrolling page feature!',
),
icon: const Icon(Icons.arrow_forward),
),

View File

@@ -1,51 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import '../../models/history/search.dart';
import '../../models/history/history_entry.dart';
import '../../routing/routes.dart';
import '../../services/datetime.dart';
import '../../settings.dart';
import 'kanji_box.dart';
import '../common/kanji_box.dart';
import '../common/loading.dart';
class SearchItem extends StatelessWidget {
final Search search;
// final Widget search;
class HistoryEntryTile extends StatelessWidget {
final HistoryEntry entry;
final int objectKey;
final void Function()? onDelete;
final void Function()? onFavourite;
const SearchItem({
required this.search,
const HistoryEntryTile({
required this.entry,
required this.objectKey,
this.onDelete,
this.onFavourite,
Key? key,
}) : super(key: key);
Widget get _child => (search.isKanji)
? KanjiBox(kanji: search.kanjiQuery!.kanji)
: Text(search.wordQuery!.query);
void Function() _onTap(context) => search.isKanji
void Function() _onTap(context) => entry.isKanji
? () => Navigator.pushNamed(
context,
Routes.kanjiSearch,
arguments: search.kanjiQuery!.kanji,
arguments: entry.kanji,
)
: () => Navigator.pushNamed(
context,
Routes.search,
arguments: search.wordQuery!.query,
arguments: entry.word,
);
MaterialPageRoute get timestamps => MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(title: const Text('Last searched')),
body: ListView(
children: [
for (final ts in search.timestamps.reversed)
ListTile(title: Text('${formatDate(ts)} ${formatTime(ts)}'))
],
body: FutureBuilder<List<DateTime>>(
future: entry.timestamps,
builder: (context, snapshot) {
// TODO: provide proper error handling
if (snapshot.hasError) return ErrorWidget(snapshot.error!);
if (!snapshot.hasData) return const LoadingScreen();
return ListView(
children: snapshot.data!
.map(
(ts) => ListTile(
title: Text('${formatDate(ts)} ${formatTime(ts)}'),
),
)
.toList(),
);
},
),
),
);
@@ -56,22 +63,11 @@ class SearchItem extends StatelessWidget {
icon: Icons.access_time,
onPressed: (_) => Navigator.push(context, timestamps),
),
SlidableAction(
backgroundColor: Colors.yellow,
icon: Icons.star,
onPressed: (_) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('TODO: implement favourites')),
);
onFavourite?.call();
},
),
SlidableAction(
backgroundColor: Colors.red,
icon: Icons.delete,
onPressed: (_) {
final Database db = GetIt.instance.get<Database>();
Search.store.record(objectKey).delete(db);
onPressed: (_) async {
await entry.delete();
onDelete?.call();
},
),
@@ -93,11 +89,16 @@ class SearchItem extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(formatTime(search.timestamp)),
child: Text(formatTime(entry.lastTimestamp)),
),
DefaultTextStyle.merge(
style: japaneseFont.textStyle,
child: _child,
child: entry.isKanji
? KanjiBox.headline4(
context: context,
kanji: entry.kanji!,
)
: Expanded(child: Text(entry.word!)),
),
],
),

View File

@@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
import '../../bloc/theme/theme_bloc.dart';
class KanjiBox extends StatelessWidget {
final String kanji;
const KanjiBox({
Key? key,
required this.kanji,
}) : super(key: key);
@override
Widget build(BuildContext context) => IntrinsicHeight(
child: AspectRatio(
aspectRatio: 1,
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final colors = state.theme.menuGreyLight;
return Container(
padding: const EdgeInsets.all(5),
alignment: Alignment.center,
decoration: BoxDecoration(
color: colors.background,
borderRadius: BorderRadius.circular(10.0),
),
child: FittedBox(
child: Text(
kanji,
style: TextStyle(
color: colors.foreground,
fontSize: 25,
),
),
),
);
},
),
),
);
}

View File

@@ -3,13 +3,14 @@ import 'package:unofficial_jisho_api/api.dart' as jisho;
import './kanji_result_body/examples.dart';
import './kanji_result_body/grade.dart';
import './kanji_result_body/header.dart';
import './kanji_result_body/jlpt_level.dart';
import './kanji_result_body/radical.dart';
import './kanji_result_body/rank.dart';
import './kanji_result_body/stroke_order_gif.dart';
import './kanji_result_body/yomi_chips.dart';
import '../../bloc/theme/theme_bloc.dart';
import '../../services/kanji_grade_conversion.dart';
import '../common/kanji_box.dart';
class KanjiResultBody extends StatelessWidget {
late final String query;
@@ -36,9 +37,22 @@ class KanjiResultBody extends StatelessWidget {
child: SizedBox(),
),
Flexible(
fit: FlexFit.tight,
child: Center(child: Header(kanji: query)),
),
child: AspectRatio(
aspectRatio: 1,
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
final colors = state.theme.kanjiResultColor;
return KanjiBox.expanded(
kanji: query,
ratio: 40,
foreground: colors.foreground,
background: colors.background,
);
},
),
),
),
Flexible(
fit: FlexFit.tight,
child: Center(
@@ -81,6 +95,7 @@ class KanjiResultBody extends StatelessWidget {
return ListView(
children: [
headerRow,
// TODO: handle case where meaning is empty. See 牃 for example
YomiChips(yomi: resultData.meaning.split(', '), type: YomiType.meaning),
(resultData.onyomi.isNotEmpty)
? YomiChips(yomi: resultData.onyomi, type: YomiType.onyomi)
@@ -101,6 +116,7 @@ class KanjiResultBody extends StatelessWidget {
onyomi: resultData.onyomiExamples,
kunyomi: resultData.kunyomiExamples,
),
// TODO: Add unicode information
],
);
}

View File

@@ -1,36 +0,0 @@
import 'package:flutter/material.dart';
import '../../../bloc/theme/theme_bloc.dart';
import '../../../settings.dart';
class Header extends StatelessWidget {
final String kanji;
const Header({
required this.kanji,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) => AspectRatio(
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),
),
);
},
),
);
}

View File

@@ -19,7 +19,11 @@ class Radical extends StatelessWidget {
final colors = state.theme.kanjiResultColor;
return InkWell(
onTap: () => Navigator.pushNamed(context, Routes.kanjiSearchRadicals, arguments: radical.symbol),
onTap: () => Navigator.pushNamed(
context,
Routes.kanjiSearchRadicals,
arguments: radical.symbol,
),
child: Container(
padding: const EdgeInsets.all(15.0),
decoration: BoxDecoration(

View File

@@ -22,6 +22,7 @@ class StrokeOrderGif extends StatelessWidget {
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
// TODO: show some kind of default icon if GIF is missing.
child: Image.network(uri),
),
);

View File

@@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:ruby_text/ruby_text.dart';
import '../../models/library/library_list.dart';
import '../common/kanji_box.dart';
import '../common/loading.dart';
Future<void> showAddToLibraryDialog({
required BuildContext context,
required String entryText,
String? furigana,
bool isKanji = false,
}) =>
showDialog(
context: context,
barrierDismissible: true,
builder: (_) => AddToLibraryDialog(
furigana: furigana,
entryText: entryText,
isKanji: isKanji,
),
);
class AddToLibraryDialog extends StatefulWidget {
final String? furigana;
final String entryText;
final bool isKanji;
const AddToLibraryDialog({
Key? key,
required this.entryText,
required this.isKanji,
this.furigana,
}) : super(key: key);
@override
State<AddToLibraryDialog> createState() => _AddToLibraryDialogState();
}
class _AddToLibraryDialogState extends State<AddToLibraryDialog> {
Map<LibraryList, bool>? librariesContainEntry;
/// A lock to make sure that the local data and the database doesn't
/// get out of sync.
bool toggleLock = false;
@override
void initState() {
super.initState();
LibraryList.allListsContains(
entryText: widget.entryText,
isKanji: widget.isKanji,
).then((data) => setState(() => librariesContainEntry = data));
}
Future<void> toggleEntry({required LibraryList lib}) async {
if (toggleLock) return;
setState(() => toggleLock = true);
await lib.toggleEntry(
entryText: widget.entryText,
isKanji: widget.isKanji,
);
setState(() {
toggleLock = false;
librariesContainEntry![lib] = !librariesContainEntry![lib]!;
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Add to library'),
contentPadding: const EdgeInsets.symmetric(vertical: 24, horizontal: 12),
content: Column(
children: [
ListTile(
title: Center(
child: widget.isKanji
? Row(
children: [
const Expanded(child: SizedBox()),
KanjiBox.headline4(
context: context,
kanji: widget.entryText,
),
const Expanded(child: SizedBox()),
],
)
: RubySpanWidget(
RubyTextData(
widget.entryText,
ruby: widget.furigana,
),
),
),
),
const Divider(thickness: 3),
Expanded(
child: SizedBox(
width: double.maxFinite,
child: librariesContainEntry == null
? const LoadingScreen()
: ListView(
children: librariesContainEntry!.entries.map((e) {
final lib = e.key;
final checked = e.value;
return ListTile(
onTap: () => toggleEntry(lib: lib),
contentPadding:
const EdgeInsets.symmetric(vertical: 5),
title: Row(
children: [
Checkbox(
value: checked,
onChanged: (_) => toggleEntry(lib: lib),
),
Text(lib.name),
],
),
);
}).toList(),
),
),
),
],
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
);
}
}

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import '../../models/library/library_entry.dart';
import '../../models/library/library_list.dart';
import '../../routing/routes.dart';
import '../../settings.dart';
import '../common/kanji_box.dart';
class LibraryListEntryTile extends StatelessWidget {
final int? index;
final LibraryList library;
final LibraryEntry entry;
final void Function()? onDelete;
final void Function()? onUpdate;
const LibraryListEntryTile({
Key? key,
required this.entry,
required this.library,
this.index,
this.onDelete,
this.onUpdate,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Slidable(
endActionPane: ActionPane(
motion: const ScrollMotion(),
children: [
SlidableAction(
backgroundColor: Colors.red,
icon: Icons.delete,
onPressed: (_) async {
await library.deleteEntry(
entryText: entry.entryText,
isKanji: entry.isKanji,
);
onDelete?.call();
},
),
],
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(vertical: 10),
onTap: () async {
await Navigator.pushNamed(
context,
entry.isKanji ? Routes.kanjiSearch : Routes.search,
arguments: entry.entryText,
);
onUpdate?.call();
},
title: Row(
children: [
if (index != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
(index! + 1).toString(),
style: Theme.of(context)
.textTheme
.titleMedium!
.merge(japaneseFont.textStyle),
),
),
entry.isKanji
? KanjiBox.headline4(context: context, kanji: entry.entryText)
: Expanded(child: Text(entry.entryText)),
],
),
),
);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import '../../models/library/library_list.dart';
import '../../routing/routes.dart';
import '../common/loading.dart';
class LibraryListTile extends StatelessWidget {
final Widget? leading;
final LibraryList library;
final void Function()? onDelete;
final void Function()? onUpdate;
final bool isEditable;
const LibraryListTile({
Key? key,
required this.library,
this.leading,
this.onDelete,
this.onUpdate,
this.isEditable = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Slidable(
endActionPane: ActionPane(
motion: const ScrollMotion(),
children: !isEditable
? []
: [
SlidableAction(
backgroundColor: Colors.blue,
icon: Icons.edit,
onPressed: (_) async {
// TODO: update name
onUpdate?.call();
},
),
SlidableAction(
backgroundColor: Colors.red,
icon: Icons.delete,
onPressed: (_) async {
await library.delete();
onDelete?.call();
},
),
],
),
child: ListTile(
leading: leading,
onTap: () => Navigator.pushNamed(
context,
Routes.libraryContent,
arguments: library,
),
title: Row(
children: [
Expanded(child: Text(library.name)),
FutureBuilder<int>(
future: library.length,
builder: (context, snapshot) {
if (snapshot.hasError) return ErrorWidget(snapshot.error!);
if (!snapshot.hasData) return const LoadingScreen();
return Text('${snapshot.data} items');
},
),
],
),
),
);
}
}

View File

@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import '../../models/library/library_list.dart';
void Function() showNewLibraryDialog(context) => () async {
final String? listName = await showDialog<String>(
context: context,
barrierDismissible: true,
builder: (_) => const NewLibraryDialog(),
);
if (listName == null) return;
LibraryList.insert(listName);
};
class NewLibraryDialog extends StatefulWidget {
const NewLibraryDialog({Key? key}) : super(key: key);
@override
State<NewLibraryDialog> createState() => _NewLibraryDialogState();
}
enum _NameState {
initial,
currentlyChecking,
invalid,
alreadyExists,
valid,
}
class _NewLibraryDialogState extends State<NewLibraryDialog> {
final controller = TextEditingController();
_NameState nameState = _NameState.initial;
Future<void> onNameUpdate(proposedListName) async {
setState(() => nameState = _NameState.currentlyChecking);
if (proposedListName == '') {
setState(() => nameState = _NameState.invalid);
return;
}
final nameAlreadyExists = await LibraryList.exists(proposedListName);
if (nameAlreadyExists) {
setState(() => nameState = _NameState.alreadyExists);
} else {
setState(() => nameState = _NameState.valid);
}
}
bool get errorStatus =>
nameState == _NameState.invalid || nameState == _NameState.alreadyExists;
String? get statusLabel => {
_NameState.invalid: 'Invalid Name',
_NameState.alreadyExists: 'Already Exists',
}[nameState];
bool get confirmButtonActive => nameState == _NameState.valid;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Add new library'),
content: TextField(
decoration: InputDecoration(
hintText: 'Library name',
errorText: statusLabel,
),
controller: controller,
onChanged: onNameUpdate,
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
style: confirmButtonActive
? null
: ElevatedButton.styleFrom(
primary: Colors.grey,
),
onPressed: confirmButtonActive
? () => Navigator.pop(context, controller.text)
: () {},
child: const Text('Add'),
),
],
);
}
}

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:ruby_text/ruby_text.dart';
import 'package:unofficial_jisho_api/api.dart';
import '../../../../services/jisho_api/kanji_furigana_separation.dart';
import '../../../../services/romaji_transliteration.dart';
import '../../../../settings.dart';
@@ -25,30 +27,13 @@ class JapaneseHeader extends StatelessWidget {
return Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 10.0),
child: Column(
children: [
// Both wordReading and word.word being present implies that the word has furigana.
// If that's not the case, then the word is usually present in wordReading.
// However, there are some exceptions where the reading is placed in word.
// I have no clue why this might be the case.
hasFurigana
? Text(
wordReading!,
style: romajiEnabled ? null : japaneseFont.textStyle,
)
: const Text(''),
hasFurigana
? Text(
word.word!,
style: japaneseFont.textStyle,
)
: Text(
wordReading ?? word.word!,
style: wordReading != null && romajiEnabled
? null
: japaneseFont.textStyle,
),
],
child: RubySpanWidget(
RubyTextData(
word.kanji,
ruby: word.furigana,
style: romajiEnabled ? null : japaneseFont.textStyle,
rubyStyle: romajiEnabled ? null : japaneseFont.textStyle,
),
),
);
}

View File

@@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import '../../../../bloc/theme/theme_bloc.dart';
import '../../../../routing/routes.dart';
import '../../../../settings.dart';
import '../../../common/kanji_box.dart';
class KanjiRow extends StatelessWidget {
final List<String> kanji;
@@ -13,36 +12,6 @@ class KanjiRow extends StatelessWidget {
this.fontSize = 20,
}) : super(key: key);
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),
),
),
);
},
),
),
),
);
@override
Widget build(BuildContext context) {
return Column(
@@ -64,7 +33,10 @@ class KanjiRow extends StatelessWidget {
Routes.kanjiSearch,
arguments: k,
),
child: _kanjiBox(k),
child: KanjiBox.headline4(
context: context,
kanji: k,
),
)
],
),

View File

@@ -1,18 +1,22 @@
import 'package:flutter/material.dart';
import 'package:jisho_study_tool/services/kanji_regex.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:unofficial_jisho_api/api.dart';
import './parts/common_badge.dart';
import './parts/header.dart';
import './parts/jlpt_badge.dart';
import './parts/other_forms.dart';
import './parts/senses.dart';
import './parts/wanikani_badge.dart';
import '../../../models/library/library_list.dart';
import '../../../services/jisho_api/kanji_furigana_separation.dart';
import '../../../services/kanji_regex.dart';
import '../../../settings.dart';
import '../../library/add_to_library_dialog.dart';
import 'parts/audio_player.dart';
import 'parts/common_badge.dart';
import 'parts/header.dart';
import 'parts/jlpt_badge.dart';
import 'parts/kanji.dart';
import 'parts/links.dart';
import 'parts/notes.dart';
import 'parts/other_forms.dart';
import 'parts/senses.dart';
import 'parts/wanikani_badge.dart';
class SearchResultCard extends StatefulWidget {
final JishoResult result;
@@ -31,20 +35,11 @@ class SearchResultCard extends StatefulWidget {
}
class _SearchResultCardState extends State<SearchResultCard> {
static const _margin = SizedBox(height: 20);
PhrasePageScrapeResultData? extraData;
bool? extraDataSearchFailed;
Future<PhrasePageScrapeResult?> _scrape(JishoResult result) =>
(!(result.japanese[0].word == null && result.japanese[0].reading == null))
? scrapeForPhrase(
widget.result.japanese[0].word ??
widget.result.japanese[0].reading!,
)
: Future(() => null);
List<JishoSenseLink> get links =>
[for (final sense in widget.result.senses) ...sense.links];
bool get hasAttribution =>
widget.result.attribution.jmdict ||
widget.result.attribution.jmnedict ||
@@ -67,30 +62,77 @@ class _SearchResultCardState extends State<SearchResultCard> {
.toSet()
.toList();
Widget get _header => IntrinsicWidth(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
JapaneseHeader(word: widget.mainWord),
Row(
children: [
WKBadge(
level: widget.result.tags.firstWhere(
(tag) => tag.contains('wanikani'),
orElse: () => '',
),
),
JLPTBadge(jlptLevel: jlptLevel),
CommonBadge(isCommon: widget.result.isCommon ?? false)
],
)
],
),
List<JishoSenseLink> get links =>
[for (final sense in widget.result.senses) ...sense.links];
Widget get _header => Row(
children: [
Expanded(child: JapaneseHeader(word: widget.mainWord)),
WKBadge(
level: widget.result.tags.firstWhere(
(tag) => tag.contains('wanikani'),
orElse: () => '',
),
),
JLPTBadge(jlptLevel: jlptLevel),
CommonBadge(isCommon: widget.result.isCommon ?? false)
],
);
static const _margin = SizedBox(height: 20);
@override
Widget build(BuildContext context) {
final backgroundColor = Theme.of(context).scaffoldBackgroundColor;
List<Widget> _withMargin(Widget w) => [_margin, w];
return Slidable(
endActionPane: ActionPane(
motion: const ScrollMotion(),
children: [
SlidableAction(
backgroundColor: Colors.yellow,
icon: Icons.star,
onPressed: (_) => LibraryList.favourites.toggleEntry(
entryText: widget.result.slug,
isKanji: false,
),
),
SlidableAction(
backgroundColor: Colors.blue,
icon: Icons.bookmark,
onPressed: (context) => showAddToLibraryDialog(
context: context,
entryText: widget.result.japanese.first.kanji,
furigana: widget.result.japanese.first.furigana
),
),
],
),
child: ExpansionTile(
collapsedBackgroundColor: backgroundColor,
backgroundColor: backgroundColor,
onExpansionChanged: (b) async {
if (extensiveSearchEnabled && extraData == null) {
final data = await _scrape(widget.result);
setState(() {
extraDataSearchFailed = !(data?.found ?? false);
extraData = !extraDataSearchFailed! ? data!.data : null;
});
}
},
title: _header,
children: [
if (extensiveSearchEnabled && extraDataSearchFailed == null)
const Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Center(child: CircularProgressIndicator()),
)
else if (!extraDataSearchFailed!)
_body(extendedData: extraData)
else
_body()
],
),
);
}
Widget _body({PhrasePageScrapeResultData? extendedData}) => Container(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
@@ -123,34 +165,13 @@ class _SearchResultCardState extends State<SearchResultCard> {
),
);
@override
Widget build(BuildContext context) {
final backgroundColor = Theme.of(context).scaffoldBackgroundColor;
Future<PhrasePageScrapeResult?> _scrape(JishoResult result) =>
(!(result.japanese[0].word == null && result.japanese[0].reading == null))
? scrapeForPhrase(
widget.result.japanese[0].word ??
widget.result.japanese[0].reading!,
)
: Future(() => null);
return ExpansionTile(
collapsedBackgroundColor: backgroundColor,
backgroundColor: backgroundColor,
onExpansionChanged: (b) async {
if (extensiveSearchEnabled && extraData == null) {
final data = await _scrape(widget.result);
setState(() {
extraDataSearchFailed = !(data?.found ?? false);
extraData = !extraDataSearchFailed! ? data!.data : null;
});
}
},
title: _header,
children: [
if (extensiveSearchEnabled && extraDataSearchFailed == null)
const Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Center(child: CircularProgressIndicator()),
)
else if (!extraDataSearchFailed!)
_body(extendedData: extraData)
else
_body()
],
);
}
List<Widget> _withMargin(Widget w) => [_margin, w];
}

View File

@@ -0,0 +1,38 @@
import 'dart:io';
import 'dart:math';
// Example file Structure:
// jisho_data_2022.01.01_1
// - history.json
// - library/
// - lista.json
// - listb.json
extension ArchiveFormat on Directory {
Directory get exportDirectory {
final dir = Directory(uri.resolve('export').path);
dir.createSync(recursive: true);
final DateTime today = DateTime.now();
final String formattedDate = '${today.year}'
'.${today.month.toString().padLeft(2, '0')}'
'.${today.day.toString().padLeft(2, '0')}';
final List<int> takenNumbers = dir
.listSync()
.map((f) => f.uri.pathSegments[f.uri.pathSegments.length - 2])
.where((p) => RegExp('^jisho_data_${formattedDate}_(\\d+)').hasMatch(p))
.map((p) => int.tryParse(p.substring('jisho_data_0000.00.00_'.length)))
.whereType<int>()
.toList();
final int nextNum = takenNumbers.fold(0, max) + 1;
return Directory(
dir.uri.resolve('jisho_data_${formattedDate}_$nextNum').path,
)..createSync();
}
File get historyFile => File(uri.resolve('history.json').path);
Directory get libraryDir => Directory(uri.resolve('library').path);
}

135
lib/data/database.dart Normal file
View File

@@ -0,0 +1,135 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
export 'package:sqflite/sqlite_api.dart';
Database db() => GetIt.instance.get<Database>();
Future<Directory> _databaseDir() async {
final Directory appDocDir = await getApplicationDocumentsDirectory();
if (!appDocDir.existsSync()) appDocDir.createSync(recursive: true);
return appDocDir;
}
Future<String> databasePath() async {
return join((await _databaseDir()).path, 'jisho.sqlite');
}
Future<void> migrate(Database db, int oldVersion, int newVersion) async {
final String assetManifest =
await rootBundle.loadString('AssetManifest.json');
final List<String> migrations =
(jsonDecode(assetManifest) as Map<String, Object?>)
.keys
.where(
(assetPath) =>
assetPath.contains(RegExp(r'migrations\/\d{4}.*\.sql')),
)
.toList();
migrations.sort();
for (int i = oldVersion + 1; i <= newVersion; i++) {
log(
'Migrating database from v$i to v${i + 1} with File(${migrations[i - 1]})',
);
final migrationContent =
await rootBundle.loadString(migrations[i - 1], cache: false);
migrationContent
.split(';')
.map(
(s) => s
.split('\n')
.where((l) => !l.startsWith(RegExp(r'\s*--')))
.join('\n')
.trim(),
)
.where((s) => s != '')
.forEach(db.execute);
}
}
Future<void> setupDatabase() async {
if (kDebugMode) {
databaseFactory.debugSetLogLevel(sqfliteLogLevelSql);
}
final Database database = await openDatabase(
await databasePath(),
version: 1,
onCreate: (db, version) => migrate(db, 0, version),
onUpgrade: migrate,
onOpen: (db) => Future.wait([db.execute('PRAGMA foreign_keys=ON')]),
);
GetIt.instance.registerSingleton<Database>(database);
}
Future<void> resetDatabase() async {
await db().close();
File(await databasePath()).deleteSync();
GetIt.instance.unregister<Database>();
await setupDatabase();
}
class TableNames {
/// Attributes:
/// - id INTEGER
static const String historyEntry = 'JST_HistoryEntry';
/// Attributes:
/// - entryId INTEGER
/// - kanji CHAR(1)
static const String historyEntryKanji = 'JST_HistoryEntryKanji';
/// Attributes:
/// - entryId INTEGER
/// - timestamp INTEGER
static const String historyEntryTimestamp = 'JST_HistoryEntryTimestamp';
/// Attributes:
/// - entryId INTEGER
/// - word TEXT
/// - language CHAR(1)?
static const String historyEntryWord = 'JST_HistoryEntryWord';
/// Attributes:
/// - name TEXT
/// - prevList TEXT
static const String libraryList = 'JST_LibraryList';
/// Attributes:
/// - listName TEXT
/// - entryText TEXT
/// - isKanji BOOLEAN
/// - lastModified TIMESTAMP
/// - prevEntryText TEXT
/// - prevEntryIsKanji BOOLEAN
static const String libraryListEntry = 'JST_LibraryListEntry';
///////////
// VIEWS //
///////////
/// Attributes:
/// - name TEXT
static const String libraryListOrdered = 'JST_LibraryListOrdered';
/// Attributes:
/// - entryId INTEGER
/// - timestamp INTEGER
/// - word TEXT?
/// - kanji CHAR(1)?
/// - language CHAR(1)?
static const String historyEntryOrderedByTimestamp =
'JST_HistoryEntry_orderedByTimestamp';
}

View File

@@ -0,0 +1,74 @@
abstract class DatabaseError implements ArgumentError {
final String? tableName;
final Map<String, dynamic>? illegalArguments;
const DatabaseError({
this.tableName,
this.illegalArguments,
});
@override
dynamic get invalidValue => illegalArguments;
@override
StackTrace? get stackTrace => null;
}
class DataAlreadyExistsError extends DatabaseError {
const DataAlreadyExistsError({
String? tableName,
Map<String, dynamic>? illegalArguments,
}) : super(
tableName: tableName,
illegalArguments: illegalArguments,
);
@override
String? get name => illegalArguments?.keys.join(', ');
String get _inTableName => tableName != null ? ' in "$tableName"' : '';
String get _invalidArgs => illegalArguments != null ? ': ($name)' : '';
@override
String get message => 'Data already exists$_inTableName$_invalidArgs';
}
class DataNotFoundError extends DatabaseError {
const DataNotFoundError({
String? tableName,
Map<String, dynamic>? illegalArguments,
}) : super(
tableName: tableName,
illegalArguments: illegalArguments,
);
@override
String? get name => illegalArguments?.keys.join(', ');
String get _inTableName => tableName != null ? ' in "$tableName"' : '';
String get _invalidArgs => illegalArguments != null ? ': ($name)' : '';
@override
String get message => 'Data not found$_inTableName$_invalidArgs';
}
class IllegalDeletionError extends DatabaseError {
const IllegalDeletionError({
String? tableName,
Map<String, dynamic>? illegalArguments,
}) : super(
tableName: tableName,
illegalArguments: illegalArguments,
);
@override
String? get name => illegalArguments?.keys.join(', ');
String get _fromTableName => tableName != null ? ' from "$tableName"' : '';
String get _args => illegalArguments != null ? '($name)' : '';
@override
String get message => 'Deleting $_args$_fromTableName is not allowed.';
}
// class IllegalInsertionError extends DatabaseError {}

50
lib/data/export.dart Normal file
View File

@@ -0,0 +1,50 @@
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import '../models/history/history_entry.dart';
import '../models/library/library_list.dart';
import 'archive_format.dart';
import 'database.dart';
Future<Directory> exportDirectory() async =>
(await getExternalStorageDirectory())!.exportDirectory;
/// Returns the path to which the data was saved.
Future<String> exportData() async {
final dir = await exportDirectory();
final libraryDir = dir.libraryDir;
libraryDir.createSync();
await Future.wait([
exportHistoryTo(dir),
exportLibraryListsTo(libraryDir),
]);
return dir.path;
}
Future<void> exportHistoryTo(Directory dir) async {
final file = File(dir.uri.resolve('history.json').path);
file.createSync();
final query = await db().query(TableNames.historyEntryOrderedByTimestamp);
final List<HistoryEntry> entries =
query.map((e) => HistoryEntry.fromDBMap(e)).toList();
/// TODO: This creates a ton of sql statements. Ideally, the whole export
/// should be done in only one query.
///
/// On second thought, is that even possible? It's a doubly nested list structure.
final List<Map<String, Object?>> jsonEntries =
await Future.wait(entries.map((he) async => he.toJson()));
file.writeAsStringSync(jsonEncode(jsonEntries));
}
Future<void> exportLibraryListsTo(Directory dir) async => Future.wait(
(await LibraryList.allLibraries).map((lib) async {
final file = File(dir.uri.resolve('${lib.name}.json').path);
file.createSync();
final entries = await lib.entries;
file.writeAsStringSync(jsonEncode(entries));
}),
);

26
lib/data/import.dart Normal file
View File

@@ -0,0 +1,26 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import '../models/history/history_entry.dart';
import 'archive_format.dart';
Future<void> importData(Directory dir) async {
await Future.wait([
importHistoryFrom(dir.historyFile),
importLibraryListsFrom(dir.libraryDir),
]);
}
Future<void> importHistoryFrom(File file) async {
final String content = file.readAsStringSync();
final List<Map<String, Object?>> json = (jsonDecode(content) as List)
.map((h) => h as Map<String, Object?>)
.toList();
log('Importing ${json.length} entries from ${file.path}');
await HistoryEntry.insertJsonEntries(json);
}
Future<void> importLibraryListsFrom(Directory libraryListsDir) async {
print('TODO: Implement importLibraryLists');
}

View File

@@ -2,8 +2,8 @@
import 'package:flutter/material.dart';
import 'bloc/theme/theme_bloc.dart';
import 'data/database.dart';
import 'routing/router.dart';
import 'services/database.dart';
import 'services/licenses.dart';
import 'services/preferences.dart';
import 'settings.dart';

View File

@@ -1,6 +0,0 @@
test.db
data/jisho
data/radkfile
data/kradfile
data/0002_radicals.json
!data/jouyou

View File

@@ -1,295 +0,0 @@
-- KANJI
CREATE TABLE Kanji_YomiExample (
exampleID INTEGER PRIMARY KEY AUTOINCREMENT,
example TEXT NOT NULL,
reading TEXT NOT NULL,
meaning TEXT NOT NULL,
UNIQUE (example, reading, meaning)
);
CREATE TABLE Kanji_Radical (
id INTEGER NOT NULL UNIQUE,
symbol CHAR(1) NOT NULL PRIMARY KEY,
strokes INTEGER NOT NULL,
meaning TEXT NOT NULL,
searchSymbol CHAR(1)
);
CREATE TABLE Kanji_Radical_Forms (
form CHAR(1) NOT NULL PRIMARY KEY,
radical CHAR(1) NOT NULL,
FOREIGN KEY(radical) REFERENCES Kanji_Radical(symbol)
);
CREATE TABLE Kanji_Kunyomi (
yomi TEXT NOT NULL PRIMARY KEY
);
CREATE TABLE Kanji_Onyomi (
yomi TEXT NOT NULL PRIMARY KEY
);
CREATE TABLE Kanji_Part (
part CHAR(1) NOT NULL PRIMARY KEY
-- FOREIGN KEY(part) REFERENCES Kanji_Radical(symbol)
);
CREATE TABLE Kanji_Result (
kanji CHAR(1) PRIMARY KEY,
taughtIn INTEGER CHECK (taughtIn BETWEEN 1 AND 7),
jlptLevel INTEGER CHECK (jlptLevel BETWEEN 1 AND 5),
newspaperFrequencyRank INTEGER,
strokeCount INTEGER NOT NULL,
meaning INTEGER NOT NULL,
radical CHAR(1) NOT NULL,
isJouyou BOOLEAN NOT NULL DEFAULT false,
FOREIGN KEY (radical) REFERENCES Kanji_Radical(symbol)
);
CREATE TABLE Kanji_ResultKunyomiExample_XRef (
exampleID INTEGER NOT NULL,
kanji CHAR(1) NOT NULL,
FOREIGN KEY(exampleID) REFERENCES Kanji_YomiExample(exampleID),
FOREIGN KEY(kanji) REFERENCES Kanji_Result(kanji),
PRIMARY KEY(exampleID, kanji)
);
CREATE TABLE Kanji_ResultOnyomiExample_XRef (
exampleID INTEGER NOT NULL,
kanji CHAR(1) NOT NULL,
FOREIGN KEY(exampleID) REFERENCES Kanji_YomiExample(exampleID),
FOREIGN KEY(kanji) REFERENCES Kanji_Result(kanji),
PRIMARY KEY(exampleID, kanji)
);
CREATE TABLE Kanji_ResultKunyomi_XRef (
yomi TEXT NOT NULL,
kanji CHAR(1) NOT NULL,
FOREIGN KEY(yomi) REFERENCES Kanji_Kunyomi(yomi),
FOREIGN KEY(kanji) REFERENCES Kanji_Result(kanji),
PRIMARY KEY(yomi, kanji)
);
CREATE TABLE Kanji_ResultOnyomi_XRef (
yomi TEXT NOT NULL,
kanji CHAR(1) NOT NULL,
FOREIGN KEY(yomi) REFERENCES Kanji_Onyomi(yomi),
FOREIGN KEY(kanji) REFERENCES Kanji_Result(kanji),
PRIMARY KEY(yomi, kanji)
);
CREATE TABLE Kanji_ResultPart_XRef (
part CHAR(1) NOT NULL,
kanji CHAR(1) NOT NULL,
FOREIGN KEY(part) REFERENCES Kanji_Part(part),
FOREIGN KEY(kanji) REFERENCES Kanji_Result(kanji),
PRIMARY KEY(part, kanji)
);
-- RADKFILE
CREATE TABLE RADKFILE (
kanji CHAR(1) NOT NULL,
radical CHAR(1) NOT NULL,
FOREIGN KEY(radical) REFERENCES Kanji_Radical(symbol)
);
CREATE INDEX RADK ON RADKFILE (radical);
CREATE INDEX KRAD ON RADKFILE (kanji);
-- Example Sentence
CREATE TABLE ExampleSentence_Result (
resultID INTEGER PRIMARY KEY AUTOINCREMENT,
kanji TEXT NOT NULL,
kana TEXT NOT NULL,
english TEXT NOT NULL
);
CREATE TABLE ExampleSentence_Piece (
orderNum INTEGER NOT NULL,
lifted TEXT,
unlifted TEXT NOT NULL,
resultID INTEGER NOT NULL,
FOREIGN KEY(resultID) REFERENCES ExampleSentence_Result(resultID),
PRIMARY KEY(resultID, orderNum)
);
-- Words
CREATE TABLE PhraseScrape_Result (
uri TEXT NOT NULL PRIMARY KEY
);
CREATE TABLE PhraseScrape_Sentence (
english TEXT NOT NULL,
japanese TEXT NOT NULL,
PRIMARY KEY (english, japanese)
);
CREATE TABLE PhraseScrape_Sentence_Piece (
orderNum INTEGER NOT NULL,
lifted TEXT,
unlifted TEXT NOT NULL,
sentenceEnglish TEXT NOT NULL,
sentenceJapanese TEXT NOT NULL,
FOREIGN KEY(sentenceEnglish, sentenceJapanese) REFERENCES PhraseScrape_Sentence(english, japanese) ON DELETE CASCADE,
PRIMARY KEY (sentenceEnglish, sentenceJapanese, orderNum)
);
CREATE TABLE PhraseScrape_Meaning_SeeAlsoTerm (
seeAlsoTerm TEXT NOT NULL,
meaningDefinition TEXT NOT NULL,
resultUri TEXT NOT NULL,
FOREIGN KEY (meaningDefinition, resultUri) REFERENCES PhraseScrape_Meaning (definition, resultUri) ON DELETE CASCADE,
PRIMARY KEY (seeAlsoTerm, meaningDefinition, resultUri)
);
CREATE TABLE PhraseScrape_Meaning_Supplemental (
supplemental TEXT NOT NULL,
meaningDefinition TEXT NOT NULL,
resultUri TEXT NOT NULL,
FOREIGN KEY (meaningDefinition, resultUri) REFERENCES PhraseScrape_Meaning (definition, resultUri) ON DELETE CASCADE,
PRIMARY KEY (supplemental, meaningDefinition, resultUri)
);
CREATE TABLE PhraseScrape_Meaning_Tag (
tag TEXT NOT NULL,
meaningDefinition TEXT NOT NULL,
resultUri TEXT NOT NULL,
FOREIGN KEY (meaningDefinition, resultUri) REFERENCES PhraseScrape_Meaning (definition, resultUri) ON DELETE CASCADE,
PRIMARY KEY (tag, meaningDefinition, resultUri)
);
CREATE TABLE PhraseScrape_Meaning (
definition TEXT NOT NULL,
definitionAbstract TEXT,
resultUri TEXT NOT NULL,
FOREIGN KEY(resultUri) REFERENCES PhraseScrape_Result(uri) ON DELETE CASCADE,
PRIMARY KEY(definition,resultUri)
);
CREATE TABLE PhraseScrape_MeaningSentence_XRef (
sentenceEnglish TEXT NOT NULL,
sentenceJapanese TEXT NOT NULL,
resultUri TEXT NOT NULL,
FOREIGN KEY(resultUri) REFERENCES PhraseScrape_Result(uri) ON DELETE CASCADE,
FOREIGN KEY(sentenceEnglish, sentenceJapanese) REFERENCES PhraseScrape_Sentence(english, japanese) ON DELETE CASCADE
);
CREATE TABLE PhraseScrape_KanjiKanaPair (
kanji TEXT NOT NULL,
kana TEXT,
resultUri TEXT NOT NULL,
FOREIGN KEY(resultUri) REFERENCES PhraseScrape_Result(uri) ON DELETE CASCADE,
PRIMARY KEY (kanji, kana, resultUri)
);
CREATE TABLE PhraseScrape_AudioFile (
uri TEXT NOT NULL PRIMARY KEY,
mimetype TEXT NOT NULL,
resultUri TEXT NOT NULL,
FOREIGN KEY(resultUri) REFERENCES PhraseScrape_Result(uri) ON DELETE CASCADE
);
CREATE TABLE PhraseScrape_Note (
note TEXT NOT NULL,
resultUri TEXT NOT NULL,
FOREIGN KEY(resultUri) REFERENCES PhraseScrape_Result(uri) ON DELETE CASCADE,
PRIMARY KEY (note, resultUri)
);
-- API
CREATE TABLE PhraseSearch_JishoResult (
slug TEXT NOT NULL PRIMARY KEY,
isCommon BOOLEAN
);
CREATE TABLE PhraseScrape_JishoResult_Tag (
tag TEXT NOT NULL,
resultSlug TEXT NOT NULl,
FOREIGN KEY(resultSlug) REFERENCES PhraseSearch_JishoResult(slug) ON DELETE CASCADE,
PRIMARY KEY (tag, resultSlug)
);
CREATE TABLE PhraseScrape_JishoResult_Jlpt (
jlpt TEXT NOT NULL,
resultSlug TEXT NOT NULl,
FOREIGN KEY(resultSlug) REFERENCES PhraseSearch_JishoResult(slug) ON DELETE CASCADE,
PRIMARY KEY (jlpt, resultSlug)
);
CREATE TABLE PhraseSearch_JapaneseWord (
word TEXT,
reading TEXT,
resultSlug TEXT NOT NULl,
FOREIGN KEY(resultSlug) REFERENCES PhraseSearch_JishoResult(slug) ON DELETE CASCADE,
PRIMARY KEY (word, reading, resultSlug),
CHECK (word NOT NULL OR reading NOT NULL)
);
CREATE TABLE PhraseSearch_WordSense (
id INTEGER PRIMARY KEY AUTOINCREMENT
);
CREATE TABLE PhraseSearch_WordSense_Link (
text TEXT NOT NULL,
url TEXT NOT NULL,
senseID INTEGER NOT NULL,
FOREIGN KEY(senseID) REFERENCES PhraseSearch_WordSense(id) ON DELETE CASCADE,
PRIMARY KEY(url, senseID)
);
CREATE TABLE PhraseSearch_WordSense_Tag (
tag TEXT NOT NULL,
senseID INTEGER NOT NULL,
FOREIGN KEY(senseID) REFERENCES PhraseSearch_WordSense(id) ON DELETE CASCADE,
PRIMARY KEY (tag, senseID)
);
CREATE TABLE PhraseSearch_WordSense_SeeAlso (
seeAlso TEXT NOT NULL,
senseID INTEGER NOT NULL,
FOREIGN KEY(senseID) REFERENCES PhraseSearch_WordSense(id) ON DELETE CASCADE,
PRIMARY KEY (seeAlso, senseID)
);
CREATE TABLE PhraseSearch_WordSense_Antonym (
antonym TEXT NOT NULL,
senseID INTEGER NOT NULL,
FOREIGN KEY(senseID) REFERENCES PhraseSearch_WordSense(id) ON DELETE CASCADE,
PRIMARY KEY (antonym, senseID)
);
CREATE TABLE PhraseSearch_WordSense_Source (
language TEXT NOT NULL,
word TEXT,
senseID INTEGER NOT NULL,
FOREIGN KEY(senseID) REFERENCES PhraseSearch_WordSense(id) ON DELETE CASCADE,
PRIMARY KEY (language, senseID)
);
CREATE TABLE PhraseSearch_WordSense_Info (
info TEXT NOT NULL,
senseID INTEGER NOT NULL,
FOREIGN KEY(senseID) REFERENCES PhraseSearch_WordSense(id) ON DELETE CASCADE,
PRIMARY KEY (info, senseID)
);
CREATE TABLE PhraseSearch_WordSense_Restriction (
restriction TEXT NOT NULL,
senseID INTEGER NOT NULL,
FOREIGN KEY(senseID) REFERENCES PhraseSearch_WordSense(id) ON DELETE CASCADE,
PRIMARY KEY (restriction, senseID)
);
CREATE TABLE PhraseSearch_Attribution (
jmdict BOOLEAN NOT NULL,
jmnedict BOOLEAN NOT NULL,
dbpedia TEXT,
resultSlug TEXT NOT NULl PRIMARY KEY,
FOREIGN KEY(resultSlug) REFERENCES PhraseSearch_JishoResult(slug) ON DELETE CASCADE
);

View File

@@ -1,254 +0,0 @@
INSERT INTO Kanji_Radical(id, symbol, strokes, meaning, searchSymbol) VALUES
(1, '', 1, 'one', NULL),
(2, '', 1, 'line', NULL),
(3, '', 1, 'dot', NULL),
(4, '', 1, 'slash', NULL),
(5, '', 1, 'second', NULL),
(6, '', 1, 'hook', NULL),
(7, '', 2, 'two', NULL),
(8, '', 2, 'lid', NULL),
(9, '', 2, 'man, human', NULL),
(10, '', 2, 'man, human', ''),
(11, '𠆢', 2, 'man, human', ''),
(12, '', 2, 'legs', NULL),
(13, '', 2, 'enter', NULL),
(14, '', 2, 'eight', NULL),
(15, '', 2, 'eight', ''),
(16, '', 2, 'open country', NULL),
(17, '', 2, 'cover', NULL),
(18, '', 2, 'ice', NULL),
(19, '', 2, 'table', NULL),
(20, '', 2, 'container, open mouth', NULL),
(21, '', 2, 'knife, sword', NULL),
(22, '', 2, 'knife, sword', ''),
(23, '', 2, 'power, force', NULL),
(24, '', 2, 'wrap, embrace', NULL),
(25, '', 2, 'spoon', NULL),
(26, '', 2, 'box', NULL),
(27, '', 2, 'ten, complete', NULL),
(28, '', 2, 'divination', NULL),
(29, '', 2, 'kneel', NULL),
(30, '', 2, 'cliff', NULL),
(31, '', 2, 'private', NULL),
(32, '', 2, 'right hand', NULL),
(33, '', 2, 'katakana, jisho search radical', NULL),
(34, '', 2, 'second', NULL),
(35, '', 2, 'katakana, jisho search radical', NULL),
(36, '', 2, 'slash', NULL),
(360, '𠂉', 2, 'slash', ''),
(37, '', 3, 'walk', ''),
(38, '', 3, 'mouth, opening', NULL),
(39, '', 3, 'enclosure', NULL),
(40, '', 3, 'earth', NULL),
(41, '', 3, 'scholar, bachelor', NULL),
(42, '', 3, 'go', NULL),
(43, '', 3, 'evening, sunset', NULL),
(44, '', 3, 'big, very', NULL),
(45, '', 3, 'woman, female', NULL),
(46, '', 3, 'child, seed', NULL),
(47, '', 3, 'roof', NULL),
(48, '', 3, 'thumb, inch', NULL),
(49, '', 3, 'small, insignificant', NULL),
(50, '', 3, 'small, insignificant', ''),
(51, '', 3, 'lame', NULL),
(52, '', 3, 'corpse', NULL),
(53, '', 3, 'sprout', NULL),
(54, '', 3, 'mountain', NULL),
(55, '', 3, 'river', NULL),
(56, '', 3, 'river', NULL),
(57, '', 3, 'work', NULL),
(58, '', 3, 'oneself', NULL),
(59, '', 3, 'turban, scarf', NULL),
(60, '', 3, 'pestle', NULL),
(61, '', 3, 'short, tiny', NULL),
(62, '广', 3, 'house on cliff', NULL),
(63, '', 3, 'long stride', NULL),
(64, '', 3, 'two hands, twenty', NULL),
(65, '', 3, 'shoot, arrow', NULL),
(66, '', 3, 'bow', NULL),
(67, '', 3, 'pig snout', NULL),
(68, '', 3, 'pig snout', NULL),
(69, '', 3, 'bristle, beard', NULL),
(70, '', 3, 'step', NULL),
(71, '', 3, 'heart', ''),
(72, '', 3, 'hand', ''),
(73, '', 3, 'water', ''),
(74, '', 3, 'dog', ''),
(75, '', 3, 'grass', ''),
(76, '', 3, 'town (阝 right)', ''),
(77, '', 3, 'mound, dam (阝 left)', ''),
(78, '', 3, 'second', NULL),
(79, '', 3, 'lid', NULL),
(80, '', 3, 'right hand', NULL),
(81, '', 3, 'slash', NULL),
(82, '', 4, 'old', ''),
(83, '', 4, 'heart', NULL),
(84, '', 4, 'spear, halberd', NULL),
(85, '', 4, 'door, house', NULL),
(86, '', 4, 'hand', NULL),
(87, '', 4, 'branch', NULL),
(88, '', 4, 'rap', NULL),
(89, '', 4, 'script, literature', NULL),
(90, '', 4, 'dipper', NULL),
(91, '', 4, 'axe', NULL),
(92, '', 4, 'square', NULL),
(93, '', 4, 'perish', NULL),
(94, '', 4, 'sun, day', NULL),
(95, '', 4, 'say', NULL),
(96, '', 4, 'moon, month', NULL),
(97, '', 4, 'tree', NULL),
(98, '', 4, 'lack, yawn', NULL),
(99, '', 4, 'stop', NULL),
(100, '', 4, 'death, decay', NULL),
(101, '', 4, 'weapon, lance', NULL),
(102, '', 4, 'compare, compete', NULL),
(103, '', 4, 'fur, hair', NULL),
(104, '', 4, 'clan', NULL),
(105, '', 4, 'steam, breath', NULL),
(106, '', 4, 'water', NULL),
(107, '', 4, 'fire', NULL),
(108, '', 4, 'fire', ''),
(109, '', 4, 'claw', NULL),
(110, '', 4, 'father', NULL),
(111, '', 4, 'mix, twine, cross', NULL),
(112, '', 4, 'split wood', NULL),
(113, '', 4, 'slice', NULL),
(114, '', 4, 'cow', NULL),
(115, '', 4, 'dog', NULL),
(116, '', 4, 'sign', ''),
(117, '', 4, 'jade (king)', NULL),
(118, '', 4, 'legs', NULL),
(119, '', 4, 'two', NULL),
(120, '', 4, 'wrap, embrace', NULL),
(121, '', 4, 'lame', NULL),
(122, '', 4, 'two', NULL),
(123, '', 4, 'sprout', NULL),
(124, '', 4, 'oneself', NULL),
(125, '', 4, 'mother, do not', NULL),
(126, '', 5, 'dark, profound', NULL),
(127, '', 5, 'tile', NULL),
(128, '', 5, 'sweet', NULL),
(129, '', 5, 'life', NULL),
(130, '', 5, 'use', NULL),
(131, '', 5, 'field', NULL),
(132, '', 5, 'bolt of cloth', NULL),
(133, '', 5, 'sickness', ''),
(134, '', 5, 'footsteps', NULL),
(135, '', 5, 'white', NULL),
(136, '', 5, 'skin', NULL),
(137, '', 5, 'dish', NULL),
(138, '', 5, 'eye', NULL),
(139, '', 5, 'spear', NULL),
(140, '', 5, 'arrow', NULL),
(141, '', 5, 'stone', NULL),
(142, '', 5, 'sign', NULL),
(143, '', 5, 'track', ''),
(144, '', 5, 'grain', NULL),
(145, '', 5, 'cave', NULL),
(146, '', 5, 'stand, erect', NULL),
(147, '', 5, 'clothes', ''),
(148, '', 5, 'one', NULL),
(149, '', 5, 'work', NULL),
(150, '', 5, 'open country', NULL),
(151, '', 5, 'mother, do not', NULL),
(152, '', 5, 'net', ''),
(153, '', 5, 'fang', NULL),
(154, '', 6, 'melon', NULL),
(155, '', 6, 'bamboo', NULL),
(156, '', 6, 'rice', NULL),
(157, '', 6, 'silk', NULL),
(158, '', 6, 'jar', NULL),
(159, '', 6, 'sheep', NULL),
(160, '', 6, 'feather', NULL),
(161, '', 6, 'beard', NULL),
(162, '', 6, 'plow', NULL),
(163, '', 6, 'ear', NULL),
(164, '', 6, 'brush', NULL),
(165, '', 6, 'meat', NULL),
(166, '', 6, 'self', NULL),
(167, '', 6, 'arrive', NULL),
(168, '', 6, 'mortar', NULL),
(169, '', 6, 'tongue', NULL),
(170, '', 6, 'boat', NULL),
(171, '', 6, 'stopping', NULL),
(172, '', 6, 'colour, prettiness', NULL),
(173, '', 6, 'tiger stripes', NULL),
(174, '', 6, 'insect', NULL),
(175, '', 6, 'blood', NULL),
(176, '', 6, 'go, do', NULL),
(177, '', 6, 'clothes', NULL),
(178, '西', 6, 'west', NULL),
(179, '', 7, 'minster, official', NULL),
(180, '', 7, 'see', NULL),
(181, '', 7, 'horn', NULL),
(182, '', 7, 'speech', NULL),
(183, '', 7, 'valley', NULL),
(184, '', 7, 'bean', NULL),
(185, '', 7, 'pig', NULL),
(186, '', 7, 'cat, badger', NULL),
(187, '', 7, 'shell', NULL),
(188, '', 7, 'red, naked', NULL),
(189, '', 7, 'run', NULL),
(190, '', 7, 'foot', NULL),
(191, '', 7, 'body', NULL),
(192, '', 7, 'cart, car', NULL),
(193, '', 7, 'bitter', NULL),
(194, '', 7, 'morning', NULL),
(195, '', 7, 'wine, alcohol', NULL),
(196, '', 7, 'divide, distinguish, choose', NULL),
(197, '', 7, 'village, mile', NULL),
(198, '', 7, 'opposite', NULL),
(199, '', 7, 'wheat', NULL),
(200, '', 8, 'metal, gold', NULL),
(201, '', 8, 'long, grow', NULL),
(202, '', 8, 'gate', NULL),
(203, '', 8, 'slave, capture', NULL),
(204, '', 8, 'small bird', NULL),
(205, '', 8, 'rain', NULL),
(206, '', 8, 'blue', NULL),
(207, '', 8, 'wrong', NULL),
(208, '', 8, 'big, very', NULL),
(209, '', 8, 'mountain', NULL),
(210, '', 8, 'legs', NULL),
(211, '', 8, 'script, literature', NULL),
(212, '', 9, 'face', NULL),
(213, '', 9, 'leather, rawhide', NULL),
(214, '', 9, 'leek', NULL),
(215, '', 9, 'sound', NULL),
(216, '', 9, 'leaf', NULL),
(217, '', 9, 'wind', NULL),
(218, '', 9, 'fly', NULL),
(219, '', 9, 'eat, food', NULL),
(220, '', 9, 'head', NULL),
(221, '', 9, 'fragrance', NULL),
(222, '', 9, 'mouth, opening', NULL),
(223, '', 10, 'horse', NULL),
(224, '', 10, 'bone', NULL),
(225, '', 10, 'tall', NULL),
(226, '', 10, 'long hair', NULL),
(227, '', 10, 'fight', NULL),
(228, '', 10, 'herbs, sacrificial wine', NULL),
(229, '', 10, 'tripod, cauldron', NULL),
(230, '', 10, 'ghost, demon', NULL),
(231, '', 10, 'stand, erect', NULL),
(232, '', 10, 'tanned leather', NULL),
(233, '', 11, 'fish', NULL),
(234, '', 11, 'bird', NULL),
(235, '', 11, 'salt', NULL),
(236, '鹿', 11, 'deer', NULL),
(237, '', 11, 'hemp, flax', NULL),
(238, '', 11, 'second', NULL),
(239, '', 11, 'mouth, opening', ''),
(240, '', 11, 'yellow', NULL),
(241, '', 11, 'black', NULL),
(242, '', 12, 'millet', NULL),
(243, '', 12, 'embroidery, needlework', NULL),
(244, '', 12, 'fire', NULL),
(245, '', 12, 'stop', NULL),
(246, '', 13, 'frog, amphibian', NULL),
(247, '', 13, 'tripod', NULL),
(248, '', 13, 'drum', NULL),
(249, '', 13, 'rat, mouse', NULL),
(250, '', 14, 'nose', NULL),
(251, '', 14, 'even, uniformly', NULL),
(252, '', 17, 'flute', NULL);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
DB_NAME=test.db
all: clean dart
sqlite3 $(DB_NAME) < 0001_initial.sql
sqlite3 $(DB_NAME) < 0002_populate_radicals.sql
sqlite3 $(DB_NAME) < 0003_populate_radkfile.sql
sqlite3 $(DB_NAME) < 0004_populate_jouyou_kanji.sql
convert_kradk:
iconv -f EUC-JP -t UTF-8 -o data/radkfile_utf8 data/radkfile
dart:
dart run tools/update_0002.dart
dart run tools/update_0003.dart
dart run tools/update_0004.dart
clean:
rm test.db || true

View File

@@ -1 +0,0 @@
一右雨円王音下火花貝学気九休玉金空月犬見五口校左三山子四糸字耳七車手十出女小上森人水正生青夕石赤千川先早草足村大男竹中虫町天田土二日入年白八百文木本名目立力林六

View File

@@ -1 +0,0 @@
引羽雲園遠何科夏家歌画回会海絵外角楽活間丸岩顔汽記帰弓牛魚京強教近兄形計元言原戸古午後語工公広交光考行高黄合谷国黒今才細作算止市矢姉思紙寺自時室社弱首秋週春書少場色食心新親図数西声星晴切雪船線前組走多太体台地池知茶昼長鳥朝直通弟店点電刀冬当東答頭同道読内南肉馬売買麦半番父風分聞米歩母方北毎妹万明鳴毛門夜野友用曜来里理話

View File

@@ -1 +0,0 @@
悪安暗医委意育員院飲運泳駅央横屋温化荷界開階寒感漢館岸起期客究急級宮球去橋業曲局銀区苦具君係軽血決研県庫湖向幸港号根祭皿仕死使始指歯詩次事持式実写者主守取酒受州拾終習集住重宿所暑助昭消商章勝乗植申身神真深進世整昔全相送想息速族他打対待代第題炭短談着注柱丁帳調追定庭笛鉄転都度投豆島湯登等動童農波配倍箱畑発反坂板皮悲美鼻筆氷表秒病品負部服福物平返勉放味命面問役薬由油有遊予羊洋葉陽様落流旅両緑礼列練路和

View File

@@ -1 +0,0 @@
愛案以衣位茨印英栄媛塩岡億加果貨課芽賀改械害街各覚潟完官管関観願岐希季旗器機議求泣給挙漁共協鏡競極熊訓軍郡群径景芸欠結建健験固功好香候康佐差菜最埼材崎昨札刷察参産散残氏司試児治滋辞鹿失借種周祝順初松笑唱焼照城縄臣信井成省清静席積折節説浅戦選然争倉巣束側続卒孫帯隊達単置仲沖兆低底的典伝徒努灯働特徳栃奈梨熱念敗梅博阪飯飛必票標不夫付府阜富副兵別辺変便包法望牧末満未民無約勇要養浴利陸良料量輪類令冷例連老労録

View File

@@ -1 +0,0 @@
圧囲移因永営衛易益液演応往桜可仮価河過快解格確額刊幹慣眼紀基寄規喜技義逆久旧救居許境均禁句型経潔件険検限現減故個護効厚耕航鉱構興講告混査再災妻採際在財罪殺雑酸賛士支史志枝師資飼示似識質舎謝授修述術準序招証象賞条状常情織職制性政勢精製税責績接設絶祖素総造像増則測属率損貸態団断築貯張停提程適統堂銅導得毒独任燃能破犯判版比肥非費備評貧布婦武復複仏粉編弁保墓報豊防貿暴脈務夢迷綿輸余容略留領歴

View File

@@ -1 +0,0 @@
胃異遺域宇映延沿恩我灰拡革閣割株干巻看簡危机揮貴疑吸供胸郷勤筋系敬警劇激穴券絹権憲源厳己呼誤后孝皇紅降鋼刻穀骨困砂座済裁策冊蚕至私姿視詞誌磁射捨尺若樹収宗就衆従縦縮熟純処署諸除承将傷障蒸針仁垂推寸盛聖誠舌宣専泉洗染銭善奏窓創装層操蔵臓存尊退宅担探誕段暖値宙忠著庁頂腸潮賃痛敵展討党糖届難乳認納脳派拝背肺俳班晩否批秘俵腹奮並陛閉片補暮宝訪亡忘棒枚幕密盟模訳郵優預幼欲翌乱卵覧裏律臨朗論

View File

@@ -1 +0,0 @@
亜哀挨曖握扱宛嵐依威為畏尉萎偉椅彙違維慰緯壱逸芋咽姻淫陰隠韻唄鬱畝浦詠影鋭疫悦越謁閲炎怨宴援煙猿鉛縁艶汚凹押旺欧殴翁奥憶臆虞乙俺卸穏佳苛架華菓渦嫁暇禍靴寡箇稼蚊牙瓦雅餓介戒怪拐悔皆塊楷潰壊懐諧劾崖涯慨蓋該概骸垣柿核殻郭較隔獲嚇穫岳顎掛括喝渇葛滑褐轄且釜鎌刈甘汗缶肝冠陥乾勘患貫喚堪換敢棺款閑勧寛歓監緩憾還環韓艦鑑含玩頑企伎忌奇祈軌既飢鬼亀幾棋棄毀畿輝騎宜偽欺儀戯擬犠菊吉喫詰却脚虐及丘朽臼糾嗅窮巨拒拠虚距御凶叫狂享況峡挟狭恐恭脅矯響驚仰暁凝巾斤菌琴僅緊錦謹襟吟駆惧愚偶遇隅串屈掘窟繰勲薫刑茎契恵啓掲渓蛍傾携継詣慶憬稽憩鶏迎鯨隙撃桁傑肩倹兼剣拳軒圏堅嫌献遣賢謙鍵繭顕懸幻玄弦舷股虎孤弧枯雇誇鼓錮顧互呉娯悟碁勾孔巧甲江坑抗攻更拘肯侯恒洪荒郊貢控梗喉慌硬絞項溝綱酵稿衡購乞拷剛傲豪克酷獄駒込頃昆恨婚痕紺魂墾懇沙唆詐鎖挫采砕宰栽彩斎債催塞歳載剤削柵索酢搾錯咲刹拶撮擦桟惨傘斬暫旨伺刺祉肢施恣脂紫嗣雌摯賜諮侍慈餌璽軸𠮟疾執湿嫉漆芝赦斜煮遮邪蛇酌釈爵寂朱狩殊珠腫趣寿呪需儒囚舟秀臭袖羞愁酬醜蹴襲汁充柔渋銃獣叔淑粛塾俊瞬旬巡盾准殉循潤遵庶緒如叙徐升召匠床抄肖尚昇沼宵症祥称渉紹訟掌晶焦硝粧詔奨詳彰憧衝償礁鐘丈冗浄剰畳壌嬢錠譲醸拭殖飾触嘱辱尻伸芯辛侵津唇娠振浸紳診寝慎審震薪刃尽迅甚陣尋腎須吹炊帥粋衰酔遂睡穂随髄枢崇据杉裾瀬是姓征斉牲凄逝婿誓請醒斥析脊隻惜戚跡籍拙窃摂仙占扇栓旋煎羨腺詮践箋潜遷薦繊鮮禅漸膳繕狙阻租措粗疎訴塑遡礎双壮荘捜挿桑掃曹曽爽喪痩葬僧遭槽踪燥霜騒藻憎贈即促捉俗賊遜汰妥唾堕惰駄耐怠胎泰堆袋逮替滞戴滝択沢卓拓託濯諾濁但脱奪棚誰丹旦胆淡嘆端綻鍛弾壇恥致遅痴稚緻畜逐蓄秩窒嫡抽衷酎鋳駐弔挑彫眺釣貼超跳徴嘲澄聴懲勅捗沈珍朕陳鎮椎墜塚漬坪爪鶴呈廷抵邸亭貞帝訂逓偵堤艇締諦泥摘滴溺迭哲徹撤添塡殿斗吐妬途渡塗賭奴怒到逃倒凍唐桃透悼盗陶塔搭棟痘筒稲踏謄藤闘騰洞胴瞳峠匿督篤凸突屯豚頓貪鈍曇丼那謎鍋軟尼弐匂虹尿妊忍寧捻粘悩濃把覇婆罵杯排廃輩培陪媒賠伯拍泊迫剝舶薄漠縛爆箸肌鉢髪伐抜罰閥氾帆汎伴畔般販斑搬煩頒範繁藩蛮盤妃彼披卑疲被扉碑罷避尾眉微膝肘匹泌姫漂苗描猫浜賓頻敏瓶扶怖附訃赴浮符普腐敷膚賦譜侮舞封伏幅覆払沸紛雰噴墳憤丙併柄塀幣弊蔽餅壁璧癖蔑偏遍哺捕舗募慕簿芳邦奉抱泡胞俸倣峰砲崩蜂飽褒縫乏忙坊妨房肪某冒剖紡傍帽貌膨謀頰朴睦僕墨撲没勃堀奔翻凡盆麻摩磨魔昧埋膜枕又抹慢漫魅岬蜜妙眠矛霧娘冥銘滅免麺茂妄盲耗猛網黙紋冶弥厄躍闇喩愉諭癒唯幽悠湧猶裕雄誘憂融与誉妖庸揚揺溶腰瘍踊窯擁謡抑沃翼拉裸羅雷頼絡酪辣濫藍欄吏痢履璃離慄柳竜粒隆硫侶虜慮了涼猟陵僚寮療瞭糧厘倫隣瑠涙累塁励戻鈴零霊隷齢麗暦劣烈裂恋廉錬呂炉賂露弄郎浪廊楼漏籠麓賄脇惑枠湾腕

View File

@@ -1 +0,0 @@
氏統保第結派案策基価提挙応企検藤沢裁証援施井護展態鮮視条幹独宮率衛張監環審義訴株姿閣衆評影松撃佐核整融製票渉響推請器士討攻崎督授催及憲離激摘系批郎健盟従修隊織拡故振弁就異献厳維浜遺塁邦素遣抗模雄益緊標宣昭廃伊江僚吉盛皇臨踏壊債興源儀創障継筋闘葬避司康善逮迫惑崩紀聴脱級博締救執房撤削密措志載陣我為抑幕染奈傷択秀徴弾償功拠秘拒刑塚致繰尾描鈴盤項喪伴養懸街契掲躍棄邸縮還属慮枠恵露沖緩節需射購揮充貢鹿却端賃獲郡併徹貴衝焦奪災浦析譲称納樹挑誘紛至宗促慎控智握宙俊銭渋銃操携診託撮誕侵括謝駆透津壁稲仮裂敏是排裕堅訳芝綱典賀扱顧弘看訟戒祉誉歓奏勧騒閥甲縄郷揺免既薦隣華範隠徳哲杉釈己妥威豪熊滞微隆症暫忠倉彦肝喚沿妙唱阿索誠襲懇俳柄驚麻李浩剤瀬趣陥斎貫仙慰序旬兼聖旨即柳舎偽較覇詳抵脅茂犠旗距雅飾網竜詩繁翼潟敵魅嫌斉敷擁圏酸罰滅礎腐脚潮梅尽僕桜滑孤炎賠句鋼頑鎖彩摩励縦輝蓄軸巡稼瞬砲噴誇祥牲秩帝宏唆阻泰賄撲堀菊絞縁唯膨矢耐塾漏慶猛芳懲剣彰棋丁恒揚冒之倫陳憶潜梨仁克岳概拘墓黙須偏雰遇諮狭卓亀糧簿炉牧殊殖艦輩穴奇慢鶴謀暖昌拍朗寛覆胞泣隔浄没暇肺貞靖鑑飼陰銘随烈尋稿丹啓也丘棟壌漫玄粘悟舗妊熟旭恩騰往豆遂狂岐陛緯培衰艇屈径淡抽披廷錦准暑磯奨浸剰胆繊駒虚霊帳悔諭惨虐翻墜沼据肥徐糖搭盾脈滝軌俵妨擦鯨荘諾雷漂懐勘栽拐駄添冠斜鏡聡浪亜覧詐壇勲魔酬紫曙紋卸奮欄逸涯拓眼獄尚彫穏顕巧矛垣欺釣萩粛栗愚嘉遭架鬼庶稚滋幻煮姫誓把践呈疎仰剛疾征砕謡嫁謙后嘆菌鎌巣頻琴班棚潔酷宰廊寂辰霞伏碁俗漠邪晶墨鎮洞履劣那殴娠奉憂朴亭淳怪鳩酔惜穫佳潤悼乏該赴桑桂髄虎盆晋穂壮堤飢傍疫累痴搬晃癒桐寸郭尿凶吐宴鷹賓虜陶鐘憾猪紘磁弥昆粗訂芽庄傘敦騎寧循忍怠如寮祐鵬鉛珠凝苗獣哀跳匠垂蛇澄縫僧眺亘呉凡憩媛溝恭刈睡錯伯笹穀陵霧魂弊妃舶餓窮掌麗綾臭悦刃縛暦宜盲粋辱毅轄猿弦稔窒炊洪摂飽冗桃狩朱渦紳枢碑鍛刀鼓裸猶塊旋弓幣膜扇腸槽慈楊伐駿漬糾亮墳坪紺娯椿舌羅峡俸厘峰圭醸蓮弔乙汁尼遍衡薫猟羊款閲偵喝敢胎酵憤豚遮扉硫赦窃泡瑞又慨紡恨肪扶戯伍忌濁奔斗蘭迅肖鉢朽殻享秦茅藩沙輔媒鶏禅嘱胴迭挿嵐椎絹陪剖譜郁悠淑帆暁傑楠笛玲奴錠拳翔遷拙侍尺峠篤肇渇叔雌亨堪叙酢吟逓嶺甚喬崇漆岬癖愉寅礁乃洲屯樺槙姻巌擬塀唇睦閑胡幽峻曹詠卑侮鋳抹尉槻隷禍蝶酪茎帥逝汽琢匿襟蛍蕉寡琉痢庸朋坑藍賊搾畔遼唄孔橘漱呂拷嬢苑巽杜渓翁廉謹瞳湧欣窯褒醜升殉煩巴禎劾租堕桟稜婿倭斐慕矯罷囚某虹魁泌鴻赳於蚊漸厄葵禄藻嫡孟嚇尭凸巳韻暢硝霜芹勅棺杏鳳儒慧馨楼愁匡彬欽眉褐薪嵯賜繕綜翠栓榛鮎艶凹蔦惣隼錬衷渚斥逐芙稀皐詔惟雛耀佑渥黛宵憧惇妄甫脩蚕酌蒼嬉頒暉肢只凱檀謄彗丑梓叶嗣絢汐伽朔抄畝黎爽蛮惰旺冴偲萌瑠壱侯允鯉蒔遥弧瑛舜彪附但卯芋綺凌茜洸皓婆毬鯛緋邑怜碧倣穣啄悌酉柚倹亦繭采詢賦紗玖眸錘弐倖諄笙痘裟侃爾洵昴耗莞銑碩伶滉宥伎晏迪朕且綸晨竣燦吏頌麿楓箇梧琳澪哉晟匁凪衿丙梢茄颯恕勺瑚蕗瞭遵虞燎侑柊斤謁捺嵩茉蓉燿袈冶誼墾栞菖勁椋旦紬叡凜胤爵亥麟脹汰莉瑳瑶椰耶丞絃奎璃昂塑熙柾諒菫崚鞠捷濫

View File

@@ -1 +0,0 @@
党協総区領県設改府査委軍団各島革村勢減再税営比防補境導副算輸述線農州武象域額欧担準賞辺造被技低復移個門課脳極含蔵量型況針専谷史階管兵接細効丸湾録省旧橋岸周材戸央券編捜竹超並療採森競介根販歴将幅般貿講林装諸劇河航鉄児禁印逆換久短油暴輪占植清倍均億圧芸署伸停爆陸玉波帯延羽固則乱普測豊厚齢囲卒略承順岩練軽了庁城患層版令角絡損募裏仏績築貨混昇池血温季星永著誌庫刊像香坂底布寺宇巨震希触依籍汚枚複郵仲栄札板骨傾届巻燃跡包駐弱紹雇替預焼簡章臓律贈照薄群秒奥詰双刺純翌快片敬悩泉皮漁荒貯硬埋柱祭袋筆訓浴童宝封胸砂塩賢腕兆床毛緑尊祝柔殿濃液衣肩零幼荷泊黄甘臣浅掃雲掘捨軟沈凍乳恋紅郊腰炭踊冊勇械菜珍卵湖喫干虫刷湯溶鉱涙匹孫鋭枝塗軒毒叫拝氷乾棒祈拾粉糸綿汗銅湿瓶咲召缶隻脂蒸肌耕鈍泥隅灯辛磨麦姓筒鼻粒詞胃畳机膚濯塔沸灰菓帽枯涼舟貝符憎皿肯燥畜挟曇滴伺

View File

@@ -1 +0,0 @@
政議民連対部合市内相定回選米実関決全表戦経最現調化当約首法性要制治務成期取都和機平加受続進数記初指権支産点報済活原共得解交資予向際勝面告反判認参利組信在件側任引求所次昨論官増係感情投示変打直両式確果容必演歳争談能位置流格疑過局放常状球職与供役構割費付由説難優夫収断石違消神番規術備宅害配警育席訪乗残想声念助労例然限追商葉伝働形景落好退頭負渡失差末守若種美命福望非観察段横深申様財港識呼達良候程満敗値突光路科積他処太客否師登易速存飛殺号単座破除完降責捕危給苦迎園具辞因馬愛富彼未舞亡冷適婦寄込顔類余王返妻背熱宿薬険頼覚船途許抜便留罪努精散静婚喜浮絶幸押倒等老曲払庭徒勤遅居雑招困欠更刻賛抱犯恐息遠戻願絵越欲痛笑互束似列探逃遊迷夢君閉緒折草暮酒悲晴掛到寝暗盗吸陽御歯忘雪吹娘誤洗慣礼窓昔貧怒泳祖杯疲皆鳴腹煙眠怖耳頂箱晩寒髪忙才靴恥偶偉猫幾

View File

@@ -1 +0,0 @@
会同事自社発者地業方新場員立開手力問代明動京目通言理体田主題意不作用度強公持野以思家世多正安院心界教文元重近考画海売知道集別物使品計死特私始朝運終台広住真有口少町料工建空急止送切転研足究楽起着店病質待試族銀早映親験英医仕去味写字答夜音注帰古歌買悪図週室歩風紙黒花春赤青館屋色走秋夏習駅洋旅服夕借曜飲肉貸堂鳥飯勉冬昼茶弟牛魚兄犬妹姉漢

View File

@@ -1 +0,0 @@
日一国人年大十二本中長出三時行見月後前生五間上東四今金九入学高円子外八六下来気小七山話女北午百書先名川千水半男西電校語土木聞食車何南万毎白天母火右読友左休父雨

View File

@@ -1,178 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:html/parser.dart';
import 'package:http/http.dart' as http;
import 'package:unofficial_jisho_api/api.dart';
class Radical {
final int id;
final String symbol;
final String? search_symbol;
final String meaning;
final int strokes;
const Radical({
required this.id,
required this.symbol,
required this.strokes,
required this.meaning,
this.search_symbol,
});
@override
String toString() {
return '$id - ($symbol, $strokes${search_symbol != null ? ", $search_symbol" : ""})';
}
String get sql_tuple => ' ('
'$id, '
"'$symbol', "
'$strokes, '
"'$meaning', "
"${search_symbol != null ? "'$search_symbol'" : 'NULL'}"
')';
factory Radical.fromJson(Map<String, dynamic> json) {
return Radical(
id: json['id'] as int,
symbol: json['symbol'] as String,
strokes: json['strokes'] as int,
meaning: json['meaning'] as String,
search_symbol: json['search_symbol'] as String?,
);
}
Map<String, Object?> toJson() => {
'id': id,
'symbol': symbol,
'strokes': strokes,
'meaning': meaning,
'search_symbol': search_symbol,
};
}
String hexToUnicode(String code) =>
String.fromCharCode(int.parse(code, radix: 16));
/// Some of the radicals in jisho are written using katakana,
/// and some are written using either the symbols from the
/// Kangxi radical block (U+2F00-U+2FDF) or the
/// Unified CJK Character block (U+4E00-U+9FFF). These have been
/// used without care, and therefore some of the radicals are not
/// easily searchable. This conversion table helps solve this issue.
///
/// See:
/// https://en.wikipedia.org/wiki/List_of_radicals_in_Unicode
/// https://second.wiki/wiki/unicodeblock_kangxi-radikale
/// https://wiki.contextgarden.net/List_of_Unicode_blocks
Future<Map<String, String>> fetchEquivalentUCJKIdeographs() async {
final response = await http.get(
Uri.parse(
'https://www.unicode.org/Public/UNIDATA/EquivalentUnifiedIdeograph.txt',
),
);
final Map<String, String> result = {};
for (final line in response.body.split('\n')) {
if (line.startsWith('#') || RegExp(r'^\s*$').hasMatch(line)) continue;
final items = line.split(RegExp(r'\s+'));
if (items[0].contains('.')) {
final startEnd = items[0].split('..');
final start = int.parse(startEnd[0], radix: 16);
final end = int.parse(startEnd[1], radix: 16);
for (int i = 0; i <= (end - start); i++) {
result[String.fromCharCode(start + i)] = hexToUnicode(items[2]);
}
} else {
result[hexToUnicode(items[0])] = hexToUnicode(items[2]);
}
}
return result;
}
final cacheFile = File('data/0002_radicals.json');
Future<void> cacheRadicals() async {
final Map<String, String> equivalentSymbols =
await fetchEquivalentUCJKIdeographs();
equivalentSymbols[''] = '';
equivalentSymbols[''] = '丿';
equivalentSymbols[''] = '';
equivalentSymbols[''] = '';
equivalentSymbols[''] = '';
final Map<String, List<String>> inverseEquivalentSymbols = {};
for (final entry in equivalentSymbols.entries) {
if (inverseEquivalentSymbols.containsKey(entry.value)) {
inverseEquivalentSymbols[entry.value]!.add(entry.key);
continue;
}
inverseEquivalentSymbols[entry.value] = [entry.key];
}
final response = await http.get(Uri.parse('https://jisho.org/'));
final document = parse(response.body);
final table = document.querySelector('.radical_table')!;
final List<Radical> radicals = [];
int i = 0;
for (final node in table.children) {
if (node.className == 'reset_icon_list_item') continue;
if (node.className == 'number') {
i = int.parse(node.innerHtml);
continue;
}
final String radical = node.innerHtml;
print('Caching: $radical');
KanjiResult? result;
for (final item in [
radical,
equivalentSymbols[radical],
...inverseEquivalentSymbols[radical] ?? [],
]) {
if (item == null) continue;
result = await searchForKanji(item);
if (result.found) break;
}
final Radical radicalData = Radical(
id: int.parse(node.attributes['data-radical']!),
symbol: radical,
strokes: i,
search_symbol: node.attributes['data-radk'],
meaning: ['', ''].contains(radical)
? 'katakana, jisho search radical'
: result!.data!.radical!.meaning,
);
radicals.add(radicalData);
}
assert(radicals.length == 252, '[ERROR] Missing radicals!');
final encoder = JsonEncoder.withIndent(' ');
cacheFile.writeAsStringSync(encoder.convert(radicals));
}
Future<void> main(List<String> args) async {
if (!cacheFile.existsSync()) {
await cacheRadicals();
}
List<Radical> radicals = (jsonDecode(cacheFile.readAsStringSync()) as List).map((e) => Radical.fromJson(e)).toList();
File('0002_populate_radicals.sql').writeAsStringSync(
'''
INSERT INTO Kanji_Radical(id, symbol, strokes, meaning, searchSymbol) VALUES
${radicals.map((r) => r.sql_tuple).join(',\n')};
''',
);
}

View File

@@ -1,28 +0,0 @@
import 'dart:io';
// TODO: Automate download of radkfile
void main() {
final String content = File('data/radkfile_utf8').readAsStringSync();
final Iterable<String> blocks =
content.replaceAll(RegExp(r'^#.*$'), '').split(r'$').skip(2);
final List<String> tuples = [];
for (final block in blocks) {
final String radical = block[1];
final List<String> kanjiList = block
.replaceFirst(RegExp(r'.*\n'), '')
.split('')
..removeWhere((e) => e == '' || e == '\n');
for (final kanji in kanjiList) {
tuples.add(" ('$radical', '$kanji')");
}
}
File('0003_populate_radkfile.sql').writeAsStringSync(
'''
INSERT INTO RADKFILE(radical, kanji) VALUES
${tuples.join(',\n')};''',
);
}

View File

@@ -1,213 +0,0 @@
// ignore_for_file: avoid_print
import 'dart:convert';
import 'dart:io';
import 'package:unofficial_jisho_api/api.dart';
Future<void> cacheData(int i) async {
final File cacheFile = File('data/jisho/grade$i.json');
final File kanjiFile = File('data/jouyou/grade$i.txt');
final List<String> kanji = [
for (final k in kanjiFile.readAsStringSync().runes) String.fromCharCode(k)
];
final List<KanjiResultData> data = [];
await Future.wait([
for (int i = 0; i < kanji.length; i++)
Future.delayed(Duration(milliseconds: 300 * i), () async {
print('$i: ${kanji[i]}');
final result = await searchForKanji(kanji[i]);
data.add(result.data!);
})
]);
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
cacheFile.writeAsStringSync(encoder.convert(data));
}
String quote(String input) => "'${input.replaceAll("'", "''")}'";
extension SQLInserts on KanjiResultData {
int? get jlptLevelNumber =>
jlptLevel != null ? int.parse(jlptLevel![1]) : null;
int? get taughtInNumber => taughtIn == null
? null
: taughtIn == 'junior high'
? 7
: int.parse(taughtIn![6]);
static String get kanjiResultCols =>
'(kanji, strokeCount, meaning, radical, jlptLevel, newspaperFrequencyRank, taughtIn, isJouyou)';
String get kanjiResultRow =>
// ignore: prefer_interpolation_to_compose_strings
'("$kanji", $strokeCount, "$meaning", "${radical!.symbol}", ' +
((jlptLevel != null) ? '$jlptLevelNumber, ' : 'NULL, ') +
((newspaperFrequencyRank != null)
? '$newspaperFrequencyRank, '
: 'NULL, ') +
((taughtIn != null) ? '$taughtInNumber, ' : 'NULL, ') +
'true'
')';
static String get yomiCols => '(yomi)';
static String get partCols => '(part)';
List<String> get onyomiRows => onyomi.map((y) => '("$y")').toList();
List<String> get kunyomiRows => kunyomi.map((y) => '("$y")').toList();
List<String> get partsRows => kunyomi.map((p) => '("$p")').toList();
static String get yomiXRefCols => '(kanji, yomi)';
static String get partXRefCols => '(kanji, part)';
List<String> get onyomiXRefRows =>
onyomi.map((y) => "('$kanji', '$y')").toList();
List<String> get kunyomiXRefRows =>
kunyomi.map((y) => "('$kanji', '$y')").toList();
List<String> get partsXRefRows =>
kunyomi.map((p) => "('$kanji', '$p')").toList();
static String get yomiExampleCols => '(example, reading, meaning)';
List<String> get onyomiExamplesRows => onyomiExamples
.map(
(y) =>
'(${quote(y.example)}, ${quote(y.reading)}, ${quote(y.meaning)})',
)
.toList();
List<String> get kunyomiExamplesRows => kunyomiExamples
.map(
(y) =>
'(${quote(y.example)}, ${quote(y.reading)}, ${quote(y.meaning)})',
)
.toList();
static String get yomiExampleXRefCols => '(exampleID, kanji)';
List<String> onyomiExamplesXRefRows(int exampleID) => [
for (int i = 0; i < onyomiExamples.length; i++)
"(${exampleID + i}, '$kanji')"
];
List<String> kunyomiExamplesXRefRows(int exampleID) => [
for (int i = 0; i < kunyomiExamples.length; i++)
"(${exampleID + i}, '$kanji')"
];
}
int exampleIDXRefCounter = 1;
List<String> generateStatements(List<KanjiResultData> kanji) {
final List<String> statements = [];
final List<String> tableKanjiResult = [];
final List<String> tableOnyomi = [];
final List<String> tableKunyomi = [];
final List<String> tablePart = [];
final List<String> tableOnyomiExamples = [];
final List<String> tableKunyomiExamples = [];
final List<String> tableOnyomiXRef = [];
final List<String> tableKunyomiXRef = [];
final List<String> tablePartXRef = [];
final List<String> tableOnyomiExamplesXRef = [];
final List<String> tableKunyomiExamplesXRef = [];
for (final k in kanji) {
tableKanjiResult.add(k.kanjiResultRow);
tableOnyomi.addAll(k.onyomiRows);
tableKunyomi.addAll(k.kunyomiRows);
tablePart.addAll(k.partsRows);
tableOnyomiExamples.addAll(k.onyomiExamplesRows);
tableKunyomiExamples.addAll(k.kunyomiExamplesRows);
tableOnyomiXRef.addAll(k.onyomiXRefRows);
tableKunyomiXRef.addAll(k.kunyomiXRefRows);
tablePartXRef.addAll(k.partsXRefRows);
}
for (final k in kanji) {
final oxr = k.onyomiExamplesXRefRows(exampleIDXRefCounter);
exampleIDXRefCounter += oxr.length;
tableOnyomiExamplesXRef.addAll(oxr);
}
for (final k in kanji) {
final kxr = k.kunyomiExamplesXRefRows(exampleIDXRefCounter);
exampleIDXRefCounter += kxr.length;
tableKunyomiExamplesXRef.addAll(kxr);
}
void insertStatement({
required String table,
required List<String> values,
orIgnore = false,
}) =>
statements.add(
'INSERT${orIgnore ? ' OR IGNORE' : ''} INTO $table VALUES\n'
'${values.join(',\n')};\n',
);
insertStatement(
table: 'Kanji_Result${SQLInserts.kanjiResultCols}',
values: tableKanjiResult,
);
for (final isOnyomi in [true, false]) {
final String name = isOnyomi ? 'Onyomi' : 'Kunyomi';
insertStatement(
table: 'Kanji_$name${SQLInserts.yomiCols}',
values: isOnyomi ? tableOnyomi : tableKunyomi,
orIgnore: true,
);
insertStatement(
table: 'Kanji_Result${name}_XRef${SQLInserts.yomiXRefCols}',
values: isOnyomi ? tableOnyomiXRef : tableKunyomiXRef,
);
insertStatement(
table: 'Kanji_YomiExample${SQLInserts.yomiExampleCols}',
values: isOnyomi ? tableOnyomiExamples : tableKunyomiExamples,
orIgnore: true,
);
insertStatement(
table: 'Kanji_Result${name}Example_XRef${SQLInserts.yomiExampleXRefCols}',
values: isOnyomi ? tableOnyomiExamplesXRef : tableKunyomiExamplesXRef,
);
}
insertStatement(
table: 'Kanji_Part${SQLInserts.partCols}',
values: tablePart,
orIgnore: true,
);
insertStatement(
table: 'Kanji_ResultPart_XRef${SQLInserts.partXRefCols}',
values: tablePartXRef,
);
return statements;
}
Future<void> main() async {
final dataDir = Directory('data/jisho');
dataDir.createSync();
final List<String> statements = [];
for (int i = 1; i <= 7; i++) {
final File cacheFile = File('data/jisho/grade$i.json');
if (!cacheFile.existsSync()) {
await cacheData(i);
}
final String content = cacheFile.readAsStringSync();
final List<KanjiResultData> kanji = (jsonDecode(content) as List)
.map((e) => KanjiResultData.fromJson(e))
.toList();
statements.addAll(generateStatements(kanji));
}
File('0004_populate_jouyou_kanji.sql')
.writeAsStringSync(statements.join('\n'));
}

View File

@@ -0,0 +1,366 @@
import 'dart:math';
import 'package:get_it/get_it.dart';
import '../../data/database.dart';
export 'package:get_it/get_it.dart';
class HistoryEntry {
int id;
final String? kanji;
final String? word;
final DateTime lastTimestamp;
/// Whether this item is a kanji search or a word search
bool get isKanji => word == null;
HistoryEntry.withKanji({
required this.id,
required this.kanji,
required this.lastTimestamp,
}) : word = null;
HistoryEntry.withWord({
required this.id,
required this.word,
required this.lastTimestamp,
}) : kanji = null;
/// Reconstruct a HistoryEntry object with data from the database
/// This is specifically intended for the historyEntryOrderedByTimestamp
/// view, but it can also be used with custom searches as long as it
/// contains the following attributes:
///
/// - entryId
/// - timestamp
/// - word?
/// - kanji?
factory HistoryEntry.fromDBMap(Map<String, Object?> dbObject) =>
dbObject['word'] != null
? HistoryEntry.withWord(
id: dbObject['entryId']! as int,
word: dbObject['word']! as String,
lastTimestamp: DateTime.fromMillisecondsSinceEpoch(
dbObject['timestamp']! as int,
),
)
: HistoryEntry.withKanji(
id: dbObject['entryId']! as int,
kanji: dbObject['kanji']! as String,
lastTimestamp: DateTime.fromMillisecondsSinceEpoch(
dbObject['timestamp']! as int,
),
);
// TODO: There is a lot in common with
// insertKanji,
// insertWord,
// insertJsonEntry,
// insertJsonEntries,
// The commonalities should be factored into a helper function
/// Insert a kanji history entry into the database.
/// If it already exists, only a timestamp will be added
static Future<HistoryEntry> insertKanji({
required String kanji,
}) =>
db().transaction((txn) async {
final DateTime timestamp = DateTime.now();
late final int id;
final existingEntry = await txn.query(
TableNames.historyEntryKanji,
where: 'kanji = ?',
whereArgs: [kanji],
);
if (existingEntry.isNotEmpty) {
// Retrieve entry record id, and add a timestamp.
id = existingEntry.first['entryId']! as int;
await txn.insert(TableNames.historyEntryTimestamp, {
'entryId': id,
'timestamp': timestamp.millisecondsSinceEpoch,
});
} else {
// Create new record, and add a timestamp.
id = await txn.insert(
TableNames.historyEntry,
{},
nullColumnHack: 'id',
);
final Batch b = txn.batch();
b.insert(TableNames.historyEntryTimestamp, {
'entryId': id,
'timestamp': timestamp.millisecondsSinceEpoch,
});
b.insert(TableNames.historyEntryKanji, {
'entryId': id,
'kanji': kanji,
});
await b.commit();
}
return HistoryEntry.withKanji(
id: id,
kanji: kanji,
lastTimestamp: timestamp,
);
});
/// Insert a word history entry into the database.
/// If it already exists, only a timestamp will be added
static Future<HistoryEntry> insertWord({
required String word,
String? language,
}) =>
db().transaction((txn) async {
final DateTime timestamp = DateTime.now();
late final int id;
final existingEntry = await txn.query(
TableNames.historyEntryWord,
where: 'word = ?',
whereArgs: [word],
);
if (existingEntry.isNotEmpty) {
// Retrieve entry record id, and add a timestamp.
id = existingEntry.first['entryId']! as int;
await txn.insert(TableNames.historyEntryTimestamp, {
'entryId': id,
'timestamp': timestamp.millisecondsSinceEpoch,
});
} else {
id = await txn.insert(
TableNames.historyEntry,
{},
nullColumnHack: 'id',
);
final Batch b = txn.batch();
b.insert(TableNames.historyEntryTimestamp, {
'entryId': id,
'timestamp': timestamp.millisecondsSinceEpoch,
});
b.insert(TableNames.historyEntryWord, {
'entryId': id,
'word': word,
'language': {
null: null,
'japanese': 'j',
'english': 'e',
}[language]
});
await b.commit();
}
return HistoryEntry.withWord(
id: id,
word: word,
lastTimestamp: timestamp,
);
});
/// All recorded timestamps for this specific HistoryEntry
/// sorted in descending order.
Future<List<DateTime>> get timestamps async => GetIt.instance
.get<Database>()
.query(
TableNames.historyEntryTimestamp,
where: 'entryId = ?',
whereArgs: [id],
orderBy: 'timestamp DESC',
)
.then(
(timestamps) => timestamps
.map(
(t) => DateTime.fromMillisecondsSinceEpoch(
t['timestamp']! as int,
),
)
.toList(),
);
/// Export to json for archival reasons
/// Combined with [insertJsonEntry], this makes up functionality for exporting
/// and importing data from the app.
Future<Map<String, Object?>> toJson() async => {
'word': word,
'kanji': kanji,
'timestamps':
(await timestamps).map((ts) => ts.millisecondsSinceEpoch).toList()
};
/// Insert archived json entry into database if it doesn't exist there already.
/// Combined with [toJson], this makes up functionality for exporting and
/// importing data from the app.
static Future<HistoryEntry> insertJsonEntry(
Map<String, Object?> json,
) async =>
db().transaction((txn) async {
final b = txn.batch();
final bool isKanji = json['word'] == null;
final existingEntry = isKanji
? await txn.query(
TableNames.historyEntryKanji,
where: 'kanji = ?',
whereArgs: [json['kanji']! as String],
)
: await txn.query(
TableNames.historyEntryWord,
where: 'word = ?',
whereArgs: [json['word']! as String],
);
late final int id;
if (existingEntry.isEmpty) {
id = await txn.insert(
TableNames.historyEntry,
{},
nullColumnHack: 'id',
);
if (isKanji) {
b.insert(TableNames.historyEntryKanji, {
'entryId': id,
'kanji': json['kanji']! as String,
});
} else {
b.insert(TableNames.historyEntryWord, {
'entryId': id,
'word': json['word']! as String,
});
}
} else {
id = existingEntry.first['entryId']! as int;
}
final List<int> timestamps =
(json['timestamps']! as List).map((ts) => ts as int).toList();
for (final timestamp in timestamps) {
b.insert(
TableNames.historyEntryTimestamp,
{
'entryId': id,
'timestamp': timestamp,
},
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
await b.commit();
return isKanji
? HistoryEntry.withKanji(
id: id,
kanji: json['kanji']! as String,
lastTimestamp:
DateTime.fromMillisecondsSinceEpoch(timestamps.reduce(max)),
)
: HistoryEntry.withWord(
id: id,
word: json['word']! as String,
lastTimestamp:
DateTime.fromMillisecondsSinceEpoch(timestamps.reduce(max)),
);
});
/// An efficient implementation of [insertJsonEntry] for multiple
/// entries.
///
/// This assumes that there are no duplicates within the elements
/// in the json.
static Future<List<HistoryEntry>> insertJsonEntries(
List<Map<String, Object?>> json,
) =>
db().transaction((txn) async {
final b = txn.batch();
final List<HistoryEntry> entries = [];
for (final jsonObject in json) {
final bool isKanji = jsonObject['word'] == null;
final existingEntry = isKanji
? await txn.query(
TableNames.historyEntryKanji,
where: 'kanji = ?',
whereArgs: [jsonObject['kanji']! as String],
)
: await txn.query(
TableNames.historyEntryWord,
where: 'word = ?',
whereArgs: [jsonObject['word']! as String],
);
late final int id;
if (existingEntry.isEmpty) {
id = await txn.insert(
TableNames.historyEntry,
{},
nullColumnHack: 'id',
);
if (isKanji) {
b.insert(TableNames.historyEntryKanji, {
'entryId': id,
'kanji': jsonObject['kanji']! as String,
});
} else {
b.insert(TableNames.historyEntryWord, {
'entryId': id,
'word': jsonObject['word']! as String,
});
}
} else {
id = existingEntry.first['entryId']! as int;
}
final List<int> timestamps = (jsonObject['timestamps']! as List)
.map((ts) => ts as int)
.toList();
for (final timestamp in timestamps) {
b.insert(
TableNames.historyEntryTimestamp,
{
'entryId': id,
'timestamp': timestamp,
},
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
entries.add(
isKanji
? HistoryEntry.withKanji(
id: id,
kanji: jsonObject['kanji']! as String,
lastTimestamp: DateTime.fromMillisecondsSinceEpoch(
timestamps.reduce(max),
),
)
: HistoryEntry.withWord(
id: id,
word: jsonObject['word']! as String,
lastTimestamp: DateTime.fromMillisecondsSinceEpoch(
timestamps.reduce(max),
),
),
);
}
await b.commit();
return entries;
});
static Future<int> amountOfEntries() async {
final query = await db().query(
TableNames.historyEntry,
columns: ['COUNT(*) AS count'],
);
return query.first['count']! as int;
}
static Future<List<HistoryEntry>> get fromDB async =>
(await db().query(TableNames.historyEntryOrderedByTimestamp))
.map((e) => HistoryEntry.fromDBMap(e))
.toList();
Future<void> delete() =>
db().delete(TableNames.historyEntry, where: 'id = ?', whereArgs: [id]);
}

View File

@@ -1,12 +0,0 @@
class KanjiQuery {
final String kanji;
KanjiQuery({
required this.kanji,
});
Map<String, Object?> toJson() => {'kanji': kanji};
factory KanjiQuery.fromJson(Map<String, dynamic> json) =>
KanjiQuery(kanji: json['kanji'] as String);
}

View File

@@ -1,120 +0,0 @@
import 'package:get_it/get_it.dart';
import 'package:sembast/sembast.dart';
import './kanji_query.dart';
import './word_query.dart';
export 'package:get_it/get_it.dart';
export 'package:sembast/sembast.dart';
class Search {
final WordQuery? wordQuery;
final KanjiQuery? kanjiQuery;
final List<DateTime> timestamps;
Search.fromKanjiQuery({
required KanjiQuery this.kanjiQuery,
List<DateTime>? timestamps,
}) : wordQuery = null,
timestamps = timestamps ?? [DateTime.now()];
Search.fromWordQuery({
required WordQuery this.wordQuery,
List<DateTime>? timestamps,
}) : kanjiQuery = null,
timestamps = timestamps ?? [DateTime.now()];
bool get isKanji => wordQuery == null;
DateTime get timestamp => timestamps.last;
Map<String, Object?> toJson() => {
'timestamps': [for (final ts in timestamps) ts.millisecondsSinceEpoch],
'lastTimestamp': timestamps.last.millisecondsSinceEpoch,
'wordQuery': wordQuery?.toJson(),
'kanjiQuery': kanjiQuery?.toJson(),
};
factory Search.fromJson(Map<String, dynamic> json) {
final List<DateTime> timestamps = [
for (final ts in json['timestamps'] as List<dynamic>)
DateTime.fromMillisecondsSinceEpoch(ts as int)
];
return json['wordQuery'] != null
? Search.fromWordQuery(
wordQuery: WordQuery.fromJson(json['wordQuery']),
timestamps: timestamps,
)
: Search.fromKanjiQuery(
kanjiQuery: KanjiQuery.fromJson(json['kanjiQuery']),
timestamps: timestamps,
);
}
static StoreRef<int, Object?> get store => intMapStoreFactory.store('search');
}
Future<void> addSearchToDatabase({
required String searchTerm,
required bool isKanji,
}) async {
final DateTime now = DateTime.now();
final db = GetIt.instance.get<Database>();
final Filter filter = Filter.equals(
isKanji ? 'kanjiQuery.kanji' : 'wordQuery.query',
searchTerm,
);
final RecordSnapshot<int, Object?>? previousSearch =
await Search.store.findFirst(db, finder: Finder(filter: filter));
if (previousSearch != null) {
final search =
Search.fromJson(previousSearch.value! as Map<String, Object?>);
search.timestamps.add(now);
Search.store.record(previousSearch.key).put(db, search.toJson());
return;
}
Search.store.add(
db,
isKanji
? Search.fromKanjiQuery(kanjiQuery: KanjiQuery(kanji: searchTerm))
.toJson()
: Search.fromWordQuery(wordQuery: WordQuery(query: searchTerm))
.toJson(),
);
}
List<Search> mergeSearches(List<Search> a, List<Search> b) {
final List<Search> result = [...a];
for (final Search search in b) {
late final Iterable<Search> matchingEntry;
if (search.isKanji) {
matchingEntry =
result.where((e) => e.kanjiQuery?.kanji == search.kanjiQuery!.kanji);
} else {
matchingEntry =
result.where((e) => e.wordQuery?.query == search.wordQuery!.query);
}
if (matchingEntry.isEmpty) {
result.add(search);
continue;
}
final timestamps = [...matchingEntry.first.timestamps];
matchingEntry.first.timestamps.clear();
matchingEntry.first.timestamps.addAll(
(timestamps
..addAll(search.timestamps)
..sort())
.toSet()
.toList(),
);
}
return result;
}

View File

@@ -1,16 +0,0 @@
class WordQuery {
final String query;
// TODO: Link query with results that the user clicks onto.
// final List<WordResult> chosenResults;
WordQuery({
required this.query,
});
Map<String, Object?> toJson() => {'query': query};
factory WordQuery.fromJson(Map<String, dynamic> json) =>
WordQuery(query: json['query'] as String);
}

View File

@@ -1,13 +0,0 @@
import 'word_query.dart';
class WordResult {
final DateTime timestamp;
final String word;
final WordQuery searchString;
WordResult({
required this.timestamp,
required this.word,
required this.searchString,
});
}

View File

@@ -0,0 +1,64 @@
class LibraryEntry {
DateTime lastModified;
String? kanji;
String? word;
bool get isKanji => word == null;
String get title => isKanji ? kanji! : word!;
String get entryText => isKanji ? kanji! : word!;
LibraryEntry({
DateTime? lastModified,
this.kanji,
this.word,
}) : lastModified = lastModified ?? DateTime.now(),
assert(kanji != null || word != null, "Library entry can't be empty");
LibraryEntry.fromWord({
required word,
DateTime? lastModified,
// ignore: prefer_initializing_formals
}) : word = word,
lastModified = lastModified ?? DateTime.now();
LibraryEntry.fromKanji({
required String kanji,
DateTime? lastModified,
// ignore: prefer_initializing_formals
}) : kanji = kanji,
lastModified = lastModified ?? DateTime.now();
Map<String, Object?> toJson() => {
'kanji': kanji,
'word': word,
'lastModified': lastModified.millisecondsSinceEpoch,
};
factory LibraryEntry.fromJson(Map<String, Object?> json) => json['kanji'] !=
null
? LibraryEntry.fromKanji(
kanji: json['kanji']! as String,
lastModified:
DateTime.fromMillisecondsSinceEpoch(json['lastModified']! as int),
)
: LibraryEntry.fromWord(
word: json['word']! as String,
lastModified:
DateTime.fromMillisecondsSinceEpoch(json['lastModified']! as int),
);
factory LibraryEntry.fromDBMap(Map<String, Object?> dbObject) =>
dbObject['isKanji']! == 1
? LibraryEntry.fromKanji(
kanji: dbObject['entryText']! as String,
lastModified: DateTime.fromMillisecondsSinceEpoch(
dbObject['lastModified']! as int,
),
)
: LibraryEntry.fromWord(
word: dbObject['entryText']! as String,
lastModified: DateTime.fromMillisecondsSinceEpoch(
dbObject['lastModified']! as int,
),
);
}

View File

@@ -0,0 +1,360 @@
import 'dart:developer';
import 'package:collection/collection.dart';
import '../../data/database.dart';
import '../../data/database_errors.dart';
import 'library_entry.dart';
class LibraryList {
final String name;
const LibraryList._byName(this.name);
static const LibraryList favourites = LibraryList._byName('favourites');
/// Get all entries within the library, in their custom order
Future<List<LibraryEntry>> get entries async {
const columns = ['entryText', 'isKanji', 'lastModified'];
final query = await db().rawQuery(
'''
WITH RECURSIVE "RecursionTable"(${columns.map((c) => '"$c"').join(', ')}) AS (
SELECT ${columns.map((c) => '"$c"').join(', ')}
FROM "${TableNames.libraryListEntry}" "I"
WHERE "I"."listName" = ? AND "I"."prevEntryText" IS NULL
UNION ALL
SELECT ${columns.map((c) => '"R"."$c"').join(', ')}
FROM "${TableNames.libraryListEntry}" "R"
JOIN "RecursionTable" ON (
"R"."prevEntryText" = "RecursionTable"."entryText"
AND "R"."prevEntryIsKanji" = "RecursionTable"."isKanji"
)
WHERE "R"."listName" = ?
)
SELECT * FROM "RecursionTable";
''',
[name, name],
);
return query.map((e) => LibraryEntry.fromDBMap(e)).toList();
}
/// Get all existing libraries in their custom order.
static Future<List<LibraryList>> get allLibraries async {
final query = await db().query(TableNames.libraryListOrdered);
return query
.map((lib) => LibraryList._byName(lib['name']! as String))
.toList();
}
/// Generates a map of all the libraries, with the value being
/// whether or not the specified entry is within the library.
static Future<Map<LibraryList, bool>> allListsContains({
required String entryText,
required bool isKanji,
}) async {
final query = await db().rawQuery(
'''
SELECT
*,
EXISTS(
SELECT * FROM "${TableNames.libraryListEntry}"
WHERE "listName" = "name" AND "entryText" = ? AND "isKanji" = ?
) AS "exists"
FROM "${TableNames.libraryListOrdered}"
''',
[entryText, isKanji ? 1 : 0],
);
return Map.fromEntries(
query.map(
(lib) => MapEntry(
LibraryList._byName(lib['name']! as String),
lib['exists']! as int == 1,
),
),
);
}
/// Whether a library contains a specific entry
Future<bool> contains({
required String entryText,
required bool isKanji,
}) async {
final query = await db().rawQuery(
'''
SELECT EXISTS(
SELECT *
FROM "${TableNames.libraryListEntry}"
WHERE "listName" = ? AND "entryText" = ? AND "isKanji" = ?
) AS "exists"
''',
[name, entryText, isKanji ? 1 : 0],
);
return query.first['exists']! as int == 1;
}
/// Whether a library contains a specific word entry
Future<bool> containsWord(String word) => contains(
entryText: word,
isKanji: false,
);
/// Whether a library contains a specific kanjientry
Future<bool> containsKanji(String kanji) => contains(
entryText: kanji,
isKanji: true,
);
/// Whether a library exists in the database
static Future<bool> exists(String libraryName) async {
final query = await db().rawQuery(
'''
SELECT EXISTS(
SELECT *
FROM "${TableNames.libraryList}"
WHERE "name" = ?
) AS "exists"
''',
[libraryName],
);
return query.first['exists']! as int == 1;
}
static Future<int> amountOfLibraries() async {
final query = await db().query(
TableNames.libraryList,
columns: ['COUNT(*) AS count'],
);
return query.first['count']! as int;
}
/// The amount of items within this library.
Future<int> get length async {
final query = await db().query(
TableNames.libraryListEntry,
columns: ['COUNT(*) AS count'],
where: 'listName = ?',
whereArgs: [name],
);
return query.first['count']! as int;
}
/// Swaps two entries within a list
/// Will throw an exception if the entry is already in the library
Future<void> insertEntry({
required String entryText,
required bool isKanji,
int? position,
DateTime? lastModified,
}) async {
// TODO: set up lastModified insertion
if (await contains(entryText: entryText, isKanji: isKanji)) {
throw DataAlreadyExistsError(
tableName: TableNames.libraryListEntry,
illegalArguments: {
'entryText': entryText,
'isKanji': isKanji,
},
);
}
if (position != null) {
final len = await length;
if (0 > position || position > len) {
throw IndexError(
position,
this,
'position',
'Data insertion position ($position) can not be between 0 and length ($len).',
len,
);
} else if (position == len) {
insertEntry(
entryText: entryText,
isKanji: isKanji,
lastModified: lastModified,
);
return;
} else {
log('Adding ${isKanji ? 'kanji ' : ''}"$entryText" to library "$name" at $position');
final b = db().batch();
final entriess = await entries;
final prevEntry = entriess[position - 1];
final nextEntry = entriess[position];
b.insert(TableNames.libraryListEntry, {
'listName': name,
'entryText': entryText,
'isKanji': isKanji ? 1 : 0,
'prevEntryText': prevEntry.word,
'prevEntryIsKanji': prevEntry.isKanji ? 1 : 0,
});
b.update(
TableNames.libraryListEntry,
{
'prevEntryText': entryText,
'prevEntryIsKanji': isKanji ? 1 : 0,
},
where: '"listName" = ? AND "entryText" = ? AND "isKanji" = ?',
whereArgs: [name, nextEntry.entryText, nextEntry.isKanji ? 1 : 0],
);
await b.commit();
return;
}
}
log('Adding ${isKanji ? 'kanji ' : ''}"$entryText" to library "$name"');
final LibraryEntry? prevEntry = (await entries).lastOrNull;
await db().insert(TableNames.libraryListEntry, {
'listName': name,
'entryText': entryText,
'isKanji': isKanji ? 1 : 0,
'prevEntryText': prevEntry?.word,
'prevEntryIsKanji': (prevEntry?.isKanji ?? false) ? 1 : 0,
});
}
/// Deletes an entry within a list
/// Will throw an exception if the entry is not in the library
Future<void> deleteEntry({
required String entryText,
required bool isKanji,
}) async {
if (!await contains(entryText: entryText, isKanji: isKanji)) {
throw DataNotFoundError(
tableName: TableNames.libraryListEntry,
illegalArguments: {
'entryText': entryText,
'isKanji': isKanji,
},
);
}
log('Deleting ${isKanji ? 'kanji ' : ''}"$entryText" from library "$name"');
// TODO: these queries might be combined into one
final entryQuery = await db().query(
TableNames.libraryListEntry,
where: '"listName" = ? AND "entryText" = ? AND "isKanji" = ?',
whereArgs: [name, entryText, isKanji],
);
final nextEntryQuery = await db().query(
TableNames.libraryListEntry,
where:
'"listName" = ? AND "prevEntryText" = ? AND "prevEntryIsKanji" = ?',
whereArgs: [name, entryText, isKanji],
);
// final LibraryEntry entry = LibraryEntry.fromDBMap(entryQuery.first);
final LibraryEntry? nextEntry =
nextEntryQuery.map((e) => LibraryEntry.fromDBMap(e)).firstOrNull;
final b = db().batch();
if (nextEntry != null) {
b.update(
TableNames.libraryListEntry,
{
'prevEntryText': entryQuery.first['prevEntryText'],
'prevEntryIsKanji': entryQuery.first['prevEntryIsKanji'],
},
where: '"listName" = ? AND "entryText" = ? AND "isKanji" = ?',
whereArgs: [name, nextEntry.entryText, nextEntry.isKanji],
);
}
b.delete(
TableNames.libraryListEntry,
where: '"listName" = ? AND "entryText" = ? AND "isKanji" = ?',
whereArgs: [name, entryText, isKanji],
);
b.commit();
}
/// Swaps two entries within a list
/// Will throw an error if both of the entries doesn't exist
Future<void> swapEntries({
required String entryText1,
required bool isKanji1,
required String entryText2,
required bool isKanji2,
}) async {
// TODO: implement function.
throw UnimplementedError();
}
/// Toggle whether an entry is in the library or not.
/// If [overrideToggleOn] is given true or false, it will specifically insert or
/// delete the entry respectively. Else, it will figure out whether the entry
/// is in the library already automatically.
Future<bool> toggleEntry({
required String entryText,
required bool isKanji,
bool? overrideToggleOn,
}) async {
overrideToggleOn ??=
!(await contains(entryText: entryText, isKanji: isKanji));
if (overrideToggleOn) {
await insertEntry(entryText: entryText, isKanji: isKanji);
} else {
await deleteEntry(entryText: entryText, isKanji: isKanji);
}
return overrideToggleOn;
}
Future<void> deleteAllEntries() => db().delete(
TableNames.libraryListEntry,
where: 'listName = ?',
whereArgs: [name],
);
/// Insert a new library list into the database
static Future<LibraryList> insert(String libraryName) async {
if (await exists(libraryName)) {
throw DataAlreadyExistsError(
tableName: TableNames.libraryList,
illegalArguments: {
'libraryName': libraryName,
},
);
}
// This is ok, because "favourites" should always exist.
final prevList = (await allLibraries).last;
await db().insert(TableNames.libraryList, {
'name': libraryName,
'prevList': prevList.name,
});
return LibraryList._byName(libraryName);
}
/// Delete this library from the database
Future<void> delete() async {
if (name == 'favourites') {
throw IllegalDeletionError(
tableName: TableNames.libraryList,
illegalArguments: {'name': name},
);
}
await db().delete(
TableNames.libraryList,
where: 'name = ?',
whereArgs: [name],
);
}
}

View File

@@ -1,22 +0,0 @@
// import 'package:objectbox/objectbox.dart';
// import 'package:unofficial_jisho_api/api.dart' as jisho;
// TODO: Rewrite for sembast
// @Entity()
// class ExampleSentencePiece {
// int id;
// String? lifted;
// String unlifted;
// ExampleSentencePiece({
// this.id = 0,
// required this.lifted,
// required this.unlifted,
// });
// ExampleSentencePiece.fromJishoObject(jisho.ExampleSentencePiece object)
// : id = 0,
// lifted = object.lifted,
// unlifted = object.unlifted;
// }

View File

@@ -1,58 +0,0 @@
// import 'package:objectbox/objectbox.dart';
// import 'package:unofficial_jisho_api/api.dart' as jisho;
// import 'common.dart';
// TODO: Rewrite for sembast
// @Entity()
// class ExampleResultData {
// int id;
// String kanji;
// String kana;
// String english;
// List<ExampleSentencePiece> pieces;
// ExampleResultData({
// this.id = 0,
// required this.kanji,
// required this.kana,
// required this.english,
// required this.pieces,
// });
// ExampleResultData.fromJishoObject(jisho.ExampleResultData object)
// : id = 0,
// kanji = object.kanji,
// kana = object.kana,
// english = object.english,
// pieces = object.pieces
// .map((p) => ExampleSentencePiece.fromJishoObject(p))
// .toList();
// }
// @Entity()
// class ExampleResults {
// int id;
// String query;
// bool found;
// String uri;
// List<ExampleResultData> results;
// ExampleResults({
// this.id = 0,
// required this.query,
// required this.found,
// required this.uri,
// required this.results,
// });
// ExampleResults.fromJishoObject(jisho.ExampleResults object)
// : id = 0,
// query = object.query,
// found = object.found,
// uri = object.uri,
// results = object.results
// .map((r) => ExampleResultData.fromJishoObject(r))
// .toList();
// }

View File

@@ -1,129 +0,0 @@
// import 'package:objectbox/objectbox.dart';
// import 'package:unofficial_jisho_api/api.dart' as jisho;
// TODO: Rewrite for sembast
// @Entity()
// class YomiExample {
// int id;
// String example;
// String reading;
// String meaning;
// YomiExample({
// this.id = 0,
// required this.example,
// required this.reading,
// required this.meaning,
// });
// YomiExample.fromJishoObject(jisho.YomiExample object)
// : id = 0,
// example = object.example,
// reading = object.reading,
// meaning = object.meaning;
// }
// @Entity()
// class Radical {
// int id = 0;
// String symbol;
// List<String> forms;
// String meaning;
// Radical({
// this.id = 0,
// required this.symbol,
// required this.forms,
// required this.meaning,
// });
// Radical.fromJishoObject(jisho.Radical object)
// : symbol = object.symbol,
// forms = object.forms,
// meaning = object.meaning;
// }
// @Entity()
// class KanjiResult {
// int id = 0;
// String query;
// bool found;
// KanjiResultData? data;
// KanjiResult({
// this.id = 0,
// required this.query,
// required this.found,
// required this.data,
// });
// KanjiResult.fromJishoObject(jisho.KanjiResult object)
// : query = object.query,
// found = object.found,
// data = (object.data == null)
// ? null
// : KanjiResultData.fromJishoObject(object.data!);
// }
// @Entity()
// class KanjiResultData {
// int id = 0;
// String? taughtIn;
// String? jlptLevel;
// int? newspaperFrequencyRank;
// int strokeCount;
// String meaning;
// List<String> kunyomi;
// List<String> onyomi;
// List<YomiExample> kunyomiExamples;
// List<YomiExample> onyomiExamples;
// Radical? radical;
// List<String> parts;
// String strokeOrderDiagramUri;
// String strokeOrderSvgUri;
// String strokeOrderGifUri;
// String uri;
// KanjiResultData({
// this.id = 0,
// required this.taughtIn,
// required this.jlptLevel,
// required this.newspaperFrequencyRank,
// required this.strokeCount,
// required this.meaning,
// required this.kunyomi,
// required this.onyomi,
// required this.kunyomiExamples,
// required this.onyomiExamples,
// required this.radical,
// required this.parts,
// required this.strokeOrderDiagramUri,
// required this.strokeOrderSvgUri,
// required this.strokeOrderGifUri,
// required this.uri,
// });
// KanjiResultData.fromJishoObject(jisho.KanjiResultData object)
// : taughtIn = object.taughtIn,
// jlptLevel = object.jlptLevel,
// newspaperFrequencyRank = object.newspaperFrequencyRank,
// strokeCount = object.strokeCount,
// meaning = object.meaning,
// kunyomi = object.kunyomi,
// onyomi = object.onyomi,
// kunyomiExamples = object.kunyomiExamples
// .map((k) => YomiExample.fromJishoObject(k))
// .toList(),
// onyomiExamples = object.onyomiExamples
// .map((o) => YomiExample.fromJishoObject(o))
// .toList(),
// radical = (object.radical == null)
// ? null
// : Radical.fromJishoObject(object.radical!),
// parts = object.parts,
// strokeOrderDiagramUri = object.strokeOrderDiagramUri,
// strokeOrderSvgUri = object.strokeOrderSvgUri,
// strokeOrderGifUri = object.strokeOrderGifUri,
// uri = object.uri;
// }

View File

@@ -1,155 +0,0 @@
// import 'package:objectbox/objectbox.dart';
// import 'package:unofficial_jisho_api/api.dart' as jisho;
// import 'common.dart';
// TODO: Rewrite for sembast
// @Entity()
// class PhraseScrapeSentence {
// int id;
// String english;
// String japanese;
// List<ExampleSentencePiece> pieces;
// PhraseScrapeSentence({
// this.id = 0,
// required this.english,
// required this.japanese,
// required this.pieces,
// });
// PhraseScrapeSentence.fromJishoObject(jisho.PhraseScrapeSentence object)
// : id = 0,
// english = object.english,
// japanese = object.japanese,
// pieces = object.pieces
// .map((p) => ExampleSentencePiece.fromJishoObject(p))
// .toList();
// }
// @Entity()
// class PhraseScrapeMeaning {
// int id;
// List<String> seeAlsoTerms;
// List<PhraseScrapeSentence> sentences;
// String definition;
// List<String> supplemental;
// String? definitionAbstract;
// List<String> tags;
// PhraseScrapeMeaning({
// this.id = 0,
// required this.seeAlsoTerms,
// required this.sentences,
// required this.definition,
// required this.supplemental,
// required this.definitionAbstract,
// required this.tags,
// });
// PhraseScrapeMeaning.fromJishoObject(jisho.PhraseScrapeMeaning object)
// : id = 0,
// seeAlsoTerms = object.seeAlsoTerms,
// sentences = object.sentences
// .map((s) => PhraseScrapeSentence.fromJishoObject(s))
// .toList(),
// definition = object.definition,
// supplemental = object.supplemental,
// definitionAbstract = object.definitionAbstract,
// tags = object.tags;
// }
// @Entity()
// class KanjiKanaPair {
// int id;
// String kanji;
// String? kana;
// KanjiKanaPair({
// this.id = 0,
// required this.kanji,
// required this.kana,
// });
// KanjiKanaPair.fromJishoObject(jisho.KanjiKanaPair object)
// : id = 0,
// kanji = object.kanji,
// kana = object.kana;
// }
// @Entity()
// class PhrasePageScrapeResult {
// int id;
// bool found;
// String query;
// PhrasePageScrapeResultData? data;
// PhrasePageScrapeResult({
// this.id = 0,
// required this.found,
// required this.query,
// required this.data,
// });
// PhrasePageScrapeResult.fromJishoObject(jisho.PhrasePageScrapeResult object)
// : id = 0,
// found = object.found,
// query = object.query,
// data = (object.data == null)
// ? null
// : PhrasePageScrapeResultData.fromJishoObject(object.data!);
// }
// @Entity()
// class AudioFile {
// int id;
// String uri;
// String mimetype;
// AudioFile({
// this.id = 0,
// required this.uri,
// required this.mimetype,
// });
// AudioFile.fromJishoObject(jisho.AudioFile object)
// : id = 0,
// uri = object.uri,
// mimetype = object.mimetype;
// }
// @Entity()
// class PhrasePageScrapeResultData {
// int id;
// String uri;
// List<String> tags;
// List<PhraseScrapeMeaning> meanings;
// List<KanjiKanaPair> otherForms;
// List<AudioFile> audio;
// List<String> notes;
// PhrasePageScrapeResultData({
// this.id = 0,
// required this.uri,
// required this.tags,
// required this.meanings,
// required this.otherForms,
// required this.audio,
// required this.notes,
// });
// PhrasePageScrapeResultData.fromJishoObject(
// jisho.PhrasePageScrapeResultData object,
// ) : id = 0,
// uri = object.uri,
// tags = object.tags,
// meanings = object.meanings
// .map((m) => PhraseScrapeMeaning.fromJishoObject(m))
// .toList(),
// otherForms = object.otherForms
// .map((f) => KanjiKanaPair.fromJishoObject(f))
// .toList(),
// audio = object.audio.map((a) => AudioFile.fromJishoObject(a)).toList(),
// notes = object.notes;
// }

View File

@@ -1,195 +0,0 @@
// import 'package:objectbox/objectbox.dart';
// import 'package:unofficial_jisho_api/api.dart' as jisho;
// TODO: Rewrite for sembast
// @Entity()
// class SearchResult {
// int id;
// final JishoResultMeta meta;
// final ToMany<JishoResult> data;
// SearchResult({
// this.id = 0,
// required this.meta,
// required this.data,
// });
// SearchResult.fromJishoObject(final jisho.JishoAPIResult object)
// : id = 0,
// meta = JishoResultMeta.fromJishoObject(object.meta),
// data = ToMany<JishoResult>()
// ..addAll(
// object.data?.map((r) => JishoResult.fromJishoObject(r)) ??
// <JishoResult>[],
// );
// }
// @Entity()
// class JishoResultMeta {
// int id;
// int status;
// JishoResultMeta({
// this.id = 0,
// required this.status,
// });
// JishoResultMeta.fromJishoObject(final jisho.JishoResultMeta object)
// : id = 0,
// status = object.status;
// }
// @Entity()
// class JishoResult {
// int id;
// JishoAttribution attribution;
// bool? is_common;
// List<JishoJapaneseWord> japanese;
// List<String> jlpt;
// List<JishoWordSense> senses;
// String slug;
// List<String> tags;
// JishoResult({
// this.id = 0,
// required this.attribution,
// required this.is_common,
// required this.japanese,
// required this.jlpt,
// required this.senses,
// required this.slug,
// required this.tags,
// });
// JishoResult.fromJishoObject(final jisho.JishoResult object)
// : id = 0,
// attribution = JishoAttribution.fromJishoObject(object.attribution),
// is_common = object.isCommon,
// japanese = object.japanese
// .map((j) => JishoJapaneseWord.fromJishoObject(j))
// .toList(),
// jlpt = object.jlpt,
// senses = object.senses
// .map((s) => JishoWordSense.fromJishoObject(s))
// .toList(),
// slug = object.slug,
// tags = object.tags;
// }
// @Entity()
// class JishoAttribution {
// int id;
// String? dbpedia;
// bool jmdict;
// bool jmnedict;
// JishoAttribution({
// this.id = 0,
// required this.dbpedia,
// required this.jmdict,
// required this.jmnedict,
// });
// JishoAttribution.fromJishoObject(final jisho.JishoAttribution object)
// : id = 0,
// dbpedia = object.dbpedia,
// jmdict = object.jmdict,
// jmnedict = object.jmnedict;
// }
// @Entity()
// class JishoJapaneseWord {
// int id;
// String? reading;
// String? word;
// JishoJapaneseWord({
// this.id = 0,
// required this.reading,
// required this.word,
// });
// JishoJapaneseWord.fromJishoObject(final jisho.JishoJapaneseWord object)
// : id = 0,
// reading = object.reading,
// word = object.word;
// }
// @Entity()
// class JishoWordSense {
// int id;
// List<String> antonyms;
// List<String> english_definitions;
// List<String> info;
// List<JishoSenseLink> links;
// List<String> parts_of_speech;
// List<String> restrictions;
// List<String> see_also;
// List<JishoWordSource> source;
// List<String> tags;
// JishoWordSense({
// this.id = 0,
// required this.antonyms,
// required this.english_definitions,
// required this.info,
// required this.links,
// required this.parts_of_speech,
// required this.restrictions,
// required this.see_also,
// required this.source,
// required this.tags,
// });
// JishoWordSense.fromJishoObject(final jisho.JishoWordSense object)
// : id = 0,
// antonyms = object.antonyms,
// english_definitions = object.englishDefinitions,
// info = object.info,
// links =
// object.links.map((l) => JishoSenseLink.fromJishoObject(l)).toList(),
// parts_of_speech = object.partsOfSpeech,
// restrictions = object.restrictions,
// see_also = object.seeAlso,
// source = object.source
// .map((s) => JishoWordSource.fromJishoObject(s))
// .toList(),
// tags = object.tags;
// }
// @Entity()
// class JishoWordSource {
// int id;
// String language;
// String? word;
// JishoWordSource({
// this.id = 0,
// required this.language,
// required this.word,
// });
// JishoWordSource.fromJishoObject(final jisho.JishoWordSource object)
// : id = 0,
// language = object.language,
// word = object.word;
// }
// @Entity()
// class JishoSenseLink {
// int id;
// String text;
// String url;
// JishoSenseLink({
// this.id = 0,
// required this.text,
// required this.url,
// });
// JishoSenseLink.fromJishoObject(final jisho.JishoSenseLink object)
// : id = 0,
// text = object.text,
// url = object.url;
// }

View File

@@ -61,7 +61,14 @@ class DarkTheme extends AppTheme {
ThemeData getMaterialTheme() {
return ThemeData(
brightness: Brightness.dark,
primarySwatch: createMaterialColor(AppTheme.jishoGreen.background),
colorScheme: ColorScheme.fromSwatch(
primarySwatch: createMaterialColor(AppTheme.jishoGreen.background),
accentColor: AppTheme.jishoGreen.background,
brightness: Brightness.dark,
),
toggleableActiveColor: AppTheme.jishoGreen.background,
// elevatedButtonTheme: ElevatedButtonThemeData(style: )
);
}
}

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import '../models/library/library_list.dart';
import '../screens/home.dart';
import '../screens/info/about.dart';
import '../screens/info/licenses.dart';
import '../screens/library/library_content_view.dart';
import '../screens/search/result_page.dart';
import '../screens/search/search_mechanisms/drawing.dart';
import '../screens/search/search_mechanisms/grade_list.dart';
@@ -40,6 +42,12 @@ Route<Widget> generateRoute(RouteSettings settings) {
builder: (_) => KanjiRadicalSearch(prechosenRadical: prechosenRadical),
);
case Routes.libraryContent:
final library = args! as LibraryList;
return MaterialPageRoute(
builder: (_) => LibraryContentView(library: library),
);
case Routes.about:
return MaterialPageRoute(builder: (_) => const AboutView());
case Routes.aboutLicenses:

View File

@@ -5,6 +5,7 @@ class Routes {
static const String kanjiSearchDraw = '/kanjiSearch/draw';
static const String kanjiSearchGrade = '/kanjiSearch/grade';
static const String kanjiSearchRadicals = '/kanjiSearch/radicals';
static const String libraryContent = '/library';
static const String about = '/info/about';
static const String aboutLicenses = '/info/licenses';
static const String errorNotFound = '/error/404';

View File

@@ -1,27 +1,58 @@
import 'package:flutter/material.dart';
import '../components/drawing_board/drawing_board.dart';
import '../components/common/kanji_box.dart';
// import '../components/drawing_board/drawing_board.dart';
// import '../components/library/add_to_library_dialog.dart';
class DebugView extends StatelessWidget {
const DebugView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
// return const Center(child: KanjiBox(kanji: '漢'));
return ListView(
children: [
DrawingBoard(
allowHiragana: true,
allowKatakana: true,
allowOther: true,
onSuggestionChosen: (s) => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Chose: $s'),
duration: const Duration(milliseconds: 600),
Row(
children: [
KanjiBox.withPadding(kanji: '', padding: 5),
KanjiBox.withFontSize(kanji: '', fontSize: 20),
const KanjiBox.withFontSizeAndPadding(
kanji: '',
fontSize: 40,
padding: 10,
),
),
KanjiBox.withFontSize(kanji: '', fontSize: 40),
KanjiBox.withPadding(kanji: '', padding: 10),
],
),
const Divider(),
KanjiBox.expanded(kanji: '', ratio: 1),
const Divider(),
KanjiBox.expanded(kanji: '')
],
);
// return Column(
// mainAxisAlignment: MainAxisAlignment.end,
// children: const [
// ElevatedButton(
// onPressed: () => showAddToLibraryDialog(
// context: context,
// entryText: 'lol',
// ),
// child: const Text('Add entry to list'),
// ),
// DrawingBoard(
// allowHiragana: true,
// allowKatakana: true,
// allowOther: true,
// onSuggestionChosen: (s) => ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// content: Text('Chose: $s'),
// duration: const Duration(milliseconds: 600),
// ),
// ),
// ),
// ],
// );
}
}

View File

@@ -3,37 +3,24 @@ import 'package:flutter/material.dart';
import '../components/common/loading.dart';
import '../components/common/opaque_box.dart';
import '../components/history/date_divider.dart';
import '../components/history/search_item.dart';
import '../models/history/search.dart';
import '../components/history/history_entry_tile.dart';
import '../models/history/history_entry.dart';
import '../services/datetime.dart';
class HistoryView extends StatelessWidget {
const HistoryView({Key? key}) : super(key: key);
Stream<Map<int, Search>> get searchStream => Search.store
.query(finder: Finder(sortOrders: [SortOrder('lastTimestamp', false)]))
.onSnapshots(_db)
.map(
(snapshot) => Map.fromEntries(
snapshot.where((snap) => snap.value != null).map(
(snap) => MapEntry(
snap.key,
Search.fromJson(snap.value! as Map<String, Object?>),
),
),
),
);
Database get _db => GetIt.instance.get<Database>();
@override
Widget build(BuildContext context) {
return StreamBuilder<Map<int, Search>>(
stream: searchStream,
// TODO: Use infinite scroll pagination
return FutureBuilder<List<HistoryEntry>>(
future: HistoryEntry.fromDB,
builder: (context, snapshot) {
// TODO: provide proper error handling
if (snapshot.hasError) return ErrorWidget(snapshot.error!);
if (!snapshot.hasData) return const LoadingScreen();
final Map<int, Search> data = snapshot.data!;
final Map<int, HistoryEntry> data = snapshot.data!.asMap();
if (data.isEmpty)
return const Center(
child: Text('The history is empty.\nTry searching for something!'),
@@ -52,25 +39,30 @@ class HistoryView extends StatelessWidget {
}
Widget Function(BuildContext, int) historyEntrySeparatorWithData(
List<Search> data,
List<HistoryEntry> data,
) =>
(context, index) {
final Search search = data[index];
final DateTime searchDate = search.timestamp;
final HistoryEntry search = data[index];
final DateTime searchDate = search.lastTimestamp;
if (index == 0 || !dateIsEqual(data[index - 1].timestamp, searchDate))
if (index == 0 ||
!dateIsEqual(data[index - 1].lastTimestamp, searchDate))
return TextDivider(text: formatDate(roundToDay(searchDate)));
return const Divider(height: 0);
return const Divider(
height: 0,
indent: 10,
endIndent: 10,
);
};
Widget Function(BuildContext, int) historyEntryWithData(
Map<int, Search> data,
Map<int, HistoryEntry> data,
) =>
(context, index) => (index == 0)
? const SizedBox.shrink()
: SearchItem(
search: data.values.toList()[index - 1],
: HistoryEntryTile(
entry: data.values.toList()[index - 1],
objectKey: data.keys.toList()[index - 1],
onDelete: () => build(context),
);

View File

@@ -4,8 +4,10 @@ import 'package:mdi/mdi.dart';
import '../bloc/theme/theme_bloc.dart';
import '../components/common/denshi_jisho_background.dart';
import '../components/library/new_library_dialog.dart';
import 'debug.dart';
import 'history.dart';
import 'library/library_view.dart';
import 'search/kanji_view.dart';
import 'search/search_view.dart';
import 'settings.dart';
@@ -20,25 +22,35 @@ class Home extends StatefulWidget {
class _HomeState extends State<Home> {
int pageNum = 0;
_Page get page => pages[pageNum];
@override
Widget build(BuildContext context) {
return BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, themeState) {
return Scaffold(
appBar: AppBar(
title: pages[pageNum].titleBar,
title: Text(page.titleBar),
centerTitle: true,
backgroundColor: AppTheme.jishoGreen.background,
foregroundColor: AppTheme.jishoGreen.foreground,
actions: page.actions,
),
body: DenshiJishoBackground(child: pages[pageNum].content),
body: DenshiJishoBackground(child: page.content),
bottomNavigationBar: BottomNavigationBar(
fixedColor: AppTheme.jishoGreen.background,
currentIndex: pageNum,
onTap: (index) => setState(() {
pageNum = index;
}),
items: pages.map((p) => p.item).toList(),
items: pages
.map(
(p) => BottomNavigationBarItem(
label: p.titleBar,
icon: p.icon,
),
)
.toList(),
showSelectedLabels: false,
showUnselectedLabels: false,
unselectedItemColor: themeState.theme.menuGreyDark.background,
@@ -51,52 +63,40 @@ class _HomeState extends State<Home> {
List<_Page> get pages => [
const _Page(
content: SearchView(),
titleBar: Text('Search'),
item: BottomNavigationBarItem(
label: 'Search',
icon: Icon(Icons.search),
),
titleBar: 'Search',
icon: Icon(Icons.search),
),
const _Page(
content: KanjiView(),
titleBar: Text('Kanji Search'),
item: BottomNavigationBarItem(
label: 'Kanji',
icon: Icon(Mdi.ideogramCjk, size: 30),
),
titleBar: 'Kanji Search',
icon: Icon(Mdi.ideogramCjk, size: 30),
),
const _Page(
content: HistoryView(),
titleBar: Text('History'),
item: BottomNavigationBarItem(
label: 'History',
icon: Icon(Icons.history),
),
titleBar: 'History',
icon: Icon(Icons.history),
),
_Page(
content: Container(),
titleBar: const Text('Saved'),
item: const BottomNavigationBarItem(
label: 'Saved',
icon: Icon(Icons.bookmark),
),
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: Text('Settings'),
item: BottomNavigationBarItem(
label: 'Settings',
icon: Icon(Icons.settings),
),
titleBar: 'Settings',
icon: Icon(Icons.settings),
),
if (kDebugMode) ...[
const _Page(
content: DebugView(),
titleBar: Text('Debug Page'),
item: BottomNavigationBarItem(
label: 'Debug',
icon: Icon(Icons.biotech),
),
titleBar: 'Debug Page',
icon: Icon(Icons.biotech),
)
],
];
@@ -104,12 +104,14 @@ class _HomeState extends State<Home> {
class _Page {
final Widget content;
final Widget titleBar;
final BottomNavigationBarItem item;
final String titleBar;
final Icon icon;
final List<Widget> actions;
const _Page({
required this.content,
required this.titleBar,
required this.item,
required this.icon,
this.actions = const [],
});
}

View File

@@ -0,0 +1,78 @@
import 'package:confirm_dialog/confirm_dialog.dart';
import 'package:flutter/material.dart';
import '../../components/common/loading.dart';
import '../../components/library/library_list_entry_tile.dart';
import '../../models/library/library_entry.dart';
import '../../models/library/library_list.dart';
class LibraryContentView extends StatefulWidget {
final LibraryList library;
const LibraryContentView({
Key? key,
required this.library,
}) : super(key: key);
@override
State<LibraryContentView> createState() => _LibraryContentViewState();
}
class _LibraryContentViewState extends State<LibraryContentView> {
List<LibraryEntry>? entries;
Future<void> getEntriesFromDatabase() =>
widget.library.entries.then((es) => setState(() => entries = es));
@override
void initState() {
super.initState();
getEntriesFromDatabase();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.library.name),
actions: [
IconButton(
onPressed: () async {
final entryCount = await widget.library.length;
if (!mounted) return;
final bool userIsSure = await confirm(
context,
content: Text(
'Are you sure that you want to clear all $entryCount entries?',
),
);
if (!userIsSure) return;
await widget.library.deleteAllEntries();
await getEntriesFromDatabase();
},
icon: const Icon(Icons.delete),
),
],
),
body: entries == null
? const LoadingScreen()
: ListView.separated(
itemCount: entries!.length,
itemBuilder: (context, index) => LibraryListEntryTile(
index: index,
entry: entries![index],
library: widget.library,
onDelete: () => setState(() {
entries!.removeAt(index);
}),
onUpdate: () => getEntriesFromDatabase(),
),
separatorBuilder: (context, index) => const Divider(
height: 0,
indent: 10,
endIndent: 10,
),
),
);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import '../../components/common/loading.dart';
import '../../components/library/library_list_tile.dart';
import '../../models/library/library_list.dart';
class LibraryView extends StatefulWidget {
const LibraryView({Key? key}) : super(key: key);
@override
State<LibraryView> createState() => _LibraryViewState();
}
class _LibraryViewState extends State<LibraryView> {
List<LibraryList>? libraries;
Future<void> getEntriesFromDatabase() =>
LibraryList.allLibraries.then((libs) => setState(() => libraries = libs));
@override
void initState() {
super.initState();
getEntriesFromDatabase();
}
@override
Widget build(BuildContext context) {
if (libraries == null) return const LoadingScreen();
return Column(
children: [
LibraryListTile(
library: LibraryList.favourites,
leading: const Icon(Icons.star),
onDelete: getEntriesFromDatabase,
onUpdate: getEntriesFromDatabase,
isEditable: false,
),
Expanded(
child: ListView(
children: libraries!
// Skip favourites
.skip(1)
.map(
(e) => LibraryListTile(
library: e,
onDelete: getEntriesFromDatabase,
onUpdate: getEntriesFromDatabase,
),
)
.toList(),
),
),
],
);
}
}

View File

@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import '../../components/common/loading.dart';
import '../../components/kanji/kanji_result_body.dart';
import '../../components/library/add_to_library_dialog.dart';
import '../../components/search/search_result_body.dart';
import '../../models/history/search.dart';
import '../../models/history/history_entry.dart';
import '../../models/library/library_list.dart';
import '../../services/jisho_api/jisho_search.dart';
import '../../services/jisho_api/kanji_search.dart';
@@ -23,24 +25,67 @@ class ResultPage extends StatefulWidget {
class _ResultPageState extends State<ResultPage> {
bool addedToDatabase = false;
bool isFavourite = false;
@override
void initState() {
super.initState();
if (!widget.isKanji) return;
LibraryList.favourites
.containsKanji(widget.searchTerm)
.then((b) => setState(() => isFavourite = b));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
appBar: AppBar(
actions: !widget.isKanji
? []
: [
IconButton(
onPressed: () async {
await showAddToLibraryDialog(
context: context,
entryText: widget.searchTerm,
isKanji: true,
);
final updatedFavouriteStatus = await LibraryList.favourites
.containsKanji(widget.searchTerm);
setState(() => isFavourite = updatedFavouriteStatus);
},
icon: const Icon(Icons.bookmark),
),
IconButton(
onPressed: () async {
await LibraryList.favourites.toggleEntry(
entryText: widget.searchTerm,
isKanji: true,
overrideToggleOn: !isFavourite,
);
setState(() => isFavourite = !isFavourite);
},
icon: isFavourite
? const Icon(Icons.star, color: Colors.yellow)
: const Icon(Icons.star_border),
)
],
),
body: FutureBuilder(
future: widget.isKanji
? fetchKanji(widget.searchTerm)
: fetchJishoResults(widget.searchTerm),
builder: (context, snapshot) {
if (!snapshot.hasData) return const LoadingScreen();
// TODO: provide proper error handling
if (snapshot.hasError) return ErrorWidget(snapshot.error!);
if (!snapshot.hasData) return const LoadingScreen();
if (!addedToDatabase) {
addSearchToDatabase(
searchTerm: widget.searchTerm,
isKanji: widget.isKanji,
);
if (widget.isKanji) {
HistoryEntry.insertKanji(kanji: widget.searchTerm);
} else {
HistoryEntry.insertWord(word: widget.searchTerm);
}
addedToDatabase = true;
}

View File

@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'package:confirm_dialog/confirm_dialog.dart';
@@ -6,15 +5,15 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:mdi/mdi.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sembast/sembast_io.dart';
import 'package:sembast/utils/sembast_import_export.dart';
import '../bloc/theme/theme_bloc.dart';
import '../components/common/denshi_jisho_background.dart';
import '../models/history/search.dart';
import '../data/database.dart';
import '../data/export.dart';
import '../data/import.dart';
import '../models/history/history_entry.dart';
import '../models/library/library_list.dart';
import '../routing/routes.dart';
import '../services/database.dart';
import '../services/open_webpage.dart';
import '../services/snackbar.dart';
import '../settings.dart';
@@ -27,22 +26,59 @@ class SettingsView extends StatefulWidget {
}
class _SettingsViewState extends State<SettingsView> {
final Database db = GetIt.instance.get<Database>();
bool dataExportIsLoading = false;
bool dataImportIsLoading = false;
Future<void> clearHistory(context) async {
final bool userIsSure = await confirm(context);
final historyCount = await HistoryEntry.amountOfEntries();
if (userIsSure) {
await Search.store.delete(db);
}
final bool userIsSure = await confirm(
context,
content: Text(
'Are you sure that you want to clear all $historyCount entries in history?',
),
);
if (!userIsSure) return;
await db().delete(TableNames.historyEntry);
showSnackbar(context, 'Cleared history');
}
Future<void> clearFavourites(context) async {
final favouritesCount = await LibraryList.favourites.length;
final bool userIsSure = await confirm(
context,
content: Text(
'Are you sure that you want to clear all $favouritesCount entries in favourites?',
),
);
if (!userIsSure) return;
await LibraryList.favourites.deleteAllEntries();
showSnackbar(context, 'Cleared favourites');
}
Future<void> clearAll(context) async {
final historyCount = await HistoryEntry.amountOfEntries();
final libraryCount = await LibraryList.amountOfLibraries();
final bool userIsSure = await confirm(
context,
content: Text(
'Are you sure you want to delete $historyCount history entries '
'and $libraryCount libraries?',
),
);
if (!userIsSure) return;
await resetDatabase();
showSnackbar(context, 'Cleared everything');
}
// ignore: avoid_positional_boolean_parameters
void toggleAutoTheme(bool b) {
final bool newThemeIsDark = b
? WidgetsBinding.instance!.window.platformBrightness == Brightness.dark
? WidgetsBinding.instance?.window.platformBrightness == Brightness.dark
: darkThemeEnabled;
BlocProvider.of<ThemeBloc>(context)
@@ -63,75 +99,19 @@ class _SettingsViewState extends State<SettingsView> {
}
/// Can assume Android for time being
Future<void> exportData(context) async {
Future<void> exportHandler(context) async {
setState(() => dataExportIsLoading = true);
final path = (await getExternalStorageDirectory())!;
final dbData = await exportDatabase(db);
final file = File('${path.path}/jisho_data.json');
file.createSync(recursive: true);
await file.writeAsString(jsonEncode(dbData));
final path = await exportData();
setState(() => dataExportIsLoading = false);
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Data exported to ${file.path}')));
showSnackbar(context, 'Data exported to $path');
}
/// Can assume Android for time being
Future<void> importData(context) async {
Future<void> importHandler(context) async {
setState(() => dataImportIsLoading = true);
final path = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
);
final file = File(path!.files[0].path!);
final List<Search> prevSearches = (await Search.store.find(db))
.map((e) => Search.fromJson(e.value! as Map<String, Object?>))
.toList();
late final List<Search> importedSearches;
try {
importedSearches = ((((jsonDecode(await file.readAsString())
as Map<String, Object?>)['stores']! as List)
.map((e) => e as Map)
.where((e) => e['name'] == 'search')
.first)['values'] as List)
.map((item) => Search.fromJson(item))
.toList();
} catch (e) {
debugPrint(e.toString());
showSnackbar(
context,
"Couldn't read file. Did you choose the right one?",
);
return;
}
final List<Search> mergedSearches =
mergeSearches(prevSearches, importedSearches);
// print(mergedSearches);
await GetIt.instance.get<Database>().close();
GetIt.instance.unregister<Database>();
final importedDb = await importDatabase(
{
'sembast_export': 1,
'version': 1,
'stores': [
{
'name': 'search',
'keys': [for (var i = 1; i <= mergedSearches.length; i++) i],
'values': mergedSearches.map((e) => e.toJson()).toList(),
}
]
},
databaseFactoryIo,
await databasePath(),
);
GetIt.instance.registerSingleton<Database>(importedDb);
final path = await FilePicker.platform.getDirectoryPath();
await importData(Directory(path!));
setState(() => dataImportIsLoading = false);
showSnackbar(context, 'Data imported successfully');
@@ -288,7 +268,7 @@ class _SettingsViewState extends State<SettingsView> {
SettingsTile(
leading: const Icon(Icons.file_upload),
title: 'Import Data',
onPressed: importData,
onPressed: importHandler,
enabled: Platform.isAndroid,
subtitle:
Platform.isAndroid ? null : 'Not available on iOS yet',
@@ -299,6 +279,7 @@ class _SettingsViewState extends State<SettingsView> {
SettingsTile(
leading: const Icon(Icons.file_download),
title: 'Export Data',
onPressed: exportHandler,
enabled: Platform.isAndroid,
subtitle:
Platform.isAndroid ? null : 'Not available on iOS yet',
@@ -315,10 +296,15 @@ class _SettingsViewState extends State<SettingsView> {
SettingsTile(
leading: const Icon(Icons.delete),
title: 'Clear Favourites',
onPressed: (c) {},
onPressed: clearFavourites,
titleTextStyle: const TextStyle(color: Colors.red),
enabled: false,
)
),
SettingsTile(
leading: const Icon(Icons.delete),
title: 'Clear Everything',
onPressed: clearAll,
titleTextStyle: const TextStyle(color: Colors.red),
),
],
),

View File

@@ -1,19 +0,0 @@
import 'dart:io';
import 'package:get_it/get_it.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';
Future<String> databasePath() async {
final Directory appDocDir = await getApplicationDocumentsDirectory();
if (!appDocDir.existsSync()) appDocDir.createSync(recursive: true);
return join(appDocDir.path, 'sembast.db');
}
Future<void> setupDatabase() async {
final Database database =
await databaseFactoryIo.openDatabase(await databasePath());
GetIt.instance.registerSingleton<Database>(database);
}

View File

@@ -1,5 +1,5 @@
import 'package:unofficial_jisho_api/api.dart' as jisho;
import 'package:unofficial_jisho_api/api.dart';
export 'package:unofficial_jisho_api/api.dart' show JishoAPIResult;
Future<jisho.JishoAPIResult> fetchJishoResults(searchTerm) =>
jisho.searchForPhrase(searchTerm);
Future<JishoAPIResult> fetchJishoResults(searchTerm) =>
searchForPhrase(searchTerm);

View File

@@ -0,0 +1,12 @@
import 'package:unofficial_jisho_api/parser.dart';
// TODO: This should be moved to the api.
extension KanjiFurigana on JishoJapaneseWord {
// Both wordReading and word.word being present implies that the word has furigana.
// If that's not the case, then the word is usually present in wordReading.
// However, there are some exceptions where the reading is placed in word.
bool get hasFurigana => word != null && reading != null;
String get kanji => word ?? reading!;
String? get furigana => hasFurigana ? reading! : null;
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,19 +3,20 @@ description: A dictionary app for studying japanese
version: 1.0.0+1
environment:
sdk: ">=2.12.0 <3.0.0"
sdk: ">=2.18.4 <3.0.0"
dependencies:
animated_size_and_fade: ^3.0.0
auto_size_text: ^3.0.0
collection: ^1.15.0
confirm_dialog: ^1.0.0
division: ^0.9.0
file_picker: ^4.5.1
file_picker: ^5.2.5
flutter:
sdk: flutter
flutter_bloc: ^8.0.0
flutter_settings_ui: ^2.0.1
flutter_slidable: ^1.1.0
flutter_slidable: ^2.0.0
flutter_svg: ^1.0.2
get_it: ^7.2.0
http: ^0.13.4
@@ -23,12 +24,16 @@ dependencies:
mdi: ^5.0.0-nullsafety.0
path: ^1.8.0
path_provider: ^2.0.2
sembast: ^3.1.1
share_plus: ^4.0.4
test: ^1.19.5
ruby_text: ^3.0.1
share_plus: ^6.3.1
shared_preferences: ^2.0.6
signature: ^5.0.0
sqflite: ^2.0.2
sqflite_common_ffi: ^2.1.1
test: ^1.19.5
unofficial_jisho_api: ^3.0.0
# unofficial_jisho_api:
# path: /home/h7x4/git/unofficial-jisho-api-dart
url_launcher: ^6.0.9
dev_dependencies:
@@ -36,7 +41,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_native_splash: ^2.1.6
flutter_launcher_icons: "^0.9.2"
flutter_launcher_icons: ^0.11.0
flutter_icons:
android: "launcher_icon"
@@ -53,6 +58,7 @@ flutter:
- assets/images/components/
- assets/images/links/
- assets/images/logo/
- assets/migrations/
- assets/licenses/

View File

@@ -1,34 +1,6 @@
import 'package:jisho_study_tool/models/history/kanji_query.dart';
import 'package:jisho_study_tool/models/history/search.dart';
import 'package:jisho_study_tool/models/history/word_query.dart';
import 'package:jisho_study_tool/models/history/history_entry.dart';
import 'package:test/test.dart';
void main() {
group('Search', () {
final List<Search> searches = [
Search.fromKanjiQuery(kanjiQuery: KanjiQuery(kanji: '')),
Search.fromWordQuery(wordQuery: WordQuery(query: 'テスト')),
Search.fromJson({'timestamps':[1648658269960],'lastTimestamp':1648658269960,'wordQuery':null,'kanjiQuery':{'kanji':''}}),
Search.fromJson({'timestamps':[1648674967535],'lastTimestamp':1648674967535,'wordQuery':{'query':'黙る'},'kanjiQuery':null}),
Search.fromJson({'timestamps':[1649079907766],'lastTimestamp':1649079907766,'wordQuery':{'query':'seal'},'kanjiQuery':null}),
Search.fromJson({'timestamps':[1649082072981],'lastTimestamp':1649082072981,'wordQuery':{'query':'感涙屋'},'kanjiQuery':null}),
Search.fromJson({'timestamps':[1644951726777,1644951732749],'lastTimestamp':1644951732749,'wordQuery':{'query':'呑める'},'kanjiQuery':null}),
];
test("mergeSearches with empty lists doesn't add data", () {
final List<Search> merged1 = mergeSearches(searches, []);
final List<Search> merged2 = mergeSearches([], searches);
for (int i = 0; i < searches.length; i++) {
expect(merged1[i], searches[i]);
expect(merged2[i], searches[i]);
}
});
test("mergeSearches with the same list doesn't add data", () {
final List<Search> merged = mergeSearches(searches, searches);
for (int i = 0; i < searches.length; i++) {
expect(merged[i], searches[i]);
}
expect(mergeSearches(searches, searches), searches);
});
});
group('Search', () {});
}

4
tools/extractDB.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
mkdir -p android-data
adb shell run-as app.jishostudytool.jisho_study_tool cat app_flutter/jisho.sqlite > android-data/jisho_extract.db

4
tools/extractExports.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
mkdir -p android-data
adb pull /storage/emulated/0/Android/data/app.jishostudytool.jisho_study_tool/files/export android-data