mirror of
https://github.com/h7x4/Jisho-Study-Tool.git
synced 2024-12-22 13:57:29 +01:00
Migrate history to SQLite + more
- remove all sembast code - setup database migration system - setup data import export system - remove sembast object tests - make everything ready for implementing "saved lists" feature
This commit is contained in:
parent
cad62f2b8b
commit
d2a3de4823
@ -1,5 +1,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="app.jishostudytool.jisho_study_tool">
|
||||
<application
|
||||
android:requestLegacyExternalStorage="true"
|
||||
>
|
||||
</application>
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
@ -1,9 +1,11 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="app.jishostudytool.jisho_study_tool">
|
||||
<application
|
||||
<application
|
||||
android:label="Jisho Study Tool"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
64
assets/migrations/0001_initial.sql
Normal file
64
assets/migrations/0001_initial.sql
Normal file
@ -0,0 +1,64 @@
|
||||
|
||||
CREATE TABLE "JST_SavedList" (
|
||||
"name" TEXT PRIMARY KEY NOT NULL,
|
||||
"nextList" TEXT REFERENCES "JST_SavedList"("name")
|
||||
);
|
||||
|
||||
CREATE INDEX "JST_SavedList_byNextList" ON "JST_SavedList"("nextList");
|
||||
|
||||
CREATE TABLE "JST_SavedListEntry" (
|
||||
"listName" TEXT NOT NULL REFERENCES "JST_SavedList"("name") ON DELETE CASCADE,
|
||||
"entryText" TEXT NOT NULL,
|
||||
"isKanji" BOOLEAN NOT NULL DEFAULT 0,
|
||||
"lastModified" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"nextEntry" TEXT NOT NULL,
|
||||
PRIMARY KEY ("listName", "entryText", "isKanji"),
|
||||
FOREIGN KEY ("listName", "nextEntry") REFERENCES "JST_SavedListEntry"("listName", "entryText"),
|
||||
CHECK ((NOT "isKanji") OR ("nextEntry" <> 0))
|
||||
);
|
||||
|
||||
CREATE INDEX "JST_SavedListEntry_byListName" ON "JST_SavedListEntry"("listName");
|
||||
|
||||
-- CREATE VIEW "JST_SavedListEntry_sortedByLists" AS
|
||||
-- WITH RECURSIVE "JST_SavedListEntry_sorted"("next") AS (
|
||||
-- -- Initial SELECT
|
||||
-- UNION ALL
|
||||
-- SELECT * FROM ""
|
||||
-- -- Recursive Select
|
||||
-- )
|
||||
-- SELECT * FROM "JST_SavedListEntry_sorted";
|
||||
|
||||
CREATE TABLE "JST_HistoryEntry" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
|
||||
);
|
||||
|
||||
CREATE TABLE "JST_HistoryEntryKanji" (
|
||||
"entryId" INTEGER NOT NULL REFERENCES "JST_HistoryEntry"("id") ON DELETE CASCADE,
|
||||
"kanji" CHAR(1) NOT NULL,
|
||||
PRIMARY KEY ("entryId", "kanji")
|
||||
);
|
||||
|
||||
CREATE TABLE "JST_HistoryEntryWord" (
|
||||
"entryId" INTEGER NOT NULL REFERENCES "JST_HistoryEntry"("id") ON DELETE CASCADE,
|
||||
"searchword" TEXT NOT NULL,
|
||||
"language" CHAR(1) CHECK ("language" IN ("e", "j")),
|
||||
PRIMARY KEY ("entryId", "searchword")
|
||||
);
|
||||
|
||||
CREATE TABLE "JST_HistoryEntryTimestamp" (
|
||||
"entryId" INTEGER NOT NULL REFERENCES "JST_HistoryEntry"("id") ON DELETE CASCADE,
|
||||
-- Here, I'm using INTEGER insted of TIMESTAMP or DATETIME, because it seems to be
|
||||
-- the easiest way to deal with global and local timeconversion between dart and
|
||||
-- SQLite.
|
||||
"timestamp" INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY ("entryId", "timestamp")
|
||||
);
|
||||
|
||||
CREATE INDEX "JST_HistoryEntryTimestamp_byTimestamp" ON "JST_HistoryEntryTimestamp"("timestamp");
|
||||
|
||||
CREATE VIEW "JST_HistoryEntry_orderedByTimestamp" AS
|
||||
SELECT * FROM "JST_HistoryEntryTimestamp"
|
||||
LEFT JOIN "JST_HistoryEntryWord" USING ("entryId")
|
||||
LEFT JOIN "JST_HistoryEntryKanji" USING ("entryId")
|
||||
GROUP BY "entryId"
|
||||
ORDER BY MAX("timestamp") DESC;
|
@ -1,51 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
|
||||
import '../../models/history/search.dart';
|
||||
import '../../models/history/history_entry.dart';
|
||||
import '../../routing/routes.dart';
|
||||
import '../../services/datetime.dart';
|
||||
import '../../services/snackbar.dart';
|
||||
import '../../settings.dart';
|
||||
import '../common/loading.dart';
|
||||
import 'kanji_box.dart';
|
||||
|
||||
class SearchItem extends StatelessWidget {
|
||||
final Search search;
|
||||
// final Widget search;
|
||||
class HistoryEntryItem extends StatelessWidget {
|
||||
final HistoryEntry entry;
|
||||
final int objectKey;
|
||||
final void Function()? onDelete;
|
||||
final void Function()? onFavourite;
|
||||
|
||||
const SearchItem({
|
||||
required this.search,
|
||||
const HistoryEntryItem({
|
||||
required this.entry,
|
||||
required this.objectKey,
|
||||
this.onDelete,
|
||||
this.onFavourite,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
Widget get _child => (search.isKanji)
|
||||
? KanjiBox(kanji: search.kanjiQuery!.kanji)
|
||||
: Text(search.wordQuery!.query);
|
||||
Widget get _child => (entry.isKanji)
|
||||
? KanjiBox(kanji: entry.kanji!)
|
||||
: Text(entry.word!);
|
||||
|
||||
void Function() _onTap(context) => search.isKanji
|
||||
void Function() _onTap(context) => entry.isKanji
|
||||
? () => Navigator.pushNamed(
|
||||
context,
|
||||
Routes.kanjiSearch,
|
||||
arguments: search.kanjiQuery!.kanji,
|
||||
arguments: entry.kanji,
|
||||
)
|
||||
: () => Navigator.pushNamed(
|
||||
context,
|
||||
Routes.search,
|
||||
arguments: search.wordQuery!.query,
|
||||
arguments: entry.word,
|
||||
);
|
||||
|
||||
MaterialPageRoute get timestamps => MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Last searched')),
|
||||
body: ListView(
|
||||
children: [
|
||||
for (final ts in search.timestamps.reversed)
|
||||
ListTile(title: Text('${formatDate(ts)} ${formatTime(ts)}'))
|
||||
],
|
||||
body: FutureBuilder<List<DateTime>>(
|
||||
future: entry.timestamps,
|
||||
builder: (context, snapshot) {
|
||||
// TODO: provide proper error handling
|
||||
if (snapshot.hasError)
|
||||
return ErrorWidget(snapshot.error!);
|
||||
if (!snapshot.hasData) return const LoadingScreen();
|
||||
return ListView(
|
||||
children: snapshot.data!
|
||||
.map(
|
||||
(ts) => ListTile(
|
||||
title: Text('${formatDate(ts)} ${formatTime(ts)}'),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -60,18 +73,15 @@ class SearchItem extends StatelessWidget {
|
||||
backgroundColor: Colors.yellow,
|
||||
icon: Icons.star,
|
||||
onPressed: (_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('TODO: implement favourites')),
|
||||
);
|
||||
showSnackbar(context, 'TODO: implement favourites');
|
||||
onFavourite?.call();
|
||||
},
|
||||
),
|
||||
SlidableAction(
|
||||
backgroundColor: Colors.red,
|
||||
icon: Icons.delete,
|
||||
onPressed: (_) {
|
||||
final Database db = GetIt.instance.get<Database>();
|
||||
Search.store.record(objectKey).delete(db);
|
||||
onPressed: (_) async {
|
||||
await entry.delete();
|
||||
onDelete?.call();
|
||||
},
|
||||
),
|
||||
@ -93,7 +103,7 @@ class SearchItem extends StatelessWidget {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(formatTime(search.timestamp)),
|
||||
child: Text(formatTime(entry.lastTimestamp)),
|
||||
),
|
||||
DefaultTextStyle.merge(
|
||||
style: japaneseFont.textStyle,
|
20
lib/data/archive_format.dart
Normal file
20
lib/data/archive_format.dart
Normal file
@ -0,0 +1,20 @@
|
||||
import 'dart:io';
|
||||
|
||||
// Example file Structure:
|
||||
// jisho_data_22.01.01_1
|
||||
// - history.json
|
||||
// - saved/
|
||||
// - lista.json
|
||||
// - listb.json
|
||||
|
||||
extension ArchiveFormat on Directory {
|
||||
// TODO: make the export dir dependent on date
|
||||
Directory get exportDirectory {
|
||||
final dir = Directory(uri.resolve('export').path);
|
||||
dir.createSync(recursive: true);
|
||||
return dir;
|
||||
}
|
||||
|
||||
File get historyFile => File(uri.resolve('history.json').path);
|
||||
Directory get savedLists => Directory(uri.resolve('savedLists').path);
|
||||
}
|
127
lib/data/database.dart
Normal file
127
lib/data/database.dart
Normal file
@ -0,0 +1,127 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
export 'package:sqflite/sqlite_api.dart';
|
||||
|
||||
Database db() => GetIt.instance.get<Database>();
|
||||
|
||||
Future<Directory> _databaseDir() async {
|
||||
final Directory appDocDir = await getApplicationDocumentsDirectory();
|
||||
if (!appDocDir.existsSync()) appDocDir.createSync(recursive: true);
|
||||
return appDocDir;
|
||||
}
|
||||
|
||||
Future<String> databasePath() async {
|
||||
return join((await _databaseDir()).path, 'jisho.sqlite');
|
||||
}
|
||||
|
||||
Future<void> migrate(Database db, int oldVersion, int newVersion) async {
|
||||
final String assetManifest =
|
||||
await rootBundle.loadString('AssetManifest.json');
|
||||
|
||||
final List<String> migrations =
|
||||
(jsonDecode(assetManifest) as Map<String, Object?>)
|
||||
.keys
|
||||
.where(
|
||||
(assetPath) =>
|
||||
assetPath.contains(RegExp(r'migrations\/\d{4}.*\.sql')),
|
||||
)
|
||||
.toList();
|
||||
|
||||
migrations.sort();
|
||||
|
||||
for (int i = oldVersion + 1; i <= newVersion; i++) {
|
||||
log(
|
||||
'Migrating database from v$i to v${i + 1} with File(${migrations[i - 1]})',
|
||||
);
|
||||
final migrationContent = await rootBundle.loadString(migrations[i - 1], cache: false);
|
||||
|
||||
migrationContent
|
||||
.split(';')
|
||||
.map(
|
||||
(s) => s
|
||||
.split('\n')
|
||||
.where((l) => !l.startsWith(RegExp(r'\s*--')))
|
||||
.join('\n')
|
||||
.trim(),
|
||||
)
|
||||
.where((s) => s != '')
|
||||
.forEach(db.execute);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setupDatabase() async {
|
||||
databaseFactory.debugSetLogLevel(sqfliteLogLevelSql);
|
||||
final Database database = await openDatabase(
|
||||
await databasePath(),
|
||||
version: 1,
|
||||
onCreate: (db, version) => migrate(db, 0, version),
|
||||
onUpgrade: migrate,
|
||||
onOpen: (db) => Future.wait([
|
||||
db.execute('PRAGMA foreign_keys=ON')
|
||||
]),
|
||||
);
|
||||
GetIt.instance.registerSingleton<Database>(database);
|
||||
}
|
||||
|
||||
Future<void> resetDatabase() async {
|
||||
await db().close();
|
||||
File(await databasePath()).deleteSync();
|
||||
GetIt.instance.unregister<Database>();
|
||||
await setupDatabase();
|
||||
}
|
||||
|
||||
class TableNames {
|
||||
/// Attributes:
|
||||
/// - id INTEGER
|
||||
static const String historyEntry = 'JST_HistoryEntry';
|
||||
|
||||
/// Attributes:
|
||||
/// - entryId INTEGER
|
||||
/// - kanji CHAR(1)
|
||||
static const String historyEntryKanji = 'JST_HistoryEntryKanji';
|
||||
|
||||
/// Attributes:
|
||||
/// - entryId INTEGER
|
||||
/// - timestamp INTEGER
|
||||
static const String historyEntryTimestamp = 'JST_HistoryEntryTimestamp';
|
||||
|
||||
/// Attributes:
|
||||
/// - entryId INTEGER
|
||||
/// - searchword TEXT
|
||||
/// - language CHAR(1)?
|
||||
static const String historyEntryWord = 'JST_HistoryEntryWord';
|
||||
|
||||
/// Attributes:
|
||||
/// - name TEXT
|
||||
/// - nextList TEXT
|
||||
static const String savedList = 'JST_SavedList';
|
||||
|
||||
/// Attributes:
|
||||
/// - listName TEXT
|
||||
/// - entryText TEXT
|
||||
/// - isKanji BOOLEAN
|
||||
/// - lastModified TIMESTAMP
|
||||
/// - nextEntry TEXT
|
||||
static const String savedListEntry = 'JST_SavedListEntry';
|
||||
|
||||
///////////
|
||||
// VIEWS //
|
||||
///////////
|
||||
|
||||
/// Attributes:
|
||||
/// - entryId INTEGER
|
||||
/// - timestamp INTEGER
|
||||
/// - searchword TEXT?
|
||||
/// - kanji CHAR(1)?
|
||||
/// - language CHAR(1)?
|
||||
static const String historyEntryOrderedByTimestamp =
|
||||
'JST_HistoryEntry_orderedByTimestamp';
|
||||
}
|
45
lib/data/export.dart
Normal file
45
lib/data/export.dart
Normal file
@ -0,0 +1,45 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../models/history/history_entry.dart';
|
||||
import 'database.dart';
|
||||
|
||||
Future<Directory> exportDirectory() async {
|
||||
final basedir = (await getExternalStorageDirectory())!;
|
||||
// TODO: fix path
|
||||
final dir = Directory(basedir.uri.resolve('export').path);
|
||||
dir.createSync(recursive: true);
|
||||
return dir;
|
||||
}
|
||||
|
||||
/// Returns the path to which the data was saved.
|
||||
Future<String> exportData() async {
|
||||
final dir = await exportDirectory();
|
||||
final savedDir = Directory.fromUri(dir.uri.resolve('saved'));
|
||||
savedDir.createSync();
|
||||
|
||||
await Future.wait([
|
||||
exportHistoryTo(dir),
|
||||
exportSavedListsTo(savedDir),
|
||||
]);
|
||||
return dir.path;
|
||||
}
|
||||
|
||||
Future<void> exportHistoryTo(Directory dir) async {
|
||||
final file = File(dir.uri.resolve('history.json').path);
|
||||
file.createSync();
|
||||
final query = await db().query(TableNames.historyEntryOrderedByTimestamp);
|
||||
final List<HistoryEntry> entries =
|
||||
query.map((e) => HistoryEntry.fromDBMap(e)).toList();
|
||||
final List<Map<String, Object?>> jsonEntries =
|
||||
await Future.wait(entries.map((he) async => he.toJson()));
|
||||
file.writeAsStringSync(jsonEncode(jsonEntries));
|
||||
}
|
||||
|
||||
Future<void> exportSavedListsTo(Directory dir) async {
|
||||
// TODO:
|
||||
// final query = db().query(TableNames.savedList);
|
||||
print('TODO: implement exportSavedLists');
|
||||
}
|
25
lib/data/import.dart
Normal file
25
lib/data/import.dart
Normal file
@ -0,0 +1,25 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import '../models/history/history_entry.dart';
|
||||
import 'archive_format.dart';
|
||||
|
||||
Future<void> importData(Directory dir) async {
|
||||
await Future.wait([
|
||||
importHistoryFrom(dir.historyFile),
|
||||
importSavedListsFrom(dir.savedLists),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> importHistoryFrom(File file) async {
|
||||
final String content = file.readAsStringSync();
|
||||
await HistoryEntry.insertJsonEntries(
|
||||
(jsonDecode(content) as List)
|
||||
.map((h) => h as Map<String, Object?>)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> importSavedListsFrom(Directory savedListsDir) async {
|
||||
print('TODO: Implement importSavedLists');
|
||||
}
|
@ -2,8 +2,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'bloc/theme/theme_bloc.dart';
|
||||
import 'data/database.dart';
|
||||
import 'routing/router.dart';
|
||||
import 'services/database.dart';
|
||||
import 'services/licenses.dart';
|
||||
import 'services/preferences.dart';
|
||||
import 'settings.dart';
|
||||
|
358
lib/models/history/history_entry.dart
Normal file
358
lib/models/history/history_entry.dart
Normal file
@ -0,0 +1,358 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import '../../data/database.dart';
|
||||
|
||||
export 'package:get_it/get_it.dart';
|
||||
|
||||
class HistoryEntry {
|
||||
int id;
|
||||
final String? kanji;
|
||||
final String? word;
|
||||
final DateTime lastTimestamp;
|
||||
|
||||
/// Whether this item is a kanji search or a word search
|
||||
bool get isKanji => word == null;
|
||||
|
||||
HistoryEntry.withKanji({
|
||||
required this.id,
|
||||
required this.kanji,
|
||||
required this.lastTimestamp,
|
||||
}) : word = null;
|
||||
|
||||
HistoryEntry.withWord({
|
||||
required this.id,
|
||||
required this.word,
|
||||
required this.lastTimestamp,
|
||||
}) : kanji = null;
|
||||
|
||||
/// Reconstruct a HistoryEntry object with data from the database
|
||||
/// This is specifically intended for the historyEntryOrderedByTimestamp
|
||||
/// view, but it can also be used with custom searches as long as it
|
||||
/// contains the following attributes:
|
||||
///
|
||||
/// - entryId
|
||||
/// - timestamp
|
||||
/// - searchword?
|
||||
/// - kanji?
|
||||
factory HistoryEntry.fromDBMap(Map<String, Object?> dbObject) =>
|
||||
dbObject['searchword'] != null
|
||||
? HistoryEntry.withWord(
|
||||
id: dbObject['entryId']! as int,
|
||||
word: dbObject['searchword']! as String,
|
||||
lastTimestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||
dbObject['timestamp']! as int,
|
||||
),
|
||||
)
|
||||
: HistoryEntry.withKanji(
|
||||
id: dbObject['entryId']! as int,
|
||||
kanji: dbObject['kanji']! as String,
|
||||
lastTimestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||
dbObject['timestamp']! as int,
|
||||
),
|
||||
);
|
||||
|
||||
// TODO: There is a lot in common with
|
||||
// insertKanji,
|
||||
// insertWord,
|
||||
// insertJsonEntry,
|
||||
// insertJsonEntries,
|
||||
// The commonalities should be factored into a helper function
|
||||
|
||||
/// Insert a kanji history entry into the database.
|
||||
/// If it already exists, only a timestamp will be added
|
||||
static Future<HistoryEntry> insertKanji({
|
||||
required String kanji,
|
||||
}) =>
|
||||
db().transaction((txn) async {
|
||||
final DateTime timestamp = DateTime.now();
|
||||
late final int id;
|
||||
|
||||
final existingEntry = await txn.query(
|
||||
TableNames.historyEntryKanji,
|
||||
where: 'kanji = ?',
|
||||
whereArgs: [kanji],
|
||||
);
|
||||
|
||||
if (existingEntry.isNotEmpty) {
|
||||
// Retrieve entry record id, and add a timestamp.
|
||||
id = existingEntry.first['entryId']! as int;
|
||||
await txn.insert(TableNames.historyEntryTimestamp, {
|
||||
'entryId': id,
|
||||
'timestamp': timestamp.millisecondsSinceEpoch,
|
||||
});
|
||||
} else {
|
||||
// Create new record, and add a timestamp.
|
||||
id = await txn.insert(
|
||||
TableNames.historyEntry,
|
||||
{},
|
||||
nullColumnHack: 'id',
|
||||
);
|
||||
final Batch b = txn.batch();
|
||||
|
||||
b.insert(TableNames.historyEntryTimestamp, {
|
||||
'entryId': id,
|
||||
'timestamp': timestamp.millisecondsSinceEpoch,
|
||||
});
|
||||
b.insert(TableNames.historyEntryKanji, {
|
||||
'entryId': id,
|
||||
'kanji': kanji,
|
||||
});
|
||||
await b.commit();
|
||||
}
|
||||
|
||||
return HistoryEntry.withKanji(
|
||||
id: id,
|
||||
kanji: kanji,
|
||||
lastTimestamp: timestamp,
|
||||
);
|
||||
});
|
||||
|
||||
/// Insert a word history entry into the database.
|
||||
/// If it already exists, only a timestamp will be added
|
||||
static Future<HistoryEntry> insertWord({
|
||||
required String word,
|
||||
String? language,
|
||||
}) =>
|
||||
db().transaction((txn) async {
|
||||
final DateTime timestamp = DateTime.now();
|
||||
late final int id;
|
||||
|
||||
final existingEntry = await txn.query(
|
||||
TableNames.historyEntryWord,
|
||||
where: 'searchword = ?',
|
||||
whereArgs: [word],
|
||||
);
|
||||
|
||||
if (existingEntry.isNotEmpty) {
|
||||
// Retrieve entry record id, and add a timestamp.
|
||||
id = existingEntry.first['entryId']! as int;
|
||||
await txn.insert(TableNames.historyEntryTimestamp, {
|
||||
'entryId': id,
|
||||
'timestamp': timestamp.millisecondsSinceEpoch,
|
||||
});
|
||||
} else {
|
||||
id = await txn.insert(
|
||||
TableNames.historyEntry,
|
||||
{},
|
||||
nullColumnHack: 'id',
|
||||
);
|
||||
final Batch b = txn.batch();
|
||||
|
||||
b.insert(TableNames.historyEntryTimestamp, {
|
||||
'entryId': id,
|
||||
'timestamp': timestamp.millisecondsSinceEpoch,
|
||||
});
|
||||
b.insert(TableNames.historyEntryWord, {
|
||||
'entryId': id,
|
||||
'searchword': word,
|
||||
'language': {
|
||||
null: null,
|
||||
'japanese': 'j',
|
||||
'english': 'e',
|
||||
}[language]
|
||||
});
|
||||
await b.commit();
|
||||
}
|
||||
|
||||
return HistoryEntry.withWord(
|
||||
id: id,
|
||||
word: word,
|
||||
lastTimestamp: timestamp,
|
||||
);
|
||||
});
|
||||
|
||||
/// All recorded timestamps for this specific HistoryEntry
|
||||
/// sorted in descending order.
|
||||
Future<List<DateTime>> get timestamps async => GetIt.instance
|
||||
.get<Database>()
|
||||
.query(
|
||||
TableNames.historyEntryTimestamp,
|
||||
where: 'entryId = ?',
|
||||
whereArgs: [id],
|
||||
orderBy: 'timestamp DESC',
|
||||
)
|
||||
.then(
|
||||
(timestamps) => timestamps
|
||||
.map(
|
||||
(t) => DateTime.fromMillisecondsSinceEpoch(
|
||||
t['timestamp']! as int,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
/// Export to json for archival reasons
|
||||
/// Combined with [insertJsonEntry], this makes up functionality for exporting
|
||||
/// and importing data from the app.
|
||||
Future<Map<String, Object?>> toJson() async => {
|
||||
'word': word,
|
||||
'kanji': kanji,
|
||||
'timestamps':
|
||||
(await timestamps).map((ts) => ts.millisecondsSinceEpoch).toList()
|
||||
};
|
||||
|
||||
/// Insert archived json entry into database if it doesn't exist there already.
|
||||
/// Combined with [toJson], this makes up functionality for exporting and
|
||||
/// importing data from the app.
|
||||
static Future<HistoryEntry> insertJsonEntry(
|
||||
Map<String, Object?> json,
|
||||
) async =>
|
||||
db().transaction((txn) async {
|
||||
final b = txn.batch();
|
||||
final bool isKanji = json['word'] == null;
|
||||
final existingEntry = isKanji
|
||||
? await txn.query(
|
||||
TableNames.historyEntryKanji,
|
||||
where: 'kanji = ?',
|
||||
whereArgs: [json['kanji']! as String],
|
||||
)
|
||||
: await txn.query(
|
||||
TableNames.historyEntryWord,
|
||||
where: 'searchword = ?',
|
||||
whereArgs: [json['word']! as String],
|
||||
);
|
||||
|
||||
late final int id;
|
||||
if (existingEntry.isEmpty) {
|
||||
id = await txn.insert(
|
||||
TableNames.historyEntry,
|
||||
{},
|
||||
nullColumnHack: 'id',
|
||||
);
|
||||
if (isKanji) {
|
||||
b.insert(TableNames.historyEntryKanji, {
|
||||
'entryId': id,
|
||||
'kanji': json['kanji']! as String,
|
||||
});
|
||||
} else {
|
||||
b.insert(TableNames.historyEntryWord, {
|
||||
'entryId': id,
|
||||
'searchword': json['word']! as String,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
id = existingEntry.first['entryId']! as int;
|
||||
}
|
||||
final List<int> timestamps =
|
||||
(json['timestamps']! as List).map((ts) => ts as int).toList();
|
||||
for (final timestamp in timestamps) {
|
||||
b.insert(
|
||||
TableNames.historyEntryTimestamp,
|
||||
{
|
||||
'entryId': id,
|
||||
'timestamp': timestamp,
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore,
|
||||
);
|
||||
}
|
||||
|
||||
await b.commit();
|
||||
|
||||
return isKanji
|
||||
? HistoryEntry.withKanji(
|
||||
id: id,
|
||||
kanji: json['kanji']! as String,
|
||||
lastTimestamp:
|
||||
DateTime.fromMillisecondsSinceEpoch(timestamps.reduce(max)),
|
||||
)
|
||||
: HistoryEntry.withWord(
|
||||
id: id,
|
||||
word: json['word']! as String,
|
||||
lastTimestamp:
|
||||
DateTime.fromMillisecondsSinceEpoch(timestamps.reduce(max)),
|
||||
);
|
||||
});
|
||||
|
||||
/// An efficient implementation of [insertJsonEntry] for multiple
|
||||
/// entries.
|
||||
///
|
||||
/// This assumes that there are no duplicates within the elements
|
||||
/// in the json.
|
||||
static Future<List<HistoryEntry>> insertJsonEntries(
|
||||
List<Map<String, Object?>> json,
|
||||
) =>
|
||||
db().transaction((txn) async {
|
||||
final b = txn.batch();
|
||||
final List<HistoryEntry> entries = [];
|
||||
for (final jsonObject in json) {
|
||||
final bool isKanji = jsonObject['word'] == null;
|
||||
final existingEntry = isKanji
|
||||
? await txn.query(
|
||||
TableNames.historyEntryKanji,
|
||||
where: 'kanji = ?',
|
||||
whereArgs: [jsonObject['kanji']! as String],
|
||||
)
|
||||
: await txn.query(
|
||||
TableNames.historyEntryWord,
|
||||
where: 'searchword = ?',
|
||||
whereArgs: [jsonObject['word']! as String],
|
||||
);
|
||||
|
||||
late final int id;
|
||||
if (existingEntry.isEmpty) {
|
||||
id = await txn.insert(
|
||||
TableNames.historyEntry,
|
||||
{},
|
||||
nullColumnHack: 'id',
|
||||
);
|
||||
if (isKanji) {
|
||||
b.insert(TableNames.historyEntryKanji, {
|
||||
'entryId': id,
|
||||
'kanji': jsonObject['kanji']! as String,
|
||||
});
|
||||
} else {
|
||||
b.insert(TableNames.historyEntryWord, {
|
||||
'entryId': id,
|
||||
'searchword': jsonObject['word']! as String,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
id = existingEntry.first['entryId']! as int;
|
||||
}
|
||||
final List<int> timestamps = (jsonObject['timestamps']! as List)
|
||||
.map((ts) => ts as int)
|
||||
.toList();
|
||||
for (final timestamp in timestamps) {
|
||||
b.insert(
|
||||
TableNames.historyEntryTimestamp,
|
||||
{
|
||||
'entryId': id,
|
||||
'timestamp': timestamp,
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore,
|
||||
);
|
||||
}
|
||||
|
||||
entries.add(
|
||||
isKanji
|
||||
? HistoryEntry.withKanji(
|
||||
id: id,
|
||||
kanji: jsonObject['kanji']! as String,
|
||||
lastTimestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||
timestamps.reduce(max),
|
||||
),
|
||||
)
|
||||
: HistoryEntry.withWord(
|
||||
id: id,
|
||||
word: jsonObject['word']! as String,
|
||||
lastTimestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||
timestamps.reduce(max),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await b.commit();
|
||||
return entries;
|
||||
});
|
||||
|
||||
static Future<List<HistoryEntry>> get fromDB async =>
|
||||
(await db().query(TableNames.historyEntryOrderedByTimestamp))
|
||||
.map((e) => HistoryEntry.fromDBMap(e))
|
||||
.toList();
|
||||
|
||||
Future<void> delete() =>
|
||||
db().delete(TableNames.historyEntry, where: 'id = ?', whereArgs: [id]);
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
class KanjiQuery {
|
||||
final String kanji;
|
||||
|
||||
KanjiQuery({
|
||||
required this.kanji,
|
||||
});
|
||||
|
||||
Map<String, Object?> toJson() => {'kanji': kanji};
|
||||
|
||||
factory KanjiQuery.fromJson(Map<String, dynamic> json) =>
|
||||
KanjiQuery(kanji: json['kanji'] as String);
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:sembast/sembast.dart';
|
||||
|
||||
import './kanji_query.dart';
|
||||
import './word_query.dart';
|
||||
|
||||
export 'package:get_it/get_it.dart';
|
||||
export 'package:sembast/sembast.dart';
|
||||
|
||||
class Search {
|
||||
final WordQuery? wordQuery;
|
||||
final KanjiQuery? kanjiQuery;
|
||||
final List<DateTime> timestamps;
|
||||
|
||||
Search.fromKanjiQuery({
|
||||
required KanjiQuery this.kanjiQuery,
|
||||
List<DateTime>? timestamps,
|
||||
}) : wordQuery = null,
|
||||
timestamps = timestamps ?? [DateTime.now()];
|
||||
|
||||
Search.fromWordQuery({
|
||||
required WordQuery this.wordQuery,
|
||||
List<DateTime>? timestamps,
|
||||
}) : kanjiQuery = null,
|
||||
timestamps = timestamps ?? [DateTime.now()];
|
||||
|
||||
bool get isKanji => wordQuery == null;
|
||||
|
||||
DateTime get timestamp => timestamps.last;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'timestamps': [for (final ts in timestamps) ts.millisecondsSinceEpoch],
|
||||
'lastTimestamp': timestamps.last.millisecondsSinceEpoch,
|
||||
'wordQuery': wordQuery?.toJson(),
|
||||
'kanjiQuery': kanjiQuery?.toJson(),
|
||||
};
|
||||
|
||||
factory Search.fromJson(Map<String, dynamic> json) {
|
||||
final List<DateTime> timestamps = [
|
||||
for (final ts in json['timestamps'] as List<dynamic>)
|
||||
DateTime.fromMillisecondsSinceEpoch(ts as int)
|
||||
];
|
||||
|
||||
return json['wordQuery'] != null
|
||||
? Search.fromWordQuery(
|
||||
wordQuery: WordQuery.fromJson(json['wordQuery']),
|
||||
timestamps: timestamps,
|
||||
)
|
||||
: Search.fromKanjiQuery(
|
||||
kanjiQuery: KanjiQuery.fromJson(json['kanjiQuery']),
|
||||
timestamps: timestamps,
|
||||
);
|
||||
}
|
||||
|
||||
static StoreRef<int, Object?> get store => intMapStoreFactory.store('search');
|
||||
}
|
||||
|
||||
Future<void> addSearchToDatabase({
|
||||
required String searchTerm,
|
||||
required bool isKanji,
|
||||
}) async {
|
||||
final DateTime now = DateTime.now();
|
||||
final db = GetIt.instance.get<Database>();
|
||||
final Filter filter = Filter.equals(
|
||||
isKanji ? 'kanjiQuery.kanji' : 'wordQuery.query',
|
||||
searchTerm,
|
||||
);
|
||||
|
||||
final RecordSnapshot<int, Object?>? previousSearch =
|
||||
await Search.store.findFirst(db, finder: Finder(filter: filter));
|
||||
|
||||
if (previousSearch != null) {
|
||||
final search =
|
||||
Search.fromJson(previousSearch.value! as Map<String, Object?>);
|
||||
search.timestamps.add(now);
|
||||
Search.store.record(previousSearch.key).put(db, search.toJson());
|
||||
return;
|
||||
}
|
||||
|
||||
Search.store.add(
|
||||
db,
|
||||
isKanji
|
||||
? Search.fromKanjiQuery(kanjiQuery: KanjiQuery(kanji: searchTerm))
|
||||
.toJson()
|
||||
: Search.fromWordQuery(wordQuery: WordQuery(query: searchTerm))
|
||||
.toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
List<Search> mergeSearches(List<Search> a, List<Search> b) {
|
||||
final List<Search> result = [...a];
|
||||
|
||||
for (final Search search in b) {
|
||||
late final Iterable<Search> matchingEntry;
|
||||
if (search.isKanji) {
|
||||
matchingEntry =
|
||||
result.where((e) => e.kanjiQuery?.kanji == search.kanjiQuery!.kanji);
|
||||
} else {
|
||||
matchingEntry =
|
||||
result.where((e) => e.wordQuery?.query == search.wordQuery!.query);
|
||||
}
|
||||
|
||||
if (matchingEntry.isEmpty) {
|
||||
result.add(search);
|
||||
continue;
|
||||
}
|
||||
|
||||
final timestamps = [...matchingEntry.first.timestamps];
|
||||
matchingEntry.first.timestamps.clear();
|
||||
matchingEntry.first.timestamps.addAll(
|
||||
(timestamps
|
||||
..addAll(search.timestamps)
|
||||
..sort())
|
||||
.toSet()
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
|
||||
class WordQuery {
|
||||
final String query;
|
||||
|
||||
// TODO: Link query with results that the user clicks onto.
|
||||
// final List<WordResult> chosenResults;
|
||||
|
||||
WordQuery({
|
||||
required this.query,
|
||||
});
|
||||
|
||||
Map<String, Object?> toJson() => {'query': query};
|
||||
|
||||
factory WordQuery.fromJson(Map<String, dynamic> json) =>
|
||||
WordQuery(query: json['query'] as String);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import 'word_query.dart';
|
||||
|
||||
class WordResult {
|
||||
final DateTime timestamp;
|
||||
final String word;
|
||||
final WordQuery searchString;
|
||||
|
||||
WordResult({
|
||||
required this.timestamp,
|
||||
required this.word,
|
||||
required this.searchString,
|
||||
});
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
// import 'package:objectbox/objectbox.dart';
|
||||
// import 'package:unofficial_jisho_api/api.dart' as jisho;
|
||||
|
||||
// TODO: Rewrite for sembast
|
||||
|
||||
// @Entity()
|
||||
// class ExampleSentencePiece {
|
||||
// int id;
|
||||
// String? lifted;
|
||||
// String unlifted;
|
||||
|
||||
// ExampleSentencePiece({
|
||||
// this.id = 0,
|
||||
// required this.lifted,
|
||||
// required this.unlifted,
|
||||
// });
|
||||
|
||||
// ExampleSentencePiece.fromJishoObject(jisho.ExampleSentencePiece object)
|
||||
// : id = 0,
|
||||
// lifted = object.lifted,
|
||||
// unlifted = object.unlifted;
|
||||
// }
|
@ -1,58 +0,0 @@
|
||||
// import 'package:objectbox/objectbox.dart';
|
||||
// import 'package:unofficial_jisho_api/api.dart' as jisho;
|
||||
|
||||
// import 'common.dart';
|
||||
|
||||
// TODO: Rewrite for sembast
|
||||
|
||||
// @Entity()
|
||||
// class ExampleResultData {
|
||||
// int id;
|
||||
// String kanji;
|
||||
// String kana;
|
||||
// String english;
|
||||
// List<ExampleSentencePiece> pieces;
|
||||
|
||||
// ExampleResultData({
|
||||
// this.id = 0,
|
||||
// required this.kanji,
|
||||
// required this.kana,
|
||||
// required this.english,
|
||||
// required this.pieces,
|
||||
// });
|
||||
|
||||
// ExampleResultData.fromJishoObject(jisho.ExampleResultData object)
|
||||
// : id = 0,
|
||||
// kanji = object.kanji,
|
||||
// kana = object.kana,
|
||||
// english = object.english,
|
||||
// pieces = object.pieces
|
||||
// .map((p) => ExampleSentencePiece.fromJishoObject(p))
|
||||
// .toList();
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class ExampleResults {
|
||||
// int id;
|
||||
// String query;
|
||||
// bool found;
|
||||
// String uri;
|
||||
// List<ExampleResultData> results;
|
||||
|
||||
// ExampleResults({
|
||||
// this.id = 0,
|
||||
// required this.query,
|
||||
// required this.found,
|
||||
// required this.uri,
|
||||
// required this.results,
|
||||
// });
|
||||
|
||||
// ExampleResults.fromJishoObject(jisho.ExampleResults object)
|
||||
// : id = 0,
|
||||
// query = object.query,
|
||||
// found = object.found,
|
||||
// uri = object.uri,
|
||||
// results = object.results
|
||||
// .map((r) => ExampleResultData.fromJishoObject(r))
|
||||
// .toList();
|
||||
// }
|
@ -1,129 +0,0 @@
|
||||
// import 'package:objectbox/objectbox.dart';
|
||||
// import 'package:unofficial_jisho_api/api.dart' as jisho;
|
||||
|
||||
// TODO: Rewrite for sembast
|
||||
|
||||
// @Entity()
|
||||
// class YomiExample {
|
||||
// int id;
|
||||
// String example;
|
||||
// String reading;
|
||||
// String meaning;
|
||||
|
||||
// YomiExample({
|
||||
// this.id = 0,
|
||||
// required this.example,
|
||||
// required this.reading,
|
||||
// required this.meaning,
|
||||
// });
|
||||
|
||||
// YomiExample.fromJishoObject(jisho.YomiExample object)
|
||||
// : id = 0,
|
||||
// example = object.example,
|
||||
// reading = object.reading,
|
||||
// meaning = object.meaning;
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class Radical {
|
||||
// int id = 0;
|
||||
// String symbol;
|
||||
// List<String> forms;
|
||||
// String meaning;
|
||||
|
||||
// Radical({
|
||||
// this.id = 0,
|
||||
// required this.symbol,
|
||||
// required this.forms,
|
||||
// required this.meaning,
|
||||
// });
|
||||
|
||||
// Radical.fromJishoObject(jisho.Radical object)
|
||||
// : symbol = object.symbol,
|
||||
// forms = object.forms,
|
||||
// meaning = object.meaning;
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class KanjiResult {
|
||||
// int id = 0;
|
||||
// String query;
|
||||
// bool found;
|
||||
// KanjiResultData? data;
|
||||
|
||||
// KanjiResult({
|
||||
// this.id = 0,
|
||||
// required this.query,
|
||||
// required this.found,
|
||||
// required this.data,
|
||||
// });
|
||||
|
||||
// KanjiResult.fromJishoObject(jisho.KanjiResult object)
|
||||
// : query = object.query,
|
||||
// found = object.found,
|
||||
// data = (object.data == null)
|
||||
// ? null
|
||||
// : KanjiResultData.fromJishoObject(object.data!);
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class KanjiResultData {
|
||||
// int id = 0;
|
||||
// String? taughtIn;
|
||||
// String? jlptLevel;
|
||||
// int? newspaperFrequencyRank;
|
||||
// int strokeCount;
|
||||
// String meaning;
|
||||
// List<String> kunyomi;
|
||||
// List<String> onyomi;
|
||||
// List<YomiExample> kunyomiExamples;
|
||||
// List<YomiExample> onyomiExamples;
|
||||
// Radical? radical;
|
||||
// List<String> parts;
|
||||
// String strokeOrderDiagramUri;
|
||||
// String strokeOrderSvgUri;
|
||||
// String strokeOrderGifUri;
|
||||
// String uri;
|
||||
|
||||
// KanjiResultData({
|
||||
// this.id = 0,
|
||||
// required this.taughtIn,
|
||||
// required this.jlptLevel,
|
||||
// required this.newspaperFrequencyRank,
|
||||
// required this.strokeCount,
|
||||
// required this.meaning,
|
||||
// required this.kunyomi,
|
||||
// required this.onyomi,
|
||||
// required this.kunyomiExamples,
|
||||
// required this.onyomiExamples,
|
||||
// required this.radical,
|
||||
// required this.parts,
|
||||
// required this.strokeOrderDiagramUri,
|
||||
// required this.strokeOrderSvgUri,
|
||||
// required this.strokeOrderGifUri,
|
||||
// required this.uri,
|
||||
// });
|
||||
|
||||
// KanjiResultData.fromJishoObject(jisho.KanjiResultData object)
|
||||
// : taughtIn = object.taughtIn,
|
||||
// jlptLevel = object.jlptLevel,
|
||||
// newspaperFrequencyRank = object.newspaperFrequencyRank,
|
||||
// strokeCount = object.strokeCount,
|
||||
// meaning = object.meaning,
|
||||
// kunyomi = object.kunyomi,
|
||||
// onyomi = object.onyomi,
|
||||
// kunyomiExamples = object.kunyomiExamples
|
||||
// .map((k) => YomiExample.fromJishoObject(k))
|
||||
// .toList(),
|
||||
// onyomiExamples = object.onyomiExamples
|
||||
// .map((o) => YomiExample.fromJishoObject(o))
|
||||
// .toList(),
|
||||
// radical = (object.radical == null)
|
||||
// ? null
|
||||
// : Radical.fromJishoObject(object.radical!),
|
||||
// parts = object.parts,
|
||||
// strokeOrderDiagramUri = object.strokeOrderDiagramUri,
|
||||
// strokeOrderSvgUri = object.strokeOrderSvgUri,
|
||||
// strokeOrderGifUri = object.strokeOrderGifUri,
|
||||
// uri = object.uri;
|
||||
// }
|
@ -1,155 +0,0 @@
|
||||
// import 'package:objectbox/objectbox.dart';
|
||||
// import 'package:unofficial_jisho_api/api.dart' as jisho;
|
||||
|
||||
// import 'common.dart';
|
||||
|
||||
// TODO: Rewrite for sembast
|
||||
|
||||
// @Entity()
|
||||
// class PhraseScrapeSentence {
|
||||
// int id;
|
||||
// String english;
|
||||
// String japanese;
|
||||
// List<ExampleSentencePiece> pieces;
|
||||
|
||||
// PhraseScrapeSentence({
|
||||
// this.id = 0,
|
||||
// required this.english,
|
||||
// required this.japanese,
|
||||
// required this.pieces,
|
||||
// });
|
||||
|
||||
// PhraseScrapeSentence.fromJishoObject(jisho.PhraseScrapeSentence object)
|
||||
// : id = 0,
|
||||
// english = object.english,
|
||||
// japanese = object.japanese,
|
||||
// pieces = object.pieces
|
||||
// .map((p) => ExampleSentencePiece.fromJishoObject(p))
|
||||
// .toList();
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class PhraseScrapeMeaning {
|
||||
// int id;
|
||||
// List<String> seeAlsoTerms;
|
||||
// List<PhraseScrapeSentence> sentences;
|
||||
// String definition;
|
||||
// List<String> supplemental;
|
||||
// String? definitionAbstract;
|
||||
// List<String> tags;
|
||||
|
||||
// PhraseScrapeMeaning({
|
||||
// this.id = 0,
|
||||
// required this.seeAlsoTerms,
|
||||
// required this.sentences,
|
||||
// required this.definition,
|
||||
// required this.supplemental,
|
||||
// required this.definitionAbstract,
|
||||
// required this.tags,
|
||||
// });
|
||||
|
||||
// PhraseScrapeMeaning.fromJishoObject(jisho.PhraseScrapeMeaning object)
|
||||
// : id = 0,
|
||||
// seeAlsoTerms = object.seeAlsoTerms,
|
||||
// sentences = object.sentences
|
||||
// .map((s) => PhraseScrapeSentence.fromJishoObject(s))
|
||||
// .toList(),
|
||||
// definition = object.definition,
|
||||
// supplemental = object.supplemental,
|
||||
// definitionAbstract = object.definitionAbstract,
|
||||
// tags = object.tags;
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class KanjiKanaPair {
|
||||
// int id;
|
||||
// String kanji;
|
||||
// String? kana;
|
||||
|
||||
// KanjiKanaPair({
|
||||
// this.id = 0,
|
||||
// required this.kanji,
|
||||
// required this.kana,
|
||||
// });
|
||||
|
||||
// KanjiKanaPair.fromJishoObject(jisho.KanjiKanaPair object)
|
||||
// : id = 0,
|
||||
// kanji = object.kanji,
|
||||
// kana = object.kana;
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class PhrasePageScrapeResult {
|
||||
// int id;
|
||||
// bool found;
|
||||
// String query;
|
||||
// PhrasePageScrapeResultData? data;
|
||||
|
||||
// PhrasePageScrapeResult({
|
||||
// this.id = 0,
|
||||
// required this.found,
|
||||
// required this.query,
|
||||
// required this.data,
|
||||
// });
|
||||
|
||||
// PhrasePageScrapeResult.fromJishoObject(jisho.PhrasePageScrapeResult object)
|
||||
// : id = 0,
|
||||
// found = object.found,
|
||||
// query = object.query,
|
||||
// data = (object.data == null)
|
||||
// ? null
|
||||
// : PhrasePageScrapeResultData.fromJishoObject(object.data!);
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class AudioFile {
|
||||
// int id;
|
||||
// String uri;
|
||||
// String mimetype;
|
||||
|
||||
// AudioFile({
|
||||
// this.id = 0,
|
||||
// required this.uri,
|
||||
// required this.mimetype,
|
||||
// });
|
||||
|
||||
// AudioFile.fromJishoObject(jisho.AudioFile object)
|
||||
// : id = 0,
|
||||
// uri = object.uri,
|
||||
// mimetype = object.mimetype;
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class PhrasePageScrapeResultData {
|
||||
// int id;
|
||||
// String uri;
|
||||
// List<String> tags;
|
||||
// List<PhraseScrapeMeaning> meanings;
|
||||
// List<KanjiKanaPair> otherForms;
|
||||
// List<AudioFile> audio;
|
||||
// List<String> notes;
|
||||
|
||||
// PhrasePageScrapeResultData({
|
||||
// this.id = 0,
|
||||
// required this.uri,
|
||||
// required this.tags,
|
||||
// required this.meanings,
|
||||
// required this.otherForms,
|
||||
// required this.audio,
|
||||
// required this.notes,
|
||||
// });
|
||||
|
||||
// PhrasePageScrapeResultData.fromJishoObject(
|
||||
// jisho.PhrasePageScrapeResultData object,
|
||||
// ) : id = 0,
|
||||
// uri = object.uri,
|
||||
// tags = object.tags,
|
||||
// meanings = object.meanings
|
||||
// .map((m) => PhraseScrapeMeaning.fromJishoObject(m))
|
||||
// .toList(),
|
||||
// otherForms = object.otherForms
|
||||
// .map((f) => KanjiKanaPair.fromJishoObject(f))
|
||||
// .toList(),
|
||||
// audio = object.audio.map((a) => AudioFile.fromJishoObject(a)).toList(),
|
||||
// notes = object.notes;
|
||||
// }
|
@ -1,195 +0,0 @@
|
||||
// import 'package:objectbox/objectbox.dart';
|
||||
// import 'package:unofficial_jisho_api/api.dart' as jisho;
|
||||
|
||||
// TODO: Rewrite for sembast
|
||||
|
||||
// @Entity()
|
||||
// class SearchResult {
|
||||
// int id;
|
||||
// final JishoResultMeta meta;
|
||||
// final ToMany<JishoResult> data;
|
||||
|
||||
// SearchResult({
|
||||
// this.id = 0,
|
||||
// required this.meta,
|
||||
// required this.data,
|
||||
// });
|
||||
|
||||
// SearchResult.fromJishoObject(final jisho.JishoAPIResult object)
|
||||
// : id = 0,
|
||||
// meta = JishoResultMeta.fromJishoObject(object.meta),
|
||||
// data = ToMany<JishoResult>()
|
||||
// ..addAll(
|
||||
// object.data?.map((r) => JishoResult.fromJishoObject(r)) ??
|
||||
// <JishoResult>[],
|
||||
// );
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class JishoResultMeta {
|
||||
// int id;
|
||||
// int status;
|
||||
|
||||
// JishoResultMeta({
|
||||
// this.id = 0,
|
||||
// required this.status,
|
||||
// });
|
||||
|
||||
// JishoResultMeta.fromJishoObject(final jisho.JishoResultMeta object)
|
||||
// : id = 0,
|
||||
// status = object.status;
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class JishoResult {
|
||||
// int id;
|
||||
// JishoAttribution attribution;
|
||||
// bool? is_common;
|
||||
// List<JishoJapaneseWord> japanese;
|
||||
// List<String> jlpt;
|
||||
// List<JishoWordSense> senses;
|
||||
// String slug;
|
||||
// List<String> tags;
|
||||
|
||||
// JishoResult({
|
||||
// this.id = 0,
|
||||
// required this.attribution,
|
||||
// required this.is_common,
|
||||
// required this.japanese,
|
||||
// required this.jlpt,
|
||||
// required this.senses,
|
||||
// required this.slug,
|
||||
// required this.tags,
|
||||
// });
|
||||
|
||||
// JishoResult.fromJishoObject(final jisho.JishoResult object)
|
||||
// : id = 0,
|
||||
// attribution = JishoAttribution.fromJishoObject(object.attribution),
|
||||
// is_common = object.isCommon,
|
||||
// japanese = object.japanese
|
||||
// .map((j) => JishoJapaneseWord.fromJishoObject(j))
|
||||
// .toList(),
|
||||
// jlpt = object.jlpt,
|
||||
// senses = object.senses
|
||||
// .map((s) => JishoWordSense.fromJishoObject(s))
|
||||
// .toList(),
|
||||
// slug = object.slug,
|
||||
// tags = object.tags;
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class JishoAttribution {
|
||||
// int id;
|
||||
// String? dbpedia;
|
||||
// bool jmdict;
|
||||
// bool jmnedict;
|
||||
|
||||
// JishoAttribution({
|
||||
// this.id = 0,
|
||||
// required this.dbpedia,
|
||||
// required this.jmdict,
|
||||
// required this.jmnedict,
|
||||
// });
|
||||
|
||||
// JishoAttribution.fromJishoObject(final jisho.JishoAttribution object)
|
||||
// : id = 0,
|
||||
// dbpedia = object.dbpedia,
|
||||
// jmdict = object.jmdict,
|
||||
// jmnedict = object.jmnedict;
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class JishoJapaneseWord {
|
||||
// int id;
|
||||
// String? reading;
|
||||
// String? word;
|
||||
|
||||
// JishoJapaneseWord({
|
||||
// this.id = 0,
|
||||
// required this.reading,
|
||||
// required this.word,
|
||||
// });
|
||||
|
||||
// JishoJapaneseWord.fromJishoObject(final jisho.JishoJapaneseWord object)
|
||||
// : id = 0,
|
||||
// reading = object.reading,
|
||||
// word = object.word;
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class JishoWordSense {
|
||||
// int id;
|
||||
// List<String> antonyms;
|
||||
// List<String> english_definitions;
|
||||
// List<String> info;
|
||||
// List<JishoSenseLink> links;
|
||||
// List<String> parts_of_speech;
|
||||
// List<String> restrictions;
|
||||
// List<String> see_also;
|
||||
// List<JishoWordSource> source;
|
||||
// List<String> tags;
|
||||
|
||||
// JishoWordSense({
|
||||
// this.id = 0,
|
||||
// required this.antonyms,
|
||||
// required this.english_definitions,
|
||||
// required this.info,
|
||||
// required this.links,
|
||||
// required this.parts_of_speech,
|
||||
// required this.restrictions,
|
||||
// required this.see_also,
|
||||
// required this.source,
|
||||
// required this.tags,
|
||||
// });
|
||||
|
||||
// JishoWordSense.fromJishoObject(final jisho.JishoWordSense object)
|
||||
// : id = 0,
|
||||
// antonyms = object.antonyms,
|
||||
// english_definitions = object.englishDefinitions,
|
||||
// info = object.info,
|
||||
// links =
|
||||
// object.links.map((l) => JishoSenseLink.fromJishoObject(l)).toList(),
|
||||
// parts_of_speech = object.partsOfSpeech,
|
||||
// restrictions = object.restrictions,
|
||||
// see_also = object.seeAlso,
|
||||
// source = object.source
|
||||
// .map((s) => JishoWordSource.fromJishoObject(s))
|
||||
// .toList(),
|
||||
// tags = object.tags;
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class JishoWordSource {
|
||||
// int id;
|
||||
// String language;
|
||||
// String? word;
|
||||
|
||||
// JishoWordSource({
|
||||
// this.id = 0,
|
||||
// required this.language,
|
||||
// required this.word,
|
||||
// });
|
||||
|
||||
// JishoWordSource.fromJishoObject(final jisho.JishoWordSource object)
|
||||
// : id = 0,
|
||||
// language = object.language,
|
||||
// word = object.word;
|
||||
// }
|
||||
|
||||
// @Entity()
|
||||
// class JishoSenseLink {
|
||||
// int id;
|
||||
// String text;
|
||||
// String url;
|
||||
|
||||
// JishoSenseLink({
|
||||
// this.id = 0,
|
||||
// required this.text,
|
||||
// required this.url,
|
||||
// });
|
||||
|
||||
// JishoSenseLink.fromJishoObject(final jisho.JishoSenseLink object)
|
||||
// : id = 0,
|
||||
// text = object.text,
|
||||
// url = object.url;
|
||||
// }
|
@ -3,37 +3,24 @@ import 'package:flutter/material.dart';
|
||||
import '../components/common/loading.dart';
|
||||
import '../components/common/opaque_box.dart';
|
||||
import '../components/history/date_divider.dart';
|
||||
import '../components/history/search_item.dart';
|
||||
import '../models/history/search.dart';
|
||||
import '../components/history/history_entry_item.dart';
|
||||
import '../models/history/history_entry.dart';
|
||||
import '../services/datetime.dart';
|
||||
|
||||
class HistoryView extends StatelessWidget {
|
||||
const HistoryView({Key? key}) : super(key: key);
|
||||
|
||||
Stream<Map<int, Search>> get searchStream => Search.store
|
||||
.query(finder: Finder(sortOrders: [SortOrder('lastTimestamp', false)]))
|
||||
.onSnapshots(_db)
|
||||
.map(
|
||||
(snapshot) => Map.fromEntries(
|
||||
snapshot.where((snap) => snap.value != null).map(
|
||||
(snap) => MapEntry(
|
||||
snap.key,
|
||||
Search.fromJson(snap.value! as Map<String, Object?>),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Database get _db => GetIt.instance.get<Database>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<Map<int, Search>>(
|
||||
stream: searchStream,
|
||||
// TODO: Use infinite scroll pagination
|
||||
return FutureBuilder<List<HistoryEntry>>(
|
||||
future: HistoryEntry.fromDB,
|
||||
builder: (context, snapshot) {
|
||||
// TODO: provide proper error handling
|
||||
if (snapshot.hasError) return ErrorWidget(snapshot.error!);
|
||||
if (!snapshot.hasData) return const LoadingScreen();
|
||||
|
||||
final Map<int, Search> data = snapshot.data!;
|
||||
final Map<int, HistoryEntry> data = snapshot.data!.asMap();
|
||||
if (data.isEmpty)
|
||||
return const Center(
|
||||
child: Text('The history is empty.\nTry searching for something!'),
|
||||
@ -52,25 +39,25 @@ class HistoryView extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget Function(BuildContext, int) historyEntrySeparatorWithData(
|
||||
List<Search> data,
|
||||
List<HistoryEntry> data,
|
||||
) =>
|
||||
(context, index) {
|
||||
final Search search = data[index];
|
||||
final DateTime searchDate = search.timestamp;
|
||||
final HistoryEntry search = data[index];
|
||||
final DateTime searchDate = search.lastTimestamp;
|
||||
|
||||
if (index == 0 || !dateIsEqual(data[index - 1].timestamp, searchDate))
|
||||
if (index == 0 || !dateIsEqual(data[index - 1].lastTimestamp, searchDate))
|
||||
return TextDivider(text: formatDate(roundToDay(searchDate)));
|
||||
|
||||
return const Divider(height: 0);
|
||||
};
|
||||
|
||||
Widget Function(BuildContext, int) historyEntryWithData(
|
||||
Map<int, Search> data,
|
||||
Map<int, HistoryEntry> data,
|
||||
) =>
|
||||
(context, index) => (index == 0)
|
||||
? const SizedBox.shrink()
|
||||
: SearchItem(
|
||||
search: data.values.toList()[index - 1],
|
||||
: HistoryEntryItem(
|
||||
entry: data.values.toList()[index - 1],
|
||||
objectKey: data.keys.toList()[index - 1],
|
||||
onDelete: () => build(context),
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import '../../components/common/loading.dart';
|
||||
import '../../components/kanji/kanji_result_body.dart';
|
||||
import '../../components/search/search_result_body.dart';
|
||||
import '../../models/history/search.dart';
|
||||
import '../../models/history/history_entry.dart';
|
||||
import '../../services/jisho_api/jisho_search.dart';
|
||||
import '../../services/jisho_api/kanji_search.dart';
|
||||
|
||||
@ -33,14 +33,16 @@ class _ResultPageState extends State<ResultPage> {
|
||||
? fetchKanji(widget.searchTerm)
|
||||
: fetchJishoResults(widget.searchTerm),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const LoadingScreen();
|
||||
// TODO: provide proper error handling
|
||||
if (snapshot.hasError) return ErrorWidget(snapshot.error!);
|
||||
if (!snapshot.hasData) return const LoadingScreen();
|
||||
|
||||
if (!addedToDatabase) {
|
||||
addSearchToDatabase(
|
||||
searchTerm: widget.searchTerm,
|
||||
isKanji: widget.isKanji,
|
||||
);
|
||||
if (widget.isKanji) {
|
||||
HistoryEntry.insertKanji(kanji: widget.searchTerm);
|
||||
} else {
|
||||
HistoryEntry.insertWord(word: widget.searchTerm);
|
||||
}
|
||||
addedToDatabase = true;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:confirm_dialog/confirm_dialog.dart';
|
||||
@ -6,15 +5,13 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
import 'package:mdi/mdi.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sembast/sembast_io.dart';
|
||||
import 'package:sembast/utils/sembast_import_export.dart';
|
||||
|
||||
import '../bloc/theme/theme_bloc.dart';
|
||||
import '../components/common/denshi_jisho_background.dart';
|
||||
import '../models/history/search.dart';
|
||||
import '../data/database.dart';
|
||||
import '../data/export.dart';
|
||||
import '../data/import.dart';
|
||||
import '../routing/routes.dart';
|
||||
import '../services/database.dart';
|
||||
import '../services/open_webpage.dart';
|
||||
import '../services/snackbar.dart';
|
||||
import '../settings.dart';
|
||||
@ -27,22 +24,39 @@ class SettingsView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SettingsViewState extends State<SettingsView> {
|
||||
final Database db = GetIt.instance.get<Database>();
|
||||
bool dataExportIsLoading = false;
|
||||
bool dataImportIsLoading = false;
|
||||
|
||||
Future<void> clearHistory(context) async {
|
||||
final bool userIsSure = await confirm(context);
|
||||
final historyCount = (await db().query(
|
||||
TableNames.historyEntry,
|
||||
columns: ['COUNT(*) AS count'],
|
||||
))[0]['count']! as int;
|
||||
|
||||
if (userIsSure) {
|
||||
await Search.store.delete(db);
|
||||
}
|
||||
final bool userIsSure = await confirm(
|
||||
context,
|
||||
content: Text(
|
||||
'Are you sure that you want to delete $historyCount entries?',
|
||||
),
|
||||
);
|
||||
if (!userIsSure) return;
|
||||
|
||||
await db().delete(TableNames.historyEntry);
|
||||
showSnackbar(context, 'Cleared history');
|
||||
}
|
||||
|
||||
Future<void> clearAll(context) async {
|
||||
final bool userIsSure = await confirm(context);
|
||||
if (!userIsSure) return;
|
||||
|
||||
await resetDatabase();
|
||||
showSnackbar(context, 'Cleared everything');
|
||||
}
|
||||
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
void toggleAutoTheme(bool b) {
|
||||
final bool newThemeIsDark = b
|
||||
? WidgetsBinding.instance!.window.platformBrightness == Brightness.dark
|
||||
? WidgetsBinding.instance.window.platformBrightness == Brightness.dark
|
||||
: darkThemeEnabled;
|
||||
|
||||
BlocProvider.of<ThemeBloc>(context)
|
||||
@ -63,75 +77,19 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
}
|
||||
|
||||
/// Can assume Android for time being
|
||||
Future<void> exportData(context) async {
|
||||
Future<void> exportHandler(context) async {
|
||||
setState(() => dataExportIsLoading = true);
|
||||
|
||||
final path = (await getExternalStorageDirectory())!;
|
||||
final dbData = await exportDatabase(db);
|
||||
final file = File('${path.path}/jisho_data.json');
|
||||
file.createSync(recursive: true);
|
||||
await file.writeAsString(jsonEncode(dbData));
|
||||
|
||||
final path = await exportData();
|
||||
setState(() => dataExportIsLoading = false);
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text('Data exported to ${file.path}')));
|
||||
showSnackbar(context, 'Data exported to $path');
|
||||
}
|
||||
|
||||
/// Can assume Android for time being
|
||||
Future<void> importData(context) async {
|
||||
Future<void> importHandler(context) async {
|
||||
setState(() => dataImportIsLoading = true);
|
||||
|
||||
final path = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
);
|
||||
final file = File(path!.files[0].path!);
|
||||
|
||||
final List<Search> prevSearches = (await Search.store.find(db))
|
||||
.map((e) => Search.fromJson(e.value! as Map<String, Object?>))
|
||||
.toList();
|
||||
late final List<Search> importedSearches;
|
||||
try {
|
||||
importedSearches = ((((jsonDecode(await file.readAsString())
|
||||
as Map<String, Object?>)['stores']! as List)
|
||||
.map((e) => e as Map)
|
||||
.where((e) => e['name'] == 'search')
|
||||
.first)['values'] as List)
|
||||
.map((item) => Search.fromJson(item))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
showSnackbar(
|
||||
context,
|
||||
"Couldn't read file. Did you choose the right one?",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final List<Search> mergedSearches =
|
||||
mergeSearches(prevSearches, importedSearches);
|
||||
|
||||
// print(mergedSearches);
|
||||
|
||||
await GetIt.instance.get<Database>().close();
|
||||
GetIt.instance.unregister<Database>();
|
||||
|
||||
final importedDb = await importDatabase(
|
||||
{
|
||||
'sembast_export': 1,
|
||||
'version': 1,
|
||||
'stores': [
|
||||
{
|
||||
'name': 'search',
|
||||
'keys': [for (var i = 1; i <= mergedSearches.length; i++) i],
|
||||
'values': mergedSearches.map((e) => e.toJson()).toList(),
|
||||
}
|
||||
]
|
||||
},
|
||||
databaseFactoryIo,
|
||||
await databasePath(),
|
||||
);
|
||||
GetIt.instance.registerSingleton<Database>(importedDb);
|
||||
final path = await FilePicker.platform.getDirectoryPath();
|
||||
await importData(Directory(path!));
|
||||
|
||||
setState(() => dataImportIsLoading = false);
|
||||
showSnackbar(context, 'Data imported successfully');
|
||||
@ -288,7 +246,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
SettingsTile(
|
||||
leading: const Icon(Icons.file_upload),
|
||||
title: 'Import Data',
|
||||
onPressed: importData,
|
||||
onPressed: importHandler,
|
||||
enabled: Platform.isAndroid,
|
||||
subtitle:
|
||||
Platform.isAndroid ? null : 'Not available on iOS yet',
|
||||
@ -299,6 +257,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
SettingsTile(
|
||||
leading: const Icon(Icons.file_download),
|
||||
title: 'Export Data',
|
||||
onPressed: exportHandler,
|
||||
enabled: Platform.isAndroid,
|
||||
subtitle:
|
||||
Platform.isAndroid ? null : 'Not available on iOS yet',
|
||||
@ -318,7 +277,13 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
onPressed: (c) {},
|
||||
titleTextStyle: const TextStyle(color: Colors.red),
|
||||
enabled: false,
|
||||
)
|
||||
),
|
||||
SettingsTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: 'Clear Everything',
|
||||
onPressed: clearAll,
|
||||
titleTextStyle: const TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sembast/sembast.dart';
|
||||
import 'package:sembast/sembast_io.dart';
|
||||
|
||||
Future<String> databasePath() async {
|
||||
final Directory appDocDir = await getApplicationDocumentsDirectory();
|
||||
if (!appDocDir.existsSync()) appDocDir.createSync(recursive: true);
|
||||
return join(appDocDir.path, 'sembast.db');
|
||||
}
|
||||
|
||||
Future<void> setupDatabase() async {
|
||||
final Database database =
|
||||
await databaseFactoryIo.openDatabase(await databasePath());
|
||||
GetIt.instance.registerSingleton<Database>(database);
|
||||
}
|
191
pubspec.lock
191
pubspec.lock
@ -7,14 +7,14 @@ packages:
|
||||
name: _fe_analyzer_shared
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "31.0.0"
|
||||
version: "40.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.0"
|
||||
version: "4.1.0"
|
||||
animated_size_and_fade:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -35,7 +35,7 @@ packages:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -49,7 +49,7 @@ packages:
|
||||
name: audio_session
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.6+1"
|
||||
version: "0.1.7"
|
||||
bloc:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -91,14 +91,14 @@ packages:
|
||||
name: build_resolvers
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
version: "2.0.9"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.10"
|
||||
version: "2.1.11"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -119,7 +119,7 @@ packages:
|
||||
name: built_value
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.2.3"
|
||||
version: "8.3.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -141,13 +141,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.5"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -168,7 +161,7 @@ packages:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.16.0"
|
||||
confirm_dialog:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -182,14 +175,14 @@ packages:
|
||||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "1.3.2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -203,14 +196,14 @@ packages:
|
||||
name: csslib
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.17.1"
|
||||
version: "0.17.2"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.3"
|
||||
division:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -218,20 +211,27 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.0"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: equatable
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.2.1"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -245,14 +245,14 @@ packages:
|
||||
name: file_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
version: "4.6.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -278,14 +278,14 @@ packages:
|
||||
name: flutter_native_splash
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
version: "2.2.2"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
version: "2.0.6"
|
||||
flutter_settings_ui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -299,14 +299,14 @@ packages:
|
||||
name: flutter_slidable
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "1.1.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -323,7 +323,7 @@ packages:
|
||||
name: frontend_server_client
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.3"
|
||||
get_it:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -379,14 +379,14 @@ packages:
|
||||
name: http_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "4.0.1"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
version: "3.2.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -400,7 +400,7 @@ packages:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.3"
|
||||
version: "0.6.4"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -414,7 +414,7 @@ packages:
|
||||
name: just_audio
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.21"
|
||||
version: "0.9.24"
|
||||
just_audio_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -456,7 +456,7 @@ packages:
|
||||
name: material_color_utilities
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
version: "0.1.4"
|
||||
mdi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -505,7 +505,7 @@ packages:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
version: "1.8.1"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -526,56 +526,56 @@ packages:
|
||||
name: path_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.9"
|
||||
version: "2.0.10"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
version: "2.0.14"
|
||||
path_provider_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
version: "2.0.9"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.1.7"
|
||||
path_provider_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
version: "2.0.6"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.0.4"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
version: "2.0.7"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
version: "5.0.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -610,7 +610,7 @@ packages:
|
||||
name: provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
version: "6.0.3"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -631,21 +631,14 @@ packages:
|
||||
name: rxdart
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.27.3"
|
||||
sembast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sembast
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "0.27.4"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
version: "4.0.5"
|
||||
share_plus_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -659,63 +652,63 @@ packages:
|
||||
name: share_plus_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
share_plus_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
share_plus_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
version: "2.0.15"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.11"
|
||||
version: "2.0.12"
|
||||
shared_preferences_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
shared_preferences_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.0.4"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -729,14 +722,14 @@ packages:
|
||||
name: shared_preferences_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.0.4"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -771,7 +764,7 @@ packages:
|
||||
name: signature
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
version: "5.0.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -797,7 +790,35 @@ packages:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
version: "1.8.2"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2+1"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1+1"
|
||||
sqflite_common_ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite_common_ffi
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -846,21 +867,21 @@ packages:
|
||||
name: test
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.19.5"
|
||||
version: "1.21.1"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.8"
|
||||
version: "0.4.9"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.9"
|
||||
version: "0.4.13"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -874,7 +895,7 @@ packages:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.3.1"
|
||||
universal_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -888,42 +909,42 @@ packages:
|
||||
name: unofficial_jisho_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
version: "3.0.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
version: "6.1.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.16"
|
||||
version: "6.0.17"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.15"
|
||||
version: "6.0.17"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -937,14 +958,14 @@ packages:
|
||||
name: url_launcher_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.9"
|
||||
version: "2.0.11"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -958,14 +979,14 @@ packages:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "7.5.0"
|
||||
version: "8.3.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -986,14 +1007,14 @@ packages:
|
||||
name: webkit_inspection_protocol
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.5.2"
|
||||
version: "2.6.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1007,14 +1028,14 @@ packages:
|
||||
name: xml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.3.1"
|
||||
version: "6.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.1"
|
||||
sdks:
|
||||
dart: ">=2.16.0 <3.0.0"
|
||||
flutter: ">=2.10.0"
|
||||
dart: ">=2.17.0 <3.0.0"
|
||||
flutter: ">=2.11.0-0.1.pre"
|
||||
|
@ -23,11 +23,12 @@ dependencies:
|
||||
mdi: ^5.0.0-nullsafety.0
|
||||
path: ^1.8.0
|
||||
path_provider: ^2.0.2
|
||||
sembast: ^3.1.1
|
||||
share_plus: ^4.0.4
|
||||
test: ^1.19.5
|
||||
shared_preferences: ^2.0.6
|
||||
signature: ^5.0.0
|
||||
sqflite: ^2.0.2
|
||||
sqflite_common_ffi: ^2.1.1
|
||||
test: ^1.19.5
|
||||
unofficial_jisho_api: ^3.0.0
|
||||
url_launcher: ^6.0.9
|
||||
|
||||
@ -53,6 +54,7 @@ flutter:
|
||||
- assets/images/components/
|
||||
- assets/images/links/
|
||||
- assets/images/logo/
|
||||
- assets/migrations/
|
||||
- assets/licenses/
|
||||
|
||||
|
||||
|
@ -1,34 +1,6 @@
|
||||
import 'package:jisho_study_tool/models/history/kanji_query.dart';
|
||||
import 'package:jisho_study_tool/models/history/search.dart';
|
||||
import 'package:jisho_study_tool/models/history/word_query.dart';
|
||||
import 'package:jisho_study_tool/models/history/history_entry.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('Search', () {
|
||||
final List<Search> searches = [
|
||||
Search.fromKanjiQuery(kanjiQuery: KanjiQuery(kanji: '何')),
|
||||
Search.fromWordQuery(wordQuery: WordQuery(query: 'テスト')),
|
||||
Search.fromJson({'timestamps':[1648658269960],'lastTimestamp':1648658269960,'wordQuery':null,'kanjiQuery':{'kanji':'日'}}),
|
||||
Search.fromJson({'timestamps':[1648674967535],'lastTimestamp':1648674967535,'wordQuery':{'query':'黙る'},'kanjiQuery':null}),
|
||||
Search.fromJson({'timestamps':[1649079907766],'lastTimestamp':1649079907766,'wordQuery':{'query':'seal'},'kanjiQuery':null}),
|
||||
Search.fromJson({'timestamps':[1649082072981],'lastTimestamp':1649082072981,'wordQuery':{'query':'感涙屋'},'kanjiQuery':null}),
|
||||
Search.fromJson({'timestamps':[1644951726777,1644951732749],'lastTimestamp':1644951732749,'wordQuery':{'query':'呑める'},'kanjiQuery':null}),
|
||||
];
|
||||
test("mergeSearches with empty lists doesn't add data", () {
|
||||
final List<Search> merged1 = mergeSearches(searches, []);
|
||||
final List<Search> merged2 = mergeSearches([], searches);
|
||||
for (int i = 0; i < searches.length; i++) {
|
||||
expect(merged1[i], searches[i]);
|
||||
expect(merged2[i], searches[i]);
|
||||
}
|
||||
});
|
||||
|
||||
test("mergeSearches with the same list doesn't add data", () {
|
||||
final List<Search> merged = mergeSearches(searches, searches);
|
||||
for (int i = 0; i < searches.length; i++) {
|
||||
expect(merged[i], searches[i]);
|
||||
}
|
||||
expect(mergeSearches(searches, searches), searches);
|
||||
});
|
||||
});
|
||||
group('Search', () {});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user