added learning summary to chat list, removed references to summary analytics
This commit is contained in:
parent
ba6d395f32
commit
c5187c7639
35 changed files with 2145 additions and 2587 deletions
|
|
@ -4113,5 +4113,8 @@
|
|||
"createSpace": "Create space",
|
||||
"createChat": "Create chat",
|
||||
"error520Title": "Please try again.",
|
||||
"error520Desc": "Sorry, we could not understand your message..."
|
||||
"error520Desc": "Sorry, we could not understand your message...",
|
||||
"wordsUsed": "Words Used",
|
||||
"errorTypes": "Error Types",
|
||||
"level": "Level"
|
||||
}
|
||||
|
|
@ -26,9 +26,7 @@ import 'package:fluffychat/pages/settings_notifications/settings_notifications.d
|
|||
import 'package:fluffychat/pages/settings_password/settings_password.dart';
|
||||
import 'package:fluffychat/pages/settings_security/settings_security.dart';
|
||||
import 'package:fluffychat/pages/settings_style/settings_style.dart';
|
||||
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
import 'package:fluffychat/pangea/guard/p_vguard.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart';
|
||||
import 'package:fluffychat/pangea/pages/find_partner/find_partner.dart';
|
||||
import 'package:fluffychat/pangea/pages/p_user_age/p_user_age.dart';
|
||||
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
|
||||
|
|
@ -162,17 +160,17 @@ abstract class AppRoutes {
|
|||
),
|
||||
routes: [
|
||||
// #Pangea
|
||||
GoRoute(
|
||||
path: 'mylearning',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const StudentAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.messages,
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
),
|
||||
// GoRoute(
|
||||
// path: 'mylearning',
|
||||
// pageBuilder: (context, state) => defaultPageBuilder(
|
||||
// context,
|
||||
// state,
|
||||
// const StudentAnalyticsPage(
|
||||
// selectedView: BarChartViewSelection.messages,
|
||||
// ),
|
||||
// ),
|
||||
// redirect: loggedOutRedirect,
|
||||
// ),
|
||||
// GoRoute(
|
||||
// path: 'analytics',
|
||||
// pageBuilder: (context, state) => defaultPageBuilder(
|
||||
|
|
|
|||
|
|
@ -475,7 +475,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// Pangea#
|
||||
if (kIsWeb && !Matrix.of(context).webHasFocus) return;
|
||||
// #Pangea
|
||||
} catch (err, s) {
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
// Pangea#
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
|||
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||
import 'package:fluffychat/pages/chat_list/space_view.dart';
|
||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/chat_list_body_text.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/chat_list_header_wrapper.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/chat_list_item_wrapper.dart';
|
||||
|
|
@ -164,6 +165,9 @@ class ChatListViewBody extends StatelessWidget {
|
|||
title: L10n.of(context)!.chats,
|
||||
icon: const Icon(Icons.forum_outlined),
|
||||
),
|
||||
// #Pangea
|
||||
const LearningProgressIndicators(),
|
||||
// Pangea#
|
||||
if (client.prevBatch != null &&
|
||||
rooms.isEmpty &&
|
||||
!controller.isSearchMode) ...[
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart';
|
||||
import 'package:fluffychat/pangea/utils/logout.dart';
|
||||
import 'package:fluffychat/pangea/utils/space_code.dart';
|
||||
|
|
@ -67,19 +66,19 @@ class ClientChooserButton extends StatelessWidget {
|
|||
// ],
|
||||
// ),
|
||||
// ),
|
||||
PopupMenuItem(
|
||||
enabled: matrix.client.rooms.any(
|
||||
(room) => !room.isSpace && !room.isArchived && !room.isAnalyticsRoom,
|
||||
),
|
||||
value: SettingsAction.myAnalytics,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.analytics_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Expanded(child: Text(L10n.of(context)!.myLearning)),
|
||||
],
|
||||
),
|
||||
),
|
||||
// PopupMenuItem(
|
||||
// enabled: matrix.client.rooms.any(
|
||||
// (room) => !room.isSpace && !room.isArchived && !room.isAnalyticsRoom,
|
||||
// ),
|
||||
// value: SettingsAction.myAnalytics,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// const Icon(Icons.analytics_outlined),
|
||||
// const SizedBox(width: 18),
|
||||
// Expanded(child: Text(L10n.of(context)!.myLearning)),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.newClass,
|
||||
child: Row(
|
||||
|
|
@ -404,9 +403,9 @@ class ClientChooserButton extends StatelessWidget {
|
|||
// case SettingsAction.spaceAnalytics:
|
||||
// context.go('/rooms/analytics');
|
||||
// break;
|
||||
case SettingsAction.myAnalytics:
|
||||
context.go('/rooms/mylearning');
|
||||
break;
|
||||
// case SettingsAction.myAnalytics:
|
||||
// context.go('/rooms/mylearning');
|
||||
// break;
|
||||
case SettingsAction.logout:
|
||||
pLogoutAction(context);
|
||||
break;
|
||||
|
|
@ -497,7 +496,7 @@ enum SettingsAction {
|
|||
learning,
|
||||
joinWithClassCode,
|
||||
// spaceAnalytics,
|
||||
myAnalytics,
|
||||
// myAnalytics,
|
||||
findAConversationPartner,
|
||||
logout,
|
||||
newClass,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,9 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -20,89 +13,87 @@ import 'package:sentry_flutter/sentry_flutter.dart';
|
|||
import '../constants/class_default_values.dart';
|
||||
import '../extensions/client_extension/client_extension.dart';
|
||||
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import '../models/analytics/chart_analytics_model.dart';
|
||||
import 'base_controller.dart';
|
||||
import 'pangea_controller.dart';
|
||||
|
||||
// controls the fetching of analytics data
|
||||
class AnalyticsController extends BaseController {
|
||||
late PangeaController _pangeaController;
|
||||
|
||||
final List<AnalyticsCacheModel> _cachedAnalyticsModels = [];
|
||||
final List<ConstructCacheEntry> _cachedConstructs = [];
|
||||
|
||||
AnalyticsController(PangeaController pangeaController) : super() {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
///////// TIME SPANS //////////
|
||||
String get _analyticsTimeSpanKey => "ANALYTICS_TIME_SPAN_KEY";
|
||||
String get langCode =>
|
||||
_pangeaController.languageController.userL2?.langCode ??
|
||||
_pangeaController.pLanguageStore.targetOptions.first.langCode;
|
||||
|
||||
TimeSpan get currentAnalyticsTimeSpan {
|
||||
try {
|
||||
final String? str = _pangeaController.pStoreService.read(
|
||||
_analyticsTimeSpanKey,
|
||||
);
|
||||
return str != null
|
||||
? TimeSpan.values.firstWhere((e) {
|
||||
final spanString = e.toString();
|
||||
return spanString == str;
|
||||
})
|
||||
: ClassDefaultValues.defaultTimeSpan;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
return ClassDefaultValues.defaultTimeSpan;
|
||||
}
|
||||
}
|
||||
// String get _analyticsTimeSpanKey => "ANALYTICS_TIME_SPAN_KEY";
|
||||
|
||||
Future<void> setCurrentAnalyticsTimeSpan(TimeSpan timeSpan) async {
|
||||
await _pangeaController.pStoreService.save(
|
||||
_analyticsTimeSpanKey,
|
||||
timeSpan.toString(),
|
||||
);
|
||||
setState();
|
||||
}
|
||||
// TimeSpan get currentAnalyticsTimeSpan {
|
||||
// try {
|
||||
// final String? str = _pangeaController.pStoreService.read(
|
||||
// _analyticsTimeSpanKey,
|
||||
// );
|
||||
// return str != null
|
||||
// ? TimeSpan.values.firstWhere((e) {
|
||||
// final spanString = e.toString();
|
||||
// return spanString == str;
|
||||
// })
|
||||
// : ClassDefaultValues.defaultTimeSpan;
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// return ClassDefaultValues.defaultTimeSpan;
|
||||
// }
|
||||
// }
|
||||
|
||||
///////// SPACE ANALYTICS LANGUAGES //////////
|
||||
String get _analyticsSpaceLangKey => "ANALYTICS_SPACE_LANG_KEY";
|
||||
// Future<void> setCurrentAnalyticsTimeSpan(TimeSpan timeSpan) async {
|
||||
// await _pangeaController.pStoreService.save(
|
||||
// _analyticsTimeSpanKey,
|
||||
// timeSpan.toString(),
|
||||
// );
|
||||
// setState();
|
||||
// }
|
||||
|
||||
LanguageModel get currentAnalyticsLang {
|
||||
try {
|
||||
final String? str = _pangeaController.pStoreService.read(
|
||||
_analyticsSpaceLangKey,
|
||||
);
|
||||
return str != null
|
||||
? PangeaLanguage.byLangCode(str)
|
||||
: _pangeaController.languageController.userL2 ??
|
||||
_pangeaController.pLanguageStore.targetOptions.first;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
return _pangeaController.pLanguageStore.targetOptions.first;
|
||||
}
|
||||
}
|
||||
// String get _analyticsSpaceLangKey => "ANALYTICS_SPACE_LANG_KEY";
|
||||
|
||||
Future<void> setCurrentAnalyticsLang(LanguageModel lang) async {
|
||||
await _pangeaController.pStoreService.save(
|
||||
_analyticsSpaceLangKey,
|
||||
lang.langCode,
|
||||
);
|
||||
setState();
|
||||
}
|
||||
// LanguageModel get currentAnalyticsLang {
|
||||
// try {
|
||||
// final String? str = _pangeaController.pStoreService.read(
|
||||
// _analyticsSpaceLangKey,
|
||||
// );
|
||||
// return str != null
|
||||
// ? PangeaLanguage.byLangCode(str)
|
||||
// : _pangeaController.languageController.userL2 ??
|
||||
// _pangeaController.pLanguageStore.targetOptions.first;
|
||||
// } catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
// return _pangeaController.pLanguageStore.targetOptions.first;
|
||||
// }
|
||||
// }
|
||||
|
||||
/// given an analytics event type and the current analytics language,
|
||||
/// get the last time the user updated their analytics
|
||||
Future<DateTime?> myAnalyticsLastUpdated(String type) async {
|
||||
final List<Room> analyticsRooms = _pangeaController
|
||||
.matrixState.client.allMyAnalyticsRooms
|
||||
.where((room) => room.isAnalyticsRoom)
|
||||
.toList();
|
||||
// Future<void> setCurrentAnalyticsLang(LanguageModel lang) async {
|
||||
// await _pangeaController.pStoreService.save(
|
||||
// _analyticsSpaceLangKey,
|
||||
// lang.langCode,
|
||||
// );
|
||||
// setState();
|
||||
// }
|
||||
|
||||
/// Get the last time the user updated their analytics.
|
||||
/// Tries to get the last time the user updated analytics for their current L2.
|
||||
/// If there isn't yet an analytics room reacted for their L2, checks if the
|
||||
/// user has any other analytics rooms and returns the most recent update time.
|
||||
Future<DateTime?> myAnalyticsLastUpdated() async {
|
||||
final List<Room> analyticsRooms =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
|
||||
final Map<String, DateTime> langCodeLastUpdates = {};
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
final String? roomLang = analyticsRoom.madeForLang;
|
||||
if (roomLang == null) continue;
|
||||
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
|
||||
type,
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
);
|
||||
if (lastUpdated != null) {
|
||||
|
|
@ -121,25 +112,20 @@ class AnalyticsController extends BaseController {
|
|||
);
|
||||
}
|
||||
|
||||
/// check if any students have recently updated their analytics
|
||||
/// if any have, then the cache needs to be updated
|
||||
Future<DateTime?> spaceAnalyticsLastUpdated(
|
||||
String type,
|
||||
Room space,
|
||||
) async {
|
||||
// check if any students have recently updated their analytics
|
||||
// if any have, then the cache needs to be updated
|
||||
// TODO - figure out how to do this on a per-student basis
|
||||
await space.requestParticipants();
|
||||
|
||||
final List<Future<DateTime?>> lastUpdatedFutures = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
|
||||
.analyticsRoomLocal(langCode, student.id);
|
||||
if (analyticsRoom == null) continue;
|
||||
lastUpdatedFutures.add(
|
||||
analyticsRoom.analyticsLastUpdated(
|
||||
type,
|
||||
student.id,
|
||||
),
|
||||
analyticsRoom.analyticsLastUpdated(student.id),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -155,372 +141,16 @@ class AnalyticsController extends BaseController {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Map of space ids to the last fetched hierarchy. Used when filtering
|
||||
// private chat analytics to determine which children are already visible
|
||||
// in the chat list
|
||||
final Map<String, List<String>> _lastFetchedHierarchies = {};
|
||||
|
||||
void setLatestHierarchy(String spaceId, GetSpaceHierarchyResponse resp) {
|
||||
final List<String> roomIds = resp.rooms.map((room) => room.roomId).toList();
|
||||
_lastFetchedHierarchies[spaceId] = roomIds;
|
||||
}
|
||||
|
||||
Future<List<String>> getLatestSpaceHierarchy(String spaceId) async {
|
||||
if (!_lastFetchedHierarchies.containsKey(spaceId)) {
|
||||
final resp =
|
||||
await _pangeaController.matrixState.client.getSpaceHierarchy(spaceId);
|
||||
setLatestHierarchy(spaceId, resp);
|
||||
}
|
||||
return _lastFetchedHierarchies[spaceId] ?? [];
|
||||
}
|
||||
|
||||
//////////////////////////// MESSAGE SUMMARY ANALYTICS ////////////////////////////
|
||||
|
||||
/// get all the summary analytics events for the current user
|
||||
/// in the current language's analytics room
|
||||
Future<List<SummaryAnalyticsEvent>> mySummaryAnalytics() async {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode);
|
||||
if (analyticsRoom == null) return [];
|
||||
|
||||
final List<AnalyticsEvent>? roomEvents =
|
||||
await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
);
|
||||
return roomEvents?.cast<SummaryAnalyticsEvent>() ?? [];
|
||||
}
|
||||
|
||||
Future<List<SummaryAnalyticsEvent>> spaceMemberAnalytics(
|
||||
Room space,
|
||||
Future<List<ConstructAnalyticsEvent>> allMyConstructs(
|
||||
TimeSpan timeSpan,
|
||||
) async {
|
||||
// gets all the summary analytics events for the students
|
||||
// in a space since the current timespace's cut off date
|
||||
|
||||
// ensure that the participants of the space are loaded
|
||||
await space.requestParticipants();
|
||||
|
||||
// TODO switch to using list of futures
|
||||
final List<SummaryAnalyticsEvent> analyticsEvents = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
|
||||
|
||||
if (analyticsRoom != null) {
|
||||
final List<AnalyticsEvent>? roomEvents =
|
||||
await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
userId: student.id,
|
||||
);
|
||||
analyticsEvents.addAll(
|
||||
roomEvents?.cast<SummaryAnalyticsEvent>() ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final List<String> spaceChildrenIds = space.allSpaceChildRoomIds;
|
||||
|
||||
// filter out the analyics events that don't belong to the space's children
|
||||
final List<SummaryAnalyticsEvent> allAnalyticsEvents = [];
|
||||
for (final analyticsEvent in analyticsEvents) {
|
||||
analyticsEvent.content.messages.removeWhere(
|
||||
(msg) => !spaceChildrenIds.contains(msg.chatId),
|
||||
);
|
||||
allAnalyticsEvents.add(analyticsEvent);
|
||||
}
|
||||
|
||||
return allAnalyticsEvents;
|
||||
}
|
||||
|
||||
ChartAnalyticsModel? getAnalyticsLocal({
|
||||
TimeSpan? timeSpan,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
bool forceUpdate = false,
|
||||
bool updateExpired = false,
|
||||
DateTime? lastUpdated,
|
||||
}) {
|
||||
timeSpan ??= currentAnalyticsTimeSpan;
|
||||
final int index = _cachedAnalyticsModels.indexWhere(
|
||||
(e) =>
|
||||
(e.timeSpan == timeSpan) &&
|
||||
(e.defaultSelected.id == defaultSelected.id) &&
|
||||
(e.defaultSelected.type == defaultSelected.type) &&
|
||||
(e.selected?.id == selected?.id) &&
|
||||
(e.selected?.type == selected?.type) &&
|
||||
(e.langCode == currentAnalyticsLang.langCode),
|
||||
);
|
||||
|
||||
if (index != -1) {
|
||||
if ((updateExpired && _cachedAnalyticsModels[index].isExpired) ||
|
||||
forceUpdate ||
|
||||
_cachedAnalyticsModels[index].needsUpdate(lastUpdated)) {
|
||||
_cachedAnalyticsModels.removeAt(index);
|
||||
} else {
|
||||
return _cachedAnalyticsModels[index].chartAnalyticsModel;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void cacheAnalytics({
|
||||
required ChartAnalyticsModel chartAnalyticsModel,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
TimeSpan? timeSpan,
|
||||
}) {
|
||||
_cachedAnalyticsModels.add(
|
||||
AnalyticsCacheModel(
|
||||
timeSpan: timeSpan ?? currentAnalyticsTimeSpan,
|
||||
chartAnalyticsModel: chartAnalyticsModel,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
langCode: currentAnalyticsLang.langCode,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<SummaryAnalyticsEvent> filterStudentAnalytics(
|
||||
List<SummaryAnalyticsEvent> unfiltered,
|
||||
String? studentId,
|
||||
) {
|
||||
final List<SummaryAnalyticsEvent> filtered =
|
||||
List<SummaryAnalyticsEvent>.from(unfiltered);
|
||||
filtered.removeWhere((e) => e.event.senderId != studentId);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Future<List<SummaryAnalyticsEvent>> filterRoomAnalytics(
|
||||
List<SummaryAnalyticsEvent> unfiltered,
|
||||
String? roomID,
|
||||
) async {
|
||||
List<SummaryAnalyticsEvent> filtered = [...unfiltered];
|
||||
Room? room;
|
||||
if (roomID != null) {
|
||||
room = _pangeaController.matrixState.client.getRoomById(roomID);
|
||||
if (room?.isSpace == true) {
|
||||
return await filterSpaceAnalytics(unfiltered, roomID);
|
||||
}
|
||||
}
|
||||
|
||||
filtered = filtered
|
||||
.where(
|
||||
(e) => (e.content).messages.any((u) => u.chatId == roomID),
|
||||
)
|
||||
.toList();
|
||||
filtered.forEachIndexed(
|
||||
(i, _) => (filtered[i].content).messages.removeWhere(
|
||||
(u) => u.chatId != roomID,
|
||||
),
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Future<List<SummaryAnalyticsEvent>> filterPrivateChatAnalytics(
|
||||
List<SummaryAnalyticsEvent> unfiltered,
|
||||
Room space,
|
||||
) async {
|
||||
final List<String> privateChatIds = space.allSpaceChildRoomIds;
|
||||
final List<String> lastFetched = await getLatestSpaceHierarchy(space.id);
|
||||
for (final id in lastFetched) {
|
||||
privateChatIds.removeWhere((e) => e == id);
|
||||
}
|
||||
|
||||
List<SummaryAnalyticsEvent> filtered =
|
||||
List<SummaryAnalyticsEvent>.from(unfiltered);
|
||||
filtered = filtered.where((e) {
|
||||
return (e.content).messages.any(
|
||||
(u) => privateChatIds.contains(u.chatId),
|
||||
);
|
||||
}).toList();
|
||||
filtered.forEachIndexed(
|
||||
(i, _) => (filtered[i].content).messages.removeWhere(
|
||||
(u) => !privateChatIds.contains(u.chatId),
|
||||
),
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Future<List<SummaryAnalyticsEvent>> filterSpaceAnalytics(
|
||||
List<SummaryAnalyticsEvent> unfiltered,
|
||||
String spaceId,
|
||||
) async {
|
||||
final List<String> chatIds = await getLatestSpaceHierarchy(spaceId);
|
||||
List<SummaryAnalyticsEvent> filtered =
|
||||
List<SummaryAnalyticsEvent>.from(unfiltered);
|
||||
|
||||
filtered = filtered
|
||||
.where(
|
||||
(e) => e.content.messages.any((u) => chatIds.contains(u.chatId)),
|
||||
)
|
||||
.toList();
|
||||
|
||||
filtered.forEachIndexed(
|
||||
(i, _) => (filtered[i].content).messages.removeWhere(
|
||||
(u) => !chatIds.contains(u.chatId),
|
||||
),
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Future<List<SummaryAnalyticsEvent>> filterAnalytics({
|
||||
required List<SummaryAnalyticsEvent> unfilteredAnalytics,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
Room? space,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
for (int i = 0; i < unfilteredAnalytics.length; i++) {
|
||||
unfilteredAnalytics[i].content.messages.removeWhere(
|
||||
(record) => record.time.isBefore(
|
||||
currentAnalyticsTimeSpan.cutOffDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
switch (selected?.type) {
|
||||
case null:
|
||||
return unfilteredAnalytics;
|
||||
case AnalyticsEntryType.student:
|
||||
if (defaultSelected.type != AnalyticsEntryType.space) {
|
||||
throw Exception(
|
||||
"student filtering not available for default filter ${defaultSelected.type}",
|
||||
);
|
||||
}
|
||||
return filterStudentAnalytics(unfilteredAnalytics, selected?.id);
|
||||
case AnalyticsEntryType.room:
|
||||
return filterRoomAnalytics(unfilteredAnalytics, selected?.id);
|
||||
case AnalyticsEntryType.privateChats:
|
||||
if (defaultSelected.type == AnalyticsEntryType.student) {
|
||||
throw "private chat filtering not available for my analytics";
|
||||
}
|
||||
if (space == null) {
|
||||
throw "space is null in filterAnalytics with selected type privateChats";
|
||||
}
|
||||
return await filterPrivateChatAnalytics(
|
||||
unfilteredAnalytics,
|
||||
space,
|
||||
);
|
||||
case AnalyticsEntryType.space:
|
||||
return await filterSpaceAnalytics(unfilteredAnalytics, selected!.id);
|
||||
default:
|
||||
throw Exception("invalid filter type - ${selected?.type}");
|
||||
}
|
||||
}
|
||||
|
||||
Future<ChartAnalyticsModel> getAnalytics({
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
bool forceUpdate = false,
|
||||
}) async {
|
||||
try {
|
||||
await _pangeaController.matrixState.client.roomsLoading;
|
||||
|
||||
// if the user is looking at space analytics, then fetch the space
|
||||
Room? space;
|
||||
if (defaultSelected.type == AnalyticsEntryType.space) {
|
||||
space = _pangeaController.matrixState.client.getRoomById(
|
||||
defaultSelected.id,
|
||||
);
|
||||
if (space == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "space not found in getAnalytics",
|
||||
data: {
|
||||
"defaultSelected": defaultSelected,
|
||||
"selected": selected,
|
||||
},
|
||||
);
|
||||
return ChartAnalyticsModel(
|
||||
msgs: [],
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? lastUpdated;
|
||||
if (defaultSelected.type != AnalyticsEntryType.space) {
|
||||
// if default selected view is my analytics, check for the last
|
||||
// time the logged in user updated their analytics events
|
||||
// this gets passed to getAnalyticsLocal to determine if the cached
|
||||
// entry is out-of-date
|
||||
lastUpdated = await myAnalyticsLastUpdated(
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
);
|
||||
} else {
|
||||
// else, get the last time a student in the space updated their analytics
|
||||
lastUpdated = await spaceAnalyticsLastUpdated(
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
space!,
|
||||
);
|
||||
}
|
||||
|
||||
final ChartAnalyticsModel? local = getAnalyticsLocal(
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
forceUpdate: forceUpdate,
|
||||
lastUpdated: lastUpdated,
|
||||
);
|
||||
if (local != null && !forceUpdate) {
|
||||
debugPrint("returning local analytics");
|
||||
return local;
|
||||
}
|
||||
debugPrint("fetching new analytics");
|
||||
|
||||
// get all the relevant summary analytics events for the current timespan
|
||||
final List<SummaryAnalyticsEvent> summaryEvents =
|
||||
defaultSelected.type == AnalyticsEntryType.space
|
||||
? await spaceMemberAnalytics(space!)
|
||||
: await mySummaryAnalytics();
|
||||
|
||||
// filter out the analytics events based on filters the user has chosen
|
||||
final List<SummaryAnalyticsEvent> filteredAnalytics =
|
||||
await filterAnalytics(
|
||||
unfilteredAnalytics: summaryEvents,
|
||||
defaultSelected: defaultSelected,
|
||||
space: space,
|
||||
selected: selected,
|
||||
);
|
||||
|
||||
// then create and return the model to be displayed
|
||||
final ChartAnalyticsModel newModel = ChartAnalyticsModel(
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
msgs: filteredAnalytics
|
||||
.map((event) => event.content.messages)
|
||||
.expand((msgs) => msgs)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
cacheAnalytics(
|
||||
chartAnalyticsModel: newModel,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
);
|
||||
|
||||
return newModel;
|
||||
} catch (err, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return ChartAnalyticsModel(
|
||||
msgs: [],
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////// CONSTRUCTS ////////////////////////////
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> allMyConstructs() async {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode);
|
||||
final Room? analyticsRoom =
|
||||
_pangeaController.matrixState.client.analyticsRoomLocal(langCode);
|
||||
if (analyticsRoom == null) return [];
|
||||
|
||||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.construct,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
since: timeSpan.cutOffDate,
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
))
|
||||
?.cast<ConstructAnalyticsEvent>();
|
||||
|
|
@ -541,17 +171,17 @@ class AnalyticsController extends BaseController {
|
|||
|
||||
Future<List<ConstructAnalyticsEvent>> allSpaceMemberConstructs(
|
||||
Room space,
|
||||
TimeSpan timeSpan,
|
||||
) async {
|
||||
await space.requestParticipants();
|
||||
final List<ConstructAnalyticsEvent> constructEvents = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
|
||||
.analyticsRoomLocal(langCode, student.id);
|
||||
if (analyticsRoom != null) {
|
||||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.construct,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
since: timeSpan.cutOffDate,
|
||||
userId: student.id,
|
||||
))
|
||||
?.cast<ConstructAnalyticsEvent>();
|
||||
|
|
@ -600,8 +230,9 @@ class AnalyticsController extends BaseController {
|
|||
Room space,
|
||||
) async {
|
||||
final List<String> privateChatIds = space.allSpaceChildRoomIds;
|
||||
final List<String> lastFetched = await getLatestSpaceHierarchy(space.id);
|
||||
for (final id in lastFetched) {
|
||||
final resp = await space.client.getSpaceHierarchy(space.id);
|
||||
final List<String> chatIds = resp.rooms.map((room) => room.roomId).toList();
|
||||
for (final id in chatIds) {
|
||||
privateChatIds.removeWhere((e) => e == id);
|
||||
}
|
||||
final List<ConstructAnalyticsEvent> filtered =
|
||||
|
|
@ -618,7 +249,8 @@ class AnalyticsController extends BaseController {
|
|||
List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
Room space,
|
||||
) async {
|
||||
final List<String> chatIds = await getLatestSpaceHierarchy(space.id);
|
||||
final resp = await space.client.getSpaceHierarchy(space.id);
|
||||
final List<String> chatIds = resp.rooms.map((room) => room.roomId).toList();
|
||||
final List<ConstructAnalyticsEvent> filtered =
|
||||
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
|
||||
|
||||
|
|
@ -633,10 +265,10 @@ class AnalyticsController extends BaseController {
|
|||
|
||||
List<ConstructAnalyticsEvent>? getConstructsLocal({
|
||||
required TimeSpan timeSpan,
|
||||
required ConstructTypeEnum constructType,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
DateTime? lastUpdated,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) {
|
||||
final index = _cachedConstructs.indexWhere(
|
||||
(e) =>
|
||||
|
|
@ -646,7 +278,7 @@ class AnalyticsController extends BaseController {
|
|||
e.defaultSelected.type == defaultSelected.type &&
|
||||
e.selected?.id == selected?.id &&
|
||||
e.selected?.type == selected?.type &&
|
||||
e.langCode == currentAnalyticsLang.langCode,
|
||||
e.langCode == langCode,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
|
|
@ -661,29 +293,31 @@ class AnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
void cacheConstructs({
|
||||
required ConstructTypeEnum constructType,
|
||||
required List<ConstructAnalyticsEvent> events,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required TimeSpan timeSpan,
|
||||
AnalyticsSelected? selected,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) {
|
||||
final entry = ConstructCacheEntry(
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
timeSpan: timeSpan,
|
||||
type: constructType,
|
||||
events: List.from(events),
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
langCode: currentAnalyticsLang.langCode,
|
||||
langCode: langCode,
|
||||
);
|
||||
_cachedConstructs.add(entry);
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> getMyConstructs({
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required ConstructTypeEnum constructType,
|
||||
required TimeSpan timeSpan,
|
||||
ConstructTypeEnum? constructType,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
final List<ConstructAnalyticsEvent> unfilteredConstructs =
|
||||
await allMyConstructs();
|
||||
await allMyConstructs(timeSpan);
|
||||
|
||||
final Room? space = selected?.type == AnalyticsEntryType.space
|
||||
? _pangeaController.matrixState.client.getRoomById(selected!.id)
|
||||
|
|
@ -694,18 +328,21 @@ class AnalyticsController extends BaseController {
|
|||
space: space,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
timeSpan: timeSpan,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> getSpaceConstructs({
|
||||
required ConstructTypeEnum constructType,
|
||||
required Room space,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required TimeSpan timeSpan,
|
||||
AnalyticsSelected? selected,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) async {
|
||||
final List<ConstructAnalyticsEvent> unfilteredConstructs =
|
||||
await allSpaceMemberConstructs(
|
||||
space,
|
||||
timeSpan,
|
||||
);
|
||||
|
||||
return filterConstructs(
|
||||
|
|
@ -713,12 +350,14 @@ class AnalyticsController extends BaseController {
|
|||
space: space,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
timeSpan: timeSpan,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> filterConstructs({
|
||||
required List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required TimeSpan timeSpan,
|
||||
Room? space,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
|
|
@ -730,7 +369,7 @@ class AnalyticsController extends BaseController {
|
|||
for (int i = 0; i < unfilteredConstructs.length; i++) {
|
||||
final construct = unfilteredConstructs[i];
|
||||
construct.content.uses.removeWhere(
|
||||
(use) => use.timeStamp.isBefore(currentAnalyticsTimeSpan.cutOffDate),
|
||||
(use) => use.timeStamp.isBefore(timeSpan.cutOffDate),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -760,11 +399,12 @@ class AnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>?> getConstructs({
|
||||
required ConstructTypeEnum constructType,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required TimeSpan timeSpan,
|
||||
AnalyticsSelected? selected,
|
||||
bool removeIT = true,
|
||||
bool forceUpdate = false,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) async {
|
||||
debugPrint("getting constructs");
|
||||
await _pangeaController.matrixState.client.roomsLoading;
|
||||
|
|
@ -792,19 +432,16 @@ class AnalyticsController extends BaseController {
|
|||
// time the logged in user updated their analytics events
|
||||
// this gets passed to getAnalyticsLocal to determine if the cached
|
||||
// entry is out-of-date
|
||||
lastUpdated = await myAnalyticsLastUpdated(
|
||||
PangeaEventTypes.construct,
|
||||
);
|
||||
lastUpdated = await myAnalyticsLastUpdated();
|
||||
} else {
|
||||
// else, get the last time a student in the space updated their analytics
|
||||
lastUpdated = await spaceAnalyticsLastUpdated(
|
||||
PangeaEventTypes.construct,
|
||||
space!,
|
||||
);
|
||||
}
|
||||
|
||||
final List<ConstructAnalyticsEvent>? local = getConstructsLocal(
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
timeSpan: timeSpan,
|
||||
constructType: constructType,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
|
|
@ -821,12 +458,14 @@ class AnalyticsController extends BaseController {
|
|||
constructType: constructType,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
timeSpan: timeSpan,
|
||||
)
|
||||
: await getSpaceConstructs(
|
||||
constructType: constructType,
|
||||
space: space,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
timeSpan: timeSpan,
|
||||
);
|
||||
|
||||
if (removeIT) {
|
||||
|
|
@ -846,6 +485,7 @@ class AnalyticsController extends BaseController {
|
|||
events: filteredConstructs,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
timeSpan: timeSpan,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -889,32 +529,15 @@ abstract class CacheEntry {
|
|||
}
|
||||
|
||||
class ConstructCacheEntry extends CacheEntry {
|
||||
final ConstructTypeEnum type;
|
||||
final ConstructTypeEnum? type;
|
||||
final List<ConstructAnalyticsEvent> events;
|
||||
|
||||
ConstructCacheEntry({
|
||||
required this.type,
|
||||
required this.events,
|
||||
required super.timeSpan,
|
||||
required super.langCode,
|
||||
required super.defaultSelected,
|
||||
this.type,
|
||||
super.selected,
|
||||
});
|
||||
}
|
||||
|
||||
class AnalyticsCacheModel extends CacheEntry {
|
||||
final ChartAnalyticsModel chartAnalyticsModel;
|
||||
|
||||
AnalyticsCacheModel({
|
||||
required this.chartAnalyticsModel,
|
||||
required super.timeSpan,
|
||||
required super.langCode,
|
||||
required super.defaultSelected,
|
||||
super.selected,
|
||||
});
|
||||
|
||||
@override
|
||||
bool get isExpired =>
|
||||
DateTime.now().difference(_createdAt).inMinutes >
|
||||
ClassDefaultValues.minutesDelayToMakeNewChartAnalytics;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,16 @@ import 'dart:developer';
|
|||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../extensions/client_extension/client_extension.dart';
|
||||
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
|
||||
/// handles the processing of analytics for
|
||||
/// 1) messages sent by the user and
|
||||
/// 2) constructs used by the user, both in sending messages and doing practice activities
|
||||
|
|
@ -56,15 +54,14 @@ class MyAnalyticsController {
|
|||
|
||||
/// If analytics haven't been updated in the last day, update them
|
||||
Future<DateTime?> _refreshAnalyticsIfOutdated() async {
|
||||
DateTime? lastUpdated = await _pangeaController.analytics
|
||||
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
|
||||
DateTime? lastUpdated =
|
||||
await _pangeaController.analytics.myAnalyticsLastUpdated();
|
||||
final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate);
|
||||
|
||||
if (lastUpdated?.isBefore(yesterday) ?? true) {
|
||||
debugPrint("analytics out-of-date, updating");
|
||||
await updateAnalytics();
|
||||
lastUpdated = await _pangeaController.analytics
|
||||
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
|
||||
lastUpdated = await _pangeaController.analytics.myAnalyticsLastUpdated();
|
||||
}
|
||||
return lastUpdated;
|
||||
}
|
||||
|
|
@ -238,7 +235,6 @@ class MyAnalyticsController {
|
|||
// get the last time analytics were updated for this room
|
||||
final DateTime? l2AnalyticsLastUpdated =
|
||||
await analyticsRoom.analyticsLastUpdated(
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
_client.userID!,
|
||||
);
|
||||
|
||||
|
|
@ -302,16 +298,6 @@ class MyAnalyticsController {
|
|||
final List<PangeaMessageEvent> allRecentMessages =
|
||||
recentPangeaMessageEvents.expand((e) => e).toList();
|
||||
|
||||
final List<RecentMessageRecord> summaryContent =
|
||||
SummaryAnalyticsModel.formatSummaryContent(allRecentMessages);
|
||||
// if there's new content to be sent, or if lastUpdated hasn't been
|
||||
// set yet for this room, send the analytics events
|
||||
if (summaryContent.isNotEmpty || l2AnalyticsLastUpdated == null) {
|
||||
await analyticsRoom.sendSummaryAnalyticsEvent(
|
||||
summaryContent,
|
||||
);
|
||||
}
|
||||
|
||||
// get constructs for messages
|
||||
final List<OneConstructUse> recentConstructUses = [];
|
||||
for (final PangeaMessageEvent message in allRecentMessages) {
|
||||
|
|
|
|||
54
lib/pangea/enum/progress_indicators_enum.dart
Normal file
54
lib/pangea/enum/progress_indicators_enum.dart
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
enum ProgressIndicatorEnum {
|
||||
level,
|
||||
wordsUsed,
|
||||
errorTypes,
|
||||
}
|
||||
|
||||
extension ProgressIndicatorsExtension on ProgressIndicatorEnum {
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case ProgressIndicatorEnum.wordsUsed:
|
||||
return Icons.text_fields_outlined;
|
||||
case ProgressIndicatorEnum.errorTypes:
|
||||
return Icons.error_outline;
|
||||
case ProgressIndicatorEnum.level:
|
||||
return Icons.star;
|
||||
}
|
||||
}
|
||||
|
||||
static bool isDarkMode(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
Color color(BuildContext context) {
|
||||
switch (this) {
|
||||
case ProgressIndicatorEnum.wordsUsed:
|
||||
return Theme.of(context).brightness == Brightness.dark
|
||||
? const Color.fromARGB(255, 169, 183, 237)
|
||||
: const Color.fromARGB(255, 38, 59, 141);
|
||||
case ProgressIndicatorEnum.errorTypes:
|
||||
return Theme.of(context).brightness == Brightness.dark
|
||||
? const Color.fromARGB(255, 212, 144, 216)
|
||||
: const Color.fromARGB(255, 163, 39, 169);
|
||||
case ProgressIndicatorEnum.level:
|
||||
return Theme.of(context).brightness == Brightness.dark
|
||||
? const Color.fromARGB(255, 250, 220, 129)
|
||||
: const Color.fromARGB(255, 255, 208, 67);
|
||||
default:
|
||||
return Theme.of(context).textTheme.bodyLarge!.color ?? Colors.blueGrey;
|
||||
}
|
||||
}
|
||||
|
||||
String tooltip(BuildContext context) {
|
||||
switch (this) {
|
||||
case ProgressIndicatorEnum.wordsUsed:
|
||||
return L10n.of(context)!.wordsUsed;
|
||||
case ProgressIndicatorEnum.errorTypes:
|
||||
return L10n.of(context)!.errorTypes;
|
||||
case ProgressIndicatorEnum.level:
|
||||
return L10n.of(context)!.level;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../models/analytics/chart_analytics_model.dart';
|
||||
|
||||
enum TimeSpan { day, week, month, sixmonths, year }
|
||||
enum TimeSpan { day, week, month, sixmonths, year, forever }
|
||||
|
||||
extension TimeSpanFunctions on TimeSpan {
|
||||
String string(BuildContext context) {
|
||||
|
|
@ -35,19 +33,8 @@ extension TimeSpanFunctions on TimeSpan {
|
|||
return 6;
|
||||
case TimeSpan.year:
|
||||
return 12;
|
||||
}
|
||||
}
|
||||
|
||||
Duration timeAgo(int index) {
|
||||
switch (this) {
|
||||
case TimeSpan.day:
|
||||
return Duration(hours: index);
|
||||
case TimeSpan.week:
|
||||
case TimeSpan.month:
|
||||
return Duration(days: index);
|
||||
case TimeSpan.year:
|
||||
case TimeSpan.sixmonths:
|
||||
return Duration(days: index * 32);
|
||||
case TimeSpan.forever:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,44 +52,8 @@ extension TimeSpanFunctions on TimeSpan {
|
|||
return DateTime.now().subtract(Duration(days: numberOfIntervals * 30));
|
||||
case TimeSpan.year:
|
||||
return DateTime.now().subtract(const Duration(days: 365));
|
||||
case TimeSpan.forever:
|
||||
return DateTime(2020);
|
||||
}
|
||||
}
|
||||
|
||||
String getMapKey(DateTime date) {
|
||||
switch (this) {
|
||||
case TimeSpan.day:
|
||||
return date.hour.toString();
|
||||
case TimeSpan.week:
|
||||
return date.weekday.toString();
|
||||
case TimeSpan.month:
|
||||
return date.day.toString();
|
||||
case TimeSpan.sixmonths:
|
||||
case TimeSpan.year:
|
||||
return date.month.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: end is same as start!!
|
||||
Map<String, TimeSeriesInterval> get emptyIntervals {
|
||||
final DateTime now = DateTime.now();
|
||||
final List<int> numbers =
|
||||
List.generate(numberOfIntervals, (index) => index);
|
||||
final Map<String, TimeSeriesInterval> map = {};
|
||||
|
||||
// debugger(when: kDebugMode);
|
||||
for (final index in numbers) {
|
||||
final timeAgos = timeAgo(index);
|
||||
final DateTime end = now.subtract(timeAgos);
|
||||
// debugger(when: end.isBefore(now.subtract(const Duration(days: 30))));
|
||||
final String mapKey = getMapKey(end);
|
||||
// debugger(when: mapKey.toString() == "5");
|
||||
map[mapKey] = TimeSeriesInterval(
|
||||
start: end,
|
||||
end: end,
|
||||
totals: TimeSeriesTotals.empty,
|
||||
);
|
||||
}
|
||||
// debugger(when: kDebugMode);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,7 +152,6 @@ extension AnalyticsClientExtension on Client {
|
|||
final Map<String, DateTime?> lastUpdatedMap = {};
|
||||
for (final analyticsRoom in allMyAnalyticsRooms) {
|
||||
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
userID!,
|
||||
);
|
||||
lastUpdatedMap[analyticsRoom.id] = lastUpdated;
|
||||
|
|
|
|||
|
|
@ -282,83 +282,6 @@ extension EventsRoomExtension on Room {
|
|||
);
|
||||
}
|
||||
|
||||
Future<List<RecentMessageRecord>> get _messageListForAllChildChats async {
|
||||
try {
|
||||
if (!isSpace) return [];
|
||||
final List<Room> spaceChats = spaceChildren
|
||||
.where((e) => e.roomId != null)
|
||||
.map((e) => client.getRoomById(e.roomId!))
|
||||
.where((element) => element != null)
|
||||
.cast<Room>()
|
||||
.where((element) => !element.isSpace)
|
||||
.toList();
|
||||
|
||||
final List<Future<List<RecentMessageRecord>>> msgListFutures = [];
|
||||
for (final chat in spaceChats) {
|
||||
msgListFutures.add(chat._messageListForChat);
|
||||
}
|
||||
final List<List<RecentMessageRecord>> msgLists =
|
||||
await Future.wait(msgListFutures);
|
||||
|
||||
final List<RecentMessageRecord> joined = [];
|
||||
for (final msgList in msgLists) {
|
||||
joined.addAll(msgList);
|
||||
}
|
||||
return joined;
|
||||
} catch (err) {
|
||||
// debugger(when: kDebugMode);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<RecentMessageRecord>> get _messageListForChat async {
|
||||
try {
|
||||
int numberOfSearches = 0;
|
||||
|
||||
if (isSpace) {
|
||||
throw Exception(
|
||||
"In messageListForChat with room that is not a chat",
|
||||
);
|
||||
}
|
||||
final Timeline timeline = await getTimeline();
|
||||
|
||||
while (timeline.canRequestHistory && numberOfSearches < 50) {
|
||||
await timeline.requestHistory(historyCount: 100);
|
||||
numberOfSearches += 1;
|
||||
}
|
||||
if (timeline.canRequestHistory) {
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
|
||||
final List<RecentMessageRecord> msgs = [];
|
||||
for (final event in timeline.events) {
|
||||
if (event.senderId == client.userID &&
|
||||
event.type == EventTypes.Message &&
|
||||
event.content['msgtype'] == MessageTypes.Text) {
|
||||
final PangeaMessageEvent pMsgEvent = PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
ownMessage: true,
|
||||
);
|
||||
msgs.add(
|
||||
RecentMessageRecord(
|
||||
eventId: event.eventId,
|
||||
chatId: id,
|
||||
useType: pMsgEvent.msgUseType,
|
||||
time: event.originServerTs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return msgs;
|
||||
} catch (err, s) {
|
||||
if (kDebugMode) rethrow;
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ConstructEvent? _vocabEventLocal(String lemma) {
|
||||
// if (!isAnalyticsRoom) throw Exception("not an analytics room");
|
||||
|
||||
|
|
@ -451,14 +374,11 @@ extension EventsRoomExtension on Room {
|
|||
return false;
|
||||
}
|
||||
|
||||
while (timeline.canRequestHistory &&
|
||||
!reachedEnd() &&
|
||||
numberOfSearches < 10) {
|
||||
while (timeline.canRequestHistory && numberOfSearches < 10) {
|
||||
await timeline.requestHistory(historyCount: 100);
|
||||
numberOfSearches += 1;
|
||||
if (reachedEnd()) {
|
||||
break;
|
||||
}
|
||||
if (!timeline.canRequestHistory) break;
|
||||
if (reachedEnd()) break;
|
||||
}
|
||||
|
||||
final List<Event> fetchedEvents = timeline.events
|
||||
|
|
|
|||
|
|
@ -9,12 +9,8 @@ import 'package:fluffychat/pangea/constants/language_constants.dart';
|
|||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
|
|
@ -80,22 +76,15 @@ extension PangeaRoom on Room {
|
|||
void inviteSpaceTeachersToAnalyticsRooms() =>
|
||||
_inviteSpaceTeachersToAnalyticsRooms();
|
||||
|
||||
Future<AnalyticsEvent?> getLastAnalyticsEvent(
|
||||
String type,
|
||||
String userId,
|
||||
) async =>
|
||||
await _getLastAnalyticsEvent(type, userId);
|
||||
|
||||
Future<DateTime?> analyticsLastUpdated(String type, String userId) async {
|
||||
return await _analyticsLastUpdated(type, userId);
|
||||
Future<DateTime?> analyticsLastUpdated(String userId) async {
|
||||
return await _analyticsLastUpdated(userId);
|
||||
}
|
||||
|
||||
Future<List<AnalyticsEvent>?> getAnalyticsEvents({
|
||||
required String type,
|
||||
Future<List<ConstructAnalyticsEvent>?> getAnalyticsEvents({
|
||||
required String userId,
|
||||
DateTime? since,
|
||||
}) async =>
|
||||
await _getAnalyticsEvents(type: type, since: since, userId: userId);
|
||||
await _getAnalyticsEvents(since: since, userId: userId);
|
||||
|
||||
String? get madeForLang => _madeForLang;
|
||||
|
||||
|
|
|
|||
|
|
@ -140,52 +140,36 @@ extension AnalyticsRoomExtension on Room {
|
|||
);
|
||||
}
|
||||
|
||||
Future<AnalyticsEvent?> _getLastAnalyticsEvent(
|
||||
String type,
|
||||
Future<ConstructAnalyticsEvent?> _getLastAnalyticsEvent(
|
||||
String userId,
|
||||
) async {
|
||||
final List<Event> events = await getEventsBySender(
|
||||
type: type,
|
||||
type: PangeaEventTypes.construct,
|
||||
sender: userId,
|
||||
count: 10,
|
||||
);
|
||||
if (events.isEmpty) return null;
|
||||
final Event event = events.first;
|
||||
AnalyticsEvent? analyticsEvent;
|
||||
switch (type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
analyticsEvent = SummaryAnalyticsEvent(event: event);
|
||||
case PangeaEventTypes.construct:
|
||||
analyticsEvent = ConstructAnalyticsEvent(event: event);
|
||||
}
|
||||
return analyticsEvent;
|
||||
return ConstructAnalyticsEvent(event: event);
|
||||
}
|
||||
|
||||
Future<DateTime?> _analyticsLastUpdated(String type, String userId) async {
|
||||
final lastEvent = await _getLastAnalyticsEvent(type, userId);
|
||||
Future<DateTime?> _analyticsLastUpdated(String userId) async {
|
||||
final lastEvent = await _getLastAnalyticsEvent(userId);
|
||||
return lastEvent?.event.originServerTs;
|
||||
}
|
||||
|
||||
Future<List<AnalyticsEvent>?> _getAnalyticsEvents({
|
||||
required String type,
|
||||
Future<List<ConstructAnalyticsEvent>?> _getAnalyticsEvents({
|
||||
required String userId,
|
||||
DateTime? since,
|
||||
}) async {
|
||||
final List<Event> events = await getEventsBySender(
|
||||
type: type,
|
||||
type: PangeaEventTypes.construct,
|
||||
sender: userId,
|
||||
since: since,
|
||||
);
|
||||
final List<AnalyticsEvent> analyticsEvents = [];
|
||||
final List<ConstructAnalyticsEvent> analyticsEvents = [];
|
||||
for (final Event event in events) {
|
||||
switch (type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
analyticsEvents.add(SummaryAnalyticsEvent(event: event));
|
||||
break;
|
||||
case PangeaEventTypes.construct:
|
||||
analyticsEvents.add(ConstructAnalyticsEvent(event: event));
|
||||
break;
|
||||
}
|
||||
analyticsEvents.add(ConstructAnalyticsEvent(event: event));
|
||||
}
|
||||
|
||||
return analyticsEvents;
|
||||
|
|
@ -203,18 +187,6 @@ extension AnalyticsRoomExtension on Room {
|
|||
creationContent?.tryGet<String>(ModelKey.oldLangCode) == langCode;
|
||||
}
|
||||
|
||||
Future<void> sendSummaryAnalyticsEvent(
|
||||
List<RecentMessageRecord> records,
|
||||
) async {
|
||||
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
|
||||
messages: records,
|
||||
);
|
||||
await sendEvent(
|
||||
analyticsModel.toJson(),
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends construct events to the server.
|
||||
///
|
||||
/// The [uses] parameter is a list of [OneConstructUse] objects representing the
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
// superclass for all analytics events
|
||||
abstract class AnalyticsEvent {
|
||||
late Event _event;
|
||||
AnalyticsModel? contentCache;
|
||||
|
||||
AnalyticsEvent({required Event event}) {
|
||||
_event = event;
|
||||
}
|
||||
|
||||
Event get event => _event;
|
||||
|
||||
AnalyticsModel get content {
|
||||
switch (_event.type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
contentCache ??= SummaryAnalyticsModel.fromJson(event.content);
|
||||
break;
|
||||
case PangeaEventTypes.construct:
|
||||
contentCache ??= ConstructAnalyticsModel.fromJson(event.content);
|
||||
break;
|
||||
}
|
||||
return contentCache!;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
|
||||
abstract class AnalyticsModel {
|
||||
static List<dynamic> formatAnalyticsContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
String type,
|
||||
) {
|
||||
switch (type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
return SummaryAnalyticsModel.formatSummaryContent(recentMsgs);
|
||||
case PangeaEventTypes.construct:
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final msg in recentMsgs) {
|
||||
uses.addAll(msg.allConstructUses);
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,149 +1,149 @@
|
|||
import 'dart:developer';
|
||||
// import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
// import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
// import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
// import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../enum/use_type.dart';
|
||||
// import '../../enum/use_type.dart';
|
||||
|
||||
class TimeSeriesTotals {
|
||||
int ta;
|
||||
int ga;
|
||||
int wa;
|
||||
int un;
|
||||
// class TimeSeriesTotals {
|
||||
// int ta;
|
||||
// int ga;
|
||||
// int wa;
|
||||
// int un;
|
||||
|
||||
int get all => ta + ga + wa + un;
|
||||
// int get all => ta + ga + wa + un;
|
||||
|
||||
TimeSeriesTotals({
|
||||
required this.ta,
|
||||
required this.ga,
|
||||
required this.wa,
|
||||
required this.un,
|
||||
});
|
||||
// TimeSeriesTotals({
|
||||
// required this.ta,
|
||||
// required this.ga,
|
||||
// required this.wa,
|
||||
// required this.un,
|
||||
// });
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
UseType.ta.string: ta,
|
||||
UseType.ga.string: ga,
|
||||
UseType.wa.string: wa,
|
||||
UseType.un.string: un,
|
||||
};
|
||||
// Map<String, dynamic> toJson() => {
|
||||
// UseType.ta.string: ta,
|
||||
// UseType.ga.string: ga,
|
||||
// UseType.wa.string: wa,
|
||||
// UseType.un.string: un,
|
||||
// };
|
||||
|
||||
factory TimeSeriesTotals.fromJson(json) => TimeSeriesTotals(
|
||||
ta: json[UseType.ta.string],
|
||||
ga: json[UseType.ga.string],
|
||||
wa: json[UseType.wa.string],
|
||||
un: json[UseType.un.string],
|
||||
);
|
||||
// factory TimeSeriesTotals.fromJson(json) => TimeSeriesTotals(
|
||||
// ta: json[UseType.ta.string],
|
||||
// ga: json[UseType.ga.string],
|
||||
// wa: json[UseType.wa.string],
|
||||
// un: json[UseType.un.string],
|
||||
// );
|
||||
|
||||
static get empty => TimeSeriesTotals(ta: 0, ga: 0, wa: 0, un: 0);
|
||||
// static get empty => TimeSeriesTotals(ta: 0, ga: 0, wa: 0, un: 0);
|
||||
|
||||
int get taPercent => all != 0 ? (ta / all * 100).round() : 0;
|
||||
int get gaPercent => all != 0 ? (ga / all * 100).round() : 0;
|
||||
int get waPercent => all != 0 ? (wa / all * 100).round() : 0;
|
||||
int get unPercent => all != 0 ? (un / all * 100).round() : 0;
|
||||
// int get taPercent => all != 0 ? (ta / all * 100).round() : 0;
|
||||
// int get gaPercent => all != 0 ? (ga / all * 100).round() : 0;
|
||||
// int get waPercent => all != 0 ? (wa / all * 100).round() : 0;
|
||||
// int get unPercent => all != 0 ? (un / all * 100).round() : 0;
|
||||
|
||||
void increment(RecentMessageRecord msg) {
|
||||
switch (msg.useType) {
|
||||
case UseType.ta:
|
||||
ta += 1;
|
||||
break;
|
||||
case UseType.wa:
|
||||
wa += 1;
|
||||
break;
|
||||
case UseType.ga:
|
||||
ga += 1;
|
||||
break;
|
||||
case UseType.un:
|
||||
un += 1;
|
||||
break;
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
debugPrint("message with bad type ${msg.toJson()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// void increment(RecentMessageRecord msg) {
|
||||
// switch (msg.useType) {
|
||||
// case UseType.ta:
|
||||
// ta += 1;
|
||||
// break;
|
||||
// case UseType.wa:
|
||||
// wa += 1;
|
||||
// break;
|
||||
// case UseType.ga:
|
||||
// ga += 1;
|
||||
// break;
|
||||
// case UseType.un:
|
||||
// un += 1;
|
||||
// break;
|
||||
// default:
|
||||
// debugger(when: kDebugMode);
|
||||
// debugPrint("message with bad type ${msg.toJson()}");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
class TimeSeriesInterval {
|
||||
DateTime start;
|
||||
DateTime end;
|
||||
TimeSeriesTotals totals;
|
||||
// class TimeSeriesInterval {
|
||||
// DateTime start;
|
||||
// DateTime end;
|
||||
// TimeSeriesTotals totals;
|
||||
|
||||
TimeSeriesInterval({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.totals,
|
||||
});
|
||||
// TimeSeriesInterval({
|
||||
// required this.start,
|
||||
// required this.end,
|
||||
// required this.totals,
|
||||
// });
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"strt": start.toIso8601String(),
|
||||
"end": end.toIso8601String(),
|
||||
"totals": totals.toJson(),
|
||||
};
|
||||
// Map<String, dynamic> toJson() => {
|
||||
// "strt": start.toIso8601String(),
|
||||
// "end": end.toIso8601String(),
|
||||
// "totals": totals.toJson(),
|
||||
// };
|
||||
|
||||
factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval(
|
||||
start: DateTime.parse(json["strt"]),
|
||||
end: DateTime.parse(json["end"]),
|
||||
totals: TimeSeriesTotals.fromJson(json["totals"]),
|
||||
);
|
||||
}
|
||||
// factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval(
|
||||
// start: DateTime.parse(json["strt"]),
|
||||
// end: DateTime.parse(json["end"]),
|
||||
// totals: TimeSeriesTotals.fromJson(json["totals"]),
|
||||
// );
|
||||
// }
|
||||
|
||||
class ChartAnalyticsModel {
|
||||
final TimeSpan timeSpan;
|
||||
final TimeSeriesTotals totals = TimeSeriesTotals.empty;
|
||||
final List<RecentMessageRecord> msgs;
|
||||
final String? chatId;
|
||||
// class ChartAnalyticsModel {
|
||||
// final TimeSpan timeSpan;
|
||||
// final TimeSeriesTotals totals = TimeSeriesTotals.empty;
|
||||
// final List<RecentMessageRecord> msgs;
|
||||
// final String? chatId;
|
||||
|
||||
late DateTime fetchedAt;
|
||||
late List<TimeSeriesInterval> timeSeries;
|
||||
DateTime? lastMessage;
|
||||
// late DateTime fetchedAt;
|
||||
// late List<TimeSeriesInterval> timeSeries;
|
||||
// DateTime? lastMessage;
|
||||
|
||||
ChartAnalyticsModel({
|
||||
required this.timeSpan,
|
||||
required this.msgs,
|
||||
this.chatId,
|
||||
}) {
|
||||
fetchedAt = DateTime.now();
|
||||
calculate();
|
||||
}
|
||||
// ChartAnalyticsModel({
|
||||
// required this.timeSpan,
|
||||
// required this.msgs,
|
||||
// this.chatId,
|
||||
// }) {
|
||||
// fetchedAt = DateTime.now();
|
||||
// calculate();
|
||||
// }
|
||||
|
||||
bool get isEmpty => (totals.ga + totals.ta + totals.wa == 0);
|
||||
// bool get isEmpty => (totals.ga + totals.ta + totals.wa == 0);
|
||||
|
||||
void calculate() {
|
||||
final Map<String, TimeSeriesInterval> intervals = timeSpan.emptyIntervals;
|
||||
final DateTime cutOff = timeSpan.cutOffDate;
|
||||
// void calculate() {
|
||||
// final Map<String, TimeSeriesInterval> intervals = timeSpan.emptyIntervals;
|
||||
// final DateTime cutOff = timeSpan.cutOffDate;
|
||||
|
||||
final filtered = msgs.where(
|
||||
(msg) =>
|
||||
(chatId == null || msg.chatId == chatId) && msg.time.isAfter(cutOff),
|
||||
);
|
||||
// final filtered = msgs.where(
|
||||
// (msg) =>
|
||||
// (chatId == null || msg.chatId == chatId) && msg.time.isAfter(cutOff),
|
||||
// );
|
||||
|
||||
//remove msgs with duplicate ids
|
||||
final Map<String, RecentMessageRecord> unique = {};
|
||||
for (final msg in filtered) {
|
||||
if (unique[msg.eventId] == null) {
|
||||
unique[msg.eventId] = msg;
|
||||
}
|
||||
}
|
||||
// //remove msgs with duplicate ids
|
||||
// final Map<String, RecentMessageRecord> unique = {};
|
||||
// for (final msg in filtered) {
|
||||
// if (unique[msg.eventId] == null) {
|
||||
// unique[msg.eventId] = msg;
|
||||
// }
|
||||
// }
|
||||
|
||||
for (final msg in unique.values) {
|
||||
final String key = timeSpan.getMapKey(msg.time);
|
||||
if (intervals[key] == null) {
|
||||
debugger(when: kDebugMode);
|
||||
} else {
|
||||
intervals[key]!.totals.increment(msg);
|
||||
totals.increment(msg);
|
||||
lastMessage = msg.time;
|
||||
}
|
||||
}
|
||||
timeSeries = intervals.values.toList().reversed.toList();
|
||||
}
|
||||
// for (final msg in unique.values) {
|
||||
// final String key = timeSpan.getMapKey(msg.time);
|
||||
// if (intervals[key] == null) {
|
||||
// debugger(when: kDebugMode);
|
||||
// } else {
|
||||
// intervals[key]!.totals.increment(msg);
|
||||
// totals.increment(msg);
|
||||
// lastMessage = msg.time;
|
||||
// }
|
||||
// }
|
||||
// timeSeries = intervals.values.toList().reversed.toList();
|
||||
// }
|
||||
|
||||
DateTime? get lastMessageTime {
|
||||
if (msgs.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return msgs.map((msg) => msg.time).reduce(
|
||||
(compare, recent) => compare.isAfter(recent) ? compare : recent,
|
||||
);
|
||||
}
|
||||
}
|
||||
// DateTime? get lastMessageTime {
|
||||
// if (msgs.isEmpty) {
|
||||
// return null;
|
||||
// }
|
||||
// return msgs.map((msg) => msg.time).reduce(
|
||||
// (compare, recent) => compare.isAfter(recent) ? compare : recent,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../constants/pangea_event_types.dart';
|
||||
|
||||
class ConstructAnalyticsEvent extends AnalyticsEvent {
|
||||
ConstructAnalyticsEvent({required Event event}) : super(event: event) {
|
||||
class ConstructAnalyticsEvent {
|
||||
late Event _event;
|
||||
ConstructAnalyticsModel? contentCache;
|
||||
ConstructAnalyticsEvent({required Event event}) {
|
||||
_event = event;
|
||||
if (event.type != PangeaEventTypes.construct) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a ConstructAnalyticsEvent",
|
||||
|
|
@ -13,7 +15,8 @@ class ConstructAnalyticsEvent extends AnalyticsEvent {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Event get event => _event;
|
||||
|
||||
ConstructAnalyticsModel get content {
|
||||
contentCache ??= ConstructAnalyticsModel.fromJson(event.content);
|
||||
return contentCache as ConstructAnalyticsModel;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../enum/construct_type_enum.dart';
|
||||
|
||||
class ConstructAnalyticsModel extends AnalyticsModel {
|
||||
class ConstructAnalyticsModel {
|
||||
List<OneConstructUse> uses;
|
||||
|
||||
ConstructAnalyticsModel({
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../constants/pangea_event_types.dart';
|
||||
|
||||
class SummaryAnalyticsEvent extends AnalyticsEvent {
|
||||
SummaryAnalyticsEvent({required Event event}) : super(event: event) {
|
||||
if (event.type != PangeaEventTypes.summaryAnalytics) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a SummaryAnalyticsEvent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
SummaryAnalyticsModel get content {
|
||||
contentCache ??= SummaryAnalyticsModel.fromJson(event.content);
|
||||
return contentCache as SummaryAnalyticsModel;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/use_type.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class SummaryAnalyticsModel extends AnalyticsModel {
|
||||
late List<RecentMessageRecord> _messages;
|
||||
|
||||
SummaryAnalyticsModel({
|
||||
required List<RecentMessageRecord> messages,
|
||||
}) {
|
||||
_messages = messages;
|
||||
}
|
||||
|
||||
List<RecentMessageRecord> get messages => _messages;
|
||||
|
||||
static const _messagesKey = "msgs";
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
_messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()),
|
||||
};
|
||||
|
||||
factory SummaryAnalyticsModel.fromJson(json) {
|
||||
List<RecentMessageRecord> savedMessages = [];
|
||||
try {
|
||||
savedMessages = json[_messagesKey] != null
|
||||
? (jsonDecode(json[_messagesKey] ?? "[]") as Iterable)
|
||||
.map((e) => RecentMessageRecord.fromJson(e))
|
||||
.toList()
|
||||
.cast<RecentMessageRecord>()
|
||||
: [];
|
||||
} catch (err, stack) {
|
||||
if (kDebugMode) rethrow;
|
||||
ErrorHandler.logError(e: err, s: stack);
|
||||
}
|
||||
return SummaryAnalyticsModel(
|
||||
messages: savedMessages,
|
||||
);
|
||||
}
|
||||
|
||||
static List<RecentMessageRecord> formatSummaryContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
) {
|
||||
final List<PangeaMessageEvent> filtered = List.from(recentMsgs);
|
||||
final List<RecentMessageRecord> records = filtered
|
||||
.map(
|
||||
(msg) => RecentMessageRecord(
|
||||
eventId: msg.eventId,
|
||||
chatId: msg.room.id,
|
||||
useType: msg.msgUseType,
|
||||
time: msg.originServerTs,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return records;
|
||||
}
|
||||
}
|
||||
|
||||
class RecentMessageRecord {
|
||||
String eventId;
|
||||
String chatId;
|
||||
UseType useType;
|
||||
DateTime time;
|
||||
|
||||
RecentMessageRecord({
|
||||
required this.eventId,
|
||||
required this.chatId,
|
||||
required this.useType,
|
||||
required this.time,
|
||||
});
|
||||
|
||||
factory RecentMessageRecord.fromJson(Map<String, dynamic> json) =>
|
||||
RecentMessageRecord(
|
||||
eventId: json[_eventIdKey],
|
||||
chatId: json[_chatIdKey],
|
||||
useType: _typeStringToEnum(json[_typeOfUseKey]),
|
||||
time: DateTime.parse(json[_timeKey]),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
_eventIdKey: eventId,
|
||||
_chatIdKey: chatId,
|
||||
_typeOfUseKey: _typeEnumToString(useType),
|
||||
_timeKey: time.toIso8601String(),
|
||||
};
|
||||
|
||||
String _typeEnumToString(dynamic status) => status.toString().split('.').last;
|
||||
|
||||
static UseType _typeStringToEnum(String useType) {
|
||||
final String lastPart = useType.toString().split('.').last;
|
||||
switch (lastPart) {
|
||||
case 'ta':
|
||||
return UseType.ta;
|
||||
case 'ga':
|
||||
return UseType.ga;
|
||||
case 'wa':
|
||||
return UseType.wa;
|
||||
default:
|
||||
return UseType.un;
|
||||
}
|
||||
}
|
||||
|
||||
static const _eventIdKey = "m.id";
|
||||
static const _chatIdKey = "c.id";
|
||||
static const _typeOfUseKey = "typ";
|
||||
static const _timeKey = "t";
|
||||
}
|
||||
|
|
@ -1,156 +1,156 @@
|
|||
import 'dart:async';
|
||||
// import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
// import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
// import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
// import 'package:go_router/go_router.dart';
|
||||
// import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../../utils/date_time_extension.dart';
|
||||
import '../../../widgets/avatar.dart';
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../models/analytics/chart_analytics_model.dart';
|
||||
import 'base_analytics.dart';
|
||||
import 'list_summary_analytics.dart';
|
||||
// import '../../../../utils/date_time_extension.dart';
|
||||
// import '../../../widgets/avatar.dart';
|
||||
// import '../../../widgets/matrix.dart';
|
||||
// import '../../models/analytics/chart_analytics_model.dart';
|
||||
// import 'base_analytics.dart';
|
||||
// import 'list_summary_analytics.dart';
|
||||
|
||||
class AnalyticsListTile extends StatefulWidget {
|
||||
const AnalyticsListTile({
|
||||
super.key,
|
||||
required this.defaultSelected,
|
||||
required this.selected,
|
||||
required this.avatar,
|
||||
required this.allowNavigateOnSelect,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
required this.pangeaController,
|
||||
this.controller,
|
||||
this.refreshStream,
|
||||
});
|
||||
// class AnalyticsListTile extends StatefulWidget {
|
||||
// const AnalyticsListTile({
|
||||
// super.key,
|
||||
// required this.defaultSelected,
|
||||
// required this.selected,
|
||||
// required this.avatar,
|
||||
// required this.allowNavigateOnSelect,
|
||||
// required this.isSelected,
|
||||
// required this.onTap,
|
||||
// required this.pangeaController,
|
||||
// this.controller,
|
||||
// this.refreshStream,
|
||||
// });
|
||||
|
||||
final void Function(AnalyticsSelected) onTap;
|
||||
// final void Function(AnalyticsSelected) onTap;
|
||||
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final AnalyticsSelected selected;
|
||||
// final AnalyticsSelected defaultSelected;
|
||||
// final AnalyticsSelected selected;
|
||||
|
||||
final Uri? avatar;
|
||||
// final Uri? avatar;
|
||||
|
||||
final bool allowNavigateOnSelect;
|
||||
final bool isSelected;
|
||||
// final bool allowNavigateOnSelect;
|
||||
// final bool isSelected;
|
||||
|
||||
final PangeaController pangeaController;
|
||||
final BaseAnalyticsController? controller;
|
||||
final StreamController? refreshStream;
|
||||
// final PangeaController pangeaController;
|
||||
// final BaseAnalyticsController? controller;
|
||||
// final StreamController? refreshStream;
|
||||
|
||||
@override
|
||||
AnalyticsListTileState createState() => AnalyticsListTileState();
|
||||
}
|
||||
// @override
|
||||
// AnalyticsListTileState createState() => AnalyticsListTileState();
|
||||
// }
|
||||
|
||||
class AnalyticsListTileState extends State<AnalyticsListTile> {
|
||||
ChartAnalyticsModel? tileData;
|
||||
StreamSubscription? refreshSubscription;
|
||||
// class AnalyticsListTileState extends State<AnalyticsListTile> {
|
||||
// ChartAnalyticsModel? tileData;
|
||||
// StreamSubscription? refreshSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setTileData();
|
||||
refreshSubscription = widget.refreshStream?.stream.listen((forceUpdate) {
|
||||
setTileData(forceUpdate: forceUpdate);
|
||||
});
|
||||
}
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// setTileData();
|
||||
// refreshSubscription = widget.refreshStream?.stream.listen((forceUpdate) {
|
||||
// setTileData(forceUpdate: forceUpdate);
|
||||
// });
|
||||
// }
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant AnalyticsListTile oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.selected != widget.selected) {
|
||||
setTileData();
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// void didUpdateWidget(covariant AnalyticsListTile oldWidget) {
|
||||
// super.didUpdateWidget(oldWidget);
|
||||
// if (oldWidget.selected != widget.selected) {
|
||||
// setTileData();
|
||||
// }
|
||||
// }
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
refreshSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
// @override
|
||||
// void dispose() {
|
||||
// refreshSubscription?.cancel();
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
Future<void> setTileData({forceUpdate = false}) async {
|
||||
tileData = await MatrixState.pangeaController.analytics.getAnalytics(
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: forceUpdate,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
// Future<void> setTileData({forceUpdate = false}) async {
|
||||
// tileData = await MatrixState.pangeaController.analytics.getAnalytics(
|
||||
// defaultSelected: widget.defaultSelected,
|
||||
// selected: widget.selected,
|
||||
// forceUpdate: forceUpdate,
|
||||
// );
|
||||
// if (mounted) setState(() {});
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Room? room =
|
||||
Matrix.of(context).client.getRoomById(widget.selected.id);
|
||||
return Material(
|
||||
color: widget.isSelected
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Colors.transparent,
|
||||
child: ListTile(
|
||||
leading: widget.selected.type == AnalyticsEntryType.privateChats
|
||||
? CircleAvatar(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
radius: Avatar.defaultSize / 2,
|
||||
child: const Icon(Icons.forum),
|
||||
)
|
||||
: Avatar(
|
||||
mxContent: widget.avatar,
|
||||
name: widget.selected.displayName,
|
||||
littleIcon: room?.roomTypeIcon,
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.selected.displayName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: L10n.of(context)!.timeOfLastMessage,
|
||||
child: Text(
|
||||
tileData?.lastMessageTime?.localizedTimeShort(context) ?? "",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: ListSummaryAnalytics(
|
||||
chartAnalytics: tileData,
|
||||
),
|
||||
selected: widget.isSelected,
|
||||
onTap: () {
|
||||
if (widget.controller?.widget.selectedView == null) {
|
||||
widget.onTap(widget.selected);
|
||||
return;
|
||||
}
|
||||
if ((room?.isSpace ?? false) && widget.allowNavigateOnSelect) {
|
||||
context.go('/rooms/analytics/${room!.id}');
|
||||
return;
|
||||
}
|
||||
widget.onTap(widget.selected);
|
||||
},
|
||||
trailing: (room?.isSpace ?? false) &&
|
||||
widget.selected.type != AnalyticsEntryType.privateChats &&
|
||||
widget.allowNavigateOnSelect
|
||||
? const Icon(Icons.chevron_right)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final Room? room =
|
||||
// Matrix.of(context).client.getRoomById(widget.selected.id);
|
||||
// return Material(
|
||||
// color: widget.isSelected
|
||||
// ? Theme.of(context).colorScheme.secondaryContainer
|
||||
// : Colors.transparent,
|
||||
// child: ListTile(
|
||||
// leading: widget.selected.type == AnalyticsEntryType.privateChats
|
||||
// ? CircleAvatar(
|
||||
// backgroundColor: Theme.of(context).primaryColor,
|
||||
// foregroundColor: Colors.white,
|
||||
// radius: Avatar.defaultSize / 2,
|
||||
// child: const Icon(Icons.forum),
|
||||
// )
|
||||
// : Avatar(
|
||||
// mxContent: widget.avatar,
|
||||
// name: widget.selected.displayName,
|
||||
// littleIcon: room?.roomTypeIcon,
|
||||
// ),
|
||||
// title: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: Text(
|
||||
// widget.selected.displayName,
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// softWrap: false,
|
||||
// style: TextStyle(
|
||||
// fontWeight: FontWeight.bold,
|
||||
// color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Tooltip(
|
||||
// message: L10n.of(context)!.timeOfLastMessage,
|
||||
// child: Text(
|
||||
// tileData?.lastMessageTime?.localizedTimeShort(context) ?? "",
|
||||
// style: TextStyle(
|
||||
// fontSize: 13,
|
||||
// color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// subtitle: ListSummaryAnalytics(
|
||||
// chartAnalytics: tileData,
|
||||
// ),
|
||||
// selected: widget.isSelected,
|
||||
// onTap: () {
|
||||
// if (widget.controller?.widget.selectedView == null) {
|
||||
// widget.onTap(widget.selected);
|
||||
// return;
|
||||
// }
|
||||
// if ((room?.isSpace ?? false) && widget.allowNavigateOnSelect) {
|
||||
// context.go('/rooms/analytics/${room!.id}');
|
||||
// return;
|
||||
// }
|
||||
// widget.onTap(widget.selected);
|
||||
// },
|
||||
// trailing: (room?.isSpace ?? false) &&
|
||||
// widget.selected.type != AnalyticsEntryType.privateChats &&
|
||||
// widget.allowNavigateOnSelect
|
||||
// ? const Icon(Icons.chevron_right)
|
||||
// : null,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,214 +1,214 @@
|
|||
import 'dart:async';
|
||||
// import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics_view.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
// import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
// import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
// import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
// import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
// import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/base_analytics_view.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
// import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../controllers/pangea_controller.dart';
|
||||
import '../../enum/bar_chart_view_enum.dart';
|
||||
import '../../enum/time_span.dart';
|
||||
import '../../models/analytics/chart_analytics_model.dart';
|
||||
// import '../../../widgets/matrix.dart';
|
||||
// import '../../controllers/pangea_controller.dart';
|
||||
// import '../../enum/bar_chart_view_enum.dart';
|
||||
// import '../../enum/time_span.dart';
|
||||
// import '../../models/analytics/chart_analytics_model.dart';
|
||||
|
||||
class BaseAnalyticsPage extends StatefulWidget {
|
||||
final String pageTitle;
|
||||
final List<TabData> tabs;
|
||||
final BarChartViewSelection selectedView;
|
||||
// class BaseAnalyticsPage extends StatefulWidget {
|
||||
// final String pageTitle;
|
||||
// final List<TabData> tabs;
|
||||
// final BarChartViewSelection selectedView;
|
||||
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final AnalyticsSelected? alwaysSelected;
|
||||
final StudentAnalyticsController? myAnalyticsController;
|
||||
final List<LanguageModel> targetLanguages;
|
||||
// final AnalyticsSelected defaultSelected;
|
||||
// final AnalyticsSelected? alwaysSelected;
|
||||
// final StudentAnalyticsController? myAnalyticsController;
|
||||
// final List<LanguageModel> targetLanguages;
|
||||
|
||||
BaseAnalyticsPage({
|
||||
super.key,
|
||||
required this.pageTitle,
|
||||
required this.tabs,
|
||||
required this.alwaysSelected,
|
||||
required this.defaultSelected,
|
||||
required this.selectedView,
|
||||
this.myAnalyticsController,
|
||||
targetLanguages,
|
||||
}) : targetLanguages = (targetLanguages?.isNotEmpty ?? false)
|
||||
? targetLanguages
|
||||
: MatrixState.pangeaController.pLanguageStore.targetOptions;
|
||||
// BaseAnalyticsPage({
|
||||
// super.key,
|
||||
// required this.pageTitle,
|
||||
// required this.tabs,
|
||||
// required this.alwaysSelected,
|
||||
// required this.defaultSelected,
|
||||
// required this.selectedView,
|
||||
// this.myAnalyticsController,
|
||||
// targetLanguages,
|
||||
// }) : targetLanguages = (targetLanguages?.isNotEmpty ?? false)
|
||||
// ? targetLanguages
|
||||
// : MatrixState.pangeaController.pLanguageStore.targetOptions;
|
||||
|
||||
@override
|
||||
State<BaseAnalyticsPage> createState() => BaseAnalyticsController();
|
||||
}
|
||||
// @override
|
||||
// State<BaseAnalyticsPage> createState() => BaseAnalyticsController();
|
||||
// }
|
||||
|
||||
class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
AnalyticsSelected? selected;
|
||||
String? currentLemma;
|
||||
ChartAnalyticsModel? chartData;
|
||||
StreamController refreshStream = StreamController.broadcast();
|
||||
BarChartViewSelection currentView = BarChartViewSelection.messages;
|
||||
// class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
||||
// final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
// AnalyticsSelected? selected;
|
||||
// String? currentLemma;
|
||||
// ChartAnalyticsModel? chartData;
|
||||
// StreamController refreshStream = StreamController.broadcast();
|
||||
// BarChartViewSelection currentView = BarChartViewSelection.messages;
|
||||
|
||||
bool isSelected(String chatOrStudentId) => chatOrStudentId == selected?.id;
|
||||
// bool isSelected(String chatOrStudentId) => chatOrStudentId == selected?.id;
|
||||
|
||||
Room? get activeSpace {
|
||||
if (widget.defaultSelected.type == AnalyticsEntryType.space) {
|
||||
return Matrix.of(context).client.getRoomById(widget.defaultSelected.id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Room? get activeSpace {
|
||||
// if (widget.defaultSelected.type == AnalyticsEntryType.space) {
|
||||
// return Matrix.of(context).client.getRoomById(widget.defaultSelected.id);
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentView = widget.selectedView;
|
||||
if (widget.defaultSelected.type == AnalyticsEntryType.student) {
|
||||
runFirstRefresh();
|
||||
}
|
||||
setChartData();
|
||||
}
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// currentView = widget.selectedView;
|
||||
// if (widget.defaultSelected.type == AnalyticsEntryType.student) {
|
||||
// runFirstRefresh();
|
||||
// }
|
||||
// setChartData();
|
||||
// }
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant BaseAnalyticsPage oldWidget) {
|
||||
// when a user is a parent space's analytics and clicks on a subspace
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.defaultSelected.id != widget.defaultSelected.id) {
|
||||
setChartData();
|
||||
refreshStream.add(false);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// void didUpdateWidget(covariant BaseAnalyticsPage oldWidget) {
|
||||
// // when a user is a parent space's analytics and clicks on a subspace
|
||||
// super.didUpdateWidget(oldWidget);
|
||||
// if (oldWidget.defaultSelected.id != widget.defaultSelected.id) {
|
||||
// setChartData();
|
||||
// refreshStream.add(false);
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<void> runFirstRefresh() async {
|
||||
final analyticsRooms =
|
||||
pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
// Future<void> runFirstRefresh() async {
|
||||
// final analyticsRooms =
|
||||
// pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
|
||||
final List<AnalyticsEvent> analyticsEvent = [];
|
||||
for (final analyticsRoom in analyticsRooms) {
|
||||
final lastSummaryEvent = await analyticsRoom.getLastAnalyticsEvent(
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
Matrix.of(context).client.userID!,
|
||||
);
|
||||
final lastConstructEvent = await analyticsRoom.getLastAnalyticsEvent(
|
||||
PangeaEventTypes.construct,
|
||||
Matrix.of(context).client.userID!,
|
||||
);
|
||||
if (lastSummaryEvent != null) {
|
||||
analyticsEvent.add(lastSummaryEvent);
|
||||
}
|
||||
if (lastConstructEvent != null) {
|
||||
analyticsEvent.add(lastConstructEvent);
|
||||
}
|
||||
}
|
||||
// final List<AnalyticsEvent> analyticsEvent = [];
|
||||
// for (final analyticsRoom in analyticsRooms) {
|
||||
// final lastSummaryEvent = await analyticsRoom.getLastAnalyticsEvent(
|
||||
// PangeaEventTypes.summaryAnalytics,
|
||||
// Matrix.of(context).client.userID!,
|
||||
// );
|
||||
// final lastConstructEvent = await analyticsRoom.getLastAnalyticsEvent(
|
||||
// PangeaEventTypes.construct,
|
||||
// Matrix.of(context).client.userID!,
|
||||
// );
|
||||
// if (lastSummaryEvent != null) {
|
||||
// analyticsEvent.add(lastSummaryEvent);
|
||||
// }
|
||||
// if (lastConstructEvent != null) {
|
||||
// analyticsEvent.add(lastConstructEvent);
|
||||
// }
|
||||
// }
|
||||
|
||||
if (analyticsEvent.isNotEmpty) return;
|
||||
onRefresh();
|
||||
}
|
||||
// if (analyticsEvent.isNotEmpty) return;
|
||||
// onRefresh();
|
||||
// }
|
||||
|
||||
Future<void> onRefresh() async {
|
||||
// postframe callback to avoid calling this function during build
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
debugPrint("updating analytics");
|
||||
await pangeaController.myAnalytics.updateAnalytics();
|
||||
await setChartData(forceUpdate: true);
|
||||
refreshStream.add(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
// Future<void> onRefresh() async {
|
||||
// // postframe callback to avoid calling this function during build
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
// await showFutureLoadingDialog(
|
||||
// context: context,
|
||||
// future: () async {
|
||||
// debugPrint("updating analytics");
|
||||
// await pangeaController.myAnalytics.updateAnalytics();
|
||||
// await setChartData(forceUpdate: true);
|
||||
// refreshStream.add(true);
|
||||
// },
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
Future<ChartAnalyticsModel> fetchChartData(
|
||||
AnalyticsSelected? params, {
|
||||
forceUpdate = false,
|
||||
}) async {
|
||||
final ChartAnalyticsModel data =
|
||||
await pangeaController.analytics.getAnalytics(
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: params,
|
||||
forceUpdate: forceUpdate,
|
||||
);
|
||||
// Future<ChartAnalyticsModel> fetchChartData(
|
||||
// AnalyticsSelected? params, {
|
||||
// forceUpdate = false,
|
||||
// }) async {
|
||||
// final ChartAnalyticsModel data =
|
||||
// await pangeaController.analytics.getAnalytics(
|
||||
// defaultSelected: widget.defaultSelected,
|
||||
// selected: params,
|
||||
// forceUpdate: forceUpdate,
|
||||
// );
|
||||
|
||||
return data;
|
||||
}
|
||||
// return data;
|
||||
// }
|
||||
|
||||
Future<void> setChartData({forceUpdate = false}) async {
|
||||
final ChartAnalyticsModel newData = await fetchChartData(
|
||||
selected,
|
||||
forceUpdate: forceUpdate,
|
||||
);
|
||||
setState(() => chartData = newData);
|
||||
}
|
||||
// Future<void> setChartData({forceUpdate = false}) async {
|
||||
// final ChartAnalyticsModel newData = await fetchChartData(
|
||||
// selected,
|
||||
// forceUpdate: forceUpdate,
|
||||
// );
|
||||
// setState(() => chartData = newData);
|
||||
// }
|
||||
|
||||
TimeSpan get currentTimeSpan =>
|
||||
pangeaController.analytics.currentAnalyticsTimeSpan;
|
||||
// TimeSpan get currentTimeSpan =>
|
||||
// pangeaController.analytics.currentAnalyticsTimeSpan;
|
||||
|
||||
Future<void> toggleSelection(AnalyticsSelected selectedParam) async {
|
||||
setState(() {
|
||||
debugPrint("selectedParam.id is ${selectedParam.id}");
|
||||
currentLemma = null;
|
||||
selected = isSelected(selectedParam.id) ? null : selectedParam;
|
||||
});
|
||||
await setChartData();
|
||||
refreshStream.add(false);
|
||||
Future.delayed(Duration.zero, () => setState(() {}));
|
||||
}
|
||||
// Future<void> toggleSelection(AnalyticsSelected selectedParam) async {
|
||||
// setState(() {
|
||||
// debugPrint("selectedParam.id is ${selectedParam.id}");
|
||||
// currentLemma = null;
|
||||
// selected = isSelected(selectedParam.id) ? null : selectedParam;
|
||||
// });
|
||||
// await setChartData();
|
||||
// refreshStream.add(false);
|
||||
// Future.delayed(Duration.zero, () => setState(() {}));
|
||||
// }
|
||||
|
||||
Future<void> toggleTimeSpan(BuildContext context, TimeSpan timeSpan) async {
|
||||
await pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
|
||||
await setChartData();
|
||||
refreshStream.add(false);
|
||||
}
|
||||
// Future<void> toggleTimeSpan(BuildContext context, TimeSpan timeSpan) async {
|
||||
// await pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
|
||||
// await setChartData();
|
||||
// refreshStream.add(false);
|
||||
// }
|
||||
|
||||
Future<void> toggleSpaceLang(LanguageModel lang) async {
|
||||
await pangeaController.analytics.setCurrentAnalyticsLang(lang);
|
||||
await setChartData();
|
||||
refreshStream.add(false);
|
||||
}
|
||||
// Future<void> toggleSpaceLang(LanguageModel lang) async {
|
||||
// await pangeaController.analytics.setCurrentAnalyticsLang(lang);
|
||||
// await setChartData();
|
||||
// refreshStream.add(false);
|
||||
// }
|
||||
|
||||
Future<void> toggleView(BarChartViewSelection view) async {
|
||||
currentView = view;
|
||||
await setChartData();
|
||||
refreshStream.add(false);
|
||||
}
|
||||
// Future<void> toggleView(BarChartViewSelection view) async {
|
||||
// currentView = view;
|
||||
// await setChartData();
|
||||
// refreshStream.add(false);
|
||||
// }
|
||||
|
||||
void setCurrentLemma(String? lemma) {
|
||||
currentLemma = lemma;
|
||||
setState(() {});
|
||||
refreshStream.add(false);
|
||||
}
|
||||
// void setCurrentLemma(String? lemma) {
|
||||
// currentLemma = lemma;
|
||||
// setState(() {});
|
||||
// refreshStream.add(false);
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BaseAnalyticsView(controller: this);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return BaseAnalyticsView(controller: this);
|
||||
// }
|
||||
// }
|
||||
|
||||
class TabData {
|
||||
AnalyticsEntryType type;
|
||||
IconData icon;
|
||||
List<TabItem> items;
|
||||
bool allowNavigateOnSelect;
|
||||
// class TabData {
|
||||
// AnalyticsEntryType type;
|
||||
// IconData icon;
|
||||
// List<TabItem> items;
|
||||
// bool allowNavigateOnSelect;
|
||||
|
||||
TabData({
|
||||
required this.type,
|
||||
required this.items,
|
||||
required this.icon,
|
||||
this.allowNavigateOnSelect = true,
|
||||
});
|
||||
}
|
||||
// TabData({
|
||||
// required this.type,
|
||||
// required this.items,
|
||||
// required this.icon,
|
||||
// this.allowNavigateOnSelect = true,
|
||||
// });
|
||||
// }
|
||||
|
||||
class TabItem {
|
||||
Uri? avatar;
|
||||
String displayName;
|
||||
String id;
|
||||
// class TabItem {
|
||||
// Uri? avatar;
|
||||
// String displayName;
|
||||
// String id;
|
||||
|
||||
TabItem({required this.avatar, required this.displayName, required this.id});
|
||||
}
|
||||
// TabItem({required this.avatar, required this.displayName, required this.id});
|
||||
// }
|
||||
|
||||
enum AnalyticsEntryType { student, room, space, privateChats }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,243 +1,243 @@
|
|||
import 'dart:math';
|
||||
// import 'dart:math';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/analytics_view_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/construct_list.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/messages_bar_chart.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
// import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
// import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
// import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/analytics_view_button.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/construct_list.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/messages_bar_chart.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart';
|
||||
// import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
// import 'package:flutter/gestures.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
// import 'package:go_router/go_router.dart';
|
||||
|
||||
class BaseAnalyticsView extends StatelessWidget {
|
||||
const BaseAnalyticsView({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
// class BaseAnalyticsView extends StatelessWidget {
|
||||
// const BaseAnalyticsView({
|
||||
// super.key,
|
||||
// required this.controller,
|
||||
// });
|
||||
|
||||
final BaseAnalyticsController controller;
|
||||
// final BaseAnalyticsController controller;
|
||||
|
||||
Widget chartView(BuildContext context) {
|
||||
switch (controller.currentView) {
|
||||
case BarChartViewSelection.messages:
|
||||
return MessagesBarChart(
|
||||
chartAnalytics: controller.chartData,
|
||||
);
|
||||
case BarChartViewSelection.grammar:
|
||||
return ConstructList(
|
||||
constructType: ConstructTypeEnum.grammar,
|
||||
defaultSelected: controller.widget.defaultSelected,
|
||||
selected: controller.selected,
|
||||
controller: controller,
|
||||
pangeaController: controller.pangeaController,
|
||||
refreshStream: controller.refreshStream,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Widget chartView(BuildContext context) {
|
||||
// switch (controller.currentView) {
|
||||
// case BarChartViewSelection.messages:
|
||||
// return MessagesBarChart(
|
||||
// chartAnalytics: controller.chartData,
|
||||
// );
|
||||
// case BarChartViewSelection.grammar:
|
||||
// return ConstructList(
|
||||
// constructType: ConstructTypeEnum.grammar,
|
||||
// defaultSelected: controller.widget.defaultSelected,
|
||||
// selected: controller.selected,
|
||||
// controller: controller,
|
||||
// pangeaController: controller.pangeaController,
|
||||
// refreshStream: controller.refreshStream,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: controller.widget.pageTitle,
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
final String route =
|
||||
"/rooms/${controller.widget.defaultSelected.type.route}";
|
||||
context.go(route);
|
||||
},
|
||||
),
|
||||
if (controller.activeSpace != null)
|
||||
const TextSpan(
|
||||
text: " > ",
|
||||
),
|
||||
if (controller.activeSpace != null)
|
||||
TextSpan(
|
||||
text: controller.activeSpace!.getLocalizedDisplayname(),
|
||||
),
|
||||
const TextSpan(
|
||||
text: " > ",
|
||||
),
|
||||
TextSpan(
|
||||
text: controller.currentView.string(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TimeSpanMenuButton(
|
||||
value: controller.currentTimeSpan,
|
||||
onChange: (TimeSpan value) =>
|
||||
controller.toggleTimeSpan(context, value),
|
||||
),
|
||||
AnalyticsViewButton(
|
||||
value: controller.currentView,
|
||||
onChange: controller.toggleView,
|
||||
),
|
||||
AnalyticsLanguageButton(
|
||||
value: controller
|
||||
.pangeaController.analytics.currentAnalyticsLang,
|
||||
onChange: (lang) => controller.toggleSpaceLang(lang),
|
||||
languages: controller.widget.targetLanguages,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: chartView(context),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: DefaultTabController(
|
||||
length: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
tabs: [
|
||||
...controller.widget.tabs.map(
|
||||
(tab) => Tab(
|
||||
icon: Icon(
|
||||
tab.icon,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: max(
|
||||
controller.widget.tabs[0].items.length + 1,
|
||||
controller.widget.tabs[1].items.length,
|
||||
) *
|
||||
72,
|
||||
child: TabBarView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
...controller.widget.tabs[0].items.map(
|
||||
(item) => AnalyticsListTile(
|
||||
refreshStream: controller.refreshStream,
|
||||
avatar: item.avatar,
|
||||
defaultSelected:
|
||||
controller.widget.defaultSelected,
|
||||
selected: AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[0].type,
|
||||
item.displayName,
|
||||
),
|
||||
isSelected:
|
||||
controller.isSelected(item.id),
|
||||
onTap: (_) => controller.toggleSelection(
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[0].type,
|
||||
item.displayName,
|
||||
),
|
||||
),
|
||||
allowNavigateOnSelect: controller
|
||||
.widget.tabs[0].allowNavigateOnSelect,
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
if (controller.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.space)
|
||||
AnalyticsListTile(
|
||||
refreshStream: controller.refreshStream,
|
||||
defaultSelected:
|
||||
controller.widget.defaultSelected,
|
||||
avatar: null,
|
||||
selected: AnalyticsSelected(
|
||||
controller.widget.defaultSelected.id,
|
||||
AnalyticsEntryType.privateChats,
|
||||
L10n.of(context)!.allPrivateChats,
|
||||
),
|
||||
allowNavigateOnSelect: false,
|
||||
isSelected: controller.isSelected(
|
||||
controller.widget.defaultSelected.id,
|
||||
),
|
||||
onTap: controller.toggleSelection,
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: controller.widget.tabs[1].items
|
||||
.map(
|
||||
(item) => AnalyticsListTile(
|
||||
refreshStream: controller.refreshStream,
|
||||
avatar: item.avatar,
|
||||
defaultSelected:
|
||||
controller.widget.defaultSelected,
|
||||
selected: AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[1].type,
|
||||
item.displayName,
|
||||
),
|
||||
isSelected:
|
||||
controller.isSelected(item.id),
|
||||
onTap: controller.toggleSelection,
|
||||
allowNavigateOnSelect: controller.widget
|
||||
.tabs[1].allowNavigateOnSelect,
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
controller: controller,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// appBar: AppBar(
|
||||
// centerTitle: true,
|
||||
// title: RichText(
|
||||
// text: TextSpan(
|
||||
// style: TextStyle(
|
||||
// color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
// fontSize: 18,
|
||||
// fontWeight: FontWeight.w700,
|
||||
// ),
|
||||
// children: [
|
||||
// TextSpan(
|
||||
// text: controller.widget.pageTitle,
|
||||
// style: const TextStyle(decoration: TextDecoration.underline),
|
||||
// recognizer: TapGestureRecognizer()
|
||||
// ..onTap = () {
|
||||
// final String route =
|
||||
// "/rooms/${controller.widget.defaultSelected.type.route}";
|
||||
// context.go(route);
|
||||
// },
|
||||
// ),
|
||||
// if (controller.activeSpace != null)
|
||||
// const TextSpan(
|
||||
// text: " > ",
|
||||
// ),
|
||||
// if (controller.activeSpace != null)
|
||||
// TextSpan(
|
||||
// text: controller.activeSpace!.getLocalizedDisplayname(),
|
||||
// ),
|
||||
// const TextSpan(
|
||||
// text: " > ",
|
||||
// ),
|
||||
// TextSpan(
|
||||
// text: controller.currentView.string(context),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
// ),
|
||||
// body: MaxWidthBody(
|
||||
// withScrolling: false,
|
||||
// child: Column(
|
||||
// children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// TimeSpanMenuButton(
|
||||
// value: controller.currentTimeSpan,
|
||||
// onChange: (TimeSpan value) =>
|
||||
// controller.toggleTimeSpan(context, value),
|
||||
// ),
|
||||
// AnalyticsViewButton(
|
||||
// value: controller.currentView,
|
||||
// onChange: controller.toggleView,
|
||||
// ),
|
||||
// AnalyticsLanguageButton(
|
||||
// value: controller
|
||||
// .pangeaController.analytics.currentAnalyticsLang,
|
||||
// onChange: (lang) => controller.toggleSpaceLang(lang),
|
||||
// languages: controller.widget.targetLanguages,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 10,
|
||||
// ),
|
||||
// Expanded(
|
||||
// flex: 1,
|
||||
// child: chartView(context),
|
||||
// ),
|
||||
// Expanded(
|
||||
// flex: 1,
|
||||
// child: DefaultTabController(
|
||||
// length: 2,
|
||||
// child: Column(
|
||||
// children: [
|
||||
// TabBar(
|
||||
// tabs: [
|
||||
// ...controller.widget.tabs.map(
|
||||
// (tab) => Tab(
|
||||
// icon: Icon(
|
||||
// tab.icon,
|
||||
// color: Theme.of(context)
|
||||
// .colorScheme
|
||||
// .onSurfaceVariant,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: SingleChildScrollView(
|
||||
// child: SizedBox(
|
||||
// height: max(
|
||||
// controller.widget.tabs[0].items.length + 1,
|
||||
// controller.widget.tabs[1].items.length,
|
||||
// ) *
|
||||
// 72,
|
||||
// child: TabBarView(
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
// children: [
|
||||
// Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
// children: [
|
||||
// ...controller.widget.tabs[0].items.map(
|
||||
// (item) => AnalyticsListTile(
|
||||
// refreshStream: controller.refreshStream,
|
||||
// avatar: item.avatar,
|
||||
// defaultSelected:
|
||||
// controller.widget.defaultSelected,
|
||||
// selected: AnalyticsSelected(
|
||||
// item.id,
|
||||
// controller.widget.tabs[0].type,
|
||||
// item.displayName,
|
||||
// ),
|
||||
// isSelected:
|
||||
// controller.isSelected(item.id),
|
||||
// onTap: (_) => controller.toggleSelection(
|
||||
// AnalyticsSelected(
|
||||
// item.id,
|
||||
// controller.widget.tabs[0].type,
|
||||
// item.displayName,
|
||||
// ),
|
||||
// ),
|
||||
// allowNavigateOnSelect: controller
|
||||
// .widget.tabs[0].allowNavigateOnSelect,
|
||||
// pangeaController:
|
||||
// controller.pangeaController,
|
||||
// controller: controller,
|
||||
// ),
|
||||
// ),
|
||||
// if (controller.widget.defaultSelected.type ==
|
||||
// AnalyticsEntryType.space)
|
||||
// AnalyticsListTile(
|
||||
// refreshStream: controller.refreshStream,
|
||||
// defaultSelected:
|
||||
// controller.widget.defaultSelected,
|
||||
// avatar: null,
|
||||
// selected: AnalyticsSelected(
|
||||
// controller.widget.defaultSelected.id,
|
||||
// AnalyticsEntryType.privateChats,
|
||||
// L10n.of(context)!.allPrivateChats,
|
||||
// ),
|
||||
// allowNavigateOnSelect: false,
|
||||
// isSelected: controller.isSelected(
|
||||
// controller.widget.defaultSelected.id,
|
||||
// ),
|
||||
// onTap: controller.toggleSelection,
|
||||
// pangeaController:
|
||||
// controller.pangeaController,
|
||||
// controller: controller,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
// children: controller.widget.tabs[1].items
|
||||
// .map(
|
||||
// (item) => AnalyticsListTile(
|
||||
// refreshStream: controller.refreshStream,
|
||||
// avatar: item.avatar,
|
||||
// defaultSelected:
|
||||
// controller.widget.defaultSelected,
|
||||
// selected: AnalyticsSelected(
|
||||
// item.id,
|
||||
// controller.widget.tabs[1].type,
|
||||
// item.displayName,
|
||||
// ),
|
||||
// isSelected:
|
||||
// controller.isSelected(item.id),
|
||||
// onTap: controller.toggleSelection,
|
||||
// allowNavigateOnSelect: controller.widget
|
||||
// .tabs[1].allowNavigateOnSelect,
|
||||
// pangeaController:
|
||||
// controller.pangeaController,
|
||||
// controller: controller,
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
|
|
@ -22,7 +23,7 @@ class ConstructList extends StatefulWidget {
|
|||
final ConstructTypeEnum constructType;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final AnalyticsSelected? selected;
|
||||
final BaseAnalyticsController controller;
|
||||
final TimeSpan timeSpan;
|
||||
final PangeaController pangeaController;
|
||||
final StreamController refreshStream;
|
||||
|
||||
|
|
@ -30,9 +31,9 @@ class ConstructList extends StatefulWidget {
|
|||
super.key,
|
||||
required this.constructType,
|
||||
required this.defaultSelected,
|
||||
required this.controller,
|
||||
required this.pangeaController,
|
||||
required this.refreshStream,
|
||||
required this.timeSpan,
|
||||
this.selected,
|
||||
});
|
||||
|
||||
|
|
@ -53,11 +54,11 @@ class ConstructListState extends State<ConstructList> {
|
|||
: Column(
|
||||
children: [
|
||||
ConstructListView(
|
||||
controller: widget.controller,
|
||||
pangeaController: widget.pangeaController,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
refreshStream: widget.refreshStream,
|
||||
timeSpan: widget.timeSpan,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -74,17 +75,17 @@ class ConstructListState extends State<ConstructList> {
|
|||
// subtitle = total uses, equal to construct.content.uses.length
|
||||
// list has a fixed height of 400 and is scrollable
|
||||
class ConstructListView extends StatefulWidget {
|
||||
final BaseAnalyticsController controller;
|
||||
final PangeaController pangeaController;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final TimeSpan timeSpan;
|
||||
final AnalyticsSelected? selected;
|
||||
final StreamController refreshStream;
|
||||
|
||||
const ConstructListView({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.pangeaController,
|
||||
required this.defaultSelected,
|
||||
required this.timeSpan,
|
||||
required this.refreshStream,
|
||||
this.selected,
|
||||
});
|
||||
|
|
@ -101,6 +102,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
bool fetchingConstructs = true;
|
||||
bool fetchingUses = false;
|
||||
StreamSubscription? refreshSubscription;
|
||||
String? currentLemma;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -112,6 +114,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: true,
|
||||
timeSpan: widget.timeSpan,
|
||||
)
|
||||
.whenComplete(() => setState(() => fetchingConstructs = false))
|
||||
.then((value) => setState(() => _constructs = value));
|
||||
|
|
@ -127,6 +130,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: true,
|
||||
timeSpan: widget.timeSpan,
|
||||
)
|
||||
.then(
|
||||
(value) => setState(() {
|
||||
|
|
@ -143,9 +147,14 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
void setCurrentLemma(String? lemma) {
|
||||
currentLemma = lemma;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
int get lemmaIndex =>
|
||||
constructs?.indexWhere(
|
||||
(element) => element.lemma == widget.controller.currentLemma,
|
||||
(element) => element.lemma == currentLemma,
|
||||
) ??
|
||||
-1;
|
||||
|
||||
|
|
@ -258,7 +267,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
}
|
||||
|
||||
ConstructUses? get currentConstruct => constructs?.firstWhereOrNull(
|
||||
(element) => element.lemma == widget.controller.currentLemma,
|
||||
(element) => element.lemma == currentLemma,
|
||||
);
|
||||
|
||||
// given the current lemma and list of message events, return a list of
|
||||
|
|
@ -266,7 +275,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
// this is because some message events may have has more than one PangeaMatch of a
|
||||
// given lemma type.
|
||||
List<MessageEventMatch> getMessageEventMatches() {
|
||||
if (widget.controller.currentLemma == null) return [];
|
||||
if (currentLemma == null) return [];
|
||||
final List<MessageEventMatch> allMsgErrorSteps = [];
|
||||
|
||||
for (final msgEvent in _msgEvents) {
|
||||
|
|
@ -277,7 +286,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
}
|
||||
// get all the pangea matches in that message which have that lemma
|
||||
final List<PangeaMatch>? msgErrorSteps = msgEvent.errorSteps(
|
||||
widget.controller.currentLemma!,
|
||||
currentLemma!,
|
||||
);
|
||||
if (msgErrorSteps == null) continue;
|
||||
|
||||
|
|
@ -327,7 +336,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
),
|
||||
onTap: () async {
|
||||
final String lemma = constructs![index].lemma;
|
||||
widget.controller.setCurrentLemma(lemma);
|
||||
setCurrentLemma(lemma);
|
||||
fetchUses().then((_) => showConstructMessagesDialog());
|
||||
},
|
||||
);
|
||||
|
|
@ -346,7 +355,7 @@ class ConstructMessagesDialog extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.widget.controller.currentLemma == null ||
|
||||
if (controller.currentLemma == null ||
|
||||
controller.constructs == null ||
|
||||
controller.lemmaIndex < 0 ||
|
||||
controller.lemmaIndex >= controller.constructs!.length) {
|
||||
|
|
@ -359,7 +368,7 @@ class ConstructMessagesDialog extends StatelessWidget {
|
|||
controller._msgEvents.length;
|
||||
|
||||
return AlertDialog(
|
||||
title: Center(child: Text(controller.widget.controller.currentLemma!)),
|
||||
title: Center(child: Text(controller.currentLemma!)),
|
||||
content: SizedBox(
|
||||
height: noData ? 90 : 250,
|
||||
width: noData ? 200 : 400,
|
||||
|
|
@ -380,7 +389,7 @@ class ConstructMessagesDialog extends StatelessWidget {
|
|||
children: [
|
||||
ConstructMessage(
|
||||
msgEvent: event.msgEvent,
|
||||
lemma: controller.widget.controller.currentLemma!,
|
||||
lemma: controller.currentLemma!,
|
||||
errorMessage: event.lemmaMatch,
|
||||
),
|
||||
if (index < msgEventMatches.length - 1)
|
||||
|
|
@ -528,42 +537,37 @@ class ConstructMessageBubble extends StatelessWidget {
|
|||
vertical: 8,
|
||||
),
|
||||
child: RichText(
|
||||
text: (end == null)
|
||||
? TextSpan(
|
||||
text: errorText,
|
||||
style: defaultStyle,
|
||||
)
|
||||
: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: errorText.substring(0, start),
|
||||
style: defaultStyle,
|
||||
),
|
||||
TextSpan(
|
||||
text: errorText.substring(start, end),
|
||||
style: defaultStyle.merge(
|
||||
TextStyle(
|
||||
backgroundColor: Colors.red.withOpacity(0.25),
|
||||
decoration: TextDecoration.lineThrough,
|
||||
decorationThickness: 2.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
const TextSpan(text: " "),
|
||||
TextSpan(
|
||||
text: replacementText,
|
||||
style: defaultStyle.merge(
|
||||
TextStyle(
|
||||
backgroundColor: Colors.green.withOpacity(0.25),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: errorText.substring(end),
|
||||
style: defaultStyle,
|
||||
),
|
||||
],
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: errorText.substring(0, start),
|
||||
style: defaultStyle,
|
||||
),
|
||||
TextSpan(
|
||||
text: errorText.substring(start, end),
|
||||
style: defaultStyle.merge(
|
||||
TextStyle(
|
||||
backgroundColor: Colors.red.withOpacity(0.25),
|
||||
decoration: TextDecoration.lineThrough,
|
||||
decorationThickness: 2.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
const TextSpan(text: " "),
|
||||
TextSpan(
|
||||
text: replacementText,
|
||||
style: defaultStyle.merge(
|
||||
TextStyle(
|
||||
backgroundColor: Colors.green.withOpacity(0.25),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: errorText.substring(end),
|
||||
style: defaultStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,101 +1,101 @@
|
|||
import 'dart:math';
|
||||
// import 'dart:math';
|
||||
|
||||
import 'package:fluffychat/pangea/models/analytics/chart_analytics_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
// import 'package:fluffychat/pangea/models/analytics/chart_analytics_model.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../../enum/use_type.dart';
|
||||
// import '../../enum/use_type.dart';
|
||||
|
||||
class ListSummaryAnalytics extends StatelessWidget {
|
||||
final ChartAnalyticsModel? chartAnalytics;
|
||||
// class ListSummaryAnalytics extends StatelessWidget {
|
||||
// final ChartAnalyticsModel? chartAnalytics;
|
||||
|
||||
const ListSummaryAnalytics({super.key, this.chartAnalytics});
|
||||
// const ListSummaryAnalytics({super.key, this.chartAnalytics});
|
||||
|
||||
TimeSeriesTotals? get totals => chartAnalytics?.totals;
|
||||
// TimeSeriesTotals? get totals => chartAnalytics?.totals;
|
||||
|
||||
String spacer(int baseLength, int number) =>
|
||||
" " * max(baseLength - number.toString().length, 0);
|
||||
// String spacer(int baseLength, int number) =>
|
||||
// " " * max(baseLength - number.toString().length, 0);
|
||||
|
||||
WidgetSpan spacerIconText(
|
||||
String toolTip,
|
||||
String space,
|
||||
IconData icon,
|
||||
int value,
|
||||
Color? color, [
|
||||
percentage = true,
|
||||
]) =>
|
||||
WidgetSpan(
|
||||
child: Tooltip(
|
||||
message: toolTip,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: space,
|
||||
),
|
||||
WidgetSpan(child: Icon(icon, size: 14, color: color)),
|
||||
TextSpan(
|
||||
text: " $value${percentage ? "%" : ""}",
|
||||
style: TextStyle(color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// WidgetSpan spacerIconText(
|
||||
// String toolTip,
|
||||
// String space,
|
||||
// IconData icon,
|
||||
// int value,
|
||||
// Color? color, [
|
||||
// percentage = true,
|
||||
// ]) =>
|
||||
// WidgetSpan(
|
||||
// child: Tooltip(
|
||||
// message: toolTip,
|
||||
// child: RichText(
|
||||
// text: TextSpan(
|
||||
// children: [
|
||||
// TextSpan(
|
||||
// text: space,
|
||||
// ),
|
||||
// WidgetSpan(child: Icon(icon, size: 14, color: color)),
|
||||
// TextSpan(
|
||||
// text: " $value${percentage ? "%" : ""}",
|
||||
// style: TextStyle(color: color),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (totals == null) {
|
||||
return const LinearProgressIndicator();
|
||||
}
|
||||
final l10n = L10n.of(context);
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// if (totals == null) {
|
||||
// return const LinearProgressIndicator();
|
||||
// }
|
||||
// final l10n = L10n.of(context);
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
spacerIconText(
|
||||
L10n.of(context) != null
|
||||
? L10n.of(context)!.totalMessages
|
||||
: "Total messages sent",
|
||||
"",
|
||||
Icons.chat_bubble,
|
||||
totals!.all,
|
||||
Theme.of(context).textTheme.bodyLarge!.color,
|
||||
false,
|
||||
),
|
||||
if (totals!.all != 0) ...[
|
||||
spacerIconText(
|
||||
l10n != null ? l10n.taTooltip : "With translation assistance",
|
||||
spacer(8, totals!.all),
|
||||
UseType.ta.iconData,
|
||||
totals!.taPercent,
|
||||
UseType.ta.color(context),
|
||||
),
|
||||
spacerIconText(
|
||||
l10n != null ? l10n.gaTooltip : "With grammar assistance",
|
||||
spacer(4, totals!.taPercent),
|
||||
UseType.ga.iconData,
|
||||
totals!.gaPercent,
|
||||
UseType.ga.color(context),
|
||||
),
|
||||
spacerIconText(
|
||||
l10n != null ? l10n.waTooltip : "Without assistance",
|
||||
spacer(4, totals!.gaPercent),
|
||||
UseType.wa.iconData,
|
||||
totals!.waPercent,
|
||||
UseType.wa.color(context),
|
||||
),
|
||||
spacerIconText(
|
||||
l10n != null ? l10n.unTooltip : "Other",
|
||||
spacer(4, totals!.waPercent),
|
||||
UseType.un.iconData,
|
||||
totals!.unPercent,
|
||||
UseType.un.color(context),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// return RichText(
|
||||
// text: TextSpan(
|
||||
// children: [
|
||||
// spacerIconText(
|
||||
// L10n.of(context) != null
|
||||
// ? L10n.of(context)!.totalMessages
|
||||
// : "Total messages sent",
|
||||
// "",
|
||||
// Icons.chat_bubble,
|
||||
// totals!.all,
|
||||
// Theme.of(context).textTheme.bodyLarge!.color,
|
||||
// false,
|
||||
// ),
|
||||
// if (totals!.all != 0) ...[
|
||||
// spacerIconText(
|
||||
// l10n != null ? l10n.taTooltip : "With translation assistance",
|
||||
// spacer(8, totals!.all),
|
||||
// UseType.ta.iconData,
|
||||
// totals!.taPercent,
|
||||
// UseType.ta.color(context),
|
||||
// ),
|
||||
// spacerIconText(
|
||||
// l10n != null ? l10n.gaTooltip : "With grammar assistance",
|
||||
// spacer(4, totals!.taPercent),
|
||||
// UseType.ga.iconData,
|
||||
// totals!.gaPercent,
|
||||
// UseType.ga.color(context),
|
||||
// ),
|
||||
// spacerIconText(
|
||||
// l10n != null ? l10n.waTooltip : "Without assistance",
|
||||
// spacer(4, totals!.gaPercent),
|
||||
// UseType.wa.iconData,
|
||||
// totals!.waPercent,
|
||||
// UseType.wa.color(context),
|
||||
// ),
|
||||
// spacerIconText(
|
||||
// l10n != null ? l10n.unTooltip : "Other",
|
||||
// spacer(4, totals!.waPercent),
|
||||
// UseType.un.iconData,
|
||||
// totals!.unPercent,
|
||||
// UseType.un.color(context),
|
||||
// ),
|
||||
// ],
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,402 +1,402 @@
|
|||
import 'dart:developer';
|
||||
// import 'dart:developer';
|
||||
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/bar_chart_placeholder_data.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
// import 'package:fl_chart/fl_chart.dart';
|
||||
// import 'package:fluffychat/config/themes.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/bar_chart_placeholder_data.dart';
|
||||
// import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:intl/intl.dart';
|
||||
|
||||
import '../../enum/time_span.dart';
|
||||
import '../../enum/use_type.dart';
|
||||
import '../../models/analytics/chart_analytics_model.dart';
|
||||
import 'bar_chart_card.dart';
|
||||
import 'messages_legend_widget.dart';
|
||||
// import '../../enum/time_span.dart';
|
||||
// import '../../enum/use_type.dart';
|
||||
// import '../../models/analytics/chart_analytics_model.dart';
|
||||
// import 'bar_chart_card.dart';
|
||||
// import 'messages_legend_widget.dart';
|
||||
|
||||
class MessagesBarChart extends StatefulWidget {
|
||||
final ChartAnalyticsModel? chartAnalytics;
|
||||
// class MessagesBarChart extends StatefulWidget {
|
||||
// final ChartAnalyticsModel? chartAnalytics;
|
||||
|
||||
const MessagesBarChart({
|
||||
super.key,
|
||||
required this.chartAnalytics,
|
||||
});
|
||||
// const MessagesBarChart({
|
||||
// super.key,
|
||||
// required this.chartAnalytics,
|
||||
// });
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => MessagesBarChartState();
|
||||
}
|
||||
// @override
|
||||
// State<StatefulWidget> createState() => MessagesBarChartState();
|
||||
// }
|
||||
|
||||
class MessagesBarChartState extends State<MessagesBarChart> {
|
||||
final double barSpace = 16;
|
||||
final List<List<TimeSeriesInterval>> intervalGroupings = [];
|
||||
// class MessagesBarChartState extends State<MessagesBarChart> {
|
||||
// final double barSpace = 16;
|
||||
// final List<List<TimeSeriesInterval>> intervalGroupings = [];
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
}
|
||||
// @override
|
||||
// initState() {
|
||||
// super.initState();
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final flLine = FlLine(
|
||||
color: Theme.of(context).dividerColor,
|
||||
strokeWidth: 1,
|
||||
);
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final flLine = FlLine(
|
||||
// color: Theme.of(context).dividerColor,
|
||||
// strokeWidth: 1,
|
||||
// );
|
||||
|
||||
final flTitlesData = FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 28,
|
||||
getTitlesWidget: bottomTitles,
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
getTitlesWidget: leftTitles,
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
);
|
||||
final barChartData = BarChartData(
|
||||
alignment: BarChartAlignment.spaceEvenly,
|
||||
barTouchData: BarTouchData(
|
||||
enabled: false,
|
||||
),
|
||||
// barTouchData: barTouchData,
|
||||
titlesData: flTitlesData,
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
// checkToShowHorizontalLine: (value) => value % 10 == 0,
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
getDrawingHorizontalLine: (value) => flLine,
|
||||
checkToShowVerticalLine: (value) => false,
|
||||
getDrawingVerticalLine: (value) => flLine,
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: false,
|
||||
),
|
||||
groupsSpace: barSpace,
|
||||
barGroups: barChartGroupData,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
);
|
||||
final barChart = BarChart(
|
||||
barChartData,
|
||||
swapAnimationDuration: const Duration(milliseconds: 250),
|
||||
);
|
||||
// final flTitlesData = FlTitlesData(
|
||||
// show: true,
|
||||
// bottomTitles: AxisTitles(
|
||||
// sideTitles: SideTitles(
|
||||
// showTitles: true,
|
||||
// reservedSize: 28,
|
||||
// getTitlesWidget: bottomTitles,
|
||||
// ),
|
||||
// ),
|
||||
// leftTitles: AxisTitles(
|
||||
// sideTitles: SideTitles(
|
||||
// showTitles: true,
|
||||
// reservedSize: 40,
|
||||
// getTitlesWidget: leftTitles,
|
||||
// ),
|
||||
// ),
|
||||
// topTitles: const AxisTitles(
|
||||
// sideTitles: SideTitles(showTitles: false),
|
||||
// ),
|
||||
// rightTitles: const AxisTitles(
|
||||
// sideTitles: SideTitles(showTitles: false),
|
||||
// ),
|
||||
// );
|
||||
// final barChartData = BarChartData(
|
||||
// alignment: BarChartAlignment.spaceEvenly,
|
||||
// barTouchData: BarTouchData(
|
||||
// enabled: false,
|
||||
// ),
|
||||
// // barTouchData: barTouchData,
|
||||
// titlesData: flTitlesData,
|
||||
// gridData: FlGridData(
|
||||
// show: true,
|
||||
// // checkToShowHorizontalLine: (value) => value % 10 == 0,
|
||||
// checkToShowHorizontalLine: (value) => true,
|
||||
// getDrawingHorizontalLine: (value) => flLine,
|
||||
// checkToShowVerticalLine: (value) => false,
|
||||
// getDrawingVerticalLine: (value) => flLine,
|
||||
// ),
|
||||
// borderData: FlBorderData(
|
||||
// show: false,
|
||||
// ),
|
||||
// groupsSpace: barSpace,
|
||||
// barGroups: barChartGroupData,
|
||||
// backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
// );
|
||||
// final barChart = BarChart(
|
||||
// barChartData,
|
||||
// swapAnimationDuration: const Duration(milliseconds: 250),
|
||||
// );
|
||||
|
||||
return BarChartCard(
|
||||
barChart: barChart,
|
||||
loadingData: widget.chartAnalytics == null,
|
||||
legend: const MessagesLegendsListWidget(),
|
||||
);
|
||||
}
|
||||
// return BarChartCard(
|
||||
// barChart: barChart,
|
||||
// loadingData: widget.chartAnalytics == null,
|
||||
// legend: const MessagesLegendsListWidget(),
|
||||
// );
|
||||
// }
|
||||
|
||||
bool showLabelBasedOnTimeSpan(
|
||||
TimeSpan timeSpan,
|
||||
TimeSeriesInterval current,
|
||||
TimeSeriesInterval? last,
|
||||
int labelIndex,
|
||||
) {
|
||||
switch (timeSpan) {
|
||||
case TimeSpan.day:
|
||||
return current.end.hour % 3 == 0;
|
||||
case TimeSpan.month:
|
||||
if (current.end.month != last?.end.month) {
|
||||
return true;
|
||||
}
|
||||
double width = MediaQuery.of(context).size.width;
|
||||
if (FluffyThemes.isColumnMode(context)) {
|
||||
width = width - FluffyThemes.navRailWidth - FluffyThemes.columnWidth;
|
||||
}
|
||||
const int numDays = 28;
|
||||
const int minSpacePerDay = 20;
|
||||
final int availableSpaces = width ~/ minSpacePerDay;
|
||||
final int showAtInterval = (numDays / availableSpaces).floor() + 1;
|
||||
// bool showLabelBasedOnTimeSpan(
|
||||
// TimeSpan timeSpan,
|
||||
// TimeSeriesInterval current,
|
||||
// TimeSeriesInterval? last,
|
||||
// int labelIndex,
|
||||
// ) {
|
||||
// switch (timeSpan) {
|
||||
// case TimeSpan.day:
|
||||
// return current.end.hour % 3 == 0;
|
||||
// case TimeSpan.month:
|
||||
// if (current.end.month != last?.end.month) {
|
||||
// return true;
|
||||
// }
|
||||
// double width = MediaQuery.of(context).size.width;
|
||||
// if (FluffyThemes.isColumnMode(context)) {
|
||||
// width = width - FluffyThemes.navRailWidth - FluffyThemes.columnWidth;
|
||||
// }
|
||||
// const int numDays = 28;
|
||||
// const int minSpacePerDay = 20;
|
||||
// final int availableSpaces = width ~/ minSpacePerDay;
|
||||
// final int showAtInterval = (numDays / availableSpaces).floor() + 1;
|
||||
|
||||
final int lastDayOfCurrentMonth =
|
||||
DateTime(current.end.year, current.end.month + 1, 0).day;
|
||||
final bool isNextToMonth = labelIndex == 1 ||
|
||||
current.end.day == 2 ||
|
||||
current.end.day == lastDayOfCurrentMonth;
|
||||
final bool shouldShowNextToMonth = showAtInterval <= 1;
|
||||
return (current.end.day % showAtInterval == 0) &&
|
||||
(!isNextToMonth || shouldShowNextToMonth);
|
||||
case TimeSpan.week:
|
||||
case TimeSpan.sixmonths:
|
||||
case TimeSpan.year:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// final int lastDayOfCurrentMonth =
|
||||
// DateTime(current.end.year, current.end.month + 1, 0).day;
|
||||
// final bool isNextToMonth = labelIndex == 1 ||
|
||||
// current.end.day == 2 ||
|
||||
// current.end.day == lastDayOfCurrentMonth;
|
||||
// final bool shouldShowNextToMonth = showAtInterval <= 1;
|
||||
// return (current.end.day % showAtInterval == 0) &&
|
||||
// (!isNextToMonth || shouldShowNextToMonth);
|
||||
// case TimeSpan.week:
|
||||
// case TimeSpan.sixmonths:
|
||||
// case TimeSpan.year:
|
||||
// default:
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
|
||||
String getLabelBasedOnTimeSpan(
|
||||
TimeSpan timeSpan,
|
||||
TimeSeriesInterval current,
|
||||
TimeSeriesInterval? last,
|
||||
int labelIndex,
|
||||
) {
|
||||
final bool showLabel = showLabelBasedOnTimeSpan(
|
||||
timeSpan,
|
||||
current,
|
||||
last,
|
||||
labelIndex,
|
||||
);
|
||||
// String getLabelBasedOnTimeSpan(
|
||||
// TimeSpan timeSpan,
|
||||
// TimeSeriesInterval current,
|
||||
// TimeSeriesInterval? last,
|
||||
// int labelIndex,
|
||||
// ) {
|
||||
// final bool showLabel = showLabelBasedOnTimeSpan(
|
||||
// timeSpan,
|
||||
// current,
|
||||
// last,
|
||||
// labelIndex,
|
||||
// );
|
||||
|
||||
if (widget.chartAnalytics == null || !showLabel) {
|
||||
return "";
|
||||
}
|
||||
if (isInSameGroup(last, current, timeSpan)) {
|
||||
return "-";
|
||||
}
|
||||
// if (widget.chartAnalytics == null || !showLabel) {
|
||||
// return "";
|
||||
// }
|
||||
// if (isInSameGroup(last, current, timeSpan)) {
|
||||
// return "-";
|
||||
// }
|
||||
|
||||
switch (widget.chartAnalytics?.timeSpan ?? TimeSpan.month) {
|
||||
case TimeSpan.day:
|
||||
return DateFormat(DateFormat.HOUR).format(current.end);
|
||||
case TimeSpan.week:
|
||||
return DateFormat(DateFormat.ABBR_WEEKDAY).format(current.end);
|
||||
case TimeSpan.month:
|
||||
return current.end.month != last?.end.month
|
||||
? DateFormat(DateFormat.ABBR_MONTH).format(current.end)
|
||||
: DateFormat(DateFormat.DAY).format(current.end);
|
||||
case TimeSpan.sixmonths:
|
||||
case TimeSpan.year:
|
||||
return DateFormat(DateFormat.ABBR_STANDALONE_MONTH).format(current.end);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
// switch (widget.chartAnalytics?.timeSpan ?? TimeSpan.month) {
|
||||
// case TimeSpan.day:
|
||||
// return DateFormat(DateFormat.HOUR).format(current.end);
|
||||
// case TimeSpan.week:
|
||||
// return DateFormat(DateFormat.ABBR_WEEKDAY).format(current.end);
|
||||
// case TimeSpan.month:
|
||||
// return current.end.month != last?.end.month
|
||||
// ? DateFormat(DateFormat.ABBR_MONTH).format(current.end)
|
||||
// : DateFormat(DateFormat.DAY).format(current.end);
|
||||
// case TimeSpan.sixmonths:
|
||||
// case TimeSpan.year:
|
||||
// return DateFormat(DateFormat.ABBR_STANDALONE_MONTH).format(current.end);
|
||||
// default:
|
||||
// return '';
|
||||
// }
|
||||
// }
|
||||
|
||||
Widget bottomTitles(double value, TitleMeta meta) {
|
||||
if (widget.chartAnalytics == null) {
|
||||
return Container();
|
||||
}
|
||||
String text;
|
||||
final index = value.toInt();
|
||||
final TimeSpan timeSpan = widget.chartAnalytics?.timeSpan ?? TimeSpan.month;
|
||||
final TimeSeriesInterval? last =
|
||||
index != 0 ? intervalGroupings[index - 1].last : null;
|
||||
final TimeSeriesInterval current = intervalGroupings[index].last;
|
||||
// Widget bottomTitles(double value, TitleMeta meta) {
|
||||
// if (widget.chartAnalytics == null) {
|
||||
// return Container();
|
||||
// }
|
||||
// String text;
|
||||
// final index = value.toInt();
|
||||
// final TimeSpan timeSpan = widget.chartAnalytics?.timeSpan ?? TimeSpan.month;
|
||||
// final TimeSeriesInterval? last =
|
||||
// index != 0 ? intervalGroupings[index - 1].last : null;
|
||||
// final TimeSeriesInterval current = intervalGroupings[index].last;
|
||||
|
||||
text = getLabelBasedOnTimeSpan(timeSpan, current, last, index);
|
||||
// text = getLabelBasedOnTimeSpan(timeSpan, current, last, index);
|
||||
|
||||
return SideTitleWidget(
|
||||
axisSide: meta.axisSide,
|
||||
child: Text(
|
||||
text,
|
||||
style: titleTextStyle(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
// return SideTitleWidget(
|
||||
// axisSide: meta.axisSide,
|
||||
// child: Text(
|
||||
// text,
|
||||
// style: titleTextStyle(context),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
TextStyle titleTextStyle(context) => TextStyle(
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
fontSize: 10,
|
||||
);
|
||||
// TextStyle titleTextStyle(context) => TextStyle(
|
||||
// color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
// fontSize: 10,
|
||||
// );
|
||||
|
||||
Widget leftTitles(double value, TitleMeta meta) {
|
||||
Widget textWidget;
|
||||
if (value != meta.max) {
|
||||
textWidget = Text(meta.formattedValue, style: titleTextStyle(context));
|
||||
} else {
|
||||
textWidget = const Icon(Icons.chat_bubble, size: 14);
|
||||
}
|
||||
return SideTitleWidget(
|
||||
axisSide: meta.axisSide,
|
||||
child: textWidget,
|
||||
);
|
||||
}
|
||||
// Widget leftTitles(double value, TitleMeta meta) {
|
||||
// Widget textWidget;
|
||||
// if (value != meta.max) {
|
||||
// textWidget = Text(meta.formattedValue, style: titleTextStyle(context));
|
||||
// } else {
|
||||
// textWidget = const Icon(Icons.chat_bubble, size: 14);
|
||||
// }
|
||||
// return SideTitleWidget(
|
||||
// axisSide: meta.axisSide,
|
||||
// child: textWidget,
|
||||
// );
|
||||
// }
|
||||
|
||||
bool isInSameGroup(
|
||||
TimeSeriesInterval? t1,
|
||||
TimeSeriesInterval t2,
|
||||
TimeSpan timeSpan,
|
||||
) {
|
||||
final DateTime? date1 = t1?.end;
|
||||
final DateTime date2 = t2.end;
|
||||
if (timeSpan == TimeSpan.sixmonths || timeSpan == TimeSpan.year) {
|
||||
return date1?.month == date2.month;
|
||||
} else if (timeSpan == TimeSpan.week) {
|
||||
return date1?.day == date2.day;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// bool isInSameGroup(
|
||||
// TimeSeriesInterval? t1,
|
||||
// TimeSeriesInterval t2,
|
||||
// TimeSpan timeSpan,
|
||||
// ) {
|
||||
// final DateTime? date1 = t1?.end;
|
||||
// final DateTime date2 = t2.end;
|
||||
// if (timeSpan == TimeSpan.sixmonths || timeSpan == TimeSpan.year) {
|
||||
// return date1?.month == date2.month;
|
||||
// } else if (timeSpan == TimeSpan.week) {
|
||||
// return date1?.day == date2.day;
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
void makeIntervalGroupings() {
|
||||
intervalGroupings.clear();
|
||||
try {
|
||||
for (final timeSeriesInterval
|
||||
in widget.chartAnalytics?.timeSeries ?? []) {
|
||||
//Note: if we decide we'd like to do some sort of grouping in the future,
|
||||
// this is where that could happen. Currently, we're just putting one
|
||||
// BarChartRod in each BarChartGroup
|
||||
final TimeSeriesInterval? last =
|
||||
intervalGroupings.isNotEmpty ? intervalGroupings.last.last : null;
|
||||
// void makeIntervalGroupings() {
|
||||
// intervalGroupings.clear();
|
||||
// try {
|
||||
// for (final timeSeriesInterval
|
||||
// in widget.chartAnalytics?.timeSeries ?? []) {
|
||||
// //Note: if we decide we'd like to do some sort of grouping in the future,
|
||||
// // this is where that could happen. Currently, we're just putting one
|
||||
// // BarChartRod in each BarChartGroup
|
||||
// final TimeSeriesInterval? last =
|
||||
// intervalGroupings.isNotEmpty ? intervalGroupings.last.last : null;
|
||||
|
||||
if (widget.chartAnalytics != null &&
|
||||
isInSameGroup(
|
||||
last,
|
||||
timeSeriesInterval,
|
||||
widget.chartAnalytics!.timeSpan,
|
||||
)) {
|
||||
intervalGroupings.last.add(timeSeriesInterval);
|
||||
} else {
|
||||
intervalGroupings.add([timeSeriesInterval]);
|
||||
}
|
||||
}
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: stack);
|
||||
}
|
||||
}
|
||||
// if (widget.chartAnalytics != null &&
|
||||
// isInSameGroup(
|
||||
// last,
|
||||
// timeSeriesInterval,
|
||||
// widget.chartAnalytics!.timeSpan,
|
||||
// )) {
|
||||
// intervalGroupings.last.add(timeSeriesInterval);
|
||||
// } else {
|
||||
// intervalGroupings.add([timeSeriesInterval]);
|
||||
// }
|
||||
// }
|
||||
// } catch (err, stack) {
|
||||
// debugger(when: kDebugMode);
|
||||
// ErrorHandler.logError(e: err, s: stack);
|
||||
// }
|
||||
// }
|
||||
|
||||
List<BarChartGroupData> get barChartGroupData {
|
||||
if (widget.chartAnalytics == null) {
|
||||
return BarChartPlaceHolderData.getRandomData(context);
|
||||
}
|
||||
// List<BarChartGroupData> get barChartGroupData {
|
||||
// if (widget.chartAnalytics == null) {
|
||||
// return BarChartPlaceHolderData.getRandomData(context);
|
||||
// }
|
||||
|
||||
makeIntervalGroupings();
|
||||
// makeIntervalGroupings();
|
||||
|
||||
final List<BarChartGroupData> chartData = [];
|
||||
// final List<BarChartGroupData> chartData = [];
|
||||
|
||||
intervalGroupings.asMap().forEach((index, intervalGroup) {
|
||||
chartData.add(
|
||||
BarChartGroupData(
|
||||
x: index,
|
||||
barsSpace: barSpace,
|
||||
// barRods: intervalGroup.map(constructBarChartRodData).toList(),
|
||||
barRods: constructBarChartRodData(intervalGroup),
|
||||
),
|
||||
);
|
||||
});
|
||||
return chartData;
|
||||
}
|
||||
// intervalGroupings.asMap().forEach((index, intervalGroup) {
|
||||
// chartData.add(
|
||||
// BarChartGroupData(
|
||||
// x: index,
|
||||
// barsSpace: barSpace,
|
||||
// // barRods: intervalGroup.map(constructBarChartRodData).toList(),
|
||||
// barRods: constructBarChartRodData(intervalGroup),
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// return chartData;
|
||||
// }
|
||||
|
||||
// BarChartRodData constructBarChartRodData(TimeSeriesInterval timeSeriesInterval) {
|
||||
// final double y1 = timeSeriesInterval.spanIT.toDouble();
|
||||
// final double y2 =
|
||||
// (timeSeriesInterval.spanIT + timeSeriesInterval.spanIGC).toDouble();
|
||||
// final double y3 = timeSeriesInterval.spanTotal.toDouble();
|
||||
// return BarChartRodData(
|
||||
// toY: y3,
|
||||
// width: 10.toDouble(),
|
||||
// rodStackItems: [
|
||||
// BarChartRodStackItem(0, y1, UseType.ta.color(context)),
|
||||
// BarChartRodStackItem(y1, y2, UseType.ga.color(context)),
|
||||
// BarChartRodStackItem(y2, y3, UseType.wa.color(context)),
|
||||
// ],
|
||||
// borderRadius: BorderRadius.zero,
|
||||
// );
|
||||
// }
|
||||
// // BarChartRodData constructBarChartRodData(TimeSeriesInterval timeSeriesInterval) {
|
||||
// // final double y1 = timeSeriesInterval.spanIT.toDouble();
|
||||
// // final double y2 =
|
||||
// // (timeSeriesInterval.spanIT + timeSeriesInterval.spanIGC).toDouble();
|
||||
// // final double y3 = timeSeriesInterval.spanTotal.toDouble();
|
||||
// // return BarChartRodData(
|
||||
// // toY: y3,
|
||||
// // width: 10.toDouble(),
|
||||
// // rodStackItems: [
|
||||
// // BarChartRodStackItem(0, y1, UseType.ta.color(context)),
|
||||
// // BarChartRodStackItem(y1, y2, UseType.ga.color(context)),
|
||||
// // BarChartRodStackItem(y2, y3, UseType.wa.color(context)),
|
||||
// // ],
|
||||
// // borderRadius: BorderRadius.zero,
|
||||
// // );
|
||||
// // }
|
||||
|
||||
List<BarChartRodData> constructBarChartRodData(
|
||||
List<TimeSeriesInterval> timeSeriesIntervalGroup,
|
||||
) {
|
||||
int y1 = 0;
|
||||
int y2 = 0;
|
||||
int y3 = 0;
|
||||
int y4 = 0;
|
||||
for (final e in timeSeriesIntervalGroup) {
|
||||
y1 += e.totals.ta;
|
||||
y2 += y1 + e.totals.ga;
|
||||
y3 += y2 + e.totals.wa;
|
||||
y4 += y3 + e.totals.un;
|
||||
}
|
||||
return [
|
||||
BarChartRodData(
|
||||
toY: y4.toDouble(),
|
||||
width: 10.toDouble(),
|
||||
rodStackItems: [
|
||||
BarChartRodStackItem(0, y1.toDouble(), UseType.ta.color(context)),
|
||||
BarChartRodStackItem(
|
||||
y1.toDouble(),
|
||||
y2.toDouble(),
|
||||
UseType.ga.color(context),
|
||||
),
|
||||
BarChartRodStackItem(
|
||||
y2.toDouble(),
|
||||
y3.toDouble(),
|
||||
UseType.wa.color(context),
|
||||
),
|
||||
BarChartRodStackItem(
|
||||
y3.toDouble(),
|
||||
y4.toDouble(),
|
||||
UseType.un.color(context),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
];
|
||||
}
|
||||
// List<BarChartRodData> constructBarChartRodData(
|
||||
// List<TimeSeriesInterval> timeSeriesIntervalGroup,
|
||||
// ) {
|
||||
// int y1 = 0;
|
||||
// int y2 = 0;
|
||||
// int y3 = 0;
|
||||
// int y4 = 0;
|
||||
// for (final e in timeSeriesIntervalGroup) {
|
||||
// y1 += e.totals.ta;
|
||||
// y2 += y1 + e.totals.ga;
|
||||
// y3 += y2 + e.totals.wa;
|
||||
// y4 += y3 + e.totals.un;
|
||||
// }
|
||||
// return [
|
||||
// BarChartRodData(
|
||||
// toY: y4.toDouble(),
|
||||
// width: 10.toDouble(),
|
||||
// rodStackItems: [
|
||||
// BarChartRodStackItem(0, y1.toDouble(), UseType.ta.color(context)),
|
||||
// BarChartRodStackItem(
|
||||
// y1.toDouble(),
|
||||
// y2.toDouble(),
|
||||
// UseType.ga.color(context),
|
||||
// ),
|
||||
// BarChartRodStackItem(
|
||||
// y2.toDouble(),
|
||||
// y3.toDouble(),
|
||||
// UseType.wa.color(context),
|
||||
// ),
|
||||
// BarChartRodStackItem(
|
||||
// y3.toDouble(),
|
||||
// y4.toDouble(),
|
||||
// UseType.un.color(context),
|
||||
// ),
|
||||
// ],
|
||||
// borderRadius: BorderRadius.zero,
|
||||
// ),
|
||||
// ];
|
||||
// }
|
||||
|
||||
// BarTouchData get barTouchData => BarTouchData(
|
||||
// touchTooltipData: BarTouchTooltipData(
|
||||
// fitInsideVertically: true,
|
||||
// tooltipBgColor: Colors.blueGrey,
|
||||
// getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||
// return BarTooltipItem(
|
||||
// "groupindex $groupIndex rodIndex $rodIndex",
|
||||
// const TextStyle(
|
||||
// color: Colors.white,
|
||||
// fontWeight: FontWeight.bold,
|
||||
// fontSize: 18,
|
||||
// ),
|
||||
// children: <TextSpan>[
|
||||
// toolTipText(rod),
|
||||
// ],
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// // touchCallback: (FlTouchEvent event, barTouchResponse) {
|
||||
// // setState(() {
|
||||
// // if (!event.isInterestedForInteractions ||
|
||||
// // barTouchResponse == null ||
|
||||
// // barTouchResponse.spot == null) {
|
||||
// // touchedIndex = -1;
|
||||
// // return;
|
||||
// // }
|
||||
// // touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex;
|
||||
// // });
|
||||
// // },
|
||||
// );
|
||||
// // BarTouchData get barTouchData => BarTouchData(
|
||||
// // touchTooltipData: BarTouchTooltipData(
|
||||
// // fitInsideVertically: true,
|
||||
// // tooltipBgColor: Colors.blueGrey,
|
||||
// // getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||
// // return BarTooltipItem(
|
||||
// // "groupindex $groupIndex rodIndex $rodIndex",
|
||||
// // const TextStyle(
|
||||
// // color: Colors.white,
|
||||
// // fontWeight: FontWeight.bold,
|
||||
// // fontSize: 18,
|
||||
// // ),
|
||||
// // children: <TextSpan>[
|
||||
// // toolTipText(rod),
|
||||
// // ],
|
||||
// // );
|
||||
// // },
|
||||
// // ),
|
||||
// // // touchCallback: (FlTouchEvent event, barTouchResponse) {
|
||||
// // // setState(() {
|
||||
// // // if (!event.isInterestedForInteractions ||
|
||||
// // // barTouchResponse == null ||
|
||||
// // // barTouchResponse.spot == null) {
|
||||
// // // touchedIndex = -1;
|
||||
// // // return;
|
||||
// // // }
|
||||
// // // touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex;
|
||||
// // // });
|
||||
// // // },
|
||||
// // );
|
||||
|
||||
// TextSpan toolTipText(BarChartRodData rodData) {
|
||||
// double rodPercentage(int index) {
|
||||
// return (rodData.rodStackItems[index].toY -
|
||||
// rodData.rodStackItems[index].fromY) /
|
||||
// rodData.toY *
|
||||
// 100;
|
||||
// }
|
||||
// // TextSpan toolTipText(BarChartRodData rodData) {
|
||||
// // double rodPercentage(int index) {
|
||||
// // return (rodData.rodStackItems[index].toY -
|
||||
// // rodData.rodStackItems[index].fromY) /
|
||||
// // rodData.toY *
|
||||
// // 100;
|
||||
// // }
|
||||
|
||||
// return TextSpan(
|
||||
// children: [
|
||||
// const WidgetSpan(
|
||||
// child: Icon(Icons.chat_bubble, size: 14),
|
||||
// ),
|
||||
// TextSpan(
|
||||
// text: " ${rodData.toY}",
|
||||
// ),
|
||||
// TextSpan(
|
||||
// text: "/nIT ${rodPercentage(0)}%",
|
||||
// style: TextStyle(color: UseType.ta.color(context)),
|
||||
// ),
|
||||
// TextSpan(
|
||||
// text: " IGC ${rodPercentage(1)}%",
|
||||
// style: TextStyle(color: UseType.ga.color(context)),
|
||||
// ),
|
||||
// TextSpan(
|
||||
// text: " Direct ${rodPercentage(2)}%",
|
||||
// style: TextStyle(color: UseType.wa.color(context)),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
}
|
||||
// // return TextSpan(
|
||||
// // children: [
|
||||
// // const WidgetSpan(
|
||||
// // child: Icon(Icons.chat_bubble, size: 14),
|
||||
// // ),
|
||||
// // TextSpan(
|
||||
// // text: " ${rodData.toY}",
|
||||
// // ),
|
||||
// // TextSpan(
|
||||
// // text: "/nIT ${rodPercentage(0)}%",
|
||||
// // style: TextStyle(color: UseType.ta.color(context)),
|
||||
// // ),
|
||||
// // TextSpan(
|
||||
// // text: " IGC ${rodPercentage(1)}%",
|
||||
// // style: TextStyle(color: UseType.ga.color(context)),
|
||||
// // ),
|
||||
// // TextSpan(
|
||||
// // text: " Direct ${rodPercentage(2)}%",
|
||||
// // style: TextStyle(color: UseType.wa.color(context)),
|
||||
// // ),
|
||||
// // ],
|
||||
// // );
|
||||
// // }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,121 +1,114 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
// import 'dart:async';
|
||||
// import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
// import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
|
||||
// import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
// import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
// import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
// import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
// import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
|
||||
// import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart';
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:go_router/go_router.dart';
|
||||
// import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../../widgets/matrix.dart';
|
||||
import '../../../utils/sync_status_util_v2.dart';
|
||||
import 'space_analytics_view.dart';
|
||||
// import '../../../../widgets/matrix.dart';
|
||||
// import '../../../utils/sync_status_util_v2.dart';
|
||||
|
||||
class SpaceAnalyticsPage extends StatefulWidget {
|
||||
final BarChartViewSelection selectedView;
|
||||
const SpaceAnalyticsPage({super.key, required this.selectedView});
|
||||
// class SpaceAnalyticsPage extends StatefulWidget {
|
||||
// final BarChartViewSelection selectedView;
|
||||
// const SpaceAnalyticsPage({super.key, required this.selectedView});
|
||||
|
||||
@override
|
||||
State<SpaceAnalyticsPage> createState() => SpaceAnalyticsV2Controller();
|
||||
}
|
||||
// @override
|
||||
// State<SpaceAnalyticsPage> createState() => SpaceAnalyticsV2Controller();
|
||||
// }
|
||||
|
||||
class SpaceAnalyticsV2Controller extends State<SpaceAnalyticsPage> {
|
||||
bool _initialized = false;
|
||||
// StreamSubscription<Event>? stateSub;
|
||||
// Timer? refreshTimer;
|
||||
// class SpaceAnalyticsV2Controller extends State<SpaceAnalyticsPage> {
|
||||
// bool _initialized = false;
|
||||
// // StreamSubscription<Event>? stateSub;
|
||||
// // Timer? refreshTimer;
|
||||
|
||||
List<SpaceRoomsChunk> chats = [];
|
||||
List<User> students = [];
|
||||
String? get spaceId => GoRouterState.of(context).pathParameters['spaceid'];
|
||||
Room? _spaceRoom;
|
||||
List<LanguageModel> targetLanguages = [];
|
||||
// List<SpaceRoomsChunk> chats = [];
|
||||
// List<User> students = [];
|
||||
// String? get spaceId => GoRouterState.of(context).pathParameters['spaceid'];
|
||||
// Room? _spaceRoom;
|
||||
// List<LanguageModel> targetLanguages = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (spaceRoom == null || (!(spaceRoom?.isSpace ?? false))) {
|
||||
context.go('/rooms');
|
||||
}
|
||||
getChatAndStudents();
|
||||
});
|
||||
}
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// Future.delayed(Duration.zero, () async {
|
||||
// if (spaceRoom == null || (!(spaceRoom?.isSpace ?? false))) {
|
||||
// context.go('/rooms');
|
||||
// }
|
||||
// getChatAndStudents();
|
||||
// });
|
||||
// }
|
||||
|
||||
Room? get spaceRoom {
|
||||
if (_spaceRoom == null || _spaceRoom!.id != spaceId) {
|
||||
debugPrint("updating _spaceRoom");
|
||||
_spaceRoom = spaceId != null
|
||||
? Matrix.of(context).client.getRoomById(spaceId!)
|
||||
: null;
|
||||
if (_spaceRoom == null) {
|
||||
context.go('/rooms/analytics');
|
||||
return null;
|
||||
}
|
||||
getChatAndStudents().then((_) => setTargetLanguages());
|
||||
}
|
||||
return _spaceRoom;
|
||||
}
|
||||
// Room? get spaceRoom {
|
||||
// if (_spaceRoom == null || _spaceRoom!.id != spaceId) {
|
||||
// debugPrint("updating _spaceRoom");
|
||||
// _spaceRoom = spaceId != null
|
||||
// ? Matrix.of(context).client.getRoomById(spaceId!)
|
||||
// : null;
|
||||
// if (_spaceRoom == null) {
|
||||
// context.go('/rooms/analytics');
|
||||
// return null;
|
||||
// }
|
||||
// getChatAndStudents().then((_) => setTargetLanguages());
|
||||
// }
|
||||
// return _spaceRoom;
|
||||
// }
|
||||
|
||||
Future<void> getChatAndStudents() async {
|
||||
try {
|
||||
await spaceRoom?.requestParticipants();
|
||||
// Future<void> getChatAndStudents() async {
|
||||
// try {
|
||||
// await spaceRoom?.requestParticipants();
|
||||
|
||||
if (spaceRoom != null) {
|
||||
final response = await Matrix.of(context).client.getSpaceHierarchy(
|
||||
spaceRoom!.id,
|
||||
);
|
||||
// if (spaceRoom != null) {
|
||||
// final response = await Matrix.of(context).client.getSpaceHierarchy(
|
||||
// spaceRoom!.id,
|
||||
// );
|
||||
|
||||
// set the latest fetched full hierarchy in message analytics controller
|
||||
// we want to avoid calling this endpoint again and again, so whenever the
|
||||
// data is made available, set it in the controller
|
||||
MatrixState.pangeaController.analytics
|
||||
.setLatestHierarchy(_spaceRoom!.id, response);
|
||||
// students = spaceRoom!.students;
|
||||
// chats = response.rooms
|
||||
// .where(
|
||||
// (room) =>
|
||||
// room.roomId != spaceRoom!.id &&
|
||||
// room.roomType != PangeaRoomTypes.analytics,
|
||||
// )
|
||||
// .toList();
|
||||
// chats.sort((a, b) => a.roomType == 'm.space' ? -1 : 1);
|
||||
// }
|
||||
|
||||
students = spaceRoom!.students;
|
||||
chats = response.rooms
|
||||
.where(
|
||||
(room) =>
|
||||
room.roomId != spaceRoom!.id &&
|
||||
room.roomType != PangeaRoomTypes.analytics,
|
||||
)
|
||||
.toList();
|
||||
chats.sort((a, b) => a.roomType == 'm.space' ? -1 : 1);
|
||||
}
|
||||
// setState(() {
|
||||
// _initialized = true;
|
||||
// });
|
||||
// } catch (err, s) {
|
||||
// debugger(when: kDebugMode);
|
||||
// ErrorHandler.logError(e: err, s: s);
|
||||
// }
|
||||
// }
|
||||
|
||||
setState(() {
|
||||
_initialized = true;
|
||||
});
|
||||
} catch (err, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
}
|
||||
}
|
||||
// Future<void> setTargetLanguages() async {
|
||||
// // get a list of language models, sorted by the
|
||||
// // number of students who are learning that language
|
||||
// targetLanguages = await spaceRoom?.targetLanguages() ?? [];
|
||||
// setState(() {});
|
||||
// }
|
||||
|
||||
Future<void> setTargetLanguages() async {
|
||||
// get a list of language models, sorted by the
|
||||
// number of students who are learning that language
|
||||
targetLanguages = await spaceRoom?.targetLanguages() ?? [];
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_initialized) return const PCircular();
|
||||
return PLoadingStatusV2(
|
||||
// if we everr want it rebuild the whole thing each time (and run initState again)
|
||||
// but this is computationally expensive!
|
||||
// key: UniqueKey(),
|
||||
shimmerChild: const ListPlaceholder(),
|
||||
// onFinish: () {
|
||||
// getChatAndStudentAnalytics(context);
|
||||
// },
|
||||
child: SpaceAnalyticsView(this),
|
||||
);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// if (!_initialized) return const PCircular();
|
||||
// return PLoadingStatusV2(
|
||||
// // if we everr want it rebuild the whole thing each time (and run initState again)
|
||||
// // but this is computationally expensive!
|
||||
// // key: UniqueKey(),
|
||||
// shimmerChild: const ListPlaceholder(),
|
||||
// // onFinish: () {
|
||||
// // getChatAndStudentAnalytics(context);
|
||||
// // },
|
||||
// child: SpaceAnalyticsView(this),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,66 +1,66 @@
|
|||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
// import 'package:fluffychat/widgets/matrix.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../base_analytics.dart';
|
||||
import 'space_analytics.dart';
|
||||
// import '../base_analytics.dart';
|
||||
// import 'space_analytics.dart';
|
||||
|
||||
class SpaceAnalyticsView extends StatelessWidget {
|
||||
final SpaceAnalyticsV2Controller controller;
|
||||
const SpaceAnalyticsView(this.controller, {super.key});
|
||||
// class SpaceAnalyticsView extends StatelessWidget {
|
||||
// final SpaceAnalyticsV2Controller controller;
|
||||
// const SpaceAnalyticsView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String pageTitle = L10n.of(context)!.spaceAnalytics;
|
||||
final TabData tab1 = TabData(
|
||||
type: AnalyticsEntryType.room,
|
||||
icon: Icons.chat_bubble_outline,
|
||||
items: controller.chats
|
||||
.map(
|
||||
(room) => TabItem(
|
||||
avatar: room.avatarUrl,
|
||||
displayName: room.name ??
|
||||
Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(room.roomId)
|
||||
?.getLocalizedDisplayname() ??
|
||||
"",
|
||||
id: room.roomId,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
final TabData tab2 = TabData(
|
||||
type: AnalyticsEntryType.student,
|
||||
icon: Icons.people_outline,
|
||||
items: controller.students
|
||||
.map(
|
||||
(s) => TabItem(
|
||||
avatar: s.avatarUrl,
|
||||
displayName: s.calcDisplayname(),
|
||||
id: s.id,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final String pageTitle = L10n.of(context)!.spaceAnalytics;
|
||||
// final TabData tab1 = TabData(
|
||||
// type: AnalyticsEntryType.room,
|
||||
// icon: Icons.chat_bubble_outline,
|
||||
// items: controller.chats
|
||||
// .map(
|
||||
// (room) => TabItem(
|
||||
// avatar: room.avatarUrl,
|
||||
// displayName: room.name ??
|
||||
// Matrix.of(context)
|
||||
// .client
|
||||
// .getRoomById(room.roomId)
|
||||
// ?.getLocalizedDisplayname() ??
|
||||
// "",
|
||||
// id: room.roomId,
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// );
|
||||
// final TabData tab2 = TabData(
|
||||
// type: AnalyticsEntryType.student,
|
||||
// icon: Icons.people_outline,
|
||||
// items: controller.students
|
||||
// .map(
|
||||
// (s) => TabItem(
|
||||
// avatar: s.avatarUrl,
|
||||
// displayName: s.calcDisplayname(),
|
||||
// id: s.id,
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// );
|
||||
|
||||
return controller.spaceId != null
|
||||
? BaseAnalyticsPage(
|
||||
selectedView: controller.widget.selectedView,
|
||||
pageTitle: pageTitle,
|
||||
tabs: [tab1, tab2],
|
||||
alwaysSelected: AnalyticsSelected(
|
||||
controller.spaceId!,
|
||||
AnalyticsEntryType.space,
|
||||
controller.spaceRoom?.name ?? "",
|
||||
),
|
||||
defaultSelected: AnalyticsSelected(
|
||||
controller.spaceId!,
|
||||
AnalyticsEntryType.space,
|
||||
controller.spaceRoom?.name ?? "",
|
||||
),
|
||||
targetLanguages: controller.targetLanguages,
|
||||
)
|
||||
: const SizedBox();
|
||||
}
|
||||
}
|
||||
// return controller.spaceId != null
|
||||
// ? BaseAnalyticsPage(
|
||||
// selectedView: controller.widget.selectedView,
|
||||
// pageTitle: pageTitle,
|
||||
// tabs: [tab1, tab2],
|
||||
// alwaysSelected: AnalyticsSelected(
|
||||
// controller.spaceId!,
|
||||
// AnalyticsEntryType.space,
|
||||
// controller.spaceRoom?.name ?? "",
|
||||
// ),
|
||||
// defaultSelected: AnalyticsSelected(
|
||||
// controller.spaceId!,
|
||||
// AnalyticsEntryType.space,
|
||||
// controller.spaceRoom?.name ?? "",
|
||||
// ),
|
||||
// targetLanguages: controller.targetLanguages,
|
||||
// )
|
||||
// : const SizedBox();
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,100 +1,100 @@
|
|||
import 'dart:async';
|
||||
// import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/space_list/space_list_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
// import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
// import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
// import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
// import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/space_list/space_list_view.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../../widgets/matrix.dart';
|
||||
import '../../../controllers/pangea_controller.dart';
|
||||
import '../../../utils/sync_status_util_v2.dart';
|
||||
import '../../../widgets/common/list_placeholder.dart';
|
||||
// import '../../../../widgets/matrix.dart';
|
||||
// import '../../../controllers/pangea_controller.dart';
|
||||
// import '../../../utils/sync_status_util_v2.dart';
|
||||
// import '../../../widgets/common/list_placeholder.dart';
|
||||
|
||||
class AnalyticsSpaceList extends StatefulWidget {
|
||||
const AnalyticsSpaceList({super.key});
|
||||
// class AnalyticsSpaceList extends StatefulWidget {
|
||||
// const AnalyticsSpaceList({super.key});
|
||||
|
||||
@override
|
||||
State<AnalyticsSpaceList> createState() => AnalyticsSpaceListController();
|
||||
}
|
||||
// @override
|
||||
// State<AnalyticsSpaceList> createState() => AnalyticsSpaceListController();
|
||||
// }
|
||||
|
||||
class AnalyticsSpaceListController extends State<AnalyticsSpaceList> {
|
||||
PangeaController pangeaController = MatrixState.pangeaController;
|
||||
List<Room> spaces = [];
|
||||
StreamSubscription? stateSub;
|
||||
List<LanguageModel> targetLanguages = [];
|
||||
// class AnalyticsSpaceListController extends State<AnalyticsSpaceList> {
|
||||
// PangeaController pangeaController = MatrixState.pangeaController;
|
||||
// List<Room> spaces = [];
|
||||
// StreamSubscription? stateSub;
|
||||
// List<LanguageModel> targetLanguages = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setSpaceList().then((_) => setTargetLanguages());
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// setSpaceList().then((_) => setTargetLanguages());
|
||||
|
||||
// reload dropdowns when their values change in analytics page
|
||||
stateSub = pangeaController.analytics.stateStream.listen(
|
||||
(_) => setState(() {}),
|
||||
);
|
||||
}
|
||||
// // reload dropdowns when their values change in analytics page
|
||||
// stateSub = pangeaController.analytics.stateStream.listen(
|
||||
// (_) => setState(() {}),
|
||||
// );
|
||||
// }
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stateSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
// @override
|
||||
// void dispose() {
|
||||
// stateSub?.cancel();
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
StreamController refreshStream = StreamController.broadcast();
|
||||
// StreamController refreshStream = StreamController.broadcast();
|
||||
|
||||
Future<void> setSpaceList() async {
|
||||
final spaceList = await Matrix.of(context).client.spacesImTeaching;
|
||||
spaces = spaceList
|
||||
.where(
|
||||
(space) => !spaceList.any(
|
||||
(parentSpace) => parentSpace.spaceChildren
|
||||
.any((child) => child.roomId == space.id),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
setState(() {});
|
||||
}
|
||||
// Future<void> setSpaceList() async {
|
||||
// final spaceList = Matrix.of(context).client.spacesImTeaching;
|
||||
// spaces = spaceList
|
||||
// .where(
|
||||
// (space) => !spaceList.any(
|
||||
// (parentSpace) => parentSpace.spaceChildren
|
||||
// .any((child) => child.roomId == space.id),
|
||||
// ),
|
||||
// )
|
||||
// .toList();
|
||||
// setState(() {});
|
||||
// }
|
||||
|
||||
Future<void> setTargetLanguages() async {
|
||||
if (spaces.isEmpty) return;
|
||||
final Map<LanguageModel, int> langCounts = {};
|
||||
for (final Room space in spaces) {
|
||||
final List<LanguageModel> targetLangs = await space.targetLanguages();
|
||||
for (final LanguageModel lang in targetLangs) {
|
||||
langCounts[lang] ??= 0;
|
||||
langCounts[lang] = langCounts[lang]! + 1;
|
||||
}
|
||||
}
|
||||
targetLanguages = langCounts.entries.map((entry) => entry.key).toList()
|
||||
..sort(
|
||||
(a, b) => langCounts[b]!.compareTo(langCounts[a]!),
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
// Future<void> setTargetLanguages() async {
|
||||
// if (spaces.isEmpty) return;
|
||||
// final Map<LanguageModel, int> langCounts = {};
|
||||
// for (final Room space in spaces) {
|
||||
// final List<LanguageModel> targetLangs = await space.targetLanguages();
|
||||
// for (final LanguageModel lang in targetLangs) {
|
||||
// langCounts[lang] ??= 0;
|
||||
// langCounts[lang] = langCounts[lang]! + 1;
|
||||
// }
|
||||
// }
|
||||
// targetLanguages = langCounts.entries.map((entry) => entry.key).toList()
|
||||
// ..sort(
|
||||
// (a, b) => langCounts[b]!.compareTo(langCounts[a]!),
|
||||
// );
|
||||
// setState(() {});
|
||||
// }
|
||||
|
||||
void toggleTimeSpan(BuildContext context, TimeSpan timeSpan) {
|
||||
pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
|
||||
refreshStream.add(false);
|
||||
setState(() {});
|
||||
}
|
||||
// void toggleTimeSpan(BuildContext context, TimeSpan timeSpan) {
|
||||
// pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
|
||||
// refreshStream.add(false);
|
||||
// setState(() {});
|
||||
// }
|
||||
|
||||
Future<void> toggleSpaceLang(LanguageModel lang) async {
|
||||
await pangeaController.analytics.setCurrentAnalyticsLang(lang);
|
||||
refreshStream.add(false);
|
||||
setState(() {});
|
||||
}
|
||||
// Future<void> toggleSpaceLang(LanguageModel lang) async {
|
||||
// await pangeaController.analytics.setCurrentAnalyticsLang(lang);
|
||||
// refreshStream.add(false);
|
||||
// setState(() {});
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PLoadingStatusV2(
|
||||
shimmerChild: const ListPlaceholder(),
|
||||
child: AnalyticsSpaceListView(this),
|
||||
onFinish: () {
|
||||
// getAllClassAnalytics(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return PLoadingStatusV2(
|
||||
// shimmerChild: const ListPlaceholder(),
|
||||
// child: AnalyticsSpaceListView(this),
|
||||
// onFinish: () {
|
||||
// // getAllClassAnalytics(context);
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,89 +1,89 @@
|
|||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
// import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
// import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../base_analytics.dart';
|
||||
import 'space_list.dart';
|
||||
// import '../base_analytics.dart';
|
||||
// import 'space_list.dart';
|
||||
|
||||
class AnalyticsSpaceListView extends StatelessWidget {
|
||||
final AnalyticsSpaceListController controller;
|
||||
const AnalyticsSpaceListView(this.controller, {super.key});
|
||||
// class AnalyticsSpaceListView extends StatelessWidget {
|
||||
// final AnalyticsSpaceListController controller;
|
||||
// const AnalyticsSpaceListView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
L10n.of(context)!.spaceAnalytics,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
overflow: TextOverflow.clip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TimeSpanMenuButton(
|
||||
value: controller
|
||||
.pangeaController.analytics.currentAnalyticsTimeSpan,
|
||||
onChange: (TimeSpan value) => controller.toggleTimeSpan(
|
||||
context,
|
||||
value,
|
||||
),
|
||||
),
|
||||
AnalyticsLanguageButton(
|
||||
value:
|
||||
controller.pangeaController.analytics.currentAnalyticsLang,
|
||||
onChange: (lang) => controller.toggleSpaceLang(lang),
|
||||
languages:
|
||||
controller.pangeaController.pLanguageStore.targetOptions,
|
||||
),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.spaces.length,
|
||||
itemBuilder: (context, i) => AnalyticsListTile(
|
||||
defaultSelected: AnalyticsSelected(
|
||||
controller.spaces[i].id,
|
||||
AnalyticsEntryType.space,
|
||||
controller.spaces[i].name,
|
||||
),
|
||||
avatar: controller.spaces[i].avatar,
|
||||
selected: AnalyticsSelected(
|
||||
controller.spaces[i].id,
|
||||
AnalyticsEntryType.space,
|
||||
controller.spaces[i].name,
|
||||
),
|
||||
onTap: (selected) {
|
||||
context.go(
|
||||
'/rooms/analytics/${selected.id}',
|
||||
);
|
||||
},
|
||||
allowNavigateOnSelect: true,
|
||||
isSelected: false,
|
||||
pangeaController: controller.pangeaController,
|
||||
refreshStream: controller.refreshStream,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// appBar: AppBar(
|
||||
// centerTitle: true,
|
||||
// title: Text(
|
||||
// L10n.of(context)!.spaceAnalytics,
|
||||
// style: TextStyle(
|
||||
// color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
// fontSize: 18,
|
||||
// fontWeight: FontWeight.w700,
|
||||
// ),
|
||||
// overflow: TextOverflow.clip,
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
// leading: IconButton(
|
||||
// icon: const Icon(Icons.close_outlined),
|
||||
// onPressed: () => context.pop(),
|
||||
// ),
|
||||
// ),
|
||||
// body: Column(
|
||||
// children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
// children: [
|
||||
// TimeSpanMenuButton(
|
||||
// value: controller
|
||||
// .pangeaController.analytics.currentAnalyticsTimeSpan,
|
||||
// onChange: (TimeSpan value) => controller.toggleTimeSpan(
|
||||
// context,
|
||||
// value,
|
||||
// ),
|
||||
// ),
|
||||
// AnalyticsLanguageButton(
|
||||
// value:
|
||||
// controller.pangeaController.analytics.currentAnalyticsLang,
|
||||
// onChange: (lang) => controller.toggleSpaceLang(lang),
|
||||
// languages:
|
||||
// controller.pangeaController.pLanguageStore.targetOptions,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Flexible(
|
||||
// child: ListView.builder(
|
||||
// itemCount: controller.spaces.length,
|
||||
// itemBuilder: (context, i) => AnalyticsListTile(
|
||||
// defaultSelected: AnalyticsSelected(
|
||||
// controller.spaces[i].id,
|
||||
// AnalyticsEntryType.space,
|
||||
// controller.spaces[i].name,
|
||||
// ),
|
||||
// avatar: controller.spaces[i].avatar,
|
||||
// selected: AnalyticsSelected(
|
||||
// controller.spaces[i].id,
|
||||
// AnalyticsEntryType.space,
|
||||
// controller.spaces[i].name,
|
||||
// ),
|
||||
// onTap: (selected) {
|
||||
// context.go(
|
||||
// '/rooms/analytics/${selected.id}',
|
||||
// );
|
||||
// },
|
||||
// allowNavigateOnSelect: true,
|
||||
// isSelected: false,
|
||||
// pangeaController: controller.pangeaController,
|
||||
// refreshStream: controller.refreshStream,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,90 +1,90 @@
|
|||
import 'dart:developer';
|
||||
// import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
// import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
// import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
// import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
// import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
// import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
// import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
// import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../../widgets/matrix.dart';
|
||||
import '../../../controllers/pangea_controller.dart';
|
||||
import '../../../utils/sync_status_util_v2.dart';
|
||||
import '../base_analytics.dart';
|
||||
import 'student_analytics_view.dart';
|
||||
// import '../../../../widgets/matrix.dart';
|
||||
// import '../../../controllers/pangea_controller.dart';
|
||||
// import '../../../utils/sync_status_util_v2.dart';
|
||||
// import '../base_analytics.dart';
|
||||
// import 'student_analytics_view.dart';
|
||||
|
||||
class StudentAnalyticsPage extends StatefulWidget {
|
||||
final BarChartViewSelection selectedView;
|
||||
const StudentAnalyticsPage({super.key, required this.selectedView});
|
||||
// class StudentAnalyticsPage extends StatefulWidget {
|
||||
// final BarChartViewSelection selectedView;
|
||||
// const StudentAnalyticsPage({super.key, required this.selectedView});
|
||||
|
||||
@override
|
||||
State<StudentAnalyticsPage> createState() => StudentAnalyticsController();
|
||||
}
|
||||
// @override
|
||||
// State<StudentAnalyticsPage> createState() => StudentAnalyticsController();
|
||||
// }
|
||||
|
||||
class StudentAnalyticsController extends State<StudentAnalyticsPage> {
|
||||
final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
AnalyticsSelected? selected;
|
||||
// class StudentAnalyticsController extends State<StudentAnalyticsPage> {
|
||||
// final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
// AnalyticsSelected? selected;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// }
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
// @override
|
||||
// void dispose() {
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
List<Room> _chats = [];
|
||||
List<Room> get chats {
|
||||
if (_chats.isEmpty) {
|
||||
_pangeaController.matrixState.client.chatsImAStudentIn.then((result) {
|
||||
setState(() => _chats = result);
|
||||
});
|
||||
}
|
||||
return _chats;
|
||||
}
|
||||
// List<Room> _chats = [];
|
||||
// List<Room> get chats {
|
||||
// if (_chats.isEmpty) {
|
||||
// _pangeaController.matrixState.client.chatsImAStudentIn.then((result) {
|
||||
// setState(() => _chats = result);
|
||||
// });
|
||||
// }
|
||||
// return _chats;
|
||||
// }
|
||||
|
||||
List<Room> get spaces =>
|
||||
_pangeaController.matrixState.client.spacesImAStudentIn;
|
||||
// List<Room> get spaces =>
|
||||
// _pangeaController.matrixState.client.spacesImAStudentIn;
|
||||
|
||||
String? get userId {
|
||||
final id = _pangeaController.matrixState.client.userID;
|
||||
debugger(when: kDebugMode && id == null);
|
||||
return id;
|
||||
}
|
||||
// String? get userId {
|
||||
// final id = _pangeaController.matrixState.client.userID;
|
||||
// debugger(when: kDebugMode && id == null);
|
||||
// return id;
|
||||
// }
|
||||
|
||||
List<LanguageModel> get targetLanguages {
|
||||
final LanguageModel? l2 =
|
||||
_pangeaController.languageController.activeL2Model();
|
||||
final List<LanguageModel> analyticsRoomLangs =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms
|
||||
.map((analyticsRoom) => analyticsRoom.madeForLang)
|
||||
.where((langCode) => langCode != null)
|
||||
.map((langCode) => PangeaLanguage.byLangCode(langCode!))
|
||||
.where(
|
||||
(langModel) => langModel.langCode != LanguageKeys.unknownLanguage,
|
||||
)
|
||||
.toList();
|
||||
if (l2 != null) {
|
||||
analyticsRoomLangs.add(l2);
|
||||
}
|
||||
return analyticsRoomLangs.toSet().toList();
|
||||
}
|
||||
// List<LanguageModel> get targetLanguages {
|
||||
// final LanguageModel? l2 =
|
||||
// _pangeaController.languageController.activeL2Model();
|
||||
// final List<LanguageModel> analyticsRoomLangs =
|
||||
// _pangeaController.matrixState.client.allMyAnalyticsRooms
|
||||
// .map((analyticsRoom) => analyticsRoom.madeForLang)
|
||||
// .where((langCode) => langCode != null)
|
||||
// .map((langCode) => PangeaLanguage.byLangCode(langCode!))
|
||||
// .where(
|
||||
// (langModel) => langModel.langCode != LanguageKeys.unknownLanguage,
|
||||
// )
|
||||
// .toList();
|
||||
// if (l2 != null) {
|
||||
// analyticsRoomLangs.add(l2);
|
||||
// }
|
||||
// return analyticsRoomLangs.toSet().toList();
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PLoadingStatusV2(
|
||||
// if we everr want it rebuild the whole thing each time (and run initState again)
|
||||
// but this is computationally expensive!
|
||||
// key: UniqueKey(),
|
||||
shimmerChild: const ListPlaceholder(),
|
||||
// onFinish: initialize,
|
||||
child: StudentAnalyticsView(this),
|
||||
);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return PLoadingStatusV2(
|
||||
// // if we everr want it rebuild the whole thing each time (and run initState again)
|
||||
// // but this is computationally expensive!
|
||||
// // key: UniqueKey(),
|
||||
// shimmerChild: const ListPlaceholder(),
|
||||
// // onFinish: initialize,
|
||||
// child: StudentAnalyticsView(this),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,66 +1,66 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../../../../utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import '../base_analytics.dart';
|
||||
import 'student_analytics.dart';
|
||||
// import '../../../../utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
// import '../base_analytics.dart';
|
||||
// import 'student_analytics.dart';
|
||||
|
||||
class StudentAnalyticsView extends StatelessWidget {
|
||||
final StudentAnalyticsController controller;
|
||||
const StudentAnalyticsView(this.controller, {super.key});
|
||||
// class StudentAnalyticsView extends StatelessWidget {
|
||||
// final StudentAnalyticsController controller;
|
||||
// const StudentAnalyticsView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String pageTitle = L10n.of(context)!.myLearning;
|
||||
final TabData chatTabData = TabData(
|
||||
type: AnalyticsEntryType.room,
|
||||
icon: Icons.chat_bubble_outline,
|
||||
items: (controller.chats)
|
||||
.map(
|
||||
(c) => TabItem(
|
||||
avatar: c.avatar,
|
||||
displayName:
|
||||
c.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
id: c.id,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
allowNavigateOnSelect: false,
|
||||
);
|
||||
final TabData classTabData = TabData(
|
||||
type: AnalyticsEntryType.space,
|
||||
icon: Icons.workspaces,
|
||||
items: (controller.spaces ?? [])
|
||||
.map(
|
||||
(c) => TabItem(
|
||||
avatar: c.avatar,
|
||||
displayName:
|
||||
c.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
id: c.id,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
allowNavigateOnSelect: false,
|
||||
);
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final String pageTitle = L10n.of(context)!.myLearning;
|
||||
// final TabData chatTabData = TabData(
|
||||
// type: AnalyticsEntryType.room,
|
||||
// icon: Icons.chat_bubble_outline,
|
||||
// items: (controller.chats)
|
||||
// .map(
|
||||
// (c) => TabItem(
|
||||
// avatar: c.avatar,
|
||||
// displayName:
|
||||
// c.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
// id: c.id,
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// allowNavigateOnSelect: false,
|
||||
// );
|
||||
// final TabData classTabData = TabData(
|
||||
// type: AnalyticsEntryType.space,
|
||||
// icon: Icons.workspaces,
|
||||
// items: (controller.spaces ?? [])
|
||||
// .map(
|
||||
// (c) => TabItem(
|
||||
// avatar: c.avatar,
|
||||
// displayName:
|
||||
// c.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
// id: c.id,
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// allowNavigateOnSelect: false,
|
||||
// );
|
||||
|
||||
return controller.userId != null
|
||||
? BaseAnalyticsPage(
|
||||
selectedView: controller.widget.selectedView,
|
||||
pageTitle: pageTitle,
|
||||
tabs: [chatTabData, classTabData],
|
||||
alwaysSelected: AnalyticsSelected(
|
||||
controller.userId!,
|
||||
AnalyticsEntryType.student,
|
||||
L10n.of(context)!.allChatsAndClasses,
|
||||
),
|
||||
myAnalyticsController: controller,
|
||||
defaultSelected: AnalyticsSelected(
|
||||
controller.userId!,
|
||||
AnalyticsEntryType.student,
|
||||
L10n.of(context)!.allChatsAndClasses,
|
||||
),
|
||||
targetLanguages: controller.targetLanguages,
|
||||
)
|
||||
: const SizedBox();
|
||||
}
|
||||
}
|
||||
// return controller.userId != null
|
||||
// ? BaseAnalyticsPage(
|
||||
// selectedView: controller.widget.selectedView,
|
||||
// pageTitle: pageTitle,
|
||||
// tabs: [chatTabData, classTabData],
|
||||
// alwaysSelected: AnalyticsSelected(
|
||||
// controller.userId!,
|
||||
// AnalyticsEntryType.student,
|
||||
// L10n.of(context)!.allChatsAndClasses,
|
||||
// ),
|
||||
// myAnalyticsController: controller,
|
||||
// defaultSelected: AnalyticsSelected(
|
||||
// controller.userId!,
|
||||
// AnalyticsEntryType.student,
|
||||
// L10n.of(context)!.allChatsAndClasses,
|
||||
// ),
|
||||
// targetLanguages: controller.targetLanguages,
|
||||
// )
|
||||
// : const SizedBox();
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart';
|
||||
import 'package:fluffychat/utils/string_color.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// A summary of "My Analytics" shown at the top of the chat list
|
||||
/// It shows a variety of progress indicators such as
|
||||
/// messages sent, words used, and error types, which can
|
||||
/// be clicked to access more fine-grained analytics data.
|
||||
class LearningProgressIndicators extends StatefulWidget {
|
||||
const LearningProgressIndicators({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
LearningProgressIndicatorsState createState() =>
|
||||
LearningProgressIndicatorsState();
|
||||
}
|
||||
|
||||
class LearningProgressIndicatorsState
|
||||
extends State<LearningProgressIndicators> {
|
||||
final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
int? wordsUsed;
|
||||
int? errorTypes;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setData();
|
||||
}
|
||||
|
||||
AnalyticsSelected get defaultSelected => AnalyticsSelected(
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
AnalyticsEntryType.student,
|
||||
"",
|
||||
);
|
||||
|
||||
Future<void> setData() async {
|
||||
await getNumLemmasUsed();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> getNumLemmasUsed() async {
|
||||
final constructs = await _pangeaController.analytics.getConstructs(
|
||||
defaultSelected: defaultSelected,
|
||||
timeSpan: TimeSpan.forever,
|
||||
);
|
||||
if (constructs == null) {
|
||||
errorTypes = 0;
|
||||
wordsUsed = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
final List<String> errorLemmas = [];
|
||||
final List<String> vocabLemmas = [];
|
||||
for (final event in constructs) {
|
||||
for (final use in event.content.uses) {
|
||||
if (use.lemma == null) continue;
|
||||
switch (use.constructType) {
|
||||
case ConstructTypeEnum.grammar:
|
||||
errorLemmas.add(use.lemma!);
|
||||
break;
|
||||
case ConstructTypeEnum.vocab:
|
||||
vocabLemmas.add(use.lemma!);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
errorTypes = errorLemmas.toSet().length;
|
||||
wordsUsed = vocabLemmas.toSet().length;
|
||||
}
|
||||
|
||||
int? getProgressPoints(ProgressIndicatorEnum indicator) {
|
||||
switch (indicator) {
|
||||
case ProgressIndicatorEnum.wordsUsed:
|
||||
return wordsUsed;
|
||||
case ProgressIndicatorEnum.errorTypes:
|
||||
return errorTypes;
|
||||
case ProgressIndicatorEnum.level:
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
int get xpPoints {
|
||||
final points = [
|
||||
wordsUsed ?? 0,
|
||||
errorTypes ?? 0,
|
||||
];
|
||||
return points.reduce((a, b) => a + b);
|
||||
}
|
||||
|
||||
int get level => xpPoints ~/ 100;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 36,
|
||||
vertical: 16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future:
|
||||
_pangeaController.matrixState.client.getProfileFromUserId(
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final mxid = Matrix.of(context).client.userID ??
|
||||
L10n.of(context)!.user;
|
||||
return Avatar(
|
||||
name: snapshot.data?.displayName ?? mxid.localpart ?? mxid,
|
||||
mxContent: snapshot.data?.avatarUrl,
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: ProgressIndicatorEnum.values
|
||||
.where(
|
||||
(indicator) => indicator != ProgressIndicatorEnum.level,
|
||||
)
|
||||
.map(
|
||||
(indicator) => ProgressIndicatorBadge(
|
||||
points: getProgressPoints(indicator),
|
||||
onTap: () {},
|
||||
progressIndicator: indicator,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SizedBox(
|
||||
height: 35,
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: FluffyThemes.columnWidth - (36 * 2) - 25,
|
||||
child: LinearProgressIndicator(
|
||||
value: (xpPoints % 100) / 100,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
minHeight: 15,
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: "$level $xpPoints".lightColorAvatar,
|
||||
radius: 16,
|
||||
child: Text(
|
||||
"$level",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A badge that represents one learning progress indicator (i.e., construct uses)
|
||||
class ProgressIndicatorBadge extends StatelessWidget {
|
||||
final int? points;
|
||||
final VoidCallback onTap;
|
||||
final ProgressIndicatorEnum progressIndicator;
|
||||
|
||||
const ProgressIndicatorBadge({
|
||||
super.key,
|
||||
required this.points,
|
||||
required this.onTap,
|
||||
required this.progressIndicator,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
||||
child: Tooltip(
|
||||
message: progressIndicator.tooltip(context),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
progressIndicator.icon,
|
||||
color: progressIndicator.color(context),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
points != null
|
||||
? Text(
|
||||
points.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)
|
||||
: const CircularProgressIndicator.adaptive(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -139,7 +139,6 @@ abstract class ClientManager {
|
|||
timeline: StateFilter(
|
||||
notTypes: [
|
||||
PangeaEventTypes.construct,
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue