import 'package:flutter/material.dart';
import '../../core/errors/api_error_code.dart';
import '../../core/formatting/localized_formatters.dart';
import '../../l10n/app_localizations.dart';
import '../../models/image_suggestion_models.dart';
import '../../repositories/image_suggestion_repository.dart';
import '../../shared/states/app_state_widgets.dart';
import 'image_suggestion_labels.dart';
class ImageSuggestionStatusPage extends StatefulWidget {
const ImageSuggestionStatusPage({
super.key,
required this.repository,
this.highlightedId,
});
final ImageSuggestionRepository repository;
final int? highlightedId;
@override
State<ImageSuggestionStatusPage> createState() =>
_ImageSuggestionStatusPageState();
}
class _ImageSuggestionStatusPageState extends State<ImageSuggestionStatusPage> {
late Future<ImageSuggestionList> _suggestions;
@override
void initState() {
super.initState();
_suggestions = widget.repository.own();
}
void _retry() {
setState(() {
_suggestions = widget.repository.own();
});
}
Future<void> _refresh() async {
final next = widget.repository.own();
setState(() {
_suggestions = next;
});
await next;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: Text(l10n.imageSuggestMyTitle)),
body: FutureBuilder<ImageSuggestionList>(
future: _suggestions,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const LoadingState();
}
if (snapshot.hasError) {
final error = snapshot.error;
final code = error is ApiException
? error.code
: ApiErrorCode.unknown;
if (code == ApiErrorCode.permissionDenied) {
return const AuthRequiredState();
}
return ErrorState(onRetry: _retry, code: code);
}
final items = snapshot.data?.items ?? const [];
if (items.isEmpty) {
return EmptyState(
title: l10n.imageSuggestMyTitle,
body: l10n.imageSuggestMyEmpty,
);
}
return RefreshIndicator(
onRefresh: _refresh,
child: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
itemCount: items.length,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final item = items[index];
final highlighted = item.id == widget.highlightedId;
return Card(
color: highlighted
? Theme.of(context).colorScheme.primaryContainer
: null,
child: ListTile(
leading: const Icon(Icons.photo_outlined),
title: Text(
item.originalFilename.isEmpty
? l10n.imageSuggestTarget(
imageSuggestionTargetLabel(l10n, item.targetType),
)
: item.originalFilename,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.imageSuggestMyItemStatus(
_statusLabel(l10n, item.status),
),
),
if (item.createdAt.isNotEmpty)
Text(
l10n.imageSuggestSubmittedAt(
formatDate(
item.createdAt,
Localizations.localeOf(context).toLanguageTag(),
),
),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
trailing: item.status == 'needs_info'
? IconButton(
tooltip: l10n.imageSuggestResponseOpen,
icon: const Icon(Icons.reply_outlined),
onPressed: () => _openResponse(item),
)
: null,
onTap: item.status == 'needs_info'
? () => _openResponse(item)
: null,
),
);
},
),
);
},
),
);
}
Future<void> _openResponse(ImageSuggestionData item) async {
final responseSent = await showDialog<bool>(
context: context,
builder: (_) => _ImageSuggestionResponseDialog(
repository: widget.repository,
suggestion: item,
),
);
if (responseSent == true && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).imageSuggestResponseSent),
),
);
_retry();
}
}
String _statusLabel(AppLocalizations l10n, String status) {
return switch (status) {
'submitted' => l10n.imageSuggestStatusSubmitted,
'in_review' => l10n.imageSuggestStatusInReview,
'accepted' => l10n.imageSuggestStatusAccepted,
'rejected' => l10n.imageSuggestStatusRejected,
'needs_info' => l10n.imageSuggestStatusNeedsInfo,
_ => l10n.imageSuggestStatusPending,
};
}
}
class _ImageSuggestionResponseDialog extends StatefulWidget {
const _ImageSuggestionResponseDialog({
required this.repository,
required this.suggestion,
});
final ImageSuggestionRepository repository;
final ImageSuggestionData suggestion;
@override
State<_ImageSuggestionResponseDialog> createState() =>
_ImageSuggestionResponseDialogState();
}
class _ImageSuggestionResponseDialogState
extends State<_ImageSuggestionResponseDialog> {
late final TextEditingController _controller;
bool _isSubmitting = false;
String? _error;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _submit() async {
final l10n = AppLocalizations.of(context);
final response = _controller.text.trim();
if (response.isEmpty) {
setState(() => _error = l10n.imageSuggestResponseRequired);
return;
}
setState(() {
_isSubmitting = true;
_error = null;
});
try {
await widget.repository.respond(
suggestionId: widget.suggestion.id,
response: response,
idempotencyKey:
'mw-response-${widget.suggestion.id}-${DateTime.now().microsecondsSinceEpoch}',
);
if (mounted) Navigator.of(context).pop(true);
} on ApiException catch (error) {
if (mounted) {
setState(() {
_isSubmitting = false;
_error = _errorText(l10n, error);
});
}
} on FormatException {
if (mounted) {
setState(() {
_isSubmitting = false;
_error = l10n.errorBody;
});
}
}
}
String _errorText(AppLocalizations l10n, ApiException error) {
final serverCode = error.serverCode ?? '';
if (serverCode.contains('response_required') ||
serverCode.contains('request_message_required')) {
return l10n.imageSuggestResponseRequired;
}
return switch (error.code) {
ApiErrorCode.networkUnavailable => l10n.errorNetworkUnavailable,
ApiErrorCode.timeout => l10n.errorTimeout,
ApiErrorCode.notFound => l10n.errorNotFound,
ApiErrorCode.permissionDenied => l10n.errorPermissionDenied,
ApiErrorCode.invalidResponse => l10n.errorInvalidResponse,
ApiErrorCode.serviceUnavailable => l10n.errorServiceUnavailable,
ApiErrorCode.conflict => l10n.errorConflict,
ApiErrorCode.validation => l10n.errorValidation,
ApiErrorCode.rateLimited => l10n.errorRateLimited,
_ => l10n.errorBody,
};
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return AlertDialog(
title: Text(l10n.imageSuggestResponseTitle),
content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.suggestion.requestMessage.isNotEmpty) ...[
Text(widget.suggestion.requestMessage),
const SizedBox(height: 16),
] else
Text(l10n.imageSuggestResponseBody),
const SizedBox(height: 16),
TextField(
controller: _controller,
enabled: !_isSubmitting,
minLines: 4,
maxLines: 8,
maxLength: 2000,
textInputAction: TextInputAction.newline,
decoration: InputDecoration(
labelText: l10n.imageSuggestResponseLabel,
errorText: _error,
alignLabelWithHint: true,
),
),
],
),
),
),
actions: [
TextButton(
onPressed: _isSubmitting ? null : () => Navigator.pop(context),
child: Text(l10n.close),
),
FilledButton(
onPressed: _isSubmitting ? null : _submit,
child: Text(
_isSubmitting
? l10n.imageSuggestResponseSubmitting
: l10n.imageSuggestResponseSubmit,
),
),
],
);
}
}
