show classname instead of teacher name in teacher timetable view
This commit is contained in:
@@ -33,6 +33,8 @@ data class WidgetLesson(
|
|||||||
val subjectShort: String,
|
val subjectShort: String,
|
||||||
val subjectLong: String?,
|
val subjectLong: String?,
|
||||||
val room: String?,
|
val room: String?,
|
||||||
|
// On teacher accounts this carries the class label ("7a") instead of the
|
||||||
|
// teacher short name (originalTeacher is null then) — mapped in Dart.
|
||||||
val teacher: String?,
|
val teacher: String?,
|
||||||
val originalTeacher: String?,
|
val originalTeacher: String?,
|
||||||
val status: WidgetLessonStatus,
|
val status: WidgetLessonStatus,
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ struct WidgetLesson: Codable {
|
|||||||
let subjectShort: String
|
let subjectShort: String
|
||||||
let subjectLong: String?
|
let subjectLong: String?
|
||||||
let room: String?
|
let room: String?
|
||||||
|
// On teacher accounts this carries the class label ("7a") instead of the
|
||||||
|
// teacher short name (originalTeacher is nil then) — mapped in Dart.
|
||||||
let teacher: String?
|
let teacher: String?
|
||||||
let originalTeacher: String?
|
let originalTeacher: String?
|
||||||
let status: WidgetLessonStatus
|
let status: WidgetLessonStatus
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ class DemoCapabilities {
|
|||||||
pushNotifications: true,
|
pushNotifications: true,
|
||||||
timetablePastDays: null,
|
timetablePastDays: null,
|
||||||
timetableFutureDays: null,
|
timetableFutureDays: null,
|
||||||
|
// Die Demo-Persona ist explizit Schüler — null hieße "Backend kennt das
|
||||||
|
// Feld nicht" (siehe CapabilitiesResponse.userType).
|
||||||
|
userType: 'STUDENT',
|
||||||
loaded: true,
|
loaded: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,16 @@ class CapabilitiesResponse {
|
|||||||
|
|
||||||
final int? timetableFutureDays;
|
final int? timetableFutureDays;
|
||||||
|
|
||||||
|
/// LDAP user type ('TEACHER' | 'STUDENT' | 'STAFF'). Null when the backend
|
||||||
|
/// predates the field or has no LDAP record for the user.
|
||||||
|
final String? userType;
|
||||||
|
|
||||||
CapabilitiesResponse({
|
CapabilitiesResponse({
|
||||||
required this.viewForeignTimetables,
|
required this.viewForeignTimetables,
|
||||||
required this.pushNotifications,
|
required this.pushNotifications,
|
||||||
this.timetablePastDays,
|
this.timetablePastDays,
|
||||||
this.timetableFutureDays,
|
this.timetableFutureDays,
|
||||||
|
this.userType,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory CapabilitiesResponse.fromJson(Map<String, dynamic> json) =>
|
factory CapabilitiesResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ CapabilitiesResponse _$CapabilitiesResponseFromJson(
|
|||||||
pushNotifications: json['pushNotifications'] as bool? ?? false,
|
pushNotifications: json['pushNotifications'] as bool? ?? false,
|
||||||
timetablePastDays: (json['timetablePastDays'] as num?)?.toInt(),
|
timetablePastDays: (json['timetablePastDays'] as num?)?.toInt(),
|
||||||
timetableFutureDays: (json['timetableFutureDays'] as num?)?.toInt(),
|
timetableFutureDays: (json['timetableFutureDays'] as num?)?.toInt(),
|
||||||
|
userType: json['userType'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$CapabilitiesResponseToJson(
|
Map<String, dynamic> _$CapabilitiesResponseToJson(
|
||||||
@@ -22,4 +23,5 @@ Map<String, dynamic> _$CapabilitiesResponseToJson(
|
|||||||
'pushNotifications': instance.pushNotifications,
|
'pushNotifications': instance.pushNotifications,
|
||||||
'timetablePastDays': instance.timetablePastDays,
|
'timetablePastDays': instance.timetablePastDays,
|
||||||
'timetableFutureDays': instance.timetableFutureDays,
|
'timetableFutureDays': instance.timetableFutureDays,
|
||||||
|
'userType': instance.userType,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import 'routing/app_routes.dart';
|
|||||||
import 'share_intent/share_intent_listener.dart';
|
import 'share_intent/share_intent_listener.dart';
|
||||||
import 'state/app/modules/app_modules.dart';
|
import 'state/app/modules/app_modules.dart';
|
||||||
import 'state/app/modules/breaker/bloc/breaker_bloc.dart';
|
import 'state/app/modules/breaker/bloc/breaker_bloc.dart';
|
||||||
|
import 'state/app/modules/capabilities/bloc/capabilities_cubit.dart';
|
||||||
import 'state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
import 'state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||||
import 'state/app/modules/settings/bloc/settings_cubit.dart';
|
import 'state/app/modules/settings/bloc/settings_cubit.dart';
|
||||||
import 'state/app/modules/timetable/bloc/timetable_bloc.dart';
|
import 'state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||||
@@ -173,6 +174,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||||||
// Mirror BLoC updates into the home-screen widget without waiting
|
// Mirror BLoC updates into the home-screen widget without waiting
|
||||||
// for the periodic background refresh.
|
// for the periodic background refresh.
|
||||||
final settingsCubit = context.read<SettingsCubit>();
|
final settingsCubit = context.read<SettingsCubit>();
|
||||||
|
final capabilitiesCubit = context.read<CapabilitiesCubit>();
|
||||||
_timetableWidgetSync?.cancel();
|
_timetableWidgetSync?.cancel();
|
||||||
_timetableWidgetSync = timetable.stream.listen((state) {
|
_timetableWidgetSync = timetable.stream.listen((state) {
|
||||||
final data = state.data;
|
final data = state.data;
|
||||||
@@ -181,6 +183,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||||||
WidgetPublisher.publishFromBlocState(
|
WidgetPublisher.publishFromBlocState(
|
||||||
data,
|
data,
|
||||||
settings: settingsCubit.val(),
|
settings: settingsCubit.val(),
|
||||||
|
isTeacher: capabilitiesCubit.isTeacher,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -192,6 +195,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||||||
WidgetPublisher.publishFromBlocState(
|
WidgetPublisher.publishFromBlocState(
|
||||||
initialData,
|
initialData,
|
||||||
settings: settingsCubit.val(),
|
settings: settingsCubit.val(),
|
||||||
|
isTeacher: capabilitiesCubit.isTeacher,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,10 @@ Future<void> _refresh() async {
|
|||||||
|
|
||||||
final lessons = timetable.entries;
|
final lessons = timetable.entries;
|
||||||
|
|
||||||
final connectDouble = await WidgetSync.getConnectDoubleLessons();
|
final [connectDouble, isTeacher] = await Future.wait([
|
||||||
|
WidgetSync.getConnectDoubleLessons(),
|
||||||
|
WidgetSync.getIsTeacher(),
|
||||||
|
]);
|
||||||
final dayData = WidgetDataMapper.buildDayData(
|
final dayData = WidgetDataMapper.buildDayData(
|
||||||
now: now,
|
now: now,
|
||||||
lessons: lessons,
|
lessons: lessons,
|
||||||
@@ -205,6 +208,7 @@ Future<void> _refresh() async {
|
|||||||
timegrid: timegrid,
|
timegrid: timegrid,
|
||||||
customEvents: customEvents,
|
customEvents: customEvents,
|
||||||
connectDoubleLessons: connectDouble,
|
connectDoubleLessons: connectDouble,
|
||||||
|
showClassInsteadOfTeacher: isTeacher,
|
||||||
);
|
);
|
||||||
final weekData = WidgetDataMapper.buildWeekData(
|
final weekData = WidgetDataMapper.buildWeekData(
|
||||||
now: now,
|
now: now,
|
||||||
@@ -215,6 +219,7 @@ Future<void> _refresh() async {
|
|||||||
timegrid: timegrid,
|
timegrid: timegrid,
|
||||||
customEvents: customEvents,
|
customEvents: customEvents,
|
||||||
connectDoubleLessons: connectDouble,
|
connectDoubleLessons: connectDouble,
|
||||||
|
showClassInsteadOfTeacher: isTeacher,
|
||||||
);
|
);
|
||||||
|
|
||||||
await WidgetSync.writeDayData(dayData);
|
await WidgetSync.writeDayData(dayData);
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ class CapabilitiesCubit extends HydratedCubit<CapabilitiesState> {
|
|||||||
|
|
||||||
int? get timetableFutureDays => state.timetableFutureDays;
|
int? get timetableFutureDays => state.timetableFutureDays;
|
||||||
|
|
||||||
|
/// Teacher accounts get the class shown on timetable tiles instead of their
|
||||||
|
/// own name (see TimetableAppointmentFactory.showClassInsteadOfTeacher).
|
||||||
|
bool get isTeacher => state.userType == 'TEACHER';
|
||||||
|
|
||||||
/// Refreshes capabilities from the server. On any failure (endpoint not yet
|
/// Refreshes capabilities from the server. On any failure (endpoint not yet
|
||||||
/// live, network error, 4xx) the previously hydrated flags are kept but the
|
/// live, network error, 4xx) the previously hydrated flags are kept but the
|
||||||
/// state is marked `loaded` — a failed fetch never silently grants a
|
/// state is marked `loaded` — a failed fetch never silently grants a
|
||||||
@@ -38,6 +42,7 @@ class CapabilitiesCubit extends HydratedCubit<CapabilitiesState> {
|
|||||||
pushNotifications: response.pushNotifications,
|
pushNotifications: response.pushNotifications,
|
||||||
timetablePastDays: response.timetablePastDays,
|
timetablePastDays: response.timetablePastDays,
|
||||||
timetableFutureDays: response.timetableFutureDays,
|
timetableFutureDays: response.timetableFutureDays,
|
||||||
|
userType: response.userType,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ abstract class CapabilitiesState with _$CapabilitiesState {
|
|||||||
// client-side clamp; the (server-narrowed) school year alone governs.
|
// client-side clamp; the (server-narrowed) school year alone governs.
|
||||||
int? timetablePastDays,
|
int? timetablePastDays,
|
||||||
int? timetableFutureDays,
|
int? timetableFutureDays,
|
||||||
|
// LDAP user type ('TEACHER' | 'STUDENT' | 'STAFF'); null = unknown.
|
||||||
|
String? userType,
|
||||||
// Whether a capability response (or a definitive failure) has been
|
// Whether a capability response (or a definitive failure) has been
|
||||||
// observed at least once this session. Lets the UI distinguish "still
|
// observed at least once this session. Lets the UI distinguish "still
|
||||||
// unknown" from "confirmed not allowed".
|
// unknown" from "confirmed not allowed".
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ mixin _$CapabilitiesState {
|
|||||||
|
|
||||||
bool get viewForeignTimetables; bool get pushNotifications;// Days into the past/future the timetable may be scrolled. Null = no
|
bool get viewForeignTimetables; bool get pushNotifications;// Days into the past/future the timetable may be scrolled. Null = no
|
||||||
// client-side clamp; the (server-narrowed) school year alone governs.
|
// client-side clamp; the (server-narrowed) school year alone governs.
|
||||||
int? get timetablePastDays; int? get timetableFutureDays;// Whether a capability response (or a definitive failure) has been
|
int? get timetablePastDays; int? get timetableFutureDays;// LDAP user type ('TEACHER' | 'STUDENT' | 'STAFF'); null = unknown.
|
||||||
|
String? get userType;// Whether a capability response (or a definitive failure) has been
|
||||||
// observed at least once this session. Lets the UI distinguish "still
|
// observed at least once this session. Lets the UI distinguish "still
|
||||||
// unknown" from "confirmed not allowed".
|
// unknown" from "confirmed not allowed".
|
||||||
bool get loaded;
|
bool get loaded;
|
||||||
@@ -33,16 +34,16 @@ $CapabilitiesStateCopyWith<CapabilitiesState> get copyWith => _$CapabilitiesStat
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is CapabilitiesState&&(identical(other.viewForeignTimetables, viewForeignTimetables) || other.viewForeignTimetables == viewForeignTimetables)&&(identical(other.pushNotifications, pushNotifications) || other.pushNotifications == pushNotifications)&&(identical(other.timetablePastDays, timetablePastDays) || other.timetablePastDays == timetablePastDays)&&(identical(other.timetableFutureDays, timetableFutureDays) || other.timetableFutureDays == timetableFutureDays)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is CapabilitiesState&&(identical(other.viewForeignTimetables, viewForeignTimetables) || other.viewForeignTimetables == viewForeignTimetables)&&(identical(other.pushNotifications, pushNotifications) || other.pushNotifications == pushNotifications)&&(identical(other.timetablePastDays, timetablePastDays) || other.timetablePastDays == timetablePastDays)&&(identical(other.timetableFutureDays, timetableFutureDays) || other.timetableFutureDays == timetableFutureDays)&&(identical(other.userType, userType) || other.userType == userType)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,viewForeignTimetables,pushNotifications,timetablePastDays,timetableFutureDays,loaded);
|
int get hashCode => Object.hash(runtimeType,viewForeignTimetables,pushNotifications,timetablePastDays,timetableFutureDays,userType,loaded);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'CapabilitiesState(viewForeignTimetables: $viewForeignTimetables, pushNotifications: $pushNotifications, timetablePastDays: $timetablePastDays, timetableFutureDays: $timetableFutureDays, loaded: $loaded)';
|
return 'CapabilitiesState(viewForeignTimetables: $viewForeignTimetables, pushNotifications: $pushNotifications, timetablePastDays: $timetablePastDays, timetableFutureDays: $timetableFutureDays, userType: $userType, loaded: $loaded)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ abstract mixin class $CapabilitiesStateCopyWith<$Res> {
|
|||||||
factory $CapabilitiesStateCopyWith(CapabilitiesState value, $Res Function(CapabilitiesState) _then) = _$CapabilitiesStateCopyWithImpl;
|
factory $CapabilitiesStateCopyWith(CapabilitiesState value, $Res Function(CapabilitiesState) _then) = _$CapabilitiesStateCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, bool loaded
|
bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, bool loaded
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -70,13 +71,14 @@ class _$CapabilitiesStateCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of CapabilitiesState
|
/// Create a copy of CapabilitiesState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? viewForeignTimetables = null,Object? pushNotifications = null,Object? timetablePastDays = freezed,Object? timetableFutureDays = freezed,Object? loaded = null,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? viewForeignTimetables = null,Object? pushNotifications = null,Object? timetablePastDays = freezed,Object? timetableFutureDays = freezed,Object? userType = freezed,Object? loaded = null,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
viewForeignTimetables: null == viewForeignTimetables ? _self.viewForeignTimetables : viewForeignTimetables // ignore: cast_nullable_to_non_nullable
|
viewForeignTimetables: null == viewForeignTimetables ? _self.viewForeignTimetables : viewForeignTimetables // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,pushNotifications: null == pushNotifications ? _self.pushNotifications : pushNotifications // ignore: cast_nullable_to_non_nullable
|
as bool,pushNotifications: null == pushNotifications ? _self.pushNotifications : pushNotifications // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,timetablePastDays: freezed == timetablePastDays ? _self.timetablePastDays : timetablePastDays // ignore: cast_nullable_to_non_nullable
|
as bool,timetablePastDays: freezed == timetablePastDays ? _self.timetablePastDays : timetablePastDays // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,timetableFutureDays: freezed == timetableFutureDays ? _self.timetableFutureDays : timetableFutureDays // ignore: cast_nullable_to_non_nullable
|
as int?,timetableFutureDays: freezed == timetableFutureDays ? _self.timetableFutureDays : timetableFutureDays // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,loaded: null == loaded ? _self.loaded : loaded // ignore: cast_nullable_to_non_nullable
|
as int?,userType: freezed == userType ? _self.userType : userType // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,loaded: null == loaded ? _self.loaded : loaded // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -162,10 +164,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, bool loaded)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, bool loaded)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _CapabilitiesState() when $default != null:
|
case _CapabilitiesState() when $default != null:
|
||||||
return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timetablePastDays,_that.timetableFutureDays,_that.loaded);case _:
|
return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timetablePastDays,_that.timetableFutureDays,_that.userType,_that.loaded);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -183,10 +185,10 @@ return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timeta
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, bool loaded) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, bool loaded) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _CapabilitiesState():
|
case _CapabilitiesState():
|
||||||
return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timetablePastDays,_that.timetableFutureDays,_that.loaded);case _:
|
return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timetablePastDays,_that.timetableFutureDays,_that.userType,_that.loaded);case _:
|
||||||
throw StateError('Unexpected subclass');
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -203,10 +205,10 @@ return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timeta
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, bool loaded)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, bool loaded)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _CapabilitiesState() when $default != null:
|
case _CapabilitiesState() when $default != null:
|
||||||
return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timetablePastDays,_that.timetableFutureDays,_that.loaded);case _:
|
return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timetablePastDays,_that.timetableFutureDays,_that.userType,_that.loaded);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -218,7 +220,7 @@ return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timeta
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|
||||||
class _CapabilitiesState implements CapabilitiesState {
|
class _CapabilitiesState implements CapabilitiesState {
|
||||||
const _CapabilitiesState({this.viewForeignTimetables = false, this.pushNotifications = false, this.timetablePastDays, this.timetableFutureDays, this.loaded = false});
|
const _CapabilitiesState({this.viewForeignTimetables = false, this.pushNotifications = false, this.timetablePastDays, this.timetableFutureDays, this.userType, this.loaded = false});
|
||||||
factory _CapabilitiesState.fromJson(Map<String, dynamic> json) => _$CapabilitiesStateFromJson(json);
|
factory _CapabilitiesState.fromJson(Map<String, dynamic> json) => _$CapabilitiesStateFromJson(json);
|
||||||
|
|
||||||
@override@JsonKey() final bool viewForeignTimetables;
|
@override@JsonKey() final bool viewForeignTimetables;
|
||||||
@@ -227,6 +229,8 @@ class _CapabilitiesState implements CapabilitiesState {
|
|||||||
// client-side clamp; the (server-narrowed) school year alone governs.
|
// client-side clamp; the (server-narrowed) school year alone governs.
|
||||||
@override final int? timetablePastDays;
|
@override final int? timetablePastDays;
|
||||||
@override final int? timetableFutureDays;
|
@override final int? timetableFutureDays;
|
||||||
|
// LDAP user type ('TEACHER' | 'STUDENT' | 'STAFF'); null = unknown.
|
||||||
|
@override final String? userType;
|
||||||
// Whether a capability response (or a definitive failure) has been
|
// Whether a capability response (or a definitive failure) has been
|
||||||
// observed at least once this session. Lets the UI distinguish "still
|
// observed at least once this session. Lets the UI distinguish "still
|
||||||
// unknown" from "confirmed not allowed".
|
// unknown" from "confirmed not allowed".
|
||||||
@@ -245,16 +249,16 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CapabilitiesState&&(identical(other.viewForeignTimetables, viewForeignTimetables) || other.viewForeignTimetables == viewForeignTimetables)&&(identical(other.pushNotifications, pushNotifications) || other.pushNotifications == pushNotifications)&&(identical(other.timetablePastDays, timetablePastDays) || other.timetablePastDays == timetablePastDays)&&(identical(other.timetableFutureDays, timetableFutureDays) || other.timetableFutureDays == timetableFutureDays)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CapabilitiesState&&(identical(other.viewForeignTimetables, viewForeignTimetables) || other.viewForeignTimetables == viewForeignTimetables)&&(identical(other.pushNotifications, pushNotifications) || other.pushNotifications == pushNotifications)&&(identical(other.timetablePastDays, timetablePastDays) || other.timetablePastDays == timetablePastDays)&&(identical(other.timetableFutureDays, timetableFutureDays) || other.timetableFutureDays == timetableFutureDays)&&(identical(other.userType, userType) || other.userType == userType)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,viewForeignTimetables,pushNotifications,timetablePastDays,timetableFutureDays,loaded);
|
int get hashCode => Object.hash(runtimeType,viewForeignTimetables,pushNotifications,timetablePastDays,timetableFutureDays,userType,loaded);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'CapabilitiesState(viewForeignTimetables: $viewForeignTimetables, pushNotifications: $pushNotifications, timetablePastDays: $timetablePastDays, timetableFutureDays: $timetableFutureDays, loaded: $loaded)';
|
return 'CapabilitiesState(viewForeignTimetables: $viewForeignTimetables, pushNotifications: $pushNotifications, timetablePastDays: $timetablePastDays, timetableFutureDays: $timetableFutureDays, userType: $userType, loaded: $loaded)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -265,7 +269,7 @@ abstract mixin class _$CapabilitiesStateCopyWith<$Res> implements $CapabilitiesS
|
|||||||
factory _$CapabilitiesStateCopyWith(_CapabilitiesState value, $Res Function(_CapabilitiesState) _then) = __$CapabilitiesStateCopyWithImpl;
|
factory _$CapabilitiesStateCopyWith(_CapabilitiesState value, $Res Function(_CapabilitiesState) _then) = __$CapabilitiesStateCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, bool loaded
|
bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, bool loaded
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -282,13 +286,14 @@ class __$CapabilitiesStateCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of CapabilitiesState
|
/// Create a copy of CapabilitiesState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? viewForeignTimetables = null,Object? pushNotifications = null,Object? timetablePastDays = freezed,Object? timetableFutureDays = freezed,Object? loaded = null,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? viewForeignTimetables = null,Object? pushNotifications = null,Object? timetablePastDays = freezed,Object? timetableFutureDays = freezed,Object? userType = freezed,Object? loaded = null,}) {
|
||||||
return _then(_CapabilitiesState(
|
return _then(_CapabilitiesState(
|
||||||
viewForeignTimetables: null == viewForeignTimetables ? _self.viewForeignTimetables : viewForeignTimetables // ignore: cast_nullable_to_non_nullable
|
viewForeignTimetables: null == viewForeignTimetables ? _self.viewForeignTimetables : viewForeignTimetables // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,pushNotifications: null == pushNotifications ? _self.pushNotifications : pushNotifications // ignore: cast_nullable_to_non_nullable
|
as bool,pushNotifications: null == pushNotifications ? _self.pushNotifications : pushNotifications // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,timetablePastDays: freezed == timetablePastDays ? _self.timetablePastDays : timetablePastDays // ignore: cast_nullable_to_non_nullable
|
as bool,timetablePastDays: freezed == timetablePastDays ? _self.timetablePastDays : timetablePastDays // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,timetableFutureDays: freezed == timetableFutureDays ? _self.timetableFutureDays : timetableFutureDays // ignore: cast_nullable_to_non_nullable
|
as int?,timetableFutureDays: freezed == timetableFutureDays ? _self.timetableFutureDays : timetableFutureDays // ignore: cast_nullable_to_non_nullable
|
||||||
as int?,loaded: null == loaded ? _self.loaded : loaded // ignore: cast_nullable_to_non_nullable
|
as int?,userType: freezed == userType ? _self.userType : userType // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,loaded: null == loaded ? _self.loaded : loaded // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ _CapabilitiesState _$CapabilitiesStateFromJson(Map<String, dynamic> json) =>
|
|||||||
pushNotifications: json['pushNotifications'] as bool? ?? false,
|
pushNotifications: json['pushNotifications'] as bool? ?? false,
|
||||||
timetablePastDays: (json['timetablePastDays'] as num?)?.toInt(),
|
timetablePastDays: (json['timetablePastDays'] as num?)?.toInt(),
|
||||||
timetableFutureDays: (json['timetableFutureDays'] as num?)?.toInt(),
|
timetableFutureDays: (json['timetableFutureDays'] as num?)?.toInt(),
|
||||||
|
userType: json['userType'] as String?,
|
||||||
loaded: json['loaded'] as bool? ?? false,
|
loaded: json['loaded'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -21,5 +22,6 @@ Map<String, dynamic> _$CapabilitiesStateToJson(_CapabilitiesState instance) =>
|
|||||||
'pushNotifications': instance.pushNotifications,
|
'pushNotifications': instance.pushNotifications,
|
||||||
'timetablePastDays': instance.timetablePastDays,
|
'timetablePastDays': instance.timetablePastDays,
|
||||||
'timetableFutureDays': instance.timetableFutureDays,
|
'timetableFutureDays': instance.timetableFutureDays,
|
||||||
|
'userType': instance.userType,
|
||||||
'loaded': instance.loaded,
|
'loaded': instance.loaded,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import '../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||||
|
|
||||||
|
final RegExp _whitespaceRun = RegExp(r'\s+');
|
||||||
|
|
||||||
|
/// Collapses any line-break or whitespace run to a single space and trims.
|
||||||
|
/// Returns null when input is null or fully whitespace. Webuntis sometimes
|
||||||
|
/// returns multi-line values like "A30\n4" — this normalizes those so labels
|
||||||
|
/// render on a single line.
|
||||||
|
String? collapseWhitespace(String? s) {
|
||||||
|
if (s == null) return null;
|
||||||
|
final cleaned = s.replaceAll(_whitespaceRun, ' ').trim();
|
||||||
|
return cleaned.isEmpty ? null : cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "7a, 7b" — shared by the calendar tile factory and the home-widget mapper
|
||||||
|
/// so both surfaces render identical class labels on teacher plans.
|
||||||
|
extension LessonClassLabel on McTimetableEntry {
|
||||||
|
String? get classLabel => collapseWhitespace(classNames.join(', '));
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
import '../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||||
|
|
||||||
/// Combines back-to-back lessons with identical subject/room/teacher/status
|
/// Combines back-to-back lessons with identical subject/room/teacher/status
|
||||||
@@ -44,6 +46,9 @@ class LessonMerger {
|
|||||||
b.teachers.firstOrNull?.shortName) {
|
b.teachers.firstOrNull?.shortName) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Relevant für Lehrerpläne: gleicher Lehrer/Fach/Raum, aber verschiedene
|
||||||
|
// Klassen dürfen nicht zu einem Block verschmelzen.
|
||||||
|
if (!listEquals(a.classNames, b.classNames)) return false;
|
||||||
if (a.status != b.status) return false;
|
if (a.status != b.status) return false;
|
||||||
// Lower bound on the gap — without it, two identical-metadata lessons that
|
// Lower bound on the gap — without it, two identical-metadata lessons that
|
||||||
// overlap in time would silently collapse into one.
|
// overlap in time would silently collapse into one.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart'
|
|||||||
import '../../../../storage/timetable_settings.dart';
|
import '../../../../storage/timetable_settings.dart';
|
||||||
import 'arbitrary_appointment.dart';
|
import 'arbitrary_appointment.dart';
|
||||||
import 'lesson_color.dart';
|
import 'lesson_color.dart';
|
||||||
|
import 'lesson_labels.dart';
|
||||||
import 'lesson_merger.dart';
|
import 'lesson_merger.dart';
|
||||||
import 'lesson_status.dart';
|
import 'lesson_status.dart';
|
||||||
import 'lesson_type_label.dart';
|
import 'lesson_type_label.dart';
|
||||||
@@ -23,6 +24,10 @@ class TimetableAppointmentFactory {
|
|||||||
final TimetableSettings settings;
|
final TimetableSettings settings;
|
||||||
final DateTime now;
|
final DateTime now;
|
||||||
|
|
||||||
|
/// Teacher plans (a teacher's own plan or a foreign teacher view) show the
|
||||||
|
/// class on the tile instead of the teacher's own name.
|
||||||
|
final bool showClassInsteadOfTeacher;
|
||||||
|
|
||||||
TimetableAppointmentFactory({
|
TimetableAppointmentFactory({
|
||||||
required this.lessons,
|
required this.lessons,
|
||||||
required this.customEvents,
|
required this.customEvents,
|
||||||
@@ -30,6 +35,7 @@ class TimetableAppointmentFactory {
|
|||||||
required this.settings,
|
required this.settings,
|
||||||
required this.now,
|
required this.now,
|
||||||
this.holidays = const [],
|
this.holidays = const [],
|
||||||
|
this.showClassInsteadOfTeacher = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
List<Appointment> build() {
|
List<Appointment> build() {
|
||||||
@@ -130,7 +136,7 @@ class TimetableAppointmentFactory {
|
|||||||
location: event.description.trim().isEmpty
|
location: event.description.trim().isEmpty
|
||||||
? null
|
? null
|
||||||
: event.description.trim(),
|
: event.description.trim(),
|
||||||
subject: _collapseWhitespace(event.title) ?? event.title,
|
subject: collapseWhitespace(event.title) ?? event.title,
|
||||||
recurrenceRule: parsed.rule,
|
recurrenceRule: parsed.rule,
|
||||||
recurrenceExceptionDates: exceptionDates.isEmpty ? null : exceptionDates,
|
recurrenceExceptionDates: exceptionDates.isEmpty ? null : exceptionDates,
|
||||||
color:
|
color:
|
||||||
@@ -222,7 +228,7 @@ class TimetableAppointmentFactory {
|
|||||||
TimetableNameMode.longName => lookup?.longName ?? subjectShort,
|
TimetableNameMode.longName => lookup?.longName ?? subjectShort,
|
||||||
TimetableNameMode.alternateName => lookup?.longName ?? subjectShort,
|
TimetableNameMode.alternateName => lookup?.longName ?? subjectShort,
|
||||||
};
|
};
|
||||||
final collapsed = _collapseWhitespace(name);
|
final collapsed = collapseWhitespace(name);
|
||||||
if (collapsed != null) return collapsed;
|
if (collapsed != null) return collapsed;
|
||||||
}
|
}
|
||||||
// Subject leer → Titel aus dem Lesson-Type ableiten. Pausenaufsicht etc.
|
// Subject leer → Titel aus dem Lesson-Type ableiten. Pausenaufsicht etc.
|
||||||
@@ -233,10 +239,13 @@ class TimetableAppointmentFactory {
|
|||||||
|
|
||||||
String _locationLabel(McTimetableEntry lesson) {
|
String _locationLabel(McTimetableEntry lesson) {
|
||||||
final roomName =
|
final roomName =
|
||||||
_collapseWhitespace(lesson.rooms.firstOrNull) ?? 'Unbekannt';
|
collapseWhitespace(lesson.rooms.firstOrNull) ?? 'Unbekannt';
|
||||||
final teacherName =
|
// Klassenlose Einträge (Aufsichten etc.) fallen auf den Lehrer zurück.
|
||||||
_teacherLabel(lesson.teachers.firstOrNull) ?? 'Unbekannt';
|
final secondLine =
|
||||||
return '$roomName\n$teacherName';
|
(showClassInsteadOfTeacher ? lesson.classLabel : null) ??
|
||||||
|
_teacherLabel(lesson.teachers.firstOrNull) ??
|
||||||
|
'Unbekannt';
|
||||||
|
return '$roomName\n$secondLine';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Backend serves teachers with their full display name ("Stefan Müller"),
|
/// Backend serves teachers with their full display name ("Stefan Müller"),
|
||||||
@@ -245,27 +254,11 @@ class TimetableAppointmentFactory {
|
|||||||
/// overview; the detail sheet still renders the full name as a subtitle.
|
/// overview; the detail sheet still renders the full name as a subtitle.
|
||||||
static String? _teacherLabel(McTimetableTeacher? teacher) {
|
static String? _teacherLabel(McTimetableTeacher? teacher) {
|
||||||
if (teacher == null) return null;
|
if (teacher == null) return null;
|
||||||
final display = _collapseWhitespace(teacher.displayName);
|
final display = collapseWhitespace(teacher.displayName);
|
||||||
if (display != null && display.isNotEmpty) {
|
if (display != null && display.isNotEmpty) {
|
||||||
final parts = display.split(' ');
|
final parts = display.split(' ');
|
||||||
return parts.isEmpty ? display : parts.last;
|
return parts.isEmpty ? display : parts.last;
|
||||||
}
|
}
|
||||||
return _collapseWhitespace(teacher.shortName);
|
return collapseWhitespace(teacher.shortName);
|
||||||
}
|
|
||||||
|
|
||||||
/// Collapses any line-break or whitespace run to a single space and trims.
|
|
||||||
/// Returns null when input is null or fully whitespace. Webuntis sometimes
|
|
||||||
/// returns multi-line room names like "A30\n4" — this normalizes those so
|
|
||||||
/// the tile renders the room on a single line.
|
|
||||||
static String? _collapseWhitespace(String? s) {
|
|
||||||
if (s == null) return null;
|
|
||||||
final cleaned = s
|
|
||||||
.replaceAll('\r\n', ' ')
|
|
||||||
.replaceAll('\n', ' ')
|
|
||||||
.replaceAll('\r', ' ')
|
|
||||||
.replaceAll('\t', ' ')
|
|
||||||
.replaceAll(RegExp(r'\s+'), ' ')
|
|
||||||
.trim();
|
|
||||||
return cleaned.isEmpty ? null : cleaned;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,9 +101,8 @@ class _TimetableState extends State<Timetable> {
|
|||||||
final loadableState = context.watch<TimetableBloc>().state;
|
final loadableState = context.watch<TimetableBloc>().state;
|
||||||
final innerState = loadableState.data;
|
final innerState = loadableState.data;
|
||||||
final atToday = innerState != null && _isOnInitialWeek(innerState);
|
final atToday = innerState != null && _isOnInitialWeek(innerState);
|
||||||
final canViewForeign = context
|
final capabilities = context.watch<CapabilitiesCubit>();
|
||||||
.watch<CapabilitiesCubit>()
|
final canViewForeign = capabilities.canViewForeignTimetables;
|
||||||
.canViewForeignTimetables;
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
// Der Kalender scrollt nicht (nur ziehen/reloaden), aber seine internen
|
// Der Kalender scrollt nicht (nur ziehen/reloaden), aber seine internen
|
||||||
@@ -166,6 +165,7 @@ class _TimetableState extends State<Timetable> {
|
|||||||
),
|
),
|
||||||
onCreateEvent: _onCreateEventAt,
|
onCreateEvent: _onCreateEventAt,
|
||||||
customEvents: state.customEvents?.events ?? const [],
|
customEvents: state.customEvents?.events ?? const [],
|
||||||
|
showClassInsteadOfTeacher: capabilities.isTeacher,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -217,6 +217,8 @@ class _TimetableState extends State<Timetable> {
|
|||||||
onAppointmentTap: (apt) =>
|
onAppointmentTap: (apt) =>
|
||||||
AppointmentDetailsDispatcher.show(context, state, apt),
|
AppointmentDetailsDispatcher.show(context, state, apt),
|
||||||
customEvents: const [],
|
customEvents: const [],
|
||||||
|
showClassInsteadOfTeacher:
|
||||||
|
selected.type == TimetableElementType.teacher,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ class TimetableCalendarView extends StatefulWidget {
|
|||||||
final void Function(DateTime start, DateTime end)? onCreateEvent;
|
final void Function(DateTime start, DateTime end)? onCreateEvent;
|
||||||
final List<CustomTimetableEvent> customEvents;
|
final List<CustomTimetableEvent> customEvents;
|
||||||
|
|
||||||
|
/// True for teacher plans — tiles then show the class instead of the
|
||||||
|
/// teacher name (see [TimetableAppointmentFactory.showClassInsteadOfTeacher]).
|
||||||
|
final bool showClassInsteadOfTeacher;
|
||||||
|
|
||||||
const TimetableCalendarView({
|
const TimetableCalendarView({
|
||||||
super.key,
|
super.key,
|
||||||
required this.state,
|
required this.state,
|
||||||
@@ -35,6 +39,7 @@ class TimetableCalendarView extends StatefulWidget {
|
|||||||
required this.onAppointmentTap,
|
required this.onAppointmentTap,
|
||||||
this.onCreateEvent,
|
this.onCreateEvent,
|
||||||
this.customEvents = const [],
|
this.customEvents = const [],
|
||||||
|
this.showClassInsteadOfTeacher = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -46,9 +51,9 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
|||||||
GlobalKey<CustomWorkWeekCalendarState>();
|
GlobalKey<CustomWorkWeekCalendarState>();
|
||||||
|
|
||||||
List<Appointment>? _cachedAppointments;
|
List<Appointment>? _cachedAppointments;
|
||||||
int? _lastDataVersion;
|
// TimetableSettings and List define no `==`, so record equality degrades to
|
||||||
TimetableSettings? _lastTimetableSettings;
|
// the same identity checks the cache always used.
|
||||||
List<CustomTimetableEvent>? _lastCustomEvents;
|
(int, TimetableSettings, List<CustomTimetableEvent>, bool)? _cacheKey;
|
||||||
|
|
||||||
DateTime _initialDisplayDate() => DateTime.now().addDays(2);
|
DateTime _initialDisplayDate() => DateTime.now().addDays(2);
|
||||||
|
|
||||||
@@ -63,15 +68,16 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
|||||||
.watch<SettingsCubit>()
|
.watch<SettingsCubit>()
|
||||||
.val()
|
.val()
|
||||||
.timetableSettings;
|
.timetableSettings;
|
||||||
if (_cachedAppointments != null &&
|
final key = (
|
||||||
_lastDataVersion == state.dataVersion &&
|
state.dataVersion,
|
||||||
identical(_lastTimetableSettings, timetableSettings) &&
|
timetableSettings,
|
||||||
identical(_lastCustomEvents, widget.customEvents)) {
|
widget.customEvents,
|
||||||
|
widget.showClassInsteadOfTeacher,
|
||||||
|
);
|
||||||
|
if (_cachedAppointments != null && _cacheKey == key) {
|
||||||
return _cachedAppointments!;
|
return _cachedAppointments!;
|
||||||
}
|
}
|
||||||
_lastDataVersion = state.dataVersion;
|
_cacheKey = key;
|
||||||
_lastTimetableSettings = timetableSettings;
|
|
||||||
_lastCustomEvents = widget.customEvents;
|
|
||||||
|
|
||||||
return _cachedAppointments = TimetableAppointmentFactory(
|
return _cachedAppointments = TimetableAppointmentFactory(
|
||||||
lessons: state.getAllKnownLessons().toList(),
|
lessons: state.getAllKnownLessons().toList(),
|
||||||
@@ -80,6 +86,7 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
|||||||
holidays: state.schoolHolidays?.result ?? const [],
|
holidays: state.schoolHolidays?.result ?? const [],
|
||||||
settings: timetableSettings,
|
settings: timetableSettings,
|
||||||
now: DateTime.now(),
|
now: DateTime.now(),
|
||||||
|
showClassInsteadOfTeacher: widget.showClassInsteadOfTeacher,
|
||||||
).build();
|
).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ abstract class WidgetLesson with _$WidgetLesson {
|
|||||||
required String subjectShort,
|
required String subjectShort,
|
||||||
String? subjectLong,
|
String? subjectLong,
|
||||||
String? room,
|
String? room,
|
||||||
|
/// On teacher accounts this carries the class label ("7a") instead of the
|
||||||
|
/// teacher short name — see `WidgetDataMapper` `showClassInsteadOfTeacher`;
|
||||||
|
/// [originalTeacher] is null in that case.
|
||||||
String? teacher,
|
String? teacher,
|
||||||
String? originalTeacher,
|
String? originalTeacher,
|
||||||
required WidgetLessonStatus status,
|
required WidgetLessonStatus status,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../api/marianumconnect/queries/timetable_get_week/timetable_get_week_res
|
|||||||
import '../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
import '../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||||
import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
|
import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
|
||||||
import '../extensions/date_time.dart';
|
import '../extensions/date_time.dart';
|
||||||
|
import '../view/pages/timetable/data/lesson_labels.dart';
|
||||||
import '../view/pages/timetable/data/lesson_merger.dart';
|
import '../view/pages/timetable/data/lesson_merger.dart';
|
||||||
import '../view/pages/timetable/data/lesson_period_schedule.dart';
|
import '../view/pages/timetable/data/lesson_period_schedule.dart';
|
||||||
import '../view/pages/timetable/data/lesson_status.dart';
|
import '../view/pages/timetable/data/lesson_status.dart';
|
||||||
@@ -58,6 +59,7 @@ class WidgetDataMapper {
|
|||||||
TimetableGetTimegridResponse? timegrid,
|
TimetableGetTimegridResponse? timegrid,
|
||||||
GetCustomTimetableEventResponse? customEvents,
|
GetCustomTimetableEventResponse? customEvents,
|
||||||
bool connectDoubleLessons = true,
|
bool connectDoubleLessons = true,
|
||||||
|
bool showClassInsteadOfTeacher = false,
|
||||||
}) {
|
}) {
|
||||||
final anchor = resolveDayAnchor(now);
|
final anchor = resolveDayAnchor(now);
|
||||||
final holiday = _findHoliday(anchor, holidays);
|
final holiday = _findHoliday(anchor, holidays);
|
||||||
@@ -68,7 +70,13 @@ class WidgetDataMapper {
|
|||||||
? LessonMerger.merge(dayLessons)
|
? LessonMerger.merge(dayLessons)
|
||||||
: dayLessons;
|
: dayLessons;
|
||||||
final mapped = <WidgetLesson>[
|
final mapped = <WidgetLesson>[
|
||||||
...source.map((l) => _mapLesson(l, now, subjects, rooms)),
|
..._mapAll(
|
||||||
|
source,
|
||||||
|
now,
|
||||||
|
subjects,
|
||||||
|
rooms,
|
||||||
|
showClassInsteadOfTeacher: showClassInsteadOfTeacher,
|
||||||
|
),
|
||||||
..._expandCustomEvents(customEvents, dayStart, dayEnd),
|
..._expandCustomEvents(customEvents, dayStart, dayEnd),
|
||||||
]..sort((a, b) => a.start.compareTo(b.start));
|
]..sort((a, b) => a.start.compareTo(b.start));
|
||||||
return WidgetTimetableData(
|
return WidgetTimetableData(
|
||||||
@@ -90,6 +98,7 @@ class WidgetDataMapper {
|
|||||||
TimetableGetTimegridResponse? timegrid,
|
TimetableGetTimegridResponse? timegrid,
|
||||||
GetCustomTimetableEventResponse? customEvents,
|
GetCustomTimetableEventResponse? customEvents,
|
||||||
bool connectDoubleLessons = true,
|
bool connectDoubleLessons = true,
|
||||||
|
bool showClassInsteadOfTeacher = false,
|
||||||
}) {
|
}) {
|
||||||
final anchor = resolveWeekAnchor(now);
|
final anchor = resolveWeekAnchor(now);
|
||||||
// The window is anchored at the *current* calendar week, not the
|
// The window is anchored at the *current* calendar week, not the
|
||||||
@@ -107,7 +116,13 @@ class WidgetDataMapper {
|
|||||||
? _mergePerDay(weekLessons)
|
? _mergePerDay(weekLessons)
|
||||||
: weekLessons;
|
: weekLessons;
|
||||||
final mapped = <WidgetLesson>[
|
final mapped = <WidgetLesson>[
|
||||||
...source.map((l) => _mapLesson(l, now, subjects, rooms)),
|
..._mapAll(
|
||||||
|
source,
|
||||||
|
now,
|
||||||
|
subjects,
|
||||||
|
rooms,
|
||||||
|
showClassInsteadOfTeacher: showClassInsteadOfTeacher,
|
||||||
|
),
|
||||||
..._expandCustomEvents(customEvents, windowStart, endExclusive),
|
..._expandCustomEvents(customEvents, windowStart, endExclusive),
|
||||||
]..sort((a, b) => a.start.compareTo(b.start));
|
]..sort((a, b) => a.start.compareTo(b.start));
|
||||||
final days = [
|
final days = [
|
||||||
@@ -282,12 +297,29 @@ class WidgetDataMapper {
|
|||||||
return [for (final group in byDay.values) ...LessonMerger.merge(group)];
|
return [for (final group in byDay.values) ...LessonMerger.merge(group)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Iterable<WidgetLesson> _mapAll(
|
||||||
|
Iterable<McTimetableEntry> source,
|
||||||
|
DateTime now,
|
||||||
|
TimetableGetSubjectsResponse? subjects,
|
||||||
|
TimetableGetRoomsResponse? rooms, {
|
||||||
|
required bool showClassInsteadOfTeacher,
|
||||||
|
}) => source.map(
|
||||||
|
(l) => _mapLesson(
|
||||||
|
l,
|
||||||
|
now,
|
||||||
|
subjects,
|
||||||
|
rooms,
|
||||||
|
showClassInsteadOfTeacher: showClassInsteadOfTeacher,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
static WidgetLesson _mapLesson(
|
static WidgetLesson _mapLesson(
|
||||||
McTimetableEntry lesson,
|
McTimetableEntry lesson,
|
||||||
DateTime now,
|
DateTime now,
|
||||||
TimetableGetSubjectsResponse? subjects,
|
TimetableGetSubjectsResponse? subjects,
|
||||||
TimetableGetRoomsResponse? rooms,
|
TimetableGetRoomsResponse? rooms, {
|
||||||
) {
|
bool showClassInsteadOfTeacher = false,
|
||||||
|
}) {
|
||||||
final start = lesson.startDateTime;
|
final start = lesson.startDateTime;
|
||||||
final end = lesson.endDateTime;
|
final end = lesson.endDateTime;
|
||||||
final status = _mapStatus(
|
final status = _mapStatus(
|
||||||
@@ -314,8 +346,14 @@ class WidgetDataMapper {
|
|||||||
roomName;
|
roomName;
|
||||||
}
|
}
|
||||||
final teacher = lesson.teachers.firstOrNull;
|
final teacher = lesson.teachers.firstOrNull;
|
||||||
final teacherName = teacher?.shortName;
|
// Lehrerpläne: Klasse in den Teacher-Slot mappen, damit die nativen
|
||||||
final originalTeacher = teacher?.originalShortName;
|
// Renderer unverändert bleiben. Klassenlose Einträge (Aufsichten) behalten
|
||||||
|
// den Lehrer als Fallback.
|
||||||
|
final classLabel = showClassInsteadOfTeacher ? lesson.classLabel : null;
|
||||||
|
final teacherName = classLabel ?? teacher?.shortName;
|
||||||
|
final originalTeacher = classLabel != null
|
||||||
|
? null
|
||||||
|
: teacher?.originalShortName;
|
||||||
return WidgetLesson(
|
return WidgetLesson(
|
||||||
start: start,
|
start: start,
|
||||||
end: end,
|
end: end,
|
||||||
|
|||||||
@@ -22,14 +22,18 @@ class WidgetPublisher {
|
|||||||
static Future<void> publishFromBlocState(
|
static Future<void> publishFromBlocState(
|
||||||
TimetableState state, {
|
TimetableState state, {
|
||||||
Settings? settings,
|
Settings? settings,
|
||||||
|
bool isTeacher = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final connectDouble =
|
final connectDouble =
|
||||||
settings?.timetableSettings.connectDoubleLessons ?? true;
|
settings?.timetableSettings.connectDoubleLessons ?? true;
|
||||||
// Mirror into widget storage so the background isolate sees the same
|
// Mirror into widget storage so the background isolate sees the same
|
||||||
// value the user just toggled.
|
// values the user just toggled — concurrently, they are independent.
|
||||||
await WidgetSync.setConnectDoubleLessons(connectDouble);
|
await Future.wait([
|
||||||
await WidgetSync.setThemeMode(_themeName(settings?.appTheme));
|
WidgetSync.setConnectDoubleLessons(connectDouble),
|
||||||
|
WidgetSync.setThemeMode(_themeName(settings?.appTheme)),
|
||||||
|
WidgetSync.setIsTeacher(isTeacher),
|
||||||
|
]);
|
||||||
final lessons = state.getAllKnownLessons();
|
final lessons = state.getAllKnownLessons();
|
||||||
final now = widgetNow();
|
final now = widgetNow();
|
||||||
final dayData = WidgetDataMapper.buildDayData(
|
final dayData = WidgetDataMapper.buildDayData(
|
||||||
@@ -41,6 +45,7 @@ class WidgetPublisher {
|
|||||||
timegrid: state.timegrid,
|
timegrid: state.timegrid,
|
||||||
customEvents: state.customEvents,
|
customEvents: state.customEvents,
|
||||||
connectDoubleLessons: connectDouble,
|
connectDoubleLessons: connectDouble,
|
||||||
|
showClassInsteadOfTeacher: isTeacher,
|
||||||
);
|
);
|
||||||
final weekData = WidgetDataMapper.buildWeekData(
|
final weekData = WidgetDataMapper.buildWeekData(
|
||||||
now: now,
|
now: now,
|
||||||
@@ -51,6 +56,7 @@ class WidgetPublisher {
|
|||||||
timegrid: state.timegrid,
|
timegrid: state.timegrid,
|
||||||
customEvents: state.customEvents,
|
customEvents: state.customEvents,
|
||||||
connectDoubleLessons: connectDouble,
|
connectDoubleLessons: connectDouble,
|
||||||
|
showClassInsteadOfTeacher: isTeacher,
|
||||||
);
|
);
|
||||||
await WidgetSync.writeDayData(dayData);
|
await WidgetSync.writeDayData(dayData);
|
||||||
await WidgetSync.writeWeekData(weekData);
|
await WidgetSync.writeWeekData(weekData);
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ class WidgetSync {
|
|||||||
static const String connectDoubleLessonsKey =
|
static const String connectDoubleLessonsKey =
|
||||||
'widget_setting_connect_double_lessons_v1';
|
'widget_setting_connect_double_lessons_v1';
|
||||||
static const String themeModeKey = 'widget_setting_theme_mode_v1';
|
static const String themeModeKey = 'widget_setting_theme_mode_v1';
|
||||||
|
// Mirrored from CapabilitiesCubit so the background isolate can render
|
||||||
|
// teacher plans (class instead of teacher name) without bloc storage.
|
||||||
|
static const String isTeacherKey = 'widget_setting_is_teacher_v1';
|
||||||
// Mirrored so the background isolate hits the same Marianum-Connect base
|
// Mirrored so the background isolate hits the same Marianum-Connect base
|
||||||
// URL the in-app settings cubit currently has selected.
|
// URL the in-app settings cubit currently has selected.
|
||||||
static const String marianumConnectBaseUrlKey = 'widget_setting_mc_base_url_v1';
|
static const String marianumConnectBaseUrlKey = 'widget_setting_mc_base_url_v1';
|
||||||
@@ -58,25 +61,30 @@ class WidgetSync {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setLoggedIn(bool loggedIn) async {
|
static Future<void> setLoggedIn(bool loggedIn) =>
|
||||||
await ensureInitialized();
|
_setBool(loggedInKey, loggedIn);
|
||||||
await HomeWidget.saveWidgetData<bool>(loggedInKey, loggedIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> setConnectDoubleLessons(bool value) async {
|
static Future<void> setConnectDoubleLessons(bool value) =>
|
||||||
await ensureInitialized();
|
_setBool(connectDoubleLessonsKey, value);
|
||||||
await HomeWidget.saveWidgetData<bool>(connectDoubleLessonsKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Default `true` matches `default_settings.dart` — fresh install behaves
|
/// Default `true` matches `default_settings.dart` — fresh install behaves
|
||||||
/// like the in-app calendar.
|
/// like the in-app calendar.
|
||||||
static Future<bool> getConnectDoubleLessons() async {
|
static Future<bool> getConnectDoubleLessons() =>
|
||||||
|
_getBool(connectDoubleLessonsKey, defaultValue: true);
|
||||||
|
|
||||||
|
static Future<void> setIsTeacher(bool value) => _setBool(isTeacherKey, value);
|
||||||
|
|
||||||
|
static Future<bool> getIsTeacher() =>
|
||||||
|
_getBool(isTeacherKey, defaultValue: false);
|
||||||
|
|
||||||
|
static Future<void> _setBool(String key, bool value) async {
|
||||||
await ensureInitialized();
|
await ensureInitialized();
|
||||||
final value = await HomeWidget.getWidgetData<bool>(
|
await HomeWidget.saveWidgetData<bool>(key, value);
|
||||||
connectDoubleLessonsKey,
|
}
|
||||||
defaultValue: true,
|
|
||||||
);
|
static Future<bool> _getBool(String key, {required bool defaultValue}) async {
|
||||||
return value ?? true;
|
await ensureInitialized();
|
||||||
|
return await HomeWidget.getWidgetData<bool>(key) ?? defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setThemeMode(String mode) async {
|
static Future<void> setThemeMode(String mode) async {
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||||
|
import 'package:marianum_mobile/storage/timetable_settings.dart';
|
||||||
|
import 'package:marianum_mobile/view/pages/timetable/data/lesson_merger.dart';
|
||||||
|
import 'package:marianum_mobile/view/pages/timetable/data/timetable_appointment_factory.dart';
|
||||||
|
import 'package:marianum_mobile/view/pages/timetable/data/timetable_name_mode.dart';
|
||||||
|
|
||||||
|
McTimetableEntry _lesson({
|
||||||
|
int id = 1,
|
||||||
|
int hour = 8,
|
||||||
|
int minute = 0,
|
||||||
|
List<String> classNames = const ['7a'],
|
||||||
|
List<String> subjects = const ['M'],
|
||||||
|
List<String> rooms = const ['A101'],
|
||||||
|
}) => McTimetableEntry(
|
||||||
|
id: id,
|
||||||
|
date: DateTime(2026, 5, 4),
|
||||||
|
startTime: DateTime(1970, 1, 1, hour, minute),
|
||||||
|
endTime: DateTime(1970, 1, 1, hour, minute + 45),
|
||||||
|
subjects: subjects,
|
||||||
|
teachers: [McTimetableTeacher(shortName: 'MUE', displayName: 'Stefan Müller')],
|
||||||
|
rooms: rooms,
|
||||||
|
classNames: classNames,
|
||||||
|
lessonType: 'LESSON',
|
||||||
|
status: 'REGULAR',
|
||||||
|
substitutionText: null,
|
||||||
|
lessonText: null,
|
||||||
|
infoText: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final _settings = TimetableSettings(
|
||||||
|
connectDoubleLessons: false,
|
||||||
|
timetableNameMode: TimetableNameMode.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
String _location({
|
||||||
|
required bool showClassInsteadOfTeacher,
|
||||||
|
List<String> classNames = const ['7a'],
|
||||||
|
}) => TimetableAppointmentFactory(
|
||||||
|
lessons: [_lesson(classNames: classNames)],
|
||||||
|
customEvents: const [],
|
||||||
|
subjects: const [],
|
||||||
|
settings: _settings,
|
||||||
|
now: DateTime(2026, 5, 4),
|
||||||
|
showClassInsteadOfTeacher: showClassInsteadOfTeacher,
|
||||||
|
).build().single.location!;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('teacher plan tile label', () {
|
||||||
|
test('shows the teacher surname by default', () {
|
||||||
|
expect(_location(showClassInsteadOfTeacher: false), 'A101\nMüller');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows the class instead of the teacher on teacher plans', () {
|
||||||
|
expect(_location(showClassInsteadOfTeacher: true), 'A101\n7a');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('joins multiple classes', () {
|
||||||
|
expect(
|
||||||
|
_location(
|
||||||
|
showClassInsteadOfTeacher: true,
|
||||||
|
classNames: const ['7a', '7b'],
|
||||||
|
),
|
||||||
|
'A101\n7a, 7b',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to the teacher when the entry has no class', () {
|
||||||
|
expect(
|
||||||
|
_location(showClassInsteadOfTeacher: true, classNames: const []),
|
||||||
|
'A101\nMüller',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('lesson merger class separation', () {
|
||||||
|
test('does not merge back-to-back lessons of different classes', () {
|
||||||
|
final merged = LessonMerger.merge([
|
||||||
|
_lesson(id: 1, hour: 8, classNames: const ['7a']),
|
||||||
|
_lesson(id: 2, hour: 8, minute: 45, classNames: const ['7b']),
|
||||||
|
]);
|
||||||
|
expect(merged, hasLength(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('still merges back-to-back lessons of the same class', () {
|
||||||
|
final merged = LessonMerger.merge([
|
||||||
|
_lesson(id: 1, hour: 8),
|
||||||
|
_lesson(id: 2, hour: 8, minute: 45),
|
||||||
|
]);
|
||||||
|
expect(merged, hasLength(1));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user