refactor(ui): Refactor Devices screen UI

This commit is contained in:
Inex Code 2024-12-06 18:59:10 +03:00
parent 1168b2219f
commit c17d87c348
No known key found for this signature in database
15 changed files with 269 additions and 244 deletions

View file

@ -249,7 +249,7 @@
"autobackup_period_disable": "Disable automatic backups",
"autobackup_set_period": "Set period",
"backups_encryption_key": "Encryption key",
"backups_encryption_key_subtitle": "Keep it in a safe place.",
"backups_encryption_key_subtitle": "Keep it in a safe place",
"backups_encryption_key_copy": "Copy the encryption key",
"backups_encryption_key_show": "Show the encryption key",
"backups_encryption_key_description": "This key is used to encrypt your backups. If you lose it, you will not be able to restore your backups. Keep it in a safe place, as it will be useful if you ever need to restore from backups manually.",
@ -598,7 +598,6 @@
"key_replace_button": "Generate new key",
"key_receiving_description": "Write down this key and put to a safe place. It is used to restore full access to your server:",
"key_receiving_info": "The key will never ever be shown again, but you will be able to replace it with another one.",
"key_receiving_done": "Done!",
"generation_error": "Couldn't generate a recovery key. {}"
},
"modals": {

View file

@ -1,15 +1,14 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class SectionHeadline extends StatelessWidget {
const SectionHeadline({
required this.title,
required this.subtitle,
this.subtitle,
super.key,
});
final String title;
final String subtitle;
final String? subtitle;
@override
Widget build(final BuildContext context) => ListTile(
@ -19,9 +18,11 @@ class SectionHeadline extends StatelessWidget {
color: Theme.of(context).colorScheme.secondary,
),
),
subtitle: Text(
subtitle,
subtitle: subtitle != null
? Text(
subtitle!,
style: Theme.of(context).textTheme.labelMedium,
),
)
: null,
);
}

View file

@ -12,7 +12,10 @@ class SectionTitle extends StatelessWidget {
@override
Widget build(final BuildContext context) => Padding(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Text(
title,
style: Theme.of(context).textTheme.labelLarge!.copyWith(

View file

@ -19,7 +19,10 @@ class BrandHeroScreen extends StatelessWidget {
this.heroTitle = '',
this.heroSubtitle,
this.onBackButtonPressed,
this.bodyPadding = const EdgeInsets.all(16.0),
this.bodyPadding = const EdgeInsets.symmetric(
vertical: 16.0,
horizontal: 8.0,
),
this.ignoreBreakpoints = false,
this.hasSupportDrawer = false,
});
@ -80,7 +83,7 @@ class BrandHeroScreen extends StatelessWidget {
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: hasHeroIcon ? TextAlign.center : TextAlign.start,
textAlign: TextAlign.center,
),
]),
),

View file

@ -0,0 +1,131 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/bloc/devices/devices_bloc.dart';
import 'package:selfprivacy/logic/bloc/tokens/tokens_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/json/api_token.dart';
class DeviceItem extends StatelessWidget {
const DeviceItem({
required this.device,
super.key,
});
final ApiToken device;
@override
Widget build(final BuildContext context) => ListTile(
title: Text(device.name),
subtitle: Text(
'devices.main_screen.access_granted_on'
.tr(args: [DateFormat.yMMMMd().format(device.date)]),
),
onTap: device.isCaller
? () => _showTokenRefreshDialog(context, device)
: () => _showConfirmationDialog(context, device),
);
Future _showConfirmationDialog(
final BuildContext context,
final ApiToken device,
) =>
showDialog(
context: context,
builder: (final context) => AlertDialog(
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.link_off_outlined),
const SizedBox(height: 16),
Text(
'devices.revoke_device_alert.header'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'devices.revoke_device_alert.description'
.tr(args: [device.name]),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
actions: <Widget>[
TextButton(
child: Text('devices.revoke_device_alert.no'.tr()),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text(
'devices.revoke_device_alert.yes'.tr(),
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
onPressed: () {
context.read<DevicesBloc>().add(DeleteDevice(device));
Navigator.of(context).pop();
},
),
],
),
);
Future _showTokenRefreshDialog(
final BuildContext context,
final ApiToken device,
) =>
showDialog(
context: context,
builder: (final context) => AlertDialog(
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.update_outlined),
const SizedBox(height: 16),
Text(
'devices.refresh_token_alert.header'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'devices.refresh_token_alert.description'
.tr(args: [device.name]),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
actions: <Widget>[
TextButton(
child: Text('devices.refresh_token_alert.no'.tr()),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text('devices.refresh_token_alert.yes'.tr()),
onPressed: () {
context
.read<TokensBloc>()
.add(const RefreshServerApiTokenEvent());
Navigator.of(context).pop();
},
),
],
),
);
}

View file

@ -0,0 +1,60 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/atoms/buttons/brand_button.dart';
import 'package:selfprivacy/ui/molecules/info_box/info_box.dart';
class KeyDisplay extends StatelessWidget {
const KeyDisplay({
required this.keyToDisplay,
required this.canCopy,
required this.infoboxText,
super.key,
});
final String keyToDisplay;
final bool canCopy;
final String infoboxText;
@override
Widget build(final BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(),
const SizedBox(height: 16),
if (canCopy)
SelectableText(
keyToDisplay,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 24,
fontFamily: 'RobotoMono',
),
textAlign: TextAlign.center,
),
if (!canCopy)
Text(
keyToDisplay,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 24,
fontFamily: 'RobotoMono',
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
InfoBox(
text: infoboxText,
),
const SizedBox(height: 16),
BrandButton.filled(
child: Text(
'basis.done'.tr(),
),
onPressed: () {
Navigator.of(context).popUntil((final route) => route.isFirst);
},
),
const SizedBox(height: 24),
],
);
}

View file

@ -257,13 +257,8 @@ class BackupDetailsPage extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text(
'backup.pending_jobs'.tr(),
style: Theme.of(context).textTheme.headlineSmall!.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
),
SectionHeadline(
title: 'backup.pending_jobs'.tr(),
),
for (final job in backupJobs)
Padding(
@ -312,7 +307,6 @@ class BackupDetailsPage extends StatelessWidget {
ListTile(
title: Text(
'backup.show_more'.tr(),
style: Theme.of(context).textTheme.labelMedium,
),
leading: const Icon(
Icons.arrow_drop_down,
@ -345,6 +339,7 @@ class BackupDetailsPage extends StatelessWidget {
Icons.cached_outlined,
color: overrideColor,
),
isThreeLine: true,
onTap: preventActions
? null
: () => context

View file

@ -3,23 +3,22 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/bloc/devices/devices_bloc.dart';
import 'package:selfprivacy/logic/bloc/tokens/tokens_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/json/api_token.dart';
import 'package:selfprivacy/ui/atoms/list_tiles/section_title.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/molecules/info_box/info_box.dart';
import 'package:selfprivacy/ui/pages/devices/new_device.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/ui/molecules/list_items/device_item.dart';
import 'package:selfprivacy/ui/router/router.dart';
@RoutePage()
class DevicesScreen extends StatefulWidget {
const DevicesScreen({super.key});
class DevicesPage extends StatefulWidget {
const DevicesPage({super.key});
@override
State<DevicesScreen> createState() => _DevicesScreenState();
State<DevicesPage> createState() => _DevicesPageState();
}
class _DevicesScreenState extends State<DevicesScreen> {
class _DevicesPageState extends State<DevicesPage> {
@override
Widget build(final BuildContext context) {
final DevicesState devicesStatus = context.watch<DevicesBloc>().state;
@ -31,6 +30,7 @@ class _DevicesScreenState extends State<DevicesScreen> {
child: BrandHeroScreen(
heroTitle: 'devices.main_screen.header'.tr(),
heroSubtitle: 'devices.main_screen.description'.tr(),
heroIcon: Icons.devices_outlined,
hasBackButton: true,
hasFlashButton: false,
children: [
@ -46,8 +46,7 @@ class _DevicesScreenState extends State<DevicesScreen> {
),
const SizedBox(height: 16),
OutlinedButton(
onPressed: () => Navigator.of(context)
.push(materialRoute(const NewDeviceScreen())),
onPressed: () => context.pushRoute(const NewDeviceRoute()),
child: Text('devices.main_screen.authorize_new_device'.tr()),
),
const SizedBox(height: 16),
@ -75,22 +74,17 @@ class _DevicesInfo extends StatelessWidget {
Widget build(final BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'devices.main_screen.this_device'.tr(),
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.secondary,
SectionTitle(
title: 'devices.main_screen.this_device'.tr(),
),
),
_DeviceTile(
DeviceItem(
device: devicesStatus.thisDevice,
),
const SizedBox(height: 8),
const Divider(height: 1),
const SizedBox(height: 16),
Text(
'devices.main_screen.other_devices'.tr(),
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(height: 8),
SectionTitle(
title: 'devices.main_screen.other_devices'.tr(),
),
if (devicesStatus is DevicesDeleting) ...[
const Center(
@ -100,127 +94,10 @@ class _DevicesInfo extends StatelessWidget {
],
if (devicesStatus is! DevicesDeleting)
...devicesStatus.otherDevices.map(
(final device) => _DeviceTile(
(final device) => DeviceItem(
device: device,
),
),
],
);
}
class _DeviceTile extends StatelessWidget {
const _DeviceTile({required this.device});
final ApiToken device;
@override
Widget build(final BuildContext context) => ListTile(
contentPadding: EdgeInsets.zero,
title: Text(device.name),
subtitle: Text(
'devices.main_screen.access_granted_on'
.tr(args: [DateFormat.yMMMMd().format(device.date)]),
),
onTap: device.isCaller
? () => _showTokenRefreshDialog(context, device)
: () => _showConfirmationDialog(context, device),
);
Future _showConfirmationDialog(
final BuildContext context,
final ApiToken device,
) =>
showDialog(
context: context,
builder: (final context) => AlertDialog(
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.link_off_outlined),
const SizedBox(height: 16),
Text(
'devices.revoke_device_alert.header'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'devices.revoke_device_alert.description'
.tr(args: [device.name]),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
actions: <Widget>[
TextButton(
child: Text('devices.revoke_device_alert.no'.tr()),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text('devices.revoke_device_alert.yes'.tr()),
onPressed: () {
context.read<DevicesBloc>().add(DeleteDevice(device));
Navigator.of(context).pop();
},
),
],
),
);
Future _showTokenRefreshDialog(
final BuildContext context,
final ApiToken device,
) =>
showDialog(
context: context,
builder: (final context) => AlertDialog(
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.update_outlined),
const SizedBox(height: 16),
Text(
'devices.refresh_token_alert.header'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'devices.refresh_token_alert.description'
.tr(args: [device.name]),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
actions: <Widget>[
TextButton(
child: Text('devices.refresh_token_alert.no'.tr()),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text('devices.refresh_token_alert.yes'.tr()),
onPressed: () {
context
.read<TokensBloc>()
.add(const RefreshServerApiTokenEvent());
Navigator.of(context).pop();
},
),
],
),
);
}

View file

@ -1,13 +1,15 @@
import 'package:auto_route/auto_route.dart';
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/bloc/devices/devices_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/atoms/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/organisms/displays/key_display.dart';
class NewDeviceScreen extends StatelessWidget {
const NewDeviceScreen({super.key});
@RoutePage()
class NewDevicePage extends StatelessWidget {
const NewDevicePage({super.key});
@override
Widget build(final BuildContext context) => BrandHeroScreen(
@ -23,8 +25,10 @@ class NewDeviceScreen extends StatelessWidget {
final AsyncSnapshot<Object?> snapshot,
) {
if (snapshot.hasData) {
return _KeyDisplay(
newDeviceKey: snapshot.data.toString(),
return KeyDisplay(
keyToDisplay: snapshot.data.toString(),
canCopy: true,
infoboxText: 'devices.add_new_device_screen.tip'.tr(),
);
} else {
return const Center(
@ -36,51 +40,3 @@ class NewDeviceScreen extends StatelessWidget {
],
);
}
class _KeyDisplay extends StatelessWidget {
const _KeyDisplay({required this.newDeviceKey});
final String newDeviceKey;
@override
Widget build(final BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(),
const SizedBox(height: 16),
SelectableText(
newDeviceKey,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 24,
fontFamily: 'RobotoMono',
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline,
color: Theme.of(context).colorScheme.onSurface,
),
const SizedBox(height: 16),
Text(
'devices.add_new_device_screen.tip'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 16),
BrandButton.filled(
child: Text(
'basis.done'.tr(),
),
onPressed: () => Navigator.of(context).pop(),
),
const SizedBox(height: 24),
],
);
}

View file

@ -43,7 +43,6 @@ class AboutApplicationPage extends StatelessWidget {
hasBackButton: true,
hasFlashButton: false,
heroTitle: 'about_application_page.title'.tr(),
bodyPadding: const EdgeInsets.symmetric(vertical: 16),
children: [
SectionTitle(title: 'about_application_page.versions'.tr()),
FutureBuilder(

View file

@ -25,7 +25,6 @@ class _DeveloperSettingsPageState extends State<DeveloperSettingsPage> {
Widget build(final BuildContext context) => BrandHeroScreen(
hasBackButton: true,
hasFlashButton: false,
bodyPadding: const EdgeInsets.symmetric(vertical: 16),
heroTitle: 'developer_settings.title'.tr(),
heroSubtitle: 'developer_settings.subtitle'.tr(),
children: [

View file

@ -250,7 +250,7 @@ class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
});
await Navigator.of(context).push(
materialRoute(
RecoveryKeyReceiving(recoveryKey: token),
NewRecoveryKeyPage(recoveryKey: token),
),
);
} on GenerationError catch (e) {

View file

@ -1,11 +1,10 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/atoms/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/molecules/info_box/info_box.dart';
import 'package:selfprivacy/ui/organisms/displays/key_display.dart';
class RecoveryKeyReceiving extends StatelessWidget {
const RecoveryKeyReceiving({required this.recoveryKey, super.key});
class NewRecoveryKeyPage extends StatelessWidget {
const NewRecoveryKeyPage({required this.recoveryKey, super.key});
final String recoveryKey;
@ -16,28 +15,10 @@ class RecoveryKeyReceiving extends StatelessWidget {
hasBackButton: false,
hasFlashButton: false,
children: [
const Divider(),
const SizedBox(height: 16),
Text(
recoveryKey,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 24,
fontFamily: 'RobotoMono',
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
InfoBox(
text: 'recovery_key.key_receiving_info'.tr(),
),
const SizedBox(height: 16),
BrandButton.filled(
child: Text('recovery_key.key_receiving_done'.tr()),
onPressed: () {
Navigator.of(context).popUntil((final route) => route.isFirst);
},
KeyDisplay(
keyToDisplay: recoveryKey,
canCopy: false,
infoboxText: 'recovery_key.key_receiving_info'.tr(),
),
],
);

View file

@ -7,6 +7,7 @@ import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/ui/pages/backups/backup_details.dart';
import 'package:selfprivacy/ui/pages/backups/backups_list.dart';
import 'package:selfprivacy/ui/pages/devices/devices.dart';
import 'package:selfprivacy/ui/pages/devices/new_device.dart';
import 'package:selfprivacy/ui/pages/dns_details/dns_details.dart';
import 'package:selfprivacy/ui/pages/more/about_application.dart';
import 'package:selfprivacy/ui/pages/more/app_settings/app_settings.dart';
@ -101,6 +102,7 @@ class RootRouter extends RootStackRouter {
AutoRoute(page: NewUserRoute.page),
AutoRoute(page: RecoveryKeyRoute.page),
AutoRoute(page: DevicesRoute.page),
AutoRoute(page: NewDeviceRoute.page),
AutoRoute(page: AboutApplicationRoute.page),
AutoRoute(page: DeveloperSettingsRoute.page),
AutoRoute(page: ServiceRoute.page),

View file

@ -198,7 +198,7 @@ class DeveloperSettingsRoute extends PageRouteInfo<void> {
}
/// generated route for
/// [DevicesScreen]
/// [DevicesPage]
class DevicesRoute extends PageRouteInfo<void> {
const DevicesRoute({List<PageRouteInfo>? children})
: super(
@ -211,7 +211,7 @@ class DevicesRoute extends PageRouteInfo<void> {
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DevicesScreen();
return const DevicesPage();
},
);
}
@ -344,6 +344,25 @@ class MoreRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [NewDevicePage]
class NewDeviceRoute extends PageRouteInfo<void> {
const NewDeviceRoute({List<PageRouteInfo>? children})
: super(
NewDeviceRoute.name,
initialChildren: children,
);
static const String name = 'NewDeviceRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const NewDevicePage();
},
);
}
/// generated route for
/// [NewUserPage]
class NewUserRoute extends PageRouteInfo<void> {