folder restructuring

This commit is contained in:
2026-05-05 21:44:23 +02:00
parent db9c3386f1
commit 4f796dac2e
102 changed files with 1254 additions and 879 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ import 'package:flutter_login/flutter_login.dart';
import '../../api/marianumcloud/talk/room/getRoom.dart';
import '../../api/marianumcloud/talk/room/getRoomParams.dart';
import '../../model/accountData.dart';
import '../../model/account_data.dart';
import '../../state/app/modules/account/bloc/account_bloc.dart';
import '../../state/app/modules/account/bloc/account_state.dart';
+4 -4
View File
@@ -12,10 +12,10 @@ import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../state/app/modules/files/bloc/files_bloc.dart';
import '../../../state/app/modules/files/bloc/files_state.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../widget/filePick.dart';
import '../../../widget/placeholderView.dart';
import 'fileElement.dart';
import 'filesUploadDialog.dart';
import '../../../widget/file_pick.dart';
import '../../../widget/placeholder_view.dart';
import 'widgets/file_element.dart';
import 'files_upload_dialog.dart';
class BetterSortOption {
String displayName;
@@ -6,8 +6,8 @@ import 'package:nextcloud/nextcloud.dart';
import 'package:uuid/uuid.dart';
import '../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../widget/confirmDialog.dart';
import '../../../widget/focusBehaviour.dart';
import '../../../widget/confirm_dialog.dart';
import '../../../widget/focus_behaviour.dart';
class FilesUploadDialog extends StatefulWidget {
final List<String> filePaths;
@@ -7,19 +7,18 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:open_filex/open_filex.dart';
import '../../../widget/infoDialog.dart';
import '../../../../widget/info_dialog.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:path_provider/path_provider.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
import '../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../model/accountData.dart';
import '../../../model/endpointData.dart';
import '../../../widget/centeredLeading.dart';
import '../../../widget/confirmDialog.dart';
import '../../../widget/fileViewer.dart';
import '../../../widget/unimplementedDialog.dart';
import 'files.dart';
import '../../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
import '../../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../../model/account_data.dart';
import '../../../../model/endpoint_data.dart';
import '../../../../routing/app_routes.dart';
import '../../../../widget/centered_leading.dart';
import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/unimplemented_dialog.dart';
class FileElement extends StatefulWidget {
final CacheableFile file;
@@ -45,12 +44,8 @@ class FileElement extends StatefulWidget {
deleteOnCancel: true,
client: Dio(BaseOptions(headers: AccountData().authHeaders())),
onDone: () {
//Future<OpenResult> result = OpenFile.open(local); // TODO legacy - refactor: remove onDone parameter
Navigator.of(context).push(MaterialPageRoute(builder: (context) => FileViewer(path: local)));
AppRoutes.openFileViewer(context, local);
onDone(OpenResult(message: 'File viewer opened', type: ResultType.done));
// result.then((value) => {
// onDone(value)
// });
},
);
@@ -101,9 +96,7 @@ class _FileElementState extends State<FileElement> {
trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null),
onTap: () {
if(widget.file.isDirectory) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Files(path: widget.path.toList()..add(widget.file.name)),
));
AppRoutes.openFolder(context, widget.path.toList()..add(widget.file.name));
} else {
if(EndpointData().getEndpointMode() == EndpointMode.stage) {
InfoDialog.show(context, 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!');
@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart';
import '../../../state/app/modules/gradeAverages/bloc/grade_averages_event.dart';
class GradeAveragesListView extends StatelessWidget {
const GradeAveragesListView({super.key});
@override
Widget build(BuildContext context) {
var bloc = context.watch<GradeAveragesBloc>();
String getGradeDisplay(int grade) {
if(bloc.isMiddleSchool()) {
return 'Note $grade';
} else {
return "$grade Punkt${grade > 1 ? "e" : ""}";
}
}
return ListView.builder(
itemCount: bloc.gradesInGradingSystem(),
itemBuilder: (context, index) {
var grade = bloc.getGradeFromIndex(index);
return Material(
child: ListTile(
tileColor: grade.isEven ? Colors.transparent : Colors.transparent.withAlpha(50),
title: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(getGradeDisplay(grade)),
const SizedBox(width: 30),
IconButton(
onPressed: () {
bloc.add(DecrementGrade(grade));
},
icon: const Icon(Icons.remove),
color: Theme.of(context).colorScheme.onSurface,
),
Text('${bloc.countOfGrade(grade)}', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
IconButton(
onPressed: () {
bloc.add(IncrementGrade(grade));
},
icon: const Icon(Icons.add),
color: Theme.of(context).colorScheme.onSurface,
),
],
),
),
trailing: Visibility(
maintainState: true,
maintainAnimation: true,
maintainSize: true,
visible: bloc.canDecrementOrDelete(grade),
child: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
bloc.add(ResetGrade(grade));
},
),
),
),
);
},
);
}
}
@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart';
import '../../../state/app/modules/gradeAverages/bloc/grade_averages_event.dart';
import '../../../state/app/modules/gradeAverages/bloc/grade_averages_state.dart';
import '../../../widget/confirm_dialog.dart';
import 'grade_averages_list_view.dart';
class GradeAveragesView extends StatelessWidget {
const GradeAveragesView({super.key});
@override
Widget build(BuildContext context) => BlocProvider<GradeAveragesBloc>(
create: (context) => GradeAveragesBloc(),
child: BlocBuilder<GradeAveragesBloc, GradeAveragesState>(
builder: (context, state) {
var bloc = context.watch<GradeAveragesBloc>();
return Scaffold(
appBar: AppBar(
title: const Text('Notendurschnittsrechner'),
actions: [
Visibility(
visible: bloc.state.grades.isNotEmpty,
child: IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
title: 'Zurücksetzen?',
content: 'Alle Einträge werden entfernt.',
confirmButton: 'Zurücksetzen',
onConfirm: () {
bloc.add(ResetAll());
},
),
);
},
icon: const Icon(Icons.delete_forever)),
),
PopupMenuButton<bool>(
initialValue: bloc.isMiddleSchool(),
icon: const Icon(Icons.more_horiz),
itemBuilder: (context) => [true, false].map((isMiddleSchool) => PopupMenuItem<bool>(
value: isMiddleSchool,
child: Row(
children: [
Icon(
isMiddleSchool ? Icons.calculate_outlined : Icons.school_outlined,
color: Theme.of(context).colorScheme.onSurface
),
const SizedBox(width: 15),
Text(isMiddleSchool ? 'Realschule' : 'Oberstufe'),
],
),
)).toList(),
onSelected: (isMiddleSchool) {
if (bloc.state.grades.isNotEmpty) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
title: 'Notensystem wechseln',
content:
'Beim Wechsel des Notensystems werden alle Einträge zurückgesetzt.',
confirmButton: 'Fortfahren',
onConfirm: () => bloc.add(GradingSystemChanged(isMiddleSchool)),
),
);
} else {
bloc.add(GradingSystemChanged(isMiddleSchool));
}
},
),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 30),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Ø', style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold)),
SizedBox(width: 5),
Text(bloc.average().toStringAsFixed(2), style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold))
],
),
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 10),
Text(bloc.isMiddleSchool() ? 'Wähle die Anzahl deiner jeweiligen Noten aus' : 'Wähle die Anzahl deiner jeweiligen Punkte aus'),
const SizedBox(height: 10),
const Expanded(
child: GradeAveragesListView()
),
],
),
);
},
),
);
}
+119
View File
@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../state/app/modules/holidays/bloc/holidays_bloc.dart';
import '../../../state/app/modules/holidays/bloc/holidays_event.dart';
import '../../../state/app/modules/holidays/bloc/holidays_state.dart';
import '../../../widget/animated_time.dart';
import '../../../widget/centered_leading.dart';
import '../../../widget/debug/debug_tile.dart';
import '../../../widget/list_view_util.dart';
import '../../../widget/string_extensions.dart';
class HolidaysView extends StatelessWidget {
const HolidaysView({super.key});
@override
Widget build(BuildContext context) => BlocModule<HolidaysBloc, LoadableState<HolidaysState>>(
create: (context) => HolidaysBloc(),
autoRebuild: true,
child: (context, bloc, state) {
void showDisclaimer() {
showDialog(context: context, builder: (context) => AlertDialog(
title: const Text('Richtigkeit und Bereitstellung der Daten'),
content: const Text(''
'Sämtliche Datumsangaben sind ohne Gewähr.\n'
'Ich übernehme weder Verantwortung für die Richtigkeit der Daten noch hafte ich für wirtschaftliche Schäden die aus der Verwendung dieser Daten entstehen können.\n\n'
'Die Daten stammen von https://ferien-api.de/'),
actions: [
TextButton(child: const Text('Okay'), onPressed: () => Navigator.of(context).pop()),
],
));
}
return Scaffold(
appBar: AppBar(
title: const Text('Schulferien in Hessen'),
actions: [
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: showDisclaimer,
),
PopupMenuButton<bool>(
initialValue: bloc.showPastHolidays(),
icon: const Icon(Icons.history),
itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>(
value: e,
enabled: e != bloc.showPastHolidays(),
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(SetPastHolidaysVisible(e)),
),
],
),
body: LoadableStateConsumer<HolidaysBloc, HolidaysState>(
onLoad: (state) {
if(state.showDisclaimer) showDisclaimer();
bloc.add(DisclaimerDismissed());
},
child: (state, loading) => ListViewUtil.fromList<Holiday>(bloc.getHolidays(), (holiday) {
var holidayType = holiday.name.split(' ').first.capitalize();
String formatDate(String date) => Jiffy.parse(date).format(pattern: 'dd.MM.yyyy');
String getYear(String date, {String format = 'yyyy'}) => Jiffy.parse(date).format(pattern: format);
String getHolidayYear(String startDate, String endDate) => getYear(startDate) == getYear(endDate)
? getYear(startDate)
: '${getYear(startDate)}/${getYear(endDate, format: 'yy')}';
return ListTile(
leading: const CenteredLeading(Icon(Icons.calendar_month)),
title: Text('$holidayType ${getHolidayYear(holiday.start, holiday.end)}'),
subtitle: Text('${formatDate(holiday.start)} - ${formatDate(holiday.end)}'),
onTap: () => showDialog(context: context, builder: (context) => SimpleDialog(
title: Text('$holidayType ${holiday.year} in Hessen'),
children: [
ListTile(
leading: const CenteredLeading(Icon(Icons.signpost_outlined)),
title: Text(holiday.name.capitalize()),
subtitle: Text(holiday.slug.capitalize()),
),
ListTile(
leading: const Icon(Icons.date_range_outlined),
title: Text('vom ${formatDate(holiday.start)}'),
),
ListTile(
leading: const Icon(Icons.date_range_outlined),
title: Text('bis zum ${formatDate(holiday.end)}'),
),
Visibility(
visible: !DateTime.parse(holiday.start).difference(DateTime.now()).isNegative,
replacement: ListTile(
leading: const CenteredLeading(Icon(Icons.content_paste_search_outlined)),
title: Text(Jiffy.parse(holiday.start).fromNow()),
),
child: ListTile(
leading: const CenteredLeading(Icon(Icons.timer_outlined)),
title: AnimatedTime(callback: () => DateTime.parse(holiday.start).difference(DateTime.now())),
subtitle: Text(Jiffy.parse(holiday.start).fromNow()),
),
),
DebugTile(context).jsonData(holiday.toJson()),
],
)),
trailing: const Icon(Icons.arrow_right),
);
}),
),
);
},
);
}
@@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../state/app/modules/marianumDates/bloc/marianum_dates_bloc.dart';
import '../../../state/app/modules/marianumDates/bloc/marianum_dates_event.dart';
import '../../../state/app/modules/marianumDates/bloc/marianum_dates_state.dart';
import '../../../widget/animated_time.dart';
import '../../../widget/centered_leading.dart';
import '../../../widget/debug/debug_tile.dart';
import '../../../widget/list_view_util.dart';
import '../timetable/custom_events/custom_event_edit_dialog.dart';
class MarianumDatesView extends StatelessWidget {
const MarianumDatesView({super.key});
@override
Widget build(BuildContext context) => BlocModule<MarianumDatesBloc, LoadableState<MarianumDatesState>>(
create: (context) => MarianumDatesBloc(),
autoRebuild: true,
child: (context, bloc, state) => Scaffold(
appBar: AppBar(
title: const Text('Marianum Termine'),
actions: [
PopupMenuButton<bool>(
initialValue: bloc.showPastEvents(),
icon: const Icon(Icons.history),
itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>(
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<MarianumDatesBloc, MarianumDatesState>(
child: (state, loading) => ListViewUtil.fromList<MarianumDate>(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()),
],
),
);
}
}
@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import '../../../routing/app_routes.dart';
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart';
import '../../../state/app/modules/marianumMessage/bloc/marianum_message_state.dart';
class MarianumMessageListView extends StatelessWidget {
const MarianumMessageListView({super.key});
@override
Widget build(BuildContext context) => BlocModule<MarianumMessageBloc, LoadableState<MarianumMessageState>>(
create: (context) => MarianumMessageBloc(),
child: (context, bloc, state) => Scaffold(
appBar: AppBar(
title: const Text('Marianum Message'),
),
body: LoadableStateConsumer<MarianumMessageBloc, MarianumMessageState>(
child: (state, loading) => ListView.builder(
itemCount: state.messageList.messages.length,
itemBuilder: (context, index) {
var message = state.messageList.messages.toList()[index];
return ListTile(
leading: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Icon(Icons.newspaper)],
),
title: Text(message.name, overflow: TextOverflow.ellipsis),
subtitle: Text('vom ${message.date}'),
trailing: const Icon(Icons.arrow_right),
onTap: () {
AppRoutes.openMarianumMessage(context, state.messageList.base, message);
},
);
}
),
),
)
);
}
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../state/app/modules/marianumMessage/bloc/marianum_message_state.dart';
import '../../../widget/confirm_dialog.dart';
class MessageView extends StatefulWidget {
final String basePath;
final MarianumMessage message;
const MessageView({super.key, required this.basePath, required this.message});
@override
State<MessageView> createState() => _MessageViewState();
}
class _MessageViewState extends State<MessageView> {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(widget.message.name),
),
body: SfPdfViewer.network(
widget.basePath + widget.message.url,
enableHyperlinkNavigation: true,
onDocumentLoadFailed: (PdfDocumentLoadFailedDetails e) {
Navigator.of(context).pop();
showDialog(context: context, builder: (context) => AlertDialog(
title: const Text('Fehler beim öffnen'),
content: Text("Dokument '${widget.message.name}' konnte nicht geladen werden:\n${e.description}"),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop();
}, child: const Text('Ok'))
],
));
},
onHyperlinkClicked: (PdfHyperlinkClickedDetails e) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
title: 'Link öffnen',
content: 'Möchtest du den folgenden Link öffnen?\n${e.uri}',
confirmButton: 'Öffnen',
onConfirm: () => launchUrl(Uri.parse(e.uri), mode: LaunchMode.externalApplication),
),
);
},
),
);
}
@@ -11,11 +11,11 @@ import 'package:badges/badges.dart' as badges;
import '../../../../api/mhsl/server/feedback/addFeedback.dart';
import '../../../../api/mhsl/server/feedback/addFeedbackParams.dart';
import '../../../../model/accountData.dart';
import '../../../../model/account_data.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/filePick.dart';
import '../../../../widget/focusBehaviour.dart';
import '../../../../widget/infoDialog.dart';
import '../../../../widget/file_pick.dart';
import '../../../../widget/focus_behaviour.dart';
import '../../../../widget/info_dialog.dart';
class FeedbackDialog extends StatefulWidget {
const FeedbackDialog({super.key});
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'appSharePlatformView.dart';
import 'app_share_platform_view.dart';
class QrShareView extends StatefulWidget {
const QrShareView({super.key});
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:share_plus/share_plus.dart';
import '../../../../widget/sharePositionOrigin.dart';
import 'qrShareView.dart';
import '../../../../widget/share_position_origin.dart';
enum ShareTargetType { qr }
class SelectShareTypeDialog extends StatelessWidget {
const SelectShareTypeDialog({super.key});
@@ -14,15 +15,14 @@ class SelectShareTypeDialog extends StatelessWidget {
leading: const Icon(Icons.qr_code_2_outlined),
title: const Text('Per QR-Code'),
trailing: const Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => const QrShareView()));
},
onTap: () => Navigator.of(context).pop(ShareTargetType.qr),
),
ListTile(
leading: const Icon(Icons.link_outlined),
title: const Text('Per Link teilen'),
trailing: const Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
SharePlus.instance.share(ShareParams(
sharePositionOrigin: SharePositionOrigin.get(context),
subject: 'App Teilen',
+16 -11
View File
@@ -4,18 +4,16 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:in_app_review/in_app_review.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../extensions/renderNotNull.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import '../../extensions/render_not_null.dart';
import '../../routing/app_routes.dart';
import '../../state/app/modules/app_modules.dart';
import '../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../storage/base/settings.dart' as model;
import '../../widget/centeredLeading.dart';
import '../../widget/infoDialog.dart';
import '../settings/defaultSettings.dart';
import '../settings/settings.dart';
import 'more/feedback/feedbackDialog.dart';
import 'more/share/selectShareTypeDialog.dart';
import '../../widget/centered_leading.dart';
import '../../widget/info_dialog.dart';
import 'settings/data/default_settings.dart';
import 'more/share/select_share_type_dialog.dart';
class Overhang extends StatefulWidget {
const Overhang({super.key});
@@ -41,7 +39,7 @@ class _OverhangState extends State<Overhang> {
icon: Icon(Icons.undo_outlined)
),
IconButton(onPressed: () => setState(() => editMode = !editMode), icon: Icon(Icons.edit_note_outlined), color: editMode ? Theme.of(context).primaryColor : null),
IconButton(onPressed: editMode ? null : () => pushScreen(context, screen: const Settings(), withNavBar: false), icon: const Icon(Icons.settings)),
IconButton(onPressed: editMode ? null : () => AppRoutes.openSettings(context), icon: const Icon(Icons.settings)),
],
),
body: editMode ? _sorting() : _overhang(),
@@ -92,7 +90,14 @@ class _OverhangState extends State<Overhang> {
title: const Text('Teile die App'),
subtitle: const Text('Mit Freunden und deiner Klasse teilen'),
trailing: const Icon(Icons.arrow_right),
onTap: () => showDialog(context: context, builder: (context) => const SelectShareTypeDialog())
onTap: () async {
final result = await showDialog<ShareTargetType>(
context: context,
builder: (_) => const SelectShareTypeDialog(),
);
if (!mounted || result != ShareTargetType.qr) return;
if (context.mounted) AppRoutes.openQrShare(context);
},
),
FutureBuilder(
future: InAppReview.instance.isAvailable(),
@@ -130,7 +135,7 @@ class _OverhangState extends State<Overhang> {
title: const Text('Du hast eine Idee?'),
subtitle: const Text('Fehler und Verbessungsvorschläge'),
trailing: const Icon(Icons.arrow_right),
onTap: () => pushScreen(context, withNavBar: false, screen: const FeedbackDialog()),
onTap: () => AppRoutes.openFeedback(context),
),
],
);
@@ -2,18 +2,18 @@ import 'dart:io';
import 'package:flutter/material.dart';
import '../../state/app/modules/app_modules.dart';
import '../../storage/base/settings.dart';
import '../../storage/devTools/devToolsSettings.dart';
import '../../storage/file/fileSettings.dart';
import '../../storage/fileView/fileViewSettings.dart';
import '../../storage/general/modulesSettings.dart';
import '../../storage/holidays/holidaysSettings.dart';
import '../../storage/notification/notificationSettings.dart';
import '../../storage/talk/talkSettings.dart';
import '../../storage/timetable/timetableSettings.dart';
import '../pages/files/files.dart';
import '../../storage/timetable/timetable_name_mode.dart';
import '../../../../state/app/modules/app_modules.dart';
import '../../../../storage/base/settings.dart';
import '../../../../storage/devTools/devToolsSettings.dart';
import '../../../../storage/file/fileSettings.dart';
import '../../../../storage/fileView/fileViewSettings.dart';
import '../../../../storage/general/modulesSettings.dart';
import '../../../../storage/holidays/holidaysSettings.dart';
import '../../../../storage/notification/notificationSettings.dart';
import '../../../../storage/talk/talkSettings.dart';
import '../../../../storage/timetable/timetable_name_mode.dart';
import '../../../../storage/timetable/timetableSettings.dart';
import '../../files/files.dart';
class DefaultSettings {
static Settings get() => Settings(
@@ -0,0 +1,135 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:jiffy/jiffy.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/centered_leading.dart';
import '../../../../widget/confirm_dialog.dart';
import '../data/default_settings.dart';
import '../widgets/privacy_info.dart';
import 'dev_tools_section.dart';
class AboutSection extends StatelessWidget {
const AboutSection({super.key});
@override
Widget build(BuildContext context) {
final settings = context.read<SettingsCubit>();
return Column(
children: [
ListTile(
leading: const Icon(Icons.live_help_outlined),
title: const Text('Informationen und Lizenzen'),
onTap: () => _showAppInfo(context),
trailing: const Icon(Icons.arrow_right),
),
ListTile(
leading: const Icon(Icons.policy_outlined),
title: const Text('Impressum & Datenschutz'),
onTap: () => _showPrivacyDialog(context),
trailing: const Icon(Icons.arrow_right),
),
const Divider(),
ListTile(
leading: const CenteredLeading(Icon(Icons.code)),
title: const Text('Quellcode MarianumMobile/Client'),
subtitle: const Text('GNU GPL v3'),
onTap: () => ConfirmDialog.openBrowser(context, 'https://mhsl.eu/gitea/MarianumMobile/Client'),
),
ListTile(
leading: const Icon(Icons.developer_mode_outlined),
title: const Text('Entwicklermodus'),
trailing: Checkbox(
value: settings.val().devToolsEnabled,
onChanged: (state) => _toggleDeveloperMode(context, settings, state),
),
),
Visibility(
visible: settings.val().devToolsEnabled,
child: DevToolsSection(settings: settings),
),
],
);
}
Future<void> _showAppInfo(BuildContext context) async {
final appInfo = await PackageInfo.fromPlatform();
if (!context.mounted) return;
showAboutDialog(
context: context,
applicationIcon: const Icon(Icons.apps),
applicationName: 'MarianumMobile',
applicationVersion: '${appInfo.appName}\n\nPackage: ${appInfo.packageName}\nVersion: ${appInfo.version}\nBuild: ${appInfo.buildNumber}',
applicationLegalese: 'Dies ist ein Inoffizieller Nextcloud & Webuntis Client und wird nicht vom Marianum selbst betrieben.\n'
'Keinerlei Gewähr für Vollständigkeit, Richtigkeit und Aktualität!\n\n'
"${kReleaseMode ? "Production" : "Development"} build\n"
'Marianum Fulda 2023-${Jiffy.now().year}\nElias Müller',
);
}
void _showPrivacyDialog(BuildContext context) => showDialog(
context: context,
builder: (context) => SimpleDialog(
children: [
ListTile(
leading: const CenteredLeading(Icon(Icons.school_outlined)),
title: const Text('Infos zum Marianum Fulda'),
subtitle: const Text('Für Talk-Chats und Dateien'),
trailing: const Icon(Icons.arrow_right),
onTap: () => PrivacyInfo(
providerText: 'Marianum',
imprintUrl: 'https://www.marianum-fulda.de/impressum',
privacyUrl: 'https://www.marianum-fulda.de/datenschutz',
).showPopup(context),
),
ListTile(
leading: const CenteredLeading(Icon(Icons.date_range_outlined)),
title: const Text('Infos zu Web-/ Untis'),
subtitle: const Text('Für den Stundenplan'),
trailing: const Icon(Icons.arrow_right),
onTap: () => PrivacyInfo(
providerText: 'Untis',
imprintUrl: 'https://www.untis.at/impressum',
privacyUrl: 'https://www.untis.at/datenschutz-wu-apps',
).showPopup(context),
),
ListTile(
leading: const CenteredLeading(Icon(Icons.send_time_extension_outlined)),
title: const Text('Infos zu mhsl'),
subtitle: const Text('Für Countdowns, Marianum Message und mehr'),
trailing: const Icon(Icons.arrow_right),
onTap: () => PrivacyInfo(
providerText: 'mhsl',
imprintUrl: 'https://mhsl.eu/id.html',
privacyUrl: 'https://mhsl.eu/datenschutz.html',
).showPopup(context),
),
],
),
);
void _toggleDeveloperMode(BuildContext context, SettingsCubit settings, bool? state) {
void apply() {
final enabled = state ?? false;
settings.val(write: true).devToolsEnabled = enabled;
if (!enabled) settings.val(write: true).devToolsSettings = DefaultSettings.get().devToolsSettings;
}
if (!state!) {
apply();
return;
}
ConfirmDialog(
title: 'Entwicklermodus',
content: 'Die Entwickleransicht bietet erweiterte Funktionen, die für den üblichen Gebrauch nicht benötigt werden.\n\n'
'Die Verwendung der Tools kann darüber hinaus bei falscher Verwendung zu Fehlern führen.\n\n'
'Aktivieren auf eigene Verantwortung.',
confirmButton: 'Ja, ich verstehe das Risiko',
cancelButton: 'Nein, zurück zur App',
onConfirm: apply,
).asDialog(context);
}
}
@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../model/account_data.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/centered_leading.dart';
import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/debug/cache_view.dart';
class AccountSection extends StatelessWidget {
const AccountSection({super.key});
@override
Widget build(BuildContext context) => ListTile(
leading: const CenteredLeading(Icon(Icons.logout_outlined)),
title: const Text('Konto abmelden'),
subtitle: Text('Angemeldet als ${AccountData().getUsername()}'),
onTap: () => _showLogoutDialog(context),
);
void _showLogoutDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
title: 'Abmelden?',
content: 'Möchtest du dich wirklich abmelden?',
confirmButton: 'Abmelden',
onConfirm: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
PaintingBinding.instance.imageCache.clear();
if (!context.mounted) return;
context.read<SettingsCubit>().reset();
const CacheView().clear();
AccountData().removeData(context: context);
},
),
);
}
}
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../theming/app_theme.dart';
class AppearanceSection extends StatelessWidget {
const AppearanceSection({super.key});
@override
Widget build(BuildContext context) {
final settings = context.read<SettingsCubit>();
return ListTile(
leading: const Icon(Icons.dark_mode_outlined),
title: const Text('Farbgebung'),
trailing: DropdownButton<ThemeMode>(
value: settings.val().appTheme,
icon: const Icon(Icons.arrow_drop_down),
items: ThemeMode.values
.map((e) => DropdownMenuItem<ThemeMode>(
value: e,
enabled: e != settings.val().appTheme,
child: Row(
children: [
Icon(AppTheme.getDisplayOptions(e).icon),
const SizedBox(width: 10),
Text(AppTheme.getDisplayOptions(e).displayName),
],
),
))
.toList(),
onChanged: (e) => settings.val(write: true).appTheme = e!,
),
);
}
}
@@ -4,21 +4,22 @@ import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../widget/centeredLeading.dart';
import '../../widget/confirmDialog.dart';
import '../../widget/debug/cacheView.dart';
import '../../widget/debug/jsonViewer.dart';
import '../../../../routing/app_routes.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/centered_leading.dart';
import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/debug/cache_view.dart';
import '../../../../widget/debug/json_viewer.dart';
class DevToolsSettings extends StatefulWidget {
class DevToolsSection extends StatefulWidget {
final SettingsCubit settings;
const DevToolsSettings({required this.settings, super.key});
const DevToolsSection({required this.settings, super.key});
@override
State<DevToolsSettings> createState() => _DevToolsSettingsState();
State<DevToolsSection> createState() => _DevToolsSectionState();
}
class _DevToolsSettingsState extends State<DevToolsSettings> {
class _DevToolsSectionState extends State<DevToolsSection> {
@override
Widget build(BuildContext context) => Column(
children: [
@@ -96,9 +97,7 @@ class _DevToolsSettingsState extends State<DevToolsSettings> {
future: const CacheView().totalSize(),
builder: (context, snapshot) => Text("etwa ${snapshot.hasError ? "?" : snapshot.hasData ? filesize(snapshot.data) : "..."}\nLange tippen um zu löschen"),
),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => const CacheView()));
},
onTap: () => AppRoutes.openCacheView(context),
onLongPress: () {
ConfirmDialog(
title: 'App-Cache löschen',
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
class FilesSection extends StatelessWidget {
const FilesSection({super.key});
@override
Widget build(BuildContext context) {
final settings = context.read<SettingsCubit>();
return Column(
children: [
ListTile(
leading: const Icon(Icons.drive_folder_upload_outlined),
title: const Text('Ordner in Dateien nach oben sortieren'),
trailing: Checkbox(
value: settings.val().fileSettings.sortFoldersToTop,
onChanged: (e) => settings.val(write: true).fileSettings.sortFoldersToTop = e!,
),
),
ListTile(
leading: const Icon(Icons.open_in_new_outlined),
title: const Text('Dateien immer mit Systemdialog öffnen'),
trailing: Checkbox(
value: settings.val().fileViewSettings.alwaysOpenExternally,
onChanged: (e) => settings.val(write: true).fileViewSettings.alwaysOpenExternally = e!,
),
),
],
);
}
}
@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../notification/notify_updater.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/centered_leading.dart';
class TalkSection extends StatelessWidget {
const TalkSection({super.key});
@override
Widget build(BuildContext context) {
final settings = context.read<SettingsCubit>();
final talkSettings = settings.val().talkSettings;
final notificationSettings = settings.val().notificationSettings;
return Column(
children: [
ListTile(
leading: const Icon(Icons.star_border),
title: const Text('Favoriten im Talk nach oben sortieren'),
trailing: Checkbox(
value: talkSettings.sortFavoritesToTop,
onChanged: (e) => settings.val(write: true).talkSettings.sortFavoritesToTop = e!,
),
),
ListTile(
leading: const Icon(Icons.mark_email_unread_outlined),
title: const Text('Ungelesene Chats nach oben sortieren'),
trailing: Checkbox(
value: talkSettings.sortUnreadToTop,
onChanged: (e) => settings.val(write: true).talkSettings.sortUnreadToTop = e!,
),
),
ListTile(
leading: const CenteredLeading(Icon(Icons.notifications_active_outlined)),
title: const Text('Push-Benachrichtigungen aktivieren'),
subtitle: const Text('Lange tippen für mehr Informationen'),
trailing: Checkbox(
value: notificationSettings.enabled,
onChanged: (e) {
if (e!) {
NotifyUpdater.enableAfterDisclaimer(settings).asDialog(context);
} else {
settings.val(write: true).notificationSettings.enabled = e;
}
},
),
onLongPress: () => _showInfoDialog(context),
),
],
);
}
void _showInfoDialog(BuildContext context) => showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Info über Push'),
content: const SingleChildScrollView(
child: Text(
"Aufgrund technischer Limitationen müssen Push-Nachrichten über einen externen Server - hier 'mhsl.eu' (Author dieser App) - erfolgen.\n\n"
'Wenn Push aktiviert wird, werden deine Zugangsdaten und ein Token verschlüsselt an den Betreiber gesendet und von ihm unverschlüsselt gespeichert.\n\n'
'Der extene Server verwendet die Zugangsdaten um sich maschinell in Nextcloud Talk anzumelden und via Websockets auf neue Nachrichten zu warten.\n\n'
'Wenn eine neue Nachricht eintrifft wird dein Telefon via FBC-Messaging (Google Firebase Push) vom externen Server benachrichtigt.\n\n'
'Behalte im Hinterkopf, dass deine Zugangsdaten auf einem externen Server gespeichert werden und dies trotz bester Absichten ein Sicherheitsrisiko sein kann!',
),
),
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Zurück')),
],
),
);
}
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../../storage/timetable/timetable_name_mode.dart';
class TimetableSection extends StatelessWidget {
const TimetableSection({super.key});
@override
Widget build(BuildContext context) {
final settings = context.read<SettingsCubit>();
final timetableBloc = context.read<TimetableBloc>();
final timetableSettings = settings.val().timetableSettings;
return Column(
children: [
ListTile(
leading: const Icon(Icons.abc_outlined),
title: const Text('Fachbezeichnung'),
trailing: DropdownButton<TimetableNameMode>(
value: timetableSettings.timetableNameMode,
icon: const Icon(Icons.arrow_drop_down),
items: TimetableNameMode.values
.map((e) => DropdownMenuItem(
value: e,
enabled: e != timetableSettings.timetableNameMode,
child: Row(
children: [
Icon(TimetableNameModes.getDisplayOptions(e).icon),
const SizedBox(width: 10),
Text(TimetableNameModes.getDisplayOptions(e).displayName),
],
),
))
.toList(),
onChanged: (value) {
settings.val(write: true).timetableSettings.timetableNameMode = value!;
timetableBloc.refresh();
},
),
),
ListTile(
leading: const Icon(Icons.calendar_view_day_outlined),
title: const Text('Doppelstunden zusammenhängend anzeigen'),
trailing: Checkbox(
value: timetableSettings.connectDoubleLessons,
onChanged: (e) {
settings.val(write: true).timetableSettings.connectDoubleLessons = e!;
timetableBloc.refresh();
},
),
),
],
);
}
}
+37
View File
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../storage/base/settings.dart' as model;
import 'sections/about_section.dart';
import 'sections/account_section.dart';
import 'sections/appearance_section.dart';
import 'sections/files_section.dart';
import 'sections/talk_section.dart';
import 'sections/timetable_section.dart';
class Settings extends StatelessWidget {
const Settings({super.key});
@override
Widget build(BuildContext context) => BlocBuilder<SettingsCubit, model.Settings>(
builder: (context, _) => Scaffold(
appBar: AppBar(title: const Text('Einstellungen')),
body: ListView(
children: const [
AccountSection(),
Divider(),
AppearanceSection(),
Divider(),
TimetableSection(),
Divider(),
TalkSection(),
Divider(),
FilesSection(),
Divider(),
AboutSection(),
],
),
),
);
}
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import '../../widget/centeredLeading.dart';
import '../../widget/confirmDialog.dart';
import '../../../../widget/centered_leading.dart';
import '../../../../widget/confirm_dialog.dart';
class PrivacyInfo {
String providerText;
-142
View File
@@ -1,142 +0,0 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
import '../../../state/app/modules/chatList/bloc/chat_list_state.dart';
import '../../../notification/notifyUpdater.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../widget/confirmDialog.dart';
import 'components/chatTile.dart';
import 'components/splitViewPlaceholder.dart';
import 'joinChat.dart';
import 'searchChat.dart';
class ChatList extends StatelessWidget {
const ChatList({super.key});
@override
Widget build(BuildContext context) => BlocModule<ChatListBloc, LoadableState<ChatListState>>(
create: (_) => ChatListBloc(),
child: (context, bloc, _) => const _ChatListView(),
);
}
class _ChatListView extends StatefulWidget {
const _ChatListView();
@override
State<_ChatListView> createState() => _ChatListViewState();
}
class _ChatListViewState extends State<_ChatListView> {
late final SettingsCubit _settings;
@override
void initState() {
super.initState();
_settings = context.read<SettingsCubit>();
WidgetsBinding.instance.addPostFrameCallback((_) => _maybeAskForNotificationPermission());
}
void _maybeAskForNotificationPermission() {
final notificationSettings = _settings.val().notificationSettings;
if (notificationSettings.enabled || notificationSettings.askUsageDismissed) return;
_settings.val(write: true).notificationSettings.askUsageDismissed = true;
ConfirmDialog(
icon: Icons.notifications_active_outlined,
title: 'Benachrichtigungen aktivieren',
content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.',
confirmButton: 'Weiter',
onConfirm: () {
FirebaseMessaging.instance.requestPermission(provisional: false).then((value) {
if (!mounted) return;
switch (value.authorizationStatus) {
case AuthorizationStatus.authorized:
NotifyUpdater.enableAfterDisclaimer(_settings).asDialog(context);
break;
case AuthorizationStatus.denied:
showDialog(
context: context,
builder: (_) => const AlertDialog(
content: Text('Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.'),
),
);
break;
default:
break;
}
});
},
).asDialog(context);
}
@override
Widget build(BuildContext context) {
final bloc = context.read<ChatListBloc>();
return SplitView.material(
placeholder: const SplitViewPlaceholder(),
breakpoint: 1000,
child: Scaffold(
appBar: AppBar(
title: const Text('Talk'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
final rooms = bloc.state.data?.rooms;
if (rooms == null) return;
showSearch(context: context, delegate: SearchChat(rooms.data.toList()));
},
),
],
),
floatingActionButton: FloatingActionButton(
heroTag: 'createChat',
backgroundColor: Theme.of(context).primaryColor,
onPressed: () {
showSearch(context: context, delegate: JoinChat()).then((username) {
if (username == null || !context.mounted) return;
ConfirmDialog(
title: 'Chat starten',
content: "Möchtest du einen Chat mit Nutzer '$username' starten?",
confirmButton: 'Chat starten',
onConfirm: () {
bloc.createDirectChat(username);
},
).asDialog(context);
});
},
child: const Icon(Icons.add_comment_outlined),
),
body: LoadableStateConsumer<ChatListBloc, ChatListState>(
child: (state, _) {
final rooms = state.rooms;
if (rooms == null) return const SizedBox.shrink();
final talkSettings = context.watch<SettingsCubit>().val().talkSettings;
final sorted = rooms.sortBy(
lastActivity: true,
favoritesToTop: talkSettings.sortFavoritesToTop,
unreadToTop: talkSettings.sortUnreadToTop,
);
return ListView(
padding: EdgeInsets.zero,
children: sorted.map((room) {
final hasDraft = _settings.val().talkSettings.drafts.containsKey(room.token);
return ChatTile(data: room, hasDraft: hasDraft);
}).toList(),
);
},
),
),
);
}
}
+180
View File
@@ -0,0 +1,180 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import '../../../routing/app_routes.dart';
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
import '../../../state/app/modules/chatList/bloc/chat_list_state.dart';
import '../../../notification/notify_updater.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../widget/confirm_dialog.dart';
import 'widgets/chat_tile.dart';
import 'widgets/split_view_placeholder.dart';
import 'join_chat.dart';
import 'search_chat.dart';
class ChatList extends StatelessWidget {
const ChatList({super.key});
@override
Widget build(BuildContext context) => BlocModule<ChatListBloc, LoadableState<ChatListState>>(
create: (_) => ChatListBloc(),
child: (context, bloc, _) => const _ChatListView(),
);
}
class _ChatListView extends StatefulWidget {
const _ChatListView();
@override
State<_ChatListView> createState() => _ChatListViewState();
}
class _ChatListViewState extends State<_ChatListView> {
late final SettingsCubit _settings;
@override
void initState() {
super.initState();
_settings = context.read<SettingsCubit>();
AppRoutes.pendingChatToken.addListener(_maybeOpenPendingChat);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_maybeAskForNotificationPermission();
_maybeOpenPendingChat();
});
}
@override
void dispose() {
AppRoutes.pendingChatToken.removeListener(_maybeOpenPendingChat);
super.dispose();
}
void _maybeOpenPendingChat() {
if (!mounted) return;
final resolved = AppRoutes.resolvePendingChat(context);
if (resolved == null) return;
AppRoutes.pendingChatToken.value = null;
// Replace any chat already pushed on top of the chat list so a freshly
// tapped notification doesn't stack indefinitely on previous chats.
final navigator = Navigator.of(context);
if (navigator.canPop()) {
navigator.popUntil((route) => route.isFirst);
}
AppRoutes.openChatView(
context,
room: resolved.room,
selfId: resolved.selfId,
avatar: resolved.avatar,
overrideToSingleSubScreen: true,
);
}
void _maybeAskForNotificationPermission() {
final notificationSettings = _settings.val().notificationSettings;
if (notificationSettings.enabled || notificationSettings.askUsageDismissed) return;
_settings.val(write: true).notificationSettings.askUsageDismissed = true;
ConfirmDialog(
icon: Icons.notifications_active_outlined,
title: 'Benachrichtigungen aktivieren',
content: 'Auf wunsch kannst du Push-Benachrichtigungen aktivieren. Deine Einstellungen kannst du jederzeit ändern.',
confirmButton: 'Weiter',
onConfirm: () {
FirebaseMessaging.instance.requestPermission(provisional: false).then((value) {
if (!mounted) return;
switch (value.authorizationStatus) {
case AuthorizationStatus.authorized:
NotifyUpdater.enableAfterDisclaimer(_settings).asDialog(context);
break;
case AuthorizationStatus.denied:
showDialog(
context: context,
builder: (_) => const AlertDialog(
content: Text('Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.'),
),
);
break;
default:
break;
}
});
},
).asDialog(context);
}
@override
Widget build(BuildContext context) {
final bloc = context.read<ChatListBloc>();
return SplitView.material(
placeholder: const SplitViewPlaceholder(),
breakpoint: 1000,
child: BlocListener<ChatListBloc, LoadableState<ChatListState>>(
listener: (_, _) => _maybeOpenPendingChat(),
child: Scaffold(
appBar: AppBar(
title: const Text('Talk'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
final rooms = bloc.state.data?.rooms;
if (rooms == null) return;
showSearch(context: context, delegate: SearchChat(rooms.data.toList()));
},
),
],
),
floatingActionButton: FloatingActionButton(
heroTag: 'createChat',
backgroundColor: Theme.of(context).primaryColor,
onPressed: () {
showSearch(context: context, delegate: JoinChat()).then((username) {
if (username == null || !context.mounted) return;
ConfirmDialog(
title: 'Chat starten',
content: "Möchtest du einen Chat mit Nutzer '$username' starten?",
confirmButton: 'Chat starten',
onConfirm: () {
bloc.createDirectChat(username);
},
).asDialog(context);
});
},
child: const Icon(Icons.add_comment_outlined),
),
body: LoadableStateConsumer<ChatListBloc, ChatListState>(
child: (state, _) {
final rooms = state.rooms;
if (rooms == null) return const SizedBox.shrink();
final talkSettings = context.watch<SettingsCubit>().val().talkSettings;
final sorted = rooms.sortBy(
lastActivity: true,
favoritesToTop: talkSettings.sortFavoritesToTop,
unreadToTop: talkSettings.sortUnreadToTop,
);
return ListView(
padding: EdgeInsets.zero,
children: sorted.map((room) {
final hasDraft = _settings.val().talkSettings.drafts.containsKey(room.token);
return ChatTile(data: room, hasDraft: hasDraft);
}).toList(),
);
},
),
),
),
);
}
}
@@ -3,17 +3,17 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../extensions/dateTime.dart';
import '../../../extensions/date_time.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../state/app/modules/chat/bloc/chat_state.dart';
import '../../../theming/appTheme.dart';
import '../../../widget/clickableAppBar.dart';
import '../../../widget/userAvatar.dart';
import 'chatDetails/chatInfo.dart';
import 'components/chatBubble.dart';
import 'components/chatTextfield.dart';
import 'talkNavigator.dart';
import '../../../theming/app_theme.dart';
import '../../../widget/clickable_app_bar.dart';
import '../../../widget/user_avatar.dart';
import 'details/chat_info.dart';
import 'widgets/chat_bubble.dart';
import 'widgets/chat_textfield.dart';
import 'talk_navigator.dart';
class ChatView extends StatefulWidget {
final GetRoomResponseObject room;
@@ -1,7 +1,7 @@
import 'package:bubble/bubble.dart';
import 'package:flutter/material.dart';
import '../../../../theming/appTheme.dart';
import '../../../../theming/app_theme.dart';
extension ColorExtensions on Color {
Color invert() {
@@ -5,9 +5,9 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
import '../../../../model/accountData.dart';
import '../../../../model/endpointData.dart';
import '../../../../utils/UrlOpener.dart';
import '../../../../model/account_data.dart';
import '../../../../model/endpoint_data.dart';
import '../../../../utils/url_opener.dart';
class ChatMessage {
String originalMessage;
@@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsCache.dart';
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart';
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../widget/largeProfilePictureView.dart';
import '../../../../widget/loadingSpinner.dart';
import '../../../../widget/userAvatar.dart';
import '../talkNavigator.dart';
import 'participants/participantsListView.dart';
import '../../../../widget/large_profile_picture_view.dart';
import '../../../../widget/loading_spinner.dart';
import '../../../../widget/user_avatar.dart';
import '../talk_navigator.dart';
import 'participants_list_view.dart';
class ChatInfo extends StatefulWidget {
final GetRoomResponseObject room;
@@ -1,14 +1,14 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../../api/marianumcloud/talk/getReactions/getReactions.dart';
import '../../../api/marianumcloud/talk/getReactions/getReactionsResponse.dart';
import '../../../model/accountData.dart';
import '../../../widget/centeredLeading.dart';
import '../../../widget/loadingSpinner.dart';
import '../../../widget/placeholderView.dart';
import '../../../widget/unimplementedDialog.dart';
import '../../../widget/userAvatar.dart';
import '../../../../api/marianumcloud/talk/getReactions/getReactions.dart';
import '../../../../api/marianumcloud/talk/getReactions/getReactionsResponse.dart';
import '../../../../model/account_data.dart';
import '../../../../widget/centered_leading.dart';
import '../../../../widget/loading_spinner.dart';
import '../../../../widget/placeholder_view.dart';
import '../../../../widget/unimplemented_dialog.dart';
import '../../../../widget/user_avatar.dart';
class MessageReactions extends StatefulWidget {
final String token;
@@ -1,8 +1,8 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import '../../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart';
import '../../../../../widget/userAvatar.dart';
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart';
import '../../../../widget/user_avatar.dart';
class ParticipantsListView extends StatelessWidget {
final GetParticipantsResponse participantsResponse;
@@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
import '../../../api/marianumcloud/autocomplete/autocompleteApi.dart';
import '../../../api/marianumcloud/autocomplete/autocompleteResponse.dart';
import '../../../model/endpointData.dart';
import '../../../widget/placeholderView.dart';
import '../../../model/endpoint_data.dart';
import '../../../widget/placeholder_view.dart';
class JoinChat extends SearchDelegate<String> {
CancelableOperation<AutocompleteResponse>? future;
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import 'components/chatTile.dart';
import 'widgets/chat_tile.dart';
class SearchChat extends SearchDelegate {
List<GetRoomResponseObject> chats;
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
import 'chatBubbleStyles.dart';
import '../data/chat_bubble_styles.dart';
class AnswerReference extends StatelessWidget {
final BuildContext context;
@@ -1,31 +1,26 @@
import 'package:bubble/bubble.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis;
import 'package:flowder/flowder.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:jiffy/jiffy.dart';
import 'package:open_filex/open_filex.dart';
import '../../../../api/marianumcloud/talk/getPoll/getPollState.dart';
import '../../../../extensions/text.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/deleteMessage/deleteMessage.dart';
import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessage.dart';
import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessageParams.dart';
import '../../../../api/marianumcloud/talk/getPoll/getPollState.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart';
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../extensions/text.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../widget/debug/debugTile.dart';
import '../../../../widget/loadingSpinner.dart';
import '../../files/fileElement.dart';
import 'answerReference.dart';
import 'chatBubbleStyles.dart';
import 'chatMessage.dart';
import '../messageReactions.dart';
import 'pollOptionsList.dart';
import '../../../../widget/loading_spinner.dart';
import '../../files/widgets/file_element.dart';
import '../data/chat_bubble_styles.dart';
import '../data/chat_message.dart';
import 'answer_reference.dart';
import 'chat_message_options_dialog.dart';
import 'poll_options_list.dart';
class ChatBubble extends StatefulWidget {
final BuildContext context;
@@ -77,176 +72,13 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
}
void showOptionsDialog() {
showDialog(context: context, builder: (context) {
var commonReactions = <String>['👍', '👎', '😆', '❤️', '👀'];
var canReact = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
return SimpleDialog(
children: [
Visibility(
visible: canReact,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
alignment: WrapAlignment.center,
children: [
...commonReactions.map((e) => TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40)
),
onPressed: () {
Navigator.of(context).pop();
ReactMessage(
chatToken: widget.chatData.token,
messageId: widget.bubbleData.id,
params: ReactMessageParams(e),
).run().then((value) => widget.refetch(renew: true));
},
child: Text(e),
),
),
IconButton(
onPressed: () {
showDialog(context: context, builder: (context) => AlertDialog(
contentPadding: const EdgeInsets.all(15),
titlePadding: const EdgeInsets.only(left: 6, top: 15),
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 10),
const Text('Reagieren'),
],
),
content: SizedBox(
width: 256,
height: 270,
child: Column(
children: [
emojis.EmojiPicker(
config: emojis.Config(
height: 256,
// swapCategoryAndBottomBar: true, // TODO this property is no longer supported, need to find an replacement
emojiViewConfig: emojis.EmojiViewConfig(
backgroundColor: Theme.of(context).canvasColor,
recentsLimit: 67,
emojiSizeMax: 25,
noRecents: const Text('Keine zuletzt verwendeten Emojis'),
columns: 7,
),
bottomActionBarConfig: const emojis.BottomActionBarConfig(
enabled: false,
),
categoryViewConfig: emojis.CategoryViewConfig(
backgroundColor: Theme.of(context).hoverColor,
iconColorSelected: Theme.of(context).primaryColor,
indicatorColor: Theme.of(context).primaryColor,
),
searchViewConfig: emojis.SearchViewConfig(
backgroundColor: Theme.of(context).dividerColor,
// buttonColor: Theme.of(context).dividerColor, // TODO property no longer supported
hintText: 'Suchen',
buttonIconColor: Colors.white,
),
),
onEmojiSelected: (emojis.Category? category, emojis.Emoji emoji) {
Navigator.of(context).pop();
Navigator.of(context).pop();
ReactMessage(
chatToken: widget.chatData.token,
messageId: widget.bubbleData.id,
params: ReactMessageParams(emoji.emoji),
).run().then((value) => widget.refetch(renew: true));
},
),
],
),
),
));
},
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
icon: const Icon(Icons.add_circle_outline_outlined),
),
],
),
const Divider(),
],
),
),
Visibility(
visible: widget.bubbleData.isReplyable,
child: ListTile(
leading: const Icon(Icons.reply_outlined),
title: const Text('Antworten'),
onTap: () {
context.read<ChatBloc>().setReferenceMessageId(widget.bubbleData.id);
Navigator.of(context).pop();
},
),
),
Visibility(
visible: canReact,
child: ListTile(
leading: const Icon(Icons.emoji_emotions_outlined),
title: const Text('Reaktionen'),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => MessageReactions(
token: widget.chatData.token,
messageId: widget.bubbleData.id,
)));
},
),
),
Visibility(
visible: widget.bubbleData.message != '{file}',
child: ListTile(
leading: const Icon(Icons.copy),
title: const Text('Nachricht kopieren'),
onTap: () => {
Clipboard.setData(ClipboardData(text: widget.bubbleData.message)),
Navigator.of(context).pop(),
},
),
),
Visibility(
visible: !kReleaseMode && !widget.isSender && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne,
child: ListTile(
leading: const Icon(Icons.sms_outlined),
title: Text("Private Nachricht an '${widget.bubbleData.actorDisplayName}'"),
onTap: () => {
Navigator.of(context).pop()
},
),
),
Visibility(
visible: widget.isSender && DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).add(const Duration(hours: 6)).isAfter(DateTime.now()),
child: ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('Nachricht löschen'),
onTap: () {
DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) {
if (!context.mounted) return;
context.read<ChatBloc>().refresh();
Navigator.of(context).pop();
});
},
),
),
DebugTile(context).jsonData(widget.bubbleData.toJson()),
],
);
});
showChatMessageOptionsDialog(
context,
chatData: widget.chatData,
bubbleData: widget.bubbleData,
isSender: widget.isSender,
onRefetch: widget.refetch,
);
}
@@ -0,0 +1,202 @@
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/deleteMessage/deleteMessage.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart';
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../routing/app_routes.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../widget/debug/debug_tile.dart';
const _commonReactions = <String>['👍', '👎', '😆', '❤️', '👀'];
/// Long-press / double-tap options dialog for a single chat message bubble.
/// The hosting [ChatBubble] keeps responsibility for rendering the bubble;
/// this file owns the modal interactions (react, reply, copy, delete, ...).
Future<void> showChatMessageOptionsDialog(
BuildContext context, {
required GetRoomResponseObject chatData,
required GetChatResponseObject bubbleData,
required bool isSender,
required void Function({bool renew}) onRefetch,
}) {
final parentContext = context;
final canReact = bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
final canDelete = isSender &&
DateTime.fromMillisecondsSinceEpoch(bubbleData.timestamp * 1000)
.add(const Duration(hours: 6))
.isAfter(DateTime.now());
return showDialog(
context: context,
builder: (dialogCtx) => SimpleDialog(
children: [
if (canReact)
_ReactionsRow(
chatToken: chatData.token,
messageId: bubbleData.id,
onRefetch: onRefetch,
dialogContext: dialogCtx,
),
if (bubbleData.isReplyable)
ListTile(
leading: const Icon(Icons.reply_outlined),
title: const Text('Antworten'),
onTap: () {
dialogCtx.read<ChatBloc>().setReferenceMessageId(bubbleData.id);
Navigator.of(dialogCtx).pop();
},
),
if (canReact)
ListTile(
leading: const Icon(Icons.emoji_emotions_outlined),
title: const Text('Reaktionen'),
onTap: () {
Navigator.of(dialogCtx).pop();
if (!parentContext.mounted) return;
AppRoutes.openMessageReactions(parentContext, chatData.token, bubbleData.id);
},
),
if (bubbleData.message != '{file}')
ListTile(
leading: const Icon(Icons.copy),
title: const Text('Nachricht kopieren'),
onTap: () {
Clipboard.setData(ClipboardData(text: bubbleData.message));
Navigator.of(dialogCtx).pop();
},
),
if (!kReleaseMode && !isSender && chatData.type != GetRoomResponseObjectConversationType.oneToOne)
ListTile(
leading: const Icon(Icons.sms_outlined),
title: Text("Private Nachricht an '${bubbleData.actorDisplayName}'"),
onTap: () => Navigator.of(dialogCtx).pop(),
),
if (canDelete)
ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('Nachricht löschen'),
onTap: () async {
await DeleteMessage(chatData.token, bubbleData.id).run();
if (!dialogCtx.mounted) return;
dialogCtx.read<ChatBloc>().refresh();
Navigator.of(dialogCtx).pop();
},
),
DebugTile(dialogCtx).jsonData(bubbleData.toJson()),
],
),
);
}
class _ReactionsRow extends StatelessWidget {
final String chatToken;
final int messageId;
final void Function({bool renew}) onRefetch;
final BuildContext dialogContext;
const _ReactionsRow({
required this.chatToken,
required this.messageId,
required this.onRefetch,
required this.dialogContext,
});
void _react(String emoji) {
Navigator.of(dialogContext).pop();
ReactMessage(
chatToken: chatToken,
messageId: messageId,
params: ReactMessageParams(emoji),
).run().then((_) => onRefetch(renew: true));
}
@override
Widget build(BuildContext context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
alignment: WrapAlignment.center,
children: [
..._commonReactions.map(
(emoji) => TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
onPressed: () => _react(emoji),
child: Text(emoji),
),
),
IconButton(
onPressed: () => _showEmojiPicker(context),
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
icon: const Icon(Icons.add_circle_outline_outlined),
),
],
),
const Divider(),
],
);
void _showEmojiPicker(BuildContext rowContext) {
showDialog(
context: rowContext,
builder: (pickerCtx) => AlertDialog(
contentPadding: const EdgeInsets.all(15),
titlePadding: const EdgeInsets.only(left: 6, top: 15),
title: Row(
children: [
IconButton(
onPressed: () => Navigator.of(pickerCtx).pop(),
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 10),
const Text('Reagieren'),
],
),
content: SizedBox(
width: 256,
height: 270,
child: emojis.EmojiPicker(
config: emojis.Config(
height: 256,
emojiViewConfig: emojis.EmojiViewConfig(
backgroundColor: Theme.of(pickerCtx).canvasColor,
recentsLimit: 67,
emojiSizeMax: 25,
noRecents: const Text('Keine zuletzt verwendeten Emojis'),
columns: 7,
),
bottomActionBarConfig: const emojis.BottomActionBarConfig(enabled: false),
categoryViewConfig: emojis.CategoryViewConfig(
backgroundColor: Theme.of(pickerCtx).hoverColor,
iconColorSelected: Theme.of(pickerCtx).primaryColor,
indicatorColor: Theme.of(pickerCtx).primaryColor,
),
searchViewConfig: emojis.SearchViewConfig(
backgroundColor: Theme.of(pickerCtx).dividerColor,
hintText: 'Suchen',
buttonIconColor: Colors.white,
),
),
onEmojiSelected: (_, emoji) {
Navigator.of(pickerCtx).pop();
_react(emoji.emoji);
},
),
),
),
);
}
}
@@ -12,10 +12,10 @@ import '../../../../api/marianumcloud/talk/sendMessage/sendMessageParams.dart';
import '../../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/filePick.dart';
import '../../../../widget/focusBehaviour.dart';
import '../../files/filesUploadDialog.dart';
import 'answerReference.dart';
import '../../../../widget/file_pick.dart';
import '../../../../widget/focus_behaviour.dart';
import '../../files/files_upload_dialog.dart';
import 'answer_reference.dart';
class ChatTextfield extends StatefulWidget {
final String sendToToken;
@@ -9,14 +9,14 @@ import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../api/marianumcloud/talk/setFavorite/setFavorite.dart';
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarker.dart';
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarkerParams.dart';
import '../../../../model/accountData.dart';
import '../../../../model/account_data.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
import '../../../../widget/confirmDialog.dart';
import '../../../../widget/debug/debugTile.dart';
import '../../../../widget/userAvatar.dart';
import '../chatView.dart';
import '../talkNavigator.dart';
import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/debug/debug_tile.dart';
import '../../../../widget/user_avatar.dart';
import '../chat_view.dart';
import '../talk_navigator.dart';
class ChatTile extends StatefulWidget {
final GetRoomResponseObject data;
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import '../../../../api/marianumcloud/talk/getPoll/getPollStateResponse.dart';
import '../../../../utils/UrlOpener.dart';
import '../../../../utils/url_opener.dart';
class PollOptionsList extends StatefulWidget {
final GetPollStateResponseObject pollData;
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import '../../../../theming/appTheme.dart';
import '../../../../theming/app_theme.dart';
class SplitViewPlaceholder extends StatelessWidget {
const SplitViewPlaceholder({super.key});
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import '../../../../theming/darkAppTheme.dart';
import '../../../../theming/dark_app_theme.dart';
enum CustomTimetableColors { orange, red, green, blue }
@@ -7,10 +7,10 @@ import 'package:rrule_generator/rrule_generator.dart';
import 'package:time_range_picker/time_range_picker.dart';
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
import '../../../../extensions/dateTime.dart';
import '../../../../extensions/date_time.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../../widget/focusBehaviour.dart';
import '../../../../widget/infoDialog.dart';
import '../../../../widget/focus_behaviour.dart';
import '../../../../widget/info_dialog.dart';
import 'custom_event_colors.dart';
class CustomEventEditDialog extends StatefulWidget {
@@ -4,8 +4,8 @@ import 'package:jiffy/jiffy.dart';
import '../../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_state.dart';
import '../../../../widget/centeredLeading.dart';
import '../../../../widget/placeholderView.dart';
import '../../../../widget/centered_leading.dart';
import '../../../../widget/placeholder_view.dart';
import '../details/delete_custom_event.dart';
import 'custom_event_edit_dialog.dart';
@@ -3,8 +3,8 @@ import 'package:jiffy/jiffy.dart';
import 'package:rrule/rrule.dart';
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
import '../../../../widget/centeredLeading.dart';
import '../../../../widget/debug/debugTile.dart';
import '../../../../widget/centered_leading.dart';
import '../../../../widget/debug/debug_tile.dart';
import '../custom_events/custom_event_edit_dialog.dart';
import '_bottom_sheet.dart';
import 'delete_custom_event.dart';
@@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../../widget/confirmDialog.dart';
import '../../../../widget/confirm_dialog.dart';
Completer<void> showDeleteCustomEventDialog(BuildContext context, CustomTimetableEvent event) {
final completer = Completer<void>();
@@ -1,17 +1,16 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
import '../../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
import '../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
import '../../../../routing/app_routes.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_state.dart';
import '../../../../widget/debug/debugTile.dart';
import '../../../../widget/unimplementedDialog.dart';
import '../../more/roomplan/roomplan.dart';
import '../../../../widget/debug/debug_tile.dart';
import '../../../../widget/unimplemented_dialog.dart';
import '_bottom_sheet.dart';
class WebuntisLessonSheet {
@@ -54,7 +53,7 @@ class WebuntisLessonSheet {
title: Text('Raum: ${room.name} (${room.longName})'),
trailing: IconButton(
icon: const Icon(Icons.house_outlined),
onPressed: () => pushScreen(context, withNavBar: false, screen: const Roomplan()),
onPressed: () => AppRoutes.openRoomplan(context),
),
),
ListTile(
+3 -3
View File
@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../extensions/dateTime.dart';
import '../../../extensions/date_time.dart';
import '../../../routing/app_routes.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../../state/app/modules/timetable/bloc/timetable_state.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import 'custom_events/custom_event_edit_dialog.dart';
import 'custom_events/custom_events_view.dart';
import 'data/arbitrary_appointment.dart';
import 'data/lesson_period_schedule.dart';
import 'data/timetable_appointment_factory.dart';
@@ -46,7 +46,7 @@ class _TimetableState extends State<Timetable> {
barrierDismissible: false,
);
case _CalendarAction.viewEvents:
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const CustomEventsView()));
AppRoutes.openCustomEvents(context);
}
}
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart';
import '../../../../extensions/dateTime.dart';
import '../../../../extensions/date_time.dart';
import '../data/calendar_layout.dart';
import '../data/lesson_period_schedule.dart';
import '../data/webuntis_time.dart';
-318
View File
@@ -1,318 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../model/accountData.dart';
import '../../notification/notifyUpdater.dart';
import '../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../state/app/modules/timetable/bloc/timetable_bloc.dart';
import '../../storage/base/settings.dart' as model;
import '../../theming/appTheme.dart';
import '../../widget/centeredLeading.dart';
import '../../widget/confirmDialog.dart';
import '../../widget/debug/cacheView.dart';
import '../../storage/timetable/timetable_name_mode.dart';
import 'defaultSettings.dart';
import 'devToolsSettings.dart';
import 'privacyInfo.dart';
class Settings extends StatefulWidget {
const Settings({super.key});
@override
State<Settings> createState() => _SettingsState();
}
class _SettingsState extends State<Settings> {
@override
void initState() {
super.initState();
}
bool developerMode = false;
@override
Widget build(BuildContext context) => BlocBuilder<SettingsCubit, model.Settings>(builder: (context, _) {
final settings = context.read<SettingsCubit>();
return Scaffold(
appBar: AppBar(
title: const Text('Einstellungen'),
),
body: ListView(
children: [
ListTile(
leading: const CenteredLeading(Icon(Icons.logout_outlined)),
title: const Text('Konto abmelden'),
subtitle: Text('Angemeldet als ${AccountData().getUsername()}'),
onTap: () {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
title: 'Abmelden?',
content: 'Möchtest du dich wirklich abmelden?',
confirmButton: 'Abmelden',
onConfirm: () {
SharedPreferences.getInstance().then((value) => {
value.clear(),
}).then((value) async {
PaintingBinding.instance.imageCache.clear();
if (!context.mounted) return;
context.read<SettingsCubit>().reset();
const CacheView().clear();
AccountData().removeData(context: context);
Navigator.popUntil(context, (route) => !Navigator.canPop(context));
});
},
),
);
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.dark_mode_outlined),
title: const Text('Farbgebung'),
trailing: DropdownButton<ThemeMode>(
value: settings.val().appTheme,
icon: const Icon(Icons.arrow_drop_down),
items: ThemeMode.values.map((e) => DropdownMenuItem<ThemeMode>(
value: e,
enabled: e != settings.val().appTheme,
child: Row(
children: [
Icon(AppTheme.getDisplayOptions(e).icon),
const SizedBox(width: 10),
Text(AppTheme.getDisplayOptions(e).displayName),
],
),
)).toList(),
onChanged: (e) {
settings.val(write: true).appTheme = e!;
},
),
),
const Divider(),
ListTile(
leading: const Icon(Icons.abc_outlined),
title: const Text('Fachbezeichnung'),
trailing: DropdownButton<TimetableNameMode>(
value: settings.val().timetableSettings.timetableNameMode,
icon: Icon(Icons.arrow_drop_down),
items: TimetableNameMode.values.map((e) => DropdownMenuItem(
value: e,
enabled: e != settings.val().timetableSettings.timetableNameMode,
child: Row(
children: [
Icon(TimetableNameModes.getDisplayOptions(e).icon),
const SizedBox(width: 10),
Text(TimetableNameModes.getDisplayOptions(e).displayName),
],
),
)).toList(),
onChanged: (value) {
settings.val(write: true).timetableSettings.timetableNameMode = value!;
context.read<TimetableBloc>().refresh();
},
)
),
ListTile(
leading: const Icon(Icons.calendar_view_day_outlined),
title: const Text('Doppelstunden zusammenhängend anzeigen'),
trailing: Checkbox(
value: settings.val().timetableSettings.connectDoubleLessons,
onChanged: (e) {
settings.val(write: true).timetableSettings.connectDoubleLessons = e!;
context.read<TimetableBloc>().refresh();
},
),
),
const Divider(),
ListTile(
leading: const Icon(Icons.star_border),
title: const Text('Favoriten im Talk nach oben sortieren'),
trailing: Checkbox(
value: settings.val().talkSettings.sortFavoritesToTop,
onChanged: (e) {
settings.val(write: true).talkSettings.sortFavoritesToTop = e!;
},
),
),
ListTile(
leading: const Icon(Icons.mark_email_unread_outlined),
title: const Text('Ungelesene Chats nach oben sortieren'),
trailing: Checkbox(
value: settings.val().talkSettings.sortUnreadToTop,
onChanged: (e) {
settings.val(write: true).talkSettings.sortUnreadToTop = e!;
},
),
),
ListTile(
leading: const CenteredLeading(Icon(Icons.notifications_active_outlined)),
title: const Text('Push-Benachrichtigungen aktivieren'),
subtitle: const Text('Lange tippen für mehr Informationen'),
trailing: Checkbox(
value: settings.val().notificationSettings.enabled,
onChanged: (e) {
if(e!) {
NotifyUpdater.enableAfterDisclaimer(settings).asDialog(context);
} else {
settings.val(write: true).notificationSettings.enabled = e;
}
},
),
onLongPress: () => showDialog(context: context, builder: (context) => AlertDialog(
title: const Text('Info über Push'),
content: const SingleChildScrollView(child: Text(''
"Aufgrund technischer Limitationen müssen Push-Nachrichten über einen externen Server - hier 'mhsl.eu' (Author dieser App) - erfolgen.\n\n"
'Wenn Push aktiviert wird, werden deine Zugangsdaten und ein Token verschlüsselt an den Betreiber gesendet und von ihm unverschlüsselt gespeichert.\n\n'
'Der extene Server verwendet die Zugangsdaten um sich maschinell in Nextcloud Talk anzumelden und via Websockets auf neue Nachrichten zu warten.\n\n'
'Wenn eine neue Nachricht eintrifft wird dein Telefon via FBC-Messaging (Google Firebase Push) vom externen Server benachrichtigt.\n\n'
'Behalte im Hinterkopf, dass deine Zugangsdaten auf einem externen Server gespeichert werden und dies trotz bester Absichten ein Sicherheitsrisiko sein kann!'
)),
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Zurück'))
],
)),
),
const Divider(),
ListTile(
leading: const Icon(Icons.drive_folder_upload_outlined),
title: const Text('Ordner in Dateien nach oben sortieren'),
trailing: Checkbox(
value: settings.val().fileSettings.sortFoldersToTop,
onChanged: (e) {
settings.val(write: true).fileSettings.sortFoldersToTop = e!;
},
),
),
ListTile(
leading: const Icon(Icons.open_in_new_outlined),
title: const Text('Dateien immer mit Systemdialog öffnen'),
trailing: Checkbox(
value: settings.val().fileViewSettings.alwaysOpenExternally,
onChanged: (e) {
settings.val(write: true).fileViewSettings.alwaysOpenExternally = e!;
},
),
),
const Divider(),
ListTile(
leading: const Icon(Icons.live_help_outlined),
title: const Text('Informationen und Lizenzen'),
onTap: () {
PackageInfo.fromPlatform().then((appInfo) {
if (!context.mounted) return;
showAboutDialog(
context: context,
applicationIcon: const Icon(Icons.apps),
applicationName: 'MarianumMobile',
applicationVersion: '${appInfo.appName}\n\nPackage: ${appInfo.packageName}\nVersion: ${appInfo.version}\nBuild: ${appInfo.buildNumber}',
applicationLegalese: 'Dies ist ein Inoffizieller Nextcloud & Webuntis Client und wird nicht vom Marianum selbst betrieben.\n'
'Keinerlei Gewähr für Vollständigkeit, Richtigkeit und Aktualität!\n\n'
"${kReleaseMode ? "Production" : "Development"} build\n"
'Marianum Fulda 2023-${Jiffy.now().year}\nElias Müller',
);
});
},
trailing: const Icon(Icons.arrow_right),
),
ListTile(
leading: const Icon(Icons.policy_outlined),
title: const Text('Impressum & Datenschutz'),
onTap: () {
showDialog(context: context, builder: (context) => SimpleDialog(
children: [
ListTile(
leading: const CenteredLeading(Icon(Icons.school_outlined)),
title: const Text('Infos zum Marianum Fulda'),
subtitle: const Text('Für Talk-Chats und Dateien'),
trailing: const Icon(Icons.arrow_right),
onTap: () => PrivacyInfo(providerText: 'Marianum', imprintUrl: 'https://www.marianum-fulda.de/impressum', privacyUrl: 'https://www.marianum-fulda.de/datenschutz').showPopup(context)
),
ListTile(
leading: const CenteredLeading(Icon(Icons.date_range_outlined)),
title: const Text('Infos zu Web-/ Untis'),
subtitle: const Text('Für den Stundenplan'),
trailing: const Icon(Icons.arrow_right),
onTap: () => PrivacyInfo(providerText: 'Untis', imprintUrl: 'https://www.untis.at/impressum', privacyUrl: 'https://www.untis.at/datenschutz-wu-apps').showPopup(context)
),
ListTile(
leading: const CenteredLeading(Icon(Icons.send_time_extension_outlined)),
title: const Text('Infos zu mhsl'),
subtitle: const Text('Für Countdowns, Marianum Message und mehr'),
trailing: const Icon(Icons.arrow_right),
onTap: () => PrivacyInfo(providerText: 'mhsl', imprintUrl: 'https://mhsl.eu/id.html', privacyUrl: 'https://mhsl.eu/datenschutz.html').showPopup(context),
),
],
));
},
trailing: const Icon(Icons.arrow_right),
),
const Divider(),
ListTile(
leading: const CenteredLeading(Icon(Icons.code)),
title: const Text('Quellcode MarianumMobile/Client'),
subtitle: const Text('GNU GPL v3'),
onTap: () => ConfirmDialog.openBrowser(context, 'https://mhsl.eu/gitea/MarianumMobile/Client'),
),
ListTile(
leading: const Icon(Icons.developer_mode_outlined),
title: const Text('Entwicklermodus'),
trailing: Checkbox(
value: settings.val().devToolsEnabled,
onChanged: (state) {
changeView() {
var enabled = state ?? false;
settings.val(write: true).devToolsEnabled = enabled;
if(!enabled) settings.val(write: true).devToolsSettings = DefaultSettings.get().devToolsSettings;
}
if(!state!) {
changeView();
return;
}
ConfirmDialog(
title: 'Entwicklermodus',
content: ''
'Die Entwickleransicht bietet erweiterte Funktionen, die für den üblichen Gebrauch nicht benötigt werden.\n\nDie Verwendung der Tools kann darüber hinaus bei falscher Verwendung zu Fehlern führen.\n\n'
'Aktivieren auf eigene Verantwortung.',
confirmButton: 'Ja, ich verstehe das Risiko',
cancelButton: 'Nein, zurück zur App',
onConfirm: changeView,
).asDialog(context);
},
),
),
Visibility(
visible: settings.val().devToolsEnabled,
child: DevToolsSettings(settings: settings),
),
],
),
);
});
}