resolve merge conflict
This commit is contained in:
commit
88f7ea400b
26 changed files with 733 additions and 100 deletions
|
|
@ -653,6 +653,9 @@ abstract class AppRoutes {
|
|||
state,
|
||||
ChatMembersPage(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
// #Pangea
|
||||
filter: state.uri.queryParameters['filter'],
|
||||
// Pangea#
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
|
|||
|
|
@ -5020,5 +5020,16 @@
|
|||
"endNow": "End now",
|
||||
"setDuration": "Set duration",
|
||||
"activityEnded": "That’s a wrap for this activity! Big thanks to everyone for chatting, learning, and making this space so lively. Language grows with conversation, and every word exchanged brings us closer to confidence and fluency.\n\nKeep practicing, stay curious, and don’t be shy to keep the conversation going!",
|
||||
"duration": "Duration"
|
||||
"duration": "Duration",
|
||||
"transcriptionFailed": "Failed to transcribe audio",
|
||||
"aUserIsKnocking": "1 user is requesting to join your space",
|
||||
"usersAreKnocking": "{users} users are requesting to join your space",
|
||||
"@usersAreKnocking": {
|
||||
"type": "int",
|
||||
"placeholders": {
|
||||
"users": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -407,7 +407,10 @@ class HtmlMessage extends StatelessWidget {
|
|||
avatar: user.avatarUrl,
|
||||
uri: href,
|
||||
outerContext: context,
|
||||
fontSize: fontSize,
|
||||
// #Pangea
|
||||
// fontSize: fontSize,
|
||||
fontSize: renderer.fontSize(context),
|
||||
// Pangea#
|
||||
color: linkStyle.color,
|
||||
// #Pangea
|
||||
userId: user.id,
|
||||
|
|
@ -428,7 +431,10 @@ class HtmlMessage extends StatelessWidget {
|
|||
avatar: room?.avatar,
|
||||
uri: href,
|
||||
outerContext: context,
|
||||
fontSize: fontSize,
|
||||
// #Pangea
|
||||
// fontSize: fontSize,
|
||||
fontSize: renderer.fontSize(context),
|
||||
// Pangea#
|
||||
color: linkStyle.color,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -158,7 +158,9 @@ class _Reaction extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
// #Pangea
|
||||
// color: color,
|
||||
// Pangea#
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
|
|
|
|||
|
|
@ -89,39 +89,45 @@ class NaviRailItem extends StatelessWidget {
|
|||
// color: isSelected
|
||||
// ? theme.colorScheme.primaryContainer
|
||||
// : theme.colorScheme.surfaceContainerHigh,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ??
|
||||
(isSelected
|
||||
? theme.colorScheme.primaryContainer
|
||||
: theme.colorScheme.surfaceContainerHigh),
|
||||
borderRadius: borderRadius,
|
||||
child: UnreadRoomsBadge(
|
||||
filter: unreadBadgeFilter ?? (_) => false,
|
||||
badgePosition: BadgePosition.topEnd(
|
||||
top: -4,
|
||||
end: isColumnMode ? 8 : 4,
|
||||
),
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: isColumnMode ? 16.0 : 12.0,
|
||||
vertical: isColumnMode ? 8.0 : 6.0,
|
||||
),
|
||||
// Pangea#
|
||||
child: Tooltip(
|
||||
message: toolTip,
|
||||
child: InkWell(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ??
|
||||
(isSelected
|
||||
? theme.colorScheme.primaryContainer
|
||||
: theme.colorScheme.surfaceContainerHigh),
|
||||
borderRadius: borderRadius,
|
||||
onTap: onTap,
|
||||
child: unreadBadgeFilter == null
|
||||
? icon
|
||||
: UnreadRoomsBadge(
|
||||
filter: unreadBadgeFilter,
|
||||
badgePosition: BadgePosition.topEnd(
|
||||
// #Pangea
|
||||
// top: -12,
|
||||
// end: -8,
|
||||
top: -20,
|
||||
end: -16,
|
||||
// Pangea#
|
||||
),
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: isColumnMode ? 16.0 : 12.0,
|
||||
vertical: isColumnMode ? 8.0 : 6.0,
|
||||
),
|
||||
// Pangea#
|
||||
child: Tooltip(
|
||||
message: toolTip,
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
onTap: onTap,
|
||||
// #Pangea
|
||||
child: icon,
|
||||
// child: unreadBadgeFilter == null
|
||||
// ? icon
|
||||
// : UnreadRoomsBadge(
|
||||
// filter: unreadBadgeFilter,
|
||||
// badgePosition: BadgePosition.topEnd(
|
||||
// top: -12,
|
||||
// end: -8,
|
||||
// ),
|
||||
// child: icon,
|
||||
// ),
|
||||
// Pangea#
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,8 +9,18 @@ import 'chat_members_view.dart';
|
|||
|
||||
class ChatMembersPage extends StatefulWidget {
|
||||
final String roomId;
|
||||
// #Pangea
|
||||
final String? filter;
|
||||
// Pangea#
|
||||
|
||||
const ChatMembersPage({required this.roomId, super.key});
|
||||
// #Pangea
|
||||
// const ChatMembersPage({required this.roomId, super.key});
|
||||
const ChatMembersPage({
|
||||
required this.roomId,
|
||||
this.filter,
|
||||
super.key,
|
||||
});
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
State<ChatMembersPage> createState() => ChatMembersController();
|
||||
|
|
@ -24,6 +34,22 @@ class ChatMembersController extends State<ChatMembersPage> {
|
|||
|
||||
final TextEditingController filterController = TextEditingController();
|
||||
|
||||
// #Pangea
|
||||
@override
|
||||
void didUpdateWidget(ChatMembersPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// Update the membership filter if the widget's filter changes
|
||||
if (oldWidget.filter != widget.filter) {
|
||||
setState(() {
|
||||
membershipFilter = Membership.values.firstWhere(
|
||||
(membership) => membership.name == widget.filter,
|
||||
orElse: () => Membership.join,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
void setMembershipFilter(Membership membership) {
|
||||
membershipFilter = membership;
|
||||
setFilter();
|
||||
|
|
@ -79,6 +105,19 @@ class ChatMembersController extends State<ChatMembersPage> {
|
|||
|
||||
if (!mounted) return;
|
||||
|
||||
// #Pangea
|
||||
final availableFilters = (participants ?? [])
|
||||
.map(
|
||||
(p) => p.membership,
|
||||
)
|
||||
.toSet();
|
||||
|
||||
if (availableFilters.length == 1 &&
|
||||
membershipFilter != availableFilters.first) {
|
||||
membershipFilter = availableFilters.first;
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
setState(() {
|
||||
members = participants;
|
||||
});
|
||||
|
|
@ -110,6 +149,15 @@ class ChatMembersController extends State<ChatMembersPage> {
|
|||
false,
|
||||
)
|
||||
.listen(refreshMembers);
|
||||
|
||||
// #Pangea
|
||||
if (widget.filter != null) {
|
||||
membershipFilter = Membership.values.firstWhere(
|
||||
(membership) => membership.name == widget.filter,
|
||||
orElse: () => Membership.join,
|
||||
);
|
||||
}
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import 'package:fluffychat/pangea/lemmas/lemma_emoji_row.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/phonetic_transcription/phonetic_transcription_widget.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_text_with_audio_button.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -26,6 +28,9 @@ class VocabDetailsView extends StatelessWidget {
|
|||
|
||||
ConstructUses get _construct => constructId.constructUses;
|
||||
|
||||
String? get _userL1 =>
|
||||
MatrixState.pangeaController.languageController.userL1?.langCode;
|
||||
|
||||
/// Get the language code for the current lemma
|
||||
String? get _userL2 =>
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
|
@ -49,14 +54,34 @@ class VocabDetailsView extends StatelessWidget {
|
|||
: _construct.lemmaCategory.darkColor(context));
|
||||
|
||||
return AnalyticsDetailsViewContent(
|
||||
title: WordTextWithAudioButton(
|
||||
text: _construct.lemma,
|
||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||
color: textColor,
|
||||
title: Column(
|
||||
children: [
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return ShrinkableText(
|
||||
text: _construct.lemma,
|
||||
maxWidth: constraints.maxWidth - 40.0,
|
||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||
color: textColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (MatrixState.pangeaController.languageController.userL2 != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: PhoneticTranscriptionWidget(
|
||||
text: _construct.lemma,
|
||||
textLanguage:
|
||||
MatrixState.pangeaController.languageController.userL2!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: textColor.withAlpha((0.7 * 255).toInt()),
|
||||
fontSize: 18,
|
||||
),
|
||||
iconSize: _iconSize * 0.8,
|
||||
),
|
||||
),
|
||||
iconSize: _iconSize,
|
||||
uniqueID: "${_construct.lemma}-${_construct.category}",
|
||||
langCode: _userL2!,
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -86,4 +86,7 @@ class PApiUrls {
|
|||
static String rcProductsTrial = "${PApiUrls.subscriptionEndpoint}/free_trial";
|
||||
|
||||
static String rcSubscription = PApiUrls.subscriptionEndpoint;
|
||||
|
||||
static String phoneticTranscription =
|
||||
"${PApiUrls.choreoEndpoint}/phonetic_transcription";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,14 @@ class PangeaTokenText {
|
|||
);
|
||||
}
|
||||
|
||||
static PangeaTokenText fromString(String content) {
|
||||
return PangeaTokenText(
|
||||
offset: 0,
|
||||
content: content,
|
||||
length: content.length,
|
||||
);
|
||||
}
|
||||
|
||||
static const String _offsetKey = "offset";
|
||||
static const String _contentKey = "content";
|
||||
static const String _lengthKey = "length";
|
||||
|
|
|
|||
|
|
@ -80,3 +80,27 @@ class LanguageModel {
|
|||
@override
|
||||
int get hashCode => langCode.hashCode;
|
||||
}
|
||||
|
||||
class LanguageArc {
|
||||
final LanguageModel l1;
|
||||
final LanguageModel l2;
|
||||
|
||||
LanguageArc({
|
||||
required this.l1,
|
||||
required this.l2,
|
||||
});
|
||||
|
||||
factory LanguageArc.fromJson(Map<String, dynamic> json) {
|
||||
return LanguageArc(
|
||||
l1: LanguageModel.fromJson(json['l1'] as Map<String, dynamic>),
|
||||
l2: LanguageModel.fromJson(json['l2'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'l1': l1.toJson(),
|
||||
'l2': l2.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,13 +192,15 @@ class SettingsLearningView extends StatelessWidget {
|
|||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context)
|
||||
.noIdenticalLanguages,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context)
|
||||
.noIdenticalLanguages,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -239,15 +239,17 @@ class LanguageDropDownEntry extends StatelessWidget {
|
|||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
languageModel.getDisplayName(context) ?? "",
|
||||
style: const TextStyle().copyWith(
|
||||
color: enabled
|
||||
? Theme.of(context).textTheme.bodyLarge!.color
|
||||
: Theme.of(context).disabledColor,
|
||||
fontSize: 14,
|
||||
Flexible(
|
||||
child: Text(
|
||||
languageModel.getDisplayName(context) ?? "",
|
||||
style: const TextStyle().copyWith(
|
||||
color: enabled
|
||||
? Theme.of(context).textTheme.bodyLarge!.color
|
||||
: Theme.of(context).disabledColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (isL2List && languageModel.l2Support != L2SupportEnum.full)
|
||||
|
|
|
|||
|
|
@ -28,17 +28,35 @@ class LemmaReactionPickerState extends State<LemmaReactionPicker> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.cId.getLemmaInfo().then((info) {
|
||||
loading = false;
|
||||
setState(() => displayEmoji = info.emoji);
|
||||
}).catchError((e, s) {
|
||||
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
|
||||
setState(() => loading = false);
|
||||
});
|
||||
_refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LemmaReactionPicker oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.cId != widget.cId) {
|
||||
_refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void setEmoji(String emoji) => widget.controller.sendEmojiAction(emoji);
|
||||
|
||||
Future<void> _refresh() async {
|
||||
setState(() {
|
||||
loading = true;
|
||||
displayEmoji = [];
|
||||
});
|
||||
|
||||
try {
|
||||
final info = await widget.cId.getLemmaInfo();
|
||||
displayEmoji = info.emoji;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ class OnboardingComplete extends StatelessWidget {
|
|||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha(20),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh
|
||||
.withAlpha(170),
|
||||
borderRadius: BorderRadius.circular(
|
||||
10.0,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/common/network/urls.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart';
|
||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PhoneticTranscriptionRepo {
|
||||
static final GetStorage _storage =
|
||||
GetStorage('phonetic_transcription_storage');
|
||||
|
||||
static void set(
|
||||
PhoneticTranscriptionRequest request,
|
||||
PhoneticTranscriptionResponse response,
|
||||
) {
|
||||
response.expireAt ??= DateTime.now().add(const Duration(days: 100));
|
||||
_storage.write(request.storageKey, response.toJson());
|
||||
}
|
||||
|
||||
static Future<PhoneticTranscriptionResponse> _fetch(
|
||||
PhoneticTranscriptionRequest request,
|
||||
) async {
|
||||
final cachedJson = _storage.read(request.storageKey);
|
||||
final cached = cachedJson == null
|
||||
? null
|
||||
: PhoneticTranscriptionResponse.fromJson(cachedJson);
|
||||
|
||||
if (cached != null) {
|
||||
if (DateTime.now().isBefore(cached.expireAt!)) {
|
||||
return cached;
|
||||
} else {
|
||||
_storage.remove(request.storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.phoneticTranscription,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
final response = PhoneticTranscriptionResponse.fromJson(decodedBody);
|
||||
set(request, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
static Future<PhoneticTranscriptionResponse> get(
|
||||
PhoneticTranscriptionRequest request,
|
||||
) async {
|
||||
try {
|
||||
return await _fetch(request);
|
||||
} catch (e) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, data: request.toJson());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
|
||||
class PhoneticTranscriptionRequest {
|
||||
final LanguageArc arc;
|
||||
final PangeaTokenText content;
|
||||
final bool requiresTokenization;
|
||||
|
||||
PhoneticTranscriptionRequest({
|
||||
required this.arc,
|
||||
required this.content,
|
||||
this.requiresTokenization = false,
|
||||
});
|
||||
|
||||
factory PhoneticTranscriptionRequest.fromJson(Map<String, dynamic> json) {
|
||||
return PhoneticTranscriptionRequest(
|
||||
arc: LanguageArc.fromJson(json['arc'] as Map<String, dynamic>),
|
||||
content:
|
||||
PangeaTokenText.fromJson(json['content'] as Map<String, dynamic>),
|
||||
requiresTokenization: json['requires_tokenization'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'arc': arc.toJson(),
|
||||
'content': content.toJson(),
|
||||
'requires_tokenization': requiresTokenization,
|
||||
};
|
||||
}
|
||||
|
||||
String get storageKey => '${arc.l1}-${arc.l2}-${content.hashCode}';
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
|
||||
enum PhoneticTranscriptionDelimEnum { sp, noSp }
|
||||
|
||||
extension PhoneticTranscriptionDelimEnumExt on PhoneticTranscriptionDelimEnum {
|
||||
String get value {
|
||||
switch (this) {
|
||||
case PhoneticTranscriptionDelimEnum.sp:
|
||||
return " ";
|
||||
case PhoneticTranscriptionDelimEnum.noSp:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static PhoneticTranscriptionDelimEnum fromString(String s) {
|
||||
switch (s) {
|
||||
case " ":
|
||||
return PhoneticTranscriptionDelimEnum.sp;
|
||||
case "":
|
||||
return PhoneticTranscriptionDelimEnum.noSp;
|
||||
default:
|
||||
return PhoneticTranscriptionDelimEnum.sp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneticTranscriptionToken {
|
||||
final LanguageArc arc;
|
||||
final PangeaTokenText tokenL2;
|
||||
final PangeaTokenText phoneticL1Transcription;
|
||||
|
||||
PhoneticTranscriptionToken({
|
||||
required this.arc,
|
||||
required this.tokenL2,
|
||||
required this.phoneticL1Transcription,
|
||||
});
|
||||
|
||||
factory PhoneticTranscriptionToken.fromJson(Map<String, dynamic> json) {
|
||||
return PhoneticTranscriptionToken(
|
||||
arc: LanguageArc.fromJson(json['arc'] as Map<String, dynamic>),
|
||||
tokenL2:
|
||||
PangeaTokenText.fromJson(json['token_l2'] as Map<String, dynamic>),
|
||||
phoneticL1Transcription: PangeaTokenText.fromJson(
|
||||
json['phonetic_l1_transcription'] as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'arc': arc.toJson(),
|
||||
'token_l2': tokenL2.toJson(),
|
||||
'phonetic_l1_transcription': phoneticL1Transcription.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
class PhoneticTranscription {
|
||||
final LanguageArc arc;
|
||||
final PangeaTokenText transcriptionL2;
|
||||
final List<PhoneticTranscriptionToken> phoneticTranscription;
|
||||
final PhoneticTranscriptionDelimEnum delim;
|
||||
|
||||
PhoneticTranscription({
|
||||
required this.arc,
|
||||
required this.transcriptionL2,
|
||||
required this.phoneticTranscription,
|
||||
this.delim = PhoneticTranscriptionDelimEnum.sp,
|
||||
});
|
||||
|
||||
factory PhoneticTranscription.fromJson(Map<String, dynamic> json) {
|
||||
return PhoneticTranscription(
|
||||
arc: LanguageArc.fromJson(json['arc'] as Map<String, dynamic>),
|
||||
transcriptionL2: PangeaTokenText.fromJson(
|
||||
json['transcription_l2'] as Map<String, dynamic>,
|
||||
),
|
||||
phoneticTranscription: (json['phonetic_transcription'] as List)
|
||||
.map(
|
||||
(e) =>
|
||||
PhoneticTranscriptionToken.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
delim: json['delim'] != null
|
||||
? PhoneticTranscriptionDelimEnumExt.fromString(
|
||||
json['delim'] as String,
|
||||
)
|
||||
: PhoneticTranscriptionDelimEnum.sp,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'arc': arc.toJson(),
|
||||
'transcription_l2': transcriptionL2.toJson(),
|
||||
'phonetic_transcription':
|
||||
phoneticTranscription.map((e) => e.toJson()).toList(),
|
||||
'delim': delim.value,
|
||||
};
|
||||
}
|
||||
|
||||
class PhoneticTranscriptionResponse {
|
||||
final LanguageArc arc;
|
||||
final PangeaTokenText content;
|
||||
final Map<String, dynamic>
|
||||
tokenization; // You can define a typesafe model if needed
|
||||
final PhoneticTranscription phoneticTranscriptionResult;
|
||||
DateTime? expireAt;
|
||||
|
||||
PhoneticTranscriptionResponse({
|
||||
required this.arc,
|
||||
required this.content,
|
||||
required this.tokenization,
|
||||
required this.phoneticTranscriptionResult,
|
||||
this.expireAt,
|
||||
});
|
||||
|
||||
factory PhoneticTranscriptionResponse.fromJson(Map<String, dynamic> json) {
|
||||
return PhoneticTranscriptionResponse(
|
||||
arc: LanguageArc.fromJson(json['arc'] as Map<String, dynamic>),
|
||||
content:
|
||||
PangeaTokenText.fromJson(json['content'] as Map<String, dynamic>),
|
||||
tokenization: Map<String, dynamic>.from(json['tokenization'] as Map),
|
||||
phoneticTranscriptionResult: PhoneticTranscription.fromJson(
|
||||
json['phonetic_transcription_result'] as Map<String, dynamic>,
|
||||
),
|
||||
expireAt: json['expireAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expireAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'arc': arc.toJson(),
|
||||
'content': content.toJson(),
|
||||
'tokenization': tokenization,
|
||||
'phonetic_transcription_result': phoneticTranscriptionResult.toJson(),
|
||||
'expireAt': expireAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PhoneticTranscriptionResponse &&
|
||||
runtimeType == other.runtimeType &&
|
||||
arc == other.arc &&
|
||||
content == other.content &&
|
||||
tokenization == other.tokenization &&
|
||||
phoneticTranscriptionResult == other.phoneticTranscriptionResult;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
arc.hashCode ^
|
||||
content.hashCode ^
|
||||
tokenization.hashCode ^
|
||||
phoneticTranscriptionResult.hashCode;
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_repo.dart';
|
||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PhoneticTranscriptionWidget extends StatefulWidget {
|
||||
final String text;
|
||||
final LanguageModel textLanguage;
|
||||
final TextStyle? style;
|
||||
final double? iconSize;
|
||||
|
||||
const PhoneticTranscriptionWidget({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.textLanguage,
|
||||
this.style,
|
||||
this.iconSize,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PhoneticTranscriptionWidget> createState() =>
|
||||
_PhoneticTranscriptionWidgetState();
|
||||
}
|
||||
|
||||
class _PhoneticTranscriptionWidgetState
|
||||
extends State<PhoneticTranscriptionWidget> {
|
||||
late Future<String?> _transcriptionFuture;
|
||||
bool _hovering = false;
|
||||
bool _isPlaying = false;
|
||||
bool _isLoading = false;
|
||||
late final StreamSubscription _loadingChoreoSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_transcriptionFuture = _fetchTranscription();
|
||||
_loadingChoreoSubscription =
|
||||
TtsController.loadingChoreoStream.stream.listen((val) {
|
||||
if (mounted) setState(() => _isLoading = val);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
TtsController.stop();
|
||||
_loadingChoreoSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<String?> _fetchTranscription() async {
|
||||
if (MatrixState.pangeaController.languageController.userL1 == null) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception('User L1 is not set'),
|
||||
data: {
|
||||
'text': widget.text,
|
||||
'textLanguageCode': widget.textLanguage.langCode,
|
||||
},
|
||||
);
|
||||
return widget.text; // Fallback to original text if no L1 is set
|
||||
}
|
||||
final req = PhoneticTranscriptionRequest(
|
||||
arc: LanguageArc(
|
||||
l1: MatrixState.pangeaController.languageController.userL1!,
|
||||
l2: widget.textLanguage,
|
||||
),
|
||||
content: PangeaTokenText.fromString(widget.text),
|
||||
// arc can be omitted for default empty map
|
||||
);
|
||||
final res = await PhoneticTranscriptionRepo.get(req);
|
||||
return res.phoneticTranscriptionResult.phoneticTranscription.first
|
||||
.phoneticL1Transcription.content;
|
||||
}
|
||||
|
||||
Future<void> _handleAudioTap(BuildContext context) async {
|
||||
if (_isPlaying) {
|
||||
await TtsController.stop();
|
||||
setState(() => _isPlaying = false);
|
||||
} else {
|
||||
await TtsController.tryToSpeak(
|
||||
widget.text,
|
||||
context: context,
|
||||
targetID: 'phonetic-transcription-${widget.text}',
|
||||
langCode: widget.textLanguage.langCode,
|
||||
onStart: () {
|
||||
if (mounted) setState(() => _isPlaying = true);
|
||||
},
|
||||
onStop: () {
|
||||
if (mounted) setState(() => _isPlaying = false);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<String?>(
|
||||
future: _transcriptionFuture,
|
||||
builder: (context, snapshot) {
|
||||
final transcription = snapshot.data ?? '';
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _hovering = true),
|
||||
onExit: (_) => setState(() => _hovering = false),
|
||||
child: 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: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
"/${transcription.isNotEmpty ? transcription : widget.text}/",
|
||||
style: widget.style ??
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Tooltip(
|
||||
message: _isPlaying
|
||||
? L10n.of(context).stop
|
||||
: L10n.of(context).playAudio,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 3),
|
||||
)
|
||||
: Icon(
|
||||
_isPlaying ? Icons.pause_outlined : Icons.volume_up,
|
||||
size: widget.iconSize ?? 24,
|
||||
color: _isPlaying
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -200,12 +200,14 @@ class PublicRoomBottomSheetState extends State<PublicRoomBottomSheet> {
|
|||
Row(
|
||||
spacing: 16.0,
|
||||
children: [
|
||||
(chunk?.avatarUrl != null)
|
||||
(chunk?.avatarUrl != null || chunk?.roomType != 'm.space')
|
||||
? Avatar(
|
||||
mxContent: chunk?.avatarUrl,
|
||||
name: chunk?.name,
|
||||
size: 160.0,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderRadius: BorderRadius.circular(
|
||||
chunk?.roomType != 'm.space' ? 80 : 24.0,
|
||||
),
|
||||
)
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
|
|
@ -242,7 +244,11 @@ class PublicRoomBottomSheetState extends State<PublicRoomBottomSheet> {
|
|||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
chunk?.topic ??
|
||||
L10n.of(context).noSpaceDescriptionYet,
|
||||
(chunk?.roomType != 'm.space'
|
||||
? L10n.of(context)
|
||||
.noChatDescriptionYet
|
||||
: L10n.of(context)
|
||||
.noSpaceDescriptionYet),
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.start,
|
||||
maxLines: null,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/stream_extension.dart';
|
||||
|
||||
|
|
@ -89,15 +90,16 @@ class KnockingUsersIndicatorState extends State<KnockingUsersIndicator> {
|
|||
Expanded(
|
||||
child: Text(
|
||||
_knockingUsers.length == 1
|
||||
? "1 user is requesting to join your space"
|
||||
: "${_knockingUsers.length} users are requesting to join your space",
|
||||
? L10n.of(context).aUserIsKnocking
|
||||
: L10n.of(context)
|
||||
.usersAreKnocking(_knockingUsers.length),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => context.push(
|
||||
"/rooms/${widget.room.id}/details/members",
|
||||
"/rooms/${widget.room.id}/details/members?filter=knock",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@ class LeaderboardParticipantListState
|
|||
return LoadParticipantsUtil(
|
||||
space: widget.space,
|
||||
builder: (participantsLoader) {
|
||||
final participants = participantsLoader.filteredParticipants("");
|
||||
final participants = participantsLoader
|
||||
.filteredParticipants("")
|
||||
.where((p) => p.membership == Membership.join)
|
||||
.toList();
|
||||
|
||||
return AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class TokenRenderingUtil {
|
|||
return readingAssistanceMode == ReadingAssistanceMode.transitionMode;
|
||||
}
|
||||
|
||||
double? _fontSize(BuildContext context) => showCenterStyling
|
||||
double? fontSize(BuildContext context) => showCenterStyling
|
||||
? overlayController != null && overlayController!.maxWidth > 600
|
||||
? Theme.of(context).textTheme.titleLarge?.fontSize
|
||||
: Theme.of(context).textTheme.bodyLarge?.fontSize
|
||||
|
|
@ -38,14 +38,14 @@ class TokenRenderingUtil {
|
|||
Color? color,
|
||||
}) =>
|
||||
existingStyle.copyWith(
|
||||
fontSize: _fontSize(context),
|
||||
fontSize: fontSize(context),
|
||||
decoration: TextDecoration.underline,
|
||||
decorationThickness: 4,
|
||||
decorationColor: color ?? Colors.white.withAlpha(0),
|
||||
);
|
||||
|
||||
double tokenTextWidthForContainer(BuildContext context, String text) {
|
||||
final tokenSizeKey = "$text-${_fontSize(context)}";
|
||||
final tokenSizeKey = "$text-${fontSize(context)}";
|
||||
if (_tokensWidthCache.containsKey(tokenSizeKey)) {
|
||||
return _tokensWidthCache[tokenSizeKey]!;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import 'package:fluffychat/pages/chat/events/message_content.dart';
|
|||
import 'package:fluffychat/pages/chat/events/reply_content.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';
|
||||
import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart';
|
||||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
|
|
@ -160,7 +163,7 @@ class OverlayMessage extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
L10n.of(context).oopsSomethingWentWrong,
|
||||
L10n.of(context).transcriptionFailed,
|
||||
style: AppConfig.messageTextStyle(
|
||||
event,
|
||||
textColor,
|
||||
|
|
@ -170,14 +173,34 @@ class OverlayMessage extends StatelessWidget {
|
|||
)
|
||||
: overlayController.transcription != null
|
||||
? SingleChildScrollView(
|
||||
child: Text(
|
||||
overlayController.transcription!.transcript.text,
|
||||
style: AppConfig.messageTextStyle(
|
||||
event,
|
||||
textColor,
|
||||
).copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
overlayController
|
||||
.transcription!.transcript.text,
|
||||
style: AppConfig.messageTextStyle(
|
||||
event,
|
||||
textColor,
|
||||
).copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
PhoneticTranscriptionWidget(
|
||||
text: overlayController
|
||||
.transcription!.transcript.text,
|
||||
textLanguage: PLanguageStore.byLangCode(
|
||||
pangeaMessageEvent!
|
||||
.messageDisplayLangCode,
|
||||
) ??
|
||||
LanguageModel.unknown,
|
||||
style: AppConfig.messageTextStyle(
|
||||
event,
|
||||
textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
|
|
|
|||
|
|
@ -37,11 +37,10 @@ enum SelectMode {
|
|||
case SelectMode.audio:
|
||||
return l10n.playAudio;
|
||||
case SelectMode.translate:
|
||||
case SelectMode.speechTranslation:
|
||||
return l10n.translationTooltip;
|
||||
case SelectMode.practice:
|
||||
return l10n.practice;
|
||||
case SelectMode.speechTranslation:
|
||||
return l10n.speechToTextTooltip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,20 +90,21 @@ void showMemberActionsPopupMenu({
|
|||
),
|
||||
const PopupMenuDivider(),
|
||||
// #Pangea
|
||||
PopupMenuItem(
|
||||
value: _MemberActions.chat,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.forum_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(
|
||||
dmRoomId == null
|
||||
? L10n.of(context).startConversation
|
||||
: L10n.of(context).sendAMessage,
|
||||
),
|
||||
],
|
||||
if (user.room.client.userID != user.id)
|
||||
PopupMenuItem(
|
||||
value: _MemberActions.chat,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.forum_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(
|
||||
dmRoomId == null
|
||||
? L10n.of(context).startConversation
|
||||
: L10n.of(context).sendAMessage,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
if (onMention != null)
|
||||
PopupMenuItem(
|
||||
|
|
|
|||
|
|
@ -48,6 +48,24 @@ class _PresenceBuilderState extends State<PresenceBuilder> {
|
|||
}
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
@override
|
||||
void didUpdateWidget(PresenceBuilder oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.userId == widget.userId) return;
|
||||
|
||||
final client = widget.client ?? Matrix.of(context).client;
|
||||
final userId = widget.userId;
|
||||
if (userId != null) {
|
||||
client.fetchCurrentPresence(userId).then(_updatePresence);
|
||||
_sub?.cancel();
|
||||
_sub = client.onPresenceChanged.stream
|
||||
.where((presence) => presence.userid == userId)
|
||||
.listen(_updatePresence);
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub?.cancel();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue