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"
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion flutter.compileSdkVersion
|
compileSdkVersion 33
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
@ -42,7 +42,6 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId "app.jishostudytool.jisho_study_tool"
|
applicationId "app.jishostudytool.jisho_study_tool"
|
||||||
// minSdkVersion flutter.minSdkVersion
|
// minSdkVersion flutter.minSdkVersion
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
// Generated file.
|
// Generated file.
|
||||||
|
//
|
||||||
// If you wish to remove Flutter's multidex support, delete this entire 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;
|
package io.flutter.app;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.CallSuper;
|
import androidx.annotation.CallSuper;
|
||||||
import androidx.multidex.MultiDex;
|
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
|
@Override
|
||||||
@CallSuper
|
@CallSuper
|
||||||
protected void attachBaseContext(Context base) {
|
protected void attachBaseContext(Context base) {
|
||||||
|
|
|
@ -1,32 +1,58 @@
|
||||||
|
|
||||||
CREATE TABLE "JST_LibraryList" (
|
CREATE TABLE "JST_LibraryList" (
|
||||||
"name" TEXT PRIMARY KEY NOT NULL,
|
"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" (
|
CREATE TABLE "JST_LibraryListEntry" (
|
||||||
"listName" TEXT NOT NULL REFERENCES "JST_LibraryList"("name") ON DELETE CASCADE,
|
"listName" TEXT NOT NULL REFERENCES "JST_LibraryList"("name") ON DELETE CASCADE,
|
||||||
"entryText" TEXT NOT NULL,
|
"entryText" TEXT NOT NULL,
|
||||||
"isKanji" BOOLEAN NOT NULL DEFAULT 0,
|
"isKanji" BOOLEAN NOT NULL DEFAULT 0,
|
||||||
"lastModified" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
-- Defaults to unix timestamp in milliseconds
|
||||||
"nextEntry" TEXT NOT NULL,
|
"lastModified" INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
||||||
|
"prevEntryText" TEXT,
|
||||||
|
"prevEntryIsKanji" BOOLEAN NOT NULL DEFAULT 0,
|
||||||
PRIMARY KEY ("listName", "entryText", "isKanji"),
|
PRIMARY KEY ("listName", "entryText", "isKanji"),
|
||||||
FOREIGN KEY ("listName", "nextEntry") REFERENCES "JST_LibraryListEntry"("listName", "entryText"),
|
FOREIGN KEY ("listName", "prevEntryText", "prevEntryIsKanji")
|
||||||
CHECK ((NOT "isKanji") OR ("nextEntry" <> 0))
|
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");
|
-- Useful when doing the recursive ordering statement
|
||||||
|
CREATE INDEX "JST_LibraryListEntry_byListNameAndPrevEntry"
|
||||||
-- CREATE VIEW "JST_LibraryListEntry_sortedByLists" AS
|
ON "JST_LibraryListEntry"("listName", "prevEntryText", "prevEntryIsKanji");
|
||||||
-- WITH RECURSIVE "JST_LibraryListEntry_sorted"("next") AS (
|
|
||||||
-- -- Initial SELECT
|
|
||||||
-- UNION ALL
|
|
||||||
-- SELECT * FROM ""
|
|
||||||
-- -- Recursive Select
|
|
||||||
-- )
|
|
||||||
-- SELECT * FROM "JST_LibraryListEntry_sorted";
|
|
||||||
|
|
||||||
CREATE TABLE "JST_HistoryEntry" (
|
CREATE TABLE "JST_HistoryEntry" (
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
|
||||||
|
@ -47,18 +73,17 @@ CREATE TABLE "JST_HistoryEntryWord" (
|
||||||
|
|
||||||
CREATE TABLE "JST_HistoryEntryTimestamp" (
|
CREATE TABLE "JST_HistoryEntryTimestamp" (
|
||||||
"entryId" INTEGER NOT NULL REFERENCES "JST_HistoryEntry"("id") ON DELETE CASCADE,
|
"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
|
-- Defaults to unix timestamp in milliseconds
|
||||||
-- the easiest way to deal with global and local timeconversion between dart and
|
"timestamp" INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
||||||
-- SQLite.
|
|
||||||
"timestamp" INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY ("entryId", "timestamp")
|
PRIMARY KEY ("entryId", "timestamp")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Useful when ordering entries by the timestamps
|
||||||
CREATE INDEX "JST_HistoryEntryTimestamp_byTimestamp" ON "JST_HistoryEntryTimestamp"("timestamp");
|
CREATE INDEX "JST_HistoryEntryTimestamp_byTimestamp" ON "JST_HistoryEntryTimestamp"("timestamp");
|
||||||
|
|
||||||
CREATE VIEW "JST_HistoryEntry_orderedByTimestamp" AS
|
CREATE VIEW "JST_HistoryEntry_orderedByTimestamp" AS
|
||||||
SELECT * FROM "JST_HistoryEntryTimestamp"
|
SELECT * FROM "JST_HistoryEntryTimestamp"
|
||||||
LEFT JOIN "JST_HistoryEntryWord" USING ("entryId")
|
LEFT JOIN "JST_HistoryEntryWord" USING ("entryId")
|
||||||
LEFT JOIN "JST_HistoryEntryKanji" USING ("entryId")
|
LEFT JOIN "JST_HistoryEntryKanji" USING ("entryId")
|
||||||
GROUP BY "entryId"
|
GROUP BY "entryId"
|
||||||
ORDER BY MAX("timestamp") DESC;
|
ORDER BY MAX("timestamp") DESC;
|
87
flake.lock
87
flake.lock
|
@ -3,17 +3,17 @@
|
||||||
"android-nixpkgs": {
|
"android-nixpkgs": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"devshell": "devshell",
|
"devshell": "devshell",
|
||||||
"flake-utils": "flake-utils_2",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1651782096,
|
"lastModified": 1677183680,
|
||||||
"narHash": "sha256-rrj0HPwmDf6Q14sljnVf2hkMvc97rndgi4PJkFtpFPk=",
|
"narHash": "sha256-xPg1gYyZ8UYNWcQYBtvmmbum3l1hx5cFpoWKrJA15DI=",
|
||||||
"owner": "tadfisher",
|
"owner": "tadfisher",
|
||||||
"repo": "android-nixpkgs",
|
"repo": "android-nixpkgs",
|
||||||
"rev": "ccd2a8f58709ea3413fcb72769b2f62a98332215",
|
"rev": "61410f48b49495f38f835bd79f98c9a0528151dd",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -24,15 +24,21 @@
|
||||||
},
|
},
|
||||||
"devshell": {
|
"devshell": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": [
|
||||||
"nixpkgs": "nixpkgs"
|
"android-nixpkgs",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"android-nixpkgs",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1650900878,
|
"lastModified": 1676293499,
|
||||||
"narHash": "sha256-qhNncMBSa9STnhiLfELEQpYC1L4GrYHNIzyCZ/pilsI=",
|
"narHash": "sha256-uIOTlTxvrXxpKeTvwBI1JGDGtCxMXE3BI0LFwoQMhiQ=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "devshell",
|
"repo": "devshell",
|
||||||
"rev": "d97df53b5ddaa1cfbea7cddbd207eb2634304733",
|
"rev": "71e3022e3ab20bbf1342640547ef5bc14fb43bf4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -43,11 +49,11 @@
|
||||||
},
|
},
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1642700792,
|
"lastModified": 1676283394,
|
||||||
"narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=",
|
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "846b2ae0fc4cc943637d3d1def4454213e203cba",
|
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -58,11 +64,11 @@
|
||||||
},
|
},
|
||||||
"flake-utils_2": {
|
"flake-utils_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1649676176,
|
"lastModified": 1676283394,
|
||||||
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
|
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
|
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -86,35 +92,20 @@
|
||||||
"type": "github"
|
"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": {
|
"nix-dart": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils_4",
|
"flake-utils": "flake-utils_3",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
],
|
],
|
||||||
"pub2nix": "pub2nix"
|
"pub2nix": "pub2nix"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1651781526,
|
"lastModified": 1652213615,
|
||||||
"narHash": "sha256-q01e+S69g4UDrMcEitaQOccr2aHeiJ+VEmPS94h/7WY=",
|
"narHash": "sha256-+eehm2JlhoKgY+Ea4DTxDMei/x4Fgz7S+ZPqWpZysuI=",
|
||||||
"owner": "tadfisher",
|
"owner": "tadfisher",
|
||||||
"repo": "nix-dart",
|
"repo": "nix-dart",
|
||||||
"rev": "71d2fda0f9590d5de917fb736dee312d9fef7e27",
|
"rev": "6f686ddf984306d944e9b5adf9f35f3a0a0a70b7",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -125,32 +116,16 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1643381941,
|
"lastModified": 1677075010,
|
||||||
"narHash": "sha256-pHTwvnN4tTsEKkWlXQ8JMY423epos8wUOhthpwJjtpc=",
|
"narHash": "sha256-X+UmR1AkdR//lPVcShmLy8p1n857IGf7y+cyCArp8bU=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "5efc8ca954272c4376ac929f4c5ffefcc20551d5",
|
"rev": "c95bf18beba4290af25c60cbaaceea1110d0f727",
|
||||||
"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",
|
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"id": "nixpkgs",
|
"id": "nixpkgs",
|
||||||
"ref": "nixos-21.11",
|
"ref": "nixos-22.11",
|
||||||
"type": "indirect"
|
"type": "indirect"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -173,9 +148,9 @@
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"android-nixpkgs": "android-nixpkgs",
|
"android-nixpkgs": "android-nixpkgs",
|
||||||
"flake-utils": "flake-utils_3",
|
"flake-utils": "flake-utils_2",
|
||||||
"nix-dart": "nix-dart",
|
"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";
|
description = "A dictionary app for studying japanese";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "nixpkgs/nixos-21.11";
|
nixpkgs.url = "nixpkgs/nixos-22.11";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
flake-utils = {
|
|
||||||
url = "github:numtide/flake-utils";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
|
|
||||||
android-nixpkgs = {
|
android-nixpkgs = {
|
||||||
url = "github:tadfisher/android-nixpkgs";
|
url = "github:tadfisher/android-nixpkgs";
|
||||||
|
@ -18,11 +14,6 @@
|
||||||
url = "github:tadfisher/nix-dart";
|
url = "github:tadfisher/nix-dart";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
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 }:
|
outputs = { self, nixpkgs, flake-utils, android-nixpkgs, nix-dart }:
|
||||||
|
@ -35,68 +26,37 @@
|
||||||
allowUnfree = true;
|
allowUnfree = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
dartVersion = "2.14.2";
|
|
||||||
dartChannel = "stable";
|
|
||||||
|
|
||||||
flutterVersion = "2.5.1";
|
|
||||||
flutterChannel = "stable";
|
|
||||||
in {
|
in {
|
||||||
|
|
||||||
packages.${system} = {
|
packages.${system} = {
|
||||||
android-sdk = android-nixpkgs.sdk.${system} (sdkPkgs: with sdkPkgs; [
|
android-sdk = android-nixpkgs.sdk.${system} (sdkPkgs: with sdkPkgs; [
|
||||||
cmdline-tools-latest
|
cmdline-tools-latest
|
||||||
|
build-tools-33-0-0
|
||||||
build-tools-32-0-0
|
build-tools-32-0-0
|
||||||
build-tools-31-0-0
|
build-tools-31-0-0
|
||||||
build-tools-30-0-2
|
build-tools-30-0-2
|
||||||
build-tools-29-0-2
|
build-tools-29-0-2
|
||||||
platform-tools
|
platform-tools
|
||||||
|
platforms-android-33
|
||||||
platforms-android-32
|
platforms-android-32
|
||||||
platforms-android-31
|
platforms-android-31
|
||||||
platforms-android-30
|
platforms-android-30
|
||||||
platforms-android-29
|
platforms-android-29
|
||||||
emulator
|
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
|
devShell.${system} = let
|
||||||
inherit (pkgs) lcov google-chrome sqlite sqlite-web;
|
inherit (pkgs) lcov google-chrome sqlite sqlite-web flutter dart;
|
||||||
inherit (self.packages.${system}) android-sdk flutter dart;
|
jdk = pkgs.jdk11;
|
||||||
|
|
||||||
|
inherit (self.packages.${system}) android-sdk;
|
||||||
inherit (nix-dart.packages.${system}) pub2nix-lock;
|
inherit (nix-dart.packages.${system}) pub2nix-lock;
|
||||||
java = pkgs.jdk8;
|
|
||||||
in pkgs.mkShell rec {
|
in pkgs.mkShell rec {
|
||||||
ANDROID_JAVA_HOME="${java.home}";
|
ANDROID_JAVA_HOME="${jdk.home}";
|
||||||
ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk";
|
ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk";
|
||||||
CHROME_EXECUTABLE = "${google-chrome}/bin/google-chrome-stable";
|
CHROME_EXECUTABLE = "${google-chrome}/bin/google-chrome-stable";
|
||||||
FLUTTER_SDK="${flutter}";
|
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}";
|
JAVA_HOME="${ANDROID_JAVA_HOME}";
|
||||||
USE_CCACHE=0;
|
USE_CCACHE=0;
|
||||||
|
|
||||||
|
@ -105,7 +65,7 @@
|
||||||
dart
|
dart
|
||||||
flutter
|
flutter
|
||||||
google-chrome
|
google-chrome
|
||||||
java
|
jdk
|
||||||
lcov
|
lcov
|
||||||
pub2nix-lock
|
pub2nix-lock
|
||||||
sqlite
|
sqlite
|
||||||
|
|
|
@ -20,7 +20,7 @@ class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
final bool autoThemeIsDark =
|
final bool autoThemeIsDark =
|
||||||
SchedulerBinding.instance!.window.platformBrightness == Brightness.dark;
|
SchedulerBinding.instance?.window.platformBrightness == Brightness.dark;
|
||||||
|
|
||||||
add(
|
add(
|
||||||
SetTheme(
|
SetTheme(
|
||||||
|
|
|
@ -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 '../../bloc/theme/theme_bloc.dart';
|
||||||
import '../../services/handwriting.dart';
|
import '../../services/handwriting.dart';
|
||||||
|
import '../../services/snackbar.dart';
|
||||||
import '../../settings.dart';
|
import '../../settings.dart';
|
||||||
|
|
||||||
class DrawingBoard extends StatefulWidget {
|
class DrawingBoard extends StatefulWidget {
|
||||||
|
@ -182,10 +183,9 @@ class _DrawingBoardState extends State<DrawingBoard> {
|
||||||
),
|
),
|
||||||
if (!widget.onlyOneCharacterSuggestions)
|
if (!widget.onlyOneCharacterSuggestions)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
|
onPressed: () => showSnackbar(
|
||||||
const SnackBar(
|
context,
|
||||||
content: Text('TODO: implement scrolling page feature!'),
|
'TODO: implement scrolling page feature!',
|
||||||
),
|
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.arrow_forward),
|
icon: const Icon(Icons.arrow_forward),
|
||||||
),
|
),
|
||||||
|
|
|
@ -4,18 +4,17 @@ import 'package:flutter_slidable/flutter_slidable.dart';
|
||||||
import '../../models/history/history_entry.dart';
|
import '../../models/history/history_entry.dart';
|
||||||
import '../../routing/routes.dart';
|
import '../../routing/routes.dart';
|
||||||
import '../../services/datetime.dart';
|
import '../../services/datetime.dart';
|
||||||
import '../../services/snackbar.dart';
|
|
||||||
import '../../settings.dart';
|
import '../../settings.dart';
|
||||||
|
import '../common/kanji_box.dart';
|
||||||
import '../common/loading.dart';
|
import '../common/loading.dart';
|
||||||
import 'kanji_box.dart';
|
|
||||||
|
|
||||||
class HistoryEntryItem extends StatelessWidget {
|
class HistoryEntryTile extends StatelessWidget {
|
||||||
final HistoryEntry entry;
|
final HistoryEntry entry;
|
||||||
final int objectKey;
|
final int objectKey;
|
||||||
final void Function()? onDelete;
|
final void Function()? onDelete;
|
||||||
final void Function()? onFavourite;
|
final void Function()? onFavourite;
|
||||||
|
|
||||||
const HistoryEntryItem({
|
const HistoryEntryTile({
|
||||||
required this.entry,
|
required this.entry,
|
||||||
required this.objectKey,
|
required this.objectKey,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
|
@ -23,10 +22,6 @@ class HistoryEntryItem extends StatelessWidget {
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
Widget get _child => (entry.isKanji)
|
|
||||||
? KanjiBox(kanji: entry.kanji!)
|
|
||||||
: Text(entry.word!);
|
|
||||||
|
|
||||||
void Function() _onTap(context) => entry.isKanji
|
void Function() _onTap(context) => entry.isKanji
|
||||||
? () => Navigator.pushNamed(
|
? () => Navigator.pushNamed(
|
||||||
context,
|
context,
|
||||||
|
@ -46,8 +41,7 @@ class HistoryEntryItem extends StatelessWidget {
|
||||||
future: entry.timestamps,
|
future: entry.timestamps,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
// TODO: provide proper error handling
|
// TODO: provide proper error handling
|
||||||
if (snapshot.hasError)
|
if (snapshot.hasError) return ErrorWidget(snapshot.error!);
|
||||||
return ErrorWidget(snapshot.error!);
|
|
||||||
if (!snapshot.hasData) return const LoadingScreen();
|
if (!snapshot.hasData) return const LoadingScreen();
|
||||||
return ListView(
|
return ListView(
|
||||||
children: snapshot.data!
|
children: snapshot.data!
|
||||||
|
@ -69,14 +63,6 @@ class HistoryEntryItem extends StatelessWidget {
|
||||||
icon: Icons.access_time,
|
icon: Icons.access_time,
|
||||||
onPressed: (_) => Navigator.push(context, timestamps),
|
onPressed: (_) => Navigator.push(context, timestamps),
|
||||||
),
|
),
|
||||||
SlidableAction(
|
|
||||||
backgroundColor: Colors.yellow,
|
|
||||||
icon: Icons.star,
|
|
||||||
onPressed: (_) {
|
|
||||||
showSnackbar(context, 'TODO: implement favourites');
|
|
||||||
onFavourite?.call();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SlidableAction(
|
SlidableAction(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
icon: Icons.delete,
|
icon: Icons.delete,
|
||||||
|
@ -107,7 +93,12 @@ class HistoryEntryItem extends StatelessWidget {
|
||||||
),
|
),
|
||||||
DefaultTextStyle.merge(
|
DefaultTextStyle.merge(
|
||||||
style: japaneseFont.textStyle,
|
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/examples.dart';
|
||||||
import './kanji_result_body/grade.dart';
|
import './kanji_result_body/grade.dart';
|
||||||
import './kanji_result_body/header.dart';
|
|
||||||
import './kanji_result_body/jlpt_level.dart';
|
import './kanji_result_body/jlpt_level.dart';
|
||||||
import './kanji_result_body/radical.dart';
|
import './kanji_result_body/radical.dart';
|
||||||
import './kanji_result_body/rank.dart';
|
import './kanji_result_body/rank.dart';
|
||||||
import './kanji_result_body/stroke_order_gif.dart';
|
import './kanji_result_body/stroke_order_gif.dart';
|
||||||
import './kanji_result_body/yomi_chips.dart';
|
import './kanji_result_body/yomi_chips.dart';
|
||||||
|
import '../../bloc/theme/theme_bloc.dart';
|
||||||
import '../../services/kanji_grade_conversion.dart';
|
import '../../services/kanji_grade_conversion.dart';
|
||||||
|
import '../common/kanji_box.dart';
|
||||||
|
|
||||||
class KanjiResultBody extends StatelessWidget {
|
class KanjiResultBody extends StatelessWidget {
|
||||||
late final String query;
|
late final String query;
|
||||||
|
@ -36,9 +37,22 @@ class KanjiResultBody extends StatelessWidget {
|
||||||
child: SizedBox(),
|
child: SizedBox(),
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
fit: FlexFit.tight,
|
child: AspectRatio(
|
||||||
child: Center(child: Header(kanji: query)),
|
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(
|
Flexible(
|
||||||
fit: FlexFit.tight,
|
fit: FlexFit.tight,
|
||||||
child: Center(
|
child: Center(
|
||||||
|
@ -81,6 +95,7 @@ class KanjiResultBody extends StatelessWidget {
|
||||||
return ListView(
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
headerRow,
|
headerRow,
|
||||||
|
// TODO: handle case where meaning is empty. See 牃 for example
|
||||||
YomiChips(yomi: resultData.meaning.split(', '), type: YomiType.meaning),
|
YomiChips(yomi: resultData.meaning.split(', '), type: YomiType.meaning),
|
||||||
(resultData.onyomi.isNotEmpty)
|
(resultData.onyomi.isNotEmpty)
|
||||||
? YomiChips(yomi: resultData.onyomi, type: YomiType.onyomi)
|
? YomiChips(yomi: resultData.onyomi, type: YomiType.onyomi)
|
||||||
|
@ -101,6 +116,7 @@ class KanjiResultBody extends StatelessWidget {
|
||||||
onyomi: resultData.onyomiExamples,
|
onyomi: resultData.onyomiExamples,
|
||||||
kunyomi: resultData.kunyomiExamples,
|
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;
|
final colors = state.theme.kanjiResultColor;
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => Navigator.pushNamed(context, Routes.kanjiSearchRadicals, arguments: radical.symbol),
|
onTap: () => Navigator.pushNamed(
|
||||||
|
context,
|
||||||
|
Routes.kanjiSearchRadicals,
|
||||||
|
arguments: radical.symbol,
|
||||||
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(15.0),
|
padding: const EdgeInsets.all(15.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
|
@ -22,6 +22,7 @@ class StrokeOrderGif extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10.0),
|
borderRadius: BorderRadius.circular(10.0),
|
||||||
|
// TODO: show some kind of default icon if GIF is missing.
|
||||||
child: Image.network(uri),
|
child: Image.network(uri),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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:flutter/material.dart';
|
||||||
|
import 'package:ruby_text/ruby_text.dart';
|
||||||
import 'package:unofficial_jisho_api/api.dart';
|
import 'package:unofficial_jisho_api/api.dart';
|
||||||
|
|
||||||
|
import '../../../../services/jisho_api/kanji_furigana_separation.dart';
|
||||||
import '../../../../services/romaji_transliteration.dart';
|
import '../../../../services/romaji_transliteration.dart';
|
||||||
import '../../../../settings.dart';
|
import '../../../../settings.dart';
|
||||||
|
|
||||||
|
@ -25,30 +27,13 @@ class JapaneseHeader extends StatelessWidget {
|
||||||
return Container(
|
return Container(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
padding: const EdgeInsets.only(left: 10.0),
|
padding: const EdgeInsets.only(left: 10.0),
|
||||||
child: Column(
|
child: RubySpanWidget(
|
||||||
children: [
|
RubyTextData(
|
||||||
// Both wordReading and word.word being present implies that the word has furigana.
|
word.kanji,
|
||||||
// If that's not the case, then the word is usually present in wordReading.
|
ruby: word.furigana,
|
||||||
// However, there are some exceptions where the reading is placed in word.
|
style: romajiEnabled ? null : japaneseFont.textStyle,
|
||||||
// I have no clue why this might be the case.
|
rubyStyle: romajiEnabled ? null : japaneseFont.textStyle,
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../../../bloc/theme/theme_bloc.dart';
|
|
||||||
import '../../../../routing/routes.dart';
|
import '../../../../routing/routes.dart';
|
||||||
import '../../../../settings.dart';
|
import '../../../common/kanji_box.dart';
|
||||||
|
|
||||||
class KanjiRow extends StatelessWidget {
|
class KanjiRow extends StatelessWidget {
|
||||||
final List<String> kanji;
|
final List<String> kanji;
|
||||||
|
@ -13,36 +12,6 @@ class KanjiRow extends StatelessWidget {
|
||||||
this.fontSize = 20,
|
this.fontSize = 20,
|
||||||
}) : super(key: key);
|
}) : 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
|
@ -64,7 +33,10 @@ class KanjiRow extends StatelessWidget {
|
||||||
Routes.kanjiSearch,
|
Routes.kanjiSearch,
|
||||||
arguments: k,
|
arguments: k,
|
||||||
),
|
),
|
||||||
child: _kanjiBox(k),
|
child: KanjiBox.headline4(
|
||||||
|
context: context,
|
||||||
|
kanji: k,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
import 'package:flutter/material.dart';
|
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 'package:unofficial_jisho_api/api.dart';
|
||||||
|
|
||||||
import './parts/common_badge.dart';
|
import '../../../models/library/library_list.dart';
|
||||||
import './parts/header.dart';
|
import '../../../services/jisho_api/kanji_furigana_separation.dart';
|
||||||
import './parts/jlpt_badge.dart';
|
import '../../../services/kanji_regex.dart';
|
||||||
import './parts/other_forms.dart';
|
|
||||||
import './parts/senses.dart';
|
|
||||||
import './parts/wanikani_badge.dart';
|
|
||||||
import '../../../settings.dart';
|
import '../../../settings.dart';
|
||||||
|
import '../../library/add_to_library_dialog.dart';
|
||||||
import 'parts/audio_player.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/kanji.dart';
|
||||||
import 'parts/links.dart';
|
import 'parts/links.dart';
|
||||||
import 'parts/notes.dart';
|
import 'parts/notes.dart';
|
||||||
|
import 'parts/other_forms.dart';
|
||||||
|
import 'parts/senses.dart';
|
||||||
|
import 'parts/wanikani_badge.dart';
|
||||||
|
|
||||||
class SearchResultCard extends StatefulWidget {
|
class SearchResultCard extends StatefulWidget {
|
||||||
final JishoResult result;
|
final JishoResult result;
|
||||||
|
@ -31,20 +35,11 @@ class SearchResultCard extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SearchResultCardState extends State<SearchResultCard> {
|
class _SearchResultCardState extends State<SearchResultCard> {
|
||||||
|
static const _margin = SizedBox(height: 20);
|
||||||
PhrasePageScrapeResultData? extraData;
|
PhrasePageScrapeResultData? extraData;
|
||||||
|
|
||||||
bool? extraDataSearchFailed;
|
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 =>
|
bool get hasAttribution =>
|
||||||
widget.result.attribution.jmdict ||
|
widget.result.attribution.jmdict ||
|
||||||
widget.result.attribution.jmnedict ||
|
widget.result.attribution.jmnedict ||
|
||||||
|
@ -67,30 +62,77 @@ class _SearchResultCardState extends State<SearchResultCard> {
|
||||||
.toSet()
|
.toSet()
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
Widget get _header => IntrinsicWidth(
|
List<JishoSenseLink> get links =>
|
||||||
child: Row(
|
[for (final sense in widget.result.senses) ...sense.links];
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
Widget get _header => Row(
|
||||||
JapaneseHeader(word: widget.mainWord),
|
children: [
|
||||||
Row(
|
Expanded(child: JapaneseHeader(word: widget.mainWord)),
|
||||||
children: [
|
WKBadge(
|
||||||
WKBadge(
|
level: widget.result.tags.firstWhere(
|
||||||
level: widget.result.tags.firstWhere(
|
(tag) => tag.contains('wanikani'),
|
||||||
(tag) => tag.contains('wanikani'),
|
orElse: () => '',
|
||||||
orElse: () => '',
|
),
|
||||||
),
|
),
|
||||||
),
|
JLPTBadge(jlptLevel: jlptLevel),
|
||||||
JLPTBadge(jlptLevel: jlptLevel),
|
CommonBadge(isCommon: widget.result.isCommon ?? false)
|
||||||
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(
|
Widget _body({PhrasePageScrapeResultData? extendedData}) => Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
|
||||||
|
@ -123,34 +165,13 @@ class _SearchResultCardState extends State<SearchResultCard> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
Future<PhrasePageScrapeResult?> _scrape(JishoResult result) =>
|
||||||
Widget build(BuildContext context) {
|
(!(result.japanese[0].word == null && result.japanese[0].reading == null))
|
||||||
final backgroundColor = Theme.of(context).scaffoldBackgroundColor;
|
? scrapeForPhrase(
|
||||||
|
widget.result.japanese[0].word ??
|
||||||
|
widget.result.japanese[0].reading!,
|
||||||
|
)
|
||||||
|
: Future(() => null);
|
||||||
|
|
||||||
return ExpansionTile(
|
List<Widget> _withMargin(Widget w) => [_margin, w];
|
||||||
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()
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,36 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
// Example file Structure:
|
// Example file Structure:
|
||||||
// jisho_data_22.01.01_1
|
// jisho_data_2022.01.01_1
|
||||||
// - history.json
|
// - history.json
|
||||||
// - library/
|
// - library/
|
||||||
// - lista.json
|
// - lista.json
|
||||||
// - listb.json
|
// - listb.json
|
||||||
|
|
||||||
extension ArchiveFormat on Directory {
|
extension ArchiveFormat on Directory {
|
||||||
// TODO: make the export dir dependent on date
|
|
||||||
Directory get exportDirectory {
|
Directory get exportDirectory {
|
||||||
final dir = Directory(uri.resolve('export').path);
|
final dir = Directory(uri.resolve('export').path);
|
||||||
dir.createSync(recursive: true);
|
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);
|
File get historyFile => File(uri.resolve('history.json').path);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
@ -41,7 +42,8 @@ Future<void> migrate(Database db, int oldVersion, int newVersion) async {
|
||||||
log(
|
log(
|
||||||
'Migrating database from v$i to v${i + 1} with File(${migrations[i - 1]})',
|
'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
|
migrationContent
|
||||||
.split(';')
|
.split(';')
|
||||||
|
@ -58,24 +60,25 @@ Future<void> migrate(Database db, int oldVersion, int newVersion) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setupDatabase() async {
|
Future<void> setupDatabase() async {
|
||||||
databaseFactory.debugSetLogLevel(sqfliteLogLevelSql);
|
if (kDebugMode) {
|
||||||
|
databaseFactory.debugSetLogLevel(sqfliteLogLevelSql);
|
||||||
|
}
|
||||||
|
|
||||||
final Database database = await openDatabase(
|
final Database database = await openDatabase(
|
||||||
await databasePath(),
|
await databasePath(),
|
||||||
version: 1,
|
version: 1,
|
||||||
onCreate: (db, version) => migrate(db, 0, version),
|
onCreate: (db, version) => migrate(db, 0, version),
|
||||||
onUpgrade: migrate,
|
onUpgrade: migrate,
|
||||||
onOpen: (db) => Future.wait([
|
onOpen: (db) => Future.wait([db.execute('PRAGMA foreign_keys=ON')]),
|
||||||
db.execute('PRAGMA foreign_keys=ON')
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
GetIt.instance.registerSingleton<Database>(database);
|
GetIt.instance.registerSingleton<Database>(database);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> resetDatabase() async {
|
Future<void> resetDatabase() async {
|
||||||
await db().close();
|
await db().close();
|
||||||
File(await databasePath()).deleteSync();
|
File(await databasePath()).deleteSync();
|
||||||
GetIt.instance.unregister<Database>();
|
GetIt.instance.unregister<Database>();
|
||||||
await setupDatabase();
|
await setupDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
class TableNames {
|
class TableNames {
|
||||||
|
@ -101,7 +104,7 @@ class TableNames {
|
||||||
|
|
||||||
/// Attributes:
|
/// Attributes:
|
||||||
/// - name TEXT
|
/// - name TEXT
|
||||||
/// - nextList TEXT
|
/// - prevList TEXT
|
||||||
static const String libraryList = 'JST_LibraryList';
|
static const String libraryList = 'JST_LibraryList';
|
||||||
|
|
||||||
/// Attributes:
|
/// Attributes:
|
||||||
|
@ -109,12 +112,17 @@ class TableNames {
|
||||||
/// - entryText TEXT
|
/// - entryText TEXT
|
||||||
/// - isKanji BOOLEAN
|
/// - isKanji BOOLEAN
|
||||||
/// - lastModified TIMESTAMP
|
/// - lastModified TIMESTAMP
|
||||||
/// - nextEntry TEXT
|
/// - prevEntryText TEXT
|
||||||
|
/// - prevEntryIsKanji BOOLEAN
|
||||||
static const String libraryListEntry = 'JST_LibraryListEntry';
|
static const String libraryListEntry = 'JST_LibraryListEntry';
|
||||||
|
|
||||||
///////////
|
///////////
|
||||||
// VIEWS //
|
// VIEWS //
|
||||||
///////////
|
///////////
|
||||||
|
|
||||||
|
/// Attributes:
|
||||||
|
/// - name TEXT
|
||||||
|
static const String libraryListOrdered = 'JST_LibraryListOrdered';
|
||||||
|
|
||||||
/// Attributes:
|
/// Attributes:
|
||||||
/// - entryId INTEGER
|
/// - entryId INTEGER
|
||||||
|
|
|
@ -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 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
import '../models/history/history_entry.dart';
|
import '../models/history/history_entry.dart';
|
||||||
|
import '../models/library/library_list.dart';
|
||||||
import 'archive_format.dart';
|
import 'archive_format.dart';
|
||||||
import 'database.dart';
|
import 'database.dart';
|
||||||
|
|
||||||
Future<Directory> exportDirectory() async {
|
Future<Directory> exportDirectory() async =>
|
||||||
final basedir = (await getExternalStorageDirectory())!;
|
(await getExternalStorageDirectory())!.exportDirectory;
|
||||||
final dir = basedir.exportDirectory;
|
|
||||||
dir.createSync(recursive: true);
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the path to which the data was saved.
|
/// Returns the path to which the data was saved.
|
||||||
Future<String> exportData() async {
|
Future<String> exportData() async {
|
||||||
|
@ -33,13 +30,21 @@ Future<void> exportHistoryTo(Directory dir) async {
|
||||||
final query = await db().query(TableNames.historyEntryOrderedByTimestamp);
|
final query = await db().query(TableNames.historyEntryOrderedByTimestamp);
|
||||||
final List<HistoryEntry> entries =
|
final List<HistoryEntry> entries =
|
||||||
query.map((e) => HistoryEntry.fromDBMap(e)).toList();
|
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 =
|
final List<Map<String, Object?>> jsonEntries =
|
||||||
await Future.wait(entries.map((he) async => he.toJson()));
|
await Future.wait(entries.map((he) async => he.toJson()));
|
||||||
file.writeAsStringSync(jsonEncode(jsonEntries));
|
file.writeAsStringSync(jsonEncode(jsonEntries));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> exportLibraryListsTo(Directory dir) async {
|
Future<void> exportLibraryListsTo(Directory dir) async => Future.wait(
|
||||||
// TODO:
|
(await LibraryList.allLibraries).map((lib) async {
|
||||||
// final query = db().query(TableNames.libraryList);
|
final file = File(dir.uri.resolve('${lib.name}.json').path);
|
||||||
print('TODO: implement exportLibraryLists');
|
file.createSync();
|
||||||
}
|
final entries = await lib.entries;
|
||||||
|
file.writeAsStringSync(jsonEncode(entries));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import '../models/history/history_entry.dart';
|
import '../models/history/history_entry.dart';
|
||||||
|
@ -13,11 +14,11 @@ Future<void> importData(Directory dir) async {
|
||||||
|
|
||||||
Future<void> importHistoryFrom(File file) async {
|
Future<void> importHistoryFrom(File file) async {
|
||||||
final String content = file.readAsStringSync();
|
final String content = file.readAsStringSync();
|
||||||
await HistoryEntry.insertJsonEntries(
|
final List<Map<String, Object?>> json = (jsonDecode(content) as List)
|
||||||
(jsonDecode(content) as List)
|
.map((h) => h as Map<String, Object?>)
|
||||||
.map((h) => h as Map<String, Object?>)
|
.toList();
|
||||||
.toList(),
|
log('Importing ${json.length} entries from ${file.path}');
|
||||||
);
|
await HistoryEntry.insertJsonEntries(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> importLibraryListsFrom(Directory libraryListsDir) async {
|
Future<void> importLibraryListsFrom(Directory libraryListsDir) async {
|
||||||
|
|
|
@ -267,7 +267,7 @@ class HistoryEntry {
|
||||||
|
|
||||||
/// An efficient implementation of [insertJsonEntry] for multiple
|
/// An efficient implementation of [insertJsonEntry] for multiple
|
||||||
/// entries.
|
/// entries.
|
||||||
///
|
///
|
||||||
/// This assumes that there are no duplicates within the elements
|
/// This assumes that there are no duplicates within the elements
|
||||||
/// in the json.
|
/// in the json.
|
||||||
static Future<List<HistoryEntry>> insertJsonEntries(
|
static Future<List<HistoryEntry>> insertJsonEntries(
|
||||||
|
@ -348,6 +348,14 @@ class HistoryEntry {
|
||||||
return entries;
|
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 =>
|
static Future<List<HistoryEntry>> get fromDB async =>
|
||||||
(await db().query(TableNames.historyEntryOrderedByTimestamp))
|
(await db().query(TableNames.historyEntryOrderedByTimestamp))
|
||||||
.map((e) => HistoryEntry.fromDBMap(e))
|
.map((e) => HistoryEntry.fromDBMap(e))
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -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() {
|
ThemeData getMaterialTheme() {
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
brightness: Brightness.dark,
|
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 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../models/library/library_list.dart';
|
||||||
import '../screens/home.dart';
|
import '../screens/home.dart';
|
||||||
import '../screens/info/about.dart';
|
import '../screens/info/about.dart';
|
||||||
import '../screens/info/licenses.dart';
|
import '../screens/info/licenses.dart';
|
||||||
|
import '../screens/library/library_content_view.dart';
|
||||||
import '../screens/search/result_page.dart';
|
import '../screens/search/result_page.dart';
|
||||||
import '../screens/search/search_mechanisms/drawing.dart';
|
import '../screens/search/search_mechanisms/drawing.dart';
|
||||||
import '../screens/search/search_mechanisms/grade_list.dart';
|
import '../screens/search/search_mechanisms/grade_list.dart';
|
||||||
|
@ -40,6 +42,12 @@ Route<Widget> generateRoute(RouteSettings settings) {
|
||||||
builder: (_) => KanjiRadicalSearch(prechosenRadical: prechosenRadical),
|
builder: (_) => KanjiRadicalSearch(prechosenRadical: prechosenRadical),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case Routes.libraryContent:
|
||||||
|
final library = args! as LibraryList;
|
||||||
|
return MaterialPageRoute(
|
||||||
|
builder: (_) => LibraryContentView(library: library),
|
||||||
|
);
|
||||||
|
|
||||||
case Routes.about:
|
case Routes.about:
|
||||||
return MaterialPageRoute(builder: (_) => const AboutView());
|
return MaterialPageRoute(builder: (_) => const AboutView());
|
||||||
case Routes.aboutLicenses:
|
case Routes.aboutLicenses:
|
||||||
|
|
|
@ -5,6 +5,7 @@ class Routes {
|
||||||
static const String kanjiSearchDraw = '/kanjiSearch/draw';
|
static const String kanjiSearchDraw = '/kanjiSearch/draw';
|
||||||
static const String kanjiSearchGrade = '/kanjiSearch/grade';
|
static const String kanjiSearchGrade = '/kanjiSearch/grade';
|
||||||
static const String kanjiSearchRadicals = '/kanjiSearch/radicals';
|
static const String kanjiSearchRadicals = '/kanjiSearch/radicals';
|
||||||
|
static const String libraryContent = '/library';
|
||||||
static const String about = '/info/about';
|
static const String about = '/info/about';
|
||||||
static const String aboutLicenses = '/info/licenses';
|
static const String aboutLicenses = '/info/licenses';
|
||||||
static const String errorNotFound = '/error/404';
|
static const String errorNotFound = '/error/404';
|
||||||
|
|
|
@ -1,27 +1,58 @@
|
||||||
import 'package:flutter/material.dart';
|
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 {
|
class DebugView extends StatelessWidget {
|
||||||
const DebugView({Key? key}) : super(key: key);
|
const DebugView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
// return const Center(child: KanjiBox(kanji: '漢'));
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
DrawingBoard(
|
Row(
|
||||||
allowHiragana: true,
|
children: [
|
||||||
allowKatakana: true,
|
KanjiBox.withPadding(kanji: '漢', padding: 5),
|
||||||
allowOther: true,
|
KanjiBox.withFontSize(kanji: '漢', fontSize: 20),
|
||||||
onSuggestionChosen: (s) => ScaffoldMessenger.of(context).showSnackBar(
|
const KanjiBox.withFontSizeAndPadding(
|
||||||
SnackBar(
|
kanji: '漢',
|
||||||
content: Text('Chose: $s'),
|
fontSize: 40,
|
||||||
duration: const Duration(milliseconds: 600),
|
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/loading.dart';
|
||||||
import '../components/common/opaque_box.dart';
|
import '../components/common/opaque_box.dart';
|
||||||
import '../components/history/date_divider.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 '../models/history/history_entry.dart';
|
||||||
import '../services/datetime.dart';
|
import '../services/datetime.dart';
|
||||||
|
|
||||||
|
@ -45,10 +45,15 @@ class HistoryView extends StatelessWidget {
|
||||||
final HistoryEntry search = data[index];
|
final HistoryEntry search = data[index];
|
||||||
final DateTime searchDate = search.lastTimestamp;
|
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 TextDivider(text: formatDate(roundToDay(searchDate)));
|
||||||
|
|
||||||
return const Divider(height: 0);
|
return const Divider(
|
||||||
|
height: 0,
|
||||||
|
indent: 10,
|
||||||
|
endIndent: 10,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Widget Function(BuildContext, int) historyEntryWithData(
|
Widget Function(BuildContext, int) historyEntryWithData(
|
||||||
|
@ -56,7 +61,7 @@ class HistoryView extends StatelessWidget {
|
||||||
) =>
|
) =>
|
||||||
(context, index) => (index == 0)
|
(context, index) => (index == 0)
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: HistoryEntryItem(
|
: HistoryEntryTile(
|
||||||
entry: data.values.toList()[index - 1],
|
entry: data.values.toList()[index - 1],
|
||||||
objectKey: data.keys.toList()[index - 1],
|
objectKey: data.keys.toList()[index - 1],
|
||||||
onDelete: () => build(context),
|
onDelete: () => build(context),
|
||||||
|
|
|
@ -4,8 +4,10 @@ import 'package:mdi/mdi.dart';
|
||||||
|
|
||||||
import '../bloc/theme/theme_bloc.dart';
|
import '../bloc/theme/theme_bloc.dart';
|
||||||
import '../components/common/denshi_jisho_background.dart';
|
import '../components/common/denshi_jisho_background.dart';
|
||||||
|
import '../components/library/new_library_dialog.dart';
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'history.dart';
|
import 'history.dart';
|
||||||
|
import 'library/library_view.dart';
|
||||||
import 'search/kanji_view.dart';
|
import 'search/kanji_view.dart';
|
||||||
import 'search/search_view.dart';
|
import 'search/search_view.dart';
|
||||||
import 'settings.dart';
|
import 'settings.dart';
|
||||||
|
@ -20,25 +22,35 @@ class Home extends StatefulWidget {
|
||||||
class _HomeState extends State<Home> {
|
class _HomeState extends State<Home> {
|
||||||
int pageNum = 0;
|
int pageNum = 0;
|
||||||
|
|
||||||
|
_Page get page => pages[pageNum];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ThemeBloc, ThemeState>(
|
return BlocBuilder<ThemeBloc, ThemeState>(
|
||||||
builder: (context, themeState) {
|
builder: (context, themeState) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: pages[pageNum].titleBar,
|
title: Text(page.titleBar),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
backgroundColor: AppTheme.jishoGreen.background,
|
backgroundColor: AppTheme.jishoGreen.background,
|
||||||
foregroundColor: AppTheme.jishoGreen.foreground,
|
foregroundColor: AppTheme.jishoGreen.foreground,
|
||||||
|
actions: page.actions,
|
||||||
),
|
),
|
||||||
body: DenshiJishoBackground(child: pages[pageNum].content),
|
body: DenshiJishoBackground(child: page.content),
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
fixedColor: AppTheme.jishoGreen.background,
|
fixedColor: AppTheme.jishoGreen.background,
|
||||||
currentIndex: pageNum,
|
currentIndex: pageNum,
|
||||||
onTap: (index) => setState(() {
|
onTap: (index) => setState(() {
|
||||||
pageNum = index;
|
pageNum = index;
|
||||||
}),
|
}),
|
||||||
items: pages.map((p) => p.item).toList(),
|
items: pages
|
||||||
|
.map(
|
||||||
|
(p) => BottomNavigationBarItem(
|
||||||
|
label: p.titleBar,
|
||||||
|
icon: p.icon,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
showSelectedLabels: false,
|
showSelectedLabels: false,
|
||||||
showUnselectedLabels: false,
|
showUnselectedLabels: false,
|
||||||
unselectedItemColor: themeState.theme.menuGreyDark.background,
|
unselectedItemColor: themeState.theme.menuGreyDark.background,
|
||||||
|
@ -51,52 +63,40 @@ class _HomeState extends State<Home> {
|
||||||
List<_Page> get pages => [
|
List<_Page> get pages => [
|
||||||
const _Page(
|
const _Page(
|
||||||
content: SearchView(),
|
content: SearchView(),
|
||||||
titleBar: Text('Search'),
|
titleBar: 'Search',
|
||||||
item: BottomNavigationBarItem(
|
icon: Icon(Icons.search),
|
||||||
label: 'Search',
|
|
||||||
icon: Icon(Icons.search),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const _Page(
|
const _Page(
|
||||||
content: KanjiView(),
|
content: KanjiView(),
|
||||||
titleBar: Text('Kanji Search'),
|
titleBar: 'Kanji Search',
|
||||||
item: BottomNavigationBarItem(
|
icon: Icon(Mdi.ideogramCjk, size: 30),
|
||||||
label: 'Kanji',
|
|
||||||
icon: Icon(Mdi.ideogramCjk, size: 30),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const _Page(
|
const _Page(
|
||||||
content: HistoryView(),
|
content: HistoryView(),
|
||||||
titleBar: Text('History'),
|
titleBar: 'History',
|
||||||
item: BottomNavigationBarItem(
|
icon: Icon(Icons.history),
|
||||||
label: 'History',
|
|
||||||
icon: Icon(Icons.history),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
_Page(
|
_Page(
|
||||||
content: Container(),
|
content: const LibraryView(),
|
||||||
titleBar: const Text('Library'),
|
titleBar: 'Library',
|
||||||
item: const BottomNavigationBarItem(
|
icon: const Icon(Icons.bookmark),
|
||||||
label: 'Library',
|
actions: [
|
||||||
icon: Icon(Icons.bookmark),
|
IconButton(
|
||||||
),
|
onPressed: showNewLibraryDialog(context),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const _Page(
|
const _Page(
|
||||||
content: SettingsView(),
|
content: SettingsView(),
|
||||||
titleBar: Text('Settings'),
|
titleBar: 'Settings',
|
||||||
item: BottomNavigationBarItem(
|
icon: Icon(Icons.settings),
|
||||||
label: 'Settings',
|
|
||||||
icon: Icon(Icons.settings),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (kDebugMode) ...[
|
if (kDebugMode) ...[
|
||||||
const _Page(
|
const _Page(
|
||||||
content: DebugView(),
|
content: DebugView(),
|
||||||
titleBar: Text('Debug Page'),
|
titleBar: 'Debug Page',
|
||||||
item: BottomNavigationBarItem(
|
icon: Icon(Icons.biotech),
|
||||||
label: 'Debug',
|
|
||||||
icon: Icon(Icons.biotech),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
@ -104,12 +104,14 @@ class _HomeState extends State<Home> {
|
||||||
|
|
||||||
class _Page {
|
class _Page {
|
||||||
final Widget content;
|
final Widget content;
|
||||||
final Widget titleBar;
|
final String titleBar;
|
||||||
final BottomNavigationBarItem item;
|
final Icon icon;
|
||||||
|
final List<Widget> actions;
|
||||||
|
|
||||||
const _Page({
|
const _Page({
|
||||||
required this.content,
|
required this.content,
|
||||||
required this.titleBar,
|
required this.titleBar,
|
||||||
required this.item,
|
required this.icon,
|
||||||
|
this.actions = const [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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/common/loading.dart';
|
||||||
import '../../components/kanji/kanji_result_body.dart';
|
import '../../components/kanji/kanji_result_body.dart';
|
||||||
|
import '../../components/library/add_to_library_dialog.dart';
|
||||||
import '../../components/search/search_result_body.dart';
|
import '../../components/search/search_result_body.dart';
|
||||||
import '../../models/history/history_entry.dart';
|
import '../../models/history/history_entry.dart';
|
||||||
|
import '../../models/library/library_list.dart';
|
||||||
import '../../services/jisho_api/jisho_search.dart';
|
import '../../services/jisho_api/jisho_search.dart';
|
||||||
import '../../services/jisho_api/kanji_search.dart';
|
import '../../services/jisho_api/kanji_search.dart';
|
||||||
|
|
||||||
|
@ -23,11 +25,52 @@ class ResultPage extends StatefulWidget {
|
||||||
|
|
||||||
class _ResultPageState extends State<ResultPage> {
|
class _ResultPageState extends State<ResultPage> {
|
||||||
bool addedToDatabase = false;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
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(
|
body: FutureBuilder(
|
||||||
future: widget.isKanji
|
future: widget.isKanji
|
||||||
? fetchKanji(widget.searchTerm)
|
? fetchKanji(widget.searchTerm)
|
||||||
|
|
|
@ -11,6 +11,8 @@ import '../components/common/denshi_jisho_background.dart';
|
||||||
import '../data/database.dart';
|
import '../data/database.dart';
|
||||||
import '../data/export.dart';
|
import '../data/export.dart';
|
||||||
import '../data/import.dart';
|
import '../data/import.dart';
|
||||||
|
import '../models/history/history_entry.dart';
|
||||||
|
import '../models/library/library_list.dart';
|
||||||
import '../routing/routes.dart';
|
import '../routing/routes.dart';
|
||||||
import '../services/open_webpage.dart';
|
import '../services/open_webpage.dart';
|
||||||
import '../services/snackbar.dart';
|
import '../services/snackbar.dart';
|
||||||
|
@ -28,15 +30,12 @@ class _SettingsViewState extends State<SettingsView> {
|
||||||
bool dataImportIsLoading = false;
|
bool dataImportIsLoading = false;
|
||||||
|
|
||||||
Future<void> clearHistory(context) async {
|
Future<void> clearHistory(context) async {
|
||||||
final historyCount = (await db().query(
|
final historyCount = await HistoryEntry.amountOfEntries();
|
||||||
TableNames.historyEntry,
|
|
||||||
columns: ['COUNT(*) AS count'],
|
|
||||||
))[0]['count']! as int;
|
|
||||||
|
|
||||||
final bool userIsSure = await confirm(
|
final bool userIsSure = await confirm(
|
||||||
context,
|
context,
|
||||||
content: Text(
|
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;
|
if (!userIsSure) return;
|
||||||
|
@ -45,10 +44,33 @@ class _SettingsViewState extends State<SettingsView> {
|
||||||
showSnackbar(context, 'Cleared history');
|
showSnackbar(context, 'Cleared history');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearAll(context) async {
|
Future<void> clearFavourites(context) async {
|
||||||
final bool userIsSure = await confirm(context);
|
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;
|
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();
|
await resetDatabase();
|
||||||
showSnackbar(context, 'Cleared everything');
|
showSnackbar(context, 'Cleared everything');
|
||||||
}
|
}
|
||||||
|
@ -56,7 +78,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||||
// ignore: avoid_positional_boolean_parameters
|
// ignore: avoid_positional_boolean_parameters
|
||||||
void toggleAutoTheme(bool b) {
|
void toggleAutoTheme(bool b) {
|
||||||
final bool newThemeIsDark = b
|
final bool newThemeIsDark = b
|
||||||
? WidgetsBinding.instance.window.platformBrightness == Brightness.dark
|
? WidgetsBinding.instance?.window.platformBrightness == Brightness.dark
|
||||||
: darkThemeEnabled;
|
: darkThemeEnabled;
|
||||||
|
|
||||||
BlocProvider.of<ThemeBloc>(context)
|
BlocProvider.of<ThemeBloc>(context)
|
||||||
|
@ -274,9 +296,8 @@ class _SettingsViewState extends State<SettingsView> {
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
leading: const Icon(Icons.delete),
|
leading: const Icon(Icons.delete),
|
||||||
title: 'Clear Favourites',
|
title: 'Clear Favourites',
|
||||||
onPressed: (c) {},
|
onPressed: clearFavourites,
|
||||||
titleTextStyle: const TextStyle(color: Colors.red),
|
titleTextStyle: const TextStyle(color: Colors.red),
|
||||||
enabled: false,
|
|
||||||
),
|
),
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
leading: const Icon(Icons.delete),
|
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;
|
export 'package:unofficial_jisho_api/api.dart' show JishoAPIResult;
|
||||||
|
|
||||||
Future<jisho.JishoAPIResult> fetchJishoResults(searchTerm) =>
|
Future<JishoAPIResult> fetchJishoResults(searchTerm) =>
|
||||||
jisho.searchForPhrase(searchTerm);
|
searchForPhrase(searchTerm);
|
||||||
|
|
|
@ -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
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.12.0 <3.0.0"
|
sdk: ">=2.18.4 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
animated_size_and_fade: ^3.0.0
|
animated_size_and_fade: ^3.0.0
|
||||||
|
auto_size_text: ^3.0.0
|
||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
confirm_dialog: ^1.0.0
|
confirm_dialog: ^1.0.0
|
||||||
division: ^0.9.0
|
division: ^0.9.0
|
||||||
file_picker: ^4.5.1
|
file_picker: ^5.2.5
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_bloc: ^8.0.0
|
flutter_bloc: ^8.0.0
|
||||||
flutter_settings_ui: ^2.0.1
|
flutter_settings_ui: ^2.0.1
|
||||||
flutter_slidable: ^1.1.0
|
flutter_slidable: ^2.0.0
|
||||||
flutter_svg: ^1.0.2
|
flutter_svg: ^1.0.2
|
||||||
get_it: ^7.2.0
|
get_it: ^7.2.0
|
||||||
http: ^0.13.4
|
http: ^0.13.4
|
||||||
|
@ -23,13 +24,16 @@ dependencies:
|
||||||
mdi: ^5.0.0-nullsafety.0
|
mdi: ^5.0.0-nullsafety.0
|
||||||
path: ^1.8.0
|
path: ^1.8.0
|
||||||
path_provider: ^2.0.2
|
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
|
shared_preferences: ^2.0.6
|
||||||
signature: ^5.0.0
|
signature: ^5.0.0
|
||||||
sqflite: ^2.0.2
|
sqflite: ^2.0.2
|
||||||
sqflite_common_ffi: ^2.1.1
|
sqflite_common_ffi: ^2.1.1
|
||||||
test: ^1.19.5
|
test: ^1.19.5
|
||||||
unofficial_jisho_api: ^3.0.0
|
unofficial_jisho_api: ^3.0.0
|
||||||
|
# unofficial_jisho_api:
|
||||||
|
# path: /home/h7x4/git/unofficial-jisho-api-dart
|
||||||
url_launcher: ^6.0.9
|
url_launcher: ^6.0.9
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
@ -37,7 +41,7 @@ dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_native_splash: ^2.1.6
|
flutter_native_splash: ^2.1.6
|
||||||
flutter_launcher_icons: "^0.9.2"
|
flutter_launcher_icons: ^0.11.0
|
||||||
|
|
||||||
flutter_icons:
|
flutter_icons:
|
||||||
android: "launcher_icon"
|
android: "launcher_icon"
|
||||||
|
|
|
@ -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
|
|
@ -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