• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

asartalo / diligence / 10052034072

23 Jul 2024 03:09AM UTC coverage: 91.074%. First build
10052034072

Pull #59

github

web-flow
Merge 24d92ed38 into cd0746447
Pull Request #59: chore: 0.1.9 release merge and prep

34 of 37 new or added lines in 6 files covered. (91.89%)

2153 of 2364 relevant lines covered (91.07%)

2.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.94
/lib/services/diligent/focus_queue_manager.dart
1
// Diligence - A Task Management App
2
//
3
// Copyright (C) 2024 Wayne Duran <asartalo@gmail.com>
4
//
5
// This program is free software: you can redistribute it and/or modify it under
6
// the terms of the GNU General Public License as published by the Free Software
7
// Foundation, either version 3 of the License, or (at your option) any later
8
// version.
9
//
10
// This program is distributed in the hope that it will be useful, but WITHOUT
11
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License along with
15
// this program. If not, see <https://www.gnu.org/licenses/>.
16

17
import 'dart:async';
18

19
import 'package:collection/collection.dart';
20
import 'package:sqlite_async/sqlite_async.dart';
21

22
import '../../models/task.dart';
23
import '../../models/task_list.dart';
24
import '../../utils/clock.dart';
25
import '../diligent.dart';
26
import 'diligent_event_register.dart';
27
import 'task_db.dart';
28
import 'task_events/added_tasks_event.dart';
29
import 'task_events/deleted_task_event.dart';
30
import 'task_events/toggled_tasks_done_event.dart';
31
import 'task_events/updated_task_event.dart';
32
import 'task_fields.dart';
33

34
class FocusQueueEvent {
35
  final DateTime at;
36

37
  FocusQueueEvent(this.at);
3✔
38
}
39

40
class FocusQueueManager extends TaskDb implements DiligentEventRegister {
41
  @override
42
  final SqliteDatabase db;
43
  final _controller = StreamController<FocusQueueEvent>();
44
  Stream<FocusQueueEvent>? _updateEventStream;
45

46
  final Clock clock;
47

48
  FocusQueueManager({required this.db, required this.clock});
4✔
49

50
  Stream<FocusQueueEvent> get updateEventStream {
1✔
51
    if (_updateEventStream != null) {
1✔
NEW
52
      return _updateEventStream as Stream<FocusQueueEvent>;
×
53
    } else {
54
      final theStream = _controller.stream.asBroadcastStream();
3✔
55
      _updateEventStream = theStream;
1✔
56
      return theStream;
57
    }
58
  }
59

60
  Future<TaskList> focusQueue({int? limit}) async {
2✔
61
    final rows = await db.getAll(
4✔
62
      '''
63
      SELECT tasks.*, focusQueue.position
64
      FROM tasks
65
      JOIN focusQueue ON focusQueue.taskId = tasks.id
66
      ORDER BY focusQueue.position DESC
67
      ${limit != null && limit > 0 ? 'LIMIT $limit' : ''}
4✔
68
      ''',
2✔
69
    );
70

71
    return rows.map(taskFromRow).toList();
6✔
72
  }
73

74
  void _broadcastUpdate() {
3✔
75
    _controller.sink.add(FocusQueueEvent(clock.now()));
18✔
76
  }
77

78
  Future<int> getFocusedCount() async {
2✔
79
    final result = await db.get(
4✔
80
      '''
81
      SELECT COUNT(focusQueue.taskId) as count
82
      FROM focusQueue
83
      ''',
84
    );
85

86
    return result['count'] as int;
2✔
87
  }
88

89
  Future<void> focus(Task task, {int position = 0}) async {
3✔
90
    await db.writeTransaction((tx) async {
9✔
91
      await focusInContext([task], tx, position: position);
6✔
92
    });
93
  }
94

95
  Future<void> focusTasks(TaskList tasks, {int position = 0}) async {
1✔
96
    await db.writeTransaction((tx) async {
3✔
97
      await focusInContext(tasks, tx, position: position);
1✔
98
    });
99
  }
100

101
  Future<void> focusInContext(
3✔
102
    TaskList tasks,
103
    SqliteWriteContext tx, {
104
    int position = 0,
105
  }) {
106
    final taskIds = tasks.map(getTaskId).toList();
9✔
107

108
    return focusByIdsInContext(taskIds, tx, position: position);
3✔
109
  }
110

111
  Future<void> focusByIdsInContext(
3✔
112
    List<int> taskIds,
113
    SqliteWriteContext tx, {
114
    int position = 0,
115
  }) async {
116
    final taskLeaves = await leavesByIdsInContext(
3✔
117
      taskIds,
118
      tx,
119
      done: false,
120
    );
121
    final toAdd =
122
        (taskLeaves.isEmpty ? taskIds : taskLeaves.map(getTaskId).toList())
9✔
123
            .reversed
3✔
124
            .toList();
3✔
125

126
    await unfocusByIdsInContext(toAdd, tx);
3✔
127

128
    if (position == 0) {
3✔
129
      // get max value of position in focusQueue
130
      final result = await tx.get(
3✔
131
        'SELECT COALESCE(MAX(position) + 1, 0) as position FROM focusQueue',
132
      );
133
      final maxPosition = result['position'] as int;
3✔
134
      await tx.executeBatch(
3✔
135
        '''
136
        INSERT INTO focusQueue (taskId, position) VALUES (?, ?)
137
        ''',
138
        toAdd.mapIndexed((i, taskId) => [taskId, i + maxPosition]).toList(),
15✔
139
      );
140
    } else {
141
      final lengthResult = await tx.get(
1✔
142
        'SELECT count(taskId) as length FROM focusQueue',
143
      );
144
      final length = lengthResult['length'] as int;
1✔
145
      final realPosition = length - position;
1✔
146
      await tx.execute(
1✔
147
        '''
148
        UPDATE focusQueue
149
        SET position = position + ?
150
        WHERE position >= ?
151
        ''',
152
        [toAdd.length + 1, realPosition],
3✔
153
      );
154
      await tx.executeBatch(
1✔
155
        '''
156
          INSERT INTO focusQueue (taskId, position) VALUES (?, ?)
157
          ''',
158
        toAdd.indexed.map((item) {
3✔
159
          final (index, taskId) = item;
160

161
          return [taskId, realPosition + index];
2✔
162
        }).toList(),
1✔
163
      );
164
    }
165

166
    _broadcastUpdate();
3✔
167
  }
168

169
  Future<bool> isFocused(int? id, SqliteReadContext tx) async {
3✔
170
    if (id == null) return false;
171
    final result = await tx.get(
3✔
172
      'SELECT count(taskId) as count FROM focusQueue WHERE taskId = ?',
173
      [id],
3✔
174
    );
175

176
    return result['count'] as int > 0;
6✔
177
  }
178

179
  Future<void> unfocus(Task task) async {
2✔
180
    await unfocusInContext([task], db);
6✔
181
  }
182

183
  Future<void> unfocusInContext(TaskList tasks, SqliteWriteContext tx) =>
2✔
184
      unfocusByIdsInContext(tasks.map(getTaskId).toList(), tx);
8✔
185

186
  Future<void> unfocusByIdsInContext(
3✔
187
    List<int> ids,
188
    SqliteWriteContext tx,
189
  ) async {
190
    await tx.execute(
3✔
191
      'DELETE FROM focusQueue WHERE taskId IN (${questionMarks(ids.length)})',
9✔
192
      ids,
193
    );
194
    await _normalizeFocusQueuePositions(tx);
3✔
195
    _broadcastUpdate();
3✔
196
  }
197

198
  Future<void> _normalizeFocusQueuePositions(SqliteWriteContext tx) async {
3✔
199
    await tx.execute(
3✔
200
      '''
201
      UPDATE focusQueue
202
      SET position = p.newPosition
203
      FROM (
204
        SELECT taskId, position,
205
          (row_number() OVER (ORDER BY position) - 1) AS newPosition
206
        FROM focusQueue
207
        ORDER BY position
208
      ) AS p
209
      WHERE p.taskId = focusQueue.taskId
210
      ''',
211
    );
212
  }
213

214
  // TODO: Is there a better way to do this?
215
  Future<void> reprioritizeInFocusQueue(Task task, int position) async {
2✔
216
    await db.writeTransaction((tx) async {
6✔
217
      final lengthResult = await tx.get(
2✔
218
        'SELECT count(taskId) as length FROM focusQueue',
219
      );
220
      final length = lengthResult['length'] as int;
2✔
221
      final realPosition = length - position;
2✔
222
      await tx.execute(
2✔
223
        '''
224
        UPDATE focusQueue
225
        SET position = position + ?
226
        WHERE position >= ?
227
        ''',
228
        [1, realPosition],
2✔
229
      );
230
      await tx.execute(
2✔
231
        '''
232
        UPDATE focusQueue
233
        SET position = ?
234
        WHERE taskId = ?
235
        ''',
236
        [realPosition, task.id],
4✔
237
      );
238
      await _normalizeFocusQueuePositions(tx);
2✔
239
    });
240

241
    _broadcastUpdate();
2✔
242
  }
243

244
  Future<void> handleAddedTasksEvent(AddedTasksEvent event) async {
3✔
245
    final AddedTasksEvent(:parentId, :tasks, :tx) = event;
9✔
246
    if (parentId is int && await isFocused(parentId, tx)) {
6✔
247
      await unfocusByIdsInContext([parentId], tx);
2✔
248
      await focusInContext(tasks, tx);
1✔
249
    }
250
  }
251

252
  Future<void> handleUpdatedTaskEvent(UpdatedTaskEvent event) async {
2✔
253
    final UpdatedTaskEvent(:modified, :tx) = event;
4✔
254
    if (modified.hasToggledDone() && modified.done == true) {
6✔
255
      await unfocusInContext([modified], tx);
4✔
256
    } else {
257
      if (await isFocused(modified.id, tx)) {
4✔
258
        _broadcastUpdate();
1✔
259
      }
260
    }
261
  }
262

263
  Future<void> handleToggledTasksDoneEvent(ToggledTasksDoneEvent event) async {
2✔
264
    final ToggledTasksDoneEvent(:tasks, :tx, :doneAt) = event;
6✔
265
    if (doneAt != null) {
266
      await unfocusInContext(tasks, tx);
2✔
267
    }
268
  }
269

270
  Future<void> handleDeletedTaskEvent(DeletedTaskEvent event) async {
3✔
271
    _broadcastUpdate();
3✔
272
  }
273

274
  @override
4✔
275
  void registerEventHandlers(Diligent diligent) {
276
    diligent.register(handleAddedTasksEvent);
8✔
277
    diligent.register(handleUpdatedTaskEvent);
8✔
278
    diligent.register(handleToggledTasksDoneEvent);
8✔
279
    diligent.register(handleDeletedTaskEvent);
8✔
280
  }
281
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc