mirror of
https://github.com/h7x4/Jisho-Study-Tool.git
synced 2024-12-21 13:37:29 +01:00
WIP
This commit is contained in:
parent
ea220e25f5
commit
d2d8ea07a6
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -1,32 +1,58 @@
|
||||
|
||||
CREATE TABLE "JST_LibraryList" (
|
||||
"name" TEXT PRIMARY KEY NOT NULL,
|
||||
"nextList" TEXT REFERENCES "JST_LibraryList"("name")
|
||||
"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)))
|
||||
);
|
||||
|
||||
CREATE INDEX "JST_LibraryList_byNextList" ON "JST_LibraryList"("nextList");
|
||||
-- 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,
|
||||
"lastModified" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"nextEntry" TEXT NOT NULL,
|
||||
-- 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", "nextEntry") REFERENCES "JST_LibraryListEntry"("listName", "entryText"),
|
||||
CHECK ((NOT "isKanji") OR ("nextEntry" <> 0))
|
||||
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))
|
||||
);
|
||||
|
||||
CREATE INDEX "JST_LibraryListEntry_byListName" ON "JST_LibraryListEntry"("listName");
|
||||
|
||||
-- CREATE VIEW "JST_LibraryListEntry_sortedByLists" AS
|
||||
-- WITH RECURSIVE "JST_LibraryListEntry_sorted"("next") AS (
|
||||
-- -- Initial SELECT
|
||||
-- UNION ALL
|
||||
-- SELECT * FROM ""
|
||||
-- -- Recursive Select
|
||||
-- )
|
||||
-- SELECT * FROM "JST_LibraryListEntry_sorted";
|
||||
-- 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
|
||||
@ -47,18 +73,17 @@ CREATE TABLE "JST_HistoryEntryWord" (
|
||||
|
||||
CREATE TABLE "JST_HistoryEntryTimestamp" (
|
||||
"entryId" INTEGER NOT NULL REFERENCES "JST_HistoryEntry"("id") ON DELETE CASCADE,
|
||||
-- Here, I'm using INTEGER insted of TIMESTAMP or DATETIME, because it seems to be
|
||||
-- the easiest way to deal with global and local timeconversion between dart and
|
||||
-- SQLite.
|
||||
"timestamp" INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
-- 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;
|
||||
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
87
flake.lock
generated
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
62
flake.nix
62
flake.nix
@ -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
|
||||
|
@ -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(
|
||||
|
184
lib/components/common/kanji_box.dart
Normal file
184
lib/components/common/kanji_box.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
),
|
||||
|
@ -4,18 +4,17 @@ import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import '../../models/history/history_entry.dart';
|
||||
import '../../routing/routes.dart';
|
||||
import '../../services/datetime.dart';
|
||||
import '../../services/snackbar.dart';
|
||||
import '../../settings.dart';
|
||||
import '../common/kanji_box.dart';
|
||||
import '../common/loading.dart';
|
||||
import 'kanji_box.dart';
|
||||
|
||||
class HistoryEntryItem extends StatelessWidget {
|
||||
class HistoryEntryTile extends StatelessWidget {
|
||||
final HistoryEntry entry;
|
||||
final int objectKey;
|
||||
final void Function()? onDelete;
|
||||
final void Function()? onFavourite;
|
||||
|
||||
const HistoryEntryItem({
|
||||
const HistoryEntryTile({
|
||||
required this.entry,
|
||||
required this.objectKey,
|
||||
this.onDelete,
|
||||
@ -23,10 +22,6 @@ class HistoryEntryItem extends StatelessWidget {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
Widget get _child => (entry.isKanji)
|
||||
? KanjiBox(kanji: entry.kanji!)
|
||||
: Text(entry.word!);
|
||||
|
||||
void Function() _onTap(context) => entry.isKanji
|
||||
? () => Navigator.pushNamed(
|
||||
context,
|
||||
@ -46,8 +41,7 @@ class HistoryEntryItem extends StatelessWidget {
|
||||
future: entry.timestamps,
|
||||
builder: (context, snapshot) {
|
||||
// TODO: provide proper error handling
|
||||
if (snapshot.hasError)
|
||||
return ErrorWidget(snapshot.error!);
|
||||
if (snapshot.hasError) return ErrorWidget(snapshot.error!);
|
||||
if (!snapshot.hasData) return const LoadingScreen();
|
||||
return ListView(
|
||||
children: snapshot.data!
|
||||
@ -69,14 +63,6 @@ class HistoryEntryItem extends StatelessWidget {
|
||||
icon: Icons.access_time,
|
||||
onPressed: (_) => Navigator.push(context, timestamps),
|
||||
),
|
||||
SlidableAction(
|
||||
backgroundColor: Colors.yellow,
|
||||
icon: Icons.star,
|
||||
onPressed: (_) {
|
||||
showSnackbar(context, 'TODO: implement favourites');
|
||||
onFavourite?.call();
|
||||
},
|
||||
),
|
||||
SlidableAction(
|
||||
backgroundColor: Colors.red,
|
||||
icon: Icons.delete,
|
||||
@ -107,7 +93,12 @@ class HistoryEntryItem extends StatelessWidget {
|
||||
),
|
||||
DefaultTextStyle.merge(
|
||||
style: japaneseFont.textStyle,
|
||||
child: _child,
|
||||
child: entry.isKanji
|
||||
? KanjiBox.headline4(
|
||||
context: context,
|
||||
kanji: entry.kanji!,
|
||||
)
|
||||
: Expanded(child: Text(entry.word!)),
|
||||
),
|
||||
],
|
||||
),
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
@ -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
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
@ -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(
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
|
139
lib/components/library/add_to_library_dialog.dart
Normal file
139
lib/components/library/add_to_library_dialog.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
76
lib/components/library/library_list_entry_tile.dart
Normal file
76
lib/components/library/library_list_entry_tile.dart
Normal 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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
73
lib/components/library/library_list_tile.dart
Normal file
73
lib/components/library/library_list_tile.dart
Normal 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');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
87
lib/components/library/new_library_dialog.dart
Normal file
87
lib/components/library/new_library_dialog.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -1,18 +1,36 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
// Example file Structure:
|
||||
// jisho_data_22.01.01_1
|
||||
// jisho_data_2022.01.01_1
|
||||
// - history.json
|
||||
// - library/
|
||||
// - lista.json
|
||||
// - listb.json
|
||||
|
||||
extension ArchiveFormat on Directory {
|
||||
// TODO: make the export dir dependent on date
|
||||
Directory get exportDirectory {
|
||||
final dir = Directory(uri.resolve('export').path);
|
||||
dir.createSync(recursive: true);
|
||||
return dir;
|
||||
|
||||
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);
|
||||
|
@ -2,6 +2,7 @@ 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';
|
||||
@ -41,7 +42,8 @@ Future<void> migrate(Database db, int oldVersion, int newVersion) async {
|
||||
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);
|
||||
final migrationContent =
|
||||
await rootBundle.loadString(migrations[i - 1], cache: false);
|
||||
|
||||
migrationContent
|
||||
.split(';')
|
||||
@ -58,24 +60,25 @@ Future<void> migrate(Database db, int oldVersion, int newVersion) async {
|
||||
}
|
||||
|
||||
Future<void> setupDatabase() async {
|
||||
databaseFactory.debugSetLogLevel(sqfliteLogLevelSql);
|
||||
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')
|
||||
]),
|
||||
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();
|
||||
await db().close();
|
||||
File(await databasePath()).deleteSync();
|
||||
GetIt.instance.unregister<Database>();
|
||||
await setupDatabase();
|
||||
}
|
||||
|
||||
class TableNames {
|
||||
@ -101,7 +104,7 @@ class TableNames {
|
||||
|
||||
/// Attributes:
|
||||
/// - name TEXT
|
||||
/// - nextList TEXT
|
||||
/// - prevList TEXT
|
||||
static const String libraryList = 'JST_LibraryList';
|
||||
|
||||
/// Attributes:
|
||||
@ -109,12 +112,17 @@ class TableNames {
|
||||
/// - entryText TEXT
|
||||
/// - isKanji BOOLEAN
|
||||
/// - lastModified TIMESTAMP
|
||||
/// - nextEntry TEXT
|
||||
/// - prevEntryText TEXT
|
||||
/// - prevEntryIsKanji BOOLEAN
|
||||
static const String libraryListEntry = 'JST_LibraryListEntry';
|
||||
|
||||
///////////
|
||||
// VIEWS //
|
||||
///////////
|
||||
|
||||
/// Attributes:
|
||||
/// - name TEXT
|
||||
static const String libraryListOrdered = 'JST_LibraryListOrdered';
|
||||
|
||||
/// Attributes:
|
||||
/// - entryId INTEGER
|
||||
|
74
lib/data/database_errors.dart
Normal file
74
lib/data/database_errors.dart
Normal 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 {}
|
@ -4,15 +4,12 @@ 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 {
|
||||
final basedir = (await getExternalStorageDirectory())!;
|
||||
final dir = basedir.exportDirectory;
|
||||
dir.createSync(recursive: true);
|
||||
return dir;
|
||||
}
|
||||
Future<Directory> exportDirectory() async =>
|
||||
(await getExternalStorageDirectory())!.exportDirectory;
|
||||
|
||||
/// Returns the path to which the data was saved.
|
||||
Future<String> exportData() async {
|
||||
@ -33,13 +30,21 @@ Future<void> exportHistoryTo(Directory dir) async {
|
||||
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 {
|
||||
// TODO:
|
||||
// final query = db().query(TableNames.libraryList);
|
||||
print('TODO: implement exportLibraryLists');
|
||||
}
|
||||
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));
|
||||
}),
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import '../models/history/history_entry.dart';
|
||||
@ -13,11 +14,11 @@ Future<void> importData(Directory dir) async {
|
||||
|
||||
Future<void> importHistoryFrom(File file) async {
|
||||
final String content = file.readAsStringSync();
|
||||
await HistoryEntry.insertJsonEntries(
|
||||
(jsonDecode(content) as List)
|
||||
.map((h) => h as Map<String, Object?>)
|
||||
.toList(),
|
||||
);
|
||||
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 {
|
||||
|
@ -267,7 +267,7 @@ class HistoryEntry {
|
||||
|
||||
/// 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(
|
||||
@ -348,6 +348,14 @@ class HistoryEntry {
|
||||
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))
|
||||
|
64
lib/models/library/library_entry.dart
Normal file
64
lib/models/library/library_entry.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
360
lib/models/library/library_list.dart
Normal file
360
lib/models/library/library_list.dart
Normal 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],
|
||||
);
|
||||
}
|
||||
}
|
@ -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: )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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';
|
||||
|
@ -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),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ 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/history_entry_item.dart';
|
||||
import '../components/history/history_entry_tile.dart';
|
||||
import '../models/history/history_entry.dart';
|
||||
import '../services/datetime.dart';
|
||||
|
||||
@ -45,10 +45,15 @@ class HistoryView extends StatelessWidget {
|
||||
final HistoryEntry search = data[index];
|
||||
final DateTime searchDate = search.lastTimestamp;
|
||||
|
||||
if (index == 0 || !dateIsEqual(data[index - 1].lastTimestamp, 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(
|
||||
@ -56,7 +61,7 @@ class HistoryView extends StatelessWidget {
|
||||
) =>
|
||||
(context, index) => (index == 0)
|
||||
? const SizedBox.shrink()
|
||||
: HistoryEntryItem(
|
||||
: HistoryEntryTile(
|
||||
entry: data.values.toList()[index - 1],
|
||||
objectKey: data.keys.toList()[index - 1],
|
||||
onDelete: () => build(context),
|
||||
|
@ -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('Library'),
|
||||
item: const BottomNavigationBarItem(
|
||||
label: 'Library',
|
||||
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 [],
|
||||
});
|
||||
}
|
||||
|
78
lib/screens/library/library_content_view.dart
Normal file
78
lib/screens/library/library_content_view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
56
lib/screens/library/library_view.dart
Normal file
56
lib/screens/library/library_view.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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/history_entry.dart';
|
||||
import '../../models/library/library_list.dart';
|
||||
import '../../services/jisho_api/jisho_search.dart';
|
||||
import '../../services/jisho_api/kanji_search.dart';
|
||||
|
||||
@ -23,11 +25,52 @@ 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)
|
||||
|
@ -11,6 +11,8 @@ import '../components/common/denshi_jisho_background.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/open_webpage.dart';
|
||||
import '../services/snackbar.dart';
|
||||
@ -28,15 +30,12 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
bool dataImportIsLoading = false;
|
||||
|
||||
Future<void> clearHistory(context) async {
|
||||
final historyCount = (await db().query(
|
||||
TableNames.historyEntry,
|
||||
columns: ['COUNT(*) AS count'],
|
||||
))[0]['count']! as int;
|
||||
final historyCount = await HistoryEntry.amountOfEntries();
|
||||
|
||||
final bool userIsSure = await confirm(
|
||||
context,
|
||||
content: Text(
|
||||
'Are you sure that you want to delete $historyCount entries?',
|
||||
'Are you sure that you want to clear all $historyCount entries in history?',
|
||||
),
|
||||
);
|
||||
if (!userIsSure) return;
|
||||
@ -45,10 +44,33 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
showSnackbar(context, 'Cleared history');
|
||||
}
|
||||
|
||||
Future<void> clearAll(context) async {
|
||||
final bool userIsSure = await confirm(context);
|
||||
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');
|
||||
}
|
||||
@ -56,7 +78,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
// 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)
|
||||
@ -274,9 +296,8 @@ 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),
|
||||
|
@ -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);
|
||||
|
12
lib/services/jisho_api/kanji_furigana_separation.dart
Normal file
12
lib/services/jisho_api/kanji_furigana_separation.dart
Normal 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;
|
||||
}
|
339
pubspec.lock
339
pubspec.lock
File diff suppressed because it is too large
Load Diff
14
pubspec.yaml
14
pubspec.yaml
@ -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,13 +24,16 @@ dependencies:
|
||||
mdi: ^5.0.0-nullsafety.0
|
||||
path: ^1.8.0
|
||||
path_provider: ^2.0.2
|
||||
share_plus: ^4.0.4
|
||||
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:
|
||||
@ -37,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"
|
||||
|
4
tools/extractDB.sh
Executable file
4
tools/extractDB.sh
Executable 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
4
tools/extractExports.sh
Executable 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
|
Loading…
Reference in New Issue
Block a user