feat(ui): Switch to a grid-based layout
This commit is contained in:
parent
4688924ec2
commit
bea3ff8b78
@ -28,32 +28,68 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
on<MangaRemovedEvent>(_onMangaRemoved);
|
on<MangaRemovedEvent>(_onMangaRemoved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Internal anime state
|
||||||
|
final List<AnimeTrackingData> _animes = List<AnimeTrackingData>.empty(growable: true);
|
||||||
|
final List<MangaTrackingData> _mangas = List<MangaTrackingData>.empty(growable: true);
|
||||||
|
|
||||||
|
List<AnimeTrackingData> _getFilteredAnime({MediumTrackingState? trackingState}) {
|
||||||
|
final filterState = trackingState ?? state.animeFilterState;
|
||||||
|
|
||||||
|
if (filterState == MediumTrackingState.all) return _animes;
|
||||||
|
|
||||||
|
return _animes
|
||||||
|
.where((anime) => anime.state == filterState)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MangaTrackingData> _getFilteredManga({MediumTrackingState? trackingState}) {
|
||||||
|
final filterState = trackingState ?? state.mangaFilterState;
|
||||||
|
|
||||||
|
if (state.mangaFilterState == MediumTrackingState.all) return _mangas;
|
||||||
|
|
||||||
|
return _mangas
|
||||||
|
.where((manga) => manga.state == filterState)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onAnimeAdded(AnimeAddedEvent event, Emitter<AnimeListState> emit) async {
|
Future<void> _onAnimeAdded(AnimeAddedEvent event, Emitter<AnimeListState> emit) async {
|
||||||
// Add the anime to the database
|
// Add the anime to the database
|
||||||
await GetIt.I.get<DatabaseService>().addAnime(event.data);
|
await GetIt.I.get<DatabaseService>().addAnime(event.data);
|
||||||
|
|
||||||
emit(
|
// Add it to the cache
|
||||||
state.copyWith(
|
_animes.add(event.data);
|
||||||
animes: List.from([
|
|
||||||
...state.animes,
|
if (event.data.state == state.animeFilterState ||
|
||||||
event.data,
|
state.animeFilterState == MediumTrackingState.all) {
|
||||||
]),
|
emit(
|
||||||
),
|
state.copyWith(
|
||||||
);
|
animes: List.from([
|
||||||
|
...state.animes,
|
||||||
|
event.data,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onMangaAdded(MangaAddedEvent event, Emitter<AnimeListState> emit) async {
|
Future<void> _onMangaAdded(MangaAddedEvent event, Emitter<AnimeListState> emit) async {
|
||||||
// Add the manga to the database
|
// Add the manga to the database
|
||||||
await GetIt.I.get<DatabaseService>().addManga(event.data);
|
await GetIt.I.get<DatabaseService>().addManga(event.data);
|
||||||
|
|
||||||
emit(
|
// Add it to the cache
|
||||||
state.copyWith(
|
_mangas.add(event.data);
|
||||||
mangas: List.from([
|
|
||||||
...state.mangas,
|
if (event.data.state == state.mangaFilterState ||
|
||||||
event.data,
|
state.mangaFilterState == MediumTrackingState.all) {
|
||||||
]),
|
emit(
|
||||||
),
|
state.copyWith(
|
||||||
);
|
mangas: List.from([
|
||||||
|
...state.mangas,
|
||||||
|
event.data,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAnimeIncremented(AnimeEpisodeIncrementedEvent event, Emitter<AnimeListState> emit) async {
|
Future<void> _onAnimeIncremented(AnimeEpisodeIncrementedEvent event, Emitter<AnimeListState> emit) async {
|
||||||
@ -101,10 +137,17 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAnimesLoaded(AnimesLoadedEvent event, Emitter<AnimeListState> emit) async {
|
Future<void> _onAnimesLoaded(AnimesLoadedEvent event, Emitter<AnimeListState> emit) async {
|
||||||
|
_animes.addAll(
|
||||||
|
await GetIt.I.get<DatabaseService>().loadAnimes(),
|
||||||
|
);
|
||||||
|
_mangas.addAll(
|
||||||
|
await GetIt.I.get<DatabaseService>().loadMangas(),
|
||||||
|
);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
animes: await GetIt.I.get<DatabaseService>().loadAnimes(),
|
animes: _getFilteredAnime(),
|
||||||
mangas: await GetIt.I.get<DatabaseService>().loadMangas(),
|
mangas: _getFilteredManga(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -113,6 +156,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
animeFilterState: event.filterState,
|
animeFilterState: event.filterState,
|
||||||
|
animes: _getFilteredAnime(trackingState: event.filterState),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -121,6 +165,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
mangaFilterState: event.filterState,
|
mangaFilterState: event.filterState,
|
||||||
|
mangas: _getFilteredManga(trackingState: event.filterState),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -135,7 +180,7 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
|
|
||||||
Future<void> _onMangaIncremented(MangaChapterIncrementedEvent event, Emitter<AnimeListState> emit) async {
|
Future<void> _onMangaIncremented(MangaChapterIncrementedEvent event, Emitter<AnimeListState> emit) async {
|
||||||
final index = state.mangas.indexWhere((item) => item.id == event.id);
|
final index = state.mangas.indexWhere((item) => item.id == event.id);
|
||||||
if (index == -1) return;
|
assert(index != -1, 'The manga must exist');
|
||||||
|
|
||||||
final manga = state.mangas[index];
|
final manga = state.mangas[index];
|
||||||
if (manga.chaptersTotal != null && manga.chaptersRead + 1 > manga.chaptersTotal!) return;
|
if (manga.chaptersTotal != null && manga.chaptersRead + 1 > manga.chaptersTotal!) return;
|
||||||
@ -146,6 +191,11 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
);
|
);
|
||||||
newList[index] = newManga;
|
newList[index] = newManga;
|
||||||
|
|
||||||
|
// Update the cache
|
||||||
|
final cacheIndex = _mangas.indexWhere((m) => m.id == event.id);
|
||||||
|
assert(cacheIndex != -1, 'The manga must exist');
|
||||||
|
_mangas[cacheIndex] = newManga;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
mangas: newList,
|
mangas: newList,
|
||||||
@ -168,6 +218,11 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
);
|
);
|
||||||
newList[index] = newManga;
|
newList[index] = newManga;
|
||||||
|
|
||||||
|
// Update the cache
|
||||||
|
final cacheIndex = _mangas.indexWhere((m) => m.id == event.id);
|
||||||
|
assert(cacheIndex != -1, 'The manga must exist');
|
||||||
|
_mangas[cacheIndex] = newManga;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
mangas: newList,
|
mangas: newList,
|
||||||
@ -178,33 +233,27 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAnimeUpdated(AnimeUpdatedEvent event, Emitter<AnimeListState> emit) async {
|
Future<void> _onAnimeUpdated(AnimeUpdatedEvent event, Emitter<AnimeListState> emit) async {
|
||||||
|
final index = _animes.indexWhere((anime) => anime.id == event.anime.id);
|
||||||
|
assert(index != -1, 'The anime must exist');
|
||||||
|
|
||||||
|
_animes[index] = event.anime;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
animes: List.from(
|
animes: _getFilteredAnime(),
|
||||||
state.animes.map((anime) {
|
|
||||||
if (anime.id == event.anime.id) {
|
|
||||||
return event.anime;
|
|
||||||
}
|
|
||||||
|
|
||||||
return anime;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onMangaUpdated(MangaUpdatedEvent event, Emitter<AnimeListState> emit) async {
|
Future<void> _onMangaUpdated(MangaUpdatedEvent event, Emitter<AnimeListState> emit) async {
|
||||||
|
final index = _mangas.indexWhere((manga) => manga.id == event.manga.id);
|
||||||
|
assert(index != -1, 'The manga must exist');
|
||||||
|
|
||||||
|
_mangas[index] = event.manga;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
mangas: List.from(
|
mangas: _getFilteredManga(),
|
||||||
state.mangas.map((manga) {
|
|
||||||
if (manga.id == event.manga.id) {
|
|
||||||
return event.manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
return manga;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -218,6 +267,11 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update the cache
|
||||||
|
final cacheIndex = _mangas.indexWhere((m) => m.id == event.id);
|
||||||
|
assert(cacheIndex != -1, 'The anime must exist');
|
||||||
|
_mangas.removeAt(cacheIndex);
|
||||||
|
|
||||||
// Update the database
|
// Update the database
|
||||||
await GetIt.I.get<DatabaseService>().deleteAnime(event.id);
|
await GetIt.I.get<DatabaseService>().deleteAnime(event.id);
|
||||||
}
|
}
|
||||||
@ -231,6 +285,11 @@ class AnimeListBloc extends Bloc<AnimeListEvent, AnimeListState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update the cache
|
||||||
|
final cacheIndex = _animes.indexWhere((a) => a.id == event.id);
|
||||||
|
assert(cacheIndex != -1, 'The manga must exist');
|
||||||
|
_animes.removeAt(cacheIndex);
|
||||||
|
|
||||||
// Update the database
|
// Update the database
|
||||||
await GetIt.I.get<DatabaseService>().deleteManga(event.id);
|
await GetIt.I.get<DatabaseService>().deleteManga(event.id);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@ 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';
|
||||||
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/grid_item.dart';
|
||||||
|
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';
|
||||||
@ -130,79 +131,103 @@ class AnimeListPage extends StatelessWidget {
|
|||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
children: [
|
children: [
|
||||||
ListView.builder(
|
Padding(
|
||||||
itemCount: state.animes.length,
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
itemBuilder: (context, index) {
|
child: GridView.builder(
|
||||||
final anime = state.animes[index];
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
if (state.animeFilterState != MediumTrackingState.all) {
|
crossAxisCount: 3,
|
||||||
if (anime.state != state.animeFilterState) return Container();
|
mainAxisSpacing: 8,
|
||||||
}
|
crossAxisSpacing: 8,
|
||||||
|
childAspectRatio: 120 / (100 * (16 / 9)),
|
||||||
return InkWell(
|
),
|
||||||
onTap: () {
|
itemCount: state.animes.length,
|
||||||
context.read<DetailsBloc>().add(
|
itemBuilder: (context, index) {
|
||||||
AnimeDetailsRequestedEvent(anime),
|
final anime = state.animes[index];
|
||||||
);
|
return GridItem(
|
||||||
},
|
minusCallback: () {
|
||||||
child: ListItem(
|
context.read<AnimeListBloc>().add(
|
||||||
title: anime.title,
|
AnimeEpisodeDecrementedEvent(
|
||||||
thumbnailUrl: anime.thumbnailUrl,
|
anime.id,
|
||||||
extra: [
|
),
|
||||||
Text(
|
);
|
||||||
'${anime.episodesWatched}/${anime.episodesTotal ?? "???"}',
|
},
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
plusCallback: () {
|
||||||
|
context.read<AnimeListBloc>().add(
|
||||||
|
AnimeEpisodeIncrementedEvent(
|
||||||
|
anime.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: AnimeCoverImage(
|
||||||
|
url: anime.thumbnailUrl,
|
||||||
|
onTap: () {
|
||||||
|
context.read<DetailsBloc>().add(
|
||||||
|
AnimeDetailsRequestedEvent(anime),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
extra: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: Text(
|
||||||
|
'${anime.episodesWatched}/${anime.episodesTotal ?? "???"}',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
onLeftSwipe: () {
|
);
|
||||||
context.read<AnimeListBloc>().add(
|
},
|
||||||
AnimeEpisodeDecrementedEvent(state.animes[index].id),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
onRightSwipe: () {
|
|
||||||
context.read<AnimeListBloc>().add(
|
|
||||||
AnimeEpisodeIncrementedEvent(state.animes[index].id),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListView.builder(
|
Padding(
|
||||||
itemCount: state.mangas.length,
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
itemBuilder: (context, index) {
|
child: GridView.builder(
|
||||||
final manga = state.mangas[index];
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
if (state.mangaFilterState != MediumTrackingState.all) {
|
crossAxisCount: 3,
|
||||||
if (manga.state != state.mangaFilterState) return Container();
|
mainAxisSpacing: 8,
|
||||||
}
|
crossAxisSpacing: 8,
|
||||||
|
childAspectRatio: 120 / (100 * (16 / 9)),
|
||||||
return InkWell(
|
),
|
||||||
onTap: () {
|
itemCount: state.mangas.length,
|
||||||
context.read<DetailsBloc>().add(
|
itemBuilder: (context, index) {
|
||||||
MangaDetailsRequestedEvent(manga),
|
final manga = state.mangas[index];
|
||||||
);
|
return GridItem(
|
||||||
},
|
minusCallback: () {
|
||||||
child: ListItem(
|
context.read<AnimeListBloc>().add(
|
||||||
title: manga.title,
|
MangaChapterDecrementedEvent(
|
||||||
thumbnailUrl: manga.thumbnailUrl,
|
manga.id,
|
||||||
extra: [
|
),
|
||||||
Text(
|
);
|
||||||
'${manga.chaptersRead}/${manga.chaptersTotal ?? "???"}',
|
},
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
plusCallback: () {
|
||||||
|
context.read<AnimeListBloc>().add(
|
||||||
|
MangaChapterIncrementedEvent(
|
||||||
|
manga.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: AnimeCoverImage(
|
||||||
|
url: manga.thumbnailUrl,
|
||||||
|
onTap: () {
|
||||||
|
context.read<DetailsBloc>().add(
|
||||||
|
MangaDetailsRequestedEvent(manga),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
extra: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: Text(
|
||||||
|
'${manga.chaptersRead}/${manga.chaptersTotal ?? "???"}',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
onLeftSwipe: () {
|
);
|
||||||
context.read<AnimeListBloc>().add(
|
},
|
||||||
MangaChapterDecrementedEvent(state.mangas[index].id),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
onRightSwipe: () {
|
|
||||||
context.read<AnimeListBloc>().add(
|
|
||||||
MangaChapterIncrementedEvent(state.mangas[index].id),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
72
lib/src/ui/widgets/grid_item.dart
Normal file
72
lib/src/ui/widgets/grid_item.dart
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class GridItem extends StatefulWidget {
|
||||||
|
const GridItem({
|
||||||
|
required this.child,
|
||||||
|
required this.plusCallback,
|
||||||
|
required this.minusCallback,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
final void Function() plusCallback;
|
||||||
|
final void Function() minusCallback;
|
||||||
|
|
||||||
|
@override
|
||||||
|
GridItemState createState() => GridItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class GridItemState extends State<GridItem> {
|
||||||
|
double _offset = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onHorizontalDragUpdate: (details) {
|
||||||
|
setState(() {
|
||||||
|
_offset += details.delta.dx;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onHorizontalDragEnd: (_) {
|
||||||
|
if (_offset <= 50) {
|
||||||
|
widget.plusCallback();
|
||||||
|
} else if (_offset >= -50) {
|
||||||
|
widget.minusCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the view
|
||||||
|
setState(() => _offset = 0);
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
const Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 15,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Icon(Icons.add),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Positioned(
|
||||||
|
left: 15,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Icon(Icons.remove),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 160 / (1 + exp(-1 * (1/30) * _offset)) - 80,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,14 +5,22 @@ class AnimeCoverImage extends StatelessWidget {
|
|||||||
const AnimeCoverImage({
|
const AnimeCoverImage({
|
||||||
required this.url,
|
required this.url,
|
||||||
this.cached = true,
|
this.cached = true,
|
||||||
|
this.extra,
|
||||||
|
this.onTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The URL to the cover image.
|
/// The URL to the cover image.
|
||||||
final String url;
|
final String url;
|
||||||
|
|
||||||
/// Flag indicating if the image should be cached
|
/// Flag indicating if the image should be cached.
|
||||||
final bool cached;
|
final bool cached;
|
||||||
|
|
||||||
|
/// An extra widget with a translucent backdrop.
|
||||||
|
final Widget? extra;
|
||||||
|
|
||||||
|
/// Callback for when the image is tapped.
|
||||||
|
final void Function()? onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -21,14 +29,41 @@ class AnimeCoverImage extends StatelessWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 100 * (16 / 9),
|
height: 100 * (16 / 9),
|
||||||
width: 120,
|
width: 120,
|
||||||
child: DecoratedBox(
|
child: InkWell(
|
||||||
decoration: BoxDecoration(
|
onTap: onTap ?? () {},
|
||||||
image: DecorationImage(
|
child: Stack(
|
||||||
image: cached ?
|
children: [
|
||||||
CachedNetworkImageProvider(url) as ImageProvider :
|
Positioned(
|
||||||
NetworkImage(url),
|
left: 0,
|
||||||
fit: BoxFit.cover,
|
right: 0,
|
||||||
),
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: cached ?
|
||||||
|
CachedNetworkImageProvider(url) as ImageProvider :
|
||||||
|
NetworkImage(url),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (extra != null)
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Colors.black54,
|
||||||
|
child: extra,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -12,6 +12,7 @@ class ListItem extends StatelessWidget {
|
|||||||
this.onRightSwipe,
|
this.onRightSwipe,
|
||||||
this.cached = true,
|
this.cached = true,
|
||||||
this.extra = const [],
|
this.extra = const [],
|
||||||
|
this.imageExtra,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -23,6 +24,8 @@ class ListItem extends StatelessWidget {
|
|||||||
|
|
||||||
/// Extra widgets.
|
/// Extra widgets.
|
||||||
final List<Widget> extra;
|
final List<Widget> extra;
|
||||||
|
|
||||||
|
final Widget? imageExtra;
|
||||||
|
|
||||||
/// Callbacks for the swipe functionality.
|
/// Callbacks for the swipe functionality.
|
||||||
final void Function()? onLeftSwipe;
|
final void Function()? onLeftSwipe;
|
||||||
@ -73,6 +76,7 @@ class ListItem extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
AnimeCoverImage(
|
AnimeCoverImage(
|
||||||
cached: cached,
|
cached: cached,
|
||||||
|
extra: imageExtra,
|
||||||
url: thumbnailUrl,
|
url: thumbnailUrl,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user