Merge branch 'main' into fluffychat-merge

This commit is contained in:
ggurdin 2025-06-27 14:06:56 -04:00
commit 8f9811f014
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
8 changed files with 304 additions and 217 deletions

View file

@ -209,28 +209,37 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
// #Pangea
// if (!kIsWeb) {
if (!kIsWeb && matrixFile != null) {
// Pangea#
final tempDir = await getTemporaryDirectory();
final fileName = Uri.encodeComponent(
// #Pangea
// widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
widget.event!.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
if (!kIsWeb) {
if (matrixFile != null) {
// Pangea#
);
file = File('${tempDir.path}/${fileName}_${matrixFile.name}');
final tempDir = await getTemporaryDirectory();
final fileName = Uri.encodeComponent(
// #Pangea
// widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
widget.event!.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
// Pangea#
);
file = File('${tempDir.path}/${fileName}_${matrixFile.name}');
await file.writeAsBytes(matrixFile.bytes);
await file.writeAsBytes(matrixFile.bytes);
if (Platform.isIOS &&
matrixFile.mimeType.toLowerCase() == 'audio/ogg') {
Logs().v('Convert ogg audio file for iOS...');
final convertedFile = File('${file.path}.caf');
if (await convertedFile.exists() == false) {
OpusCaf().convertOpusToCaf(file.path, convertedFile.path);
if (Platform.isIOS &&
matrixFile.mimeType.toLowerCase() == 'audio/ogg') {
Logs().v('Convert ogg audio file for iOS...');
final convertedFile = File('${file.path}.caf');
if (await convertedFile.exists() == false) {
OpusCaf().convertOpusToCaf(file.path, convertedFile.path);
}
file = convertedFile;
}
file = convertedFile;
// #Pangea
} else if (widget.matrixFile != null) {
final tempDir = await getTemporaryDirectory();
file = File('${tempDir.path}/${widget.matrixFile!.name}');
await file.writeAsBytes(widget.matrixFile!.bytes);
}
// Pangea#
}
setState(() {
@ -314,22 +323,19 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
final audioPlayer = matrix.audioPlayer;
// #Pangea
// if (audioPlayer == null) return;
if (audioPlayer == null ||
matrix.voiceMessageEventId.value != widget.eventId) {
switch (playbackSpeed) {
case 1.0:
setState(() => playbackSpeed = 0.75);
case 0.75:
setState(() => playbackSpeed = 0.5);
case 0.5:
setState(() => playbackSpeed = 1.25);
case 1.25:
setState(() => playbackSpeed = 1.5);
default:
setState(() => playbackSpeed = 1.0);
}
return;
switch (playbackSpeed) {
case 1.0:
setState(() => playbackSpeed = 0.75);
case 0.75:
setState(() => playbackSpeed = 0.5);
case 0.5:
setState(() => playbackSpeed = 1.25);
case 1.25:
setState(() => playbackSpeed = 1.5);
default:
setState(() => playbackSpeed = 1.0);
}
if (audioPlayer == null) return;
// Pangea#
switch (audioPlayer.speed) {
// #Pangea

View file

@ -221,7 +221,7 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
),
AnimatedSize(
duration: FluffyThemes.animationDuration,
child: _timeout
child: (_timeout || !_loading && cards.isEmpty)
? Padding(
padding: const EdgeInsets.all(8.0),
child: RichText(
@ -236,8 +236,10 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
),
const TextSpan(text: " "),
TextSpan(
text:
L10n.of(context).activitySuggestionTimeoutMessage,
text: _timeout
? L10n.of(context)
.activitySuggestionTimeoutMessage
: L10n.of(context).oopsSomethingWentWrong,
),
],
),

View file

@ -477,8 +477,8 @@ class GetAnalyticsController extends BaseController {
// generate level up analytics as a construct summary
ConstructSummary summary;
try {
final int maxXP = constructListModel.calculateXpWithLevel(upperLevel);
final int minXP = constructListModel.calculateXpWithLevel(lowerLevel);
final int minXP = constructListModel.calculateXpWithLevel(upperLevel);
final int maxXP = constructListModel.calculateXpWithLevel(lowerLevel);
int diffXP = maxXP - minXP;
if (diffXP < 0) diffXP = 0;
@ -492,23 +492,41 @@ class GetAnalyticsController extends BaseController {
}
// extract construct use message bodies for analytics
List<String?>? constructUseMessageContentBodies = [];
final Map<String, Set<String>> useEventIds = {};
for (final use in constructUseOfCurrentLevel) {
try {
final useMessage = await use.getEvent(_client);
final useMessageBody = useMessage?.content["body"];
if (useMessageBody is String) {
constructUseMessageContentBodies.add(useMessageBody);
} else {
constructUseMessageContentBodies.add(null);
}
} catch (e) {
constructUseMessageContentBodies.add(null);
}
if (use.metadata.roomId == null) continue;
if (use.metadata.eventId == null) continue;
useEventIds[use.metadata.roomId!] ??= {};
useEventIds[use.metadata.roomId!]!.add(use.metadata.eventId!);
}
if (constructUseMessageContentBodies.length !=
constructUseOfCurrentLevel.length) {
constructUseMessageContentBodies = null;
final List<String?> constructUseMessageContentBodies = [];
for (final entry in useEventIds.entries) {
final String roomId = entry.key;
final room = _client.getRoomById(roomId);
if (room == null) continue;
final List<String?> messageBodies = [];
for (final eventId in entry.value) {
try {
final Event? event = await room.getEventById(eventId);
if (event?.content["body"] is! String) continue;
final String body = event?.content["body"] as String;
if (body.isEmpty) continue;
messageBodies.add(body);
} catch (e, s) {
debugPrint("Error getting event by ID: $e");
ErrorHandler.logError(
e: e,
s: s,
data: {
'roomId': roomId,
'eventId': eventId,
},
);
continue;
}
}
constructUseMessageContentBodies.addAll(messageBodies);
}
final request = ConstructSummaryRequest(

View file

@ -91,6 +91,7 @@ class LevelUpBannerState extends State<LevelUpBanner>
ConstructSummary? _constructSummary;
String? _error;
bool _loading = true;
@override
void initState() {
@ -143,6 +144,7 @@ class LevelUpBannerState extends State<LevelUpBanner>
Future<void> _setConstructSummary() async {
try {
setState(() => _loading = true);
_constructSummary = await MatrixState.pangeaController.getAnalytics
.generateLevelUpAnalytics(
widget.level,
@ -150,6 +152,10 @@ class LevelUpBannerState extends State<LevelUpBanner>
);
} catch (e) {
_error = e.toString();
} finally {
if (mounted) {
setState(() => _loading = false);
}
}
}
@ -364,144 +370,178 @@ class LevelUpBannerState extends State<LevelUpBanner>
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 24.0,
children: [
Table(
columnWidths: const {
0: IntrinsicColumnWidth(),
1: FlexColumnWidth(),
2: IntrinsicColumnWidth(),
},
defaultVerticalAlignment:
TableCellVerticalAlignment.middle,
children: [
...LearningSkillsEnum.values
.where(
(v) =>
v.isVisible && _skillsPoints(v) > -1,
child: _loading
? const Center(
child: CircularProgressIndicator.adaptive(),
)
: _error != null
? Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
Icons.error,
color: Theme.of(context)
.colorScheme
.error,
),
const SizedBox(width: 8.0),
Text(
L10n.of(context)
.oopsSomethingWentWrong,
),
],
)
.map((skill) {
return TableRow(
: SingleChildScrollView(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.center,
spacing: 24.0,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 9.0,
horizontal: 18.0,
),
child: Icon(
skill.icon,
size: 25,
color: Colors.white,
),
Table(
columnWidths: const {
0: IntrinsicColumnWidth(),
1: FlexColumnWidth(),
2: IntrinsicColumnWidth(),
},
defaultVerticalAlignment:
TableCellVerticalAlignment
.middle,
children: [
...LearningSkillsEnum.values
.where(
(v) =>
v.isVisible &&
_skillsPoints(v) > -1,
)
.map((skill) {
return TableRow(
children: [
Padding(
padding: const EdgeInsets
.symmetric(
vertical: 9.0,
horizontal: 18.0,
),
child: Icon(
skill.icon,
size: 25,
color: Colors.white,
),
),
Padding(
padding: const EdgeInsets
.symmetric(
vertical: 9.0,
horizontal: 18.0,
),
child: Text(
skill.tooltip(context),
style: const TextStyle(
fontSize: 16,
fontWeight:
FontWeight.w600,
color: Colors.white,
),
textAlign:
TextAlign.center,
),
),
Padding(
padding: const EdgeInsets
.symmetric(
vertical: 9.0,
horizontal: 18.0,
),
child: Text(
"+ ${_skillsPoints(skill)} XP",
style: const TextStyle(
fontSize: 16,
fontWeight:
FontWeight.w600,
color: Colors.white,
),
textAlign:
TextAlign.center,
),
),
],
);
}),
],
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 9.0,
horizontal: 18.0,
),
child: Text(
skill.tooltip(context),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}",
width: 400,
fit: BoxFit.cover,
),
if (_constructSummary?.textSummary !=
null)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.secondaryContainer,
borderRadius:
BorderRadius.circular(8),
),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 9.0,
horizontal: 18.0,
),
child: Text(
"+ ${_skillsPoints(skill)} XP",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
child: Text(
_constructSummary!.textSummary,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
textAlign: TextAlign.center,
),
textAlign: TextAlign.center,
),
const SizedBox(
height: 24,
),
// Share button, currently no functionality
// ElevatedButton(
// onPressed: () {
// // Add share functionality
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: Colors.white,
// foregroundColor: Colors.black,
// padding: const EdgeInsets.symmetric(
// vertical: 12,
// horizontal: 24,
// ),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(8),
// ),
// ),
// child: const Row(
// mainAxisSize: MainAxisSize
// .min,
// children: [
// Text(
// "Share with Friends",
// style: TextStyle(
// fontSize: 16,
// fontWeight: FontWeight.bold,
// ),
// ),
// SizedBox(
// width: 8,
// ),
// Icon(
// Icons.ios_share,
// size: 20,
// ),
// ),
// ),
// ),
],
);
}),
],
),
CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}",
width: 400,
fit: BoxFit.cover,
),
if (_constructSummary?.textSummary != null)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.secondaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(
_constructSummary!.textSummary,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
textAlign: TextAlign.center,
),
),
const SizedBox(
height: 24,
),
// Share button, currently no functionality
// ElevatedButton(
// onPressed: () {
// // Add share functionality
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: Colors.white,
// foregroundColor: Colors.black,
// padding: const EdgeInsets.symmetric(
// vertical: 12,
// horizontal: 24,
// ),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(8),
// ),
// ),
// child: const Row(
// mainAxisSize: MainAxisSize
// .min,
// children: [
// Text(
// "Share with Friends",
// style: TextStyle(
// fontSize: 16,
// fontWeight: FontWeight.bold,
// ),
// ),
// SizedBox(
// width: 8,
// ),
// Icon(
// Icons.ios_share,
// size: 20,
// ),
// ),
// ),
// ),
],
),
),
),
),
],

View file

@ -31,8 +31,9 @@ Future<void> showInviteDialog(Room room, BuildContext context) async {
room.isSpace ? "/rooms?spaceId=${room.id}" : "/rooms/${room.id}",
);
return room.id;
} else if (acceptInvite == OkCancelResult.cancel) {
await room.leave();
}
await room.leave();
},
);

View file

@ -125,9 +125,12 @@ class SettingsLearningController extends State<SettingsLearning> {
if (formKey.currentState!.validate()) {
await showFutureLoadingDialog(
context: context,
future: () async => pangeaController.userController.updateProfile(
(_) => _profile,
),
future: () async => pangeaController.userController
.updateProfile(
(_) => _profile,
waitForDataInSync: true,
)
.timeout(const Duration(seconds: 15)),
);
Navigator.of(context).pop();
}

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@ -20,6 +22,7 @@ import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart
import 'package:fluffychat/pangea/toolbar/widgets/stt_transcript_tokens.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/file_description.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
// @ggurdin be great to explain the need/function of a widget like this
@ -150,8 +153,13 @@ class OverlayMessage extends StatelessWidget {
final transcription = showTranscription
? Container(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
constraints: BoxConstraints(
maxWidth: min(
FluffyThemes.columnWidth * 1.5,
MediaQuery.of(context).size.width -
(ownMessage ? 0 : Avatar.defaultSize) -
24.0,
),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
@ -198,8 +206,8 @@ class OverlayMessage extends StatelessWidget {
text: overlayController
.transcription!.transcript.text,
textLanguage: PLanguageStore.byLangCode(
pangeaMessageEvent!
.messageDisplayLangCode,
overlayController
.transcription!.langCode,
) ??
LanguageModel.unknown,
style: AppConfig.messageTextStyle(
@ -230,8 +238,13 @@ class OverlayMessage extends StatelessWidget {
final translation = showTranslation || showSpeechTranslation
? Container(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
constraints: BoxConstraints(
maxWidth: min(
FluffyThemes.columnWidth * 1.5,
MediaQuery.of(context).size.width -
(ownMessage ? 0 : Avatar.defaultSize) -
24.0,
),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart';
@ -126,7 +125,8 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
void _clear() {
setState(() {
_audioError = null;
// Audio errors do not go away when I switch modes and back
// Is there any reason to wipe error records on clear?
_translationError = null;
_speechTranslationError = null;
});
@ -149,8 +149,10 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
}
setState(
() => _selectedMode =
_selectedMode == mode && mode != SelectMode.audio ? null : mode,
() => _selectedMode = _selectedMode == mode &&
(mode != SelectMode.audio || _audioError != null)
? null
: mode,
);
if (_selectedMode == SelectMode.audio) {
@ -202,12 +204,10 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
File? file;
file = File('${tempDir.path}/${_audioBytes!.name}');
await file.writeAsBytes(_audioBytes!.bytes);
setState(() => _audioFile = file);
_audioFile = file;
}
if (mounted) setState(() => _isLoadingAudio = false);
} catch (e, s) {
debugger(when: kDebugMode);
_audioError = e.toString();
ErrorHandler.logError(
e: e,
s: s,
@ -217,6 +217,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
messageEvent?.messageDisplayLangCode,
},
);
} finally {
if (mounted) setState(() => _isLoadingAudio = false);
}
}
@ -289,7 +290,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
}
TtsController.stop();
matrix?.audioPlayer?.play();
await matrix?.audioPlayer?.play();
} catch (e, s) {
setState(() => _audioError = e.toString());
ErrorHandler.logError(
@ -487,25 +488,28 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
spacing: 4.0,
children: [
for (final mode in modes)
Tooltip(
message: mode.tooltip(context),
child: PressableButton(
depressed: mode == _selectedMode,
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.primaryContainer,
onPressed: () => _updateMode(mode),
playSound: true,
colorFactor: Theme.of(context).brightness == Brightness.light
? 0.55
: 0.3,
child: Container(
height: buttonSize,
width: buttonSize,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
shape: BoxShape.circle,
TooltipVisibility(
visible: (!_isError || mode != _selectedMode),
child: Tooltip(
message: mode.tooltip(context),
child: PressableButton(
depressed: mode == _selectedMode,
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.primaryContainer,
onPressed: () => _updateMode(mode),
playSound: mode != SelectMode.audio,
colorFactor: Theme.of(context).brightness == Brightness.light
? 0.55
: 0.3,
child: Container(
height: buttonSize,
width: buttonSize,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
shape: BoxShape.circle,
),
child: icon(mode),
),
child: icon(mode),
),
),
),