feat(ui): Hide the button when scrolled to the bottom

This commit is contained in:
PapaTutuWawa 2023-04-12 16:11:05 +02:00
parent 99021f2668
commit d38ee1692b
9 changed files with 396 additions and 167 deletions

View File

@ -1,12 +1,15 @@
{ {
"nodes": { "nodes": {
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": { "locked": {
"lastModified": 1667395993, "lastModified": 1681202837,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -17,16 +20,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1669165918, "lastModified": 1676076353,
"narHash": "sha256-hIVruk2+0wmw/Kfzy11rG3q7ev3VTi/IKVODeHcVjFo=", "narHash": "sha256-mdUtE8Tp40cZETwcq5tCwwLqkJVV1ULJQ5GKRtbshag=",
"owner": "NixOS", "owner": "AtaraxiaSjel",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3b400a525d92e4085e46141ff48cbf89fd89739e", "rev": "5deb99bdccbbb97e7562dee4ba8a3ee3021688e6",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "AtaraxiaSjel",
"ref": "nixpkgs-unstable", "ref": "update/flutter",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@ -36,6 +39,21 @@
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@ -1,7 +1,7 @@
{ {
description = "AniTrack"; description = "AniTrack";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:AtaraxiaSjel/nixpkgs/update/flutter";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
}; };

View File

@ -26,6 +26,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
on<MangaUpdatedEvent>(_onMangaUpdated); on<MangaUpdatedEvent>(_onMangaUpdated);
on<AnimeRemovedEvent>(_onAnimeRemoved); on<AnimeRemovedEvent>(_onAnimeRemoved);
on<MangaRemovedEvent>(_onMangaRemoved); on<MangaRemovedEvent>(_onMangaRemoved);
on<AddButtonVisibilitySetEvent>(_onButtonVisibilityToggled);
} }
/// Internal anime state /// Internal anime state
@ -162,6 +163,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
emit( emit(
state.copyWith( state.copyWith(
trackingType: event.type, trackingType: event.type,
buttonVisibility: true,
), ),
); );
} }
@ -281,4 +283,12 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
// Update the database // Update the database
await GetIt.I.get<DatabaseService>().deleteManga(event.id); await GetIt.I.get<DatabaseService>().deleteManga(event.id);
} }
Future<void> _onButtonVisibilityToggled(AddButtonVisibilitySetEvent event, Emitter<AnimeListState> emit) async {
emit(
state.copyWith(
buttonVisibility: event.state,
),
);
}
} }

View File

@ -16,6 +16,7 @@ final _privateConstructorUsedError = UnsupportedError(
/// @nodoc /// @nodoc
mixin _$AnimeListState { mixin _$AnimeListState {
bool get buttonVisibility => throw _privateConstructorUsedError;
List<AnimeTrackingData> get animes => throw _privateConstructorUsedError; List<AnimeTrackingData> get animes => throw _privateConstructorUsedError;
List<MangaTrackingData> get mangas => throw _privateConstructorUsedError; List<MangaTrackingData> get mangas => throw _privateConstructorUsedError;
MediumTrackingState get animeFilterState => MediumTrackingState get animeFilterState =>
@ -35,7 +36,8 @@ abstract class $AnimeListStateCopyWith<$Res> {
AnimeListState value, $Res Function(AnimeListState) then) = AnimeListState value, $Res Function(AnimeListState) then) =
_$AnimeListStateCopyWithImpl<$Res>; _$AnimeListStateCopyWithImpl<$Res>;
$Res call( $Res call(
{List<AnimeTrackingData> animes, {bool buttonVisibility,
List<AnimeTrackingData> animes,
List<MangaTrackingData> mangas, List<MangaTrackingData> mangas,
MediumTrackingState animeFilterState, MediumTrackingState animeFilterState,
MediumTrackingState mangaFilterState, MediumTrackingState mangaFilterState,
@ -53,6 +55,7 @@ class _$AnimeListStateCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? buttonVisibility = freezed,
Object? animes = freezed, Object? animes = freezed,
Object? mangas = freezed, Object? mangas = freezed,
Object? animeFilterState = freezed, Object? animeFilterState = freezed,
@ -60,6 +63,10 @@ class _$AnimeListStateCopyWithImpl<$Res>
Object? trackingType = freezed, Object? trackingType = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
buttonVisibility: buttonVisibility == freezed
? _value.buttonVisibility
: buttonVisibility // ignore: cast_nullable_to_non_nullable
as bool,
animes: animes == freezed animes: animes == freezed
? _value.animes ? _value.animes
: animes // ignore: cast_nullable_to_non_nullable : animes // ignore: cast_nullable_to_non_nullable
@ -92,7 +99,8 @@ abstract class _$$_AnimeListStateCopyWith<$Res>
__$$_AnimeListStateCopyWithImpl<$Res>; __$$_AnimeListStateCopyWithImpl<$Res>;
@override @override
$Res call( $Res call(
{List<AnimeTrackingData> animes, {bool buttonVisibility,
List<AnimeTrackingData> animes,
List<MangaTrackingData> mangas, List<MangaTrackingData> mangas,
MediumTrackingState animeFilterState, MediumTrackingState animeFilterState,
MediumTrackingState mangaFilterState, MediumTrackingState mangaFilterState,
@ -112,6 +120,7 @@ class __$$_AnimeListStateCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? buttonVisibility = freezed,
Object? animes = freezed, Object? animes = freezed,
Object? mangas = freezed, Object? mangas = freezed,
Object? animeFilterState = freezed, Object? animeFilterState = freezed,
@ -119,6 +128,10 @@ class __$$_AnimeListStateCopyWithImpl<$Res>
Object? trackingType = freezed, Object? trackingType = freezed,
}) { }) {
return _then(_$_AnimeListState( return _then(_$_AnimeListState(
buttonVisibility: buttonVisibility == freezed
? _value.buttonVisibility
: buttonVisibility // ignore: cast_nullable_to_non_nullable
as bool,
animes: animes == freezed animes: animes == freezed
? _value._animes ? _value._animes
: animes // ignore: cast_nullable_to_non_nullable : animes // ignore: cast_nullable_to_non_nullable
@ -147,7 +160,8 @@ class __$$_AnimeListStateCopyWithImpl<$Res>
class _$_AnimeListState implements _AnimeListState { class _$_AnimeListState implements _AnimeListState {
_$_AnimeListState( _$_AnimeListState(
{final List<AnimeTrackingData> animes = const [], {this.buttonVisibility = true,
final List<AnimeTrackingData> animes = const [],
final List<MangaTrackingData> mangas = const [], final List<MangaTrackingData> mangas = const [],
this.animeFilterState = MediumTrackingState.ongoing, this.animeFilterState = MediumTrackingState.ongoing,
this.mangaFilterState = MediumTrackingState.ongoing, this.mangaFilterState = MediumTrackingState.ongoing,
@ -155,6 +169,9 @@ class _$_AnimeListState implements _AnimeListState {
: _animes = animes, : _animes = animes,
_mangas = mangas; _mangas = mangas;
@override
@JsonKey()
final bool buttonVisibility;
final List<AnimeTrackingData> _animes; final List<AnimeTrackingData> _animes;
@override @override
@JsonKey() @JsonKey()
@ -183,7 +200,7 @@ class _$_AnimeListState implements _AnimeListState {
@override @override
String toString() { String toString() {
return 'AnimeListState(animes: $animes, mangas: $mangas, animeFilterState: $animeFilterState, mangaFilterState: $mangaFilterState, trackingType: $trackingType)'; return 'AnimeListState(buttonVisibility: $buttonVisibility, animes: $animes, mangas: $mangas, animeFilterState: $animeFilterState, mangaFilterState: $mangaFilterState, trackingType: $trackingType)';
} }
@override @override
@ -191,6 +208,8 @@ class _$_AnimeListState implements _AnimeListState {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$_AnimeListState && other is _$_AnimeListState &&
const DeepCollectionEquality()
.equals(other.buttonVisibility, buttonVisibility) &&
const DeepCollectionEquality().equals(other._animes, _animes) && const DeepCollectionEquality().equals(other._animes, _animes) &&
const DeepCollectionEquality().equals(other._mangas, _mangas) && const DeepCollectionEquality().equals(other._mangas, _mangas) &&
const DeepCollectionEquality() const DeepCollectionEquality()
@ -204,6 +223,7 @@ class _$_AnimeListState implements _AnimeListState {
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
const DeepCollectionEquality().hash(buttonVisibility),
const DeepCollectionEquality().hash(_animes), const DeepCollectionEquality().hash(_animes),
const DeepCollectionEquality().hash(_mangas), const DeepCollectionEquality().hash(_mangas),
const DeepCollectionEquality().hash(animeFilterState), const DeepCollectionEquality().hash(animeFilterState),
@ -218,12 +238,15 @@ class _$_AnimeListState implements _AnimeListState {
abstract class _AnimeListState implements AnimeListState { abstract class _AnimeListState implements AnimeListState {
factory _AnimeListState( factory _AnimeListState(
{final List<AnimeTrackingData> animes, {final bool buttonVisibility,
final List<AnimeTrackingData> animes,
final List<MangaTrackingData> mangas, final List<MangaTrackingData> mangas,
final MediumTrackingState animeFilterState, final MediumTrackingState animeFilterState,
final MediumTrackingState mangaFilterState, final MediumTrackingState mangaFilterState,
final TrackingMediumType trackingType}) = _$_AnimeListState; final TrackingMediumType trackingType}) = _$_AnimeListState;
@override
bool get buttonVisibility;
@override @override
List<AnimeTrackingData> get animes; List<AnimeTrackingData> get animes;
@override @override

View File

@ -96,3 +96,10 @@ class MangaRemovedEvent extends AnimeListEvent {
/// The ID of the manga to be removed from the list. /// The ID of the manga to be removed from the list.
final String id; final String id;
} }
class AddButtonVisibilitySetEvent extends AnimeListEvent {
AddButtonVisibilitySetEvent(this.state);
/// The visibility of the button
final bool state;
}

View File

@ -3,6 +3,7 @@ part of 'anime_list_bloc.dart';
@freezed @freezed
class AnimeListState with _$AnimeListState { class AnimeListState with _$AnimeListState {
factory AnimeListState({ factory AnimeListState({
@Default(true) bool buttonVisibility,
@Default([]) List<AnimeTrackingData> animes, @Default([]) List<AnimeTrackingData> animes,
@Default([]) List<MangaTrackingData> mangas, @Default([]) List<MangaTrackingData> mangas,
@Default(MediumTrackingState.ongoing) MediumTrackingState animeFilterState, @Default(MediumTrackingState.ongoing) MediumTrackingState animeFilterState,

View File

@ -8,9 +8,10 @@ import 'package:anitrack/src/ui/widgets/image.dart';
import 'package:bottom_bar/bottom_bar.dart'; import 'package:bottom_bar/bottom_bar.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';
class AnimeListPage extends StatelessWidget { class AnimeListPage extends StatefulWidget {
AnimeListPage({ const AnimeListPage({
super.key, super.key,
}); });
@ -21,7 +22,37 @@ class AnimeListPage extends StatelessWidget {
), ),
); );
@override
AnimeListPageState createState() => AnimeListPageState();
}
class AnimeListPageState extends State<AnimeListPage> {
final PageController _controller = PageController(); final PageController _controller = PageController();
final ScrollController _animeScrollController = ScrollController();
@override
void initState() {
super.initState();
_animeScrollController.addListener(_onAnimeListScrolled);
}
void _onAnimeListScrolled() {
//print(_animeScrollController.position.maxScrollExtent);
final bloc = GetIt.I.get<AnimeListBloc>();
if (_animeScrollController.offset + 20 >= _animeScrollController.position.maxScrollExtent) {
if (bloc.state.buttonVisibility) {
bloc.add(
AddButtonVisibilitySetEvent(false),
);
}
} else {
if (!bloc.state.buttonVisibility) {
bloc.add(
AddButtonVisibilitySetEvent(true),
);
}
}
}
String _getPageTitle(TrackingMediumType type) { String _getPageTitle(TrackingMediumType type) {
switch (type) { switch (type) {
@ -141,6 +172,7 @@ class AnimeListPage extends StatelessWidget {
childAspectRatio: 120 / (100 * (16 / 9)), childAspectRatio: 120 / (100 * (16 / 9)),
), ),
itemCount: state.animes.length, itemCount: state.animes.length,
controller: _animeScrollController,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final anime = state.animes[index]; final anime = state.animes[index];
return GridItem( return GridItem(
@ -233,15 +265,27 @@ class AnimeListPage extends StatelessWidget {
), ),
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: BlocBuilder<AnimeListBloc, AnimeListState>(
buildWhen: (prev, next) => prev.buttonVisibility != next.buttonVisibility,
builder: (context, state) {
return AnimatedScale(
duration: const Duration(milliseconds: 250),
scale: state.buttonVisibility ?
1 :
0,
curve: Curves.easeInOutQuint,
child: FloatingActionButton(
onPressed: () { onPressed: () {
context.read<AnimeSearchBloc>().add( context.read<AnimeSearchBloc>().add(
AnimeSearchRequestedEvent(state.trackingType), AnimeSearchRequestedEvent(state.trackingType),
); );
}, },
tooltip: 'Increment', tooltip: 'Add new item',
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),
);
},
),
bottomNavigationBar: BottomBar( bottomNavigationBar: BottomBar(
selectedIndex: state.trackingType == TrackingMediumType.anime ? selectedIndex: state.trackingType == TrackingMediumType.anime ?
0 : 0 :

View File

@ -36,6 +36,15 @@ class IntegerInputState extends State<IntegerInput> {
_controller.text = _value.toString(); _controller.text = _value.toString();
} }
void _handleSubmit(String text) {
final value = int.parse(text);
if (value < 0) return;
_value = value;
_controller.text = '$_value';
widget.onChanged(_value);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
@ -56,6 +65,13 @@ class IntegerInputState extends State<IntegerInput> {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
), ),
child: Focus(
onFocusChange: (hasFocus) {
if (!hasFocus) {
print('Handle focus loss');
_handleSubmit(_controller.text);
}
},
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
@ -64,14 +80,8 @@ class IntegerInputState extends State<IntegerInput> {
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
controller: _controller, controller: _controller,
onSubmitted: (text) { onSubmitted: _handleSubmit,
final value = int.parse(text); ),
if (value < 0) return;
_value = value;
_controller.text = '$_value';
widget.onChanged(_value);
},
), ),
), ),
), ),

File diff suppressed because it is too large Load Diff