refactored data providers with centralized cache resolution, unified UI using custom dialogs and bottom sheets, and enhanced network error handling for Dio and TLS errors
This commit is contained in:
@@ -7,6 +7,7 @@ import 'package:nextcloud/nextcloud.dart';
|
||||
import '../../../api/marianumcloud/webdav/webdav_api.dart';
|
||||
import '../../../widget/confirm_dialog.dart';
|
||||
import '../../../widget/focus_behaviour.dart';
|
||||
import '../../../widget/info_dialog.dart';
|
||||
|
||||
class FilesUploadDialog extends StatefulWidget {
|
||||
final List<String> filePaths;
|
||||
@@ -47,20 +48,12 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void showHttpErrorCode(int httpErrorCode){
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: const Text('Ein Fehler ist aufgetreten'),
|
||||
contentPadding: const EdgeInsets.all(10),
|
||||
content: Text('Error code: $httpErrorCode'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Schließen', textAlign: TextAlign.center),
|
||||
),
|
||||
],
|
||||
)
|
||||
void showHttpErrorCode(int httpErrorCode) {
|
||||
InfoDialog.show(
|
||||
context,
|
||||
'Error code: $httpErrorCode',
|
||||
title: 'Ein Fehler ist aufgetreten',
|
||||
copyable: true,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,20 +63,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
_overallProgressValue = 0.0;
|
||||
_infoText = '';
|
||||
});
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: const Text('Upload fehlgeschlagen'),
|
||||
contentPadding: const EdgeInsets.all(10),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Schließen', textAlign: TextAlign.center),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
InfoDialog.show(context, message, title: 'Upload fehlgeschlagen', copyable: true);
|
||||
}
|
||||
|
||||
Future<void> uploadFiles({bool override = false}) async {
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../../../../utils/download_manager.dart';
|
||||
import '../../../../utils/file_clipboard.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
import '../../../../widget/info_dialog.dart';
|
||||
import 'file_details_sheet.dart';
|
||||
|
||||
@@ -77,13 +78,7 @@ class _FileElementState extends State<FileElement> {
|
||||
DownloadManager.instance.clear(widget.file.path);
|
||||
_detachJob();
|
||||
setState(() {});
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Download'),
|
||||
content: Text(message),
|
||||
),
|
||||
);
|
||||
InfoDialog.show(context, message, title: 'Download', copyable: true);
|
||||
} else if (status is DownloadCancelled) {
|
||||
DownloadManager.instance.clear(widget.file.path);
|
||||
_detachJob();
|
||||
@@ -172,32 +167,36 @@ class _FileElementState extends State<FileElement> {
|
||||
|
||||
Future<void> _rename() async {
|
||||
final controller = TextEditingController(text: widget.file.name);
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogCtx) => AlertDialog(
|
||||
title: const Text('Umbenennen'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(labelText: 'Neuer Name'),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('Abbrechen')),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogCtx).pop(controller.text.trim()),
|
||||
child: const Text('Umbenennen'),
|
||||
try {
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogCtx) => AlertDialog(
|
||||
title: const Text('Umbenennen'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(labelText: 'Neuer Name'),
|
||||
autofocus: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (newName == null || newName.isEmpty || newName == widget.file.name) return;
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('Abbrechen')),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogCtx).pop(controller.text.trim()),
|
||||
child: const Text('Umbenennen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (newName == null || newName.isEmpty || newName == widget.file.name) return;
|
||||
|
||||
final parent = _parentPathOf(widget.file.path);
|
||||
final destination = _joinPath(parent, newName, isDirectory: widget.file.isDirectory);
|
||||
await _runWebdavOp(() async {
|
||||
final webdav = await WebdavApi.webdav;
|
||||
await webdav.move(PathUri.parse(widget.file.path), PathUri.parse(destination));
|
||||
}, errorTitle: 'Umbenennen fehlgeschlagen');
|
||||
final parent = _parentPathOf(widget.file.path);
|
||||
final destination = _joinPath(parent, newName, isDirectory: widget.file.isDirectory);
|
||||
await _runWebdavOp(() async {
|
||||
final webdav = await WebdavApi.webdav;
|
||||
await webdav.move(PathUri.parse(widget.file.path), PathUri.parse(destination));
|
||||
}, errorTitle: 'Umbenennen fehlgeschlagen');
|
||||
} finally {
|
||||
controller.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void _putOnClipboard({required bool copy}) {
|
||||
@@ -234,68 +233,55 @@ class _FileElementState extends State<FileElement> {
|
||||
widget.refetch();
|
||||
} on Object catch (e) {
|
||||
if (!mounted) return;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogCtx) => AlertDialog(
|
||||
title: Text(errorTitle),
|
||||
content: Text(e.toString()),
|
||||
actions: [TextButton(onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
InfoDialog.show(context, e.toString(), title: errorTitle, copyable: true);
|
||||
}
|
||||
}
|
||||
|
||||
void _showActionSheet() {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (sheetCtx) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.info_outline)),
|
||||
title: const Text('Info'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
showFileDetailsSheet(context, widget.file);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.drive_file_rename_outline)),
|
||||
title: const Text('Umbenennen'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_rename();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.drive_file_move_outline)),
|
||||
title: const Text('Verschieben'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_putOnClipboard(copy: false);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.copy_outlined)),
|
||||
title: const Text('Kopieren'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_putOnClipboard(copy: true);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.delete_outline)),
|
||||
title: const Text('Löschen'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_delete();
|
||||
},
|
||||
),
|
||||
],
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
children: (sheetCtx) => [
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.info_outline)),
|
||||
title: const Text('Info'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
showFileDetailsSheet(context, widget.file);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.drive_file_rename_outline)),
|
||||
title: const Text('Umbenennen'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_rename();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.drive_file_move_outline)),
|
||||
title: const Text('Verschieben'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_putOnClipboard(copy: false);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.copy_outlined)),
|
||||
title: const Text('Kopieren'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_putOnClipboard(copy: true);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.delete_outline)),
|
||||
title: const Text('Löschen'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_delete();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,6 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../data/sort_options.dart';
|
||||
|
||||
/// AppBar action buttons for sort direction (asc/desc) and sort field
|
||||
/// (name/date/size). Pure UI – owners pass current values + selection
|
||||
/// callbacks.
|
||||
class FilesSortActions extends StatelessWidget {
|
||||
final SortOption currentSort;
|
||||
final bool ascending;
|
||||
|
||||
@@ -10,6 +10,8 @@ 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/details_bottom_sheet.dart';
|
||||
import '../../../widget/info_dialog.dart';
|
||||
import '../../../widget/list_view_util.dart';
|
||||
import '../../../widget/string_extensions.dart';
|
||||
|
||||
@@ -21,18 +23,13 @@ class HolidaysView extends StatelessWidget {
|
||||
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()),
|
||||
],
|
||||
));
|
||||
}
|
||||
void showDisclaimer() => InfoDialog.show(
|
||||
context,
|
||||
'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/',
|
||||
title: 'Richtigkeit und Bereitstellung der Daten',
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -78,9 +75,16 @@ class HolidaysView extends StatelessWidget {
|
||||
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: [
|
||||
onTap: () => showDetailsBottomSheet(
|
||||
context,
|
||||
header: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 12),
|
||||
child: Text(
|
||||
'$holidayType ${holiday.year} in Hessen',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
children: (sheetCtx) => [
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.signpost_outlined)),
|
||||
title: Text(holiday.name.capitalize()),
|
||||
@@ -94,21 +98,20 @@ class HolidaysView extends StatelessWidget {
|
||||
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(
|
||||
if (DateTime.parse(holiday.start).difference(DateTime.now()).isNegative)
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.content_paste_search_outlined)),
|
||||
title: Text(Jiffy.parse(holiday.start).fromNow()),
|
||||
),
|
||||
child: ListTile(
|
||||
)
|
||||
else
|
||||
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()),
|
||||
DebugTile(sheetCtx).jsonData(holiday.toJson()),
|
||||
],
|
||||
)),
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -5,34 +5,43 @@ import '../../../../widget/share_position_origin.dart';
|
||||
|
||||
enum ShareTargetType { qr }
|
||||
|
||||
class SelectShareTypeDialog extends StatelessWidget {
|
||||
const SelectShareTypeDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SimpleDialog(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.qr_code_2_outlined),
|
||||
title: const Text('Per QR-Code'),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
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',
|
||||
text: 'Hol dir die für das Marianum maßgeschneiderte App:'
|
||||
'\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client '
|
||||
'\niOS: https://apps.apple.com/us/app/marianum-fulda/id6458789560 '
|
||||
'\n\nViel Spaß!',
|
||||
));
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
/// Bottom sheet that lets the user pick how they want to share the app.
|
||||
/// Resolves with [ShareTargetType.qr] for the QR option, or `null` when the
|
||||
/// sheet is dismissed (link sharing fires immediately and resolves null).
|
||||
Future<ShareTargetType?> showSelectShareTypeSheet(BuildContext context) {
|
||||
return showModalBottomSheet<ShareTargetType>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
useSafeArea: true,
|
||||
builder: (sheetCtx) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.qr_code_2_outlined),
|
||||
title: const Text('Per QR-Code'),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () => Navigator.of(sheetCtx).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(sheetCtx).pop();
|
||||
SharePlus.instance.share(ShareParams(
|
||||
sharePositionOrigin: SharePositionOrigin.get(sheetCtx),
|
||||
subject: 'App Teilen',
|
||||
text: 'Hol dir die für das Marianum maßgeschneiderte App:'
|
||||
'\n\nAndroid: https://play.google.com/store/apps/details?id=eu.mhsl.marianum.mobile.client '
|
||||
'\niOS: https://apps.apple.com/us/app/marianum-fulda/id6458789560 '
|
||||
'\n\nViel Spaß!',
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,10 +42,7 @@ class _OverhangState extends State<Overhang> {
|
||||
subtitle: const Text('Mit Freunden und deiner Klasse teilen'),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () async {
|
||||
final result = await showDialog<ShareTargetType>(
|
||||
context: context,
|
||||
builder: (_) => const SelectShareTypeDialog(),
|
||||
);
|
||||
final result = await showSelectShareTypeSheet(context);
|
||||
if (!mounted || result != ShareTargetType.qr) return;
|
||||
if (context.mounted) AppRoutes.openQrShare(context);
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ 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 '../../../../widget/details_bottom_sheet.dart';
|
||||
import '../data/default_settings.dart';
|
||||
import '../widgets/privacy_info.dart';
|
||||
import 'dev_tools_section.dart';
|
||||
@@ -69,45 +70,43 @@ class AboutSection extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
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 _showPrivacyDialog(BuildContext context) => showDetailsBottomSheet(
|
||||
context,
|
||||
children: (sheetCtx) => [
|
||||
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(sheetCtx),
|
||||
),
|
||||
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(sheetCtx),
|
||||
),
|
||||
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(sheetCtx),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
void _toggleDeveloperMode(BuildContext context, SettingsCubit settings, bool? state) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/debug/cache_view.dart';
|
||||
import '../../../../widget/debug/json_viewer.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
|
||||
class DevToolsSection extends StatefulWidget {
|
||||
final SettingsCubit settings;
|
||||
@@ -29,42 +30,45 @@ class _DevToolsSectionState extends State<DevToolsSection> {
|
||||
title: const Text('Performance overlays'),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogCtx) => BlocBuilder<SettingsCubit, model.Settings>(
|
||||
bloc: widget.settings,
|
||||
builder: (_, _) {
|
||||
final dev = widget.settings.val().devToolsSettings;
|
||||
return SimpleDialog(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.auto_graph_outlined),
|
||||
title: const Text('Performance graph'),
|
||||
trailing: Checkbox(
|
||||
value: dev.showPerformanceOverlay,
|
||||
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.showPerformanceOverlay = e!,
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
children: (sheetCtx) => [
|
||||
BlocBuilder<SettingsCubit, model.Settings>(
|
||||
bloc: widget.settings,
|
||||
builder: (_, _) {
|
||||
final dev = widget.settings.val().devToolsSettings;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.auto_graph_outlined),
|
||||
title: const Text('Performance graph'),
|
||||
trailing: Checkbox(
|
||||
value: dev.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: dev.checkerboardOffscreenLayers,
|
||||
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.checkerboardOffscreenLayers = e!,
|
||||
ListTile(
|
||||
leading: const Icon(Icons.screen_search_desktop_outlined),
|
||||
title: const Text('Indicate offscreen layers'),
|
||||
trailing: Checkbox(
|
||||
value: dev.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: dev.checkerboardRasterCacheImages,
|
||||
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.checkerboardRasterCacheImages = e!,
|
||||
ListTile(
|
||||
leading: const Icon(Icons.imagesearch_roller_outlined),
|
||||
title: const Text('Indicate raster cache images'),
|
||||
trailing: Checkbox(
|
||||
value: dev.checkerboardRasterCacheImages,
|
||||
onChanged: (e) => widget.settings.val(write: true).devToolsSettings.checkerboardRasterCacheImages = e!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -122,9 +126,6 @@ class _DevToolsSectionState extends State<DevToolsSection> {
|
||||
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',
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
|
||||
class PrivacyInfo {
|
||||
String providerText;
|
||||
@@ -11,22 +12,29 @@ class PrivacyInfo {
|
||||
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),
|
||||
),
|
||||
],
|
||||
));
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
header: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 12),
|
||||
child: Text(
|
||||
'Betreiberinformation | $providerText',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
children: (sheetCtx) => [
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.person_pin_outlined)),
|
||||
title: const Text('Impressum'),
|
||||
subtitle: Text(imprintUrl),
|
||||
onTap: () => ConfirmDialog.openBrowser(sheetCtx, imprintUrl),
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.privacy_tip_outlined)),
|
||||
title: const Text('Datenschutzerklärung'),
|
||||
subtitle: Text(privacyUrl),
|
||||
onTap: () => ConfirmDialog.openBrowser(sheetCtx, privacyUrl),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||
import '../../../state/app/modules/chat_list/bloc/chat_list_state.dart';
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../widget/confirm_dialog.dart';
|
||||
import '../../../widget/info_dialog.dart';
|
||||
import '../../../widget/placeholder_view.dart';
|
||||
import 'join_chat.dart';
|
||||
import 'search_chat.dart';
|
||||
@@ -98,11 +99,9 @@ class _ChatListViewState extends State<_ChatListView> {
|
||||
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.'),
|
||||
),
|
||||
InfoDialog.show(
|
||||
context,
|
||||
'Du kannst die Benachrichtigungen später jederzeit in den App-Einstellungen aktivieren.',
|
||||
);
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -40,9 +40,8 @@ class BubbleStyle {
|
||||
final double borderRadius;
|
||||
}
|
||||
|
||||
/// Lightweight chat bubble. Replaces the abandoned `bubble` package: renders a
|
||||
/// rounded container with optional shadow / border. The nip is conveyed by
|
||||
/// flattening one corner so the bubble visually anchors to the speaker side.
|
||||
/// The "nip" is faked by flattening one corner so the bubble anchors to
|
||||
/// the speaker side.
|
||||
class Bubble extends StatelessWidget {
|
||||
const Bubble({required this.child, required this.style, super.key});
|
||||
|
||||
|
||||
@@ -246,9 +246,6 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
|
||||
}
|
||||
}
|
||||
|
||||
/// Stack inside the bubble: actor name (top-left, optional), message body
|
||||
/// (centre), timestamp + read marker (bottom-right, optional), and a
|
||||
/// download progress bar overlaid at the bottom while a job is running.
|
||||
class _BubbleContent extends StatelessWidget {
|
||||
final Text actorText;
|
||||
final Text timeText;
|
||||
|
||||
@@ -14,13 +14,14 @@ import '../../../../utils/clipboard_helper.dart';
|
||||
import '../../../../widget/app_progress_indicator.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/debug/debug_tile.dart';
|
||||
import '../../../../widget/details_bottom_sheet.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(
|
||||
void showChatMessageOptionsDialog(
|
||||
BuildContext context, {
|
||||
required GetRoomResponseObject chatData,
|
||||
required GetChatResponseObject bubbleData,
|
||||
@@ -34,63 +35,61 @@ Future<void> showChatMessageOptionsDialog(
|
||||
.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: () {
|
||||
copyToClipboard(parentContext, 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)
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('Nachricht löschen'),
|
||||
onPressed: () async {
|
||||
await DeleteMessage(chatData.token, bubbleData.id).run();
|
||||
if (dialogCtx.mounted) dialogCtx.read<ChatBloc>().refresh();
|
||||
},
|
||||
),
|
||||
DebugTile(dialogCtx).jsonData(bubbleData.toJson()),
|
||||
],
|
||||
),
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
children: (sheetCtx) => [
|
||||
if (canReact)
|
||||
_ReactionsRow(
|
||||
chatToken: chatData.token,
|
||||
messageId: bubbleData.id,
|
||||
onRefetch: onRefetch,
|
||||
sheetContext: sheetCtx,
|
||||
),
|
||||
if (bubbleData.isReplyable)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.reply_outlined),
|
||||
title: const Text('Antworten'),
|
||||
onTap: () {
|
||||
sheetCtx.read<ChatBloc>().setReferenceMessageId(bubbleData.id);
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
),
|
||||
if (canReact)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.emoji_emotions_outlined),
|
||||
title: const Text('Reaktionen'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).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: () {
|
||||
copyToClipboard(parentContext, bubbleData.message);
|
||||
Navigator.of(sheetCtx).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(sheetCtx).pop(),
|
||||
),
|
||||
if (canDelete)
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('Nachricht löschen'),
|
||||
onPressed: () async {
|
||||
await DeleteMessage(chatData.token, bubbleData.id).run();
|
||||
if (sheetCtx.mounted) sheetCtx.read<ChatBloc>().refresh();
|
||||
},
|
||||
),
|
||||
DebugTile(sheetCtx).jsonData(bubbleData.toJson()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,13 +97,13 @@ class _ReactionsRow extends StatefulWidget {
|
||||
final String chatToken;
|
||||
final int messageId;
|
||||
final void Function({bool renew}) onRefetch;
|
||||
final BuildContext dialogContext;
|
||||
final BuildContext sheetContext;
|
||||
|
||||
const _ReactionsRow({
|
||||
required this.chatToken,
|
||||
required this.messageId,
|
||||
required this.onRefetch,
|
||||
required this.dialogContext,
|
||||
required this.sheetContext,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -131,7 +130,7 @@ class _ReactionsRowState extends State<_ReactionsRow> {
|
||||
if (!mounted) return;
|
||||
if (ok) {
|
||||
widget.onRefetch(renew: true);
|
||||
if (widget.dialogContext.mounted) Navigator.of(widget.dialogContext).pop();
|
||||
if (widget.sheetContext.mounted) Navigator.of(widget.sheetContext).pop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import '../../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/debug/debug_tile.dart';
|
||||
import '../../../../widget/details_bottom_sheet.dart';
|
||||
import '../../../../widget/user_avatar.dart';
|
||||
import '../chat_view.dart';
|
||||
import '../talk_navigator.dart';
|
||||
@@ -124,8 +125,9 @@ class _ChatTileState extends State<ChatTile> {
|
||||
},
|
||||
onLongPress: () {
|
||||
if (widget.disableContextActions) return;
|
||||
showDialog(context: context, builder: (dialogCtx) => SimpleDialog(
|
||||
children: [
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
children: (sheetCtx) => [
|
||||
if (widget.data.unreadMessages > 0)
|
||||
AsyncListTile(
|
||||
leading: const Icon(Icons.mark_chat_read_outlined),
|
||||
@@ -163,7 +165,7 @@ class _ChatTileState extends State<ChatTile> {
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('Konversation verlassen'),
|
||||
onTap: () {
|
||||
Navigator.of(dialogCtx).pop();
|
||||
Navigator.of(sheetCtx).pop();
|
||||
ConfirmDialog(
|
||||
title: 'Chat verlassen',
|
||||
content: 'Du benötigst ggf. eine Einladung um erneut beizutreten.',
|
||||
@@ -175,9 +177,9 @@ class _ChatTileState extends State<ChatTile> {
|
||||
).asDialog(context);
|
||||
},
|
||||
),
|
||||
DebugTile(dialogCtx).jsonData(widget.data.toJson()),
|
||||
DebugTile(sheetCtx).jsonData(widget.data.toJson()),
|
||||
],
|
||||
));
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ class _PollOptionsListState extends State<PollOptionsList> {
|
||||
final portion = numVoters == 0 ? 0.0 : (votes / numVoters);
|
||||
|
||||
return ListTile(
|
||||
// enabled: false,
|
||||
isThreeLine: portionsVisible,
|
||||
dense: true,
|
||||
title: Text(
|
||||
|
||||
@@ -3,14 +3,22 @@ const double kCalendarEndHour = 17.25;
|
||||
const Duration kCalendarTimeInterval = Duration(minutes: 30);
|
||||
const double kCalendarViewHeaderHeight = 60;
|
||||
|
||||
/// Minimum pixels per hour. Below this, the grid scrolls vertically rather
|
||||
/// than compressing further.
|
||||
/// Below this, the grid scrolls vertically rather than compressing further.
|
||||
const double kCalendarMinPxPerHour = 56;
|
||||
|
||||
/// Minimum height of a lesson block in the period-based layout. The grid
|
||||
/// scrolls vertically once lessons would otherwise be smaller than this.
|
||||
/// The grid scrolls vertically once lessons would otherwise be smaller.
|
||||
const double kLessonBlockMinHeight = 50;
|
||||
|
||||
/// Fixed height of a break block in the period-based layout. Independent of
|
||||
/// the actual break duration; breaks are rendered as a compact indicator.
|
||||
/// Fixed (independent of actual break duration); breaks render as a compact
|
||||
/// indicator.
|
||||
const double kBreakBlockHeight = 28;
|
||||
|
||||
const int kOutsideChipsMaxVisible = 2;
|
||||
const double kOutsideChipHeight = 22;
|
||||
const double kOutsideChipSpacing = 3;
|
||||
const double kOutsideStripVerticalPadding = 3;
|
||||
|
||||
const double kAppointmentTitleFontSize = 15;
|
||||
const double kAppointmentTitleMinFontSize = 11;
|
||||
const double kAppointmentBodyFontSize = 10;
|
||||
const double kAppointmentBodyLineHeight = 1.15;
|
||||
|
||||
@@ -2,14 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
||||
|
||||
import '../data/arbitrary_appointment.dart';
|
||||
import '../data/calendar_layout.dart';
|
||||
import 'cross_painter.dart';
|
||||
|
||||
class AppointmentTile extends StatelessWidget {
|
||||
static const _radius = BorderRadius.all(Radius.circular(7));
|
||||
static const _titleFontSize = 15.0;
|
||||
static const _titleMinFontSize = 11.0;
|
||||
static const _bodyFontSize = 10.0;
|
||||
static const _bodyLineHeight = 1.15;
|
||||
|
||||
final Appointment appointment;
|
||||
final bool crossedOut;
|
||||
@@ -42,8 +39,8 @@ class AppointmentTile extends StatelessWidget {
|
||||
children: [
|
||||
_AdaptiveTitle(
|
||||
text: appointment.subject,
|
||||
fontSize: _titleFontSize,
|
||||
minFontSize: _titleMinFontSize,
|
||||
fontSize: kAppointmentTitleFontSize,
|
||||
minFontSize: kAppointmentTitleMinFontSize,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
if (isCustom) ...[
|
||||
@@ -53,8 +50,8 @@ class AppointmentTile extends StatelessWidget {
|
||||
padding: const EdgeInsets.only(top: 1),
|
||||
child: _WrappingBody(
|
||||
text: description,
|
||||
fontSize: _bodyFontSize,
|
||||
lineHeight: _bodyLineHeight,
|
||||
fontSize: kAppointmentBodyFontSize,
|
||||
lineHeight: kAppointmentBodyLineHeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -63,7 +60,7 @@ class AppointmentTile extends StatelessWidget {
|
||||
.split('\n')
|
||||
.where((p) => p.isNotEmpty)
|
||||
.take(2))
|
||||
_ScaledLine(text: line, fontSize: _bodyFontSize),
|
||||
_ScaledLine(text: line, fontSize: kAppointmentBodyFontSize),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
part of '../custom_workweek_calendar.dart';
|
||||
|
||||
class _OutsideHoursStrip extends StatelessWidget {
|
||||
static const int _maxVisibleChips = 2;
|
||||
static const double _chipHeight = 22;
|
||||
static const double _chipSpacing = 3;
|
||||
static const double _verticalPadding = 3;
|
||||
|
||||
final DateTime weekStart;
|
||||
final List<Appointment> appointments;
|
||||
final double rulerWidth;
|
||||
@@ -28,17 +23,17 @@ class _OutsideHoursStrip extends StatelessWidget {
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final maxChipsPerDay = outside
|
||||
.map((day) => day.length > _maxVisibleChips ? _maxVisibleChips : day.length)
|
||||
.map((day) => day.length > kOutsideChipsMaxVisible ? kOutsideChipsMaxVisible : day.length)
|
||||
.fold<int>(0, (m, c) => c > m ? c : m);
|
||||
final stripHeight = _verticalPadding * 2 +
|
||||
maxChipsPerDay * _chipHeight +
|
||||
(maxChipsPerDay > 1 ? (maxChipsPerDay - 1) * _chipSpacing : 0);
|
||||
final stripHeight = kOutsideStripVerticalPadding * 2 +
|
||||
maxChipsPerDay * kOutsideChipHeight +
|
||||
(maxChipsPerDay > 1 ? (maxChipsPerDay - 1) * kOutsideChipSpacing : 0);
|
||||
|
||||
return Container(
|
||||
color: theme.colorScheme.surfaceContainerLowest,
|
||||
padding: const EdgeInsets.symmetric(vertical: _verticalPadding),
|
||||
padding: const EdgeInsets.symmetric(vertical: kOutsideStripVerticalPadding),
|
||||
child: SizedBox(
|
||||
height: stripHeight - _verticalPadding * 2,
|
||||
height: stripHeight - kOutsideStripVerticalPadding * 2,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -47,9 +42,6 @@ class _OutsideHoursStrip extends StatelessWidget {
|
||||
Expanded(
|
||||
child: _OutsideDayColumn(
|
||||
appointments: outside[d],
|
||||
maxVisible: _maxVisibleChips,
|
||||
chipHeight: _chipHeight,
|
||||
chipSpacing: _chipSpacing,
|
||||
onAppointmentTap: onAppointmentTap,
|
||||
isCrossedOut: isCrossedOut,
|
||||
),
|
||||
@@ -63,17 +55,11 @@ class _OutsideHoursStrip extends StatelessWidget {
|
||||
|
||||
class _OutsideDayColumn extends StatelessWidget {
|
||||
final List<Appointment> appointments;
|
||||
final int maxVisible;
|
||||
final double chipHeight;
|
||||
final double chipSpacing;
|
||||
final void Function(Appointment) onAppointmentTap;
|
||||
final bool Function(Appointment) isCrossedOut;
|
||||
|
||||
const _OutsideDayColumn({
|
||||
required this.appointments,
|
||||
required this.maxVisible,
|
||||
required this.chipHeight,
|
||||
required this.chipSpacing,
|
||||
required this.onAppointmentTap,
|
||||
required this.isCrossedOut,
|
||||
});
|
||||
@@ -132,11 +118,12 @@ class _OutsideDayColumn extends StatelessWidget {
|
||||
if (!aLike && bLike) return 1;
|
||||
return a.startTime.compareTo(b.startTime);
|
||||
});
|
||||
final visible = sorted.length <= maxVisible
|
||||
final visible = sorted.length <= kOutsideChipsMaxVisible
|
||||
? sorted
|
||||
: sorted.take(maxVisible - 1).toList();
|
||||
final overflow =
|
||||
sorted.length <= maxVisible ? const <Appointment>[] : sorted.skip(maxVisible - 1).toList();
|
||||
: sorted.take(kOutsideChipsMaxVisible - 1).toList();
|
||||
final overflow = sorted.length <= kOutsideChipsMaxVisible
|
||||
? const <Appointment>[]
|
||||
: sorted.skip(kOutsideChipsMaxVisible - 1).toList();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
@@ -145,9 +132,9 @@ class _OutsideDayColumn extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (var i = 0; i < visible.length; i++) ...[
|
||||
if (i > 0) SizedBox(height: chipSpacing),
|
||||
if (i > 0) const SizedBox(height: kOutsideChipSpacing),
|
||||
SizedBox(
|
||||
height: chipHeight,
|
||||
height: kOutsideChipHeight,
|
||||
child: _OutsideChip(
|
||||
appointment: visible[i],
|
||||
onTap: () => onAppointmentTap(visible[i]),
|
||||
@@ -155,9 +142,9 @@ class _OutsideDayColumn extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
if (overflow.isNotEmpty) ...[
|
||||
SizedBox(height: chipSpacing),
|
||||
const SizedBox(height: kOutsideChipSpacing),
|
||||
SizedBox(
|
||||
height: chipHeight,
|
||||
height: kOutsideChipHeight,
|
||||
child: _OutsideOverflowChip(
|
||||
count: overflow.length,
|
||||
onTap: () => _showOverflow(context, overflow),
|
||||
|
||||
@@ -429,8 +429,7 @@ class _OverflowTile extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Card peeking out at the bottom — visual hint that more cards lie
|
||||
// underneath the visible one.
|
||||
// Stacked-cards effect: a darker layer peeks out below the front card.
|
||||
Positioned(
|
||||
top: 4,
|
||||
left: 2,
|
||||
@@ -443,7 +442,6 @@ class _OverflowTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Front card with the "+N" indicator.
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
|
||||
Reference in New Issue
Block a user