Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
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,11 +19,13 @@ 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';
import 'package:uni/view/about/about.dart';
import 'package:uni/view/academic_path/academic_path.dart';
import 'package:uni/view/academic_path/schedule_page.dart';
import 'package:uni/view/bug_report/bug_report.dart';
import 'package:uni/view/calendar/calendar.dart';
import 'package:uni/view/course_unit_info/course_unit_info.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
65 changes: 62 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,81 @@ 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,
institutionalEmail: _extractInstitutionalEmail(json),
rooms: _extractRooms(json),
isRegent: isRegent,
);
Comment thread
PedroLunet marked this conversation as resolved.
}

static String? _extractInstitutionalEmail(Map<String, dynamic> json) {
final candidates = [
json['email_institucional'],
json['email'],
json['mail'],
json['e_mail'],
json['contacto'],
];

for (final candidate in candidates) {
final value = candidate?.toString().trim();
if (value != null && value.isNotEmpty && value.contains('@')) {
return value;
}
}

return null;
}

static List<String> _extractRooms(Map<String, dynamic> json) {
final candidates = [
json['salas'],
json['sala'],
json['gabinete'],
json['gabinetes'],
];

final rooms = <String>{};

for (final candidate in candidates) {
if (candidate is List) {
for (final room in candidate) {
final value = room.toString().trim();
if (value.isNotEmpty) {
rooms.add(value);
}
}
} else {
final value = candidate?.toString().trim();
if (value != null && value.isNotEmpty) {
rooms.add(value);
}
}
}

return rooms.toList();
}
Comment thread
PedroLunet marked this conversation as resolved.
Outdated

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

@override
Expand Down
1 change: 1 addition & 0 deletions packages/uni_app/lib/utils/navigation_items.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ enum NavigationItem {
navPersonalArea('area'),
navExams('exames'),
navCourseUnit('cadeira'),
navProfessorSchedule('horario_docente'),
navStops('autocarros'),
navLocations('locais', faculties: {'feup'}),
navRestaurants('restaurantes'),
Expand Down
132 changes: 102 additions & 30 deletions packages/uni_app/lib/view/academic_path/schedule_page.dart
Comment thread
PedroLunet marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,54 +1,100 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher_new_api.dart';
import 'package:uni/model/entities/course_units/sheet.dart';
import 'package:uni/model/entities/lecture.dart';
import 'package:uni/model/providers/riverpod/default_consumer.dart';
import 'package:uni/model/providers/riverpod/lecture_provider.dart';
import 'package:uni/model/providers/riverpod/session_provider.dart';
import 'package:uni/view/academic_path/widgets/no_classes_widget.dart';
import 'package:uni/view/academic_path/widgets/schedule_page_shimmer.dart';
import 'package:uni/view/academic_path/widgets/schedule_page_view.dart';
import 'package:uni/view/widgets/pages_layouts/secondary/secondary.dart';

final professorLecturesProvider = FutureProvider.autoDispose
.family<List<Lecture>, String>((ref, professorCode) async {
final session = await ref.watch(sessionProvider.future);
if (session == null) {
return [];
}
return ScheduleFetcherNewApiProfessor(
professorCode: professorCode,
).getLectures(session);
});

class SchedulePage extends ConsumerWidget {
SchedulePage({super.key, DateTime? now}) : now = now ?? DateTime.now();
SchedulePage({super.key, DateTime? now, this.professorCode})
: now = now ?? DateTime.now();

final DateTime now;
final String? professorCode;

@override
Widget build(BuildContext context, WidgetRef ref) {
return MediaQuery.removePadding(
context: context,
removeBottom: true,
child: DefaultConsumer<List<Lecture>>(
provider: lectureProvider,
builder: (context, ref, lectures) {
final startOfWeek = _getStartOfWeek(now, lectures);

return SchedulePageView(lectures, startOfWeek: startOfWeek, now: now);
},
nullContentWidget: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Container(
height: constraints.maxHeight,
padding: const EdgeInsets.only(bottom: 120),
child: const Center(child: NoClassesWidget()),
),
child: professorCode != null
? _buildProfessorSchedule(context, ref)
: _buildStudentSchedule(context, ref),
);
}

Widget _buildProfessorSchedule(BuildContext context, WidgetRef ref) {
final asyncLectures = ref.watch(professorLecturesProvider(professorCode!));
return asyncLectures.when(
loading: () => const ShimmerSchedulePage(),
error: (_, _) => const Center(child: NoClassesWidget()),
data: (allLectures) {
final startOfWeek = _getStartOfWeek(now, allLectures);
final endOfNextWeek = startOfWeek.add(const Duration(days: 14));
final lectures = allLectures
.where(
(l) =>
l.startTime.isAfter(startOfWeek) &&
l.startTime.isBefore(endOfNextWeek),
)
.toList();
if (lectures.isEmpty) {
return const Center(child: NoClassesWidget());
}
return SchedulePageView(lectures, startOfWeek: startOfWeek, now: now);
},
);
}

Widget _buildStudentSchedule(BuildContext context, WidgetRef ref) {
return DefaultConsumer<List<Lecture>>(
provider: lectureProvider,
builder: (context, ref, lectures) {
final startOfWeek = _getStartOfWeek(now, lectures);

return SchedulePageView(lectures, startOfWeek: startOfWeek, now: now);
},
nullContentWidget: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Container(
height: constraints.maxHeight,
padding: const EdgeInsets.only(bottom: 120),
child: const Center(child: NoClassesWidget()),
),
),
hasContent: (lectures) => lectures.isNotEmpty,
mapper: (lectures) {
final startOfWeek = _getStartOfWeek(now, lectures);
final endOfNextWeek = startOfWeek.add(const Duration(days: 14));

return lectures
.where(
(lecture) =>
lecture.startTime.isAfter(startOfWeek) &&
lecture.startTime.isBefore(endOfNextWeek),
)
.toList();
},
loadingWidget: const ShimmerSchedulePage(),
),
hasContent: (lectures) => lectures.isNotEmpty,
mapper: (lectures) {
final startOfWeek = _getStartOfWeek(now, lectures);
final endOfNextWeek = startOfWeek.add(const Duration(days: 14));

return lectures
.where(
(lecture) =>
lecture.startTime.isAfter(startOfWeek) &&
lecture.startTime.isBefore(endOfNextWeek),
)
.toList();
},
loadingWidget: const ShimmerSchedulePage(),
);
}

Expand All @@ -65,3 +111,29 @@ class SchedulePage extends ConsumerWidget {
return !hasLecturesThisWeek ? secondSunday : initialSunday;
}
}

class ProfessorSchedulePageView extends ConsumerStatefulWidget {
const ProfessorSchedulePageView(this.professor, {super.key});

final Professor professor;

@override
ConsumerState<ProfessorSchedulePageView> createState() =>
_ProfessorSchedulePageViewState();
}

class _ProfessorSchedulePageViewState
extends SecondaryPageViewState<ProfessorSchedulePageView> {
@override
Future<void> onRefresh() async {
ref.invalidate(professorLecturesProvider(widget.professor.code));
}

@override
String? getTitle() => widget.professor.name;

@override
Widget getBody(BuildContext context) {
return SchedulePage(professorCode: widget.professor.code);
}
}
Loading
Loading