refactored broad range of the application, split files, modularized calendar and file views, centralized bottom sheets and clipboard handling, and implemented unit test coverage
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../extensions/date_time.dart';
|
||||
import '../../../../state/app/modules/marianum_dates/bloc/marianum_dates_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 '../data/event_formatter.dart';
|
||||
|
||||
void showEventDetailsSheet(BuildContext context, MarianumDate event) {
|
||||
final isUpcoming = !event.start.difference(DateTime.now()).isNegative;
|
||||
showDetailsBottomSheet(
|
||||
context,
|
||||
header: ListTile(
|
||||
leading: const Icon(Icons.event_outlined, size: 32),
|
||||
title: Text(
|
||||
event.title.isEmpty ? '(ohne Titel)' : event.title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
children: (sheetContext) => [
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.date_range_outlined)),
|
||||
title: Text(EventFormatter.longRange(event)),
|
||||
),
|
||||
if (event.description != null && event.description!.trim().isNotEmpty)
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.notes_outlined)),
|
||||
title: Text(event.description!.trim()),
|
||||
),
|
||||
if (isUpcoming)
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.timer_outlined)),
|
||||
title: AnimatedTime(callback: () => event.start.difference(DateTime.now())),
|
||||
subtitle: Text(event.start.formatRelative()),
|
||||
)
|
||||
else
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.content_paste_search_outlined)),
|
||||
title: Text(event.start.formatRelative()),
|
||||
),
|
||||
DebugTile(sheetContext).jsonData(event.toJson()),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../state/app/modules/marianum_dates/bloc/marianum_dates_state.dart';
|
||||
import '../../timetable/custom_events/custom_event_edit_dialog.dart';
|
||||
import '../data/event_formatter.dart';
|
||||
import 'event_details_sheet.dart';
|
||||
|
||||
class MarianumDateRow extends StatelessWidget {
|
||||
final MarianumDate event;
|
||||
const MarianumDateRow({required this.event, super.key});
|
||||
|
||||
String _dayLabel() => event.start.day.toString().padLeft(2, '0');
|
||||
|
||||
String _monthYearLabel() =>
|
||||
'${event.start.month.toString().padLeft(2, '0')}.${event.start.year}';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return InkWell(
|
||||
onTap: () => showEventDetailsSheet(context, event),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 4, 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 44,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_dayLabel(),
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_monthYearLabel(),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.visible,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
event.title.isEmpty ? '(ohne Titel)' : event.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurface),
|
||||
),
|
||||
if (event.description != null && event.description!.trim().isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
event.description!.trim(),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
EventFormatter.trailingLabel(event),
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
icon: _CalendarPlusIcon(color: theme.colorScheme.onSurfaceVariant),
|
||||
tooltip: 'In Stundenplan übernehmen',
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => CustomEventEditDialog(
|
||||
initialTitle: event.title,
|
||||
initialDescription: event.description,
|
||||
initialStart: event.start,
|
||||
initialEnd: event.end,
|
||||
),
|
||||
barrierDismissible: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Composite icon: calendar with a small plus badge in the bottom-right.
|
||||
/// Material's bundled icon set has no `calendar_add_on`, so we layer
|
||||
/// `Icons.event_outlined` and `Icons.add` to get the same affordance.
|
||||
class _CalendarPlusIcon extends StatelessWidget {
|
||||
final Color color;
|
||||
const _CalendarPlusIcon({required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Icon(Icons.event_outlined, size: 22, color: color),
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: Icon(Icons.add_circle, size: 12, color: color),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MonthHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||
final String label;
|
||||
MonthHeaderDelegate({required this.label});
|
||||
|
||||
static const double _height = 38;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
height: _height,
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double get maxExtent => _height;
|
||||
|
||||
@override
|
||||
double get minExtent => _height;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant MonthHeaderDelegate oldDelegate) => oldDelegate.label != label;
|
||||
}
|
||||
Reference in New Issue
Block a user