diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index b57a618d..18cfcaa5 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -41,6 +41,7 @@ class MessageLookup extends MessageLookupByLibrary { "accountLock": MessageLookupByLibrary.simpleMessage("Account is locked, please try again in 15 minutes"), "accountNull": MessageLookupByLibrary.simpleMessage("Please enter your account"), "accountPasswordError": MessageLookupByLibrary.simpleMessage("Account password incorrect"), + "aduit": MessageLookupByLibrary.simpleMessage("Aduit"), "aestheticDimension": MessageLookupByLibrary.simpleMessage("Aesthetic dimension"), "alertError": MessageLookupByLibrary.simpleMessage("An error occurred"), "androidPrivateBrowseGuideSubTitle": @@ -122,6 +123,8 @@ class MessageLookup extends MessageLookupByLibrary { "getCalendar": MessageLookupByLibrary.simpleMessage("Getting calendar..."), "getCalendarError": MessageLookupByLibrary.simpleMessage("Getting calendar error"), "getCourse": MessageLookupByLibrary.simpleMessage("Get schedule..."), + "getCourseClassmateList": MessageLookupByLibrary.simpleMessage("Getting classmate list..."), + "getCourseClassmateListError": MessageLookupByLibrary.simpleMessage("Get classmate list error"), "getCourseDetail": MessageLookupByLibrary.simpleMessage("Reading course materials..."), "getCourseDetailError": MessageLookupByLibrary.simpleMessage("Course data reading error"), "getCourseError": MessageLookupByLibrary.simpleMessage("Getting schedule error"), @@ -181,6 +184,8 @@ class MessageLookup extends MessageLookupByLibrary { "logoutWarning": MessageLookupByLibrary.simpleMessage("Are you sure you want to log out? \nAll data will be cleared"), "missingRequiredInformation": MessageLookupByLibrary.simpleMessage("Missing required information"), + "name": MessageLookupByLibrary.simpleMessage("name"), + "nationalTaipeiUniversity": MessageLookupByLibrary.simpleMessage("NTPU"), "naturalDimension": MessageLookupByLibrary.simpleMessage("Natural dimension"), "needsVerifyMobileWarning": MessageLookupByLibrary.simpleMessage( "You may be asked to verify your phone number\nplease check the website entrance for further information."), @@ -270,10 +275,12 @@ class MessageLookup extends MessageLookupByLibrary { "sociologicalDimension": MessageLookupByLibrary.simpleMessage("Sociological Dimension"), "sortBy": MessageLookupByLibrary.simpleMessage("Sort by"), "startClass": MessageLookupByLibrary.simpleMessage("Start class"), + "studentId": MessageLookupByLibrary.simpleMessage("student id"), "studentList": MessageLookupByLibrary.simpleMessage("Student list"), "subscribe": MessageLookupByLibrary.simpleMessage("Subscribe"), "sure": MessageLookupByLibrary.simpleMessage("Sure"), "syllabus": MessageLookupByLibrary.simpleMessage("Syllabus"), + "taipeiMedicineUniversity": MessageLookupByLibrary.simpleMessage("TMU"), "takeCore": MessageLookupByLibrary.simpleMessage("Take core"), "takeForeignDepartmentCredits": MessageLookupByLibrary.simpleMessage("Foreign Dep Credits"), "takeForeignDepartmentCreditsLimit": MessageLookupByLibrary.simpleMessage("Credit limit"), @@ -285,9 +292,11 @@ class MessageLookup extends MessageLookupByLibrary { "titleScore": MessageLookupByLibrary.simpleMessage("Score"), "totalAverage": MessageLookupByLibrary.simpleMessage("Total average"), "totalPeople": MessageLookupByLibrary.simpleMessage("Total people"), + "unknownDepartment": MessageLookupByLibrary.simpleMessage("Unknown Department"), "unknownError": MessageLookupByLibrary.simpleMessage("An unknown error occurred"), "unknownServerError": MessageLookupByLibrary.simpleMessage( "A problem occurred when communicating with the campus server, most features may be affected\nPlease confirm that the campus system can be used normally and try again, thanks"), + "unknownStudent": MessageLookupByLibrary.simpleMessage("Unknown Student"), "update": MessageLookupByLibrary.simpleMessage("Update"), "useOldPassword": MessageLookupByLibrary.simpleMessage("Use old password"), "versionInfo": MessageLookupByLibrary.simpleMessage("Version info"), diff --git a/lib/generated/intl/messages_zh_TW.dart b/lib/generated/intl/messages_zh_TW.dart index c6d61bf8..b072674b 100644 --- a/lib/generated/intl/messages_zh_TW.dart +++ b/lib/generated/intl/messages_zh_TW.dart @@ -40,6 +40,7 @@ class MessageLookup extends MessageLookupByLibrary { "accountLock": MessageLookupByLibrary.simpleMessage("帳號已被鎖住,請15分鐘後再試"), "accountNull": MessageLookupByLibrary.simpleMessage("請輸入帳號"), "accountPasswordError": MessageLookupByLibrary.simpleMessage("帳號密碼錯誤"), + "aduit": MessageLookupByLibrary.simpleMessage("隨班附讀"), "aestheticDimension": MessageLookupByLibrary.simpleMessage("美學向度"), "alertError": MessageLookupByLibrary.simpleMessage("發生錯誤"), "androidPrivateBrowseGuideSubTitle": MessageLookupByLibrary.simpleMessage("開啟隱私瀏覽,安全更有保障"), @@ -120,6 +121,8 @@ class MessageLookup extends MessageLookupByLibrary { "getCalendar": MessageLookupByLibrary.simpleMessage("取得行事曆中..."), "getCalendarError": MessageLookupByLibrary.simpleMessage("取得行事曆錯誤"), "getCourse": MessageLookupByLibrary.simpleMessage("取得課表..."), + "getCourseClassmateList": MessageLookupByLibrary.simpleMessage("獲取學生名單中..."), + "getCourseClassmateListError": MessageLookupByLibrary.simpleMessage("獲取學生名單錯誤"), "getCourseDetail": MessageLookupByLibrary.simpleMessage("課程資料讀取中..."), "getCourseDetailError": MessageLookupByLibrary.simpleMessage("課程資料讀取錯誤"), "getCourseError": MessageLookupByLibrary.simpleMessage("取得課表錯誤"), @@ -175,6 +178,8 @@ class MessageLookup extends MessageLookupByLibrary { "logout": MessageLookupByLibrary.simpleMessage("登出"), "logoutWarning": MessageLookupByLibrary.simpleMessage("確定要登出嗎? \n將會清除所有資料"), "missingRequiredInformation": MessageLookupByLibrary.simpleMessage("缺少必要資訊"), + "name": MessageLookupByLibrary.simpleMessage("姓名"), + "nationalTaipeiUniversity": MessageLookupByLibrary.simpleMessage("國立臺北大學"), "naturalDimension": MessageLookupByLibrary.simpleMessage("自然向度"), "needsVerifyMobileWarning": MessageLookupByLibrary.simpleMessage("您可能被要求進行手機號碼驗證\n請至網頁版入口網站進行查看"), "networkError": MessageLookupByLibrary.simpleMessage("網路發生錯誤"), @@ -255,10 +260,12 @@ class MessageLookup extends MessageLookupByLibrary { "sociologicalDimension": MessageLookupByLibrary.simpleMessage("社哲向度"), "sortBy": MessageLookupByLibrary.simpleMessage("排序"), "startClass": MessageLookupByLibrary.simpleMessage("開課班級"), + "studentId": MessageLookupByLibrary.simpleMessage("學號"), "studentList": MessageLookupByLibrary.simpleMessage("學生清單"), "subscribe": MessageLookupByLibrary.simpleMessage("訂閱"), "sure": MessageLookupByLibrary.simpleMessage("確定"), "syllabus": MessageLookupByLibrary.simpleMessage("教學大綱"), + "taipeiMedicineUniversity": MessageLookupByLibrary.simpleMessage("臺北醫學大學"), "takeCore": MessageLookupByLibrary.simpleMessage("實得核心"), "takeForeignDepartmentCredits": MessageLookupByLibrary.simpleMessage("外系學分"), "takeForeignDepartmentCreditsLimit": MessageLookupByLibrary.simpleMessage("學分上限"), @@ -270,8 +277,10 @@ class MessageLookup extends MessageLookupByLibrary { "titleScore": MessageLookupByLibrary.simpleMessage("成績"), "totalAverage": MessageLookupByLibrary.simpleMessage("總平均"), "totalPeople": MessageLookupByLibrary.simpleMessage("總人數"), + "unknownDepartment": MessageLookupByLibrary.simpleMessage("未知系所"), "unknownError": MessageLookupByLibrary.simpleMessage("發生未知錯誤"), "unknownServerError": MessageLookupByLibrary.simpleMessage("與校園系統溝通時發生問題,大部分功能可能將受影響\n請確認校園系統能正常使用後再試一次,謝謝"), + "unknownStudent": MessageLookupByLibrary.simpleMessage("未知學生"), "update": MessageLookupByLibrary.simpleMessage("更新"), "useOldPassword": MessageLookupByLibrary.simpleMessage("延長原始密碼時間"), "versionInfo": MessageLookupByLibrary.simpleMessage("版本資訊"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 0b085eb9..f7dcff55 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2617,6 +2617,96 @@ class S { args: [], ); } + + /// `Unknown Department` + String get unknownDepartment { + return Intl.message( + 'Unknown Department', + name: 'unknownDepartment', + desc: '', + args: [], + ); + } + + /// `Unknown Student` + String get unknownStudent { + return Intl.message( + 'Unknown Student', + name: 'unknownStudent', + desc: '', + args: [], + ); + } + + /// `name` + String get name { + return Intl.message( + 'name', + name: 'name', + desc: '', + args: [], + ); + } + + /// `student id` + String get studentId { + return Intl.message( + 'student id', + name: 'studentId', + desc: '', + args: [], + ); + } + + /// `Getting classmate list...` + String get getCourseClassmateList { + return Intl.message( + 'Getting classmate list...', + name: 'getCourseClassmateList', + desc: '', + args: [], + ); + } + + /// `Get classmate list error` + String get getCourseClassmateListError { + return Intl.message( + 'Get classmate list error', + name: 'getCourseClassmateListError', + desc: '', + args: [], + ); + } + + /// `NTPU` + String get nationalTaipeiUniversity { + return Intl.message( + 'NTPU', + name: 'nationalTaipeiUniversity', + desc: '', + args: [], + ); + } + + /// `TMU` + String get taipeiMedicineUniversity { + return Intl.message( + 'TMU', + name: 'taipeiMedicineUniversity', + desc: '', + args: [], + ); + } + + /// `Aduit` + String get aduit { + return Intl.message( + 'Aduit', + name: 'aduit', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 6215966b..24ce6687 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -256,5 +256,14 @@ "androidPrivateBrowseGuideTitle": "About Incognito Browse", "androidPrivateBrowseGuideSubTitle": "Open Incognito browsing to enhanced security", "kClass": "class", - "kDepartment": "department" + "kDepartment": "department", + "unknownDepartment": "Unknown Department", + "unknownStudent": "Unknown Student", + "name": "name", + "studentId": "student id", + "getCourseClassmateList": "Getting classmate list...", + "getCourseClassmateListError": "Get classmate list error", + "nationalTaipeiUniversity": "NTPU", + "taipeiMedicineUniversity": "TMU", + "aduit": "Aduit" } \ No newline at end of file diff --git a/lib/l10n/intl_zh_TW.arb b/lib/l10n/intl_zh_TW.arb index 19cf8144..23f1f3f1 100644 --- a/lib/l10n/intl_zh_TW.arb +++ b/lib/l10n/intl_zh_TW.arb @@ -256,5 +256,14 @@ "androidPrivateBrowseGuideTitle": "關於隱私瀏覽", "androidPrivateBrowseGuideSubTitle": "開啟隱私瀏覽,安全更有保障", "kClass": "班級", - "kDepartment": "系所" + "kDepartment": "系所", + "unknownDepartment": "未知系所", + "unknownStudent": "未知學生", + "name": "姓名", + "studentId": "學號", + "getCourseClassmateList": "獲取學生名單中...", + "getCourseClassmateListError": "獲取學生名單錯誤", + "nationalTaipeiUniversity": "國立臺北大學", + "taipeiMedicineUniversity": "臺北醫學大學", + "aduit": "隨班附讀" } \ No newline at end of file diff --git a/lib/src/connector/course_connector.dart b/lib/src/connector/course_connector.dart index aa92a42d..a5930570 100644 --- a/lib/src/connector/course_connector.dart +++ b/lib/src/connector/course_connector.dart @@ -29,6 +29,7 @@ class CourseConnector { static const String _postTeacherCourseCNUrl = "${_courseCNHost}Teach.jsp"; static const String _postCourseENUrl = "${_courseENHost}Select.jsp"; static const String _creditUrl = "${_courseCNHost}Cprog.jsp"; + static const String _getCourseDepartmentUrl = "${_courseCNHost}Subj.jsp"; static Future login() async { String result; @@ -64,6 +65,78 @@ class CourseConnector { } } + // It should use code with key (59 -> CSIE, 32 -> Electric), and department name with value. + static Future> getDepartmentMap(String year, String semester) async { + try { + ConnectorParameter parameter = ConnectorParameter(_getCourseDepartmentUrl); + parameter.data = {"format": "-2", "year": year, "sem": semester}; + String result = await Connector.getDataByGet(parameter); + + Document tagNode = parse(result); + List departmentNodes = tagNode.getElementsByTagName("a"); + + Map departmentMap = {}; + for (Element element in departmentNodes) { + final href = element.attributes["href"]; + if (href.isEmpty) { + continue; + } + String codeParameter = href.split("&").firstWhere((parameter) => parameter.contains("code"), orElse: () => ""); + if (codeParameter == "") { + continue; + } + String code = codeParameter.split("=")[1]; + String departmentName = element.text; + departmentMap.putIfAbsent(code, () => departmentName); + } + + return departmentMap; + } catch (e, stack) { + Log.eWithStack(e, stack); + return null; + } + } + + static Future> getTwoYearUndergraduateDepartmentMap(String year) async { + try { + ConnectorParameter parameter = ConnectorParameter(_creditUrl); + parameter.data = {"format": "-3", "year": year, "matric": "6"}; + String result = await Connector.getDataByGet(parameter); + + Document tagNode = parse(result); + List departmentNodes = tagNode.getElementsByTagName("a"); + + Map departmentMap = {}; + for (Element element in departmentNodes) { + final href = element.attributes["href"]; + if (href.isEmpty) { + continue; + } + String divisionParameter = + href.split("&").firstWhere((parameter) => parameter.contains("division"), orElse: () => ""); + if (divisionParameter == "") { + continue; + } + final String code = divisionParameter.split("=")[1]; + final RegExp regExp = RegExp(".+【(.+)】"); + final RegExpMatch matches = regExp.firstMatch(element.text); + if (matches == null || matches.groupCount == 0) { + continue; + } + final departmentName = matches.group(1); + if (departmentName.isEmpty) { + continue; + } + departmentMap.putIfAbsent(code, () => departmentName); + } + + return departmentMap; + } catch (e, stack) { + Log.eWithStack(e, stack); + return null; + } + } + static Future getCourseENName(String url) async { try { ConnectorParameter parameter; diff --git a/lib/src/connector/ischool_plus_connector.dart b/lib/src/connector/ischool_plus_connector.dart index 4086ccb1..663e5a60 100644 --- a/lib/src/connector/ischool_plus_connector.dart +++ b/lib/src/connector/ischool_plus_connector.dart @@ -13,6 +13,7 @@ import 'package:flutter_app/src/util/html_utils.dart'; import 'package:html/dom.dart' as html; import 'package:html/parser.dart' as html; +import '../model/course/course_student.dart'; import 'core/connector_parameter.dart'; import 'ntut_connector.dart'; @@ -32,6 +33,7 @@ class ISchoolPlusConnector { //static final String _postLoginISchoolUrl = _iSchoolPlusUrl + "login.php"; //static final String _iSchoolPlusIndexUrl = _iSchoolPlusUrl + "mooc/index.php"; static const String _getCourseName = "${_iSchoolPlusUrl}learn/mooc_sysbar.php"; + static const _getCourseStudentList = "${_iSchoolPlusUrl}learn/learn_ranking.php"; static const _ssoLoginUrl = "${NTUTConnector.host}ssoIndex.do"; /// The Authorization Step of ISchool (2023-10-21) @@ -112,6 +114,53 @@ class ISchoolPlusConnector { } } + static Future>> getCourseStudent(String courseId) async { + try { + if (!await _selectCourse(courseId)) { + final returnResult = ReturnWithStatus(); + returnResult.status = IPlusReturnStatus.noPermission; + return returnResult; + } + + ConnectorParameter parameter = ConnectorParameter(_getCourseStudentList); + String result = await Connector.getDataByGet(parameter); + + html.Document tagNode = html.parse(result); + html.Element table = tagNode.querySelectorAll('table')[1]; + List nodes = table.querySelectorAll('tr'); + + List courseStudents = []; + for (int i = 0; i < nodes.length; i++) { + html.Element node = nodes[i].querySelectorAll('td')[1]; + + String information = node.querySelector('div').innerHtml; + int splitIndex = information.indexOf(' '); + + String studentId = information.substring(0, splitIndex); + String studentName = information.substring(splitIndex + 2, information.length - 1); + + // 過濾掉校務人士,如有多身分考慮枚舉或過濾 Email + if (studentId == 'istudyoaa') { + continue; + } + + CourseStudent courseStudent = CourseStudent(department: "", id: studentId, name: studentName); + courseStudents.add(courseStudent); + } + courseStudents.sort((a, b) => a.id.compareTo(b.id)); + + final returnResult = ReturnWithStatus>(); + returnResult.status = IPlusReturnStatus.success; + returnResult.result = courseStudents; + return returnResult; + } catch (e, stack) { + Log.eWithStack(e, stack); + final returnResult = ReturnWithStatus(); + returnResult.status = IPlusReturnStatus.fail; + return returnResult; + } + } + static Future>> getCourseFile(String courseId) async { ConnectorParameter parameter; String result; diff --git a/lib/src/model/course/course_student.dart b/lib/src/model/course/course_student.dart new file mode 100644 index 00000000..6e95cd46 --- /dev/null +++ b/lib/src/model/course/course_student.dart @@ -0,0 +1,11 @@ +class CourseStudent { + final String department; + final String id; + final String name; + + CourseStudent.origin() + : department = "", + id = "", + name = ""; + CourseStudent({required this.department, required this.id, required this.name}); +} diff --git a/lib/src/task/course/course_department_map_task.dart b/lib/src/task/course/course_department_map_task.dart new file mode 100644 index 00000000..8c883d16 --- /dev/null +++ b/lib/src/task/course/course_department_map_task.dart @@ -0,0 +1,37 @@ +// ignore_for_file: import_of_legacy_library_into_null_safe + +import 'package:flutter_app/src/connector/course_connector.dart'; +import 'package:flutter_app/src/r.dart'; + +import '../task.dart'; +import 'course_system_task.dart'; + +class CourseDepartmentMapTask extends CourseSystemTask> { + final String year; + final String semester; + + CourseDepartmentMapTask({required this.year, required this.semester}) : super("CourseDepartmentMapTask"); + + @override + Future execute() async { + final status = await super.execute(); + if (status == TaskStatus.success) { + super.onStart(R.current.searchingCreditInfo); + final value = await CourseConnector.getDepartmentMap(year, semester); + final twoYearProgramDepartmentMap = await CourseConnector.getTwoYearUndergraduateDepartmentMap(year); + super.onEnd(); + + final collection = {}; + collection.addAll(value); + collection.addAll(twoYearProgramDepartmentMap); + + if (collection.isNotEmpty) { + result = collection; + return TaskStatus.success; + } else { + return TaskStatus.shouldGiveUp; + } + } + return status; + } +} diff --git a/lib/src/task/iplus/iplus_get_course_student_list_task.dart b/lib/src/task/iplus/iplus_get_course_student_list_task.dart new file mode 100644 index 00000000..5f70d03a --- /dev/null +++ b/lib/src/task/iplus/iplus_get_course_student_list_task.dart @@ -0,0 +1,43 @@ +// ignore_for_file: import_of_legacy_library_into_null_safe + +import 'package:awesome_dialog/awesome_dialog.dart'; +import 'package:flutter_app/src/connector/ischool_plus_connector.dart'; +import 'package:flutter_app/src/model/course/course_student.dart'; +import 'package:flutter_app/src/r.dart'; +import 'package:flutter_app/ui/other/msg_dialog.dart'; + +import '../task.dart'; +import 'iplus_system_task.dart'; + +class IPlusGetStudentListTask extends IPlusSystemTask> { + String courseId; + + IPlusGetStudentListTask({required this.courseId}) : super("IPlusCourseAnnouncementTask"); + + @override + Future execute() async { + final status = await super.execute(); + if (status == TaskStatus.success) { + super.onStart(R.current.getISchoolPlusCourseAnnouncement); + final value = await ISchoolPlusConnector.getCourseStudent(courseId); + super.onEnd(); + switch (value.status) { + case IPlusReturnStatus.success: + result = value.result; + return TaskStatus.success; + case IPlusReturnStatus.fail: + return super.onError(R.current.getISchoolPlusCourseAnnouncementError); + case IPlusReturnStatus.noPermission: + final parameter = MsgDialogParameter( + title: R.current.warning, + dialogType: DialogType.info, + desc: R.current.iPlusNoThisClass, + okButtonText: R.current.sure, + removeCancelButton: true, + ); + return super.onErrorParameter(parameter); + } + } + return status; + } +} diff --git a/lib/ui/pages/coursedetail/screen/course_info_page.dart b/lib/ui/pages/coursedetail/screen/course_info_page.dart index 6ff49c14..cd6824a5 100644 --- a/lib/ui/pages/coursedetail/screen/course_info_page.dart +++ b/lib/ui/pages/coursedetail/screen/course_info_page.dart @@ -5,14 +5,17 @@ import 'dart:async'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_app/src/model/course/course_main_extra_json.dart'; +import 'package:flutter_app/src/model/course/course_student.dart'; import 'package:flutter_app/src/model/coursetable/course_table_json.dart'; import 'package:flutter_app/src/r.dart'; +import 'package:flutter_app/src/task/course/course_department_map_task.dart'; import 'package:flutter_app/src/task/course/course_extra_info_task.dart'; import 'package:flutter_app/src/task/task_flow.dart'; import 'package:flutter_app/ui/other/route_utils.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:get/get.dart'; import 'package:sprintf/sprintf.dart'; +import 'package:flutter_app/src/task/iplus/iplus_get_course_student_list_task.dart'; class CourseInfoPage extends StatefulWidget { final CourseInfoJson courseInfo; @@ -39,6 +42,7 @@ class _CourseInfoPageState extends State with AutomaticKeepAlive super.initState(); isLoading = true; BackButtonInterceptor.add(myInterceptor); + Future.microtask(() => _loadCourseStudent()); Future.delayed(Duration.zero, () { _addTask(); }); @@ -90,6 +94,31 @@ class _CourseInfoPageState extends State with AutomaticKeepAlive listItem.add(_buildInfoTitle(R.current.courseData)); listItem.addAll(courseData); + List students = await _getCourseStudent(); + Map departmentMap = await _getCourseDepartmentMap(); + + if (students.isNotEmpty) { + listItem.add( + Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 0), + child: _buildClassmateInfo(1, R.current.kDepartment, R.current.studentId, R.current.name, isHeader: true), + ), + ); + + for (int i = 0; i < students.length; i++) { + final student = students.elementAt(i); + final studentName = student.name.isEmpty ? R.current.unknownStudent : student.name; + final studentId = student.id; + final department = getDepartment(departmentMap, studentId); + listItem.add( + Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 0), + child: _buildClassmateInfo(i, department, studentId, studentName), + ), + ); + } + } + isLoading = false; setState(() {}); } @@ -171,6 +200,116 @@ class _CourseInfoPageState extends State with AutomaticKeepAlive } } + Future> _getCourseStudent() async { + TaskFlow taskFlow = TaskFlow(); + final task = IPlusGetStudentListTask(courseId: courseMainInfo.course.id); + taskFlow.addTask(task); + if (await taskFlow.start()) { + List students = task.result; + if (students != null) { + return students; + } + } + return []; + } + + Future> _getCourseDepartmentMap() async { + final semester = widget.courseInfo.extra.courseSemester; + TaskFlow taskFlow = TaskFlow(); + final task = CourseDepartmentMapTask(year: semester.year, semester: semester.semester); + taskFlow.addTask(task); + if (await taskFlow.start()) { + Map departmentMap = task.result; + if (departmentMap != null) { + return departmentMap; + } + } + return {}; + } + + void _loadCourseStudent() async { + TaskFlow taskFlow = TaskFlow(); + final task = IPlusGetStudentListTask(courseId: courseMainInfo.course.id); + taskFlow.addTask(task); + if (await taskFlow.start()) { + task.result; + } + } + + String getDepartment(Map departmentMap, String studentId) { + /* + * Since we don't have official data to describe the following hard-coded rule is correct. + * It may need to confirm or just leave it. + */ + if (studentId.substring(0, 1) == "4") { + return R.current.nationalTaipeiUniversity; + } + + if (studentId.substring(0, 1) == "B") { + return R.current.taipeiMedicineUniversity; + } + + if (studentId.substring(3, 6) == "054") { + return R.current.aduit; + } + + String department = departmentMap[studentId.substring(3, 5)]; + + if (department != null) { + return department; + } + + department = departmentMap[studentId.substring(3, 6)]; + + if (department != null) { + return department; + } + + return R.current.unknownDepartment; + } + + Widget _buildClassmateInfo(int index, String departmentName, String studentId, String studentName, + {bool isHeader = false}) { + double height = isHeader ? 25 : 50; + + final color = (index % 2 == 1) + ? Theme.of(context).colorScheme.surface + : Theme.of(context).colorScheme.surfaceVariant.withAlpha(widget.courseInfoWithAlpha); + return Container( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox(width: 4, height: height), + Expanded( + child: Text( + departmentName, + textAlign: TextAlign.center, + ), + ), + SizedBox(width: 4, height: height), + Expanded( + child: Text( + studentId, + textAlign: TextAlign.center, + ), + ), + SizedBox(width: 4, height: height), + Expanded( + child: Text( + studentName, + textAlign: TextAlign.center, + ), + ), + ], + ), + ); + } + Widget _buildCourseInfoWithButton(String text, String buttonText, String url) { return Container( padding: const EdgeInsets.only(bottom: 5),