diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fb9b0c9..95653ea 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -32,5 +32,5 @@ android:value="2" /> - + diff --git a/lib/main.dart b/lib/main.dart index c0184db..d8fd7c3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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/details_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/pages/about.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/details.dart'; +import 'package:anitrack/src/ui/pages/settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; @@ -28,6 +30,7 @@ void main() async { GetIt.I.registerSingleton(AnimeSearchBloc()); GetIt.I.registerSingleton(DetailsBloc()); GetIt.I.registerSingleton(NavigationBloc(navKey)); + GetIt.I.registerSingleton(SettingsBloc()); // Load animes GetIt.I.get().add( @@ -49,6 +52,9 @@ void main() async { BlocProvider( create: (_) => GetIt.I.get(), ), + BlocProvider( + create: (_) => GetIt.I.get(), + ), ], child: MyApp(navKey), ), @@ -89,6 +95,8 @@ class MyApp extends StatelessWidget { return DetailsPage.route; case aboutRoute: return AboutPage.route; + case settingsRoute: + return SettingsPage.route; } return null; diff --git a/lib/src/data/type.dart b/lib/src/data/type.dart index 03e4659..16068ca 100644 --- a/lib/src/data/type.dart +++ b/lib/src/data/type.dart @@ -8,10 +8,22 @@ enum TrackingMediumType { /// The state of the medium we're tracking, i.e. reading/watching, dropped, ... enum MediumTrackingState { + /// Currently watching or reading ongoing, + + /// Done completed, + + /// Plan to watch or read planned, + + /// Dropped dropped, + + /// Paused + paused, + + /// Meta state all, } @@ -45,6 +57,8 @@ extension MediumStateExtension on MediumTrackingState { return 2; case MediumTrackingState.dropped: return 3; + case MediumTrackingState.paused: + return 4; case MediumTrackingState.all: return -1; } @@ -75,6 +89,8 @@ extension MediumStateExtension on MediumTrackingState { } case MediumTrackingState.dropped: return 'Dropped'; + case MediumTrackingState.paused: + return 'Paused'; case MediumTrackingState.all: return 'All'; } @@ -96,6 +112,8 @@ class MediumTrackingStateConverter return MediumTrackingState.planned; case 3: return MediumTrackingState.dropped; + case 4: + return MediumTrackingState.paused; } return MediumTrackingState.planned; diff --git a/lib/src/service/database.dart b/lib/src/service/database.dart index 50d14b1..0571929 100644 --- a/lib/src/service/database.dart +++ b/lib/src/service/database.dart @@ -1,5 +1,6 @@ import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/manga.dart'; +import 'package:anitrack/src/service/migrations/0000_score.dart'; import 'package:sqflite/sqflite.dart'; const animeTable = 'Anime'; @@ -14,7 +15,8 @@ Future _createDatabase(Database db, int version) async { episodesTotal INTEGER, episodesWatched INTEGER NOT NULL, thumbnailUrl TEXT NOT NULL, - title TEXT NOT NULL + title TEXT NOT NULL, + score INTEGER )''', ); await db.execute( @@ -26,7 +28,8 @@ Future _createDatabase(Database db, int version) async { chaptersRead INTEGER NOT NULL, volumesOwned INTEGER NOT NULL, thumbnailUrl TEXT NOT NULL, - title TEXT NOT NULL + title TEXT NOT NULL, + score INTEGER )''', ); } @@ -37,8 +40,23 @@ class DatabaseService { Future initialize() async { _db = await openDatabase( '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, + onUpgrade: (db, oldVersion, newVersion) async { + if (oldVersion < 2) { + await migrateFromV1ToV2(db); + } + }, ); } @@ -64,6 +82,7 @@ class DatabaseService { await _db.insert( animeTable, data.toJson(), + conflictAlgorithm: ConflictAlgorithm.ignore, ); } @@ -88,6 +107,7 @@ class DatabaseService { await _db.insert( mangaTable, data.toJson(), + conflictAlgorithm: ConflictAlgorithm.ignore, ); } diff --git a/lib/src/service/migrations/0000_score.dart b/lib/src/service/migrations/0000_score.dart new file mode 100644 index 0000000..736b636 --- /dev/null +++ b/lib/src/service/migrations/0000_score.dart @@ -0,0 +1,11 @@ +import 'package:anitrack/src/service/database.dart'; +import 'package:sqflite/sqflite.dart'; + +Future 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;', + ); +} diff --git a/lib/src/ui/bloc/settings_bloc.dart b/lib/src/ui/bloc/settings_bloc.dart new file mode 100644 index 0000000..88956ee --- /dev/null +++ b/lib/src/ui/bloc/settings_bloc.dart @@ -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 { + SettingsBloc() : super(SettingsState()) { + on(_onAnimeListImported); + on(_onMangaListImported); + } + + void _showLoadingSpinner(Emitter emit) { + emit( + state.copyWith( + importSpinnerVisible: true, + ), + ); + } + + void _hideLoadingSpinner(Emitter emit) { + emit( + state.copyWith( + importSpinnerVisible: false, + ), + ); + } + + Future _onAnimeListImported( + AnimeListImportedEvent event, + Emitter 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.delayed(const Duration(milliseconds: 500)); + + // Query the MAL api + final data = await Jikan().getAnime(int.parse(id)); + + // Add the anime + await GetIt.I.get().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 _onMangaListImported( + MangaListImportedEvent event, + Emitter 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.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().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); + } +} diff --git a/lib/src/ui/bloc/settings_bloc.freezed.dart b/lib/src/ui/bloc/settings_bloc.freezed.dart new file mode 100644 index 0000000..36a24c9 --- /dev/null +++ b/lib/src/ui/bloc/settings_bloc.freezed.dart @@ -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 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 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; +} diff --git a/lib/src/ui/bloc/settings_event.dart b/lib/src/ui/bloc/settings_event.dart new file mode 100644 index 0000000..dbc3674 --- /dev/null +++ b/lib/src/ui/bloc/settings_event.dart @@ -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; +} diff --git a/lib/src/ui/bloc/settings_state.dart b/lib/src/ui/bloc/settings_state.dart new file mode 100644 index 0000000..dcf0a91 --- /dev/null +++ b/lib/src/ui/bloc/settings_state.dart @@ -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; +} diff --git a/lib/src/ui/constants.dart b/lib/src/ui/constants.dart index aabfe51..8838633 100644 --- a/lib/src/ui/constants.dart +++ b/lib/src/ui/constants.dart @@ -2,3 +2,4 @@ const animeListRoute = '/anime/list'; const animeSearchRoute = '/anime/search'; const detailsRoute = '/anime/details'; const aboutRoute = '/about'; +const settingsRoute = '/settings'; diff --git a/lib/src/ui/pages/anime_list.dart b/lib/src/ui/pages/anime_list.dart index 9df1228..69a153f 100644 --- a/lib/src/ui/pages/anime_list.dart +++ b/lib/src/ui/pages/anime_list.dart @@ -84,6 +84,10 @@ class AnimeListPageState extends State { value: MediumTrackingState.dropped, child: Text(MediumTrackingState.dropped.toNameString(type)), ), + PopupMenuItem( + value: MediumTrackingState.paused, + child: Text(MediumTrackingState.paused.toNameString(type)), + ), const PopupMenuItem( value: MediumTrackingState.all, child: Text('All'), @@ -150,6 +154,13 @@ class AnimeListPageState extends State { ), ), ), + ListTile( + leading: const Icon(Icons.settings), + title: const Text('Settings'), + onTap: () { + Navigator.of(context).pushNamed(settingsRoute); + }, + ), ListTile( leading: const Icon(Icons.info), title: const Text('About'), diff --git a/lib/src/ui/pages/details.dart b/lib/src/ui/pages/details.dart index fe9ebaf..90bb652 100644 --- a/lib/src/ui/pages/details.dart +++ b/lib/src/ui/pages/details.dart @@ -166,6 +166,11 @@ class DetailsPage extends StatelessWidget { MediumTrackingState.dropped .toNameString(state.trackingType), ), + SelectorItem( + MediumTrackingState.paused, + MediumTrackingState.paused + .toNameString(state.trackingType), + ), ], initialValue: state.data!.state, ), diff --git a/lib/src/ui/pages/settings.dart b/lib/src/ui/pages/settings.dart new file mode 100644 index 0000000..363c101 --- /dev/null +++ b/lib/src/ui/pages/settings.dart @@ -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 get route => MaterialPageRoute( + builder: (_) => const SettingsPage(), + settings: const RouteSettings( + name: settingsRoute, + ), + ); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + 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( + 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().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( + 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().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, + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 7ce94bf..6484cb3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -18,13 +18,13 @@ packages: source: hosted version: "5.2.0" archive: - dependency: transitive + dependency: "direct main" description: name: archive - sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d + sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" url: "https://pub.dev" source: hosted - version: "3.3.6" + version: "3.3.7" args: dependency: transitive description: @@ -194,7 +194,7 @@ packages: source: hosted version: "4.4.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 @@ -265,6 +265,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -326,6 +334,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct dev" description: flutter @@ -934,13 +950,13 @@ packages: source: hosted version: "0.2.0+3" xml: - dependency: transitive + dependency: "direct main" description: name: xml - sha256: ac0e3f4bf00ba2708c33fbabbbe766300e509f8c82dbd4ab6525039813f7e2fb + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.2" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 24bc67a..6c2f485 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,16 +2,19 @@ name: anitrack description: An anime and manga tracker publish_to: 'none' -version: 0.1.1+6 +version: 0.1.2+8 environment: sdk: '>=2.18.4 <3.0.0' dependencies: + archive: ^3.3.7 bloc: ^8.1.0 bottom_bar: ^2.0.3 cached_network_image: ^3.2.3 + collection: ^1.17.0 cupertino_icons: ^1.0.2 + file_picker: ^5.2.8 flutter: sdk: flutter flutter_bloc: ^8.1.1 @@ -22,6 +25,7 @@ dependencies: sqflite: ^2.2.4+1 swipeable_tile: ^2.0.0+3 url_launcher: ^6.1.8 + xml: ^6.2.2 dev_dependencies: build_runner: ^2.1.11