feat(todo): filters in Todoist's language, and a view of each label (ADR 0033, D35)

A filter is a query read on the device, as the server sees no task, kept
sealed in `saved_filter`. Filters & labels is a screen of its own under
Browse; a label opens its tasks, and favourite filters and labels join
Browse's Favourites. D36 records what of Todoist's Pro stays out.

Whats-New: Filters: keep views like "today & p1", and open a label to see its tasks, under Browse in Filters & labels.
Whats-New-De: Filter: Speichere Ansichten wie „heute & p1“ und öffne ein Label, um seine Aufgaben zu sehen, in der Übersicht unter Filter & Labels.

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-06 19:44:46 +01:00
commit b1f2078357
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
47 changed files with 21688 additions and 71 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,613 @@
{
"__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": "saved_filter",
"dartName": "SavedFilter",
"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": "saved_filter_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "saved_filter_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": "saved_filter",
"dartName": "SavedFilter",
"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": "saved_filter_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "saved_filter_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -7,6 +7,7 @@ import 'package:serverpod_database/serverpod_database.dart';
part '20261101000019144-projects/migration.dart';
part '20261101000604776-tasks/migration.dart';
part '20261101180755366-settings/migration.dart';
part '20261106183802735-filters/migration.dart';
/// Migration registry for the client-side database.
class MigrationRegistry {
@ -16,6 +17,7 @@ class MigrationRegistry {
_Migration20261101000019144_projects(),
_Migration20261101000604776_tasks(),
_Migration20261101180755366_settings(),
_Migration20261106183802735_filters(),
];
/// List of all client-side database migration versions.

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;
/// A saved filter (ADR 0033, D35): its name, its query and how it shows, in
/// its sealed payload (D5); the instance sees that it exists, its size and
/// when it changed, and never runs it: tasks are matched on the device.
/// Changed when edited, the last writer winning. Synced.
abstract class SavedFilter
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
SavedFilter._({
this.id,
this.spaceId,
this.keyId,
required this.payload,
this.nonce,
});
factory SavedFilter({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _SavedFilterImpl;
factory SavedFilter.fromJson(Map<String, dynamic> jsonSerialization) {
return SavedFilter(
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 = SavedFilterTable();
static const db = SavedFilterRepository._();
@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 [SavedFilter]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
SavedFilter copyWith({
_isc.UuidValue? id,
int? spaceId,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'SavedFilter',
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__': 'SavedFilter',
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 SavedFilterInclude include() {
return SavedFilterInclude._();
}
static SavedFilterIncludeList includeList({
_isd.WhereExpressionBuilder<SavedFilterTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<SavedFilterTable>? orderBy,
_isd.OrderByListBuilder<SavedFilterTable>? orderByList,
SavedFilterInclude? include,
}) {
return SavedFilterIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(SavedFilter.t),
orderByList: orderByList?.call(SavedFilter.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _SavedFilterImpl extends SavedFilter {
_SavedFilterImpl({
_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 [SavedFilter]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
SavedFilter copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return SavedFilter(
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 SavedFilterUpdateTable extends _isd.UpdateTable<SavedFilterTable> {
SavedFilterUpdateTable(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 SavedFilterTable extends _isd.Table<_isc.UuidValue?> {
SavedFilterTable({super.tableRelation}) : super(tableName: 'saved_filter') {
updateTable = SavedFilterUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final SavedFilterUpdateTable 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 SavedFilterInclude extends _isd.IncludeObject {
SavedFilterInclude._();
@override
Map<String, _isd.Include?> get includes => {};
@override
_isd.Table<_isc.UuidValue?> get table => SavedFilter.t;
}
class SavedFilterIncludeList extends _isd.IncludeList {
SavedFilterIncludeList._({
_isd.WhereExpressionBuilder<SavedFilterTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(SavedFilter.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => SavedFilter.t;
}
class SavedFilterRepository {
const SavedFilterRepository._();
/// Returns a list of [SavedFilter]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<SavedFilter>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<SavedFilterTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<SavedFilterTable>? orderBy,
_isd.OrderByListBuilder<SavedFilterTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<SavedFilter>(
where: where?.call(SavedFilter.t),
orderBy: orderBy?.call(SavedFilter.t),
orderByList: orderByList?.call(SavedFilter.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [SavedFilter] 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<SavedFilter?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<SavedFilterTable>? where,
int? offset,
_isd.OrderByBuilder<SavedFilterTable>? orderBy,
_isd.OrderByListBuilder<SavedFilterTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<SavedFilter>(
where: where?.call(SavedFilter.t),
orderBy: orderBy?.call(SavedFilter.t),
orderByList: orderByList?.call(SavedFilter.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [SavedFilter] by its [id] or null if no such row exists.
Future<SavedFilter?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<SavedFilter>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [SavedFilter]s in the list and returns the inserted rows.
///
/// The returned [SavedFilter]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<SavedFilter>> insert(
_isd.DatabaseSession session,
List<SavedFilter> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<SavedFilter>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [SavedFilter] and returns the inserted row.
///
/// The returned [SavedFilter] will have its `id` field set.
Future<SavedFilter> insertRow(
_isd.DatabaseSession session,
SavedFilter row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<SavedFilter>(
row,
transaction: transaction,
);
}
/// Upserts all [SavedFilter]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 [SavedFilter]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<SavedFilter>> upsert(
_isd.DatabaseSession session,
List<SavedFilter> rows, {
required _isd.ColumnSelections<SavedFilterTable> conflictColumns,
_isd.ColumnSelections<SavedFilterTable>? updateColumns,
_isd.WhereExpressionBuilder<SavedFilterTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<SavedFilter>(
rows,
conflictColumns: conflictColumns(SavedFilter.t),
updateColumns: updateColumns?.call(SavedFilter.t),
updateWhere: updateWhere?.call(SavedFilter.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [SavedFilter] 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 [SavedFilter] will have its `id` field set.
Future<SavedFilter?> upsertRow(
_isd.DatabaseSession session,
SavedFilter row, {
required _isd.ColumnSelections<SavedFilterTable> conflictColumns,
_isd.ColumnSelections<SavedFilterTable>? updateColumns,
_isd.WhereExpressionBuilder<SavedFilterTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<SavedFilter>(
row,
conflictColumns: conflictColumns(SavedFilter.t),
updateColumns: updateColumns?.call(SavedFilter.t),
updateWhere: updateWhere?.call(SavedFilter.t),
transaction: transaction,
);
}
/// Updates all [SavedFilter]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<SavedFilter>> update(
_isd.DatabaseSession session,
List<SavedFilter> rows, {
_isd.ColumnSelections<SavedFilterTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<SavedFilter>(
rows,
columns: columns?.call(SavedFilter.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [SavedFilter]. 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<SavedFilter> updateRow(
_isd.DatabaseSession session,
SavedFilter row, {
_isd.ColumnSelections<SavedFilterTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<SavedFilter>(
row,
columns: columns?.call(SavedFilter.t),
transaction: transaction,
);
}
/// Updates a single [SavedFilter] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<SavedFilter?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<SavedFilterUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<SavedFilter>(
id,
columnValues: columnValues(SavedFilter.t.updateTable),
transaction: transaction,
);
}
/// Updates all [SavedFilter]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<SavedFilter>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<SavedFilterUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<SavedFilterTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<SavedFilterTable>? orderBy,
_isd.OrderByListBuilder<SavedFilterTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<SavedFilter>(
columnValues: columnValues(SavedFilter.t.updateTable),
where: where(SavedFilter.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(SavedFilter.t),
orderByList: orderByList?.call(SavedFilter.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [SavedFilter]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<SavedFilter>> delete(
_isd.DatabaseSession session,
List<SavedFilter> rows, {
_isd.OrderByBuilder<SavedFilterTable>? orderBy,
_isd.OrderByListBuilder<SavedFilterTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<SavedFilter>(
rows,
orderBy: orderBy?.call(SavedFilter.t),
orderByList: orderByList?.call(SavedFilter.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [SavedFilter].
Future<SavedFilter> deleteRow(
_isd.DatabaseSession session,
SavedFilter row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<SavedFilter>(
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<SavedFilter>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<SavedFilterTable> where,
_isd.OrderByBuilder<SavedFilterTable>? orderBy,
_isd.OrderByListBuilder<SavedFilterTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<SavedFilter>(
where: where(SavedFilter.t),
orderBy: orderBy?.call(SavedFilter.t),
orderByList: orderByList?.call(SavedFilter.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<SavedFilterTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<SavedFilter>(
where: where?.call(SavedFilter.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [SavedFilter] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<SavedFilterTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<SavedFilter>(
where: where(SavedFilter.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -23,6 +23,7 @@ import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart'
as _ipulbpi2;
import 'filter/saved_filter.dart' as _i9q9fhid;
import 'label/label.dart' as _ivak08z4;
import 'privacy/content_key.dart' as _imed3x5d;
import 'project/project.dart' as _i9c44spp;
@ -33,6 +34,7 @@ import 'search/place_point.dart' as _itf0ur2u;
import 'search/search_refused.dart' as _ihbxkwke;
import 'settings/todo_settings.dart' as _icak60vd;
import 'task/task.dart' as _ib4f8uyw;
export 'filter/saved_filter.dart';
export 'label/label.dart';
export 'privacy/content_key.dart';
export 'project/project.dart';
@ -249,6 +251,73 @@ class Protocol extends _isd.DatabaseSerializationManager {
],
managed: true,
),
_isd.TableDefinition(
name: 'saved_filter',
dartName: 'SavedFilter',
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: 'saved_filter_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: 'saved_filter_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'section',
dartName: 'Section',
@ -499,6 +568,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
}
if (t == _i9q9fhid.SavedFilter) {
return _i9q9fhid.SavedFilter.fromJson(data) as T;
}
if (t == _ivak08z4.Label) {
return _ivak08z4.Label.fromJson(data) as T;
}
@ -529,6 +601,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _ib4f8uyw.Task) {
return _ib4f8uyw.Task.fromJson(data) as T;
}
if (t == _isc.getType<_i9q9fhid.SavedFilter?>()) {
return (data != null ? _i9q9fhid.SavedFilter.fromJson(data) : null) as T;
}
if (t == _isc.getType<_ivak08z4.Label?>()) {
return (data != null ? _ivak08z4.Label.fromJson(data) : null) as T;
}
@ -588,6 +663,7 @@ class Protocol extends _isd.DatabaseSerializationManager {
static String? getClassNameForType(Type type) {
return switch (type) {
_i9q9fhid.SavedFilter => 'SavedFilter',
_ivak08z4.Label => 'Label',
_imed3x5d.ContentKey => 'ContentKey',
_i9c44spp.Project => 'Project',
@ -615,6 +691,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
switch (data) {
case _i9q9fhid.SavedFilter():
return 'SavedFilter';
case _ivak08z4.Label():
return 'Label';
case _imed3x5d.ContentKey():
@ -671,6 +749,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName is! String) {
return super.deserializeByClassName(data);
}
if (dataClassName == 'SavedFilter') {
return deserialize<_i9q9fhid.SavedFilter>(data['data']);
}
if (dataClassName == 'Label') {
return deserialize<_ivak08z4.Label>(data['data']);
}
@ -780,6 +861,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
}
switch (t) {
case _i9q9fhid.SavedFilter:
return _i9q9fhid.SavedFilter.t;
case _ivak08z4.Label:
return _ivak08z4.Label.t;
case _imed3x5d.ContentKey:

View file

@ -11,6 +11,7 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import 'filter/saved_filter.dart' as _i9q9fhid;
import 'label/label.dart' as _ivak08z4;
import 'privacy/content_key.dart' as _imed3x5d;
import 'project/project.dart' as _i9c44spp;
@ -25,6 +26,7 @@ final List<_isd.Table> syncTables = [
_imed3x5d.ContentKey.t,
_ivak08z4.Label.t,
_i9c44spp.Project.t,
_i9q9fhid.SavedFilter.t,
_imadkvi9.Section.t,
_ib4f8uyw.Task.t,
_icak60vd.TodoSettings.t,

View file

@ -31,6 +31,30 @@
"deadlineOn": "Frist {day}",
"deadlineNone": "Keine Frist",
"keysMissing": "Dieses Gerät kann nichts speichern: Es hat die Schlüssel deines Kontos nicht. Melde dich mit deinem Passwort oder deiner Wiederherstellungsphrase an, um sie hinzuzufügen.",
"filtersLabelsTitle": "Filter & Labels",
"filtersLabelsHint": "Gespeicherte Ansichten deiner Aufgaben",
"filtersTitle": "Meine Filter",
"filtersNone": "Noch keine Filter.",
"filterAdd": "Neuer Filter",
"filterEdit": "Filter bearbeiten",
"filterDelete": "Filter löschen",
"filterDeleteTitle": "Filter löschen?",
"filterName": "Name",
"filterDescription": "Beschreibung",
"filterQuery": "Abfrage",
"filterQueryHint": "heute & p1",
"filterFavorite": "Zu Favoriten hinzufügen",
"filterUnfavorite": "Aus Favoriten entfernen",
"filterPreviewEmpty": "Noch passt nichts.",
"filterNothing": "Gerade sind keine Aufgaben in diesem Filter.",
"filterHelp": "heute, morgen, überfällig, ohne datum, 7 tage, p1 bis p4, #Projekt, ##Projekt mit Unterprojekten, /Abschnitt, @label, suche: Wort. Verbinde sie mit & (und), | (oder), ! (nicht) und Klammern; * steht für Beliebiges in einem Namen.",
"filterMenu": "Filtermenü",
"labelMenu": "Labelmenü",
"labelNothing": "Keine offene Aufgabe hat dieses Label.",
"labelEditEntry": "Label bearbeiten",
"filterMatches": "{count, plural, =1{1 Aufgabe passt} other{{count} Aufgaben passen}}",
"filterNotRead": "Nicht verstanden bei „{near}“",
"filterNotReadEnd": "Die Abfrage endet zu früh",
"labelsTitle": "Labels",
"labelsNone": "Noch keine Labels.",
"labelsNoneSet": "Keine Labels",

View file

@ -72,6 +72,49 @@
},
"deadlineNone": "No deadline",
"keysMissing": "This device can't save: it doesn't hold your account's keys. Sign in with your password or recovery phrase to add them.",
"filtersLabelsTitle": "Filters & labels",
"filtersLabelsHint": "Views of your tasks, kept",
"filtersTitle": "My filters",
"filtersNone": "No filters yet.",
"filterAdd": "New filter",
"filterEdit": "Edit filter",
"filterDelete": "Delete filter",
"filterDeleteTitle": "Delete the filter?",
"filterName": "Name",
"filterDescription": "Description",
"filterQuery": "Query",
"filterQueryHint": "today & p1",
"filterFavorite": "Add to favourites",
"filterUnfavorite": "Remove from favourites",
"filterPreviewEmpty": "Nothing matches yet.",
"filterNothing": "No tasks in this filter at the moment.",
"filterHelp": "today, tomorrow, overdue, no date, 7 days, p1 to p4, #Project, ##Project with its sub-projects, /Section, @label, search: word. Combine them with & (and), | (or), ! (not) and parentheses; * stands for anything in a name.",
"filterMenu": "Filter menu",
"labelMenu": "Label menu",
"labelNothing": "No open tasks have this label.",
"labelEditEntry": "Edit label",
"filterMatches": "{count, plural, =1{1 task matches} other{{count} tasks match}}",
"@filterMatches": {
"description": "The filter editor's preview.",
"placeholders": {
"count": {
"type": "int"
}
}
},
"filterNotRead": "Not understood near “{near}”",
"@filterNotRead": {
"description": "The filter editor when its query cannot be read.",
"placeholders": {
"near": {
"type": "String"
}
}
},
"filterNotReadEnd": "This query ends too soon",
"@filterNotReadEnd": {
"description": "The filter editor when its query stops before it is complete."
},
"labelsTitle": "Labels",
"labelsNone": "No labels yet.",
"labelsNoneSet": "No labels",

View file

@ -286,6 +286,150 @@ abstract class AppLocalizations {
/// **'This device can\'t save: it doesn\'t hold your account\'s keys. Sign in with your password or recovery phrase to add them.'**
String get keysMissing;
/// No description provided for @filtersLabelsTitle.
///
/// In en, this message translates to:
/// **'Filters & labels'**
String get filtersLabelsTitle;
/// No description provided for @filtersLabelsHint.
///
/// In en, this message translates to:
/// **'Views of your tasks, kept'**
String get filtersLabelsHint;
/// No description provided for @filtersTitle.
///
/// In en, this message translates to:
/// **'My filters'**
String get filtersTitle;
/// No description provided for @filtersNone.
///
/// In en, this message translates to:
/// **'No filters yet.'**
String get filtersNone;
/// No description provided for @filterAdd.
///
/// In en, this message translates to:
/// **'New filter'**
String get filterAdd;
/// No description provided for @filterEdit.
///
/// In en, this message translates to:
/// **'Edit filter'**
String get filterEdit;
/// No description provided for @filterDelete.
///
/// In en, this message translates to:
/// **'Delete filter'**
String get filterDelete;
/// No description provided for @filterDeleteTitle.
///
/// In en, this message translates to:
/// **'Delete the filter?'**
String get filterDeleteTitle;
/// No description provided for @filterName.
///
/// In en, this message translates to:
/// **'Name'**
String get filterName;
/// No description provided for @filterDescription.
///
/// In en, this message translates to:
/// **'Description'**
String get filterDescription;
/// No description provided for @filterQuery.
///
/// In en, this message translates to:
/// **'Query'**
String get filterQuery;
/// No description provided for @filterQueryHint.
///
/// In en, this message translates to:
/// **'today & p1'**
String get filterQueryHint;
/// No description provided for @filterFavorite.
///
/// In en, this message translates to:
/// **'Add to favourites'**
String get filterFavorite;
/// No description provided for @filterUnfavorite.
///
/// In en, this message translates to:
/// **'Remove from favourites'**
String get filterUnfavorite;
/// No description provided for @filterPreviewEmpty.
///
/// In en, this message translates to:
/// **'Nothing matches yet.'**
String get filterPreviewEmpty;
/// No description provided for @filterNothing.
///
/// In en, this message translates to:
/// **'No tasks in this filter at the moment.'**
String get filterNothing;
/// No description provided for @filterHelp.
///
/// In en, this message translates to:
/// **'today, tomorrow, overdue, no date, 7 days, p1 to p4, #Project, ##Project with its sub-projects, /Section, @label, search: word. Combine them with & (and), | (or), ! (not) and parentheses; * stands for anything in a name.'**
String get filterHelp;
/// No description provided for @filterMenu.
///
/// In en, this message translates to:
/// **'Filter menu'**
String get filterMenu;
/// No description provided for @labelMenu.
///
/// In en, this message translates to:
/// **'Label menu'**
String get labelMenu;
/// No description provided for @labelNothing.
///
/// In en, this message translates to:
/// **'No open tasks have this label.'**
String get labelNothing;
/// No description provided for @labelEditEntry.
///
/// In en, this message translates to:
/// **'Edit label'**
String get labelEditEntry;
/// The filter editor's preview.
///
/// In en, this message translates to:
/// **'{count, plural, =1{1 task matches} other{{count} tasks match}}'**
String filterMatches(int count);
/// The filter editor when its query cannot be read.
///
/// In en, this message translates to:
/// **'Not understood near “{near}”'**
String filterNotRead(String near);
/// The filter editor when its query stops before it is complete.
///
/// In en, this message translates to:
/// **'This query ends too soon'**
String get filterNotReadEnd;
/// No description provided for @labelsTitle.
///
/// In en, this message translates to:

View file

@ -120,6 +120,89 @@ class AppLocalizationsDe extends AppLocalizations {
String get keysMissing =>
'Dieses Gerät kann nichts speichern: Es hat die Schlüssel deines Kontos nicht. Melde dich mit deinem Passwort oder deiner Wiederherstellungsphrase an, um sie hinzuzufügen.';
@override
String get filtersLabelsTitle => 'Filter & Labels';
@override
String get filtersLabelsHint => 'Gespeicherte Ansichten deiner Aufgaben';
@override
String get filtersTitle => 'Meine Filter';
@override
String get filtersNone => 'Noch keine Filter.';
@override
String get filterAdd => 'Neuer Filter';
@override
String get filterEdit => 'Filter bearbeiten';
@override
String get filterDelete => 'Filter löschen';
@override
String get filterDeleteTitle => 'Filter löschen?';
@override
String get filterName => 'Name';
@override
String get filterDescription => 'Beschreibung';
@override
String get filterQuery => 'Abfrage';
@override
String get filterQueryHint => 'heute & p1';
@override
String get filterFavorite => 'Zu Favoriten hinzufügen';
@override
String get filterUnfavorite => 'Aus Favoriten entfernen';
@override
String get filterPreviewEmpty => 'Noch passt nichts.';
@override
String get filterNothing => 'Gerade sind keine Aufgaben in diesem Filter.';
@override
String get filterHelp =>
'heute, morgen, überfällig, ohne datum, 7 tage, p1 bis p4, #Projekt, ##Projekt mit Unterprojekten, /Abschnitt, @label, suche: Wort. Verbinde sie mit & (und), | (oder), ! (nicht) und Klammern; * steht für Beliebiges in einem Namen.';
@override
String get filterMenu => 'Filtermenü';
@override
String get labelMenu => 'Labelmenü';
@override
String get labelNothing => 'Keine offene Aufgabe hat dieses Label.';
@override
String get labelEditEntry => 'Label bearbeiten';
@override
String filterMatches(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count Aufgaben passen',
one: '1 Aufgabe passt',
);
return '$_temp0';
}
@override
String filterNotRead(String near) {
return 'Nicht verstanden bei „$near“';
}
@override
String get filterNotReadEnd => 'Die Abfrage endet zu früh';
@override
String get labelsTitle => 'Labels';

View file

@ -120,6 +120,89 @@ class AppLocalizationsEn extends AppLocalizations {
String get keysMissing =>
'This device can\'t save: it doesn\'t hold your account\'s keys. Sign in with your password or recovery phrase to add them.';
@override
String get filtersLabelsTitle => 'Filters & labels';
@override
String get filtersLabelsHint => 'Views of your tasks, kept';
@override
String get filtersTitle => 'My filters';
@override
String get filtersNone => 'No filters yet.';
@override
String get filterAdd => 'New filter';
@override
String get filterEdit => 'Edit filter';
@override
String get filterDelete => 'Delete filter';
@override
String get filterDeleteTitle => 'Delete the filter?';
@override
String get filterName => 'Name';
@override
String get filterDescription => 'Description';
@override
String get filterQuery => 'Query';
@override
String get filterQueryHint => 'today & p1';
@override
String get filterFavorite => 'Add to favourites';
@override
String get filterUnfavorite => 'Remove from favourites';
@override
String get filterPreviewEmpty => 'Nothing matches yet.';
@override
String get filterNothing => 'No tasks in this filter at the moment.';
@override
String get filterHelp =>
'today, tomorrow, overdue, no date, 7 days, p1 to p4, #Project, ##Project with its sub-projects, /Section, @label, search: word. Combine them with & (and), | (or), ! (not) and parentheses; * stands for anything in a name.';
@override
String get filterMenu => 'Filter menu';
@override
String get labelMenu => 'Label menu';
@override
String get labelNothing => 'No open tasks have this label.';
@override
String get labelEditEntry => 'Edit label';
@override
String filterMatches(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count tasks match',
one: '1 task matches',
);
return '$_temp0';
}
@override
String filterNotRead(String near) {
return 'Not understood near “$near”';
}
@override
String get filterNotReadEnd => 'This query ends too soon';
@override
String get labelsTitle => 'Labels';

View file

@ -11,6 +11,8 @@ import 'package:halkyon_todo_client/halkyon_todo_client.dart'
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
import 'package:halkyon_todo_flutter/src/core/privacy/sync_content_key_gateway.dart';
import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
import 'package:halkyon_todo_flutter/src/feature/filter/data/filter_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/filter/data/sync_filter_gateway.dart';
import 'package:halkyon_todo_flutter/src/feature/label/data/label_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/label/data/sync_label_gateway.dart';
import 'package:halkyon_todo_flutter/src/feature/location/data/location_access.dart';
@ -62,6 +64,9 @@ class TodoPerson(
/// Their labels, sealed and synced.
required final LabelRepository labels,
/// Their filters, sealed and synced (D35).
required final FilterRepository filters,
/// Their settings of Todo's (Upcoming's Display), sealed and synced.
required final SettingsRepository settings,
@ -406,6 +411,7 @@ Future<TodoPerson> _openPersonData(
automaticReminder: () async => (await settings.read()).reminders.automatic,
);
final labels = LabelRepository(SyncLabelGateway(db, sync), keys);
final filters = FilterRepository(SyncFilterGateway(db, sync), keys);
// The Inbox, made once by whichever device is first; nothing where this
// device holds none of the account's keys.
unawaited(
@ -422,6 +428,7 @@ Future<TodoPerson> _openPersonData(
sections: sections,
tasks: tasks,
labels: labels,
filters: filters,
settings: settings,
reminders: reminders,
notifications: notifications,
@ -434,6 +441,7 @@ Future<TodoPerson> _openPersonData(
await sections.dispose();
await tasks.dispose();
await labels.dispose();
await filters.dispose();
await settings.dispose();
await sync.dispose();
await db.close();

View file

@ -16,6 +16,15 @@ abstract final class TodoPaths() {
/// A project's screen, in Browse.
static String project(String id) => '/browse/projects/$id';
/// Filters and labels (D35), in Browse.
static const filters = '/browse/filters';
/// A filter's tasks.
static String filter(String id) => '/browse/filters/$id';
/// A label's tasks.
static String label(String id) => '/browse/labels/$id';
/// Settings, over the tabs.
static const settings = '/settings';

View file

@ -1,6 +1,8 @@
import 'package:go_router/go_router.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_todo_flutter/src/app/todo_paths.dart';
import 'package:halkyon_todo_flutter/src/feature/filter/widget/filter_screen.dart';
import 'package:halkyon_todo_flutter/src/feature/filter/widget/filters_screen.dart';
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_screen.dart';
import 'package:halkyon_todo_flutter/src/settings/settings_screen.dart';
import 'package:halkyon_todo_flutter/src/tabs/browse_tab.dart';
@ -58,6 +60,22 @@ GoRouter buildTodoRouter() => GoRouter(
builder: (context, state) =>
ProjectScreen(projectId: state.pathParameters['id']!),
),
GoRoute(
path: 'filters',
builder: (context, state) => const FiltersScreen(),
routes: [
GoRoute(
path: ':id',
builder: (context, state) =>
FilterScreen(filterId: state.pathParameters['id']!),
),
],
),
GoRoute(
path: 'labels/:id',
builder: (context, state) =>
LabelScreen(labelId: state.pathParameters['id']!),
),
],
),
],

View file

@ -0,0 +1,34 @@
import 'dart:typed_data';
import 'package:halkyon_todo_flutter/src/core/sealed/json_payload.dart';
import 'package:halkyon_todo_flutter/src/feature/filter/model/saved_filter.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
/// The schema of a filter's payload this app writes and reads.
const filterSchemaVersion = 1;
/// [content] as its row's payload, before sealing.
ByteData encodeFilter(FilterContent content) =>
encodeJson(filterSchemaVersion, {
'name': content.name,
'query': content.query,
if (content.description.isNotEmpty) 'description': content.description,
'color': ?content.color?.name,
if (content.favorite) 'favorite': true,
'position': content.position,
});
/// The filter [payload] holds, opened; null for one it can't read.
FilterContent? decodeFilter(ByteData payload) => decodeJson(
payload,
version: filterSchemaVersion,
name: 'Filter',
read: (json) => FilterContent(
name: json['name']! as String,
query: json['query']! as String,
description: json['description'] as String? ?? '',
color: PaletteColor.byName(json['color'] as String?),
favorite: json['favorite'] as bool? ?? false,
position: (json['position'] as num?)?.toDouble() ?? 0,
),
);

View file

@ -0,0 +1,37 @@
import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
import 'package:halkyon_todo_flutter/src/core/sealed/sealed_row_gateway.dart';
import 'package:halkyon_todo_flutter/src/core/sealed/sealed_rows.dart';
import 'package:halkyon_todo_flutter/src/feature/filter/data/filter_payload.dart';
import 'package:halkyon_todo_flutter/src/feature/filter/model/saved_filter.dart';
/// The person's filters (ADR 0033, D35), each sealed with their key for its
/// row (D5, [SealedRows]).
class FilterRepository(final SealedRowGateway _gateway, final TodoKeys _keys) {
late final _rows = SealedRows<FilterContent>(
_gateway,
_keys,
table: 'saved_filter',
encode: encodeFilter,
decode: decodeFilter,
);
/// Fires when the filters changed, here or on another device.
Stream<void> get changes => _rows.changes;
/// Every filter, by when it was made.
Future<List<StoredFilter>> all() async => [
for (final (:id, :content) in await _rows.all()) StoredFilter(id, content),
];
/// Keeps a new filter and returns its ID; throws [KeysMissing] where this
/// device can't seal.
Future<String> add(FilterContent content) => _rows.insert(content);
Future<void> update(String id, FilterContent content) =>
_rows.update(id, content);
Future<void> delete(String id) => _rows.delete([id]);
/// Closes what it holds open.
Future<void> dispose() => _rows.dispose();
}

View file

@ -0,0 +1,99 @@
import 'dart:typed_data';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_todo_client/halkyon_todo_client.dart' as protocol;
import 'package:halkyon_todo_flutter/src/core/sealed/sealed_row_gateway.dart';
import 'package:serverpod_offline_sync_client/offline_sync.dart';
/// The person's filters on their local database ([SealedRowGateway]), which the
/// sync engine keeps in step with their other devices.
class SyncFilterGateway(
final OfflineSyncDatabaseSession _db,
final SyncService _sync,
) implements SealedRowGateway {
@override
Future<List<SealedRow>> rows() async => [
for (final row in await protocol.SavedFilter.db.find(
_db,
orderBy: (t) => t.id,
))
(
id: row.id!.uuid,
envelope: (keyId: row.keyId, nonce: row.nonce, payload: row.payload),
),
];
@override
Future<void> insert(SealedRow row) async {
await protocol.SavedFilter.db.insertRow(
_db,
protocol.SavedFilter(
id: protocol.UuidValue.withValidation(row.id),
keyId: row.envelope.keyId,
nonce: row.envelope.nonce,
payload: row.envelope.payload,
),
);
_sync.requestSync();
}
@override
Future<void> update(String id, Envelope envelope) async {
if (await protocol.SavedFilter.db.findById(
_db,
protocol.UuidValue.withValidation(id),
)
case final row?) {
// Only the named columns change (ADR 0001, rule 1).
await protocol.SavedFilter.db.updateRow(
_db,
row.copyWith(
keyId: envelope.keyId,
nonce: envelope.nonce,
payload: envelope.payload,
),
columns: (t) => [t.keyId, t.nonce, t.payload],
);
_sync.requestSync();
}
}
@override
Future<void> delete(Iterable<String> ids) async {
final uuids = <protocol.UuidValue>{
for (final id in ids) protocol.UuidValue.withValidation(id),
};
if (uuids.isEmpty) {
return;
}
await _db.db.transaction((transaction) async {
// Blanked first (ADR 0002, rule 2): the instance keeps deleted rows as
// they last were, until the space goes.
for (final row in await protocol.SavedFilter.db.find(
_db,
where: (t) => t.id.inSet(uuids),
transaction: transaction,
)) {
await protocol.SavedFilter.db.updateRow(
_db,
protocol.SavedFilter(
id: row.id,
spaceId: row.spaceId,
payload: ByteData(0),
),
columns: (t) => [t.keyId, t.nonce, t.payload],
transaction: transaction,
);
}
await protocol.SavedFilter.db.deleteWhere(
_db,
where: (t) => t.id.inSet(uuids),
transaction: transaction,
);
});
_sync.requestSync();
}
@override
Stream<void> get remoteChanges => _sync.merges;
}

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