diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart index 10b6b36..81bb355 100644 --- a/lib/state/app/modules/app_modules.dart +++ b/lib/state/app/modules/app_modules.dart @@ -15,6 +15,7 @@ import 'settings/bloc/settings_cubit.dart'; import '../infrastructure/loadableState/loadable_state.dart'; import 'gradeAverages/view/grade_averages_view.dart'; import 'holidays/view/holidays_view.dart'; +import 'marianumDates/view/marianum_dates_view.dart'; import 'marianumMessage/view/marianum_message_list_view.dart'; import 'package:badges/badges.dart' as badges; @@ -98,6 +99,13 @@ class AppModule { breakerArea: BreakerArea.more, create: HolidaysView.new, ), + Modules.marianumDates: AppModule( + Modules.marianumDates, + name: 'Marianum Termine', + icon: () => Icon(Icons.event_note), + breakerArea: BreakerArea.more, + create: MarianumDatesView.new, + ), }; if(!showFiltered) available.removeWhere((key, value) => settings.val().modulesSettings.hiddenModules.contains(key)); @@ -140,4 +148,5 @@ enum Modules { roomPlan, gradeAveragesCalculator, holidays, + marianumDates, } diff --git a/lib/state/app/modules/marianumDates/bloc/marianum_dates_bloc.dart b/lib/state/app/modules/marianumDates/bloc/marianum_dates_bloc.dart new file mode 100644 index 0000000..3f7961b --- /dev/null +++ b/lib/state/app/modules/marianumDates/bloc/marianum_dates_bloc.dart @@ -0,0 +1,33 @@ +import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart'; +import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; +import '../repository/marianum_dates_repository.dart'; +import 'marianum_dates_event.dart'; +import 'marianum_dates_state.dart'; + +class MarianumDatesBloc extends LoadableHydratedBloc { + MarianumDatesBloc() { + on((event, emit) { + add(Emit((state) => state.copyWith(showPastEvents: event.shouldBeVisible))); + }); + } + + bool showPastEvents() => innerState?.showPastEvents ?? false; + + List? getEvents() => innerState?.events + .where((e) => showPastEvents() || e.end.isAfter(DateTime.now())) + .toList() ?? []; + + @override + fromNothing() => const MarianumDatesState(showPastEvents: false, events: []); + @override + fromStorage(Map json) => MarianumDatesState.fromJson(json); + @override + Future gatherData() async { + final events = await repo.getEvents(); + add(DataGathered((state) => state.copyWith(events: events))); + } + @override + repository() => MarianumDatesRepository(); + @override + Map? toStorage(state) => state.toJson(); +} diff --git a/lib/state/app/modules/marianumDates/bloc/marianum_dates_event.dart b/lib/state/app/modules/marianumDates/bloc/marianum_dates_event.dart new file mode 100644 index 0000000..34b5b8d --- /dev/null +++ b/lib/state/app/modules/marianumDates/bloc/marianum_dates_event.dart @@ -0,0 +1,9 @@ +import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart'; +import 'marianum_dates_state.dart'; + +sealed class MarianumDatesEvent extends LoadableHydratedBlocEvent {} + +class SetPastEventsVisible extends MarianumDatesEvent { + final bool shouldBeVisible; + SetPastEventsVisible(this.shouldBeVisible); +} diff --git a/lib/state/app/modules/marianumDates/bloc/marianum_dates_state.dart b/lib/state/app/modules/marianumDates/bloc/marianum_dates_state.dart new file mode 100644 index 0000000..d3a7d14 --- /dev/null +++ b/lib/state/app/modules/marianumDates/bloc/marianum_dates_state.dart @@ -0,0 +1,29 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter/foundation.dart'; + +part 'marianum_dates_state.freezed.dart'; +part 'marianum_dates_state.g.dart'; + +@freezed +abstract class MarianumDatesState with _$MarianumDatesState { + const factory MarianumDatesState({ + required bool showPastEvents, + required List events, + }) = _MarianumDatesState; + + factory MarianumDatesState.fromJson(Map json) => _$MarianumDatesStateFromJson(json); +} + +@freezed +abstract class MarianumDate with _$MarianumDate { + const factory MarianumDate({ + required String uid, + required String title, + required String? description, + required DateTime start, + required DateTime end, + required bool isAllDay, + }) = _MarianumDate; + + factory MarianumDate.fromJson(Map json) => _$MarianumDateFromJson(json); +} diff --git a/lib/state/app/modules/marianumDates/bloc/marianum_dates_state.freezed.dart b/lib/state/app/modules/marianumDates/bloc/marianum_dates_state.freezed.dart new file mode 100644 index 0000000..7ef98c8 --- /dev/null +++ b/lib/state/app/modules/marianumDates/bloc/marianum_dates_state.freezed.dart @@ -0,0 +1,588 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'marianum_dates_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$MarianumDatesState implements DiagnosticableTreeMixin { + + bool get showPastEvents; List get events; +/// Create a copy of MarianumDatesState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$MarianumDatesStateCopyWith get copyWith => _$MarianumDatesStateCopyWithImpl(this as MarianumDatesState, _$identity); + + /// Serializes this MarianumDatesState to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'MarianumDatesState')) + ..add(DiagnosticsProperty('showPastEvents', showPastEvents))..add(DiagnosticsProperty('events', events)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumDatesState&&(identical(other.showPastEvents, showPastEvents) || other.showPastEvents == showPastEvents)&&const DeepCollectionEquality().equals(other.events, events)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,showPastEvents,const DeepCollectionEquality().hash(events)); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'MarianumDatesState(showPastEvents: $showPastEvents, events: $events)'; +} + + +} + +/// @nodoc +abstract mixin class $MarianumDatesStateCopyWith<$Res> { + factory $MarianumDatesStateCopyWith(MarianumDatesState value, $Res Function(MarianumDatesState) _then) = _$MarianumDatesStateCopyWithImpl; +@useResult +$Res call({ + bool showPastEvents, List events +}); + + + + +} +/// @nodoc +class _$MarianumDatesStateCopyWithImpl<$Res> + implements $MarianumDatesStateCopyWith<$Res> { + _$MarianumDatesStateCopyWithImpl(this._self, this._then); + + final MarianumDatesState _self; + final $Res Function(MarianumDatesState) _then; + +/// Create a copy of MarianumDatesState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? showPastEvents = null,Object? events = null,}) { + return _then(_self.copyWith( +showPastEvents: null == showPastEvents ? _self.showPastEvents : showPastEvents // ignore: cast_nullable_to_non_nullable +as bool,events: null == events ? _self.events : events // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [MarianumDatesState]. +extension MarianumDatesStatePatterns on MarianumDatesState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _MarianumDatesState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _MarianumDatesState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _MarianumDatesState value) $default,){ +final _that = this; +switch (_that) { +case _MarianumDatesState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _MarianumDatesState value)? $default,){ +final _that = this; +switch (_that) { +case _MarianumDatesState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( bool showPastEvents, List events)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _MarianumDatesState() when $default != null: +return $default(_that.showPastEvents,_that.events);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( bool showPastEvents, List events) $default,) {final _that = this; +switch (_that) { +case _MarianumDatesState(): +return $default(_that.showPastEvents,_that.events);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool showPastEvents, List events)? $default,) {final _that = this; +switch (_that) { +case _MarianumDatesState() when $default != null: +return $default(_that.showPastEvents,_that.events);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _MarianumDatesState with DiagnosticableTreeMixin implements MarianumDatesState { + const _MarianumDatesState({required this.showPastEvents, required final List events}): _events = events; + factory _MarianumDatesState.fromJson(Map json) => _$MarianumDatesStateFromJson(json); + +@override final bool showPastEvents; + final List _events; +@override List get events { + if (_events is EqualUnmodifiableListView) return _events; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_events); +} + + +/// Create a copy of MarianumDatesState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$MarianumDatesStateCopyWith<_MarianumDatesState> get copyWith => __$MarianumDatesStateCopyWithImpl<_MarianumDatesState>(this, _$identity); + +@override +Map toJson() { + return _$MarianumDatesStateToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'MarianumDatesState')) + ..add(DiagnosticsProperty('showPastEvents', showPastEvents))..add(DiagnosticsProperty('events', events)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumDatesState&&(identical(other.showPastEvents, showPastEvents) || other.showPastEvents == showPastEvents)&&const DeepCollectionEquality().equals(other._events, _events)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,showPastEvents,const DeepCollectionEquality().hash(_events)); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'MarianumDatesState(showPastEvents: $showPastEvents, events: $events)'; +} + + +} + +/// @nodoc +abstract mixin class _$MarianumDatesStateCopyWith<$Res> implements $MarianumDatesStateCopyWith<$Res> { + factory _$MarianumDatesStateCopyWith(_MarianumDatesState value, $Res Function(_MarianumDatesState) _then) = __$MarianumDatesStateCopyWithImpl; +@override @useResult +$Res call({ + bool showPastEvents, List events +}); + + + + +} +/// @nodoc +class __$MarianumDatesStateCopyWithImpl<$Res> + implements _$MarianumDatesStateCopyWith<$Res> { + __$MarianumDatesStateCopyWithImpl(this._self, this._then); + + final _MarianumDatesState _self; + final $Res Function(_MarianumDatesState) _then; + +/// Create a copy of MarianumDatesState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? showPastEvents = null,Object? events = null,}) { + return _then(_MarianumDatesState( +showPastEvents: null == showPastEvents ? _self.showPastEvents : showPastEvents // ignore: cast_nullable_to_non_nullable +as bool,events: null == events ? _self._events : events // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + + +/// @nodoc +mixin _$MarianumDate implements DiagnosticableTreeMixin { + + String get uid; String get title; String? get description; DateTime get start; DateTime get end; bool get isAllDay; +/// Create a copy of MarianumDate +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$MarianumDateCopyWith get copyWith => _$MarianumDateCopyWithImpl(this as MarianumDate, _$identity); + + /// Serializes this MarianumDate to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'MarianumDate')) + ..add(DiagnosticsProperty('uid', uid))..add(DiagnosticsProperty('title', title))..add(DiagnosticsProperty('description', description))..add(DiagnosticsProperty('start', start))..add(DiagnosticsProperty('end', end))..add(DiagnosticsProperty('isAllDay', isAllDay)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumDate&&(identical(other.uid, uid) || other.uid == uid)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.start, start) || other.start == start)&&(identical(other.end, end) || other.end == end)&&(identical(other.isAllDay, isAllDay) || other.isAllDay == isAllDay)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,uid,title,description,start,end,isAllDay); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'MarianumDate(uid: $uid, title: $title, description: $description, start: $start, end: $end, isAllDay: $isAllDay)'; +} + + +} + +/// @nodoc +abstract mixin class $MarianumDateCopyWith<$Res> { + factory $MarianumDateCopyWith(MarianumDate value, $Res Function(MarianumDate) _then) = _$MarianumDateCopyWithImpl; +@useResult +$Res call({ + String uid, String title, String? description, DateTime start, DateTime end, bool isAllDay +}); + + + + +} +/// @nodoc +class _$MarianumDateCopyWithImpl<$Res> + implements $MarianumDateCopyWith<$Res> { + _$MarianumDateCopyWithImpl(this._self, this._then); + + final MarianumDate _self; + final $Res Function(MarianumDate) _then; + +/// Create a copy of MarianumDate +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? uid = null,Object? title = null,Object? description = freezed,Object? start = null,Object? end = null,Object? isAllDay = null,}) { + return _then(_self.copyWith( +uid: null == uid ? _self.uid : uid // ignore: cast_nullable_to_non_nullable +as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,start: null == start ? _self.start : start // ignore: cast_nullable_to_non_nullable +as DateTime,end: null == end ? _self.end : end // ignore: cast_nullable_to_non_nullable +as DateTime,isAllDay: null == isAllDay ? _self.isAllDay : isAllDay // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [MarianumDate]. +extension MarianumDatePatterns on MarianumDate { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _MarianumDate value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _MarianumDate() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _MarianumDate value) $default,){ +final _that = this; +switch (_that) { +case _MarianumDate(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _MarianumDate value)? $default,){ +final _that = this; +switch (_that) { +case _MarianumDate() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String uid, String title, String? description, DateTime start, DateTime end, bool isAllDay)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _MarianumDate() when $default != null: +return $default(_that.uid,_that.title,_that.description,_that.start,_that.end,_that.isAllDay);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String uid, String title, String? description, DateTime start, DateTime end, bool isAllDay) $default,) {final _that = this; +switch (_that) { +case _MarianumDate(): +return $default(_that.uid,_that.title,_that.description,_that.start,_that.end,_that.isAllDay);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String uid, String title, String? description, DateTime start, DateTime end, bool isAllDay)? $default,) {final _that = this; +switch (_that) { +case _MarianumDate() when $default != null: +return $default(_that.uid,_that.title,_that.description,_that.start,_that.end,_that.isAllDay);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _MarianumDate with DiagnosticableTreeMixin implements MarianumDate { + const _MarianumDate({required this.uid, required this.title, required this.description, required this.start, required this.end, required this.isAllDay}); + factory _MarianumDate.fromJson(Map json) => _$MarianumDateFromJson(json); + +@override final String uid; +@override final String title; +@override final String? description; +@override final DateTime start; +@override final DateTime end; +@override final bool isAllDay; + +/// Create a copy of MarianumDate +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$MarianumDateCopyWith<_MarianumDate> get copyWith => __$MarianumDateCopyWithImpl<_MarianumDate>(this, _$identity); + +@override +Map toJson() { + return _$MarianumDateToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'MarianumDate')) + ..add(DiagnosticsProperty('uid', uid))..add(DiagnosticsProperty('title', title))..add(DiagnosticsProperty('description', description))..add(DiagnosticsProperty('start', start))..add(DiagnosticsProperty('end', end))..add(DiagnosticsProperty('isAllDay', isAllDay)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumDate&&(identical(other.uid, uid) || other.uid == uid)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.start, start) || other.start == start)&&(identical(other.end, end) || other.end == end)&&(identical(other.isAllDay, isAllDay) || other.isAllDay == isAllDay)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,uid,title,description,start,end,isAllDay); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'MarianumDate(uid: $uid, title: $title, description: $description, start: $start, end: $end, isAllDay: $isAllDay)'; +} + + +} + +/// @nodoc +abstract mixin class _$MarianumDateCopyWith<$Res> implements $MarianumDateCopyWith<$Res> { + factory _$MarianumDateCopyWith(_MarianumDate value, $Res Function(_MarianumDate) _then) = __$MarianumDateCopyWithImpl; +@override @useResult +$Res call({ + String uid, String title, String? description, DateTime start, DateTime end, bool isAllDay +}); + + + + +} +/// @nodoc +class __$MarianumDateCopyWithImpl<$Res> + implements _$MarianumDateCopyWith<$Res> { + __$MarianumDateCopyWithImpl(this._self, this._then); + + final _MarianumDate _self; + final $Res Function(_MarianumDate) _then; + +/// Create a copy of MarianumDate +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? uid = null,Object? title = null,Object? description = freezed,Object? start = null,Object? end = null,Object? isAllDay = null,}) { + return _then(_MarianumDate( +uid: null == uid ? _self.uid : uid // ignore: cast_nullable_to_non_nullable +as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,start: null == start ? _self.start : start // ignore: cast_nullable_to_non_nullable +as DateTime,end: null == end ? _self.end : end // ignore: cast_nullable_to_non_nullable +as DateTime,isAllDay: null == isAllDay ? _self.isAllDay : isAllDay // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/lib/state/app/modules/marianumDates/bloc/marianum_dates_state.g.dart b/lib/state/app/modules/marianumDates/bloc/marianum_dates_state.g.dart new file mode 100644 index 0000000..e923443 --- /dev/null +++ b/lib/state/app/modules/marianumDates/bloc/marianum_dates_state.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'marianum_dates_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_MarianumDatesState _$MarianumDatesStateFromJson(Map json) => + _MarianumDatesState( + showPastEvents: json['showPastEvents'] as bool, + events: (json['events'] as List) + .map((e) => MarianumDate.fromJson(e as Map)) + .toList(), + ); + +Map _$MarianumDatesStateToJson(_MarianumDatesState instance) => + { + 'showPastEvents': instance.showPastEvents, + 'events': instance.events, + }; + +_MarianumDate _$MarianumDateFromJson(Map json) => + _MarianumDate( + uid: json['uid'] as String, + title: json['title'] as String, + description: json['description'] as String?, + start: DateTime.parse(json['start'] as String), + end: DateTime.parse(json['end'] as String), + isAllDay: json['isAllDay'] as bool, + ); + +Map _$MarianumDateToJson(_MarianumDate instance) => + { + 'uid': instance.uid, + 'title': instance.title, + 'description': instance.description, + 'start': instance.start.toIso8601String(), + 'end': instance.end.toIso8601String(), + 'isAllDay': instance.isAllDay, + }; diff --git a/lib/state/app/modules/marianumDates/dataProvider/marianum_dates_get_events.dart b/lib/state/app/modules/marianumDates/dataProvider/marianum_dates_get_events.dart new file mode 100644 index 0000000..2ab5ecd --- /dev/null +++ b/lib/state/app/modules/marianumDates/dataProvider/marianum_dates_get_events.dart @@ -0,0 +1,48 @@ +import 'package:dio/dio.dart'; +import 'package:enough_icalendar/enough_icalendar.dart'; + +import '../bloc/marianum_dates_state.dart'; + +class MarianumDatesGetEvents { + static const String url = 'https://public-cal.marianumlan.de/cal_public/ad4c5da8-7466-9c72-89cb-8b8d9a5cf26c'; + + final Dio _dio = Dio(BaseOptions( + connectTimeout: const Duration(seconds: 10).inMilliseconds, + receiveTimeout: const Duration(seconds: 30).inMilliseconds, + )); + + Future> run() async { + final response = await _dio.get(url); + final body = response.data; + if (body == null || body.isEmpty) return []; + + final root = VComponent.parse(body); + final calendar = root is VCalendar ? root : null; + final source = calendar?.children ?? root.children; + + final events = source.whereType().map(_toMarianumDate).whereType().toList(); + events.sort((a, b) => a.start.compareTo(b.start)); + return events; + } + + static MarianumDate? _toMarianumDate(VEvent e) { + final start = e.start; + if (start == null) return null; + final end = e.end ?? start; + final isAllDay = _isAllDay(start, end); + return MarianumDate( + uid: e.uid, + title: e.summary ?? '', + description: e.description, + start: start, + end: end, + isAllDay: isAllDay, + ); + } + + static bool _isAllDay(DateTime start, DateTime end) { + final startMidnight = start.hour == 0 && start.minute == 0 && start.second == 0; + final endMidnight = end.hour == 0 && end.minute == 0 && end.second == 0; + return startMidnight && endMidnight && end.difference(start).inHours % 24 == 0; + } +} diff --git a/lib/state/app/modules/marianumDates/repository/marianum_dates_repository.dart b/lib/state/app/modules/marianumDates/repository/marianum_dates_repository.dart new file mode 100644 index 0000000..416b4d5 --- /dev/null +++ b/lib/state/app/modules/marianumDates/repository/marianum_dates_repository.dart @@ -0,0 +1,7 @@ +import '../../../infrastructure/repository/repository.dart'; +import '../bloc/marianum_dates_state.dart'; +import '../dataProvider/marianum_dates_get_events.dart'; + +class MarianumDatesRepository extends Repository { + Future> getEvents() => MarianumDatesGetEvents().run(); +} diff --git a/lib/state/app/modules/marianumDates/view/marianum_dates_view.dart b/lib/state/app/modules/marianumDates/view/marianum_dates_view.dart new file mode 100644 index 0000000..4f283d3 --- /dev/null +++ b/lib/state/app/modules/marianumDates/view/marianum_dates_view.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; + +import '../../../../../view/pages/timetable/custom_events/custom_event_edit_dialog.dart'; +import '../../../../../widget/animatedTime.dart'; +import '../../../../../widget/centeredLeading.dart'; +import '../../../../../widget/debug/debugTile.dart'; +import '../../../../../widget/list_view_util.dart'; +import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart'; +import '../../../infrastructure/utilityWidgets/bloc_module.dart'; +import '../../../infrastructure/loadableState/loadable_state.dart'; +import '../bloc/marianum_dates_bloc.dart'; +import '../bloc/marianum_dates_event.dart'; +import '../bloc/marianum_dates_state.dart'; + +class MarianumDatesView extends StatelessWidget { + const MarianumDatesView({super.key}); + + @override + Widget build(BuildContext context) => BlocModule>( + create: (context) => MarianumDatesBloc(), + autoRebuild: true, + child: (context, bloc, state) => Scaffold( + appBar: AppBar( + title: const Text('Marianum Termine'), + actions: [ + PopupMenuButton( + initialValue: bloc.showPastEvents(), + icon: const Icon(Icons.history), + itemBuilder: (context) => [true, false].map((e) => PopupMenuItem( + value: e, + enabled: e != bloc.showPastEvents(), + child: Row( + children: [ + Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, color: Theme.of(context).colorScheme.onSurface), + const SizedBox(width: 15), + Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen'), + ], + ), + )).toList(), + onSelected: (e) => bloc.add(SetPastEventsVisible(e)), + ), + ], + ), + body: LoadableStateConsumer( + child: (state, loading) => ListViewUtil.fromList(bloc.getEvents(), (event) => _MarianumDateTile(event: event)), + ), + ), + ); +} + +class _MarianumDateTile extends StatelessWidget { + final MarianumDate event; + const _MarianumDateTile({required this.event}); + + String _formatSubtitle() { + final start = Jiffy.parseFromDateTime(event.start); + final end = Jiffy.parseFromDateTime(event.end); + + if (event.isAllDay) { + // iCal end is exclusive for multi-day all-day events. The feed sets + // DTSTART == DTEND for single-day all-day events, so only subtract a + // day when end actually advances past start. + final inclusiveEnd = event.end.isAfter(event.start) ? end.subtract(days: 1) : end; + final sameAllDay = start.format(pattern: 'yyyy-MM-dd') == inclusiveEnd.format(pattern: 'yyyy-MM-dd'); + return sameAllDay + ? '${start.format(pattern: 'dd.MM.yyyy')} · Ganztägig' + : '${start.format(pattern: 'dd.MM.yyyy')} – ${inclusiveEnd.format(pattern: 'dd.MM.yyyy')} · Ganztägig'; + } + + final sameDay = start.format(pattern: 'yyyy-MM-dd') == end.format(pattern: 'yyyy-MM-dd'); + if (sameDay) { + if (event.start == event.end) { + return '${start.format(pattern: 'dd.MM.yyyy')} · ${start.format(pattern: 'HH:mm')}'; + } + return '${start.format(pattern: 'dd.MM.yyyy')} · ${start.format(pattern: 'HH:mm')} – ${end.format(pattern: 'HH:mm')}'; + } + return '${start.format(pattern: 'dd.MM.yyyy HH:mm')} – ${end.format(pattern: 'dd.MM.yyyy HH:mm')}'; + } + + @override + Widget build(BuildContext context) => ListTile( + leading: const CenteredLeading(Icon(Icons.event)), + title: Text(event.title.isEmpty ? '(ohne Titel)' : event.title), + subtitle: Text(_formatSubtitle()), + onTap: () => _showDetails(context), + trailing: IconButton( + icon: const Icon(Icons.add_circle_outline), + tooltip: 'In Stundenplan übernehmen', + onPressed: () => showDialog( + context: context, + builder: (_) => CustomEventEditDialog( + initialTitle: event.title, + initialDescription: event.description, + initialStart: event.start, + initialEnd: event.end, + ), + barrierDismissible: false, + ), + ), + ); + + void _showDetails(BuildContext context) { + showDialog( + context: context, + builder: (context) => SimpleDialog( + title: Text(event.title.isEmpty ? '(ohne Titel)' : event.title), + children: [ + ListTile( + leading: const CenteredLeading(Icon(Icons.date_range_outlined)), + title: Text(_formatSubtitle()), + ), + if (event.description != null && event.description!.trim().isNotEmpty) + ListTile( + leading: const CenteredLeading(Icon(Icons.notes_outlined)), + title: Text(event.description!.trim()), + ), + Visibility( + visible: !event.start.difference(DateTime.now()).isNegative, + replacement: ListTile( + leading: const CenteredLeading(Icon(Icons.content_paste_search_outlined)), + title: Text(Jiffy.parseFromDateTime(event.start).fromNow()), + ), + child: ListTile( + leading: const CenteredLeading(Icon(Icons.timer_outlined)), + title: AnimatedTime(callback: () => event.start.difference(DateTime.now())), + subtitle: Text(Jiffy.parseFromDateTime(event.start).fromNow()), + ), + ), + DebugTile(context).jsonData(event.toJson()), + ], + ), + ); + } +} diff --git a/lib/state/app/modules/settings/bloc/settings_cubit.dart b/lib/state/app/modules/settings/bloc/settings_cubit.dart index 19cbc2d..68ca12a 100644 --- a/lib/state/app/modules/settings/bloc/settings_cubit.dart +++ b/lib/state/app/modules/settings/bloc/settings_cubit.dart @@ -5,6 +5,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import '../../../../../storage/base/settings.dart'; import '../../../../../view/settings/defaultSettings.dart'; +import '../../app_modules.dart'; class SettingsCubit extends HydratedCubit { static const _debounceTag = 'settings_persist'; @@ -36,16 +37,28 @@ class SettingsCubit extends HydratedCubit { @override Settings fromJson(Map json) { try { - return Settings.fromJson(json); + return _appendNewModules(Settings.fromJson(json)); } catch (_) { try { - return Settings.fromJson(_mergeSettings(json, DefaultSettings.get().toJson())); + return _appendNewModules(Settings.fromJson(_mergeSettings(json, DefaultSettings.get().toJson()))); } catch (_) { return DefaultSettings.get(); } } } + // Modules added in newer app versions won't appear in a previously persisted + // moduleOrder. Append any enum value that is neither ordered nor hidden so it + // becomes visible in the "Mehr" menu without forcing a full settings reset. + Settings _appendNewModules(Settings s) { + final order = s.modulesSettings.moduleOrder; + final hidden = s.modulesSettings.hiddenModules; + final missing = Modules.values.where((m) => !order.contains(m) && !hidden.contains(m)); + if (missing.isEmpty) return s; + s.modulesSettings.moduleOrder = [...order, ...missing]; + return s; + } + @override Map? toJson(Settings state) => state.toJson(); diff --git a/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart b/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart index 58d6f9e..f886131 100644 --- a/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart +++ b/lib/view/pages/timetable/custom_events/custom_event_edit_dialog.dart @@ -17,11 +17,15 @@ class CustomEventEditDialog extends StatefulWidget { final CustomTimetableEvent? existingEvent; final DateTime? initialStart; final DateTime? initialEnd; + final String? initialTitle; + final String? initialDescription; const CustomEventEditDialog({ this.existingEvent, this.initialStart, this.initialEnd, + this.initialTitle, + this.initialDescription, super.key, }); @@ -30,15 +34,21 @@ class CustomEventEditDialog extends StatefulWidget { } class _CustomEventEditDialogState extends State { + // Visible window of the timetable / time picker (matches `_pickTimeRange`'s + // `disabledTime`). Pre-filled times from outside this window are clamped in. + static const TimeOfDay _windowStart = TimeOfDay(hour: 8, minute: 0); + static const TimeOfDay _windowEnd = TimeOfDay(hour: 16, minute: 30); + static const int _minDurationMinutes = 15; + late DateTime _date = widget.existingEvent?.startDate ?? widget.initialStart ?? DateTime.now(); - late TimeOfDay _startTime = widget.existingEvent?.startDate.toTimeOfDay() ?? - widget.initialStart?.toTimeOfDay() ?? - const TimeOfDay(hour: 8, minute: 0); - late TimeOfDay _endTime = widget.existingEvent?.endDate.toTimeOfDay() ?? - widget.initialEnd?.toTimeOfDay() ?? - const TimeOfDay(hour: 9, minute: 30); - late final TextEditingController _name = TextEditingController(text: widget.existingEvent?.title); - late final TextEditingController _description = TextEditingController(text: widget.existingEvent?.description); + late TimeOfDay _startTime; + late TimeOfDay _endTime; + late final TextEditingController _name = TextEditingController( + text: widget.existingEvent?.title ?? widget.initialTitle, + ); + late final TextEditingController _description = TextEditingController( + text: widget.existingEvent?.description ?? widget.initialDescription, + ); late String _rrule = widget.existingEvent?.rrule ?? ''; late CustomTimetableColors _color = CustomTimetableColors.values.firstWhere( (e) => e.name == widget.existingEvent?.color, @@ -47,6 +57,37 @@ class _CustomEventEditDialogState extends State { bool get _isEditing => widget.existingEvent != null; + @override + void initState() { + super.initState(); + if (_isEditing) { + _startTime = widget.existingEvent!.startDate.toTimeOfDay(); + _endTime = widget.existingEvent!.endDate.toTimeOfDay(); + return; + } + final rawStart = widget.initialStart?.toTimeOfDay() ?? _windowStart; + final rawEnd = widget.initialEnd?.toTimeOfDay() ?? const TimeOfDay(hour: 9, minute: 30); + final clamped = _clampToVisibleWindow(rawStart, rawEnd); + _startTime = clamped.$1; + _endTime = clamped.$2; + } + + static (TimeOfDay, TimeOfDay) _clampToVisibleWindow(TimeOfDay rawStart, TimeOfDay rawEnd) { + int toMin(TimeOfDay t) => t.hour * 60 + t.minute; + TimeOfDay fromMin(int m) => TimeOfDay(hour: m ~/ 60, minute: m % 60); + + final windowStart = toMin(_windowStart); + final windowEnd = toMin(_windowEnd); + var start = toMin(rawStart).clamp(windowStart, windowEnd - _minDurationMinutes); + var end = toMin(rawEnd); + if (end < start + _minDurationMinutes) end = start + _minDurationMinutes; + if (end > windowEnd) { + end = windowEnd; + start = end - _minDurationMinutes; + } + return (fromMin(start), fromMin(end)); + } + bool _validate() => _name.text.isNotEmpty; void _save() { @@ -79,11 +120,14 @@ class _CustomEventEditDialogState extends State { } Future _pickDate() async { + final now = DateTime.now(); + final defaultFirst = now.subtract(const Duration(days: 30)); + final defaultLast = now.add(const Duration(days: 365)); final picked = await showDatePicker( context: context, initialDate: _date, - firstDate: DateTime.now().subtract(const Duration(days: 30)), - lastDate: DateTime.now().add(const Duration(days: 30)), + firstDate: _date.isBefore(defaultFirst) ? _date : defaultFirst, + lastDate: _date.isAfter(defaultLast) ? _date : defaultLast, ); if (picked != null && picked != _date) setState(() => _date = picked); } diff --git a/lib/view/settings/defaultSettings.dart b/lib/view/settings/defaultSettings.dart index 9348a6d..3e2fac8 100644 --- a/lib/view/settings/defaultSettings.dart +++ b/lib/view/settings/defaultSettings.dart @@ -27,7 +27,8 @@ class DefaultSettings { Modules.marianumMessage, Modules.roomPlan, Modules.gradeAveragesCalculator, - Modules.holidays + Modules.holidays, + Modules.marianumDates, ], hiddenModules: [], ), diff --git a/pubspec.yaml b/pubspec.yaml index a8be8b4..776b411 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -84,6 +84,7 @@ dependencies: time_range_picker: ^2.3.0 url_launcher: ^6.3.1 uuid: ^4.5.1 + enough_icalendar: ^0.17.0 dev_dependencies: flutter_launcher_icons: ^0.14.3