diff --git a/android/app/build.gradle b/android/app/build.gradle index f3e594e..47b3a65 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 33 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -42,7 +42,6 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "app.jishostudytool.jisho_study_tool" // minSdkVersion flutter.minSdkVersion minSdkVersion 19 diff --git a/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java b/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java index 9213f13..752fc18 100644 --- a/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java +++ b/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java @@ -1,16 +1,21 @@ // Generated file. +// // If you wish to remove Flutter's multidex support, delete this entire file. +// +// Modifications to this file should be done in a copy under a different name +// as this file may be regenerated. package io.flutter.app; +import android.app.Application; import android.content.Context; import androidx.annotation.CallSuper; import androidx.multidex.MultiDex; /** - * Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support. + * Extension of {@link android.app.Application}, adding multidex support. */ -public class FlutterMultiDexApplication extends FlutterApplication { +public class FlutterMultiDexApplication extends Application { @Override @CallSuper protected void attachBaseContext(Context base) { diff --git a/assets/migrations/0001_initial.sql b/assets/migrations/0001_initial.sql index e400754..0ca385d 100644 --- a/assets/migrations/0001_initial.sql +++ b/assets/migrations/0001_initial.sql @@ -1,32 +1,58 @@ - CREATE TABLE "JST_LibraryList" ( "name" TEXT PRIMARY KEY NOT NULL, - "nextList" TEXT REFERENCES "JST_LibraryList"("name") + "prevList" TEXT + UNIQUE + REFERENCES "JST_LibraryList"("name"), + -- The list can't link to itself + CHECK("prevList" != "name"), + -- 'favourites' should always be the first list + CHECK (NOT (("name" = 'favourites') <> ("prevList" IS NULL))) ); -CREATE INDEX "JST_LibraryList_byNextList" ON "JST_LibraryList"("nextList"); +-- This entry should always exist +INSERT INTO "JST_LibraryList"("name") VALUES ('favourites'); + +-- Useful for the view below +CREATE INDEX "JST_LibraryList_byPrevList" ON "JST_LibraryList"("prevList"); + +-- A view that sorts the LibraryLists in their custom order. +CREATE VIEW "JST_LibraryListOrdered" AS + WITH RECURSIVE "RecursionTable"("name") AS ( + SELECT "name" + FROM "JST_LibraryList" "I" + WHERE "I"."prevList" IS NULL + + UNION ALL + + SELECT "R"."name" + FROM "JST_LibraryList" "R" + JOIN "RecursionTable" ON + ("R"."prevList" = "RecursionTable"."name") + ) + SELECT * FROM "RecursionTable"; CREATE TABLE "JST_LibraryListEntry" ( "listName" TEXT NOT NULL REFERENCES "JST_LibraryList"("name") ON DELETE CASCADE, "entryText" TEXT NOT NULL, "isKanji" BOOLEAN NOT NULL DEFAULT 0, - "lastModified" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "nextEntry" TEXT NOT NULL, + -- Defaults to unix timestamp in milliseconds + "lastModified" INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000), + "prevEntryText" TEXT, + "prevEntryIsKanji" BOOLEAN NOT NULL DEFAULT 0, PRIMARY KEY ("listName", "entryText", "isKanji"), - FOREIGN KEY ("listName", "nextEntry") REFERENCES "JST_LibraryListEntry"("listName", "entryText"), - CHECK ((NOT "isKanji") OR ("nextEntry" <> 0)) + FOREIGN KEY ("listName", "prevEntryText", "prevEntryIsKanji") + REFERENCES "JST_LibraryListEntry"("listName", "entryText", "isKanji"), + -- Two entries can not appear directly after the same entry + UNIQUE("listName", "prevEntryText", "prevEntryIsKanji"), + -- The entry can't link to itself + CHECK(NOT ("prevEntryText" == "entryText" AND "prevEntryIsKanji" == "isKanji")), + -- Kanji entries should only have a length of 1 + CHECK ((NOT "isKanji") OR ("isKanji" AND length("entryText") = 1)) ); -CREATE INDEX "JST_LibraryListEntry_byListName" ON "JST_LibraryListEntry"("listName"); - --- CREATE VIEW "JST_LibraryListEntry_sortedByLists" AS --- WITH RECURSIVE "JST_LibraryListEntry_sorted"("next") AS ( --- -- Initial SELECT --- UNION ALL --- SELECT * FROM "" --- -- Recursive Select --- ) --- SELECT * FROM "JST_LibraryListEntry_sorted"; +-- Useful when doing the recursive ordering statement +CREATE INDEX "JST_LibraryListEntry_byListNameAndPrevEntry" + ON "JST_LibraryListEntry"("listName", "prevEntryText", "prevEntryIsKanji"); CREATE TABLE "JST_HistoryEntry" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT @@ -47,18 +73,17 @@ CREATE TABLE "JST_HistoryEntryWord" ( CREATE TABLE "JST_HistoryEntryTimestamp" ( "entryId" INTEGER NOT NULL REFERENCES "JST_HistoryEntry"("id") ON DELETE CASCADE, - -- Here, I'm using INTEGER insted of TIMESTAMP or DATETIME, because it seems to be - -- the easiest way to deal with global and local timeconversion between dart and - -- SQLite. - "timestamp" INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, + -- Defaults to unix timestamp in milliseconds + "timestamp" INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000), PRIMARY KEY ("entryId", "timestamp") ); +-- Useful when ordering entries by the timestamps CREATE INDEX "JST_HistoryEntryTimestamp_byTimestamp" ON "JST_HistoryEntryTimestamp"("timestamp"); CREATE VIEW "JST_HistoryEntry_orderedByTimestamp" AS -SELECT * FROM "JST_HistoryEntryTimestamp" -LEFT JOIN "JST_HistoryEntryWord" USING ("entryId") -LEFT JOIN "JST_HistoryEntryKanji" USING ("entryId") -GROUP BY "entryId" -ORDER BY MAX("timestamp") DESC; \ No newline at end of file + SELECT * FROM "JST_HistoryEntryTimestamp" + LEFT JOIN "JST_HistoryEntryWord" USING ("entryId") + LEFT JOIN "JST_HistoryEntryKanji" USING ("entryId") + GROUP BY "entryId" + ORDER BY MAX("timestamp") DESC; \ No newline at end of file diff --git a/flake.lock b/flake.lock index f75652d..3a07e2a 100644 --- a/flake.lock +++ b/flake.lock @@ -3,17 +3,17 @@ "android-nixpkgs": { "inputs": { "devshell": "devshell", - "flake-utils": "flake-utils_2", + "flake-utils": "flake-utils", "nixpkgs": [ "nixpkgs" ] }, "locked": { - "lastModified": 1651782096, - "narHash": "sha256-rrj0HPwmDf6Q14sljnVf2hkMvc97rndgi4PJkFtpFPk=", + "lastModified": 1677183680, + "narHash": "sha256-xPg1gYyZ8UYNWcQYBtvmmbum3l1hx5cFpoWKrJA15DI=", "owner": "tadfisher", "repo": "android-nixpkgs", - "rev": "ccd2a8f58709ea3413fcb72769b2f62a98332215", + "rev": "61410f48b49495f38f835bd79f98c9a0528151dd", "type": "github" }, "original": { @@ -24,15 +24,21 @@ }, "devshell": { "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "flake-utils": [ + "android-nixpkgs", + "nixpkgs" + ], + "nixpkgs": [ + "android-nixpkgs", + "nixpkgs" + ] }, "locked": { - "lastModified": 1650900878, - "narHash": "sha256-qhNncMBSa9STnhiLfELEQpYC1L4GrYHNIzyCZ/pilsI=", + "lastModified": 1676293499, + "narHash": "sha256-uIOTlTxvrXxpKeTvwBI1JGDGtCxMXE3BI0LFwoQMhiQ=", "owner": "numtide", "repo": "devshell", - "rev": "d97df53b5ddaa1cfbea7cddbd207eb2634304733", + "rev": "71e3022e3ab20bbf1342640547ef5bc14fb43bf4", "type": "github" }, "original": { @@ -43,11 +49,11 @@ }, "flake-utils": { "locked": { - "lastModified": 1642700792, - "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", + "lastModified": 1676283394, + "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=", "owner": "numtide", "repo": "flake-utils", - "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", + "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073", "type": "github" }, "original": { @@ -58,11 +64,11 @@ }, "flake-utils_2": { "locked": { - "lastModified": 1649676176, - "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", + "lastModified": 1676283394, + "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=", "owner": "numtide", "repo": "flake-utils", - "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", + "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073", "type": "github" }, "original": { @@ -86,35 +92,20 @@ "type": "github" } }, - "flake-utils_4": { - "locked": { - "lastModified": 1649676176, - "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "nix-dart": { "inputs": { - "flake-utils": "flake-utils_4", + "flake-utils": "flake-utils_3", "nixpkgs": [ "nixpkgs" ], "pub2nix": "pub2nix" }, "locked": { - "lastModified": 1651781526, - "narHash": "sha256-q01e+S69g4UDrMcEitaQOccr2aHeiJ+VEmPS94h/7WY=", + "lastModified": 1652213615, + "narHash": "sha256-+eehm2JlhoKgY+Ea4DTxDMei/x4Fgz7S+ZPqWpZysuI=", "owner": "tadfisher", "repo": "nix-dart", - "rev": "71d2fda0f9590d5de917fb736dee312d9fef7e27", + "rev": "6f686ddf984306d944e9b5adf9f35f3a0a0a70b7", "type": "github" }, "original": { @@ -125,32 +116,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1643381941, - "narHash": "sha256-pHTwvnN4tTsEKkWlXQ8JMY423epos8wUOhthpwJjtpc=", + "lastModified": 1677075010, + "narHash": "sha256-X+UmR1AkdR//lPVcShmLy8p1n857IGf7y+cyCArp8bU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5efc8ca954272c4376ac929f4c5ffefcc20551d5", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1651743098, - "narHash": "sha256-NuQNu6yHh54li0kZffM59FRC5bWCJusygL4Cy+3O0fY=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "d4191fe35cbe52f755ef73009d4d37b9e002efa2", + "rev": "c95bf18beba4290af25c60cbaaceea1110d0f727", "type": "github" }, "original": { "id": "nixpkgs", - "ref": "nixos-21.11", + "ref": "nixos-22.11", "type": "indirect" } }, @@ -173,9 +148,9 @@ "root": { "inputs": { "android-nixpkgs": "android-nixpkgs", - "flake-utils": "flake-utils_3", + "flake-utils": "flake-utils_2", "nix-dart": "nix-dart", - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index 6f3f0c9..99e0ef2 100644 --- a/flake.nix +++ b/flake.nix @@ -2,12 +2,8 @@ description = "A dictionary app for studying japanese"; inputs = { - nixpkgs.url = "nixpkgs/nixos-21.11"; - - flake-utils = { - url = "github:numtide/flake-utils"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + nixpkgs.url = "nixpkgs/nixos-22.11"; + flake-utils.url = "github:numtide/flake-utils"; android-nixpkgs = { url = "github:tadfisher/android-nixpkgs"; @@ -18,11 +14,6 @@ url = "github:tadfisher/nix-dart"; inputs.nixpkgs.follows = "nixpkgs"; }; - - # nix-flutter = { - # url = "path:/home/h7x4/git/flutter_linux_2.5.1-stable/flutter"; - # inputs.nixpkgs.follows = "nixpkgs"; - # }; }; outputs = { self, nixpkgs, flake-utils, android-nixpkgs, nix-dart }: @@ -35,68 +26,37 @@ allowUnfree = true; }; }; - - dartVersion = "2.14.2"; - dartChannel = "stable"; - - flutterVersion = "2.5.1"; - flutterChannel = "stable"; in { - packages.${system} = { android-sdk = android-nixpkgs.sdk.${system} (sdkPkgs: with sdkPkgs; [ cmdline-tools-latest + build-tools-33-0-0 build-tools-32-0-0 build-tools-31-0-0 build-tools-30-0-2 build-tools-29-0-2 platform-tools + platforms-android-33 platforms-android-32 platforms-android-31 platforms-android-30 platforms-android-29 emulator ]); - - # dart = nix-dart.packages.${system}.dart; - dart = (pkgs.callPackage ./nix/dart.nix {}); - - inherit (pkgs.callPackage ./nix/flutter.nix { inherit (self.packages.${system}) dart; }) flutter; - - # pub2nix-lock = nix-dart.packages.${system}.pub2nix-lock; }; - # apps.${system} = { - # web-debug = { - # type = "app"; - # program = ""; - # }; - # web-release = { - # type = "app"; - # program = ""; - # }; - # apk-debug = { - # type = "app"; - # program = ""; - # }; - # apk-release = { - # type = "app"; - # program = "${self.packages.${system}.flutter}/bin/flutter run --release"; - # }; - # default = self.apps.${system}.apk-debug; - # }; - devShell.${system} = let - inherit (pkgs) lcov google-chrome sqlite sqlite-web; - inherit (self.packages.${system}) android-sdk flutter dart; + inherit (pkgs) lcov google-chrome sqlite sqlite-web flutter dart; + jdk = pkgs.jdk11; + + inherit (self.packages.${system}) android-sdk; inherit (nix-dart.packages.${system}) pub2nix-lock; - java = pkgs.jdk8; in pkgs.mkShell rec { - ANDROID_JAVA_HOME="${java.home}"; + ANDROID_JAVA_HOME="${jdk.home}"; ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk"; CHROME_EXECUTABLE = "${google-chrome}/bin/google-chrome-stable"; FLUTTER_SDK="${flutter}"; - GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/32.0.0/aapt2"; + GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/33.0.0/aapt2"; JAVA_HOME="${ANDROID_JAVA_HOME}"; USE_CCACHE=0; @@ -105,7 +65,7 @@ dart flutter google-chrome - java + jdk lcov pub2nix-lock sqlite diff --git a/lib/bloc/theme/theme_bloc.dart b/lib/bloc/theme/theme_bloc.dart index 2b7c43b..4828ef2 100644 --- a/lib/bloc/theme/theme_bloc.dart +++ b/lib/bloc/theme/theme_bloc.dart @@ -20,7 +20,7 @@ class ThemeBloc extends Bloc { ); final bool autoThemeIsDark = - SchedulerBinding.instance!.window.platformBrightness == Brightness.dark; + SchedulerBinding.instance?.window.platformBrightness == Brightness.dark; add( SetTheme( diff --git a/lib/components/common/kanji_box.dart b/lib/components/common/kanji_box.dart new file mode 100644 index 0000000..93df844 --- /dev/null +++ b/lib/components/common/kanji_box.dart @@ -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( + 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), + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/components/drawing_board/drawing_board.dart b/lib/components/drawing_board/drawing_board.dart index fc21c47..fdfe852 100644 --- a/lib/components/drawing_board/drawing_board.dart +++ b/lib/components/drawing_board/drawing_board.dart @@ -3,6 +3,7 @@ import 'package:signature/signature.dart'; import '../../bloc/theme/theme_bloc.dart'; import '../../services/handwriting.dart'; +import '../../services/snackbar.dart'; import '../../settings.dart'; class DrawingBoard extends StatefulWidget { @@ -182,10 +183,9 @@ class _DrawingBoardState extends State { ), if (!widget.onlyOneCharacterSuggestions) IconButton( - onPressed: () => ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('TODO: implement scrolling page feature!'), - ), + onPressed: () => showSnackbar( + context, + 'TODO: implement scrolling page feature!', ), icon: const Icon(Icons.arrow_forward), ), diff --git a/lib/components/history/history_entry_item.dart b/lib/components/history/history_entry_tile.dart similarity index 82% rename from lib/components/history/history_entry_item.dart rename to lib/components/history/history_entry_tile.dart index 74d23dc..4883f1c 100644 --- a/lib/components/history/history_entry_item.dart +++ b/lib/components/history/history_entry_tile.dart @@ -4,18 +4,17 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import '../../models/history/history_entry.dart'; import '../../routing/routes.dart'; import '../../services/datetime.dart'; -import '../../services/snackbar.dart'; import '../../settings.dart'; +import '../common/kanji_box.dart'; import '../common/loading.dart'; -import 'kanji_box.dart'; -class HistoryEntryItem extends StatelessWidget { +class HistoryEntryTile extends StatelessWidget { final HistoryEntry entry; final int objectKey; final void Function()? onDelete; final void Function()? onFavourite; - const HistoryEntryItem({ + const HistoryEntryTile({ required this.entry, required this.objectKey, this.onDelete, @@ -23,10 +22,6 @@ class HistoryEntryItem extends StatelessWidget { Key? key, }) : super(key: key); - Widget get _child => (entry.isKanji) - ? KanjiBox(kanji: entry.kanji!) - : Text(entry.word!); - void Function() _onTap(context) => entry.isKanji ? () => Navigator.pushNamed( context, @@ -46,8 +41,7 @@ class HistoryEntryItem extends StatelessWidget { future: entry.timestamps, builder: (context, snapshot) { // TODO: provide proper error handling - if (snapshot.hasError) - return ErrorWidget(snapshot.error!); + if (snapshot.hasError) return ErrorWidget(snapshot.error!); if (!snapshot.hasData) return const LoadingScreen(); return ListView( children: snapshot.data! @@ -69,14 +63,6 @@ class HistoryEntryItem extends StatelessWidget { icon: Icons.access_time, onPressed: (_) => Navigator.push(context, timestamps), ), - SlidableAction( - backgroundColor: Colors.yellow, - icon: Icons.star, - onPressed: (_) { - showSnackbar(context, 'TODO: implement favourites'); - onFavourite?.call(); - }, - ), SlidableAction( backgroundColor: Colors.red, icon: Icons.delete, @@ -107,7 +93,12 @@ class HistoryEntryItem extends StatelessWidget { ), DefaultTextStyle.merge( style: japaneseFont.textStyle, - child: _child, + child: entry.isKanji + ? KanjiBox.headline4( + context: context, + kanji: entry.kanji!, + ) + : Expanded(child: Text(entry.word!)), ), ], ), diff --git a/lib/components/history/kanji_box.dart b/lib/components/history/kanji_box.dart deleted file mode 100644 index 4c67f80..0000000 --- a/lib/components/history/kanji_box.dart +++ /dev/null @@ -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( - 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, - ), - ), - ), - ); - }, - ), - ), - ); -} diff --git a/lib/components/kanji/kanji_result_body.dart b/lib/components/kanji/kanji_result_body.dart index 614beab..7eb8019 100644 --- a/lib/components/kanji/kanji_result_body.dart +++ b/lib/components/kanji/kanji_result_body.dart @@ -3,13 +3,14 @@ import 'package:unofficial_jisho_api/api.dart' as jisho; import './kanji_result_body/examples.dart'; import './kanji_result_body/grade.dart'; -import './kanji_result_body/header.dart'; import './kanji_result_body/jlpt_level.dart'; import './kanji_result_body/radical.dart'; import './kanji_result_body/rank.dart'; import './kanji_result_body/stroke_order_gif.dart'; import './kanji_result_body/yomi_chips.dart'; +import '../../bloc/theme/theme_bloc.dart'; import '../../services/kanji_grade_conversion.dart'; +import '../common/kanji_box.dart'; class KanjiResultBody extends StatelessWidget { late final String query; @@ -36,9 +37,22 @@ class KanjiResultBody extends StatelessWidget { child: SizedBox(), ), Flexible( - fit: FlexFit.tight, - child: Center(child: Header(kanji: query)), - ), + child: AspectRatio( + aspectRatio: 1, + child: BlocBuilder( + builder: (context, state) { + final colors = state.theme.kanjiResultColor; + return KanjiBox.expanded( + kanji: query, + ratio: 40, + foreground: colors.foreground, + background: colors.background, + ); + }, + ), + ), + ), + Flexible( fit: FlexFit.tight, child: Center( @@ -81,6 +95,7 @@ class KanjiResultBody extends StatelessWidget { return ListView( children: [ headerRow, + // TODO: handle case where meaning is empty. See 牃 for example YomiChips(yomi: resultData.meaning.split(', '), type: YomiType.meaning), (resultData.onyomi.isNotEmpty) ? YomiChips(yomi: resultData.onyomi, type: YomiType.onyomi) @@ -101,6 +116,7 @@ class KanjiResultBody extends StatelessWidget { onyomi: resultData.onyomiExamples, kunyomi: resultData.kunyomiExamples, ), + // TODO: Add unicode information ], ); } diff --git a/lib/components/kanji/kanji_result_body/header.dart b/lib/components/kanji/kanji_result_body/header.dart deleted file mode 100644 index 461454a..0000000 --- a/lib/components/kanji/kanji_result_body/header.dart +++ /dev/null @@ -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( - 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), - ), - ); - }, - ), - ); -} diff --git a/lib/components/kanji/kanji_result_body/radical.dart b/lib/components/kanji/kanji_result_body/radical.dart index 6184b1d..ef2475a 100644 --- a/lib/components/kanji/kanji_result_body/radical.dart +++ b/lib/components/kanji/kanji_result_body/radical.dart @@ -19,7 +19,11 @@ class Radical extends StatelessWidget { final colors = state.theme.kanjiResultColor; return InkWell( - onTap: () => Navigator.pushNamed(context, Routes.kanjiSearchRadicals, arguments: radical.symbol), + onTap: () => Navigator.pushNamed( + context, + Routes.kanjiSearchRadicals, + arguments: radical.symbol, + ), child: Container( padding: const EdgeInsets.all(15.0), decoration: BoxDecoration( diff --git a/lib/components/kanji/kanji_result_body/stroke_order_gif.dart b/lib/components/kanji/kanji_result_body/stroke_order_gif.dart index fcb40c0..f03e1e3 100644 --- a/lib/components/kanji/kanji_result_body/stroke_order_gif.dart +++ b/lib/components/kanji/kanji_result_body/stroke_order_gif.dart @@ -22,6 +22,7 @@ class StrokeOrderGif extends StatelessWidget { ), child: ClipRRect( borderRadius: BorderRadius.circular(10.0), + // TODO: show some kind of default icon if GIF is missing. child: Image.network(uri), ), ); diff --git a/lib/components/library/add_to_library_dialog.dart b/lib/components/library/add_to_library_dialog.dart new file mode 100644 index 0000000..41833da --- /dev/null +++ b/lib/components/library/add_to_library_dialog.dart @@ -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 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 createState() => _AddToLibraryDialogState(); +} + +class _AddToLibraryDialogState extends State { + Map? 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 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'), + ), + ], + ); + } +} diff --git a/lib/components/library/library_list_entry_tile.dart b/lib/components/library/library_list_entry_tile.dart new file mode 100644 index 0000000..c688bb6 --- /dev/null +++ b/lib/components/library/library_list_entry_tile.dart @@ -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)), + ], + ), + ), + ); + } +} diff --git a/lib/components/library/library_list_tile.dart b/lib/components/library/library_list_tile.dart new file mode 100644 index 0000000..98ff6cf --- /dev/null +++ b/lib/components/library/library_list_tile.dart @@ -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( + future: library.length, + builder: (context, snapshot) { + if (snapshot.hasError) return ErrorWidget(snapshot.error!); + if (!snapshot.hasData) return const LoadingScreen(); + return Text('${snapshot.data} items'); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/library/new_library_dialog.dart b/lib/components/library/new_library_dialog.dart new file mode 100644 index 0000000..18aa034 --- /dev/null +++ b/lib/components/library/new_library_dialog.dart @@ -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( + 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 createState() => _NewLibraryDialogState(); +} + +enum _NameState { + initial, + currentlyChecking, + invalid, + alreadyExists, + valid, +} + +class _NewLibraryDialogState extends State { + final controller = TextEditingController(); + _NameState nameState = _NameState.initial; + + Future 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'), + ), + ], + ); + } +} diff --git a/lib/components/search/search_results_body/parts/header.dart b/lib/components/search/search_results_body/parts/header.dart index 0822a8b..3164536 100644 --- a/lib/components/search/search_results_body/parts/header.dart +++ b/lib/components/search/search_results_body/parts/header.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:ruby_text/ruby_text.dart'; import 'package:unofficial_jisho_api/api.dart'; +import '../../../../services/jisho_api/kanji_furigana_separation.dart'; import '../../../../services/romaji_transliteration.dart'; import '../../../../settings.dart'; @@ -25,30 +27,13 @@ class JapaneseHeader extends StatelessWidget { return Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.only(left: 10.0), - child: Column( - children: [ - // Both wordReading and word.word being present implies that the word has furigana. - // If that's not the case, then the word is usually present in wordReading. - // However, there are some exceptions where the reading is placed in word. - // I have no clue why this might be the case. - hasFurigana - ? Text( - wordReading!, - style: romajiEnabled ? null : japaneseFont.textStyle, - ) - : const Text(''), - hasFurigana - ? Text( - word.word!, - style: japaneseFont.textStyle, - ) - : Text( - wordReading ?? word.word!, - style: wordReading != null && romajiEnabled - ? null - : japaneseFont.textStyle, - ), - ], + child: RubySpanWidget( + RubyTextData( + word.kanji, + ruby: word.furigana, + style: romajiEnabled ? null : japaneseFont.textStyle, + rubyStyle: romajiEnabled ? null : japaneseFont.textStyle, + ), ), ); } diff --git a/lib/components/search/search_results_body/parts/kanji.dart b/lib/components/search/search_results_body/parts/kanji.dart index 9ebab9d..12574ee 100644 --- a/lib/components/search/search_results_body/parts/kanji.dart +++ b/lib/components/search/search_results_body/parts/kanji.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import '../../../../bloc/theme/theme_bloc.dart'; import '../../../../routing/routes.dart'; -import '../../../../settings.dart'; +import '../../../common/kanji_box.dart'; class KanjiRow extends StatelessWidget { final List kanji; @@ -13,36 +12,6 @@ class KanjiRow extends StatelessWidget { this.fontSize = 20, }) : super(key: key); - Widget _kanjiBox(String kanji) => UnconstrainedBox( - child: IntrinsicHeight( - child: AspectRatio( - aspectRatio: 1, - child: BlocBuilder( - builder: (context, state) { - final colors = state.theme.menuGreyLight; - return Container( - padding: const EdgeInsets.all(10), - alignment: Alignment.center, - decoration: BoxDecoration( - color: colors.background, - borderRadius: BorderRadius.circular(10), - ), - child: FittedBox( - child: Text( - kanji, - style: TextStyle( - color: colors.foreground, - fontSize: fontSize, - ).merge(japaneseFont.textStyle), - ), - ), - ); - }, - ), - ), - ), - ); - @override Widget build(BuildContext context) { return Column( @@ -64,7 +33,10 @@ class KanjiRow extends StatelessWidget { Routes.kanjiSearch, arguments: k, ), - child: _kanjiBox(k), + child: KanjiBox.headline4( + context: context, + kanji: k, + ), ) ], ), diff --git a/lib/components/search/search_results_body/search_card.dart b/lib/components/search/search_results_body/search_card.dart index 63a9bbe..434e9f8 100644 --- a/lib/components/search/search_results_body/search_card.dart +++ b/lib/components/search/search_results_body/search_card.dart @@ -1,18 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:jisho_study_tool/services/kanji_regex.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:unofficial_jisho_api/api.dart'; -import './parts/common_badge.dart'; -import './parts/header.dart'; -import './parts/jlpt_badge.dart'; -import './parts/other_forms.dart'; -import './parts/senses.dart'; -import './parts/wanikani_badge.dart'; +import '../../../models/library/library_list.dart'; +import '../../../services/jisho_api/kanji_furigana_separation.dart'; +import '../../../services/kanji_regex.dart'; import '../../../settings.dart'; +import '../../library/add_to_library_dialog.dart'; import 'parts/audio_player.dart'; +import 'parts/common_badge.dart'; +import 'parts/header.dart'; +import 'parts/jlpt_badge.dart'; import 'parts/kanji.dart'; import 'parts/links.dart'; import 'parts/notes.dart'; +import 'parts/other_forms.dart'; +import 'parts/senses.dart'; +import 'parts/wanikani_badge.dart'; class SearchResultCard extends StatefulWidget { final JishoResult result; @@ -31,20 +35,11 @@ class SearchResultCard extends StatefulWidget { } class _SearchResultCardState extends State { + static const _margin = SizedBox(height: 20); PhrasePageScrapeResultData? extraData; + bool? extraDataSearchFailed; - Future _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 get links => - [for (final sense in widget.result.senses) ...sense.links]; - bool get hasAttribution => widget.result.attribution.jmdict || widget.result.attribution.jmnedict || @@ -67,30 +62,77 @@ class _SearchResultCardState extends State { .toSet() .toList(); - Widget get _header => IntrinsicWidth( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - JapaneseHeader(word: widget.mainWord), - Row( - children: [ - WKBadge( - level: widget.result.tags.firstWhere( - (tag) => tag.contains('wanikani'), - orElse: () => '', - ), - ), - JLPTBadge(jlptLevel: jlptLevel), - CommonBadge(isCommon: widget.result.isCommon ?? false) - ], - ) - ], - ), + List get links => + [for (final sense in widget.result.senses) ...sense.links]; + + Widget get _header => Row( + children: [ + Expanded(child: JapaneseHeader(word: widget.mainWord)), + WKBadge( + level: widget.result.tags.firstWhere( + (tag) => tag.contains('wanikani'), + orElse: () => '', + ), + ), + JLPTBadge(jlptLevel: jlptLevel), + CommonBadge(isCommon: widget.result.isCommon ?? false) + ], ); - static const _margin = SizedBox(height: 20); + @override + Widget build(BuildContext context) { + final backgroundColor = Theme.of(context).scaffoldBackgroundColor; - List _withMargin(Widget w) => [_margin, w]; + return Slidable( + endActionPane: ActionPane( + motion: const ScrollMotion(), + children: [ + SlidableAction( + backgroundColor: Colors.yellow, + icon: Icons.star, + onPressed: (_) => LibraryList.favourites.toggleEntry( + entryText: widget.result.slug, + isKanji: false, + ), + ), + SlidableAction( + backgroundColor: Colors.blue, + icon: Icons.bookmark, + onPressed: (context) => showAddToLibraryDialog( + context: context, + entryText: widget.result.japanese.first.kanji, + furigana: widget.result.japanese.first.furigana + ), + ), + ], + ), + child: ExpansionTile( + collapsedBackgroundColor: backgroundColor, + backgroundColor: backgroundColor, + onExpansionChanged: (b) async { + if (extensiveSearchEnabled && extraData == null) { + final data = await _scrape(widget.result); + setState(() { + extraDataSearchFailed = !(data?.found ?? false); + extraData = !extraDataSearchFailed! ? data!.data : null; + }); + } + }, + title: _header, + children: [ + if (extensiveSearchEnabled && extraDataSearchFailed == null) + const Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Center(child: CircularProgressIndicator()), + ) + else if (!extraDataSearchFailed!) + _body(extendedData: extraData) + else + _body() + ], + ), + ); + } Widget _body({PhrasePageScrapeResultData? extendedData}) => Container( padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10), @@ -123,34 +165,13 @@ class _SearchResultCardState extends State { ), ); - @override - Widget build(BuildContext context) { - final backgroundColor = Theme.of(context).scaffoldBackgroundColor; + Future _scrape(JishoResult result) => + (!(result.japanese[0].word == null && result.japanese[0].reading == null)) + ? scrapeForPhrase( + widget.result.japanese[0].word ?? + widget.result.japanese[0].reading!, + ) + : Future(() => null); - return ExpansionTile( - collapsedBackgroundColor: backgroundColor, - backgroundColor: backgroundColor, - onExpansionChanged: (b) async { - if (extensiveSearchEnabled && extraData == null) { - final data = await _scrape(widget.result); - setState(() { - extraDataSearchFailed = !(data?.found ?? false); - extraData = !extraDataSearchFailed! ? data!.data : null; - }); - } - }, - title: _header, - children: [ - if (extensiveSearchEnabled && extraDataSearchFailed == null) - const Padding( - padding: EdgeInsets.symmetric(vertical: 10), - child: Center(child: CircularProgressIndicator()), - ) - else if (!extraDataSearchFailed!) - _body(extendedData: extraData) - else - _body() - ], - ); - } + List _withMargin(Widget w) => [_margin, w]; } diff --git a/lib/data/archive_format.dart b/lib/data/archive_format.dart index 825026f..d56d790 100644 --- a/lib/data/archive_format.dart +++ b/lib/data/archive_format.dart @@ -1,18 +1,36 @@ import 'dart:io'; +import 'dart:math'; // Example file Structure: -// jisho_data_22.01.01_1 +// jisho_data_2022.01.01_1 // - history.json // - library/ // - lista.json // - listb.json extension ArchiveFormat on Directory { - // TODO: make the export dir dependent on date Directory get exportDirectory { final dir = Directory(uri.resolve('export').path); dir.createSync(recursive: true); - return dir; + + final DateTime today = DateTime.now(); + final String formattedDate = '${today.year}' + '.${today.month.toString().padLeft(2, '0')}' + '.${today.day.toString().padLeft(2, '0')}'; + + final List 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() + .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); diff --git a/lib/data/database.dart b/lib/data/database.dart index 234deab..ff0f400 100644 --- a/lib/data/database.dart +++ b/lib/data/database.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:developer'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:get_it/get_it.dart'; import 'package:path/path.dart'; @@ -41,7 +42,8 @@ Future migrate(Database db, int oldVersion, int newVersion) async { log( 'Migrating database from v$i to v${i + 1} with File(${migrations[i - 1]})', ); - final migrationContent = await rootBundle.loadString(migrations[i - 1], cache: false); + final migrationContent = + await rootBundle.loadString(migrations[i - 1], cache: false); migrationContent .split(';') @@ -58,24 +60,25 @@ Future migrate(Database db, int oldVersion, int newVersion) async { } Future setupDatabase() async { - databaseFactory.debugSetLogLevel(sqfliteLogLevelSql); + if (kDebugMode) { + databaseFactory.debugSetLogLevel(sqfliteLogLevelSql); + } + final Database database = await openDatabase( await databasePath(), version: 1, onCreate: (db, version) => migrate(db, 0, version), onUpgrade: migrate, - onOpen: (db) => Future.wait([ - db.execute('PRAGMA foreign_keys=ON') - ]), + onOpen: (db) => Future.wait([db.execute('PRAGMA foreign_keys=ON')]), ); GetIt.instance.registerSingleton(database); } Future resetDatabase() async { - await db().close(); - File(await databasePath()).deleteSync(); - GetIt.instance.unregister(); - await setupDatabase(); + await db().close(); + File(await databasePath()).deleteSync(); + GetIt.instance.unregister(); + await setupDatabase(); } class TableNames { @@ -101,7 +104,7 @@ class TableNames { /// Attributes: /// - name TEXT - /// - nextList TEXT + /// - prevList TEXT static const String libraryList = 'JST_LibraryList'; /// Attributes: @@ -109,12 +112,17 @@ class TableNames { /// - entryText TEXT /// - isKanji BOOLEAN /// - lastModified TIMESTAMP - /// - nextEntry TEXT + /// - prevEntryText TEXT + /// - prevEntryIsKanji BOOLEAN static const String libraryListEntry = 'JST_LibraryListEntry'; /////////// // VIEWS // /////////// + + /// Attributes: + /// - name TEXT + static const String libraryListOrdered = 'JST_LibraryListOrdered'; /// Attributes: /// - entryId INTEGER diff --git a/lib/data/database_errors.dart b/lib/data/database_errors.dart new file mode 100644 index 0000000..b35aaf7 --- /dev/null +++ b/lib/data/database_errors.dart @@ -0,0 +1,74 @@ +abstract class DatabaseError implements ArgumentError { + final String? tableName; + final Map? 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? 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? 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? 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 {} diff --git a/lib/data/export.dart b/lib/data/export.dart index 462a320..219c716 100644 --- a/lib/data/export.dart +++ b/lib/data/export.dart @@ -4,15 +4,12 @@ import 'dart:io'; import 'package:path_provider/path_provider.dart'; import '../models/history/history_entry.dart'; +import '../models/library/library_list.dart'; import 'archive_format.dart'; import 'database.dart'; -Future exportDirectory() async { - final basedir = (await getExternalStorageDirectory())!; - final dir = basedir.exportDirectory; - dir.createSync(recursive: true); - return dir; -} +Future exportDirectory() async => + (await getExternalStorageDirectory())!.exportDirectory; /// Returns the path to which the data was saved. Future exportData() async { @@ -33,13 +30,21 @@ Future exportHistoryTo(Directory dir) async { final query = await db().query(TableNames.historyEntryOrderedByTimestamp); final List entries = query.map((e) => HistoryEntry.fromDBMap(e)).toList(); + + /// TODO: This creates a ton of sql statements. Ideally, the whole export + /// should be done in only one query. + /// + /// On second thought, is that even possible? It's a doubly nested list structure. final List> jsonEntries = await Future.wait(entries.map((he) async => he.toJson())); file.writeAsStringSync(jsonEncode(jsonEntries)); } -Future exportLibraryListsTo(Directory dir) async { - // TODO: - // final query = db().query(TableNames.libraryList); - print('TODO: implement exportLibraryLists'); -} +Future exportLibraryListsTo(Directory dir) async => Future.wait( + (await LibraryList.allLibraries).map((lib) async { + final file = File(dir.uri.resolve('${lib.name}.json').path); + file.createSync(); + final entries = await lib.entries; + file.writeAsStringSync(jsonEncode(entries)); + }), + ); diff --git a/lib/data/import.dart b/lib/data/import.dart index badec94..bc2142d 100644 --- a/lib/data/import.dart +++ b/lib/data/import.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:developer'; import 'dart:io'; import '../models/history/history_entry.dart'; @@ -13,11 +14,11 @@ Future importData(Directory dir) async { Future importHistoryFrom(File file) async { final String content = file.readAsStringSync(); - await HistoryEntry.insertJsonEntries( - (jsonDecode(content) as List) - .map((h) => h as Map) - .toList(), - ); + final List> json = (jsonDecode(content) as List) + .map((h) => h as Map) + .toList(); + log('Importing ${json.length} entries from ${file.path}'); + await HistoryEntry.insertJsonEntries(json); } Future importLibraryListsFrom(Directory libraryListsDir) async { diff --git a/lib/models/history/history_entry.dart b/lib/models/history/history_entry.dart index 94f0296..fa0806e 100644 --- a/lib/models/history/history_entry.dart +++ b/lib/models/history/history_entry.dart @@ -267,7 +267,7 @@ class HistoryEntry { /// An efficient implementation of [insertJsonEntry] for multiple /// entries. - /// + /// /// This assumes that there are no duplicates within the elements /// in the json. static Future> insertJsonEntries( @@ -348,6 +348,14 @@ class HistoryEntry { return entries; }); + static Future amountOfEntries() async { + final query = await db().query( + TableNames.historyEntry, + columns: ['COUNT(*) AS count'], + ); + return query.first['count']! as int; + } + static Future> get fromDB async => (await db().query(TableNames.historyEntryOrderedByTimestamp)) .map((e) => HistoryEntry.fromDBMap(e)) diff --git a/lib/models/library/library_entry.dart b/lib/models/library/library_entry.dart new file mode 100644 index 0000000..7a6f90e --- /dev/null +++ b/lib/models/library/library_entry.dart @@ -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 toJson() => { + 'kanji': kanji, + 'word': word, + 'lastModified': lastModified.millisecondsSinceEpoch, + }; + + factory LibraryEntry.fromJson(Map 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 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, + ), + ); +} diff --git a/lib/models/library/library_list.dart b/lib/models/library/library_list.dart new file mode 100644 index 0000000..724b986 --- /dev/null +++ b/lib/models/library/library_list.dart @@ -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> 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> 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> 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 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 containsWord(String word) => contains( + entryText: word, + isKanji: false, + ); + + /// Whether a library contains a specific kanjientry + Future containsKanji(String kanji) => contains( + entryText: kanji, + isKanji: true, + ); + + /// Whether a library exists in the database + static Future 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 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 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 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 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 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 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 deleteAllEntries() => db().delete( + TableNames.libraryListEntry, + where: 'listName = ?', + whereArgs: [name], + ); + + /// Insert a new library list into the database + static Future 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 delete() async { + if (name == 'favourites') { + throw IllegalDeletionError( + tableName: TableNames.libraryList, + illegalArguments: {'name': name}, + ); + } + await db().delete( + TableNames.libraryList, + where: 'name = ?', + whereArgs: [name], + ); + } +} diff --git a/lib/models/themes/dark.dart b/lib/models/themes/dark.dart index 16dc12d..71a622d 100644 --- a/lib/models/themes/dark.dart +++ b/lib/models/themes/dark.dart @@ -61,7 +61,14 @@ class DarkTheme extends AppTheme { ThemeData getMaterialTheme() { return ThemeData( brightness: Brightness.dark, - primarySwatch: createMaterialColor(AppTheme.jishoGreen.background), + colorScheme: ColorScheme.fromSwatch( + primarySwatch: createMaterialColor(AppTheme.jishoGreen.background), + accentColor: AppTheme.jishoGreen.background, + brightness: Brightness.dark, + ), + toggleableActiveColor: AppTheme.jishoGreen.background, + + // elevatedButtonTheme: ElevatedButtonThemeData(style: ) ); } } diff --git a/lib/routing/router.dart b/lib/routing/router.dart index e5ada8b..784bbff 100644 --- a/lib/routing/router.dart +++ b/lib/routing/router.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import '../models/library/library_list.dart'; import '../screens/home.dart'; import '../screens/info/about.dart'; import '../screens/info/licenses.dart'; +import '../screens/library/library_content_view.dart'; import '../screens/search/result_page.dart'; import '../screens/search/search_mechanisms/drawing.dart'; import '../screens/search/search_mechanisms/grade_list.dart'; @@ -40,6 +42,12 @@ Route generateRoute(RouteSettings settings) { builder: (_) => KanjiRadicalSearch(prechosenRadical: prechosenRadical), ); + case Routes.libraryContent: + final library = args! as LibraryList; + return MaterialPageRoute( + builder: (_) => LibraryContentView(library: library), + ); + case Routes.about: return MaterialPageRoute(builder: (_) => const AboutView()); case Routes.aboutLicenses: diff --git a/lib/routing/routes.dart b/lib/routing/routes.dart index d4a8f8e..1383c3e 100644 --- a/lib/routing/routes.dart +++ b/lib/routing/routes.dart @@ -5,6 +5,7 @@ class Routes { static const String kanjiSearchDraw = '/kanjiSearch/draw'; static const String kanjiSearchGrade = '/kanjiSearch/grade'; static const String kanjiSearchRadicals = '/kanjiSearch/radicals'; + static const String libraryContent = '/library'; static const String about = '/info/about'; static const String aboutLicenses = '/info/licenses'; static const String errorNotFound = '/error/404'; diff --git a/lib/screens/debug.dart b/lib/screens/debug.dart index 2639bee..109b3c2 100644 --- a/lib/screens/debug.dart +++ b/lib/screens/debug.dart @@ -1,27 +1,58 @@ import 'package:flutter/material.dart'; -import '../components/drawing_board/drawing_board.dart'; +import '../components/common/kanji_box.dart'; +// import '../components/drawing_board/drawing_board.dart'; +// import '../components/library/add_to_library_dialog.dart'; class DebugView extends StatelessWidget { const DebugView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.end, + // return const Center(child: KanjiBox(kanji: '漢')); + return ListView( children: [ - DrawingBoard( - allowHiragana: true, - allowKatakana: true, - allowOther: true, - onSuggestionChosen: (s) => ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Chose: $s'), - duration: const Duration(milliseconds: 600), + Row( + children: [ + KanjiBox.withPadding(kanji: '漢', padding: 5), + KanjiBox.withFontSize(kanji: '漢', fontSize: 20), + const KanjiBox.withFontSizeAndPadding( + kanji: '漢', + fontSize: 40, + padding: 10, ), - ), + KanjiBox.withFontSize(kanji: '漢', fontSize: 40), + KanjiBox.withPadding(kanji: '漢', padding: 10), + ], ), + const Divider(), + KanjiBox.expanded(kanji: '彼', ratio: 1), + const Divider(), + KanjiBox.expanded(kanji: '例') ], ); + // return Column( + // mainAxisAlignment: MainAxisAlignment.end, + // children: const [ + // ElevatedButton( + // onPressed: () => showAddToLibraryDialog( + // context: context, + // entryText: 'lol', + // ), + // child: const Text('Add entry to list'), + // ), + // DrawingBoard( + // allowHiragana: true, + // allowKatakana: true, + // allowOther: true, + // onSuggestionChosen: (s) => ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text('Chose: $s'), + // duration: const Duration(milliseconds: 600), + // ), + // ), + // ), + // ], + // ); } } diff --git a/lib/screens/history.dart b/lib/screens/history.dart index 70825aa..8518ad0 100644 --- a/lib/screens/history.dart +++ b/lib/screens/history.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import '../components/common/loading.dart'; import '../components/common/opaque_box.dart'; import '../components/history/date_divider.dart'; -import '../components/history/history_entry_item.dart'; +import '../components/history/history_entry_tile.dart'; import '../models/history/history_entry.dart'; import '../services/datetime.dart'; @@ -45,10 +45,15 @@ class HistoryView extends StatelessWidget { final HistoryEntry search = data[index]; final DateTime searchDate = search.lastTimestamp; - if (index == 0 || !dateIsEqual(data[index - 1].lastTimestamp, searchDate)) + if (index == 0 || + !dateIsEqual(data[index - 1].lastTimestamp, searchDate)) return TextDivider(text: formatDate(roundToDay(searchDate))); - return const Divider(height: 0); + return const Divider( + height: 0, + indent: 10, + endIndent: 10, + ); }; Widget Function(BuildContext, int) historyEntryWithData( @@ -56,7 +61,7 @@ class HistoryView extends StatelessWidget { ) => (context, index) => (index == 0) ? const SizedBox.shrink() - : HistoryEntryItem( + : HistoryEntryTile( entry: data.values.toList()[index - 1], objectKey: data.keys.toList()[index - 1], onDelete: () => build(context), diff --git a/lib/screens/home.dart b/lib/screens/home.dart index b20099f..cf2ca92 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -4,8 +4,10 @@ import 'package:mdi/mdi.dart'; import '../bloc/theme/theme_bloc.dart'; import '../components/common/denshi_jisho_background.dart'; +import '../components/library/new_library_dialog.dart'; import 'debug.dart'; import 'history.dart'; +import 'library/library_view.dart'; import 'search/kanji_view.dart'; import 'search/search_view.dart'; import 'settings.dart'; @@ -20,25 +22,35 @@ class Home extends StatefulWidget { class _HomeState extends State { int pageNum = 0; + _Page get page => pages[pageNum]; + @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, themeState) { return Scaffold( appBar: AppBar( - title: pages[pageNum].titleBar, + title: Text(page.titleBar), centerTitle: true, backgroundColor: AppTheme.jishoGreen.background, foregroundColor: AppTheme.jishoGreen.foreground, + actions: page.actions, ), - body: DenshiJishoBackground(child: pages[pageNum].content), + body: DenshiJishoBackground(child: page.content), bottomNavigationBar: BottomNavigationBar( fixedColor: AppTheme.jishoGreen.background, currentIndex: pageNum, onTap: (index) => setState(() { pageNum = index; }), - items: pages.map((p) => p.item).toList(), + items: pages + .map( + (p) => BottomNavigationBarItem( + label: p.titleBar, + icon: p.icon, + ), + ) + .toList(), showSelectedLabels: false, showUnselectedLabels: false, unselectedItemColor: themeState.theme.menuGreyDark.background, @@ -51,52 +63,40 @@ class _HomeState extends State { List<_Page> get pages => [ const _Page( content: SearchView(), - titleBar: Text('Search'), - item: BottomNavigationBarItem( - label: 'Search', - icon: Icon(Icons.search), - ), + titleBar: 'Search', + icon: Icon(Icons.search), ), const _Page( content: KanjiView(), - titleBar: Text('Kanji Search'), - item: BottomNavigationBarItem( - label: 'Kanji', - icon: Icon(Mdi.ideogramCjk, size: 30), - ), + titleBar: 'Kanji Search', + icon: Icon(Mdi.ideogramCjk, size: 30), ), const _Page( content: HistoryView(), - titleBar: Text('History'), - item: BottomNavigationBarItem( - label: 'History', - icon: Icon(Icons.history), - ), + titleBar: 'History', + icon: Icon(Icons.history), ), _Page( - content: Container(), - titleBar: const Text('Library'), - item: const BottomNavigationBarItem( - label: 'Library', - icon: Icon(Icons.bookmark), - ), + content: const LibraryView(), + titleBar: 'Library', + icon: const Icon(Icons.bookmark), + actions: [ + IconButton( + onPressed: showNewLibraryDialog(context), + icon: const Icon(Icons.add), + ) + ], ), const _Page( content: SettingsView(), - titleBar: Text('Settings'), - item: BottomNavigationBarItem( - label: 'Settings', - icon: Icon(Icons.settings), - ), + titleBar: 'Settings', + icon: Icon(Icons.settings), ), if (kDebugMode) ...[ const _Page( content: DebugView(), - titleBar: Text('Debug Page'), - item: BottomNavigationBarItem( - label: 'Debug', - icon: Icon(Icons.biotech), - ), + titleBar: 'Debug Page', + icon: Icon(Icons.biotech), ) ], ]; @@ -104,12 +104,14 @@ class _HomeState extends State { class _Page { final Widget content; - final Widget titleBar; - final BottomNavigationBarItem item; + final String titleBar; + final Icon icon; + final List actions; const _Page({ required this.content, required this.titleBar, - required this.item, + required this.icon, + this.actions = const [], }); } diff --git a/lib/screens/library/library_content_view.dart b/lib/screens/library/library_content_view.dart new file mode 100644 index 0000000..43f1fc5 --- /dev/null +++ b/lib/screens/library/library_content_view.dart @@ -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 createState() => _LibraryContentViewState(); +} + +class _LibraryContentViewState extends State { + List? entries; + + Future 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, + ), + ), + ); + } +} diff --git a/lib/screens/library/library_view.dart b/lib/screens/library/library_view.dart new file mode 100644 index 0000000..9358e68 --- /dev/null +++ b/lib/screens/library/library_view.dart @@ -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 createState() => _LibraryViewState(); +} + +class _LibraryViewState extends State { + List? libraries; + + Future 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(), + ), + ), + ], + ); + } +} diff --git a/lib/screens/search/result_page.dart b/lib/screens/search/result_page.dart index 3bb8939..613db38 100644 --- a/lib/screens/search/result_page.dart +++ b/lib/screens/search/result_page.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import '../../components/common/loading.dart'; import '../../components/kanji/kanji_result_body.dart'; +import '../../components/library/add_to_library_dialog.dart'; import '../../components/search/search_result_body.dart'; import '../../models/history/history_entry.dart'; +import '../../models/library/library_list.dart'; import '../../services/jisho_api/jisho_search.dart'; import '../../services/jisho_api/kanji_search.dart'; @@ -23,11 +25,52 @@ class ResultPage extends StatefulWidget { class _ResultPageState extends State { bool addedToDatabase = false; + bool isFavourite = false; + + @override + void initState() { + super.initState(); + if (!widget.isKanji) return; + LibraryList.favourites + .containsKanji(widget.searchTerm) + .then((b) => setState(() => isFavourite = b)); + } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(), + appBar: AppBar( + actions: !widget.isKanji + ? [] + : [ + IconButton( + onPressed: () async { + await showAddToLibraryDialog( + context: context, + entryText: widget.searchTerm, + isKanji: true, + ); + final updatedFavouriteStatus = await LibraryList.favourites + .containsKanji(widget.searchTerm); + setState(() => isFavourite = updatedFavouriteStatus); + }, + icon: const Icon(Icons.bookmark), + ), + IconButton( + onPressed: () async { + await LibraryList.favourites.toggleEntry( + entryText: widget.searchTerm, + isKanji: true, + overrideToggleOn: !isFavourite, + ); + setState(() => isFavourite = !isFavourite); + }, + icon: isFavourite + ? const Icon(Icons.star, color: Colors.yellow) + : const Icon(Icons.star_border), + ) + ], + ), body: FutureBuilder( future: widget.isKanji ? fetchKanji(widget.searchTerm) diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index cdfb1bf..10316a1 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -11,6 +11,8 @@ import '../components/common/denshi_jisho_background.dart'; import '../data/database.dart'; import '../data/export.dart'; import '../data/import.dart'; +import '../models/history/history_entry.dart'; +import '../models/library/library_list.dart'; import '../routing/routes.dart'; import '../services/open_webpage.dart'; import '../services/snackbar.dart'; @@ -28,15 +30,12 @@ class _SettingsViewState extends State { bool dataImportIsLoading = false; Future clearHistory(context) async { - final historyCount = (await db().query( - TableNames.historyEntry, - columns: ['COUNT(*) AS count'], - ))[0]['count']! as int; + final historyCount = await HistoryEntry.amountOfEntries(); final bool userIsSure = await confirm( context, content: Text( - 'Are you sure that you want to delete $historyCount entries?', + 'Are you sure that you want to clear all $historyCount entries in history?', ), ); if (!userIsSure) return; @@ -45,10 +44,33 @@ class _SettingsViewState extends State { showSnackbar(context, 'Cleared history'); } - Future clearAll(context) async { - final bool userIsSure = await confirm(context); + Future clearFavourites(context) async { + final favouritesCount = await LibraryList.favourites.length; + final bool userIsSure = await confirm( + context, + content: Text( + 'Are you sure that you want to clear all $favouritesCount entries in favourites?', + ), + ); if (!userIsSure) return; + await LibraryList.favourites.deleteAllEntries(); + showSnackbar(context, 'Cleared favourites'); + } + + Future clearAll(context) async { + final historyCount = await HistoryEntry.amountOfEntries(); + final libraryCount = await LibraryList.amountOfLibraries(); + + final bool userIsSure = await confirm( + context, + content: Text( + 'Are you sure you want to delete $historyCount history entries ' + 'and $libraryCount libraries?', + ), + ); + + if (!userIsSure) return; await resetDatabase(); showSnackbar(context, 'Cleared everything'); } @@ -56,7 +78,7 @@ class _SettingsViewState extends State { // ignore: avoid_positional_boolean_parameters void toggleAutoTheme(bool b) { final bool newThemeIsDark = b - ? WidgetsBinding.instance.window.platformBrightness == Brightness.dark + ? WidgetsBinding.instance?.window.platformBrightness == Brightness.dark : darkThemeEnabled; BlocProvider.of(context) @@ -274,9 +296,8 @@ class _SettingsViewState extends State { SettingsTile( leading: const Icon(Icons.delete), title: 'Clear Favourites', - onPressed: (c) {}, + onPressed: clearFavourites, titleTextStyle: const TextStyle(color: Colors.red), - enabled: false, ), SettingsTile( leading: const Icon(Icons.delete), diff --git a/lib/services/jisho_api/jisho_search.dart b/lib/services/jisho_api/jisho_search.dart index 3df4c01..c6cdc60 100644 --- a/lib/services/jisho_api/jisho_search.dart +++ b/lib/services/jisho_api/jisho_search.dart @@ -1,5 +1,5 @@ -import 'package:unofficial_jisho_api/api.dart' as jisho; +import 'package:unofficial_jisho_api/api.dart'; export 'package:unofficial_jisho_api/api.dart' show JishoAPIResult; -Future fetchJishoResults(searchTerm) => - jisho.searchForPhrase(searchTerm); +Future fetchJishoResults(searchTerm) => + searchForPhrase(searchTerm); diff --git a/lib/services/jisho_api/kanji_furigana_separation.dart b/lib/services/jisho_api/kanji_furigana_separation.dart new file mode 100644 index 0000000..8da8aab --- /dev/null +++ b/lib/services/jisho_api/kanji_furigana_separation.dart @@ -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; +} diff --git a/pubspec.lock b/pubspec.lock index 43e2a86..0c06fd7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,56 +7,63 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "40.0.0" + version: "47.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.7.0" animated_size_and_fade: dependency: "direct main" description: name: animated_size_and_fade url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" archive: dependency: transitive description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.3.6" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.3.1" + version: "2.4.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" audio_session: dependency: transitive description: name: audio_session url: "https://pub.dartlang.org" source: hosted - version: "0.1.7" + version: "0.1.13" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" bloc: dependency: transitive description: name: bloc url: "https://pub.dartlang.org" source: hosted - version: "8.0.3" + version: "8.1.1" boolean_selector: dependency: transitive description: @@ -70,42 +77,42 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.1" build_config: dependency: transitive description: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.1.11" + version: "2.3.0" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.2.3" + version: "7.2.7" built_collection: dependency: transitive description: @@ -119,42 +126,42 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.3.2" + version: "8.4.3" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.2.1" checked_yaml: dependency: transitive description: name: checked_yaml url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.4.0" collection: dependency: "direct main" description: @@ -175,14 +182,21 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.1" coverage: dependency: transitive description: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.3.2" + version: "1.6.3" + cross_file: + dependency: transitive + description: + name: cross_file + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3+4" crypto: dependency: transitive description: @@ -203,7 +217,7 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "2.2.3" + version: "2.2.4" division: dependency: "direct main" description: @@ -217,35 +231,35 @@ packages: name: equatable url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.5" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "2.0.1" file: dependency: transitive description: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.2" + version: "6.1.4" file_picker: dependency: "direct main" description: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "4.6.1" + version: "5.2.5" fixnum: dependency: transitive description: @@ -264,28 +278,35 @@ packages: name: flutter_bloc url: "https://pub.dartlang.org" source: hosted - version: "8.0.1" + version: "8.1.2" + flutter_hooks: + dependency: transitive + description: + name: flutter_hooks + url: "https://pub.dartlang.org" + source: hosted + version: "0.18.6" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.9.2" + version: "0.11.0" flutter_native_splash: dependency: "direct dev" description: name: flutter_native_splash url: "https://pub.dartlang.org" source: hosted - version: "2.2.2" + version: "2.2.16" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.8" flutter_settings_ui: dependency: "direct main" description: @@ -299,14 +320,14 @@ packages: name: flutter_slidable url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "2.0.0" flutter_svg: dependency: "direct main" description: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.6" flutter_test: dependency: "direct dev" description: flutter @@ -337,21 +358,21 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.1" graphs: dependency: transitive description: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" html: dependency: transitive description: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.15.0" + version: "0.15.1" html_unescape: dependency: transitive description: @@ -365,35 +386,35 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.13.4" + version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.0.2" image: dependency: transitive description: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.3.0" io: dependency: transitive description: name: io url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: @@ -407,21 +428,21 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.5.0" + version: "4.8.0" just_audio: dependency: "direct main" description: name: just_audio url: "https://pub.dartlang.org" source: hosted - version: "0.9.24" + version: "0.9.31" just_audio_platform_interface: dependency: transitive description: name: just_audio_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.2.0" just_audio_web: dependency: transitive description: @@ -429,34 +450,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.7" - lint: - dependency: transitive - description: - name: lint - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.1.1" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" mdi: dependency: "direct main" description: @@ -470,14 +484,14 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.4" nested: dependency: transitive description: @@ -498,84 +512,77 @@ packages: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: "direct main" description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_drawing: dependency: transitive description: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.10" + version: "2.0.13" path_provider_android: dependency: transitive description: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.14" - path_provider_ios: + version: "2.0.23" + path_provider_foundation: dependency: transitive description: - name: path_provider_ios + name: path_provider_foundation url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.1.2" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" + version: "2.1.9" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.1.4" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.1.0" platform: dependency: transitive description: @@ -589,14 +596,21 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.4" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.2" pool: dependency: transitive description: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.5.1" process: dependency: transitive description: @@ -610,161 +624,133 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.3" + version: "6.0.5" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" + ruby_text: + dependency: "direct main" + description: + name: ruby_text + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" rxdart: dependency: transitive description: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.27.4" + version: "0.27.7" share_plus: dependency: "direct main" description: name: share_plus url: "https://pub.dartlang.org" source: hosted - version: "4.0.5" - share_plus_linux: - dependency: transitive - description: - name: share_plus_linux - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - share_plus_macos: - dependency: transitive - description: - name: share_plus_macos - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" + version: "6.3.1" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.0.3" - share_plus_web: - dependency: transitive - description: - name: share_plus_web - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - share_plus_windows: - dependency: transitive - description: - name: share_plus_windows - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" + version: "3.2.0" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.15" + version: "2.0.18" shared_preferences_android: dependency: transitive description: name: shared_preferences_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.12" - shared_preferences_ios: + version: "2.0.16" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_ios + name: shared_preferences_foundation url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.4" + version: "2.1.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.4" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.4.0" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" shelf_static: dependency: transitive description: name: shelf_static url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.3" signature: dependency: "direct main" description: name: signature url: "https://pub.dartlang.org" source: hosted - version: "5.0.1" + version: "5.3.0" sky_engine: dependency: transitive description: flutter @@ -776,49 +762,49 @@ packages: name: source_map_stack_trace url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" source_maps: dependency: transitive description: name: source_maps url: "https://pub.dartlang.org" source: hosted - version: "0.10.10" + version: "0.10.12" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" sqflite: dependency: "direct main" description: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.2+1" + version: "2.2.4+1" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.2.1+1" + version: "2.4.2+2" sqflite_common_ffi: dependency: "direct main" description: name: sqflite_common_ffi url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.2.1+1" sqlite3: dependency: transitive description: name: sqlite3 url: "https://pub.dartlang.org" source: hosted - version: "1.7.1" + version: "1.9.1" stack_trace: dependency: transitive description: @@ -839,56 +825,63 @@ packages: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" synchronized: dependency: transitive description: name: synchronized url: "https://pub.dartlang.org" source: hosted - version: "3.0.0+2" + version: "3.0.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test: dependency: "direct main" description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.1" + version: "1.21.4" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.13" + version: "0.4.16" timing: dependency: transitive description: name: timing url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" typed_data: dependency: transitive description: @@ -902,7 +895,7 @@ packages: name: universal_io url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.2.0" unofficial_jisho_api: dependency: "direct main" description: @@ -916,63 +909,63 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.1.2" + version: "6.1.10" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.17" + version: "6.0.24" url_launcher_ios: dependency: transitive description: name: url_launcher_ios url: "https://pub.dartlang.org" source: hosted - version: "6.0.17" + version: "6.1.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.3" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.3" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.15" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.4" uuid: dependency: transitive description: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "3.0.6" + version: "3.0.7" vector_math: dependency: transitive description: @@ -986,42 +979,42 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "8.3.0" + version: "9.4.0" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.3.0" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "3.1.3" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0+1" + version: "1.0.0" xml: dependency: transitive description: @@ -1037,5 +1030,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=2.11.0-0.1.pre" + dart: ">=2.18.4 <3.0.0" + flutter: ">=3.3.0" diff --git a/pubspec.yaml b/pubspec.yaml index d302ac0..b925e29 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,19 +3,20 @@ description: A dictionary app for studying japanese version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.18.4 <3.0.0" dependencies: animated_size_and_fade: ^3.0.0 + auto_size_text: ^3.0.0 collection: ^1.15.0 confirm_dialog: ^1.0.0 division: ^0.9.0 - file_picker: ^4.5.1 + file_picker: ^5.2.5 flutter: sdk: flutter flutter_bloc: ^8.0.0 flutter_settings_ui: ^2.0.1 - flutter_slidable: ^1.1.0 + flutter_slidable: ^2.0.0 flutter_svg: ^1.0.2 get_it: ^7.2.0 http: ^0.13.4 @@ -23,13 +24,16 @@ dependencies: mdi: ^5.0.0-nullsafety.0 path: ^1.8.0 path_provider: ^2.0.2 - share_plus: ^4.0.4 + ruby_text: ^3.0.1 + share_plus: ^6.3.1 shared_preferences: ^2.0.6 signature: ^5.0.0 sqflite: ^2.0.2 sqflite_common_ffi: ^2.1.1 test: ^1.19.5 unofficial_jisho_api: ^3.0.0 + # unofficial_jisho_api: + # path: /home/h7x4/git/unofficial-jisho-api-dart url_launcher: ^6.0.9 dev_dependencies: @@ -37,7 +41,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_native_splash: ^2.1.6 - flutter_launcher_icons: "^0.9.2" + flutter_launcher_icons: ^0.11.0 flutter_icons: android: "launcher_icon" diff --git a/tools/extractDB.sh b/tools/extractDB.sh new file mode 100755 index 0000000..5e391b1 --- /dev/null +++ b/tools/extractDB.sh @@ -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 \ No newline at end of file diff --git a/tools/extractExports.sh b/tools/extractExports.sh new file mode 100755 index 0000000..2164fd9 --- /dev/null +++ b/tools/extractExports.sh @@ -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 \ No newline at end of file