feat(ui): Hide the button when scrolled to the bottom
This commit is contained in:
parent
99021f2668
commit
d38ee1692b
36
flake.lock
36
flake.lock
@ -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",
|
||||
|
@ -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";
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,7 +22,37 @@ class AnimeListPage extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
|
||||
@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) {
|
||||
@ -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,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: () {
|
||||
context.read<AnimeSearchBloc>().add(
|
||||
AnimeSearchRequestedEvent(state.trackingType),
|
||||
);
|
||||
},
|
||||
tooltip: 'Increment',
|
||||
tooltip: 'Add new item',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: BottomBar(
|
||||
selectedIndex: state.trackingType == TrackingMediumType.anime ?
|
||||
0 :
|
||||
|
@ -36,6 +36,15 @@ class IntegerInputState extends State<IntegerInput> {
|
||||
_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) {
|
||||
return Row(
|
||||
@ -56,6 +65,13 @@ class IntegerInputState extends State<IntegerInput> {
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Focus(
|
||||
onFocusChange: (hasFocus) {
|
||||
if (!hasFocus) {
|
||||
print('Handle focus loss');
|
||||
_handleSubmit(_controller.text);
|
||||
}
|
||||
},
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
@ -64,14 +80,8 @@ class IntegerInputState extends State<IntegerInput> {
|
||||
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);
|
||||
},
|
||||
onSubmitted: _handleSubmit,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
372
pubspec.lock
372
pubspec.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user