fluffychat/lib/pangea/toolbar/message_practice/token_practice_button.dart
2026-01-15 13:13:56 -05:00

303 lines
9.6 KiB
Dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:shimmer/shimmer.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/dotted_border_painter.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_practice_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/morph_selection.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/practice_controller.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_controller.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
const double tokenButtonHeight = 40.0;
const double tokenButtonDefaultFontSize = 10;
const int maxEmojisPerLemma = 1;
class TokenPracticeButton extends StatelessWidget {
final PracticeController controller;
final PangeaToken token;
final TextStyle textStyle;
final double width;
final Color textColor;
const TokenPracticeButton({
super.key,
required this.controller,
required this.token,
required this.textStyle,
required this.width,
required this.textColor,
});
TextStyle get _emojiStyle => TextStyle(
fontSize: (textStyle.fontSize ?? tokenButtonDefaultFontSize) + 4,
);
PracticeTarget? get _activity => controller.practiceTargetForToken(token);
bool get isActivityCompleteOrNullForToken {
if (_activity == null) return true;
return PracticeRecordController.isCompleteByToken(_activity!, token);
}
bool get _isEmpty => controller.isPracticeButtonEmpty(token);
bool get _isSelected =>
controller.selectedMorph?.token == token &&
controller.selectedMorph?.morph == _activity?.morphFeature;
void _onMatch(PracticeChoice form) {
controller.onChoiceSelect(null);
controller.onMatch(token, form);
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: controller,
builder: (context, _) {
final practiceMode = controller.practiceMode;
Widget child;
if (isActivityCompleteOrNullForToken || _activity == null) {
child = _NoActivityContentButton(
practiceMode: practiceMode,
token: token,
target: _activity,
emojiStyle: _emojiStyle,
width: tokenButtonHeight,
);
} else if (practiceMode == MessagePracticeMode.wordMorph) {
child = _MorphMatchButton(
active: _isSelected,
textColor: textColor,
width: tokenButtonHeight,
onTap: () => controller.onSelectMorph(
MorphSelection(
token,
_activity!.morphFeature!,
),
),
shimmer: controller.selectedMorph == null &&
_activity != null &&
!PracticeRecordController.hasAnyCorrectChoices(_activity!),
);
} else {
child = _StandardMatchButton(
selectedChoice: controller.selectedChoice,
width: width,
borderColor: textColor,
onMatch: (choice) => _onMatch(choice),
);
}
return AnimatedSize(
duration: const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
),
curve: Curves.easeOut,
alignment: Alignment.bottomCenter,
child: _isEmpty
? const SizedBox()
: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 4.0),
SizedBox(height: tokenButtonHeight, child: child),
],
),
);
},
);
}
}
class _StandardMatchButton extends StatelessWidget {
final PracticeChoice? selectedChoice;
final double width;
final Color borderColor;
final Function(PracticeChoice choice) onMatch;
const _StandardMatchButton({
required this.selectedChoice,
required this.width,
required this.borderColor,
required this.onMatch,
});
@override
Widget build(BuildContext context) {
return DragTarget<PracticeChoice>(
builder: (BuildContext context, accepted, rejected) {
final double colorAlpha = 0.3 +
(selectedChoice != null ? 0.4 : 0.0) +
(accepted.isNotEmpty ? 0.3 : 0.0);
final theme = Theme.of(context);
final borderRadius = BorderRadius.circular(AppConfig.borderRadius - 4);
return Material(
type: MaterialType.transparency,
child: InkWell(
onTap:
selectedChoice != null ? () => onMatch(selectedChoice!) : null,
borderRadius: borderRadius,
child: CustomPaint(
painter: DottedBorderPainter(
color: borderColor.withAlpha((colorAlpha * 255).toInt()),
borderRadius: borderRadius,
),
child: Shimmer.fromColors(
enabled: selectedChoice != null,
baseColor: selectedChoice != null
? AppConfig.gold.withAlpha(20)
: Colors.transparent,
highlightColor: selectedChoice != null
? AppConfig.gold.withAlpha(50)
: Colors.transparent,
child: Container(
padding: const EdgeInsets.only(top: 10.0),
width: max(width, 24.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: borderRadius,
),
),
),
),
),
);
},
onAcceptWithDetails: (details) => onMatch(details.data),
);
}
}
class _MorphMatchButton extends StatelessWidget {
final Function()? onTap;
final bool active;
final Color textColor;
final bool shimmer;
final double width;
const _MorphMatchButton({
required this.active,
required this.textColor,
required this.width,
this.shimmer = false,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: HoverBuilder(
builder: (context, hovered) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppConfig.borderRadius - 4),
child: ShimmerBackground(
enabled: shimmer,
child: SizedBox(
width: width,
child: Center(
child: Opacity(
opacity: active ? 1.0 : 0.6,
child: AnimatedScale(
scale: hovered || active ? 1.25 : 1.0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: Icon(
Symbols.toys_and_games,
color: textColor,
size: 24.0,
),
),
),
),
),
),
);
},
),
);
}
}
class _NoActivityContentButton extends StatelessWidget {
final MessagePracticeMode practiceMode;
final PangeaToken token;
final PracticeTarget? target;
final TextStyle emojiStyle;
final double width;
const _NoActivityContentButton({
required this.practiceMode,
required this.token,
required this.target,
required this.emojiStyle,
required this.width,
});
@override
Widget build(BuildContext context) {
if (practiceMode == MessagePracticeMode.wordEmoji && target != null) {
final displayEmoji =
PracticeRecordController.correctResponse(target!, token)?.text ??
token.vocabConstructID.userSetEmoji ??
'';
return Text(
displayEmoji,
style: emojiStyle,
);
}
if (practiceMode == MessagePracticeMode.wordMorph && target != null) {
final morphFeature = target!.morphFeature!;
final morphTag = token.morphIdByFeature(morphFeature);
if (morphTag != null) {
return Tooltip(
message: getGrammarCopy(
category: morphFeature.toShortString(),
lemma: morphTag.lemma,
context: context,
),
child: SizedBox(
width: width,
child: Center(
child: CircleAvatar(
radius: width / 2,
backgroundColor:
Theme.of(context).brightness != Brightness.light
? Theme.of(context).colorScheme.surface.withAlpha(100)
: null,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: MorphIcon(
morphFeature: morphFeature,
morphTag: morphTag.lemma,
size: Size.fromWidth(width - 8.0),
),
),
),
),
),
);
}
}
return const SizedBox();
}
}