You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fluffychat/lib/pangea/message_token_text/message_token_button.dart

414 lines
12 KiB
Dart

import 'dart:developer';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/message_token_text/dotted_border_painter.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/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_selection.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
const double tokenButtonHeight = 40.0;
const double tokenButtonDefaultFontSize = 10;
const int maxEmojisPerLemma = 1;
const double estimatedEmojiWidthRatio = 2;
class MessageTokenButton extends StatefulWidget {
final MessageOverlayController? overlayController;
final PangeaToken token;
final TextStyle textStyle;
final double width;
final bool animateIn;
const MessageTokenButton({
super.key,
required this.overlayController,
required this.token,
required this.textStyle,
required this.width,
this.animateIn = false,
});
@override
MessageTokenButtonState createState() => MessageTokenButtonState();
}
class MessageTokenButtonState extends State<MessageTokenButton>
with TickerProviderStateMixin {
AnimationController? _controller;
Animation<double>? _heightAnimation;
// New controller and animation for icon size
AnimationController? _iconSizeController;
Animation<double>? _iconSizeAnimation;
bool _isHovered = false;
bool _isSelected = false;
bool _finishedInitialAnimation = false;
bool _wasEmpty = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
),
);
_heightAnimation = Tween<double>(
begin: 0,
end: tokenButtonHeight,
).animate(CurvedAnimation(parent: _controller!, curve: Curves.easeOut));
// Initialize the new icon size controller and animation
_iconSizeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_iconSizeAnimation = Tween<double>(
begin: 24, // Default icon size
end: 30, // Enlarged icon size
).animate(
CurvedAnimation(parent: _iconSizeController!, curve: Curves.easeInOut),
);
_setSelected(); // Call _setSelected after initializing _iconSizeController
_wasEmpty = _isEmpty;
if (!_isEmpty) {
_controller?.forward().then((_) {
if (mounted) setState(() => _finishedInitialAnimation = true);
});
} else {
setState(() => _finishedInitialAnimation = true);
}
}
@override
void didUpdateWidget(covariant MessageTokenButton oldWidget) {
super.didUpdateWidget(oldWidget);
_setSelected();
if (_isEmpty != _wasEmpty) {
if (_isEmpty && _animate) {
_controller?.reverse();
} else if (!_isEmpty && _animate) {
_controller?.forward();
}
setState(() => _wasEmpty = _isEmpty);
}
}
@override
void dispose() {
_controller?.dispose();
_iconSizeController?.dispose(); // Dispose the new controller
super.dispose();
}
PracticeTarget? get _activity =>
widget.overlayController?.practiceTargetForToken(widget.token);
bool get _animate => widget.animateIn || _finishedInitialAnimation;
bool get _isActivityCompleteOrNullForToken =>
_activity?.isCompleteByToken(
widget.token,
_activity!.morphFeature,
) ==
true;
void _setSelected() {
final selected =
widget.overlayController?.selectedMorph?.token == widget.token &&
widget.overlayController?.selectedMorph?.morph ==
_activity?.morphFeature;
if (selected != _isSelected) {
setState(() {
_isSelected = selected;
});
_isSelected
? _iconSizeController?.forward()
: _iconSizeController?.reverse();
}
}
void _setHovered(bool isHovered) {
if (isHovered != _isHovered) {
setState(() {
_isHovered = isHovered;
});
if (!_isHovered && _isSelected) {
return;
}
_isHovered
? _iconSizeController?.forward()
: _iconSizeController?.reverse();
}
}
void _onMatch(PracticeChoice form) {
if (widget.overlayController?.activity == null) {
debugger(when: kDebugMode);
ErrorHandler.logError(
m: "should not be in onAcceptWithDetails with null activity",
data: {"details": form},
);
return;
}
widget.overlayController!.onChoiceSelect(null);
widget.overlayController!.activity!.onMatch(
widget.token,
form,
widget.overlayController!.pangeaMessageEvent,
() => widget.overlayController!.setState(() {}),
);
}
bool get _isEmpty {
final mode = widget.overlayController?.toolbarMode;
if (MessageMode.wordEmoji == mode &&
widget.token.vocabConstructID.userSetEmoji.firstOrNull != null) {
return false;
}
return _activity == null ||
(_isActivityCompleteOrNullForToken &&
![MessageMode.wordEmoji, MessageMode.wordMorph].contains(mode)) ||
(MessageMode.wordMorph == mode && _activity?.morphFeature == null);
}
@override
Widget build(BuildContext context) {
if (widget.overlayController == null) {
return const SizedBox.shrink();
}
if (!_animate && _iconSizeAnimation != null) {
return MessageTokenButtonContent(
activity: _activity,
messageMode: widget.overlayController!.toolbarMode,
token: widget.token,
selectedChoice: widget.overlayController?.selectedChoice,
isActivityCompleteOrNullForToken: _isActivityCompleteOrNullForToken,
isSelected: _isSelected,
height: tokenButtonHeight,
width: widget.width,
textStyle: widget.textStyle,
sizeAnimation: _iconSizeAnimation!,
onHover: _setHovered,
onTap: () => widget.overlayController!.onMorphActivitySelect(
MorphSelection(widget.token, _activity!.morphFeature!),
),
onMatch: _onMatch,
);
}
if (_heightAnimation != null && _iconSizeAnimation != null) {
return AnimatedBuilder(
animation: _heightAnimation!,
builder: (context, child) {
return MessageTokenButtonContent(
activity: _activity,
messageMode: widget.overlayController!.toolbarMode,
token: widget.token,
selectedChoice: widget.overlayController?.selectedChoice,
isActivityCompleteOrNullForToken: _isActivityCompleteOrNullForToken,
isSelected: _isSelected,
height: _heightAnimation!.value,
width: widget.width,
textStyle: widget.textStyle,
sizeAnimation: _iconSizeAnimation!,
onHover: _setHovered,
onTap: () => widget.overlayController!.onMorphActivitySelect(
MorphSelection(widget.token, _activity!.morphFeature!),
),
onMatch: _onMatch,
);
},
);
}
return const SizedBox.shrink();
}
}
class MessageTokenButtonContent extends StatelessWidget {
final PracticeTarget? activity;
final MessageMode messageMode;
final PangeaToken token;
final PracticeChoice? selectedChoice;
final bool isActivityCompleteOrNullForToken;
final bool isSelected;
final double height;
final double width;
final TextStyle textStyle;
final Animation<double> sizeAnimation;
final Function(bool)? onHover;
final Function()? onTap;
final Function(PracticeChoice)? onMatch;
const MessageTokenButtonContent({
super.key,
required this.activity,
required this.messageMode,
required this.token,
required this.selectedChoice,
required this.isActivityCompleteOrNullForToken,
required this.isSelected,
required this.height,
required this.width,
required this.textStyle,
required this.sizeAnimation,
this.onHover,
this.onTap,
this.onMatch,
});
TextStyle get _emojiStyle => textStyle.copyWith(
fontSize: (textStyle.fontSize ?? tokenButtonDefaultFontSize) + 4,
);
static final _borderRadius =
BorderRadius.circular(AppConfig.borderRadius - 4);
Color _color(BuildContext context) {
final bool isLight = Theme.of(context).brightness == Brightness.light;
final defaultColor = isLight
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.primaryContainer;
return activity != null && isActivityCompleteOrNullForToken
? AppConfig.gold
: defaultColor;
}
@override
Widget build(BuildContext context) {
if (isActivityCompleteOrNullForToken || activity == null) {
if (MessageMode.wordEmoji == messageMode) {
return SizedBox(
height: height,
child: Text(
token.vocabConstructID.userSetEmoji.firstOrNull ?? '',
style: _emojiStyle,
),
);
}
if (MessageMode.wordMorph == messageMode && activity != null) {
final morphFeature = activity!.morphFeature!;
final morphTag = token.morphIdByFeature(morphFeature);
if (morphTag != null) {
return Tooltip(
message: getGrammarCopy(
category: morphFeature.toShortString(),
lemma: morphTag.lemma,
context: context,
),
child: SizedBox(
width: 24.0,
child: Center(
child: MorphIcon(
morphFeature: morphFeature,
morphTag: morphTag.lemma,
),
),
),
);
}
} else {
return SizedBox(height: height);
}
}
if (MessageMode.wordMorph == messageMode) {
if (activity?.morphFeature == null) {
return SizedBox(height: height);
}
return InkWell(
onHover: onHover,
onTap: onTap,
borderRadius: _borderRadius,
child: SizedBox(
height: height,
child: Opacity(
opacity: isSelected ? 1.0 : 0.4,
child: AnimatedBuilder(
animation: sizeAnimation,
builder: (context, child) {
return Icon(
Symbols.toys_and_games,
color: _color(context),
size: sizeAnimation.value, // Use the new animation
);
},
),
),
),
);
}
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);
return InkWell(
onTap: selectedChoice != null
? () => onMatch?.call(selectedChoice!)
: null,
borderRadius: _borderRadius,
child: CustomPaint(
painter: DottedBorderPainter(
color: theme.brightness == Brightness.light
? theme.colorScheme.primary
.withAlpha((colorAlpha * 255).toInt())
: theme.colorScheme.primaryContainer
.withAlpha((colorAlpha * 255).toInt()),
borderRadius: _borderRadius,
),
child: Container(
height: height,
padding: const EdgeInsets.only(top: 10.0),
width: max(width, 24.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: theme.colorScheme.primary
.withAlpha((max(0, colorAlpha - 0.7) * 255).toInt()),
borderRadius: _borderRadius,
),
),
),
);
},
onAcceptWithDetails: (details) => onMatch?.call(details.data),
);
}
}