further improved accessibility across the application and add tooltips to action buttons

This commit is contained in:
2026-07-15 19:29:40 +02:00
parent 478f0ff20b
commit e8c6ac1c65
13 changed files with 366 additions and 268 deletions
+53 -41
View File
@@ -18,6 +18,14 @@ class LoginErrorBanner extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final showDetails = details != null
? () => InfoDialog.show(
context,
details!,
copyable: true,
title: 'Fehlerdetails',
)
: null;
return AnimatedSize( return AnimatedSize(
duration: const Duration(milliseconds: 180), duration: const Duration(milliseconds: 180),
curve: Curves.easeOut, curve: Curves.easeOut,
@@ -25,52 +33,56 @@ class LoginErrorBanner extends StatelessWidget {
? const SizedBox(height: 0, width: double.infinity) ? const SizedBox(height: 0, width: double.infinity)
: Padding( : Padding(
padding: const EdgeInsets.only(top: 14), padding: const EdgeInsets.only(top: 14),
child: Material( child: Semantics(
color: theme.colorScheme.errorContainer.withValues(alpha: 0.6), button: showDetails != null,
borderRadius: BorderRadius.circular(12), label: showDetails != null
child: InkWell( ? '${message!}, Fehlerdetails anzeigen'
onTap: details != null : message,
? () => InfoDialog.show( onTap: showDetails,
context, child: ExcludeSemantics(
details!, child: Material(
copyable: true, color: theme.colorScheme.errorContainer.withValues(
title: 'Fehlerdetails', alpha: 0.6,
)
: null,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
), ),
child: Row( borderRadius: BorderRadius.circular(12),
children: [ child: InkWell(
Icon( onTap: showDetails,
Icons.error_outline, borderRadius: BorderRadius.circular(12),
size: 20, child: Padding(
color: theme.colorScheme.onErrorContainer, padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
), ),
const SizedBox(width: 10), child: Row(
Expanded( children: [
child: Text( Icon(
message!, Icons.error_outline,
style: TextStyle( size: 20,
color: theme.colorScheme.onErrorContainer, color: theme.colorScheme.onErrorContainer,
fontSize: 13,
height: 1.3,
), ),
), const SizedBox(width: 10),
Expanded(
child: Text(
message!,
style: TextStyle(
color: theme.colorScheme.onErrorContainer,
fontSize: 13,
height: 1.3,
),
),
),
if (details != null) ...[
const SizedBox(width: 8),
Icon(
Icons.chevron_right,
size: 20,
color: theme.colorScheme.onErrorContainer
.withValues(alpha: 0.7),
),
],
],
), ),
if (details != null) ...[ ),
const SizedBox(width: 8),
Icon(
Icons.chevron_right,
size: 20,
color: theme.colorScheme.onErrorContainer
.withValues(alpha: 0.7),
),
],
],
), ),
), ),
), ),
@@ -23,6 +23,8 @@ class FilesSortActions extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
PopupMenuButton<bool>( PopupMenuButton<bool>(
tooltip:
'Sortierrichtung: ${ascending ? 'aufsteigend' : 'absteigend'}',
icon: Icon( icon: Icon(
ascending ? Icons.text_rotate_up : Icons.text_rotation_down, ascending ? Icons.text_rotate_up : Icons.text_rotation_down,
), ),
@@ -47,6 +49,8 @@ class FilesSortActions extends StatelessWidget {
onSelected: onDirectionChanged, onSelected: onDirectionChanged,
), ),
PopupMenuButton<SortOption>( PopupMenuButton<SortOption>(
tooltip:
'Sortieren nach: ${SortOptions.getOption(currentSort).displayName}',
icon: const Icon(Icons.sort), icon: const Icon(Icons.sort),
itemBuilder: (context) => SortOptions.options.keys itemBuilder: (context) => SortOptions.options.keys
.map( .map(
@@ -35,6 +35,8 @@ class GradeAveragesView extends StatelessWidget {
), ),
), ),
PopupMenuButton<bool>( PopupMenuButton<bool>(
tooltip:
'Notensystem: ${bloc.isMiddleSchool() ? 'Realschule' : 'Oberstufe'}',
initialValue: bloc.isMiddleSchool(), initialValue: bloc.isMiddleSchool(),
icon: const Icon(Icons.more_horiz), icon: const Icon(Icons.more_horiz),
itemBuilder: (context) => [true, false] itemBuilder: (context) => [true, false]
+5 -8
View File
@@ -43,6 +43,8 @@ class HolidaysView extends StatelessWidget {
onPressed: showDisclaimer, onPressed: showDisclaimer,
), ),
PopupMenuButton<bool>( PopupMenuButton<bool>(
tooltip:
'Vergangene Ferien ${bloc.showPastHolidays() ? 'ausblenden' : 'anzeigen'}',
initialValue: bloc.showPastHolidays(), initialValue: bloc.showPastHolidays(),
icon: const Icon(Icons.history), icon: const Icon(Icons.history),
itemBuilder: (context) => [true, false] itemBuilder: (context) => [true, false]
@@ -82,9 +84,7 @@ class HolidaysView extends StatelessWidget {
text: 'Keine Schulferien verfügbar', text: 'Keine Schulferien verfügbar',
); );
} }
return ListViewUtil.fromList<McHoliday>( return ListViewUtil.fromList<McHoliday>(holidays, (holiday) {
holidays,
(holiday) {
String holidayYear() { String holidayYear() {
final startYear = holiday.startDate.year; final startYear = holiday.startDate.year;
final endYear = holiday.endDate.year; final endYear = holiday.endDate.year;
@@ -94,9 +94,7 @@ class HolidaysView extends StatelessWidget {
return ListTile( return ListTile(
leading: const CenteredLeading(Icon(Icons.calendar_month)), leading: const CenteredLeading(Icon(Icons.calendar_month)),
title: Text( title: Text('${holiday.longName} ${holidayYear()}'),
'${holiday.longName} ${holidayYear()}',
),
subtitle: Text( subtitle: Text(
'${holiday.startDate.formatDate()} - ${holiday.endDate.formatDate()}', '${holiday.startDate.formatDate()} - ${holiday.endDate.formatDate()}',
), ),
@@ -148,8 +146,7 @@ class HolidaysView extends StatelessWidget {
), ),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
); );
}, });
);
}, },
), ),
); );
@@ -32,89 +32,90 @@ class MarianumDatesView extends StatelessWidget {
} }
@override @override
Widget build(BuildContext context) => Widget build(
BlocModule<MarianumDatesBloc, LoadableState<MarianumDatesState>>( BuildContext context,
create: (context) => MarianumDatesBloc(), ) => BlocModule<MarianumDatesBloc, LoadableState<MarianumDatesState>>(
autoRebuild: true, create: (context) => MarianumDatesBloc(),
child: (context, bloc, state) => Scaffold( autoRebuild: true,
appBar: AppBar( child: (context, bloc, state) => Scaffold(
title: const Text('Marianum Termine'), appBar: AppBar(
actions: [ title: const Text('Marianum Termine'),
PopupMenuButton<bool>( actions: [
initialValue: bloc.showPastEvents(), PopupMenuButton<bool>(
icon: const Icon(Icons.history), tooltip:
itemBuilder: (context) => [true, false] 'Vergangene Termine ${bloc.showPastEvents() ? 'ausblenden' : 'anzeigen'}',
.map( initialValue: bloc.showPastEvents(),
(e) => PopupMenuItem<bool>( icon: const Icon(Icons.history),
value: e, itemBuilder: (context) => [true, false]
enabled: e != bloc.showPastEvents(), .map(
child: Row( (e) => PopupMenuItem<bool>(
children: [ value: e,
Icon( enabled: e != bloc.showPastEvents(),
e child: Row(
? Icons.history_outlined children: [
: Icons.history_toggle_off_outlined, Icon(
color: Theme.of(context).colorScheme.onSurface, e
), ? Icons.history_outlined
const SizedBox(width: 15), : Icons.history_toggle_off_outlined,
Text( color: Theme.of(context).colorScheme.onSurface,
e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen',
),
],
),
),
)
.toList(),
onSelected: (e) => bloc.add(SetPastEventsVisible(e)),
),
IconButton(
tooltip: 'Suchen',
icon: const Icon(Icons.search),
onPressed: () {
final events = bloc.getEvents() ?? const <MarianumDate>[];
showSearch(
context: context,
delegate: SearchMarianumDates(events),
);
},
),
],
),
body: LoadableStateConsumer<MarianumDatesBloc, MarianumDatesState>(
child: (state, loading) {
final events = bloc.getEvents() ?? const <MarianumDate>[];
final groups = _groupByMonth(events);
if (groups.isEmpty) {
return const PlaceholderView(
icon: Icons.event_busy_outlined,
text: 'Keine Termine',
);
}
return CustomScrollView(
slivers: [
for (final group in groups)
SliverMainAxisGroup(
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: MonthHeaderDelegate(label: group.label),
),
SliverList.builder(
itemCount: group.events.length,
itemBuilder: (_, i) =>
MarianumDateRow(event: group.events[i]),
), ),
const SizedBox(width: 15),
Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen'),
], ],
), ),
const SliverToBoxAdapter(child: SizedBox(height: 24)), ),
], )
.toList(),
onSelected: (e) => bloc.add(SetPastEventsVisible(e)),
),
IconButton(
tooltip: 'Suchen',
icon: const Icon(Icons.search),
onPressed: () {
final events = bloc.getEvents() ?? const <MarianumDate>[];
showSearch(
context: context,
delegate: SearchMarianumDates(events),
); );
}, },
), ),
), ],
); ),
body: LoadableStateConsumer<MarianumDatesBloc, MarianumDatesState>(
child: (state, loading) {
final events = bloc.getEvents() ?? const <MarianumDate>[];
final groups = _groupByMonth(events);
if (groups.isEmpty) {
return const PlaceholderView(
icon: Icons.event_busy_outlined,
text: 'Keine Termine',
);
}
return CustomScrollView(
slivers: [
for (final group in groups)
SliverMainAxisGroup(
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: MonthHeaderDelegate(label: group.label),
),
SliverList.builder(
itemCount: group.events.length,
itemBuilder: (_, i) =>
MarianumDateRow(event: group.events[i]),
),
],
),
const SliverToBoxAdapter(child: SizedBox(height: 24)),
],
);
},
),
),
);
} }
class _MonthGroup { class _MonthGroup {
+8 -1
View File
@@ -91,7 +91,14 @@ class _ChatTileState extends State<ChatTile> {
: null, : null,
leading: Stack( leading: Stack(
children: [ children: [
circleAvatar, // Der Name steht bereits im Titel der Zeile das Avatarbild selbst
// muss der Screenreader nicht ansagen. Nur bei Gruppen ist der
// Hinweis „Gruppe" nützlich (sonst nicht vom Einzelchat zu
// unterscheiden).
Semantics(
label: isGroup ? A11yLabels.group : null,
child: ExcludeSemantics(child: circleAvatar),
),
Visibility( Visibility(
visible: widget.data.isFavorite, visible: widget.data.isFavorite,
child: Positioned( child: Positioned(
+11 -1
View File
@@ -106,6 +106,10 @@ class _TimetableState extends State<Timetable> {
.canViewForeignTimetables; .canViewForeignTimetables;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
// Der Kalender scrollt nicht (nur ziehen/reloaden), aber seine internen
// Scrollables feuern ScrollNotifications, die sonst den Material-3
// "scrolled under"-Farbwechsel der AppBar dauerhaft auslösen.
notificationPredicate: (_) => false,
title: const Text('Stunden & Vertretungsplan'), title: const Text('Stunden & Vertretungsplan'),
actions: [ actions: [
IconButton( IconButton(
@@ -114,6 +118,7 @@ class _TimetableState extends State<Timetable> {
onPressed: atToday ? null : _jumpToToday, onPressed: atToday ? null : _jumpToToday,
), ),
PopupMenuButton<_CalendarAction>( PopupMenuButton<_CalendarAction>(
tooltip: 'Kalendereinträge',
icon: const Icon(Icons.edit_calendar_outlined), icon: const Icon(Icons.edit_calendar_outlined),
onSelected: _onAction, onSelected: _onAction,
itemBuilder: (_) => const [ itemBuilder: (_) => const [
@@ -176,6 +181,9 @@ class _TimetableState extends State<Timetable> {
.canViewForeignTimetables; .canViewForeignTimetables;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
// Siehe _buildOwnPlan: den scroll-under-Farbwechsel unterdrücken, weil
// der Kalender nicht scrollt, aber ScrollNotifications feuert.
notificationPredicate: (_) => false,
title: const Text('Stunden & Vertretungsplan'), title: const Text('Stunden & Vertretungsplan'),
actions: [ actions: [
IconButton( IconButton(
@@ -285,7 +293,9 @@ class _ViewingBanner extends StatelessWidget {
), ),
compactButton( compactButton(
icon: isFavorite ? Icons.star : Icons.star_border, icon: isFavorite ? Icons.star : Icons.star_border,
tooltip: isFavorite ? 'Favorit entfernen' : 'Als Favorit markieren', tooltip: isFavorite
? 'Favorit entfernen'
: 'Als Favorit markieren',
onPressed: () => _toggleFavorite(context), onPressed: () => _toggleFavorite(context),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
@@ -34,7 +34,15 @@ class _WeekGrid extends StatelessWidget {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_PeriodRuler(schedule: schedule, layout: layout, width: rulerWidth), // Reine Orientierungshilfe der Screenreader soll die Zeitleiste nicht
// vorlesen; jede Termin-Kachel trägt ihre Uhrzeit selbst im Label.
ExcludeSemantics(
child: _PeriodRuler(
schedule: schedule,
layout: layout,
width: rulerWidth,
),
),
for (var d = 0; d < 5; d++) for (var d = 0; d < 5; d++)
Expanded( Expanded(
child: _DayColumn( child: _DayColumn(
@@ -279,104 +287,134 @@ class _DayColumn extends StatelessWidget {
final isTablet = MediaQuery.of(context).size.shortestSide >= 600; final isTablet = MediaQuery.of(context).size.shortestSide >= 600;
final laidOut = assignLanes(dayAppointments, maxLanes: isTablet ? 3 : 2); final laidOut = assignLanes(dayAppointments, maxLanes: isTablet ? 3 : 2);
return GestureDetector( final dayName = DateFormat(
behavior: HitTestBehavior.translucent, 'EEEE',
onLongPressStart: (details) => _handleLongPress(details, dayAppointments), Localizations.localeOf(context).toString(),
child: DecoratedBox( ).format(date);
decoration: BoxDecoration( final headerLabel = isToday
color: isToday ? theme.colorScheme.primary.withAlpha(14) : null, ? '$dayName, ${date.formatDateShort()}, ${A11yLabels.today}'
border: Border( : '$dayName, ${date.formatDateShort()}';
left: BorderSide(
color: theme.dividerColor.withAlpha(90), // container + OrdinalSortKey ⇒ der Screenreader liest Tag für Tag (Mo→Fr),
width: 0.5, // jeden Tag vollständig, statt zeilenweise quer über alle Spalten. Der
// Header steckt als erstes Element in der Spalte, damit „Montag: …" vor den
// Stunden angesagt wird.
return Semantics(
container: true,
sortKey: OrdinalSortKey(date.weekday.toDouble()),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onLongPressStart: (details) =>
_handleLongPress(details, dayAppointments),
child: DecoratedBox(
decoration: BoxDecoration(
color: isToday ? theme.colorScheme.primary.withAlpha(14) : null,
border: Border(
left: BorderSide(
color: theme.dividerColor.withAlpha(90),
width: 0.5,
),
), ),
), ),
), child: LayoutBuilder(
child: LayoutBuilder( builder: (context, constraints) {
builder: (context, constraints) { final width = constraints.maxWidth;
final width = constraints.maxWidth; return Stack(
return Stack( clipBehavior: Clip.none,
clipBehavior: Clip.none, children: [
children: [
for (final period in schedule.periods)
Positioned( Positioned(
top: layout.topOf(period), top: 0,
left: 0, left: 0,
right: 0, right: 0,
child: Container( child: Semantics(
height: 0.5, header: true,
color: theme.dividerColor.withAlpha(60), label: headerLabel,
child: const SizedBox(height: 1),
), ),
), ),
for (final region in dayRegions) for (final period in schedule.periods)
Positioned( Positioned(
top: layout.yOfDateTime(region.start), top: layout.topOf(period),
height: left: 0,
(layout.yOfDateTime(region.end) - right: 0,
layout.yOfDateTime(region.start)) child: Container(
.clamp(0, double.infinity), height: 0.5,
left: 0, color: theme.dividerColor.withAlpha(60),
right: 0, ),
child: TimeRegionTile(region: region.region), ),
), for (final region in dayRegions)
for (final cell in laidOut) Positioned(
Positioned( top: layout.yOfDateTime(region.start),
top: layout.yOfDateTime(cell.startTime), height:
height: (layout.yOfDateTime(region.end) -
(layout.yOfDateTime(cell.endTime) - layout.yOfDateTime(region.start))
layout.yOfDateTime(cell.startTime)) .clamp(0, double.infinity),
.clamp(0, double.infinity), left: 0,
left: cell.lane * width / cell.laneCount, right: 0,
width: width / cell.laneCount, child: TimeRegionTile(region: region.region),
child: switch (cell) { ),
LaidOutAppointment(:final appointment) => Semantics( for (final cell in laidOut)
button: true, Positioned(
label: A11yLabels.appointmentLabel( top: layout.yOfDateTime(cell.startTime),
subject: appointment.subject, height:
location: appointment.location ?? '', (layout.yOfDateTime(cell.endTime) -
start: appointment.startTime, layout.yOfDateTime(cell.startTime))
end: appointment.endTime, .clamp(0, double.infinity),
crossedOut: isCrossedOut(appointment), left: cell.lane * width / cell.laneCount,
), width: width / cell.laneCount,
onTap: () => onAppointmentTap(appointment), child: switch (cell) {
child: ExcludeSemantics( LaidOutAppointment(:final appointment) => Semantics(
child: GestureDetector( button: true,
behavior: HitTestBehavior.opaque, label: A11yLabels.appointmentLabel(
onTap: () => onAppointmentTap(appointment), subject: appointment.subject,
child: AppointmentTile( location: appointment.location ?? '',
appointment: appointment, start: appointment.startTime,
crossedOut: isCrossedOut(appointment), end: appointment.endTime,
crossedOut: isCrossedOut(appointment),
),
onTap: () => onAppointmentTap(appointment),
child: ExcludeSemantics(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onAppointmentTap(appointment),
child: AppointmentTile(
appointment: appointment,
crossedOut: isCrossedOut(appointment),
),
), ),
), ),
), ),
), LaidOutOverflow(:final appointments) => Semantics(
LaidOutOverflow(:final appointments) => Semantics( button: true,
button: true, label: A11yLabels.moreAppointments(
label: A11yLabels.moreAppointments(appointments.length), appointments.length,
onTap: () => _showOverflowSheet(context, appointments), ),
child: ExcludeSemantics( onTap: () =>
child: GestureDetector( _showOverflowSheet(context, appointments),
behavior: HitTestBehavior.opaque, child: ExcludeSemantics(
onTap: () => child: GestureDetector(
_showOverflowSheet(context, appointments), behavior: HitTestBehavior.opaque,
child: _OverflowTile(count: appointments.length), onTap: () =>
_showOverflowSheet(context, appointments),
child: _OverflowTile(count: appointments.length),
),
), ),
), ),
), },
},
),
if (isToday)
ValueListenableBuilder<DateTime>(
valueListenable: nowNotifier,
builder: (_, now, child) => _CurrentTimeMarker(
now: now,
layout: layout,
theme: theme,
), ),
), if (isToday)
], ValueListenableBuilder<DateTime>(
); valueListenable: nowNotifier,
}, builder: (_, now, child) => _CurrentTimeMarker(
now: now,
layout: layout,
theme: theme,
),
),
],
);
},
),
), ),
), ),
); );
@@ -9,6 +9,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart';
@@ -103,11 +104,7 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
final newTotalWeeks = final newTotalWeeks =
newLastMonday.difference(newFirstMonday).inDays ~/ 7 + 1; newLastMonday.difference(newFirstMonday).inDays ~/ 7 + 1;
final visibleWeekStart = _firstMonday.addDays(_currentWeekIndex * 7); final visibleWeekStart = _firstMonday.addDays(_currentWeekIndex * 7);
final newIndex = visibleWeekStart final newIndex = visibleWeekStart.difference(newFirstMonday).inDays ~/ 7;
.difference(newFirstMonday)
.inDays
~/
7;
final clampedIndex = newIndex.clamp(0, newTotalWeeks - 1); final clampedIndex = newIndex.clamp(0, newTotalWeeks - 1);
final oldController = _pageController; final oldController = _pageController;
_pageController = PageController(initialPage: clampedIndex); _pageController = PageController(initialPage: clampedIndex);
@@ -167,11 +164,16 @@ class CustomWorkWeekCalendarState extends State<CustomWorkWeekCalendar> {
child: child, child: child,
), ),
), ),
child: _DayHeaderStrip( // Die visuelle „MO/14"-Leiste ist für den Screenreader unbrauchbar;
key: ValueKey(visibleWeekStart), // die Tag-Info wird stattdessen als Header in jede Tagesspalte
weekStart: visibleWeekStart, // eingehängt (siehe _DayColumn).
today: _today, child: ExcludeSemantics(
rulerWidth: _rulerWidth, child: _DayHeaderStrip(
key: ValueKey(visibleWeekStart),
weekStart: visibleWeekStart,
today: _today,
rulerWidth: _rulerWidth,
),
), ),
), ),
), ),
+3 -1
View File
@@ -16,15 +16,17 @@ abstract final class A11yLabels {
static const draft = 'Entwurf'; static const draft = 'Entwurf';
static const yourReaction = 'deine Reaktion'; static const yourReaction = 'deine Reaktion';
// Bilder // Bilder / Kontakte
static const profilePicture = 'Profilbild'; static const profilePicture = 'Profilbild';
static const groupPicture = 'Gruppenbild'; static const groupPicture = 'Gruppenbild';
static const group = 'Gruppe';
static const roomPlan = 'Raumplan der Schule als Grafik'; static const roomPlan = 'Raumplan der Schule als Grafik';
// Stundenplan // Stundenplan
static const cancelled = 'Ausfall'; static const cancelled = 'Ausfall';
static const breakTime = 'Pause'; static const breakTime = 'Pause';
static const allDay = 'Ganztägig'; static const allDay = 'Ganztägig';
static const today = 'heute';
/// Beschreibt eine Stundenplan-Kachel für den Screenreader, z.B. /// Beschreibt eine Stundenplan-Kachel für den Screenreader, z.B.
/// „Mathe, Raum 101, 08:0008:45, Ausfall". Zeilenumbrüche in [location] /// „Mathe, Raum 101, 08:0008:45, Ausfall". Zeilenumbrüche in [location]
+12 -7
View File
@@ -22,13 +22,18 @@ Future<void> showDetailsBottomSheet(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: 16 + MediaQuery.viewInsetsOf(sheetContext).bottom, bottom: 16 + MediaQuery.viewInsetsOf(sheetContext).bottom,
), ),
child: Column( // Ohne dieses innere Material malt ListTile-Ink aufs Sheet-Material
mainAxisSize: MainAxisSize.min, // außerhalb des Scrollbereichs und stretcht beim Overscroll nicht mit.
crossAxisAlignment: CrossAxisAlignment.stretch, child: Material(
children: [ type: MaterialType.transparency,
if (header != null) ...[header, const Divider(height: 1)], child: Column(
...children(sheetContext), mainAxisSize: MainAxisSize.min,
], crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (header != null) ...[header, const Divider(height: 1)],
...children(sheetContext),
],
),
), ),
), ),
), ),
+49 -34
View File
@@ -154,13 +154,14 @@ class _DownloadTrayHostState extends State<DownloadTrayHost>
/// The chip only shows when there's something the inline UI can't already /// The chip only shows when there's something the inline UI can't already
/// convey: multiple downloads, a finished/failed one (no inline progress /// convey: multiple downloads, a finished/failed one (no inline progress
/// left), or a lone download whose originating screen the user has left. /// left), or a lone download whose originating screen the user has left.
bool _shouldShowChip(List<DownloadJob> jobs, int epoch) => shouldShowDownloadChip( bool _shouldShowChip(List<DownloadJob> jobs, int epoch) =>
sheetOpen: _sheetOpen, shouldShowDownloadChip(
jobCount: jobs.length, sheetOpen: _sheetOpen,
anySurfaced: jobs.any( jobCount: jobs.length,
(j) => j.isDone || j.isFailed || (_originEpoch[j] ?? epoch) != epoch, anySurfaced: jobs.any(
), (j) => j.isDone || j.isFailed || (_originEpoch[j] ?? epoch) != epoch,
); ),
);
Future<void> _openSheet() async { Future<void> _openSheet() async {
if (_sheetOpen) return; if (_sheetOpen) return;
@@ -311,34 +312,48 @@ class _TrayChip extends StatelessWidget {
label = doneCount == 1 ? 'Download fertig' : '$doneCount fertig'; label = doneCount == 1 ? 'Download fertig' : '$doneCount fertig';
} }
return Material( return Semantics(
color: colors.surfaceContainerHigh, button: true,
elevation: 4, label: '$label, Downloads anzeigen',
borderRadius: BorderRadius.circular(24), onTap: onTap,
shadowColor: Colors.black45, child: ExcludeSemantics(
child: InkWell( child: Material(
borderRadius: BorderRadius.circular(24), color: colors.surfaceContainerHigh,
onTap: onTap, elevation: 4,
child: Padding( borderRadius: BorderRadius.circular(24),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shadowColor: Colors.black45,
child: Row( child: InkWell(
mainAxisSize: MainAxisSize.min, borderRadius: BorderRadius.circular(24),
children: [ onTap: onTap,
leading, child: Padding(
const SizedBox(width: 12), padding: const EdgeInsets.symmetric(
Flexible( horizontal: 16,
child: Text( vertical: 10,
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
), ),
const SizedBox(width: 8), child: Row(
Icon(Icons.expand_less, size: 20, color: colors.onSurfaceVariant), mainAxisSize: MainAxisSize.min,
], children: [
leading,
const SizedBox(width: 12),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 8),
Icon(
Icons.expand_less,
size: 20,
color: colors.onSurfaceVariant,
),
],
),
),
), ),
), ),
), ),
+5 -2
View File
@@ -186,6 +186,7 @@ class _FileViewerState extends State<FileViewer> {
...actions, ...actions,
if (showActionsMenu) if (showActionsMenu)
PopupMenuButton<FileViewingActions>( PopupMenuButton<FileViewingActions>(
tooltip: 'Dateiaktionen',
onSelected: _handleAction, onSelected: _handleAction,
itemBuilder: (context) => _availableActions() itemBuilder: (context) => _availableActions()
.map( .map(
@@ -282,8 +283,10 @@ class _FileViewerState extends State<FileViewer> {
), ),
); );
Widget _buildPdfView() => Widget _buildPdfView() => Scaffold(
Scaffold(appBar: _appbar(), body: DeferredPdfViewer(path: widget.path)); appBar: _appbar(),
body: DeferredPdfViewer(path: widget.path),
);
Widget _buildVideoView() => Scaffold( Widget _buildVideoView() => Scaffold(
appBar: _appbar(), appBar: _appbar(),