diff --git a/lib/screens/timer/TimerEditor.dart b/lib/screens/timer/TimerEditor.dart index f5760fa..a85902b 100644 --- a/lib/screens/timer/TimerEditor.dart +++ b/lib/screens/timer/TimerEditor.dart @@ -29,9 +29,9 @@ import 'package:timecop/blocs/settings/settings_bloc.dart'; import 'package:timecop/blocs/timers/bloc.dart'; import 'package:timecop/components/ProjectColour.dart'; import 'package:timecop/l10n.dart'; +import 'package:timecop/models/clone_time.dart'; import 'package:timecop/models/project.dart'; import 'package:timecop/models/timer_entry.dart'; -import 'package:timecop/models/clone_time.dart'; import 'package:timecop/themes.dart'; enum _DateTimeMenuItems { now } @@ -147,248 +147,225 @@ class _TimerEditorState extends State { ), body: Form( key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - BlocBuilder( - builder: (BuildContext context, ProjectsState projectsState) => + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints.tightFor( + height: MediaQuery.of(context).size.height - + (MediaQuery.of(context).padding.top + kToolbarHeight)), + child: Container( + height: double.infinity, + child: Column( + // mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + BlocBuilder( + builder: (BuildContext context, + ProjectsState projectsState) => + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: DropdownButton( + value: (_project?.archived ?? true) + ? null + : _project, + underline: const SizedBox(), + onChanged: (Project? newProject) { + setState(() { + _project = newProject; + }); + }, + items: >[ + DropdownMenuItem( + value: null, + child: Row( + children: [ + const ProjectColour(project: null), + Padding( + padding: const EdgeInsets.fromLTRB( + 8.0, 0, 0, 0), + child: Text( + L10N.of(context).tr.noProject, + style: TextStyle( + color: ThemeUtil + .getOnBackgroundLighter( + context))), + ), + ], + ), + ) + ] + .followedBy(projectsState.projects + .where((p) => !p.archived) + .map((Project project) => + DropdownMenuItem( + value: project, + child: Row( + children: [ + ProjectColour( + project: project, + ), + Padding( + padding: + const EdgeInsets.fromLTRB( + 8.0, 0, 0, 0), + child: Text(project.name), + ), + ], + ), + ))) + .toList(), + )), + ), Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: DropdownButton( - value: (_project?.archived ?? true) ? null : _project, - underline: const SizedBox(), - onChanged: (Project? newProject) { - setState(() { - _project = newProject; - }); - }, - items: >[ - DropdownMenuItem( - value: null, - child: Row( - children: [ - const ProjectColour(project: null), - Padding( - padding: - const EdgeInsets.fromLTRB(8.0, 0, 0, 0), - child: Text(L10N.of(context).tr.noProject, - style: TextStyle( - color: - ThemeUtil.getOnBackgroundLighter( - context))), - ), - ], + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: settingsBloc.state.autocompleteDescription + ? TypeAheadField( + direction: AxisDirection.down, + textFieldConfiguration: TextFieldConfiguration( + controller: _descriptionController, + autocorrect: true, + decoration: InputDecoration( + labelText: L10N.of(context).tr.description, + hintText: L10N.of(context).tr.whatWereYouDoing, + ), ), + noItemsFoundBuilder: (context) => ListTile( + title: Text(L10N.of(context).tr.noItemsFound), + enabled: false), + itemBuilder: (BuildContext context, String? desc) => + ListTile(title: Text(desc!)), + onSuggestionSelected: (String? description) => + _descriptionController!.text = description!, + suggestionsCallback: (pattern) async { + if (pattern.length < 2) return []; + + List descriptions = timers.state.timers + .where((timer) => timer.description != null) + .where((timer) => !(_projectsBloc + .getProjectByID(timer.projectID) + ?.archived == + true)) + .where((timer) => + timer.description + ?.toLowerCase() + .contains(pattern.toLowerCase()) ?? + false) + .map((timer) => timer.description) + .toSet() + .toList(); + return descriptions; + }, ) - ] - .followedBy(projectsState.projects - .where((p) => !p.archived) - .map((Project project) => - DropdownMenuItem( - value: project, - child: Row( - children: [ - ProjectColour( - project: project, - ), - Padding( - padding: const EdgeInsets.fromLTRB( - 8.0, 0, 0, 0), - child: Text(project.name), - ), - ], - ), - ))) - .toList(), - )), - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: settingsBloc.state.autocompleteDescription - ? TypeAheadField( - direction: AxisDirection.down, - textFieldConfiguration: TextFieldConfiguration( - controller: _descriptionController, - autocorrect: true, - decoration: InputDecoration( - labelText: L10N.of(context).tr.description, - hintText: L10N.of(context).tr.whatWereYouDoing, - ), - ), - noItemsFoundBuilder: (context) => ListTile( - title: Text(L10N.of(context).tr.noItemsFound), - enabled: false), - itemBuilder: (BuildContext context, String? desc) => - ListTile(title: Text(desc!)), - onSuggestionSelected: (String? description) => - _descriptionController!.text = description!, - suggestionsCallback: (pattern) async { - if (pattern.length < 2) return []; - - List descriptions = timers.state.timers - .where((timer) => timer.description != null) - .where((timer) => !(_projectsBloc - .getProjectByID(timer.projectID) - ?.archived == - true)) - .where((timer) => - timer.description - ?.toLowerCase() - .contains(pattern.toLowerCase()) ?? - false) - .map((timer) => timer.description) - .toSet() - .toList(); - return descriptions; - }, - ) - : TextFormField( - controller: _descriptionController, - autocorrect: true, - decoration: InputDecoration( - labelText: L10N.of(context).tr.description, - hintText: L10N.of(context).tr.whatWereYouDoing, - ), - ), - ), - ListTile( - title: Row(mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: Text(L10N.of(context).tr.startTime), - ), - Expanded( - flex: 3, - child: Text( - dateFormat.format(_startTime), - textAlign: TextAlign.right, - style: theme.textTheme.bodyMedium, - )), - PopupMenuButton<_DateTimeMenuItems>( - onSelected: (menuItem) { - switch (menuItem) { - case _DateTimeMenuItems.now: - _oldStartTime = _startTime; - _oldEndTime = _endTime; - setStartTime(DateTime.now()); - break; - } - }, - itemBuilder: (_) => [ - PopupMenuItem( - value: _DateTimeMenuItems.now, - child: Text(L10N.of(context).tr.setToCurrentTime), - ) - ]), - ]), - onTap: () async { - _oldStartTime = _startTime.clone(); - _oldEndTime = _endTime?.clone(); - DateTime? newStartTime = - await dt.DatePicker.showDateTimePicker(context, - currentTime: _startTime, - maxTime: _endTime == null ? DateTime.now() : null, - onChanged: (DateTime dt) => setStartTime(dt), - onConfirm: (DateTime dt) => setStartTime(dt), - theme: dt.DatePickerTheme( - cancelStyle: theme.textTheme.labelLarge!, - doneStyle: theme.textTheme.labelLarge!, - itemStyle: theme.textTheme.bodyMedium!, - backgroundColor: theme.colorScheme.surface, - )); - - // if the user cancelled, this should be null - if (newStartTime == null) { - setState(() { - _startTime = _oldStartTime!; - _endTime = _oldEndTime; - }); - } - }, - ), - ListTile( - title: Row(children: [ - Expanded(child: Text(L10N.of(context).tr.endTime)), - Expanded( - flex: 3, - child: Text( - _endTime == null ? "--" : dateFormat.format(_endTime!), - textAlign: TextAlign.right, - style: theme.textTheme.bodyMedium, - )), - if (_endTime != null) - IconButton( - visualDensity: VisualDensity.compact, - padding: const EdgeInsetsDirectional.only(start: 16), - tooltip: L10N.of(context).tr.remove, - icon: const Icon(FontAwesomeIcons.circleMinus), - onPressed: () { - setState(() { - _endTime = null; - }); - }, + : TextFormField( + controller: _descriptionController, + autocorrect: true, + decoration: InputDecoration( + labelText: L10N.of(context).tr.description, + hintText: L10N.of(context).tr.whatWereYouDoing, + ), + ), ), - PopupMenuButton<_DateTimeMenuItems>( - onSelected: (menuItem) { - switch (menuItem) { - case _DateTimeMenuItems.now: - setState(() => _endTime = DateTime.now()); + ListTile( + title: Row(mainAxisSize: MainAxisSize.min, children: [ + Expanded( + child: Text(L10N.of(context).tr.startTime), + ), + Expanded( + flex: 3, + child: Text( + dateFormat.format(_startTime), + textAlign: TextAlign.right, + style: theme.textTheme.bodyMedium, + )), + PopupMenuButton<_DateTimeMenuItems>( + onSelected: (menuItem) { + switch (menuItem) { + case _DateTimeMenuItems.now: + _oldStartTime = _startTime; + _oldEndTime = _endTime; + setStartTime(DateTime.now()); + break; + } + }, + itemBuilder: (_) => [ + PopupMenuItem( + value: _DateTimeMenuItems.now, + child: Text( + L10N.of(context).tr.setToCurrentTime), + ) + ]), + ]), + onTap: () async { + _oldStartTime = _startTime.clone(); + _oldEndTime = _endTime?.clone(); + DateTime? newStartTime = + await dt.DatePicker.showDateTimePicker(context, + currentTime: _startTime, + maxTime: _endTime == null ? DateTime.now() : null, + onChanged: (DateTime dt) => setStartTime(dt), + onConfirm: (DateTime dt) => setStartTime(dt), + theme: dt.DatePickerTheme( + cancelStyle: theme.textTheme.labelLarge!, + doneStyle: theme.textTheme.labelLarge!, + itemStyle: theme.textTheme.bodyMedium!, + backgroundColor: theme.colorScheme.surface, + )); + + // if the user cancelled, this should be null + if (newStartTime == null) { + setState(() { + _startTime = _oldStartTime!; + _endTime = _oldEndTime; + }); } }, - itemBuilder: (_) => [ - PopupMenuItem( - value: _DateTimeMenuItems.now, - child: Text(L10N.of(context).tr.setToCurrentTime), - ) - ]) - ]), - onTap: () async { - _oldEndTime = _endTime?.clone(); - DateTime? newEndTime = await dt.DatePicker.showDateTimePicker( - context, - currentTime: _endTime, - minTime: _startTime, - onChanged: (DateTime dt) => setState(() => _endTime = dt), - onConfirm: (DateTime dt) => setState(() => _endTime = dt), - theme: dt.DatePickerTheme( - cancelStyle: theme.textTheme.labelLarge!, - doneStyle: theme.textTheme.labelLarge!, - itemStyle: theme.textTheme.bodyMedium!, - backgroundColor: theme.colorScheme.surface, - )); - - // if the user cancelled, this should be null - if (newEndTime == null) { - setState(() { - _endTime = _oldEndTime; - }); - } - }, - ), - StreamBuilder( - initialData: DateTime.now(), - stream: _updateTimerStreamController.stream, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - final duration = _endTime == null - ? snapshot.data!.difference(_startTime) - : _endTime!.difference(_startTime); - return ListTile( - title: Text(L10N.of(context).tr.duration), - trailing: Text( - TimerEntry.formatDuration(duration), - style: theme.textTheme.bodyMedium?.copyWith( - fontFeatures: [const FontFeature.tabularFigures()]), ), - onTap: _endTime != null - ? () async { - _oldEndTime = _endTime!.clone(); - DateTime? newEndTime = await dt.DatePicker.showPicker( - context, - pickerModel: _DurationPickerModel( - startDateTime: _startTime, - endTime: _endTime!), + ListTile( + title: Row(children: [ + Expanded(child: Text(L10N.of(context).tr.endTime)), + Expanded( + flex: 3, + child: Text( + _endTime == null + ? "--" + : dateFormat.format(_endTime!), + textAlign: TextAlign.right, + style: theme.textTheme.bodyMedium, + )), + if (_endTime != null) + IconButton( + visualDensity: VisualDensity.compact, + padding: const EdgeInsetsDirectional.only(start: 16), + tooltip: L10N.of(context).tr.remove, + icon: const Icon(FontAwesomeIcons.circleMinus), + onPressed: () { + setState(() { + _endTime = null; + }); + }, + ), + PopupMenuButton<_DateTimeMenuItems>( + onSelected: (menuItem) { + switch (menuItem) { + case _DateTimeMenuItems.now: + setState(() => _endTime = DateTime.now()); + } + }, + itemBuilder: (_) => [ + PopupMenuItem( + value: _DateTimeMenuItems.now, + child: Text( + L10N.of(context).tr.setToCurrentTime), + ) + ]) + ]), + onTap: () async { + _oldEndTime = _endTime?.clone(); + DateTime? newEndTime = + await dt.DatePicker.showDateTimePicker(context, + currentTime: _endTime, + minTime: _startTime, onChanged: (DateTime dt) => setState(() => _endTime = dt), onConfirm: (DateTime dt) => @@ -400,28 +377,80 @@ class _TimerEditorState extends State { backgroundColor: theme.colorScheme.surface, )); - // if the user cancelled, this should be null - if (newEndTime == null) { - setState(() { - _endTime = _oldEndTime; - }); - } - } - : null, - ); - }, - ), - ListTile( - title: Text(L10N.of(context).tr.notes), - onTap: () async => await _editNotes(context), - ), - Expanded( - child: InkWell( + // if the user cancelled, this should be null + if (newEndTime == null) { + setState(() { + _endTime = _oldEndTime; + }); + } + }, + ), + StreamBuilder( + initialData: DateTime.now(), + stream: _updateTimerStreamController.stream, + builder: (BuildContext context, + AsyncSnapshot snapshot) { + final duration = _endTime == null + ? snapshot.data!.difference(_startTime) + : _endTime!.difference(_startTime); + return ListTile( + title: Text(L10N.of(context).tr.duration), + trailing: Text( + TimerEntry.formatDuration(duration), + style: theme.textTheme.bodyMedium?.copyWith( + fontFeatures: [ + const FontFeature.tabularFigures() + ]), + ), + onTap: _endTime != null + ? () async { + _oldEndTime = _endTime!.clone(); + DateTime? newEndTime = + await dt.DatePicker.showPicker(context, + pickerModel: _DurationPickerModel( + startDateTime: _startTime, + endTime: _endTime!), + onChanged: (DateTime dt) => + setState(() => _endTime = dt), + onConfirm: (DateTime dt) => + setState(() => _endTime = dt), + theme: dt.DatePickerTheme( + cancelStyle: + theme.textTheme.labelLarge!, + doneStyle: + theme.textTheme.labelLarge!, + itemStyle: + theme.textTheme.bodyMedium!, + backgroundColor: + theme.colorScheme.surface, + )); + + // if the user cancelled, this should be null + if (newEndTime == null) { + setState(() { + _endTime = _oldEndTime; + }); + } + } + : null, + ); + }, + ), + ListTile( + title: Text(L10N.of(context).tr.notes), onTap: () async => await _editNotes(context), - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: Markdown(data: _notes!)))), - ], + ), + Expanded( + child: InkWell( + onTap: () async => await _editNotes(context), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: Markdown(data: _notes!))), + ), + ], + ), + ), + ), ), ), floatingActionButton: FloatingActionButton(