From 8712dbb9de7b4b8115f56003be723abf06125431 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Mon, 6 Feb 2023 15:19:55 +0100 Subject: [PATCH] feat(meta): Cleanup --- analysis_options.yaml | 37 ++--- lib/main.dart | 3 +- lib/src/data/anime.dart | 57 +------ lib/src/data/anime.freezed.dart | 24 +-- lib/src/data/anime.g.dart | 4 +- lib/src/data/manga.dart | 57 +------ lib/src/data/manga.freezed.dart | 24 +-- lib/src/data/manga.g.dart | 4 +- lib/src/data/type.dart | 63 ++++++++ lib/src/service/database.dart | 4 +- lib/src/ui/bloc/anime_list_bloc.dart | 1 - lib/src/ui/bloc/anime_list_bloc.freezed.dart | 38 ++--- lib/src/ui/bloc/anime_list_event.dart | 4 +- lib/src/ui/bloc/anime_list_state.dart | 4 +- lib/src/ui/bloc/anime_search_bloc.dart | 10 +- lib/src/ui/bloc/details_bloc.dart | 10 +- lib/src/ui/bloc/navigation_bloc.dart | 1 - lib/src/ui/pages/anime_list.dart | 89 +++++------ lib/src/ui/pages/anime_search.dart | 19 +-- lib/src/ui/pages/details.dart | 160 ++++++------------- lib/src/ui/widgets/dropdown.dart | 129 +++++++++++++++ lib/src/ui/widgets/image.dart | 1 + lib/src/ui/widgets/list_item.dart | 7 +- pubspec.lock | 7 + pubspec.yaml | 5 +- 25 files changed, 384 insertions(+), 378 deletions(-) create mode 100644 lib/src/ui/widgets/dropdown.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4d..f434b72 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,29 +1,14 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - +include: package:very_good_analysis/analysis_options.yaml linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + public_member_api_docs: false + lines_longer_than_80_chars: false + use_setters_to_change_properties: false + avoid_positional_boolean_parameters: false + avoid_bool_literals_in_conditional_expressions: false -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + - "test/" diff --git a/lib/main.dart b/lib/main.dart index 1c34dac..908063d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:anitrack/src/service/database.dart'; 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'; @@ -6,7 +7,6 @@ import 'package:anitrack/src/ui/constants.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/service/database.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; @@ -66,7 +66,6 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'AniTrack', - themeMode: ThemeMode.system, theme: ThemeData( brightness: Brightness.light, primarySwatch: Colors.blue, diff --git a/lib/src/data/anime.dart b/lib/src/data/anime.dart index 951f63c..2203924 100644 --- a/lib/src/data/anime.dart +++ b/lib/src/data/anime.dart @@ -1,62 +1,9 @@ +import 'package:anitrack/src/data/type.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'anime.freezed.dart'; part 'anime.g.dart'; -/// The watch state of an anime -enum AnimeTrackingState { - watching, // 0 - completed, // 1 - planToWatch, // 2 - dropped, // 3 - /// This is a pseudo state, i.e. it should never be set - all, // -1 -} - -extension AnimeTrackStateExtension on AnimeTrackingState { - int toInteger() { - assert(this != AnimeTrackingState.all, 'AnimeTrackingState.all must not be serialized'); - switch (this) { - case AnimeTrackingState.watching: return 0; - case AnimeTrackingState.completed: return 1; - case AnimeTrackingState.planToWatch: return 2; - case AnimeTrackingState.dropped: return 3; - case AnimeTrackingState.all: return -1; - } - } - - String toNameString() { - assert(this != AnimeTrackingState.all, 'AnimeTrackingState.all must not be stringified'); - - switch (this) { - case AnimeTrackingState.watching: return 'Watching'; - case AnimeTrackingState.completed: return 'Completed'; - case AnimeTrackingState.planToWatch: return 'Plan to watch'; - case AnimeTrackingState.dropped: return 'Dropped'; - case AnimeTrackingState.all: return 'All'; - } - } -} - -class AnimeTrackingStateConverter implements JsonConverter { - const AnimeTrackingStateConverter(); - - @override - AnimeTrackingState fromJson(int json) { - switch (json) { - case 0: return AnimeTrackingState.watching; - case 1: return AnimeTrackingState.completed; - case 2: return AnimeTrackingState.planToWatch; - case 3: return AnimeTrackingState.dropped; - } - - return AnimeTrackingState.planToWatch; - } - - @override - int toJson(AnimeTrackingState state) => state.toInteger(); -} - /// Data about a tracked anime @freezed class AnimeTrackingData with _$AnimeTrackingData{ @@ -64,7 +11,7 @@ class AnimeTrackingData with _$AnimeTrackingData{ /// The ID of the anime String id, /// The state of the anime - @AnimeTrackingStateConverter() AnimeTrackingState state, + @MediumTrackingStateConverter() MediumTrackingState state, /// The title of the anime String title, /// Episodes in total. diff --git a/lib/src/data/anime.freezed.dart b/lib/src/data/anime.freezed.dart index 8ba182f..7e5c9e7 100644 --- a/lib/src/data/anime.freezed.dart +++ b/lib/src/data/anime.freezed.dart @@ -24,8 +24,8 @@ mixin _$AnimeTrackingData { String get id => throw _privateConstructorUsedError; /// The state of the anime - @AnimeTrackingStateConverter() - AnimeTrackingState get state => throw _privateConstructorUsedError; + @MediumTrackingStateConverter() + MediumTrackingState get state => throw _privateConstructorUsedError; /// The title of the anime String get title => throw _privateConstructorUsedError; @@ -52,7 +52,7 @@ abstract class $AnimeTrackingDataCopyWith<$Res> { _$AnimeTrackingDataCopyWithImpl<$Res>; $Res call( {String id, - @AnimeTrackingStateConverter() AnimeTrackingState state, + @MediumTrackingStateConverter() MediumTrackingState state, String title, int episodesWatched, int? episodesTotal, @@ -85,7 +85,7 @@ class _$AnimeTrackingDataCopyWithImpl<$Res> state: state == freezed ? _value.state : state // ignore: cast_nullable_to_non_nullable - as AnimeTrackingState, + as MediumTrackingState, title: title == freezed ? _value.title : title // ignore: cast_nullable_to_non_nullable @@ -115,7 +115,7 @@ abstract class _$$_AnimeTrackingDataCopyWith<$Res> @override $Res call( {String id, - @AnimeTrackingStateConverter() AnimeTrackingState state, + @MediumTrackingStateConverter() MediumTrackingState state, String title, int episodesWatched, int? episodesTotal, @@ -150,7 +150,7 @@ class __$$_AnimeTrackingDataCopyWithImpl<$Res> state == freezed ? _value.state : state // ignore: cast_nullable_to_non_nullable - as AnimeTrackingState, + as MediumTrackingState, title == freezed ? _value.title : title // ignore: cast_nullable_to_non_nullable @@ -174,7 +174,7 @@ class __$$_AnimeTrackingDataCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$_AnimeTrackingData implements _AnimeTrackingData { - _$_AnimeTrackingData(this.id, @AnimeTrackingStateConverter() this.state, + _$_AnimeTrackingData(this.id, @MediumTrackingStateConverter() this.state, this.title, this.episodesWatched, this.episodesTotal, this.thumbnailUrl); factory _$_AnimeTrackingData.fromJson(Map json) => @@ -186,8 +186,8 @@ class _$_AnimeTrackingData implements _AnimeTrackingData { /// The state of the anime @override - @AnimeTrackingStateConverter() - final AnimeTrackingState state; + @MediumTrackingStateConverter() + final MediumTrackingState state; /// The title of the anime @override @@ -254,7 +254,7 @@ class _$_AnimeTrackingData implements _AnimeTrackingData { abstract class _AnimeTrackingData implements AnimeTrackingData { factory _AnimeTrackingData( final String id, - @AnimeTrackingStateConverter() final AnimeTrackingState state, + @MediumTrackingStateConverter() final MediumTrackingState state, final String title, final int episodesWatched, final int? episodesTotal, @@ -270,8 +270,8 @@ abstract class _AnimeTrackingData implements AnimeTrackingData { @override /// The state of the anime - @AnimeTrackingStateConverter() - AnimeTrackingState get state; + @MediumTrackingStateConverter() + MediumTrackingState get state; @override /// The title of the anime diff --git a/lib/src/data/anime.g.dart b/lib/src/data/anime.g.dart index 4f1b630..4282cbf 100644 --- a/lib/src/data/anime.g.dart +++ b/lib/src/data/anime.g.dart @@ -9,7 +9,7 @@ part of 'anime.dart'; _$_AnimeTrackingData _$$_AnimeTrackingDataFromJson(Map json) => _$_AnimeTrackingData( json['id'] as String, - const AnimeTrackingStateConverter().fromJson(json['state'] as int), + const MediumTrackingStateConverter().fromJson(json['state'] as int), json['title'] as String, json['episodesWatched'] as int, json['episodesTotal'] as int?, @@ -20,7 +20,7 @@ Map _$$_AnimeTrackingDataToJson( _$_AnimeTrackingData instance) => { 'id': instance.id, - 'state': const AnimeTrackingStateConverter().toJson(instance.state), + 'state': const MediumTrackingStateConverter().toJson(instance.state), 'title': instance.title, 'episodesWatched': instance.episodesWatched, 'episodesTotal': instance.episodesTotal, diff --git a/lib/src/data/manga.dart b/lib/src/data/manga.dart index 47a0c42..ec66d59 100644 --- a/lib/src/data/manga.dart +++ b/lib/src/data/manga.dart @@ -1,62 +1,9 @@ +import 'package:anitrack/src/data/type.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'manga.freezed.dart'; part 'manga.g.dart'; -/// The watch state of an manga -enum MangaTrackingState { - reading, // 0 - completed, // 1 - planToRead, // 2 - dropped, // 3 - /// This is a pseudo state, i.e. it should never be set - all, // -1 -} - -extension MangaTrackStateExtension on MangaTrackingState { - int toInteger() { - assert(this != MangaTrackingState.all, 'MangaTrackingState.all must not be serialized'); - switch (this) { - case MangaTrackingState.reading: return 0; - case MangaTrackingState.completed: return 1; - case MangaTrackingState.planToRead: return 2; - case MangaTrackingState.dropped: return 3; - case MangaTrackingState.all: return -1; - } - } - - String toNameString() { - assert(this != MangaTrackingState.all, 'MangaTrackingState.all must not be stringified'); - - switch (this) { - case MangaTrackingState.reading: return 'Reading'; - case MangaTrackingState.completed: return 'Completed'; - case MangaTrackingState.planToRead: return 'Plan to read'; - case MangaTrackingState.dropped: return 'Dropped'; - case MangaTrackingState.all: return 'All'; - } - } -} - -class MangaTrackingStateConverter implements JsonConverter { - const MangaTrackingStateConverter(); - - @override - MangaTrackingState fromJson(int json) { - switch (json) { - case 0: return MangaTrackingState.reading; - case 1: return MangaTrackingState.completed; - case 2: return MangaTrackingState.planToRead; - case 3: return MangaTrackingState.dropped; - } - - return MangaTrackingState.planToRead; - } - - @override - int toJson(MangaTrackingState state) => state.toInteger(); -} - /// Data about a tracked anime @freezed class MangaTrackingData with _$MangaTrackingData{ @@ -64,7 +11,7 @@ class MangaTrackingData with _$MangaTrackingData{ /// The ID of the manga String id, /// The state of the manga - @MangaTrackingStateConverter() MangaTrackingState state, + @MediumTrackingStateConverter() MediumTrackingState state, /// The title of the manga String title, /// Chapters read. diff --git a/lib/src/data/manga.freezed.dart b/lib/src/data/manga.freezed.dart index 4b95b1a..7d38794 100644 --- a/lib/src/data/manga.freezed.dart +++ b/lib/src/data/manga.freezed.dart @@ -24,8 +24,8 @@ mixin _$MangaTrackingData { String get id => throw _privateConstructorUsedError; /// The state of the manga - @MangaTrackingStateConverter() - MangaTrackingState get state => throw _privateConstructorUsedError; + @MediumTrackingStateConverter() + MediumTrackingState get state => throw _privateConstructorUsedError; /// The title of the manga String get title => throw _privateConstructorUsedError; @@ -55,7 +55,7 @@ abstract class $MangaTrackingDataCopyWith<$Res> { _$MangaTrackingDataCopyWithImpl<$Res>; $Res call( {String id, - @MangaTrackingStateConverter() MangaTrackingState state, + @MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, @@ -90,7 +90,7 @@ class _$MangaTrackingDataCopyWithImpl<$Res> state: state == freezed ? _value.state : state // ignore: cast_nullable_to_non_nullable - as MangaTrackingState, + as MediumTrackingState, title: title == freezed ? _value.title : title // ignore: cast_nullable_to_non_nullable @@ -124,7 +124,7 @@ abstract class _$$_MangaTrackingDataCopyWith<$Res> @override $Res call( {String id, - @MangaTrackingStateConverter() MangaTrackingState state, + @MediumTrackingStateConverter() MediumTrackingState state, String title, int chaptersRead, int volumesOwned, @@ -161,7 +161,7 @@ class __$$_MangaTrackingDataCopyWithImpl<$Res> state == freezed ? _value.state : state // ignore: cast_nullable_to_non_nullable - as MangaTrackingState, + as MediumTrackingState, title == freezed ? _value.title : title // ignore: cast_nullable_to_non_nullable @@ -191,7 +191,7 @@ class __$$_MangaTrackingDataCopyWithImpl<$Res> class _$_MangaTrackingData implements _MangaTrackingData { _$_MangaTrackingData( this.id, - @MangaTrackingStateConverter() this.state, + @MediumTrackingStateConverter() this.state, this.title, this.chaptersRead, this.volumesOwned, @@ -207,8 +207,8 @@ class _$_MangaTrackingData implements _MangaTrackingData { /// The state of the manga @override - @MangaTrackingStateConverter() - final MangaTrackingState state; + @MediumTrackingStateConverter() + final MediumTrackingState state; /// The title of the manga @override @@ -282,7 +282,7 @@ class _$_MangaTrackingData implements _MangaTrackingData { abstract class _MangaTrackingData implements MangaTrackingData { factory _MangaTrackingData( final String id, - @MangaTrackingStateConverter() final MangaTrackingState state, + @MediumTrackingStateConverter() final MediumTrackingState state, final String title, final int chaptersRead, final int volumesOwned, @@ -299,8 +299,8 @@ abstract class _MangaTrackingData implements MangaTrackingData { @override /// The state of the manga - @MangaTrackingStateConverter() - MangaTrackingState get state; + @MediumTrackingStateConverter() + MediumTrackingState get state; @override /// The title of the manga diff --git a/lib/src/data/manga.g.dart b/lib/src/data/manga.g.dart index 063c83a..b40c3be 100644 --- a/lib/src/data/manga.g.dart +++ b/lib/src/data/manga.g.dart @@ -9,7 +9,7 @@ part of 'manga.dart'; _$_MangaTrackingData _$$_MangaTrackingDataFromJson(Map json) => _$_MangaTrackingData( json['id'] as String, - const MangaTrackingStateConverter().fromJson(json['state'] as int), + const MediumTrackingStateConverter().fromJson(json['state'] as int), json['title'] as String, json['chaptersRead'] as int, json['volumesOwned'] as int, @@ -21,7 +21,7 @@ Map _$$_MangaTrackingDataToJson( _$_MangaTrackingData instance) => { 'id': instance.id, - 'state': const MangaTrackingStateConverter().toJson(instance.state), + 'state': const MediumTrackingStateConverter().toJson(instance.state), 'title': instance.title, 'chaptersRead': instance.chaptersRead, 'volumesOwned': instance.volumesOwned, diff --git a/lib/src/data/type.dart b/lib/src/data/type.dart index 2071ccf..209c0a8 100644 --- a/lib/src/data/type.dart +++ b/lib/src/data/type.dart @@ -1,5 +1,68 @@ +import 'package:json_annotation/json_annotation.dart'; + /// The type of medium we are tracking. Useful for UI stuff. enum TrackingMediumType { anime, manga, } + +/// The state of the medium we're tracking, i.e. reading/watching, dropped, ... +enum MediumTrackingState { + ongoing, + completed, + planned, + dropped, + all, +} + +extension MediumStateExtension on MediumTrackingState { + int toInteger() { + 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.all: return -1; + } + } + + String toNameString(TrackingMediumType type) { + assert(this != MediumTrackingState.all, 'MediumTrackingState.all must not be stringified'); + + switch (this) { + case MediumTrackingState.ongoing: + switch (type) { + case TrackingMediumType.anime: return 'Watching'; + case TrackingMediumType.manga: return 'Reading'; + } + case MediumTrackingState.completed: return 'Completed'; + case MediumTrackingState.planned: + switch (type) { + case TrackingMediumType.anime: return 'Plan to watch'; + case TrackingMediumType.manga: return 'Plan to read'; + } + case MediumTrackingState.dropped: return 'Dropped'; + case MediumTrackingState.all: return 'All'; + } + } +} + +class MediumTrackingStateConverter implements JsonConverter { + const MediumTrackingStateConverter(); + + @override + MediumTrackingState fromJson(int json) { + switch (json) { + case 0: return MediumTrackingState.ongoing; + case 1: return MediumTrackingState.completed; + case 2: return MediumTrackingState.planned; + case 3: return MediumTrackingState.dropped; + } + + return MediumTrackingState.planned; + } + + @override + int toJson(MediumTrackingState state) => state.toInteger(); +} diff --git a/lib/src/service/database.dart b/lib/src/service/database.dart index def694b..29cb60e 100644 --- a/lib/src/service/database.dart +++ b/lib/src/service/database.dart @@ -47,7 +47,7 @@ class DatabaseService { return animes .cast>() - .map((Map anime) => AnimeTrackingData.fromJson(anime)) + .map(AnimeTrackingData.fromJson) .toList(); } @@ -56,7 +56,7 @@ class DatabaseService { return mangas .cast>() - .map((Map manga) => MangaTrackingData.fromJson(manga)) + .map(MangaTrackingData.fromJson) .toList(); } diff --git a/lib/src/ui/bloc/anime_list_bloc.dart b/lib/src/ui/bloc/anime_list_bloc.dart index 483c0d7..9bfc0c8 100644 --- a/lib/src/ui/bloc/anime_list_bloc.dart +++ b/lib/src/ui/bloc/anime_list_bloc.dart @@ -1,4 +1,3 @@ -import 'dart:math'; import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/manga.dart'; import 'package:anitrack/src/data/type.dart'; diff --git a/lib/src/ui/bloc/anime_list_bloc.freezed.dart b/lib/src/ui/bloc/anime_list_bloc.freezed.dart index b9ccd32..b06c72d 100644 --- a/lib/src/ui/bloc/anime_list_bloc.freezed.dart +++ b/lib/src/ui/bloc/anime_list_bloc.freezed.dart @@ -18,8 +18,10 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$AnimeListState { List get animes => throw _privateConstructorUsedError; List get mangas => throw _privateConstructorUsedError; - AnimeTrackingState get animeFilterState => throw _privateConstructorUsedError; - MangaTrackingState get mangaFilterState => throw _privateConstructorUsedError; + MediumTrackingState get animeFilterState => + throw _privateConstructorUsedError; + MediumTrackingState get mangaFilterState => + throw _privateConstructorUsedError; TrackingMediumType get trackingType => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -35,8 +37,8 @@ abstract class $AnimeListStateCopyWith<$Res> { $Res call( {List animes, List mangas, - AnimeTrackingState animeFilterState, - MangaTrackingState mangaFilterState, + MediumTrackingState animeFilterState, + MediumTrackingState mangaFilterState, TrackingMediumType trackingType}); } @@ -69,11 +71,11 @@ class _$AnimeListStateCopyWithImpl<$Res> animeFilterState: animeFilterState == freezed ? _value.animeFilterState : animeFilterState // ignore: cast_nullable_to_non_nullable - as AnimeTrackingState, + as MediumTrackingState, mangaFilterState: mangaFilterState == freezed ? _value.mangaFilterState : mangaFilterState // ignore: cast_nullable_to_non_nullable - as MangaTrackingState, + as MediumTrackingState, trackingType: trackingType == freezed ? _value.trackingType : trackingType // ignore: cast_nullable_to_non_nullable @@ -92,8 +94,8 @@ abstract class _$$_AnimeListStateCopyWith<$Res> $Res call( {List animes, List mangas, - AnimeTrackingState animeFilterState, - MangaTrackingState mangaFilterState, + MediumTrackingState animeFilterState, + MediumTrackingState mangaFilterState, TrackingMediumType trackingType}); } @@ -128,11 +130,11 @@ class __$$_AnimeListStateCopyWithImpl<$Res> animeFilterState: animeFilterState == freezed ? _value.animeFilterState : animeFilterState // ignore: cast_nullable_to_non_nullable - as AnimeTrackingState, + as MediumTrackingState, mangaFilterState: mangaFilterState == freezed ? _value.mangaFilterState : mangaFilterState // ignore: cast_nullable_to_non_nullable - as MangaTrackingState, + as MediumTrackingState, trackingType: trackingType == freezed ? _value.trackingType : trackingType // ignore: cast_nullable_to_non_nullable @@ -147,8 +149,8 @@ class _$_AnimeListState implements _AnimeListState { _$_AnimeListState( {final List animes = const [], final List mangas = const [], - this.animeFilterState = AnimeTrackingState.watching, - this.mangaFilterState = MangaTrackingState.reading, + this.animeFilterState = MediumTrackingState.ongoing, + this.mangaFilterState = MediumTrackingState.ongoing, this.trackingType = TrackingMediumType.anime}) : _animes = animes, _mangas = mangas; @@ -171,10 +173,10 @@ class _$_AnimeListState implements _AnimeListState { @override @JsonKey() - final AnimeTrackingState animeFilterState; + final MediumTrackingState animeFilterState; @override @JsonKey() - final MangaTrackingState mangaFilterState; + final MediumTrackingState mangaFilterState; @override @JsonKey() final TrackingMediumType trackingType; @@ -218,8 +220,8 @@ abstract class _AnimeListState implements AnimeListState { factory _AnimeListState( {final List animes, final List mangas, - final AnimeTrackingState animeFilterState, - final MangaTrackingState mangaFilterState, + final MediumTrackingState animeFilterState, + final MediumTrackingState mangaFilterState, final TrackingMediumType trackingType}) = _$_AnimeListState; @override @@ -227,9 +229,9 @@ abstract class _AnimeListState implements AnimeListState { @override List get mangas; @override - AnimeTrackingState get animeFilterState; + MediumTrackingState get animeFilterState; @override - MangaTrackingState get mangaFilterState; + MediumTrackingState get mangaFilterState; @override TrackingMediumType get trackingType; @override diff --git a/lib/src/ui/bloc/anime_list_event.dart b/lib/src/ui/bloc/anime_list_event.dart index 69b55b4..1b393d3 100644 --- a/lib/src/ui/bloc/anime_list_event.dart +++ b/lib/src/ui/bloc/anime_list_event.dart @@ -31,7 +31,7 @@ class AnimeFilterChangedEvent extends AnimeListEvent { AnimeFilterChangedEvent(this.filterState); /// The state to filter - final AnimeTrackingState filterState; + final MediumTrackingState filterState; } /// Triggered when the view is changed from the anime or the manga view @@ -60,7 +60,7 @@ class MangaFilterChangedEvent extends AnimeListEvent { MangaFilterChangedEvent(this.filterState); /// The state to filter - final MangaTrackingState filterState; + final MediumTrackingState filterState; } class MangaChapterIncrementedEvent extends AnimeListEvent { diff --git a/lib/src/ui/bloc/anime_list_state.dart b/lib/src/ui/bloc/anime_list_state.dart index efc1398..c306547 100644 --- a/lib/src/ui/bloc/anime_list_state.dart +++ b/lib/src/ui/bloc/anime_list_state.dart @@ -5,8 +5,8 @@ class AnimeListState with _$AnimeListState { factory AnimeListState({ @Default([]) List animes, @Default([]) List mangas, - @Default(AnimeTrackingState.watching) AnimeTrackingState animeFilterState, - @Default(MangaTrackingState.reading) MangaTrackingState mangaFilterState, + @Default(MediumTrackingState.ongoing) MediumTrackingState animeFilterState, + @Default(MediumTrackingState.ongoing) MediumTrackingState mangaFilterState, @Default(TrackingMediumType.anime) TrackingMediumType trackingType, }) = _AnimeListState; } diff --git a/lib/src/ui/bloc/anime_search_bloc.dart b/lib/src/ui/bloc/anime_search_bloc.dart index 7def042..a3883b1 100644 --- a/lib/src/ui/bloc/anime_search_bloc.dart +++ b/lib/src/ui/bloc/anime_search_bloc.dart @@ -2,9 +2,9 @@ import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/manga.dart'; import 'package:anitrack/src/data/search_result.dart'; import 'package:anitrack/src/data/type.dart'; -import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/bloc/anime_list_bloc.dart' as list; import 'package:anitrack/src/ui/bloc/navigation_bloc.dart'; +import 'package:anitrack/src/ui/constants.dart'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:get_it/get_it.dart'; @@ -34,7 +34,7 @@ class AnimeSearchBloc extends Bloc { GetIt.I.get().add( PushedNamedEvent( - NavigationDestination(animeSearchRoute), + const NavigationDestination(animeSearchRoute), ), ); } @@ -101,7 +101,7 @@ class AnimeSearchBloc extends Bloc { list.AnimeAddedEvent( AnimeTrackingData( event.result.id, - AnimeTrackingState.watching, + MediumTrackingState.ongoing, event.result.title, 0, event.result.total, @@ -111,14 +111,14 @@ class AnimeSearchBloc extends Bloc { list.MangaAddedEvent( MangaTrackingData( event.result.id, - MangaTrackingState.reading, + MediumTrackingState.ongoing, event.result.title, 0, 0, event.result.total, event.result.thumbnailUrl, ), - ) + ), ); GetIt.I.get().add( diff --git a/lib/src/ui/bloc/details_bloc.dart b/lib/src/ui/bloc/details_bloc.dart index 500b9d6..86a909a 100644 --- a/lib/src/ui/bloc/details_bloc.dart +++ b/lib/src/ui/bloc/details_bloc.dart @@ -1,10 +1,10 @@ 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:anitrack/src/ui/bloc/navigation_bloc.dart'; import 'package:anitrack/src/ui/constants.dart'; -import 'package:anitrack/src/service/database.dart'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:get_it/get_it.dart'; @@ -30,9 +30,7 @@ class DetailsBloc extends Bloc { GetIt.I.get().add( PushedNamedEvent( - NavigationDestination( - detailsRoute, - ), + const NavigationDestination(detailsRoute), ), ); } @@ -47,9 +45,7 @@ class DetailsBloc extends Bloc { GetIt.I.get().add( PushedNamedEvent( - NavigationDestination( - detailsRoute, - ), + const NavigationDestination(detailsRoute), ), ); } diff --git a/lib/src/ui/bloc/navigation_bloc.dart b/lib/src/ui/bloc/navigation_bloc.dart index e68101b..e7a3f0d 100644 --- a/lib/src/ui/bloc/navigation_bloc.dart +++ b/lib/src/ui/bloc/navigation_bloc.dart @@ -1,6 +1,5 @@ import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; part 'navigation_event.dart'; part 'navigation_state.dart'; diff --git a/lib/src/ui/pages/anime_list.dart b/lib/src/ui/pages/anime_list.dart index 5903277..87f3150 100644 --- a/lib/src/ui/pages/anime_list.dart +++ b/lib/src/ui/pages/anime_list.dart @@ -1,5 +1,3 @@ -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/ui/bloc/anime_list_bloc.dart'; import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart'; @@ -11,6 +9,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class AnimeListPage extends StatelessWidget { + AnimeListPage({ + super.key, + }); + static MaterialPageRoute get route => MaterialPageRoute( builder: (_) => AnimeListPage(), settings: const RouteSettings( @@ -27,11 +29,36 @@ class AnimeListPage extends StatelessWidget { } } + List> _getPopupButtonItems(TrackingMediumType type) { + return [ + PopupMenuItem( + value: MediumTrackingState.ongoing, + child: Text(MediumTrackingState.ongoing.toNameString(type)), + ), + PopupMenuItem( + value: MediumTrackingState.completed, + child: Text(MediumTrackingState.completed.toNameString(type)), + ), + PopupMenuItem( + value: MediumTrackingState.planned, + child: Text(MediumTrackingState.planned.toNameString(type)), + ), + PopupMenuItem( + value: MediumTrackingState.dropped, + child: Text(MediumTrackingState.dropped.toNameString(type)), + ), + const PopupMenuItem( + value: MediumTrackingState.all, + child: Text('All'), + ), + ]; + } + Widget _getPopupButton(BuildContext context, AnimeListState state) { switch (state.trackingType) { case TrackingMediumType.anime: return PopupMenuButton( - icon: Icon( + icon: const Icon( Icons.filter_list, ), initialValue: state.animeFilterState, @@ -40,32 +67,11 @@ class AnimeListPage extends StatelessWidget { AnimeFilterChangedEvent(filterState), ); }, - itemBuilder: (_) => [ - const PopupMenuItem( - value: AnimeTrackingState.watching, - child: Text('Watching'), - ), - const PopupMenuItem( - value: AnimeTrackingState.completed, - child: Text('Completed'), - ), - const PopupMenuItem( - value: AnimeTrackingState.planToWatch, - child: Text('Plan to watch'), - ), - const PopupMenuItem( - value: AnimeTrackingState.dropped, - child: Text('Dropped'), - ), - const PopupMenuItem( - value: AnimeTrackingState.all, - child: Text('All'), - ), - ], + itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.anime), ); case TrackingMediumType.manga: return PopupMenuButton( - icon: Icon( + icon: const Icon( Icons.filter_list, ), initialValue: state.mangaFilterState, @@ -74,28 +80,7 @@ class AnimeListPage extends StatelessWidget { MangaFilterChangedEvent(filterState), ); }, - itemBuilder: (_) => [ - const PopupMenuItem( - value: MangaTrackingState.reading, - child: Text('Reading'), - ), - const PopupMenuItem( - value: MangaTrackingState.completed, - child: Text('Completed'), - ), - const PopupMenuItem( - value: MangaTrackingState.planToRead, - child: Text('Plan to read'), - ), - const PopupMenuItem( - value: MangaTrackingState.dropped, - child: Text('Dropped'), - ), - const PopupMenuItem( - value: MangaTrackingState.all, - child: Text('All'), - ), - ], + itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.manga), ); } } @@ -107,7 +92,7 @@ class AnimeListPage extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text( - _getPageTitle(state.trackingType) + _getPageTitle(state.trackingType), ), actions: [ _getPopupButton(context, state), @@ -120,7 +105,7 @@ class AnimeListPage extends StatelessWidget { itemCount: state.animes.length, itemBuilder: (context, index) { final anime = state.animes[index]; - if (state.animeFilterState != AnimeTrackingState.all) { + if (state.animeFilterState != MediumTrackingState.all) { if (anime.state != state.animeFilterState) return Container(); } @@ -157,7 +142,7 @@ class AnimeListPage extends StatelessWidget { itemCount: state.mangas.length, itemBuilder: (context, index) { final manga = state.mangas[index]; - if (state.mangaFilterState != MangaTrackingState.all) { + if (state.mangaFilterState != MediumTrackingState.all) { if (manga.state != state.mangaFilterState) return Container(); } @@ -216,7 +201,7 @@ class AnimeListPage extends StatelessWidget { _controller.jumpToPage(index); }, - items: [ + items: const [ BottomBarItem( icon: Icon(Icons.tv), title: Text('Anime'), diff --git a/lib/src/ui/pages/anime_search.dart b/lib/src/ui/pages/anime_search.dart index cee0b27..7fccc62 100644 --- a/lib/src/ui/pages/anime_search.dart +++ b/lib/src/ui/pages/anime_search.dart @@ -1,15 +1,17 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/type.dart'; import 'package:anitrack/src/ui/bloc/anime_search_bloc.dart'; import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/widgets/list_item.dart'; -import 'package:anitrack/src/ui/widgets/image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class AnimeSearchPage extends StatelessWidget { + const AnimeSearchPage({ + super.key, + }); + static MaterialPageRoute get route => MaterialPageRoute( - builder: (_) => AnimeSearchPage(), + builder: (_) => const AnimeSearchPage(), settings: const RouteSettings( name: animeSearchRoute, ), @@ -32,7 +34,7 @@ class AnimeSearchPage extends StatelessWidget { Padding( padding: const EdgeInsets.all(8), child: TextField( - decoration: InputDecoration( + decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Search query', ), @@ -50,9 +52,8 @@ class AnimeSearchPage extends StatelessWidget { ), if (state.working) - Expanded( + const Expanded( child: Align( - alignment: Alignment.center, child: CircularProgressIndicator(), ), ) @@ -86,7 +87,7 @@ class AnimeSearchPage extends StatelessWidget { ], ), ); - } + }, ), ), ], diff --git a/lib/src/ui/pages/details.dart b/lib/src/ui/pages/details.dart index 202c588..ea90ff6 100644 --- a/lib/src/ui/pages/details.dart +++ b/lib/src/ui/pages/details.dart @@ -3,44 +3,28 @@ import 'package:anitrack/src/data/manga.dart'; import 'package:anitrack/src/data/type.dart'; import 'package:anitrack/src/ui/bloc/details_bloc.dart'; import 'package:anitrack/src/ui/constants.dart'; +import 'package:anitrack/src/ui/widgets/dropdown.dart'; import 'package:anitrack/src/ui/widgets/image.dart'; -import 'package:anitrack/src/ui/widgets/list_item.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -Widget _makeListTile(BuildContext context, dynamic value, String text, bool selected) { - return ListTile( - title: Text(text), - trailing: selected ? - Icon(Icons.check) : - null, - onTap: () { - Navigator.of(context).pop(value); - }, - ); -} - class DetailsPage extends StatelessWidget { + const DetailsPage({ + super.key, + }); + static MaterialPageRoute get route => MaterialPageRoute( - builder: (_) => DetailsPage(), + builder: (_) => const DetailsPage(), settings: const RouteSettings( name: detailsRoute, ), ); - - String _getTrackingStateText(DetailsState state) { - if (state.trackingType == TrackingMediumType.anime) { - return (state.data as AnimeTrackingData).state.toNameString(); - } else { - return (state.data as MangaTrackingData).state.toNameString(); - } - } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Details'), + title: const Text('Details'), ), body: BlocBuilder( builder: (context, state) { @@ -69,101 +53,63 @@ class DetailsPage extends StatelessWidget { children: [ Text( state.trackingType == TrackingMediumType.anime ? - (state.data as AnimeTrackingData).title : - (state.data as MangaTrackingData).title, + (state.data as AnimeTrackingData).title : + (state.data as MangaTrackingData).title, textAlign: TextAlign.left, style: Theme.of(context).textTheme.titleLarge, maxLines: 2, softWrap: true, overflow: TextOverflow.ellipsis, ), - - TextButton( - child: Text( - _getTrackingStateText(state), - ), - onPressed: () async { - final result = await showModalBottomSheet( - context: context, - builder: (context) { - return ListView( - shrinkWrap: true, - children: [ - _makeListTile( - context, - state.trackingType == TrackingMediumType.anime ? - AnimeTrackingState.watching : - MangaTrackingState.reading, - state.trackingType == TrackingMediumType.anime ? - 'Watching' : - 'Reading', - state.trackingType == TrackingMediumType.anime ? - (state.data as AnimeTrackingData).state == AnimeTrackingState.watching : - (state.data as MangaTrackingData).state == MangaTrackingState.reading, - ), - _makeListTile( - context, - state.trackingType == TrackingMediumType.anime ? - AnimeTrackingState.completed : - MangaTrackingState.completed, - 'Completed', - state.trackingType == TrackingMediumType.anime ? - (state.data as AnimeTrackingData).state == AnimeTrackingState.completed : - (state.data as MangaTrackingData).state == MangaTrackingState.completed, - ), - _makeListTile( - context, - state.trackingType == TrackingMediumType.anime ? - AnimeTrackingState.planToWatch : - MangaTrackingState.planToRead, - state.trackingType == TrackingMediumType.anime ? - 'Plan to watch' : - 'Plan to read', - state.trackingType == TrackingMediumType.anime ? - (state.data as AnimeTrackingData).state == AnimeTrackingState.planToWatch : - (state.data as MangaTrackingData).state == MangaTrackingState.planToRead, - ), - _makeListTile( - context, - state.trackingType == TrackingMediumType.anime ? - AnimeTrackingState.dropped : - MangaTrackingState.dropped, - 'Dropped', - state.trackingType == TrackingMediumType.anime ? - (state.data as AnimeTrackingData).state == AnimeTrackingState.dropped : - (state.data as MangaTrackingData).state == MangaTrackingState.dropped, - ), - ], - ); - }, - ); - - if (result == null) return; - - if (state.trackingType == TrackingMediumType.anime) { - context.read().add( - DetailsUpdatedEvent( - (state.data as AnimeTrackingData).copyWith( - state: result as AnimeTrackingState, - ), - ), - ); - } else { - context.read().add( - DetailsUpdatedEvent( - (state.data as MangaTrackingData).copyWith( - state: result as MangaTrackingState, - ), - ), - ); - } - }, - ), ], ), ), ], ), + + DropdownSelector( + title: 'Watchstate', + onChanged: (MediumTrackingState newState) { + if (state.trackingType == TrackingMediumType.anime) { + context.read().add( + DetailsUpdatedEvent( + (state.data as AnimeTrackingData).copyWith( + state: newState, + ), + ), + ); + } else if (state.trackingType == TrackingMediumType.manga) { + context.read().add( + DetailsUpdatedEvent( + (state.data as MangaTrackingData).copyWith( + state: newState, + ), + ), + ); + } + }, + values: [ + SelectorItem( + MediumTrackingState.ongoing, + MediumTrackingState.ongoing.toNameString(state.trackingType), + ), + SelectorItem( + MediumTrackingState.completed, + MediumTrackingState.completed.toNameString(state.trackingType), + ), + SelectorItem( + MediumTrackingState.planned, + MediumTrackingState.planned.toNameString(state.trackingType), + ), + SelectorItem( + MediumTrackingState.dropped, + MediumTrackingState.dropped.toNameString(state.trackingType), + ), + ], + initialValue: state.trackingType == TrackingMediumType.anime ? + (state.data as AnimeTrackingData).state : + (state.data as MangaTrackingData).state, + ), ], ), ); diff --git a/lib/src/ui/widgets/dropdown.dart b/lib/src/ui/widgets/dropdown.dart new file mode 100644 index 0000000..09b4d40 --- /dev/null +++ b/lib/src/ui/widgets/dropdown.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; + +class SelectorItem { + const SelectorItem(this.value, this.text); + + final T value; + + final String text; +} + +class DropdownSelector extends StatefulWidget { + const DropdownSelector({ + required this.title, + required this.onChanged, + required this.values, + required this.initialValue, + super.key, + }); + + /// The title of the dropdown + final String title; + + final List> values; + + final T initialValue; + + /// Called when the selection has changed + final void Function(T) onChanged; + + @override + DropdownSelectorState createState() => DropdownSelectorState(); +} + +class DropdownSelectorDialog extends StatelessWidget { + const DropdownSelectorDialog({ + required this.values, + super.key, + }); + + final List> values; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Material( + color: Theme.of(context).scaffoldBackgroundColor, + child: ListView.builder( + itemCount: values.length, + itemBuilder: (context, index) => InkWell( + onTap: () { + Navigator.of(context).pop(values[index].value); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + values[index].text, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class DropdownSelectorState extends State> { + late int index; + + @override + void initState() { + super.initState(); + + index = widget.values.indexWhere((item) => item.value == widget.initialValue); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Card( + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () async { + final result = await showDialog( + context: context, + builder: (context) => DropdownSelectorDialog( + values: widget.values.cast>(), + ), + ); + + if (result == null) return; + if (result == widget.values[index].value) return; + + setState(() { + index = widget.values.indexWhere((item) => item.value == result); + }); + + widget.onChanged(result); + }, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: Theme.of(context).textTheme.titleSmall, + ), + Text( + widget.values[index].text, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/ui/widgets/image.dart b/lib/src/ui/widgets/image.dart index 9300150..c5fb11e 100644 --- a/lib/src/ui/widgets/image.dart +++ b/lib/src/ui/widgets/image.dart @@ -9,6 +9,7 @@ class AnimeCoverImage extends StatelessWidget { /// The URL to the cover image. final String url; + @override Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(8), diff --git a/lib/src/ui/widgets/list_item.dart b/lib/src/ui/widgets/list_item.dart index d53d2fa..dddaefa 100644 --- a/lib/src/ui/widgets/list_item.dart +++ b/lib/src/ui/widgets/list_item.dart @@ -1,4 +1,3 @@ -import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/ui/widgets/image.dart'; import 'package:anitrack/src/ui/widgets/swipe_icon.dart'; import 'package:flutter/material.dart'; @@ -6,7 +5,7 @@ import 'package:swipeable_tile/swipeable_tile.dart'; /// A widget for displaying simple data about an anime in the listview class ListItem extends StatelessWidget { - ListItem({ + const ListItem({ required this.thumbnailUrl, required this.title, this.onLeftSwipe, @@ -42,14 +41,14 @@ class ListItem extends StatelessWidget { key: UniqueKey(), backgroundBuilder: (_, direction, __) { if (direction == SwipeDirection.endToStart) { - return Align( + return const Align( alignment: Alignment.centerRight, child: SwipeIcon( Icons.add, ), ); } else if (direction == SwipeDirection.startToEnd) { - return Align( + return const Align( alignment: Alignment.centerLeft, child: SwipeIcon( Icons.remove, diff --git a/pubspec.lock b/pubspec.lock index 10e98bb..b5f204c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -576,6 +576,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + very_good_analysis: + dependency: "direct dev" + description: + name: very_good_analysis + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 136e9b2..e32981b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,12 +23,13 @@ dependencies: dev_dependencies: build_runner: ^2.1.11 - flutter_test: - sdk: flutter flutter_launcher_icons: ^0.11.0 flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter freezed: ^2.1.0+1 json_serializable: ^6.3.1 + very_good_analysis: ^3.0.1 flutter: uses-material-design: true