folder restructuring
This commit is contained in:
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
+11
-18
@@ -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()
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
+4
-4
@@ -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
-1
@@ -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});
|
||||
+5
-5
@@ -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',
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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/timetable_name_mode.dart';
|
||||
import '../../../../storage/timetable/timetableSettings.dart';
|
||||
import '../../files/files.dart';
|
||||
|
||||
class DefaultSettings {
|
||||
static Settings get() => Settings(
|
||||
appTheme: ThemeMode.system,
|
||||
devToolsEnabled: false,
|
||||
modulesSettings: ModulesSettings(
|
||||
moduleOrder: [
|
||||
Modules.timetable,
|
||||
Modules.talk,
|
||||
Modules.files,
|
||||
Modules.marianumMessage,
|
||||
Modules.roomPlan,
|
||||
Modules.gradeAveragesCalculator,
|
||||
Modules.holidays,
|
||||
Modules.marianumDates,
|
||||
],
|
||||
hiddenModules: [],
|
||||
),
|
||||
timetableSettings: TimetableSettings(
|
||||
connectDoubleLessons: true,
|
||||
timetableNameMode: TimetableNameMode.name
|
||||
),
|
||||
talkSettings: TalkSettings(
|
||||
sortFavoritesToTop: true,
|
||||
sortUnreadToTop: false,
|
||||
drafts: {},
|
||||
draftReplies: {},
|
||||
),
|
||||
fileSettings: FileSettings(
|
||||
sortFoldersToTop: true,
|
||||
ascending: true,
|
||||
sortBy: SortOption.name
|
||||
),
|
||||
holidaysSettings: HolidaysSettings(
|
||||
dismissedDisclaimer: false,
|
||||
showPastEvents: false,
|
||||
),
|
||||
fileViewSettings: FileViewSettings(
|
||||
alwaysOpenExternally: Platform.isIOS,
|
||||
),
|
||||
notificationSettings: NotificationSettings(
|
||||
askUsageDismissed: false,
|
||||
enabled: false,
|
||||
),
|
||||
devToolsSettings: DevToolsSettings(
|
||||
checkerboardOffscreenLayers: false,
|
||||
checkerboardRasterCacheImages: false,
|
||||
showPerformanceOverlay: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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!,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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 DevToolsSection extends StatefulWidget {
|
||||
final SettingsCubit settings;
|
||||
const DevToolsSection({required this.settings, super.key});
|
||||
|
||||
@override
|
||||
State<DevToolsSection> createState() => _DevToolsSectionState();
|
||||
}
|
||||
|
||||
class _DevToolsSectionState extends State<DevToolsSection> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.speed_outlined)),
|
||||
title: const Text('Performance overlays'),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () {
|
||||
showDialog(context: context, builder: (context) => SimpleDialog(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.auto_graph_outlined),
|
||||
title: const Text('Performance graph'),
|
||||
trailing: Checkbox(
|
||||
value: widget.settings.val().devToolsSettings.showPerformanceOverlay,
|
||||
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.showPerformanceOverlay = e!,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.screen_search_desktop_outlined),
|
||||
title: const Text('Indicate offscreen layers'),
|
||||
trailing: Checkbox(
|
||||
value: widget.settings.val().devToolsSettings.checkerboardOffscreenLayers,
|
||||
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.checkerboardOffscreenLayers = e!,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.imagesearch_roller_outlined),
|
||||
title: const Text('Indicate raster cache images'),
|
||||
trailing: Checkbox(
|
||||
value: widget.settings.val().devToolsSettings.checkerboardRasterCacheImages,
|
||||
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.checkerboardRasterCacheImages = e!,
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.image_outlined)),
|
||||
title: const Text('Thumb-storage'),
|
||||
subtitle: Text('etwa ${filesize(PaintingBinding.instance.imageCache.currentSizeBytes)}\nLange tippen um zu löschen'),
|
||||
onLongPress: () {
|
||||
ConfirmDialog(
|
||||
title: 'Thumbs cache löschen',
|
||||
content: 'Alle zwischengespeicherten Bilder werden gelöscht.',
|
||||
confirmButton: 'Unwiederruflich löschen',
|
||||
onConfirm: () => PaintingBinding.instance.imageCache.clear(),
|
||||
).asDialog(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.settings_applications_outlined)),
|
||||
title: const Text('Settings-storage JSON dump'),
|
||||
subtitle: Text('etwa ${filesize(widget.settings.val().toJson().toString().length * 8)}\nLange tippen um zu löschen'),
|
||||
onTap: () {
|
||||
JsonViewer.asDialog(context, widget.settings.val().toJson());
|
||||
},
|
||||
onLongPress: () {
|
||||
ConfirmDialog(
|
||||
title: 'Einstellungen löschen',
|
||||
content: 'Alle Einstellungen gehen verloren! Accountdaten sowie App-Daten sind nicht betroffen.',
|
||||
confirmButton: 'Unwiederruflich Löschen',
|
||||
onConfirm: () {
|
||||
context.read<SettingsCubit>().reset();
|
||||
},
|
||||
).asDialog(context);
|
||||
},
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.data_object)),
|
||||
title: const Text('Cache-storage JSON dump'),
|
||||
subtitle: FutureBuilder(
|
||||
future: const CacheView().totalSize(),
|
||||
builder: (context, snapshot) => Text("etwa ${snapshot.hasError ? "?" : snapshot.hasData ? filesize(snapshot.data) : "..."}\nLange tippen um zu löschen"),
|
||||
),
|
||||
onTap: () => AppRoutes.openCacheView(context),
|
||||
onLongPress: () {
|
||||
ConfirmDialog(
|
||||
title: 'App-Cache löschen',
|
||||
content: 'Alle cache Einträge werden gelöscht. Der Cache wird bei Nutzung der App automatisch erneut aufgebaut',
|
||||
confirmButton: 'Unwiederruflich löschen',
|
||||
onConfirm: () => const CacheView().clear().then((value) => setState((){})),
|
||||
).asDialog(context);
|
||||
},
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.data_object)),
|
||||
title: const Text('BLOC-storage state cache'),
|
||||
subtitle: const Text('Lange tippen um zu löschen'),
|
||||
onTap: () {
|
||||
// Navigator.push(context, MaterialPageRoute(builder: (context) => const CacheView()));
|
||||
},
|
||||
onLongPress: () {
|
||||
ConfirmDialog(
|
||||
title: 'BLOC-Cache löschen',
|
||||
content: 'Alle cache Einträge werden gelöscht. Der Cache wird bei Nutzung der App automatisch erneut aufgebaut',
|
||||
confirmButton: 'Unwiederruflich löschen',
|
||||
onConfirm: () => HydratedBloc.storage.clear(),
|
||||
).asDialog(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
|
||||
class PrivacyInfo {
|
||||
String providerText;
|
||||
String privacyUrl;
|
||||
String imprintUrl;
|
||||
|
||||
PrivacyInfo({required this.providerText, required this.imprintUrl, required this.privacyUrl});
|
||||
|
||||
void showPopup(BuildContext context) {
|
||||
showDialog(context: context, builder: (context) => SimpleDialog(
|
||||
title: Text('Betreiberinformation | $providerText'),
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.person_pin_outlined)),
|
||||
title: const Text('Impressum'),
|
||||
subtitle: Text(imprintUrl),
|
||||
onTap: () => ConfirmDialog.openBrowser(context, imprintUrl),
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.privacy_tip_outlined)),
|
||||
title: const Text('Datenschutzerklärung'),
|
||||
subtitle: Text(privacyUrl),
|
||||
onTap: () => ConfirmDialog.openBrowser(context, privacyUrl),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
-1
@@ -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() {
|
||||
+3
-3
@@ -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;
|
||||
+5
-5
@@ -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;
|
||||
+8
-8
@@ -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;
|
||||
+2
-2
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
+17
-185
@@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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;
|
||||
+6
-6
@@ -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;
|
||||
+1
-1
@@ -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
-1
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user