fluffychat/lib/pangea/choreographer/igc/span_card.dart

321 lines
9.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
import 'package:fluffychat/pangea/choreographer/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/igc/pangea_match_state_model.dart';
import 'package:fluffychat/pangea/choreographer/igc/pangea_match_status_enum.dart';
import 'package:fluffychat/pangea/choreographer/igc/span_choice_type_enum.dart';
import 'package:fluffychat/pangea/choreographer/igc/span_data_model.dart';
import 'package:fluffychat/pangea/common/utils/async_state.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import '../../../widgets/matrix.dart';
import '../../common/widgets/choice_array.dart';
class SpanCard extends StatefulWidget {
final PangeaMatchState match;
final Choreographer choreographer;
final VoidCallback showNextMatch;
const SpanCard({
super.key,
required this.match,
required this.choreographer,
required this.showNextMatch,
});
@override
State<SpanCard> createState() => SpanCardState();
}
class SpanCardState extends State<SpanCard> {
bool _loadingChoices = true;
final ValueNotifier<AsyncState<String>> _feedbackState =
ValueNotifier<AsyncState<String>>(const AsyncIdle<String>());
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_fetchChoices();
}
@override
void dispose() {
_feedbackState.dispose();
scrollController.dispose();
super.dispose();
}
List<SpanChoice>? get _choices => widget.match.updatedMatch.match.choices;
SpanChoice? get _selectedChoice =>
widget.match.updatedMatch.match.selectedChoice;
String? get _selectedFeedback => _selectedChoice?.feedback;
Future<void> _fetchChoices() async {
if (_choices != null && _choices!.length > 1) {
setState(() => _loadingChoices = false);
return;
}
setState(() => _loadingChoices = true);
try {
await widget.choreographer.igcController.fetchSpanDetails(
match: widget.match,
);
if (_choices == null || _choices!.isEmpty) {
widget.choreographer.clearMatches(
'No choices available for span ${widget.match.updatedMatch.match.message}',
);
}
} catch (e) {
widget.choreographer.clearMatches(e);
} finally {
if (mounted) {
setState(() => _loadingChoices = false);
}
}
}
Future<void> _fetchFeedback() async {
if (_selectedFeedback != null) {
_feedbackState.value = AsyncLoaded<String>(_selectedFeedback!);
return;
}
try {
_feedbackState.value = const AsyncLoading<String>();
await widget.choreographer.igcController.fetchSpanDetails(
match: widget.match,
force: true,
);
if (!mounted) return;
if (_selectedFeedback != null) {
_feedbackState.value = AsyncLoaded<String>(_selectedFeedback!);
} else {
_feedbackState.value = AsyncError<String>(
L10n.of(context).failedToLoadFeedback,
);
}
} catch (e) {
if (mounted) {
_feedbackState.value = AsyncError<String>(e);
}
}
}
void _onChoiceSelect(int index) {
final selected = _choices![index];
widget.match.selectChoice(index);
_feedbackState.value = selected.feedback != null
? AsyncLoaded<String>(selected.feedback!)
: const AsyncIdle<String>();
setState(() {});
}
void _updateMatch(PangeaMatchStatusEnum status) {
try {
widget.choreographer.igcController.updateMatch(
widget.match,
status,
);
widget.showNextMatch();
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
level: SentryLevel.warning,
data: {
"match": widget.match.toJson(),
},
);
widget.choreographer.clearMatches(e);
return;
}
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 300.0,
child: Column(
children: [
Expanded(
child: Scrollbar(
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 24.0,
),
child: Column(
spacing: 12.0,
children: [
ChoicesArray(
isLoading: _loadingChoices,
choices: widget.match.updatedMatch.match.choices
?.map(
(e) => Choice(
text: e.value,
color: e.selected ? e.type.color : null,
isGold: e.type.name == 'bestCorrection',
),
)
.toList(),
onPressed: (value, index) => _onChoiceSelect(index),
selectedChoiceIndex:
widget.match.updatedMatch.match.selectedChoiceIndex,
id: widget.match.hashCode.toString(),
langCode: MatrixState
.pangeaController.userController.userL2Code!,
),
_SpanCardFeedback(
_selectedChoice != null,
_fetchFeedback,
_feedbackState,
),
],
),
),
),
),
),
_SpanCardButtons(
onAccept: () => _updateMatch(PangeaMatchStatusEnum.accepted),
onIgnore: () => _updateMatch(PangeaMatchStatusEnum.ignored),
selectedChoice: _selectedChoice,
),
],
),
);
}
}
class _SpanCardFeedback extends StatelessWidget {
final bool hasSelectedChoice;
final VoidCallback fetchFeedback;
final ValueNotifier<AsyncState<String>> feedbackState;
const _SpanCardFeedback(
this.hasSelectedChoice,
this.fetchFeedback,
this.feedbackState,
);
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 100.0,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ValueListenableBuilder(
valueListenable: feedbackState,
builder: (context, state, __) {
return switch (state) {
AsyncIdle<String>() => hasSelectedChoice
? IconButton(
onPressed: fetchFeedback,
icon: const Icon(Icons.lightbulb_outline, size: 24),
)
: Text(
L10n.of(context).correctionDefaultPrompt,
style: BotStyle.text(context).copyWith(
fontStyle: FontStyle.italic,
),
),
AsyncLoading<String>() => const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(),
),
AsyncError<String>(:final error) =>
ErrorIndicator(message: error.toString()),
AsyncLoaded<String>(:final value) =>
Text(value, style: BotStyle.text(context)),
};
},
),
],
),
);
}
}
class _SpanCardButtons extends StatelessWidget {
final VoidCallback onAccept;
final VoidCallback onIgnore;
final SpanChoice? selectedChoice;
const _SpanCardButtons({
required this.onAccept,
required this.onIgnore,
required this.selectedChoice,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
),
padding: const EdgeInsets.only(top: 12.0),
child: Row(
spacing: 10.0,
children: [
Expanded(
child: Opacity(
opacity: 0.8,
child: TextButton(
style: TextButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primary.withAlpha(25),
),
onPressed: onIgnore,
child: Center(
child: Text(L10n.of(context).ignoreInThisText),
),
),
),
),
Expanded(
child: Opacity(
opacity: selectedChoice != null ? 1.0 : 0.5,
child: TextButton(
onPressed: selectedChoice != null ? onAccept : null,
style: TextButton.styleFrom(
backgroundColor: (selectedChoice?.color ??
Theme.of(context).colorScheme.primary)
.withAlpha(50),
side: selectedChoice != null
? BorderSide(
color: selectedChoice!.color,
style: BorderStyle.solid,
width: 2.0,
)
: null,
),
child: Text(L10n.of(context).replace),
),
),
),
],
),
);
}
}