diff --git a/android/app/build.gradle b/android/app/build.gradle index 4aa064e2..5e265753 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -39,7 +39,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "pro.kherel.selfprivacy" - minSdkVersion 16 + minSdkVersion 18 targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c30b8e19..b50fa5a1 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -15,11 +15,9 @@ android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> - + android:windowSoftInputMode="adjustResize" + android:allowBackup="false" > + AppSettingsCubit(isDarkModeOn: isDark)), - BlocProvider(create: (_) => InitializingCubit()), + BlocProvider( + create: (_) => AppSettingsCubit( + isDarkModeOn: isDark, + isOnbordingShowing: true, + )..load(), + ), + BlocProvider( + create: (_) => AppConfigCubit()..load(), + ), BlocProvider(create: (_) => ServicesCubit()), BlocProvider(create: (_) => ProvidersCubit()), BlocProvider(create: (_) => UsersCubit()), diff --git a/lib/config/brand_colors.dart b/lib/config/brand_colors.dart index 0bd0c44c..c68d6dd8 100644 --- a/lib/config/brand_colors.dart +++ b/lib/config/brand_colors.dart @@ -1,33 +1,23 @@ import 'package:flutter/material.dart'; class BrandColors { - /// ![](https://www.colorhexa.com/093CEF.png) static const Color blue = Color(0xFF093CEF); static const Color white = Colors.white; static const Color black = Colors.black; - /// ![](https://www.colorhexa.com/555555.png) static const Color gray1 = Color(0xFF555555); - - /// ![](https://www.colorhexa.com/7C7C7C.png) static const Color gray2 = Color(0xFF7C7C7C); - - /// ![](https://www.colorhexa.com/fafafa.png) static const Color gray3 = Color(0xFFFAFAFA); - - /// ![](https://www.colorhexa.com/DDDDDD.png) static const Color gray4 = Color(0xFFDDDDDD); - - /// ![](https://www.colorhexa.com/EDEEF1.png) static const Color gray5 = Color(0xFFEDEEF1); + static Color gray6 = Color(0xFF181818).withOpacity(0.7); + static const Color grey7 = Color(0xFFABABAB); - /// ![](https://www.colorhexa.com/FA0E0E.png) - static const Color red = Color(0xFFFA0E0E); + static const Color red1 = Color(0xFFFA0E0E); + static const Color red2 = Color(0xFFE65527); - /// ![](https://www.colorhexa.com/00AF54.png) static const Color green1 = Color(0xFF00AF54); - /// ![](https://www.colorhexa.com/0F8849.png) static const Color green2 = Color(0xFF0F8849); static get navBackgroundLight => white.withOpacity(0.8); @@ -60,5 +50,5 @@ class BrandColors { static const textColor1 = black; static const textColor2 = gray1; static const dividerColor = gray5; - static const warning = red; + static const warning = red1; } diff --git a/lib/config/brand_theme.dart b/lib/config/brand_theme.dart index 477b8980..0505e471 100644 --- a/lib/config/brand_theme.dart +++ b/lib/config/brand_theme.dart @@ -23,20 +23,20 @@ final ligtTheme = ThemeData( borderRadius: BorderRadius.all(Radius.circular(4)), borderSide: BorderSide( width: 1, - color: BrandColors.red, + color: BrandColors.red1, ), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(4)), borderSide: BorderSide( width: 1, - color: BrandColors.red, + color: BrandColors.red1, ), ), errorStyle: GoogleFonts.inter( textStyle: TextStyle( fontSize: 12, - color: BrandColors.red, + color: BrandColors.red1, ), ), ), diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart new file mode 100644 index 00000000..b0ca4009 --- /dev/null +++ b/lib/config/hive_config.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/server_details.dart'; +import 'package:selfprivacy/logic/models/user.dart'; + +class HiveConfig { + static Future init() async { + await Hive.initFlutter(); + Hive.registerAdapter(UserAdapter()); + Hive.registerAdapter(HetznerServerDetailsAdapter()); + Hive.registerAdapter(CloudFlareDomainAdapter()); + + await Hive.openBox(BNames.appSettings); + var cipher = HiveAesCipher(await getEncriptedKey()); + + await Hive.openBox(BNames.appConfig, encryptionCipher: cipher); + } + + static Future getEncriptedKey() async { + final FlutterSecureStorage secureStorage = FlutterSecureStorage(); + var containsEncryptionKey = + await secureStorage.containsKey(key: BNames.key); + if (!containsEncryptionKey) { + var key = Hive.generateSecureKey(); + await secureStorage.write(key: BNames.key, value: base64UrlEncode(key)); + } + + return base64Url.decode(await secureStorage.read(key: BNames.key)); + } +} + +class BNames { + static String appConfig = 'appConfig'; + static String isDarkModeOn = 'isDarkModeOn'; + static String isOnbordingShowing = 'isOnbordingShowing'; + + static String appSettings = 'appSettings'; + + static String key = 'key'; + + static String domain = 'domain'; + static String hetznerKey = 'hetznerKey'; + static String cloudFlareKey = 'cloudFlareKey'; + static String rootUser = 'rootUser'; + static String server = 'server'; + static String isDnsCheckedAndDkimSet = 'isDnsCheckedAndDkimSet'; + static String serverInitStart = 'serverInitStart'; +} diff --git a/lib/config/text_themes.dart b/lib/config/text_themes.dart index acc9425f..54d53ee4 100644 --- a/lib/config/text_themes.dart +++ b/lib/config/text_themes.dart @@ -18,6 +18,12 @@ final headline1Style = GoogleFonts.inter( ); final headline2Style = GoogleFonts.inter( + fontSize: 24, + fontWeight: NamedFontWeight.extraBold, + color: BrandColors.headlineColor, +); + +final onboardingTitle = GoogleFonts.inter( fontSize: 30, fontWeight: NamedFontWeight.extraBold, color: BrandColors.headlineColor, @@ -40,6 +46,8 @@ final body2Style = defaultTextStyle.copyWith( color: BrandColors.textColor2, ); +final mediumStyle = defaultTextStyle.copyWith(fontSize: 13, height: 1.53); + final smallStyle = defaultTextStyle.copyWith(fontSize: 11, height: 1.45); final linkStyle = defaultTextStyle.copyWith(color: BrandColors.blue); diff --git a/lib/logic/api_maps/api_map.dart b/lib/logic/api_maps/api_map.dart new file mode 100644 index 00000000..53c34524 --- /dev/null +++ b/lib/logic/api_maps/api_map.dart @@ -0,0 +1,11 @@ +import 'package:dio/dio.dart'; + +abstract class ApiMap { + String rootAddress; + + Dio client = Dio(); + + void close() { + client.close(); + } +} diff --git a/lib/logic/api_maps/cloud_flare.dart b/lib/logic/api_maps/cloud_flare.dart new file mode 100644 index 00000000..70b0d555 --- /dev/null +++ b/lib/logic/api_maps/cloud_flare.dart @@ -0,0 +1,127 @@ +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:selfprivacy/logic/api_maps/api_map.dart'; +import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/dns_records.dart'; + +class CloudflareApi extends ApiMap { + CloudflareApi([String token]) { + if (token != null) { + client.options = BaseOptions(headers: {'Authorization': 'Bearer $token'}); + } + } + + @override + String rootAddress = 'https://api.cloudflare.com/client/v4'; + + Future isValid(String token) async { + var url = '$rootAddress/user/tokens/verify'; + var options = Options( + headers: {'Authorization': 'Bearer $token'}, + validateStatus: (status) { + return status == HttpStatus.ok || status == HttpStatus.unauthorized; + }, + ); + + Response response = await client.get(url, options: options); + + if (response.statusCode == HttpStatus.ok) { + return true; + } else if (response.statusCode == HttpStatus.unauthorized) { + return false; + } else { + throw Exception('something bad happend'); + } + } + + Future getZoneId(String token, String domain) async { + var url = '$rootAddress/zones'; + + var options = Options( + headers: {'Authorization': 'Bearer $token'}, + validateStatus: (status) { + return status == HttpStatus.ok || status == HttpStatus.forbidden; + }, + ); + + Response response = await client.get( + url, + options: options, + queryParameters: {'name': domain}, + ); + + try { + return response.data['result'][0]['id']; + } catch (error) { + return null; + } + } + + Future createMultipleDnsRecords({ + String ip4, + CloudFlareDomain cloudFlareDomain, + }) async { + var domainName = cloudFlareDomain.name; + var domainZoneId = cloudFlareDomain.zoneId; + + var domainA = DnsRecords(type: 'A', name: domainName, content: ip4); + var apiA = DnsRecords(type: 'A', name: 'api', content: ip4); + var cloudA = DnsRecords(type: 'A', name: 'cloud', content: ip4); + var gitA = DnsRecords(type: 'A', name: 'git', content: ip4); + var meetA = DnsRecords(type: 'A', name: 'meet', content: ip4); + var passwordA = DnsRecords(type: 'A', name: 'password', content: ip4); + var socialA = DnsRecords(type: 'A', name: 'social', content: ip4); + var mx = DnsRecords(type: 'MX', name: '@', content: domainName); + + var txt1 = DnsRecords( + type: 'TXT', + name: '_dmarc', + content: 'v=DMARC1; p=none', + ttl: 18000, + ); + + var txt2 = DnsRecords( + type: 'TXT', + name: cloudFlareDomain.name, + content: 'v=spf1 a mx ip4:$ip4 -all', + ttl: 18000, + ); + + var listDnsRecords = [ + domainA, + apiA, + cloudA, + gitA, + meetA, + passwordA, + socialA, + mx, + txt1, + txt2 + ]; + + var allFutures = []; + + for (var record in listDnsRecords) { + var url = '$rootAddress/zones/$domainZoneId/dns_records'; + + allFutures.add( + client.post( + url, + data: record.toJson(), + ), + ); + } + + await Future.wait(allFutures); + } + + // createSDKIM(String dkim) { + // var txt3 = DnsRecords( + // type: 'TXT', + // name: 'selector._domainkey', + // content: dkim, + // ttl: 18000, + // ); + // } +} diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart new file mode 100644 index 00000000..a14027c1 --- /dev/null +++ b/lib/logic/api_maps/hetzner.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:selfprivacy/logic/api_maps/api_map.dart'; +import 'package:selfprivacy/logic/models/server_details.dart'; +import 'package:selfprivacy/logic/models/user.dart'; + +class HetznerApi extends ApiMap { + HetznerApi([String token]) { + if (token != null) { + client.options = BaseOptions(headers: {'Authorization': 'Bearer $token'}); + } + } + + @override + String rootAddress = 'https://api.hetzner.cloud/v1/servers'; + + Future isValid(String token) async { + var options = Options( + headers: {'Authorization': 'Bearer $token'}, + validateStatus: (status) { + return status == HttpStatus.ok || status == HttpStatus.unauthorized; + }, + ); + + Response response = await client.get(rootAddress, options: options); + + if (response.statusCode == HttpStatus.ok) { + return true; + } else if (response.statusCode == HttpStatus.unauthorized) { + return false; + } else { + throw Exception('something bad happend'); + } + } + + Future createServer({ + @required User rootUser, + @required String domainName, + }) async { + var data = { + "name": "selfprivacy-server", + "server_type": "cx11", + "start_after_create": true, + "image": "ubuntu-20.04", + "ssh_keys": [], + "volumes": [], + "networks": [], + "user_data": + "#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/ilchub/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-20.09 DOMAIN=$domainName USER=${rootUser.login} PASSWORD=${rootUser.password} HASHED_PASSWORD=${rootUser.hashPassword} bash 2>&1 | tee /tmp/infect.log \nruncmd:\n- curl https://git.selfprivacy.org/ilchub/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-20.09 DOMAIN=$domainName USER=${rootUser.login} PASSWORD=${rootUser.password} HASHED_PASSWORD=${rootUser.hashPassword} bash 2>&1 | tee /tmp/infect.log", + }; + Response response = await client.post( + rootAddress, + data: data, + ); + + return HetznerServerDetails( + id: response.data['server']['id'], + ip4: response.data['server']['public_net']['ipv4']['ip'], + serverInitializaionDateTime: DateTime.now(), + ); + } +} diff --git a/lib/logic/cubit/app_config/app_config_cubit.dart b/lib/logic/cubit/app_config/app_config_cubit.dart new file mode 100644 index 00000000..56ca79cb --- /dev/null +++ b/lib/logic/cubit/app_config/app_config_cubit.dart @@ -0,0 +1,86 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:selfprivacy/config/hive_config.dart'; +import 'package:selfprivacy/logic/api_maps/cloud_flare.dart'; +import 'package:selfprivacy/logic/api_maps/hetzner.dart'; +import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/server_details.dart'; +import 'package:selfprivacy/logic/models/user.dart'; + +part 'app_config_state.dart'; + +class AppConfigCubit extends Cubit { + AppConfigCubit() : super(InitialAppConfigState()); + + Box box = Hive.box(BNames.appConfig); + + void load() { + emit( + state.copyWith( + hetznerKey: box.get(BNames.hetznerKey), + cloudFlareKey: box.get(BNames.cloudFlareKey), + domain: box.get(BNames.domain), + rootUser: box.get(BNames.rootUser), + hetznerServer: box.get(BNames.server), + isDnsCheckedAndDkimSet: box.get(BNames.isDnsCheckedAndDkimSet), + ), + ); + } + + void reset() { + box.clear(); + emit(InitialAppConfigState()); + } + + void setHetznerKey(String key) { + box.put(BNames.hetznerKey, key); + emit(state.copyWith(hetznerKey: key)); + } + + void setCloudFlare(String cloudFlareKey) { + box.put(BNames.cloudFlareKey, cloudFlareKey); + emit(state.copyWith(cloudFlareKey: cloudFlareKey)); + } + + void setDomain(CloudFlareDomain domain) { + box.put(BNames.domain, domain); + emit(state.copyWith(domain: domain)); + } + + void setRootUser(User rootUser) { + box.put(BNames.rootUser, rootUser); + emit(state.copyWith(rootUser: rootUser)); + } + + void setIsDnsCheckedAndDkimSet() { + box.put(BNames.isDnsCheckedAndDkimSet, true); + emit(state.copyWith(isDnsCheckedAndDkimSet: true)); + } + + void createServer() async { + emit(state.copyWith(isLoading: true)); + var hetznerApi = HetznerApi(state.hetznerKey); + var cloudflareApi = CloudflareApi(state.cloudFlareKey); + + var serverDetails = await hetznerApi.createServer( + rootUser: state.rootUser, + domainName: state.cloudFlareDomain.name, + ); + + cloudflareApi + .createMultipleDnsRecords( + ip4: serverDetails.ip4, + cloudFlareDomain: state.cloudFlareDomain, + ) + .then((_) => cloudflareApi.close()); + await box.put(BNames.server, serverDetails); + + hetznerApi.close(); + + emit(state.copyWith( + isLoading: false, + hetznerServer: serverDetails, + )); + } +} diff --git a/lib/logic/cubit/app_config/app_config_state.dart b/lib/logic/cubit/app_config/app_config_state.dart new file mode 100644 index 00000000..9cb4da35 --- /dev/null +++ b/lib/logic/cubit/app_config/app_config_state.dart @@ -0,0 +1,76 @@ +part of 'app_config_cubit.dart'; + +class AppConfigState extends Equatable { + const AppConfigState({ + this.hetznerKey, + this.cloudFlareKey, + this.cloudFlareDomain, + this.rootUser, + this.server, + this.isDnsCheckedAndDkimSet = false, + this.isLoading = false, + }); + + @override + List get props => [ + hetznerKey, + cloudFlareKey, + cloudFlareDomain, + rootUser, + server, + isDnsCheckedAndDkimSet, + isLoading, + ]; + + final String hetznerKey; + final String cloudFlareKey; + final CloudFlareDomain cloudFlareDomain; + final User rootUser; + final HetznerServerDetails server; + final bool isDnsCheckedAndDkimSet; + + final isLoading; + + AppConfigState copyWith({ + String hetznerKey, + String cloudFlareKey, + CloudFlareDomain domain, + User rootUser, + HetznerServerDetails hetznerServer, + bool isDnsCheckedAndDkimSet, + bool isLoading, + DateTime serverInitStart, + }) => + AppConfigState( + hetznerKey: hetznerKey ?? this.hetznerKey, + cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, + cloudFlareDomain: domain ?? this.cloudFlareDomain, + rootUser: rootUser ?? this.rootUser, + server: hetznerServer ?? this.server, + isDnsCheckedAndDkimSet: isDnsCheckedAndDkimSet ?? this.isDnsCheckedAndDkimSet, + isLoading: isLoading ?? this.isLoading, + ); + + bool get isHetznerFilled => hetznerKey != null; + bool get isCloudFlareFilled => cloudFlareKey != null; + bool get isDomainFilled => cloudFlareDomain != null; + bool get isUserFilled => rootUser != null; + bool get isServerFilled => server != null; + + bool get isFullyInitilized => _fulfilementList.every((el) => el); + + int get progress => _fulfilementList.where((el) => el).length; + + List get _fulfilementList => [ + isHetznerFilled, + isCloudFlareFilled, + isDomainFilled, + isUserFilled, + isServerFilled, + isDnsCheckedAndDkimSet, + ]; +} + +class InitialAppConfigState extends AppConfigState { + InitialAppConfigState() : super(); +} diff --git a/lib/logic/cubit/app_settings/app_settings_cubit.dart b/lib/logic/cubit/app_settings/app_settings_cubit.dart index a14f1577..9435b3c4 100644 --- a/lib/logic/cubit/app_settings/app_settings_cubit.dart +++ b/lib/logic/cubit/app_settings/app_settings_cubit.dart @@ -1,18 +1,44 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; +import 'package:hive/hive.dart'; +import 'package:selfprivacy/config/hive_config.dart'; export 'package:provider/provider.dart'; part 'app_settings_state.dart'; class AppSettingsCubit extends Cubit { AppSettingsCubit({ - bool isDarkModeOn, + @required bool isDarkModeOn, + @required bool isOnbordingShowing, }) : super( - AppSettingsState(isDarkModeOn: isDarkModeOn), + AppSettingsState( + isDarkModeOn: isDarkModeOn, + isOnbordingShowing: isOnbordingShowing, + ), ); - void update({@required bool isDarkModeOn}) { - emit(AppSettingsState(isDarkModeOn: isDarkModeOn)); + Box box = Hive.box(BNames.appSettings); + + void load() { + bool isDarkModeOn = box.get(BNames.isDarkModeOn); + bool isOnbordingShowing = box.get(BNames.isOnbordingShowing); + + emit(state.copyWith( + isDarkModeOn: isDarkModeOn, + isOnbordingShowing: isOnbordingShowing, + )); + } + + void updateDarkMode({@required bool isDarkModeOn}) { + box.put(BNames.isDarkModeOn, isDarkModeOn); + + emit(state.copyWith(isDarkModeOn: isDarkModeOn)); + } + + void turnOffOnboarding() { + box.put(BNames.isOnbordingShowing, false); + + emit(state.copyWith(isOnbordingShowing: false)); } } diff --git a/lib/logic/cubit/app_settings/app_settings_state.dart b/lib/logic/cubit/app_settings/app_settings_state.dart index 1ad68404..023ad2d0 100644 --- a/lib/logic/cubit/app_settings/app_settings_state.dart +++ b/lib/logic/cubit/app_settings/app_settings_state.dart @@ -3,10 +3,18 @@ part of 'app_settings_cubit.dart'; class AppSettingsState extends Equatable { const AppSettingsState({ @required this.isDarkModeOn, + @required this.isOnbordingShowing, }); final bool isDarkModeOn; + final bool isOnbordingShowing; + + AppSettingsState copyWith({isDarkModeOn, isOnbordingShowing}) => + AppSettingsState( + isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn, + isOnbordingShowing: isOnbordingShowing ?? this.isOnbordingShowing, + ); @override - List get props => [isDarkModeOn]; + List get props => [isDarkModeOn, isOnbordingShowing]; } diff --git a/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart b/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart index e69de29b..91d12ff5 100644 --- a/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/api_maps/cloud_flare.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; + +class CloudFlareFormCubit extends FormCubit { + CloudflareApi apiClient = CloudflareApi(); + + CloudFlareFormCubit(this.initializingCubit) { + var regExp = RegExp(r"\s+|[!$%^&*()_@+|~=`{}\[\]:<>?,.\/]"); + apiKey = FieldCubit( + initalValue: '', + validations: [ + RequiredStringValidation('required'), + ValidationModel( + (s) => regExp.hasMatch(s), 'invalid key format'), + LegnthStringValidationWithLenghShowing(40, 'length is [] shoud be 40') + ], + ); + + super.setFields([apiKey]); + } + + @override + FutureOr onSubmit() async { + initializingCubit.setCloudFlare(apiKey.state.value); + } + + final AppConfigCubit initializingCubit; + + FieldCubit apiKey; + + @override + FutureOr asyncValidation() async { + var isKeyValid = await apiClient.isValid(apiKey.state.value); + + if (!isKeyValid) { + apiKey.setError('bad key'); + return false; + } + return true; + } + + @override + Future close() async { + apiClient.close(); + + return super.close(); + } +} diff --git a/lib/logic/cubit/forms/initializing/domain_form_cubit.dart b/lib/logic/cubit/forms/initializing/domain_form_cubit.dart index e69de29b..49de42f3 100644 --- a/lib/logic/cubit/forms/initializing/domain_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/domain_form_cubit.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/api_maps/cloud_flare.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; + +class DomainFormCubit extends FormCubit { + CloudflareApi apiClient = CloudflareApi(); + + DomainFormCubit(this.initializingCubit) { + var regExp = + RegExp(r"^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}"); + domainName = FieldCubit( + initalValue: '', + validations: [ + RequiredStringValidation('required'), + ValidationModel( + (s) => !regExp.hasMatch(s), 'invalid domain format'), + ], + ); + + super.setFields([domainName]); + } + + @override + FutureOr onSubmit() async { + var domain = CloudFlareDomain( + name: domainName.state.value, + zoneId: zoneId, + ); + initializingCubit.setDomain(domain); + } + + final AppConfigCubit initializingCubit; + + FieldCubit domainName; + String zoneId; + + @override + FutureOr asyncValidation() async { + var key = initializingCubit.state.cloudFlareKey; + + var zoneId = await apiClient.getZoneId(key, domainName.state.value); + + if (zoneId == null) { + domainName.setError('Domain not in the list'); + return false; + } + this.zoneId = zoneId; + return true; + } + + @override + Future close() async { + apiClient.close(); + + return super.close(); + } +} diff --git a/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart index 0a62d715..657147aa 100644 --- a/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart @@ -1,18 +1,22 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; -import 'package:selfprivacy/logic/cubit/initializing/initializing_cubit.dart'; +import 'package:selfprivacy/logic/api_maps/hetzner.dart'; +import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; class HetznerFormCubit extends FormCubit { + HetznerApi apiClient = HetznerApi(); + HetznerFormCubit(this.initializingCubit) { - var regExp = RegExp(r"\s+|[-!$%^&*()_@+|~=`{}\[\]:" ";<>?,.\/]"); + var regExp = RegExp(r"\s+|[-!$%^&*()_@+|~=`{}\[\]:<>?,.\/]"); apiKey = FieldCubit( initalValue: '', validations: [ RequiredStringValidation('required'), ValidationModel( (s) => regExp.hasMatch(s), 'invalid key format'), - LegnthStringValidation(11, 'length is [] shoud be 11') + LegnthStringValidationWithLenghShowing(64, 'length is [] shoud be 64') ], ); @@ -21,24 +25,28 @@ class HetznerFormCubit extends FormCubit { @override FutureOr onSubmit() async { - print(apiKey.state.value); - await Future.delayed(const Duration(milliseconds: 300)); - // initializingCubit.setHetznerKey(apiKey.state.value); + initializingCubit.setHetznerKey(apiKey.state.value); } - final InitializingCubit initializingCubit; + final AppConfigCubit initializingCubit; FieldCubit apiKey; -} - -class LegnthStringValidation extends ValidationModel { - LegnthStringValidation(int length, String errorText) - : super((n) => n.length != length, errorText); @override - String check(String val) { - var length = val.length; - var errorMassage = this.errorMassage.replaceAll("[]", length.toString()); - return test(val) ? errorMassage : null; + FutureOr asyncValidation() async { + var isKeyValid = await apiClient.isValid(apiKey.state.value); + + if (!isKeyValid) { + apiKey.setError('bad key'); + return false; + } + return true; + } + + @override + Future close() async { + apiClient.close(); + + return super.close(); } } diff --git a/lib/logic/cubit/forms/initializing/user_form_cubit.dart b/lib/logic/cubit/forms/initializing/user_form_cubit.dart index e69de29b..47ea1365 100644 --- a/lib/logic/cubit/forms/initializing/user_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/user_form_cubit.dart @@ -0,0 +1,56 @@ +import 'dart:async'; + +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/api_maps/hetzner.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/models/user.dart'; + +class UserFormCubit extends FormCubit { + HetznerApi apiClient = HetznerApi(); + + UserFormCubit(this.initializingCubit) { + var userRegExp = RegExp(r"\W"); + var passwordRegExp = RegExp(r"[\n\r\s]+"); + + userName = FieldCubit( + initalValue: '', + validations: [ + RequiredStringValidation('required'), + ValidationModel( + (s) => userRegExp.hasMatch(s), 'invalid format'), + ], + ); + + password = FieldCubit( + initalValue: '', + validations: [ + RequiredStringValidation('required'), + ValidationModel( + (s) => passwordRegExp.hasMatch(s), 'invalid format'), + ], + ); + + super.setFields([userName, password]); + } + + @override + FutureOr onSubmit() async { + var user = User( + login: userName.state.value, + password: password.state.value, + ); + initializingCubit.setRootUser(user); + } + + final AppConfigCubit initializingCubit; + + FieldCubit userName; + FieldCubit password; + + @override + Future close() async { + apiClient.close(); + + return super.close(); + } +} diff --git a/lib/logic/cubit/forms/user/user.dart b/lib/logic/cubit/forms/user/user.dart new file mode 100644 index 00000000..b51c5c48 --- /dev/null +++ b/lib/logic/cubit/forms/user/user.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; +import 'package:selfprivacy/logic/models/user.dart'; + +class CloudFlareFormCubit extends FormCubit { + CloudFlareFormCubit({ + this.userCubit, + User user, + }) { + var isEdit = user != null; + + var userRegExp = RegExp(r"\W"); + var passwordRegExp = RegExp(r"[\n\r\s]+"); + + login = FieldCubit( + initalValue: isEdit ? user.login : '', + validations: [ + RequiredStringValidation('required'), + ValidationModel( + (s) => userRegExp.hasMatch(s), 'invalid format'), + ], + ); + + password = FieldCubit( + initalValue: isEdit ? user.password : '', + validations: [ + RequiredStringValidation('required'), + ValidationModel( + (s) => passwordRegExp.hasMatch(s), 'invalid format'), + ], + ); + + super.setFields([login, password]); + } + + @override + FutureOr onSubmit() { + var user = User( + login: login.state.value, + password: password.state.value, + ); + userCubit.add(user); + } + + FieldCubit login; + FieldCubit password; + + UsersCubit userCubit; +} diff --git a/lib/logic/cubit/forms/validations/validations.dart b/lib/logic/cubit/forms/validations/validations.dart new file mode 100644 index 00000000..bf39f787 --- /dev/null +++ b/lib/logic/cubit/forms/validations/validations.dart @@ -0,0 +1,13 @@ +import 'package:cubit_form/cubit_form.dart'; + +class LegnthStringValidationWithLenghShowing extends ValidationModel { + LegnthStringValidationWithLenghShowing(int length, String errorText) + : super((n) => n.length != length, errorText); + + @override + String check(String val) { + var length = val.length; + var errorMassage = this.errorMassage.replaceAll("[]", length.toString()); + return test(val) ? errorMassage : null; + } +} diff --git a/lib/logic/cubit/initializing/initializing_cubit.dart b/lib/logic/cubit/initializing/initializing_cubit.dart deleted file mode 100644 index 0fe6bcd9..00000000 --- a/lib/logic/cubit/initializing/initializing_cubit.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:selfprivacy/logic/models/config.dart'; -import 'package:selfprivacy/logic/models/user.dart'; - -part 'initializing_state.dart'; - -class InitializingCubit extends Cubit { - InitializingCubit() : super(InitialInitializingState()); - - void setHetznerKey(String key) { - var newCofig = state.appConfig.copyWith(hatzner: key); - emit(InitializingState(newCofig)); - } - - void setCloudFlare(String cloudFlareKey) { - var newCofig = state.appConfig.copyWith(cloudFlare: cloudFlareKey); - emit(InitializingState(newCofig)); - } - - void setDomain(String domain) { - var newCofig = state.appConfig.copyWith(domain: domain); - emit(InitializingState(newCofig)); - } - - void setRootUser(User rootUser) { - var newCofig = state.appConfig.copyWith(rootUser: rootUser); - emit(InitializingState(newCofig)); - } -} diff --git a/lib/logic/cubit/initializing/initializing_state.dart b/lib/logic/cubit/initializing/initializing_state.dart deleted file mode 100644 index 7827987d..00000000 --- a/lib/logic/cubit/initializing/initializing_state.dart +++ /dev/null @@ -1,26 +0,0 @@ -part of 'initializing_cubit.dart'; - -class InitializingState extends Equatable { - const InitializingState(this.appConfig); - - final AppConfig appConfig; - - @override - List get props => [appConfig]; - - bool get isHatznerFilled => appConfig.hatzner != null; - bool get isCloudFlareFilled => appConfig.cloudFlare != null; - bool get isDomainFilled => appConfig.domain != null; - bool get isUserFilled => appConfig.rootUser != null; - - bool get isFullyInitilized => _fulfilementList.every((el) => el); - - int get progress => _fulfilementList.where((el) => el).length; - - List get _fulfilementList => - [isHatznerFilled, isCloudFlareFilled, isDomainFilled, isUserFilled]; -} - -class InitialInitializingState extends InitializingState { - InitialInitializingState() : super(AppConfig.empty()); -} diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index 6febf023..6dcdd371 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -1,13 +1,12 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/logic/models/user.dart'; -import 'package:selfprivacy/utils/password_generator.dart'; export 'package:provider/provider.dart'; part 'users_state.dart'; class UsersCubit extends Cubit { - UsersCubit() : super(UsersState(initMockUsers)); + UsersCubit() : super(UsersState([])); void add(User user) { var users = state.users; @@ -24,20 +23,20 @@ class UsersCubit extends Cubit { } } -final initMockUsers = [ - User(login: 'Heartbreaking.Goose', password: genPass()), - User(login: 'Alma.lawson', password: genPass()), - User(login: 'Bee.gees', password: genPass()), - User(login: 'Bim.jennings', password: genPass()), - User(login: 'Debra.holt', password: genPass()), - User(login: 'Georgia.young', password: genPass()), - User(login: 'Kenzi.lawson', password: genPass()), - User(login: 'Le.jennings', password: genPass()), - User(login: 'Kirill.Zh', password: genPass()), - User(login: 'Tina.Bolton', password: genPass()), - User(login: 'Rebekah.Lynn', password: genPass()), - User(login: 'Aleena.Armstrong', password: genPass()), - User(login: 'Rosemary.Williams', password: genPass()), - User(login: 'Sullivan.Nixon', password: genPass()), - User(login: 'Aleena.Armstrong', password: genPass()), -]; +// final initMockUsers = [ +// User(login: 'Heartbreaking.Goose', password: genPass()), +// User(login: 'Alma.lawson', password: genPass()), +// User(login: 'Bee.gees', password: genPass()), +// User(login: 'Bim.jennings', password: genPass()), +// User(login: 'Debra.holt', password: genPass()), +// User(login: 'Georgia.young', password: genPass()), +// User(login: 'Kenzi.lawson', password: genPass()), +// User(login: 'Le.jennings', password: genPass()), +// User(login: 'Kirill.Zh', password: genPass()), +// User(login: 'Tina.Bolton', password: genPass()), +// User(login: 'Rebekah.Lynn', password: genPass()), +// User(login: 'Aleena.Armstrong', password: genPass()), +// User(login: 'Rosemary.Williams', password: genPass()), +// User(login: 'Sullivan.Nixon', password: genPass()), +// User(login: 'Aleena.Armstrong', password: genPass()), +// ]; diff --git a/lib/logic/cubit/users/users_state.dart b/lib/logic/cubit/users/users_state.dart index a7412631..88c56146 100644 --- a/lib/logic/cubit/users/users_state.dart +++ b/lib/logic/cubit/users/users_state.dart @@ -7,4 +7,6 @@ class UsersState extends Equatable { @override List get props => users; + + bool get isEmpty => users.isEmpty; } diff --git a/lib/logic/models/cloudflare_domain.dart b/lib/logic/models/cloudflare_domain.dart new file mode 100644 index 00000000..0cc24781 --- /dev/null +++ b/lib/logic/models/cloudflare_domain.dart @@ -0,0 +1,19 @@ +import 'package:hive/hive.dart'; + +part 'cloudflare_domain.g.dart'; + +@HiveType(typeId: 3) +class CloudFlareDomain { + CloudFlareDomain({this.name, this.zoneId}); + + @HiveField(0) + final String name; + + @HiveField(1) + final String zoneId; + + @override + String toString() { + return '$name: $zoneId'; + } +} diff --git a/lib/logic/models/cloudflare_domain.g.dart b/lib/logic/models/cloudflare_domain.g.dart new file mode 100644 index 00000000..c1dee8bd --- /dev/null +++ b/lib/logic/models/cloudflare_domain.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cloudflare_domain.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class CloudFlareDomainAdapter extends TypeAdapter { + @override + final int typeId = 3; + + @override + CloudFlareDomain read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return CloudFlareDomain( + name: fields[0] as String, + zoneId: fields[1] as String, + ); + } + + @override + void write(BinaryWriter writer, CloudFlareDomain obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.name) + ..writeByte(1) + ..write(obj.zoneId); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CloudFlareDomainAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/logic/models/config.dart b/lib/logic/models/config.dart deleted file mode 100644 index 82552241..00000000 --- a/lib/logic/models/config.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:selfprivacy/logic/models/user.dart'; - -class AppConfig extends Equatable { - const AppConfig({ - this.hatzner, - this.cloudFlare, - this.domain, - this.rootUser, - }); - - final String hatzner; - final String cloudFlare; - final String domain; - final User rootUser; - - factory AppConfig.empty() { - return AppConfig(); - } - - AppConfig copyWith({ - hatzner, - cloudFlare, - domain, - rootUser, - }) => - AppConfig( - hatzner: hatzner ?? this.hatzner, - cloudFlare: cloudFlare ?? this.cloudFlare, - domain: domain ?? this.domain, - rootUser: rootUser ?? this.rootUser, - ); - - @override - List get props => [hatzner, cloudFlare, domain, rootUser]; -} diff --git a/lib/logic/models/dns_records.dart b/lib/logic/models/dns_records.dart new file mode 100644 index 00000000..748ec763 --- /dev/null +++ b/lib/logic/models/dns_records.dart @@ -0,0 +1,25 @@ +import 'package:flutter/foundation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'dns_records.g.dart'; + +@JsonSerializable(createToJson: true, createFactory: false) +class DnsRecords { + DnsRecords({ + @required this.type, + @required this.name, + @required this.content, + this.ttl = 3600, + this.priority = 10, + this.proxied = false, + }); + + final String type; + final String name; + final String content; + final int ttl; + final int priority; + final bool proxied; + + toJson() => _$DnsRecordsToJson(this); +} diff --git a/lib/logic/models/dns_records.g.dart b/lib/logic/models/dns_records.g.dart new file mode 100644 index 00000000..315cb447 --- /dev/null +++ b/lib/logic/models/dns_records.g.dart @@ -0,0 +1,17 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'dns_records.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Map _$DnsRecordsToJson(DnsRecords instance) => + { + 'type': instance.type, + 'name': instance.name, + 'content': instance.content, + 'ttl': instance.ttl, + 'priority': instance.priority, + 'proxied': instance.proxied, + }; diff --git a/lib/logic/models/server_details.dart b/lib/logic/models/server_details.dart new file mode 100644 index 00000000..c628f073 --- /dev/null +++ b/lib/logic/models/server_details.dart @@ -0,0 +1,24 @@ +import 'package:flutter/widgets.dart'; +import 'package:hive/hive.dart'; + +part 'server_details.g.dart'; + +@HiveType(typeId: 2) +class HetznerServerDetails { + HetznerServerDetails({ + @required this.ip4, + @required this.id, + @required this.serverInitializaionDateTime, + }); + + @HiveField(0) + final String ip4; + + @HiveField(1) + final int id; + + @HiveField(2) + final DateTime serverInitializaionDateTime; + + String toString() => id.toString(); +} diff --git a/lib/logic/models/server_details.g.dart b/lib/logic/models/server_details.g.dart new file mode 100644 index 00000000..a51e3140 --- /dev/null +++ b/lib/logic/models/server_details.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'server_details.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class HetznerServerDetailsAdapter extends TypeAdapter { + @override + final int typeId = 2; + + @override + HetznerServerDetails read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return HetznerServerDetails( + ip4: fields[0] as String, + id: fields[1] as int, + serverInitializaionDateTime: fields[2] as DateTime, + ); + } + + @override + void write(BinaryWriter writer, HetznerServerDetails obj) { + writer + ..writeByte(3) + ..writeByte(0) + ..write(obj.ip4) + ..writeByte(1) + ..write(obj.id) + ..writeByte(2) + ..write(obj.serverInitializaionDateTime); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is HetznerServerDetailsAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/logic/models/user.dart b/lib/logic/models/user.dart index 4a96cee6..3c37a356 100644 --- a/lib/logic/models/user.dart +++ b/lib/logic/models/user.dart @@ -3,18 +3,32 @@ import 'dart:ui'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:selfprivacy/utils/color_utils.dart'; +import 'package:hive/hive.dart'; +import 'package:selfprivacy/utils/crypto.dart'; +part 'user.g.dart'; + +@HiveType(typeId: 1) class User extends Equatable { User({ @required this.login, @required this.password, }); + @HiveField(0) final String login; + + @HiveField(1) final String password; @override List get props => [login, password]; Color get color => stringToColor(login); + + String get hashPassword => convertToSha512Hash(password); + + String toString() { + return login; + } } diff --git a/lib/logic/models/user.g.dart b/lib/logic/models/user.g.dart new file mode 100644 index 00000000..796ea5dd --- /dev/null +++ b/lib/logic/models/user.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class UserAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + User read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return User( + login: fields[0] as String, + password: fields[1] as String, + ); + } + + @override + void write(BinaryWriter writer, User obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.login) + ..writeByte(1) + ..write(obj.password); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/main.dart b/lib/main.dart index 61f8194e..731665bc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:selfprivacy/config/hive_config.dart'; +import 'package:selfprivacy/ui/pages/initializing/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; - import 'config/bloc_config.dart'; import 'config/brand_theme.dart'; import 'config/localization.dart'; import 'logic/cubit/app_settings/app_settings_cubit.dart'; -void main() { +void main() async { + await HiveConfig.init(); + WidgetsFlutterBinding.ensureInitialized(); runApp( @@ -21,11 +24,11 @@ void main() { ); } -var _showOnbording = true; - class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + var appSettings = context.watch().state; + return AnnotatedRegion( value: SystemUiOverlayStyle.light, // Manually changnig appbar color child: MaterialApp( @@ -34,10 +37,10 @@ class MyApp extends StatelessWidget { locale: context.locale, debugShowCheckedModeBanner: false, title: 'SelfPrivacy', - theme: context.watch().state.isDarkModeOn - ? darkTheme - : ligtTheme, - home: _showOnbording ? OnboardingPage() : RootPage(), + theme: appSettings.isDarkModeOn ? darkTheme : ligtTheme, + home: appSettings.isOnbordingShowing + ? OnboardingPage(nextPage: InitializingPage()) + : RootPage(), ), ); } diff --git a/lib/ui/components/brand_card/brand_card.dart b/lib/ui/components/brand_card/brand_card.dart index 6908b363..a30ef5b1 100644 --- a/lib/ui/components/brand_card/brand_card.dart +++ b/lib/ui/components/brand_card/brand_card.dart @@ -1,21 +1,18 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/utils/extensions/elevation_extension.dart'; class BrandCard extends StatelessWidget { const BrandCard({ Key key, this.child, - this.isBlocked = false, }) : super(key: key); final Widget child; - final bool isBlocked; @override Widget build(BuildContext context) { - Widget res = Container( + return Container( margin: EdgeInsets.only(bottom: 30), decoration: BoxDecoration( color: Theme.of(context).brightness == Brightness.dark @@ -29,44 +26,5 @@ class BrandCard extends StatelessWidget { ), child: child, ); - - if (!isBlocked) { - return res; - } - - return IgnorePointer( - child: Stack( - children: [ - ColorFiltered( - colorFilter: ColorFilter.mode( - Colors.white, - BlendMode.saturation, - ), - child: res, - ), - Positioned( - top: 0, - left: 0, - right: 0, - bottom: 0, - child: Container( - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.white.withOpacity(0.8), - ), - padding: EdgeInsets.all(10), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - BrandText.h3('Blocked'), - BrandText.h4('finish initializing first') - ], - ), - ), - ) - ], - ), - ); } } diff --git a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart index e06600a6..544dc6db 100644 --- a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart +++ b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart @@ -64,7 +64,9 @@ class _BrandTabBarState extends State { var acitivColor = Theme.of(context).brightness == Brightness.dark ? BrandColors.white : BrandColors.black; - var color = currentIndex == index ? acitivColor : BrandColors.inactive; + + var isActive = currentIndex == index; + var color = isActive ? acitivColor : BrandColors.inactive; return InkWell( onTap: () => widget.controller.animateTo(index), child: Padding( @@ -75,7 +77,22 @@ class _BrandTabBarState extends State { children: [ Icon(iconData, color: color), SizedBox(height: 3), - Text(label, style: TextStyle(fontSize: 9, color: color)) + Row( + children: [ + if (isActive) ...[ + Container( + height: 5, + width: 5, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: BrandColors.red2, + ), + ), + SizedBox(width: 5), + ], + Text(label, style: TextStyle(fontSize: 9, color: color)), + ], + ) ], ), ), diff --git a/lib/ui/components/brand_text/brand_text.dart b/lib/ui/components/brand_text/brand_text.dart index fb7cae10..b216e61b 100644 --- a/lib/ui/components/brand_text/brand_text.dart +++ b/lib/ui/components/brand_text/brand_text.dart @@ -8,24 +8,27 @@ enum TextType { h4, // caption body1, // normal body2, // with opacity - small + medium, + small, + onboardingTitle } class BrandText extends StatelessWidget { - const BrandText( - this.text, { - Key key, - this.style, - @required this.type, - this.overflow, - this.softWrap, - }) : super(key: key); + const BrandText(this.text, + {Key key, + this.style, + @required this.type, + this.overflow, + this.softWrap, + this.textAlign}) + : super(key: key); final String text; final TextStyle style; final TextType type; final TextOverflow overflow; final bool softWrap; + final TextAlign textAlign; factory BrandText.h1( String text, { @@ -38,6 +41,13 @@ class BrandText extends StatelessWidget { type: TextType.h1, style: style, ); + + factory BrandText.onboardingTitle(String text, {TextStyle style}) => + BrandText( + text, + type: TextType.onboardingTitle, + style: style, + ); factory BrandText.h2(String text, {TextStyle style}) => BrandText( text, type: TextType.h2, @@ -63,6 +73,10 @@ class BrandText extends StatelessWidget { type: TextType.body2, style: style, ); + factory BrandText.medium(String text, + {TextStyle style, TextAlign textAlign}) => + BrandText(text, + type: TextType.medium, style: style, textAlign: textAlign); factory BrandText.small(String text, {TextStyle style}) => BrandText( text, type: TextType.small, @@ -105,6 +119,15 @@ class BrandText extends StatelessWidget { case TextType.small: style = isDark ? smallStyle.copyWith(color: Colors.white) : smallStyle; break; + case TextType.onboardingTitle: + style = isDark + ? onboardingTitle.copyWith(color: Colors.white) + : onboardingTitle; + break; + case TextType.medium: + style = + isDark ? mediumStyle.copyWith(color: Colors.white) : mediumStyle; + break; } if (this.style != null) { style = style.merge(this.style); @@ -114,6 +137,7 @@ class BrandText extends StatelessWidget { style: style, overflow: overflow, softWrap: softWrap, + textAlign: textAlign, ); } } diff --git a/lib/ui/components/icon_status_mask/icon_status_mask.dart b/lib/ui/components/icon_status_mask/icon_status_mask.dart index d81643cf..f02167da 100644 --- a/lib/ui/components/icon_status_mask/icon_status_mask.dart +++ b/lib/ui/components/icon_status_mask/icon_status_mask.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; -class IconStatusMaks extends StatelessWidget { - IconStatusMaks({this.child, this.status}); +class IconStatusMask extends StatelessWidget { + IconStatusMask({this.child, this.status}); final Icon child; final StateType status; diff --git a/lib/ui/components/not_ready_card/not_ready_card.dart b/lib/ui/components/not_ready_card/not_ready_card.dart new file mode 100644 index 00000000..94ab541c --- /dev/null +++ b/lib/ui/components/not_ready_card/not_ready_card.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/brand_colors.dart'; + +class NotReadyCard extends StatelessWidget { + const NotReadyCard({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 15, vertical: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), color: BrandColors.gray6), + child: Text( + 'Завершите настройку приложения используя "Мастер подключения" для продолжения работы', + style: TextStyle(color: BrandColors.white), + ), + ); + } +} diff --git a/lib/ui/components/progress_bar/progress_bar.dart b/lib/ui/components/progress_bar/progress_bar.dart index ed3fc7ea..00f196cc 100644 --- a/lib/ui/components/progress_bar/progress_bar.dart +++ b/lib/ui/components/progress_bar/progress_bar.dart @@ -24,54 +24,37 @@ class _ProgressBarState extends State { @override Widget build(BuildContext context) { double progress = 1 / widget.steps.length * (widget.activeIndex + 0.3); + var isDark = context.watch().state.isDarkModeOn; + var style = isDark ? progressTextStyleDark : progressTextStyleLight; + + var allSteps = widget.steps.asMap().map( + (i, step) { + var value = _stepTitle(index: i, style: style, step: step); + return MapEntry(i, value); + }, + ).values; + + List odd = []; + List even = []; + + var i = 0; + for (var step in allSteps) { + if (i.isEven) { + even.add(step); + } else { + odd.add(step); + } + i++; + } + even.add(Spacer()); + odd.insert(0, Spacer()); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - BrandText.h4('Progress'), + BrandText.h2('Progress'), SizedBox(height: 10), - Row( - children: widget.steps - .asMap() - .map( - (i, step) { - var isActive = i == widget.activeIndex; - var checked = i < widget.activeIndex; - - var isDark = - context.watch().state.isDarkModeOn; - var style = - isDark ? progressTextStyleDark : progressTextStyleLight; - - style = isActive - ? style.copyWith(fontWeight: FontWeight.w700) - : style; - return MapEntry( - i, - Expanded( - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: progressTextStyleLight, - children: [ - checked - ? WidgetSpan( - child: Padding( - padding: const EdgeInsets.only( - bottom: 1, right: 2), - child: Icon(BrandIcons.check, size: 14), - )) - : TextSpan(text: '${i + 1}.', style: style), - TextSpan(text: step, style: style) - ], - ), - ), - )); - }, - ) - .values - .toList(), - ), + Row(children: even), SizedBox(height: 3), Container( alignment: Alignment.centerLeft, @@ -98,8 +81,42 @@ class _ProgressBarState extends State { ); }, ), - ) + ), + SizedBox(height: 3), + Row( + children: odd, + ), ], ); } + + Expanded _stepTitle({ + int index, + TextStyle style, + String step, + }) { + var isActive = index == widget.activeIndex; + var checked = index < widget.activeIndex; + + style = isActive ? style.copyWith(fontWeight: FontWeight.w700) : style; + return Expanded( + flex: 2, + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: progressTextStyleLight, + children: [ + checked + ? WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 1, right: 2), + child: Icon(BrandIcons.check, size: 14), + )) + : TextSpan(text: '${index + 1}.', style: style), + TextSpan(text: step, style: style) + ], + ), + ), + ); + } } diff --git a/lib/ui/pages/initializing/initializing.dart b/lib/ui/pages/initializing/initializing.dart index 4e8448ee..e2e68758 100644 --- a/lib/ui/pages/initializing/initializing.dart +++ b/lib/ui/pages/initializing/initializing.dart @@ -3,10 +3,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/text_themes.dart'; +import 'package:selfprivacy/logic/cubit/forms/initializing/cloudflare_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/initializing/domain_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/initializing/hetzner_form_cubit.dart'; -import 'package:selfprivacy/logic/cubit/initializing/initializing_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/initializing/user_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart'; -import 'package:selfprivacy/logic/models/user.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_card/brand_card.dart'; import 'package:selfprivacy/ui/components/brand_modal_sheet/brand_modal_sheet.dart'; @@ -16,96 +18,71 @@ import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; -class InitializingPage extends StatefulWidget { - const InitializingPage({Key key}) : super(key: key); - - @override - _InitializingPageState createState() => _InitializingPageState(); -} - -class _InitializingPageState extends State { - PageController pageController = PageController(viewportFraction: 1); - - @override - void dispose() { - pageController.dispose(); - super.dispose(); - } - +class InitializingPage extends StatelessWidget { @override Widget build(BuildContext context) { - var cubit = context.watch(); - - return SafeArea( - child: Scaffold( - body: ListView( - children: [ - Padding( - padding: brandPagePadding1, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BrandText.h4('Начало'), - BrandText.h1('SelfPrivacy'), - SizedBox( - height: 10, - ), - RichText( - text: TextSpan( - children: [ - TextSpan( - text: - 'Для устойчивости и приватности требует много учёток. Полная инструкция на ', - style: body2Style, - ), - BrandSpanButton.link( - text: - 'https://selfprivacy.org/posts/getting_started/', - urlString: - 'https://selfprivacy.org/posts/getting_started/', - ), + var cubit = context.watch(); + var actualPage = [ + _stepHetzner(cubit), + _stepCloudflare(cubit), + _stepDomain(cubit), + _stepUser(cubit), + _stepServer(cubit), + Container(child: Text('Everythigng is initialized')) + ][cubit.state.progress]; + return BlocListener( + listener: (context, state) { + if (state.isFullyInitilized) { + Navigator.of(context).pushReplacement(materialRoute(RootPage())); + } + }, + child: SafeArea( + child: Scaffold( + body: ListView( + children: [ + Padding( + padding: brandPagePadding1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProgressBar( + steps: [ + 'Hetzner', + 'CloudFlare', + 'Domain', + 'User', + 'Server', + 'Check' ], + activeIndex: cubit.state.progress, ), - ), - SizedBox(height: 10), - ProgressBar( - steps: ['Server', 'DNS', 'Domain', 'User'], - // progress: cubit.state.progress, - activeIndex: cubit.state.progress, - ), - SizedBox(height: 20), - ], + ], + ), ), - ), - Container( - height: 500, - child: PageView( - // physics: NeverScrollableScrollPhysics(), - controller: pageController, - children: [ - _addCard(_stepOne(cubit)), - _addCard(_stepTwo(cubit)), - _addCard(_stepThree(cubit)), - _addCard(_stepFour(cubit)), - ], + _addCard( + AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: actualPage, + ), ), - ), - BrandButton.text(title: 'Настрою потом', onPressed: _goToMainPage), - SizedBox(height: 30), - ], + BrandButton.text( + title: + cubit.state.isFullyInitilized ? 'Close' : 'Настрою потом', + onPressed: () { + Navigator.of(context).pushAndRemoveUntil( + materialRoute(RootPage()), + (predicate) => predicate == null, + ); + }), + SizedBox(height: 30), + ], + ), ), ), ); } - void _goToMainPage() { - Navigator.of(context).pushAndRemoveUntil( - materialRoute(RootPage()), - (predicate) => predicate == null, - ); - } - - Widget _stepOne(InitializingCubit initializingCubit) { + Widget _stepHetzner(AppConfigCubit initializingCubit) { return BlocProvider( create: (context) => HetznerFormCubit(initializingCubit), child: Builder(builder: (context) { @@ -124,19 +101,17 @@ class _InitializingPageState extends State { CubitFormTextField( formFieldCubit: formCubit.apiKey, textAlign: TextAlign.center, - keyboardType: TextInputType.number, scrollPadding: EdgeInsets.only(bottom: 70), decoration: InputDecoration( hintText: 'Hetzner API Token', ), ), - SizedBox(height: 20), + Spacer(), BrandButton.rised( onPressed: formCubit.state.isSubmitting ? null : formCubit.trySubmit, title: 'Подключить', ), - Spacer(), SizedBox(height: 10), BrandButton.text( onPressed: () => _showModal(context, _HowHetzner()), @@ -159,110 +134,161 @@ class _InitializingPageState extends State { ); } - Widget _stepTwo(InitializingCubit cubit) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset('assets/images/logos/cloudflare.png'), - BrandText.h2('Подключите CloudFlare DNS'), - SizedBox(height: 10), - BrandText.body2('Для управления DNS вашего домена'), - Expanded( - child: _MockForm( - hintText: 'CloudFlare API Token', - length: 64, - onPressed: () { - cubit.setCloudFlare('key'); - pageController.animateToPage( - 2, - curve: Curves.easeIn, - duration: Duration(milliseconds: 200), - ); - }, - ), - ), - SizedBox(height: 20), - BrandButton.text( - onPressed: () {}, - title: 'Как получить API Token', - ), - ], + Widget _stepCloudflare(AppConfigCubit initializingCubit) { + return BlocProvider( + create: (context) => CloudFlareFormCubit(initializingCubit), + child: Builder(builder: (context) { + var formCubit = context.watch(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Spacer(), + Image.asset('assets/images/logos/cloudflare.png'), + BrandText.h2('Подключите CloudFlare'), + SizedBox(height: 10), + BrandText.body2('Для управления DNS вашего домена'), + Spacer(), + CubitFormTextField( + formFieldCubit: formCubit.apiKey, + textAlign: TextAlign.center, + scrollPadding: EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'CloudFlare API Token', + ), + ), + Spacer(), + BrandButton.rised( + onPressed: + formCubit.state.isSubmitting ? null : formCubit.trySubmit, + title: 'Подключить', + ), + SizedBox(height: 10), + BrandButton.text( + onPressed: () {}, + title: 'Как получить API Token', + ), + ], + ); + }), ); } - Widget _stepThree(InitializingCubit cubit) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 10), - BrandText.h2('Введите домен:'), - Expanded( - child: _MockForm( - hintText: 'домен', - length: 10, - onPressed: () { - cubit.setDomain('domain'); - pageController.animateToPage( - 3, - curve: Curves.easeIn, - duration: Duration(milliseconds: 200), - ); - }, - ), - ), - SizedBox(height: 10), - BrandButton.text( - onPressed: () => _showModal(context, _HowHetzner()), - title: 'Как получить API Token', - ), - ], + Widget _stepDomain(AppConfigCubit initializingCubit) { + return BlocProvider( + create: (context) => DomainFormCubit(initializingCubit), + child: Builder(builder: (context) { + var formCubit = context.watch(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Spacer(), + BrandText.h2('Введите домен:'), + SizedBox(height: 10), + CubitFormTextField( + keyboardType: TextInputType.emailAddress, + formFieldCubit: formCubit.domainName, + textAlign: TextAlign.center, + scrollPadding: EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'Домен', + ), + ), + Spacer(), + BrandButton.rised( + onPressed: + formCubit.state.isSubmitting ? null : formCubit.trySubmit, + title: 'Подключить', + ), + SizedBox(height: 10), + BrandButton.text( + onPressed: () => _showModal(context, _HowHetzner()), + title: 'Как получить API Token', + ), + ], + ); + }), ); } - Widget _stepFour(InitializingCubit cubit) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 10), - Expanded( - child: Column( - children: [ - TextField( - decoration: InputDecoration( - hintText: 'нинейм', - ), + Widget _stepUser(AppConfigCubit initializingCubit) { + return BlocProvider( + create: (context) => UserFormCubit(initializingCubit), + child: Builder(builder: (context) { + var formCubit = context.watch(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Spacer(), + SizedBox(height: 10), + CubitFormTextField( + formFieldCubit: formCubit.userName, + textAlign: TextAlign.center, + scrollPadding: EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'Никнейм', ), - SizedBox(height: 10), - TextField( - obscureText: true, - decoration: InputDecoration( - hintText: 'пароль', - ), + ), + SizedBox(height: 10), + CubitFormTextField( + formFieldCubit: formCubit.password, + textAlign: TextAlign.center, + scrollPadding: EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'Пароль', ), - Spacer(), - BrandButton.rised( - onPressed: () { - cubit.setRootUser( - User(login: 'aa', password: 'bbb'), - ); - _goToMainPage(); - }, - title: 'some text', - ), - ], - ), - ), - SizedBox(height: 10), - BrandButton.text( - onPressed: () => _showModal(context, _HowHetzner()), - title: 'Как получить API Token', - ), - ], + ), + Spacer(), + BrandButton.rised( + onPressed: + formCubit.state.isSubmitting ? null : formCubit.trySubmit, + title: 'Подключить', + ), + SizedBox(height: 10), + BrandButton.text( + onPressed: () => _showModal(context, _HowHetzner()), + title: 'Как получить API Token', + ), + ], + ); + }), ); } + Widget _stepServer(AppConfigCubit appConfigCubit) { + var isLoading = appConfigCubit.state.isLoading; + return Builder(builder: (context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Spacer( + flex: 2, + ), + BrandText.h2('Создать сервер'), + SizedBox(height: 10), + BrandText.body2('Создать сервер'), + Spacer(), + BrandButton.rised( + onPressed: isLoading ? null : appConfigCubit.createServer, + title: isLoading ? 'loading' : 'Создать сервер', + ), + Spacer( + flex: 2, + ), + BrandButton.text( + onPressed: () => _showModal(context, _HowHetzner()), + title: 'Что это значит?', + ), + ], + ); + }); + } + Widget _addCard(Widget child) { - return Padding( + return Container( + height: 500, padding: brandPagePadding2, child: BrandCard( child: child, @@ -322,235 +348,3 @@ class _HowHetzner extends StatelessWidget { ); } } - -// class _MockSuccess extends StatelessWidget { -// const _MockSuccess({Key key, this.type}) : super(key: key); - -// final ProviderType type; - -// @override -// Widget build(BuildContext context) { -// String text; - -// switch (type) { -// case ProviderType.server: -// text = '1. Cервер подключен'; -// break; -// case ProviderType.domain: -// text = '2. Домен настроен'; -// break; -// case ProviderType.backup: -// text = '3. Резервное копирование настроенно'; -// break; -// } -// return BrandCard( -// child: Row( -// mainAxisAlignment: MainAxisAlignment.spaceBetween, -// children: [ -// BrandText.h3(text), -// Icon( -// Icons.check, -// color: BrandColors.green1, -// ), -// ], -// ), -// ); -// } -// } - -class _MockForm extends StatefulWidget { - const _MockForm({ - Key key, - @required this.hintText, - this.submitButtonText = 'Подключить', - @required this.onPressed, - @required this.length, - }) : super(key: key); - - final String hintText; - final String submitButtonText; - final int length; - - final VoidCallback onPressed; - - @override - __MockFormState createState() => __MockFormState(); -} - -class __MockFormState extends State<_MockForm> { - String text = ''; - bool _valid = true; - bool _touched = false; - - onPressed() { - if (text.length == widget.length) { - setState(() { - _touched = true; - _valid = true; - widget.onPressed(); - }); - } else { - setState(() { - _touched = true; - _valid = false; - }); - } - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 20), - TextField( - onChanged: (value) { - if (_touched) { - if (value.length == widget.length) { - setState(() { - _valid = true; - text = value; - }); - } else { - setState(() { - _valid = false; - text = value; - }); - } - } else { - setState(() { - text = value; - }); - } - }, - decoration: InputDecoration( - hintText: widget.hintText, - errorText: - _valid ? null : 'Длинна должна быть ${widget.length} символа', - ), - ), - Spacer(), - BrandButton.rised( - onPressed: _valid ? onPressed : null, - title: widget.submitButtonText, - ), - ], - ); - } -} - -// Widget getCard(BuildContext context, ProviderModel model) { -// var cubit = context.watch(); -// if (model.state == StateType.stable) { -// return _MockSuccess(type: model.type); -// } -// switch (model.type) { -// case ProviderType.server: -// return BrandCard( -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Image.asset('assets/images/logos/hetzner.png'), -// SizedBox(height: 10), -// BrandText.h2('1. Подключите сервер Hetzner'), -// SizedBox(height: 10), -// BrandText.body2( -// 'Здесь будут жить наши данные и SelfPrivacy-сервисы'), -// _MockForm( -// hintText: 'Hetzner API Token', -// length: 48, -// onPressed: () { -// var provider = cubit.state.all -// .firstWhere((p) => p.type == ProviderType.server); -// cubit.connect(provider); -// }, -// ), -// SizedBox(height: 20), -// BrandButton.text( -// onPressed: () => _showModal(context, _HowHetzner()), -// title: 'Как получить API Token', -// ), -// ], -// ), -// ); -// break; -// case ProviderType.domain: -// return BrandCard( -// isBlocked: true, -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Image.asset('assets/images/logos/namecheap.png'), -// SizedBox(height: 10), -// BrandText.h2('2. Настройте домен'), -// SizedBox(height: 10), -// RichText( -// text: TextSpan( -// children: [ -// TextSpan( -// text: 'Зарегистрируйте домен в ', -// style: body2Style, -// ), -// BrandSpanButton.link( -// text: 'NameCheap', -// urlString: 'https://www.namecheap.com', -// ), -// TextSpan( -// text: -// ' или у любого другого регистратора. После этого настройте его на DNS-сервер CloudFlare', -// style: body2Style, -// ), -// ], -// ), -// ), -// _MockForm( -// hintText: 'Домен, например, selfprivacy.org', -// submitButtonText: 'Проверить DNS', -// length: 2, -// onPressed: () {}, -// ), -// SizedBox(height: 20), -// BrandButton.text( -// onPressed: () {}, -// title: 'Как настроить DNS CloudFlare', -// ), -// SizedBox(height: 10), -// Image.asset('assets/images/logos/cloudflare.png'), -// SizedBox(height: 10), -// ], -// ), -// ); -// break; -// case ProviderType.backup: -// return BrandCard( -// isBlocked: true, -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Image.asset('assets/images/logos/aws.png'), -// SizedBox(height: 10), -// BrandText.h2('4. Подключите Amazon AWS для бекапа'), -// SizedBox(height: 10), -// BrandText.body2( -// 'IaaS-провайдер, для бесплатного хранения резервных копии ваших данных в зашифрованном виде'), -// _MockForm( -// hintText: 'Amazon AWS Access Key', -// length: 2, -// onPressed: () { -// var provider = cubit.state.all -// .firstWhere((p) => p.type == ProviderType.backup); -// cubit.connect(provider); -// }, -// ), -// SizedBox(height: 20), -// BrandButton.text( -// onPressed: () {}, -// title: 'Как получить API Token', -// ), -// ], -// ), -// ); -// } - -// return null; -// } diff --git a/lib/ui/pages/more/app_settings/app_setting.dart b/lib/ui/pages/more/app_settings/app_setting.dart index 7988ee00..2e6113f5 100644 --- a/lib/ui/pages/more/app_settings/app_setting.dart +++ b/lib/ui/pages/more/app_settings/app_setting.dart @@ -2,9 +2,11 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; +import 'package:selfprivacy/utils/named_font_weight.dart'; class AppSettingsPage extends StatefulWidget { const AppSettingsPage({Key key}) : super(key: key); @@ -53,11 +55,75 @@ class _AppSettingsPageState extends State { activeColor: BrandColors.green1, activeTrackColor: BrandColors.green2, value: Theme.of(context).brightness == Brightness.dark, - onChanged: (value) => - appSettings.update(isDarkModeOn: !isDarkModeOn), + onChanged: (value) => appSettings.updateDarkMode( + isDarkModeOn: !isDarkModeOn), ), ], ), + ), + Container( + padding: EdgeInsets.only(top: 20, bottom: 5), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(width: 1, color: BrandColors.dividerColor), + )), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: _TextColumn( + title: 'Reset app config', + value: 'Reset api keys and root user', + ), + ), + SizedBox(width: 5), + RaisedButton( + color: BrandColors.red1, + child: Text( + 'Reset', + style: TextStyle( + color: BrandColors.white, + fontWeight: NamedFontWeight.demiBold, + ), + ), + onPressed: () { + showDialog( + context: context, + child: AlertDialog( + title: Text('Are you sure?'), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text('Reset all keys?'), + ], + ), + ), + actions: [ + TextButton( + child: Text( + 'Reset', + style: TextStyle( + color: BrandColors.red1, + ), + ), + onPressed: () { + context.read().reset(); + Navigator.of(context)..pop()..pop(); + }, + ), + TextButton( + child: Text('Cancel'), + onPressed: () { + Navigator.of(context)..pop(); + }, + ), + ], + )); + }, + ) + ], + ), ) ], ), @@ -88,10 +154,6 @@ class _TextColumn extends StatelessWidget { style: TextStyle(color: hasWarning ? BrandColors.warning : null), ), SizedBox(height: 5), - BrandText.body1( - title, - style: TextStyle(color: hasWarning ? BrandColors.warning : null), - ), BrandText.body1(value, style: TextStyle( fontSize: 13, diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 6a712b54..253a41ea 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -6,6 +6,8 @@ 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_text/brand_text.dart'; import 'package:selfprivacy/ui/pages/initializing/initializing.dart'; +import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; +import 'package:selfprivacy/ui/pages/rootRoute.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'about/about.dart'; @@ -49,6 +51,11 @@ class MorePage extends StatelessWidget { iconData: BrandIcons.help, goTo: InfoPage(), ), + _NavItem( + title: 'Onboarding', + iconData: BrandIcons.triangle, + goTo: OnboardingPage(nextPage: RootPage()), + ), ], ), ) diff --git a/lib/ui/pages/onboarding/onboarding.dart b/lib/ui/pages/onboarding/onboarding.dart index 2b10a335..e7205814 100644 --- a/lib/ui/pages/onboarding/onboarding.dart +++ b/lib/ui/pages/onboarding/onboarding.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; -import 'package:selfprivacy/ui/pages/initializing/initializing.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; class OnboardingPage extends StatefulWidget { - const OnboardingPage({Key key}) : super(key: key); + const OnboardingPage({Key key, @required this.nextPage}) : super(key: key); + final Widget nextPage; @override _OnboardingPageState createState() => _OnboardingPageState(); } @@ -107,8 +108,9 @@ class _OnboardingPageState extends State { ), BrandButton.rised( onPressed: () { + context.read().turnOffOnboarding(); Navigator.of(context) - .pushReplacement(materialRoute(InitializingPage())); + .pushReplacement(materialRoute(widget.nextPage)); }, title: 'Понял', ), diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index dc9552a3..40743943 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/logic/cubit/initializing/initializing_cubit.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart'; import 'package:selfprivacy/logic/models/provider.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; @@ -9,6 +9,7 @@ import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_modal_sheet/brand_modal_sheet.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:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; import 'package:selfprivacy/ui/pages/providers/settings/settings.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; @@ -22,9 +23,12 @@ class ProvidersPage extends StatefulWidget { class _ProvidersPageState extends State { @override Widget build(BuildContext context) { + var isReady = context.watch().state.isFullyInitilized; + final cards = ProviderType.values - .map((type) => - _Card(provider: ProviderModel(state: StateType.stable, type: type))) + .map((type) => _Card( + provider: + ProviderModel(state: StateType.uninitialized, type: type))) .toList(); return Scaffold( appBar: PreferredSize( @@ -33,7 +37,13 @@ class _ProvidersPageState extends State { ), body: ListView( padding: brandPagePadding2, - children: cards, + children: [ + if (!isReady) ...[ + NotReadyCard(), + SizedBox(height: 24), + ], + ...cards, + ], ), ); } @@ -48,8 +58,6 @@ class _Card extends StatelessWidget { String title; String message; String stableText; - var isFullyInitilized = - context.watch().state.isFullyInitilized; switch (provider.type) { case ProviderType.server: @@ -80,11 +88,10 @@ class _Card extends StatelessWidget { }, ), child: BrandCard( - isBlocked: !isFullyInitilized, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - IconStatusMaks( + IconStatusMask( status: provider.state, child: Icon(provider.icon, size: 30, color: Colors.white), ), @@ -176,7 +183,7 @@ class _ProviderDetails extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 13), - IconStatusMaks( + IconStatusMask( status: provider.state, child: Icon(provider.icon, size: 40, color: Colors.white), diff --git a/lib/ui/pages/rootRoute.dart b/lib/ui/pages/rootRoute.dart index 949180b4..8182a5c8 100644 --- a/lib/ui/pages/rootRoute.dart +++ b/lib/ui/pages/rootRoute.dart @@ -1,9 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/initializing/initializing_cubit.dart'; -import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart'; import 'package:selfprivacy/ui/components/brand_tab_bar/brand_tab_bar.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/pages/more/more.dart'; import 'package:selfprivacy/ui/pages/providers/providers.dart'; import 'package:selfprivacy/ui/pages/services/services.dart'; @@ -34,9 +31,6 @@ class _RootPageState extends State @override Widget build(BuildContext context) { - var isUserFilled = - context.watch().state.isFullyInitilized; - return SafeArea( child: Scaffold( body: TabBarView( @@ -44,7 +38,7 @@ class _RootPageState extends State children: [ ProvidersPage(), ServicesPage(), - isUserFilled ? UsersPage() : _NotReady(), + UsersPage(), MorePage(), ], ), @@ -55,20 +49,3 @@ class _RootPageState extends State ); } } - -class _NotReady extends StatelessWidget { - const _NotReady({Key key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - BrandText.h3('Not ready'), - BrandText.body2('Finish providers initialization first'), - ], - ), - ); - } -} diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index 8fde4106..f92c07e4 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/logic/cubit/initializing/initializing_cubit.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/models/service.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; @@ -9,6 +9,7 @@ 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_text/brand_text.dart'; import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart'; +import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; class ServicesPage extends StatefulWidget { ServicesPage({Key key}) : super(key: key); @@ -23,6 +24,8 @@ class _ServicesPageState extends State { final serviceCubit = context.watch(); final connected = serviceCubit.state.connected; final uninitialized = serviceCubit.state.uninitialized; + var isReady = context.watch().state.isFullyInitilized; + return Scaffold( appBar: PreferredSize( child: BrandHeader(title: 'Сервисы'), @@ -31,6 +34,7 @@ class _ServicesPageState extends State { body: ListView( padding: brandPagePadding2, children: [ + if (!isReady) NotReadyCard(), SizedBox(height: 24), ...connected.map((service) => _Card(service: service)).toList(), if (uninitialized.isNotEmpty) ...[ @@ -53,7 +57,6 @@ class _Card extends StatelessWidget { String title; IconData iconData; String description; - var isFullyInitilized = context.watch().state.isFullyInitilized; switch (service.type) { case ServiceTypes.messanger: @@ -85,11 +88,10 @@ class _Card extends StatelessWidget { break; } return BrandCard( - isBlocked: !isFullyInitilized, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - IconStatusMaks( + IconStatusMask( status: service.state, child: Icon(iconData, size: 30, color: Colors.white), ), diff --git a/lib/ui/pages/users/empty.dart b/lib/ui/pages/users/empty.dart new file mode 100644 index 00000000..8f89cc6a --- /dev/null +++ b/lib/ui/pages/users/empty.dart @@ -0,0 +1,37 @@ +part of 'users.dart'; + +class _NoUsers extends StatelessWidget { + const _NoUsers({Key key, @required this.text}) + : assert(text != null), + super(key: key); + + final String text; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(BrandIcons.users, size: 50, color: BrandColors.grey7), + SizedBox(height: 20), + BrandText.h2( + 'Здесь пока никого', + style: TextStyle( + color: BrandColors.grey7, + ), + ), + SizedBox(height: 10), + BrandText.medium( + text, + textAlign: TextAlign.center, + style: TextStyle( + color: BrandColors.grey7, + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/pages/users/fab.dart b/lib/ui/pages/users/fab.dart new file mode 100644 index 00000000..7aa72227 --- /dev/null +++ b/lib/ui/pages/users/fab.dart @@ -0,0 +1,34 @@ +part of 'users.dart'; + +class _Fab extends StatelessWidget { + const _Fab({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: 48.0, + height: 48.0, + child: RawMaterialButton( + fillColor: BrandColors.blue, + shape: CircleBorder(), + elevation: 0.0, + highlightElevation: 2, + child: Icon( + Icons.add, + color: Colors.white, + size: 34, + ), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return _NewUser(); + }, + ); + }, + ), + ); + } +} diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart new file mode 100644 index 00000000..d76715c5 --- /dev/null +++ b/lib/ui/pages/users/new_user.dart @@ -0,0 +1,74 @@ +part of 'users.dart'; + +class _NewUser extends StatefulWidget { + const _NewUser({Key key}) : super(key: key); + + @override + __NewUserState createState() => __NewUserState(); +} + +class __NewUserState extends State<_NewUser> { + var passController = TextEditingController(text: genPass()); + + @override + Widget build(BuildContext context) { + // final usersCubit = context.watch(); + + return BrandModalSheet( + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BrandHeader(title: 'Новый пользователь'), + SizedBox(width: 14), + Padding( + padding: brandPagePadding2, + child: Column( + children: [ + TextField( + decoration: InputDecoration( + labelText: 'Логин', + suffixText: '@example', + ), + ), + SizedBox(height: 20), + TextField( + controller: passController, + decoration: InputDecoration( + alignLabelWithHint: false, + labelText: 'Пароль', + suffixIcon: Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + icon: Icon( + BrandIcons.refresh, + color: BrandColors.blue, + ), + onPressed: () { + passController.value = + TextEditingValue(text: genPass()); + }, + ), + ), + ), + ), + SizedBox(height: 30), + BrandButton.rised( + onPressed: () { + Navigator.pop(context); + }, + title: 'Создать', + ), + SizedBox(height: 40), + Text( + 'Новый пользователь автоматически получит доступ ко всем сервисам. Ещё какое-то описание.'), + SizedBox(height: 30), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/pages/users/user.dart b/lib/ui/pages/users/user.dart new file mode 100644 index 00000000..8084969d --- /dev/null +++ b/lib/ui/pages/users/user.dart @@ -0,0 +1,40 @@ +part of 'users.dart'; + +class _User extends StatelessWidget { + const _User({Key key, this.user}) : super(key: key); + + final User user; + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return _UserDetails(user: user); + }, + ); + }, + child: Container( + padding: brandPagePadding2, + height: 48, + child: Row( + children: [ + Container( + width: 17, + height: 17, + decoration: BoxDecoration( + color: user.color, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 20), + BrandText.h4(user.login), + ], + ), + ), + ); + } +} diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart new file mode 100644 index 00000000..6311fc4d --- /dev/null +++ b/lib/ui/pages/users/user_details.dart @@ -0,0 +1,159 @@ +part of 'users.dart'; + +class _UserDetails extends StatelessWidget { + const _UserDetails({ + Key key, + this.user, + }) : super(key: key); + + final User user; + + @override + Widget build(BuildContext context) { + return BrandModalSheet( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 200, + decoration: BoxDecoration( + color: user.color, + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 4, + horizontal: 2, + ), + child: PopupMenuButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + onSelected: (PopupMenuItemType result) { + switch (result) { + case PopupMenuItemType.reset: + break; + case PopupMenuItemType.delete: + showDialog( + context: context, + child: AlertDialog( + title: Text('Подтверждение '), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text('удалить учетную запись?'), + ], + ), + ), + actions: [ + TextButton( + child: Text('Отменить'), + onPressed: () { + Navigator.of(context)..pop(); + }, + ), + TextButton( + child: Text( + 'Удалить', + style: TextStyle( + color: BrandColors.red1, + ), + ), + onPressed: () { + Navigator.of(context)..pop()..pop(); + }, + ), + ], + )); + break; + } + }, + icon: Icon(Icons.more_vert), + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + value: PopupMenuItemType.reset, + child: Container( + padding: EdgeInsets.only(left: 5), + child: Text('Сбросить пароль'), + ), + ), + PopupMenuItem( + value: PopupMenuItemType.delete, + child: Container( + padding: EdgeInsets.only(left: 5), + child: Text( + 'Удалить', + style: TextStyle(color: BrandColors.red1), + ), + ), + ), + ], + ), + ), + ), + Spacer(), + Padding( + padding: EdgeInsets.symmetric( + vertical: 20, + horizontal: 15, + ), + child: BrandText.h1( + user.login, + softWrap: true, + overflow: TextOverflow.ellipsis, + )), + ], + ), + ), + SizedBox(height: 20), + Padding( + padding: brandPagePadding2.copyWith(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BrandText.small('Учетная запись'), + Container( + height: 40, + alignment: Alignment.centerLeft, + child: BrandText.h4('${user.login}@example.com'), + ), + SizedBox(height: 14), + BrandText.small('Пароль'), + Container( + height: 40, + alignment: Alignment.centerLeft, + child: BrandText.h4(user.password), + ), + SizedBox(height: 24), + BrandDivider(), + SizedBox(height: 20), + BrandButton.iconText( + title: 'Отправить реквизиты для входа', + icon: Icon(BrandIcons.share), + onPressed: () {}, + ), + SizedBox(height: 20), + BrandDivider(), + SizedBox(height: 20), + Text( + 'Вам был создан доступ к сервисам с логином и паролем к сервисам:- E-mail с адресом - Менеджер паролей: - Файловое облако: - Видеоконференция - Git сервер '), + ], + ), + ) + ], + ), + ); + } +} + +enum PopupMenuItemType { + reset, + delete, +} diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 5bfebb1c..809e6a70 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; import 'package:selfprivacy/logic/models/user.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; @@ -9,318 +10,74 @@ 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_modal_sheet/brand_modal_sheet.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; +import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; import 'package:selfprivacy/utils/password_generator.dart'; +part 'fab.dart'; +part 'new_user.dart'; +part 'user_details.dart'; +part 'user.dart'; +part 'empty.dart'; + class UsersPage extends StatelessWidget { const UsersPage({Key key}) : super(key: key); @override Widget build(BuildContext context) { final usersCubit = context.watch(); + var isReady = context.watch().state.isFullyInitilized; final users = usersCubit.state.users; + final isEmpty = usersCubit.state.isEmpty; + + Widget child; + + if (!isReady) { + child = isNotReady(); + } else { + child = isEmpty + ? Container( + alignment: Alignment.center, + child: _NoUsers( + text: 'Добавьте первого пользователя', + ), + ) + : ListView( + children: [ + ...users.map((user) => _User(user: user)), + ], + ); + } + return Scaffold( appBar: PreferredSize( child: BrandHeader(title: 'Пользователи'), preferredSize: Size.fromHeight(52), ), - floatingActionButton: Container( - width: 48.0, - height: 48.0, - child: RawMaterialButton( - fillColor: BrandColors.blue, - shape: CircleBorder(), - elevation: 0.0, - highlightElevation: 2, - child: Icon( - Icons.add, - color: Colors.white, - size: 34, - ), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return _NewUser(); - }, - ); - }, - ), - ), - body: ListView( - children: [ - ...users.map((user) => _User(user: user)), - ], - ), + floatingActionButton: isReady ? _Fab() : null, + body: child, ); } -} -class _User extends StatelessWidget { - const _User({Key key, this.user}) : super(key: key); - - final User user; - @override - Widget build(BuildContext context) { - return InkWell( - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return _UserDetails(user: user); - }, - ); - }, - child: Container( - padding: brandPagePadding2, - height: 48, - child: Row( - children: [ - Container( - width: 17, - height: 17, - decoration: BoxDecoration( - color: user.color, - shape: BoxShape.circle, - ), - ), - SizedBox(width: 20), - BrandText.h4(user.login), - ], + Widget isNotReady() { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: NotReadyCard(), ), - ), - ); - } -} - -class _NewUser extends StatefulWidget { - const _NewUser({Key key}) : super(key: key); - - @override - __NewUserState createState() => __NewUserState(); -} - -class __NewUserState extends State<_NewUser> { - var passController = TextEditingController(text: genPass()); - - @override - Widget build(BuildContext context) { - return BrandModalSheet( - child: Container( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BrandHeader(title: 'Новый пользователь'), - SizedBox(width: 14), - Padding( - padding: brandPagePadding2, - child: Column( - children: [ - TextField( - decoration: InputDecoration( - labelText: 'Логин', - suffixText: '@example', - ), - ), - SizedBox(height: 20), - TextField( - controller: passController, - decoration: InputDecoration( - alignLabelWithHint: false, - labelText: 'Пароль', - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 8), - child: IconButton( - icon: Icon( - BrandIcons.refresh, - color: BrandColors.blue, - ), - onPressed: () { - passController.value = - TextEditingValue(text: genPass()); - }, - ), - ), - ), - ), - SizedBox(height: 30), - BrandButton.rised( - onPressed: () { - Navigator.pop(context); - }, - title: 'Создать', - ), - SizedBox(height: 40), - Text( - 'Новый пользователь автоматически получит доступ ко всем сервисам. Ещё какое-то описание.'), - SizedBox(height: 30), - ], + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Center( + child: _NoUsers( + text: + 'Подключите сервер, домен и DNS в разеде Провайдеры, чтобы добавить первого пользователя', ), ), - ], - ), - ), - ); - } -} - -class _UserDetails extends StatelessWidget { - const _UserDetails({ - Key key, - this.user, - }) : super(key: key); - - final User user; - - @override - Widget build(BuildContext context) { - return BrandModalSheet( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 200, - decoration: BoxDecoration( - color: user.color, - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: 2, - ), - child: PopupMenuButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - onSelected: (PopupMenuItemType result) { - switch (result) { - case PopupMenuItemType.reset: - break; - case PopupMenuItemType.delete: - showDialog( - context: context, - child: AlertDialog( - title: Text('Подтверждение '), - content: SingleChildScrollView( - child: ListBody( - children: [ - Text('удалить учетную запись?'), - ], - ), - ), - actions: [ - TextButton( - child: Text('Отменить'), - onPressed: () { - Navigator.of(context)..pop(); - }, - ), - TextButton( - child: Text( - 'Удалить', - style: TextStyle( - color: BrandColors.red, - ), - ), - onPressed: () { - Navigator.of(context)..pop()..pop(); - }, - ), - ], - )); - break; - } - }, - icon: Icon(Icons.more_vert), - itemBuilder: (BuildContext context) => [ - PopupMenuItem( - value: PopupMenuItemType.reset, - child: Container( - padding: EdgeInsets.only(left: 5), - child: Text('Сбросить пароль'), - ), - ), - PopupMenuItem( - value: PopupMenuItemType.delete, - child: Container( - padding: EdgeInsets.only(left: 5), - child: Text( - 'Удалить', - style: TextStyle(color: BrandColors.red), - ), - ), - ), - ], - ), - ), - ), - Spacer(), - Padding( - padding: EdgeInsets.symmetric( - vertical: 20, - horizontal: 15, - ), - child: BrandText.h1( - user.login, - softWrap: true, - overflow: TextOverflow.ellipsis, - )), - ], - ), ), - SizedBox(height: 20), - Padding( - padding: brandPagePadding2.copyWith(bottom: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BrandText.small('Учетная запись'), - Container( - height: 40, - alignment: Alignment.centerLeft, - child: BrandText.h4('${user.login}@example.com'), - ), - SizedBox(height: 14), - BrandText.small('Пароль'), - Container( - height: 40, - alignment: Alignment.centerLeft, - child: BrandText.h4(user.password), - ), - SizedBox(height: 24), - BrandDivider(), - SizedBox(height: 20), - BrandButton.iconText( - title: 'Отправить реквизиты для входа', - icon: Icon(BrandIcons.share), - onPressed: () {}, - ), - SizedBox(height: 20), - BrandDivider(), - SizedBox(height: 20), - Text( - 'Вам был создан доступ к сервисам с логином и паролем к сервисам:- E-mail с адресом - Менеджер паролей: - Файловое облако: - Видеоконференция - Git сервер '), - ], - ), - ) - ], - ), + ) + ], ); } } - -enum PopupMenuItemType { - reset, - delete, -} diff --git a/lib/utils/crypto.dart b/lib/utils/crypto.dart new file mode 100644 index 00000000..092f1175 --- /dev/null +++ b/lib/utils/crypto.dart @@ -0,0 +1,10 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; + +String convertToSha512Hash(String text) { + var bytes = utf8.encode(text); + + var hash = sha512.convert(bytes); + return hash.toString(); +} diff --git a/pubspec.lock b/pubspec.lock index 82c5c198..58e73f3a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "14.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.41.1" archive: dependency: transitive description: @@ -36,6 +50,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0-nullsafety.1" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.5" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.11" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.5" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.2" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "7.1.0" characters: dependency: transitive description: @@ -50,6 +120,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0-nullsafety.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -57,6 +141,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0-nullsafety.1" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.0" collection: dependency: transitive description: @@ -72,7 +163,7 @@ packages: source: hosted version: "2.1.1" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto url: "https://pub.dartlang.org" @@ -84,7 +175,7 @@ packages: name: cubit_form url: "https://pub.dartlang.org" source: hosted - version: "0.0.14" + version: "0.0.15" cupertino_icons: dependency: "direct main" description: @@ -92,6 +183,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.10" + dartx: + dependency: transitive + description: + name: dartx + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.0" + dio: + dependency: "direct main" + description: + name: dio + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.10" easy_localization: dependency: "direct main" description: @@ -134,6 +246,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.2.1" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.11" flutter: dependency: "direct main" description: flutter @@ -158,6 +277,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.5" flutter_test: dependency: "direct dev" description: flutter @@ -168,6 +294,13 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" google_fonts: dependency: "direct main" description: @@ -175,6 +308,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + hive: + dependency: "direct main" + description: + name: hive + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4+1" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.2" http: dependency: transitive description: @@ -182,6 +343,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" http_parser: dependency: transitive description: @@ -203,6 +371,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.16.1" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.4" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.2" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.4" mask_text_input_formatter: dependency: transitive description: @@ -224,6 +427,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0-nullsafety.3" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7" nested: dependency: transitive description: @@ -231,6 +441,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.4" + node_interop: + dependency: transitive + description: + name: node_interop + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + node_io: + dependency: transitive + description: + name: node_io + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" package_info: dependency: "direct main" description: @@ -308,6 +539,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" process: dependency: transitive description: @@ -322,6 +560,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.3.2+2" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.7" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" shared_preferences: dependency: transitive description: @@ -364,6 +623,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1+3" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.9" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" shortuuid: dependency: transitive description: @@ -376,6 +649,13 @@ packages: description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.10+1" source_span: dependency: transitive description: @@ -397,6 +677,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0-nullsafety.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" string_scanner: dependency: transitive description: @@ -418,6 +705,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.19-nullsafety.2" + time: + dependency: transitive + description: + name: time + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+3" typed_data: dependency: transitive description: @@ -481,6 +782,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0-nullsafety.3" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+15" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" win32: dependency: transitive description: @@ -510,5 +825,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=2.10.0-110 <2.11.0" + dart: ">=2.10.0 <2.11.0" flutter: ">=1.22.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8c176b10..e7feef1c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,12 +9,18 @@ environment: dependencies: flutter: sdk: flutter - cubit_form: ^0.0.14 + crypto: ^2.1.5 + cubit_form: ^0.0.15 cupertino_icons: ^1.0.0 + dio: ^3.0.10 easy_localization: ^2.3.3 equatable: ^1.2.5 flutter_bloc: ^6.1.1 + flutter_secure_storage: ^3.3.5 google_fonts: ^1.1.1 + hive: ^1.4.4+1 + hive_flutter: ^0.3.1 + json_annotation: ^3.1.1 package_info: ^0.4.3+2 provider: ^4.3.2+2 url_launcher: ^5.7.10 @@ -22,12 +28,16 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + build_runner: ^1.10.11 flutter_launcher_icons: ^0.8.1 + hive_generator: ^0.8.2 + json_serializable: ^3.5.1 flutter_icons: android: "launcher_icon" ios: true - image_path: "assets/images/icon/logo.png" + image_path_android: "assets/images/icon/logo_android.png" + image_path_ios: "assets/images/icon/logo_ios.png" flutter: uses-material-design: true