Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@
[submodule "external/nextcloud-openapi-extractor"]
path = external/nextcloud-openapi-extractor
url = https://github.com/nextcloud/openapi-extractor.git
[submodule "external/nextcloud-drop_account"]
path = external/nextcloud-drop_account
url = https://framagit.org/framasoft/nextcloud/drop_account.git
1 change: 1 addition & 0 deletions external/nextcloud-drop_account
Submodule nextcloud-drop_account added at 7829dd
11 changes: 11 additions & 0 deletions packages/neon_framework/lib/l10n/en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"globalOptionsNavigationMode": "Navigation mode",
"globalOptionsNavigationModeDrawer": "Drawer",
"globalOptionsNavigationModeDrawerAlwaysVisible": "Drawer always visible",
"accountOptionsDeleteOnServer": "Delete account on the server",
"accountOptionsRemove": "Remove account",
"accountOptionsRemoveConfirm": "Are you sure you want to remove the account {id}?",
"@accountOptionsRemoveConfirm": {
Expand All @@ -166,6 +167,16 @@
}
}
},
"accountOptionsRemoveLocal": "Remove the account from the device",
"accountOptionsRemoveRemote": "Request deleting the account on the server",
"accountOptionsRemoveRemoteDelay": "The account will be deleted after {time}",
"@accountOptionsRemoveRemoteDelay": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"accountOptionsCategoryStorageInfo": "Storage info",
"accountOptionsQuotaUsedOf": "{used} used of {total} ({relative}%)",
"@accountOptionsQuotaUsedOf": {
Expand Down
24 changes: 24 additions & 0 deletions packages/neon_framework/lib/l10n/localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,12 @@ abstract class NeonLocalizations {
/// **'Drawer always visible'**
String get globalOptionsNavigationModeDrawerAlwaysVisible;

/// No description provided for @accountOptionsDeleteOnServer.
///
/// In en, this message translates to:
/// **'Delete account on the server'**
String get accountOptionsDeleteOnServer;

/// No description provided for @accountOptionsRemove.
///
/// In en, this message translates to:
Expand All @@ -677,6 +683,24 @@ abstract class NeonLocalizations {
/// **'Are you sure you want to remove the account {id}?'**
String accountOptionsRemoveConfirm(String id);

/// No description provided for @accountOptionsRemoveLocal.
///
/// In en, this message translates to:
/// **'Remove the account from the device'**
String get accountOptionsRemoveLocal;

/// No description provided for @accountOptionsRemoveRemote.
///
/// In en, this message translates to:
/// **'Request deleting the account on the server'**
String get accountOptionsRemoveRemote;

/// No description provided for @accountOptionsRemoveRemoteDelay.
///
/// In en, this message translates to:
/// **'The account will be deleted after {time}'**
String accountOptionsRemoveRemoteDelay(String time);

/// No description provided for @accountOptionsCategoryStorageInfo.
///
/// In en, this message translates to:
Expand Down
14 changes: 14 additions & 0 deletions packages/neon_framework/lib/l10n/localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,9 @@ class NeonLocalizationsEn extends NeonLocalizations {
@override
String get globalOptionsNavigationModeDrawerAlwaysVisible => 'Drawer always visible';

@override
String get accountOptionsDeleteOnServer => 'Delete account on the server';

@override
String get accountOptionsRemove => 'Remove account';

Expand All @@ -340,6 +343,17 @@ class NeonLocalizationsEn extends NeonLocalizations {
return 'Are you sure you want to remove the account $id?';
}

@override
String get accountOptionsRemoveLocal => 'Remove the account from the device';

@override
String get accountOptionsRemoveRemote => 'Request deleting the account on the server';

@override
String accountOptionsRemoveRemoteDelay(String time) {
return 'The account will be deleted after $time';
}

@override
String get accountOptionsCategoryStorageInfo => 'Storage info';

Expand Down
44 changes: 25 additions & 19 deletions packages/neon_framework/lib/src/pages/account_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:neon_framework/src/theme/dialog.dart';
import 'package:neon_framework/src/utils/adaptive.dart';
import 'package:neon_framework/src/widgets/dialog.dart';
import 'package:neon_framework/src/widgets/error.dart';
import 'package:url_launcher/url_launcher.dart';

/// Account settings page.
///
Expand Down Expand Up @@ -45,32 +46,37 @@ class AccountSettingsPage extends StatelessWidget {
actions: [
IconButton(
onPressed: () async {
final decision = await showAdaptiveDialog<bool>(
final decision = await showAdaptiveDialog<AccountDeletion>(
context: context,
builder: (context) => NeonConfirmationDialog(
icon: const Icon(Icons.logout),
title: NeonLocalizations.of(context).accountOptionsRemove,
content: Text(
NeonLocalizations.of(context).accountOptionsRemoveConfirm(account.humanReadableID),
),
builder: (context) => NeonAccountDeletionDialog(
account: account,
),
);

if (decision ?? false) {
final isActive = bloc.activeAccount.valueOrNull == account;
switch (decision) {
case null:
break;
case AccountDeletion.remote:
await launchUrl(
account.serverURL.replace(
path: '${account.serverURL.path}/index.php/settings/user/drop_account',
),
);
case AccountDeletion.local:
final isActive = bloc.activeAccount.valueOrNull == account;

options.reset();
bloc.removeAccount(account);
options.reset();
bloc.removeAccount(account);

if (!context.mounted) {
return;
}
if (!context.mounted) {
return;
}

if (isActive) {
const HomeRoute().go(context);
} else {
Navigator.of(context).pop();
}
if (isActive) {
const HomeRoute().go(context);
} else {
Navigator.of(context).pop();
}
}
},
tooltip: NeonLocalizations.of(context).accountOptionsRemove,
Expand Down
138 changes: 138 additions & 0 deletions packages/neon_framework/lib/src/widgets/dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import 'package:meta/meta.dart';
import 'package:neon_framework/blocs.dart';
import 'package:neon_framework/src/models/account.dart';
import 'package:neon_framework/src/utils/global_options.dart';
import 'package:neon_framework/src/utils/relative_time.dart';
import 'package:neon_framework/src/utils/user_status_clear_at.dart';
import 'package:neon_framework/src/widgets/account_tile.dart';
import 'package:neon_framework/src/widgets/error.dart';
import 'package:neon_framework/src/widgets/linear_progress_indicator.dart';
import 'package:neon_framework/src/widgets/user_status_icon.dart';
import 'package:neon_framework/theme.dart';
import 'package:neon_framework/utils.dart';
import 'package:nextcloud/core.dart' as core;
import 'package:nextcloud/user_status.dart' as user_status;
import 'package:url_launcher/url_launcher_string.dart';

Expand Down Expand Up @@ -517,6 +519,142 @@ class NeonAccountSelectionDialog extends StatelessWidget {
}
}

/// The way the account will be deleted.
@internal
enum AccountDeletion {
/// The account is removed from the app.
local,

/// The account is deleted on the server.
remote,
}

@internal

/// Displays a confirmation dialog for deleting the [account].
///
/// If the `drop_account` app is enabled the user can also choose to delete the account on the server
/// instead of only logging out the account.
///
/// Will pop a value of type [AccountDeletion] or null if the user canceled the dialog.
class NeonAccountDeletionDialog extends StatefulWidget {
const NeonAccountDeletionDialog({
required this.account,
super.key,
});

final Account account;

@override
State<NeonAccountDeletionDialog> createState() => _NeonAccountDeletionDialogState();
}

class _NeonAccountDeletionDialogState extends State<NeonAccountDeletionDialog> {
core.DropAccountCapabilities_DropAccount? dropAccountCapabilities;
AccountDeletion value = AccountDeletion.local;

void update(AccountDeletion value) {
setState(() {
this.value = value;
});
}

@override
void initState() {
super.initState();

NeonProvider.of<AccountsBloc>(context).getCapabilitiesBlocFor(widget.account).capabilities.listen((result) {
setState(() {
dropAccountCapabilities = result.data?.capabilities.dropAccountCapabilities?.dropAccount;
if (!(dropAccountCapabilities?.enabled ?? false)) {
value = AccountDeletion.local;
}
});
});
}

@override
Widget build(BuildContext context) {
final localizations = NeonLocalizations.of(context);

const icon = Icon(Icons.logout);
final title = localizations.accountOptionsRemove;
final confirmAction = NeonDialogAction(
isDestructiveAction: true,
onPressed: () {
Navigator.of(context).pop(value);
},
child: Text(
localizations.actionContinue,
textAlign: TextAlign.end,
),
);
final declineAction = NeonDialogAction(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
localizations.actionCancel,
textAlign: TextAlign.end,
),
);

final capabilities = dropAccountCapabilities;
if (capabilities == null) {
return NeonConfirmationDialog(
icon: icon,
title: title,
content: Text(localizations.accountOptionsRemoveConfirm(widget.account.humanReadableID)),
confirmAction: confirmAction,
declineAction: declineAction,
);
}

Widget? subtitle;
final details = capabilities.details;
if (details != null) {
subtitle = Text(details);
} else if (capabilities.delay.enabled) {
subtitle = Text(
localizations.accountOptionsRemoveRemoteDelay(
Duration(hours: capabilities.delay.hours).formatRelative(
localizations,
includeSign: false,
),
),
);
}

return NeonDialog(
icon: icon,
title: Text(title),
content: SingleChildScrollView(
child: Column(
children: [
RadioListTile(
value: AccountDeletion.local,
groupValue: value,
onChanged: (value) => update(value!),
title: Text(localizations.accountOptionsRemoveLocal),
),
RadioListTile<AccountDeletion>(
value: AccountDeletion.remote,
groupValue: value,
onChanged: capabilities.enabled ? (value) => update(value!) : null,
title: Text(localizations.accountOptionsRemoveRemote),
subtitle: subtitle,
),
],
),
),
actions: [
declineAction,
confirmAction,
],
);
}
}

/// A [NeonDialog] to inform the user about the UnifiedPush feature of neon.
@internal
class NeonUnifiedPushDialog extends StatelessWidget {
Expand Down
3 changes: 2 additions & 1 deletion packages/neon_framework/lib/widgets.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export 'package:neon_framework/src/widgets/custom_background.dart';
export 'package:neon_framework/src/widgets/dialog.dart' hide NeonAccountSelectionDialog, NeonUnifiedPushDialog;
export 'package:neon_framework/src/widgets/dialog.dart'
hide AccountDeletion, NeonAccountDeletionDialog, NeonAccountSelectionDialog, NeonUnifiedPushDialog;
export 'package:neon_framework/src/widgets/error.dart';
export 'package:neon_framework/src/widgets/image.dart' hide NeonImage;
export 'package:neon_framework/src/widgets/linear_progress_indicator.dart';
Expand Down
Loading