feat: Implement importing/exporting data
This commit is contained in:
parent
9fed2116b1
commit
918e42b424
@ -33,4 +33,5 @@
|
|||||||
</application>
|
</application>
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -9,7 +9,15 @@
|
|||||||
"importMangaDesc": "Import manga list exported from MyAnimeList.",
|
"importMangaDesc": "Import manga list exported from MyAnimeList.",
|
||||||
"invalidMangaListTitle": "Invalid manga list",
|
"invalidMangaListTitle": "Invalid manga list",
|
||||||
"invalidMangaListBody": "The selected file is not a MAL manga list. It lacks the \".xml.gz\" suffix.",
|
"invalidMangaListBody": "The selected file is not a MAL manga list. It lacks the \".xml.gz\" suffix.",
|
||||||
"importIndicator": "$current of $total"
|
"importIndicator": "$current of $total",
|
||||||
|
"exportData": "Export data",
|
||||||
|
"importData": "Import data",
|
||||||
|
"dataExportSuccess": "Data successfully exported",
|
||||||
|
"dataImportSuccess": "Data successfully imported",
|
||||||
|
"importInvalidData": {
|
||||||
|
"title": "Invalid AniTrack Data",
|
||||||
|
"content": "The selected file is not an AniTrack data export. It lacks the \".json.gz\" suffix."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"title": "About",
|
"title": "About",
|
||||||
|
@ -10,62 +10,46 @@ enum TrackingMediumType {
|
|||||||
/// The state of the medium we're tracking, i.e. reading/watching, dropped, ...
|
/// The state of the medium we're tracking, i.e. reading/watching, dropped, ...
|
||||||
enum MediumTrackingState {
|
enum MediumTrackingState {
|
||||||
/// Currently watching or reading
|
/// Currently watching or reading
|
||||||
ongoing,
|
ongoing(0),
|
||||||
|
|
||||||
/// Done
|
/// Done
|
||||||
completed,
|
completed(1),
|
||||||
|
|
||||||
/// Plan to watch or read
|
/// Plan to watch or read
|
||||||
planned,
|
planned(2),
|
||||||
|
|
||||||
/// Dropped
|
/// Dropped
|
||||||
dropped,
|
dropped(3),
|
||||||
|
|
||||||
/// Paused
|
/// Paused
|
||||||
paused,
|
paused(4),
|
||||||
|
|
||||||
/// Meta state
|
/// Meta state
|
||||||
all,
|
all(-1);
|
||||||
}
|
|
||||||
|
|
||||||
/// Interface for the Anime and Manga data classes
|
const MediumTrackingState(this.id);
|
||||||
abstract class TrackingMedium {
|
|
||||||
/// The ID of the medium
|
|
||||||
final String id = '';
|
|
||||||
|
|
||||||
/// The title of the medium
|
factory MediumTrackingState.fromInt(int id) {
|
||||||
final String title = '';
|
switch (id) {
|
||||||
|
case 0:
|
||||||
/// The URL of the cover image.
|
return MediumTrackingState.ongoing;
|
||||||
final String thumbnailUrl = '';
|
case 1:
|
||||||
|
return MediumTrackingState.completed;
|
||||||
/// The tracking state
|
case 2:
|
||||||
final MediumTrackingState state = MediumTrackingState.planned;
|
return MediumTrackingState.planned;
|
||||||
}
|
case 3:
|
||||||
|
return MediumTrackingState.dropped;
|
||||||
extension MediumStateExtension on MediumTrackingState {
|
case 4:
|
||||||
int toInteger() {
|
return MediumTrackingState.paused;
|
||||||
assert(
|
|
||||||
this != MediumTrackingState.all,
|
|
||||||
'MediumTrackingState.all must not be serialized',
|
|
||||||
);
|
|
||||||
switch (this) {
|
|
||||||
case MediumTrackingState.ongoing:
|
|
||||||
return 0;
|
|
||||||
case MediumTrackingState.completed:
|
|
||||||
return 1;
|
|
||||||
case MediumTrackingState.planned:
|
|
||||||
return 2;
|
|
||||||
case MediumTrackingState.dropped:
|
|
||||||
return 3;
|
|
||||||
case MediumTrackingState.paused:
|
|
||||||
return 4;
|
|
||||||
case MediumTrackingState.all:
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String toNameString(TrackingMediumType type) {
|
return MediumTrackingState.planned;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The id of the value.
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
String getName(TrackingMediumType type) {
|
||||||
assert(
|
assert(
|
||||||
this != MediumTrackingState.all,
|
this != MediumTrackingState.all,
|
||||||
'MediumTrackingState.all must not be stringified',
|
'MediumTrackingState.all must not be stringified',
|
||||||
@ -98,28 +82,28 @@ extension MediumStateExtension on MediumTrackingState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Interface for the Anime and Manga data classes
|
||||||
|
abstract class TrackingMedium {
|
||||||
|
/// The ID of the medium
|
||||||
|
final String id = '';
|
||||||
|
|
||||||
|
/// The title of the medium
|
||||||
|
final String title = '';
|
||||||
|
|
||||||
|
/// The URL of the cover image.
|
||||||
|
final String thumbnailUrl = '';
|
||||||
|
|
||||||
|
/// The tracking state
|
||||||
|
final MediumTrackingState state = MediumTrackingState.planned;
|
||||||
|
}
|
||||||
|
|
||||||
class MediumTrackingStateConverter
|
class MediumTrackingStateConverter
|
||||||
implements JsonConverter<MediumTrackingState, int> {
|
implements JsonConverter<MediumTrackingState, int> {
|
||||||
const MediumTrackingStateConverter();
|
const MediumTrackingStateConverter();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MediumTrackingState fromJson(int json) {
|
MediumTrackingState fromJson(int json) => MediumTrackingState.fromInt(json);
|
||||||
switch (json) {
|
|
||||||
case 0:
|
|
||||||
return MediumTrackingState.ongoing;
|
|
||||||
case 1:
|
|
||||||
return MediumTrackingState.completed;
|
|
||||||
case 2:
|
|
||||||
return MediumTrackingState.planned;
|
|
||||||
case 3:
|
|
||||||
return MediumTrackingState.dropped;
|
|
||||||
case 4:
|
|
||||||
return MediumTrackingState.paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MediumTrackingState.planned;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int toJson(MediumTrackingState state) => state.toInteger();
|
int toJson(MediumTrackingState state) => state.id;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import 'package:anitrack/src/data/manga.dart';
|
|||||||
import 'package:anitrack/src/data/type.dart';
|
import 'package:anitrack/src/data/type.dart';
|
||||||
import 'package:anitrack/src/service/database.dart';
|
import 'package:anitrack/src/service/database.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:get_it/get_it.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);
|
await GetIt.I.get<DatabaseService>().addAnime(event.data);
|
||||||
|
|
||||||
// Add it to the cache
|
// Add it to the cache
|
||||||
|
if (event.checkIfExists) {
|
||||||
|
final shouldAdd =
|
||||||
|
_animes.firstWhereOrNull((element) => element.id == event.data.id) ==
|
||||||
|
null;
|
||||||
|
if (shouldAdd) {
|
||||||
_animes.add(event.data);
|
_animes.add(event.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_animes.add(event.data);
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -80,7 +90,17 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
await GetIt.I.get<DatabaseService>().addManga(event.data);
|
await GetIt.I.get<DatabaseService>().addManga(event.data);
|
||||||
|
|
||||||
// Add it to the cache
|
// Add it to the cache
|
||||||
|
// 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);
|
_mangas.add(event.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_mangas.add(event.data);
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
|
@ -17,10 +17,14 @@ class AnimeEpisodeDecrementedEvent extends AnimeListEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AnimeAddedEvent extends AnimeListEvent {
|
class AnimeAddedEvent extends AnimeListEvent {
|
||||||
AnimeAddedEvent(this.data);
|
AnimeAddedEvent(this.data, {this.checkIfExists = false});
|
||||||
|
|
||||||
/// The anime to add.
|
/// The anime to add.
|
||||||
final AnimeTrackingData data;
|
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
|
/// Triggered when animes are to be loaded from the database
|
||||||
@ -59,10 +63,14 @@ class AnimeRemovedEvent extends AnimeListEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MangaAddedEvent extends AnimeListEvent {
|
class MangaAddedEvent extends AnimeListEvent {
|
||||||
MangaAddedEvent(this.data);
|
MangaAddedEvent(this.data, {this.checkIfExists = false});
|
||||||
|
|
||||||
/// The manga to add.
|
/// The manga to add.
|
||||||
final MangaTrackingData data;
|
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
|
/// Triggered when the manga filter is changed
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
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/anime.dart';
|
||||||
import 'package:anitrack/src/data/manga.dart';
|
import 'package:anitrack/src/data/manga.dart';
|
||||||
import 'package:anitrack/src/data/type.dart';
|
import 'package:anitrack/src/data/type.dart';
|
||||||
import 'package:anitrack/src/service/database.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:archive/archive_io.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:jikan_api/jikan_api.dart';
|
import 'package:jikan_api/jikan_api.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:xml/xml.dart';
|
import 'package:xml/xml.dart';
|
||||||
|
|
||||||
part 'settings_state.dart';
|
|
||||||
part 'settings_event.dart';
|
|
||||||
part 'settings_bloc.freezed.dart';
|
part 'settings_bloc.freezed.dart';
|
||||||
|
part 'settings_event.dart';
|
||||||
|
part 'settings_state.dart';
|
||||||
|
|
||||||
MediumTrackingState malStatusToTrackingState(String status) {
|
MediumTrackingState malStatusToTrackingState(String status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@ -39,6 +45,8 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
|||||||
SettingsBloc() : super(SettingsState()) {
|
SettingsBloc() : super(SettingsState()) {
|
||||||
on<AnimeListImportedEvent>(_onAnimeListImported);
|
on<AnimeListImportedEvent>(_onAnimeListImported);
|
||||||
on<MangaListImportedEvent>(_onMangaListImported);
|
on<MangaListImportedEvent>(_onMangaListImported);
|
||||||
|
on<DataExportedEvent>(_onDataExported);
|
||||||
|
on<DataImportedEvent>(_onDataImported);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showLoadingSpinner(Emitter<SettingsState> emit) {
|
void _showLoadingSpinner(Emitter<SettingsState> emit) {
|
||||||
@ -205,4 +213,69 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
|||||||
// Hide the spinner again
|
// Hide the spinner again
|
||||||
_hideLoadingSpinner(emit);
|
_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
|
/// The type of list we're importing
|
||||||
final ImportListType type;
|
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;
|
||||||
|
}
|
||||||
|
@ -72,23 +72,23 @@ class AnimeListPageState extends State<AnimeListPage> {
|
|||||||
return [
|
return [
|
||||||
PopupMenuItem<MediumTrackingState>(
|
PopupMenuItem<MediumTrackingState>(
|
||||||
value: MediumTrackingState.ongoing,
|
value: MediumTrackingState.ongoing,
|
||||||
child: Text(MediumTrackingState.ongoing.toNameString(type)),
|
child: Text(MediumTrackingState.ongoing.getName(type)),
|
||||||
),
|
),
|
||||||
PopupMenuItem<MediumTrackingState>(
|
PopupMenuItem<MediumTrackingState>(
|
||||||
value: MediumTrackingState.completed,
|
value: MediumTrackingState.completed,
|
||||||
child: Text(MediumTrackingState.completed.toNameString(type)),
|
child: Text(MediumTrackingState.completed.getName(type)),
|
||||||
),
|
),
|
||||||
PopupMenuItem<MediumTrackingState>(
|
PopupMenuItem<MediumTrackingState>(
|
||||||
value: MediumTrackingState.planned,
|
value: MediumTrackingState.planned,
|
||||||
child: Text(MediumTrackingState.planned.toNameString(type)),
|
child: Text(MediumTrackingState.planned.getName(type)),
|
||||||
),
|
),
|
||||||
PopupMenuItem<MediumTrackingState>(
|
PopupMenuItem<MediumTrackingState>(
|
||||||
value: MediumTrackingState.dropped,
|
value: MediumTrackingState.dropped,
|
||||||
child: Text(MediumTrackingState.dropped.toNameString(type)),
|
child: Text(MediumTrackingState.dropped.getName(type)),
|
||||||
),
|
),
|
||||||
PopupMenuItem<MediumTrackingState>(
|
PopupMenuItem<MediumTrackingState>(
|
||||||
value: MediumTrackingState.paused,
|
value: MediumTrackingState.paused,
|
||||||
child: Text(MediumTrackingState.paused.toNameString(type)),
|
child: Text(MediumTrackingState.paused.getName(type)),
|
||||||
),
|
),
|
||||||
const PopupMenuItem<MediumTrackingState>(
|
const PopupMenuItem<MediumTrackingState>(
|
||||||
value: MediumTrackingState.all,
|
value: MediumTrackingState.all,
|
||||||
|
@ -158,27 +158,27 @@ class DetailsPage extends StatelessWidget {
|
|||||||
SelectorItem(
|
SelectorItem(
|
||||||
MediumTrackingState.ongoing,
|
MediumTrackingState.ongoing,
|
||||||
MediumTrackingState.ongoing
|
MediumTrackingState.ongoing
|
||||||
.toNameString(state.trackingType),
|
.getName(state.trackingType),
|
||||||
),
|
),
|
||||||
SelectorItem(
|
SelectorItem(
|
||||||
MediumTrackingState.completed,
|
MediumTrackingState.completed,
|
||||||
MediumTrackingState.completed
|
MediumTrackingState.completed
|
||||||
.toNameString(state.trackingType),
|
.getName(state.trackingType),
|
||||||
),
|
),
|
||||||
SelectorItem(
|
SelectorItem(
|
||||||
MediumTrackingState.planned,
|
MediumTrackingState.planned,
|
||||||
MediumTrackingState.planned
|
MediumTrackingState.planned
|
||||||
.toNameString(state.trackingType),
|
.getName(state.trackingType),
|
||||||
),
|
),
|
||||||
SelectorItem(
|
SelectorItem(
|
||||||
MediumTrackingState.dropped,
|
MediumTrackingState.dropped,
|
||||||
MediumTrackingState.dropped
|
MediumTrackingState.dropped
|
||||||
.toNameString(state.trackingType),
|
.getName(state.trackingType),
|
||||||
),
|
),
|
||||||
SelectorItem(
|
SelectorItem(
|
||||||
MediumTrackingState.paused,
|
MediumTrackingState.paused,
|
||||||
MediumTrackingState.paused
|
MediumTrackingState.paused
|
||||||
.toNameString(state.trackingType),
|
.getName(state.trackingType),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
initialValue: state.data!.state,
|
initialValue: state.data!.state,
|
||||||
|
@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
const SettingsPage({super.key});
|
const SettingsPage({super.key});
|
||||||
@ -91,6 +92,51 @@ class SettingsPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(t.settings.exportData),
|
||||||
|
onTap: () async {
|
||||||
|
// Pick the file
|
||||||
|
final result =
|
||||||
|
await FilePicker.platform.getDirectoryPath();
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
if (!(await Permission.manageExternalStorage
|
||||||
|
.request())
|
||||||
|
.isGranted) return;
|
||||||
|
|
||||||
|
GetIt.I.get<SettingsBloc>().add(
|
||||||
|
DataExportedEvent(
|
||||||
|
result,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(t.settings.importData),
|
||||||
|
onTap: () async {
|
||||||
|
// Pick the file
|
||||||
|
final result = await FilePicker.platform.pickFiles();
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
if (!result.files.first.path!.endsWith('.json.gz')) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: Text(t.settings.importInvalidData.title),
|
||||||
|
content:
|
||||||
|
Text(t.settings.importInvalidData.content),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetIt.I.get<SettingsBloc>().add(
|
||||||
|
DataImportedEvent(
|
||||||
|
result.files.first.path!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
50
pubspec.lock
50
pubspec.lock
@ -360,6 +360,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
fluttertoast:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: fluttertoast
|
||||||
|
sha256: "474f7d506230897a3cd28c965ec21c5328ae5605fc9c400cd330e9e9d6ac175c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.2.2"
|
||||||
freezed:
|
freezed:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -561,7 +569,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
|
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
|
||||||
@ -624,6 +632,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.1"
|
version: "1.11.1"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.4.3"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: c0c9754479a4c4b1c1f3862ddc11930c9b3f03bef2816bb4ea6eed1e13551d6f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.3.2"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.1.4"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.11.3"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -18,10 +18,13 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_bloc: ^8.1.1
|
flutter_bloc: ^8.1.1
|
||||||
|
fluttertoast: ^8.2.2
|
||||||
freezed_annotation: 2.1.0
|
freezed_annotation: 2.1.0
|
||||||
get_it: ^7.2.0
|
get_it: ^7.2.0
|
||||||
jikan_api: ^2.0.0
|
jikan_api: ^2.0.0
|
||||||
json_annotation: 4.6.0
|
json_annotation: 4.6.0
|
||||||
|
path: ^1.8.2
|
||||||
|
permission_handler: ^10.4.3
|
||||||
slang: 3.19.0
|
slang: 3.19.0
|
||||||
slang_flutter: 3.19.0
|
slang_flutter: 3.19.0
|
||||||
sqflite: ^2.2.4+1
|
sqflite: ^2.2.4+1
|
||||||
|
Loading…
Reference in New Issue
Block a user