Fix 413 Request Entity Too Large and refine image display aesthetics
This commit is contained in:
parent
3c91dc64c9
commit
92d8920183
|
|
@ -1,6 +1,8 @@
|
|||
server {
|
||||
server_name api.sojorn.net;
|
||||
|
||||
client_max_body_size 100M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header Host $host;
|
||||
|
|
@ -11,6 +13,11 @@ server {
|
|||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# Increase timeouts for large file uploads
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
listen 443 ssl; # managed by Certbot
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:path_provider/path_provider.dart';
|
|||
import '../services/auth_service.dart';
|
||||
import '../services/api_service.dart';
|
||||
import '../services/image_upload_service.dart';
|
||||
import 'feed_refresh_provider.dart';
|
||||
|
||||
// Define the state class
|
||||
class QuipUploadState {
|
||||
|
|
@ -36,14 +37,13 @@ class QuipUploadState {
|
|||
}
|
||||
}
|
||||
|
||||
// Create a Notifier class for Riverpod 3.2.0+
|
||||
class QuipUploadNotifier extends Notifier<QuipUploadState> {
|
||||
@override
|
||||
QuipUploadState build() {
|
||||
return QuipUploadState(isUploading: false, progress: 0.0);
|
||||
}
|
||||
|
||||
Future<void> startUpload(File videoFile, String caption) async {
|
||||
Future<void> startUpload(File videoFile, String caption, {double? thumbnailTimestampMs}) async {
|
||||
try {
|
||||
state = state.copyWith(
|
||||
isUploading: true, progress: 0.0, error: null, successMessage: null);
|
||||
|
|
@ -60,8 +60,12 @@ class QuipUploadNotifier extends Notifier<QuipUploadState> {
|
|||
final tempDir = await getTemporaryDirectory();
|
||||
final thumbnailPath = '${tempDir.path}/${timestamp}_thumb.jpg';
|
||||
|
||||
final ss = thumbnailTimestampMs != null
|
||||
? (thumbnailTimestampMs / 1000.0).toStringAsFixed(3)
|
||||
: '00:00:01';
|
||||
|
||||
final session = await FFmpegKit.execute(
|
||||
'-y -ss 00:00:01 -i "${videoFile.path}" -vframes 1 -q:v 5 "$thumbnailPath"'
|
||||
'-y -ss $ss -i "${videoFile.path}" -vframes 1 -q:v 2 "$thumbnailPath"'
|
||||
);
|
||||
|
||||
final returnCode = await session.getReturnCode();
|
||||
|
|
@ -102,7 +106,6 @@ class QuipUploadNotifier extends Notifier<QuipUploadState> {
|
|||
state = state.copyWith(progress: 0.8);
|
||||
|
||||
// Publish post via Go API
|
||||
// Video goes to video_url, thumbnail to thumbnail_url
|
||||
await ApiService.instance.publishPost(
|
||||
body: caption,
|
||||
videoUrl: videoUrl,
|
||||
|
|
@ -110,10 +113,20 @@ class QuipUploadNotifier extends Notifier<QuipUploadState> {
|
|||
categoryId: null, // Default
|
||||
);
|
||||
|
||||
// Trigger feed refresh
|
||||
ref.read(feedRefreshProvider.notifier).state++;
|
||||
|
||||
state = state.copyWith(
|
||||
isUploading: false,
|
||||
progress: 1.0,
|
||||
successMessage: 'Upload successful');
|
||||
|
||||
// Auto-reset after 3 seconds so UI goes back to + button
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (state.progress == 1.0 && !state.isUploading) {
|
||||
state = QuipUploadState(isUploading: false, progress: 0.0);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
state = state.copyWith(isUploading: false, error: e.toString());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,49 +112,69 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
|
|||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Outer Ring for Upload Progress
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final upload = ref.watch(quipUploadProvider);
|
||||
|
||||
if (!upload.isUploading && upload.progress == 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: 64,
|
||||
height: 64,
|
||||
child: CircularProgressIndicator(
|
||||
value: upload.progress,
|
||||
strokeWidth: 4,
|
||||
backgroundColor: AppTheme.egyptianBlue.withOpacity(0.1),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
upload.progress >= 0.99 ? Colors.green : AppTheme.brightNavy
|
||||
),
|
||||
final isDone = !upload.isUploading && upload.progress >= 1.0;
|
||||
final isUploading = upload.isUploading;
|
||||
final hasState = isUploading || isDone;
|
||||
|
||||
return Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: hasState ? AppTheme.brightNavy : AppTheme.navyBlue,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (hasState ? AppTheme.brightNavy : AppTheme.navyBlue).withOpacity(0.4),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Inside Border Progress
|
||||
if (hasState)
|
||||
SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: CustomPaint(
|
||||
painter: _VerticalBorderProgressPainter(
|
||||
progress: upload.progress,
|
||||
color: Colors.white,
|
||||
backgroundColor: Colors.white.withOpacity(0.2),
|
||||
strokeWidth: 3.5,
|
||||
borderRadius: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Content: Icon(+) or Percent or Check
|
||||
if (isDone)
|
||||
const Icon(Icons.check, color: Colors.white, size: 28)
|
||||
else if (isUploading)
|
||||
Text(
|
||||
'${(upload.progress * 100).toInt()}%',
|
||||
style: GoogleFonts.outfit(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)
|
||||
else
|
||||
const Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.navyBlue,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.navyBlue.withOpacity(0.4),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -331,6 +351,69 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
|
|||
}
|
||||
}
|
||||
|
||||
class _VerticalBorderProgressPainter extends CustomPainter {
|
||||
final double progress;
|
||||
final Color color;
|
||||
final Color backgroundColor;
|
||||
final double strokeWidth;
|
||||
final double borderRadius;
|
||||
|
||||
_VerticalBorderProgressPainter({
|
||||
required this.progress,
|
||||
required this.color,
|
||||
required this.backgroundColor,
|
||||
this.strokeWidth = 3.0,
|
||||
this.borderRadius = 16.0,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final bgPaint = Paint()
|
||||
..color = backgroundColor
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final rect = Rect.fromLTWH(
|
||||
strokeWidth / 2,
|
||||
strokeWidth / 2,
|
||||
size.width - strokeWidth,
|
||||
size.height - strokeWidth,
|
||||
);
|
||||
final rrect = RRect.fromRectAndRadius(rect, Radius.circular(borderRadius));
|
||||
|
||||
// Draw background border
|
||||
canvas.drawRRect(rrect, bgPaint);
|
||||
|
||||
// Draw progress border
|
||||
if (progress > 0) {
|
||||
final progressPaint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
// Clip to vertical progress
|
||||
canvas.save();
|
||||
final clipRect = Rect.fromLTWH(
|
||||
0,
|
||||
size.height * (1.0 - progress),
|
||||
size.width,
|
||||
size.height * progress,
|
||||
);
|
||||
canvas.clipRect(clipRect);
|
||||
canvas.drawRRect(rrect, progressPaint);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _VerticalBorderProgressPainter oldDelegate) {
|
||||
return oldDelegate.progress != progress ||
|
||||
oldDelegate.color != color ||
|
||||
oldDelegate.backgroundColor != backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the current navigation shell index to descendants that need to
|
||||
/// react (e.g. pausing quip playback when the tab is not active).
|
||||
class NavigationShellScope extends InheritedWidget {
|
||||
|
|
|
|||
|
|
@ -574,7 +574,7 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
|
|||
width: double.infinity,
|
||||
child: SignedMediaImage(
|
||||
url: imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:video_player/video_player.dart';
|
|||
import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
|
||||
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
// import '../../../providers/quip_upload_provider.dart'; // Removed missing provider
|
||||
import '../../../providers/quip_upload_provider.dart';
|
||||
import '../../../services/image_upload_service.dart';
|
||||
import '../../../providers/api_provider.dart';
|
||||
import '../../../providers/feed_refresh_provider.dart';
|
||||
|
|
@ -23,8 +23,8 @@ class _QuipMetadataScreenState extends ConsumerState<QuipMetadataScreen> {
|
|||
late VideoPlayerController _controller;
|
||||
final TextEditingController _captionController = TextEditingController();
|
||||
double _coverTimestamp = 0.0;
|
||||
bool _isUploading = false;
|
||||
final ImageUploadService _uploadService = ImageUploadService();
|
||||
// bool _isUploading = false;
|
||||
// final ImageUploadService _uploadService = ImageUploadService();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -41,77 +41,25 @@ class _QuipMetadataScreenState extends ConsumerState<QuipMetadataScreen> {
|
|||
}
|
||||
|
||||
Future<void> _postQuip() async {
|
||||
if (_isUploading) return;
|
||||
setState(() => _isUploading = true);
|
||||
final uploadNotifier = ref.read(quipUploadProvider.notifier);
|
||||
|
||||
try {
|
||||
// 1. Generate Thumbnail
|
||||
String? thumbnailUrl;
|
||||
try {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final thumbPath = '${tempDir.path}/thumbnail_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final seconds = _coverTimestamp / 1000.0;
|
||||
|
||||
// Execute FFmpeg to extract frame
|
||||
final session = await FFmpegKit.executeWithArguments([
|
||||
'-y',
|
||||
'-user_agent', 'SojornApp/1.0',
|
||||
'-ss', seconds.toStringAsFixed(3),
|
||||
'-i', widget.videoFile.path,
|
||||
'-vframes', '1',
|
||||
'-q:v', '5',
|
||||
thumbPath
|
||||
]);
|
||||
final returnCode = await session.getReturnCode();
|
||||
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
final thumbFile = File(thumbPath);
|
||||
if (await thumbFile.exists()) {
|
||||
thumbnailUrl = await _uploadService.uploadImage(thumbFile);
|
||||
}
|
||||
} else {
|
||||
final logs = await session.getLogs();
|
||||
final logContent = logs.map((l) => l.getMessage()).join('\n');
|
||||
print('FFmpeg thumbnail failed. ReturnCode: $returnCode');
|
||||
print('Logs:\n$logContent');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Thumbnail generation error: $e');
|
||||
// Continue without thumbnail
|
||||
}
|
||||
// We already have the logic to generate a specific thumbnail in the provider,
|
||||
// but the screen allows choosing a timestamp.
|
||||
// To support the chosen cover, we'll generate it here and then pass it
|
||||
// or just pass the timestamp to the provider.
|
||||
// Let's pass the chosen timestamp to startUpload.
|
||||
|
||||
uploadNotifier.startUpload(
|
||||
widget.videoFile,
|
||||
_captionController.text.trim(),
|
||||
thumbnailTimestampMs: _coverTimestamp,
|
||||
);
|
||||
|
||||
// 2. Upload Video
|
||||
final videoUrl = await _uploadService.uploadVideo(
|
||||
widget.videoFile,
|
||||
onProgress: (progress) {
|
||||
// Optional: Update progress UI
|
||||
},
|
||||
if (mounted) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Upload started in background")),
|
||||
);
|
||||
|
||||
// 3. Create the Post in the database
|
||||
final apiService = ref.read(apiServiceProvider);
|
||||
await apiService.publishPost(
|
||||
body: _captionController.text.trim(),
|
||||
videoUrl: videoUrl,
|
||||
thumbnailUrl: thumbnailUrl,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
// Refresh feed to show new quip
|
||||
ref.read(feedRefreshProvider.notifier).state++;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Quip posted successfully!")),
|
||||
);
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Upload failed: $e")),
|
||||
);
|
||||
setState(() => _isUploading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,14 +83,12 @@ class _QuipMetadataScreenState extends ConsumerState<QuipMetadataScreen> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: ElevatedButton(
|
||||
onPressed: _isUploading ? null : _postQuip,
|
||||
onPressed: _postQuip,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.brightNavy,
|
||||
shape: const StadiumBorder(),
|
||||
),
|
||||
child: _isUploading
|
||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: const Text("Post"),
|
||||
child: const Text("Post"),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
|
|
|||
|
|
@ -486,14 +486,16 @@ class NotificationService {
|
|||
break;
|
||||
|
||||
case 'like':
|
||||
case 'quip_reaction':
|
||||
case 'save':
|
||||
case 'comment':
|
||||
case 'reply':
|
||||
case 'mention':
|
||||
final postId = data['post_id'];
|
||||
case 'share':
|
||||
final postId = data['post_id'] ?? data['beacon_id'];
|
||||
final target = data['target'];
|
||||
if (postId != null) {
|
||||
_navigateToPost(navigator, postId, target);
|
||||
_navigateToPost(navigator, postId.toString(), target?.toString());
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -503,7 +505,7 @@ class NotificationService {
|
|||
case 'follow_accepted':
|
||||
final followerId = data['follower_id'];
|
||||
if (followerId != null) {
|
||||
navigator.context.push('/u/$followerId');
|
||||
navigator.context.push('${AppRoutes.userPrefix}/$followerId');
|
||||
} else {
|
||||
navigator.context.go(AppRoutes.profile);
|
||||
}
|
||||
|
|
@ -511,7 +513,12 @@ class NotificationService {
|
|||
|
||||
case 'beacon_vouch':
|
||||
case 'beacon_report':
|
||||
navigator.context.go(AppRoutes.beaconPrefix);
|
||||
final beaconId = data['beacon_id'] ?? data['post_id'];
|
||||
if (beaconId != null) {
|
||||
_navigateToPost(navigator, beaconId.toString(), 'beacon_map');
|
||||
} else {
|
||||
navigator.context.go(AppRoutes.beaconPrefix);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -44,31 +44,28 @@ class PostMedia extends StatelessWidget {
|
|||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: _imageHeight),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: SignedMediaImage(
|
||||
url: post!.imageUrl!,
|
||||
fit: BoxFit.contain,
|
||||
loadingBuilder: (context) => Container(
|
||||
color: AppTheme.queenPink.withValues(alpha: 0.3),
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
color: Colors.red.withValues(alpha: 0.3),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.broken_image,
|
||||
size: 48, color: Colors.white),
|
||||
const SizedBox(height: 8),
|
||||
Text('Error: $error',
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 10)),
|
||||
],
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: SignedMediaImage(
|
||||
url: post!.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context) => Container(
|
||||
color: AppTheme.queenPink.withValues(alpha: 0.3),
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
color: Colors.red.withValues(alpha: 0.3),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.broken_image,
|
||||
size: 48, color: Colors.white),
|
||||
const SizedBox(height: 8),
|
||||
Text('Error: $error',
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 10)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ class sojornPostCard extends StatelessWidget {
|
|||
color: Colors.transparent,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 16), // Add spacing between cards
|
||||
padding: _padding,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.cardSurface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
|
|
@ -103,109 +102,127 @@ class sojornPostCard extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Chain Context (The Quote Box) - only show in thread view
|
||||
if (isThreadView && showChainContext && post.chainParent != null) ...[
|
||||
ChainQuoteWidget(
|
||||
parent: post.chainParent!,
|
||||
onTap: onChainParentTap,
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingSm),
|
||||
],
|
||||
|
||||
// Main Post Content
|
||||
const SizedBox(height: 4),
|
||||
// Header row with menu - only header is clickable for profile
|
||||
Row(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: _padding.copyWith(left: 0, right: 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
final handle = post.author?.handle ?? 'unknown';
|
||||
if (handle != 'unknown' && handle.trim().isNotEmpty) {
|
||||
AppRoutes.navigateToProfile(context, handle);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 4,
|
||||
// Internal horizontal padding for text/actions
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: _padding.left),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Chain Context (The Quote Box) - only show in thread view
|
||||
if (isThreadView && showChainContext && post.chainParent != null) ...[
|
||||
ChainQuoteWidget(
|
||||
parent: post.chainParent!,
|
||||
onTap: onChainParentTap,
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingSm),
|
||||
],
|
||||
|
||||
// Main Post Content
|
||||
const SizedBox(height: 4),
|
||||
// Header row with menu - only header is clickable for profile
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
final handle = post.author?.handle ?? 'unknown';
|
||||
if (handle != 'unknown' && handle.trim().isNotEmpty) {
|
||||
AppRoutes.navigateToProfile(context, handle);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 4,
|
||||
),
|
||||
child: PostHeader(
|
||||
post: post,
|
||||
avatarSize: _avatarSize,
|
||||
mode: mode,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => SanctuarySheet.show(context, post),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.ksuPurple.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text("!", style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppTheme.royalPurple.withOpacity(0.7),
|
||||
)),
|
||||
),
|
||||
),
|
||||
PostMenu(
|
||||
post: post,
|
||||
onPostDeleted: onPostChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: PostHeader(
|
||||
post: post,
|
||||
avatarSize: _avatarSize,
|
||||
mode: mode,
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Body text - clickable for post detail with full background coverage
|
||||
InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: PostBody(
|
||||
text: post.body,
|
||||
bodyFormat: post.bodyFormat,
|
||||
backgroundId: post.backgroundId,
|
||||
mode: mode,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => SanctuarySheet.show(context, post),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.ksuPurple.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
|
||||
// Media (if available) - clickable for post detail
|
||||
if (post.imageUrl != null && post.imageUrl!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
InkWell(
|
||||
onTap: onTap,
|
||||
child: PostMedia(
|
||||
post: post,
|
||||
mode: mode,
|
||||
),
|
||||
child: Text("!", style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppTheme.royalPurple.withOpacity(0.7),
|
||||
)),
|
||||
),
|
||||
],
|
||||
|
||||
// Actions section - with padding
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: _padding.left),
|
||||
child: PostActions(
|
||||
post: post,
|
||||
onChain: onChain,
|
||||
onPostChanged: onPostChanged,
|
||||
isThreadView: isThreadView,
|
||||
showReactions: isThreadView,
|
||||
),
|
||||
),
|
||||
PostMenu(
|
||||
post: post,
|
||||
onPostDeleted: onPostChanged,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Body text - clickable for post detail with full background coverage
|
||||
InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: PostBody(
|
||||
text: post.body,
|
||||
bodyFormat: post.bodyFormat,
|
||||
backgroundId: post.backgroundId,
|
||||
mode: mode,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Media (if available) - clickable for post detail
|
||||
if (post.imageUrl != null && post.imageUrl!.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||
child: PostMedia(
|
||||
post: post,
|
||||
mode: mode,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Actions
|
||||
PostActions(
|
||||
post: post,
|
||||
onChain: onChain,
|
||||
onPostChanged: onPostChanged,
|
||||
isThreadView: isThreadView,
|
||||
showReactions: isThreadView,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue