feat(all): Implement MAL import for anime and manga lists

This commit is contained in:
PapaTutuWawa 2023-04-12 23:04:28 +02:00
parent d407a90724
commit 7530fe5b80
15 changed files with 689 additions and 12 deletions

View File

@ -3,11 +3,13 @@ import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart';
import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart'; import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart';
import 'package:anitrack/src/ui/bloc/details_bloc.dart'; import 'package:anitrack/src/ui/bloc/details_bloc.dart';
import 'package:anitrack/src/ui/bloc/navigation_bloc.dart'; import 'package:anitrack/src/ui/bloc/navigation_bloc.dart';
import 'package:anitrack/src/ui/bloc/settings_bloc.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
import 'package:anitrack/src/ui/pages/about.dart'; import 'package:anitrack/src/ui/pages/about.dart';
import 'package:anitrack/src/ui/pages/anime_list.dart'; import 'package:anitrack/src/ui/pages/anime_list.dart';
import 'package:anitrack/src/ui/pages/anime_search.dart'; import 'package:anitrack/src/ui/pages/anime_search.dart';
import 'package:anitrack/src/ui/pages/details.dart'; import 'package:anitrack/src/ui/pages/details.dart';
import 'package:anitrack/src/ui/pages/settings.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';
@ -28,6 +30,7 @@ void main() async {
GetIt.I.registerSingleton<AnimeSearchBloc>(AnimeSearchBloc()); GetIt.I.registerSingleton<AnimeSearchBloc>(AnimeSearchBloc());
GetIt.I.registerSingleton<DetailsBloc>(DetailsBloc()); GetIt.I.registerSingleton<DetailsBloc>(DetailsBloc());
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navKey)); GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navKey));
GetIt.I.registerSingleton<SettingsBloc>(SettingsBloc());
// Load animes // Load animes
GetIt.I.get<AnimeListBloc>().add( GetIt.I.get<AnimeListBloc>().add(
@ -49,6 +52,9 @@ void main() async {
BlocProvider<NavigationBloc>( BlocProvider<NavigationBloc>(
create: (_) => GetIt.I.get<NavigationBloc>(), create: (_) => GetIt.I.get<NavigationBloc>(),
), ),
BlocProvider<SettingsBloc>(
create: (_) => GetIt.I.get<SettingsBloc>(),
),
], ],
child: MyApp(navKey), child: MyApp(navKey),
), ),
@ -89,6 +95,8 @@ class MyApp extends StatelessWidget {
return DetailsPage.route; return DetailsPage.route;
case aboutRoute: case aboutRoute:
return AboutPage.route; return AboutPage.route;
case settingsRoute:
return SettingsPage.route;
} }
return null; return null;

View File

@ -8,10 +8,22 @@ 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
ongoing, ongoing,
/// Done
completed, completed,
/// Plan to watch or read
planned, planned,
/// Dropped
dropped, dropped,
/// Paused
paused,
/// Meta state
all, all,
} }
@ -45,6 +57,8 @@ extension MediumStateExtension on MediumTrackingState {
return 2; return 2;
case MediumTrackingState.dropped: case MediumTrackingState.dropped:
return 3; return 3;
case MediumTrackingState.paused:
return 4;
case MediumTrackingState.all: case MediumTrackingState.all:
return -1; return -1;
} }
@ -75,6 +89,8 @@ extension MediumStateExtension on MediumTrackingState {
} }
case MediumTrackingState.dropped: case MediumTrackingState.dropped:
return 'Dropped'; return 'Dropped';
case MediumTrackingState.paused:
return 'Paused';
case MediumTrackingState.all: case MediumTrackingState.all:
return 'All'; return 'All';
} }
@ -96,6 +112,8 @@ class MediumTrackingStateConverter
return MediumTrackingState.planned; return MediumTrackingState.planned;
case 3: case 3:
return MediumTrackingState.dropped; return MediumTrackingState.dropped;
case 4:
return MediumTrackingState.paused;
} }
return MediumTrackingState.planned; return MediumTrackingState.planned;

View File

@ -1,5 +1,6 @@
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/service/migrations/0000_score.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
const animeTable = 'Anime'; const animeTable = 'Anime';
@ -14,7 +15,8 @@ Future<void> _createDatabase(Database db, int version) async {
episodesTotal INTEGER, episodesTotal INTEGER,
episodesWatched INTEGER NOT NULL, episodesWatched INTEGER NOT NULL,
thumbnailUrl TEXT NOT NULL, thumbnailUrl TEXT NOT NULL,
title TEXT NOT NULL title TEXT NOT NULL,
score INTEGER
)''', )''',
); );
await db.execute( await db.execute(
@ -26,7 +28,8 @@ Future<void> _createDatabase(Database db, int version) async {
chaptersRead INTEGER NOT NULL, chaptersRead INTEGER NOT NULL,
volumesOwned INTEGER NOT NULL, volumesOwned INTEGER NOT NULL,
thumbnailUrl TEXT NOT NULL, thumbnailUrl TEXT NOT NULL,
title TEXT NOT NULL title TEXT NOT NULL,
score INTEGER
)''', )''',
); );
} }
@ -37,8 +40,23 @@ class DatabaseService {
Future<void> initialize() async { Future<void> initialize() async {
_db = await openDatabase( _db = await openDatabase(
'anitrack.db', 'anitrack.db',
version: 1, version: 2,
onConfigure: (db) async {
// In order to do schema changes during database upgrades, we disable foreign
// keys in the onConfigure phase, but re-enable them here.
// See https://github.com/tekartik/sqflite/issues/624#issuecomment-813324273
// for the "solution".
await db.execute('PRAGMA foreign_keys = OFF');
},
onOpen: (db) async {
await db.execute('PRAGMA foreign_keys = ON');
},
onCreate: _createDatabase, onCreate: _createDatabase,
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
await migrateFromV1ToV2(db);
}
},
); );
} }
@ -64,6 +82,7 @@ class DatabaseService {
await _db.insert( await _db.insert(
animeTable, animeTable,
data.toJson(), data.toJson(),
conflictAlgorithm: ConflictAlgorithm.ignore,
); );
} }
@ -88,6 +107,7 @@ class DatabaseService {
await _db.insert( await _db.insert(
mangaTable, mangaTable,
data.toJson(), data.toJson(),
conflictAlgorithm: ConflictAlgorithm.ignore,
); );
} }

View File

@ -0,0 +1,11 @@
import 'package:anitrack/src/service/database.dart';
import 'package:sqflite/sqflite.dart';
Future<void> migrateFromV1ToV2(Database db) async {
await db.execute(
'ALTER TABLE $animeTable ADD COLUMN score INTEGER DEFAULT NULL;',
);
await db.execute(
'ALTER TABLE $mangaTable ADD COLUMN score INTEGER DEFAULT NULL;',
);
}

View File

@ -0,0 +1,205 @@
import 'dart:async';
import 'dart:convert';
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:archive/archive_io.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get_it/get_it.dart';
import 'package:jikan_api/jikan_api.dart';
import 'package:xml/xml.dart';
part 'settings_state.dart';
part 'settings_event.dart';
part 'settings_bloc.freezed.dart';
MediumTrackingState malStatusToTrackingState(String status) {
switch (status) {
case 'Completed':
return MediumTrackingState.completed;
case 'Reading':
case 'Watching':
return MediumTrackingState.ongoing;
case 'Plan to Read':
case 'Plan to Watch':
return MediumTrackingState.planned;
case 'Dropped':
return MediumTrackingState.dropped;
case 'On-Hold':
return MediumTrackingState.paused;
default:
assert(false, 'Invalid status $status');
return MediumTrackingState.planned;
}
}
class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
SettingsBloc() : super(SettingsState()) {
on<AnimeListImportedEvent>(_onAnimeListImported);
on<MangaListImportedEvent>(_onMangaListImported);
}
void _showLoadingSpinner(Emitter<SettingsState> emit) {
emit(
state.copyWith(
importSpinnerVisible: true,
),
);
}
void _hideLoadingSpinner(Emitter<SettingsState> emit) {
emit(
state.copyWith(
importSpinnerVisible: false,
),
);
}
Future<void> _onAnimeListImported(
AnimeListImportedEvent event,
Emitter<SettingsState> emit,
) async {
assert(
event.type == ImportListType.mal,
'Only MAL imports are currently supported',
);
_showLoadingSpinner(emit);
final inputStream = InputFileStream(event.path);
final listRaw = GZipDecoder().decodeBuffer(inputStream);
final listXml = utf8.decode(listRaw);
final document = XmlDocument.parse(listXml);
final mal = document.getElement('myanimelist');
if (mal == null) {
print('Invalid MAL list export');
_hideLoadingSpinner(emit);
return;
}
emit(
state.copyWith(
importCurrent: 0,
importTotal: mal.childElements.length - 1,
),
);
for (final anime in mal.childElements) {
if (anime.qualifiedName == 'myinfo') {
continue;
}
emit(
state.copyWith(
importCurrent: state.importCurrent + 1,
),
);
final title = anime.getElement('series_title')!.text;
final totalEpisodes =
int.parse(anime.getElement('series_episodes')!.text);
final id = anime.getElement('series_animedb_id')!.text;
print('Waiting 500ms to not hammer Jikan ($title)');
await Future<void>.delayed(const Duration(milliseconds: 500));
// Query the MAL api
final data = await Jikan().getAnime(int.parse(id));
// Add the anime
await GetIt.I.get<DatabaseService>().addAnime(
AnimeTrackingData(
id,
malStatusToTrackingState(
anime.getElement('my_status')!.text,
),
title,
int.parse(anime.getElement('my_watched_episodes')!.text),
// 0 means that MAL does not know
totalEpisodes == 0 ? null : totalEpisodes,
data.imageUrl,
),
);
}
// Hide the spinner again
_hideLoadingSpinner(emit);
}
Future<void> _onMangaListImported(
MangaListImportedEvent event,
Emitter<SettingsState> emit,
) async {
assert(
event.type == ImportListType.mal,
'Only MAL imports are currently supported',
);
_showLoadingSpinner(emit);
final inputStream = InputFileStream(event.path);
final listRaw = GZipDecoder().decodeBuffer(inputStream);
final listXml = utf8.decode(listRaw);
final document = XmlDocument.parse(listXml);
final mal = document.getElement('myanimelist');
if (mal == null) {
print('Invalid MAL list export');
_hideLoadingSpinner(emit);
return;
}
emit(
state.copyWith(
importCurrent: 0,
importTotal: mal.childElements.length - 1,
),
);
for (final manga in mal.childElements) {
if (manga.qualifiedName == 'myinfo') {
continue;
}
emit(
state.copyWith(
importCurrent: state.importCurrent + 1,
),
);
final title = manga.getElement('manga_title')!.text;
final totalChapters = int.parse(manga.getElement('manga_chapters')!.text);
final id = manga.getElement('manga_mangadb_id')!.text;
print('Waiting 500ms to not hammer Jikan ($title)');
await Future<void>.delayed(const Duration(milliseconds: 500));
// Query the MAL api
Manga data;
try {
data = await Jikan().getManga(int.parse(id));
} catch (_) {
print('API request failed');
continue;
}
// Add the manga
await GetIt.I.get<DatabaseService>().addManga(
MangaTrackingData(
id,
malStatusToTrackingState(
manga.getElement('my_status')!.text,
),
title,
int.parse(manga.getElement('my_read_chapters')!.text),
0,
// 0 means that MAL does not know
totalChapters == 0 ? null : totalChapters,
data.imageUrl,
),
);
}
// Hide the spinner again
_hideLoadingSpinner(emit);
}
}

View File

@ -0,0 +1,177 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
part of 'settings_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$SettingsState {
bool get importSpinnerVisible => throw _privateConstructorUsedError;
int get importCurrent => throw _privateConstructorUsedError;
int get importTotal => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$SettingsStateCopyWith<SettingsState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SettingsStateCopyWith<$Res> {
factory $SettingsStateCopyWith(
SettingsState value, $Res Function(SettingsState) then) =
_$SettingsStateCopyWithImpl<$Res>;
$Res call({bool importSpinnerVisible, int importCurrent, int importTotal});
}
/// @nodoc
class _$SettingsStateCopyWithImpl<$Res>
implements $SettingsStateCopyWith<$Res> {
_$SettingsStateCopyWithImpl(this._value, this._then);
final SettingsState _value;
// ignore: unused_field
final $Res Function(SettingsState) _then;
@override
$Res call({
Object? importSpinnerVisible = freezed,
Object? importCurrent = freezed,
Object? importTotal = freezed,
}) {
return _then(_value.copyWith(
importSpinnerVisible: importSpinnerVisible == freezed
? _value.importSpinnerVisible
: importSpinnerVisible // ignore: cast_nullable_to_non_nullable
as bool,
importCurrent: importCurrent == freezed
? _value.importCurrent
: importCurrent // ignore: cast_nullable_to_non_nullable
as int,
importTotal: importTotal == freezed
? _value.importTotal
: importTotal // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
abstract class _$$_SettingsStateCopyWith<$Res>
implements $SettingsStateCopyWith<$Res> {
factory _$$_SettingsStateCopyWith(
_$_SettingsState value, $Res Function(_$_SettingsState) then) =
__$$_SettingsStateCopyWithImpl<$Res>;
@override
$Res call({bool importSpinnerVisible, int importCurrent, int importTotal});
}
/// @nodoc
class __$$_SettingsStateCopyWithImpl<$Res>
extends _$SettingsStateCopyWithImpl<$Res>
implements _$$_SettingsStateCopyWith<$Res> {
__$$_SettingsStateCopyWithImpl(
_$_SettingsState _value, $Res Function(_$_SettingsState) _then)
: super(_value, (v) => _then(v as _$_SettingsState));
@override
_$_SettingsState get _value => super._value as _$_SettingsState;
@override
$Res call({
Object? importSpinnerVisible = freezed,
Object? importCurrent = freezed,
Object? importTotal = freezed,
}) {
return _then(_$_SettingsState(
importSpinnerVisible: importSpinnerVisible == freezed
? _value.importSpinnerVisible
: importSpinnerVisible // ignore: cast_nullable_to_non_nullable
as bool,
importCurrent: importCurrent == freezed
? _value.importCurrent
: importCurrent // ignore: cast_nullable_to_non_nullable
as int,
importTotal: importTotal == freezed
? _value.importTotal
: importTotal // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
class _$_SettingsState implements _SettingsState {
_$_SettingsState(
{this.importSpinnerVisible = false,
this.importCurrent = 0,
this.importTotal = 0});
@override
@JsonKey()
final bool importSpinnerVisible;
@override
@JsonKey()
final int importCurrent;
@override
@JsonKey()
final int importTotal;
@override
String toString() {
return 'SettingsState(importSpinnerVisible: $importSpinnerVisible, importCurrent: $importCurrent, importTotal: $importTotal)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_SettingsState &&
const DeepCollectionEquality()
.equals(other.importSpinnerVisible, importSpinnerVisible) &&
const DeepCollectionEquality()
.equals(other.importCurrent, importCurrent) &&
const DeepCollectionEquality()
.equals(other.importTotal, importTotal));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(importSpinnerVisible),
const DeepCollectionEquality().hash(importCurrent),
const DeepCollectionEquality().hash(importTotal));
@JsonKey(ignore: true)
@override
_$$_SettingsStateCopyWith<_$_SettingsState> get copyWith =>
__$$_SettingsStateCopyWithImpl<_$_SettingsState>(this, _$identity);
}
abstract class _SettingsState implements SettingsState {
factory _SettingsState(
{final bool importSpinnerVisible,
final int importCurrent,
final int importTotal}) = _$_SettingsState;
@override
bool get importSpinnerVisible;
@override
int get importCurrent;
@override
int get importTotal;
@override
@JsonKey(ignore: true)
_$$_SettingsStateCopyWith<_$_SettingsState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,36 @@
part of 'settings_bloc.dart';
enum ImportListType {
// MyAnimeList
mal,
}
abstract class SettingsEvent {}
/// Triggered when an anime list is imported
class AnimeListImportedEvent extends SettingsEvent {
AnimeListImportedEvent(
this.path,
this.type,
);
/// The path to the list we're importing
final String path;
/// The type of list we're importing
final ImportListType type;
}
/// Triggered when a manga list is imported
class MangaListImportedEvent extends SettingsEvent {
MangaListImportedEvent(
this.path,
this.type,
);
/// The path to the list we're importing
final String path;
/// The type of list we're importing
final ImportListType type;
}

View File

@ -0,0 +1,10 @@
part of 'settings_bloc.dart';
@freezed
class SettingsState with _$SettingsState {
factory SettingsState({
@Default(false) bool importSpinnerVisible,
@Default(0) int importCurrent,
@Default(0) int importTotal,
}) = _SettingsState;
}

View File

@ -2,3 +2,4 @@ const animeListRoute = '/anime/list';
const animeSearchRoute = '/anime/search'; const animeSearchRoute = '/anime/search';
const detailsRoute = '/anime/details'; const detailsRoute = '/anime/details';
const aboutRoute = '/about'; const aboutRoute = '/about';
const settingsRoute = '/settings';

View File

@ -84,6 +84,10 @@ class AnimeListPageState extends State<AnimeListPage> {
value: MediumTrackingState.dropped, value: MediumTrackingState.dropped,
child: Text(MediumTrackingState.dropped.toNameString(type)), child: Text(MediumTrackingState.dropped.toNameString(type)),
), ),
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.paused,
child: Text(MediumTrackingState.paused.toNameString(type)),
),
const PopupMenuItem<MediumTrackingState>( const PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.all, value: MediumTrackingState.all,
child: Text('All'), child: Text('All'),
@ -150,6 +154,13 @@ class AnimeListPageState extends State<AnimeListPage> {
), ),
), ),
), ),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Settings'),
onTap: () {
Navigator.of(context).pushNamed(settingsRoute);
},
),
ListTile( ListTile(
leading: const Icon(Icons.info), leading: const Icon(Icons.info),
title: const Text('About'), title: const Text('About'),

View File

@ -166,6 +166,11 @@ class DetailsPage extends StatelessWidget {
MediumTrackingState.dropped MediumTrackingState.dropped
.toNameString(state.trackingType), .toNameString(state.trackingType),
), ),
SelectorItem(
MediumTrackingState.paused,
MediumTrackingState.paused
.toNameString(state.trackingType),
),
], ],
initialValue: state.data!.state, initialValue: state.data!.state,
), ),

View File

@ -0,0 +1,155 @@
import 'package:anitrack/src/ui/bloc/settings_bloc.dart';
import 'package:anitrack/src/ui/constants.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
builder: (_) => const SettingsPage(),
settings: const RouteSettings(
name: settingsRoute,
),
);
@override
Widget build(BuildContext context) {
return BlocBuilder<SettingsBloc, SettingsState>(
builder: (context, state) {
return WillPopScope(
onWillPop: () async {
return !state.importSpinnerVisible;
},
child: Stack(
children: [
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: ListView(
children: [
ListTile(
title: const Text('Import anime list'),
subtitle: const Text(
'Import anime list exported from MyAnimeList.',
),
onTap: () async {
// Pick the file
final result = await FilePicker.platform.pickFiles();
if (result == null) return;
if (!result.files.first.path!.endsWith('.xml.gz')) {
await showDialog<void>(
context: context,
builder: (_) => const AlertDialog(
title: Text('Invalid anime list'),
content: Text(
'The selected file is not a MAL anime list. It lacks the ".xml.gz" suffix.',
),
),
);
return;
}
GetIt.I.get<SettingsBloc>().add(
AnimeListImportedEvent(
result.files.first.path!,
ImportListType.mal,
),
);
},
),
ListTile(
title: const Text('Import manga list'),
subtitle: const Text(
'Import manga list exported from MyAnimeList.',
),
onTap: () async {
// Pick the file
final result = await FilePicker.platform.pickFiles();
if (result == null) return;
if (!result.files.first.path!.endsWith('.xml.gz')) {
await showDialog<void>(
context: context,
builder: (_) => const AlertDialog(
title: Text('Invalid manga list'),
content: Text(
'The selected file is not a MAL manga list. It lacks the ".xml.gz" suffix.',
),
),
);
return;
}
GetIt.I.get<SettingsBloc>().add(
MangaListImportedEvent(
result.files.first.path!,
ImportListType.mal,
),
);
},
),
],
),
),
),
if (state.importSpinnerVisible)
const Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: ModalBarrier(
dismissible: false,
color: Colors.black54,
),
),
if (state.importSpinnerVisible)
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: Center(
child: SizedBox(
width: 150,
height: 150,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.grey.shade800,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.all(25),
child: CircularProgressIndicator(),
),
Text(
'${state.importCurrent} of ${state.importTotal}',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
),
),
],
),
);
},
);
}
}

View File

@ -18,13 +18,13 @@ packages:
source: hosted source: hosted
version: "5.2.0" version: "5.2.0"
archive: archive:
dependency: transitive dependency: "direct main"
description: description:
name: archive name: archive
sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.6" version: "3.3.7"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -194,7 +194,7 @@ packages:
source: hosted source: hosted
version: "4.4.0" version: "4.4.0"
collection: collection:
dependency: transitive dependency: "direct main"
description: description:
name: collection name: collection
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
@ -265,6 +265,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.4" version: "6.1.4"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: dd328189f2f4ccea042bb5b382d5e981691cc74b5a3429b9317bff2b19704489
url: "https://pub.dev"
source: hosted
version: "5.2.8"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -326,6 +334,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: c224ac897bed083dabf11f238dd11a239809b446740be0c2044608c50029ffdf
url: "https://pub.dev"
source: hosted
version: "2.0.9"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -934,13 +950,13 @@ packages:
source: hosted source: hosted
version: "0.2.0+3" version: "0.2.0+3"
xml: xml:
dependency: transitive dependency: "direct main"
description: description:
name: xml name: xml
sha256: ac0e3f4bf00ba2708c33fbabbbe766300e509f8c82dbd4ab6525039813f7e2fb sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.0" version: "6.2.2"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:

View File

@ -2,16 +2,19 @@ name: anitrack
description: An anime and manga tracker description: An anime and manga tracker
publish_to: 'none' publish_to: 'none'
version: 0.1.1+6 version: 0.1.2+8
environment: environment:
sdk: '>=2.18.4 <3.0.0' sdk: '>=2.18.4 <3.0.0'
dependencies: dependencies:
archive: ^3.3.7
bloc: ^8.1.0 bloc: ^8.1.0
bottom_bar: ^2.0.3 bottom_bar: ^2.0.3
cached_network_image: ^3.2.3 cached_network_image: ^3.2.3
collection: ^1.17.0
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
file_picker: ^5.2.8
flutter: flutter:
sdk: flutter sdk: flutter
flutter_bloc: ^8.1.1 flutter_bloc: ^8.1.1
@ -22,6 +25,7 @@ dependencies:
sqflite: ^2.2.4+1 sqflite: ^2.2.4+1
swipeable_tile: ^2.0.0+3 swipeable_tile: ^2.0.0+3
url_launcher: ^6.1.8 url_launcher: ^6.1.8
xml: ^6.2.2
dev_dependencies: dev_dependencies:
build_runner: ^2.1.11 build_runner: ^2.1.11