Introduce new brand screen, use it for backups

pull/85/head
Inex Code 2022-02-16 10:01:05 +03:00
parent 8de33ea19b
commit 83a2d19e37
8 changed files with 292 additions and 288 deletions

View File

@ -111,6 +111,7 @@
"restoring": "Restoring from backup",
"error_pending": "Server returned error, check it below",
"restore_alert": "You are about to restore from backup created on {}. All current data will be lost. Are you sure?",
"refresh": "Refresh status",
"refetchBackups": "Refetch backup list",
"refetchingList": "In a few minutes list will be updated"
}

View File

@ -111,6 +111,7 @@
"restoring": "Восстановление из копии",
"error_pending": "Сервер вернул ошибку: проверьте её ниже.",
"restore_alert": "Вы собираетесь восстановить из копии созданной {}. Все текущие данные будут потеряны. Вы уверены?",
"refresh": "Обновить статус",
"refetchBackups": "Обновить список копий",
"refetchingList": "Через несколько минут список будет обновлён"

View File

@ -38,10 +38,14 @@ final ligtTheme = ThemeData(
color: BrandColors.red1,
),
),
listTileTheme: ListTileThemeData(
minLeadingWidth: 24.0,
),
textTheme: TextTheme(
headline1: headline1Style,
headline2: headline2Style,
caption: headline4Style,
headline3: headline3Style,
headline4: headline4Style,
bodyText1: body1Style,
subtitle1: TextStyle(fontSize: 15, height: 1.6), // text input style
),
@ -56,7 +60,8 @@ var darkTheme = ligtTheme.copyWith(
textTheme: TextTheme(
headline1: headline1Style.copyWith(color: BrandColors.white),
headline2: headline2Style.copyWith(color: BrandColors.white),
caption: headline4Style.copyWith(color: BrandColors.white),
headline3: headline3Style.copyWith(color: BrandColors.white),
headline4: headline4Style.copyWith(color: BrandColors.white),
bodyText1: body1Style.copyWith(color: BrandColors.white),
subtitle1: TextStyle(fontSize: 15, height: 1.6), // text input style
),

View File

@ -20,6 +20,9 @@ class BrandCards {
shadow: bigShadow,
borderRadius: BorderRadius.circular(10),
);
static Widget outlined({required Widget child}) => _OutlinedCard(
child: child,
);
}
class _BrandCard extends StatelessWidget {
@ -52,6 +55,29 @@ class _BrandCard extends StatelessWidget {
}
}
class _OutlinedCard extends StatelessWidget {
const _OutlinedCard({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return Card(
elevation: 0.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(
color: Colors.grey.withOpacity(0.2),
width: 1,
),
),
child: child,
);
}
}
final bigShadow = [
BoxShadow(
offset: Offset(0, 4),

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
class BrandHeroScreen extends StatelessWidget {
const BrandHeroScreen({
Key? key,
this.headerTitle = '',
this.hasBackButton = true,
this.hasFlashButton = true,
required this.children,
this.heroIcon,
this.heroTitle,
this.heroSubtitle,
}) : super(key: key);
final List<Widget> children;
final String headerTitle;
final bool hasBackButton;
final bool hasFlashButton;
final IconData? heroIcon;
final String? heroTitle;
final String? heroSubtitle;
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(52.0),
child: BrandHeader(
title: headerTitle,
hasBackButton: hasBackButton,
hasFlashButton: hasFlashButton,
),
),
body: ListView(
padding: EdgeInsets.all(16.0),
children: <Widget>[
if (heroIcon != null)
Icon(
heroIcon,
size: 48.0,
),
SizedBox(height: 16.0),
if (heroTitle != null)
Text(heroTitle!,
style: Theme.of(context).textTheme.headline2,
textAlign: TextAlign.center),
SizedBox(height: 8.0),
if (heroSubtitle != null)
Text(heroSubtitle!,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center),
SizedBox(height: 16.0),
...children,
],
),
),
);
}
}

View File

@ -1,6 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
@ -9,13 +9,11 @@ import 'package:selfprivacy/logic/models/state_types.dart';
import 'package:selfprivacy/ui/components/action_button/action_button.dart';
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
import 'package:easy_localization/easy_localization.dart';
part 'header.dart';
import '../../components/brand_cards/brand_cards.dart';
var navigatorKey = GlobalKey<NavigatorState>();
@ -44,203 +42,200 @@ class _BackupDetailsState extends State<BackupDetails>
var backups = context.watch<BackupsCubit>().state.backups;
var refreshing = context.watch<BackupsCubit>().state.refreshing;
return Scaffold(
appBar: PreferredSize(
child: Column(
children: [
Container(
height: 51,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 15),
child: BrandText.h4('basis.details'.tr()),
),
BrandDivider(),
],
),
preferredSize: Size.fromHeight(52),
),
body: SingleChildScrollView(
physics: ClampingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: paddingH15V0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Header(
providerState: providerState,
refreshing: refreshing,
return BrandHeroScreen(
heroIcon: BrandIcons.save,
heroTitle: 'providers.backup.card_title'.tr(),
heroSubtitle: 'providers.backup.bottom_sheet.1'.tr(),
children: [
if (isReady && !isBackupInitialized)
BrandButton.rised(
onPressed: preventActions
? null
: () async {
await context.read<BackupsCubit>().createBucket();
},
text: 'providers.backup.initialize'.tr(),
),
if (backupStatus == BackupStatusEnum.initializing)
BrandText.body1('providers.backup.waitingForRebuild'.tr()),
if (backupStatus != BackupStatusEnum.initializing &&
backupStatus != BackupStatusEnum.noKey)
BrandCards.outlined(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (backupStatus == BackupStatusEnum.initialized)
ListTile(
onTap: preventActions
? null
: () async {
await context.read<BackupsCubit>().createBackup();
},
leading: Icon(
Icons.add_circle_outline_rounded,
),
title: Text(
'providers.backup.create_new'.tr(),
style: Theme.of(context).textTheme.headline6,
),
),
SizedBox(height: 10),
BrandText.h2('providers.backup.card_title'.tr()),
SizedBox(height: 10),
BrandText.body1('providers.backup.bottom_sheet.1'.tr()),
SizedBox(height: 20),
if (isReady && !isBackupInitialized)
BrandButton.rised(
onPressed: preventActions
? null
: () async {
await context.read<BackupsCubit>().createBucket();
},
text: 'providers.backup.initialize'.tr(),
if (backupStatus == BackupStatusEnum.backingUp)
ListTile(
title: Text(
'providers.backup.creating'.tr(
args: [(backupProgress * 100).round().toString()]),
style: Theme.of(context).textTheme.headline6,
),
if (backupStatus == BackupStatusEnum.initializing)
BrandText.body1('providers.backup.waitingForRebuild'.tr()),
if (backupStatus != BackupStatusEnum.initializing &&
backupStatus != BackupStatusEnum.noKey)
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(
color: Colors.grey.withOpacity(0.2),
width: 1,
),
),
elevation: 0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (backupStatus == BackupStatusEnum.initialized)
ListTile(
onTap: preventActions
? null
: () async {
await context
.read<BackupsCubit>()
.createBackup();
},
leading: Icon(
Icons.add_circle_outline_rounded,
color: BrandColors.textColor1,
),
title: BrandText.h5(
'providers.backup.create_new'.tr()),
),
if (backupStatus == BackupStatusEnum.backingUp)
ListTile(
title: BrandText.h5('providers.backup.creating'
.tr(args: [
(backupProgress * 100).round().toString()
])),
subtitle: LinearProgressIndicator(
value: backupProgress,
backgroundColor: Colors.grey.withOpacity(0.2),
),
),
if (backupStatus == BackupStatusEnum.restoring)
ListTile(
title: BrandText.h5('providers.backup.restoring'
.tr(args: [
(backupProgress * 100).round().toString()
])),
subtitle: LinearProgressIndicator(
backgroundColor: Colors.grey.withOpacity(0.2),
),
),
if (backupStatus == BackupStatusEnum.error)
ListTile(
leading: Icon(
Icons.error_outline,
color: BrandColors.red1,
),
title: BrandText.h5(
'providers.backup.error_pending'.tr()),
),
],
),
subtitle: LinearProgressIndicator(
value: backupProgress,
backgroundColor: Colors.grey.withOpacity(0.2),
),
SizedBox(height: 16),
// Card with a list of existing backups
// Each list item has a date
// When clicked, starts the restore action
if (backupStatus != BackupStatusEnum.initializing &&
backupStatus != BackupStatusEnum.noKey)
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(
color: Colors.grey.withOpacity(0.2),
width: 1,
),
),
elevation: 0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: Icon(
Icons.refresh,
color: BrandColors.textColor1,
),
title:
BrandText.h5('providers.backup.restore'.tr()),
),
Divider(
height: 1.0,
),
if (backups.isEmpty)
ListTile(
leading: Icon(
Icons.error_outline,
),
title: Text('providers.backup.no_backups'.tr()),
),
if (backups.isNotEmpty)
Column(
children: backups.map((backup) {
return ListTile(
onTap: preventActions
? null
: () {
var nav = getIt<NavigationService>();
nav.showPopUpDialog(BrandAlert(
title: 'providers.backup.restoring'
.tr(),
contentText:
'providers.backup.restore_alert'
.tr(args: [
backup.time.toString()
]),
actions: [
ActionButton(
text: 'basis.cancel'.tr(),
),
ActionButton(
onPressed: () => {
context
.read<BackupsCubit>()
.restoreBackup(backup.id)
},
text: 'modals.yes'.tr(),
)
],
));
},
title: Text(
MaterialLocalizations.of(context)
.formatShortDate(backup.time) +
' ' +
TimeOfDay.fromDateTime(backup.time)
.format(context),
),
);
}).toList(),
),
],
),
),
if (backupStatus == BackupStatusEnum.restoring)
ListTile(
title: Text(
'providers.backup.restoring'.tr(
args: [(backupProgress * 100).round().toString()]),
style: Theme.of(context).textTheme.headline6,
),
if (backupStatus == BackupStatusEnum.error)
BrandText.body1(backupError.toString()),
],
),
subtitle: LinearProgressIndicator(
backgroundColor: Colors.grey.withOpacity(0.2),
),
),
if (backupStatus == BackupStatusEnum.error)
ListTile(
leading: Icon(
Icons.error_outline,
color: BrandColors.red1,
),
title: Text(
'providers.backup.error_pending'.tr(),
style: Theme.of(context).textTheme.headline6,
),
),
],
),
SizedBox(height: 10),
],
),
SizedBox(height: 16),
// Card with a list of existing backups
// Each list item has a date
// When clicked, starts the restore action
if (backupStatus != BackupStatusEnum.initializing &&
backupStatus != BackupStatusEnum.noKey)
BrandCards.outlined(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: Icon(
Icons.refresh,
),
title: Text(
'providers.backup.restore'.tr(),
style: Theme.of(context).textTheme.headline6,
),
),
Divider(
height: 1.0,
),
if (backups.isEmpty)
ListTile(
leading: Icon(
Icons.error_outline,
),
title: Text('providers.backup.no_backups'.tr()),
),
if (backups.isNotEmpty)
Column(
children: backups.map((backup) {
return ListTile(
onTap: preventActions
? null
: () {
var nav = getIt<NavigationService>();
nav.showPopUpDialog(BrandAlert(
title: 'providers.backup.restoring'.tr(),
contentText: 'providers.backup.restore_alert'
.tr(args: [backup.time.toString()]),
actions: [
ActionButton(
text: 'basis.cancel'.tr(),
),
ActionButton(
onPressed: () => {
context
.read<BackupsCubit>()
.restoreBackup(backup.id)
},
text: 'modals.yes'.tr(),
)
],
));
},
title: Text(
MaterialLocalizations.of(context)
.formatShortDate(backup.time) +
' ' +
TimeOfDay.fromDateTime(backup.time)
.format(context),
),
);
}).toList(),
),
],
),
),
SizedBox(height: 16),
BrandCards.outlined(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text(
'providers.backup.refresh'.tr(),
),
onTap: refreshing
? null
: () => {context.read<BackupsCubit>().updateBackups()},
enabled: !refreshing,
),
if (providerState != StateType.uninitialized)
Column(
children: [
Divider(
height: 1.0,
),
ListTile(
title: Text(
'providers.backup.refetchBackups'.tr(),
),
onTap: preventActions
? null
: () => {
context
.read<BackupsCubit>()
.forceUpdateBackups()
},
),
Divider(
height: 1.0,
),
ListTile(
title: Text(
'providers.backup.reuploadKey'.tr(),
),
onTap: preventActions
? null
: () => {context.read<BackupsCubit>().reuploadKey()},
),
],
),
],
),
),
),
if (backupStatus == BackupStatusEnum.error)
BrandText.body1(backupError.toString()),
],
);
}
}

View File

@ -1,80 +0,0 @@
part of 'backup_details.dart';
class _Header extends StatelessWidget {
const _Header(
{Key? key, required this.providerState, required this.refreshing})
: super(key: key);
final StateType providerState;
final bool refreshing;
@override
Widget build(BuildContext context) {
return Row(
children: [
IconStatusMask(
status: providerState,
child: Icon(
BrandIcons.save,
size: 40,
color: Colors.white,
),
),
Spacer(),
Padding(
padding: EdgeInsets.symmetric(
vertical: 4,
horizontal: 2,
),
child: IconButton(
onPressed: refreshing
? null
: () => {context.read<BackupsCubit>().updateBackups()},
icon: const Icon(Icons.refresh_rounded),
),
),
Padding(
padding: EdgeInsets.symmetric(
vertical: 4,
horizontal: 2,
),
child: PopupMenuButton<_PopupMenuItemType>(
enabled: providerState != StateType.uninitialized,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
onSelected: (_PopupMenuItemType result) {
switch (result) {
case _PopupMenuItemType.reuploadKey:
context.read<BackupsCubit>().reuploadKey();
break;
case _PopupMenuItemType.refetchBackups:
context.read<BackupsCubit>().forceUpdateBackups();
break;
}
},
icon: Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => [
PopupMenuItem<_PopupMenuItemType>(
value: _PopupMenuItemType.reuploadKey,
child: Container(
padding: EdgeInsets.only(left: 5),
child: Text('providers.backup.reuploadKey'.tr()),
),
),
PopupMenuItem<_PopupMenuItemType>(
value: _PopupMenuItemType.refetchBackups,
child: Container(
padding: EdgeInsets.only(left: 5),
child: Text('providers.backup.refetchBackups'.tr()),
),
),
],
),
),
],
);
}
}
enum _PopupMenuItemType { reuploadKey, refetchBackups }

View File

@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
@ -15,7 +16,7 @@ import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
import 'package:selfprivacy/ui/pages/backup_details/backup_details.dart';
import 'package:selfprivacy/ui/pages/server_details/server_details.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/utils/ui_helpers.dart';
var navigatorKey = GlobalKey<NavigatorState>();
@ -120,15 +121,9 @@ class _Card extends StatelessWidget {
title = 'providers.backup.card_title'.tr();
stableText = 'providers.backup.status'.tr();
onTap = () => showBrandBottomSheet(
context: context,
builder: (BuildContext context) {
return BrandBottomSheet(
isExpended: true,
child: BackupDetails(),
);
},
);
onTap = () => Navigator.of(context).push(materialRoute(
BackupDetails(),
));
break;
}
return GestureDetector(