Add devices screen

pull/90/head
Inex Code 2022-05-31 17:30:44 +03:00
parent 7810c2a279
commit e8d5ecccf6
7 changed files with 293 additions and 4 deletions

View File

@ -28,7 +28,8 @@
"no_data": "No data",
"wait": "Wait",
"remove": "Remove",
"apply": "Apply"
"apply": "Apply",
"done": "Done"
},
"more": {
"_comment": "'More' tab",
@ -286,7 +287,7 @@
"recovery_main_header": "Connect to an existing server",
"domain_recovery_description": "Enter a server domain you want to get access for:",
"domain_recover_placeholder": "Your domain",
"domain_recover_error": "Server with such domain is not found",
"domain_recover_error": "Server with such domain was not found",
"method_select_description": "Select a recovery method:",
"method_select_other_device": "I have access on another device",
"method_select_recovery_key": "I have a recovery key",
@ -324,6 +325,31 @@
"confirm_backblaze": "Connect to Backblaze",
"confirm_backblaze_description": "Enter a Backblaze token with access to backup storage:"
},
"devices": {
"main_screen": {
"header": "Devices",
"description": "These devices have full access to the server via SelfPrivacy app.",
"this_device": "This device",
"other_devices": "Other devices",
"authorize_new_device": "Authorize new device",
"access_granted_on" : "Access granted on {}",
"tip": "Press on the device to revoke access."
},
"add_new_device_screen": {
"header": "Authorizing new device",
"description": "Enter the key on the device you want to authorize:",
"please_wait": "Please wait",
"tip": "The key is valid for 10 minutes.",
"expired": "The key has expired.",
"get_new_key": "Get new key"
},
"revoke_device_alert": {
"header": "Revoke access?",
"description": "The device {} will no longer have access to the server.",
"yes": "Revoke",
"no": "Cancel"
}
},
"recovery_key": {
"key_connection_error": "Couldn't connect to the server.",
"key_synchronizing": "Synchronizing...",

View File

@ -28,7 +28,8 @@
"no_data": "Нет данных",
"wait": "Загрузка",
"remove": "Удалить",
"apply": "Подать"
"apply": "Подать",
"done": "Готово"
},
"more": {
"_comment": "вкладка ещё",
@ -322,6 +323,31 @@
"confirm_backblze": "Подключение к Backblaze",
"confirm_backblaze_description": "Введите токен Backblaze, который имеет права на хранилище резервных копий:"
},
"devices": {
"main_screen": {
"header": "Устройства",
"description": "Эти устройства имеют полный доступ к управлению сервером через приложение SelfPrivacy.",
"this_device": "Это устройство",
"other_devices": "Другие устройства",
"authorize_new_device": "Авторизовать новое устройство",
"access_granted_on" : "Доступ выдан {}",
"tip": "Нажмите на устройство, чтобы отозвать доступ."
},
"add_new_device_screen": {
"header": "Авторизация нового устройства",
"description": "Введите этот ключ на новом устройстве:",
"please_wait": "Пожалуйста, подождите",
"tip": "Ключ действителен 10 минут.",
"expired": "Срок действия ключа истёк.",
"get_new_key": "Получить новый ключ"
},
"revoke_device_alert": {
"header": "Отозвать доступ?",
"description": "Устройство {} больше не сможет управлять сервером.",
"yes": "Отозвать",
"no": "Отмена"
}
},
"recovery_key": {
"key_connection_error": "Не удалось соединиться с сервером",
"key_synchronizing": "Синхронизация...",

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
@ -24,6 +25,7 @@ class BlocAndProviderConfig extends StatelessWidget {
var backupsCubit = BackupsCubit(serverInstallationCubit);
var dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit);
var recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit);
var apiDevicesCubit = ApiDevicesCubit(serverInstallationCubit);
return MultiProvider(
providers: [
BlocProvider(
@ -39,6 +41,7 @@ class BlocAndProviderConfig extends StatelessWidget {
BlocProvider(create: (_) => backupsCubit..load(), lazy: false),
BlocProvider(create: (_) => dnsRecordsCubit..load()),
BlocProvider(create: (_) => recoveryKeyCubit..load()),
BlocProvider(create: (_) => apiDevicesCubit..load()),
BlocProvider(
create: (_) =>
JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit),

View File

@ -847,7 +847,7 @@ class ServerApi extends ApiMap {
response = await client.delete(
'/auth/tokens',
data: {
'device': device,
'token_name': device,
},
);
} on DioError catch (e) {

View File

@ -0,0 +1,143 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.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/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/ui/pages/devices/new_device.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
class DevicesScreen extends StatefulWidget {
const DevicesScreen({Key? key}) : super(key: key);
@override
State<DevicesScreen> createState() => _DevicesScreenState();
}
class _DevicesScreenState extends State<DevicesScreen> {
@override
Widget build(BuildContext context) {
final devicesStatus = context.watch<ApiDevicesCubit>().state;
return RefreshIndicator(
onRefresh: () async {
context.read<ApiDevicesCubit>().refresh();
},
child: BrandHeroScreen(
heroTitle: 'devices.main_screen.header'.tr(),
heroSubtitle: 'devices.main_screen.description'.tr(),
hasBackButton: true,
hasFlashButton: false,
children: [
Text(
'devices.main_screen.this_device'.tr(),
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
),
_DeviceTile(device: devicesStatus.thisDevice),
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,
),
),
...devicesStatus.otherDevices
.map((device) => _DeviceTile(device: device))
.toList(),
const SizedBox(height: 16),
OutlinedButton(
onPressed: () => Navigator.of(context)
.push(materialRoute(const NewDeviceScreen())),
child: Text('devices.main_screen.authorize_new_device'.tr()),
),
const SizedBox(height: 16),
const Divider(height: 1),
const SizedBox(height: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline,
color: Theme.of(context).colorScheme.onBackground,
),
const SizedBox(height: 16),
Text(
'devices.main_screen.tip'.tr(),
style: Theme.of(context).textTheme.bodyMedium!,
),
],
),
const SizedBox(height: 24),
],
),
);
}
}
class _DeviceTile extends StatelessWidget {
const _DeviceTile({Key? key, required this.device}) : super(key: key);
final ApiToken device;
@override
Widget build(BuildContext context) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4),
title: Text(device.name),
subtitle: Text('devices.main_screen.access_granted_on'
.tr(args: [DateFormat.yMMMMd().format(device.date)])),
onTap: device.isCaller
? null
: () => _showConfirmationDialog(context, device),
);
}
_showConfirmationDialog(BuildContext context, ApiToken device) => showDialog(
context: context,
builder: (context) {
return 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<ApiDevicesCubit>().deleteDevice(device);
Navigator.of(context).pop();
},
),
],
);
},
);
}

View File

@ -0,0 +1,84 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/brand_button/filled_button.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
class NewDeviceScreen extends StatelessWidget {
const NewDeviceScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BrandHeroScreen(
heroTitle: 'devices.add_new_device_screen.header'.tr(),
heroSubtitle: 'devices.add_new_device_screen.description'.tr(),
hasBackButton: true,
hasFlashButton: false,
children: [
FutureBuilder(
future: context.read<ApiDevicesCubit>().getNewDeviceKey(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return _KeyDisplay(
newDeviceKey: snapshot.data.toString(),
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
),
],
);
}
}
class _KeyDisplay extends StatelessWidget {
const _KeyDisplay({Key? key, required this.newDeviceKey}) : super(key: key);
final String newDeviceKey;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(),
const SizedBox(height: 16),
Text(
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.onBackground,
),
const SizedBox(height: 16),
Text(
'devices.add_new_device_screen.tip'.tr(),
style: Theme.of(context).textTheme.bodyMedium!,
),
],
),
const SizedBox(height: 16),
FilledButton(
child: Text(
'basis.done'.tr(),
),
onPressed: () => Navigator.of(context).pop(),
),
const SizedBox(height: 24),
],
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/pages/devices/devices.dart';
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
@ -61,6 +62,12 @@ class MorePage extends StatelessWidget {
goTo: const RecoveryKey(),
title: 'recovery_key.key_main_header'.tr(),
),
if (isReady)
_MoreMenuItem(
iconData: Icons.devices_outlined,
goTo: const DevicesScreen(),
title: 'devices.main_screen.header'.tr(),
),
_MoreMenuItem(
title: 'more.settings.title'.tr(),
iconData: Icons.settings_outlined,