557 lines
21 KiB
Dart
557 lines
21 KiB
Dart
|
|
// lib/screens/contribute_screen.dart
|
||
|
|
import 'dart:io';
|
||
|
|
import '../core/app_decoration.dart';
|
||
|
|
|
||
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:image_picker/image_picker.dart';
|
||
|
|
|
||
|
|
class ContributeScreen extends StatefulWidget {
|
||
|
|
const ContributeScreen({Key? key}) : super(key: key);
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<ContributeScreen> createState() => _ContributeScreenState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _ContributeScreenState extends State<ContributeScreen> with SingleTickerProviderStateMixin {
|
||
|
|
// Primary accent used for buttons / active tab (kept as a single constant)
|
||
|
|
static const Color _primary = Color(0xFF0B63D6);
|
||
|
|
|
||
|
|
// single corner radius to use everywhere
|
||
|
|
static const double _cornerRadius = 18.0;
|
||
|
|
|
||
|
|
// Form controllers
|
||
|
|
final TextEditingController _titleCtl = TextEditingController();
|
||
|
|
final TextEditingController _locationCtl = TextEditingController();
|
||
|
|
final TextEditingController _organizerCtl = TextEditingController();
|
||
|
|
final TextEditingController _descriptionCtl = TextEditingController();
|
||
|
|
DateTime? _selectedDate;
|
||
|
|
String _selectedCategory = 'Music';
|
||
|
|
|
||
|
|
// Image pickers
|
||
|
|
final ImagePicker _picker = ImagePicker();
|
||
|
|
XFile? _coverImageFile;
|
||
|
|
XFile? _thumbImageFile;
|
||
|
|
|
||
|
|
bool _submitting = false;
|
||
|
|
|
||
|
|
// Tab state: 0 = Contribute, 1 = Leaderboard, 2 = Achievements
|
||
|
|
int _activeTab = 0;
|
||
|
|
|
||
|
|
// Example progress value (0..1)
|
||
|
|
double _progress = 0.45;
|
||
|
|
|
||
|
|
// A few category options
|
||
|
|
final List<String> _categories = ['Music', 'Food', 'Arts', 'Sports', 'Tech', 'Community'];
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_titleCtl.dispose();
|
||
|
|
_locationCtl.dispose();
|
||
|
|
_organizerCtl.dispose();
|
||
|
|
_descriptionCtl.dispose();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _pickCoverImage() async {
|
||
|
|
try {
|
||
|
|
final XFile? picked = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 85);
|
||
|
|
if (picked != null) setState(() => _coverImageFile = picked);
|
||
|
|
} catch (e) {
|
||
|
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pick image: $e')));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _pickThumbnailImage() async {
|
||
|
|
try {
|
||
|
|
final XFile? picked = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 85);
|
||
|
|
if (picked != null) setState(() => _thumbImageFile = picked);
|
||
|
|
} catch (e) {
|
||
|
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pick image: $e')));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _pickDate() async {
|
||
|
|
final now = DateTime.now();
|
||
|
|
final picked = await showDatePicker(
|
||
|
|
context: context,
|
||
|
|
initialDate: _selectedDate ?? now,
|
||
|
|
firstDate: DateTime(now.year - 2),
|
||
|
|
lastDate: DateTime(now.year + 3),
|
||
|
|
builder: (context, child) {
|
||
|
|
return Theme(
|
||
|
|
data: Theme.of(context).copyWith(colorScheme: ColorScheme.light(primary: _primary)),
|
||
|
|
child: child!,
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
if (picked != null) setState(() => _selectedDate = picked);
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _submit() async {
|
||
|
|
if (_titleCtl.text.trim().isEmpty) {
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter an event title')));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setState(() => _submitting = true);
|
||
|
|
// simulate work
|
||
|
|
await Future.delayed(const Duration(milliseconds: 800));
|
||
|
|
|
||
|
|
setState(() => _submitting = false);
|
||
|
|
if (mounted) {
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Submitted for verification (demo)')));
|
||
|
|
_clearForm();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void _clearForm() {
|
||
|
|
_titleCtl.clear();
|
||
|
|
_locationCtl.clear();
|
||
|
|
_organizerCtl.clear();
|
||
|
|
_descriptionCtl.clear();
|
||
|
|
_selectedDate = null;
|
||
|
|
_selectedCategory = _categories.first;
|
||
|
|
_coverImageFile = null;
|
||
|
|
_thumbImageFile = null;
|
||
|
|
setState(() {});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------- UI Builders ----------
|
||
|
|
|
||
|
|
Widget _buildHeader(BuildContext context) {
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
// header uses AppDecoration.blueGradient for background (project-specific)
|
||
|
|
return Container(
|
||
|
|
width: double.infinity,
|
||
|
|
padding: const EdgeInsets.fromLTRB(20, 32, 20, 24),
|
||
|
|
decoration: AppDecoration.blueGradient.copyWith(
|
||
|
|
borderRadius: BorderRadius.only(
|
||
|
|
bottomLeft: Radius.circular(_cornerRadius),
|
||
|
|
bottomRight: Radius.circular(_cornerRadius),
|
||
|
|
),
|
||
|
|
// subtle shadow only
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(color: Colors.black.withOpacity(0.06), blurRadius: 8, offset: const Offset(0, 4)),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: SafeArea(
|
||
|
|
bottom: false,
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
// increased spacing to create a breathable layout
|
||
|
|
const SizedBox(height: 6),
|
||
|
|
|
||
|
|
// Title & subtitle (centered) — smaller title weight, clearer hierarchy
|
||
|
|
Text(
|
||
|
|
'Contributor Dashboard',
|
||
|
|
textAlign: TextAlign.center,
|
||
|
|
style: theme.textTheme.titleLarge?.copyWith(
|
||
|
|
color: Colors.white,
|
||
|
|
fontWeight: FontWeight.w700,
|
||
|
|
fontSize: 20,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 12), // more space between title & subtitle
|
||
|
|
Text(
|
||
|
|
'Track your impact, earn rewards, and climb the ranks!',
|
||
|
|
textAlign: TextAlign.center,
|
||
|
|
style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white.withOpacity(0.92), fontSize: 13, height: 1.35),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 20), // more space before tabs
|
||
|
|
|
||
|
|
// Pill-style segmented tabs (animated active) — slimmer / minimal
|
||
|
|
_buildSegmentedTabs(context),
|
||
|
|
|
||
|
|
const SizedBox(height: 18), // comfortable spacing before contributor card
|
||
|
|
|
||
|
|
// Contributor level card — lighter, soft border, thinner progress
|
||
|
|
Container(
|
||
|
|
width: double.infinity,
|
||
|
|
padding: const EdgeInsets.all(14),
|
||
|
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white.withOpacity(0.08), // slightly lighter than before
|
||
|
|
borderRadius: BorderRadius.circular(_cornerRadius - 2),
|
||
|
|
border: Border.all(color: Colors.white.withOpacity(0.09)), // soft border
|
||
|
|
),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Text('Contributor Level',
|
||
|
|
style: theme.textTheme.titleSmall?.copyWith(color: Colors.white, fontWeight: FontWeight.w700)),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
Text('Start earning rewards by contributing!', style: theme.textTheme.bodySmall?.copyWith(color: Colors.white70)),
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
Row(
|
||
|
|
children: [
|
||
|
|
Expanded(
|
||
|
|
// animated progress using TweenAnimationBuilder
|
||
|
|
child: TweenAnimationBuilder<double>(
|
||
|
|
tween: Tween(begin: 0.0, end: _progress),
|
||
|
|
duration: const Duration(milliseconds: 700),
|
||
|
|
builder: (context, value, _) => ClipRRect(
|
||
|
|
borderRadius: BorderRadius.circular(6),
|
||
|
|
child: LinearProgressIndicator(
|
||
|
|
value: value,
|
||
|
|
minHeight: 6, // thinner progress bar
|
||
|
|
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
|
||
|
|
backgroundColor: Colors.white24,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 12),
|
||
|
|
Container(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white.withOpacity(0.12),
|
||
|
|
borderRadius: BorderRadius.circular(12),
|
||
|
|
),
|
||
|
|
child: Text('Explorer', style: theme.textTheme.bodySmall?.copyWith(color: Colors.white, fontWeight: FontWeight.w600)),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
Row(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
|
|
children: [
|
||
|
|
Text('30 pts', style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white, fontWeight: FontWeight.w600)),
|
||
|
|
Text('Next: Enthusiast (50 pts)', style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white70)),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildSegmentedTabs(BuildContext context) {
|
||
|
|
final tabs = ['Contribute', 'Leaderboard', 'Achievements'];
|
||
|
|
|
||
|
|
return Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||
|
|
child: Container(
|
||
|
|
padding: const EdgeInsets.all(8),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white.withOpacity(0.06),
|
||
|
|
borderRadius: BorderRadius.circular(_cornerRadius + 6),
|
||
|
|
border: Border.all(color: Colors.white.withOpacity(0.10)),
|
||
|
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 6))],
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
children: List.generate(tabs.length, (i) {
|
||
|
|
final active = i == _activeTab;
|
||
|
|
return Expanded(
|
||
|
|
child: GestureDetector(
|
||
|
|
onTap: () => setState(() => _activeTab = i),
|
||
|
|
child: AnimatedContainer(
|
||
|
|
duration: const Duration(milliseconds: 220),
|
||
|
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 10),
|
||
|
|
margin: EdgeInsets.only(right: i == tabs.length - 1 ? 0 : 8),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: active ? Colors.white : Colors.transparent,
|
||
|
|
borderRadius: BorderRadius.circular(_cornerRadius - 4),
|
||
|
|
boxShadow: active ? [BoxShadow(color: Colors.black.withOpacity(0.06), blurRadius: 8, offset: const Offset(0, 4))] : null,
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
||
|
|
children: [
|
||
|
|
// show a small icon only for active tab
|
||
|
|
if (active) ...[
|
||
|
|
Icon(Icons.edit, size: 14, color: _primary),
|
||
|
|
const SizedBox(width: 8),
|
||
|
|
],
|
||
|
|
// FittedBox ensures the whole word is visible (scales down if necessary)
|
||
|
|
Flexible(
|
||
|
|
child: FittedBox(
|
||
|
|
fit: BoxFit.scaleDown,
|
||
|
|
alignment: Alignment.center,
|
||
|
|
child: Text(
|
||
|
|
tabs[i],
|
||
|
|
textAlign: TextAlign.center,
|
||
|
|
style: TextStyle(
|
||
|
|
color: active ? _primary : Colors.white.withOpacity(0.95),
|
||
|
|
fontWeight: active ? FontWeight.w800 : FontWeight.w600,
|
||
|
|
fontSize: 16, // preferred size; FittedBox will shrink if needed
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Widget _buildForm(BuildContext ctx) {
|
||
|
|
final theme = Theme.of(ctx);
|
||
|
|
return Container(
|
||
|
|
width: double.infinity,
|
||
|
|
margin: const EdgeInsets.only(top: 18),
|
||
|
|
padding: const EdgeInsets.fromLTRB(18, 20, 18, 28),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: theme.scaffoldBackgroundColor,
|
||
|
|
borderRadius: BorderRadius.circular(_cornerRadius),
|
||
|
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 12, offset: const Offset(0, 6))],
|
||
|
|
),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
// sheet title
|
||
|
|
Center(
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
Text('Contribute an Event', style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
Text('Share local events. Earn points for every verified submission!',
|
||
|
|
textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor)),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 18),
|
||
|
|
|
||
|
|
// small helper button
|
||
|
|
Center(
|
||
|
|
child: OutlinedButton(
|
||
|
|
onPressed: () {
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Want to edit an existing event? (demo)')));
|
||
|
|
},
|
||
|
|
style: OutlinedButton.styleFrom(
|
||
|
|
side: BorderSide(color: Colors.grey.shade300),
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(_cornerRadius - 6)),
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||
|
|
),
|
||
|
|
child: const Text('Want to edit an existing event?'),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 18),
|
||
|
|
|
||
|
|
// Event Title
|
||
|
|
Text('Event Title', style: theme.textTheme.labelLarge),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
_roundedTextField(controller: _titleCtl, hint: 'e.g. Local Food Festival'),
|
||
|
|
|
||
|
|
const SizedBox(height: 14),
|
||
|
|
|
||
|
|
// Category
|
||
|
|
Text('Category', style: theme.textTheme.labelLarge),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
Container(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
|
|
decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)),
|
||
|
|
child: DropdownButtonHideUnderline(
|
||
|
|
child: DropdownButton<String>(
|
||
|
|
value: _selectedCategory,
|
||
|
|
isExpanded: true,
|
||
|
|
items: _categories.map((c) => DropdownMenuItem(value: c, child: Text(c))).toList(),
|
||
|
|
onChanged: (v) => setState(() => _selectedCategory = v ?? _selectedCategory),
|
||
|
|
icon: const Icon(Icons.keyboard_arrow_down),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 14),
|
||
|
|
|
||
|
|
// Date
|
||
|
|
Text('Date', style: theme.textTheme.labelLarge),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
GestureDetector(
|
||
|
|
onTap: _pickDate,
|
||
|
|
child: Container(
|
||
|
|
height: 48,
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
|
|
alignment: Alignment.centerLeft,
|
||
|
|
decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)),
|
||
|
|
child: Text(_selectedDate == null ? 'Select date' : '${_selectedDate!.day}/${_selectedDate!.month}/${_selectedDate!.year}', style: theme.textTheme.bodyMedium),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 14),
|
||
|
|
|
||
|
|
// Location
|
||
|
|
Text('Location', style: theme.textTheme.labelLarge),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
_roundedTextField(controller: _locationCtl, hint: 'e.g. City Park, Calicut'),
|
||
|
|
|
||
|
|
const SizedBox(height: 14),
|
||
|
|
|
||
|
|
// Organizer
|
||
|
|
Text('Organizer Name', style: theme.textTheme.labelLarge),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
_roundedTextField(controller: _organizerCtl, hint: 'Individual or Organization Name'),
|
||
|
|
|
||
|
|
const SizedBox(height: 14),
|
||
|
|
|
||
|
|
// Description
|
||
|
|
Text('Description', style: theme.textTheme.labelLarge),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
TextField(
|
||
|
|
controller: _descriptionCtl,
|
||
|
|
minLines: 4,
|
||
|
|
maxLines: 6,
|
||
|
|
decoration: InputDecoration(
|
||
|
|
hintText: 'Tell us more about the event...',
|
||
|
|
filled: true,
|
||
|
|
fillColor: theme.cardColor,
|
||
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(_cornerRadius - 8), borderSide: BorderSide.none),
|
||
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 18),
|
||
|
|
|
||
|
|
// Event Images header
|
||
|
|
Text('Event Images', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)),
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
|
||
|
|
// Cover image
|
||
|
|
Text('Cover Image', style: theme.textTheme.bodySmall),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
GestureDetector(
|
||
|
|
onTap: _pickCoverImage,
|
||
|
|
child: _imagePickerPlaceholder(file: _coverImageFile, label: 'Cover Image'),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
|
||
|
|
// Thumbnail image
|
||
|
|
Text('Thumbnail', style: theme.textTheme.bodySmall),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
GestureDetector(
|
||
|
|
onTap: _pickThumbnailImage,
|
||
|
|
child: _imagePickerPlaceholder(file: _thumbImageFile, label: 'Thumbnail'),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 22),
|
||
|
|
|
||
|
|
// Submit button
|
||
|
|
SizedBox(
|
||
|
|
width: double.infinity,
|
||
|
|
height: 52,
|
||
|
|
child: ElevatedButton(
|
||
|
|
onPressed: _submitting ? null : _submit,
|
||
|
|
style: ElevatedButton.styleFrom(
|
||
|
|
backgroundColor: _primary,
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(_cornerRadius - 6)),
|
||
|
|
),
|
||
|
|
child: _submitting
|
||
|
|
? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.2))
|
||
|
|
: const Text('Submit for Verification', style: TextStyle(fontWeight: FontWeight.w600)),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _roundedTextField({required TextEditingController controller, required String hint}) {
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
return TextField(
|
||
|
|
controller: controller,
|
||
|
|
decoration: InputDecoration(
|
||
|
|
hintText: hint,
|
||
|
|
filled: true,
|
||
|
|
fillColor: theme.cardColor,
|
||
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(_cornerRadius - 8), borderSide: BorderSide.none),
|
||
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _imagePickerPlaceholder({XFile? file, required String label}) {
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
if (file == null) {
|
||
|
|
return Container(
|
||
|
|
width: double.infinity,
|
||
|
|
height: 120,
|
||
|
|
decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)),
|
||
|
|
child: Center(
|
||
|
|
child: Column(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
Icon(Icons.image, size: 28, color: theme.hintColor),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor)),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// show picked image (file or network depending on platform)
|
||
|
|
if (kIsWeb || file.path.startsWith('http')) {
|
||
|
|
return ClipRRect(
|
||
|
|
borderRadius: BorderRadius.circular(_cornerRadius - 8),
|
||
|
|
child: Image.network(file.path, width: double.infinity, height: 120, fit: BoxFit.cover, errorBuilder: (_, __, ___) {
|
||
|
|
return Container(
|
||
|
|
width: double.infinity,
|
||
|
|
height: 120,
|
||
|
|
decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)),
|
||
|
|
child: Center(child: Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor))),
|
||
|
|
);
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
final f = File(file.path);
|
||
|
|
if (!f.existsSync()) {
|
||
|
|
return Container(
|
||
|
|
width: double.infinity,
|
||
|
|
height: 120,
|
||
|
|
decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)),
|
||
|
|
child: Center(child: Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor))),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return ClipRRect(
|
||
|
|
borderRadius: BorderRadius.circular(_cornerRadius - 8),
|
||
|
|
child: Image.file(f, width: double.infinity, height: 120, fit: BoxFit.cover, errorBuilder: (_, __, ___) {
|
||
|
|
return Container(
|
||
|
|
width: double.infinity,
|
||
|
|
height: 120,
|
||
|
|
decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)),
|
||
|
|
child: Center(child: Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor))),
|
||
|
|
);
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
// The whole screen is scrollable — header is part of the normal scroll (not floating).
|
||
|
|
return Scaffold(
|
||
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||
|
|
body: SafeArea(
|
||
|
|
bottom: false,
|
||
|
|
child: SingleChildScrollView(
|
||
|
|
physics: const BouncingScrollPhysics(),
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
_buildHeader(context),
|
||
|
|
Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
|
|
child: _buildForm(context),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 36),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|