Jisho-Study-Tool/lib/models/history/history_entry.dart

367 lines
11 KiB
Dart

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
/// - word?
/// - kanji?
factory HistoryEntry.fromDBMap(Map<String, Object?> dbObject) =>
dbObject['word'] != null
? HistoryEntry.withWord(
id: dbObject['entryId']! as int,
word: dbObject['word']! 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: 'word = ?',
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,
'word': 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: 'word = ?',
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,
'word': 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: 'word = ?',
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,
'word': 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<int> amountOfEntries() async {
final query = await db().query(
TableNames.historyEntry,
columns: ['COUNT(*) AS count'],
);
return query.first['count']! as int;
}
static Future<List<HistoryEntry>> get fromDB async =>
(await db().query(TableNames.historyEntryOrderedByTimestamp))
.map((e) => HistoryEntry.fromDBMap(e))
.toList();
Future<void> delete() =>
db().delete(TableNames.historyEntry, where: 'id = ?', whereArgs: [id]);
}