From fa8ce5bc7a3c064ce9a3c1e4a5b342d83316810b Mon Sep 17 00:00:00 2001 From: Bhasher Date: Sat, 17 Jun 2023 23:37:41 +0200 Subject: [PATCH] Groups models & move export settings from model to localData --- lib/data/local/group.dart | 74 +++++++++++ lib/data/local/group_membership.dart | 63 +++++++++ lib/data/local/instance.dart | 47 ++++++- lib/data/local/item.dart | 69 +++++++++- lib/data/local/item_part.dart | 51 +++++++- lib/data/local/participant.dart | 49 ++++++- lib/data/local/project.dart | 121 ++++++++++++++--- lib/data/pocketbase/group.dart | 88 +++++++++++++ lib/data/pocketbase/group_membership.dart | 88 +++++++++++++ lib/data/pocketbase/provider.dart | 7 + lib/models/app_data.dart | 6 +- lib/models/group.dart | 50 +++++++ lib/models/group_membership.dart | 36 +++++ lib/models/instance.dart | 43 ------ lib/models/item.dart | 59 --------- lib/models/item_part.dart | 48 ------- lib/models/participant.dart | 46 +------ lib/models/project.dart | 75 ++--------- lib/screens/projects_list/projects_list.dart | 1 + lib/services/database.dart | 131 ++++++++++++------- 20 files changed, 819 insertions(+), 333 deletions(-) create mode 100644 lib/data/local/group.dart create mode 100644 lib/data/local/group_membership.dart create mode 100644 lib/data/pocketbase/group.dart create mode 100644 lib/data/pocketbase/group_membership.dart create mode 100644 lib/models/group.dart create mode 100644 lib/models/group_membership.dart diff --git a/lib/data/local/group.dart b/lib/data/local/group.dart new file mode 100644 index 0000000..cc5774c --- /dev/null +++ b/lib/data/local/group.dart @@ -0,0 +1,74 @@ +import 'package:splitr/data/local/group_membership.dart'; +import 'package:sqflite/sqflite.dart'; + +import '../../models/app_data.dart'; +import '../../models/group.dart'; +import '../../models/project.dart'; +import 'generic.dart'; + +String tableGroup = 'groups'; + +class LocalGroupFields { + static const values = [ + localId, + remoteId, + projectId, + lastUpdate, + deleted, + ]; + + static const String localId = 'local_id'; + static const String remoteId = 'remote_id'; + static const String projectId = 'project_id'; + static const String name = 'name'; + static const String lastUpdate = 'last_update'; + static const String deleted = 'deleted'; +} + +class LocalGroup extends LocalGeneric { + LocalGroup(this.group); + + final Group group; + + @override + Future save() async { + group.localId = await AppData.db.insert( + tableGroup, + toJson(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + return true; + } + + Map toJson() => { + LocalGroupFields.localId: group.localId, + LocalGroupFields.remoteId: group.remoteId, + LocalGroupFields.projectId: group.project.localId, + LocalGroupFields.name: group.name, + LocalGroupFields.lastUpdate: group.lastUpdate, + LocalGroupFields.deleted: group.deleted, + }; + + static Group fromJson(Project p, Map json) { + return Group( + localId: json[LocalGroupFields.localId] as int?, + remoteId: json[LocalGroupFields.remoteId] as String?, + project: p, + name: json[LocalGroupFields.name] as String, + lastUpdate: DateTime.fromMillisecondsSinceEpoch( + json[LocalGroupFields.lastUpdate] as int), + deleted: (json[LocalGroupFields.deleted] as int) == 1, + ); + } + + Future loadMembers() async { + group.members = (await AppData.db.query( + tableGroupMembership, + columns: LocalGroupMembershipFields.values, + where: '${LocalGroupMembershipFields.groupId} = ?', + whereArgs: [group.localId], + )) + .map((e) => LocalGroupMembership.fromJson(group, e)) + .toSet(); + } +} diff --git a/lib/data/local/group_membership.dart b/lib/data/local/group_membership.dart new file mode 100644 index 0000000..84e836d --- /dev/null +++ b/lib/data/local/group_membership.dart @@ -0,0 +1,63 @@ +import 'package:splitr/models/group.dart'; +import 'package:sqflite/sqflite.dart'; + +import '../../models/app_data.dart'; +import '../../models/group_membership.dart'; +import 'generic.dart'; + +String tableGroupMembership = 'groupMemberships'; + +class LocalGroupMembershipFields { + static const values = [ + localId, + remoteId, + groupId, + participantId, + lastUpdate, + deleted, + ]; + + static const String localId = 'local_id'; + static const String remoteId = 'remote_id'; + static const String groupId = 'group_id'; + static const String participantId = 'participant_id'; + static const String lastUpdate = 'last_update'; + static const String deleted = 'deleted'; +} + +class LocalGroupMembership extends LocalGeneric { + LocalGroupMembership(this.groupMembership); + + final GroupMembership groupMembership; + + @override + Future save() async { + groupMembership.localId = await AppData.db.insert( + tableGroupMembership, + toJson(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + return true; + } + + Map toJson() => { + LocalGroupMembershipFields.localId: groupMembership.localId, + LocalGroupMembershipFields.remoteId: groupMembership.remoteId, + LocalGroupMembershipFields.groupId: groupMembership.group.localId, + LocalGroupMembershipFields.lastUpdate: groupMembership.lastUpdate, + LocalGroupMembershipFields.deleted: groupMembership.deleted, + }; + + static GroupMembership fromJson(Group group, Map json) { + return GroupMembership( + localId: json[LocalGroupMembershipFields.localId] as int?, + remoteId: json[LocalGroupMembershipFields.remoteId] as String?, + group: group, + participant: group.project.participants.firstWhere((e) => + e.localId == json[LocalGroupMembershipFields.participantId] as int), + lastUpdate: DateTime.fromMillisecondsSinceEpoch( + json[LocalGroupMembershipFields.lastUpdate] as int), + deleted: (json[LocalGroupMembershipFields.deleted] as int) == 1, + ); + } +} diff --git a/lib/data/local/instance.dart b/lib/data/local/instance.dart index cdfd138..a137368 100644 --- a/lib/data/local/instance.dart +++ b/lib/data/local/instance.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:sqflite/sqflite.dart'; import '../../models/app_data.dart'; @@ -5,6 +7,20 @@ import '../../models/instance.dart'; const String tableInstances = 'instances'; +class LocalInstanceFields { + static const values = [ + localId, + type, + name, + data, + ]; + + static const String localId = 'local_id'; + static const String type = 'type'; + static const String name = 'name'; + static const String data = 'data'; +} + class LocalInstance { LocalInstance(this.instance); @@ -13,7 +29,7 @@ class LocalInstance { Future save() async { instance.localId = await AppData.db.insert( tableInstances, - instance.toJson(), + toJson(), conflictAlgorithm: ConflictAlgorithm.replace, ); } @@ -21,9 +37,36 @@ class LocalInstance { Future delete() async { return await AppData.db.delete( tableInstances, - where: '${InstanceFields.localId} = ?', + where: '${LocalInstanceFields.localId} = ?', whereArgs: [instance.localId], ) > 0; } + + Map toJson() { + return { + LocalInstanceFields.localId: instance.localId, + LocalInstanceFields.type: instance.type, + LocalInstanceFields.name: instance.name, + LocalInstanceFields.data: json.encode(instance.data), + }; + } + + static Instance fromJson(Map jsonMap) { + return Instance( + localId: jsonMap[LocalInstanceFields.localId] as int?, + type: jsonMap[LocalInstanceFields.type] as String, + name: jsonMap[LocalInstanceFields.name] as String, + data: json.decode(jsonMap[LocalInstanceFields.data] as String) + as Map, + ); + } + + static Future> getAllInstances() async { + final res = await AppData.db.query( + tableInstances, + columns: LocalInstanceFields.values, + ); + return res.map((e) => fromJson(e)).toSet(); + } } diff --git a/lib/data/local/item.dart b/lib/data/local/item.dart index 4cf16b9..2c4058f 100644 --- a/lib/data/local/item.dart +++ b/lib/data/local/item.dart @@ -4,10 +4,35 @@ import 'package:sqflite/sqlite_api.dart'; import '../../models/app_data.dart'; import '../../models/item.dart'; import '../../models/item_part.dart'; +import '../../models/project.dart'; import 'item_part.dart'; const String tableItems = 'items'; +class LocalItemFields { + static const values = [ + localId, + remoteId, + project, + title, + emitter, + amount, + date, + lastUpdate, + deleted, + ]; + + static const String localId = 'local_id'; + static const String remoteId = 'remote_id'; + static const String project = 'project'; + static const String title = 'title'; + static const String emitter = 'emitter'; + static const String amount = 'amount'; + static const String date = 'date'; + static const String lastUpdate = 'last_update'; + static const String deleted = 'deleted'; +} + class LocalItem extends LocalGeneric { LocalItem(this.item); @@ -16,11 +41,11 @@ class LocalItem extends LocalGeneric { Future loadParts() async { item.itemParts = (await AppData.db.query( tableItemParts, - columns: ItemPartFields.values, - where: '${ItemPartFields.itemId} = ?', + columns: LocalItemPartFields.values, + where: '${LocalItemPartFields.itemId} = ?', whereArgs: [item.localId], )) - .map((e) => ItemPart.fromJson(e, item)) + .map((e) => LocalItemPart.fromJson(e, item)) .toList(); } @@ -28,7 +53,7 @@ class LocalItem extends LocalGeneric { Future save() async { item.localId = await AppData.db.insert( tableItems, - item.toJson(), + toJson(), conflictAlgorithm: ConflictAlgorithm.replace, ); item.project.notSyncCount++; @@ -41,4 +66,40 @@ class LocalItem extends LocalGeneric { await ip.conn.save(); } } + + Map toJson() => { + LocalItemFields.localId: item.localId, + LocalItemFields.remoteId: item.remoteId, + LocalItemFields.project: item.project.localId, + LocalItemFields.title: item.title, + LocalItemFields.emitter: item.emitter.localId, + LocalItemFields.amount: item.amount, + LocalItemFields.date: item.date.millisecondsSinceEpoch, + LocalItemFields.lastUpdate: DateTime.now().millisecondsSinceEpoch, + LocalItemFields.deleted: item.deleted ? 1 : 0, + }; + + static Item fromJson(Map json, {Project? project}) { + Project p; + if (project != null) { + p = project; + } else { + p = Project.fromId(json[LocalItemFields.project] as int)!; + } + + return Item( + localId: json[LocalItemFields.localId] as int?, + remoteId: json[LocalItemFields.remoteId] as String?, + project: p, + title: json[LocalItemFields.title] as String, + emitter: p.participants.firstWhere((participant) => + participant.localId == json[LocalItemFields.emitter] as int), + amount: json[LocalItemFields.amount] as double, + date: DateTime.fromMillisecondsSinceEpoch( + json[LocalItemFields.date] as int), + lastUpdate: DateTime.fromMillisecondsSinceEpoch( + json[LocalItemFields.lastUpdate] as int), + deleted: (json[LocalItemFields.deleted] as int) == 1, + ); + } } diff --git a/lib/data/local/item_part.dart b/lib/data/local/item_part.dart index 1e8b9f9..920c1df 100644 --- a/lib/data/local/item_part.dart +++ b/lib/data/local/item_part.dart @@ -2,10 +2,33 @@ import 'package:splitr/data/local/generic.dart'; import 'package:sqflite/sqflite.dart'; import '../../models/app_data.dart'; +import '../../models/item.dart'; import '../../models/item_part.dart'; const String tableItemParts = 'itemParts'; +class LocalItemPartFields { + static const values = [ + localId, + remoteId, + itemId, + participantId, + rate, + amount, + lastUpdate, + deleted, + ]; + + static const String localId = 'local_id'; + static const String remoteId = 'remote_id'; + static const String itemId = 'item'; + static const String participantId = 'participant'; + static const String rate = 'rate'; + static const String amount = 'amount'; + static const String lastUpdate = 'last_update'; + static const String deleted = 'deleted'; +} + class LocalItemPart extends LocalGeneric { LocalItemPart(this.itemPart); @@ -15,11 +38,37 @@ class LocalItemPart extends LocalGeneric { Future save() async { itemPart.localId = await AppData.db.insert( tableItemParts, - itemPart.toJson(), + toJson(), conflictAlgorithm: ConflictAlgorithm.replace, ); itemPart.item.project.notSyncCount++; return true; } + + Map toJson() => { + LocalItemPartFields.localId: itemPart.localId, + LocalItemPartFields.remoteId: itemPart.remoteId, + LocalItemPartFields.itemId: itemPart.item.localId, + LocalItemPartFields.participantId: itemPart.participant.localId, + LocalItemPartFields.rate: itemPart.rate, + LocalItemPartFields.amount: itemPart.amount, + LocalItemPartFields.lastUpdate: DateTime.now().millisecondsSinceEpoch, + LocalItemPartFields.deleted: itemPart.deleted ? 1 : 0, + }; + + static ItemPart fromJson(Map json, Item item) { + return ItemPart( + localId: json[LocalItemPartFields.localId] as int?, + remoteId: json[LocalItemPartFields.remoteId] as String?, + item: item, + participant: item.project.participants.firstWhere( + (e) => e.localId == json[LocalItemPartFields.participantId] as int), + rate: json[LocalItemPartFields.rate] as double?, + amount: json[LocalItemPartFields.amount] as double?, + lastUpdate: DateTime.fromMillisecondsSinceEpoch( + json[LocalItemPartFields.lastUpdate] as int), + deleted: (json[LocalItemPartFields.deleted] as int) == 1, + ); + } } diff --git a/lib/data/local/participant.dart b/lib/data/local/participant.dart index 2d89c7b..aab270d 100644 --- a/lib/data/local/participant.dart +++ b/lib/data/local/participant.dart @@ -1,11 +1,34 @@ -import 'package:splitr/data/local/generic.dart'; import 'package:sqflite/sqflite.dart'; import '../../models/app_data.dart'; import '../../models/participant.dart'; +import '../../models/project.dart'; +import 'generic.dart'; const String tableParticipants = 'participants'; +class LocalParticipantFields { + static const values = [ + localId, + remoteId, + projectId, + pseudo, + lastname, + firstname, + lastUpdate, + deleted, + ]; + + static const String localId = 'local_id'; + static const String remoteId = 'remote_id'; + static const String projectId = 'project_id'; + static const String pseudo = 'pseudo'; + static const String lastname = 'lastname'; + static const String firstname = 'firstname'; + static const String lastUpdate = 'last_update'; + static const String deleted = 'deleted'; +} + class LocalParticipant extends LocalGeneric { LocalParticipant(this.participant); @@ -15,10 +38,32 @@ class LocalParticipant extends LocalGeneric { Future save() async { participant.localId = await AppData.db.insert( tableParticipants, - participant.toJson(), + toJson(), conflictAlgorithm: ConflictAlgorithm.replace, ); participant.project.notSyncCount++; return true; } + + Map toJson() => { + LocalParticipantFields.localId: participant.localId, + LocalParticipantFields.remoteId: participant.remoteId, + LocalParticipantFields.projectId: participant.project.localId, + LocalParticipantFields.pseudo: participant.pseudo, + LocalParticipantFields.lastUpdate: + DateTime.now().millisecondsSinceEpoch, + LocalParticipantFields.deleted: participant.deleted ? 1 : 0, + }; + + static Participant fromJson(Project p, Map json) { + return Participant( + localId: json[LocalParticipantFields.localId] as int?, + remoteId: json[LocalParticipantFields.remoteId] as String?, + project: p, + pseudo: json[LocalParticipantFields.pseudo] as String, + lastUpdate: DateTime.fromMillisecondsSinceEpoch( + json[LocalParticipantFields.lastUpdate] as int), + deleted: (json[LocalParticipantFields.deleted] as int) == 1, + ); + } } diff --git a/lib/data/local/project.dart b/lib/data/local/project.dart index 3bcf671..fd5b403 100644 --- a/lib/data/local/project.dart +++ b/lib/data/local/project.dart @@ -1,27 +1,84 @@ -import 'package:splitr/data/local/generic.dart'; +import 'package:splitr/data/local/group.dart'; +import 'package:splitr/models/group.dart'; import 'package:sqflite/sqflite.dart'; import '../../models/app_data.dart'; +import '../../models/instance.dart'; import '../../models/item.dart'; import '../../models/participant.dart'; import '../../models/project.dart'; +import '../../utils/helper/random.dart'; +import 'generic.dart'; import 'item.dart'; import 'participant.dart'; const String tableProjects = 'projects'; +class ProjectFields { + static const values = [ + localId, + remoteId, + name, + code, + currentParticipant, + instance, + lastSync, + lastUpdate, + deleted, + ]; + + static const String localId = 'local_id'; + static const String remoteId = 'remote_id'; + static const String name = 'name'; + static const String code = 'code'; + static const String currentParticipant = 'current_participant'; + static const String instance = 'instance'; + static const String lastSync = 'last_sync'; + static const String lastUpdate = 'last_update'; + static const String deleted = 'deleted'; +} + class LocalProject extends LocalGeneric { LocalProject(this.project); final Project project; + static Future> getAllProjects() async { + final res = await AppData.db.query( + tableProjects, + columns: ProjectFields.values, + ); + return res.map((e) => fromJson(e)).toSet(); + } + + Future loadParticipants() async { + final rawParticipants = await AppData.db.query( + tableParticipants, + columns: LocalParticipantFields.values, + where: + '${LocalParticipantFields.projectId} = ? AND (${LocalParticipantFields.deleted} == 0 OR ${LocalParticipantFields.lastUpdate} > ?)', + whereArgs: [project.localId, project.lastSync.millisecondsSinceEpoch], + ); + + project.participants.clear(); + + for (Map e in rawParticipants) { + Participant participant = LocalParticipant.fromJson(project, e); + if (project.currentParticipant == null && + project.currentParticipantId == participant.localId) { + project.currentParticipant = participant; + } + project.participants.add(participant); + } + } + Future loadEntries() async { final rawItems = await AppData.db.query( tableItems, where: - '${ItemFields.project} = ? AND (${ItemFields.deleted} == 0 OR ${ItemFields.lastUpdate} > ?)', + '${LocalItemFields.project} = ? AND (${LocalItemFields.deleted} == 0 OR ${LocalItemFields.lastUpdate} > ?)', whereArgs: [project.localId, project.lastSync.millisecondsSinceEpoch], - orderBy: '${ItemFields.date} DESC', + orderBy: '${LocalItemFields.date} DESC', ); project.items.clear(); @@ -29,7 +86,7 @@ class LocalProject extends LocalGeneric { for (Map e in rawItems) { try { - Item item = Item.fromJson(e, project: project); + Item item = LocalItem.fromJson(e, project: project); project.items.add(item); await (item.conn as LocalItem).loadParts(); } on StateError { @@ -39,24 +96,20 @@ class LocalProject extends LocalGeneric { return err; } - Future loadParticipants() async { - final rawParticipants = await AppData.db.query( - tableParticipants, - columns: ParticipantFields.values, + Future loadGroups() async { + final rawGroups = await AppData.db.query( + tableGroup, where: - '${ParticipantFields.projectId} = ? AND (${ParticipantFields.deleted} == 0 OR ${ParticipantFields.lastUpdate} > ?)', + '${LocalGroupFields.projectId} = ? AND (${LocalGroupFields.deleted} == 0 OR ${LocalGroupFields.lastUpdate} > ?)', whereArgs: [project.localId, project.lastSync.millisecondsSinceEpoch], ); - project.participants.clear(); + project.groups.clear(); - for (Map e in rawParticipants) { - Participant participant = Participant.fromJson(project, e); - if (project.currentParticipant == null && - project.currentParticipantId == participant.localId) { - project.currentParticipant = participant; - } - project.participants.add(participant); + for (Map e in rawGroups) { + Group group = LocalGroup.fromJson(project, e); + project.groups.add(group); + await (group.conn as LocalGroup).loadMembers(); } } @@ -65,10 +118,42 @@ class LocalProject extends LocalGeneric { project.lastUpdate = DateTime.now(); project.localId = await AppData.db.insert( tableProjects, - project.toJson(), + toJson(), conflictAlgorithm: ConflictAlgorithm.replace, ); project.notSyncCount++; return true; } + + Map toJson() { + return { + ProjectFields.localId: project.localId, + ProjectFields.remoteId: project.remoteId, + ProjectFields.name: project.name, + ProjectFields.code: project.code ?? getRandom(5), + ProjectFields.currentParticipant: project.currentParticipant?.localId, + ProjectFields.instance: project.provider.instance.localId, + ProjectFields.lastSync: project.lastSync.millisecondsSinceEpoch, + ProjectFields.lastUpdate: project.lastUpdate.millisecondsSinceEpoch, + ProjectFields.deleted: project.deleted ? 1 : 0, + }; + } + + static Project fromJson(Map json) { + return Project( + localId: json[ProjectFields.localId] as int?, + remoteId: json[ProjectFields.remoteId] as String?, + name: json[ProjectFields.name] as String, + code: json[ProjectFields.code] as String?, + currentParticipantId: json[ProjectFields.currentParticipant] as int?, + instance: Instance.fromId(json[ProjectFields.instance] as int)!, + lastSync: DateTime.fromMillisecondsSinceEpoch( + json[ProjectFields.lastSync] as int), + lastUpdate: DateTime.fromMillisecondsSinceEpoch( + json[ProjectFields.lastUpdate] + as int //? ?? DateTime.now().millisecondsSinceEpoch + ), + deleted: (json[ProjectFields.deleted] as int) == 1, + ); + } } diff --git a/lib/data/pocketbase/group.dart b/lib/data/pocketbase/group.dart new file mode 100644 index 0000000..83b2d81 --- /dev/null +++ b/lib/data/pocketbase/group.dart @@ -0,0 +1,88 @@ +import 'package:pocketbase/pocketbase.dart'; +import 'package:splitr/models/group.dart'; +import 'package:splitr/utils/ext/datetime.dart'; +import 'package:splitr/utils/ext/list.dart'; +import 'package:splitr/utils/ext/record_service.dart'; +import 'package:tuple/tuple.dart'; + +import '../../models/project.dart'; +import 'item_part.dart'; + +class PocketBaseGroupFields { + static const String id = 'id'; + static const String projectId = 'project_id'; + static const String name = 'name'; + static const String deleted = 'deleted'; +} + +class PocketBaseGroup { + static Map toJson(Group g) { + return { + PocketBaseGroupFields.projectId: g.project.remoteId, + PocketBaseGroupFields.name: g.name, + PocketBaseItemPartsFields.deleted: g.deleted, + }; + } + + static Tuple2 fromRecord(RecordModel e, Project project) { + Group? g = project.groupByRemoteId(e.id); + + String name = e.getStringValue(PocketBaseGroupFields.name); + DateTime lastUpdate = DateTime.parse(e.updated); + bool deleted = e.getBoolValue(PocketBaseItemPartsFields.deleted); + + if (g == null) { + g = Group( + project: project, + name: name, + lastUpdate: lastUpdate, + remoteId: e.id, + deleted: deleted, + ); + return Tuple2(true, g); + } + if (lastUpdate > g.lastUpdate) { + g.name = name; + g.lastUpdate = lastUpdate; + g.deleted = deleted; + return Tuple2(true, g); + } + + return Tuple2(false, g); + } + + static Future sync(PocketBase pb, Project project) async { + RecordService collection = pb.collection('groups'); + + // Get new dist records + List records = await collection.getFullList( + filter: + 'updated > "${project.lastSync.toUtc()}" && ${PocketBaseGroupFields.projectId} = "${project.remoteId}"', + ); + + // Apply new dist records if newer + Set distUpdated = {}; + for (RecordModel e in records) { + Tuple2 res = fromRecord(e, project); + if (res.item1) { + project.groups.setPresence(!res.item2.deleted, res.item2); + distUpdated.add(res.item2); + await res.item2.conn.save(); + } + } + + // Send local new records + for (Group g in project.groups.toSet()) { + if (distUpdated.contains(g)) continue; + + if (g.lastUpdate > project.lastSync) { + RecordModel rm = + await collection.updateOrCreate(id: g.remoteId, body: toJson(g)); + g.remoteId = rm.id; + project.groups.setPresence(!g.deleted, g); + } + } + + return true; + } +} diff --git a/lib/data/pocketbase/group_membership.dart b/lib/data/pocketbase/group_membership.dart new file mode 100644 index 0000000..894df18 --- /dev/null +++ b/lib/data/pocketbase/group_membership.dart @@ -0,0 +1,88 @@ +import 'package:pocketbase/pocketbase.dart'; +import 'package:splitr/models/group.dart'; +import 'package:splitr/models/group_membership.dart'; +import 'package:splitr/utils/ext/datetime.dart'; +import 'package:splitr/utils/ext/record_service.dart'; +import 'package:tuple/tuple.dart'; + +import '../../models/participant.dart'; +import 'item_part.dart'; + +class PocketBaseGroupMembershipFields { + static const String id = 'id'; + static const String groupId = 'group_id'; + static const String participantId = 'participant_id'; + static const String deleted = 'deleted'; +} + +class PocketBaseGroupMembership { + static Map toJson(GroupMembership gm) { + return { + PocketBaseGroupMembershipFields.groupId: gm.group.remoteId, + PocketBaseGroupMembershipFields.participantId: gm.participant.remoteId, + PocketBaseItemPartsFields.deleted: gm.deleted, + }; + } + + static Tuple2 fromRecord(RecordModel e, Group group) { + GroupMembership? g = group.memberByRemoteId(e.id); + + Participant participant = group.project.participantByRemoteId( + e.getStringValue(PocketBaseGroupMembershipFields.participantId))!; + DateTime lastUpdate = DateTime.parse(e.updated); + bool deleted = e.getBoolValue(PocketBaseItemPartsFields.deleted); + + if (g == null) { + g = GroupMembership( + group: group, + participant: participant, + lastUpdate: lastUpdate, + remoteId: e.id, + deleted: deleted, + ); + return Tuple2(true, g); + } + if (lastUpdate > g.lastUpdate) { + g.participant = participant; + g.lastUpdate = lastUpdate; + g.deleted = deleted; + return Tuple2(true, g); + } + + return Tuple2(false, g); + } + + static Future sync(PocketBase pb, Group group) async { + RecordService collection = pb.collection('groupMemberships'); + + // Get new dist records + List records = await collection.getFullList( + filter: + 'updated > "${group.project.lastSync.toUtc()}" && ${PocketBaseGroupMembershipFields.groupId} = "${group.remoteId}"', + ); + + // Apply new dist records if newer + Set distUpdated = {}; + for (RecordModel e in records) { + Tuple2 res = fromRecord(e, group); + if (res.item1) { + if (!group.members.contains(res.item2)) group.members.add(res.item2); + distUpdated.add(res.item2); + await res.item2.conn.save(); + } + } + + // Send local new records + for (GroupMembership gm in group.members) { + if (distUpdated.contains(gm)) continue; + + if (gm.lastUpdate > group.project.lastSync) { + RecordModel rm = + await collection.updateOrCreate(id: gm.remoteId, body: toJson(gm)); + gm.remoteId = rm.id; + } + } + + return true; + } +} diff --git a/lib/data/pocketbase/provider.dart b/lib/data/pocketbase/provider.dart index 8a530b5..4865113 100644 --- a/lib/data/pocketbase/provider.dart +++ b/lib/data/pocketbase/provider.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:pocketbase/pocketbase.dart'; +import 'package:splitr/data/pocketbase/group.dart'; +import 'package:splitr/data/pocketbase/group_membership.dart'; +import 'package:splitr/models/group.dart'; import '../../models/instance.dart'; import '../../models/item.dart'; @@ -34,6 +37,10 @@ class PocketBaseProvider extends Provider { await PocketBaseProject.sync(pb, project); await PocketBaseParticipant.sync(pb, project); + await PocketBaseGroup.sync(pb, project); + for (Group group in project.groups) { + await PocketBaseGroupMembership.sync(pb, group); + } await PocketBaseItem.sync(pb, project); for (Item item in project.items) { await PocketBaseItemPart.sync(pb, item); diff --git a/lib/models/app_data.dart b/lib/models/app_data.dart index 201df44..0f26841 100644 --- a/lib/models/app_data.dart +++ b/lib/models/app_data.dart @@ -3,6 +3,7 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:splitr/data/local/instance.dart'; import 'package:splitr/data/local/project.dart'; import 'package:splitr/utils/ext/set.dart'; import 'package:splitr/utils/helper/release_version.dart'; @@ -54,9 +55,9 @@ class AppData { db = await SplitrDatabase.instance.database; - AppData.instances = await Instance.getAllInstances(); + AppData.instances = await LocalInstance.getAllInstances(); - AppData.projects = await Project.getAllProjects(); + AppData.projects = await LocalProject.getAllProjects(); if (!sharedPreferences.containsKey('firstRun')) { firstRun = AppData.projects.enabled().isEmpty; @@ -69,6 +70,7 @@ class AppData { _current = Project.fromName(sharedPreferences.getString('lastProject')!); await (_current!.conn as LocalProject).loadParticipants(); + await (_current!.conn as LocalProject).loadGroups(); await (_current!.conn as LocalProject).loadEntries(); } catch (e) { sharedPreferences.remove('lastProject'); diff --git a/lib/models/group.dart b/lib/models/group.dart new file mode 100644 index 0000000..4cfcd4c --- /dev/null +++ b/lib/models/group.dart @@ -0,0 +1,50 @@ +import 'package:splitr/models/data.dart'; +import 'package:splitr/models/group_membership.dart'; + +import '../data/local/group.dart'; +import 'project.dart'; + +class Group extends Data { + Group({ + super.localId, + super.remoteId, + required this.project, + required String name, + super.lastUpdate, + super.deleted, + }) { + _name = name; + super.conn = LocalGroup(this); + } + + Project project; + late String _name; + Set members = {}; + + String get name => _name; + + set name(String v) { + _name = v; + lastUpdate = DateTime.now(); + } + + @override + bool operator ==(Object other) { + return other is Group && remoteId != null && other.remoteId != null + ? remoteId == other.remoteId + : name == (other as Group).name; + } + + @override + int get hashCode { + return name.hashCode; + } + + GroupMembership? memberByRemoteId(String id) { + try { + return members.firstWhere((element) => element.remoteId == id); + } catch (e) { + return null; + } + } +} diff --git a/lib/models/group_membership.dart b/lib/models/group_membership.dart new file mode 100644 index 0000000..001fe1b --- /dev/null +++ b/lib/models/group_membership.dart @@ -0,0 +1,36 @@ +import 'package:splitr/models/data.dart'; +import 'package:splitr/models/group.dart'; +import 'package:splitr/models/participant.dart'; + +import '../data/local/group_membership.dart'; + +class GroupMembership extends Data { + GroupMembership({ + super.localId, + super.remoteId, + required this.group, + required this.participant, + super.lastUpdate, + super.deleted, + }) { + super.conn = LocalGroupMembership(this); + } + + Group group; + Participant participant; + + @override + bool operator ==(Object other) { + return other is GroupMembership && + remoteId != null && + other.remoteId != null + ? remoteId == other.remoteId + : group == (other as GroupMembership).group && + participant == other.participant; + } + + @override + int get hashCode { + return group.hashCode + participant.hashCode; + } +} diff --git a/lib/models/instance.dart b/lib/models/instance.dart index 21ba0aa..f6bbcd5 100644 --- a/lib/models/instance.dart +++ b/lib/models/instance.dart @@ -1,22 +1,6 @@ -import 'dart:convert'; - import 'app_data.dart'; import '../data/local/instance.dart'; -class InstanceFields { - static const values = [ - localId, - type, - name, - data, - ]; - - static const String localId = 'local_id'; - static const String type = 'type'; - static const String name = 'name'; - static const String data = 'data'; -} - class Instance { Instance({ this.localId, @@ -33,38 +17,11 @@ class Instance { Map data; late LocalInstance conn; - Map toJson() { - return { - InstanceFields.localId: localId, - InstanceFields.type: type, - InstanceFields.name: name, - InstanceFields.data: json.encode(data), - }; - } - - static Instance fromJson(Map jsonMap) { - return Instance( - localId: jsonMap[InstanceFields.localId] as int?, - type: jsonMap[InstanceFields.type] as String, - name: jsonMap[InstanceFields.name] as String, - data: json.decode(jsonMap[InstanceFields.data] as String) - as Map, - ); - } - static Instance? fromId(int localId) { return AppData.instances .firstWhere((element) => element.localId == localId); } - static Future> getAllInstances() async { - final res = await AppData.db.query( - tableInstances, - columns: InstanceFields.values, - ); - return res.map((e) => fromJson(e)).toSet(); - } - static Instance? fromName(String s) { return AppData.instances.isEmpty ? null diff --git a/lib/models/item.dart b/lib/models/item.dart index b0d45a9..ca75f72 100644 --- a/lib/models/item.dart +++ b/lib/models/item.dart @@ -7,30 +7,6 @@ import 'project.dart'; import '../data/local/item.dart'; -class ItemFields { - static const values = [ - localId, - remoteId, - project, - title, - emitter, - amount, - date, - lastUpdate, - deleted, - ]; - - static const String localId = 'local_id'; - static const String remoteId = 'remote_id'; - static const String project = 'project'; - static const String title = 'title'; - static const String emitter = 'emitter'; - static const String amount = 'amount'; - static const String date = 'date'; - static const String lastUpdate = 'last_update'; - static const String deleted = 'deleted'; -} - class Item extends Data { Item({ super.localId, @@ -82,18 +58,6 @@ class Item extends Data { lastUpdate = DateTime.now(); } - Map toJson() => { - ItemFields.localId: localId, - ItemFields.remoteId: remoteId, - ItemFields.project: project.localId, - ItemFields.title: title, - ItemFields.emitter: emitter.localId, - ItemFields.amount: amount, - ItemFields.date: date.millisecondsSinceEpoch, - ItemFields.lastUpdate: DateTime.now().millisecondsSinceEpoch, - ItemFields.deleted: deleted ? 1 : 0, - }; - double shareOf(Participant participant) { double totalRate = 0; ItemPart? pip; @@ -135,29 +99,6 @@ class Item extends Data { return possibilites.first; } - static Item fromJson(Map json, {Project? project}) { - Project p; - if (project != null) { - p = project; - } else { - p = Project.fromId(json[ItemFields.project] as int)!; - } - - return Item( - localId: json[ItemFields.localId] as int?, - remoteId: json[ItemFields.remoteId] as String?, - project: p, - title: json[ItemFields.title] as String, - emitter: p.participants.firstWhere((participant) => - participant.localId == json[ItemFields.emitter] as int), - amount: json[ItemFields.amount] as double, - date: DateTime.fromMillisecondsSinceEpoch(json[ItemFields.date] as int), - lastUpdate: DateTime.fromMillisecondsSinceEpoch( - json[ItemFields.lastUpdate] as int), - deleted: (json[ItemFields.deleted] as int) == 1, - ); - } - ItemPart? partByRemoteId(String id) { try { return itemParts.firstWhere((element) => element.remoteId == id); diff --git a/lib/models/item_part.dart b/lib/models/item_part.dart index f83e29f..173784d 100644 --- a/lib/models/item_part.dart +++ b/lib/models/item_part.dart @@ -4,28 +4,6 @@ import 'item.dart'; import '../data/local/item_part.dart'; -class ItemPartFields { - static const values = [ - localId, - remoteId, - itemId, - participantId, - rate, - amount, - lastUpdate, - deleted, - ]; - - static const String localId = 'local_id'; - static const String remoteId = 'remote_id'; - static const String itemId = 'item'; - static const String participantId = 'participant'; - static const String rate = 'rate'; - static const String amount = 'amount'; - static const String lastUpdate = 'last_update'; - static const String deleted = 'deleted'; -} - class ItemPart extends Data { ItemPart({ super.localId, @@ -59,30 +37,4 @@ class ItemPart extends Data { _amount = amount; lastUpdate = DateTime.now(); } - - Map toJson() => { - ItemPartFields.localId: localId, - ItemPartFields.remoteId: remoteId, - ItemPartFields.itemId: item.localId, - ItemPartFields.participantId: participant.localId, - ItemPartFields.rate: rate, - ItemPartFields.amount: amount, - ItemPartFields.lastUpdate: DateTime.now().millisecondsSinceEpoch, - ItemPartFields.deleted: deleted ? 1 : 0, - }; - - static ItemPart fromJson(Map json, Item item) { - return ItemPart( - localId: json[ItemPartFields.localId] as int?, - remoteId: json[ItemPartFields.remoteId] as String?, - item: item, - participant: item.project.participants.firstWhere( - (e) => e.localId == json[ItemPartFields.participantId] as int), - rate: json[ItemPartFields.rate] as double?, - amount: json[ItemPartFields.amount] as double?, - lastUpdate: DateTime.fromMillisecondsSinceEpoch( - json[ItemPartFields.lastUpdate] as int), - deleted: (json[ItemPartFields.deleted] as int) == 1, - ); - } } diff --git a/lib/models/participant.dart b/lib/models/participant.dart index 5ac666e..03ec030 100644 --- a/lib/models/participant.dart +++ b/lib/models/participant.dart @@ -1,30 +1,7 @@ -import 'package:splitr/models/data.dart'; - import '../data/local/participant.dart'; +import 'data.dart'; import 'project.dart'; -class ParticipantFields { - static const values = [ - localId, - remoteId, - projectId, - pseudo, - lastname, - firstname, - lastUpdate, - deleted, - ]; - - static const String localId = 'local_id'; - static const String remoteId = 'remote_id'; - static const String projectId = 'project_id'; - static const String pseudo = 'pseudo'; - static const String lastname = 'lastname'; - static const String firstname = 'firstname'; - static const String lastUpdate = 'last_update'; - static const String deleted = 'deleted'; -} - class Participant extends Data { Participant({ super.localId, @@ -48,27 +25,6 @@ class Participant extends Data { lastUpdate = DateTime.now(); } - Map toJson() => { - ParticipantFields.localId: localId, - ParticipantFields.remoteId: remoteId, - ParticipantFields.projectId: project.localId, - ParticipantFields.pseudo: pseudo, - ParticipantFields.lastUpdate: DateTime.now().millisecondsSinceEpoch, - ParticipantFields.deleted: deleted ? 1 : 0, - }; - - static Participant fromJson(Project p, Map json) { - return Participant( - localId: json[ParticipantFields.localId] as int?, - remoteId: json[ParticipantFields.remoteId] as String?, - project: p, - pseudo: json[ParticipantFields.pseudo] as String, - lastUpdate: DateTime.fromMillisecondsSinceEpoch( - json[ParticipantFields.lastUpdate] as int), - deleted: (json[ParticipantFields.deleted] as int) == 1, - ); - } - @override bool operator ==(Object other) { return other is Participant && pseudo == other.pseudo; diff --git a/lib/models/project.dart b/lib/models/project.dart index edbd507..1bad342 100644 --- a/lib/models/project.dart +++ b/lib/models/project.dart @@ -1,8 +1,8 @@ import 'package:splitr/models/data.dart'; +import 'package:splitr/models/group.dart'; import 'package:splitr/utils/ext/set.dart'; import 'package:tuple/tuple.dart'; -import '../utils/helper/random.dart'; import 'app_data.dart'; import '../data/local/project.dart'; import '../data/provider.dart'; @@ -11,30 +11,6 @@ import 'item.dart'; import 'item_part.dart'; import 'participant.dart'; -class ProjectFields { - static const values = [ - localId, - remoteId, - name, - code, - currentParticipant, - instance, - lastSync, - lastUpdate, - deleted, - ]; - - static const String localId = 'local_id'; - static const String remoteId = 'remote_id'; - static const String name = 'name'; - static const String code = 'code'; - static const String currentParticipant = 'current_participant'; - static const String instance = 'instance'; - static const String lastSync = 'last_sync'; - static const String lastUpdate = 'last_update'; - static const String deleted = 'deleted'; -} - class Project extends Data { Project({ super.localId, @@ -69,6 +45,7 @@ class Project extends Data { late Provider provider; final List items = []; final List participants = []; + final List groups = []; late DateTime lastSync; int notSyncCount = 0; @@ -90,52 +67,12 @@ class Project extends Data { .reduce((a, b) => a + b); } - Map toJson() { - return { - ProjectFields.localId: localId, - ProjectFields.remoteId: remoteId, - ProjectFields.name: name, - ProjectFields.code: code ?? getRandom(5), - ProjectFields.currentParticipant: currentParticipant?.localId, - ProjectFields.instance: provider.instance.localId, - ProjectFields.lastSync: lastSync.millisecondsSinceEpoch, - ProjectFields.lastUpdate: lastUpdate.millisecondsSinceEpoch, - ProjectFields.deleted: deleted ? 1 : 0, - }; - } - - static Project fromJson(Map json) { - return Project( - localId: json[ProjectFields.localId] as int?, - remoteId: json[ProjectFields.remoteId] as String?, - name: json[ProjectFields.name] as String, - code: json[ProjectFields.code] as String?, - currentParticipantId: json[ProjectFields.currentParticipant] as int?, - instance: Instance.fromId(json[ProjectFields.instance] as int)!, - lastSync: DateTime.fromMillisecondsSinceEpoch( - json[ProjectFields.lastSync] as int), - lastUpdate: DateTime.fromMillisecondsSinceEpoch( - json[ProjectFields.lastUpdate] - as int //? ?? DateTime.now().millisecondsSinceEpoch - ), - deleted: (json[ProjectFields.deleted] as int) == 1, - ); - } - static Project? fromId(int localId) { return AppData.projects .enabled() .firstWhere((element) => element.localId == localId); } - static Future> getAllProjects() async { - final res = await AppData.db.query( - tableProjects, - columns: ProjectFields.values, - ); - return res.map((e) => fromJson(e)).toSet(); - } - @override bool operator ==(Object other) { return other is Project && @@ -225,4 +162,12 @@ class Project extends Data { await participant.conn.save(); } + + Group? groupByRemoteId(String id) { + try { + return groups.firstWhere((element) => element.remoteId == id); + } catch (e) { + return null; + } + } } diff --git a/lib/screens/projects_list/projects_list.dart b/lib/screens/projects_list/projects_list.dart index db016ec..499cb87 100644 --- a/lib/screens/projects_list/projects_list.dart +++ b/lib/screens/projects_list/projects_list.dart @@ -62,6 +62,7 @@ class _ProjectsListState extends State { onTap: () async { AppData.current = project; await (project.conn as LocalProject).loadParticipants(); + await (project.conn as LocalProject).loadGroups(); int err = await (project.conn as LocalProject).loadEntries(); if (context.mounted) { diff --git a/lib/services/database.dart b/lib/services/database.dart index 4420154..9725f73 100644 --- a/lib/services/database.dart +++ b/lib/services/database.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:convert'; import 'package:path/path.dart'; +import 'package:splitr/data/local/group.dart'; +import 'package:splitr/data/local/group_membership.dart'; import 'package:sqflite/sqflite.dart'; import '../data/local/instance.dart'; @@ -9,11 +11,6 @@ import '../data/local/item.dart'; import '../data/local/item_part.dart'; import '../data/local/participant.dart'; import '../data/local/project.dart'; -import '../models/instance.dart'; -import '../models/item.dart'; -import '../models/item_part.dart'; -import '../models/participant.dart'; -import '../models/project.dart'; class SplitrDatabase { static final SplitrDatabase instance = SplitrDatabase._init(); @@ -35,7 +32,7 @@ class SplitrDatabase { return await openDatabase( path, - version: 4, + version: 5, onCreate: _createDB, onUpgrade: _updateDB, ); @@ -58,58 +55,80 @@ CREATE TABLE $tableProjects ( await db.execute(''' CREATE TABLE $tableParticipants ( - ${ParticipantFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, - ${ParticipantFields.remoteId} TEXT, - ${ParticipantFields.projectId} INTEGER, - ${ParticipantFields.pseudo} TEXT NOT NULL, - ${ParticipantFields.lastname} TEXT, - ${ParticipantFields.firstname} TEXT, - ${ParticipantFields.lastUpdate} INTEGER, - ${ParticipantFields.deleted} INTEGER NOT NULL + ${LocalParticipantFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, + ${LocalParticipantFields.remoteId} TEXT, + ${LocalParticipantFields.projectId} INTEGER, + ${LocalParticipantFields.pseudo} TEXT NOT NULL, + ${LocalParticipantFields.lastname} TEXT, + ${LocalParticipantFields.firstname} TEXT, + ${LocalParticipantFields.lastUpdate} INTEGER, + ${LocalParticipantFields.deleted} INTEGER NOT NULL ) '''); await db.execute(''' CREATE TABLE $tableItems ( - ${ItemFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, - ${ItemFields.remoteId} TEXT, - ${ItemFields.project} INTEGER NOT NULL, - ${ItemFields.title} TEXT NOT NULL, - ${ItemFields.emitter} INTEGER NOT NULL, - ${ItemFields.amount} REAL NOT NULL, - ${ItemFields.date} INTEGER NOT NULL, - ${ItemFields.lastUpdate} INTEGER, - ${ItemFields.deleted} INTEGER NOT NULL + ${LocalItemFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, + ${LocalItemFields.remoteId} TEXT, + ${LocalItemFields.project} INTEGER NOT NULL, + ${LocalItemFields.title} TEXT NOT NULL, + ${LocalItemFields.emitter} INTEGER NOT NULL, + ${LocalItemFields.amount} REAL NOT NULL, + ${LocalItemFields.date} INTEGER NOT NULL, + ${LocalItemFields.lastUpdate} INTEGER, + ${LocalItemFields.deleted} INTEGER NOT NULL ) '''); await db.execute(''' CREATE TABLE $tableItemParts ( - ${ItemPartFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, - ${ItemPartFields.remoteId} TEXT, - ${ItemPartFields.itemId} INTEGER NOT NULL, - ${ItemPartFields.participantId} INTEGER NOT NULL, - ${ItemPartFields.rate} REAL, - ${ItemPartFields.amount} REAL, - ${ItemPartFields.lastUpdate} INTEGER, - ${ItemPartFields.deleted} INTEGER NOT NULL + ${LocalItemPartFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, + ${LocalItemPartFields.remoteId} TEXT, + ${LocalItemPartFields.itemId} INTEGER NOT NULL, + ${LocalItemPartFields.participantId} INTEGER NOT NULL, + ${LocalItemPartFields.rate} REAL, + ${LocalItemPartFields.amount} REAL, + ${LocalItemPartFields.lastUpdate} INTEGER, + ${LocalItemPartFields.deleted} INTEGER NOT NULL ) '''); await db.execute(''' CREATE TABLE $tableInstances ( - ${InstanceFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, - ${InstanceFields.type} TEXT NOT NULL, - ${InstanceFields.name} TEXT NOT NULL, - ${InstanceFields.data} TEXT + ${LocalInstanceFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, + ${LocalInstanceFields.type} TEXT NOT NULL, + ${LocalInstanceFields.name} TEXT NOT NULL, + ${LocalInstanceFields.data} TEXT ) '''); await db.insert(tableInstances, { - InstanceFields.type: 'local', - InstanceFields.name: 'local', - InstanceFields.data: json.encode({}), + LocalInstanceFields.type: 'local', + LocalInstanceFields.name: 'local', + LocalInstanceFields.data: json.encode({}), }); + + await db.execute(''' +CREATE TABLE $tableGroup ( + ${LocalGroupFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, + ${LocalGroupFields.remoteId} TEXT, + ${LocalGroupFields.projectId} INTEGER NOT NULL, + ${LocalGroupFields.name} TEXT NOT NULL, + ${LocalGroupFields.lastUpdate} INTEGER, + ${LocalGroupFields.deleted} INTEGER NOT NULL +) +'''); + + await db.execute(''' +CREATE TABLE $tableGroupMembership ( + ${LocalGroupMembershipFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, + ${LocalGroupMembershipFields.remoteId} TEXT, + ${LocalGroupMembershipFields.groupId} INTEGER NOT NULL, + ${LocalGroupMembershipFields.participantId} INTEGER NOT NULL, + ${LocalGroupMembershipFields.lastUpdate} INTEGER, + ${LocalGroupMembershipFields.deleted} INTEGER NOT NULL +) +'''); } Future close() async { @@ -123,17 +142,41 @@ CREATE TABLE $tableInstances ( if (oldVersion < 4) { await db.execute(''' CREATE TABLE $tableInstances ( - ${InstanceFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, - ${InstanceFields.name} TEXT NUL NULL, - ${InstanceFields.data} JSON + ${LocalInstanceFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, + ${LocalInstanceFields.name} TEXT NUL NULL, + ${LocalInstanceFields.data} JSON ) '''); await db.insert(tableInstances, { - InstanceFields.type: 'local', - InstanceFields.name: 'local', - InstanceFields.data: json.encode({}), + LocalInstanceFields.type: 'local', + LocalInstanceFields.name: 'local', + LocalInstanceFields.data: json.encode({}), }); } + + if (oldVersion < 5) { + await db.execute(''' +CREATE TABLE $tableGroup ( + ${LocalGroupFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, + ${LocalGroupFields.remoteId} TEXT, + ${LocalGroupFields.projectId} INTEGER NOT NULL, + ${LocalGroupFields.name} TEXT NOT NULL, + ${LocalGroupFields.lastUpdate} INTEGER, + ${LocalGroupFields.deleted} INTEGER NOT NULL +) +'''); + + await db.execute(''' +CREATE TABLE $tableGroupMembership ( + ${LocalGroupMembershipFields.localId} INTEGER PRIMARY KEY AUTOINCREMENT, + ${LocalGroupMembershipFields.remoteId} TEXT, + ${LocalGroupMembershipFields.groupId} INTEGER NOT NULL, + ${LocalGroupMembershipFields.participantId} INTEGER NOT NULL, + ${LocalGroupMembershipFields.lastUpdate} INTEGER, + ${LocalGroupMembershipFields.deleted} INTEGER NOT NULL +) +'''); + } } }