Skip to content

Commit 14dc847

Browse files
committed
Support error reporting in TransferControl
1 parent a1bcf93 commit 14dc847

7 files changed

Lines changed: 212 additions & 33 deletions

File tree

src/Platforms/SecureFolderFS.Maui/UserControls/Browser/BrowserControl.DragDrop.xaml.cs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,9 @@ private static async Task MoveItemToFolderAsync(BrowserItemViewModel draggedItem
209209
transferViewModel.TransferType = TransferType.Move;
210210
using var cts = transferViewModel.GetCancellation();
211211

212-
// Ensure the destination has content already loaded
212+
// Ensure the destination has content already loaded before collision checks
213213
if (destinationViewModel.Items.IsEmpty())
214-
_ = destinationViewModel.ListContentsAsync(cts.Token);
214+
await destinationViewModel.ListContentsAsync(cts.Token);
215215

216216
await transferViewModel.TransferAsync([ itemToMove ], async (item, reporter, token) =>
217217
{
@@ -233,10 +233,13 @@ await transferViewModel.TransferAsync([ itemToMove ], async (item, reporter, tok
233233
}, browserViewModel.Layouts.GetSorter());
234234
}, cts.Token);
235235
}
236-
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
236+
catch (OperationCanceledException)
237237
{
238-
_ = ex;
239-
// TODO: Report error
238+
// Cancellation is user intent - nothing to report
239+
}
240+
catch (Exception)
241+
{
242+
await transferViewModel.ReportErrorAsync("OperationFailed".ToLocalized());
240243
}
241244
finally
242245
{
@@ -426,9 +429,9 @@ private static async Task CopyExternalFilesToFolderAsync(DropEventArgs dropEvent
426429
transferViewModel.TransferType = TransferType.Copy;
427430
using var cts = transferViewModel.GetCancellation();
428431

429-
// Ensure the destination has content already loaded
432+
// Ensure the destination has content already loaded before collision checks
430433
if (destinationViewModel.Items.IsEmpty())
431-
_ = destinationViewModel.ListContentsAsync(cts.Token);
434+
await destinationViewModel.ListContentsAsync(cts.Token);
432435

433436
await transferViewModel.TransferAsync(itemsToProcess, async (item, reporter, token) =>
434437
{
@@ -463,10 +466,13 @@ await transferViewModel.TransferAsync(itemsToProcess, async (item, reporter, tok
463466
}
464467
}, x => x.Name, cts.Token);
465468
}
466-
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
469+
catch (OperationCanceledException)
470+
{
471+
// Cancellation is user intent - nothing to report
472+
}
473+
catch (Exception)
467474
{
468-
_ = ex;
469-
// TODO: Report error
475+
await transferViewModel.ReportErrorAsync("OperationFailed".ToLocalized());
470476
}
471477
finally
472478
{

src/Platforms/SecureFolderFS.Maui/UserControls/Browser/TransferControl.xaml

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
xmlns:av="clr-namespace:Xe.AcrylicView;assembly=Xe.AcrylicView"
66
xmlns:av2="clr-namespace:Xe.AcrylicView.Controls;assembly=Xe.AcrylicView"
77
xmlns:local="clr-namespace:SecureFolderFS.Maui.UserControls.Browser"
8+
xmlns:mi="http://www.aathifmahir.com/dotnet/2022/maui/icons"
9+
xmlns:mi_cupertino="clr-namespace:MauiIcons.Cupertino;assembly=MauiIcons.Cupertino"
10+
xmlns:mi_material="clr-namespace:MauiIcons.Material;assembly=MauiIcons.Material"
811
xmlns:uc="clr-namespace:SecureFolderFS.Maui.UserControls"
912
x:Name="ThisControl">
1013

@@ -54,11 +57,26 @@
5457
WidthRequest="32" />
5558
</uc:AnimatedRevealLayout.RevealContent>
5659
<uc:AnimatedRevealLayout.MainContent>
57-
<Label
58-
FontSize="16"
59-
LineBreakMode="TailTruncation"
60-
Text="{Binding Title, Mode=OneWay}"
61-
VerticalOptions="Center" />
60+
<HorizontalStackLayout Spacing="8" VerticalOptions="Center">
61+
<mi:MauiIcon
62+
Icon="{mi_material:Material ErrorOutline}"
63+
IconSize="20"
64+
IsVisible="{Binding IsError, Mode=OneWay}"
65+
OnPlatforms="Android"
66+
VerticalOptions="Center" />
67+
<mi:MauiIcon
68+
Icon="{mi_cupertino:Cupertino ExclamationmarkCircle}"
69+
IconSize="20"
70+
IsVisible="{Binding IsError, Mode=OneWay}"
71+
OnPlatforms="iOS"
72+
VerticalOptions="Center" />
73+
<Label
74+
FontSize="14"
75+
LineBreakMode="TailTruncation"
76+
MaxLines="2"
77+
Text="{Binding Title, Mode=OneWay}"
78+
VerticalOptions="Center" />
79+
</HorizontalStackLayout>
6280
</uc:AnimatedRevealLayout.MainContent>
6381
</uc:AnimatedRevealLayout>
6482

@@ -67,7 +85,7 @@
6785
Padding="16,8"
6886
Command="{Binding PrimaryCommand, Mode=OneWay}"
6987
CornerRadius="10"
70-
IsVisible="{Binding IsProgressing, Mode=OneWay, Converter={StaticResource BoolInvertConverter}}"
88+
IsVisible="{Binding IsConfirmShown, Mode=OneWay}"
7189
Style="{StaticResource AccentButtonStyle}"
7290
Text="{Binding PrimaryButtonText, Mode=OneWay}" />
7391
</Grid>

src/Platforms/SecureFolderFS.Maui/UserControls/Browser/TransferControl.xaml.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ private async void Panel_PanUpdated(object? sender, PanUpdatedEventArgs e)
2121
if (_isDismissing)
2222
return;
2323

24+
// Swipe-down dismiss cancels the operation. Disallow it for non-cancellable operations
25+
if (!CanCancel)
26+
{
27+
if (RootPanel.TranslationY != 0d)
28+
await RootPanel.TranslateToAsync(0, 0, 250U, Easing.SpringOut);
29+
30+
return;
31+
}
32+
2433
switch (e.StatusType)
2534
{
2635
case GestureStatus.Running:
@@ -121,6 +130,22 @@ public bool IsProgressing
121130
public static readonly BindableProperty IsProgressingProperty =
122131
BindableProperty.Create(nameof(IsProgressing), typeof(bool), typeof(TransferControl), false);
123132

133+
public bool IsConfirmShown
134+
{
135+
get => (bool)GetValue(IsConfirmShownProperty);
136+
set => SetValue(IsConfirmShownProperty, value);
137+
}
138+
public static readonly BindableProperty IsConfirmShownProperty =
139+
BindableProperty.Create(nameof(IsConfirmShown), typeof(bool), typeof(TransferControl), false);
140+
141+
public bool IsError
142+
{
143+
get => (bool)GetValue(IsErrorProperty);
144+
set => SetValue(IsErrorProperty, value);
145+
}
146+
public static readonly BindableProperty IsErrorProperty =
147+
BindableProperty.Create(nameof(IsError), typeof(bool), typeof(TransferControl), false);
148+
124149
public string? Title
125150
{
126151
get => (string?)GetValue(TitleProperty);

src/Sdk/SecureFolderFS.Sdk.GoogleDrive/ViewModels/GDriveAccountViewModel.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Threading.Tasks;
77
using CommunityToolkit.Mvvm.ComponentModel;
88
using CommunityToolkit.Mvvm.Input;
9+
using Microsoft.Extensions.Logging;
910
using Google;
1011
using Google.Apis.Auth.OAuth2;
1112
using Google.Apis.Drive.v3;
@@ -17,6 +18,7 @@
1718
using SecureFolderFS.Sdk.GoogleDrive.AppModels;
1819
using SecureFolderFS.Sdk.GoogleDrive.DataModels;
1920
using SecureFolderFS.Sdk.GoogleDrive.Storage;
21+
using SecureFolderFS.Shared;
2022
using SecureFolderFS.Shared.Api;
2123
using SecureFolderFS.Shared.ComponentModel;
2224
using SecureFolderFS.Shared.Extensions;
@@ -222,8 +224,13 @@ private async Task ConnectAccountPromptAsync(CancellationToken cancellationToken
222224
await ConnectFromUserInputAsync(cancellationToken);
223225
// User info is already fetched in ConnectFromUserInputAsync
224226
}
225-
catch (Exception)
227+
catch (OperationCanceledException)
228+
{
229+
// The user dismissed the authorization prompt
230+
}
231+
catch (Exception ex)
226232
{
233+
DI.OptionalService<ILogger>()?.LogError(ex, "Failed to connect the Google Drive account.");
227234
UserDisplayName = null;
228235
UserPhotoUri = null;
229236
UserEmail = null;
@@ -253,14 +260,18 @@ private async Task FetchUserInfoAsync(CancellationToken cancellationToken)
253260
}
254261
catch (Exception ex)
255262
{
256-
// TODO: Log or handle the error appropriately
257-
// For now, just set default values
263+
// Fall back to a best-effort display name while keeping the account usable
258264
UserDisplayName = _credential?.UserId ?? "Unknown user";
259265
UserEmail = null;
260266

261-
// Re-throw if it's a critical error
262-
if (ex is GoogleApiException apiEx && apiEx.HttpStatusCode == HttpStatusCode.Unauthorized)
267+
// Surface an expired/revoked token so the caller can trigger re-authentication and update the stored config
268+
if (ex is GoogleApiException { HttpStatusCode: HttpStatusCode.Unauthorized })
269+
{
270+
DI.OptionalService<ILogger>()?.LogWarning(ex, "Google Drive authorization expired; re-authentication is required.");
263271
throw;
272+
}
273+
274+
DI.OptionalService<ILogger>()?.LogError(ex, "Failed to fetch Google Drive user information.");
264275
}
265276
}
266277

src/Sdk/SecureFolderFS.Sdk/AppModels/VaultCollectionModel.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Threading;
66
using System.Threading.Tasks;
77
using CommunityToolkit.Mvvm.Messaging;
8+
using Microsoft.Extensions.Logging;
89
using OwlCore.Storage;
910
using SecureFolderFS.Sdk.Attributes;
1011
using SecureFolderFS.Sdk.DataModels;
@@ -100,8 +101,8 @@ public async Task InitAsync(CancellationToken cancellationToken = default)
100101
}
101102
catch (Exception ex)
102103
{
103-
_ = ex;
104-
continue;
104+
// A single unreadable vault entry must not prevent the rest of the list from loading
105+
DI.OptionalService<ILogger>()?.LogWarning(ex, "Skipped a vault entry that could not be loaded.");
105106
}
106107
}
107108
}

src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Linq;
43
using System.Threading;
54
using System.Threading.Tasks;
65
using OwlCore.Storage;
@@ -19,6 +18,10 @@ public static bool IsPickingItems(this TransferViewModel transferViewModel)
1918

2019
public static async Task HideAsync(this TransferViewModel transferViewModel)
2120
{
21+
// An error banner is in control of its own dismissal (see TransferViewModel.ReportErrorAsync)
22+
if (transferViewModel.IsErrorVisible)
23+
return;
24+
2225
transferViewModel.IsPickingFolder = false;
2326
transferViewModel.IsVisible = false;
2427
await Task.Delay(350);
@@ -43,6 +46,7 @@ public static async Task TransferAsync<TTransferred>(
4346
CancellationToken cancellationToken = default)
4447
{
4548
var collection = items.ToOrAsCollection();
49+
transferViewModel.ClearError();
4650
transferViewModel.IsProgressing = true;
4751
transferViewModel.IsVisible = true;
4852
transferViewModel.Report(new(0, collection.Count, collection.Count));
@@ -53,18 +57,34 @@ public static async Task TransferAsync<TTransferred>(
5357
transferViewModel.Report(new(counter, 0, x.Name));
5458
});
5559

56-
for (var i = 0; i < collection.Count; i++)
60+
var failedCount = 0;
61+
var index = 0;
62+
foreach (var item in collection)
5763
{
5864
cancellationToken.ThrowIfCancellationRequested();
65+
transferViewModel.Report(new(index++, collection.Count, itemName(item)));
5966

60-
var item = collection.ElementAt(i);
61-
transferViewModel.Report(new(i, collection.Count, itemName(item)));
62-
await callback(item, reporter, cancellationToken);
67+
try
68+
{
69+
await callback(item, reporter, cancellationToken);
70+
}
71+
catch (OperationCanceledException)
72+
{
73+
throw;
74+
}
75+
catch (Exception)
76+
{
77+
// A failed item must not abandon the remaining ones
78+
failedCount++;
79+
}
6380
}
6481

6582
transferViewModel.Report(new(collection.Count, collection.Count, string.Empty));
6683
await Task.Delay(1000, CancellationToken.None);
6784
await transferViewModel.HideAsync();
85+
86+
if (failedCount > 0)
87+
await transferViewModel.ReportErrorAsync("TransferItemsFailedPlural".ToLocalized(failedCount));
6888
}
6989

7090
public static async Task TransferAsync<TTransferred>(
@@ -85,22 +105,39 @@ public static async Task TransferAsync<TTransferred>(
85105
CancellationToken cancellationToken = default)
86106
{
87107
var collection = items.ToOrAsCollection();
108+
transferViewModel.ClearError();
88109
transferViewModel.IsProgressing = true;
89110
transferViewModel.IsVisible = true;
90111
transferViewModel.Report(new(0, collection.Count, collection.Count));
91112

92-
for (var i = 0; i < collection.Count; i++)
113+
var failedCount = 0;
114+
var index = 0;
115+
foreach (var item in collection)
93116
{
94117
cancellationToken.ThrowIfCancellationRequested();
118+
transferViewModel.Report(new(index++, collection.Count, itemName(item)));
95119

96-
var item = collection.ElementAt(i);
97-
transferViewModel.Report(new(i, collection.Count, itemName(item)));
98-
await callback(item, cancellationToken);
120+
try
121+
{
122+
await callback(item, cancellationToken);
123+
}
124+
catch (OperationCanceledException)
125+
{
126+
throw;
127+
}
128+
catch (Exception)
129+
{
130+
// A failed item must not abandon the remaining ones
131+
failedCount++;
132+
}
99133
}
100134

101135
transferViewModel.Report(new(collection.Count, collection.Count, string.Empty));
102136
await Task.Delay(1000, CancellationToken.None);
103137
await transferViewModel.HideAsync();
138+
139+
if (failedCount > 0)
140+
await transferViewModel.ReportErrorAsync("TransferItemsFailedPlural".ToLocalized(failedCount));
104141
}
105142

106143
public static async Task PerformOperationAsync(this TransferViewModel transferViewModel, Func<CancellationToken, Task> operation, CancellationToken cancellationToken = default)
@@ -132,7 +169,7 @@ public static async Task PerformOperationAsync(this TransferViewModel transferVi
132169
if (uiShown)
133170
{
134171
transferViewModel.Title = "TransferDone".ToLocalized();
135-
await Task.Delay(300, showUiCts.Token); // Allow user to see the "Done" message
172+
await Task.Delay(300, CancellationToken.None); // Allow user to see the "Done" message
136173
}
137174
}
138175
catch (OperationCanceledException)

0 commit comments

Comments
 (0)