feat: Implement importing/exporting data
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:anitrack/src/data/manga.dart';
|
||||
import 'package:anitrack/src/data/type.dart';
|
||||
import 'package:anitrack/src/service/database.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
@@ -63,7 +64,16 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
||||
await GetIt.I.get<DatabaseService>().addAnime(event.data);
|
||||
|
||||
// Add it to the cache
|
||||
_animes.add(event.data);
|
||||
if (event.checkIfExists) {
|
||||
final shouldAdd =
|
||||
_animes.firstWhereOrNull((element) => element.id == event.data.id) ==
|
||||
null;
|
||||
if (shouldAdd) {
|
||||
_animes.add(event.data);
|
||||
}
|
||||
} else {
|
||||
_animes.add(event.data);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -80,7 +90,17 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
||||
await GetIt.I.get<DatabaseService>().addManga(event.data);
|
||||
|
||||
// Add it to the cache
|
||||
_mangas.add(event.data);
|
||||
// Add it to the cache
|
||||
if (event.checkIfExists) {
|
||||
final shouldAdd =
|
||||
_mangas.firstWhereOrNull((element) => element.id == event.data.id) ==
|
||||
null;
|
||||
if (shouldAdd) {
|
||||
_mangas.add(event.data);
|
||||
}
|
||||
} else {
|
||||
_mangas.add(event.data);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
||||
@@ -17,10 +17,14 @@ class AnimeEpisodeDecrementedEvent extends AnimeListEvent {
|
||||
}
|
||||
|
||||
class AnimeAddedEvent extends AnimeListEvent {
|
||||
AnimeAddedEvent(this.data);
|
||||
AnimeAddedEvent(this.data, {this.checkIfExists = false});
|
||||
|
||||
/// The anime to add.
|
||||
final AnimeTrackingData data;
|
||||
|
||||
/// If true, checks if the anime with the id is already in the list.
|
||||
/// If it is, does nothing.
|
||||
final bool checkIfExists;
|
||||
}
|
||||
|
||||
/// Triggered when animes are to be loaded from the database
|
||||
@@ -59,10 +63,14 @@ class AnimeRemovedEvent extends AnimeListEvent {
|
||||
}
|
||||
|
||||
class MangaAddedEvent extends AnimeListEvent {
|
||||
MangaAddedEvent(this.data);
|
||||
MangaAddedEvent(this.data, {this.checkIfExists = false});
|
||||
|
||||
/// The manga to add.
|
||||
final MangaTrackingData data;
|
||||
|
||||
/// If true, checks if the manga with the id is already in the list.
|
||||
/// If it is, does nothing.
|
||||
final bool checkIfExists;
|
||||
}
|
||||
|
||||
/// Triggered when the manga filter is changed
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:anitrack/i18n/strings.g.dart';
|
||||
import 'package:anitrack/src/data/anime.dart';
|
||||
import 'package:anitrack/src/data/manga.dart';
|
||||
import 'package:anitrack/src/data/type.dart';
|
||||
import 'package:anitrack/src/service/database.dart';
|
||||
import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
|
||||
import 'package:archive/archive.dart' as archive;
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:jikan_api/jikan_api.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
part 'settings_state.dart';
|
||||
part 'settings_event.dart';
|
||||
part 'settings_bloc.freezed.dart';
|
||||
part 'settings_event.dart';
|
||||
part 'settings_state.dart';
|
||||
|
||||
MediumTrackingState malStatusToTrackingState(String status) {
|
||||
switch (status) {
|
||||
@@ -39,6 +45,8 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
||||
SettingsBloc() : super(SettingsState()) {
|
||||
on<AnimeListImportedEvent>(_onAnimeListImported);
|
||||
on<MangaListImportedEvent>(_onMangaListImported);
|
||||
on<DataExportedEvent>(_onDataExported);
|
||||
on<DataImportedEvent>(_onDataImported);
|
||||
}
|
||||
|
||||
void _showLoadingSpinner(Emitter<SettingsState> emit) {
|
||||
@@ -205,4 +213,69 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
||||
// Hide the spinner again
|
||||
_hideLoadingSpinner(emit);
|
||||
}
|
||||
|
||||
Future<void> _onDataExported(
|
||||
DataExportedEvent event,
|
||||
Emitter<SettingsState> emit,
|
||||
) async {
|
||||
final al = GetIt.I.get<AnimeListBloc>();
|
||||
final data = {
|
||||
// TODO(Unknown): Track the version here to (maybe) to migrations
|
||||
'animes': al.state.animes.map((anime) => anime.toJson()).toList(),
|
||||
'mangas': al.state.mangas.map((manga) => manga.toJson()).toList(),
|
||||
};
|
||||
final exportData = jsonEncode(data);
|
||||
final date = DateTime.now();
|
||||
final outputPath = path.join(
|
||||
event.path,
|
||||
'anitrack_${date.year}${date.month}${date.day}.json.gz',
|
||||
);
|
||||
archive.GZipEncoder().encode(
|
||||
InputStream(utf8.encode(exportData)),
|
||||
output: OutputFileStream(outputPath),
|
||||
);
|
||||
|
||||
await Fluttertoast.showToast(
|
||||
msg: t.settings.dataExportSuccess,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onDataImported(
|
||||
DataImportedEvent event,
|
||||
Emitter<SettingsState> emit,
|
||||
) async {
|
||||
final al = GetIt.I.get<AnimeListBloc>();
|
||||
final exportArchive = archive.GZipDecoder().decodeBytes(
|
||||
await File(event.path).readAsBytes(),
|
||||
);
|
||||
final json = jsonDecode(utf8.decode(exportArchive)) as Map<String, dynamic>;
|
||||
|
||||
// Process anime
|
||||
for (final animeRaw
|
||||
in (json['animes']! as List<dynamic>).cast<Map<dynamic, dynamic>>()) {
|
||||
final anime = AnimeTrackingData.fromJson(
|
||||
animeRaw.cast<String, dynamic>(),
|
||||
);
|
||||
|
||||
al.add(
|
||||
AnimeAddedEvent(anime, checkIfExists: true),
|
||||
);
|
||||
}
|
||||
|
||||
// Process manga
|
||||
for (final mangaRaw
|
||||
in (json['mangas']! as List<dynamic>).cast<Map<dynamic, dynamic>>()) {
|
||||
final manga = MangaTrackingData.fromJson(
|
||||
mangaRaw.cast<String, dynamic>(),
|
||||
);
|
||||
|
||||
al.add(
|
||||
MangaAddedEvent(manga, checkIfExists: true),
|
||||
);
|
||||
}
|
||||
|
||||
await Fluttertoast.showToast(
|
||||
msg: t.settings.dataImportSuccess,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,3 +34,19 @@ class MangaListImportedEvent extends SettingsEvent {
|
||||
/// The type of list we're importing
|
||||
final ImportListType type;
|
||||
}
|
||||
|
||||
/// Triggered when a data export should be produced.
|
||||
class DataExportedEvent extends SettingsEvent {
|
||||
DataExportedEvent(this.path);
|
||||
|
||||
/// The path where the export should be stored.
|
||||
final String path;
|
||||
}
|
||||
|
||||
/// Triggered when a data export has been picked for import.
|
||||
class DataImportedEvent extends SettingsEvent {
|
||||
DataImportedEvent(this.path);
|
||||
|
||||
/// The path of the data export to import.
|
||||
final String path;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user