diff --git a/lib/src/data/type.dart b/lib/src/data/type.dart index fa7dcd2..14c00f2 100644 --- a/lib/src/data/type.dart +++ b/lib/src/data/type.dart @@ -17,6 +17,9 @@ enum 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 = ''; diff --git a/lib/src/service/database.dart b/lib/src/service/database.dart index 29cb60e..66c8b63 100644 --- a/lib/src/service/database.dart +++ b/lib/src/service/database.dart @@ -76,6 +76,14 @@ class DatabaseService { ); } + Future deleteAnime(String id) async { + await _db.delete( + animeTable, + where: 'id = ?', + whereArgs: [id], + ); + } + Future addManga(MangaTrackingData data) async { await _db.insert( mangaTable, @@ -91,4 +99,12 @@ class DatabaseService { whereArgs: [data.id], ); } + + Future deleteManga(String id) async { + await _db.delete( + mangaTable, + where: 'id = ?', + whereArgs: [id], + ); + } } diff --git a/lib/src/ui/bloc/anime_list_bloc.dart b/lib/src/ui/bloc/anime_list_bloc.dart index 9bfc0c8..12d9fe7 100644 --- a/lib/src/ui/bloc/anime_list_bloc.dart +++ b/lib/src/ui/bloc/anime_list_bloc.dart @@ -24,6 +24,8 @@ class AnimeListBloc extends Bloc { on(_onMangaDecremented); on(_onAnimeUpdated); on(_onMangaUpdated); + on(_onAnimeRemoved); + on(_onMangaRemoved); } Future _onAnimeAdded(AnimeAddedEvent event, Emitter emit) async { @@ -206,4 +208,30 @@ class AnimeListBloc extends Bloc { ), ); } + + Future _onAnimeRemoved(AnimeRemovedEvent event, Emitter emit) async { + emit( + state.copyWith( + animes: List.from( + state.animes.where((anime) => anime.id != event.id), + ), + ), + ); + + // Update the database + await GetIt.I.get().deleteAnime(event.id); + } + + Future _onMangaRemoved(MangaRemovedEvent event, Emitter emit) async { + emit( + state.copyWith( + mangas: List.from( + state.mangas.where((manga) => manga.id != event.id), + ), + ), + ); + + // Update the database + await GetIt.I.get().deleteManga(event.id); + } } diff --git a/lib/src/ui/bloc/anime_list_event.dart b/lib/src/ui/bloc/anime_list_event.dart index 1b393d3..464cdf0 100644 --- a/lib/src/ui/bloc/anime_list_event.dart +++ b/lib/src/ui/bloc/anime_list_event.dart @@ -48,6 +48,13 @@ class AnimeUpdatedEvent extends AnimeListEvent { final AnimeTrackingData anime; } +class AnimeRemovedEvent extends AnimeListEvent { + AnimeRemovedEvent(this.id); + + /// The ID of the anime to be removed from the list. + final String id; +} + class MangaAddedEvent extends AnimeListEvent { MangaAddedEvent(this.data); @@ -82,3 +89,10 @@ class MangaUpdatedEvent extends AnimeListEvent { final MangaTrackingData manga; } + +class MangaRemovedEvent extends AnimeListEvent { + MangaRemovedEvent(this.id); + + /// The ID of the manga to be removed from the list. + final String id; +} diff --git a/lib/src/ui/bloc/details_bloc.dart b/lib/src/ui/bloc/details_bloc.dart index 86a909a..d8fa9f4 100644 --- a/lib/src/ui/bloc/details_bloc.dart +++ b/lib/src/ui/bloc/details_bloc.dart @@ -18,6 +18,7 @@ class DetailsBloc extends Bloc { on(_onAnimeRequested); on(_onMangaRequested); on(_onDetailsUpdated); + on(_onItemRemoved); } Future _onAnimeRequested(AnimeDetailsRequestedEvent event, Emitter emit) async { @@ -77,4 +78,26 @@ class DetailsBloc extends Bloc { ); } } + + Future _onItemRemoved(ItemRemovedEvent event, Emitter emit) async { + emit( + state.copyWith( + data: null, + ), + ); + + /// Remove the item from the database + final bloc = GetIt.I.get(); + switch (event.trackingType) { + case TrackingMediumType.anime: + bloc.add(AnimeRemovedEvent(event.id)); + break; + case TrackingMediumType.manga: + bloc.add(MangaRemovedEvent(event.id)); + break; + } + + // Navigate back + GetIt.I.get().add(PoppedRouteEvent()); + } } diff --git a/lib/src/ui/bloc/details_event.dart b/lib/src/ui/bloc/details_event.dart index abc39cc..2d535bc 100644 --- a/lib/src/ui/bloc/details_event.dart +++ b/lib/src/ui/bloc/details_event.dart @@ -19,5 +19,15 @@ class MangaDetailsRequestedEvent extends DetailsEvent { class DetailsUpdatedEvent extends DetailsEvent { DetailsUpdatedEvent(this.data); - final dynamic data; + final TrackingMedium data; +} + +class ItemRemovedEvent extends DetailsEvent { + ItemRemovedEvent(this.id, this.trackingType); + + /// The ID of the item to be removed + final String id; + + /// The type of medium of the item + final TrackingMediumType trackingType; } diff --git a/lib/src/ui/pages/about.dart b/lib/src/ui/pages/about.dart index dff1940..182acf4 100644 --- a/lib/src/ui/pages/about.dart +++ b/lib/src/ui/pages/about.dart @@ -1,16 +1,15 @@ -import 'dart:io'; import 'package:anitrack/licenses.g.dart'; import 'package:anitrack/src/ui/constants.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class AboutPage extends StatelessWidget { - AboutPage({ + const AboutPage({ super.key, }); static MaterialPageRoute get route => MaterialPageRoute( - builder: (_) => AboutPage(), + builder: (_) => const AboutPage(), settings: const RouteSettings( name: aboutRoute, ), @@ -20,7 +19,7 @@ class AboutPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('About'), + title: const Text('About'), ), body: ListView.builder( itemCount: ossLicenses.length + 1, @@ -29,7 +28,6 @@ class AboutPage extends StatelessWidget { return Padding( padding: const EdgeInsets.all(16), child: Column( - crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Text( @@ -44,7 +42,7 @@ class AboutPage extends StatelessWidget { mode: LaunchMode.externalApplication, ); }, - child: Text('Source') + child: const Text('Source'), ), ], ), diff --git a/lib/src/ui/pages/anime_list.dart b/lib/src/ui/pages/anime_list.dart index d969c34..6decf4c 100644 --- a/lib/src/ui/pages/anime_list.dart +++ b/lib/src/ui/pages/anime_list.dart @@ -101,7 +101,7 @@ class AnimeListPage extends StatelessWidget { drawer: Drawer( child: ListView( children: [ - DrawerHeader( + const DrawerHeader( decoration: BoxDecoration( color: Color(0xffcf4aff), ), @@ -115,8 +115,8 @@ class AnimeListPage extends StatelessWidget { ), ListTile( - leading: Icon(Icons.info), - title: Text('About'), + leading: const Icon(Icons.info), + title: const Text('About'), onTap: () { Navigator.of(context).pushNamed(aboutRoute); }, diff --git a/lib/src/ui/pages/details.dart b/lib/src/ui/pages/details.dart index b2c88e2..e49dea1 100644 --- a/lib/src/ui/pages/details.dart +++ b/lib/src/ui/pages/details.dart @@ -60,6 +60,48 @@ class DetailsPage extends StatelessWidget { softWrap: true, overflow: TextOverflow.ellipsis, ), + + ElevatedButton( + onPressed: () async { + final result = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Remove "${state.data!.title}"?'), + content: Text('Are you sure you want to remove "${state.data!.title}" from the list?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text('Remove'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), + ), + ], + ); + }, + ); + + if (result != true) return; + + // ignore: use_build_context_synchronously + context.read().add( + ItemRemovedEvent( + state.data!.id, + state.trackingType, + ), + ); + }, + child: const Icon(Icons.delete), + ), ], ), ), @@ -79,7 +121,7 @@ class DetailsPage extends StatelessWidget { if (state.trackingType == TrackingMediumType.anime) { context.read().add( DetailsUpdatedEvent( - (state.data as AnimeTrackingData).copyWith( + (state.data! as AnimeTrackingData).copyWith( state: newState, ), ), @@ -87,7 +129,7 @@ class DetailsPage extends StatelessWidget { } else if (state.trackingType == TrackingMediumType.manga) { context.read().add( DetailsUpdatedEvent( - (state.data as MangaTrackingData).copyWith( + (state.data! as MangaTrackingData).copyWith( state: newState, ), ), @@ -127,7 +169,7 @@ class DetailsPage extends StatelessWidget { onChanged: (value) { switch (state.trackingType) { case TrackingMediumType.anime: - final data = state.data as AnimeTrackingData; + final data = state.data! as AnimeTrackingData; context.read().add( DetailsUpdatedEvent( data.copyWith( @@ -137,7 +179,7 @@ class DetailsPage extends StatelessWidget { ); break; case TrackingMediumType.manga: - final data = state.data as MangaTrackingData; + final data = state.data! as MangaTrackingData; context.read().add( DetailsUpdatedEvent( data.copyWith( @@ -149,8 +191,8 @@ class DetailsPage extends StatelessWidget { } }, initialValue: state.trackingType == TrackingMediumType.anime ? - (state.data as AnimeTrackingData).episodesWatched : - (state.data as MangaTrackingData).chaptersRead, + (state.data! as AnimeTrackingData).episodesWatched : + (state.data! as MangaTrackingData).chaptersRead, ), ), @@ -162,7 +204,7 @@ class DetailsPage extends StatelessWidget { child: IntegerInput( labelText: 'Volumes owned', onChanged: (value) { - final data = state.data as MangaTrackingData; + final data = state.data! as MangaTrackingData; context.read().add( DetailsUpdatedEvent( data.copyWith( @@ -171,7 +213,7 @@ class DetailsPage extends StatelessWidget { ), ); }, - initialValue: (GetIt.I.get().state.data as MangaTrackingData).volumesOwned, + initialValue: (GetIt.I.get().state.data! as MangaTrackingData).volumesOwned, ), ), ], diff --git a/pubspec.yaml b/pubspec.yaml index 2e35215..5b236f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,9 +19,9 @@ dependencies: get_it: ^7.2.0 jikan_api: ^2.0.0 json_annotation: 4.6.0 - url_launcher: ^6.1.8 sqflite: ^2.2.4+1 swipeable_tile: ^2.0.0+3 + url_launcher: ^6.1.8 dev_dependencies: build_runner: ^2.1.11