1- using MAUITodo . Models ;
1+ using System . Runtime . CompilerServices ;
2+
3+ using MAUITodo . Attachments ;
4+ using MAUITodo . Models ;
25
36using Microsoft . Extensions . Logging ;
47
8+ using PowerSync . Common . Attachments ;
59using PowerSync . Common . Client ;
610using PowerSync . Common . MDSQLite ;
711using PowerSync . Maui . SQLite ;
@@ -11,6 +15,7 @@ namespace MAUITodo.Data;
1115public class PowerSyncData
1216{
1317 public PowerSyncDatabase Db ;
18+ public AttachmentQueue PhotoAttachmentQueue { get ; }
1419 private string UserId { get ; }
1520
1621 public PowerSyncData ( )
@@ -39,8 +44,38 @@ public PowerSyncData()
3944 UserId = nodeConnector . UserId ;
4045
4146 Db . Connect ( nodeConnector ) ;
47+
48+ var attachmentsDir = Path . Combine ( FileSystem . AppDataDirectory , "attachments" ) ;
49+ var localStorage = new FileManagerLocalStorage ( attachmentsDir ) ;
50+ var remoteStorage = new NodeRemoteStorageAdapter ( new HttpClient ( ) , nodeConnector . BackendUrl ) ;
51+
52+ PhotoAttachmentQueue = new AttachmentQueue ( new AttachmentQueueOptions
53+ {
54+ Db = Db ,
55+ LocalStorage = localStorage ,
56+ RemoteStorage = remoteStorage ,
57+ WatchAttachments = ct => WatchTodoPhotos ( Db , ct ) ,
58+ } ) ;
59+
60+ _ = Task . Run ( ( ) => PhotoAttachmentQueue . StartSyncAsync ( ) ) ;
61+ }
62+
63+ private static async IAsyncEnumerable < WatchedAttachmentItem [ ] > WatchTodoPhotos (
64+ PowerSyncDatabase db ,
65+ [ EnumeratorCancellation ] CancellationToken ct = default )
66+ {
67+ var stream = db . Watch < PhotoIdResult > (
68+ "SELECT photo_id FROM todos WHERE photo_id IS NOT NULL" ,
69+ [ ] ,
70+ new SQLWatchOptions { TriggerImmediately = true , Signal = ct } ) ;
71+ await foreach ( var rows in stream . WithCancellation ( ct ) )
72+ {
73+ yield return [ .. rows . Select ( r => new WatchedAttachmentItem ( r . photo_id , fileExtension : "jpg" ) ) ] ;
74+ }
4275 }
4376
77+ private record PhotoIdResult ( string photo_id ) ;
78+
4479 public async Task SaveListAsync ( TodoList list )
4580 {
4681 if ( list . ID != "" )
@@ -59,32 +94,45 @@ await Db.Execute(
5994
6095 public async Task DeleteListAsync ( TodoList list )
6196 {
62- var listId = list . ID ;
63- // First delete all todo items in this list
64- await Db . Execute ( "DELETE FROM todos WHERE list_id = ?" , [ listId ] ) ;
65- await Db . Execute ( "DELETE FROM lists WHERE id = ?" , [ listId ] ) ;
97+ await Db . WriteTransaction ( async tx =>
98+ {
99+ // Prevent attachments from being orphaned when their owning todo is deleted;
100+ // `has_synced = 0` prevents the attachments from becoming archived instead of deleted.
101+ //
102+ // It would be nice to be able to do this sort of bulk-delete using the attachments API directly.
103+ await tx . Execute (
104+ @"UPDATE attachments
105+ SET state = ?, has_synced = 0
106+ WHERE id IN (SELECT photo_id FROM todos WHERE list_id = ? AND photo_id IS NOT NULL)" ,
107+ [ ( int ) AttachmentState . QueuedDelete , list . ID ] ) ;
108+ await tx . Execute ( "DELETE FROM todos WHERE list_id = ?" , [ list . ID ] ) ;
109+ await tx . Execute ( "DELETE FROM lists WHERE id = ?" , [ list . ID ] ) ;
110+ } ) ;
111+
112+ // Force the attachments queue to acknowledge the changes immediately
113+ PhotoAttachmentQueue . TriggerSync ( ) ;
66114 }
67115
68116 public async Task SaveItemAsync ( TodoItem item )
69117 {
70118 if ( item . ID != "" )
71119 {
72120 await Db . Execute (
73- @"UPDATE todos
121+ @"UPDATE todos
74122 SET description = ?, completed = ?, completed_at = ?, completed_by = ?
75123 WHERE id = ?" ,
76124 [
77125 item . Description ,
78- item . Completed ? 1 : 0 ,
79- item . CompletedAt ! ,
126+ item . Completed ,
127+ item . CompletedAt ,
80128 item . Completed ? UserId : null ,
81129 item . ID
82130 ] ) ;
83131 }
84132 else
85133 {
86134 await Db . Execute (
87- @"INSERT INTO todos
135+ @"INSERT INTO todos
88136 (id, list_id, description, created_at, created_by, completed, completed_at, completed_by)
89137 VALUES (uuid(), ?, ?, datetime(), ?, ?, ?, ?)" ,
90138 [
@@ -98,12 +146,30 @@ await Db.Execute(
98146 }
99147 }
100148
149+ public async Task SaveTodoPhotoAsync ( string todoId , Stream photoData )
150+ {
151+ await PhotoAttachmentQueue . SaveFileAsync (
152+ data : photoData ,
153+ fileExtension : "jpg" ,
154+ mediaType : "image/jpeg" ,
155+ updateHook : ( tx , attachment ) =>
156+ tx . Execute ( "UPDATE todos SET photo_id = ? WHERE id = ?" , [ attachment . Id , todoId ] ) ) ;
157+ }
158+
159+ public async Task RemoveTodoPhotoAsync ( string todoId , string photoId )
160+ {
161+ await PhotoAttachmentQueue . DeleteFileAsync (
162+ id : photoId ,
163+ updateHook : ( tx , _ ) =>
164+ tx . Execute ( "UPDATE todos SET photo_id = NULL WHERE id = ?" , [ todoId ] ) ) ;
165+ }
166+
101167 public async Task SaveTodoCompletedAsync ( string todoId , bool completed )
102168 {
103169 if ( completed )
104170 {
105171 await Db . Execute (
106- @"UPDATE todos
172+ @"UPDATE todos
107173 SET completed = 1, completed_at = datetime(), completed_by = ?
108174 WHERE id = ?" ,
109175 [
@@ -114,7 +180,7 @@ await Db.Execute(
114180 else
115181 {
116182 await Db . Execute (
117- @"UPDATE todos
183+ @"UPDATE todos
118184 SET completed = 0, completed_at = NULL, completed_by = NULL
119185 WHERE id = ?" ,
120186 [
@@ -125,6 +191,26 @@ await Db.Execute(
125191
126192 public async Task DeleteItemAsync ( TodoItem item )
127193 {
128- await Db . Execute ( "DELETE FROM todos WHERE id = ?" , [ item . ID ] ) ;
194+ if ( item . PhotoId != null )
195+ {
196+ // DeleteFileAsync can fail if a `todos` row is synced with a photo_id, but the
197+ // corresponding `attachments` row/item hasn't been created yet. Fallback to regular
198+ // `DELETE FROM todos`.
199+ try
200+ {
201+ await PhotoAttachmentQueue . DeleteFileAsync (
202+ id : item . PhotoId ,
203+ updateHook : ( tx , _ ) =>
204+ tx . Execute ( "DELETE FROM todos WHERE id = ?" , [ item . ID ] ) ) ;
205+ }
206+ catch
207+ {
208+ await Db . Execute ( "DELETE FROM todos WHERE id = ?" , [ item . ID ] ) ;
209+ }
210+ }
211+ else
212+ {
213+ await Db . Execute ( "DELETE FROM todos WHERE id = ?" , [ item . ID ] ) ;
214+ }
129215 }
130216}
0 commit comments