Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c2bbb25
updated minimumOSVersion
PedroLunet Mar 9, 2026
86e4725
uncommented code
PedroLunet Mar 9, 2026
f6f1753
updated parser and sheet
PedroLunet Mar 9, 2026
7fc25be
updated rooms fetching logic
PedroLunet Mar 9, 2026
cfd1e99
fixed emial fetch logic
PedroLunet Mar 9, 2026
3c2df5c
simplified email fetch logic
PedroLunet Mar 9, 2026
e1b0eae
added shimmer
PedroLunet Mar 9, 2026
b968af7
add professor schedule support to SchedulePage
PedroLunet Mar 9, 2026
e71ada9
add navProfessorSchedule route
PedroLunet Mar 9, 2026
b820949
register professor schedule route
PedroLunet Mar 9, 2026
b28dc81
show professor schedule in-app instead of external link
PedroLunet Mar 9, 2026
37c0451
Merge branch 'develop' into addmoreInfoToTeachersModal
PedroLunet Mar 14, 2026
b377e6f
Merge branch 'develop' into addmoreInfoToTeachersModal
pedroafmonteiro Mar 15, 2026
ba00179
Merge branch 'develop' into addmoreInfoToTeachersModal
pedroafmonteiro Mar 15, 2026
8754710
refactor: Remove institutional email and rooms fields and their extra…
PedroLunet Mar 15, 2026
912fb8e
Refactor: Extract professor extra information fetching logic into a d…
PedroLunet Mar 15, 2026
32ab148
refactor: extract `_ShimmerInfoRow` into a new, reusable `ShimmerInfo…
PedroLunet Mar 15, 2026
99360e9
refactor: separate professor schedule display into its own page and f…
PedroLunet Mar 15, 2026
75bec0c
reverted changes on schedule_page
PedroLunet Mar 15, 2026
769998b
refactor: update widget constructors to use super.key and clean up im…
PedroLunet Mar 15, 2026
40bc85e
fixed lint
PedroLunet Mar 15, 2026
f46b052
Merge branch 'develop' into addmoreInfoToTeachersModal
pedroafmonteiro Mar 23, 2026
3672a46
Merge branch 'develop' into addmoreInfoToTeachersModal
PedroLunet Apr 2, 2026
05daa11
removed unnecessary data duplication
PedroLunet Apr 2, 2026
68ef74b
refactored modal
PedroLunet Apr 2, 2026
8c6a234
removed duplicated logic
PedroLunet Apr 2, 2026
0858c73
removed duplicated (or more) classes
PedroLunet Apr 2, 2026
899d30f
fix lint
PedroLunet Apr 2, 2026
ce686d7
Merge branch 'develop' into addmoreInfoToTeachersModal
PedroLunet Apr 8, 2026
a514bb8
Merge branch 'develop' into addmoreInfoToTeachersModal
PedroLunet Apr 22, 2026
c9138bd
refactor: professor_info_provider - keep FutureProvider.family due to…
PedroLunet Apr 22, 2026
3167eaa
refactor: replace ProfessorLecturesParams class with record type
PedroLunet Apr 22, 2026
27d67a7
refactor: simplify deduplication in professorLecturesProvider
PedroLunet Apr 22, 2026
fd59585
fix: update shimmer_info_row colors for dark mode support
PedroLunet Apr 22, 2026
e26680f
fix: improve icon contrast in course unit info modal
PedroLunet Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions packages/uni_app/lib/controller/fetchers/professor_info_fetcher.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import 'package:html/parser.dart';
import 'package:uni/controller/networking/network_router.dart';
import 'package:uni/model/entities/course_units/sheet.dart';
import 'package:uni/session/flows/base/session.dart';

class ProfessorInfoFetcher {
Future<Professor> fetchProfessorInfo(
Professor professor,
Session session,
List<String> baseUrls,
) async {
final email = professor.institutionalEmail;
final rooms = {...professor.rooms};

if (email != null && rooms.isNotEmpty) {
return Professor(
code: professor.code,
name: professor.name,
classes: professor.classes,
institutionalEmail: email,
rooms: rooms.toList(),
picture: professor.picture,
isRegent: professor.isRegent,
);
}

for (final baseUrl in baseUrls) {
final profileUrl =
'${baseUrl}func_geral.formview?p_codigo=${professor.code}';
try {
final response = await NetworkRouter.getWithCookies(
profileUrl,
{},
session,
);
final document = parse(response.body);

var parsedEmail = email;

for (final link in document.querySelectorAll('a[href]')) {
final href = link.attributes['href'] ?? '';
if (!href.toLowerCase().startsWith('mailto:')) {
continue;
}
final value = href
.substring('mailto:'.length)
.split('?')
.first
.trim();
if (value.contains('@')) {
parsedEmail = value;
break;
}
}

if (parsedEmail == null) {
for (final link in document.querySelectorAll('a[onclick]')) {
final onclick = link.attributes['onclick'] ?? '';
final m = RegExp(
r"lto'\+':([A-Za-z0-9._%+\-]+)'\+secure\+'([A-Za-z0-9.\-]+\.[A-Za-z]{2,})'",
).firstMatch(onclick);
if (m != null) {
parsedEmail = '${m.group(1)}@${m.group(2)}';
break;
}
}
}

if (parsedEmail == null) {
continue;
}

Comment thread
PedroLunet marked this conversation as resolved.
for (final roomLink in document.querySelectorAll(
'a[href*="instal_geral.espaco_view"]',
)) {
final room = roomLink.text.trim();
if (room.isNotEmpty) {
rooms.add(room);
}
}

final roomLabelRegex = RegExp(
r'(Sala|Salas|Gabinete|Gabinetes|Room|Rooms)\s*:?\s*(.+)',
caseSensitive: false,
);

for (final row in document.querySelectorAll('tr')) {
final cells = row.querySelectorAll('th,td');
if (cells.length < 2) {
continue;
}

final label = cells.first.text.trim();
if (!RegExp(
'(Sala|Gabinete|Room)',
caseSensitive: false,
).hasMatch(label)) {
continue;
}

final value = cells[1].text.trim();
if (value.isNotEmpty) {
rooms.add(value);
}
}

for (final element in document.querySelectorAll('p,li,span,div')) {
final text = element.text.trim().replaceAll('\n', ' ');
final match = roomLabelRegex.firstMatch(text);
final value = match?.group(2)?.trim();
if (value != null && value.isNotEmpty && value.length <= 64) {
rooms.add(value);
}
}

return Professor(
code: professor.code,
name: professor.name,
classes: professor.classes,
institutionalEmail: parsedEmail,
rooms: _dedupeRooms(rooms),
picture: professor.picture,
isRegent: professor.isRegent,
);
} catch (_) {
continue;
}
}

return Professor(
code: professor.code,
name: professor.name,
classes: professor.classes,
institutionalEmail: email,
rooms: _dedupeRooms(rooms),
picture: professor.picture,
isRegent: professor.isRegent,
);
}

List<String> _dedupeRooms(Iterable<String> roomValues) {
final normalizedToDisplay = <String, String>{};

for (final raw in roomValues) {
for (final room in _splitAndCleanRooms(raw)) {
final key = room.replaceAll(RegExp(r'\s+'), '').toUpperCase();
normalizedToDisplay.putIfAbsent(key, () => room);
}
}

return normalizedToDisplay.values.toList();
}

Iterable<String> _splitAndCleanRooms(String raw) sync* {
for (final token in raw.split(RegExp(r'\s*,\s*|\s*;\s*'))) {
final value = token
.trim()
.replaceAll(
RegExp(
r'^(Salas?|Gabinetes?|Rooms?)\s*:?\s*',
caseSensitive: false,
),
'',
)
.replaceAll(RegExp(r'^[sS]\s*:\s*'), '')
.trim();
if (value.isNotEmpty) {
yield value;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Future<Sheet> parseSheet(http.Response response) async {
);

final regents = (json['responsabilidades'] as List).map((element) {
return Professor.fromJson(element as Map<String, dynamic>);
return Professor.fromJson(element as Map<String, dynamic>, isRegent: true);
}).toList();

for (final regent in regents) {
Expand Down Expand Up @@ -96,15 +96,23 @@ List<Professor> getCourseUnitProfessors(List<Map<String, dynamic>> ds) {
final professors = <Professor>[];
for (final map in ds) {
for (final docente in map['docentes'] as List<dynamic>) {
final professor = Professor(
code: (docente as Map<String, dynamic>)['doc_codigo'].toString(),
name: shortName(docente['nome'].toString()),
final professor = Professor.fromJson(
docente as Map<String, dynamic>,
classes: [map['tipo'].toString()],
);

if (professors.contains(professor)) {
professors[professors.indexWhere((element) => element == professor)]
.classes
.add(map['tipo'].toString());
final existingProfessor =
professors[professors.indexWhere(
(element) => element == professor,
)];

existingProfessor.classes.add(map['tipo'].toString());
existingProfessor.institutionalEmail ??= professor.institutionalEmail;
existingProfessor.rooms = {
...existingProfessor.rooms,
...professor.rooms,
}.toList();
} else {
professors.add(professor);
}
Expand Down
8 changes: 8 additions & 0 deletions packages/uni_app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:uni/controller/local_storage/migrations/migration_controller.dar
import 'package:uni/controller/local_storage/preferences_controller.dart';
import 'package:uni/generated/l10n.dart';
import 'package:uni/model/entities/course_units/course_unit.dart';
import 'package:uni/model/entities/course_units/sheet.dart';
import 'package:uni/model/providers/plausible/plausible_provider.dart';
import 'package:uni/model/providers/riverpod/theme_provider.dart';
import 'package:uni/utils/navigation_items.dart';
Expand All @@ -33,6 +34,7 @@ import 'package:uni/view/home/home.dart';
import 'package:uni/view/locale_notifier.dart';
import 'package:uni/view/login/login.dart';
import 'package:uni/view/map/map.dart';
import 'package:uni/view/professor/professor_schedule_page.dart';
import 'package:uni/view/profile/profile.dart';
import 'package:uni/view/restaurant/restaurant_page_view.dart';
import 'package:uni/view/splash/splash.dart';
Expand Down Expand Up @@ -181,6 +183,7 @@ class ApplicationState extends ConsumerState<Application> {
onGenerateRoute: (settings) {
final args = settings.arguments;
final courseUnit = args is CourseUnit ? args : null;
final professor = args is Professor ? args : null;
final transitionFunctions = <String, Route<dynamic> Function()>{
'/${NavigationItem.navSplash.route}': () =>
PageTransition.splashTransitionRoute(
Expand Down Expand Up @@ -250,6 +253,11 @@ class ApplicationState extends ConsumerState<Application> {
page: CourseUnitDetailPageView(courseUnit!),
settings: settings,
),
'/${NavigationItem.navProfessorSchedule.route}': () =>
PageTransition.makePageTransition(
page: ProfessorSchedulePageView(professor!),
settings: settings,
),
};

final builder = transitionFunctions[settings.name];
Expand Down
15 changes: 12 additions & 3 deletions packages/uni_app/lib/model/entities/course_units/sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,31 @@ class Professor {
required this.code,
required this.name,
required this.classes,
this.institutionalEmail,
this.rooms = const [],
this.picture,
this.isRegent = false,
});

factory Professor.fromJson(Map<String, dynamic> json) {
factory Professor.fromJson(
Map<String, dynamic> json, {
List<String> classes = const [],
bool isRegent = false,
}) {
return Professor(
code: json['codigo'].toString(),
code: (json['codigo'] ?? json['doc_codigo']).toString(),
name: shortName(json['nome'].toString()),
classes: [],
classes: classes,
isRegent: isRegent,
);
Comment thread
PedroLunet marked this conversation as resolved.
}

File? picture;
String code;
String name;
List<String> classes;
String? institutionalEmail;
List<String> rooms;
bool isRegent;

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import 'dart:collection';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:uni/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart';
import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher_new_api.dart';
import 'package:uni/model/entities/course_units/course_unit.dart';
import 'package:uni/model/entities/course_units/course_unit_class.dart';
import 'package:uni/model/entities/course_units/course_unit_directory.dart';
import 'package:uni/model/entities/course_units/sheet.dart';
import 'package:uni/model/entities/lecture.dart';
import 'package:uni/model/providers/riverpod/cached_async_notifier.dart';
import 'package:uni/model/providers/riverpod/professor_lectures_provider.dart';
import 'package:uni/model/providers/riverpod/session_provider.dart';

typedef SheetsMap = Map<CourseUnit, Sheet>;
Expand Down Expand Up @@ -205,11 +205,6 @@ class CourseUnitsInfoNotifier
}

Future<void> fetchClassProfessors(CourseUnit courseUnit) async {
final session = await ref.read(sessionProvider.future);
if (session == null) {
return;
}

final sheet = courseUnitsSheets[courseUnit];
if (sheet == null) {
return;
Expand All @@ -229,14 +224,14 @@ class CourseUnitsInfoNotifier
}

for (final professor in professors) {
final fetcher = ScheduleFetcherNewApiProfessor(
professorCode: professor.code,
);

try {
final lectures = await fetcher.getLectures(
session,
lectiveYear: lectiveYear,
final lectures = await ref.read(
professorLecturesProvider(
ProfessorLecturesParams(
professor: professor,
lectiveYear: lectiveYear,
),
).future,
);

for (final lecture in lectures) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:uni/controller/fetchers/professor_info_fetcher.dart';
import 'package:uni/model/entities/course_units/sheet.dart';
import 'package:uni/model/providers/riverpod/session_provider.dart';

final professorInfoProvider = FutureProvider.family<Professor, Professor>((
ref,
professor,
) async {
final session = await ref.read(sessionProvider.future);
final baseUrls = session != null
? List<String>.from(
session.faculties.map((f) => 'https://sigarra.up.pt/$f/pt/'),
)
: <String>[];
return ProfessorInfoFetcher().fetchProfessorInfo(
professor,
session!,
baseUrls,
);
});
Comment thread
PedroLunet marked this conversation as resolved.
Loading
Loading