Merge pull request #128 from pangeachat/error-analytics
Error Analytics
This commit is contained in:
commit
64f57e0e48
25 changed files with 1849 additions and 1041 deletions
|
|
@ -3949,5 +3949,6 @@
|
|||
}
|
||||
},
|
||||
"kickBotWarning": "Kicking Pangea Bot will remove the conversation bot from this chat.",
|
||||
"joinToView": "Join this room to view details",
|
||||
"refresh": "Refresh"
|
||||
}
|
||||
|
|
@ -105,9 +105,6 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
});
|
||||
_playAction();
|
||||
} catch (e, s) {
|
||||
// #Pangea
|
||||
debugger();
|
||||
// Pangea#
|
||||
Logs().v('Could not download audio file', e, s);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/class_code.dart';
|
||||
import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart';
|
||||
|
|
@ -57,7 +58,11 @@ class ClientChooserButton extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
enabled: matrix.client.classesAndExchangesImTeaching.isNotEmpty,
|
||||
enabled: matrix.client.rooms.any(
|
||||
(room) =>
|
||||
room.isSpace &&
|
||||
room.ownPowerLevel >= ClassDefaultValues.powerLevelOfAdmin,
|
||||
),
|
||||
value: SettingsAction.classAnalytics,
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
@ -68,7 +73,7 @@ class ClientChooserButton extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
enabled: matrix.client.classesImIn.isNotEmpty,
|
||||
enabled: matrix.client.classesAndExchangesImIn.isNotEmpty,
|
||||
value: SettingsAction.myAnalytics,
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/class_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart';
|
||||
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
|
||||
|
|
@ -142,11 +143,25 @@ class NewSpaceView extends StatelessWidget {
|
|||
? AddToClassMode.exchange
|
||||
: AddToClassMode.chat,
|
||||
),
|
||||
RoomRulesEditor(
|
||||
key: controller.rulesEditorKey,
|
||||
roomId: null,
|
||||
startOpen: false,
|
||||
initialRules: Matrix.of(context).client.lastUpdatedRoomRules,
|
||||
FutureBuilder<PangeaRoomRules?>(
|
||||
future: Matrix.of(context).client.lastUpdatedRoomRules,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return RoomRulesEditor(
|
||||
key: controller.rulesEditorKey,
|
||||
roomId: null,
|
||||
startOpen: false,
|
||||
initialRules: snapshot.data,
|
||||
);
|
||||
} else {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
// SwitchListTile.adaptive(
|
||||
// title: Text(L10n.of(context)!.spaceIsPublic),
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ class ClassController extends BaseController {
|
|||
Future<void> fixClassPowerLevels() async {
|
||||
try {
|
||||
final List<Future<void>> classFixes = [];
|
||||
for (final room in _pangeaController
|
||||
.matrixState.client.classesAndExchangesImTeaching) {
|
||||
for (final room in (await _pangeaController
|
||||
.matrixState.client.classesAndExchangesImTeaching)) {
|
||||
classFixes.add(room.setClassPowerlLevels());
|
||||
}
|
||||
await Future.wait(classFixes);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/models/headwords.dart';
|
||||
import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics_page.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -22,6 +23,7 @@ class AnalyticsController extends BaseController {
|
|||
late PangeaController _pangeaController;
|
||||
|
||||
final List<CacheModel> _cachedModels = [];
|
||||
final List<ConstructCacheEntry> _cachedConstructs = [];
|
||||
|
||||
AnalyticsController(PangeaController pangeaController) : super() {
|
||||
_pangeaController = pangeaController;
|
||||
|
|
@ -50,10 +52,10 @@ class AnalyticsController extends BaseController {
|
|||
.save(_analyticsTimeSpanKey, timeSpan.toString());
|
||||
}
|
||||
|
||||
Future<List<ChartAnalyticsModel?>> allClassAnalytics() {
|
||||
Future<List<ChartAnalyticsModel?>> allClassAnalytics() async {
|
||||
final List<Future<ChartAnalyticsModel?>> classAnalyticFutures = [];
|
||||
for (final classRoom
|
||||
in _pangeaController.matrixState.client.classesAndExchangesImTeaching) {
|
||||
for (final classRoom in (await _pangeaController
|
||||
.matrixState.client.classesAndExchangesImTeaching)) {
|
||||
classAnalyticFutures.add(
|
||||
getAnalytics(classRoom: classRoom),
|
||||
);
|
||||
|
|
@ -146,6 +148,7 @@ class AnalyticsController extends BaseController {
|
|||
debugPrint("studentAnalyticsSummaryEvent is null");
|
||||
}
|
||||
}
|
||||
|
||||
final newModel = ChartAnalyticsModel(
|
||||
timeSpan: timeSpan,
|
||||
msgs: msgs,
|
||||
|
|
@ -170,24 +173,6 @@ class AnalyticsController extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
Future<VocabHeadwords> vocabHeadwordsWithTotals(
|
||||
String langCode,
|
||||
List<ConstructEvent> vocab, [
|
||||
String? chatId,
|
||||
]) async {
|
||||
final VocabHeadwords vocabHeadwords =
|
||||
await VocabHeadwords.getHeadwords(langCode);
|
||||
for (final vocabList in vocabHeadwords.lists) {
|
||||
for (final vocabEvent in vocab) {
|
||||
vocabList.addVocabUse(
|
||||
vocabEvent.content.lemma,
|
||||
vocabEvent.content.uses,
|
||||
);
|
||||
}
|
||||
}
|
||||
return vocabHeadwords;
|
||||
}
|
||||
|
||||
Future<ChartAnalyticsModel> getAnalyticsForPrivateChats({
|
||||
TimeSpan? timeSpan,
|
||||
required Room? classRoom,
|
||||
|
|
@ -248,185 +233,400 @@ class AnalyticsController extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
List<ConstructEvent>? _constructs;
|
||||
bool settingConstructs = false;
|
||||
|
||||
List<ConstructEvent>? get constructs => _constructs;
|
||||
|
||||
String? getLangCode({
|
||||
Room? space,
|
||||
String? roomID,
|
||||
}) {
|
||||
final String? targetRoomID = space?.id ?? roomID;
|
||||
final String? roomLangCode =
|
||||
_pangeaController.languageController.activeL2Code(roomID: targetRoomID);
|
||||
final String? userLangCode =
|
||||
_pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
return roomLangCode ?? userLangCode;
|
||||
}
|
||||
|
||||
Future<Room> myAnalyticsRoom(String langCode) =>
|
||||
_pangeaController.matrixState.client.getMyAnalyticsRoom(langCode);
|
||||
|
||||
Future<List<ConstructEvent>> myConstructs(String langCode) async {
|
||||
final Room analyticsRoom = await myAnalyticsRoom(langCode);
|
||||
|
||||
return analyticsRoom.allConstructEvents;
|
||||
}
|
||||
|
||||
Future<List<ConstructEvent>> studentConstructs(
|
||||
String studentId,
|
||||
String langCode,
|
||||
) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(langCode, studentId);
|
||||
if (analyticsRoom == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "analyticsRoom missing in studentConstructs",
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
"studentId": studentId,
|
||||
"langCode": langCode,
|
||||
},
|
||||
Room? studentAnalyticsRoom(String studentId, String langCode) =>
|
||||
_pangeaController.matrixState.client.analyticsRoomLocal(
|
||||
langCode,
|
||||
studentId,
|
||||
);
|
||||
|
||||
Future<List<ConstructEvent>> allMyConstructs(
|
||||
String langCode, {
|
||||
ConstructType? type,
|
||||
}) async {
|
||||
final Room analyticsRoom = await myAnalyticsRoom(langCode);
|
||||
final List<String> adminSpaceRooms =
|
||||
await _pangeaController.matrixState.client.teacherRoomIds;
|
||||
|
||||
final allConstructs = type == null
|
||||
? await analyticsRoom.allConstructEvents
|
||||
: (await analyticsRoom.allConstructEvents)
|
||||
.where((e) => e.content.type == type)
|
||||
.toList();
|
||||
|
||||
for (int i = 0; i < allConstructs.length; i++) {
|
||||
final construct = allConstructs[i];
|
||||
final uses = construct.content.uses;
|
||||
uses.removeWhere((u) => adminSpaceRooms.contains(u.chatId));
|
||||
}
|
||||
return analyticsRoom?.allConstructEvents ?? Future.value([]);
|
||||
|
||||
return allConstructs
|
||||
.where((construct) => construct.content.uses.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<ConstructEvent>> spaceMemberVocab(String spaceId) async {
|
||||
await _pangeaController.matrixState.client.roomsLoading;
|
||||
final Room? space =
|
||||
_pangeaController.matrixState.client.getRoomById(spaceId);
|
||||
if (space == null) {
|
||||
throw Exception("space missing in spaceVocab");
|
||||
}
|
||||
final String? langCode = space.firstLanguageSettings?.targetLanguage;
|
||||
|
||||
final List<Future<List<ConstructEvent>>> vocabEventFutures = [];
|
||||
Future<List<ConstructEvent>> allSpaceMemberConstructs(
|
||||
Room space,
|
||||
String langCode, {
|
||||
ConstructType? type,
|
||||
}) async {
|
||||
final List<Future<List<ConstructEvent>>> constructEventFutures = [];
|
||||
await space.postLoad();
|
||||
await space.requestParticipants();
|
||||
for (final student in space.students) {
|
||||
final Room? room = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(langCode, student.id);
|
||||
if (room != null) vocabEventFutures.add(room.allConstructEvents);
|
||||
if (room != null) constructEventFutures.add(room.allConstructEvents);
|
||||
}
|
||||
|
||||
final List<List<ConstructEvent>> allVocabLists =
|
||||
await Future.wait(vocabEventFutures);
|
||||
final List<List<ConstructEvent>> constructLists =
|
||||
await Future.wait(constructEventFutures);
|
||||
|
||||
final List<ConstructEvent> allVocab = [];
|
||||
for (final vocabList in allVocabLists) {
|
||||
allVocab.addAll(vocabList);
|
||||
}
|
||||
return allVocab;
|
||||
}
|
||||
|
||||
/// in student analytics page, the [defaultSelected] is the student
|
||||
/// in class analytics page, the [defaultSelected] is the class
|
||||
/// [defaultSelected] should never be a chat
|
||||
/// the specific [selected] will be those items in the lists - chat, student or class
|
||||
Future<VocabHeadwords> vocabHeadsByAnalyticsSelected({
|
||||
required AnalyticsSelected? selected,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
}) async {
|
||||
Future<List<ConstructEvent>> eventsFuture;
|
||||
String langCode;
|
||||
|
||||
if (defaultSelected.type == AnalyticsEntryType.space) {
|
||||
// as long as a student isn't selected, we want the vocab events for the whole class
|
||||
final Room? classRoom =
|
||||
_pangeaController.matrixState.client.getRoomById(defaultSelected.id);
|
||||
if (classRoom?.classSettings == null) {
|
||||
throw Exception("classRoom missing in spaceMemberVocab");
|
||||
}
|
||||
langCode = classRoom!.classSettings!.targetLanguage;
|
||||
eventsFuture = selected?.type == AnalyticsEntryType.student
|
||||
? studentConstructs(selected!.id, langCode)
|
||||
: spaceMemberVocab(defaultSelected.id);
|
||||
} else if (defaultSelected.type == AnalyticsEntryType.student) {
|
||||
// in this case, we're on an individual's own analytics page
|
||||
if (selected?.type == AnalyticsEntryType.space ||
|
||||
selected?.type == AnalyticsEntryType.student) {
|
||||
langCode = _pangeaController.languageController
|
||||
.activeL2Code(roomID: selected!.id)!;
|
||||
eventsFuture = myConstructs(langCode);
|
||||
} else {
|
||||
if (_pangeaController.languageController.userL2 == null) {
|
||||
throw Exception("userL2 missing in vocabHeadsByAnalyticsSelected");
|
||||
}
|
||||
langCode = _pangeaController.languageController.userL2!.langCode;
|
||||
eventsFuture = myConstructs(langCode);
|
||||
}
|
||||
} else {
|
||||
throw Exception("invalid defaultSelected.type - ${defaultSelected.type}");
|
||||
}
|
||||
|
||||
return vocabHeadwordsWithTotals(langCode, await eventsFuture);
|
||||
}
|
||||
|
||||
/// in student analytics page, the [defaultSelected] is the student
|
||||
/// in class analytics page, the [defaultSelected] is the class
|
||||
/// [defaultSelected] should never be a chat
|
||||
/// the specific [selected] will be those items in the lists - chat, student or class
|
||||
Future<List<ConstructEvent>> constuctEventsByAnalyticsSelected({
|
||||
required AnalyticsSelected? selected,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required ConstructType constructType,
|
||||
}) async {
|
||||
late Future<List<ConstructEvent>> eventFutures;
|
||||
String? langCode;
|
||||
if (defaultSelected.type == AnalyticsEntryType.space) {
|
||||
// as long as a student isn't selected, we want the vocab events for the whole class
|
||||
final Room? space =
|
||||
_pangeaController.matrixState.client.getRoomById(defaultSelected.id);
|
||||
if (space == null) {
|
||||
throw "No space available";
|
||||
}
|
||||
langCode = space.firstLanguageSettings?.targetLanguage;
|
||||
if (langCode == null) {
|
||||
throw "No target language available";
|
||||
}
|
||||
|
||||
eventFutures = selected?.type == AnalyticsEntryType.student
|
||||
? studentConstructs(selected!.id, langCode)
|
||||
: spaceMemberVocab(defaultSelected.id);
|
||||
} else if (defaultSelected.type == AnalyticsEntryType.student) {
|
||||
// in this case, we're on an individual's own analytics page
|
||||
|
||||
if (selected?.type == AnalyticsEntryType.space ||
|
||||
selected?.type == AnalyticsEntryType.student) {
|
||||
langCode = _pangeaController.languageController
|
||||
.activeL2Code(roomID: selected!.id)!;
|
||||
eventFutures = myConstructs(langCode);
|
||||
} else {
|
||||
if (_pangeaController.languageController.userL2 == null) {
|
||||
throw "userL2 missing in constuctEventsByAnalyticsSelected";
|
||||
}
|
||||
langCode = _pangeaController.languageController.userL2!.langCode;
|
||||
eventFutures = myConstructs(langCode);
|
||||
}
|
||||
} else {
|
||||
throw "invalid defaultSelected.type - ${defaultSelected.type}";
|
||||
}
|
||||
|
||||
final List<ConstructEvent> events = (await eventFutures)
|
||||
.where(
|
||||
(element) => element.content.type == constructType,
|
||||
)
|
||||
final List<String> spaceChildrenIds = space.spaceChildren
|
||||
.map((e) => e.roomId)
|
||||
.where((e) => e != null)
|
||||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
final List<String> chatIdsToFilterBy = [];
|
||||
if (selected?.type == AnalyticsEntryType.room) {
|
||||
chatIdsToFilterBy.add(selected!.id);
|
||||
} else if (selected?.type == AnalyticsEntryType.privateChats) {
|
||||
chatIdsToFilterBy.addAll(
|
||||
_pangeaController.matrixState.client
|
||||
.getRoomById(defaultSelected.id)
|
||||
?.childrenAndGrandChildrenDirectChatIds ??
|
||||
[],
|
||||
);
|
||||
} else if (defaultSelected.type == AnalyticsEntryType.space) {
|
||||
chatIdsToFilterBy.addAll(
|
||||
_pangeaController.matrixState.client
|
||||
.getRoomById(defaultSelected.id)
|
||||
?.childrenAndGrandChildren
|
||||
.where((e) => e.roomId != null)
|
||||
.map((e) => e.roomId!) ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
if (chatIdsToFilterBy.isNotEmpty) {
|
||||
for (final event in events) {
|
||||
event.content.uses
|
||||
.removeWhere((u) => !chatIdsToFilterBy.contains(u.chatId));
|
||||
final List<ConstructEvent> allConstructs = [];
|
||||
for (final constructList in constructLists) {
|
||||
for (int i = 0; i < constructList.length; i++) {
|
||||
final construct = constructList[i];
|
||||
final uses = construct.content.uses;
|
||||
uses.removeWhere((u) => !spaceChildrenIds.contains(u.chatId));
|
||||
}
|
||||
events.removeWhere((e) => e.content.uses.isEmpty);
|
||||
allConstructs.addAll(
|
||||
constructList.where((e) => e.content.uses.isNotEmpty),
|
||||
);
|
||||
}
|
||||
|
||||
return events;
|
||||
return type == null
|
||||
? allConstructs
|
||||
: allConstructs.where((e) => e.content.type == type).toList();
|
||||
}
|
||||
|
||||
List<ConstructEvent> filterStudentConstructs(
|
||||
List<ConstructEvent> unfilteredConstructs,
|
||||
String? studentId,
|
||||
) {
|
||||
final List<ConstructEvent> filtered =
|
||||
List<ConstructEvent>.from(unfilteredConstructs);
|
||||
filtered.removeWhere((e) => e.event.senderId != studentId);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<ConstructEvent> filterRoomConstructs(
|
||||
List<ConstructEvent> unfilteredConstructs,
|
||||
String? roomID,
|
||||
) {
|
||||
List<ConstructEvent> filtered = [...unfilteredConstructs];
|
||||
filtered = unfilteredConstructs
|
||||
.where((e) => e.content.uses.any((u) => u.chatId == roomID))
|
||||
.toList();
|
||||
filtered.forEachIndexed(
|
||||
(i, _) => filtered[i].content.uses.removeWhere((u) => u.chatId != roomID),
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<ConstructEvent> filterPrivateChatConstructs(
|
||||
List<ConstructEvent> unfilteredConstructs,
|
||||
Room parentSpace,
|
||||
) {
|
||||
final List<String> directChatIds =
|
||||
parentSpace.childrenAndGrandChildrenDirectChatIds;
|
||||
List<ConstructEvent> filtered =
|
||||
List<ConstructEvent>.from(unfilteredConstructs);
|
||||
filtered = filtered.where((e) {
|
||||
return e.content.uses.any((u) => directChatIds.contains(u.chatId));
|
||||
}).toList();
|
||||
filtered.forEachIndexed(
|
||||
(i, _) => filtered[i].content.uses.removeWhere(
|
||||
(u) => !directChatIds.contains(u.chatId),
|
||||
),
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<ConstructEvent> filterSpaceConstructs(
|
||||
List<ConstructEvent> unfilteredConstructs,
|
||||
Room space,
|
||||
) {
|
||||
final List<String> chatIds = space.spaceChildren
|
||||
.map((e) => e.roomId)
|
||||
.where((e) => e != null)
|
||||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
List<ConstructEvent> filtered =
|
||||
List<ConstructEvent>.from(unfilteredConstructs);
|
||||
filtered = filtered
|
||||
.where((e) => e.content.uses.any((u) => chatIds.contains(u.chatId)))
|
||||
.toList();
|
||||
|
||||
filtered.forEachIndexed(
|
||||
(i, _) => filtered[i].content.uses.removeWhere(
|
||||
(u) => !chatIds.contains(u.chatId),
|
||||
),
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<ConstructEvent>? getConstructsLocal({
|
||||
required TimeSpan timeSpan,
|
||||
required ConstructType constructType,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
}) {
|
||||
final cachedEntry = _cachedConstructs
|
||||
.firstWhereOrNull(
|
||||
(e) =>
|
||||
e.timeSpan == timeSpan &&
|
||||
e.type == constructType &&
|
||||
e.defaultSelected.id == defaultSelected.id &&
|
||||
e.defaultSelected.type == defaultSelected.type &&
|
||||
e.selected?.id == selected?.id &&
|
||||
e.selected?.type == selected?.type,
|
||||
)
|
||||
?.events;
|
||||
return cachedEntry;
|
||||
}
|
||||
|
||||
void cacheConstructs({
|
||||
required ConstructType constructType,
|
||||
required List<ConstructEvent> events,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
}) {
|
||||
_cachedConstructs.add(
|
||||
ConstructCacheEntry(
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
type: constructType,
|
||||
events: events,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ConstructEvent>> getMyConstructs({
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required ConstructType constructType,
|
||||
required String langCode,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
final List<ConstructEvent> unfilteredConstructs = await allMyConstructs(
|
||||
langCode,
|
||||
type: constructType,
|
||||
);
|
||||
|
||||
final Room? space = selected?.type == AnalyticsEntryType.space
|
||||
? _pangeaController.matrixState.client.getRoomById(selected!.id)
|
||||
: null;
|
||||
|
||||
return filterConstructs(
|
||||
unfilteredConstructs: unfilteredConstructs,
|
||||
langCode: langCode,
|
||||
space: space,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ConstructEvent>> getSpaceConstructs({
|
||||
required ConstructType constructType,
|
||||
required Room space,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required String langCode,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
final List<ConstructEvent> unfilteredConstructs =
|
||||
await allSpaceMemberConstructs(
|
||||
space,
|
||||
langCode,
|
||||
type: constructType,
|
||||
);
|
||||
|
||||
return filterConstructs(
|
||||
unfilteredConstructs: unfilteredConstructs,
|
||||
langCode: langCode,
|
||||
space: space,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ConstructEvent>> filterConstructs({
|
||||
required List<ConstructEvent> unfilteredConstructs,
|
||||
required String langCode,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
Room? space,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
if ([AnalyticsEntryType.privateChats, AnalyticsEntryType.space]
|
||||
.contains(selected?.type)) {
|
||||
assert(space != null);
|
||||
}
|
||||
|
||||
for (int i = 0; i < unfilteredConstructs.length; i++) {
|
||||
final construct = unfilteredConstructs[i];
|
||||
final uses = construct.content.uses;
|
||||
uses.removeWhere(
|
||||
(u) => u.timeStamp.isBefore(currentAnalyticsTimeSpan.cutOffDate),
|
||||
);
|
||||
}
|
||||
|
||||
unfilteredConstructs.removeWhere((e) => e.content.uses.isEmpty);
|
||||
|
||||
switch (selected?.type) {
|
||||
case null:
|
||||
return unfilteredConstructs;
|
||||
case AnalyticsEntryType.student:
|
||||
if (defaultSelected.type != AnalyticsEntryType.space) {
|
||||
throw Exception(
|
||||
"student filtering not available for default filter ${defaultSelected.type}",
|
||||
);
|
||||
}
|
||||
final Room? analyticsRoom =
|
||||
studentAnalyticsRoom(selected!.id, langCode);
|
||||
if (analyticsRoom == null) {
|
||||
throw Exception("analyticsRoom missing in filterConstructs");
|
||||
}
|
||||
return filterStudentConstructs(unfilteredConstructs, selected.id);
|
||||
case AnalyticsEntryType.room:
|
||||
return filterRoomConstructs(unfilteredConstructs, selected?.id);
|
||||
case AnalyticsEntryType.privateChats:
|
||||
return defaultSelected.type == AnalyticsEntryType.student
|
||||
? throw "private chat filtering not available for my analytics"
|
||||
: filterPrivateChatConstructs(unfilteredConstructs, space!);
|
||||
case AnalyticsEntryType.space:
|
||||
return filterSpaceConstructs(unfilteredConstructs, space!);
|
||||
default:
|
||||
throw Exception("invalid filter type - ${selected?.type}");
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ConstructEvent>?> setConstructs({
|
||||
required ConstructType constructType,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
bool removeIT = false,
|
||||
bool forceUpdate = false,
|
||||
}) async {
|
||||
final List<ConstructEvent>? local = getConstructsLocal(
|
||||
timeSpan: currentAnalyticsTimeSpan,
|
||||
constructType: constructType,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
);
|
||||
if (local != null && !forceUpdate) {
|
||||
_constructs = local;
|
||||
return _constructs;
|
||||
}
|
||||
|
||||
if (settingConstructs) return _constructs;
|
||||
settingConstructs = true;
|
||||
await _pangeaController.matrixState.client.roomsLoading;
|
||||
Room? space;
|
||||
if (defaultSelected.type == AnalyticsEntryType.space) {
|
||||
space = _pangeaController.matrixState.client.getRoomById(
|
||||
defaultSelected.id,
|
||||
);
|
||||
}
|
||||
|
||||
final String? roomID = space?.id ?? selected?.id;
|
||||
final String? langCode = getLangCode(
|
||||
space: space,
|
||||
roomID: roomID,
|
||||
);
|
||||
|
||||
if (langCode == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "langCode missing in getConstructs",
|
||||
data: {
|
||||
"constructType": constructType,
|
||||
"AnalyticsEntryType": defaultSelected.type,
|
||||
"AnalyticsEntryId": defaultSelected.id,
|
||||
"space": space,
|
||||
},
|
||||
);
|
||||
throw "langCode missing in getConstructs";
|
||||
}
|
||||
|
||||
final filteredConstructs = space == null
|
||||
? await getMyConstructs(
|
||||
constructType: constructType,
|
||||
langCode: langCode,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
)
|
||||
: await getSpaceConstructs(
|
||||
constructType: constructType,
|
||||
space: space,
|
||||
langCode: langCode,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
);
|
||||
|
||||
_constructs = removeIT
|
||||
? filteredConstructs
|
||||
.where(
|
||||
(element) =>
|
||||
element.content.lemma != "Try interactive translation" &&
|
||||
element.content.lemma != "itStart" &&
|
||||
element.content.lemma != MatchRuleIds.interactiveTranslation,
|
||||
)
|
||||
.toList()
|
||||
: filteredConstructs;
|
||||
|
||||
if (local == null) {
|
||||
cacheConstructs(
|
||||
constructType: constructType,
|
||||
events: _constructs!,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
);
|
||||
}
|
||||
|
||||
settingConstructs = false;
|
||||
return _constructs;
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructCacheEntry {
|
||||
final TimeSpan timeSpan;
|
||||
final ConstructType type;
|
||||
final List<ConstructEvent> events;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
AnalyticsSelected? selected;
|
||||
|
||||
ConstructCacheEntry({
|
||||
required this.timeSpan,
|
||||
required this.type,
|
||||
required this.events,
|
||||
required this.defaultSelected,
|
||||
this.selected,
|
||||
});
|
||||
}
|
||||
|
||||
class CacheModel {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ class MyAnalyticsController {
|
|||
final List<StudentAnalyticsEvent?> events = await analyticsEvents(spaces);
|
||||
|
||||
for (final event in events) {
|
||||
debugPrint("adding to total ${event?.content.messages.length}");
|
||||
if (event != null) {
|
||||
event.handleNewMessage(messageRecord);
|
||||
}
|
||||
|
|
@ -69,9 +68,10 @@ class MyAnalyticsController {
|
|||
return Future.wait(events);
|
||||
}
|
||||
|
||||
Future<List<StudentAnalyticsEvent?>> allMyAnalyticsEvents() =>
|
||||
Future<List<StudentAnalyticsEvent?>> allMyAnalyticsEvents() async =>
|
||||
analyticsEvents(
|
||||
_pangeaController.matrixState.client.classesAndExchangesImStudyingIn,
|
||||
await _pangeaController
|
||||
.matrixState.client.classesAndExchangesImStudyingIn,
|
||||
);
|
||||
|
||||
Future<void> saveConstructsMixed(
|
||||
|
|
|
|||
|
|
@ -24,13 +24,22 @@ extension PangeaClient on Client {
|
|||
)
|
||||
.toList();
|
||||
|
||||
List<Room> get classesAndExchangesImTeaching => rooms
|
||||
.where(
|
||||
(e) =>
|
||||
(e.isPangeaClass || e.isExchange) &&
|
||||
e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin,
|
||||
)
|
||||
.toList();
|
||||
Future<List<Room>> get classesAndExchangesImTeaching async {
|
||||
for (final Room space in rooms.where((room) => room.isSpace)) {
|
||||
if (space.getState(EventTypes.RoomPowerLevels) == null) {
|
||||
await space.postLoad();
|
||||
}
|
||||
}
|
||||
|
||||
final spaces = rooms
|
||||
.where(
|
||||
(e) =>
|
||||
(e.isPangeaClass || e.isExchange) &&
|
||||
e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin,
|
||||
)
|
||||
.toList();
|
||||
return spaces;
|
||||
}
|
||||
|
||||
List<Room> get classesImIn => rooms
|
||||
.where(
|
||||
|
|
@ -40,20 +49,43 @@ extension PangeaClient on Client {
|
|||
)
|
||||
.toList();
|
||||
|
||||
List<Room> get classesAndExchangesImStudyingIn => rooms
|
||||
.where(
|
||||
(e) =>
|
||||
(e.isPangeaClass || e.isExchange) &&
|
||||
e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin,
|
||||
)
|
||||
.toList();
|
||||
Future<List<Room>> get classesAndExchangesImStudyingIn async {
|
||||
for (final Room space in rooms.where((room) => room.isSpace)) {
|
||||
if (space.getState(EventTypes.RoomPowerLevels) == null) {
|
||||
await space.postLoad();
|
||||
}
|
||||
}
|
||||
|
||||
final spaces = rooms
|
||||
.where(
|
||||
(e) =>
|
||||
(e.isPangeaClass || e.isExchange) &&
|
||||
e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin,
|
||||
)
|
||||
.toList();
|
||||
return spaces;
|
||||
}
|
||||
|
||||
List<Room> get classesAndExchangesImIn =>
|
||||
rooms.where((e) => e.isPangeaClass || e.isExchange).toList();
|
||||
|
||||
Future<List<String>> get teacherRoomIds async {
|
||||
final List<String> adminRoomIds = [];
|
||||
for (final Room adminSpace in (await classesAndExchangesImTeaching)) {
|
||||
adminRoomIds.add(adminSpace.id);
|
||||
final children = adminSpace.childrenAndGrandChildren;
|
||||
final List<String> adminSpaceRooms = children
|
||||
.where((e) => e.roomId != null)
|
||||
.map((e) => e.roomId!)
|
||||
.toList();
|
||||
adminRoomIds.addAll(adminSpaceRooms);
|
||||
}
|
||||
return adminRoomIds;
|
||||
}
|
||||
|
||||
Future<List<User>> get myTeachers async {
|
||||
final List<User> teachers = [];
|
||||
for (final classRoom in classesImIn) {
|
||||
for (final classRoom in classesAndExchangesImIn) {
|
||||
for (final teacher in await classRoom.teachers) {
|
||||
if (!teachers.any((e) => e.id == teacher.id)) {
|
||||
teachers.add(teacher);
|
||||
|
|
@ -68,7 +100,7 @@ extension PangeaClient on Client {
|
|||
]) async {
|
||||
try {
|
||||
final List<Future<void>> updateFutures = [];
|
||||
for (final classRoom in classesImIn) {
|
||||
for (final classRoom in classesAndExchangesImIn) {
|
||||
updateFutures
|
||||
.add(classRoom.updateMyLearningAnalyticsForClass(storageService));
|
||||
}
|
||||
|
|
@ -152,13 +184,14 @@ extension PangeaClient on Client {
|
|||
return getRoomById(roomId)!;
|
||||
}
|
||||
|
||||
PangeaRoomRules? get lastUpdatedRoomRules => classesAndExchangesImTeaching
|
||||
.where((space) => space.rulesUpdatedAt != null)
|
||||
.sorted(
|
||||
(a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!),
|
||||
)
|
||||
.firstOrNull
|
||||
?.pangeaRoomRules;
|
||||
Future<PangeaRoomRules?> get lastUpdatedRoomRules async =>
|
||||
(await classesAndExchangesImTeaching)
|
||||
.where((space) => space.rulesUpdatedAt != null)
|
||||
.sorted(
|
||||
(a, b) => b.rulesUpdatedAt!.compareTo(a.rulesUpdatedAt!),
|
||||
)
|
||||
.firstOrNull
|
||||
?.pangeaRoomRules;
|
||||
|
||||
ClassSettingsModel? get lastUpdatedClassSettings => classesImTeaching
|
||||
.where((space) => space.classSettingsUpdatedAt != null)
|
||||
|
|
|
|||
|
|
@ -170,6 +170,24 @@ extension PangeaRoom on Room {
|
|||
}
|
||||
|
||||
//note this only will return rooms that the user has joined or been invited to
|
||||
List<Room> get joinedChildren {
|
||||
if (!isSpace) return [];
|
||||
return spaceChildren
|
||||
.where((child) => child.roomId != null)
|
||||
.map(
|
||||
(child) => client.getRoomById(child.roomId!),
|
||||
)
|
||||
.where((child) => child != null)
|
||||
.cast<Room>()
|
||||
.where(
|
||||
(child) => child.membership == Membership.join,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<String> get joinedChildrenRoomIds =>
|
||||
joinedChildren.map((child) => child.id).toList();
|
||||
|
||||
List<SpaceChild> get childrenAndGrandChildren {
|
||||
if (!isSpace) return [];
|
||||
final List<SpaceChild> kids = [];
|
||||
|
|
@ -331,7 +349,6 @@ extension PangeaRoom on Room {
|
|||
bool forcedUpdate = false,
|
||||
}) async {
|
||||
try {
|
||||
debugPrint("getStudentAnalytics $studentId");
|
||||
if (!isSpace) {
|
||||
debugger(when: kDebugMode);
|
||||
throw Exception("calling getStudentAnalyticsLocal on non-space room");
|
||||
|
|
@ -475,7 +492,7 @@ extension PangeaRoom on Room {
|
|||
|
||||
if (storageService?.read(migratedAnalyticsKey) ?? false) return;
|
||||
|
||||
if (!isPangeaClass) {
|
||||
if (!isPangeaClass && !isExchange) {
|
||||
throw Exception(
|
||||
"In updateMyLearningAnalyticsForClass with room that is not not a class",
|
||||
);
|
||||
|
|
@ -558,24 +575,21 @@ extension PangeaRoom on Room {
|
|||
final List<RecentMessageRecord> msgs = [];
|
||||
for (final event in timeline.events) {
|
||||
if (event.senderId == client.userID &&
|
||||
event.type == EventTypes.Message) {
|
||||
if (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.useType,
|
||||
time: event.originServerTs,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
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.useType,
|
||||
time: event.originServerTs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return msgs;
|
||||
|
|
@ -702,7 +716,6 @@ extension PangeaRoom on Room {
|
|||
|
||||
Future<List<ConstructEvent>> get allConstructEvents async {
|
||||
await postLoad();
|
||||
|
||||
return states[PangeaEventTypes.vocab]
|
||||
?.values
|
||||
.map((Event event) => ConstructEvent(event: event))
|
||||
|
|
@ -753,7 +766,7 @@ extension PangeaRoom on Room {
|
|||
.map((e) => e.id),
|
||||
BotName.byEnvironment,
|
||||
];
|
||||
for (final teacher in await client.myTeachers) {
|
||||
for (final teacher in (await client.myTeachers)) {
|
||||
if (!toAdd.contains(teacher.id)) {
|
||||
debugPrint("inviting ${teacher.id} to analytics room");
|
||||
await invite(teacher.id);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import '../enum/construct_type_enum.dart';
|
||||
|
||||
class ConstructUses {
|
||||
|
|
@ -178,4 +179,14 @@ class OneConstructUse {
|
|||
|
||||
return data;
|
||||
}
|
||||
|
||||
Room? getRoom(Client client) {
|
||||
return client.getRoomById(chatId);
|
||||
}
|
||||
|
||||
Future<Event?> getEvent(Client client) async {
|
||||
final Room? room = getRoom(client);
|
||||
if (room == null || msgId == null) return null;
|
||||
return room.getEventById(msgId!);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
|||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/class_model.dart';
|
||||
import 'package:fluffychat/pangea/models/message_data_models.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
|
||||
|
|
@ -470,6 +471,19 @@ class PangeaMessageEvent {
|
|||
return langCode ?? LanguageKeys.unknownLanguage;
|
||||
}
|
||||
|
||||
PangeaMatch? firstErrorStep(String lemma) {
|
||||
final RepresentationEvent? repEvent = originalSent ?? originalWritten;
|
||||
if (repEvent?.choreo == null) return null;
|
||||
|
||||
final PangeaMatch? step = repEvent!.choreo!.choreoSteps
|
||||
.firstWhereOrNull(
|
||||
(element) =>
|
||||
element.acceptedOrIgnoredMatch?.match.shortMessage == lemma,
|
||||
)
|
||||
?.acceptedOrIgnoredMatch;
|
||||
return step;
|
||||
}
|
||||
|
||||
// List<SpanData> get activities =>
|
||||
//each match is turned into an activity that other students can access
|
||||
//they're not told the answer but have to find it themselves
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import 'package:fluffychat/pangea/extensions/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/extensions/pangea_room_extension.dart';
|
||||
import '../../../../utils/date_time_extension.dart';
|
||||
import '../../../widgets/avatar.dart';
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../models/chart_analytics_model.dart';
|
||||
import 'base_analytics_page.dart';
|
||||
import 'base_analytics.dart';
|
||||
import 'list_summary_analytics.dart';
|
||||
|
||||
class AnalyticsListTile extends StatelessWidget {
|
||||
class AnalyticsListTile extends StatefulWidget {
|
||||
const AnalyticsListTile({
|
||||
super.key,
|
||||
required this.model,
|
||||
|
|
@ -20,9 +19,11 @@ class AnalyticsListTile extends StatelessWidget {
|
|||
required this.avatar,
|
||||
required this.type,
|
||||
required this.id,
|
||||
required this.allowNavigateOnSelect,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
required this.allowNavigateOnSelect,
|
||||
this.enabled = true,
|
||||
this.showSpaceAnalytics = true,
|
||||
});
|
||||
|
||||
final Uri? avatar;
|
||||
|
|
@ -30,78 +31,95 @@ class AnalyticsListTile extends StatelessWidget {
|
|||
final AnalyticsEntryType type;
|
||||
final String id;
|
||||
final ChartAnalyticsModel? model;
|
||||
final bool selected;
|
||||
final bool allowNavigateOnSelect;
|
||||
|
||||
final void Function(AnalyticsSelected) onTap;
|
||||
final bool selected;
|
||||
final bool enabled;
|
||||
final bool showSpaceAnalytics;
|
||||
|
||||
@override
|
||||
AnalyticsListTileState createState() => AnalyticsListTileState();
|
||||
}
|
||||
|
||||
class AnalyticsListTileState extends State<AnalyticsListTile> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Room? room = Matrix.of(context).client.getRoomById(id);
|
||||
final Room? room = Matrix.of(context).client.getRoomById(widget.id);
|
||||
return Material(
|
||||
color: selected
|
||||
color: widget.selected
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Colors.transparent,
|
||||
child: ListTile(
|
||||
leading: type == AnalyticsEntryType.privateChats
|
||||
? CircleAvatar(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
radius: Avatar.defaultSize / 2,
|
||||
child: const Icon(Icons.forum),
|
||||
)
|
||||
: Avatar(
|
||||
mxContent: avatar,
|
||||
name: displayName,
|
||||
littleIcon: room?.roomTypeIcon,
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
displayName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
child: Opacity(
|
||||
opacity: widget.enabled ? 1 : 0.5,
|
||||
child: Tooltip(
|
||||
message: widget.enabled ? "" : L10n.of(context)!.joinToView,
|
||||
child: ListTile(
|
||||
leading: widget.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.displayName,
|
||||
littleIcon: room?.roomTypeIcon,
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.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(
|
||||
widget.model?.lastMessage?.localizedTimeShort(context) ??
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Tooltip(
|
||||
message: L10n.of(context)!.timeOfLastMessage,
|
||||
child: Text(
|
||||
model?.lastMessage?.localizedTimeShort(context) ?? "",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
subtitle: widget.showSpaceAnalytics || !(room?.isSpace ?? false)
|
||||
? ListSummaryAnalytics(
|
||||
chartAnalytics: widget.model,
|
||||
)
|
||||
: null,
|
||||
selected: widget.selected,
|
||||
enabled: widget.enabled,
|
||||
onTap: () =>
|
||||
(room?.isSpace ?? false) && widget.allowNavigateOnSelect
|
||||
? context.go(
|
||||
'/rooms/analytics/${room!.id}',
|
||||
)
|
||||
: widget.onTap(
|
||||
AnalyticsSelected(
|
||||
widget.id,
|
||||
widget.type,
|
||||
widget.displayName,
|
||||
),
|
||||
),
|
||||
trailing: (room?.isSpace ?? false) &&
|
||||
widget.type != AnalyticsEntryType.privateChats &&
|
||||
widget.allowNavigateOnSelect
|
||||
? const Icon(Icons.chevron_right)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
subtitle: ListSummaryAnalytics(
|
||||
chartAnalytics: model,
|
||||
),
|
||||
selected: selected,
|
||||
onTap: () => (room?.isSpace ?? false) && allowNavigateOnSelect
|
||||
? context.go(
|
||||
'/rooms/analytics/${room!.id}',
|
||||
)
|
||||
: onTap(
|
||||
AnalyticsSelected(
|
||||
id,
|
||||
type,
|
||||
displayName,
|
||||
),
|
||||
),
|
||||
trailing: (room?.isSpace ?? false) &&
|
||||
type != AnalyticsEntryType.privateChats &&
|
||||
allowNavigateOnSelect
|
||||
? const Icon(Icons.chevron_right)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BarChartCard extends StatelessWidget {
|
||||
const BarChartCard({
|
||||
super.key,
|
||||
required this.barChartTitle,
|
||||
required this.barChart,
|
||||
required this.legend,
|
||||
required this.loadingData,
|
||||
});
|
||||
|
||||
final String barChartTitle;
|
||||
final BarChart? barChart;
|
||||
final Widget legend;
|
||||
final bool loadingData;
|
||||
|
|
@ -28,11 +25,6 @@ class BarChartCard extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
barChartTitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Expanded(
|
||||
child: loadingData || barChart == null
|
||||
|
|
|
|||
191
lib/pangea/pages/analytics/base_analytics.dart
Normal file
191
lib/pangea/pages/analytics/base_analytics.dart
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.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: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/chart_analytics_model.dart';
|
||||
|
||||
class BaseAnalyticsPage extends StatefulWidget {
|
||||
final String pageTitle;
|
||||
final List<TabData> tabs;
|
||||
final Future Function(BuildContext) refreshData;
|
||||
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final AnalyticsSelected? alwaysSelected;
|
||||
final StudentAnalyticsController? myAnalyticsController;
|
||||
|
||||
const BaseAnalyticsPage({
|
||||
super.key,
|
||||
required this.pageTitle,
|
||||
required this.tabs,
|
||||
required this.refreshData,
|
||||
required this.alwaysSelected,
|
||||
required this.defaultSelected,
|
||||
this.myAnalyticsController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BaseAnalyticsPage> createState() => BaseAnalyticsController();
|
||||
}
|
||||
|
||||
class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
BarChartViewSelection? selectedView;
|
||||
AnalyticsSelected? selected;
|
||||
String? currentLemma;
|
||||
|
||||
bool isSelected(String chatOrStudentId) => chatOrStudentId == selected?.id;
|
||||
|
||||
ChartAnalyticsModel? chartData(
|
||||
BuildContext context,
|
||||
AnalyticsSelected? selectedParam,
|
||||
) {
|
||||
final AnalyticsSelected analyticsSelected =
|
||||
selectedParam ?? widget.defaultSelected;
|
||||
|
||||
if (analyticsSelected.type == AnalyticsEntryType.privateChats) {
|
||||
return pangeaController.analytics.getAnalyticsLocal(
|
||||
classId: analyticsSelected.id,
|
||||
chatId: AnalyticsEntryType.privateChats.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
String? chatId = analyticsSelected.type == AnalyticsEntryType.room
|
||||
? analyticsSelected.id
|
||||
: null;
|
||||
chatId ??= widget.alwaysSelected?.type == AnalyticsEntryType.room
|
||||
? widget.alwaysSelected?.id
|
||||
: null;
|
||||
|
||||
String? studentId = analyticsSelected.type == AnalyticsEntryType.student
|
||||
? analyticsSelected.id
|
||||
: null;
|
||||
studentId ??= widget.alwaysSelected?.type == AnalyticsEntryType.student
|
||||
? widget.alwaysSelected?.id
|
||||
: null;
|
||||
|
||||
String? classId = analyticsSelected.type == AnalyticsEntryType.space
|
||||
? analyticsSelected.id
|
||||
: null;
|
||||
classId ??= widget.alwaysSelected?.type == AnalyticsEntryType.space
|
||||
? widget.alwaysSelected?.id
|
||||
: null;
|
||||
|
||||
final data = pangeaController.analytics.getAnalyticsLocal(
|
||||
classId: classId,
|
||||
chatId: chatId,
|
||||
studentId: studentId,
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
TimeSpan get currentTimeSpan =>
|
||||
pangeaController.analytics.currentAnalyticsTimeSpan;
|
||||
|
||||
void navigate() {
|
||||
if (currentLemma != null) {
|
||||
setCurrentLemma(null);
|
||||
} else if (selectedView != null) {
|
||||
setSelectedView(null);
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
void toggleSelection(AnalyticsSelected selectedParam) {
|
||||
setState(() {
|
||||
debugPrint("selectedParam.id is ${selectedParam.id}");
|
||||
currentLemma = null;
|
||||
selected = isSelected(selectedParam.id) ? null : selectedParam;
|
||||
});
|
||||
pangeaController.analytics.setConstructs(
|
||||
constructType: ConstructType.grammar,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: selected,
|
||||
removeIT: true,
|
||||
);
|
||||
Future.delayed(Duration.zero, () => setState(() {}));
|
||||
}
|
||||
|
||||
Future<void> toggleTimeSpan(BuildContext context, TimeSpan timeSpan) async {
|
||||
await pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
|
||||
await widget.refreshData(context);
|
||||
await pangeaController.analytics.setConstructs(
|
||||
constructType: ConstructType.grammar,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: selected,
|
||||
removeIT: true,
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setSelectedView(BarChartViewSelection? view) {
|
||||
currentLemma = null;
|
||||
selectedView = view;
|
||||
if (!enableSelection(selected)) {
|
||||
toggleSelection(selected!);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setCurrentLemma(String? lemma) {
|
||||
currentLemma = lemma;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
bool enableSelection(AnalyticsSelected? selectedParam) {
|
||||
return selectedView == BarChartViewSelection.grammar &&
|
||||
selectedParam?.type == AnalyticsEntryType.room
|
||||
? Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(selectedParam!.id)
|
||||
?.membership ==
|
||||
Membership.join
|
||||
: true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BaseAnalyticsView(controller: this);
|
||||
}
|
||||
}
|
||||
|
||||
class TabData {
|
||||
AnalyticsEntryType type;
|
||||
IconData icon;
|
||||
List<TabItem> items;
|
||||
bool allowNavigateOnSelect;
|
||||
|
||||
TabData({
|
||||
required this.type,
|
||||
required this.items,
|
||||
required this.icon,
|
||||
this.allowNavigateOnSelect = true,
|
||||
});
|
||||
}
|
||||
|
||||
class TabItem {
|
||||
Uri? avatar;
|
||||
String displayName;
|
||||
String id;
|
||||
|
||||
TabItem({required this.avatar, required this.displayName, required this.id});
|
||||
}
|
||||
|
||||
enum AnalyticsEntryType { student, room, space, privateChats }
|
||||
|
||||
class AnalyticsSelected {
|
||||
String id;
|
||||
AnalyticsEntryType type;
|
||||
String displayName;
|
||||
|
||||
AnalyticsSelected(this.id, this.type, this.displayName);
|
||||
}
|
||||
|
|
@ -1,365 +0,0 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import '../../../widgets/layouts/max_width_body.dart';
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../controllers/pangea_controller.dart';
|
||||
import '../../enum/bar_chart_view_enum.dart';
|
||||
import '../../enum/time_span.dart';
|
||||
import '../../models/chart_analytics_model.dart';
|
||||
import 'analytics_list_tile.dart';
|
||||
import 'construct_list.dart';
|
||||
import 'messages_bar_chart.dart';
|
||||
import 'time_span_menu_button.dart';
|
||||
|
||||
class BaseAnalyticsPage extends StatefulWidget {
|
||||
final String pageTitle;
|
||||
final TabData tabData1;
|
||||
final TabData tabData2;
|
||||
final Future Function(BuildContext) refreshData;
|
||||
|
||||
final AnalyticsSelected defaultAnalyticsSelected;
|
||||
final AnalyticsSelected? alwaysSelected;
|
||||
|
||||
const BaseAnalyticsPage({
|
||||
super.key,
|
||||
required this.pageTitle,
|
||||
required this.tabData1,
|
||||
required this.tabData2,
|
||||
required this.defaultAnalyticsSelected,
|
||||
required this.refreshData,
|
||||
required this.alwaysSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BaseAnalyticsPage> createState() => BaseAnalyticsController();
|
||||
}
|
||||
|
||||
class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
||||
final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
AnalyticsSelected? selected;
|
||||
BarChartViewSelection selectedView = BarChartViewSelection.grammar;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool isSelected(String chatOrStudentId) => chatOrStudentId == selected?.id;
|
||||
|
||||
ChartAnalyticsModel? chartData(
|
||||
BuildContext context,
|
||||
AnalyticsSelected? selectedParam,
|
||||
) {
|
||||
final AnalyticsSelected analyticsSelected =
|
||||
selectedParam ?? widget.defaultAnalyticsSelected;
|
||||
|
||||
if (analyticsSelected.type == AnalyticsEntryType.privateChats) {
|
||||
return _pangeaController.analytics.getAnalyticsLocal(
|
||||
classId: analyticsSelected.id,
|
||||
chatId: AnalyticsEntryType.privateChats.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
String? chatId = analyticsSelected.type == AnalyticsEntryType.room
|
||||
? analyticsSelected.id
|
||||
: null;
|
||||
chatId ??= widget.alwaysSelected?.type == AnalyticsEntryType.room
|
||||
? widget.alwaysSelected?.id
|
||||
: null;
|
||||
|
||||
String? studentId = analyticsSelected.type == AnalyticsEntryType.student
|
||||
? analyticsSelected.id
|
||||
: null;
|
||||
studentId ??= widget.alwaysSelected?.type == AnalyticsEntryType.student
|
||||
? widget.alwaysSelected?.id
|
||||
: null;
|
||||
|
||||
String? classId = analyticsSelected.type == AnalyticsEntryType.space
|
||||
? analyticsSelected.id
|
||||
: null;
|
||||
classId ??= widget.alwaysSelected?.type == AnalyticsEntryType.space
|
||||
? widget.alwaysSelected?.id
|
||||
: null;
|
||||
|
||||
final data = _pangeaController.analytics.getAnalyticsLocal(
|
||||
classId: classId,
|
||||
chatId: chatId,
|
||||
studentId: studentId,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
String barTitle(BuildContext context) =>
|
||||
"${selectedView.string(context)}: ${selected == null ? widget.defaultAnalyticsSelected.displayName : selected!.displayName}";
|
||||
|
||||
TimeSpan get currentTimeSpan =>
|
||||
_pangeaController.analytics.currentAnalyticsTimeSpan;
|
||||
|
||||
void toggleSelection(AnalyticsSelected selectedParam) {
|
||||
setState(() {
|
||||
debugPrint("selectedParam.id is ${selectedParam.id}");
|
||||
selected = isSelected(selectedParam.id) ? null : selectedParam;
|
||||
});
|
||||
Future.delayed(Duration.zero, () => setState(() {}));
|
||||
}
|
||||
|
||||
void toggleTimeSpan(BuildContext context, TimeSpan timeSpan) {
|
||||
_pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
|
||||
setState(() {});
|
||||
widget.refreshData(context).then((value) => setState(() {}));
|
||||
}
|
||||
|
||||
void toggleSelectedView(BarChartViewSelection view) {
|
||||
selectedView = view;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BaseAnalyticsView(controller: this);
|
||||
}
|
||||
|
||||
class BaseAnalyticsView extends StatelessWidget {
|
||||
const BaseAnalyticsView({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final BaseAnalyticsController controller;
|
||||
|
||||
Widget chartView(BuildContext context) {
|
||||
switch (controller.selectedView) {
|
||||
case BarChartViewSelection.messages:
|
||||
return MessagesBarChart(
|
||||
chartAnalytics: controller.chartData(context, controller.selected),
|
||||
barChartTitle: controller.barTitle(context),
|
||||
);
|
||||
// case BarChartViewSelection.vocab:
|
||||
// return ConstructList(
|
||||
// selected: controller.selected,
|
||||
// defaultSelected: controller.widget.defaultAnalyticsSelected,
|
||||
// constructType: ConstructType.vocab,
|
||||
// );
|
||||
case BarChartViewSelection.grammar:
|
||||
return ConstructList(
|
||||
selected: controller.selected,
|
||||
defaultSelected: controller.widget.defaultAnalyticsSelected,
|
||||
constructType: ConstructType.grammar,
|
||||
title: controller.barTitle(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
controller.widget.pageTitle,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
overflow: TextOverflow.clip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
actions: [
|
||||
for (final view in BarChartViewSelection.values)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: controller.selectedView == view
|
||||
? AppConfig.primaryColor
|
||||
: null,
|
||||
),
|
||||
child: IconButton(
|
||||
isSelected: controller.selectedView == view,
|
||||
icon: Icon(view.icon),
|
||||
tooltip: view.string(context),
|
||||
onPressed: () => controller.toggleSelectedView(view),
|
||||
),
|
||||
),
|
||||
TimeSpanMenuButton(
|
||||
value: controller.currentTimeSpan,
|
||||
onChange: (TimeSpan value) =>
|
||||
controller.toggleTimeSpan(context, value),
|
||||
),
|
||||
// ChartViewPickerButton(
|
||||
// selected: controller.selectedView,
|
||||
// onChange: controller.toggleSelectedView,
|
||||
// ),
|
||||
],
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: chartView(context),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: DefaultTabController(
|
||||
length: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
tabs: [
|
||||
Tab(
|
||||
icon: Icon(
|
||||
controller.widget.tabData1.icon,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
controller.widget.tabData2.icon,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: max(
|
||||
controller.widget.tabData1.items.length + 1,
|
||||
controller.widget.tabData2.items.length,
|
||||
) *
|
||||
72,
|
||||
child: TabBarView(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
...controller.widget.tabData1.items.map(
|
||||
(item) => AnalyticsListTile(
|
||||
avatar: item.avatar,
|
||||
model: controller.chartData(
|
||||
context,
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabData1.type,
|
||||
"",
|
||||
),
|
||||
),
|
||||
displayName: item.displayName,
|
||||
id: item.id,
|
||||
type: controller.widget.tabData1.type,
|
||||
selected: controller.isSelected(item.id),
|
||||
onTap: controller.toggleSelection,
|
||||
allowNavigateOnSelect: controller.widget
|
||||
.tabData1.allowNavigateOnSelect,
|
||||
),
|
||||
),
|
||||
if (controller.widget.defaultAnalyticsSelected
|
||||
.type ==
|
||||
AnalyticsEntryType.space)
|
||||
AnalyticsListTile(
|
||||
avatar: null,
|
||||
model: controller.chartData(
|
||||
context,
|
||||
AnalyticsSelected(
|
||||
controller.widget
|
||||
.defaultAnalyticsSelected.id,
|
||||
AnalyticsEntryType.privateChats,
|
||||
L10n.of(context)!.allPrivateChats,
|
||||
),
|
||||
),
|
||||
displayName:
|
||||
L10n.of(context)!.allPrivateChats,
|
||||
id: controller
|
||||
.widget.defaultAnalyticsSelected.id,
|
||||
type: AnalyticsEntryType.privateChats,
|
||||
selected: controller.isSelected(
|
||||
controller
|
||||
.widget.defaultAnalyticsSelected.id,
|
||||
),
|
||||
onTap: controller.toggleSelection,
|
||||
allowNavigateOnSelect: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: controller.widget.tabData2.items
|
||||
.map(
|
||||
(item) => AnalyticsListTile(
|
||||
avatar: item.avatar,
|
||||
model: controller.chartData(
|
||||
context,
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabData2.type,
|
||||
"",
|
||||
),
|
||||
),
|
||||
displayName: item.displayName,
|
||||
id: item.id,
|
||||
type: controller.widget.tabData2.type,
|
||||
selected:
|
||||
controller.isSelected(item.id),
|
||||
onTap: controller.toggleSelection,
|
||||
allowNavigateOnSelect: controller.widget
|
||||
.tabData2.allowNavigateOnSelect,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabData {
|
||||
AnalyticsEntryType type;
|
||||
IconData icon;
|
||||
List<TabItem> items;
|
||||
bool allowNavigateOnSelect;
|
||||
|
||||
TabData({
|
||||
required this.type,
|
||||
required this.items,
|
||||
required this.icon,
|
||||
this.allowNavigateOnSelect = true,
|
||||
});
|
||||
}
|
||||
|
||||
class TabItem {
|
||||
Uri? avatar;
|
||||
String displayName;
|
||||
String id;
|
||||
|
||||
TabItem({required this.avatar, required this.displayName, required this.id});
|
||||
}
|
||||
|
||||
enum AnalyticsEntryType { student, room, space, privateChats }
|
||||
|
||||
class AnalyticsSelected {
|
||||
String id;
|
||||
AnalyticsEntryType type;
|
||||
String displayName;
|
||||
|
||||
AnalyticsSelected(this.id, this.type, this.displayName);
|
||||
}
|
||||
302
lib/pangea/pages/analytics/base_analytics_view.dart
Normal file
302
lib/pangea/pages/analytics/base_analytics_view.dart
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
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_list_tile.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';
|
||||
|
||||
class BaseAnalyticsView extends StatelessWidget {
|
||||
const BaseAnalyticsView({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final BaseAnalyticsController controller;
|
||||
|
||||
Widget chartView(BuildContext context) {
|
||||
if (controller.selectedView == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
switch (controller.selectedView!) {
|
||||
case BarChartViewSelection.messages:
|
||||
return MessagesBarChart(
|
||||
chartAnalytics: controller.chartData(
|
||||
context,
|
||||
controller.selected,
|
||||
),
|
||||
);
|
||||
case BarChartViewSelection.grammar:
|
||||
return ConstructList(
|
||||
constructType: ConstructType.grammar,
|
||||
defaultSelected: controller.widget.defaultSelected,
|
||||
selected: controller.selected,
|
||||
controller: controller,
|
||||
pangeaController: controller.pangeaController,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@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 = () => controller.selectedView != null
|
||||
? controller.setSelectedView(null)
|
||||
: null,
|
||||
),
|
||||
if (controller.selectedView != null)
|
||||
const TextSpan(
|
||||
text: " > ",
|
||||
),
|
||||
if (controller.selectedView != null)
|
||||
TextSpan(
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
text: controller.selectedView!.string(context),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => controller.currentLemma != null
|
||||
? controller.setCurrentLemma(null)
|
||||
: null,
|
||||
),
|
||||
if (controller.currentLemma != null)
|
||||
const TextSpan(
|
||||
text: " > ",
|
||||
),
|
||||
if (controller.currentLemma != null)
|
||||
TextSpan(
|
||||
text: controller.currentLemma,
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
recognizer: TapGestureRecognizer()..onTap = () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: controller.navigate,
|
||||
),
|
||||
actions: [
|
||||
TimeSpanMenuButton(
|
||||
value: controller.currentTimeSpan,
|
||||
onChange: (TimeSpan value) =>
|
||||
controller.toggleTimeSpan(context, value),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: false,
|
||||
child: controller.selectedView != null
|
||||
? Column(
|
||||
children: [
|
||||
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(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
...controller.widget.tabs[0].items.map(
|
||||
(item) => AnalyticsListTile(
|
||||
avatar: item.avatar,
|
||||
model: controller.chartData(
|
||||
context,
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[0].type,
|
||||
"",
|
||||
),
|
||||
),
|
||||
displayName: item.displayName,
|
||||
id: item.id,
|
||||
type:
|
||||
controller.widget.tabs[0].type,
|
||||
selected:
|
||||
controller.isSelected(item.id),
|
||||
enabled: controller.enableSelection(
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[0].type,
|
||||
"",
|
||||
),
|
||||
),
|
||||
showSpaceAnalytics: false,
|
||||
onTap: (_) =>
|
||||
controller.toggleSelection(
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[0].type,
|
||||
item.displayName,
|
||||
),
|
||||
),
|
||||
allowNavigateOnSelect: controller
|
||||
.widget
|
||||
.tabs[0]
|
||||
.allowNavigateOnSelect,
|
||||
),
|
||||
),
|
||||
if (controller
|
||||
.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.space)
|
||||
AnalyticsListTile(
|
||||
avatar: null,
|
||||
model: controller.chartData(
|
||||
context,
|
||||
AnalyticsSelected(
|
||||
controller
|
||||
.widget.defaultSelected.id,
|
||||
AnalyticsEntryType.privateChats,
|
||||
L10n.of(context)!
|
||||
.allPrivateChats,
|
||||
),
|
||||
),
|
||||
displayName: L10n.of(context)!
|
||||
.allPrivateChats,
|
||||
id: controller
|
||||
.widget.defaultSelected.id,
|
||||
type:
|
||||
AnalyticsEntryType.privateChats,
|
||||
allowNavigateOnSelect: false,
|
||||
selected: controller.isSelected(
|
||||
controller
|
||||
.widget.defaultSelected.id,
|
||||
),
|
||||
onTap: controller.toggleSelection,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: controller.widget.tabs[1].items
|
||||
.map(
|
||||
(item) => AnalyticsListTile(
|
||||
avatar: item.avatar,
|
||||
model: controller.chartData(
|
||||
context,
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller
|
||||
.widget.tabs[1].type,
|
||||
"",
|
||||
),
|
||||
),
|
||||
displayName: item.displayName,
|
||||
id: item.id,
|
||||
type: controller
|
||||
.widget.tabs[1].type,
|
||||
selected: controller
|
||||
.isSelected(item.id),
|
||||
onTap: controller.toggleSelection,
|
||||
allowNavigateOnSelect: controller
|
||||
.widget
|
||||
.tabs[1]
|
||||
.allowNavigateOnSelect,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: const Text("Error Analytics"),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor:
|
||||
Theme.of(context).textTheme.bodyLarge!.color,
|
||||
child: Icon(BarChartViewSelection.grammar.icon),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => controller.setSelectedView(
|
||||
BarChartViewSelection.grammar,
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: const Text("Message Analytics"),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor:
|
||||
Theme.of(context).textTheme.bodyLarge!.color,
|
||||
child: Icon(BarChartViewSelection.messages.icon),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => controller.setSelectedView(
|
||||
BarChartViewSelection.messages,
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,10 @@ import 'dart:async';
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/chart_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.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';
|
||||
|
|
@ -33,9 +35,43 @@ class ClassAnalyticsV2Controller extends State<ClassAnalyticsPage> {
|
|||
StreamSubscription<Event>? stateSub;
|
||||
Timer? refreshTimer;
|
||||
|
||||
List<Room> chats = [];
|
||||
List<SpaceRoomsChunk> chats = [];
|
||||
List<User> students = [];
|
||||
|
||||
String? get classId => GoRouterState.of(context).pathParameters['classid'];
|
||||
|
||||
Room? _classRoom;
|
||||
Room? get classRoom {
|
||||
if (_classRoom == null || _classRoom!.id != classId) {
|
||||
debugPrint("updating _classRoom");
|
||||
_classRoom = classId != null
|
||||
? Matrix.of(context).client.getRoomById(classId!)
|
||||
: null;
|
||||
|
||||
getChatAndStudents()
|
||||
.then(
|
||||
(_) => _pangeaController.analytics.setConstructs(
|
||||
constructType: ConstructType.grammar,
|
||||
defaultSelected: AnalyticsSelected(
|
||||
classId!,
|
||||
AnalyticsEntryType.space,
|
||||
className(context),
|
||||
),
|
||||
removeIT: true,
|
||||
forceUpdate: true,
|
||||
),
|
||||
)
|
||||
.then(
|
||||
(_) => getChatAndStudentAnalytics(context, true),
|
||||
);
|
||||
}
|
||||
return _classRoom;
|
||||
}
|
||||
|
||||
String className(BuildContext context) {
|
||||
return classRoom?.name ?? "";
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -43,6 +79,7 @@ class ClassAnalyticsV2Controller extends State<ClassAnalyticsPage> {
|
|||
if (classRoom == null || (!(classRoom?.isSpace ?? false))) {
|
||||
context.go('/rooms');
|
||||
}
|
||||
|
||||
stateSub = _pangeaController.matrixState.client.onRoomState.stream
|
||||
.where(
|
||||
(event) =>
|
||||
|
|
@ -59,13 +96,16 @@ class ClassAnalyticsV2Controller extends State<ClassAnalyticsPage> {
|
|||
await classRoom?.requestParticipants();
|
||||
|
||||
if (classRoom != null) {
|
||||
final response = await Matrix.of(context).client.getSpaceHierarchy(
|
||||
classRoom!.id,
|
||||
maxDepth: 1,
|
||||
);
|
||||
|
||||
students = classRoom!.students;
|
||||
chats = classRoom!.spaceChildren
|
||||
.where((element) => element.roomId != null)
|
||||
.map((e) => Matrix.of(context).client.getRoomById(e.roomId!))
|
||||
.where((r) => r != null)
|
||||
.cast<Room>()
|
||||
chats = response.rooms
|
||||
.where((room) => room.roomId != classRoom!.id)
|
||||
.toList();
|
||||
chats.sort((a, b) => a.roomType == 'm.space' ? -1 : 1);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
|
|
@ -131,7 +171,7 @@ class ClassAnalyticsV2Controller extends State<ClassAnalyticsPage> {
|
|||
analyticsFutures.add(
|
||||
_pangeaController.analytics.getAnalytics(
|
||||
classRoom: classRoom,
|
||||
chatId: chat.id,
|
||||
chatId: chat.roomId,
|
||||
forceUpdate: forceUpdate,
|
||||
),
|
||||
);
|
||||
|
|
@ -154,18 +194,4 @@ class ClassAnalyticsV2Controller extends State<ClassAnalyticsPage> {
|
|||
debugger(when: kDebugMode);
|
||||
}
|
||||
}
|
||||
|
||||
String? get classId => GoRouterState.of(context).pathParameters['classid'];
|
||||
|
||||
Room? _classRoom;
|
||||
Room? get classRoom {
|
||||
_classRoom ??= classId != null
|
||||
? Matrix.of(context).client.getRoomById(classId!)
|
||||
: null;
|
||||
return _classRoom;
|
||||
}
|
||||
|
||||
String className(BuildContext context) {
|
||||
return classRoom?.name ?? "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../../../../utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import '../base_analytics_page.dart';
|
||||
import '../base_analytics.dart';
|
||||
import 'class_analytics.dart';
|
||||
|
||||
class ClassAnalyticsView extends StatelessWidget {
|
||||
|
|
@ -21,10 +19,9 @@ class ClassAnalyticsView extends StatelessWidget {
|
|||
items: controller.chats
|
||||
.map(
|
||||
(room) => TabItem(
|
||||
avatar: room.avatar,
|
||||
displayName:
|
||||
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
id: room.id,
|
||||
avatar: room.avatarUrl,
|
||||
displayName: room.name ?? "",
|
||||
id: room.roomId,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
@ -46,15 +43,14 @@ class ClassAnalyticsView extends StatelessWidget {
|
|||
return controller.classId != null
|
||||
? BaseAnalyticsPage(
|
||||
pageTitle: pageTitle,
|
||||
tabData1: tab1,
|
||||
tabData2: tab2,
|
||||
defaultAnalyticsSelected: AnalyticsSelected(
|
||||
tabs: [tab1, tab2],
|
||||
refreshData: controller.getChatAndStudentAnalytics,
|
||||
alwaysSelected: AnalyticsSelected(
|
||||
controller.classId!,
|
||||
AnalyticsEntryType.space,
|
||||
controller.className(context),
|
||||
),
|
||||
refreshData: controller.getChatAndStudentAnalytics,
|
||||
alwaysSelected: AnalyticsSelected(
|
||||
defaultSelected: AnalyticsSelected(
|
||||
controller.classId!,
|
||||
AnalyticsEntryType.space,
|
||||
controller.className(context),
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
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/extensions/client_extension.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 '../../../../widgets/matrix.dart';
|
||||
import '../../../enum/time_span.dart';
|
||||
import '../base_analytics_page.dart';
|
||||
import '../base_analytics.dart';
|
||||
import 'class_list.dart';
|
||||
|
||||
class AnalyticsClassListView extends StatelessWidget {
|
||||
|
|
@ -18,8 +16,6 @@ class AnalyticsClassListView extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Room> classesAndExchanges =
|
||||
Matrix.of(context).client.classesAndExchangesImTeaching;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
|
|
@ -48,25 +44,25 @@ class AnalyticsClassListView extends StatelessWidget {
|
|||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// MessagesBarChart(
|
||||
// chartAnalytics: controller.chartData(context),
|
||||
// barChartTitle: "",
|
||||
// ),
|
||||
Flexible(
|
||||
child: ListView.builder(
|
||||
itemCount: classesAndExchanges.length,
|
||||
itemBuilder: (context, i) => AnalyticsListTile(
|
||||
avatar: classesAndExchanges[i].avatar,
|
||||
model: controller.pangeaController.analytics
|
||||
.getAnalyticsLocal(classId: classesAndExchanges[i].id),
|
||||
displayName: classesAndExchanges[i].name,
|
||||
id: classesAndExchanges[i].id,
|
||||
type: AnalyticsEntryType.space,
|
||||
selected: false,
|
||||
onTap: (selected) => context.go(
|
||||
'/rooms/analytics/${selected.id}',
|
||||
child: FutureBuilder(
|
||||
future: Matrix.of(context).client.classesAndExchangesImTeaching,
|
||||
builder: (context, snapshot) => ListView.builder(
|
||||
itemCount: snapshot.hasData ? snapshot.data?.length ?? 0 : 0,
|
||||
itemBuilder: (context, i) => AnalyticsListTile(
|
||||
avatar: snapshot.data![i].avatar,
|
||||
model: controller.pangeaController.analytics
|
||||
.getAnalyticsLocal(classId: snapshot.data![i].id),
|
||||
displayName: snapshot.data![i].name,
|
||||
id: snapshot.data![i].id,
|
||||
type: AnalyticsEntryType.space,
|
||||
// selected: false,
|
||||
onTap: (selected) => context.go(
|
||||
'/rooms/analytics/${selected.id}',
|
||||
),
|
||||
allowNavigateOnSelect: true,
|
||||
selected: false,
|
||||
),
|
||||
allowNavigateOnSelect: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,30 +1,37 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/construct_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/constructs_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/string_color.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';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics_page.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../constants/pangea_event_types.dart';
|
||||
import '../../models/construct_analytics_event.dart';
|
||||
import '../../utils/error_handler.dart';
|
||||
|
||||
class ConstructList extends StatefulWidget {
|
||||
final AnalyticsSelected? selected;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final ConstructType constructType;
|
||||
final String title;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final AnalyticsSelected? selected;
|
||||
final BaseAnalyticsController controller;
|
||||
final PangeaController pangeaController;
|
||||
|
||||
const ConstructList({
|
||||
super.key,
|
||||
required this.selected,
|
||||
required this.defaultSelected,
|
||||
required this.constructType,
|
||||
required this.title,
|
||||
required this.defaultSelected,
|
||||
required this.controller,
|
||||
required this.pangeaController,
|
||||
this.selected,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -32,77 +39,27 @@ class ConstructList extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ConstructListState extends State<ConstructList> {
|
||||
List<ConstructEvent> constructs = [];
|
||||
bool initialized = false;
|
||||
String? langCode;
|
||||
String? error;
|
||||
|
||||
StreamSubscription<Event>? stateSub;
|
||||
Timer? refreshTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_updateConstructs();
|
||||
|
||||
stateSub = MatrixState
|
||||
.pangeaController.matrixState.client.onRoomState.stream
|
||||
//could optimize here be determing if the vocab event is relevant for
|
||||
//currently displayed data
|
||||
.where((event) => event.type == PangeaEventTypes.vocab)
|
||||
.listen(onStateUpdate);
|
||||
}
|
||||
|
||||
void onStateUpdate(Event newState) {
|
||||
if (!(refreshTimer?.isActive ?? false)) {
|
||||
refreshTimer = Timer(
|
||||
const Duration(seconds: 3),
|
||||
() => _updateConstructs(),
|
||||
);
|
||||
}
|
||||
widget.pangeaController.analytics
|
||||
.setConstructs(
|
||||
constructType: widget.constructType,
|
||||
removeIT: true,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: true,
|
||||
)
|
||||
.then((_) => setState(() => initialized = true));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
refreshTimer?.cancel();
|
||||
stateSub?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ConstructList oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget.selected?.id != oldWidget.selected?.id) {
|
||||
_updateConstructs();
|
||||
}
|
||||
}
|
||||
|
||||
void _updateConstructs() {
|
||||
setState(() {
|
||||
initialized = false;
|
||||
});
|
||||
MatrixState.pangeaController.analytics
|
||||
.constuctEventsByAnalyticsSelected(
|
||||
selected: widget.selected,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
constructType: widget.constructType,
|
||||
)
|
||||
.then((value) {
|
||||
setState(() {
|
||||
constructs = value;
|
||||
initialized = true;
|
||||
error = null;
|
||||
});
|
||||
}).onError((error, stackTrace) {
|
||||
ErrorHandler.logError(e: error, s: stackTrace);
|
||||
setState(() {
|
||||
constructs = [];
|
||||
initialized = true;
|
||||
error = error?.toString();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -113,20 +70,12 @@ class ConstructListState extends State<ConstructList> {
|
|||
)
|
||||
: Column(
|
||||
children: [
|
||||
Text(
|
||||
widget.title,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
ConstructListView(
|
||||
constructs: constructs.where((element) {
|
||||
debugPrint("element type is ${element.content.type}");
|
||||
return element.content.lemma !=
|
||||
"Try interactive translation" &&
|
||||
element.content.lemma != "itStart" &&
|
||||
element.content.lemma !=
|
||||
MatchRuleIds.interactiveTranslation;
|
||||
}).toList(),
|
||||
init: initialized,
|
||||
controller: widget.controller,
|
||||
pangeaController: widget.pangeaController,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -142,39 +91,404 @@ class ConstructListState extends State<ConstructList> {
|
|||
// title = construct.content.lemma
|
||||
// subtitle = total uses, equal to construct.content.uses.length
|
||||
// list has a fixed height of 400 and is scrollable
|
||||
class ConstructListView extends StatelessWidget {
|
||||
final List<ConstructEvent> constructs;
|
||||
class ConstructListView extends StatefulWidget {
|
||||
// final List<ConstructEvent> constructs;
|
||||
final bool init;
|
||||
final BaseAnalyticsController controller;
|
||||
final PangeaController pangeaController;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final AnalyticsSelected? selected;
|
||||
|
||||
const ConstructListView({
|
||||
super.key,
|
||||
required this.constructs,
|
||||
required this.init,
|
||||
required this.controller,
|
||||
required this.pangeaController,
|
||||
required this.defaultSelected,
|
||||
this.selected,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => ConstructListViewState();
|
||||
}
|
||||
|
||||
class ConstructListViewState extends State<ConstructListView> {
|
||||
final Map<String, Timeline> _timelinesCache = {};
|
||||
final Map<String, PangeaMessageEvent> _msgEventCache = {};
|
||||
final List<PangeaMessageEvent> _msgEvents = [];
|
||||
bool fetchingUses = false;
|
||||
|
||||
StreamSubscription<Event>? stateSub;
|
||||
Timer? refreshTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
stateSub = Matrix.of(context)
|
||||
.client
|
||||
.onRoomState
|
||||
.stream
|
||||
//could optimize here be determing if the vocab event is relevant for
|
||||
//currently displayed data
|
||||
.where((event) => event.type == PangeaEventTypes.vocab)
|
||||
.listen(onStateUpdate);
|
||||
}
|
||||
|
||||
Future<void> onStateUpdate(Event? newState) async {
|
||||
debugPrint("onStateUpdate construct list");
|
||||
if (refreshTimer?.isActive ?? false) return;
|
||||
refreshTimer = Timer(
|
||||
const Duration(seconds: 3),
|
||||
() async {
|
||||
await widget.pangeaController.analytics.setConstructs(
|
||||
constructType: ConstructType.grammar,
|
||||
removeIT: true,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: true,
|
||||
);
|
||||
await fetchUses();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
refreshTimer?.cancel();
|
||||
stateSub?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ConstructListView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
fetchUses();
|
||||
}
|
||||
|
||||
int get lemmaIndex =>
|
||||
constructs?.indexWhere(
|
||||
(element) => element.content.lemma == widget.controller.currentLemma,
|
||||
) ??
|
||||
-1;
|
||||
|
||||
Future<PangeaMessageEvent?> getMessageEvent(
|
||||
OneConstructUse use,
|
||||
) async {
|
||||
final Client client = Matrix.of(context).client;
|
||||
PangeaMessageEvent msgEvent;
|
||||
if (_msgEventCache.containsKey(use.msgId!)) {
|
||||
return _msgEventCache[use.msgId!]!;
|
||||
}
|
||||
final Room? msgRoom = use.getRoom(client);
|
||||
if (msgRoom == null || use.msgId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Timeline? timeline;
|
||||
if (_timelinesCache.containsKey(use.chatId)) {
|
||||
timeline = _timelinesCache[use.chatId];
|
||||
} else {
|
||||
timeline = await msgRoom.getTimeline();
|
||||
_timelinesCache[use.chatId] = timeline;
|
||||
}
|
||||
|
||||
final Event? event = await use.getEvent(client);
|
||||
if (event == null || timeline == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
msgEvent = PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
ownMessage: event.senderId == client.userID,
|
||||
);
|
||||
_msgEventCache[use.msgId!] = msgEvent;
|
||||
return msgEvent;
|
||||
}
|
||||
|
||||
Future<void> fetchUses() async {
|
||||
if (fetchingUses) return;
|
||||
if (currentConstruct == null) {
|
||||
setState(() => _msgEvents.clear());
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => fetchingUses = true);
|
||||
final List<OneConstructUse> uses = currentConstruct!.content.uses;
|
||||
_msgEvents.clear();
|
||||
|
||||
for (final OneConstructUse use in uses) {
|
||||
final PangeaMessageEvent? msgEvent = await getMessageEvent(use);
|
||||
final RepresentationEvent? repEvent =
|
||||
msgEvent?.originalSent ?? msgEvent?.originalWritten;
|
||||
if (repEvent?.choreo == null) {
|
||||
continue;
|
||||
}
|
||||
_msgEvents.add(msgEvent!);
|
||||
}
|
||||
setState(() => fetchingUses = false);
|
||||
}
|
||||
|
||||
List<ConstructEvent>? get constructs =>
|
||||
widget.pangeaController.analytics.constructs;
|
||||
|
||||
ConstructEvent? get currentConstruct => constructs?.firstWhereOrNull(
|
||||
(element) => element.content.lemma == widget.controller.currentLemma,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!init) {
|
||||
if (!widget.init || fetchingUses) {
|
||||
return const Expanded(
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
if (constructs.isEmpty) {
|
||||
if ((constructs?.isEmpty ?? true) ||
|
||||
(widget.controller.currentLemma != null && currentConstruct == null)) {
|
||||
return Expanded(
|
||||
child: Center(child: Text(L10n.of(context)!.noDataFound)),
|
||||
);
|
||||
}
|
||||
return Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: constructs.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title: Text(constructs[index].content.lemma),
|
||||
subtitle: Text(
|
||||
'${L10n.of(context)!.total} ${constructs[index].content.uses.length}',
|
||||
|
||||
return widget.controller.currentLemma == null
|
||||
? Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: constructs!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
constructs![index].content.lemma,
|
||||
),
|
||||
subtitle: Text(
|
||||
'${L10n.of(context)!.total} ${constructs![index].content.uses.length}',
|
||||
),
|
||||
onTap: () {
|
||||
final String lemma = constructs![index].content.lemma;
|
||||
widget.controller.setCurrentLemma(lemma);
|
||||
fetchUses();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (constructs![lemmaIndex].content.uses.length >
|
||||
_msgEvents.length)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
"Some data may be missing from rooms in which you are not a member.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider(height: 1),
|
||||
itemCount: _msgEvents.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ConstructMessage(
|
||||
msgEvent: _msgEvents[index],
|
||||
lemma: widget.controller.currentLemma!,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructMessage extends StatelessWidget {
|
||||
final PangeaMessageEvent msgEvent;
|
||||
final String lemma;
|
||||
|
||||
const ConstructMessage({
|
||||
super.key,
|
||||
required this.msgEvent,
|
||||
required this.lemma,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final PangeaMatch? errorMessage = msgEvent.firstErrorStep(lemma);
|
||||
if (errorMessage == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final String? chosen = errorMessage.match.choices
|
||||
?.firstWhereOrNull(
|
||||
(element) => element.selected == true,
|
||||
)
|
||||
?.value;
|
||||
|
||||
if (chosen == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
ConstructMessageMetadata(msgEvent: msgEvent),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FutureBuilder<User?>(
|
||||
future: msgEvent.event.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
final displayname = snapshot.data?.calcDisplayname() ??
|
||||
msgEvent.event.senderFromMemoryOrFallback
|
||||
.calcDisplayname();
|
||||
return Text(
|
||||
displayname,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: (Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? displayname.color
|
||||
: displayname.lightColorText),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ConstructMessageBubble(
|
||||
errorText: errorMessage.match.fullText,
|
||||
replacementText: chosen,
|
||||
start: errorMessage.match.offset,
|
||||
end:
|
||||
errorMessage.match.offset + errorMessage.match.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructMessageBubble extends StatelessWidget {
|
||||
final String errorText;
|
||||
final String replacementText;
|
||||
final int? start;
|
||||
final int? end;
|
||||
|
||||
const ConstructMessageBubble({
|
||||
super.key,
|
||||
required this.errorText,
|
||||
required this.replacementText,
|
||||
this.start,
|
||||
this.end,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultStyle = TextStyle(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor,
|
||||
height: 1.3,
|
||||
);
|
||||
|
||||
return IntrinsicWidth(
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
topRight: Radius.circular(AppConfig.borderRadius),
|
||||
bottomLeft: Radius.circular(AppConfig.borderRadius),
|
||||
bottomRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: RichText(
|
||||
text: (start == null || 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructMessageMetadata extends StatelessWidget {
|
||||
final PangeaMessageEvent msgEvent;
|
||||
|
||||
const ConstructMessageMetadata({
|
||||
super.key,
|
||||
required this.msgEvent,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 30, 0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
msgEvent.event.originServerTs.localizedTime(context),
|
||||
style: TextStyle(fontSize: 13 * AppConfig.fontSizeFactor),
|
||||
),
|
||||
Text(msgEvent.event.room.name),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,10 @@ import 'messages_legend_widget.dart';
|
|||
|
||||
class MessagesBarChart extends StatefulWidget {
|
||||
final ChartAnalyticsModel? chartAnalytics;
|
||||
final String barChartTitle;
|
||||
|
||||
const MessagesBarChart({
|
||||
super.key,
|
||||
required this.chartAnalytics,
|
||||
required this.barChartTitle,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -95,7 +93,6 @@ class MessagesBarChartState extends State<MessagesBarChart> {
|
|||
);
|
||||
|
||||
return BarChartCard(
|
||||
barChartTitle: widget.barChartTitle,
|
||||
barChart: barChart,
|
||||
loadingData: widget.chartAnalytics == null,
|
||||
legend: const MessagesLegendsListWidget(),
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/chart_analytics_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 '../../../extensions/client_extension.dart';
|
||||
import '../../../utils/sync_status_util_v2.dart';
|
||||
import '../base_analytics_page.dart';
|
||||
import '../base_analytics.dart';
|
||||
import 'student_analytics_view.dart';
|
||||
|
||||
class StudentAnalyticsPage extends StatefulWidget {
|
||||
|
|
@ -24,16 +25,42 @@ class StudentAnalyticsPage extends StatefulWidget {
|
|||
|
||||
class StudentAnalyticsController extends State<StudentAnalyticsPage> {
|
||||
final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
|
||||
AnalyticsSelected? selected;
|
||||
StreamSubscription<Event>? stateSub;
|
||||
Timer? refreshTimer;
|
||||
|
||||
List<Room> _chats = [];
|
||||
List<Room> _spaces = [];
|
||||
|
||||
void onStateUpdate(Event newState) {
|
||||
if (!(refreshTimer?.isActive ?? false)) {
|
||||
refreshTimer = Timer(
|
||||
const Duration(seconds: 3),
|
||||
() => getClassAndChatAnalytics(context, true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_pangeaController.matrixState.client
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
refreshTimer?.cancel();
|
||||
stateSub?.cancel();
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
await _pangeaController.matrixState.client
|
||||
.updateMyLearningAnalyticsForAllClassesImIn(
|
||||
_pangeaController.pStoreService,
|
||||
);
|
||||
super.initState();
|
||||
await getClassAndChatAnalytics(context);
|
||||
stateSub = _pangeaController.matrixState.client.onRoomState.stream
|
||||
.where(
|
||||
(event) =>
|
||||
event.type == PangeaEventTypes.studentAnalyticsSummary &&
|
||||
event.senderId == userId,
|
||||
)
|
||||
.listen(onStateUpdate);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -43,55 +70,84 @@ class StudentAnalyticsController extends State<StudentAnalyticsPage> {
|
|||
// but this is computationally expensive!
|
||||
// key: UniqueKey(),
|
||||
shimmerChild: const ListPlaceholder(),
|
||||
onFinish: () {
|
||||
getClassAndChatAnalytics(context);
|
||||
},
|
||||
onFinish: initialize,
|
||||
child: StudentAnalyticsView(this),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getClassAndChatAnalytics(BuildContext context) async {
|
||||
Future<void> getClassAndChatAnalytics(
|
||||
BuildContext context, [
|
||||
forceUpdate = false,
|
||||
]) async {
|
||||
final List<Future<ChartAnalyticsModel?>> analyticsFutures = [];
|
||||
for (final chat in chats(context)) {
|
||||
for (final chat in (await getChats())) {
|
||||
analyticsFutures.add(
|
||||
_pangeaController.analytics.getAnalytics(
|
||||
chatId: chat.id,
|
||||
studentId: userId,
|
||||
forceUpdate: forceUpdate,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final space in spaces(context)) {
|
||||
for (final space in (await getSpaces())) {
|
||||
analyticsFutures.add(
|
||||
_pangeaController.analytics.getAnalytics(
|
||||
classRoom: space,
|
||||
studentId: userId,
|
||||
forceUpdate: forceUpdate,
|
||||
),
|
||||
);
|
||||
}
|
||||
analyticsFutures.add(
|
||||
_pangeaController.analytics.getAnalytics(studentId: userId),
|
||||
_pangeaController.analytics.getAnalytics(
|
||||
studentId: userId,
|
||||
forceUpdate: forceUpdate,
|
||||
),
|
||||
);
|
||||
await Future.wait(analyticsFutures);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
List<Room> spaces(BuildContext context) {
|
||||
Future<List<Room>> getSpaces() async {
|
||||
final List<Room> rooms = await _pangeaController
|
||||
.matrixState.client.classesAndExchangesImStudyingIn;
|
||||
setState(() => _spaces = rooms);
|
||||
return rooms;
|
||||
}
|
||||
|
||||
List<Room>? get spaces {
|
||||
try {
|
||||
return _pangeaController
|
||||
.matrixState.client.classesAndExchangesImStudyingIn;
|
||||
if (_spaces.isNotEmpty) return _spaces;
|
||||
getSpaces();
|
||||
return _spaces;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
List<Room> chats(BuildContext context) {
|
||||
Future<List<Room>> getChats() async {
|
||||
final List<String> teacherRoomIds =
|
||||
await Matrix.of(context).client.teacherRoomIds;
|
||||
_chats = Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
.where(
|
||||
(r) =>
|
||||
!r.isSpace &&
|
||||
!r.isAnalyticsRoom &&
|
||||
!teacherRoomIds.contains(r.id),
|
||||
)
|
||||
.toList();
|
||||
setState(() => _chats = _chats);
|
||||
return _chats;
|
||||
}
|
||||
|
||||
List<Room>? get chats {
|
||||
try {
|
||||
return Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
.where((r) => !r.isSpace && !r.isAnalyticsRoom)
|
||||
.toList();
|
||||
if (_chats.isNotEmpty) return _chats;
|
||||
getChats();
|
||||
return _chats;
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../../utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import '../base_analytics_page.dart';
|
||||
import '../base_analytics.dart';
|
||||
import 'student_analytics.dart';
|
||||
|
||||
class StudentAnalyticsView extends StatelessWidget {
|
||||
|
|
@ -13,14 +11,11 @@ class StudentAnalyticsView extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Room> chats = controller.chats(context);
|
||||
final List<Room> spaces = controller.spaces(context);
|
||||
|
||||
final String pageTitle = L10n.of(context)!.myLearning;
|
||||
final TabData chatTabData = TabData(
|
||||
type: AnalyticsEntryType.room,
|
||||
icon: Icons.chat_bubble_outline,
|
||||
items: chats
|
||||
items: (controller.chats ?? [])
|
||||
.map(
|
||||
(c) => TabItem(
|
||||
avatar: c.avatar,
|
||||
|
|
@ -35,7 +30,7 @@ class StudentAnalyticsView extends StatelessWidget {
|
|||
final TabData classTabData = TabData(
|
||||
type: AnalyticsEntryType.space,
|
||||
icon: Icons.workspaces,
|
||||
items: spaces
|
||||
items: (controller.spaces ?? [])
|
||||
.map(
|
||||
(c) => TabItem(
|
||||
avatar: c.avatar,
|
||||
|
|
@ -51,15 +46,15 @@ class StudentAnalyticsView extends StatelessWidget {
|
|||
return controller.userId != null
|
||||
? BaseAnalyticsPage(
|
||||
pageTitle: pageTitle,
|
||||
tabData1: chatTabData,
|
||||
tabData2: classTabData,
|
||||
defaultAnalyticsSelected: AnalyticsSelected(
|
||||
tabs: [chatTabData, classTabData],
|
||||
refreshData: controller.getClassAndChatAnalytics,
|
||||
alwaysSelected: AnalyticsSelected(
|
||||
controller.userId!,
|
||||
AnalyticsEntryType.student,
|
||||
L10n.of(context)!.allChatsAndClasses,
|
||||
),
|
||||
refreshData: controller.getClassAndChatAnalytics,
|
||||
alwaysSelected: AnalyticsSelected(
|
||||
myAnalyticsController: controller,
|
||||
defaultSelected: AnalyticsSelected(
|
||||
controller.userId!,
|
||||
AnalyticsEntryType.student,
|
||||
L10n.of(context)!.allChatsAndClasses,
|
||||
|
|
|
|||
|
|
@ -1,174 +1,174 @@
|
|||
import 'package:flutter/material.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
// import 'package:fl_chart/fl_chart.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/headwords.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics_page.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'bar_chart_card.dart';
|
||||
import 'messages_legend_widget.dart';
|
||||
// import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
// import 'package:fluffychat/pangea/models/headwords.dart';
|
||||
// import 'package:fluffychat/pangea/pages/analytics/base_analytics_page.dart';
|
||||
// import 'package:fluffychat/widgets/matrix.dart';
|
||||
// import 'bar_chart_card.dart';
|
||||
// import 'messages_legend_widget.dart';
|
||||
|
||||
class VocabBarChart extends StatefulWidget {
|
||||
final AnalyticsSelected? selected;
|
||||
final AnalyticsSelected defaultSelected;
|
||||
// class VocabBarChart extends StatefulWidget {
|
||||
// final AnalyticsSelected? selected;
|
||||
// final AnalyticsSelected defaultSelected;
|
||||
|
||||
const VocabBarChart({
|
||||
super.key,
|
||||
required this.selected,
|
||||
required this.defaultSelected,
|
||||
});
|
||||
// const VocabBarChart({
|
||||
// super.key,
|
||||
// required this.selected,
|
||||
// required this.defaultSelected,
|
||||
// });
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => VocabBarChartState();
|
||||
}
|
||||
// @override
|
||||
// State<StatefulWidget> createState() => VocabBarChartState();
|
||||
// }
|
||||
|
||||
class VocabBarChartState extends State<VocabBarChart> {
|
||||
final double barSpace = 16;
|
||||
// class VocabBarChartState extends State<VocabBarChart> {
|
||||
// final double barSpace = 16;
|
||||
|
||||
final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
// final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
}
|
||||
// @override
|
||||
// initState() {
|
||||
// super.initState();
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<VocabHeadwords>(
|
||||
future: _pangeaController.analytics.vocabHeadsByAnalyticsSelected(
|
||||
selected: widget.selected,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
),
|
||||
builder: ((context, snapshot) => BarChartCard(
|
||||
barChartTitle: (widget.selected != null
|
||||
? widget.selected!
|
||||
: widget.defaultSelected)
|
||||
.displayName,
|
||||
barChart: snapshot.hasData
|
||||
? buildBarChart(context, snapshot.data!)
|
||||
: null,
|
||||
loadingData: snapshot.connectionState != ConnectionState.done,
|
||||
legend: const MessagesLegendsListWidget(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return FutureBuilder<VocabHeadwords>(
|
||||
// future: _pangeaController.analytics.vocabHeadsByAnalyticsSelected(
|
||||
// selected: widget.selected,
|
||||
// defaultSelected: widget.defaultSelected,
|
||||
// ),
|
||||
// builder: ((context, snapshot) => BarChartCard(
|
||||
// barChartTitle: (widget.selected != null
|
||||
// ? widget.selected!
|
||||
// : widget.defaultSelected)
|
||||
// .displayName,
|
||||
// barChart: snapshot.hasData
|
||||
// ? buildBarChart(context, snapshot.data!)
|
||||
// : null,
|
||||
// loadingData: snapshot.connectionState != ConnectionState.done,
|
||||
// legend: const MessagesLegendsListWidget(),
|
||||
// )),
|
||||
// );
|
||||
// }
|
||||
|
||||
TextStyle titleTextStyle(BuildContext context) => TextStyle(
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
fontSize: 10,
|
||||
);
|
||||
// TextStyle titleTextStyle(BuildContext context) => TextStyle(
|
||||
// color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
// fontSize: 10,
|
||||
// );
|
||||
|
||||
BarChart buildBarChart(BuildContext context, VocabHeadwords vocabHeadwords) {
|
||||
final flLine = FlLine(
|
||||
color: Theme.of(context).dividerColor,
|
||||
strokeWidth: 1,
|
||||
);
|
||||
// BarChart buildBarChart(BuildContext context, VocabHeadwords vocabHeadwords) {
|
||||
// final flLine = FlLine(
|
||||
// color: Theme.of(context).dividerColor,
|
||||
// strokeWidth: 1,
|
||||
// );
|
||||
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceEvenly,
|
||||
barTouchData: BarTouchData(
|
||||
enabled: false,
|
||||
),
|
||||
// barTouchData: barTouchData,
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 28,
|
||||
getTitlesWidget: (double value, TitleMeta meta) =>
|
||||
SideTitleWidget(
|
||||
axisSide: meta.axisSide,
|
||||
child: Text(
|
||||
vocabHeadwords.lists[value.toInt()].name,
|
||||
style: titleTextStyle(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
getTitlesWidget: (double value, TitleMeta meta) {
|
||||
Widget textWidget;
|
||||
if (value != meta.max) {
|
||||
textWidget =
|
||||
Text(meta.formattedValue, style: titleTextStyle(context));
|
||||
} else {
|
||||
textWidget = const Icon(Icons.abc_outlined, size: 14);
|
||||
}
|
||||
return SideTitleWidget(
|
||||
axisSide: meta.axisSide,
|
||||
child: textWidget,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
topTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
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(vocabHeadwords),
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
swapAnimationDuration: const Duration(milliseconds: 250),
|
||||
);
|
||||
}
|
||||
// return BarChart(
|
||||
// BarChartData(
|
||||
// alignment: BarChartAlignment.spaceEvenly,
|
||||
// barTouchData: BarTouchData(
|
||||
// enabled: false,
|
||||
// ),
|
||||
// // barTouchData: barTouchData,
|
||||
// titlesData: FlTitlesData(
|
||||
// show: true,
|
||||
// bottomTitles: AxisTitles(
|
||||
// sideTitles: SideTitles(
|
||||
// showTitles: true,
|
||||
// reservedSize: 28,
|
||||
// getTitlesWidget: (double value, TitleMeta meta) =>
|
||||
// SideTitleWidget(
|
||||
// axisSide: meta.axisSide,
|
||||
// child: Text(
|
||||
// vocabHeadwords.lists[value.toInt()].name,
|
||||
// style: titleTextStyle(context),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// leftTitles: AxisTitles(
|
||||
// sideTitles: SideTitles(
|
||||
// showTitles: true,
|
||||
// reservedSize: 40,
|
||||
// getTitlesWidget: (double value, TitleMeta meta) {
|
||||
// Widget textWidget;
|
||||
// if (value != meta.max) {
|
||||
// textWidget =
|
||||
// Text(meta.formattedValue, style: titleTextStyle(context));
|
||||
// } else {
|
||||
// textWidget = const Icon(Icons.abc_outlined, size: 14);
|
||||
// }
|
||||
// return SideTitleWidget(
|
||||
// axisSide: meta.axisSide,
|
||||
// child: textWidget,
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// topTitles: AxisTitles(
|
||||
// sideTitles: SideTitles(showTitles: false),
|
||||
// ),
|
||||
// rightTitles: AxisTitles(
|
||||
// sideTitles: SideTitles(showTitles: false),
|
||||
// ),
|
||||
// ),
|
||||
// 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(vocabHeadwords),
|
||||
// backgroundColor: Colors.transparent,
|
||||
// ),
|
||||
// swapAnimationDuration: const Duration(milliseconds: 250),
|
||||
// );
|
||||
// }
|
||||
|
||||
List<BarChartGroupData> barChartGroupData(VocabHeadwords vocabHeadwords) {
|
||||
// sort vocab into lists
|
||||
// calculate levels based on vocab data
|
||||
// List<BarChartGroupData> barChartGroupData(VocabHeadwords vocabHeadwords) {
|
||||
// // sort vocab into lists
|
||||
// // calculate levels based on vocab data
|
||||
|
||||
final List<BarChartGroupData> chartData = [];
|
||||
// final List<BarChartGroupData> chartData = [];
|
||||
|
||||
vocabHeadwords.lists.asMap().forEach((index, intervalGroup) {
|
||||
chartData.add(
|
||||
BarChartGroupData(
|
||||
x: index,
|
||||
barsSpace: barSpace,
|
||||
// barRods: intervalGroup.map(constructBarChartRodData).toList(),
|
||||
barRods: constructBarChartRodData(intervalGroup),
|
||||
),
|
||||
);
|
||||
});
|
||||
return chartData;
|
||||
}
|
||||
// vocabHeadwords.lists.asMap().forEach((index, intervalGroup) {
|
||||
// chartData.add(
|
||||
// BarChartGroupData(
|
||||
// x: index,
|
||||
// barsSpace: barSpace,
|
||||
// // barRods: intervalGroup.map(constructBarChartRodData).toList(),
|
||||
// barRods: constructBarChartRodData(intervalGroup),
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// return chartData;
|
||||
// }
|
||||
|
||||
List<BarChartRodData> constructBarChartRodData(VocabList vocabList) {
|
||||
final ListTotals listTotals = vocabList.calculuateTotals();
|
||||
final y1 = listTotals.low;
|
||||
final y2 = y1 + listTotals.medium;
|
||||
final y3 = y2 + listTotals.high;
|
||||
// List<BarChartRodData> constructBarChartRodData(VocabList vocabList) {
|
||||
// final ListTotals listTotals = vocabList.calculuateTotals();
|
||||
// final y1 = listTotals.low;
|
||||
// final y2 = y1 + listTotals.medium;
|
||||
// final y3 = y2 + listTotals.high;
|
||||
|
||||
return [
|
||||
BarChartRodData(
|
||||
toY: y3.toDouble(),
|
||||
width: 10.toDouble(),
|
||||
rodStackItems: [
|
||||
BarChartRodStackItem(0, y1.toDouble(), Colors.red),
|
||||
BarChartRodStackItem(y1.toDouble(), y2.toDouble(), Colors.grey),
|
||||
BarChartRodStackItem(y2.toDouble(), y3.toDouble(), Colors.green),
|
||||
],
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
// return [
|
||||
// BarChartRodData(
|
||||
// toY: y3.toDouble(),
|
||||
// width: 10.toDouble(),
|
||||
// rodStackItems: [
|
||||
// BarChartRodStackItem(0, y1.toDouble(), Colors.red),
|
||||
// BarChartRodStackItem(y1.toDouble(), y2.toDouble(), Colors.grey),
|
||||
// BarChartRodStackItem(y2.toDouble(), y3.toDouble(), Colors.green),
|
||||
// ],
|
||||
// borderRadius: BorderRadius.zero,
|
||||
// ),
|
||||
// ];
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ name: fluffychat
|
|||
# #Pangea
|
||||
# description: Chat with your friends.
|
||||
description: Learn a language while texting your friends.
|
||||
# !!!!!! flutter version: 3.16.9 !!!!!!
|
||||
# Pangea#
|
||||
publish_to: none
|
||||
version: 1.11.2+3453
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue