Add import export functionality

This commit is contained in:
Oystein Kristoffer Tveit 2022-05-04 23:01:53 +02:00
parent 7ad6540962
commit edd3d7c9a9
7 changed files with 388 additions and 69 deletions

View File

@ -86,3 +86,35 @@ Future<void> addSearchToDatabase({
.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;
}

View File

@ -1,13 +1,22 @@
import 'dart:convert';
import 'dart:io';
import 'package:confirm_dialog/confirm_dialog.dart';
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 '../routing/routes.dart';
import '../services/database.dart';
import '../services/open_webpage.dart';
import '../services/snackbar.dart';
import '../settings.dart';
class SettingsView extends StatefulWidget {
@ -19,6 +28,8 @@ 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);
@ -40,6 +51,92 @@ class _SettingsViewState extends State<SettingsView> {
setState(() => autoThemeEnabled = b);
}
Future<void> changeFont(context) async {
final int? i = await _chooseFromList(
list: [for (final font in JapaneseFont.values) font.name],
chosen: japaneseFont.index,
)(context);
if (i != null)
setState(() {
japaneseFont = JapaneseFont.values[i];
});
}
/// Can assume Android for time being
Future<void> exportData(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));
setState(() => dataExportIsLoading = false);
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Data exported to ${file.path}')));
}
/// Can assume Android for time being
Future<void> importData(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);
setState(() => dataImportIsLoading = false);
showSnackbar(context, 'Data imported successfully');
}
Future<int?> Function(BuildContext) _chooseFromList({
required List<String> list,
int? chosen,
@ -89,9 +186,7 @@ class _SettingsViewState extends State<SettingsView> {
SettingsTile.switchTile(
title: 'Use romaji',
leading: const Icon(Mdi.alphabetical),
onToggle: (b) {
setState(() => romajiEnabled = b);
},
onToggle: (b) => setState(() => romajiEnabled = b),
switchValue: romajiEnabled,
theme: theme,
switchActiveColor: AppTheme.jishoGreen.background,
@ -99,9 +194,7 @@ class _SettingsViewState extends State<SettingsView> {
SettingsTile.switchTile(
title: 'Extensive search',
leading: const Icon(Icons.downloading),
onToggle: (b) {
setState(() => extensiveSearchEnabled = b);
},
onToggle: (b) => setState(() => extensiveSearchEnabled = b),
switchValue: extensiveSearchEnabled,
theme: theme,
switchActiveColor: AppTheme.jishoGreen.background,
@ -114,18 +207,7 @@ class _SettingsViewState extends State<SettingsView> {
SettingsTile(
title: 'Japanese font',
leading: const Icon(Icons.format_size),
onPressed: (context) async {
final int? i = await _chooseFromList(
list: [
for (final font in JapaneseFont.values) font.name
],
chosen: japaneseFont.index,
)(context);
if (i != null)
setState(() {
japaneseFont = JapaneseFont.values[i];
});
},
onPressed: changeFont,
theme: theme,
trailing: Text(japaneseFont.name),
// subtitle:
@ -134,6 +216,7 @@ class _SettingsViewState extends State<SettingsView> {
),
],
),
SettingsSection(
title: 'Theme',
titleTextStyle: _titleTextStyle,
@ -161,6 +244,7 @@ class _SettingsViewState extends State<SettingsView> {
),
],
),
// TODO: This will be left commented until caching is implemented
// SettingsSection(
// title: 'Cache',
@ -196,14 +280,31 @@ class _SettingsViewState extends State<SettingsView> {
// ),
// ],
// ),
SettingsSection(
title: 'Data',
titleTextStyle: _titleTextStyle,
tiles: <SettingsTile>[
SettingsTile(
leading: const Icon(Icons.file_upload),
title: 'Import Data',
onPressed: importData,
enabled: Platform.isAndroid,
subtitle:
Platform.isAndroid ? null : 'Not available on iOS yet',
subtitleWidget: dataImportIsLoading
? const LinearProgressIndicator()
: null,
),
SettingsTile(
leading: const Icon(Icons.file_download),
title: 'Export Data',
enabled: false,
enabled: Platform.isAndroid,
subtitle:
Platform.isAndroid ? null : 'Not available on iOS yet',
subtitleWidget: dataExportIsLoading
? const LinearProgressIndicator()
: null,
),
SettingsTile(
leading: const Icon(Icons.delete),
@ -220,6 +321,7 @@ class _SettingsViewState extends State<SettingsView> {
)
],
),
SettingsSection(
title: 'Info',
titleTextStyle: _titleTextStyle,

View File

@ -6,10 +6,14 @@ import 'package:path_provider/path_provider.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';
Future<void> setupDatabase() async {
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(join(appDocDir.path, 'sembast.db'));
await databaseFactoryIo.openDatabase(await databasePath());
GetIt.instance.registerSingleton<Database>(database);
}

View File

@ -0,0 +1,4 @@
import 'package:flutter/material.dart';
void showSnackbar(BuildContext context, String text) =>
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));

View File

@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "33.0.0"
version: "31.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
version: "2.8.0"
animated_size_and_fade:
dependency: "direct main"
description:
@ -28,7 +28,7 @@ packages:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.8"
version: "3.3.0"
args:
dependency: transitive
description:
@ -56,7 +56,7 @@ packages:
name: bloc
url: "https://pub.dartlang.org"
source: hosted
version: "8.0.2"
version: "8.0.3"
boolean_selector:
dependency: transitive
description:
@ -70,7 +70,7 @@ packages:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
version: "2.3.0"
build_config:
dependency: transitive
description:
@ -84,7 +84,7 @@ packages:
name: build_daemon
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.1.0"
build_resolvers:
dependency: transitive
description:
@ -98,7 +98,7 @@ packages:
name: build_runner
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.7"
version: "2.1.10"
build_runner_core:
dependency: transitive
description:
@ -119,7 +119,7 @@ packages:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "8.1.3"
version: "8.2.3"
characters:
dependency: transitive
description:
@ -175,7 +175,7 @@ packages:
name: confirm_dialog
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.0.1"
convert:
dependency: transitive
description:
@ -183,13 +183,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
coverage:
dependency: transitive
description:
name: coverage
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.0.2"
csslib:
dependency: transitive
description:
@ -232,6 +239,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.2"
file_picker:
dependency: "direct main"
description:
name: file_picker
url: "https://pub.dartlang.org"
source: hosted
version: "4.5.1"
fixnum:
dependency: transitive
description:
@ -264,7 +278,14 @@ packages:
name: flutter_native_splash
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.3"
version: "2.1.6"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
flutter_settings_ui:
dependency: "direct main"
description:
@ -351,7 +372,7 @@ packages:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.2.0"
http_parser:
dependency: transitive
description:
@ -365,7 +386,7 @@ packages:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
version: "3.1.3"
io:
dependency: transitive
description:
@ -386,28 +407,35 @@ packages:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "4.4.0"
version: "4.5.0"
just_audio:
dependency: "direct main"
description:
name: just_audio
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.18"
version: "0.9.21"
just_audio_platform_interface:
dependency: transitive
description:
name: just_audio_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
version: "4.1.0"
just_audio_web:
dependency: transitive
description:
name: just_audio_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.3"
version: "0.4.7"
lint:
dependency: transitive
description:
name: lint
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
logging:
dependency: transitive
description:
@ -422,6 +450,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.11"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
mdi:
dependency: "direct main"
description:
@ -442,7 +477,7 @@ packages:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
version: "1.0.2"
nested:
dependency: transitive
description:
@ -450,6 +485,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
package_config:
dependency: transitive
description:
@ -484,21 +526,21 @@ packages:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.0.9"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
version: "2.0.13"
path_provider_ios:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
version: "2.0.8"
path_provider_linux:
dependency: transitive
description:
@ -575,7 +617,7 @@ packages:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.1"
pubspec_parse:
dependency: transitive
description:
@ -596,42 +638,84 @@ packages:
name: sembast
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.1+1"
version: "3.2.0"
share_plus:
dependency: "direct main"
description:
name: share_plus
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.4"
share_plus_linux:
dependency: transitive
description:
name: share_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
share_plus_macos:
dependency: transitive
description:
name: share_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
share_plus_web:
dependency: transitive
description:
name: share_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
share_plus_windows:
dependency: transitive
description:
name: share_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.12"
version: "2.0.13"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.10"
version: "2.0.11"
shared_preferences_ios:
dependency: transitive
description:
name: shared_preferences_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.9"
version: "2.1.0"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.1.0"
shared_preferences_macos:
dependency: transitive
description:
name: shared_preferences_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.0.3"
shared_preferences_platform_interface:
dependency: transitive
description:
@ -652,14 +736,28 @@ packages:
name: shared_preferences_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.1.0"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.3.0"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
shelf_static:
dependency: transitive
description:
name: shelf_static
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
shelf_web_socket:
dependency: transitive
description:
@ -679,6 +777,20 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
source_maps:
dependency: transitive
description:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.10"
source_span:
dependency: transitive
description:
@ -720,7 +832,7 @@ packages:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
version: "3.0.0+2"
term_glyph:
dependency: transitive
description:
@ -728,13 +840,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
test:
dependency: "direct main"
description:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.19.5"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.3"
version: "0.4.8"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.9"
timing:
dependency: transitive
description:
@ -769,35 +895,35 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.18"
version: "6.1.0"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.14"
version: "6.0.16"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.14"
version: "6.0.15"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "3.0.0"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "3.0.0"
url_launcher_platform_interface:
dependency: transitive
description:
@ -811,21 +937,21 @@ packages:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
version: "2.0.9"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "3.0.0"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.5"
version: "3.0.6"
vector_math:
dependency: transitive
description:
@ -833,6 +959,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
vm_service:
dependency: transitive
description:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "7.5.0"
watcher:
dependency: transitive
description:
@ -846,21 +979,28 @@ packages:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.2.0"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.3"
version: "2.5.2"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
version: "0.2.0+1"
xml:
dependency: transitive
description:
@ -876,5 +1016,5 @@ packages:
source: hosted
version: "3.1.0"
sdks:
dart: ">=2.15.1 <3.0.0"
flutter: ">=2.5.0"
dart: ">=2.16.0 <3.0.0"
flutter: ">=2.10.0"

View File

@ -10,9 +10,11 @@ dependencies:
collection: ^1.15.0
confirm_dialog: ^1.0.0
division: ^0.9.0
file_picker: ^4.5.1
flutter:
sdk: flutter
flutter_bloc: ^8.0.0
flutter_settings_ui: ^2.0.1
flutter_slidable: ^1.1.0
flutter_svg: ^1.0.2
get_it: ^7.2.0
@ -22,7 +24,8 @@ dependencies:
path: ^1.8.0
path_provider: ^2.0.2
sembast: ^3.1.1
flutter_settings_ui: ^2.0.1
share_plus: ^4.0.4
test: ^1.19.5
shared_preferences: ^2.0.6
signature: ^5.0.0
unofficial_jisho_api: ^2.0.4
@ -32,8 +35,8 @@ dev_dependencies:
build_runner: ^2.0.6
flutter_test:
sdk: flutter
flutter_native_splash: ^1.2.0
flutter_launcher_icons: "^0.9.1"
flutter_native_splash: ^2.1.6
flutter_launcher_icons: "^0.9.2"
flutter_icons:
android: "launcher_icon"

View File

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