feat: release v2.0.5 with 10 QoL features (AMOLED, Dynamic Color, Grid switch, Share, Presets, Cache Manager, Backup & Gestures)
This commit is contained in:
@@ -54,26 +54,63 @@ class PostCard extends ConsumerStatefulWidget {
|
||||
ConsumerState<PostCard> createState() => _PostCardState();
|
||||
}
|
||||
|
||||
class _PostCardState extends ConsumerState<PostCard> {
|
||||
class _PostCardState extends ConsumerState<PostCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static final Map<String, Post> _resolvedRealbooruPosts = {};
|
||||
|
||||
bool _hovered = false;
|
||||
Post? _resolvedPost;
|
||||
bool _showHeart = false;
|
||||
late final AnimationController _heartController;
|
||||
late final Animation<double> _heartScale;
|
||||
late final Animation<double> _heartOpacity;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_resolvedPost = _resolvedRealbooruPosts[widget.post.cacheKey];
|
||||
_maybeResolveRealbooruPost();
|
||||
_heartController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 600),
|
||||
);
|
||||
_heartScale = TweenSequence<double>([
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 0.4, end: 1.3)
|
||||
.chain(CurveTween(curve: Curves.easeOut)),
|
||||
weight: 40),
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 1.3, end: 1.0)
|
||||
.chain(CurveTween(curve: Curves.easeIn)),
|
||||
weight: 30),
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 1.0, end: 1.0), weight: 30),
|
||||
]).animate(_heartController);
|
||||
_heartOpacity = TweenSequence<double>([
|
||||
TweenSequenceItem(tween: ConstantTween(1.0), weight: 60),
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 1.0, end: 0.0)
|
||||
.chain(CurveTween(curve: Curves.easeIn)),
|
||||
weight: 40),
|
||||
]).animate(_heartController);
|
||||
_heartController.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed && mounted) {
|
||||
setState(() => _showHeart = false);
|
||||
_heartController.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant PostCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.post.cacheKey != widget.post.cacheKey) {
|
||||
_resolvedPost = _resolvedRealbooruPosts[widget.post.cacheKey];
|
||||
_maybeResolveRealbooruPost();
|
||||
}
|
||||
void dispose() {
|
||||
_heartController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _triggerDoubleTapFavorite() {
|
||||
widget.onFavorite();
|
||||
setState(() => _showHeart = true);
|
||||
_heartController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -97,6 +134,7 @@ class _PostCardState extends ConsumerState<PostCard> {
|
||||
}
|
||||
widget.onOpen();
|
||||
},
|
||||
onDoubleTap: _triggerDoubleTapFavorite,
|
||||
onLongPress: () {
|
||||
if (widget.selectionMode && widget.onToggleSelected != null) {
|
||||
widget.onToggleSelected!();
|
||||
@@ -247,6 +285,32 @@ class _PostCardState extends ConsumerState<PostCard> {
|
||||
onPressed: widget.onFavorite,
|
||||
),
|
||||
),
|
||||
// Double-tap heart animation overlay
|
||||
if (_showHeart)
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: _heartController,
|
||||
builder: (context, _) => Opacity(
|
||||
opacity: _heartOpacity.value,
|
||||
child: Transform.scale(
|
||||
scale: _heartScale.value,
|
||||
child: const Icon(
|
||||
Icons.favorite_rounded,
|
||||
color: Colors.white,
|
||||
size: 72,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black54,
|
||||
blurRadius: 12,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
ignoring: !_hovered || mobile,
|
||||
|
||||
@@ -27,6 +27,7 @@ class PostMasonryGrid extends StatelessWidget {
|
||||
this.downloadedKeys = const {},
|
||||
this.loading = false,
|
||||
this.controller,
|
||||
this.gridMode = 'masonry',
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -49,39 +50,89 @@ class PostMasonryGrid extends StatelessWidget {
|
||||
final Set<String> downloadedKeys;
|
||||
final bool loading;
|
||||
final ScrollController? controller;
|
||||
final String gridMode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mobile = Responsive.isMobile(context);
|
||||
final pad = EdgeInsets.all(mobile ? 8 : 16);
|
||||
final spacing = mobile ? 8.0 : 12.0;
|
||||
|
||||
Widget buildCard(Post post) {
|
||||
return PostCard(
|
||||
post: post,
|
||||
blurExplicit: blurExplicit && !nsfwEnabled,
|
||||
showBadges: showBadges,
|
||||
isFavorite: favoriteKeys.contains(post.cacheKey),
|
||||
isViewed: viewedKeys.contains(post.cacheKey),
|
||||
isDownloaded: downloadedKeys.contains(post.cacheKey),
|
||||
mediaQualityMode: mediaQualityMode,
|
||||
onOpen: () => onOpen(post),
|
||||
onFavorite: () => onFavorite(post),
|
||||
onPreview: onPreview == null ? null : () => onPreview!(post),
|
||||
onHide: onHide == null ? null : () => onHide!(post),
|
||||
selectionMode: selectionMode,
|
||||
selected: selectedKeys.contains(post.cacheKey),
|
||||
onToggleSelected:
|
||||
onToggleSelected == null ? null : () => onToggleSelected!(post),
|
||||
onAddToCollection:
|
||||
onAddToCollection == null ? null : () => onAddToCollection!(post),
|
||||
);
|
||||
}
|
||||
|
||||
if (gridMode == 'grid') {
|
||||
// Uniform square grid
|
||||
return GridView.builder(
|
||||
controller: controller,
|
||||
padding: pad,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
childAspectRatio: 1.0,
|
||||
),
|
||||
itemCount: posts.length + (loading ? columns : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= posts.length) return const LoadingSkeleton();
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: SizedBox.expand(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: buildCard(posts[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (gridMode == 'list') {
|
||||
// Vertical list with larger cards
|
||||
return ListView.separated(
|
||||
controller: controller,
|
||||
padding: pad,
|
||||
separatorBuilder: (_, __) => SizedBox(height: spacing),
|
||||
itemCount: posts.length + (loading ? 3 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= posts.length) return const LoadingSkeleton();
|
||||
return buildCard(posts[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Default: masonry
|
||||
return MasonryGridView.count(
|
||||
controller: controller,
|
||||
padding: EdgeInsets.all(mobile ? 8 : 16),
|
||||
padding: pad,
|
||||
crossAxisCount: columns,
|
||||
mainAxisSpacing: mobile ? 8 : 12,
|
||||
crossAxisSpacing: mobile ? 8 : 12,
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
itemCount: posts.length + (loading ? columns : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= posts.length) return const LoadingSkeleton();
|
||||
final post = posts[index];
|
||||
return PostCard(
|
||||
post: post,
|
||||
blurExplicit: blurExplicit && !nsfwEnabled,
|
||||
showBadges: showBadges,
|
||||
isFavorite: favoriteKeys.contains(post.cacheKey),
|
||||
isViewed: viewedKeys.contains(post.cacheKey),
|
||||
isDownloaded: downloadedKeys.contains(post.cacheKey),
|
||||
mediaQualityMode: mediaQualityMode,
|
||||
onOpen: () => onOpen(post),
|
||||
onFavorite: () => onFavorite(post),
|
||||
onPreview: onPreview == null ? null : () => onPreview!(post),
|
||||
onHide: onHide == null ? null : () => onHide!(post),
|
||||
selectionMode: selectionMode,
|
||||
selected: selectedKeys.contains(post.cacheKey),
|
||||
onToggleSelected:
|
||||
onToggleSelected == null ? null : () => onToggleSelected!(post),
|
||||
onAddToCollection:
|
||||
onAddToCollection == null ? null : () => onAddToCollection!(post),
|
||||
);
|
||||
return buildCard(posts[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user