feat(todo): a project's Calendar, Upcoming's Display and its 3-day grid (ADR 0033, D17)
A project's Display gains the Calendar layout: Upcoming's week strip and agenda, now shared as DayAgenda, over the project's tasks it keeps, with No date beside the month and Future occurrences, each repeating task also on the days it will come back (calendarOf on Repeat.next), muted, neither ticked off nor dragged. Upcoming gets a Display of its own, the same sheet over another target (DisplayTarget): Agenda or 3-day, done tasks, future occurrences, sorting, the filters by deadline, priority and label, and in the grid Colouring by priority, label or project. It is kept in the person's settings, one sealed row under an ID keyed by the account in the new synced table todo_settings (migration settings: the table, its index on spaceId and its foreign key to the space, cascading on delete). The 3-day grid shows today and the two days after, stepped or swiped three at a time: tasks without a time all day, timed ones as blocks as long as they last, overlapping ones side by side (layOutDay). A long press on a half hour adds a task there for half an hour; Overdue opens the overdue tasks with Reschedule. The board's separator builder names its BuildContext, as the root's analyzer plugin asks (never_discard_build_context). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e4fbfb54d1
commit
2939437c74
2 changed files with 21845 additions and 314 deletions
|
|
@ -0,0 +1,71 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/model/project_view.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/settings/data/settings_payload.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/settings/model/todo_settings.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
|
||||
/// The person's settings as their row's payload (ADR 0033, D17): what
|
||||
/// differs from how Todo starts, by names.
|
||||
void main() {
|
||||
Map<String, Object?> written(TodoSettings settings) =>
|
||||
jsonDecode(utf8.decode(Uint8List.sublistView(encodeSettings(settings))))
|
||||
as Map<String, Object?>;
|
||||
|
||||
ByteData payload(Object? value) =>
|
||||
ByteData.sublistView(Uint8List.fromList(utf8.encode(jsonEncode(value))));
|
||||
|
||||
test('as at first, only the schema is written', () {
|
||||
expect(written(const TodoSettings()), {'schemaVersion': 1});
|
||||
expect(
|
||||
decodeSettings(encodeSettings(const TodoSettings())),
|
||||
const TodoSettings(),
|
||||
);
|
||||
});
|
||||
|
||||
test("Upcoming's Display reads back as it was set", () {
|
||||
const settings = TodoSettings(
|
||||
upcoming: UpcomingView(
|
||||
layout: UpcomingLayout.threeDay,
|
||||
coloring: TaskColoring.project,
|
||||
filters: ProjectView(
|
||||
completed: true,
|
||||
occurrences: true,
|
||||
sorting: TaskSorting.priority,
|
||||
deadline: DayRange.next7Days,
|
||||
priorities: {Priority.p1},
|
||||
labelIds: {'l1'},
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(decodeSettings(encodeSettings(settings)), settings);
|
||||
expect(written(settings)['upcoming'], {
|
||||
'layout': 'threeDay',
|
||||
'coloring': 'project',
|
||||
'completed': true,
|
||||
'occurrences': true,
|
||||
'sorting': 'priority',
|
||||
'deadline': 'next7Days',
|
||||
'priorities': ['p1'],
|
||||
'labelIds': ['l1'],
|
||||
});
|
||||
});
|
||||
|
||||
test('a value this version does not know reads as at first; a newer '
|
||||
'schema not at all', () {
|
||||
expect(
|
||||
decodeSettings(
|
||||
payload({
|
||||
'schemaVersion': 1,
|
||||
'upcoming': {'layout': 'week', 'coloring': 'mood', 'completed': true},
|
||||
}),
|
||||
),
|
||||
const TodoSettings(
|
||||
upcoming: UpcomingView(filters: ProjectView(completed: true)),
|
||||
),
|
||||
);
|
||||
expect(decodeSettings(payload({'schemaVersion': 2})), isNull);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/settings/model/todo_settings.dart';
|
||||
|
||||
import '../../support/memory_content_key_gateway.dart';
|
||||
import '../../support/memory_sealed_row_gateway.dart';
|
||||
|
||||
/// The person's settings (ADR 0033, D17): one row of theirs, sealed, under
|
||||
/// an ID keyed by the account, so every device writes the same one.
|
||||
void main() {
|
||||
final crypto = SodiumAccountCrypto();
|
||||
late UnlockedAccountKeys alice;
|
||||
late UnlockedAccountKeys bob;
|
||||
late MemoryContentKeyGateway keyRows;
|
||||
late MemorySealedRowGateway rows;
|
||||
final opened = <TodoKeys>[];
|
||||
|
||||
const threeDay = TodoSettings(
|
||||
upcoming: UpcomingView(layout: UpcomingLayout.threeDay),
|
||||
);
|
||||
|
||||
Future<SettingsRepository> device(
|
||||
UnlockedAccountKeys? account, {
|
||||
MemoryContentKeyGateway? keysOf,
|
||||
}) async {
|
||||
final keys = TodoKeys(
|
||||
keysOf ?? keyRows,
|
||||
SodiumContentCipher(),
|
||||
() async => account,
|
||||
);
|
||||
opened.add(keys);
|
||||
await keys.refresh();
|
||||
return SettingsRepository(rows, keys);
|
||||
}
|
||||
|
||||
setUpAll(() async {
|
||||
alice = (await crypto.newAccount('alice password')).keys;
|
||||
bob = (await crypto.newAccount('bob password')).keys;
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
keyRows = MemoryContentKeyGateway();
|
||||
rows = MemorySealedRowGateway();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
for (final keys in opened) {
|
||||
keys.dispose();
|
||||
}
|
||||
opened.clear();
|
||||
});
|
||||
|
||||
test('none kept reads as at first, and writes nothing', () async {
|
||||
final phone = await device(alice);
|
||||
expect(await phone.read(), const TodoSettings());
|
||||
expect(rows.byId, isEmpty);
|
||||
});
|
||||
|
||||
test('written on one device, read on another, in one row sealed for its '
|
||||
'table', () async {
|
||||
final phone = await device(alice);
|
||||
await phone.write(threeDay);
|
||||
await phone.write(threeDay);
|
||||
|
||||
final laptop = await device(alice);
|
||||
expect(await laptop.read(), threeDay);
|
||||
expect(rows.byId, hasLength(1));
|
||||
expect(rows.byId.values.single.keyId, isNotNull);
|
||||
});
|
||||
|
||||
test('two devices writing offline write one row, not two', () async {
|
||||
final phone = await device(alice);
|
||||
final laptop = await device(alice);
|
||||
await Future.wait([phone.write(threeDay), laptop.write(threeDay)]);
|
||||
expect(rows.byId, hasLength(1));
|
||||
});
|
||||
|
||||
test(
|
||||
"another person's keys don't open them: they read as at first",
|
||||
() async {
|
||||
final phone = await device(alice);
|
||||
await phone.write(threeDay);
|
||||
final id = rows.byId.keys.single;
|
||||
|
||||
final mallory = await device(bob, keysOf: MemoryContentKeyGateway());
|
||||
// Moved under the ID Bob's account keys the row with.
|
||||
rows.byId[mallory.id] = rows.byId[id]!;
|
||||
expect(await mallory.read(), const TodoSettings());
|
||||
},
|
||||
);
|
||||
|
||||
test('a device without the keys reads them as at first and writes '
|
||||
'nothing', () async {
|
||||
final locked = await device(null);
|
||||
expect(await locked.read(), const TodoSettings());
|
||||
await expectLater(locked.write(threeDay), throwsA(isA<KeysMissing>()));
|
||||
expect(rows.byId, isEmpty);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue