Compare commits
29 Commits
keep-data-
...
fix-reinit
| Author | SHA1 | Date | |
|---|---|---|---|
|
5f829fea12
|
|||
|
0dad84c603
|
|||
|
26fe8ce4e1
|
|||
|
62bc0d3d71
|
|||
|
15996b9b77
|
|||
|
a326712794
|
|||
|
60d5ecd25b
|
|||
|
23793f72a1
|
|||
|
ec73851f1b
|
|||
|
09d96254d8
|
|||
|
96182be5c7
|
|||
|
2120d4e54a
|
|||
|
ae5bbf8702
|
|||
|
696a0cd3ca
|
|||
|
c9ec88d3ca
|
|||
|
4dcfb15db8
|
|||
|
83f15718c1
|
|||
|
2531b2bbab
|
|||
|
4c49874a01
|
|||
|
124b98c342
|
|||
|
2d158079d1
|
|||
|
e2a20fa731
|
|||
|
04cb44c39c
|
|||
|
48cfa7f1c6
|
|||
|
908891ea2f
|
|||
|
45a5810de1
|
|||
|
923f5e48f6
|
|||
|
353d5c4c39
|
|||
|
a280c9e77a
|
@@ -1,6 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="mugiten"
|
||||
android:label="Mugiten"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
17
docs/changelog/v0.1.0.md
Normal file
17
docs/changelog/v0.1.0.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# v0.1.0
|
||||
|
||||
Initial beta version of the project.
|
||||
|
||||
This version denotes the first version after the complete rewrite from JST.
|
||||
In this version, we are no longer depending on jisho.org for any data - everything
|
||||
is managed with sqlite through `jadb`. This comes at a cost of bigger size, less accurate
|
||||
search, temporarily disabling existing features such as library lists and history tracking,
|
||||
and several missing pieces of data. But it is a step towards more control, and offline performant
|
||||
search.
|
||||
|
||||
## Current features ✨
|
||||
|
||||
- Word search by input
|
||||
- Kanji search, including search by radical, drawing, grade and input
|
||||
- Font selection
|
||||
- Romaji mode
|
||||
13
docs/changelog/v0.2.0.md
Normal file
13
docs/changelog/v0.2.0.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# v0.2.0
|
||||
|
||||
Major update readding some big features from JST, plus some more
|
||||
|
||||
## New features ✨
|
||||
|
||||
- Readd history tracking
|
||||
- Readd library lists
|
||||
- Add kanji drawing input for word search
|
||||
|
||||
## Bugfixes 🐞
|
||||
|
||||
- Fix kanji filtering sometimes suggesting invalid kanji.
|
||||
34
docs/changelog/v0.3.0.md
Normal file
34
docs/changelog/v0.3.0.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# v0.3.0
|
||||
|
||||
A large update with some significant additions and fixes.
|
||||
|
||||
## New features ✨
|
||||
|
||||
- Added offline kanji drawing search via Google ML Kit 🔥
|
||||
- Allow users to import and export their data
|
||||
- Added a toggle in settings for enabling/disabling history tracking
|
||||
- Display the number of history searches on the history page
|
||||
- Embed the changelog in the app
|
||||
- Added search count bubbles to the history entries
|
||||
- Added a toggle in settings to shrink the kanji drawing board
|
||||
- Added an initialization view to perform updates and large downloads on launch
|
||||
|
||||
## Changes 🔧
|
||||
|
||||
- Removed the "Clear Favourites" button from settings in favour of using the delete button on the favourites page.
|
||||
- Removed the "Reset database" button from settings in favour of "Clear History" and deleting individual library lists.
|
||||
|
||||
## Bugfixes 🐞
|
||||
|
||||
- Always show exact matches first in the word search results.
|
||||
- Respect the cursor position when inserting a kanji into the search bar from the kanji drawing page.
|
||||
- Remove word search duplicates
|
||||
- Fix version number in the info page.
|
||||
- Fix a broken query for adding items to library lists.
|
||||
- Ensure that the kanji drawing search is located above navigation buttons
|
||||
- Capitalize app name
|
||||
|
||||
## Other 📝
|
||||
|
||||
- Significant performance improvements for word search.
|
||||
- A small size reduction of the database.
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1744463964,
|
||||
"narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=",
|
||||
"lastModified": 1751792365,
|
||||
"narHash": "sha256-J1kI6oAj25IG4EdVlg2hQz8NZTBNYvIS0l4wpr9KcUo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650",
|
||||
"rev": "1fd8bada0b6117e6c7eb54aad5813023eed37ccb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -237,22 +237,32 @@ class _DrawingBoardState extends State<DrawingBoard> {
|
||||
),
|
||||
);
|
||||
|
||||
Widget drawingPanel() => AspectRatio(
|
||||
aspectRatio: 1.2,
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
ClipRect(
|
||||
child: Signature(
|
||||
key: signatureW,
|
||||
controller: controller,
|
||||
backgroundColor: panelColor.background,
|
||||
),
|
||||
Widget drawingPanel() {
|
||||
final board = AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
ClipRect(
|
||||
child: Signature(
|
||||
key: signatureW,
|
||||
controller: controller,
|
||||
backgroundColor: panelColor.background,
|
||||
),
|
||||
buttonRow(),
|
||||
],
|
||||
),
|
||||
),
|
||||
buttonRow(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (reduceKanjiDrawingBoardSize) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(50, 0, 50, 30),
|
||||
child: board,
|
||||
);
|
||||
}
|
||||
return board;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:mugiten/bloc/theme/theme_bloc.dart';
|
||||
import 'package:mugiten/components/search/search_results_body/parts/circle_badge.dart';
|
||||
|
||||
import '../../models/history/history_entry.dart';
|
||||
import '../../routing/routes.dart';
|
||||
@@ -92,14 +95,24 @@ class HistoryEntryTile extends StatelessWidget {
|
||||
child: Text(formatTime(entry.lastTimestamp)),
|
||||
),
|
||||
DefaultTextStyle.merge(
|
||||
style: japaneseFont.textStyle,
|
||||
child: entry.isKanji
|
||||
? KanjiBox.headline4(
|
||||
context: context,
|
||||
kanji: entry.kanji!,
|
||||
)
|
||||
: Expanded(child: Text(entry.word!)),
|
||||
),
|
||||
style: japaneseFont.textStyle,
|
||||
child: entry.isKanji
|
||||
? KanjiBox.headline4(
|
||||
context: context,
|
||||
kanji: entry.kanji!,
|
||||
)
|
||||
: Expanded(child: Text(entry.word!))),
|
||||
if (entry.isKanji) Expanded(child: SizedBox.shrink()),
|
||||
if ((entry.timestampCount ?? 0) > 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: BlocBuilder<ThemeBloc, ThemeState>(
|
||||
builder: (context, themeState) => CircleBadge(
|
||||
color: themeState.theme.menuGreyNormal.background,
|
||||
child: Text('${entry.timestampCount}'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -7,7 +7,8 @@ import '../../settings.dart';
|
||||
import 'language_selector.dart';
|
||||
|
||||
class GlobalSearchBar extends StatelessWidget {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
final TextEditingController textController = TextEditingController();
|
||||
final FocusNode textFocus = FocusNode();
|
||||
|
||||
GlobalSearchBar({super.key});
|
||||
|
||||
@@ -25,7 +26,8 @@ class GlobalSearchBar extends StatelessWidget {
|
||||
children: [
|
||||
TextField(
|
||||
onSubmitted: (text) => _search(context, text),
|
||||
controller: controller,
|
||||
controller: textController,
|
||||
focusNode: textFocus,
|
||||
style: japaneseFont.textStyle,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Search',
|
||||
@@ -41,8 +43,8 @@ class GlobalSearchBar extends StatelessWidget {
|
||||
color: AppTheme.mugitenWheat.background,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (controller.text.isNotEmpty) {
|
||||
_search(context, controller.text);
|
||||
if (textController.text.isNotEmpty) {
|
||||
_search(context, textController.text);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
@@ -63,7 +65,21 @@ class GlobalSearchBar extends StatelessWidget {
|
||||
onPressed: () async {
|
||||
final result = await _drawKanji()(context);
|
||||
if (result != null && result.isNotEmpty) {
|
||||
controller.text += result;
|
||||
if (textController.selection.isValid) {
|
||||
final pos = textController.selection.baseOffset;
|
||||
textController.text = textController.text.substring(0, pos) +
|
||||
result +
|
||||
textController.text.substring(pos);
|
||||
textController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: pos + result.length),
|
||||
);
|
||||
} else {
|
||||
textController.text += result;
|
||||
textController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: textController.text.length),
|
||||
);
|
||||
textFocus.requestFocus();
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -78,17 +94,19 @@ class GlobalSearchBar extends StatelessWidget {
|
||||
final MaterialPageRoute<String> route = MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Draw a kanji')),
|
||||
body: Column(
|
||||
children: [
|
||||
const Expanded(child: Column()),
|
||||
DrawingBoard(
|
||||
onlyOneCharacterSuggestions: true,
|
||||
onSuggestionChosen: (suggestion) => Navigator.pop(
|
||||
context,
|
||||
suggestion,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const Expanded(child: Column()),
|
||||
DrawingBoard(
|
||||
onlyOneCharacterSuggestions: true,
|
||||
onSuggestionChosen: (suggestion) => Navigator.pop(
|
||||
context,
|
||||
suggestion,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -5,12 +5,15 @@ import 'dart:io';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:jadb/search.dart';
|
||||
import 'package:mugiten/models/verify_tables.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
export 'package:sqflite/sqlite_api.dart';
|
||||
|
||||
const int expectedDatabaseVersion = 2;
|
||||
|
||||
Database db() => GetIt.instance.get<Database>();
|
||||
|
||||
/// Returns the directory where mugiten's database file is stored.
|
||||
@@ -25,6 +28,35 @@ Future<String> databasePath() async {
|
||||
return join((await _databaseDir()).path, 'mugiten.sqlite');
|
||||
}
|
||||
|
||||
Future<bool> databaseNeedsInitialization() async {
|
||||
final String dbPath = await databasePath();
|
||||
|
||||
if (!await File(dbPath).exists()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Database database = await openDatabase(
|
||||
dbPath,
|
||||
readOnly: true,
|
||||
singleInstance: true,
|
||||
);
|
||||
final databaseVersion = await database.getVersion();
|
||||
await database.close();
|
||||
|
||||
if (databaseVersion < expectedDatabaseVersion) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> quickInitializeDatabase() async {
|
||||
// TODO: Create more lightweight solution
|
||||
await setupDatabase();
|
||||
}
|
||||
|
||||
/// Migration logic and heavy initialization
|
||||
|
||||
class DatabaseMigration {
|
||||
final String path;
|
||||
final String content;
|
||||
@@ -95,6 +127,35 @@ Future<void> migrate(Database db, List<DatabaseMigration> migrations) async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Database> openDatabaseWithoutMigrations(
|
||||
String dbPath, {
|
||||
bool readOnly = false,
|
||||
bool verifyTables = true,
|
||||
}) async {
|
||||
log('Opening database at $dbPath');
|
||||
final Database database = await openDatabase(
|
||||
dbPath,
|
||||
version: expectedDatabaseVersion,
|
||||
readOnly: readOnly,
|
||||
onConfigure: (db) async {
|
||||
// Enable foreign key constraints
|
||||
await db.execute('PRAGMA foreign_keys=ON');
|
||||
},
|
||||
onOpen: (db) async {
|
||||
if (verifyTables) {
|
||||
log('Verifying jadb tables...');
|
||||
db.jadbVerifyTables();
|
||||
|
||||
log('Verifying mugiten tables...');
|
||||
verifyMugitenTablesWithDbConnection(db);
|
||||
|
||||
log('Database tables verified successfully');
|
||||
}
|
||||
},
|
||||
);
|
||||
return database;
|
||||
}
|
||||
|
||||
Future<Database> openAndMigrateDatabase(
|
||||
String dbPath,
|
||||
List<DatabaseMigration> migrations,
|
||||
@@ -102,7 +163,7 @@ Future<Database> openAndMigrateDatabase(
|
||||
log('Opening database at $dbPath');
|
||||
final Database database = await openDatabase(
|
||||
dbPath,
|
||||
version: 2,
|
||||
version: expectedDatabaseVersion,
|
||||
readOnly: false,
|
||||
onUpgrade: (db, oldVersion, newVersion) async {
|
||||
log('Migrating database from v$oldVersion to v$newVersion...');
|
||||
@@ -119,10 +180,12 @@ Future<Database> openAndMigrateDatabase(
|
||||
},
|
||||
onOpen: (db) async {
|
||||
log('Verifying jadb tables...');
|
||||
|
||||
db.jadbVerifyTables();
|
||||
|
||||
log('jadb opened successfully');
|
||||
log('Verifying jadb tables...');
|
||||
verifyMugitenTablesWithDbConnection(db);
|
||||
|
||||
log('Database tables verified successfully');
|
||||
},
|
||||
);
|
||||
return database;
|
||||
@@ -134,14 +197,17 @@ Future<void> setupDatabase() async {
|
||||
|
||||
final String dbPath = await databasePath();
|
||||
|
||||
if (!await File(dbPath).exists()) {
|
||||
log('Extracting jadb.sqlite from assets...');
|
||||
await extractJadbFromAssets(dbPath);
|
||||
log('jadb.sqlite extracted to $dbPath');
|
||||
}
|
||||
assert(
|
||||
await File(dbPath).exists(), 'Database file should exist at this point');
|
||||
|
||||
final List<DatabaseMigration> migrations = await readMigrationsFromAssets();
|
||||
final Database database = await openAndMigrateDatabase(dbPath, migrations);
|
||||
final database = await openDatabaseWithoutMigrations(
|
||||
dbPath,
|
||||
readOnly: false,
|
||||
verifyTables: true,
|
||||
);
|
||||
|
||||
assert(await database.getVersion() == expectedDatabaseVersion,
|
||||
'Database version should be $expectedDatabaseVersion');
|
||||
|
||||
log('Registering database in GetIt...');
|
||||
GetIt.instance.registerSingleton<Database>(database);
|
||||
|
||||
@@ -31,4 +31,12 @@ abstract class HistoryTableNames {
|
||||
/// - language CHAR(1)?
|
||||
static const String historyEntryOrderedByTimestamp =
|
||||
'Mugiten_HistoryEntry_orderedByTimestamp';
|
||||
|
||||
static Set<String> get allTables => {
|
||||
historyEntry,
|
||||
historyEntryKanji,
|
||||
historyEntryTimestamp,
|
||||
historyEntryWord,
|
||||
historyEntryOrderedByTimestamp,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,4 +20,10 @@ abstract class LibraryListTableNames {
|
||||
/// Attributes:
|
||||
/// - name TEXT
|
||||
static const String libraryListOrdered = 'Mugiten_LibraryList_Ordered';
|
||||
|
||||
static Set<String> get allTables => {
|
||||
libraryList,
|
||||
libraryListEntry,
|
||||
libraryListOrdered,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_mlkit_digital_ink_recognition/google_mlkit_digital_ink_recognition.dart';
|
||||
import 'package:mugiten/database/database.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:mugiten/screens/initialization.dart';
|
||||
import 'package:mugiten/services/initialization/initialization_logic.dart';
|
||||
|
||||
import 'bloc/theme/theme_bloc.dart';
|
||||
import 'routing/router.dart';
|
||||
import 'services/licenses.dart';
|
||||
import 'services/preferences.dart';
|
||||
import 'settings.dart';
|
||||
|
||||
void runInitializationScreen(bool deleteDatabase) {
|
||||
runApp(
|
||||
InitializationView(
|
||||
onInitializationComplete: () =>
|
||||
quickInitialization().then((_) => runApp(const MyApp())),
|
||||
deleteDatabase: deleteDatabase,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
|
||||
await Future.wait([
|
||||
setupDatabase(),
|
||||
setupSharedPreferences(),
|
||||
(() async {
|
||||
final modelManager = DigitalInkRecognizerModelManager();
|
||||
if (!await modelManager.isModelDownloaded('ja')) {
|
||||
await modelManager.downloadModel('ja');
|
||||
}
|
||||
})()
|
||||
]);
|
||||
|
||||
registerExtraLicenses();
|
||||
|
||||
runApp(const MyApp());
|
||||
if (await needsInitialization()) {
|
||||
runInitializationScreen(false);
|
||||
} else {
|
||||
await quickInitialization();
|
||||
runApp(const MyApp());
|
||||
}
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
|
||||
@@ -10,6 +10,7 @@ class HistoryEntry {
|
||||
final String? kanji;
|
||||
final String? word;
|
||||
final DateTime lastTimestamp;
|
||||
final int? timestampCount;
|
||||
|
||||
/// Whether this item is a kanji search or a word search
|
||||
bool get isKanji => word == null;
|
||||
@@ -18,12 +19,14 @@ class HistoryEntry {
|
||||
required this.id,
|
||||
required this.kanji,
|
||||
required this.lastTimestamp,
|
||||
this.timestampCount,
|
||||
}) : word = null;
|
||||
|
||||
HistoryEntry.withWord({
|
||||
required this.id,
|
||||
required this.word,
|
||||
required this.lastTimestamp,
|
||||
this.timestampCount,
|
||||
}) : kanji = null;
|
||||
|
||||
/// Reconstruct a HistoryEntry object with data from the database
|
||||
@@ -43,6 +46,9 @@ class HistoryEntry {
|
||||
lastTimestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||
dbObject['timestamp']! as int,
|
||||
),
|
||||
timestampCount: dbObject.containsKey('timestampCount')
|
||||
? dbObject['timestampCount']! as int
|
||||
: null,
|
||||
)
|
||||
: HistoryEntry.withKanji(
|
||||
id: dbObject['entryId']! as int,
|
||||
@@ -50,6 +56,9 @@ class HistoryEntry {
|
||||
lastTimestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||
dbObject['timestamp']! as int,
|
||||
),
|
||||
timestampCount: dbObject.containsKey('timestampCount')
|
||||
? dbObject['timestampCount']! as int
|
||||
: null,
|
||||
);
|
||||
|
||||
// TODO: There is a lot in common with
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:mugiten/database/library_list/table_names.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
@@ -10,14 +11,14 @@ import 'library_entry.dart';
|
||||
class LibraryList {
|
||||
final String name;
|
||||
|
||||
const LibraryList._byName(this.name);
|
||||
const LibraryList.byName(this.name);
|
||||
|
||||
static const LibraryList favourites = LibraryList._byName('favourites');
|
||||
static const LibraryList favourites = LibraryList.byName('favourites');
|
||||
|
||||
/// Get all entries within the library, in their custom order
|
||||
Future<List<LibraryEntry>> get entries async {
|
||||
Future<List<LibraryEntry>> entries(DatabaseExecutor db) async {
|
||||
const columns = ['jmdictEntryId', 'kanji', 'lastModified'];
|
||||
final query = await db().rawQuery(
|
||||
final query = await db.rawQuery(
|
||||
'''
|
||||
WITH RECURSIVE
|
||||
"RecursionTable"(${columns.map((c) => '"$c"').join(', ')}) AS (
|
||||
@@ -48,7 +49,7 @@ class LibraryList {
|
||||
static Future<List<LibraryList>> get allLibraries async {
|
||||
final query = await db().query(LibraryListTableNames.libraryListOrdered);
|
||||
return query
|
||||
.map((lib) => LibraryList._byName(lib['name']! as String))
|
||||
.map((lib) => LibraryList.byName(lib['name']! as String))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -83,7 +84,7 @@ class LibraryList {
|
||||
return Map.fromEntries(
|
||||
query.map(
|
||||
(lib) => MapEntry(
|
||||
LibraryList._byName(lib['name']! as String),
|
||||
LibraryList.byName(lib['name']! as String),
|
||||
lib['exists']! as int == 1,
|
||||
),
|
||||
),
|
||||
@@ -206,7 +207,7 @@ class LibraryList {
|
||||
|
||||
final b = db().batch();
|
||||
|
||||
final entries_ = await entries;
|
||||
final entries_ = await entries(db());
|
||||
final prevEntry = entries_[position - 1];
|
||||
final nextEntry = entries_[position];
|
||||
|
||||
@@ -238,7 +239,7 @@ class LibraryList {
|
||||
'Adding ${jmdictEntryId != null ? 'jmdict entry $jmdictEntryId' : 'kanji "$kanji"'} to library "$name"',
|
||||
);
|
||||
|
||||
final LibraryEntry? prevEntry = (await entries).lastOrNull;
|
||||
final LibraryEntry? prevEntry = (await entries(db())).lastOrNull;
|
||||
|
||||
await db().insert(LibraryListTableNames.libraryListEntry, {
|
||||
'listName': name,
|
||||
@@ -249,6 +250,32 @@ class LibraryList {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> insertJsonEntries(
|
||||
List<Map<String, Object?>> jsonEntries,
|
||||
) async {
|
||||
List<LibraryEntry> entries =
|
||||
jsonEntries.map((e) => LibraryEntry.fromJson(e)).toList();
|
||||
|
||||
// TODO: batch
|
||||
for (final entry in entries) {
|
||||
if (entry.kanji != null) {
|
||||
await insertEntry(
|
||||
kanji: entry.kanji,
|
||||
jmdictEntryId: null,
|
||||
position: null,
|
||||
lastModified: entry.lastModified,
|
||||
);
|
||||
} else if (entry.jmdictEntryId != null) {
|
||||
await insertEntry(
|
||||
jmdictEntryId: entry.jmdictEntryId,
|
||||
kanji: null,
|
||||
position: null,
|
||||
lastModified: entry.lastModified,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes an entry within a list
|
||||
/// Will throw an exception if the entry is not in the library
|
||||
Future<void> deleteEntry({
|
||||
@@ -418,7 +445,7 @@ class LibraryList {
|
||||
'name': libraryName,
|
||||
'prevList': prevList.name,
|
||||
});
|
||||
return LibraryList._byName(libraryName);
|
||||
return LibraryList.byName(libraryName);
|
||||
}
|
||||
|
||||
/// Delete this library from the database
|
||||
@@ -429,10 +456,87 @@ class LibraryList {
|
||||
illegalArguments: {'name': name},
|
||||
);
|
||||
}
|
||||
await db().delete(
|
||||
|
||||
await db().transaction((txn) async {
|
||||
await txn.delete(
|
||||
LibraryListTableNames.libraryListEntry,
|
||||
where: 'listName = ?',
|
||||
whereArgs: [name],
|
||||
);
|
||||
|
||||
final String prevName = await txn
|
||||
.query(
|
||||
LibraryListTableNames.libraryList,
|
||||
columns: ['prevList'],
|
||||
where: 'name = ?',
|
||||
whereArgs: [name],
|
||||
)
|
||||
.then((rows) => rows.first['prevList']! as String);
|
||||
|
||||
final String? nextName = await txn
|
||||
.query(
|
||||
LibraryListTableNames.libraryList,
|
||||
columns: ['name'],
|
||||
where: 'prevList = ?',
|
||||
whereArgs: [name],
|
||||
)
|
||||
.then((rows) => rows.firstOrNull?['name'] as String?);
|
||||
|
||||
await txn.delete(
|
||||
LibraryListTableNames.libraryList,
|
||||
where: 'name = ?',
|
||||
whereArgs: [name],
|
||||
);
|
||||
|
||||
if (nextName != null) {
|
||||
await txn.update(
|
||||
LibraryListTableNames.libraryList,
|
||||
{'prevList': prevName},
|
||||
where: 'name = ?',
|
||||
whereArgs: [nextName],
|
||||
);
|
||||
}
|
||||
|
||||
if (!await verifyLibrariesLinkedList(txn)) {
|
||||
print(
|
||||
'Library list "$name" has a broken linked list after deletion, rolling back');
|
||||
txn.execute('ROLLBACK');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> verifyLibrariesLinkedList(
|
||||
DatabaseExecutor db,
|
||||
) async {
|
||||
final int allItemsCount = await db.query(
|
||||
LibraryListTableNames.libraryList,
|
||||
where: 'name = ?',
|
||||
whereArgs: [name],
|
||||
);
|
||||
columns: ['COUNT(*) AS count'],
|
||||
).then((rows) => rows.first['count']! as int);
|
||||
|
||||
final int distinctPrevListCount = await db.query(
|
||||
LibraryListTableNames.libraryList,
|
||||
columns: ['COUNT(DISTINCT prevList) AS count'],
|
||||
).then((rows) => (rows.first['count']! as int) + 1);
|
||||
|
||||
final int recursiveCount = await db.query(
|
||||
LibraryListTableNames.libraryListOrdered,
|
||||
columns: ['COUNT(*) AS count'],
|
||||
).then((rows) => rows.first['count']! as int);
|
||||
|
||||
if (allItemsCount != distinctPrevListCount) {
|
||||
log(
|
||||
'Library list "$name" has a mismatch between all items count ($allItemsCount) and distinct prevList count ($distinctPrevListCount).',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (recursiveCount != allItemsCount) {
|
||||
log(
|
||||
'Library list "$name" has a mismatch between recursive count ($recursiveCount) and all items count ($allItemsCount).',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
35
lib/models/verify_tables.dart
Normal file
35
lib/models/verify_tables.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:mugiten/database/history/table_names.dart';
|
||||
import 'package:mugiten/database/library_list/table_names.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
Future<void> verifyMugitenTablesWithDbConnection(DatabaseExecutor db) async {
|
||||
final Set<String> tables = await db
|
||||
.query(
|
||||
'sqlite_master',
|
||||
columns: ['name'],
|
||||
where: 'type IN (?, ?)',
|
||||
whereArgs: ['table', 'view'],
|
||||
)
|
||||
.then((result) {
|
||||
return result.map((row) => row['name'] as String).toSet();
|
||||
});
|
||||
|
||||
final Set<String> expectedTables = {
|
||||
...HistoryTableNames.allTables,
|
||||
...LibraryListTableNames.allTables,
|
||||
};
|
||||
|
||||
final missingTables = expectedTables.difference(tables);
|
||||
|
||||
if (missingTables.isNotEmpty) {
|
||||
throw Exception([
|
||||
'Missing tables:',
|
||||
missingTables.map((table) => ' - $table').join('\n'),
|
||||
'',
|
||||
'Found tables:\n',
|
||||
tables.map((table) => ' - $table').join('\n'),
|
||||
'',
|
||||
'Please ensure the database is correctly set up.',
|
||||
].join('\n'));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mugiten/models/library/library_list.dart';
|
||||
import 'package:mugiten/screens/info/changelog.dart';
|
||||
import 'package:mugiten/screens/library/library_content_view.dart';
|
||||
import 'package:mugiten/screens/search/kanji_search_result_page.dart';
|
||||
import 'package:mugiten/screens/search/word_search_result_page.dart';
|
||||
@@ -52,6 +53,8 @@ Route<Widget> generateRoute(RouteSettings settings) {
|
||||
|
||||
case Routes.aboutLicenses:
|
||||
return MaterialPageRoute(builder: (_) => const LicensesView());
|
||||
case Routes.aboutChangelog:
|
||||
return MaterialPageRoute(builder: (_) => const ChangelogView());
|
||||
|
||||
// TODO: Add more specific error screens.
|
||||
case Routes.errorNotFound:
|
||||
|
||||
@@ -6,6 +6,7 @@ abstract class Routes {
|
||||
static const String kanjiSearchGrade = '/kanjiSearch/grade';
|
||||
static const String kanjiSearchRadicals = '/kanjiSearch/radicals';
|
||||
static const String aboutLicenses = '/info/licenses';
|
||||
static const String aboutChangelog = '/info/changelog';
|
||||
static const String errorNotFound = '/error/404';
|
||||
static const String errorNetwork = '/error/network';
|
||||
static const String errorOther = '/error/other';
|
||||
|
||||
@@ -29,7 +29,7 @@ class HistoryView extends StatelessWidget {
|
||||
|
||||
return OpaqueBox(
|
||||
child: ListView.separated(
|
||||
itemCount: data.length + 1,
|
||||
itemCount: data.length + 2,
|
||||
itemBuilder: historyEntryWithData(data),
|
||||
separatorBuilder:
|
||||
historyEntrySeparatorWithData(data.values.toList()),
|
||||
@@ -46,8 +46,9 @@ 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 != 1 &&
|
||||
(index == 0 ||
|
||||
!dateIsEqual(data[index - 2].lastTimestamp, searchDate))) {
|
||||
return TextDivider(text: formatDate(roundToDay(searchDate)));
|
||||
}
|
||||
|
||||
@@ -60,12 +61,28 @@ class HistoryView extends StatelessWidget {
|
||||
|
||||
Widget Function(BuildContext, int) historyEntryWithData(
|
||||
Map<int, HistoryEntry> data,
|
||||
) =>
|
||||
(context, index) => (index == 0)
|
||||
? const SizedBox.shrink()
|
||||
: HistoryEntryTile(
|
||||
entry: data.values.toList()[index - 1],
|
||||
objectKey: data.keys.toList()[index - 1],
|
||||
onDelete: () => build(context),
|
||||
);
|
||||
) {
|
||||
return (context, index) {
|
||||
return switch (index) {
|
||||
0 => const SizedBox.shrink(),
|
||||
1 => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${data.length} distinct searches made',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
int i => HistoryEntryTile(
|
||||
entry: data.values.toList()[i - 2],
|
||||
objectKey: data.keys.toList()[i - 2],
|
||||
onDelete: () => build(context),
|
||||
),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:mdi/mdi.dart';
|
||||
import 'package:mugiten/screens/search/kanji_search_view.dart';
|
||||
import 'package:mugiten/screens/search/word_search_view.dart';
|
||||
import 'package:mugiten/services/snackbar.dart';
|
||||
import 'package:mugiten/settings.dart';
|
||||
|
||||
import '../bloc/theme/theme_bloc.dart';
|
||||
import '../components/common/denshi_jisho_background.dart';
|
||||
@@ -61,16 +63,30 @@ class _HomeState extends State<Home> {
|
||||
}
|
||||
|
||||
List<_Page> get pages => [
|
||||
const _Page(
|
||||
content: WordSearchView(),
|
||||
titleBar: 'Search',
|
||||
icon: Icon(Icons.search),
|
||||
),
|
||||
const _Page(
|
||||
content: KanjiSearchView(),
|
||||
titleBar: 'Kanji Search',
|
||||
icon: Icon(Mdi.ideogramCjk, size: 30),
|
||||
),
|
||||
_Page(
|
||||
content: WordSearchView(),
|
||||
titleBar: 'Search',
|
||||
icon: Icon(Icons.search),
|
||||
actions: [
|
||||
if (incognitoModeEnabled)
|
||||
IconButton(
|
||||
icon: const Icon(Mdi.incognito),
|
||||
onPressed: () =>
|
||||
showSnackbar(context, 'History tracking is disabled'),
|
||||
),
|
||||
]),
|
||||
_Page(
|
||||
content: KanjiSearchView(),
|
||||
titleBar: 'Kanji Search',
|
||||
icon: Icon(Mdi.ideogramCjk, size: 30),
|
||||
actions: [
|
||||
if (incognitoModeEnabled)
|
||||
IconButton(
|
||||
icon: const Icon(Mdi.incognito),
|
||||
onPressed: () =>
|
||||
showSnackbar(context, 'History tracking is disabled'),
|
||||
),
|
||||
]),
|
||||
const _Page(
|
||||
content: HistoryView(),
|
||||
titleBar: 'History',
|
||||
|
||||
122
lib/screens/info/changelog.dart
Normal file
122
lib/screens/info/changelog.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||
import 'package:markdown/markdown.dart' show ExtensionSet;
|
||||
|
||||
class ChangelogView extends StatelessWidget {
|
||||
const ChangelogView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Changelog'),
|
||||
),
|
||||
body: FutureBuilder<List<String>>(
|
||||
future: _fetchChangelogs(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('Error: ${snapshot.error}'));
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final versions = snapshot.data!;
|
||||
return _buildChangelogList(versions);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<String>> _fetchChangelogs() async {
|
||||
final String assetManifest =
|
||||
await rootBundle.loadString('AssetManifest.json');
|
||||
|
||||
final List<String> changelogs =
|
||||
(jsonDecode(assetManifest) as Map<String, Object?>)
|
||||
.keys
|
||||
.where(
|
||||
(assetPath) =>
|
||||
RegExp(r'^docs/changelog/v.*\.md$').hasMatch(assetPath),
|
||||
)
|
||||
.map((assetPath) => assetPath
|
||||
.replaceFirst('docs/changelog/', '')
|
||||
.replaceFirst('.md', ''))
|
||||
.toList();
|
||||
|
||||
changelogs.sort((a, b) {
|
||||
final aVersion =
|
||||
a.replaceFirst(RegExp('^v'), '').split('.').map(int.parse).toList();
|
||||
final bVersion =
|
||||
b.replaceFirst(RegExp('^v'), '').split('.').map(int.parse).toList();
|
||||
for (int i = 0; i < aVersion.length && i < bVersion.length; i++) {
|
||||
if (aVersion[i] != bVersion[i]) {
|
||||
return bVersion[i].compareTo(aVersion[i]);
|
||||
}
|
||||
}
|
||||
return bVersion.length.compareTo(aVersion.length);
|
||||
});
|
||||
|
||||
return changelogs;
|
||||
}
|
||||
|
||||
Widget _buildChangelogList(List<String> versions) {
|
||||
return ListView.builder(
|
||||
itemCount: versions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final version = versions[index];
|
||||
return ListTile(
|
||||
title: Text(version),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
_buildChangelogDetailRoute(version),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _removeHeaders(String markdown) {
|
||||
final lines = markdown.split('\n');
|
||||
final filteredLines = lines.where((line) => !line.startsWith('# '));
|
||||
return filteredLines.join('\n');
|
||||
}
|
||||
|
||||
MaterialPageRoute _buildChangelogDetailRoute(String version) {
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(version),
|
||||
),
|
||||
body: FutureBuilder<String>(
|
||||
future: rootBundle.loadString('docs/changelog/$version.md'),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('Error: ${snapshot.error}'));
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 100),
|
||||
child: MarkdownBody(
|
||||
data: _removeHeaders(snapshot.data!),
|
||||
selectable: true,
|
||||
extensionSet: ExtensionSet.gitHubFlavored,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../settings.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class LicensesView extends StatelessWidget {
|
||||
const LicensesView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => LicensePage(
|
||||
Widget build(BuildContext context) => FutureBuilder<PackageInfo>(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('Error: ${snapshot.error}'));
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final packageInfo = snapshot.data!;
|
||||
return _buildLicensePage(packageInfo);
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildLicensePage(PackageInfo packageInfo) => LicensePage(
|
||||
applicationName: '麦典',
|
||||
applicationVersion: 'Version: $appVersion',
|
||||
applicationVersion: 'Version: ${packageInfo.version}',
|
||||
applicationIcon: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 30),
|
||||
child: Row(
|
||||
|
||||
98
lib/screens/initialization.dart
Normal file
98
lib/screens/initialization.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:mugiten/services/initialization/initialization_cubit.dart';
|
||||
import 'package:mugiten/services/initialization/initialization_status.dart';
|
||||
|
||||
class InitializationView extends StatelessWidget {
|
||||
final VoidCallback? onInitializationComplete;
|
||||
final InitializationCubit cubit;
|
||||
|
||||
InitializationView({
|
||||
super.key,
|
||||
required this.onInitializationComplete,
|
||||
required bool deleteDatabase,
|
||||
}) : cubit = InitializationCubit(deleteDatabase);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
darkTheme: ThemeData.dark(),
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 100),
|
||||
Image.asset(
|
||||
'assets/images/logo/mugi.png',
|
||||
height: 100,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
BlocBuilder<InitializationCubit, InitializationStatus>(
|
||||
bloc: cubit,
|
||||
builder: (context, state) {
|
||||
switch (state) {
|
||||
case InitializationNotStarted _:
|
||||
cubit.start();
|
||||
return const CircularProgressIndicator();
|
||||
|
||||
case InitializationPending _:
|
||||
return const CircularProgressIndicator();
|
||||
|
||||
case CheckMLKitDigitalInkModel _:
|
||||
return const Text('Checking for ML Kit updates...');
|
||||
|
||||
case DownloadMLKitDigitalInkModel _:
|
||||
return const Text('Downloading ML Kit model...');
|
||||
|
||||
case FinishDownloadMLKitDigitalInkModel _:
|
||||
return const Text('ML Kit model downloaded successfully');
|
||||
|
||||
case CheckDatabase _:
|
||||
return const Text('Checking for database updates...');
|
||||
|
||||
case BackupUserData s:
|
||||
return Column(
|
||||
children: [
|
||||
const Text('Backing up user data...'),
|
||||
LinearProgressIndicator(value: s.progress / s.total),
|
||||
],
|
||||
);
|
||||
|
||||
case MigrateDatabase s:
|
||||
return Column(
|
||||
children: [
|
||||
const Text('Performing database migrations...'),
|
||||
LinearProgressIndicator(value: s.progress / s.total),
|
||||
],
|
||||
);
|
||||
|
||||
case RestoreUserData s:
|
||||
return Column(
|
||||
children: [
|
||||
const Text('Restoring user data...'),
|
||||
LinearProgressIndicator(value: s.progress / s.total),
|
||||
],
|
||||
);
|
||||
|
||||
case DatabaseUpdateFinished _:
|
||||
return const Text('Database update finished');
|
||||
|
||||
case InitializationComplete _:
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
onInitializationComplete?.call();
|
||||
});
|
||||
return const Text('Initialization Complete');
|
||||
|
||||
default:
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:confirm_dialog/confirm_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mugiten/database/database.dart';
|
||||
|
||||
import '../../components/common/loading.dart';
|
||||
import '../../components/library/library_list_entry_tile.dart';
|
||||
@@ -21,7 +22,7 @@ class _LibraryContentViewState extends State<LibraryContentView> {
|
||||
List<LibraryEntry>? entries;
|
||||
|
||||
Future<void> getEntriesFromDatabase() =>
|
||||
widget.library.entries.then((es) => setState(() => entries = es));
|
||||
widget.library.entries(db()).then((es) => setState(() => entries = es));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
@@ -2,9 +2,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:jadb/models/kanji_search/kanji_search_result.dart';
|
||||
import 'package:jadb/search.dart';
|
||||
import 'package:mdi/mdi.dart';
|
||||
import 'package:mugiten/components/library/add_to_library_dialog.dart';
|
||||
import 'package:mugiten/models/history/history_entry.dart';
|
||||
import 'package:mugiten/models/library/library_list.dart';
|
||||
import 'package:mugiten/services/snackbar.dart';
|
||||
import 'package:mugiten/settings.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
// import './kanji_result_body/examples.dart';
|
||||
@@ -101,6 +104,12 @@ class _KanjiSearchResultPageState extends State<KanjiSearchResultPage> {
|
||||
Widget _body(KanjiSearchResult result) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(actions: [
|
||||
if (incognitoModeEnabled)
|
||||
IconButton(
|
||||
icon: const Icon(Mdi.incognito),
|
||||
onPressed: () =>
|
||||
showSnackbar(context, 'History tracking is disabled'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.star),
|
||||
color: isFavourite ? Colors.yellow : null,
|
||||
@@ -171,7 +180,7 @@ class _KanjiSearchResultPageState extends State<KanjiSearchResultPage> {
|
||||
}
|
||||
if (snapshot.hasError) return ErrorWidget(snapshot.error!);
|
||||
|
||||
if (!addedToDatabase) {
|
||||
if (!incognitoModeEnabled && !addedToDatabase) {
|
||||
HistoryEntry.insertKanji(kanji: widget.kanji);
|
||||
addedToDatabase = true;
|
||||
}
|
||||
|
||||
@@ -10,18 +10,20 @@ class KanjiDrawingSearch extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Draw a kanji')),
|
||||
body: Column(
|
||||
children: [
|
||||
const Expanded(child: Column()),
|
||||
DrawingBoard(
|
||||
onlyOneCharacterSuggestions: true,
|
||||
onSuggestionChosen: (suggestion) => Navigator.popAndPushNamed(
|
||||
context,
|
||||
Routes.kanjiSearch,
|
||||
arguments: suggestion,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const Expanded(child: Column()),
|
||||
DrawingBoard(
|
||||
onlyOneCharacterSuggestions: true,
|
||||
onSuggestionChosen: (suggestion) => Navigator.popAndPushNamed(
|
||||
context,
|
||||
Routes.kanjiSearch,
|
||||
arguments: suggestion,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:jadb/models/word_search/word_search_result.dart';
|
||||
import 'package:jadb/search.dart' show JaDBConnection;
|
||||
import 'package:mdi/mdi.dart';
|
||||
import 'package:mugiten/models/history/history_entry.dart';
|
||||
import 'package:mugiten/services/snackbar.dart';
|
||||
import 'package:mugiten/settings.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
import '../../components/search/search_results_body/search_card.dart';
|
||||
@@ -27,14 +30,28 @@ class _WordSearchResultPageState extends State<WordSearchResultPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
appBar: AppBar(
|
||||
title: Text('Search'),
|
||||
actions: [
|
||||
if (incognitoModeEnabled)
|
||||
IconButton(
|
||||
icon: const Icon(Mdi.incognito),
|
||||
onPressed: () =>
|
||||
showSnackbar(context, 'History tracking is disabled'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: (() async {
|
||||
final jadbConnection = GetIt.instance.get<Database>();
|
||||
|
||||
final results = await Future.wait([
|
||||
jadbConnection.jadbSearchWordCount(widget.searchTerm),
|
||||
jadbConnection.jadbSearchWord(widget.searchTerm),
|
||||
jadbConnection
|
||||
.jadbSearchWordCount(widget.searchTerm)
|
||||
.then((v) => v ?? 0),
|
||||
jadbConnection
|
||||
.jadbSearchWord(widget.searchTerm)
|
||||
.then((v) => v ?? <WordSearchResult>[]),
|
||||
]);
|
||||
|
||||
return (results[0] as int, results[1] as List<WordSearchResult>);
|
||||
@@ -45,7 +62,7 @@ class _WordSearchResultPageState extends State<WordSearchResultPage> {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (!addedToDatabase) {
|
||||
if (!incognitoModeEnabled && !addedToDatabase) {
|
||||
HistoryEntry.insertWord(word: widget.searchTerm);
|
||||
addedToDatabase = true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:confirm_dialog/confirm_dialog.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
@@ -10,12 +11,14 @@ import 'package:mugiten/bloc/theme/theme_bloc.dart';
|
||||
import 'package:mugiten/components/common/denshi_jisho_background.dart';
|
||||
import 'package:mugiten/database/database.dart';
|
||||
import 'package:mugiten/database/history/table_names.dart';
|
||||
import 'package:mugiten/main.dart';
|
||||
import 'package:mugiten/models/history/history_entry.dart';
|
||||
import 'package:mugiten/models/library/library_list.dart';
|
||||
import 'package:mugiten/routing/routes.dart';
|
||||
import 'package:mugiten/services/data_export_import.dart';
|
||||
import 'package:mugiten/services/snackbar.dart';
|
||||
import 'package:mugiten/settings.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SettingsView extends StatefulWidget {
|
||||
const SettingsView({super.key});
|
||||
@@ -29,16 +32,6 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
bool dataExportIsLoading = false;
|
||||
bool dataImportIsLoading = false;
|
||||
|
||||
// Future<bool?> assertExternalStorePermissions() async {
|
||||
// final permissionStatus = await Permission.storage.status;
|
||||
|
||||
// if (permissionStatus.isGranted) return null;
|
||||
|
||||
// final permissionResult = await Permission.storage.request();
|
||||
|
||||
// return permissionResult.isGranted;
|
||||
// }
|
||||
|
||||
Future<void> clearHistory(context) async {
|
||||
final historyCount = await HistoryEntry.amountOfEntries();
|
||||
|
||||
@@ -59,51 +52,6 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
showSnackbar(context, 'Cleared history');
|
||||
}
|
||||
|
||||
Future<void> clearFavourites(context) async {
|
||||
final favouritesCount = await LibraryList.favourites.length;
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
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();
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
showSnackbar(context, 'Cleared favourites');
|
||||
}
|
||||
|
||||
Future<void> clearAll(context) async {
|
||||
final historyCount = await HistoryEntry.amountOfEntries();
|
||||
final libraryCount = await LibraryList.libraryCount();
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
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();
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
showSnackbar(
|
||||
context,
|
||||
'Database reset successfully.',
|
||||
);
|
||||
}
|
||||
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
void toggleAutoTheme(bool b) {
|
||||
final bool newThemeIsDark = b
|
||||
@@ -226,7 +174,10 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
// titleTextStyle: _titleTextStyle,
|
||||
tiles: <SettingsTile>[
|
||||
SettingsTile.switchTile(
|
||||
title: const Text('Use romaji'),
|
||||
title: const Text('Romaji mode'),
|
||||
description: const Text(
|
||||
'Display romaji instead of kana for word readings',
|
||||
),
|
||||
leading: const Icon(Mdi.alphabetical),
|
||||
onToggle: (b) => setState(() => romajiEnabled = b),
|
||||
initialValue: romajiEnabled,
|
||||
@@ -250,6 +201,8 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
tiles: <SettingsTile>[
|
||||
SettingsTile.switchTile(
|
||||
title: const Text('Automatic theme'),
|
||||
description:
|
||||
const Text('Let theme be determined by system'),
|
||||
leading: const Icon(Icons.brightness_auto),
|
||||
onToggle: toggleAutoTheme,
|
||||
initialValue: autoThemeEnabled,
|
||||
@@ -278,6 +231,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
enabled: true,
|
||||
leading: const Icon(Icons.file_upload),
|
||||
title: const Text('Import Data'),
|
||||
description: const Text('Import user data from a file'),
|
||||
onPressed: importHandler,
|
||||
value: dataImportIsLoading
|
||||
? const LinearProgressIndicator()
|
||||
@@ -287,6 +241,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
enabled: true,
|
||||
leading: const Icon(Icons.file_download),
|
||||
title: const Text('Export Data'),
|
||||
description: const Text('Export user data to a file'),
|
||||
onPressed: exportHandler,
|
||||
value: dataExportIsLoading
|
||||
? const LinearProgressIndicator()
|
||||
@@ -299,25 +254,59 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
'Clear History',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
description: const Text('Delete all search history'),
|
||||
onPressed: clearHistory,
|
||||
),
|
||||
SettingsTile(
|
||||
enabled: true,
|
||||
leading: const Icon(Icons.delete),
|
||||
title: const Text(
|
||||
'Clear Favourites',
|
||||
style: TextStyle(color: Colors.red),
|
||||
],
|
||||
),
|
||||
SettingsSection(
|
||||
title: Text('Misc', style: titleTextStyle),
|
||||
tiles: <SettingsTile>[
|
||||
SettingsTile.switchTile(
|
||||
leading: const Icon(Mdi.incognito),
|
||||
title: const Text('Disable history tracking'),
|
||||
description: const Text(
|
||||
'Useful for reviewing history for library lists without cluttering the order',
|
||||
),
|
||||
onPressed: clearFavourites,
|
||||
onToggle: (b) => setState(() => incognitoModeEnabled = b),
|
||||
initialValue: incognitoModeEnabled,
|
||||
activeSwitchColor: AppTheme.mugitenWheat.background,
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
leading: const Icon(Icons.close_fullscreen),
|
||||
title: const Text('Shrink kanji drawing board'),
|
||||
description: const Text(
|
||||
'Useful if you keep accidentally activating system gestures',
|
||||
),
|
||||
onToggle: (b) =>
|
||||
setState(() => reduceKanjiDrawingBoardSize = b),
|
||||
initialValue: reduceKanjiDrawingBoardSize,
|
||||
activeSwitchColor: AppTheme.mugitenWheat.background,
|
||||
),
|
||||
SettingsTile(
|
||||
enabled: true,
|
||||
leading: const Icon(Icons.delete),
|
||||
leading: const Icon(Icons.cached),
|
||||
title: const Text(
|
||||
'Reset database',
|
||||
'Reinitialize application',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
onPressed: clearAll,
|
||||
description: const Text(
|
||||
'Reinstall dictionary data and set up internal workings anew',
|
||||
),
|
||||
onPressed: (_) async {
|
||||
if (!await confirm(
|
||||
context,
|
||||
content: const Text(
|
||||
'Are you sure you want to reinitialize the application?',
|
||||
),
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
GetIt.instance.get<Database>().close();
|
||||
GetIt.instance.reset();
|
||||
runInitializationScreen(true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -326,10 +315,26 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
tiles: <SettingsTile>[
|
||||
SettingsTile(
|
||||
leading: const Icon(Icons.copyright),
|
||||
title: const Text('Licenses'),
|
||||
title: const Text('About'),
|
||||
description: const Text(
|
||||
'Information about Mugiten and licenses used'),
|
||||
onPressed: (c) =>
|
||||
Navigator.pushNamed(context, Routes.aboutLicenses),
|
||||
),
|
||||
SettingsTile(
|
||||
leading: const Icon(Icons.notes),
|
||||
title: const Text('Changelog'),
|
||||
onPressed: (c) =>
|
||||
Navigator.pushNamed(context, Routes.aboutChangelog),
|
||||
),
|
||||
SettingsTile(
|
||||
leading: const Icon(Mdi.git),
|
||||
title: const Text('Repository'),
|
||||
description: const Text('https://git.pvv.ntnu.no/mugiten'),
|
||||
onPressed: (c) => launchUrl(
|
||||
Uri.parse('https://git.pvv.ntnu.no/mugiten'),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:mugiten/database/history/table_names.dart';
|
||||
import 'package:mugiten/database/library_list/table_names.dart';
|
||||
import 'package:mugiten/models/history/history_entry.dart';
|
||||
import 'package:mugiten/models/library/library_list.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
@@ -16,8 +17,8 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
// - listb.json
|
||||
|
||||
extension ArchiveFormat on Directory {
|
||||
File get historyFile => File(uri.resolve('history.json').path);
|
||||
Directory get libraryDir => Directory(uri.resolve('library').path);
|
||||
File get historyFile => File(uri.resolve('history.json').toFilePath());
|
||||
Directory get libraryDir => Directory(uri.resolve('library').toFilePath());
|
||||
}
|
||||
|
||||
Future<Directory> tmpdir() async =>
|
||||
@@ -25,7 +26,7 @@ Future<Directory> tmpdir() async =>
|
||||
|
||||
Future<Directory> unpackZipToTempDir(String zipFilePath) async {
|
||||
final outputDir = await tmpdir();
|
||||
extractFileToDisk(
|
||||
await extractFileToDisk(
|
||||
zipFilePath,
|
||||
outputDir.path,
|
||||
);
|
||||
@@ -38,7 +39,7 @@ Future<File> packZip(
|
||||
}) async {
|
||||
if (outputFile == null || !outputFile.existsSync()) {
|
||||
final outputDir = await tmpdir();
|
||||
outputFile = File(outputDir.uri.resolve('mugiten_data.zip').path);
|
||||
outputFile = File(outputDir.uri.resolve('mugiten_data.zip').toFilePath());
|
||||
outputFile.createSync();
|
||||
}
|
||||
|
||||
@@ -69,12 +70,12 @@ String getExportFileNameNoSuffix() {
|
||||
Future<File> exportData(DatabaseExecutor db) async {
|
||||
final dir = await tmpdir();
|
||||
|
||||
final libraryDir = Directory(dir.uri.resolve('library').path);
|
||||
final libraryDir = Directory(dir.uri.resolve('library').toFilePath());
|
||||
libraryDir.createSync();
|
||||
|
||||
await Future.wait([
|
||||
exportHistoryTo(db, dir),
|
||||
exportLibraryListsTo(libraryDir),
|
||||
exportLibraryListsTo(db, libraryDir),
|
||||
]);
|
||||
|
||||
final zipFile = await packZip(dir);
|
||||
@@ -134,19 +135,64 @@ Future<void> importHistoryFrom(File file) async {
|
||||
///////////////////
|
||||
|
||||
Future<void> exportLibraryListsTo(
|
||||
// DatabaseExecutor db,
|
||||
DatabaseExecutor db,
|
||||
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));
|
||||
}),
|
||||
);
|
||||
) async {
|
||||
final libraryNames = await db.query(
|
||||
LibraryListTableNames.libraryList,
|
||||
columns: ['name'],
|
||||
).then((result) => result.map((row) => row['name'] as String).toList());
|
||||
|
||||
await Future.wait([
|
||||
for (final libraryName in libraryNames)
|
||||
exportLibraryListTo(db, libraryName, dir),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> exportLibraryListTo(
|
||||
DatabaseExecutor db,
|
||||
String libraryName,
|
||||
Directory dir,
|
||||
) async {
|
||||
final file = File(dir.uri.resolve('$libraryName.json').toFilePath());
|
||||
await file.create();
|
||||
|
||||
final entries = (await LibraryList.byName(libraryName).entries(db))
|
||||
.map((e) => e.toJson())
|
||||
.toList();
|
||||
|
||||
await file.writeAsString(jsonEncode(entries));
|
||||
}
|
||||
|
||||
// TODO: how do we handle lists that already exist? There seems to be no good way to merge them?
|
||||
Future<void> importLibraryListsFrom(Directory libraryListsDir) async {
|
||||
print('TODO: Implement importLibraryLists');
|
||||
for (final file in libraryListsDir.listSync()) {
|
||||
if (file is! File) continue;
|
||||
|
||||
assert(file.path.endsWith('.json'));
|
||||
|
||||
final libraryName =
|
||||
file.uri.pathSegments.last.replaceFirst(RegExp(r'\.json$'), '');
|
||||
|
||||
if (await LibraryList.exists(libraryName)) {
|
||||
if ((await LibraryList.byName(libraryName).length) > 0) {
|
||||
print(
|
||||
'Library list "$libraryName" already exists and is not empty. Skipping import.');
|
||||
continue;
|
||||
} else {
|
||||
print('Library list "$libraryName" already exists but is empty. '
|
||||
'Importing entries from file ${file.path}.');
|
||||
}
|
||||
} else {
|
||||
LibraryList.insert(libraryName);
|
||||
}
|
||||
|
||||
final content = await file.readAsString();
|
||||
final List<Map<String, Object?>> jsonEntries = (jsonDecode(content) as List)
|
||||
.map((e) => e as Map<String, Object?>)
|
||||
.toList();
|
||||
|
||||
final libraryList = LibraryList.byName(libraryName);
|
||||
await libraryList.insertJsonEntries(jsonEntries);
|
||||
}
|
||||
}
|
||||
|
||||
94
lib/services/initialization/initialization_cubit.dart
Normal file
94
lib/services/initialization/initialization_cubit.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:google_mlkit_digital_ink_recognition/google_mlkit_digital_ink_recognition.dart';
|
||||
import 'package:mugiten/database/database.dart'
|
||||
show
|
||||
DatabaseMigration,
|
||||
databaseNeedsInitialization,
|
||||
databasePath,
|
||||
extractJadbFromAssets,
|
||||
openAndMigrateDatabase,
|
||||
openDatabaseWithoutMigrations,
|
||||
readMigrationsFromAssets;
|
||||
import 'package:mugiten/services/data_export_import.dart';
|
||||
import 'package:mugiten/services/initialization/initialization_status.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
class InitializationCubit extends Cubit<InitializationStatus> {
|
||||
final bool deleteDatabase;
|
||||
|
||||
InitializationCubit(this.deleteDatabase) : super(InitializationNotStarted());
|
||||
|
||||
Future<void> start() async {
|
||||
emit(InitializationPending());
|
||||
|
||||
emit(CheckMLKitDigitalInkModel());
|
||||
final modelManager = DigitalInkRecognizerModelManager();
|
||||
final isDownloaded = await modelManager.isModelDownloaded('ja');
|
||||
|
||||
if (!isDownloaded) {
|
||||
emit(DownloadMLKitDigitalInkModel());
|
||||
await modelManager.downloadModel('ja');
|
||||
}
|
||||
|
||||
emit(FinishDownloadMLKitDigitalInkModel());
|
||||
|
||||
emit(CheckDatabase());
|
||||
if (deleteDatabase || await databaseNeedsInitialization()) {
|
||||
final String dbPath = await databasePath();
|
||||
final databaseAlreadyExists = await File(dbPath).exists();
|
||||
|
||||
late final File? tmpdirDataDump;
|
||||
|
||||
if (!databaseAlreadyExists) {
|
||||
await extractJadbFromAssets(dbPath);
|
||||
} else {
|
||||
emit(BackupUserData(total: 2, progress: 1));
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final database = await openDatabaseWithoutMigrations(dbPath);
|
||||
|
||||
GetIt.instance.registerSingleton<Database>(database);
|
||||
final dataDump = await exportData(database);
|
||||
GetIt.instance.unregister<Database>();
|
||||
|
||||
await database.close();
|
||||
|
||||
tmpdirDataDump =
|
||||
await dataDump.copy('${tempDir.path}/mugiten_data_backup.zip');
|
||||
emit(BackupUserData(total: 2, progress: 2));
|
||||
}
|
||||
|
||||
if (deleteDatabase) {
|
||||
await File(dbPath).delete();
|
||||
await extractJadbFromAssets(dbPath);
|
||||
}
|
||||
|
||||
emit(MigrateDatabase(total: 2, progress: 1));
|
||||
|
||||
final List<DatabaseMigration> migrations =
|
||||
await readMigrationsFromAssets();
|
||||
final database = await openAndMigrateDatabase(dbPath, migrations);
|
||||
|
||||
emit(MigrateDatabase(total: 2, progress: 2));
|
||||
|
||||
if (databaseAlreadyExists) {
|
||||
emit(RestoreUserData(total: 2, progress: 1));
|
||||
|
||||
GetIt.instance.registerSingleton<Database>(database);
|
||||
await importData(database, tmpdirDataDump!);
|
||||
GetIt.instance.unregister<Database>();
|
||||
|
||||
emit(RestoreUserData(total: 2, progress: 2));
|
||||
}
|
||||
|
||||
database.close();
|
||||
}
|
||||
emit(DatabaseUpdateFinished());
|
||||
|
||||
// Initialization complete
|
||||
emit(InitializationComplete());
|
||||
}
|
||||
}
|
||||
59
lib/services/initialization/initialization_logic.dart
Normal file
59
lib/services/initialization/initialization_logic.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:google_mlkit_digital_ink_recognition/google_mlkit_digital_ink_recognition.dart';
|
||||
import 'package:mugiten/database/database.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
|
||||
/// Determine whether the application needs to show the initialization screen.
|
||||
Future<bool> needsInitialization() async {
|
||||
final modelManager = DigitalInkRecognizerModelManager();
|
||||
if (!await modelManager.isModelDownloaded('ja')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await databaseNeedsInitialization()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Quick initialization used for normal startup without initialization screen.
|
||||
Future<void> quickInitialization() async {
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
|
||||
await Future.wait([
|
||||
quickInitializeDatabase(),
|
||||
setupSharedPreferences(),
|
||||
(() async {
|
||||
final modelManager = DigitalInkRecognizerModelManager();
|
||||
final isDownloaded = await modelManager.isModelDownloaded('ja');
|
||||
assert(isDownloaded, 'Japanese model should be downloaded at this point');
|
||||
})(),
|
||||
]);
|
||||
|
||||
registerExtraLicenses();
|
||||
}
|
||||
|
||||
// TODO: should this be deferred in case preferences are modified externally?
|
||||
Future<void> setupSharedPreferences() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
GetIt.instance.registerSingleton<SharedPreferences>(prefs);
|
||||
}
|
||||
|
||||
void registerExtraLicenses() => LicenseRegistry.addLicense(
|
||||
() async* {
|
||||
final jsonString = await rootBundle.loadString('assets/licenses.json');
|
||||
final Map<String, dynamic> jsonData = jsonDecode(jsonString);
|
||||
for (final license in jsonData.entries) {
|
||||
yield LicenseEntryWithLineBreaks(
|
||||
[license.key],
|
||||
await rootBundle.loadString(license.value as String),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
53
lib/services/initialization/initialization_status.dart
Normal file
53
lib/services/initialization/initialization_status.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
abstract class InitializationStatus {}
|
||||
|
||||
class InitializationNotStarted extends InitializationStatus {}
|
||||
|
||||
class InitializationPending extends InitializationStatus {}
|
||||
|
||||
//
|
||||
|
||||
class CheckMLKitDigitalInkModel extends InitializationStatus {}
|
||||
|
||||
class DownloadMLKitDigitalInkModel extends InitializationStatus {}
|
||||
|
||||
class FinishDownloadMLKitDigitalInkModel extends InitializationStatus {}
|
||||
|
||||
//
|
||||
|
||||
class CheckDatabase extends InitializationStatus {}
|
||||
|
||||
class BackupUserData extends InitializationStatus {
|
||||
final int progress;
|
||||
final int total;
|
||||
|
||||
BackupUserData({
|
||||
required this.progress,
|
||||
required this.total,
|
||||
});
|
||||
}
|
||||
|
||||
class MigrateDatabase extends InitializationStatus {
|
||||
final int progress;
|
||||
final int total;
|
||||
|
||||
MigrateDatabase({
|
||||
required this.progress,
|
||||
required this.total,
|
||||
});
|
||||
}
|
||||
|
||||
class RestoreUserData extends InitializationStatus {
|
||||
final int progress;
|
||||
final int total;
|
||||
|
||||
RestoreUserData({
|
||||
required this.progress,
|
||||
required this.total,
|
||||
});
|
||||
}
|
||||
|
||||
class DatabaseUpdateFinished extends InitializationStatus {}
|
||||
|
||||
//
|
||||
|
||||
class InitializationComplete extends InitializationStatus {}
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
void registerExtraLicenses() => LicenseRegistry.addLicense(() async* {
|
||||
final jsonString = await rootBundle.loadString('assets/licenses.json');
|
||||
final Map<String, dynamic> jsonData = jsonDecode(jsonString);
|
||||
for (final license in jsonData.entries) {
|
||||
yield LicenseEntryWithLineBreaks(
|
||||
[license.key],
|
||||
await rootBundle.loadString(license.value as String),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
Future<void> setupSharedPreferences() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
GetIt.instance.registerSingleton<SharedPreferences>(prefs);
|
||||
}
|
||||
@@ -39,27 +39,34 @@ extension Methods on JapaneseFont {
|
||||
'';
|
||||
}
|
||||
|
||||
const String appVersion = '0.1 Beta';
|
||||
|
||||
const Map<String, dynamic> _defaults = {
|
||||
'incognitoModeEnabled': false,
|
||||
'romajiEnabled': false,
|
||||
'darkThemeEnabled': false,
|
||||
'autoThemeEnabled': false,
|
||||
'japaneseFont': JapaneseFont.droidSansJapanese,
|
||||
'reduceKanjiDrawingBoardSize': false,
|
||||
};
|
||||
|
||||
bool _getSettingOrDefault(String settingName) =>
|
||||
_prefs.getBool(settingName) ?? _defaults[settingName];
|
||||
|
||||
bool get incognitoModeEnabled =>
|
||||
_getSettingOrDefault('incognitoModeEnabled');
|
||||
bool get romajiEnabled => _getSettingOrDefault('romajiEnabled');
|
||||
bool get darkThemeEnabled => _getSettingOrDefault('darkThemeEnabled');
|
||||
bool get autoThemeEnabled => _getSettingOrDefault('autoThemeEnabled');
|
||||
bool get reduceKanjiDrawingBoardSize =>
|
||||
_getSettingOrDefault('reduceKanjiDrawingBoardSize');
|
||||
JapaneseFont get japaneseFont {
|
||||
final int? i = _prefs.getInt('japaneseFont');
|
||||
return (i != null) ? JapaneseFont.values[i] : _defaults['japaneseFont'];
|
||||
}
|
||||
|
||||
set incognitoModeEnabled(b) => _prefs.setBool('incognitoModeEnabled', b);
|
||||
set romajiEnabled(b) => _prefs.setBool('romajiEnabled', b);
|
||||
set darkThemeEnabled(b) => _prefs.setBool('darkThemeEnabled', b);
|
||||
set autoThemeEnabled(b) => _prefs.setBool('autoThemeEnabled', b);
|
||||
set reduceKanjiDrawingBoardSize(b) =>
|
||||
_prefs.setBool('reduceKanjiDrawingBoardSize', b);
|
||||
set japaneseFont(JapaneseFont jf) => _prefs.setInt('japaneseFont', jf.index);
|
||||
|
||||
@@ -26,7 +26,10 @@ CREATE TABLE "Mugiten_HistoryEntryTimestamp" (
|
||||
CREATE INDEX "Mugiten_HistoryEntryTimestamp_byTimestamp" ON "Mugiten_HistoryEntryTimestamp"("timestamp");
|
||||
|
||||
CREATE VIEW "Mugiten_HistoryEntry_orderedByTimestamp" AS
|
||||
SELECT * FROM "Mugiten_HistoryEntryTimestamp"
|
||||
SELECT
|
||||
*,
|
||||
COUNT("timestamp") AS "timestampCount"
|
||||
FROM "Mugiten_HistoryEntryTimestamp"
|
||||
LEFT JOIN "Mugiten_HistoryEntryWord" USING ("entryId")
|
||||
LEFT JOIN "Mugiten_HistoryEntryKanji" USING ("entryId")
|
||||
GROUP BY "entryId"
|
||||
|
||||
40
pubspec.lock
40
pubspec.lock
@@ -198,6 +198,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_markdown_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown_plus
|
||||
sha256: fe74214c5ac2f850d93efda290dcde3f18006e90a87caa9e3e6c13222a5db4de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -293,7 +301,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "7978b74f8d125a404b42465d4dacd52de0224693"
|
||||
resolved-ref: "1868c6fb41c781bb9c02cde172b39ab630f28421"
|
||||
url: "https://git.pvv.ntnu.no/oysteikt/jadb.git"
|
||||
source: git
|
||||
version: "1.0.0"
|
||||
@@ -329,6 +337,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: markdown
|
||||
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -377,6 +393,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -469,10 +501,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
version: "6.0.3"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -864,4 +896,4 @@ packages:
|
||||
version: "6.5.0"
|
||||
sdks:
|
||||
dart: ">=3.6.2 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
flutter: ">=3.27.1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: mugiten
|
||||
description: "A new Flutter project."
|
||||
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.0.0+1
|
||||
version: 0.3.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.2
|
||||
@@ -37,6 +37,9 @@ dependencies:
|
||||
ruby_text: ^3.0.3
|
||||
google_mlkit_digital_ink_recognition: ^0.14.1
|
||||
archive: ^4.0.7
|
||||
package_info_plus: ^8.3.0
|
||||
flutter_markdown_plus: ^1.0.3
|
||||
markdown: ^7.3.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -47,7 +50,6 @@ flutter:
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
- migrations/
|
||||
- assets/
|
||||
- assets/fonts/
|
||||
- assets/images/
|
||||
@@ -55,6 +57,8 @@ flutter:
|
||||
- assets/images/links/
|
||||
- assets/images/logo/
|
||||
- assets/licenses/
|
||||
- docs/changelog/
|
||||
- migrations/
|
||||
|
||||
fonts:
|
||||
- family: Droid Sans Japanese
|
||||
|
||||
Reference in New Issue
Block a user