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",
|
||||
"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...",
|
||||
|
|
|
@ -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": "Синхронизация...",
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -847,7 +847,7 @@ class ServerApi extends ApiMap {
|
|||
response = await client.delete(
|
||||
'/auth/tokens',
|
||||
data: {
|
||||
'device': device,
|
||||
'token_name': device,
|
||||
},
|
||||
);
|
||||
} 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_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,
|
||||
|
|
Loading…
Reference in a new issue