mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-01-27 11:16:45 +00:00
Add devices screen
This commit is contained in:
parent
7810c2a279
commit
e8d5ecccf6
|
@ -28,7 +28,8 @@
|
||||||
"no_data": "No data",
|
"no_data": "No data",
|
||||||
"wait": "Wait",
|
"wait": "Wait",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"apply": "Apply"
|
"apply": "Apply",
|
||||||
|
"done": "Done"
|
||||||
},
|
},
|
||||||
"more": {
|
"more": {
|
||||||
"_comment": "'More' tab",
|
"_comment": "'More' tab",
|
||||||
|
@ -286,7 +287,7 @@
|
||||||
"recovery_main_header": "Connect to an existing server",
|
"recovery_main_header": "Connect to an existing server",
|
||||||
"domain_recovery_description": "Enter a server domain you want to get access for:",
|
"domain_recovery_description": "Enter a server domain you want to get access for:",
|
||||||
"domain_recover_placeholder": "Your domain",
|
"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_description": "Select a recovery method:",
|
||||||
"method_select_other_device": "I have access on another device",
|
"method_select_other_device": "I have access on another device",
|
||||||
"method_select_recovery_key": "I have a recovery key",
|
"method_select_recovery_key": "I have a recovery key",
|
||||||
|
@ -324,6 +325,31 @@
|
||||||
"confirm_backblaze": "Connect to Backblaze",
|
"confirm_backblaze": "Connect to Backblaze",
|
||||||
"confirm_backblaze_description": "Enter a Backblaze token with access to backup storage:"
|
"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": {
|
"recovery_key": {
|
||||||
"key_connection_error": "Couldn't connect to the server.",
|
"key_connection_error": "Couldn't connect to the server.",
|
||||||
"key_synchronizing": "Synchronizing...",
|
"key_synchronizing": "Synchronizing...",
|
||||||
|
|
|
@ -28,7 +28,8 @@
|
||||||
"no_data": "Нет данных",
|
"no_data": "Нет данных",
|
||||||
"wait": "Загрузка",
|
"wait": "Загрузка",
|
||||||
"remove": "Удалить",
|
"remove": "Удалить",
|
||||||
"apply": "Подать"
|
"apply": "Подать",
|
||||||
|
"done": "Готово"
|
||||||
},
|
},
|
||||||
"more": {
|
"more": {
|
||||||
"_comment": "вкладка ещё",
|
"_comment": "вкладка ещё",
|
||||||
|
@ -322,6 +323,31 @@
|
||||||
"confirm_backblze": "Подключение к Backblaze",
|
"confirm_backblze": "Подключение к Backblaze",
|
||||||
"confirm_backblaze_description": "Введите токен 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": {
|
"recovery_key": {
|
||||||
"key_connection_error": "Не удалось соединиться с сервером",
|
"key_connection_error": "Не удалось соединиться с сервером",
|
||||||
"key_synchronizing": "Синхронизация...",
|
"key_synchronizing": "Синхронизация...",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/recovery_key/recovery_key_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_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 backupsCubit = BackupsCubit(serverInstallationCubit);
|
||||||
var dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit);
|
var dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit);
|
||||||
var recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit);
|
var recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit);
|
||||||
|
var apiDevicesCubit = ApiDevicesCubit(serverInstallationCubit);
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
|
@ -39,6 +41,7 @@ class BlocAndProviderConfig extends StatelessWidget {
|
||||||
BlocProvider(create: (_) => backupsCubit..load(), lazy: false),
|
BlocProvider(create: (_) => backupsCubit..load(), lazy: false),
|
||||||
BlocProvider(create: (_) => dnsRecordsCubit..load()),
|
BlocProvider(create: (_) => dnsRecordsCubit..load()),
|
||||||
BlocProvider(create: (_) => recoveryKeyCubit..load()),
|
BlocProvider(create: (_) => recoveryKeyCubit..load()),
|
||||||
|
BlocProvider(create: (_) => apiDevicesCubit..load()),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (_) =>
|
create: (_) =>
|
||||||
JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit),
|
JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit),
|
||||||
|
|
|
@ -847,7 +847,7 @@ class ServerApi extends ApiMap {
|
||||||
response = await client.delete(
|
response = await client.delete(
|
||||||
'/auth/tokens',
|
'/auth/tokens',
|
||||||
data: {
|
data: {
|
||||||
'device': device,
|
'token_name': device,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} on DioError catch (e) {
|
} on DioError catch (e) {
|
||||||
|
|
143
lib/ui/pages/devices/devices.dart
Normal file
143
lib/ui/pages/devices/devices.dart
Normal 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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
84
lib/ui/pages/devices/new_device.dart
Normal file
84
lib/ui/pages/devices/new_device.dart
Normal 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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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_cards/brand_cards.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_header/brand_header.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/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/recovery_key/recovery_key.dart';
|
||||||
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
|
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
|
||||||
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
|
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
|
||||||
|
@ -61,6 +62,12 @@ class MorePage extends StatelessWidget {
|
||||||
goTo: const RecoveryKey(),
|
goTo: const RecoveryKey(),
|
||||||
title: 'recovery_key.key_main_header'.tr(),
|
title: 'recovery_key.key_main_header'.tr(),
|
||||||
),
|
),
|
||||||
|
if (isReady)
|
||||||
|
_MoreMenuItem(
|
||||||
|
iconData: Icons.devices_outlined,
|
||||||
|
goTo: const DevicesScreen(),
|
||||||
|
title: 'devices.main_screen.header'.tr(),
|
||||||
|
),
|
||||||
_MoreMenuItem(
|
_MoreMenuItem(
|
||||||
title: 'more.settings.title'.tr(),
|
title: 'more.settings.title'.tr(),
|
||||||
iconData: Icons.settings_outlined,
|
iconData: Icons.settings_outlined,
|
||||||
|
|
Loading…
Reference in a new issue