3770 total vocab grammar and xp calculations per user and activity (#3775)
This commit is contained in:
parent
b4cb8f6edc
commit
ece75b7f74
21 changed files with 689 additions and 458 deletions
|
|
@ -973,33 +973,13 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// There's a listen in my_analytics_controller that decides when to auto-update
|
||||
// analytics based on when / how many messages the logged in user send. This
|
||||
// stream sends the data for newly sent messages.
|
||||
final metadata = ConstructUseMetaData(
|
||||
roomId: roomId,
|
||||
timeStamp: DateTime.now(),
|
||||
eventId: msgEventId,
|
||||
_sendMessageAnalytics(
|
||||
msgEventId,
|
||||
originalSent: originalSent,
|
||||
tokensSent: tokensSent,
|
||||
choreo: choreo,
|
||||
);
|
||||
|
||||
if (msgEventId != null && originalSent != null && tokensSent != null) {
|
||||
final List<OneConstructUse> constructs = [
|
||||
...originalSent.vocabAndMorphUses(
|
||||
choreo: choreo,
|
||||
tokens: tokensSent.tokens,
|
||||
metadata: metadata,
|
||||
),
|
||||
];
|
||||
|
||||
_showAnalyticsFeedback(constructs, msgEventId);
|
||||
|
||||
pangeaController.putAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
eventId: msgEventId,
|
||||
targetID: msgEventId,
|
||||
roomId: room.id,
|
||||
constructs: constructs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (previousEdit != null) {
|
||||
pangeaEditingEvent = previousEdit;
|
||||
}
|
||||
|
|
@ -2091,6 +2071,48 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _sendMessageAnalytics(
|
||||
String? eventId, {
|
||||
PangeaRepresentation? originalSent,
|
||||
PangeaMessageTokens? tokensSent,
|
||||
ChoreoRecord? choreo,
|
||||
}) async {
|
||||
// There's a listen in my_analytics_controller that decides when to auto-update
|
||||
// analytics based on when / how many messages the logged in user send. This
|
||||
// stream sends the data for newly sent messages.
|
||||
if (originalSent?.langCode.split("-").first !=
|
||||
choreographer.l2Lang?.langCodeShort) {
|
||||
return;
|
||||
}
|
||||
|
||||
final metadata = ConstructUseMetaData(
|
||||
roomId: roomId,
|
||||
timeStamp: DateTime.now(),
|
||||
eventId: eventId,
|
||||
);
|
||||
|
||||
if (eventId != null && originalSent != null && tokensSent != null) {
|
||||
final List<OneConstructUse> constructs = [
|
||||
...originalSent.vocabAndMorphUses(
|
||||
choreo: choreo,
|
||||
tokens: tokensSent.tokens,
|
||||
metadata: metadata,
|
||||
),
|
||||
];
|
||||
|
||||
_showAnalyticsFeedback(constructs, eventId);
|
||||
|
||||
pangeaController.putAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
eventId: eventId,
|
||||
targetID: eventId,
|
||||
roomId: room.id,
|
||||
constructs: constructs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendVoiceMessageAnalytics(String? eventId) async {
|
||||
if (eventId == null) {
|
||||
ErrorHandler.logError(
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import 'package:fluffychat/l10n/l10n.dart';
|
|||
import 'package:fluffychat/pages/settings/settings.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/pangea_chat_details.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/download/download_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/download/download_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/file_selector.dart';
|
||||
|
|
@ -238,7 +238,26 @@ class ChatDetailsController extends State<ChatDetails> {
|
|||
],
|
||||
);
|
||||
if (type == null) return;
|
||||
downloadChat(room, type, context);
|
||||
|
||||
try {
|
||||
await room.download(type, context);
|
||||
} on EmptyChatException {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
L10n.of(context).emptyChatDownloadWarning,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"${L10n.of(context).oopsSomethingWentWrong} ${L10n.of(context).errorPleaseRefresh}",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setBotOptions(BotOptionsModel botOptions) async {
|
||||
|
|
|
|||
36
lib/pangea/activity_sessions/activity_analytics_chip.dart
Normal file
36
lib/pangea/activity_sessions/activity_analytics_chip.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ActivityAnalyticsChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
const ActivityAnalyticsChip(
|
||||
this.icon,
|
||||
this.text, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 12.0),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,10 +7,13 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_analytics_chip.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_participant_indicator.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_results_carousel.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
|
|
@ -103,6 +106,21 @@ class ActivityFinishedStatusMessage extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (summary.analytics != null)
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ActivityAnalyticsChip(
|
||||
ConstructTypeEnum.vocab.indicator.icon,
|
||||
"${summary.analytics!.uniqueConstructCount(ConstructTypeEnum.vocab)}",
|
||||
),
|
||||
ActivityAnalyticsChip(
|
||||
ConstructTypeEnum.morph.indicator.icon,
|
||||
"${summary.analytics!.uniqueConstructCount(ConstructTypeEnum.morph)}",
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
if (_highlightedRole != null && userSummary != null)
|
||||
ClipRRect(
|
||||
|
|
@ -114,9 +132,11 @@ class ActivityFinishedStatusMessage extends StatelessWidget {
|
|||
child: Column(
|
||||
children: [
|
||||
ActivityResultsCarousel(
|
||||
userId: _highlightedRole!.userId,
|
||||
selectedRole: _highlightedRole!,
|
||||
user: user,
|
||||
summary: userSummary,
|
||||
analytics: summary.analytics,
|
||||
),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
|
|
|
|||
|
|
@ -3,19 +3,27 @@ import 'package:flutter/material.dart';
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_analytics_chip.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
|
||||
class ActivityResultsCarousel extends StatelessWidget {
|
||||
final String userId;
|
||||
final ActivityRoleModel selectedRole;
|
||||
final ParticipantSummaryModel summary;
|
||||
final ActivitySummaryAnalyticsModel? analytics;
|
||||
|
||||
final User? user;
|
||||
|
||||
const ActivityResultsCarousel({
|
||||
super.key,
|
||||
required this.userId,
|
||||
required this.selectedRole,
|
||||
required this.summary,
|
||||
required this.analytics,
|
||||
this.user,
|
||||
});
|
||||
|
||||
|
|
@ -48,25 +56,19 @@ class ActivityResultsCarousel extends StatelessWidget {
|
|||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
if (analytics != null)
|
||||
ActivityAnalyticsChip(
|
||||
ConstructTypeEnum.vocab.indicator.icon,
|
||||
"${analytics!.uniqueConstructCountForUser(userId, ConstructTypeEnum.vocab)}",
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.school, size: 12.0),
|
||||
Text(
|
||||
summary.cefrLevel,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (analytics != null)
|
||||
ActivityAnalyticsChip(
|
||||
ConstructTypeEnum.morph.indicator.icon,
|
||||
"${analytics!.uniqueConstructCountForUser(userId, ConstructTypeEnum.morph)}",
|
||||
),
|
||||
ActivityAnalyticsChip(
|
||||
Icons.school,
|
||||
summary.cefrLevel,
|
||||
),
|
||||
...summary.superlatives.map(
|
||||
(sup) => Container(
|
||||
|
|
|
|||
|
|
@ -8,15 +8,16 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_repo.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_request_model.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import '../activity_summary/activity_summary_repo.dart';
|
||||
|
||||
extension ActivityRoomExtension on Room {
|
||||
Future<void> sendActivityPlan(
|
||||
|
|
@ -119,7 +120,9 @@ extension ActivityRoomExtension on Room {
|
|||
}
|
||||
|
||||
Future<void> fetchSummaries() async {
|
||||
if (activitySummary?.summary != null) return;
|
||||
if (activitySummary?.summary != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await setActivitySummary(
|
||||
ActivitySummaryModel(
|
||||
|
|
@ -128,15 +131,18 @@ extension ActivityRoomExtension on Room {
|
|||
),
|
||||
);
|
||||
|
||||
final events = await getAllEvents(this);
|
||||
final events = await getAllEvents();
|
||||
final List<ActivitySummaryResultsMessage> messages = [];
|
||||
final ActivitySummaryAnalyticsModel analytics =
|
||||
ActivitySummaryAnalyticsModel();
|
||||
|
||||
final timeline = this.timeline ?? await getTimeline();
|
||||
for (final event in events) {
|
||||
if (event.type != EventTypes.Message ||
|
||||
event.messageType != MessageTypes.Text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final timeline = this.timeline ?? await getTimeline();
|
||||
final pangeaMessage = PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
|
|
@ -155,6 +161,7 @@ extension ActivityRoomExtension on Room {
|
|||
);
|
||||
|
||||
messages.add(activityMessage);
|
||||
analytics.addConstructs(pangeaMessage);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -163,11 +170,15 @@ extension ActivityRoomExtension on Room {
|
|||
activity: activityPlan!,
|
||||
activityResults: messages,
|
||||
contentFeedback: [],
|
||||
analytics: analytics,
|
||||
),
|
||||
);
|
||||
|
||||
await setActivitySummary(
|
||||
ActivitySummaryModel(summary: resp),
|
||||
ActivitySummaryModel(
|
||||
summary: resp,
|
||||
analytics: analytics,
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
|
||||
class ActivitySummaryAnalyticsModel {
|
||||
final Map<String, UserConstructAnalytics> constructs = {};
|
||||
|
||||
ActivitySummaryAnalyticsModel();
|
||||
|
||||
Map<ConstructTypeEnum, int> uniqueConstructCountsByType() {
|
||||
final Map<ConstructTypeEnum, Set<ConstructIdentifier>> typeToIds = {};
|
||||
|
||||
for (final userAnalytics in constructs.values) {
|
||||
for (final usage in userAnalytics.usages.values) {
|
||||
final id = usage.identifier;
|
||||
typeToIds.putIfAbsent(id.type, () => <ConstructIdentifier>{}).add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
for (final entry in typeToIds.entries) entry.key: entry.value.length,
|
||||
};
|
||||
}
|
||||
|
||||
int uniqueConstructCount(ConstructTypeEnum type) =>
|
||||
uniqueConstructCountsByType()[type] ?? 0;
|
||||
|
||||
/// Unique constructs of a given type for a specific user
|
||||
int uniqueConstructCountForUser(String userId, ConstructTypeEnum type) {
|
||||
final userAnalytics = constructs[userId];
|
||||
if (userAnalytics == null) return 0;
|
||||
return userAnalytics.constructsOfType(type).length;
|
||||
}
|
||||
|
||||
void addConstructs(PangeaMessageEvent event) {
|
||||
final uses = event.originalSent?.vocabAndMorphUses();
|
||||
if (uses == null || uses.isEmpty) return;
|
||||
|
||||
final user =
|
||||
constructs[event.senderId] ??= UserConstructAnalytics(event.senderId);
|
||||
|
||||
for (final use in uses) {
|
||||
user.addUsage(use.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
factory ActivitySummaryAnalyticsModel.fromJson(Map<String, dynamic> json) {
|
||||
final model = ActivitySummaryAnalyticsModel();
|
||||
|
||||
for (final userEntry in json.entries) {
|
||||
final userId = userEntry.key;
|
||||
final constructList = userEntry.value as List<dynamic>;
|
||||
|
||||
final userAnalytics = UserConstructAnalytics(userId);
|
||||
|
||||
for (final constructJson in constructList) {
|
||||
final constructId = ConstructIdentifier.fromJson(constructJson);
|
||||
final timesUsed = constructJson['times_used'] as int? ?? 0;
|
||||
|
||||
final usage = ConstructUsage(constructId)..timesUsed = timesUsed;
|
||||
userAnalytics.usages[constructId.string] = usage;
|
||||
}
|
||||
|
||||
model.constructs[userId] = userAnalytics;
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
for (final entry in constructs.entries)
|
||||
entry.key: entry.value.toJsonList(),
|
||||
};
|
||||
}
|
||||
|
||||
class ConstructUsage {
|
||||
final ConstructIdentifier identifier;
|
||||
int timesUsed;
|
||||
|
||||
ConstructUsage(this.identifier) : timesUsed = 0;
|
||||
|
||||
void increment() => timesUsed++;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
...identifier.toJson(),
|
||||
'times_used': timesUsed,
|
||||
};
|
||||
}
|
||||
|
||||
class UserConstructAnalytics {
|
||||
final String userId;
|
||||
final Map<String, ConstructUsage> usages;
|
||||
|
||||
UserConstructAnalytics(this.userId) : usages = {};
|
||||
|
||||
/// Unique constructs of a given type
|
||||
Set<ConstructIdentifier> constructsOfType(ConstructTypeEnum type) =>
|
||||
usages.values
|
||||
.map((u) => u.identifier)
|
||||
.where((id) => id.type == type)
|
||||
.toSet();
|
||||
|
||||
void addUsage(ConstructIdentifier id) {
|
||||
usages[id.string] ??= ConstructUsage(id);
|
||||
usages[id.string]!.increment();
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> toJsonList() =>
|
||||
usages.values.map((u) => u.toJson()).toList();
|
||||
}
|
||||
|
|
@ -1,14 +1,17 @@
|
|||
import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart';
|
||||
|
||||
class ActivitySummaryModel {
|
||||
final ActivitySummaryResponseModel? summary;
|
||||
final DateTime? requestedAt;
|
||||
final DateTime? errorAt;
|
||||
final ActivitySummaryAnalyticsModel? analytics;
|
||||
|
||||
ActivitySummaryModel({
|
||||
this.summary,
|
||||
this.requestedAt,
|
||||
this.errorAt,
|
||||
this.analytics,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -16,6 +19,7 @@ class ActivitySummaryModel {
|
|||
"summary": summary?.toJson(),
|
||||
"requested_at": requestedAt?.toIso8601String(),
|
||||
"error_at": errorAt?.toIso8601String(),
|
||||
"analytics": analytics?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +33,9 @@ class ActivitySummaryModel {
|
|||
: null,
|
||||
errorAt:
|
||||
json['error_at'] != null ? DateTime.parse(json['error_at']) : null,
|
||||
analytics: json['analytics'] != null
|
||||
? ActivitySummaryAnalyticsModel.fromJson(json['analytics'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Add this import for the participant summary model
|
||||
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart';
|
||||
|
||||
class ActivitySummaryResultsMessage {
|
||||
|
|
@ -67,11 +68,13 @@ class ActivitySummaryRequestModel {
|
|||
final ActivityPlanModel activity;
|
||||
final List<ActivitySummaryResultsMessage> activityResults;
|
||||
final List<ContentFeedbackModel> contentFeedback;
|
||||
final ActivitySummaryAnalyticsModel analytics;
|
||||
|
||||
ActivitySummaryRequestModel({
|
||||
required this.activity,
|
||||
required this.activityResults,
|
||||
required this.contentFeedback,
|
||||
required this.analytics,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -79,6 +82,7 @@ class ActivitySummaryRequestModel {
|
|||
'activity': activity.toJson(),
|
||||
'activity_results': activityResults.map((e) => e.toJson()).toList(),
|
||||
'content_feedback': contentFeedback.map((e) => e.toJson()).toList(),
|
||||
'analytics': analytics.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@ import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
|||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/download/download_file_util.dart';
|
||||
import 'package:fluffychat/pangea/download/download_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
|
|
@ -101,12 +102,12 @@ class AnalyticsDownloadDialogState extends State<AnalyticsDownloadDialog> {
|
|||
"analytics_morph_${MatrixState.pangeaController.matrixState.client.userID?.localpart}_${DateFormat('yyyy-MM-dd-hh:mm:ss').format(DateTime.now())}.csv";
|
||||
|
||||
final futures = [
|
||||
downloadFile(
|
||||
DownloadUtil.downloadFile(
|
||||
vocabContent,
|
||||
vocabFileName,
|
||||
_downloadType,
|
||||
),
|
||||
downloadFile(
|
||||
DownloadUtil.downloadFile(
|
||||
morphContent,
|
||||
morphFileName,
|
||||
_downloadType,
|
||||
|
|
@ -123,7 +124,7 @@ class AnalyticsDownloadDialogState extends State<AnalyticsDownloadDialog> {
|
|||
final fileName =
|
||||
"analytics_${MatrixState.pangeaController.matrixState.client.userID?.localpart}_${DateFormat('yyyy-MM-dd-hh:mm:ss').format(DateTime.now())}.xlsx'}";
|
||||
|
||||
await downloadFile(
|
||||
await DownloadUtil.downloadFile(
|
||||
content,
|
||||
fileName,
|
||||
_downloadType,
|
||||
|
|
|
|||
|
|
@ -1,338 +0,0 @@
|
|||
// ignore_for_file: implementation_imports
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:csv/csv.dart';
|
||||
import 'package:excel/excel.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/models/timeline_chunk.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
|
||||
Future<void> downloadChat(
|
||||
Room room,
|
||||
DownloadType type,
|
||||
BuildContext context,
|
||||
) async {
|
||||
List<PangeaMessageEvent> allPangeaMessages;
|
||||
|
||||
try {
|
||||
final List<Event> allEvents = await getAllEvents(room);
|
||||
final TimelineChunk chunk = TimelineChunk(events: allEvents);
|
||||
final Timeline timeline = Timeline(
|
||||
room: room,
|
||||
chunk: chunk,
|
||||
);
|
||||
|
||||
allPangeaMessages = getPangeaMessageEvents(
|
||||
allEvents,
|
||||
timeline,
|
||||
room,
|
||||
);
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": room.id,
|
||||
},
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"${L10n.of(context).oopsSomethingWentWrong} ${L10n.of(context).errorPleaseRefresh}",
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (allPangeaMessages.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
L10n.of(context).emptyChatDownloadWarning,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final String filename = getFilename(room, type);
|
||||
|
||||
switch (type) {
|
||||
case DownloadType.txt:
|
||||
final String content =
|
||||
getTxtContent(allPangeaMessages, context, filename, room);
|
||||
downloadFile(content, filename, DownloadType.txt);
|
||||
break;
|
||||
case DownloadType.csv:
|
||||
final String content =
|
||||
getCSVContent(allPangeaMessages, context, filename);
|
||||
downloadFile(content, filename, DownloadType.csv);
|
||||
return;
|
||||
case DownloadType.xlsx:
|
||||
final List<int> content =
|
||||
getExcelContent(allPangeaMessages, context, filename);
|
||||
downloadFile(content, filename, DownloadType.xlsx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Event>> getAllEvents(Room room) async {
|
||||
final GetRoomEventsResponse initalResp =
|
||||
await room.client.getRoomEvents(room.id, Direction.b);
|
||||
if (initalResp.end == null) return [];
|
||||
String? nextStartToken = initalResp.end;
|
||||
List<MatrixEvent> allMatrixEvents = initalResp.chunk;
|
||||
while (nextStartToken != null) {
|
||||
final GetRoomEventsResponse resp = await room.client.getRoomEvents(
|
||||
room.id,
|
||||
Direction.b,
|
||||
from: nextStartToken,
|
||||
);
|
||||
final chunkMessages = resp.chunk;
|
||||
allMatrixEvents.addAll(chunkMessages);
|
||||
resp.end != nextStartToken
|
||||
? nextStartToken = resp.end
|
||||
: nextStartToken = null;
|
||||
}
|
||||
allMatrixEvents = allMatrixEvents.reversed.toList();
|
||||
final List<Event> allEvents = allMatrixEvents
|
||||
.map((MatrixEvent message) => Event.fromMatrixEvent(message, room))
|
||||
.toList();
|
||||
return allEvents;
|
||||
}
|
||||
|
||||
List<PangeaMessageEvent> getPangeaMessageEvents(
|
||||
List<Event> events,
|
||||
Timeline timeline,
|
||||
Room room,
|
||||
) {
|
||||
final List<PangeaMessageEvent> allPangeaMessages = events
|
||||
.where(
|
||||
(Event event) =>
|
||||
event.type == EventTypes.Message &&
|
||||
event.content['msgtype'] == MessageTypes.Text,
|
||||
)
|
||||
.map(
|
||||
(Event message) => PangeaMessageEvent(
|
||||
event: message,
|
||||
timeline: timeline,
|
||||
ownMessage: false,
|
||||
),
|
||||
)
|
||||
.cast<PangeaMessageEvent>()
|
||||
.toList();
|
||||
return allPangeaMessages;
|
||||
}
|
||||
|
||||
String getSentText(PangeaMessageEvent message) =>
|
||||
message.originalSent?.text ?? message.body;
|
||||
|
||||
bool usageIsAvailable(PangeaMessageEvent message) {
|
||||
try {
|
||||
return message.originalSent?.choreo != null;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String getFilename(Room room, DownloadType type) {
|
||||
final String roomName = room
|
||||
.getLocalizedDisplayname()
|
||||
.trim()
|
||||
.replaceAll(RegExp(r'[^A-Za-z0-9\s]'), "")
|
||||
.replaceAll(RegExp(r'\s+'), "-");
|
||||
final String timestamp =
|
||||
DateFormat('yyyy-MM-dd-hh:mm:ss').format(DateTime.now());
|
||||
final String extension = type == DownloadType.txt
|
||||
? 'txt'
|
||||
: type == DownloadType.csv
|
||||
? 'csv'
|
||||
: 'xlsx';
|
||||
return "$roomName-$timestamp.$extension";
|
||||
}
|
||||
|
||||
String mimetype(DownloadType fileType) {
|
||||
switch (fileType) {
|
||||
case DownloadType.txt:
|
||||
return 'text/plain';
|
||||
case DownloadType.csv:
|
||||
return 'text/csv';
|
||||
case DownloadType.xlsx:
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
}
|
||||
|
||||
String getTxtContent(
|
||||
List<PangeaMessageEvent> messages,
|
||||
BuildContext context,
|
||||
String filename,
|
||||
Room room,
|
||||
) {
|
||||
String formattedInfo = "";
|
||||
for (final PangeaMessageEvent message in messages) {
|
||||
final String timestamp =
|
||||
DateFormat('yyyy-MM-dd hh:mm:ss').format(message.originServerTs);
|
||||
final String sender = message.senderId;
|
||||
final String originalMsg = message.originalWrittenContent;
|
||||
final String sentMsg = getSentText(message);
|
||||
final bool usageAvailable = usageIsAvailable(message);
|
||||
|
||||
if (!usageAvailable) {
|
||||
formattedInfo +=
|
||||
"${L10n.of(context).sender}: $sender\n${L10n.of(context).time}: $timestamp\n${L10n.of(context).originalMessage}: $originalMsg\n${L10n.of(context).sentMessage}: $sentMsg\n${L10n.of(context).useType}: ${L10n.of(context).notAvailable}\n\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
final bool includedIT = message.originalSent!.choreo!.includedIT;
|
||||
final bool includedIGC = message.originalSent!.choreo!.includedIGC;
|
||||
|
||||
formattedInfo +=
|
||||
"${L10n.of(context).sender}: $sender\n${L10n.of(context).time}: $timestamp\n${L10n.of(context).originalMessage}: $originalMsg\n${L10n.of(context).sentMessage}: $sentMsg\n${L10n.of(context).useType}: ";
|
||||
if (includedIT && includedIGC) {
|
||||
formattedInfo += L10n.of(context).taAndGaTooltip;
|
||||
} else if (includedIT) {
|
||||
formattedInfo += L10n.of(context).taTooltip;
|
||||
} else if (includedIGC) {
|
||||
formattedInfo += L10n.of(context).gaTooltip;
|
||||
} else {
|
||||
formattedInfo += L10n.of(context).waTooltip;
|
||||
}
|
||||
formattedInfo += "\n\n";
|
||||
}
|
||||
formattedInfo = "${room.getLocalizedDisplayname()}\n\n$formattedInfo";
|
||||
return formattedInfo;
|
||||
}
|
||||
|
||||
String getCSVContent(
|
||||
List<PangeaMessageEvent> messages,
|
||||
BuildContext context,
|
||||
String fileName,
|
||||
) {
|
||||
final List<List<String>> csvData = [
|
||||
[
|
||||
L10n.of(context).sender,
|
||||
L10n.of(context).time,
|
||||
L10n.of(context).originalMessage,
|
||||
L10n.of(context).sentMessage,
|
||||
L10n.of(context).taTooltip,
|
||||
L10n.of(context).gaTooltip,
|
||||
]
|
||||
];
|
||||
for (final PangeaMessageEvent message in messages) {
|
||||
final String timestamp =
|
||||
DateFormat('yyyy-MM-dd hh:mm:ss').format(message.originServerTs);
|
||||
final String sender = message.senderId;
|
||||
final String originalMsg = message.originalWrittenContent;
|
||||
final String sentMsg = getSentText(message);
|
||||
final bool usageAvailable = usageIsAvailable(message);
|
||||
|
||||
if (!usageAvailable) {
|
||||
csvData.add([
|
||||
sender,
|
||||
timestamp,
|
||||
originalMsg,
|
||||
sentMsg,
|
||||
L10n.of(context).notAvailable,
|
||||
L10n.of(context).notAvailable,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
final bool includedIT = message.originalSent!.choreo!.includedIT;
|
||||
final bool includedIGC = message.originalSent!.choreo!.includedIGC;
|
||||
|
||||
csvData.add([
|
||||
sender,
|
||||
timestamp,
|
||||
originalMsg,
|
||||
sentMsg,
|
||||
includedIT.toString(),
|
||||
includedIGC.toString(),
|
||||
]);
|
||||
}
|
||||
final String fileString = const ListToCsvConverter().convert(csvData);
|
||||
return fileString;
|
||||
}
|
||||
|
||||
List<int> getExcelContent(
|
||||
List<PangeaMessageEvent> messages,
|
||||
BuildContext context,
|
||||
String filename,
|
||||
) {
|
||||
final excel = Excel.createExcel();
|
||||
final Sheet sheetObject = excel['Sheet1'];
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 0))
|
||||
.value = TextCellValue(L10n.of(context).sender);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: 0))
|
||||
.value = TextCellValue(L10n.of(context).time);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: 0))
|
||||
.value = TextCellValue(L10n.of(context).originalMessage);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: 0))
|
||||
.value = TextCellValue(L10n.of(context).sentMessage);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: 0))
|
||||
.value = TextCellValue(L10n.of(context).taTooltip);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: 0))
|
||||
.value = TextCellValue(L10n.of(context).gaTooltip);
|
||||
|
||||
for (int i = 0; i < messages.length; i++) {
|
||||
final PangeaMessageEvent message = messages[i];
|
||||
final String sender = message.senderId;
|
||||
final String originalMsg = message.originalWrittenContent;
|
||||
final String sentMsg = getSentText(message);
|
||||
final bool usageAvailable = usageIsAvailable(message);
|
||||
|
||||
bool includedIT = false;
|
||||
bool includedIGC = false;
|
||||
|
||||
if (usageAvailable) {
|
||||
includedIT = message.originalSent!.choreo!.includedIT;
|
||||
includedIGC = message.originalSent!.choreo!.includedIGC;
|
||||
}
|
||||
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: i + 2))
|
||||
.value = TextCellValue(sender);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: i + 2))
|
||||
.value = DateTimeCellValue(
|
||||
year: message.originServerTs.year,
|
||||
month: message.originServerTs.month,
|
||||
day: message.originServerTs.day,
|
||||
hour: message.originServerTs.hour,
|
||||
minute: message.originServerTs.minute,
|
||||
second: message.originServerTs.second,
|
||||
);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: i + 2))
|
||||
.value = TextCellValue(originalMsg);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: i + 2))
|
||||
.value = TextCellValue(sentMsg);
|
||||
if (usageAvailable) {
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: i + 2))
|
||||
.value = TextCellValue(includedIT.toString());
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: i + 2))
|
||||
.value = TextCellValue(includedIGC.toString());
|
||||
}
|
||||
}
|
||||
|
||||
final List<int>? bytes = excel.encode();
|
||||
return bytes ?? [];
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:universal_html/html.dart' as webfile;
|
||||
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
||||
enum DownloadType { txt, csv, xlsx }
|
||||
|
||||
Future<void> downloadFile(
|
||||
dynamic contents,
|
||||
String filename,
|
||||
DownloadType fileType,
|
||||
) async {
|
||||
if (kIsWeb) {
|
||||
final blob = webfile.Blob([contents], mimetype(fileType), 'native');
|
||||
webfile.AnchorElement(
|
||||
href: webfile.Url.createObjectUrlFromBlob(blob).toString(),
|
||||
)
|
||||
..setAttribute("download", filename)
|
||||
..click();
|
||||
return;
|
||||
}
|
||||
if (await Permission.storage.request().isGranted) {
|
||||
Directory? directory;
|
||||
try {
|
||||
if (Platform.isIOS) {
|
||||
directory = await getApplicationDocumentsDirectory();
|
||||
} else {
|
||||
directory = Directory('/storage/emulated/0/Download');
|
||||
if (!await directory.exists()) {
|
||||
directory = await getExternalStorageDirectory();
|
||||
}
|
||||
}
|
||||
} catch (err, s) {
|
||||
debugPrint("Failed to get download folder path");
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
data: {},
|
||||
);
|
||||
}
|
||||
if (directory != null) {
|
||||
final File f = File("${directory.path}/$filename");
|
||||
File resp;
|
||||
if (fileType == DownloadType.txt || fileType == DownloadType.csv) {
|
||||
resp = await f.writeAsString(contents);
|
||||
} else {
|
||||
resp = await f.writeAsBytes(contents);
|
||||
}
|
||||
OpenFile.open(resp.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ class ConstructIdentifier {
|
|||
|
||||
@override
|
||||
int get hashCode {
|
||||
return lemma.hashCode ^ type.hashCode;
|
||||
return lemma.hashCode ^ type.hashCode ^ category.hashCode;
|
||||
}
|
||||
|
||||
String get string {
|
||||
|
|
|
|||
59
lib/pangea/download/download_file_util.dart
Normal file
59
lib/pangea/download/download_file_util.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:universal_html/html.dart' as webfile;
|
||||
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/download/download_type_enum.dart';
|
||||
|
||||
class DownloadUtil {
|
||||
static Future<void> downloadFile(
|
||||
dynamic contents,
|
||||
String filename,
|
||||
DownloadType fileType,
|
||||
) async {
|
||||
if (kIsWeb) {
|
||||
final blob = webfile.Blob([contents], fileType.mimetype, 'native');
|
||||
webfile.AnchorElement(
|
||||
href: webfile.Url.createObjectUrlFromBlob(blob).toString(),
|
||||
)
|
||||
..setAttribute("download", filename)
|
||||
..click();
|
||||
return;
|
||||
}
|
||||
if (await Permission.storage.request().isGranted) {
|
||||
Directory? directory;
|
||||
try {
|
||||
if (Platform.isIOS) {
|
||||
directory = await getApplicationDocumentsDirectory();
|
||||
} else {
|
||||
directory = Directory('/storage/emulated/0/Download');
|
||||
if (!await directory.exists()) {
|
||||
directory = await getExternalStorageDirectory();
|
||||
}
|
||||
}
|
||||
} catch (err, s) {
|
||||
debugPrint("Failed to get download folder path");
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
data: {},
|
||||
);
|
||||
}
|
||||
if (directory != null) {
|
||||
final File f = File("${directory.path}/$filename");
|
||||
File resp;
|
||||
if (fileType == DownloadType.txt || fileType == DownloadType.csv) {
|
||||
resp = await f.writeAsString(contents);
|
||||
} else {
|
||||
resp = await f.writeAsBytes(contents);
|
||||
}
|
||||
OpenFile.open(resp.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
241
lib/pangea/download/download_room_extension.dart
Normal file
241
lib/pangea/download/download_room_extension.dart
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
// ignore_for_file: implementation_imports
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:csv/csv.dart';
|
||||
import 'package:excel/excel.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/models/timeline_chunk.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/download/download_file_util.dart';
|
||||
import 'package:fluffychat/pangea/download/download_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
|
||||
class _EventDownloadInfo {
|
||||
final String sender;
|
||||
final DateTime timestamp;
|
||||
final String originalMsg;
|
||||
final String sentMsg;
|
||||
final bool usageAvailable;
|
||||
|
||||
final bool includedIT;
|
||||
final bool includedIGC;
|
||||
|
||||
_EventDownloadInfo({
|
||||
required this.sender,
|
||||
required this.timestamp,
|
||||
required this.originalMsg,
|
||||
required this.sentMsg,
|
||||
required this.usageAvailable,
|
||||
required this.includedIT,
|
||||
required this.includedIGC,
|
||||
});
|
||||
}
|
||||
|
||||
class EmptyChatException implements Exception {}
|
||||
|
||||
extension DownloadExtension on Room {
|
||||
Future<void> download(
|
||||
DownloadType type,
|
||||
BuildContext context,
|
||||
) async {
|
||||
List<PangeaMessageEvent> allPangeaMessages;
|
||||
|
||||
final List<Event> allEvents = await getAllEvents();
|
||||
final TimelineChunk chunk = TimelineChunk(events: allEvents);
|
||||
final Timeline timeline = Timeline(
|
||||
room: this,
|
||||
chunk: chunk,
|
||||
);
|
||||
|
||||
allPangeaMessages = getPangeaMessageEvents(
|
||||
allEvents,
|
||||
timeline,
|
||||
);
|
||||
|
||||
if (allPangeaMessages.isEmpty) {
|
||||
throw EmptyChatException();
|
||||
}
|
||||
|
||||
dynamic content;
|
||||
final List<_EventDownloadInfo> eventInfo = allPangeaMessages.map((message) {
|
||||
return _EventDownloadInfo(
|
||||
sender: message.senderId,
|
||||
timestamp: message.originServerTs,
|
||||
originalMsg: message.originalWrittenContent,
|
||||
sentMsg: message.originalSent?.text ?? message.body,
|
||||
usageAvailable: message.originalSent?.choreo != null,
|
||||
includedIT: message.originalSent?.choreo?.includedIT ?? false,
|
||||
includedIGC: message.originalSent?.choreo?.includedIGC ?? false,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
switch (type) {
|
||||
case DownloadType.txt:
|
||||
content = _getTxtContent(eventInfo, context);
|
||||
case DownloadType.csv:
|
||||
content = _getCSVContent(eventInfo, context);
|
||||
case DownloadType.xlsx:
|
||||
content = _getExcelContent(eventInfo, context);
|
||||
}
|
||||
DownloadUtil.downloadFile(content, _getFilename(type), type);
|
||||
}
|
||||
|
||||
String _getFilename(DownloadType type) {
|
||||
final String roomName = getLocalizedDisplayname()
|
||||
.trim()
|
||||
.replaceAll(RegExp(r'[^A-Za-z0-9\s]'), "")
|
||||
.replaceAll(RegExp(r'\s+'), "-");
|
||||
|
||||
final String timestamp =
|
||||
DateFormat('yyyy-MM-dd-hh:mm:ss').format(DateTime.now());
|
||||
|
||||
return "$roomName-$timestamp.${type.extension}";
|
||||
}
|
||||
|
||||
String _getTxtContent(
|
||||
List<_EventDownloadInfo> eventInfo,
|
||||
BuildContext context,
|
||||
) {
|
||||
String formattedInfo = "";
|
||||
final l10n = L10n.of(context);
|
||||
for (final _EventDownloadInfo info in eventInfo) {
|
||||
final String timestamp =
|
||||
DateFormat('yyyy-MM-dd hh:mm:ss').format(info.timestamp);
|
||||
|
||||
if (!info.usageAvailable) {
|
||||
formattedInfo +=
|
||||
"${l10n.sender}: ${info.sender}\n${l10n.time}: $timestamp\n${l10n.originalMessage}: ${info.originalMsg}\n${l10n.sentMessage}: ${info.sentMsg}\n${l10n.useType}: ${l10n.notAvailable}\n\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
formattedInfo +=
|
||||
"${l10n.sender}: ${info.sender}\n${l10n.time}: $timestamp\n${l10n.originalMessage}: ${info.originalMsg}\n${l10n.sentMessage}: ${info.sentMsg}\n${l10n.useType}: ";
|
||||
|
||||
if (info.includedIT && info.includedIGC) {
|
||||
formattedInfo += l10n.taAndGaTooltip;
|
||||
} else if (info.includedIT) {
|
||||
formattedInfo += l10n.taTooltip;
|
||||
} else if (info.includedIGC) {
|
||||
formattedInfo += l10n.gaTooltip;
|
||||
} else {
|
||||
formattedInfo += l10n.waTooltip;
|
||||
}
|
||||
formattedInfo += "\n\n";
|
||||
}
|
||||
formattedInfo = "${getLocalizedDisplayname()}\n\n$formattedInfo";
|
||||
return formattedInfo;
|
||||
}
|
||||
|
||||
String _getCSVContent(
|
||||
List<_EventDownloadInfo> eventInfo,
|
||||
BuildContext context,
|
||||
) {
|
||||
final l10n = L10n.of(context);
|
||||
final List<List<String>> csvData = [
|
||||
[
|
||||
l10n.sender,
|
||||
l10n.time,
|
||||
l10n.originalMessage,
|
||||
l10n.sentMessage,
|
||||
l10n.taTooltip,
|
||||
l10n.gaTooltip,
|
||||
]
|
||||
];
|
||||
for (final _EventDownloadInfo info in eventInfo) {
|
||||
final String timestamp =
|
||||
DateFormat('yyyy-MM-dd hh:mm:ss').format(info.timestamp);
|
||||
|
||||
if (!info.usageAvailable) {
|
||||
csvData.add([
|
||||
info.sender,
|
||||
timestamp,
|
||||
info.originalMsg,
|
||||
info.sentMsg,
|
||||
l10n.notAvailable,
|
||||
l10n.notAvailable,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
csvData.add([
|
||||
info.sender,
|
||||
timestamp,
|
||||
info.originalMsg,
|
||||
info.sentMsg,
|
||||
info.includedIT.toString(),
|
||||
info.includedIGC.toString(),
|
||||
]);
|
||||
}
|
||||
final String fileString = const ListToCsvConverter().convert(csvData);
|
||||
return fileString;
|
||||
}
|
||||
|
||||
List<int> _getExcelContent(
|
||||
List<_EventDownloadInfo> eventInfo,
|
||||
BuildContext context,
|
||||
) {
|
||||
final l10n = L10n.of(context);
|
||||
final excel = Excel.createExcel();
|
||||
final Sheet sheetObject = excel['Sheet1'];
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 0))
|
||||
.value = TextCellValue(l10n.sender);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: 0))
|
||||
.value = TextCellValue(l10n.time);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: 0))
|
||||
.value = TextCellValue(l10n.originalMessage);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: 0))
|
||||
.value = TextCellValue(l10n.sentMessage);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: 0))
|
||||
.value = TextCellValue(l10n.taTooltip);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: 0))
|
||||
.value = TextCellValue(l10n.gaTooltip);
|
||||
|
||||
for (int i = 0; i < eventInfo.length; i++) {
|
||||
final _EventDownloadInfo info = eventInfo[i];
|
||||
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: i + 2))
|
||||
.value = TextCellValue(info.sender);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: i + 2))
|
||||
.value = DateTimeCellValue(
|
||||
year: info.timestamp.year,
|
||||
month: info.timestamp.month,
|
||||
day: info.timestamp.day,
|
||||
hour: info.timestamp.hour,
|
||||
minute: info.timestamp.minute,
|
||||
second: info.timestamp.second,
|
||||
);
|
||||
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: i + 2))
|
||||
.value = TextCellValue(info.originalMsg);
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: i + 2))
|
||||
.value = TextCellValue(info.sentMsg);
|
||||
if (info.usageAvailable) {
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: i + 2))
|
||||
.value = TextCellValue(info.includedIT.toString());
|
||||
sheetObject
|
||||
.cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: i + 2))
|
||||
.value = TextCellValue(info.includedIGC.toString());
|
||||
}
|
||||
}
|
||||
|
||||
final List<int>? bytes = excel.encode();
|
||||
return bytes ?? [];
|
||||
}
|
||||
}
|
||||
27
lib/pangea/download/download_type_enum.dart
Normal file
27
lib/pangea/download/download_type_enum.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
enum DownloadType {
|
||||
txt,
|
||||
csv,
|
||||
xlsx;
|
||||
|
||||
String get mimetype {
|
||||
switch (this) {
|
||||
case DownloadType.txt:
|
||||
return 'text/plain';
|
||||
case DownloadType.csv:
|
||||
return 'text/csv';
|
||||
case DownloadType.xlsx:
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
}
|
||||
|
||||
String get extension {
|
||||
switch (this) {
|
||||
case DownloadType.txt:
|
||||
return 'txt';
|
||||
case DownloadType.csv:
|
||||
return 'csv';
|
||||
case DownloadType.xlsx:
|
||||
return 'xlsx';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:matrix/src/utils/markdown.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/event_wrappers/pangea_choreo_event.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/language_detection_model.dart';
|
||||
|
|
@ -336,4 +337,22 @@ class RepresentationEvent {
|
|||
.toList() ??
|
||||
[];
|
||||
}
|
||||
|
||||
List<OneConstructUse> vocabAndMorphUses() {
|
||||
if (tokens == null || tokens!.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final metadata = ConstructUseMetaData(
|
||||
roomId: parentMessageEvent.room.id,
|
||||
timeStamp: parentMessageEvent.originServerTs,
|
||||
eventId: parentMessageEvent.eventId,
|
||||
);
|
||||
|
||||
return content.vocabAndMorphUses(
|
||||
tokens: tokens!,
|
||||
metadata: metadata,
|
||||
choreo: choreo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
|||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/speech_to_text_models.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
/// this class is contained within a [RepresentationEvent]
|
||||
/// this event is the child of a [EventTypes.Message]
|
||||
|
|
@ -105,8 +104,6 @@ class PangeaRepresentation {
|
|||
ChoreoRecord? choreo,
|
||||
}) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
final l2 = MatrixState.pangeaController.languageController.userL2;
|
||||
if (langCode.split("-")[0] != l2?.langCodeShort) return uses;
|
||||
|
||||
// missing vital info so return
|
||||
if (event?.roomId == null && metadata?.roomId == null) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart
|
|||
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/extensions/join_rule_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
|
|
|
|||
|
|
@ -321,4 +321,55 @@ extension EventsRoomExtension on Room {
|
|||
|
||||
return resp.chunk.map((e) => Event.fromMatrixEvent(e, this)).toList();
|
||||
}
|
||||
|
||||
Future<List<Event>> getAllEvents() async {
|
||||
final GetRoomEventsResponse initalResp =
|
||||
await client.getRoomEvents(id, Direction.b);
|
||||
|
||||
if (initalResp.end == null) return [];
|
||||
String? nextStartToken = initalResp.end;
|
||||
List<MatrixEvent> allMatrixEvents = initalResp.chunk;
|
||||
while (nextStartToken != null) {
|
||||
final GetRoomEventsResponse resp = await client.getRoomEvents(
|
||||
id,
|
||||
Direction.b,
|
||||
from: nextStartToken,
|
||||
);
|
||||
final chunkMessages = resp.chunk;
|
||||
allMatrixEvents.addAll(chunkMessages);
|
||||
resp.end != nextStartToken
|
||||
? nextStartToken = resp.end
|
||||
: nextStartToken = null;
|
||||
}
|
||||
|
||||
allMatrixEvents = allMatrixEvents.reversed.toList();
|
||||
final List<Event> allEvents = allMatrixEvents
|
||||
.map((MatrixEvent message) => Event.fromMatrixEvent(message, this))
|
||||
.toList();
|
||||
|
||||
return allEvents;
|
||||
}
|
||||
|
||||
List<PangeaMessageEvent> getPangeaMessageEvents(
|
||||
List<Event> events,
|
||||
Timeline timeline,
|
||||
) {
|
||||
final List<PangeaMessageEvent> allPangeaMessages = events
|
||||
.where(
|
||||
(Event event) =>
|
||||
event.type == EventTypes.Message &&
|
||||
event.content['msgtype'] == MessageTypes.Text,
|
||||
)
|
||||
.map(
|
||||
(Event message) => PangeaMessageEvent(
|
||||
event: message,
|
||||
timeline: timeline,
|
||||
ownMessage: client.userID == message.senderId,
|
||||
),
|
||||
)
|
||||
.cast<PangeaMessageEvent>()
|
||||
.toList();
|
||||
|
||||
return allPangeaMessages;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import 'package:fluffychat/pangea/analytics_downloads/space_analytics_summary_mo
|
|||
import 'package:fluffychat/pangea/analytics_misc/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/download/download_file_util.dart';
|
||||
import 'package:fluffychat/pangea/download/download_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -143,7 +144,7 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
|
|||
final fileName =
|
||||
"analytics_${widget.space.name}_${DateTime.now().toIso8601String()}.${_downloadType == DownloadType.xlsx ? 'xlsx' : 'csv'}";
|
||||
|
||||
await downloadFile(
|
||||
await DownloadUtil.downloadFile(
|
||||
content,
|
||||
fileName,
|
||||
DownloadType.csv,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue