resolve merge conflicts
This commit is contained in:
commit
17726c9ca1
24 changed files with 579 additions and 422 deletions
|
|
@ -54,7 +54,7 @@ abstract class AppConfig {
|
|||
final bigEmotes = event != null &&
|
||||
event.onlyEmotes &&
|
||||
event.numberEmotes > 0 &&
|
||||
event.numberEmotes <= 10;
|
||||
event.numberEmotes <= 3;
|
||||
|
||||
return TextStyle(
|
||||
color: textColor,
|
||||
|
|
|
|||
|
|
@ -5033,5 +5033,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"failedToFetchTranscription": "Failed to fetch transcription"
|
||||
"failedToFetchTranscription": "Failed to fetch transcription",
|
||||
"deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:device_info_plus/device_info_plus.dart';
|
|||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
|
@ -26,6 +27,7 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_view.dart';
|
||||
import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
||||
import 'package:fluffychat/pages/chat/events/audio_player.dart';
|
||||
import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
|
|
@ -33,6 +35,7 @@ import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
|||
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/chat/utils/unlocked_morphs_snackbar.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/event_too_large_dialog.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
|
|
@ -46,6 +49,7 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
|||
import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
|
||||
|
|
@ -149,6 +153,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
|
||||
StreamSubscription? _levelSubscription;
|
||||
StreamSubscription? _analyticsSubscription;
|
||||
StreamSubscription? _botAudioSubscription;
|
||||
// Pangea#
|
||||
Room get room => sendingClient.getRoomById(roomId) ?? widget.room;
|
||||
|
||||
|
|
@ -473,6 +478,43 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
ignorePointer: true,
|
||||
);
|
||||
});
|
||||
|
||||
_botAudioSubscription = room.client.onSync.stream
|
||||
.where(
|
||||
(update) => update.rooms?.join?[roomId]?.timeline?.events != null,
|
||||
)
|
||||
.listen((update) async {
|
||||
final timeline = update.rooms!.join![roomId]!.timeline!;
|
||||
final botAudioEvent = timeline.events!.firstWhereOrNull(
|
||||
(e) =>
|
||||
e.senderId == BotName.byEnvironment &&
|
||||
e.content.tryGet<String>('msgtype') == MessageTypes.Audio &&
|
||||
DateTime.now().difference(e.originServerTs) <
|
||||
const Duration(seconds: 10),
|
||||
);
|
||||
if (botAudioEvent == null) return;
|
||||
|
||||
final matrix = Matrix.of(context);
|
||||
matrix.voiceMessageEventId.value = botAudioEvent.eventId;
|
||||
matrix.audioPlayer?.dispose();
|
||||
matrix.audioPlayer = AudioPlayer();
|
||||
|
||||
final event = Event.fromMatrixEvent(botAudioEvent, room);
|
||||
final audioFile = await event.getPangeaAudioFile();
|
||||
debugPrint(
|
||||
"audiofile: ${audioFile?.mimeType} ${audioFile?.bytes.length}",
|
||||
);
|
||||
if (audioFile == null) return;
|
||||
|
||||
matrix.audioPlayer!.setAudioSource(
|
||||
BytesAudioSource(
|
||||
audioFile.bytes,
|
||||
audioFile.mimeType,
|
||||
),
|
||||
);
|
||||
|
||||
matrix.audioPlayer!.play();
|
||||
});
|
||||
// Pangea#
|
||||
_tryLoadTimeline();
|
||||
if (kIsWeb) {
|
||||
|
|
@ -719,6 +761,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
stopMediaStream.close();
|
||||
_levelSubscription?.cancel();
|
||||
_analyticsSubscription?.cancel();
|
||||
_botAudioSubscription?.cancel();
|
||||
_router.routeInformationProvider.removeListener(_onRouteChanged);
|
||||
//Pangea#
|
||||
super.dispose();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
|
|
@ -10,7 +11,9 @@ import 'package:fluffychat/pages/chat/events/message.dart';
|
|||
import 'package:fluffychat/pages/chat/seen_by_row.dart';
|
||||
import 'package:fluffychat/pages/chat/typing_indicators.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_message.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
|
@ -40,6 +43,30 @@ class ChatEventList extends StatelessWidget {
|
|||
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
|
||||
|
||||
final events = timeline.events.filterByVisibleInGui();
|
||||
// #Pangea
|
||||
if (timeline.room.activityPlan?.endAt != null &&
|
||||
timeline.room.activityPlan!.endAt!.isBefore(DateTime.now())) {
|
||||
final eventIndex = events.indexWhere(
|
||||
(e) => e.originServerTs.isBefore(
|
||||
timeline.room.activityPlan!.endAt!,
|
||||
),
|
||||
);
|
||||
|
||||
if (eventIndex != -1) {
|
||||
events.insert(
|
||||
eventIndex,
|
||||
Event(
|
||||
type: PangeaEventTypes.activityPlanEnd,
|
||||
eventId: timeline.room.client.generateUniqueTransactionId(),
|
||||
senderId: timeline.room.client.userID!,
|
||||
originServerTs: timeline.room.activityPlan!.endAt!,
|
||||
room: timeline.room,
|
||||
content: {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
final animateInEventIndex = controller.animateInEventIndex;
|
||||
|
||||
// create a map of eventId --> index to greatly improve performance of
|
||||
|
|
|
|||
|
|
@ -254,10 +254,10 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
_onAudioPositionChanged =
|
||||
matrix.audioPlayer!.positionStream.listen((state) {
|
||||
// Pass current timestamp to overlay, so it can highlight as necessary
|
||||
if (widget.matrixFile != null) {
|
||||
if (widget.matrixFile?.tokens != null) {
|
||||
widget.overlayController?.highlightCurrentText(
|
||||
state.inMilliseconds,
|
||||
widget.matrixFile!.tokens,
|
||||
widget.matrixFile!.tokens!,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -308,19 +308,34 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
final audioPlayer = matrix.audioPlayer;
|
||||
if (audioPlayer == null) return;
|
||||
switch (audioPlayer.speed) {
|
||||
// #Pangea
|
||||
// case 1.0:
|
||||
// await audioPlayer.setSpeed(1.25);
|
||||
// break;
|
||||
// case 1.25:
|
||||
// await audioPlayer.setSpeed(1.5);
|
||||
// break;
|
||||
// case 1.5:
|
||||
// await audioPlayer.setSpeed(2.0);
|
||||
// break;
|
||||
// case 2.0:
|
||||
// await audioPlayer.setSpeed(0.5);
|
||||
// break;
|
||||
// case 0.5:
|
||||
case 1.0:
|
||||
await audioPlayer.setSpeed(0.75);
|
||||
break;
|
||||
case 0.75:
|
||||
await audioPlayer.setSpeed(0.5);
|
||||
break;
|
||||
case 0.5:
|
||||
await audioPlayer.setSpeed(1.25);
|
||||
break;
|
||||
case 1.25:
|
||||
await audioPlayer.setSpeed(1.5);
|
||||
break;
|
||||
case 1.5:
|
||||
await audioPlayer.setSpeed(2.0);
|
||||
break;
|
||||
case 2.0:
|
||||
await audioPlayer.setSpeed(0.5);
|
||||
break;
|
||||
case 0.5:
|
||||
// Pangea#
|
||||
default:
|
||||
await audioPlayer.setSpeed(1.0);
|
||||
break;
|
||||
|
|
@ -522,11 +537,13 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
// #Pangea
|
||||
// thumbColor: widget.event.senderId ==
|
||||
// widget.event.room.client.userID
|
||||
// ? theme.colorScheme.onPrimary
|
||||
// : theme.colorScheme.primary,
|
||||
thumbColor: widget.senderId ==
|
||||
Matrix.of(context).client.userID
|
||||
// Pangea#
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.primary,
|
||||
? widget.color
|
||||
: theme.colorScheme.onSurface,
|
||||
// Pangea#
|
||||
activeColor: waveform == null
|
||||
? widget.color
|
||||
: Colors.transparent,
|
||||
|
|
@ -568,43 +585,68 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
),
|
||||
// Pangea#
|
||||
const SizedBox(width: 8),
|
||||
AnimatedCrossFade(
|
||||
firstChild: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Icon(
|
||||
Icons.mic_none_outlined,
|
||||
color: widget.color,
|
||||
),
|
||||
),
|
||||
secondChild: Material(
|
||||
color: widget.color.withAlpha(64),
|
||||
// #Pangea
|
||||
Material(
|
||||
color: widget.color.withAlpha(64),
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
onTap: _toggleSpeed,
|
||||
child: SizedBox(
|
||||
width: 32,
|
||||
height: 20,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${audioPlayer?.speed.toString()}x',
|
||||
style: TextStyle(
|
||||
color: widget.color,
|
||||
fontSize: 9,
|
||||
),
|
||||
onTap: _toggleSpeed,
|
||||
child: SizedBox(
|
||||
width: 32,
|
||||
height: 20,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${audioPlayer?.speed.toString() ?? 1}x',
|
||||
style: TextStyle(
|
||||
color: widget.color,
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
crossFadeState: audioPlayer == null
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
),
|
||||
// AnimatedCrossFade(
|
||||
// firstChild: Padding(
|
||||
// padding: const EdgeInsets.only(right: 8.0),
|
||||
// child: Icon(
|
||||
// Icons.mic_none_outlined,
|
||||
// color: widget.color,
|
||||
// ),
|
||||
// ),
|
||||
// secondChild: Material(
|
||||
// color: widget.color.withAlpha(64),
|
||||
// borderRadius:
|
||||
// BorderRadius.circular(AppConfig.borderRadius),
|
||||
// child: InkWell(
|
||||
// borderRadius:
|
||||
// BorderRadius.circular(AppConfig.borderRadius),
|
||||
// onTap: _toggleSpeed,
|
||||
// child: SizedBox(
|
||||
// width: 32,
|
||||
// height: 20,
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// '${audioPlayer?.speed.toString()}x',
|
||||
// style: TextStyle(
|
||||
// color: widget.color,
|
||||
// fontSize: 9,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// alignment: Alignment.center,
|
||||
// crossFadeState: audioPlayer == null
|
||||
// ? CrossFadeState.showFirst
|
||||
// : CrossFadeState.showSecond,
|
||||
// duration: FluffyThemes.animationDuration,
|
||||
// ),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -134,6 +134,10 @@ class Message extends StatelessWidget {
|
|||
? ActivityStateEvent(event: event)
|
||||
: const SizedBox();
|
||||
}
|
||||
|
||||
if (event.type == PangeaEventTypes.activityPlanEnd) {
|
||||
return const ActivityFinishedEvent();
|
||||
}
|
||||
// Pangea#
|
||||
return StateMessage(event);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,10 +222,11 @@ class MessageContent extends StatelessWidget {
|
|||
fontSize: fontSize,
|
||||
// #Pangea
|
||||
chatController: controller,
|
||||
eventId: event.eventId,
|
||||
eventId:
|
||||
"${event.eventId}${overlayController != null ? '_overlay' : ''}",
|
||||
roomId: event.room.id,
|
||||
senderId: event.senderId,
|
||||
autoplay: overlayController != null,
|
||||
autoplay: overlayController != null && isTransitionAnimation,
|
||||
// Pangea#
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,14 +40,17 @@ class RecordingDialogState extends State<RecordingDialog> {
|
|||
Future<void> startRecording() async {
|
||||
final store = Matrix.of(context).store;
|
||||
try {
|
||||
final codec = kIsWeb
|
||||
// Web seems to create webm instead of ogg when using opus encoder
|
||||
// which does not play on iOS right now. So we use wav for now:
|
||||
? AudioEncoder.wav
|
||||
// Everywhere else we use opus if supported by the platform:
|
||||
: await _audioRecorder.isEncoderSupported(AudioEncoder.opus)
|
||||
? AudioEncoder.opus
|
||||
: AudioEncoder.aacLc;
|
||||
// #Pangea
|
||||
// final codec = kIsWeb
|
||||
// // Web seems to create webm instead of ogg when using opus encoder
|
||||
// // which does not play on iOS right now. So we use wav for now:
|
||||
// ? AudioEncoder.wav
|
||||
// // Everywhere else we use opus if supported by the platform:
|
||||
// : await _audioRecorder.isEncoderSupported(AudioEncoder.opus)
|
||||
// ? AudioEncoder.opus
|
||||
// : AudioEncoder.aacLc;
|
||||
const codec = AudioEncoder.wav;
|
||||
// Pangea#
|
||||
fileName =
|
||||
'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}';
|
||||
String? path;
|
||||
|
|
|
|||
|
|
@ -923,7 +923,7 @@ class ChatListController extends State<ChatList>
|
|||
),
|
||||
),
|
||||
// #Pangea
|
||||
if (room.isRoomAdmin)
|
||||
if (room.isRoomAdmin && !room.isDirectChat)
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.delete,
|
||||
child: Row(
|
||||
|
|
|
|||
|
|
@ -57,11 +57,6 @@ class ActivityStateEventState extends State<ActivityStateEvent> {
|
|||
}
|
||||
}
|
||||
|
||||
bool get _activityIsOver {
|
||||
return activityPlan?.endAt != null &&
|
||||
DateTime.now().isAfter(activityPlan!.endAt!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (activityPlan == null) {
|
||||
|
|
@ -83,186 +78,151 @@ class ActivityStateEventState extends State<ActivityStateEvent> {
|
|||
spacing: 12.0,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(_activityIsOver ? 24.0 : 16.0),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: _activityIsOver
|
||||
? Column(
|
||||
spacing: 12.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).activityEnded,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
CachedNetworkImage(
|
||||
width: 120.0,
|
||||
imageUrl:
|
||||
"${AppConfig.assetsBaseURL}/${ActivityConstants.activityFinishedAsset}",
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (context, url, error) =>
|
||||
const SizedBox(),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
activityPlan!.markdown,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontSize: AppConfig.fontSizeFactor *
|
||||
AppConfig.messageFontSize,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
activityPlan!.markdown,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontSize:
|
||||
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: _activityIsOver
|
||||
? const SizedBox()
|
||||
: IntrinsicHeight(
|
||||
child: Row(
|
||||
spacing: 12.0,
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Container(
|
||||
height: imageWidth,
|
||||
width: imageWidth,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: activityPlan!.imageURL != null
|
||||
? activityPlan!.imageURL!.startsWith("mxc")
|
||||
? MxcImage(
|
||||
uri: Uri.parse(
|
||||
activityPlan!.imageURL!,
|
||||
),
|
||||
width: imageWidth,
|
||||
height: imageWidth,
|
||||
cacheKey: activityPlan!.bookmarkId,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: activityPlan!.imageURL!,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (
|
||||
context,
|
||||
url,
|
||||
error,
|
||||
) =>
|
||||
const SizedBox(),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 9.0,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
height: imageWidth,
|
||||
width: imageWidth,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: activityPlan!.imageURL != null
|
||||
? activityPlan!.imageURL!.startsWith("mxc")
|
||||
? MxcImage(
|
||||
uri: Uri.parse(
|
||||
activityPlan!.imageURL!,
|
||||
),
|
||||
width: imageWidth,
|
||||
height: imageWidth,
|
||||
cacheKey: activityPlan!.bookmarkId,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: activityPlan!.imageURL!,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) =>
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (
|
||||
context,
|
||||
url,
|
||||
error,
|
||||
) =>
|
||||
const SizedBox(),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 9.0,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox.expand(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(20),
|
||||
),
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
onPressed: () async {
|
||||
final Duration? duration =
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ActivityDurationPopup(
|
||||
initialValue:
|
||||
activityPlan?.duration ??
|
||||
const Duration(days: 1),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (duration == null) return;
|
||||
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.event.room
|
||||
.sendActivityPlan(
|
||||
activityPlan!.copyWith(
|
||||
endAt:
|
||||
DateTime.now().add(duration),
|
||||
duration: duration,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: CountDown(
|
||||
deadline: activityPlan!.endAt,
|
||||
iconSize: 20.0,
|
||||
textSize: 16.0,
|
||||
),
|
||||
),
|
||||
child: SizedBox.expand(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
), // Optional spacing between buttons
|
||||
Expanded(
|
||||
child: SizedBox.expand(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(20),
|
||||
),
|
||||
backgroundColor:
|
||||
theme.colorScheme.error,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: () {
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.event.room
|
||||
.sendActivityPlan(
|
||||
activityPlan!.copyWith(
|
||||
endAt: DateTime.now(),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
L10n.of(context).endNow,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
onPressed: () async {
|
||||
final Duration? duration = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ActivityDurationPopup(
|
||||
initialValue: activityPlan?.duration ??
|
||||
const Duration(days: 1),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (duration == null) return;
|
||||
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
widget.event.room.sendActivityPlan(
|
||||
activityPlan!.copyWith(
|
||||
endAt: DateTime.now().add(duration),
|
||||
duration: duration,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: CountDown(
|
||||
deadline: activityPlan!.endAt,
|
||||
iconSize: 20.0,
|
||||
textSize: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
), // Optional spacing between buttons
|
||||
Expanded(
|
||||
child: SizedBox.expand(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
backgroundColor: theme.colorScheme.error,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: () {
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () =>
|
||||
widget.event.room.sendActivityPlan(
|
||||
activityPlan!.copyWith(
|
||||
endAt: DateTime.now(),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
L10n.of(context).endNow,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -270,3 +230,50 @@ class ActivityStateEventState extends State<ActivityStateEvent> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityFinishedEvent extends StatelessWidget {
|
||||
const ActivityFinishedEvent({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 400.0,
|
||||
),
|
||||
margin: const EdgeInsets.all(18.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).activityEnded,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
CachedNetworkImage(
|
||||
width: 120.0,
|
||||
imageUrl:
|
||||
"${AppConfig.assetsBaseURL}/${ActivityConstants.activityFinishedAsset}",
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (context, url, error) => const SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,17 @@ class ActivityPlannerPageAppBar extends StatelessWidget
|
|||
final theme = Theme.of(context);
|
||||
|
||||
return AppBar(
|
||||
leadingWidth: FluffyThemes.isColumnMode(context) ? 150.0 : null,
|
||||
leading: FluffyThemes.isColumnMode(context)
|
||||
? Row(
|
||||
children: [
|
||||
const SizedBox(width: 8.0),
|
||||
BackButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
title: pageMode == PageMode.savedActivities
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
|
|||
|
|
@ -145,6 +145,8 @@ class LevelUpBannerState extends State<LevelUpBanner>
|
|||
LevelUpManager.instance.markPopupSeen();
|
||||
_showedDetails = true;
|
||||
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => const LevelUpPopup(),
|
||||
|
|
|
|||
|
|
@ -475,7 +475,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
|
|||
context.go("/rooms?spaceId=clear");
|
||||
}
|
||||
},
|
||||
visible: room.isRoomAdmin,
|
||||
visible: room.isRoomAdmin && !room.isDirectChat,
|
||||
showInMainView: false,
|
||||
),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ class DeleteSpaceDialogState extends State<DeleteSpaceDialog> {
|
|||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 400,
|
||||
maxHeight: 600,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -138,75 +139,78 @@ class DeleteSpaceDialogState extends State<DeleteSpaceDialog> {
|
|||
vertical: 8.0,
|
||||
),
|
||||
child: Text(
|
||||
L10n.of(context).deleteSpaceDesc,
|
||||
widget.space.spaceChildCount > 0
|
||||
? L10n.of(context).deleteSpaceDesc
|
||||
: L10n.of(context).deleteEmptySpaceDesc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 300,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (_loadingRooms) {
|
||||
return const Center(
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (_loadingRooms) {
|
||||
return const Center(
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_roomLoadError != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
Text(L10n.of(context).oopsSomethingWentWrong),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _rooms.length,
|
||||
itemBuilder: (context, index) {
|
||||
final chunk = _rooms[index];
|
||||
|
||||
final room =
|
||||
widget.space.client.getRoomById(chunk.roomId);
|
||||
final isMember = room != null &&
|
||||
room.membership == Membership.join &&
|
||||
room.isRoomAdmin;
|
||||
|
||||
final displayname = chunk.name ??
|
||||
chunk.canonicalAlias ??
|
||||
L10n.of(context).emptyChat;
|
||||
|
||||
return AnimatedOpacity(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
opacity: isMember ? 1 : 0.5,
|
||||
child: CheckboxListTile(
|
||||
value: _roomsToDelete.contains(chunk),
|
||||
onChanged: isMember
|
||||
? (value) => _onRoomSelected(value, chunk)
|
||||
: null,
|
||||
title: Text(displayname),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_roomLoadError != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
Text(L10n.of(context).oopsSomethingWentWrong),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _rooms.length,
|
||||
itemBuilder: (context, index) {
|
||||
final chunk = _rooms[index];
|
||||
|
||||
final room =
|
||||
widget.space.client.getRoomById(chunk.roomId);
|
||||
final isMember = room != null &&
|
||||
room.membership == Membership.join &&
|
||||
room.isRoomAdmin;
|
||||
|
||||
final displayname = chunk.name ??
|
||||
chunk.canonicalAlias ??
|
||||
L10n.of(context).emptyChat;
|
||||
|
||||
return AnimatedOpacity(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
opacity: isMember ? 1 : 0.5,
|
||||
child: CheckboxListTile(
|
||||
value: _roomsToDelete.contains(chunk),
|
||||
onChanged: isMember
|
||||
? (value) => _onRoomSelected(value, chunk)
|
||||
: null,
|
||||
title: Text(displayname),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/it_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/word_data_card.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
|
||||
|
|
@ -17,7 +18,6 @@ import '../../common/utils/overlay.dart';
|
|||
import '../controllers/it_feedback_controller.dart';
|
||||
import '../models/it_response_model.dart';
|
||||
import 'choice_array.dart';
|
||||
import 'igc/word_data_card.dart';
|
||||
|
||||
class ITBar extends StatefulWidget {
|
||||
final Choreographer choreographer;
|
||||
|
|
@ -333,7 +333,13 @@ class ITChoices extends StatelessWidget {
|
|||
fullTextLang: sourceText != null
|
||||
? controller.sourceLangCode
|
||||
: controller.targetLangCode,
|
||||
hasInfo: controller.currentITStep!.continuances[index].hasInfo,
|
||||
// IMPORTANT COMMENT TO KEEP: We're going to forace hasInfo to false for now
|
||||
// because we don't want to show the word data card for correct choices and the contextual definition
|
||||
// for incorrect choices. This gives away the answer (if you're Kel at least).
|
||||
// The reason hasInfo is false for incorrect choices is that we're not includng the tokens for distractors.
|
||||
// Correct choices will have the tokens, but we don't want to show something different for them.
|
||||
// hasInfo: controller.currentITStep!.continuances[index].hasInfo,
|
||||
hasInfo: false,
|
||||
choiceFeedback: choiceFeedback,
|
||||
room: controller.choreographer.chatController.room,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ class TransparentBackdropState extends State<TransparentBackdrop>
|
|||
curve: FluffyThemes.animationCurve,
|
||||
),
|
||||
);
|
||||
_blurTween = Tween<double>(begin: 0.0, end: 2.5).animate(
|
||||
_blurTween = Tween<double>(begin: 0.0, end: 3.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class PangeaEventTypes {
|
|||
static const capacity = "pangea.capacity";
|
||||
|
||||
static const activityPlan = "pangea.activity_plan";
|
||||
static const activityPlanEnd = "pangea.activity.end";
|
||||
|
||||
static const userAge = "pangea.user_age";
|
||||
|
||||
|
|
|
|||
|
|
@ -59,24 +59,22 @@ extension PangeaEvent on Event {
|
|||
content.tryGetMap<String, dynamic>(ModelKey.transcription);
|
||||
final audioContent =
|
||||
content.tryGetMap<String, dynamic>('org.matrix.msc1767.audio');
|
||||
if (transcription == null || audioContent == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "Called getPangeaAudioFile on an audio message without transcription or audio content",
|
||||
data: {},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final matrixFile = await downloadAndDecryptAttachment();
|
||||
final duration = audioContent.tryGet<int>('duration');
|
||||
final waveform = audioContent.tryGetList<int>('waveform');
|
||||
|
||||
final duration = audioContent?.tryGet<int>('duration') ??
|
||||
content.tryGetMap<String, dynamic>('info')?.tryGet<int>('duration');
|
||||
|
||||
final waveform = audioContent?.tryGetList<int>('waveform') ??
|
||||
content
|
||||
.tryGetMap<String, dynamic>('org.matrix.msc1767.audio')
|
||||
?.tryGetList<int>('waveform');
|
||||
|
||||
// old audio messages will not have tokens
|
||||
final tokensContent = transcription.tryGetList(ModelKey.tokens);
|
||||
if (tokensContent == null) return null;
|
||||
final tokensContent = transcription?.tryGetList(ModelKey.tokens);
|
||||
|
||||
final tokens = tokensContent
|
||||
.map((e) => TTSToken.fromJson(e as Map<String, dynamic>))
|
||||
?.map((e) => TTSToken.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
return PangeaAudioFile(
|
||||
|
|
|
|||
|
|
@ -15,10 +15,13 @@ import 'package:fluffychat/widgets/matrix.dart';
|
|||
class PhoneticTranscriptionWidget extends StatefulWidget {
|
||||
final String text;
|
||||
final LanguageModel textLanguage;
|
||||
|
||||
final TextStyle? style;
|
||||
final double? iconSize;
|
||||
final Color? iconColor;
|
||||
|
||||
final bool enabled;
|
||||
|
||||
const PhoneticTranscriptionWidget({
|
||||
super.key,
|
||||
required this.text,
|
||||
|
|
@ -26,6 +29,7 @@ class PhoneticTranscriptionWidget extends StatefulWidget {
|
|||
this.style,
|
||||
this.iconSize,
|
||||
this.iconColor,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -114,69 +118,74 @@ class _PhoneticTranscriptionWidgetState
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HoverBuilder(
|
||||
builder: (context, hovering) {
|
||||
return GestureDetector(
|
||||
onTap: () => _handleAudioTap(context),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
decoration: BoxDecoration(
|
||||
color: hovering
|
||||
? Colors.grey.withAlpha((0.2 * 255).round())
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_error != null)
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
return IgnorePointer(
|
||||
ignoring: !widget.enabled,
|
||||
child: HoverBuilder(
|
||||
builder: (context, hovering) {
|
||||
return GestureDetector(
|
||||
onTap: () => _handleAudioTap(context),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
decoration: BoxDecoration(
|
||||
color: hovering
|
||||
? Colors.grey.withAlpha((0.2 * 255).round())
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_error != null)
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: widget.iconSize ?? 24,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).failedToFetchTranscription,
|
||||
style: widget.style,
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (_isLoading || _transcription == null)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: Text(
|
||||
"/$_transcription/",
|
||||
style: widget.style ??
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (_transcription != null &&
|
||||
_error == null &&
|
||||
widget.enabled)
|
||||
Tooltip(
|
||||
message: _isPlaying
|
||||
? L10n.of(context).stop
|
||||
: L10n.of(context).playAudio,
|
||||
child: Icon(
|
||||
_isPlaying ? Icons.pause_outlined : Icons.volume_up,
|
||||
size: widget.iconSize ?? 24,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
color: widget.iconColor ??
|
||||
Theme.of(context).iconTheme.color,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).failedToFetchTranscription,
|
||||
style: widget.style,
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (_isLoading || _transcription == null)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: Text(
|
||||
"/$_transcription/",
|
||||
style: widget.style ??
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (_transcription != null && _error == null)
|
||||
Tooltip(
|
||||
message: _isPlaying
|
||||
? L10n.of(context).stop
|
||||
: L10n.of(context).playAudio,
|
||||
child: Icon(
|
||||
_isPlaying ? Icons.pause_outlined : Icons.volume_up,
|
||||
size: widget.iconSize ?? 24,
|
||||
color:
|
||||
widget.iconColor ?? Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
|
||||
class PangeaAudioFile extends MatrixAudioFile {
|
||||
List<int>? waveform;
|
||||
List<TTSToken> tokens;
|
||||
List<TTSToken>? tokens;
|
||||
|
||||
PangeaAudioFile({
|
||||
required super.bytes,
|
||||
|
|
|
|||
|
|
@ -550,7 +550,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
return 0.8;
|
||||
case ReadingAssistanceMode.selectMode:
|
||||
case null:
|
||||
return 0.4;
|
||||
return 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:fluffychat/l10n/l10n.dart';
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/message_content.dart';
|
||||
import 'package:fluffychat/pages/chat/events/reply_content.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
|
|
@ -205,6 +206,8 @@ class OverlayMessage extends StatelessWidget {
|
|||
textColor,
|
||||
),
|
||||
iconColor: textColor,
|
||||
enabled:
|
||||
event.senderId != BotName.byEnvironment,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -93,34 +93,13 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
|
||||
Completer<String>? _transcriptionCompleter;
|
||||
|
||||
AudioPlayer? get _audioPlayer => Matrix.of(context).audioPlayer!;
|
||||
MatrixState? matrix;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final matrix = Matrix.of(context);
|
||||
matrix.audioPlayer?.dispose();
|
||||
matrix.audioPlayer = AudioPlayer();
|
||||
matrix.voiceMessageEventId.value =
|
||||
widget.overlayController.pangeaMessageEvent?.eventId;
|
||||
|
||||
_onPlayerStateChanged = _audioPlayer?.playerStateStream.listen((state) {
|
||||
if (state.processingState == ProcessingState.completed) {
|
||||
_updateMode(null);
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_onAudioPositionChanged ??= _audioPlayer?.positionStream.listen((state) {
|
||||
if (_audioBytes != null) {
|
||||
widget.overlayController.highlightCurrentText(
|
||||
state.inMilliseconds,
|
||||
_audioBytes!.tokens,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
matrix = Matrix.of(context);
|
||||
if (messageEvent?.isAudioMessage == true) {
|
||||
_fetchTranscription();
|
||||
}
|
||||
|
|
@ -128,9 +107,9 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_audioPlayer?.dispose();
|
||||
Matrix.of(context).audioPlayer = null;
|
||||
Matrix.of(context).voiceMessageEventId.value = null;
|
||||
matrix?.audioPlayer?.dispose();
|
||||
matrix?.audioPlayer = null;
|
||||
matrix?.voiceMessageEventId.value = null;
|
||||
|
||||
_onPlayerStateChanged?.cancel();
|
||||
_onAudioPositionChanged?.cancel();
|
||||
|
|
@ -162,8 +141,8 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
|
||||
if (mode == null) {
|
||||
setState(() {
|
||||
_audioPlayer?.stop();
|
||||
_audioPlayer?.seek(null);
|
||||
matrix?.audioPlayer?.stop();
|
||||
matrix?.audioPlayer?.seek(null);
|
||||
_selectedMode = null;
|
||||
});
|
||||
return;
|
||||
|
|
@ -178,8 +157,8 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
_playAudio();
|
||||
return;
|
||||
} else {
|
||||
_audioPlayer?.stop();
|
||||
_audioPlayer?.seek(null);
|
||||
matrix?.audioPlayer?.stop();
|
||||
matrix?.audioPlayer?.seek(null);
|
||||
}
|
||||
|
||||
if (_selectedMode == SelectMode.practice) {
|
||||
|
|
@ -243,13 +222,52 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
}
|
||||
|
||||
Future<void> _playAudio() async {
|
||||
try {
|
||||
if (_audioPlayer != null && _audioPlayer!.playerState.playing) {
|
||||
await _audioPlayer?.pause();
|
||||
final playerID =
|
||||
"${widget.overlayController.pangeaMessageEvent?.eventId}_button";
|
||||
|
||||
if (matrix?.audioPlayer != null &&
|
||||
matrix?.voiceMessageEventId.value == playerID) {
|
||||
// If the audio player is already initialized and playing the same message, pause it
|
||||
if (matrix!.audioPlayer!.playerState.playing) {
|
||||
await matrix?.audioPlayer?.pause();
|
||||
return;
|
||||
} else if (_audioPlayer?.position != Duration.zero) {
|
||||
}
|
||||
// If the audio player is paused, resume it
|
||||
await matrix?.audioPlayer?.play();
|
||||
return;
|
||||
}
|
||||
|
||||
matrix?.audioPlayer?.dispose();
|
||||
matrix?.audioPlayer = AudioPlayer();
|
||||
matrix?.voiceMessageEventId.value =
|
||||
widget.overlayController.pangeaMessageEvent?.eventId;
|
||||
|
||||
_onPlayerStateChanged =
|
||||
matrix?.audioPlayer?.playerStateStream.listen((state) {
|
||||
if (state.processingState == ProcessingState.completed) {
|
||||
_updateMode(null);
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_onAudioPositionChanged ??=
|
||||
matrix?.audioPlayer?.positionStream.listen((state) {
|
||||
if (_audioBytes?.tokens != null) {
|
||||
widget.overlayController.highlightCurrentText(
|
||||
state.inMilliseconds,
|
||||
_audioBytes!.tokens!,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
if (matrix?.audioPlayer != null &&
|
||||
matrix!.audioPlayer!.playerState.playing) {
|
||||
await matrix?.audioPlayer?.pause();
|
||||
return;
|
||||
} else if (matrix?.audioPlayer?.position != Duration.zero) {
|
||||
TtsController.stop();
|
||||
await _audioPlayer?.play();
|
||||
await matrix?.audioPlayer?.play();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -260,9 +278,9 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
if (_audioBytes == null) return;
|
||||
|
||||
if (_audioFile != null) {
|
||||
await _audioPlayer?.setFilePath(_audioFile!.path);
|
||||
await matrix?.audioPlayer?.setFilePath(_audioFile!.path);
|
||||
} else {
|
||||
await _audioPlayer?.setAudioSource(
|
||||
await matrix?.audioPlayer?.setAudioSource(
|
||||
BytesAudioSource(
|
||||
_audioBytes!.bytes,
|
||||
_audioBytes!.mimeType,
|
||||
|
|
@ -271,7 +289,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
}
|
||||
|
||||
TtsController.stop();
|
||||
_audioPlayer?.play();
|
||||
matrix?.audioPlayer?.play();
|
||||
} catch (e, s) {
|
||||
setState(() => _audioError = e.toString());
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -441,7 +459,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
|
|||
|
||||
if (mode == SelectMode.audio) {
|
||||
return Icon(
|
||||
_audioPlayer?.playerState.playing == true
|
||||
matrix?.audioPlayer?.playerState.playing == true
|
||||
? Icons.pause_outlined
|
||||
: Icons.volume_up,
|
||||
size: 20,
|
||||
|
|
|
|||
|
|
@ -5,33 +5,9 @@ import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
|||
import '../../config/app_config.dart';
|
||||
|
||||
extension VisibleInGuiExtension on List<Event> {
|
||||
List<Event> filterByVisibleInGui({String? exceptionEventId}) {
|
||||
final visibleEvents =
|
||||
where((e) => e.isVisibleInGui || e.eventId == exceptionEventId)
|
||||
.toList();
|
||||
|
||||
// Hide creation state events:
|
||||
if (visibleEvents.isNotEmpty &&
|
||||
visibleEvents.last.type == EventTypes.RoomCreate) {
|
||||
var i = visibleEvents.length - 2;
|
||||
while (i > 0) {
|
||||
final event = visibleEvents[i];
|
||||
if (!event.isState) break;
|
||||
if (event.type == EventTypes.Encryption) {
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
if (event.type == EventTypes.RoomMember &&
|
||||
event.roomMemberChangeType == RoomMemberChangeType.acceptInvite) {
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
visibleEvents.removeAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
return visibleEvents;
|
||||
}
|
||||
List<Event> filterByVisibleInGui({String? exceptionEventId}) => where(
|
||||
(event) => event.isVisibleInGui || event.eventId == exceptionEventId,
|
||||
).toList();
|
||||
}
|
||||
|
||||
extension IsStateExtension on Event {
|
||||
|
|
@ -89,6 +65,7 @@ extension IsStateExtension on Event {
|
|||
EventTypes.RoomTombstone,
|
||||
EventTypes.CallInvite,
|
||||
PangeaEventTypes.activityPlan,
|
||||
PangeaEventTypes.activityPlanEnd,
|
||||
};
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue