feat(meta): Cleanup

This commit is contained in:
PapaTutuWawa 2023-02-06 15:19:55 +01:00
parent 8b17f68eed
commit 8712dbb9de
25 changed files with 384 additions and 378 deletions

View File

@ -1,29 +1,14 @@
# This file configures the analyzer, which statically analyzes Dart code to include: package:very_good_analysis/analysis_options.yaml
# 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
linter: 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: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule public_member_api_docs: false
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 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 analyzer:
# https://dart.dev/guides/language/analysis-options exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "test/"

View File

@ -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_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';
@ -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_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/service/database.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';
@ -66,7 +66,6 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'AniTrack', title: 'AniTrack',
themeMode: ThemeMode.system,
theme: ThemeData( theme: ThemeData(
brightness: Brightness.light, brightness: Brightness.light,
primarySwatch: Colors.blue, primarySwatch: Colors.blue,

View File

@ -1,62 +1,9 @@
import 'package:anitrack/src/data/type.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'anime.freezed.dart'; part 'anime.freezed.dart';
part 'anime.g.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<AnimeTrackingState, int> {
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 /// Data about a tracked anime
@freezed @freezed
class AnimeTrackingData with _$AnimeTrackingData{ class AnimeTrackingData with _$AnimeTrackingData{
@ -64,7 +11,7 @@ class AnimeTrackingData with _$AnimeTrackingData{
/// The ID of the anime /// The ID of the anime
String id, String id,
/// The state of the anime /// The state of the anime
@AnimeTrackingStateConverter() AnimeTrackingState state, @MediumTrackingStateConverter() MediumTrackingState state,
/// The title of the anime /// The title of the anime
String title, String title,
/// Episodes in total. /// Episodes in total.

View File

@ -24,8 +24,8 @@ mixin _$AnimeTrackingData {
String get id => throw _privateConstructorUsedError; String get id => throw _privateConstructorUsedError;
/// The state of the anime /// The state of the anime
@AnimeTrackingStateConverter() @MediumTrackingStateConverter()
AnimeTrackingState get state => throw _privateConstructorUsedError; MediumTrackingState get state => throw _privateConstructorUsedError;
/// The title of the anime /// The title of the anime
String get title => throw _privateConstructorUsedError; String get title => throw _privateConstructorUsedError;
@ -52,7 +52,7 @@ abstract class $AnimeTrackingDataCopyWith<$Res> {
_$AnimeTrackingDataCopyWithImpl<$Res>; _$AnimeTrackingDataCopyWithImpl<$Res>;
$Res call( $Res call(
{String id, {String id,
@AnimeTrackingStateConverter() AnimeTrackingState state, @MediumTrackingStateConverter() MediumTrackingState state,
String title, String title,
int episodesWatched, int episodesWatched,
int? episodesTotal, int? episodesTotal,
@ -85,7 +85,7 @@ class _$AnimeTrackingDataCopyWithImpl<$Res>
state: state == freezed state: state == freezed
? _value.state ? _value.state
: state // ignore: cast_nullable_to_non_nullable : state // ignore: cast_nullable_to_non_nullable
as AnimeTrackingState, as MediumTrackingState,
title: title == freezed title: title == freezed
? _value.title ? _value.title
: title // ignore: cast_nullable_to_non_nullable : title // ignore: cast_nullable_to_non_nullable
@ -115,7 +115,7 @@ abstract class _$$_AnimeTrackingDataCopyWith<$Res>
@override @override
$Res call( $Res call(
{String id, {String id,
@AnimeTrackingStateConverter() AnimeTrackingState state, @MediumTrackingStateConverter() MediumTrackingState state,
String title, String title,
int episodesWatched, int episodesWatched,
int? episodesTotal, int? episodesTotal,
@ -150,7 +150,7 @@ class __$$_AnimeTrackingDataCopyWithImpl<$Res>
state == freezed state == freezed
? _value.state ? _value.state
: state // ignore: cast_nullable_to_non_nullable : state // ignore: cast_nullable_to_non_nullable
as AnimeTrackingState, as MediumTrackingState,
title == freezed title == freezed
? _value.title ? _value.title
: title // ignore: cast_nullable_to_non_nullable : title // ignore: cast_nullable_to_non_nullable
@ -174,7 +174,7 @@ class __$$_AnimeTrackingDataCopyWithImpl<$Res>
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _$_AnimeTrackingData implements _AnimeTrackingData { class _$_AnimeTrackingData implements _AnimeTrackingData {
_$_AnimeTrackingData(this.id, @AnimeTrackingStateConverter() this.state, _$_AnimeTrackingData(this.id, @MediumTrackingStateConverter() this.state,
this.title, this.episodesWatched, this.episodesTotal, this.thumbnailUrl); this.title, this.episodesWatched, this.episodesTotal, this.thumbnailUrl);
factory _$_AnimeTrackingData.fromJson(Map<String, dynamic> json) => factory _$_AnimeTrackingData.fromJson(Map<String, dynamic> json) =>
@ -186,8 +186,8 @@ class _$_AnimeTrackingData implements _AnimeTrackingData {
/// The state of the anime /// The state of the anime
@override @override
@AnimeTrackingStateConverter() @MediumTrackingStateConverter()
final AnimeTrackingState state; final MediumTrackingState state;
/// The title of the anime /// The title of the anime
@override @override
@ -254,7 +254,7 @@ class _$_AnimeTrackingData implements _AnimeTrackingData {
abstract class _AnimeTrackingData implements AnimeTrackingData { abstract class _AnimeTrackingData implements AnimeTrackingData {
factory _AnimeTrackingData( factory _AnimeTrackingData(
final String id, final String id,
@AnimeTrackingStateConverter() final AnimeTrackingState state, @MediumTrackingStateConverter() final MediumTrackingState state,
final String title, final String title,
final int episodesWatched, final int episodesWatched,
final int? episodesTotal, final int? episodesTotal,
@ -270,8 +270,8 @@ abstract class _AnimeTrackingData implements AnimeTrackingData {
@override @override
/// The state of the anime /// The state of the anime
@AnimeTrackingStateConverter() @MediumTrackingStateConverter()
AnimeTrackingState get state; MediumTrackingState get state;
@override @override
/// The title of the anime /// The title of the anime

View File

@ -9,7 +9,7 @@ part of 'anime.dart';
_$_AnimeTrackingData _$$_AnimeTrackingDataFromJson(Map<String, dynamic> json) => _$_AnimeTrackingData _$$_AnimeTrackingDataFromJson(Map<String, dynamic> json) =>
_$_AnimeTrackingData( _$_AnimeTrackingData(
json['id'] as String, json['id'] as String,
const AnimeTrackingStateConverter().fromJson(json['state'] as int), const MediumTrackingStateConverter().fromJson(json['state'] as int),
json['title'] as String, json['title'] as String,
json['episodesWatched'] as int, json['episodesWatched'] as int,
json['episodesTotal'] as int?, json['episodesTotal'] as int?,
@ -20,7 +20,7 @@ Map<String, dynamic> _$$_AnimeTrackingDataToJson(
_$_AnimeTrackingData instance) => _$_AnimeTrackingData instance) =>
<String, dynamic>{ <String, dynamic>{
'id': instance.id, 'id': instance.id,
'state': const AnimeTrackingStateConverter().toJson(instance.state), 'state': const MediumTrackingStateConverter().toJson(instance.state),
'title': instance.title, 'title': instance.title,
'episodesWatched': instance.episodesWatched, 'episodesWatched': instance.episodesWatched,
'episodesTotal': instance.episodesTotal, 'episodesTotal': instance.episodesTotal,

View File

@ -1,62 +1,9 @@
import 'package:anitrack/src/data/type.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'manga.freezed.dart'; part 'manga.freezed.dart';
part 'manga.g.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<MangaTrackingState, int> {
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 /// Data about a tracked anime
@freezed @freezed
class MangaTrackingData with _$MangaTrackingData{ class MangaTrackingData with _$MangaTrackingData{
@ -64,7 +11,7 @@ class MangaTrackingData with _$MangaTrackingData{
/// The ID of the manga /// The ID of the manga
String id, String id,
/// The state of the manga /// The state of the manga
@MangaTrackingStateConverter() MangaTrackingState state, @MediumTrackingStateConverter() MediumTrackingState state,
/// The title of the manga /// The title of the manga
String title, String title,
/// Chapters read. /// Chapters read.

View File

@ -24,8 +24,8 @@ mixin _$MangaTrackingData {
String get id => throw _privateConstructorUsedError; String get id => throw _privateConstructorUsedError;
/// The state of the manga /// The state of the manga
@MangaTrackingStateConverter() @MediumTrackingStateConverter()
MangaTrackingState get state => throw _privateConstructorUsedError; MediumTrackingState get state => throw _privateConstructorUsedError;
/// The title of the manga /// The title of the manga
String get title => throw _privateConstructorUsedError; String get title => throw _privateConstructorUsedError;
@ -55,7 +55,7 @@ abstract class $MangaTrackingDataCopyWith<$Res> {
_$MangaTrackingDataCopyWithImpl<$Res>; _$MangaTrackingDataCopyWithImpl<$Res>;
$Res call( $Res call(
{String id, {String id,
@MangaTrackingStateConverter() MangaTrackingState state, @MediumTrackingStateConverter() MediumTrackingState state,
String title, String title,
int chaptersRead, int chaptersRead,
int volumesOwned, int volumesOwned,
@ -90,7 +90,7 @@ class _$MangaTrackingDataCopyWithImpl<$Res>
state: state == freezed state: state == freezed
? _value.state ? _value.state
: state // ignore: cast_nullable_to_non_nullable : state // ignore: cast_nullable_to_non_nullable
as MangaTrackingState, as MediumTrackingState,
title: title == freezed title: title == freezed
? _value.title ? _value.title
: title // ignore: cast_nullable_to_non_nullable : title // ignore: cast_nullable_to_non_nullable
@ -124,7 +124,7 @@ abstract class _$$_MangaTrackingDataCopyWith<$Res>
@override @override
$Res call( $Res call(
{String id, {String id,
@MangaTrackingStateConverter() MangaTrackingState state, @MediumTrackingStateConverter() MediumTrackingState state,
String title, String title,
int chaptersRead, int chaptersRead,
int volumesOwned, int volumesOwned,
@ -161,7 +161,7 @@ class __$$_MangaTrackingDataCopyWithImpl<$Res>
state == freezed state == freezed
? _value.state ? _value.state
: state // ignore: cast_nullable_to_non_nullable : state // ignore: cast_nullable_to_non_nullable
as MangaTrackingState, as MediumTrackingState,
title == freezed title == freezed
? _value.title ? _value.title
: title // ignore: cast_nullable_to_non_nullable : title // ignore: cast_nullable_to_non_nullable
@ -191,7 +191,7 @@ class __$$_MangaTrackingDataCopyWithImpl<$Res>
class _$_MangaTrackingData implements _MangaTrackingData { class _$_MangaTrackingData implements _MangaTrackingData {
_$_MangaTrackingData( _$_MangaTrackingData(
this.id, this.id,
@MangaTrackingStateConverter() this.state, @MediumTrackingStateConverter() this.state,
this.title, this.title,
this.chaptersRead, this.chaptersRead,
this.volumesOwned, this.volumesOwned,
@ -207,8 +207,8 @@ class _$_MangaTrackingData implements _MangaTrackingData {
/// The state of the manga /// The state of the manga
@override @override
@MangaTrackingStateConverter() @MediumTrackingStateConverter()
final MangaTrackingState state; final MediumTrackingState state;
/// The title of the manga /// The title of the manga
@override @override
@ -282,7 +282,7 @@ class _$_MangaTrackingData implements _MangaTrackingData {
abstract class _MangaTrackingData implements MangaTrackingData { abstract class _MangaTrackingData implements MangaTrackingData {
factory _MangaTrackingData( factory _MangaTrackingData(
final String id, final String id,
@MangaTrackingStateConverter() final MangaTrackingState state, @MediumTrackingStateConverter() final MediumTrackingState state,
final String title, final String title,
final int chaptersRead, final int chaptersRead,
final int volumesOwned, final int volumesOwned,
@ -299,8 +299,8 @@ abstract class _MangaTrackingData implements MangaTrackingData {
@override @override
/// The state of the manga /// The state of the manga
@MangaTrackingStateConverter() @MediumTrackingStateConverter()
MangaTrackingState get state; MediumTrackingState get state;
@override @override
/// The title of the manga /// The title of the manga

View File

@ -9,7 +9,7 @@ part of 'manga.dart';
_$_MangaTrackingData _$$_MangaTrackingDataFromJson(Map<String, dynamic> json) => _$_MangaTrackingData _$$_MangaTrackingDataFromJson(Map<String, dynamic> json) =>
_$_MangaTrackingData( _$_MangaTrackingData(
json['id'] as String, json['id'] as String,
const MangaTrackingStateConverter().fromJson(json['state'] as int), const MediumTrackingStateConverter().fromJson(json['state'] as int),
json['title'] as String, json['title'] as String,
json['chaptersRead'] as int, json['chaptersRead'] as int,
json['volumesOwned'] as int, json['volumesOwned'] as int,
@ -21,7 +21,7 @@ Map<String, dynamic> _$$_MangaTrackingDataToJson(
_$_MangaTrackingData instance) => _$_MangaTrackingData instance) =>
<String, dynamic>{ <String, dynamic>{
'id': instance.id, 'id': instance.id,
'state': const MangaTrackingStateConverter().toJson(instance.state), 'state': const MediumTrackingStateConverter().toJson(instance.state),
'title': instance.title, 'title': instance.title,
'chaptersRead': instance.chaptersRead, 'chaptersRead': instance.chaptersRead,
'volumesOwned': instance.volumesOwned, 'volumesOwned': instance.volumesOwned,

View File

@ -1,5 +1,68 @@
import 'package:json_annotation/json_annotation.dart';
/// The type of medium we are tracking. Useful for UI stuff. /// The type of medium we are tracking. Useful for UI stuff.
enum TrackingMediumType { enum TrackingMediumType {
anime, anime,
manga, 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<MediumTrackingState, int> {
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();
}

View File

@ -47,7 +47,7 @@ class DatabaseService {
return animes return animes
.cast<Map<String, dynamic>>() .cast<Map<String, dynamic>>()
.map((Map<String, dynamic> anime) => AnimeTrackingData.fromJson(anime)) .map(AnimeTrackingData.fromJson)
.toList(); .toList();
} }
@ -56,7 +56,7 @@ class DatabaseService {
return mangas return mangas
.cast<Map<String, dynamic>>() .cast<Map<String, dynamic>>()
.map((Map<String, dynamic> manga) => MangaTrackingData.fromJson(manga)) .map(MangaTrackingData.fromJson)
.toList(); .toList();
} }

View File

@ -1,4 +1,3 @@
import 'dart:math';
import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/manga.dart'; import 'package:anitrack/src/data/manga.dart';
import 'package:anitrack/src/data/type.dart'; import 'package:anitrack/src/data/type.dart';

View File

@ -18,8 +18,10 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$AnimeListState { mixin _$AnimeListState {
List<AnimeTrackingData> get animes => throw _privateConstructorUsedError; List<AnimeTrackingData> get animes => throw _privateConstructorUsedError;
List<MangaTrackingData> get mangas => throw _privateConstructorUsedError; List<MangaTrackingData> get mangas => throw _privateConstructorUsedError;
AnimeTrackingState get animeFilterState => throw _privateConstructorUsedError; MediumTrackingState get animeFilterState =>
MangaTrackingState get mangaFilterState => throw _privateConstructorUsedError; throw _privateConstructorUsedError;
MediumTrackingState get mangaFilterState =>
throw _privateConstructorUsedError;
TrackingMediumType get trackingType => throw _privateConstructorUsedError; TrackingMediumType get trackingType => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
@ -35,8 +37,8 @@ abstract class $AnimeListStateCopyWith<$Res> {
$Res call( $Res call(
{List<AnimeTrackingData> animes, {List<AnimeTrackingData> animes,
List<MangaTrackingData> mangas, List<MangaTrackingData> mangas,
AnimeTrackingState animeFilterState, MediumTrackingState animeFilterState,
MangaTrackingState mangaFilterState, MediumTrackingState mangaFilterState,
TrackingMediumType trackingType}); TrackingMediumType trackingType});
} }
@ -69,11 +71,11 @@ class _$AnimeListStateCopyWithImpl<$Res>
animeFilterState: animeFilterState == freezed animeFilterState: animeFilterState == freezed
? _value.animeFilterState ? _value.animeFilterState
: animeFilterState // ignore: cast_nullable_to_non_nullable : animeFilterState // ignore: cast_nullable_to_non_nullable
as AnimeTrackingState, as MediumTrackingState,
mangaFilterState: mangaFilterState == freezed mangaFilterState: mangaFilterState == freezed
? _value.mangaFilterState ? _value.mangaFilterState
: mangaFilterState // ignore: cast_nullable_to_non_nullable : mangaFilterState // ignore: cast_nullable_to_non_nullable
as MangaTrackingState, as MediumTrackingState,
trackingType: trackingType == freezed trackingType: trackingType == freezed
? _value.trackingType ? _value.trackingType
: trackingType // ignore: cast_nullable_to_non_nullable : trackingType // ignore: cast_nullable_to_non_nullable
@ -92,8 +94,8 @@ abstract class _$$_AnimeListStateCopyWith<$Res>
$Res call( $Res call(
{List<AnimeTrackingData> animes, {List<AnimeTrackingData> animes,
List<MangaTrackingData> mangas, List<MangaTrackingData> mangas,
AnimeTrackingState animeFilterState, MediumTrackingState animeFilterState,
MangaTrackingState mangaFilterState, MediumTrackingState mangaFilterState,
TrackingMediumType trackingType}); TrackingMediumType trackingType});
} }
@ -128,11 +130,11 @@ class __$$_AnimeListStateCopyWithImpl<$Res>
animeFilterState: animeFilterState == freezed animeFilterState: animeFilterState == freezed
? _value.animeFilterState ? _value.animeFilterState
: animeFilterState // ignore: cast_nullable_to_non_nullable : animeFilterState // ignore: cast_nullable_to_non_nullable
as AnimeTrackingState, as MediumTrackingState,
mangaFilterState: mangaFilterState == freezed mangaFilterState: mangaFilterState == freezed
? _value.mangaFilterState ? _value.mangaFilterState
: mangaFilterState // ignore: cast_nullable_to_non_nullable : mangaFilterState // ignore: cast_nullable_to_non_nullable
as MangaTrackingState, as MediumTrackingState,
trackingType: trackingType == freezed trackingType: trackingType == freezed
? _value.trackingType ? _value.trackingType
: trackingType // ignore: cast_nullable_to_non_nullable : trackingType // ignore: cast_nullable_to_non_nullable
@ -147,8 +149,8 @@ class _$_AnimeListState implements _AnimeListState {
_$_AnimeListState( _$_AnimeListState(
{final List<AnimeTrackingData> animes = const [], {final List<AnimeTrackingData> animes = const [],
final List<MangaTrackingData> mangas = const [], final List<MangaTrackingData> mangas = const [],
this.animeFilterState = AnimeTrackingState.watching, this.animeFilterState = MediumTrackingState.ongoing,
this.mangaFilterState = MangaTrackingState.reading, this.mangaFilterState = MediumTrackingState.ongoing,
this.trackingType = TrackingMediumType.anime}) this.trackingType = TrackingMediumType.anime})
: _animes = animes, : _animes = animes,
_mangas = mangas; _mangas = mangas;
@ -171,10 +173,10 @@ class _$_AnimeListState implements _AnimeListState {
@override @override
@JsonKey() @JsonKey()
final AnimeTrackingState animeFilterState; final MediumTrackingState animeFilterState;
@override @override
@JsonKey() @JsonKey()
final MangaTrackingState mangaFilterState; final MediumTrackingState mangaFilterState;
@override @override
@JsonKey() @JsonKey()
final TrackingMediumType trackingType; final TrackingMediumType trackingType;
@ -218,8 +220,8 @@ abstract class _AnimeListState implements AnimeListState {
factory _AnimeListState( factory _AnimeListState(
{final List<AnimeTrackingData> animes, {final List<AnimeTrackingData> animes,
final List<MangaTrackingData> mangas, final List<MangaTrackingData> mangas,
final AnimeTrackingState animeFilterState, final MediumTrackingState animeFilterState,
final MangaTrackingState mangaFilterState, final MediumTrackingState mangaFilterState,
final TrackingMediumType trackingType}) = _$_AnimeListState; final TrackingMediumType trackingType}) = _$_AnimeListState;
@override @override
@ -227,9 +229,9 @@ abstract class _AnimeListState implements AnimeListState {
@override @override
List<MangaTrackingData> get mangas; List<MangaTrackingData> get mangas;
@override @override
AnimeTrackingState get animeFilterState; MediumTrackingState get animeFilterState;
@override @override
MangaTrackingState get mangaFilterState; MediumTrackingState get mangaFilterState;
@override @override
TrackingMediumType get trackingType; TrackingMediumType get trackingType;
@override @override

View File

@ -31,7 +31,7 @@ class AnimeFilterChangedEvent extends AnimeListEvent {
AnimeFilterChangedEvent(this.filterState); AnimeFilterChangedEvent(this.filterState);
/// The state to filter /// The state to filter
final AnimeTrackingState filterState; final MediumTrackingState filterState;
} }
/// Triggered when the view is changed from the anime or the manga view /// Triggered when the view is changed from the anime or the manga view
@ -60,7 +60,7 @@ class MangaFilterChangedEvent extends AnimeListEvent {
MangaFilterChangedEvent(this.filterState); MangaFilterChangedEvent(this.filterState);
/// The state to filter /// The state to filter
final MangaTrackingState filterState; final MediumTrackingState filterState;
} }
class MangaChapterIncrementedEvent extends AnimeListEvent { class MangaChapterIncrementedEvent extends AnimeListEvent {

View File

@ -5,8 +5,8 @@ class AnimeListState with _$AnimeListState {
factory AnimeListState({ factory AnimeListState({
@Default([]) List<AnimeTrackingData> animes, @Default([]) List<AnimeTrackingData> animes,
@Default([]) List<MangaTrackingData> mangas, @Default([]) List<MangaTrackingData> mangas,
@Default(AnimeTrackingState.watching) AnimeTrackingState animeFilterState, @Default(MediumTrackingState.ongoing) MediumTrackingState animeFilterState,
@Default(MangaTrackingState.reading) MangaTrackingState mangaFilterState, @Default(MediumTrackingState.ongoing) MediumTrackingState mangaFilterState,
@Default(TrackingMediumType.anime) TrackingMediumType trackingType, @Default(TrackingMediumType.anime) TrackingMediumType trackingType,
}) = _AnimeListState; }) = _AnimeListState;
} }

View File

@ -2,9 +2,9 @@ import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/manga.dart'; import 'package:anitrack/src/data/manga.dart';
import 'package:anitrack/src/data/search_result.dart'; import 'package:anitrack/src/data/search_result.dart';
import 'package:anitrack/src/data/type.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/anime_list_bloc.dart' as list;
import 'package:anitrack/src/ui/bloc/navigation_bloc.dart'; import 'package:anitrack/src/ui/bloc/navigation_bloc.dart';
import 'package:anitrack/src/ui/constants.dart';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@ -34,7 +34,7 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent( PushedNamedEvent(
NavigationDestination(animeSearchRoute), const NavigationDestination(animeSearchRoute),
), ),
); );
} }
@ -101,7 +101,7 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
list.AnimeAddedEvent( list.AnimeAddedEvent(
AnimeTrackingData( AnimeTrackingData(
event.result.id, event.result.id,
AnimeTrackingState.watching, MediumTrackingState.ongoing,
event.result.title, event.result.title,
0, 0,
event.result.total, event.result.total,
@ -111,14 +111,14 @@ class AnimeSearchBloc extends Bloc<AnimeSearchEvent, AnimeSearchState> {
list.MangaAddedEvent( list.MangaAddedEvent(
MangaTrackingData( MangaTrackingData(
event.result.id, event.result.id,
MangaTrackingState.reading, MediumTrackingState.ongoing,
event.result.title, event.result.title,
0, 0,
0, 0,
event.result.total, event.result.total,
event.result.thumbnailUrl, event.result.thumbnailUrl,
), ),
) ),
); );
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(

View File

@ -1,10 +1,10 @@
import 'package:anitrack/src/data/anime.dart'; import 'package:anitrack/src/data/anime.dart';
import 'package:anitrack/src/data/manga.dart'; import 'package:anitrack/src/data/manga.dart';
import 'package:anitrack/src/data/type.dart'; import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/service/database.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.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/bloc/navigation_bloc.dart';
import 'package:anitrack/src/ui/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
import 'package:anitrack/src/service/database.dart';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@ -30,9 +30,7 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent( PushedNamedEvent(
NavigationDestination( const NavigationDestination(detailsRoute),
detailsRoute,
),
), ),
); );
} }
@ -47,9 +45,7 @@ class DetailsBloc extends Bloc<DetailsEvent, DetailsState> {
GetIt.I.get<NavigationBloc>().add( GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent( PushedNamedEvent(
NavigationDestination( const NavigationDestination(detailsRoute),
detailsRoute,
),
), ),
); );
} }

View File

@ -1,6 +1,5 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'navigation_event.dart'; part 'navigation_event.dart';
part 'navigation_state.dart'; part 'navigation_state.dart';

View File

@ -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/data/type.dart';
import 'package:anitrack/src/ui/bloc/anime_list_bloc.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/anime_search_bloc.dart';
@ -11,6 +9,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class AnimeListPage extends StatelessWidget { class AnimeListPage extends StatelessWidget {
AnimeListPage({
super.key,
});
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>( static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
builder: (_) => AnimeListPage(), builder: (_) => AnimeListPage(),
settings: const RouteSettings( settings: const RouteSettings(
@ -27,11 +29,36 @@ class AnimeListPage extends StatelessWidget {
} }
} }
List<PopupMenuItem<MediumTrackingState>> _getPopupButtonItems(TrackingMediumType type) {
return [
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.ongoing,
child: Text(MediumTrackingState.ongoing.toNameString(type)),
),
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.completed,
child: Text(MediumTrackingState.completed.toNameString(type)),
),
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.planned,
child: Text(MediumTrackingState.planned.toNameString(type)),
),
PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.dropped,
child: Text(MediumTrackingState.dropped.toNameString(type)),
),
const PopupMenuItem<MediumTrackingState>(
value: MediumTrackingState.all,
child: Text('All'),
),
];
}
Widget _getPopupButton(BuildContext context, AnimeListState state) { Widget _getPopupButton(BuildContext context, AnimeListState state) {
switch (state.trackingType) { switch (state.trackingType) {
case TrackingMediumType.anime: case TrackingMediumType.anime:
return PopupMenuButton( return PopupMenuButton(
icon: Icon( icon: const Icon(
Icons.filter_list, Icons.filter_list,
), ),
initialValue: state.animeFilterState, initialValue: state.animeFilterState,
@ -40,32 +67,11 @@ class AnimeListPage extends StatelessWidget {
AnimeFilterChangedEvent(filterState), AnimeFilterChangedEvent(filterState),
); );
}, },
itemBuilder: (_) => [ itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.anime),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.watching,
child: Text('Watching'),
),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.completed,
child: Text('Completed'),
),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.planToWatch,
child: Text('Plan to watch'),
),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.dropped,
child: Text('Dropped'),
),
const PopupMenuItem<AnimeTrackingState>(
value: AnimeTrackingState.all,
child: Text('All'),
),
],
); );
case TrackingMediumType.manga: case TrackingMediumType.manga:
return PopupMenuButton( return PopupMenuButton(
icon: Icon( icon: const Icon(
Icons.filter_list, Icons.filter_list,
), ),
initialValue: state.mangaFilterState, initialValue: state.mangaFilterState,
@ -74,28 +80,7 @@ class AnimeListPage extends StatelessWidget {
MangaFilterChangedEvent(filterState), MangaFilterChangedEvent(filterState),
); );
}, },
itemBuilder: (_) => [ itemBuilder: (_) => _getPopupButtonItems(TrackingMediumType.manga),
const PopupMenuItem<MangaTrackingState>(
value: MangaTrackingState.reading,
child: Text('Reading'),
),
const PopupMenuItem<MangaTrackingState>(
value: MangaTrackingState.completed,
child: Text('Completed'),
),
const PopupMenuItem<MangaTrackingState>(
value: MangaTrackingState.planToRead,
child: Text('Plan to read'),
),
const PopupMenuItem<MangaTrackingState>(
value: MangaTrackingState.dropped,
child: Text('Dropped'),
),
const PopupMenuItem<MangaTrackingState>(
value: MangaTrackingState.all,
child: Text('All'),
),
],
); );
} }
} }
@ -107,7 +92,7 @@ class AnimeListPage extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
_getPageTitle(state.trackingType) _getPageTitle(state.trackingType),
), ),
actions: [ actions: [
_getPopupButton(context, state), _getPopupButton(context, state),
@ -120,7 +105,7 @@ class AnimeListPage extends StatelessWidget {
itemCount: state.animes.length, itemCount: state.animes.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final anime = state.animes[index]; final anime = state.animes[index];
if (state.animeFilterState != AnimeTrackingState.all) { if (state.animeFilterState != MediumTrackingState.all) {
if (anime.state != state.animeFilterState) return Container(); if (anime.state != state.animeFilterState) return Container();
} }
@ -157,7 +142,7 @@ class AnimeListPage extends StatelessWidget {
itemCount: state.mangas.length, itemCount: state.mangas.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final manga = state.mangas[index]; final manga = state.mangas[index];
if (state.mangaFilterState != MangaTrackingState.all) { if (state.mangaFilterState != MediumTrackingState.all) {
if (manga.state != state.mangaFilterState) return Container(); if (manga.state != state.mangaFilterState) return Container();
} }
@ -216,7 +201,7 @@ class AnimeListPage extends StatelessWidget {
_controller.jumpToPage(index); _controller.jumpToPage(index);
}, },
items: <BottomBarItem>[ items: const [
BottomBarItem( BottomBarItem(
icon: Icon(Icons.tv), icon: Icon(Icons.tv),
title: Text('Anime'), title: Text('Anime'),

View File

@ -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/data/type.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/constants.dart'; import 'package:anitrack/src/ui/constants.dart';
import 'package:anitrack/src/ui/widgets/list_item.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 { class AnimeSearchPage extends StatelessWidget {
const AnimeSearchPage({
super.key,
});
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>( static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
builder: (_) => AnimeSearchPage(), builder: (_) => const AnimeSearchPage(),
settings: const RouteSettings( settings: const RouteSettings(
name: animeSearchRoute, name: animeSearchRoute,
), ),
@ -32,7 +34,7 @@ class AnimeSearchPage extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: TextField( child: TextField(
decoration: InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
labelText: 'Search query', labelText: 'Search query',
), ),
@ -50,9 +52,8 @@ class AnimeSearchPage extends StatelessWidget {
), ),
if (state.working) if (state.working)
Expanded( const Expanded(
child: Align( child: Align(
alignment: Alignment.center,
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
) )
@ -86,7 +87,7 @@ class AnimeSearchPage extends StatelessWidget {
], ],
), ),
); );
} },
), ),
), ),
], ],

View File

@ -3,44 +3,28 @@ import 'package:anitrack/src/data/manga.dart';
import 'package:anitrack/src/data/type.dart'; import 'package:anitrack/src/data/type.dart';
import 'package:anitrack/src/ui/bloc/details_bloc.dart'; import 'package:anitrack/src/ui/bloc/details_bloc.dart';
import 'package:anitrack/src/ui/constants.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/image.dart';
import 'package:anitrack/src/ui/widgets/list_item.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';
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 { class DetailsPage extends StatelessWidget {
const DetailsPage({
super.key,
});
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>( static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
builder: (_) => DetailsPage(), builder: (_) => const DetailsPage(),
settings: const RouteSettings( settings: const RouteSettings(
name: detailsRoute, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Details'), title: const Text('Details'),
), ),
body: BlocBuilder<DetailsBloc, DetailsState>( body: BlocBuilder<DetailsBloc, DetailsState>(
builder: (context, state) { builder: (context, state) {
@ -69,101 +53,63 @@ class DetailsPage extends StatelessWidget {
children: [ children: [
Text( Text(
state.trackingType == TrackingMediumType.anime ? state.trackingType == TrackingMediumType.anime ?
(state.data as AnimeTrackingData).title : (state.data as AnimeTrackingData).title :
(state.data as MangaTrackingData).title, (state.data as MangaTrackingData).title,
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
maxLines: 2, maxLines: 2,
softWrap: true, softWrap: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
TextButton(
child: Text(
_getTrackingStateText(state),
),
onPressed: () async {
final result = await showModalBottomSheet<dynamic>(
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<DetailsBloc>().add(
DetailsUpdatedEvent(
(state.data as AnimeTrackingData).copyWith(
state: result as AnimeTrackingState,
),
),
);
} else {
context.read<DetailsBloc>().add(
DetailsUpdatedEvent(
(state.data as MangaTrackingData).copyWith(
state: result as MangaTrackingState,
),
),
);
}
},
),
], ],
), ),
), ),
], ],
), ),
DropdownSelector<MediumTrackingState>(
title: 'Watchstate',
onChanged: (MediumTrackingState newState) {
if (state.trackingType == TrackingMediumType.anime) {
context.read<DetailsBloc>().add(
DetailsUpdatedEvent(
(state.data as AnimeTrackingData).copyWith(
state: newState,
),
),
);
} else if (state.trackingType == TrackingMediumType.manga) {
context.read<DetailsBloc>().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,
),
], ],
), ),
); );

View File

@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
class SelectorItem<T> {
const SelectorItem(this.value, this.text);
final T value;
final String text;
}
class DropdownSelector<T> 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<SelectorItem<T>> values;
final T initialValue;
/// Called when the selection has changed
final void Function(T) onChanged;
@override
DropdownSelectorState<T> createState() => DropdownSelectorState<T>();
}
class DropdownSelectorDialog<T> extends StatelessWidget {
const DropdownSelectorDialog({
required this.values,
super.key,
});
final List<SelectorItem<T>> 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<T> extends State<DropdownSelector<T>> {
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<T>(
context: context,
builder: (context) => DropdownSelectorDialog<T>(
values: widget.values.cast<SelectorItem<T>>(),
),
);
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,
),
],
),
),
),
),
),
],
);
}
}

View File

@ -9,6 +9,7 @@ class AnimeCoverImage extends StatelessWidget {
/// The URL to the cover image. /// The URL to the cover image.
final String url; final String url;
@override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),

View File

@ -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/image.dart';
import 'package:anitrack/src/ui/widgets/swipe_icon.dart'; import 'package:anitrack/src/ui/widgets/swipe_icon.dart';
import 'package:flutter/material.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 /// A widget for displaying simple data about an anime in the listview
class ListItem extends StatelessWidget { class ListItem extends StatelessWidget {
ListItem({ const ListItem({
required this.thumbnailUrl, required this.thumbnailUrl,
required this.title, required this.title,
this.onLeftSwipe, this.onLeftSwipe,
@ -42,14 +41,14 @@ class ListItem extends StatelessWidget {
key: UniqueKey(), key: UniqueKey(),
backgroundBuilder: (_, direction, __) { backgroundBuilder: (_, direction, __) {
if (direction == SwipeDirection.endToStart) { if (direction == SwipeDirection.endToStart) {
return Align( return const Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: SwipeIcon( child: SwipeIcon(
Icons.add, Icons.add,
), ),
); );
} else if (direction == SwipeDirection.startToEnd) { } else if (direction == SwipeDirection.startToEnd) {
return Align( return const Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: SwipeIcon( child: SwipeIcon(
Icons.remove, Icons.remove,

View File

@ -576,6 +576,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" 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: watcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -23,12 +23,13 @@ dependencies:
dev_dependencies: dev_dependencies:
build_runner: ^2.1.11 build_runner: ^2.1.11
flutter_test:
sdk: flutter
flutter_launcher_icons: ^0.11.0 flutter_launcher_icons: ^0.11.0
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
flutter_test:
sdk: flutter
freezed: ^2.1.0+1 freezed: ^2.1.0+1
json_serializable: ^6.3.1 json_serializable: ^6.3.1
very_good_analysis: ^3.0.1
flutter: flutter:
uses-material-design: true uses-material-design: true