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": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
@ -17,16 +20,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1669165918,
"narHash": "sha256-hIVruk2+0wmw/Kfzy11rG3q7ev3VTi/IKVODeHcVjFo=",
"owner": "NixOS",
"lastModified": 1676076353,
"narHash": "sha256-mdUtE8Tp40cZETwcq5tCwwLqkJVV1ULJQ5GKRtbshag=",
"owner": "AtaraxiaSjel",
"repo": "nixpkgs",
"rev": "3b400a525d92e4085e46141ff48cbf89fd89739e",
"rev": "5deb99bdccbbb97e7562dee4ba8a3ee3021688e6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"owner": "AtaraxiaSjel",
"ref": "update/flutter",
"repo": "nixpkgs",
"type": "github"
}
@ -36,6 +39,21 @@
"flake-utils": "flake-utils",
"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",

View File

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

View File

@ -26,6 +26,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
on<MangaUpdatedEvent>(_onMangaUpdated);
on<AnimeRemovedEvent>(_onAnimeRemoved);
on<MangaRemovedEvent>(_onMangaRemoved);
on<AddButtonVisibilitySetEvent>(_onButtonVisibilityToggled);
}
/// Internal anime state
@ -162,6 +163,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
emit(
state.copyWith(
trackingType: event.type,
buttonVisibility: true,
),
);
}
@ -281,4 +283,12 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
// Update the database
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
mixin _$AnimeListState {
bool get buttonVisibility => throw _privateConstructorUsedError;
List<AnimeTrackingData> get animes => throw _privateConstructorUsedError;
List<MangaTrackingData> get mangas => throw _privateConstructorUsedError;
MediumTrackingState get animeFilterState =>
@ -35,7 +36,8 @@ abstract class $AnimeListStateCopyWith<$Res> {
AnimeListState value, $Res Function(AnimeListState) then) =
_$AnimeListStateCopyWithImpl<$Res>;
$Res call(
{List<AnimeTrackingData> animes,
{bool buttonVisibility,
List<AnimeTrackingData> animes,
List<MangaTrackingData> mangas,
MediumTrackingState animeFilterState,
MediumTrackingState mangaFilterState,
@ -53,6 +55,7 @@ class _$AnimeListStateCopyWithImpl<$Res>
@override
$Res call({
Object? buttonVisibility = freezed,
Object? animes = freezed,
Object? mangas = freezed,
Object? animeFilterState = freezed,
@ -60,6 +63,10 @@ class _$AnimeListStateCopyWithImpl<$Res>
Object? trackingType = freezed,
}) {
return _then(_value.copyWith(
buttonVisibility: buttonVisibility == freezed
? _value.buttonVisibility
: buttonVisibility // ignore: cast_nullable_to_non_nullable
as bool,
animes: animes == freezed
? _value.animes
: animes // ignore: cast_nullable_to_non_nullable
@ -92,7 +99,8 @@ abstract class _$$_AnimeListStateCopyWith<$Res>
__$$_AnimeListStateCopyWithImpl<$Res>;
@override
$Res call(
{List<AnimeTrackingData> animes,
{bool buttonVisibility,
List<AnimeTrackingData> animes,
List<MangaTrackingData> mangas,
MediumTrackingState animeFilterState,
MediumTrackingState mangaFilterState,
@ -112,6 +120,7 @@ class __$$_AnimeListStateCopyWithImpl<$Res>
@override
$Res call({
Object? buttonVisibility = freezed,
Object? animes = freezed,
Object? mangas = freezed,
Object? animeFilterState = freezed,
@ -119,6 +128,10 @@ class __$$_AnimeListStateCopyWithImpl<$Res>
Object? trackingType = freezed,
}) {
return _then(_$_AnimeListState(
buttonVisibility: buttonVisibility == freezed
? _value.buttonVisibility
: buttonVisibility // ignore: cast_nullable_to_non_nullable
as bool,
animes: animes == freezed
? _value._animes
: animes // ignore: cast_nullable_to_non_nullable
@ -147,7 +160,8 @@ class __$$_AnimeListStateCopyWithImpl<$Res>
class _$_AnimeListState implements _AnimeListState {
_$_AnimeListState(
{final List<AnimeTrackingData> animes = const [],
{this.buttonVisibility = true,
final List<AnimeTrackingData> animes = const [],
final List<MangaTrackingData> mangas = const [],
this.animeFilterState = MediumTrackingState.ongoing,
this.mangaFilterState = MediumTrackingState.ongoing,
@ -155,6 +169,9 @@ class _$_AnimeListState implements _AnimeListState {
: _animes = animes,
_mangas = mangas;
@override
@JsonKey()
final bool buttonVisibility;
final List<AnimeTrackingData> _animes;
@override
@JsonKey()
@ -183,7 +200,7 @@ class _$_AnimeListState implements _AnimeListState {
@override
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
@ -191,6 +208,8 @@ class _$_AnimeListState implements _AnimeListState {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_AnimeListState &&
const DeepCollectionEquality()
.equals(other.buttonVisibility, buttonVisibility) &&
const DeepCollectionEquality().equals(other._animes, _animes) &&
const DeepCollectionEquality().equals(other._mangas, _mangas) &&
const DeepCollectionEquality()
@ -204,6 +223,7 @@ class _$_AnimeListState implements _AnimeListState {
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(buttonVisibility),
const DeepCollectionEquality().hash(_animes),
const DeepCollectionEquality().hash(_mangas),
const DeepCollectionEquality().hash(animeFilterState),
@ -218,12 +238,15 @@ class _$_AnimeListState implements _AnimeListState {
abstract class _AnimeListState implements AnimeListState {
factory _AnimeListState(
{final List<AnimeTrackingData> animes,
{final bool buttonVisibility,
final List<AnimeTrackingData> animes,
final List<MangaTrackingData> mangas,
final MediumTrackingState animeFilterState,
final MediumTrackingState mangaFilterState,
final TrackingMediumType trackingType}) = _$_AnimeListState;
@override
bool get buttonVisibility;
@override
List<AnimeTrackingData> get animes;
@override

View File

@ -96,3 +96,10 @@ class MangaRemovedEvent extends AnimeListEvent {
/// The ID of the manga to be removed from the list.
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
class AnimeListState with _$AnimeListState {
factory AnimeListState({
@Default(true) bool buttonVisibility,
@Default([]) List<AnimeTrackingData> animes,
@Default([]) List<MangaTrackingData> mangas,
@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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
class AnimeListPage extends StatelessWidget {
AnimeListPage({
class AnimeListPage extends StatefulWidget {
const AnimeListPage({
super.key,
});
@ -21,8 +22,38 @@ class AnimeListPage extends StatelessWidget {
),
);
final PageController _controller = PageController();
@override
AnimeListPageState createState() => AnimeListPageState();
}
class AnimeListPageState extends State<AnimeListPage> {
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) {
switch (type) {
case TrackingMediumType.anime: return 'Anime';
@ -141,6 +172,7 @@ class AnimeListPage extends StatelessWidget {
childAspectRatio: 120 / (100 * (16 / 9)),
),
itemCount: state.animes.length,
controller: _animeScrollController,
itemBuilder: (context, index) {
final anime = state.animes[index];
return GridItem(
@ -233,14 +265,26 @@ class AnimeListPage extends StatelessWidget {
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<AnimeSearchBloc>().add(
AnimeSearchRequestedEvent(state.trackingType),
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: () {
context.read<AnimeSearchBloc>().add(
AnimeSearchRequestedEvent(state.trackingType),
);
},
tooltip: 'Add new item',
child: const Icon(Icons.add),
),
);
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
bottomNavigationBar: BottomBar(
selectedIndex: state.trackingType == TrackingMediumType.anime ?

View File

@ -35,6 +35,15 @@ class IntegerInputState extends State<IntegerInput> {
_value = widget.initialValue;
_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
Widget build(BuildContext context) {
@ -56,22 +65,23 @@ class IntegerInputState extends State<IntegerInput> {
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
child: TextField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: widget.labelText,
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
controller: _controller,
onSubmitted: (text) {
final value = int.parse(text);
if (value < 0) return;
_value = value;
_controller.text = '$_value';
widget.onChanged(_value);
child: Focus(
onFocusChange: (hasFocus) {
if (!hasFocus) {
print('Handle focus loss');
_handleSubmit(_controller.text);
}
},
child: TextField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: widget.labelText,
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
controller: _controller,
onSubmitted: _handleSubmit,
),
),
),
),

File diff suppressed because it is too large Load Diff