From 45beed7548c2987ee4bb576aaeb61566269bcd79 Mon Sep 17 00:00:00 2001 From: Kenton Hamaluik Date: Thu, 19 Mar 2020 00:16:17 -0600 Subject: [PATCH] added ability to group timers when exporting as well --- design/user-stories.md | 2 +- lib/models/project_description_pair.dart | 24 +++ .../dashboard/components/StoppedTimers.dart | 11 +- lib/screens/export/ExportScreen.dart | 161 ++++++------------ 4 files changed, 82 insertions(+), 116 deletions(-) create mode 100644 lib/models/project_description_pair.dart diff --git a/design/user-stories.md b/design/user-stories.md index 6fe5b8ef..675cdf77 100644 --- a/design/user-stories.md +++ b/design/user-stories.md @@ -49,7 +49,7 @@ - [x] Optionally filtered by: 1. Date 2. Project - - [ ] Optionally grouped by timer descriptions on a daily basis + - [x] Optionally grouped by timer descriptions on a daily basis - [x] Export format should be able to include: - [x] Timer description - [x] Timer project diff --git a/lib/models/project_description_pair.dart b/lib/models/project_description_pair.dart new file mode 100644 index 00000000..96feccdf --- /dev/null +++ b/lib/models/project_description_pair.dart @@ -0,0 +1,24 @@ +// Copyright 2020 Kenton Hamaluik +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:equatable/equatable.dart'; + +class ProjectDescriptionPair extends Equatable { + final int project; + final String description; + + ProjectDescriptionPair(this.project, this.description); + + @override List get props => [project, description]; +} \ No newline at end of file diff --git a/lib/screens/dashboard/components/StoppedTimers.dart b/lib/screens/dashboard/components/StoppedTimers.dart index 15267bca..bef35b14 100644 --- a/lib/screens/dashboard/components/StoppedTimers.dart +++ b/lib/screens/dashboard/components/StoppedTimers.dart @@ -14,24 +14,15 @@ import 'dart:collection'; -import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:timecop/blocs/timers/bloc.dart'; +import 'package:timecop/models/project_description_pair.dart'; import 'package:timecop/models/timer_entry.dart'; import 'package:timecop/screens/dashboard/components/GroupedStoppedTimersRow.dart'; import 'StoppedTimerRow.dart'; -class ProjectDescriptionPair extends Equatable { - final int project; - final String description; - - ProjectDescriptionPair(this.project, this.description); - - @override List get props => [project, description]; -} - class DayGrouping { final DateTime date; List entries = []; diff --git a/lib/screens/export/ExportScreen.dart b/lib/screens/export/ExportScreen.dart index 5da30499..e9124bbf 100644 --- a/lib/screens/export/ExportScreen.dart +++ b/lib/screens/export/ExportScreen.dart @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:collection'; import 'dart:io'; import 'package:path/path.dart' as p; import 'package:flutter_share/flutter_share.dart'; @@ -31,6 +32,7 @@ import 'package:timecop/blocs/timers/bloc.dart'; import 'package:timecop/components/ProjectColour.dart'; import 'package:timecop/l10n.dart'; import 'package:timecop/models/project.dart'; +import 'package:timecop/models/project_description_pair.dart'; import 'package:timecop/models/timer_entry.dart'; class ExportScreen extends StatefulWidget { @@ -54,7 +56,7 @@ class _ExportScreenState extends State { List selectedProjects = []; static DateFormat _dateFormat = DateFormat("EE, MMM d, yyyy"); static DateFormat _exportDateFormat = DateFormat.yMd(); - final GlobalKey _scaffoldKey = new GlobalKey(); + final GlobalKey _scaffoldKey = GlobalKey(); @override void initState() { @@ -106,30 +108,6 @@ class _ExportScreenState extends State { ), body: ListView( children: [ - /*Padding( - padding: EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 4.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - L10N.of(context).tr.options, - style: TextStyle( - color: Theme.of(context).accentColor, - fontWeight: FontWeight.w700 - ) - ), - ], - ), - ), - BlocBuilder( - bloc: settingsBloc, - builder: (BuildContext context, SettingsState settingsState) => SwitchListTile( - title: Text(L10N.of(context).tr.groupTimers), - value: settingsState.exportGroupTimers, - onChanged: (bool value) => settingsBloc.add(SetExportGroupTimers(value)), - activeColor: Theme.of(context).accentColor, - ), - ),*/ ExpansionTile( title: Text( L10N.of(context).tr.filter, @@ -331,41 +309,26 @@ class _ExportScreenState extends State { ) ).toList(), ), - /*ListTile( + ExpansionTile( title: Text( - L10N.of(context).tr.includeProjects, + L10N.of(context).tr.options, style: TextStyle( color: Theme.of(context).accentColor, - fontSize: Theme.of(context).textTheme.body1.fontSize, fontWeight: FontWeight.w700 ) ), - trailing: Checkbox( - tristate: true, - value: selectedProjects.length == projectsBloc.state.projects.length + 1 - ? true - : (selectedProjects.isEmpty - ? false - : null), - activeColor: Theme.of(context).accentColor, - onChanged: (_) => setState(() { - if(selectedProjects.length == projectsBloc.state.projects.length + 1) { - selectedProjects.clear(); - } - else { - selectedProjects = [null].followedBy(projectsBloc.state.projects.map((p) => Project.clone(p))).toList(); - } - }), - ), - onTap: () => setState(() { - if(selectedProjects.length == projectsBloc.state.projects.length + 1) { - selectedProjects.clear(); - } - else { - selectedProjects = [null].followedBy(projectsBloc.state.projects.map((p) => Project.clone(p))).toList(); - } - }) - ),*/ + children: [ + BlocBuilder( + bloc: settingsBloc, + builder: (BuildContext context, SettingsState settingsState) => SwitchListTile( + title: Text(L10N.of(context).tr.groupTimers), + value: settingsState.exportGroupTimers, + onChanged: (bool value) => settingsBloc.add(SetExportGroupTimers(value)), + activeColor: Theme.of(context).accentColor, + ), + ) + ], + ), ] .toList(), ), @@ -412,66 +375,54 @@ class _ExportScreenState extends State { headers.add(L10N.of(context).tr.timeH); } - // TODO: this isn't working.. - /*List filteredTimers; + List filteredTimers = timers.state.timers + .where((t) => t.endTime != null) + .where((t) => selectedProjects.any((p) => p?.id == t.projectID)) + .where((t) => _startDate == null ? true : t.startTime.isAfter(_startDate)) + .where((t) => _endDate == null ? true : t.endTime.isBefore(_endDate)) + .toList(); + filteredTimers.sort((a, b) => a.startTime.compareTo(b.startTime)); + + // group similar timers if that's what you're in to if(settingsBloc.state.exportGroupTimers && !(settingsBloc.state.exportIncludeStartTime || settingsBloc.state.exportIncludeEndTime)) { - print("grouping timers..."); filteredTimers = timers.state.timers .where((t) => t.endTime != null) .where((t) => selectedProjects.any((p) => p?.id == t.projectID)) - .map((t) => TimerEntry.clone(t)) - .fold([], (List days, TimerEntry t) { - // find which day this timer belongs to - DayGroup currentDay = days.firstWhere((DayGroup day) => (day.date.year == t.startTime.year && day.date.month == t.startTime.month && day.date.day == t.startTime.day)); - if(currentDay == null) { - currentDay = DayGroup(t.startTime); - days.add(currentDay); - } + .where((t) => _startDate == null ? true : t.startTime.isAfter(_startDate)) + .where((t) => _endDate == null ? true : t.endTime.isBefore(_endDate)) + .toList(); + filteredTimers.sort((a, b) => a.startTime.compareTo(b.startTime)); - // determine if there are any timers this day that match it - TimerEntry match = currentDay.timers.firstWhere((TimerEntry et) => et.projectID == t.projectID && et.description == t.description); - if(match != null) { - // if it does match, just extend its end time - currentDay.timers = currentDay.timers.map((TimerEntry et) { - if(et.id == match.id) { - return TimerEntry.clone( - match, - endTime: match.endTime.add((t.endTime.difference(t.startTime))) - ); - } - else { - return et; - } - }).toList(); - } - else { - currentDay.timers.add(t); - } + // now start grouping those suckers + LinkedHashMap>> derp = LinkedHashMap(); + for(TimerEntry timer in filteredTimers) { + String date = _exportDateFormat.format(timer.startTime); + LinkedHashMap> pairedEntries = derp.putIfAbsent(date, () => LinkedHashMap()); + List pairedList = pairedEntries.putIfAbsent(ProjectDescriptionPair(timer.projectID, timer.description), () => []); + pairedList.add(timer); + } - return days; - }) - .expand((DayGroup day) => day.timers) - .toList(); - } - else { - filteredTimers = - timers.state.timers - .where((t) => t.endTime != null) - .where((t) => selectedProjects.any((p) => p?.id == t.projectID)) - .toList(); - }*/ + // ok, now they're grouped based on date, then combined project + description pairs + // time to get them back into a flat list + filteredTimers = derp.values.expand((LinkedHashMap> pairedEntries) { + return pairedEntries.values.map((List groupedEntries) { + assert(groupedEntries.isNotEmpty); + + // not a grouped entry + if(groupedEntries.length == 1) return groupedEntries[0]; - //print('start date: ' + (_startDate == null ? "null" : _startDate.toUtc().toIso8601String())); - //print('end date: ' + (_endDate == null ? "null" : _endDate.toUtc().toIso8601String())); + // yes a group entry, build a dummy timer entry + Duration totalTime = groupedEntries.fold(Duration(), (Duration d, TimerEntry t) => d + t.endTime.difference(t.startTime)); + return TimerEntry.clone(groupedEntries[0], endTime: groupedEntries[0].startTime.add(totalTime)); + }); + }) + .toList(); + } List> data = >[headers] .followedBy( - timers.state.timers - .where((t) => t.endTime != null) - .where((t) => selectedProjects.any((p) => p?.id == t.projectID)) - .where((t) => _startDate == null ? true : t.startTime.isAfter(_startDate)) - .where((t) => _endDate == null ? true : t.endTime.isBefore(_endDate)) + filteredTimers .map( (timer) { List row = []; @@ -501,8 +452,8 @@ class _ExportScreenState extends State { ) ).toList(); String csv = ListToCsvConverter().convert(data); - //print('CSV:'); - //print(csv); + print('CSV:'); + print(csv); Directory directory; if (Platform.isAndroid) {