Polish mobile UX downloads and comments
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../app/app.dart';
|
||||
@@ -17,12 +14,22 @@ import '../../../shared/widgets/error_view.dart';
|
||||
import '../../../shared/widgets/post_card.dart';
|
||||
import '../../../shared/widgets/rating_badge.dart';
|
||||
import '../../collections/presentation/collection_form_dialog.dart';
|
||||
import '../../favorites/presentation/favorites_controller.dart';
|
||||
import '../../feed/presentation/feed_controller.dart';
|
||||
import 'post_details_controller.dart';
|
||||
import 'widgets/post_action_bar.dart';
|
||||
import 'widgets/post_media_viewer.dart';
|
||||
import 'widgets/post_tags_panel.dart';
|
||||
|
||||
final postCommentsProvider =
|
||||
FutureProvider.family<List<PostComment>, PostDetailsArgs>(
|
||||
(ref, args) async {
|
||||
final result = await ref
|
||||
.watch(providerManagerProvider)
|
||||
.getComments(args.providerId, args.postId);
|
||||
return result is Success<List<PostComment>> ? result.data : const [];
|
||||
});
|
||||
|
||||
class PostDetailsScreen extends ConsumerWidget {
|
||||
const PostDetailsScreen({
|
||||
required this.providerId,
|
||||
@@ -47,6 +54,7 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
ref.watch(appSettingsProvider).value ?? AppSettings.defaults;
|
||||
final feedPosts =
|
||||
ref.watch(feedControllerProvider).value?.posts ?? const <Post>[];
|
||||
final favoriteKeys = ref.watch(favoriteKeysProvider).value ?? <String>{};
|
||||
return AdaptiveScaffold(
|
||||
title: 'Post',
|
||||
actions: [
|
||||
@@ -70,7 +78,28 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
? feedPosts[currentIndex + 1]
|
||||
: null;
|
||||
if (Responsive.isMobile(context)) {
|
||||
return _buildMobileDetails(context, ref, post, settings);
|
||||
if (currentIndex >= 0 && feedPosts.length > 1) {
|
||||
return PageView.builder(
|
||||
controller: PageController(initialPage: currentIndex),
|
||||
itemCount: feedPosts.length,
|
||||
onPageChanged: (index) =>
|
||||
_replacePost(context, feedPosts[index]),
|
||||
itemBuilder: (context, index) => _buildMobileDetails(
|
||||
context,
|
||||
ref,
|
||||
feedPosts[index],
|
||||
settings,
|
||||
favoriteKeys,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _buildMobileDetails(
|
||||
context,
|
||||
ref,
|
||||
post,
|
||||
settings,
|
||||
favoriteKeys,
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -103,14 +132,14 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PostActionBar(
|
||||
onFavorite: () =>
|
||||
ref.read(favoriteServiceProvider).addFavorite(post),
|
||||
isFavorite: favoriteKeys.contains(post.cacheKey),
|
||||
onFavorite: () => _toggleFavorite(ref, post, favoriteKeys),
|
||||
onCollection: () => _addToCollection(context, ref, post),
|
||||
onOpen: () => launchUrl(Uri.parse(post.fileUrl)),
|
||||
onCopy: () =>
|
||||
Clipboard.setData(ClipboardData(text: post.fileUrl)),
|
||||
onDownload: settings.allowDownloads
|
||||
? () => _download(context, post)
|
||||
? () => _download(context, ref, post)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -133,6 +162,8 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
Text('Tags', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 8),
|
||||
PostTagsPanel(post: post),
|
||||
const SizedBox(height: 16),
|
||||
_CommentsSection(post: post),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -145,6 +176,7 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
WidgetRef ref,
|
||||
Post post,
|
||||
AppSettings settings,
|
||||
Set<String> favoriteKeys,
|
||||
) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 16),
|
||||
@@ -157,12 +189,14 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
PostActionBar(
|
||||
onFavorite: () => ref.read(favoriteServiceProvider).addFavorite(post),
|
||||
isFavorite: favoriteKeys.contains(post.cacheKey),
|
||||
onFavorite: () => _toggleFavorite(ref, post, favoriteKeys),
|
||||
onCollection: () => _addToCollection(context, ref, post),
|
||||
onOpen: () => launchUrl(Uri.parse(post.fileUrl)),
|
||||
onCopy: () => Clipboard.setData(ClipboardData(text: post.fileUrl)),
|
||||
onDownload:
|
||||
settings.allowDownloads ? () => _download(context, post) : null,
|
||||
onDownload: settings.allowDownloads
|
||||
? () => _download(context, ref, post)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
@@ -191,6 +225,7 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
_CommentsSection(post: post),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -216,23 +251,47 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _download(BuildContext context, Post post) async {
|
||||
final filename = p.basename(Uri.parse(post.fileUrl).path);
|
||||
final location = await getSaveLocation(suggestedName: filename);
|
||||
if (location == null) return;
|
||||
Future<void> _download(BuildContext context, WidgetRef ref, Post post) async {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Downloading...')),
|
||||
);
|
||||
await Dio().download(post.fileUrl, location.path);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Download complete')),
|
||||
);
|
||||
try {
|
||||
final saved = await ref.read(downloadServiceProvider).downloadPost(post);
|
||||
if (!context.mounted || saved == null) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Download complete: $saved')),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Download failed: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleFavorite(
|
||||
WidgetRef ref,
|
||||
Post post,
|
||||
Set<String> favoriteKeys,
|
||||
) async {
|
||||
if (favoriteKeys.contains(post.cacheKey)) {
|
||||
await ref
|
||||
.read(favoriteServiceProvider)
|
||||
.removeFavorite(post.id, post.providerId);
|
||||
} else {
|
||||
await ref.read(favoriteServiceProvider).addFavorite(post);
|
||||
}
|
||||
ref.invalidate(favoriteKeysProvider);
|
||||
ref.invalidate(favoritesControllerProvider);
|
||||
}
|
||||
|
||||
void _openPost(BuildContext context, Post post) {
|
||||
context.go('/post/${post.providerId}/${post.id}', extra: post);
|
||||
_replacePost(context, post);
|
||||
}
|
||||
|
||||
void _replacePost(BuildContext context, Post post) {
|
||||
context.replace('/post/${post.providerId}/${post.id}', extra: post);
|
||||
}
|
||||
|
||||
void _close(BuildContext context) {
|
||||
@@ -243,3 +302,77 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
context.go('/');
|
||||
}
|
||||
}
|
||||
|
||||
class _CommentsSection extends ConsumerStatefulWidget {
|
||||
const _CommentsSection({required this.post});
|
||||
|
||||
final Post post;
|
||||
|
||||
@override
|
||||
ConsumerState<_CommentsSection> createState() => _CommentsSectionState();
|
||||
}
|
||||
|
||||
class _CommentsSectionState extends ConsumerState<_CommentsSection> {
|
||||
bool _expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final comments = _expanded
|
||||
? ref.watch(
|
||||
postCommentsProvider(
|
||||
PostDetailsArgs(
|
||||
providerId: widget.post.providerId,
|
||||
postId: widget.post.id,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
return ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
initiallyExpanded: false,
|
||||
onExpansionChanged: (value) => setState(() => _expanded = value),
|
||||
title: Text('Comments', style: Theme.of(context).textTheme.titleMedium),
|
||||
children: [
|
||||
(comments ?? const AsyncValue<List<PostComment>>.data([])).when(
|
||||
loading: () => const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
error: (_, __) => const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
child: Text('Comments unavailable'),
|
||||
),
|
||||
),
|
||||
data: (items) {
|
||||
if (items.isEmpty) {
|
||||
return const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
child: Text('Comments unavailable'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
for (final comment in items)
|
||||
ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
comment.authorName.isEmpty
|
||||
? 'Anonymous'
|
||||
: comment.authorName,
|
||||
),
|
||||
subtitle: Text(comment.body),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ class PostActionBar extends StatelessWidget {
|
||||
required this.onCollection,
|
||||
required this.onOpen,
|
||||
required this.onCopy,
|
||||
required this.isFavorite,
|
||||
this.onDownload,
|
||||
super.key,
|
||||
});
|
||||
@@ -14,6 +15,7 @@ class PostActionBar extends StatelessWidget {
|
||||
final VoidCallback onCollection;
|
||||
final VoidCallback onOpen;
|
||||
final VoidCallback onCopy;
|
||||
final bool isFavorite;
|
||||
final VoidCallback? onDownload;
|
||||
|
||||
@override
|
||||
@@ -24,8 +26,10 @@ class PostActionBar extends StatelessWidget {
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
onPressed: onFavorite,
|
||||
icon: const Icon(Icons.favorite_rounded),
|
||||
label: const Text('Favorite'),
|
||||
icon: Icon(
|
||||
isFavorite ? Icons.favorite_rounded : Icons.favorite_border_rounded,
|
||||
),
|
||||
label: Text(isFavorite ? 'Unfavorite' : 'Favorite'),
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: onCollection,
|
||||
|
||||
Reference in New Issue
Block a user