Skip to content

Commit

Permalink
added ability to group timers when exporting as well
Browse files Browse the repository at this point in the history
  • Loading branch information
hamaluik committed Mar 19, 2020
1 parent cda44c0 commit 45beed7
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 116 deletions.
2 changes: 1 addition & 1 deletion design/user-stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions lib/models/project_description_pair.dart
Original file line number Diff line number Diff line change
@@ -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<Object> get props => [project, description];
}
11 changes: 1 addition & 10 deletions lib/screens/dashboard/components/StoppedTimers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object> get props => [project, description];
}

class DayGrouping {
final DateTime date;
List<TimerEntry> entries = [];
Expand Down
161 changes: 56 additions & 105 deletions lib/screens/export/ExportScreen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand All @@ -54,7 +56,7 @@ class _ExportScreenState extends State<ExportScreen> {
List<Project> selectedProjects = [];
static DateFormat _dateFormat = DateFormat("EE, MMM d, yyyy");
static DateFormat _exportDateFormat = DateFormat.yMd();
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

@override
void initState() {
Expand Down Expand Up @@ -106,30 +108,6 @@ class _ExportScreenState extends State<ExportScreen> {
),
body: ListView(
children: <Widget>[
/*Padding(
padding: EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
L10N.of(context).tr.options,
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.w700
)
),
],
),
),
BlocBuilder<SettingsBloc, SettingsState>(
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,
Expand Down Expand Up @@ -331,41 +309,26 @@ class _ExportScreenState extends State<ExportScreen> {
)
).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 = <Project>[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 = <Project>[null].followedBy(projectsBloc.state.projects.map((p) => Project.clone(p))).toList();
}
})
),*/
children: <Widget>[
BlocBuilder<SettingsBloc, SettingsState>(
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(),
),
Expand Down Expand Up @@ -412,66 +375,54 @@ class _ExportScreenState extends State<ExportScreen> {
headers.add(L10N.of(context).tr.timeH);
}

// TODO: this isn't working..
/*List<TimerEntry> filteredTimers;
List<TimerEntry> 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(<DayGroup>[], (List<DayGroup> 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<String, LinkedHashMap<ProjectDescriptionPair, List<TimerEntry>>> derp = LinkedHashMap();
for(TimerEntry timer in filteredTimers) {
String date = _exportDateFormat.format(timer.startTime);
LinkedHashMap<ProjectDescriptionPair, List<TimerEntry>> pairedEntries = derp.putIfAbsent(date, () => LinkedHashMap());
List<TimerEntry> pairedList = pairedEntries.putIfAbsent(ProjectDescriptionPair(timer.projectID, timer.description), () => <TimerEntry>[]);
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<ProjectDescriptionPair, List<TimerEntry>> pairedEntries) {
return pairedEntries.values.map((List<TimerEntry> 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<List<String>> data = <List<String>>[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<String> row = [];
Expand Down Expand Up @@ -501,8 +452,8 @@ class _ExportScreenState extends State<ExportScreen> {
)
).toList();
String csv = ListToCsvConverter().convert(data);
//print('CSV:');
//print(csv);
print('CSV:');
print(csv);

Directory directory;
if (Platform.isAndroid) {
Expand Down

0 comments on commit 45beed7

Please sign in to comment.