Compare commits

..

1 Commits

Author SHA1 Message Date
ff89b78775 WIP: keep data across database wipes 2025-06-26 00:05:19 +02:00
39 changed files with 304 additions and 1166 deletions

View File

@@ -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

View File

@@ -1,17 +0,0 @@
# 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

View File

@@ -1,13 +0,0 @@
# 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.

View File

@@ -1,34 +0,0 @@
# 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.

View File

@@ -1,10 +0,0 @@
# v0.3.1
Bugfixes and bugfixes and maybe some more bugs.
## Bugfixes 🐞
- Fix a bug where deleting a library list would remove more than one list.
- Fix filename encoding in user data exports.
- Don't throw errors on empty search results.
- Fix reinitialization button, so it migrates the database from the bottom up.

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1751792365,
"narHash": "sha256-J1kI6oAj25IG4EdVlg2hQz8NZTBNYvIS0l4wpr9KcUo=",
"lastModified": 1744463964,
"narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1fd8bada0b6117e6c7eb54aad5813023eed37ccb",
"rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650",
"type": "github"
},
"original": {

View File

@@ -237,32 +237,22 @@ class _DrawingBoardState extends State<DrawingBoard> {
),
);
Widget drawingPanel() {
final board = AspectRatio(
aspectRatio: 1.0,
child: Stack(
alignment: Alignment.bottomRight,
children: [
ClipRect(
child: Signature(
key: signatureW,
controller: controller,
backgroundColor: panelColor.background,
Widget drawingPanel() => AspectRatio(
aspectRatio: 1.2,
child: Stack(
alignment: Alignment.bottomRight,
children: [
ClipRect(
child: Signature(
key: signatureW,
controller: controller,
backgroundColor: panelColor.background,
),
),
),
buttonRow(),
],
),
);
if (reduceKanjiDrawingBoardSize) {
return Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 30),
child: board,
buttonRow(),
],
),
);
}
return board;
}
@override
Widget build(BuildContext context) {

View File

@@ -1,8 +1,5 @@
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';
@@ -95,24 +92,14 @@ 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!))),
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}'),
),
),
),
style: japaneseFont.textStyle,
child: entry.isKanji
? KanjiBox.headline4(
context: context,
kanji: entry.kanji!,
)
: Expanded(child: Text(entry.word!)),
),
],
),
),

View File

@@ -7,8 +7,7 @@ import '../../settings.dart';
import 'language_selector.dart';
class GlobalSearchBar extends StatelessWidget {
final TextEditingController textController = TextEditingController();
final FocusNode textFocus = FocusNode();
final TextEditingController controller = TextEditingController();
GlobalSearchBar({super.key});
@@ -26,8 +25,7 @@ class GlobalSearchBar extends StatelessWidget {
children: [
TextField(
onSubmitted: (text) => _search(context, text),
controller: textController,
focusNode: textFocus,
controller: controller,
style: japaneseFont.textStyle,
decoration: InputDecoration(
labelText: 'Search',
@@ -43,8 +41,8 @@ class GlobalSearchBar extends StatelessWidget {
color: AppTheme.mugitenWheat.background,
child: IconButton(
onPressed: () {
if (textController.text.isNotEmpty) {
_search(context, textController.text);
if (controller.text.isNotEmpty) {
_search(context, controller.text);
}
},
icon: const Icon(
@@ -65,21 +63,7 @@ class GlobalSearchBar extends StatelessWidget {
onPressed: () async {
final result = await _drawKanji()(context);
if (result != null && result.isNotEmpty) {
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();
}
controller.text += result;
}
},
)
@@ -94,19 +78,17 @@ class GlobalSearchBar extends StatelessWidget {
final MaterialPageRoute<String> route = MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(title: const Text('Draw a kanji')),
body: SafeArea(
child: Column(
children: [
const Expanded(child: Column()),
DrawingBoard(
onlyOneCharacterSuggestions: true,
onSuggestionChosen: (suggestion) => Navigator.pop(
context,
suggestion,
),
body: Column(
children: [
const Expanded(child: Column()),
DrawingBoard(
onlyOneCharacterSuggestions: true,
onSuggestionChosen: (suggestion) => Navigator.pop(
context,
suggestion,
),
],
),
),
],
),
),
);

View File

@@ -5,15 +5,13 @@ 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:mugiten/services/replace_jadb.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.
@@ -28,35 +26,6 @@ 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;
@@ -127,35 +96,6 @@ 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,
@@ -163,10 +103,13 @@ Future<Database> openAndMigrateDatabase(
log('Opening database at $dbPath');
final Database database = await openDatabase(
dbPath,
version: expectedDatabaseVersion,
version: 2,
readOnly: false,
onCreate: (db, version) async {
print('WARNING: database does not exist, this should not be happening');
},
onUpgrade: (db, oldVersion, newVersion) async {
log('Migrating database from v$oldVersion to v$newVersion...');
print('Migrating database from v$oldVersion to v$newVersion...');
final migrationsToRun = migrations
.where((migration) =>
migration.version > oldVersion && migration.version <= newVersion)
@@ -180,12 +123,10 @@ Future<Database> openAndMigrateDatabase(
},
onOpen: (db) async {
log('Verifying jadb tables...');
db.jadbVerifyTables();
log('Verifying jadb tables...');
verifyMugitenTablesWithDbConnection(db);
await db.jadbVerifyTables();
log('Database tables verified successfully');
log('jadb opened successfully');
},
);
return database;
@@ -197,17 +138,14 @@ Future<void> setupDatabase() async {
final String dbPath = await databasePath();
assert(
await File(dbPath).exists(), 'Database file should exist at this point');
if (!await File(dbPath).exists()) {
print('Extracting jadb.sqlite from assets...');
await extractJadbFromAssets(dbPath);
print('jadb.sqlite extracted to $dbPath');
}
final database = await openDatabaseWithoutMigrations(
dbPath,
readOnly: false,
verifyTables: true,
);
assert(await database.getVersion() == expectedDatabaseVersion,
'Database version should be $expectedDatabaseVersion');
final List<DatabaseMigration> migrations = await readMigrationsFromAssets();
final Database database = await openAndMigrateDatabase(dbPath, migrations);
log('Registering database in GetIt...');
GetIt.instance.registerSingleton<Database>(database);
@@ -239,5 +177,6 @@ Future<void> extractJadbFromAssets(String path) async {
ByteData data = await rootBundle.load('assets/jadb.sqlite');
await jadbFile.writeAsBytes(
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes));
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes),
);
}

View File

@@ -31,12 +31,4 @@ abstract class HistoryTableNames {
/// - language CHAR(1)?
static const String historyEntryOrderedByTimestamp =
'Mugiten_HistoryEntry_orderedByTimestamp';
static Set<String> get allTables => {
historyEntry,
historyEntryKanji,
historyEntryTimestamp,
historyEntryWord,
historyEntryOrderedByTimestamp,
};
}

View File

@@ -20,10 +20,4 @@ abstract class LibraryListTableNames {
/// Attributes:
/// - name TEXT
static const String libraryListOrdered = 'Mugiten_LibraryList_Ordered';
static Set<String> get allTables => {
libraryList,
libraryListEntry,
libraryListOrdered,
};
}

View File

@@ -1,30 +1,36 @@
import 'package:flutter/material.dart';
import 'package:mugiten/screens/initialization.dart';
import 'package:mugiten/services/initialization/initialization_logic.dart';
import 'package:google_mlkit_digital_ink_recognition/google_mlkit_digital_ink_recognition.dart';
import 'package:mugiten/database/database.dart';
import 'package:mugiten/services/replace_jadb.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.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();
if (await needsInitialization()) {
runInitializationScreen(false);
} else {
await quickInitialization();
runApp(const MyApp());
}
databaseFactory = databaseFactoryFfi;
await Future.wait([
setupDatabase(),
setupSharedPreferences(),
(() async {
final modelManager = DigitalInkRecognizerModelManager();
if (!await modelManager.isModelDownloaded('ja')) {
await modelManager.downloadModel('ja');
}
})()
]);
await replaceJadb();
registerExtraLicenses();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {

View File

@@ -10,7 +10,6 @@ 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;
@@ -19,14 +18,12 @@ 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
@@ -46,9 +43,6 @@ 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,
@@ -56,9 +50,6 @@ 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

View File

@@ -1,7 +1,6 @@
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';
@@ -11,14 +10,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>> entries(DatabaseExecutor db) async {
Future<List<LibraryEntry>> get entries 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 (
@@ -49,7 +48,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();
}
@@ -84,7 +83,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,
),
),
@@ -207,7 +206,7 @@ class LibraryList {
final b = db().batch();
final entries_ = await entries(db());
final entries_ = await entries;
final prevEntry = entries_[position - 1];
final nextEntry = entries_[position];
@@ -239,7 +238,7 @@ class LibraryList {
'Adding ${jmdictEntryId != null ? 'jmdict entry $jmdictEntryId' : 'kanji "$kanji"'} to library "$name"',
);
final LibraryEntry? prevEntry = (await entries(db())).lastOrNull;
final LibraryEntry? prevEntry = (await entries).lastOrNull;
await db().insert(LibraryListTableNames.libraryListEntry, {
'listName': name,
@@ -250,32 +249,6 @@ 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({
@@ -445,7 +418,43 @@ class LibraryList {
'name': libraryName,
'prevList': prevList.name,
});
return LibraryList.byName(libraryName);
return LibraryList._byName(libraryName);
}
static Future<LibraryList> insertList(LibraryList list) async {
if (await exists(list.name)) {
throw DataAlreadyExistsError(
tableName: LibraryListTableNames.libraryList,
illegalArguments: {
'libraryName': list.name,
},
);
}
// This is ok, because "favourites" should always exist.
final prevList = (await allLibraries).last;
await db().insert(LibraryListTableNames.libraryList, {
'name': list.name,
'prevList': prevList.name,
});
final batch = db().batch();
int? prevEntryJmdictEntryId;
String? prevEntryKanji;
for (final entry in await list.entries) {
batch.insert(LibraryListTableNames.libraryListEntry, {
'listName': list.name,
'jmdictEntryId': entry.jmdictEntryId,
'kanji': entry.kanji,
'prevEntryJmdictEntryId': prevEntryJmdictEntryId,
'prevEntryKanji': prevEntryKanji,
});
prevEntryJmdictEntryId = entry.jmdictEntryId;
prevEntryKanji = entry.kanji;
}
batch.commit();
return LibraryList._byName(list.name);
}
/// Delete this library from the database
@@ -456,87 +465,10 @@ class LibraryList {
illegalArguments: {'name': name},
);
}
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(
await db().delete(
LibraryListTableNames.libraryList,
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;
where: 'name = ?',
whereArgs: [name],
);
}
}

View File

@@ -1,35 +0,0 @@
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'));
}
}

View File

@@ -1,6 +1,5 @@
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';
@@ -53,8 +52,6 @@ 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:

View File

@@ -6,7 +6,6 @@ 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';

View File

@@ -29,7 +29,7 @@ class HistoryView extends StatelessWidget {
return OpaqueBox(
child: ListView.separated(
itemCount: data.length + 2,
itemCount: data.length + 1,
itemBuilder: historyEntryWithData(data),
separatorBuilder:
historyEntrySeparatorWithData(data.values.toList()),
@@ -46,9 +46,8 @@ class HistoryView extends StatelessWidget {
final HistoryEntry search = data[index];
final DateTime searchDate = search.lastTimestamp;
if (index != 1 &&
(index == 0 ||
!dateIsEqual(data[index - 2].lastTimestamp, searchDate))) {
if (index == 0 ||
!dateIsEqual(data[index - 1].lastTimestamp, searchDate)) {
return TextDivider(text: formatDate(roundToDay(searchDate)));
}
@@ -61,28 +60,12 @@ class HistoryView extends StatelessWidget {
Widget Function(BuildContext, int) historyEntryWithData(
Map<int, HistoryEntry> data,
) {
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),
),
};
};
}
) =>
(context, index) => (index == 0)
? const SizedBox.shrink()
: HistoryEntryTile(
entry: data.values.toList()[index - 1],
objectKey: data.keys.toList()[index - 1],
onDelete: () => build(context),
);
}

View File

@@ -3,8 +3,6 @@ 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';
@@ -63,30 +61,16 @@ class _HomeState extends State<Home> {
}
List<_Page> get pages => [
_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: WordSearchView(),
titleBar: 'Search',
icon: Icon(Icons.search),
),
const _Page(
content: KanjiSearchView(),
titleBar: 'Kanji Search',
icon: Icon(Mdi.ideogramCjk, size: 30),
),
const _Page(
content: HistoryView(),
titleBar: 'History',

View File

@@ -1,122 +0,0 @@
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,
),
),
);
},
),
),
);
}
}

View File

@@ -1,28 +1,14 @@
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../settings.dart';
class LicensesView extends StatelessWidget {
const LicensesView({super.key});
@override
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(
Widget build(BuildContext context) => LicensePage(
applicationName: '麦典',
applicationVersion: 'Version: ${packageInfo.version}',
applicationVersion: 'Version: $appVersion',
applicationIcon: Padding(
padding: const EdgeInsets.symmetric(vertical: 30),
child: Row(

View File

@@ -1,98 +0,0 @@
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();
}
},
),
],
),
),
),
);
}
}

View File

@@ -1,6 +1,5 @@
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';
@@ -22,7 +21,7 @@ class _LibraryContentViewState extends State<LibraryContentView> {
List<LibraryEntry>? entries;
Future<void> getEntriesFromDatabase() =>
widget.library.entries(db()).then((es) => setState(() => entries = es));
widget.library.entries.then((es) => setState(() => entries = es));
@override
void initState() {

View File

@@ -2,12 +2,9 @@ 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';
@@ -104,12 +101,6 @@ 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,
@@ -180,7 +171,7 @@ class _KanjiSearchResultPageState extends State<KanjiSearchResultPage> {
}
if (snapshot.hasError) return ErrorWidget(snapshot.error!);
if (!incognitoModeEnabled && !addedToDatabase) {
if (!addedToDatabase) {
HistoryEntry.insertKanji(kanji: widget.kanji);
addedToDatabase = true;
}

View File

@@ -10,20 +10,18 @@ class KanjiDrawingSearch extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Draw a kanji')),
body: SafeArea(
child: Column(
children: [
const Expanded(child: Column()),
DrawingBoard(
onlyOneCharacterSuggestions: true,
onSuggestionChosen: (suggestion) => Navigator.popAndPushNamed(
context,
Routes.kanjiSearch,
arguments: suggestion,
),
body: Column(
children: [
const Expanded(child: Column()),
DrawingBoard(
onlyOneCharacterSuggestions: true,
onSuggestionChosen: (suggestion) => Navigator.popAndPushNamed(
context,
Routes.kanjiSearch,
arguments: suggestion,
),
],
),
),
],
),
);
}

View File

@@ -2,10 +2,7 @@ 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';
@@ -30,28 +27,14 @@ class _WordSearchResultPageState extends State<WordSearchResultPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Search'),
actions: [
if (incognitoModeEnabled)
IconButton(
icon: const Icon(Mdi.incognito),
onPressed: () =>
showSnackbar(context, 'History tracking is disabled'),
),
],
),
appBar: AppBar(),
body: FutureBuilder(
future: (() async {
final jadbConnection = GetIt.instance.get<Database>();
final results = await Future.wait([
jadbConnection
.jadbSearchWordCount(widget.searchTerm)
.then((v) => v ?? 0),
jadbConnection
.jadbSearchWord(widget.searchTerm)
.then((v) => v ?? <WordSearchResult>[]),
jadbConnection.jadbSearchWordCount(widget.searchTerm),
jadbConnection.jadbSearchWord(widget.searchTerm),
]);
return (results[0] as int, results[1] as List<WordSearchResult>);
@@ -62,7 +45,7 @@ class _WordSearchResultPageState extends State<WordSearchResultPage> {
return const Center(child: CircularProgressIndicator());
}
if (!incognitoModeEnabled && !addedToDatabase) {
if (!addedToDatabase) {
HistoryEntry.insertWord(word: widget.searchTerm);
addedToDatabase = true;
}

View File

@@ -1,5 +1,4 @@
import 'dart:io';
import 'dart:developer';
import 'package:confirm_dialog/confirm_dialog.dart';
import 'package:file_picker/file_picker.dart';
@@ -11,14 +10,12 @@ 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});
@@ -32,6 +29,16 @@ 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();
@@ -52,6 +59,51 @@ 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
@@ -174,10 +226,7 @@ class _SettingsViewState extends State<SettingsView> {
// titleTextStyle: _titleTextStyle,
tiles: <SettingsTile>[
SettingsTile.switchTile(
title: const Text('Romaji mode'),
description: const Text(
'Display romaji instead of kana for word readings',
),
title: const Text('Use romaji'),
leading: const Icon(Mdi.alphabetical),
onToggle: (b) => setState(() => romajiEnabled = b),
initialValue: romajiEnabled,
@@ -201,8 +250,6 @@ 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,
@@ -231,7 +278,6 @@ 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()
@@ -241,7 +287,6 @@ 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()
@@ -254,59 +299,25 @@ class _SettingsViewState extends State<SettingsView> {
'Clear History',
style: TextStyle(color: Colors.red),
),
description: const Text('Delete all search history'),
onPressed: clearHistory,
),
],
),
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',
),
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.cached),
leading: const Icon(Icons.delete),
title: const Text(
'Reinitialize application',
'Clear Favourites',
style: TextStyle(color: Colors.red),
),
description: const Text(
'Reinstall dictionary data and set up internal workings anew',
onPressed: clearFavourites,
),
SettingsTile(
enabled: true,
leading: const Icon(Icons.delete),
title: const Text(
'Reset database',
style: TextStyle(color: Colors.red),
),
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);
},
onPressed: clearAll,
),
],
),
@@ -315,26 +326,10 @@ class _SettingsViewState extends State<SettingsView> {
tiles: <SettingsTile>[
SettingsTile(
leading: const Icon(Icons.copyright),
title: const Text('About'),
description: const Text(
'Information about Mugiten and licenses used'),
title: const Text('Licenses'),
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'),
),
)
],
),
],

View File

@@ -4,7 +4,6 @@ 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';
@@ -17,8 +16,8 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart';
// - listb.json
extension ArchiveFormat on Directory {
File get historyFile => File(uri.resolve('history.json').toFilePath());
Directory get libraryDir => Directory(uri.resolve('library').toFilePath());
File get historyFile => File(uri.resolve('history.json').path);
Directory get libraryDir => Directory(uri.resolve('library').path);
}
Future<Directory> tmpdir() async =>
@@ -26,7 +25,7 @@ Future<Directory> tmpdir() async =>
Future<Directory> unpackZipToTempDir(String zipFilePath) async {
final outputDir = await tmpdir();
await extractFileToDisk(
extractFileToDisk(
zipFilePath,
outputDir.path,
);
@@ -39,7 +38,7 @@ Future<File> packZip(
}) async {
if (outputFile == null || !outputFile.existsSync()) {
final outputDir = await tmpdir();
outputFile = File(outputDir.uri.resolve('mugiten_data.zip').toFilePath());
outputFile = File(outputDir.uri.resolve('mugiten_data.zip').path);
outputFile.createSync();
}
@@ -70,12 +69,12 @@ String getExportFileNameNoSuffix() {
Future<File> exportData(DatabaseExecutor db) async {
final dir = await tmpdir();
final libraryDir = Directory(dir.uri.resolve('library').toFilePath());
final libraryDir = Directory(dir.uri.resolve('library').path);
libraryDir.createSync();
await Future.wait([
exportHistoryTo(db, dir),
exportLibraryListsTo(db, libraryDir),
exportLibraryListsTo(libraryDir),
]);
final zipFile = await packZip(dir);
@@ -135,64 +134,19 @@ Future<void> importHistoryFrom(File file) async {
///////////////////
Future<void> exportLibraryListsTo(
DatabaseExecutor db,
// DatabaseExecutor db,
Directory dir,
) 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));
}
) 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));
}),
);
// 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 {
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);
}
print('TODO: Implement importLibraryLists');
}

View File

@@ -1,94 +0,0 @@
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());
}
}

View File

@@ -1,59 +0,0 @@
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),
);
}
},
);

View File

@@ -1,53 +0,0 @@
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 {}

View File

@@ -0,0 +1,15 @@
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),
);
}
});

View File

@@ -0,0 +1,7 @@
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);
}

View File

@@ -0,0 +1,15 @@
import 'package:get_it/get_it.dart';
import 'package:mugiten/database/database.dart';
import 'package:mugiten/services/data_export_import.dart';
Future<void> replaceJadb() async {
if (!GetIt.instance.isRegistered<Database>()) {
throw Exception("Database connection is not initialized.");
}
final backup = await exportData(db());
await resetDatabase();
await importData(db(), backup);
}

View File

@@ -39,34 +39,27 @@ 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);

View File

@@ -26,10 +26,7 @@ CREATE TABLE "Mugiten_HistoryEntryTimestamp" (
CREATE INDEX "Mugiten_HistoryEntryTimestamp_byTimestamp" ON "Mugiten_HistoryEntryTimestamp"("timestamp");
CREATE VIEW "Mugiten_HistoryEntry_orderedByTimestamp" AS
SELECT
*,
COUNT("timestamp") AS "timestampCount"
FROM "Mugiten_HistoryEntryTimestamp"
SELECT * FROM "Mugiten_HistoryEntryTimestamp"
LEFT JOIN "Mugiten_HistoryEntryWord" USING ("entryId")
LEFT JOIN "Mugiten_HistoryEntryKanji" USING ("entryId")
GROUP BY "entryId"

View File

@@ -198,14 +198,6 @@ 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:
@@ -301,7 +293,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: "1868c6fb41c781bb9c02cde172b39ab630f28421"
resolved-ref: "7978b74f8d125a404b42465d4dacd52de0224693"
url: "https://git.pvv.ntnu.no/oysteikt/jadb.git"
source: git
version: "1.0.0"
@@ -337,14 +329,6 @@ 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:
@@ -393,22 +377,6 @@ 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:
@@ -501,10 +469,10 @@ packages:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62
url: "https://pub.dev"
source: hosted
version: "6.0.3"
version: "6.0.2"
provider:
dependency: transitive
description:
@@ -896,4 +864,4 @@ packages:
version: "6.5.0"
sdks:
dart: ">=3.6.2 <4.0.0"
flutter: ">=3.27.1"
flutter: ">=3.27.0"

View File

@@ -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: 0.3.1+1
version: 1.0.0+1
environment:
sdk: ^3.6.2
@@ -37,9 +37,6 @@ 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:
@@ -50,6 +47,7 @@ flutter:
uses-material-design: true
assets:
- migrations/
- assets/
- assets/fonts/
- assets/images/
@@ -57,8 +55,6 @@ flutter:
- assets/images/links/
- assets/images/logo/
- assets/licenses/
- docs/changelog/
- migrations/
fonts:
- family: Droid Sans Japanese