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:
Tizian 「ティツィアーン」 2026-11-01 20:01:00 +01:00
commit 2939437c74
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
57 changed files with 21845 additions and 314 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,530 @@
{
"__className__": "serverpod.DatabaseDefinition",
"schemaVersion": 2,
"moduleName": "halkyon_todo",
"tables": [
{
"__className__": "serverpod.TableDefinition",
"name": "content_key",
"dartName": "ContentKey",
"module": "halkyon_todo",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "dataKeyId",
"fieldName": "dataKeyId",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "sealedKey",
"fieldName": "sealedKey",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "content_key_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "content_key_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "label",
"dartName": "Label",
"module": "halkyon_todo",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "label_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "label_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "project",
"dartName": "Project",
"module": "halkyon_todo",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "project_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "project_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "section",
"dartName": "Section",
"module": "halkyon_todo",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "section_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "section_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "task",
"dartName": "Task",
"module": "halkyon_todo",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "task_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "task_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
},
{
"__className__": "serverpod.TableDefinition",
"name": "todo_settings",
"dartName": "TodoSettings",
"module": "halkyon_todo",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "todo_settings_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "todo_settings_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
],
"installedModules": [
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "halkyon_base",
"version": "20261026140329453-assistant-budgets"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod",
"version": "20260824182259319"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_idp",
"version": "20260924105404509"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_offline_sync",
"version": "20260914143806119"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "halkyon_oidc",
"version": "20261023180644988"
},
{
"__className__": "serverpod.DatabaseMigrationVersion",
"module": "serverpod_auth_core",
"version": "20260924105232991"
}
],
"migrationApiVersion": 1
}

View file

@ -0,0 +1,94 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "todo_settings",
"dartName": "TodoSettings",
"module": "halkyon_todo",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "todo_settings_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "todo_settings_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -6,6 +6,7 @@ import 'package:serverpod_database/serverpod_database.dart';
part '20261101000019144-projects/migration.dart';
part '20261101000604776-tasks/migration.dart';
part '20261101180755366-settings/migration.dart';
/// Migration registry for the client-side database.
class MigrationRegistry {
@ -14,6 +15,7 @@ class MigrationRegistry {
static final List<MigrationVersionSql> migrations = [
_Migration20261101000019144_projects(),
_Migration20261101000604776_tasks(),
_Migration20261101180755366_settings(),
];
/// List of all client-side database migration versions.

View file

@ -25,11 +25,13 @@ import 'label/label.dart' as _ivak08z4;
import 'privacy/content_key.dart' as _imed3x5d;
import 'project/project.dart' as _i9c44spp;
import 'project/section.dart' as _imadkvi9;
import 'settings/todo_settings.dart' as _icak60vd;
import 'task/task.dart' as _ib4f8uyw;
export 'label/label.dart';
export 'privacy/content_key.dart';
export 'project/project.dart';
export 'project/section.dart';
export 'settings/todo_settings.dart';
export 'task/task.dart';
export 'client.dart';
export 'sync_tables.dart';
@ -371,6 +373,73 @@ class Protocol extends _isd.DatabaseSerializationManager {
],
managed: true,
),
_isd.TableDefinition(
name: 'todo_settings',
dartName: 'TodoSettings',
schema: 'public',
module: 'halkyon_todo',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'keyId',
columnType: _isd.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isd.ColumnDefinition(
name: 'payload',
columnType: _isd.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isd.ColumnDefinition(
name: 'nonce',
columnType: _isd.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'todo_settings_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'todo_settings_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
..._ihk4p5t7.Protocol() is _isd.DatabaseSerializationManager
? (_ihk4p5t7.Protocol() as _isd.DatabaseSerializationManager)
.getTargetTableDefinitions()
@ -432,6 +501,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _imadkvi9.Section) {
return _imadkvi9.Section.fromJson(data) as T;
}
if (t == _icak60vd.TodoSettings) {
return _icak60vd.TodoSettings.fromJson(data) as T;
}
if (t == _ib4f8uyw.Task) {
return _ib4f8uyw.Task.fromJson(data) as T;
}
@ -447,6 +519,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _isc.getType<_imadkvi9.Section?>()) {
return (data != null ? _imadkvi9.Section.fromJson(data) : null) as T;
}
if (t == _isc.getType<_icak60vd.TodoSettings?>()) {
return (data != null ? _icak60vd.TodoSettings.fromJson(data) : null) as T;
}
if (t == _isc.getType<_ib4f8uyw.Task?>()) {
return (data != null ? _ib4f8uyw.Task.fromJson(data) : null) as T;
}
@ -474,6 +549,7 @@ class Protocol extends _isd.DatabaseSerializationManager {
_imed3x5d.ContentKey => 'ContentKey',
_i9c44spp.Project => 'Project',
_imadkvi9.Section => 'Section',
_icak60vd.TodoSettings => 'TodoSettings',
_ib4f8uyw.Task => 'Task',
_ => null,
};
@ -500,6 +576,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
return 'Project';
case _imadkvi9.Section():
return 'Section';
case _icak60vd.TodoSettings():
return 'TodoSettings';
case _ib4f8uyw.Task():
return 'Task';
}
@ -550,6 +628,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'Section') {
return deserialize<_imadkvi9.Section>(data['data']);
}
if (dataClassName == 'TodoSettings') {
return deserialize<_icak60vd.TodoSettings>(data['data']);
}
if (dataClassName == 'Task') {
return deserialize<_ib4f8uyw.Task>(data['data']);
}
@ -640,6 +721,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
return _i9c44spp.Project.t;
case _imadkvi9.Section:
return _imadkvi9.Section.t;
case _icak60vd.TodoSettings:
return _icak60vd.TodoSettings.t;
case _ib4f8uyw.Task:
return _ib4f8uyw.Task.t;
}

View file

@ -0,0 +1,672 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
/// The person's settings of Todo's (ADR 0033, D17): one row of theirs, under
/// an ID keyed by their account, holding Upcoming's Display (and D4's
/// settings to come) in its sealed payload (D5); the instance sees that it
/// exists, its size and when it changed. The last writer wins. Synced.
abstract class TodoSettings
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
TodoSettings._({
this.id,
this.spaceId,
this.keyId,
required this.payload,
this.nonce,
});
factory TodoSettings({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _TodoSettingsImpl;
factory TodoSettings.fromJson(Map<String, dynamic> jsonSerialization) {
return TodoSettings(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
),
nonce: jsonSerialization['nonce'] == null
? null
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = TodoSettingsTable();
static const db = TodoSettingsRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
/// The key it is sealed with (ADR 0002's envelope); never null in Todo,
/// where everything is sealed.
String? keyId;
/// The content, sealed: UTF-8 JSON with a `schemaVersion` once opened.
_idt.ByteData payload;
/// The sealing's nonce (ADR 0002's envelope).
_idt.ByteData? nonce;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [TodoSettings]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
TodoSettings copyWith({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'TodoSettings',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'TodoSettings',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static TodoSettingsInclude include() {
return TodoSettingsInclude._();
}
static TodoSettingsIncludeList includeList({
_isd.WhereExpressionBuilder<TodoSettingsTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<TodoSettingsTable>? orderBy,
_isd.OrderByListBuilder<TodoSettingsTable>? orderByList,
TodoSettingsInclude? include,
}) {
return TodoSettingsIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(TodoSettings.t),
orderByList: orderByList?.call(TodoSettings.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _TodoSettingsImpl extends TodoSettings {
_TodoSettingsImpl({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [TodoSettings]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
TodoSettings copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return TodoSettings(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class TodoSettingsUpdateTable extends _isd.UpdateTable<TodoSettingsTable> {
TodoSettingsUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
table.keyId,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_isd.ColumnValue(
table.payload,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_isd.ColumnValue(
table.nonce,
value,
);
}
class TodoSettingsTable extends _isd.Table<_isc.UuidValue?> {
TodoSettingsTable({super.tableRelation}) : super(tableName: 'todo_settings') {
updateTable = TodoSettingsUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final TodoSettingsUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
/// The key it is sealed with (ADR 0002's envelope); never null in Todo,
/// where everything is sealed.
late final _isd.ColumnString keyId;
/// The content, sealed: UTF-8 JSON with a `schemaVersion` once opened.
late final _isd.ColumnByteData payload;
/// The sealing's nonce (ADR 0002's envelope).
late final _isd.ColumnByteData nonce;
@override
List<_isd.Column> get columns => [
id,
spaceId,
keyId,
payload,
nonce,
];
}
class TodoSettingsInclude extends _isd.IncludeObject {
TodoSettingsInclude._();
@override
Map<String, _isd.Include?> get includes => {};
@override
_isd.Table<_isc.UuidValue?> get table => TodoSettings.t;
}
class TodoSettingsIncludeList extends _isd.IncludeList {
TodoSettingsIncludeList._({
_isd.WhereExpressionBuilder<TodoSettingsTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(TodoSettings.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => TodoSettings.t;
}
class TodoSettingsRepository {
const TodoSettingsRepository._();
/// Returns a list of [TodoSettings]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<TodoSettings>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<TodoSettingsTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<TodoSettingsTable>? orderBy,
_isd.OrderByListBuilder<TodoSettingsTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<TodoSettings>(
where: where?.call(TodoSettings.t),
orderBy: orderBy?.call(TodoSettings.t),
orderByList: orderByList?.call(TodoSettings.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [TodoSettings] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<TodoSettings?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<TodoSettingsTable>? where,
int? offset,
_isd.OrderByBuilder<TodoSettingsTable>? orderBy,
_isd.OrderByListBuilder<TodoSettingsTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<TodoSettings>(
where: where?.call(TodoSettings.t),
orderBy: orderBy?.call(TodoSettings.t),
orderByList: orderByList?.call(TodoSettings.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [TodoSettings] by its [id] or null if no such row exists.
Future<TodoSettings?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<TodoSettings>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [TodoSettings]s in the list and returns the inserted rows.
///
/// The returned [TodoSettings]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<TodoSettings>> insert(
_isd.DatabaseSession session,
List<TodoSettings> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<TodoSettings>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [TodoSettings] and returns the inserted row.
///
/// The returned [TodoSettings] will have its `id` field set.
Future<TodoSettings> insertRow(
_isd.DatabaseSession session,
TodoSettings row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<TodoSettings>(
row,
transaction: transaction,
);
}
/// Upserts all [TodoSettings]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [TodoSettings]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<TodoSettings>> upsert(
_isd.DatabaseSession session,
List<TodoSettings> rows, {
required _isd.ColumnSelections<TodoSettingsTable> conflictColumns,
_isd.ColumnSelections<TodoSettingsTable>? updateColumns,
_isd.WhereExpressionBuilder<TodoSettingsTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<TodoSettings>(
rows,
conflictColumns: conflictColumns(TodoSettings.t),
updateColumns: updateColumns?.call(TodoSettings.t),
updateWhere: updateWhere?.call(TodoSettings.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [TodoSettings] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [TodoSettings] will have its `id` field set.
Future<TodoSettings?> upsertRow(
_isd.DatabaseSession session,
TodoSettings row, {
required _isd.ColumnSelections<TodoSettingsTable> conflictColumns,
_isd.ColumnSelections<TodoSettingsTable>? updateColumns,
_isd.WhereExpressionBuilder<TodoSettingsTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<TodoSettings>(
row,
conflictColumns: conflictColumns(TodoSettings.t),
updateColumns: updateColumns?.call(TodoSettings.t),
updateWhere: updateWhere?.call(TodoSettings.t),
transaction: transaction,
);
}
/// Updates all [TodoSettings]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<TodoSettings>> update(
_isd.DatabaseSession session,
List<TodoSettings> rows, {
_isd.ColumnSelections<TodoSettingsTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<TodoSettings>(
rows,
columns: columns?.call(TodoSettings.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [TodoSettings]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<TodoSettings> updateRow(
_isd.DatabaseSession session,
TodoSettings row, {
_isd.ColumnSelections<TodoSettingsTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<TodoSettings>(
row,
columns: columns?.call(TodoSettings.t),
transaction: transaction,
);
}
/// Updates a single [TodoSettings] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<TodoSettings?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<TodoSettingsUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<TodoSettings>(
id,
columnValues: columnValues(TodoSettings.t.updateTable),
transaction: transaction,
);
}
/// Updates all [TodoSettings]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<TodoSettings>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<TodoSettingsUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<TodoSettingsTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<TodoSettingsTable>? orderBy,
_isd.OrderByListBuilder<TodoSettingsTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<TodoSettings>(
columnValues: columnValues(TodoSettings.t.updateTable),
where: where(TodoSettings.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(TodoSettings.t),
orderByList: orderByList?.call(TodoSettings.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [TodoSettings]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<TodoSettings>> delete(
_isd.DatabaseSession session,
List<TodoSettings> rows, {
_isd.OrderByBuilder<TodoSettingsTable>? orderBy,
_isd.OrderByListBuilder<TodoSettingsTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<TodoSettings>(
rows,
orderBy: orderBy?.call(TodoSettings.t),
orderByList: orderByList?.call(TodoSettings.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [TodoSettings].
Future<TodoSettings> deleteRow(
_isd.DatabaseSession session,
TodoSettings row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<TodoSettings>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<TodoSettings>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<TodoSettingsTable> where,
_isd.OrderByBuilder<TodoSettingsTable>? orderBy,
_isd.OrderByListBuilder<TodoSettingsTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<TodoSettings>(
where: where(TodoSettings.t),
orderBy: orderBy?.call(TodoSettings.t),
orderByList: orderByList?.call(TodoSettings.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<TodoSettingsTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<TodoSettings>(
where: where?.call(TodoSettings.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [TodoSettings] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<TodoSettingsTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<TodoSettings>(
where: where(TodoSettings.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -15,6 +15,7 @@ import 'label/label.dart' as _ivak08z4;
import 'privacy/content_key.dart' as _imed3x5d;
import 'project/project.dart' as _i9c44spp;
import 'project/section.dart' as _imadkvi9;
import 'settings/todo_settings.dart' as _icak60vd;
import 'task/task.dart' as _ib4f8uyw;
/// The tables synchronized between client and server by the
@ -26,4 +27,5 @@ final List<_isd.Table> syncTables = [
_i9c44spp.Project.t,
_imadkvi9.Section.t,
_ib4f8uyw.Task.t,
_icak60vd.TodoSettings.t,
];

View file

@ -206,5 +206,19 @@
"layoutTitle": "Layout",
"layoutList": "Liste",
"layoutBoard": "Board",
"boardNoSection": "(Kein Abschnitt)"
"boardNoSection": "(Kein Abschnitt)",
"layoutCalendar": "Kalender",
"displayOccurrences": "Künftige Wiederholungen",
"calendarNoDate": "Ohne Datum",
"calendarNoDateEmpty": "Jede Aufgabe hier hat ein Datum",
"calendarNoDateHint": "Aufgaben dieses Projekts ohne Datum stehen hier.",
"taskLater": "Eine spätere Wiederholung",
"sortingSmart": "Smart",
"layoutAgenda": "Agenda",
"layoutThreeDay": "3 Tage",
"displayColoring": "Farben",
"coloringProject": "Projekt",
"allDay": "Ganztägig",
"daysPrevious": "Frühere Tage",
"daysNext": "Spätere Tage"
}

View file

@ -624,5 +624,61 @@
"boardNoSection": "(No section)",
"@boardNoSection": {
"description": "A project's layout in its Display, and its board's columns (ADR 0033, D17)."
},
"layoutCalendar": "Calendar",
"@layoutCalendar": {
"description": "A project's Calendar layout and its Display (ADR 0033, D17)."
},
"displayOccurrences": "Future occurrences",
"@displayOccurrences": {
"description": "A project's Calendar layout and its Display (ADR 0033, D17)."
},
"calendarNoDate": "No date",
"@calendarNoDate": {
"description": "A project's Calendar layout and its Display (ADR 0033, D17)."
},
"calendarNoDateEmpty": "Every task here has a date",
"@calendarNoDateEmpty": {
"description": "A project's Calendar layout and its Display (ADR 0033, D17)."
},
"calendarNoDateHint": "Tasks of this project without a date show here.",
"@calendarNoDateHint": {
"description": "A project's Calendar layout and its Display (ADR 0033, D17)."
},
"taskLater": "A later time of a repeating task",
"@taskLater": {
"description": "A project's Calendar layout and its Display (ADR 0033, D17)."
},
"sortingSmart": "Smart",
"@sortingSmart": {
"description": "Upcoming's Display and its 3-day grid (ADR 0033, D17)."
},
"layoutAgenda": "Agenda",
"@layoutAgenda": {
"description": "Upcoming's Display and its 3-day grid (ADR 0033, D17)."
},
"layoutThreeDay": "3-day",
"@layoutThreeDay": {
"description": "Upcoming's Display and its 3-day grid (ADR 0033, D17)."
},
"displayColoring": "Colouring",
"@displayColoring": {
"description": "Upcoming's Display and its 3-day grid (ADR 0033, D17)."
},
"coloringProject": "Project",
"@coloringProject": {
"description": "Upcoming's Display and its 3-day grid (ADR 0033, D17)."
},
"allDay": "All day",
"@allDay": {
"description": "Upcoming's Display and its 3-day grid (ADR 0033, D17)."
},
"daysPrevious": "Earlier days",
"@daysPrevious": {
"description": "Upcoming's Display and its 3-day grid (ADR 0033, D17)."
},
"daysNext": "Later days",
"@daysNext": {
"description": "Upcoming's Display and its 3-day grid (ADR 0033, D17)."
}
}

View file

@ -1341,6 +1341,90 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'(No section)'**
String get boardNoSection;
/// A project's Calendar layout and its Display (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'Calendar'**
String get layoutCalendar;
/// A project's Calendar layout and its Display (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'Future occurrences'**
String get displayOccurrences;
/// A project's Calendar layout and its Display (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'No date'**
String get calendarNoDate;
/// A project's Calendar layout and its Display (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'Every task here has a date'**
String get calendarNoDateEmpty;
/// A project's Calendar layout and its Display (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'Tasks of this project without a date show here.'**
String get calendarNoDateHint;
/// A project's Calendar layout and its Display (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'A later time of a repeating task'**
String get taskLater;
/// Upcoming's Display and its 3-day grid (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'Smart'**
String get sortingSmart;
/// Upcoming's Display and its 3-day grid (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'Agenda'**
String get layoutAgenda;
/// Upcoming's Display and its 3-day grid (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'3-day'**
String get layoutThreeDay;
/// Upcoming's Display and its 3-day grid (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'Colouring'**
String get displayColoring;
/// Upcoming's Display and its 3-day grid (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'Project'**
String get coloringProject;
/// Upcoming's Display and its 3-day grid (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'All day'**
String get allDay;
/// Upcoming's Display and its 3-day grid (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'Earlier days'**
String get daysPrevious;
/// Upcoming's Display and its 3-day grid (ADR 0033, D17).
///
/// In en, this message translates to:
/// **'Later days'**
String get daysNext;
}
class _AppLocalizationsDelegate

View file

@ -833,4 +833,47 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get boardNoSection => '(Kein Abschnitt)';
@override
String get layoutCalendar => 'Kalender';
@override
String get displayOccurrences => 'Künftige Wiederholungen';
@override
String get calendarNoDate => 'Ohne Datum';
@override
String get calendarNoDateEmpty => 'Jede Aufgabe hier hat ein Datum';
@override
String get calendarNoDateHint =>
'Aufgaben dieses Projekts ohne Datum stehen hier.';
@override
String get taskLater => 'Eine spätere Wiederholung';
@override
String get sortingSmart => 'Smart';
@override
String get layoutAgenda => 'Agenda';
@override
String get layoutThreeDay => '3 Tage';
@override
String get displayColoring => 'Farben';
@override
String get coloringProject => 'Projekt';
@override
String get allDay => 'Ganztägig';
@override
String get daysPrevious => 'Frühere Tage';
@override
String get daysNext => 'Spätere Tage';
}

View file

@ -832,6 +832,49 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get boardNoSection => '(No section)';
@override
String get layoutCalendar => 'Calendar';
@override
String get displayOccurrences => 'Future occurrences';
@override
String get calendarNoDate => 'No date';
@override
String get calendarNoDateEmpty => 'Every task here has a date';
@override
String get calendarNoDateHint =>
'Tasks of this project without a date show here.';
@override
String get taskLater => 'A later time of a repeating task';
@override
String get sortingSmart => 'Smart';
@override
String get layoutAgenda => 'Agenda';
@override
String get layoutThreeDay => '3-day';
@override
String get displayColoring => 'Colouring';
@override
String get coloringProject => 'Project';
@override
String get allDay => 'All day';
@override
String get daysPrevious => 'Earlier days';
@override
String get daysNext => 'Later days';
}
/// The translations for English, as used in the United Kingdom (`en_GB`).

View file

@ -17,6 +17,8 @@ import 'package:halkyon_todo_flutter/src/feature/reminder/data/reminder_gateway.
import 'package:halkyon_todo_flutter/src/feature/reminder/data/system_reminder_gateway.dart';
import 'package:halkyon_todo_flutter/src/feature/section/data/section_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/section/data/sync_section_gateway.dart';
import 'package:halkyon_todo_flutter/src/feature/settings/data/settings_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/settings/data/sync_settings_gateway.dart';
import 'package:halkyon_todo_flutter/src/feature/task/data/sync_task_gateway.dart';
import 'package:halkyon_todo_flutter/src/feature/task/data/task_repository.dart';
import 'package:package_info_plus/package_info_plus.dart';
@ -27,7 +29,7 @@ import 'package:serverpod_flutter/serverpod_flutter.dart';
import 'package:serverpod_offline_sync_client/offline_sync.dart';
/// A signed-in person's dependencies: who they are, their projects, sections,
/// tasks and labels, on
/// tasks, labels and settings, on
/// their own local database in their folder, which the sync engine keeps in
/// step with their other devices, every row sealed with their key (ADR 0033,
/// D5, D6). halkyon_app's `PersonScope` opens and disposes them with the
@ -51,6 +53,9 @@ class TodoPerson(
/// Their labels, sealed and synced.
required final LabelRepository labels,
/// Their settings of Todo's (Upcoming's Display), sealed and synced.
required final SettingsRepository settings,
/// The device's notifications, where their tasks' reminders go (ADR
/// 0033, D5); none in tests that don't say.
final ReminderGateway reminders = const NoReminders(),
@ -318,6 +323,7 @@ Future<TodoPerson> _openPersonData(
final sections = SectionRepository(SyncSectionGateway(db, sync), keys);
final tasks = TaskRepository(SyncTaskGateway(db, sync), keys);
final labels = LabelRepository(SyncLabelGateway(db, sync), keys);
final settings = SettingsRepository(SyncSettingsGateway(db, sync), keys);
// The Inbox, made once by whichever device is first; nothing where this
// device holds none of the account's keys.
unawaited(
@ -334,6 +340,7 @@ Future<TodoPerson> _openPersonData(
sections: sections,
tasks: tasks,
labels: labels,
settings: settings,
reminders: reminders,
onDispose: () async {
try {
@ -341,6 +348,7 @@ Future<TodoPerson> _openPersonData(
await sections.dispose();
await tasks.dispose();
await labels.dispose();
await settings.dispose();
await sync.dispose();
await db.close();
} finally {

View file

@ -17,13 +17,14 @@ ByteData encodeProject(ProjectContent content) =>
'color': ?content.color?.name,
'position': content.position,
// As at first, nothing: a project's Display is written once set (D17).
if (!content.view.isDefault) 'view': _encodeView(content.view),
if (!content.view.isDefault) 'view': encodeView(content.view),
});
/// [view] by its values' names, those left as at first left out.
Map<String, Object?> _encodeView(ProjectView view) => {
Map<String, Object?> encodeView(ProjectView view) => {
if (view.layout != ProjectLayout.list) 'layout': view.layout.name,
if (view.completed) 'completed': true,
if (view.occurrences) 'occurrences': true,
if (view.grouping != TaskGrouping.none) 'grouping': view.grouping.name,
if (view.sorting != TaskSorting.manual) 'sorting': view.sorting.name,
if (view.date != DayRange.all) 'date': view.date.name,
@ -34,7 +35,7 @@ Map<String, Object?> _encodeView(ProjectView view) => {
};
/// The view [json] holds; a value this version doesn't know, as at first.
ProjectView _decodeView(Object? json) {
ProjectView decodeView(Object? json) {
if (json is! Map<String, Object?>) {
return const ProjectView();
}
@ -44,6 +45,7 @@ ProjectView _decodeView(Object? json) {
return ProjectView(
layout: named(ProjectLayout.values, 'layout', ProjectLayout.list),
completed: json['completed'] == true,
occurrences: json['occurrences'] == true,
grouping: named(TaskGrouping.values, 'grouping', TaskGrouping.none),
sorting: named(TaskSorting.values, 'sorting', TaskSorting.manual),
date: named(DayRange.values, 'date', DayRange.all),
@ -69,6 +71,6 @@ ProjectContent? decodeProject(ByteData payload) => decodeJson(
name: json['name']! as String,
color: PaletteColor.byName(json['color'] as String?),
position: (json['position'] as num?)?.toDouble() ?? 0,
view: _decodeView(json['view']),
view: decodeView(json['view']),
),
);

View file

@ -0,0 +1,95 @@
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
import 'package:halkyon_todo_flutter/src/feature/task/model/task_views.dart';
/// A row of a project's Calendar (ADR 0033, D17): a task on its own day,
/// or, where `occurrence`, a later time of a repeating one, the task as it
/// will be then (its ID kept, its date moved).
typedef CalendarEntry = ({OpenTask task, bool occurrence});
/// What a project's Calendar shows (D17): the `overdue` tasks, then each of
/// its `days` with its rows; the days with open tasks or later times are
/// `busy`, marked in the week strip.
typedef ProjectCalendar = ({
List<OpenTask> overdue,
List<(Day, List<CalendarEntry>)> days,
Set<Day> busy,
});
/// How many later times of one task at most, so that a daily one doesn't
/// fill a year picked far ahead.
const _occurrencesAtMost = 400;
/// [tasks] (those the project's Display keeps, done ones only where it
/// shows them) as a Calendar from [today] on for [days] days (D17): as
/// Upcoming's, overdue first, a day's tasks by time or [order]; where
/// [occurrences], each repeating task not done also on the days it will
/// come back, the first as if done today (`Repeat.next`), until its
/// recurrence ends.
ProjectCalendar calendarOf(
List<OpenTask> tasks,
Day today, {
required int days,
bool occurrences = false,
int Function(OpenTask a, OpenTask b) order = byTime,
}) {
final (:overdue, days: listed) = upcomingOf(
tasks,
today,
days: days,
completed: true,
);
final byDay = {
for (final (day, dayTasks) in listed)
day: <CalendarEntry>[
for (final task in dayTasks) (task: task, occurrence: false),
],
};
final busy = {
for (final task in tasks)
if (task.content case TaskContent(
completed: false,
date: TaskDate(:final day),
))
day,
};
if (occurrences) {
final end = today.plus(days);
for (final task in tasks) {
final content = task.content;
if (content.completed) {
continue;
}
if (content.date case final date? when date.repeat != null) {
var repeat = date.repeat!;
var next = repeat.next(date.day, today: today);
for (
var count = 0;
next != null && next < end && count < _occurrencesAtMost;
count++
) {
final day = next;
repeat = repeat.counted();
byDay[day]?.add((
task: (
id: task.id,
content: content.copyWith(
date: () => date.on(day).repeating(repeat),
),
),
occurrence: true,
));
busy.add(day);
next = repeat.next(day, today: day);
}
}
}
}
return (
overdue: overdue,
days: [
for (final MapEntry(:key, :value) in byDay.entries)
(key, value..sort((a, b) => order(a.task, b.task))),
],
busy: busy,
);
}

View file

@ -63,12 +63,16 @@ enum DayRange() {
}
/// A project's Display (ADR 0033, D17, as Todoist's): its [layout], whether
/// it shows its done tasks, how it groups and sorts them, and the tasks it
/// it shows its done tasks (and, as a Calendar, the later times of
/// repeating ones), how it groups and sorts them, and the tasks it
/// keeps, by date, deadline, priority and label. Kept in the project's
/// sealed row, so every device of the person's shows the project alike.
final class const ProjectView({
final ProjectLayout layout = ProjectLayout.list,
final bool completed = false,
/// Whether a Calendar shows repeating tasks on their later days too.
final bool occurrences = false,
final TaskGrouping grouping = TaskGrouping.none,
final TaskSorting sorting = TaskSorting.manual,
final DayRange date = DayRange.all,
@ -140,6 +144,7 @@ final class const ProjectView({
ProjectView copyWith({
ProjectLayout? layout,
bool? completed,
bool? occurrences,
TaskGrouping? grouping,
TaskSorting? sorting,
DayRange? date,
@ -149,6 +154,7 @@ final class const ProjectView({
}) => ProjectView(
layout: layout ?? this.layout,
completed: completed ?? this.completed,
occurrences: occurrences ?? this.occurrences,
grouping: grouping ?? this.grouping,
sorting: sorting ?? this.sorting,
date: date ?? this.date,
@ -161,6 +167,7 @@ final class const ProjectView({
List<Object?> get props => [
layout,
completed,
occurrences,
grouping,
sorting,
date,

View file

@ -4,6 +4,7 @@ import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
import 'package:halkyon_todo_flutter/src/feature/project/model/project_view.dart';
import 'package:halkyon_todo_flutter/src/feature/settings/model/todo_settings.dart';
import 'package:halkyon_todo_flutter/src/feature/task/bloc/todo_cubit.dart';
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_pickers.dart';
@ -12,52 +13,204 @@ import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// The button opening a project's Display (ADR 0033, D17), a dot on it
/// while the project shows otherwise than at first.
class const DisplayButton({required final String projectId, super.key})
/// What a Display sheet sets (ADR 0033, D17): a project's view, or
/// Upcoming's in the person's settings. [viewOf] reads its done tasks,
/// future occurrences, sorting and filters, [write] keeps them, [layout]
/// builds the rows above them (the layout's picker and what goes with
/// it), and [reset] puts it all back as at first, which [isDefault] says
/// it is.
final class const DisplayTarget({
required final ProjectView Function(TodoState state) viewOf,
required final Future<void> Function(
TodoPerson todo,
TodoState state,
ProjectView view,
)
write,
required final bool Function(TodoState state) isDefault,
required final Future<void> Function(TodoPerson todo, TodoState state) reset,
required final List<Widget> Function(
BuildContext context,
TodoState state,
Future<void> Function(Future<void> Function(TodoPerson todo) change) apply,
)
layout,
/// Whether it groups (a project's list and board).
final bool Function(TodoState state) grouping = _never,
/// Whether it filters by date (a project; Upcoming is days already).
final bool dateFilter = true,
/// Whether it shows repeating tasks' later times (a Calendar, Upcoming).
final bool Function(TodoState state) occurrences = _never,
/// The sortings it offers; manual first, named [manualText].
final List<TaskSorting> sortings = TaskSorting.values,
final String Function(AppLocalizations l10n)? manualText,
});
bool _never(TodoState state) => false;
/// A project's Display (D17): its layout (list, board, calendar), done
/// tasks, grouping but in a Calendar, future occurrences in one, sorting,
/// filters; kept in the project's sealed row.
DisplayTarget projectDisplay(String projectId) {
ProjectView viewOf(TodoState state) =>
state.projectOf(projectId)?.content?.view ?? const ProjectView();
Future<void> write(TodoPerson todo, TodoState state, ProjectView view) async {
if (state.projectOf(projectId)?.content case final content?) {
await todo.projects.update(projectId, content.copyWith(view: view));
}
}
bool calendar(TodoState state) =>
viewOf(state).layout == ProjectLayout.calendar;
return DisplayTarget(
viewOf: viewOf,
write: write,
isDefault: (state) => viewOf(state).isDefault,
reset: (todo, state) => write(todo, state, const ProjectView()),
grouping: (state) => !calendar(state),
occurrences: calendar,
layout: (context, state, apply) {
final l10n = AppLocalizations.of(context);
final view = viewOf(state);
return [
_LayoutPicker<ProjectLayout>(
selected: view.layout,
segments: [
(ProjectLayout.list, LucideIcons.list, l10n.layoutList),
(ProjectLayout.board, LucideIcons.squareKanban, l10n.layoutBoard),
(
ProjectLayout.calendar,
LucideIcons.calendarDays,
l10n.layoutCalendar,
),
],
onPicked: (layout) => apply(
(todo) => write(todo, state, view.copyWith(layout: layout)),
),
),
];
},
);
}
/// Upcoming's Display (D17, as Todoist's): agenda or 3-day, in the grid
/// what tasks are coloured by, done tasks, future occurrences, sorting
/// (smart: by time, then priority), and the filters by deadline, priority
/// and label; kept in the person's settings.
final upcomingDisplay = DisplayTarget(
viewOf: (state) => state.settings.upcoming.filters,
write: _writeUpcoming,
isDefault: (state) => state.settings.upcoming.isDefault,
reset: (todo, state) => todo.settings.write(
state.settings.copyWith(upcoming: const UpcomingView()),
),
dateFilter: false,
occurrences: (_) => true,
sortings: const [
TaskSorting.manual,
TaskSorting.name,
TaskSorting.deadline,
TaskSorting.addedDate,
TaskSorting.priority,
],
manualText: (l10n) => l10n.sortingSmart,
layout: (context, state, apply) {
final l10n = AppLocalizations.of(context);
final upcoming = state.settings.upcoming;
Future<void> set(UpcomingView view) => apply(
(todo) => todo.settings.write(state.settings.copyWith(upcoming: view)),
);
return [
_LayoutPicker<UpcomingLayout>(
selected: upcoming.layout,
segments: [
(UpcomingLayout.agenda, LucideIcons.list, l10n.layoutAgenda),
(UpcomingLayout.threeDay, LucideIcons.columns3, l10n.layoutThreeDay),
],
onPicked: (layout) => set(upcoming.copyWith(layout: layout)),
),
if (upcoming.layout == UpcomingLayout.threeDay)
ListTile(
leading: const Icon(LucideIcons.palette),
title: Text(l10n.displayColoring),
subtitle: Text(coloringText(l10n, upcoming.coloring)),
onTap: () async {
if (await _pickOne(
context,
l10n.displayColoring,
TaskColoring.values,
upcoming.coloring,
(coloring) => coloringText(l10n, coloring),
)
case final coloring?) {
await set(upcoming.copyWith(coloring: coloring));
}
},
),
];
},
);
Future<void> _writeUpcoming(
TodoPerson todo,
TodoState state,
ProjectView view,
) => todo.settings.write(
state.settings.copyWith(
upcoming: state.settings.upcoming.copyWith(filters: view),
),
);
/// The button opening [target]'s Display (ADR 0033, D17), a dot on it
/// while anything differs from how it shows at first.
class const DisplayButton({required final DisplayTarget target, super.key})
extends StatelessWidget {
@override
Widget build(BuildContext context) {
final view = context.select<TodoCubit, ProjectView>(
(cubit) =>
cubit.state.projectOf(projectId)?.content?.view ??
const ProjectView(),
final changed = context.select<TodoCubit, bool>(
(cubit) => !target.isDefault(cubit.state),
);
return IconButton(
tooltip: AppLocalizations.of(context).display,
icon: Badge(
smallSize: 8,
isLabelVisible: !view.isDefault,
isLabelVisible: changed,
child: const Icon(LucideIcons.slidersHorizontal),
),
onPressed: () => showDisplaySheet(context, projectId),
onPressed: () => showDisplaySheet(context, target),
);
}
}
/// A project's Display (D17, as Todoist's): its done tasks shown or not,
/// how it groups and sorts, and what it filters by date, deadline, priority
/// and label; each change written to the project at once, synced to every
/// [target]'s Display (D17, as Todoist's): its layout, done tasks shown or
/// not, how it groups and sorts, and what it filters by date, deadline,
/// priority and label; each change written at once, synced to every
/// device, Reset all putting it back as at first.
Future<void> showDisplaySheet(BuildContext context, String projectId) =>
Future<void> showDisplaySheet(BuildContext context, DisplayTarget target) =>
showAdaptiveSheet<void>(
context,
builder: (context) => _DisplaySheet(projectId: projectId),
builder: (context) => _DisplaySheet(target: target),
);
class const _DisplaySheet({required final String projectId})
class const _DisplaySheet({required final DisplayTarget target})
extends StatelessWidget {
Future<void> _set(BuildContext context, ProjectView view) async {
/// Runs [change] with the person's data, saying so where this device
/// can't seal.
Future<void> _apply(
BuildContext context,
Future<void> Function(TodoPerson todo) change,
) async {
final l10n = AppLocalizations.of(context);
final messenger = ScaffoldMessenger.of(context);
final todo = PersonScope.of<TodoPerson>(context);
final project = context.read<TodoCubit>().state.projectOf(projectId);
if (project?.content case final content?) {
try {
await todo.projects.update(projectId, content.copyWith(view: view));
} on KeysMissing {
messenger.showSnackBar(SnackBar(content: Text(l10n.keysMissing)));
}
try {
await change(todo);
} on KeysMissing {
messenger.showSnackBar(SnackBar(content: Text(l10n.keysMissing)));
}
}
@ -66,8 +219,15 @@ class const _DisplaySheet({required final String projectId})
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context);
final state = context.watch<TodoCubit>().state;
final view =
state.projectOf(projectId)?.content?.view ?? const ProjectView();
final view = target.viewOf(state);
Future<void> set(ProjectView view) =>
_apply(context, (todo) => target.write(todo, state, view));
Future<void> apply(Future<void> Function(TodoPerson todo) change) =>
_apply(context, change);
String sortingName(TaskSorting sorting) =>
sorting == TaskSorting.manual && target.manualText != null
? target.manualText!(l10n)
: sortingText(l10n, sorting);
final labels = {
for (final label in state.labels)
if (label.content case final content?) label.id: content.name,
@ -99,99 +259,75 @@ class const _DisplaySheet({required final String projectId})
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
heading(l10n.layoutTitle),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: SegmentedButton<ProjectLayout>(
showSelectedIcon: false,
segments: [
ButtonSegment(
value: ProjectLayout.list,
icon: const Icon(LucideIcons.list),
label: Text(l10n.layoutList),
),
ButtonSegment(
value: ProjectLayout.board,
icon: const Icon(LucideIcons.squareKanban),
label: Text(l10n.layoutBoard),
),
],
// The calendar comes with its own step (D17); until then a
// project laid out so shows as a list.
selected: {
if (view.layout == ProjectLayout.board)
ProjectLayout.board
else
ProjectLayout.list,
},
onSelectionChanged: (picked) =>
_set(context, view.copyWith(layout: picked.single)),
),
),
...target.layout(context, state, apply),
SwitchListTile(
secondary: const Icon(LucideIcons.circleCheck),
title: Text(l10n.displayCompleted),
value: view.completed,
onChanged: (on) => _set(context, view.copyWith(completed: on)),
onChanged: (on) => set(view.copyWith(completed: on)),
),
if (target.occurrences(state))
SwitchListTile(
secondary: const Icon(LucideIcons.repeat),
title: Text(l10n.displayOccurrences),
value: view.occurrences,
onChanged: (on) => set(view.copyWith(occurrences: on)),
),
heading(l10n.displaySort),
choice(
LucideIcons.group,
l10n.displayGrouping,
groupingText(l10n, view.grouping),
() async {
if (await _pickOne(
context,
l10n.displayGrouping,
TaskGrouping.values,
view.grouping,
(grouping) => groupingText(l10n, grouping),
)
case final grouping?) {
if (context.mounted) {
await _set(context, view.copyWith(grouping: grouping));
if (target.grouping(state))
choice(
LucideIcons.group,
l10n.displayGrouping,
groupingText(l10n, view.grouping),
() async {
if (await _pickOne(
context,
l10n.displayGrouping,
TaskGrouping.values,
view.grouping,
(grouping) => groupingText(l10n, grouping),
)
case final grouping?) {
await set(view.copyWith(grouping: grouping));
}
}
},
),
},
),
choice(
LucideIcons.arrowUpDown,
l10n.displaySorting,
sortingText(l10n, view.sorting),
sortingName(view.sorting),
() async {
if (await _pickOne(
context,
l10n.displaySorting,
TaskSorting.values,
target.sortings,
view.sorting,
(sorting) => sortingText(l10n, sorting),
sortingName,
)
case final sorting?) {
if (context.mounted) {
await _set(context, view.copyWith(sorting: sorting));
}
await set(view.copyWith(sorting: sorting));
}
},
),
heading(l10n.displayFilter),
choice(
LucideIcons.calendar,
l10n.dateTitle,
rangeText(l10n, view.date),
() async {
if (await _pickOne(
context,
l10n.dateTitle,
DayRange.values,
view.date,
(range) => rangeText(l10n, range),
)
case final range?) {
if (context.mounted) {
await _set(context, view.copyWith(date: range));
if (target.dateFilter)
choice(
LucideIcons.calendar,
l10n.dateTitle,
rangeText(l10n, view.date),
() async {
if (await _pickOne(
context,
l10n.dateTitle,
DayRange.values,
view.date,
(range) => rangeText(l10n, range),
)
case final range?) {
await set(view.copyWith(date: range));
}
}
},
),
},
),
choice(
LucideIcons.target,
l10n.displayDeadline,
@ -205,9 +341,7 @@ class const _DisplaySheet({required final String projectId})
(range) => rangeText(l10n, range, deadline: true),
)
case final range?) {
if (context.mounted) {
await _set(context, view.copyWith(deadline: range));
}
await set(view.copyWith(deadline: range));
}
},
),
@ -224,9 +358,7 @@ class const _DisplaySheet({required final String projectId})
() async {
if (await _pickPriorities(context, view.priorities)
case final picked?) {
if (context.mounted) {
await _set(context, view.copyWith(priorities: picked));
}
await set(view.copyWith(priorities: picked));
}
},
),
@ -241,9 +373,7 @@ class const _DisplaySheet({required final String projectId})
() async {
if (await pickLabels(context, [...view.labelIds])
case final picked?) {
if (context.mounted) {
await _set(context, view.copyWith(labelIds: {...picked}));
}
await set(view.copyWith(labelIds: {...picked}));
}
},
),
@ -252,9 +382,9 @@ class const _DisplaySheet({required final String projectId})
footer: Row(
children: [
TextButton(
onPressed: view.isDefault
onPressed: target.isDefault(state)
? null
: () => _set(context, const ProjectView()),
: () => apply((todo) => target.reset(todo, state)),
child: Text(l10n.displayReset),
),
const Spacer(),
@ -268,6 +398,27 @@ class const _DisplaySheet({required final String projectId})
}
}
/// A Display's layouts side by side, [selected] marked.
class const _LayoutPicker<T>({
required final T selected,
required final List<(T, IconData, String)> segments,
required final ValueChanged<T> onPicked,
}) extends StatelessWidget {
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: SegmentedButton<T>(
showSelectedIcon: false,
segments: [
for (final (value, icon, label) in segments)
ButtonSegment(value: value, icon: Icon(icon), label: Text(label)),
],
selected: {selected},
onSelectionChanged: (picked) => onPicked(picked.single),
),
);
}
/// Asks for one of [values], [current] ticked.
Future<T?> _pickOne<T>(
BuildContext context,
@ -362,6 +513,14 @@ String sortingText(AppLocalizations l10n, TaskSorting sorting) =>
TaskSorting.priority => l10n.sortingPriority,
};
/// [coloring]'s name.
String coloringText(AppLocalizations l10n, TaskColoring coloring) =>
switch (coloring) {
TaskColoring.priority => l10n.priorityTitle,
TaskColoring.label => l10n.displayLabel,
TaskColoring.project => l10n.coloringProject,
};
/// [range]'s name, for a filter by date or, where [deadline], by deadline.
String rangeText(
AppLocalizations l10n,

View file

@ -102,7 +102,7 @@ class _ProjectBoardState() extends State<ProjectBoard> {
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: count,
separatorBuilder: (_, _) => const SizedBox(width: 12),
separatorBuilder: (context, index) => const SizedBox(width: 12),
itemBuilder: (context, index) =>
SizedBox(width: 320, child: column(index)),
);

Some files were not shown because too many files have changed in this diff Show more