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 {
|
||||||
server_name api.sojorn.net;
|
server_name api.sojorn.net;
|
||||||
|
|
||||||
|
client_max_body_size 100M;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://localhost:8080;
|
proxy_pass http://localhost:8080;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|
@ -11,6 +13,11 @@ server {
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "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
|
listen 443 ssl; # managed by Certbot
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:path_provider/path_provider.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
import '../services/api_service.dart';
|
import '../services/api_service.dart';
|
||||||
import '../services/image_upload_service.dart';
|
import '../services/image_upload_service.dart';
|
||||||
|
import 'feed_refresh_provider.dart';
|
||||||
|
|
||||||
// Define the state class
|
// Define the state class
|
||||||
class QuipUploadState {
|
class QuipUploadState {
|
||||||
|
|
@ -36,14 +37,13 @@ class QuipUploadState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a Notifier class for Riverpod 3.2.0+
|
|
||||||
class QuipUploadNotifier extends Notifier<QuipUploadState> {
|
class QuipUploadNotifier extends Notifier<QuipUploadState> {
|
||||||
@override
|
@override
|
||||||
QuipUploadState build() {
|
QuipUploadState build() {
|
||||||
return QuipUploadState(isUploading: false, progress: 0.0);
|
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 {
|
try {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isUploading: true, progress: 0.0, error: null, successMessage: null);
|
isUploading: true, progress: 0.0, error: null, successMessage: null);
|
||||||
|
|
@ -60,8 +60,12 @@ class QuipUploadNotifier extends Notifier<QuipUploadState> {
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
final thumbnailPath = '${tempDir.path}/${timestamp}_thumb.jpg';
|
final thumbnailPath = '${tempDir.path}/${timestamp}_thumb.jpg';
|
||||||
|
|
||||||
|
final ss = thumbnailTimestampMs != null
|
||||||
|
? (thumbnailTimestampMs / 1000.0).toStringAsFixed(3)
|
||||||
|
: '00:00:01';
|
||||||
|
|
||||||
final session = await FFmpegKit.execute(
|
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();
|
final returnCode = await session.getReturnCode();
|
||||||
|
|
@ -102,7 +106,6 @@ class QuipUploadNotifier extends Notifier<QuipUploadState> {
|
||||||
state = state.copyWith(progress: 0.8);
|
state = state.copyWith(progress: 0.8);
|
||||||
|
|
||||||
// Publish post via Go API
|
// Publish post via Go API
|
||||||
// Video goes to video_url, thumbnail to thumbnail_url
|
|
||||||
await ApiService.instance.publishPost(
|
await ApiService.instance.publishPost(
|
||||||
body: caption,
|
body: caption,
|
||||||
videoUrl: videoUrl,
|
videoUrl: videoUrl,
|
||||||
|
|
@ -110,10 +113,20 @@ class QuipUploadNotifier extends Notifier<QuipUploadState> {
|
||||||
categoryId: null, // Default
|
categoryId: null, // Default
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Trigger feed refresh
|
||||||
|
ref.read(feedRefreshProvider.notifier).state++;
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
progress: 1.0,
|
progress: 1.0,
|
||||||
successMessage: 'Upload successful');
|
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) {
|
} catch (e) {
|
||||||
state = state.copyWith(isUploading: false, error: e.toString());
|
state = state.copyWith(isUploading: false, error: e.toString());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,48 +112,68 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Outer Ring for Upload Progress
|
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final upload = ref.watch(quipUploadProvider);
|
final upload = ref.watch(quipUploadProvider);
|
||||||
|
final isDone = !upload.isUploading && upload.progress >= 1.0;
|
||||||
|
final isUploading = upload.isUploading;
|
||||||
|
final hasState = isUploading || isDone;
|
||||||
|
|
||||||
if (!upload.isUploading && upload.progress == 0) {
|
return Container(
|
||||||
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
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 56,
|
width: 56,
|
||||||
height: 56,
|
height: 56,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppTheme.navyBlue,
|
color: hasState ? AppTheme.brightNavy : AppTheme.navyBlue,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: AppTheme.navyBlue.withOpacity(0.4),
|
color: (hasState ? AppTheme.brightNavy : AppTheme.navyBlue).withOpacity(0.4),
|
||||||
blurRadius: 12,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Icon(
|
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,
|
Icons.add,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 32,
|
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
|
/// Provides the current navigation shell index to descendants that need to
|
||||||
/// react (e.g. pausing quip playback when the tab is not active).
|
/// react (e.g. pausing quip playback when the tab is not active).
|
||||||
class NavigationShellScope extends InheritedWidget {
|
class NavigationShellScope extends InheritedWidget {
|
||||||
|
|
|
||||||
|
|
@ -574,7 +574,7 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: SignedMediaImage(
|
child: SignedMediaImage(
|
||||||
url: imageUrl,
|
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/ffmpeg_kit.dart';
|
||||||
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
|
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
|
||||||
import 'package:path_provider/path_provider.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 '../../../services/image_upload_service.dart';
|
||||||
import '../../../providers/api_provider.dart';
|
import '../../../providers/api_provider.dart';
|
||||||
import '../../../providers/feed_refresh_provider.dart';
|
import '../../../providers/feed_refresh_provider.dart';
|
||||||
|
|
@ -23,8 +23,8 @@ class _QuipMetadataScreenState extends ConsumerState<QuipMetadataScreen> {
|
||||||
late VideoPlayerController _controller;
|
late VideoPlayerController _controller;
|
||||||
final TextEditingController _captionController = TextEditingController();
|
final TextEditingController _captionController = TextEditingController();
|
||||||
double _coverTimestamp = 0.0;
|
double _coverTimestamp = 0.0;
|
||||||
bool _isUploading = false;
|
// bool _isUploading = false;
|
||||||
final ImageUploadService _uploadService = ImageUploadService();
|
// final ImageUploadService _uploadService = ImageUploadService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -41,77 +41,25 @@ class _QuipMetadataScreenState extends ConsumerState<QuipMetadataScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _postQuip() async {
|
Future<void> _postQuip() async {
|
||||||
if (_isUploading) return;
|
final uploadNotifier = ref.read(quipUploadProvider.notifier);
|
||||||
setState(() => _isUploading = true);
|
|
||||||
|
|
||||||
try {
|
// We already have the logic to generate a specific thumbnail in the provider,
|
||||||
// 1. Generate Thumbnail
|
// but the screen allows choosing a timestamp.
|
||||||
String? thumbnailUrl;
|
// To support the chosen cover, we'll generate it here and then pass it
|
||||||
try {
|
// or just pass the timestamp to the provider.
|
||||||
final tempDir = await getTemporaryDirectory();
|
// Let's pass the chosen timestamp to startUpload.
|
||||||
final thumbPath = '${tempDir.path}/thumbnail_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
|
||||||
final seconds = _coverTimestamp / 1000.0;
|
|
||||||
|
|
||||||
// Execute FFmpeg to extract frame
|
uploadNotifier.startUpload(
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Upload Video
|
|
||||||
final videoUrl = await _uploadService.uploadVideo(
|
|
||||||
widget.videoFile,
|
widget.videoFile,
|
||||||
onProgress: (progress) {
|
_captionController.text.trim(),
|
||||||
// Optional: Update progress UI
|
thumbnailTimestampMs: _coverTimestamp,
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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) {
|
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);
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text("Upload failed: $e")),
|
const SnackBar(content: Text("Upload started in background")),
|
||||||
);
|
);
|
||||||
setState(() => _isUploading = false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,14 +83,12 @@ class _QuipMetadataScreenState extends ConsumerState<QuipMetadataScreen> {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _isUploading ? null : _postQuip,
|
onPressed: _postQuip,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppTheme.brightNavy,
|
backgroundColor: AppTheme.brightNavy,
|
||||||
shape: const StadiumBorder(),
|
shape: const StadiumBorder(),
|
||||||
),
|
),
|
||||||
child: _isUploading
|
child: const Text("Post"),
|
||||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
|
||||||
: const Text("Post"),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -486,14 +486,16 @@ class NotificationService {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'like':
|
case 'like':
|
||||||
|
case 'quip_reaction':
|
||||||
case 'save':
|
case 'save':
|
||||||
case 'comment':
|
case 'comment':
|
||||||
case 'reply':
|
case 'reply':
|
||||||
case 'mention':
|
case 'mention':
|
||||||
final postId = data['post_id'];
|
case 'share':
|
||||||
|
final postId = data['post_id'] ?? data['beacon_id'];
|
||||||
final target = data['target'];
|
final target = data['target'];
|
||||||
if (postId != null) {
|
if (postId != null) {
|
||||||
_navigateToPost(navigator, postId, target);
|
_navigateToPost(navigator, postId.toString(), target?.toString());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -503,7 +505,7 @@ class NotificationService {
|
||||||
case 'follow_accepted':
|
case 'follow_accepted':
|
||||||
final followerId = data['follower_id'];
|
final followerId = data['follower_id'];
|
||||||
if (followerId != null) {
|
if (followerId != null) {
|
||||||
navigator.context.push('/u/$followerId');
|
navigator.context.push('${AppRoutes.userPrefix}/$followerId');
|
||||||
} else {
|
} else {
|
||||||
navigator.context.go(AppRoutes.profile);
|
navigator.context.go(AppRoutes.profile);
|
||||||
}
|
}
|
||||||
|
|
@ -511,7 +513,12 @@ class NotificationService {
|
||||||
|
|
||||||
case 'beacon_vouch':
|
case 'beacon_vouch':
|
||||||
case 'beacon_report':
|
case 'beacon_report':
|
||||||
|
final beaconId = data['beacon_id'] ?? data['post_id'];
|
||||||
|
if (beaconId != null) {
|
||||||
|
_navigateToPost(navigator, beaconId.toString(), 'beacon_map');
|
||||||
|
} else {
|
||||||
navigator.context.go(AppRoutes.beaconPrefix);
|
navigator.context.go(AppRoutes.beaconPrefix);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -44,13 +44,11 @@ class PostMedia extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxHeight: _imageHeight),
|
constraints: BoxConstraints(maxHeight: _imageHeight),
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: SignedMediaImage(
|
child: SignedMediaImage(
|
||||||
url: post!.imageUrl!,
|
url: post!.imageUrl!,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.cover,
|
||||||
loadingBuilder: (context) => Container(
|
loadingBuilder: (context) => Container(
|
||||||
color: AppTheme.queenPink.withValues(alpha: 0.3),
|
color: AppTheme.queenPink.withValues(alpha: 0.3),
|
||||||
child: const Center(child: CircularProgressIndicator()),
|
child: const Center(child: CircularProgressIndicator()),
|
||||||
|
|
@ -74,7 +72,6 @@ class PostMedia extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,6 @@ class sojornPostCard extends StatelessWidget {
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16), // Add spacing between cards
|
margin: const EdgeInsets.only(bottom: 16), // Add spacing between cards
|
||||||
padding: _padding,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppTheme.cardSurface,
|
color: AppTheme.cardSurface,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
|
@ -103,6 +102,16 @@ class sojornPostCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Container(
|
||||||
|
padding: _padding.copyWith(left: 0, right: 0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Internal horizontal padding for text/actions
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: _padding.left),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -182,32 +191,40 @@ class sojornPostCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Media (if available) - clickable for post detail
|
// Media (if available) - clickable for post detail
|
||||||
if (post.imageUrl != null && post.imageUrl!.isNotEmpty) ...[
|
if (post.imageUrl != null && post.imageUrl!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 12),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
|
||||||
child: PostMedia(
|
child: PostMedia(
|
||||||
post: post,
|
post: post,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Actions
|
// Actions section - with padding
|
||||||
PostActions(
|
const SizedBox(height: 16),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: _padding.left),
|
||||||
|
child: PostActions(
|
||||||
post: post,
|
post: post,
|
||||||
onChain: onChain,
|
onChain: onChain,
|
||||||
onPostChanged: onPostChanged,
|
onPostChanged: onPostChanged,
|
||||||
isThreadView: isThreadView,
|
isThreadView: isThreadView,
|
||||||
showReactions: isThreadView,
|
showReactions: isThreadView,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue