diff --git a/android/app/build.gradle b/android/app/build.gradle index 5e265753..6c963e03 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7051d271..e4a41966 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> usersCubit..load(), lazy: false), BlocProvider(create: (_) => servicesCubit..load(), lazy: false), BlocProvider(create: (_) => backupsCubit..load(), lazy: false), + BlocProvider(create: (_) => dnsRecordsCubit..load()), BlocProvider( create: (_) => JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit), diff --git a/lib/config/brand_theme.dart b/lib/config/brand_theme.dart index 32b1b453..23755de3 100644 --- a/lib/config/brand_theme.dart +++ b/lib/config/brand_theme.dart @@ -3,7 +3,7 @@ import 'package:selfprivacy/config/text_themes.dart'; import 'brand_colors.dart'; -final ligtTheme = ThemeData( +final lightTheme = ThemeData( primaryColor: BrandColors.primary, fontFamily: 'Inter', brightness: Brightness.light, @@ -38,16 +38,20 @@ final ligtTheme = ThemeData( color: BrandColors.red1, ), ), + listTileTheme: ListTileThemeData( + minLeadingWidth: 24.0, + ), textTheme: TextTheme( headline1: headline1Style, headline2: headline2Style, - caption: headline4Style, + headline3: headline3Style, + headline4: headline4Style, bodyText1: body1Style, subtitle1: TextStyle(fontSize: 15, height: 1.6), // text input style ), ); -var darkTheme = ligtTheme.copyWith( +var darkTheme = lightTheme.copyWith( brightness: Brightness.dark, scaffoldBackgroundColor: Color(0xFF202120), iconTheme: IconThemeData(color: BrandColors.gray3), @@ -56,7 +60,8 @@ var darkTheme = ligtTheme.copyWith( textTheme: TextTheme( headline1: headline1Style.copyWith(color: BrandColors.white), headline2: headline2Style.copyWith(color: BrandColors.white), - caption: headline4Style.copyWith(color: BrandColors.white), + headline3: headline3Style.copyWith(color: BrandColors.white), + headline4: headline4Style.copyWith(color: BrandColors.white), bodyText1: body1Style.copyWith(color: BrandColors.white), subtitle1: TextStyle(fontSize: 15, height: 1.6), // text input style ), diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 26b5f995..e7ed84e3 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -2,7 +2,6 @@ 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/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/backblaze_credential.dart'; @@ -24,14 +23,14 @@ class HiveConfig { await Hive.openBox(BNames.users); await Hive.openBox(BNames.servicesState); - var cipher = HiveAesCipher(await getEncriptedKey(BNames.key)); + var cipher = HiveAesCipher(await getEncryptedKey(BNames.key)); await Hive.openBox(BNames.appConfig, encryptionCipher: cipher); - var sshCipher = HiveAesCipher(await getEncriptedKey(BNames.sshEnckey)); + var sshCipher = HiveAesCipher(await getEncryptedKey(BNames.sshEnckey)); await Hive.openBox(BNames.sshConfig, encryptionCipher: sshCipher); } - static Future getEncriptedKey(String encKey) async { + static Future getEncryptedKey(String encKey) async { final secureStorage = FlutterSecureStorage(); var hasEncryptionKey = await secureStorage.containsKey(key: encKey); if (!hasEncryptionKey) { @@ -49,6 +48,7 @@ class BNames { static String isDarkModeOn = 'isDarkModeOn'; static String isOnbordingShowing = 'isOnbordingShowing'; static String users = 'users'; + static String rootKeys = 'rootKeys'; static String appSettings = 'appSettings'; static String servicesState = 'servicesState'; diff --git a/lib/logic/api_maps/api_map.dart b/lib/logic/api_maps/api_map.dart index a818e38d..5e07d838 100644 --- a/lib/logic/api_maps/api_map.dart +++ b/lib/logic/api_maps/api_map.dart @@ -6,13 +6,12 @@ import 'package:dio/adapter.dart'; import 'package:dio/dio.dart'; import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/get_it/console.dart'; import 'package:selfprivacy/logic/models/message.dart'; abstract class ApiMap { Future getClient() async { var dio = Dio(await options); - if (hasLoger) { + if (hasLogger) { dio.interceptors.add(PrettyDioLogger()); } dio.interceptors.add(ConsoleInterceptor()); @@ -38,7 +37,7 @@ abstract class ApiMap { FutureOr get options; abstract final String rootAddress; - abstract final bool hasLoger; + abstract final bool hasLogger; abstract final bool isWithToken; ValidateStatus? validateStatus; diff --git a/lib/logic/api_maps/backblaze.dart b/lib/logic/api_maps/backblaze.dart index 695a2ec6..6a56788c 100644 --- a/lib/logic/api_maps/backblaze.dart +++ b/lib/logic/api_maps/backblaze.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/api_map.dart'; @@ -20,7 +21,7 @@ class BackblazeApplicationKey { } class BackblazeApi extends ApiMap { - BackblazeApi({this.hasLoger = false, this.isWithToken = true}); + BackblazeApi({this.hasLogger = false, this.isWithToken = true}); BaseOptions get options { var options = BaseOptions(baseUrl: rootAddress); @@ -142,7 +143,7 @@ class BackblazeApi extends ApiMap { } @override - bool hasLoger; + bool hasLogger; @override bool isWithToken; diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index 7dc9994d..5aaf9dc7 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/api_map.dart'; @@ -6,7 +7,7 @@ import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; import 'package:selfprivacy/logic/models/dns_records.dart'; class CloudflareApi extends ApiMap { - CloudflareApi({this.hasLoger = false, this.isWithToken = true}); + CloudflareApi({this.hasLogger = false, this.isWithToken = true}); BaseOptions get options { var options = BaseOptions(baseUrl: rootAddress); @@ -87,6 +88,36 @@ class CloudflareApi extends ApiMap { close(client); } + Future> getDnsRecords({ + required CloudFlareDomain cloudFlareDomain, + }) async { + var domainName = cloudFlareDomain.domainName; + var domainZoneId = cloudFlareDomain.zoneId; + + var url = '/zones/$domainZoneId/dns_records'; + + var client = await getClient(); + Response response = await client.get(url); + + List records = response.data['result'] ?? []; + var allRecords = []; + + for (var record in records) { + if (record['zone_name'] == domainName) { + allRecords.add(DnsRecord( + name: record['name'], + type: record['type'], + content: record['content'], + ttl: record['ttl'], + proxied: record['proxied'], + )); + } + } + + close(client); + return allRecords; + } + Future createMultipleDnsRecords({ String? ip4, required CloudFlareDomain cloudFlareDomain, @@ -113,33 +144,33 @@ class CloudflareApi extends ApiMap { close(client); } - List projectDnsRecords(String? domainName, String? ip4) { - var domainA = DnsRecords(type: 'A', name: domainName, content: ip4); + List projectDnsRecords(String? domainName, String? ip4) { + var domainA = DnsRecord(type: 'A', name: domainName, content: ip4); - var mx = DnsRecords(type: 'MX', name: '@', content: domainName); - 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 vpn = DnsRecords(type: 'A', name: 'vpn', content: ip4); + var mx = DnsRecord(type: 'MX', name: '@', content: domainName); + var apiA = DnsRecord(type: 'A', name: 'api', content: ip4); + var cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); + var gitA = DnsRecord(type: 'A', name: 'git', content: ip4); + var meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); + var passwordA = DnsRecord(type: 'A', name: 'password', content: ip4); + var socialA = DnsRecord(type: 'A', name: 'social', content: ip4); + var vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); - var txt1 = DnsRecords( + var txt1 = DnsRecord( type: 'TXT', name: '_dmarc', content: 'v=DMARC1; p=none', ttl: 18000, ); - var txt2 = DnsRecords( + var txt2 = DnsRecord( type: 'TXT', name: domainName, content: 'v=spf1 a mx ip4:$ip4 -all', ttl: 18000, ); - return [ + return [ domainA, apiA, cloudA, @@ -154,6 +185,27 @@ class CloudflareApi extends ApiMap { ]; } + Future setDkim( + String dkimRecordString, CloudFlareDomain cloudFlareDomain) async { + final domainZoneId = cloudFlareDomain.zoneId; + final url = '$rootAddress/zones/$domainZoneId/dns_records'; + + final dkimRecord = DnsRecord( + type: 'TXT', + name: 'selector._domainkey', + content: dkimRecordString, + ttl: 18000, + ); + + var client = await getClient(); + await client.post( + url, + data: dkimRecord.toJson(), + ); + + client.close(); + } + Future> domainList() async { var url = '$rootAddress/zones?per_page=50'; var client = await getClient(); @@ -170,7 +222,7 @@ class CloudflareApi extends ApiMap { } @override - final bool hasLoger; + final bool hasLogger; @override final bool isWithToken; diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index f78595c1..1f906f85 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; @@ -10,10 +9,10 @@ import 'package:selfprivacy/logic/models/user.dart'; import 'package:selfprivacy/utils/password_generator.dart'; class HetznerApi extends ApiMap { - bool hasLoger; + bool hasLogger; bool isWithToken; - HetznerApi({this.hasLoger = false, this.isWithToken = true}); + HetznerApi({this.hasLogger = false, this.isWithToken = true}); BaseOptions get options { var options = BaseOptions(baseUrl: rootAddress); @@ -115,19 +114,50 @@ class HetznerApi extends ApiMap { final apiToken = StringGenerators.apiToken(); // Replace all non-alphanumeric characters with an underscore - final hostname = domainName.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); + var hostname = + domainName.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); + // if hostname ends with -, remove it + if (hostname.endsWith('-')) { + hostname = hostname.substring(0, hostname.length - 1); + } + // if hostname starts with -, remove it + if (hostname.startsWith('-')) { + hostname = hostname.substring(1); + } + // if hostname is empty, use default + if (hostname.isEmpty) { + hostname = 'selfprivacy-server'; + } + + print("hostname: $hostname"); /// add ssh key when you need it: e.g. "ssh_keys":["kherel"] /// check the branch name, it could be "development" or "master". + /// + final userdataString = + "#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${escapeQuotes(rootUser.login)}' PASSWORD='${escapeQuotes(rootUser.password ?? 'PASS')}' CF_TOKEN=$cloudFlareKey DB_PASSWORD=${escapeQuotes(dbPassword)} API_TOKEN=$apiToken HOSTNAME=${escapeQuotes(hostname)} bash 2>&1 | tee /tmp/infect.log"; + print(userdataString); - var data = jsonDecode( - '''{"name":"$domainName","server_type":"cx11","start_after_create":false,"image":"ubuntu-20.04", "volumes":[$dbId], "networks":[], "user_data":"#cloud-config\\nruncmd:\\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-21.05 DOMAIN=$domainName LUSER=${rootUser.login} PASSWORD=${rootUser.password} CF_TOKEN=$cloudFlareKey DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$hostname bash 2>&1 | tee /tmp/infect.log","labels":{},"automount":true, "location": "fsn1"}'''); + final data = { + "name": hostname, + "server_type": "cx11", + "start_after_create": false, + "image": "ubuntu-20.04", + "volumes": [dbId], + "networks": [], + "user_data": userdataString, + "labels": {}, + "automount": true, + "location": "fsn1" + }; + print("Decoded data: $data"); Response serverCreateResponse = await client.post( '/servers', data: data, ); + print(serverCreateResponse.data); client.close(); return HetznerServerDetails( id: serverCreateResponse.data['server']['id'], @@ -226,3 +256,14 @@ class HetznerApi extends ApiMap { close(client); } } + +String escapeQuotes(String str) { + // replace all single quotes with escaped single quotes for bash strong quotes (i.e. '\'' ) + // print("Escaping single quotes for bash: $str"); + // print("Escaping result: ${str.replaceAll(RegExp(r"'"), "'\\''")}"); + // also escape all double quotes for json + // return str.replaceAll(RegExp(r"'"), "'\\''"); + + // Pass for now + return str; +} diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 1792e2c1..0e8930e5 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -1,20 +1,37 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/backup.dart'; +import 'package:selfprivacy/logic/models/timezone_settings.dart'; import 'package:selfprivacy/logic/models/user.dart'; import 'api_map.dart'; +class ApiResponse { + final int statusCode; + final String? errorMessage; + final D data; + + get isSuccess => statusCode >= 200 && statusCode < 300; + + ApiResponse({ + required this.statusCode, + this.errorMessage, + required this.data, + }); +} + class ServerApi extends ApiMap { - bool hasLoger; + bool hasLogger; bool isWithToken; - ServerApi({this.hasLoger = false, this.isWithToken = true}); + ServerApi({this.hasLogger = false, this.isWithToken = true}); BaseOptions get options { var options = BaseOptions(); @@ -47,31 +64,152 @@ class ServerApi extends ApiMap { return res; } - Future createUser(User user) async { - bool res; + Future> createUser(User user) async { Response response; var client = await getClient(); // POST request with JSON body containing username and password - try { - response = await client.post( - '/users', - data: { - 'username': user.login, - 'password': user.password, - }, - options: Options( - contentType: 'application/json', + + response = await client.post( + '/users', + data: { + 'username': user.login, + 'password': user.password, + }, + options: Options( + contentType: 'application/json', + ), + ); + + close(client); + + if (response.statusCode == HttpStatus.created) { + return ApiResponse( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: User( + login: user.login, + password: user.password, + isFoundOnServer: true, ), ); - res = response.statusCode == HttpStatus.created; + } else { + return ApiResponse( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: User( + login: user.login, + password: user.password, + isFoundOnServer: false, + note: response.data['message'] ?? null, + ), + errorMessage: response.data?.containsKey('error') ?? false + ? response.data['error'] + : null, + ); + } + } + + Future>> getUsersList() async { + List res = []; + Response response; + + var client = await getClient(); + response = await client.get('/users'); + try { + for (var user in response.data) { + res.add(user.toString()); + } } catch (e) { print(e); - res = false; + res = []; } close(client); - return res; + return ApiResponse>( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: res, + ); + } + + Future> addUserSshKey(User user, String sshKey) async { + Response response; + + var client = await getClient(); + response = await client.post( + '/services/ssh/keys/${user.login}', + data: { + 'public_key': sshKey, + }, + ); + + close(client); + return ApiResponse( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: null, + errorMessage: response.data?.containsKey('error') ?? false + ? response.data['error'] + : null, + ); + } + + Future> addRootSshKey(String ssh) async { + Response response; + + var client = await getClient(); + response = await client.put( + '/services/ssh/key/send', + data: {"public_key": ssh}, + ); + close(client); + + return ApiResponse( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: null, + errorMessage: response.data?.containsKey('error') ?? false + ? response.data['error'] + : null, + ); + } + + Future>> getUserSshKeys(User user) async { + List res; + Response response; + + var client = await getClient(); + response = await client.get('/services/ssh/keys/${user.login}'); + try { + res = (response.data as List).map((e) => e as String).toList(); + } catch (e) { + print(e); + res = []; + } + + close(client); + return ApiResponse>( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: res, + errorMessage: response.data is List + ? null + : response.data?.containsKey('error') ?? false + ? response.data['error'] + : null, + ); + } + + Future> deleteUserSshKey(User user, String sshKey) async { + Response response; + + var client = await getClient(); + response = await client.delete('/services/ssh/keys/${user.login}', + data: {"public_key": sshKey}); + close(client); + + return ApiResponse( + statusCode: response.statusCode ?? HttpStatus.internalServerError, + data: null, + errorMessage: response.data?.containsKey('error') ?? false + ? response.data['error'] + : null, + ); } Future deleteUser(User user) async { @@ -86,7 +224,8 @@ class ServerApi extends ApiMap { contentType: 'application/json', ), ); - res = response.statusCode == HttpStatus.ok; + res = response.statusCode == HttpStatus.ok || + response.statusCode == HttpStatus.notFound; } catch (e) { print(e); res = false; @@ -122,16 +261,7 @@ class ServerApi extends ApiMap { Future switchService(ServiceTypes type, bool needToTurnOn) async { var client = await getClient(); client.post('/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}'); - client.close(); - } - - Future sendSsh(String ssh) async { - var client = await getClient(); - client.put( - '/services/ssh/key/send', - data: {"public_key": ssh}, - ); - client.close(); + close(client); } Future> servicesPowerCheck() async { @@ -158,13 +288,13 @@ class ServerApi extends ApiMap { 'bucket': bucket.bucketName, }, ); - client.close(); + close(client); } Future startBackup() async { var client = await getClient(); client.put('/services/restic/backup/create'); - client.close(); + close(client); } Future> getBackups() async { @@ -207,13 +337,13 @@ class ServerApi extends ApiMap { Future forceBackupListReload() async { var client = await getClient(); client.get('/services/restic/backup/reload'); - client.close(); + close(client); } Future restoreBackup(String backupId) async { var client = await getClient(); client.put('/services/restic/backup/restore', data: {'backupId': backupId}); - client.close(); + close(client); } Future pullConfigurationUpdate() async { @@ -226,27 +356,79 @@ class ServerApi extends ApiMap { Future reboot() async { var client = await getClient(); Response response = await client.get('/system/reboot'); - client.close(); + close(client); return response.statusCode == HttpStatus.ok; } Future upgrade() async { var client = await getClient(); Response response = await client.get('/system/configuration/upgrade'); - client.close(); + close(client); return response.statusCode == HttpStatus.ok; } + + Future getAutoUpgradeSettings() async { + var client = await getClient(); + Response response = await client.get('/system/configuration/autoUpgrade'); + close(client); + return AutoUpgradeSettings.fromJson(response.data); + } + + Future updateAutoUpgradeSettings(AutoUpgradeSettings settings) async { + var client = await getClient(); + await client.put( + '/system/configuration/autoUpgrade', + data: settings.toJson(), + ); + close(client); + } + + Future getServerTimezone() async { + var client = await getClient(); + Response response = await client.get('/system/configuration/timezone'); + close(client); + + return TimeZoneSettings.fromString(response.data); + } + + Future updateServerTimezone(TimeZoneSettings settings) async { + var client = await getClient(); + await client.put( + '/system/configuration/timezone', + data: settings.toJson(), + ); + close(client); + } + + Future getDkim() async { + var client = await getClient(); + Response response = await client.get('/services/mailserver/dkim'); + close(client); + + // if got 404 raise exception + if (response.statusCode == HttpStatus.notFound) { + throw Exception('No DKIM key found'); + } + + final base64toString = utf8.fuse(base64); + + return base64toString + .decode(response.data) + .split('(')[1] + .split(')')[0] + .replaceAll('"', ''); + } } extension UrlServerExt on ServiceTypes { String get url { switch (this) { // case ServiceTypes.mail: - // return ''; // cannot be swithch off + // return ''; // cannot be switch off // case ServiceTypes.messenger: // return ''; // external service // case ServiceTypes.video: - // return ''; // jeetsu meet not working + // return ''; // jitsi meet not working case ServiceTypes.passwordManager: return 'bitwarden'; case ServiceTypes.cloud: diff --git a/lib/logic/common_enum/common_enum.dart b/lib/logic/common_enum/common_enum.dart index 9903f304..8568c383 100644 --- a/lib/logic/common_enum/common_enum.dart +++ b/lib/logic/common_enum/common_enum.dart @@ -4,7 +4,7 @@ import 'package:ionicons/ionicons.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; enum InitializingSteps { - setHeznerKey, + setHetznerKey, setCloudFlareKey, setDomainName, setRootUser, diff --git a/lib/logic/cubit/app_config/app_config_cubit.dart b/lib/logic/cubit/app_config/app_config_cubit.dart index 8c881dc6..69259976 100644 --- a/lib/logic/cubit/app_config/app_config_cubit.dart +++ b/lib/logic/cubit/app_config/app_config_cubit.dart @@ -6,11 +6,11 @@ import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/get_it/ssh.dart'; import 'package:selfprivacy/logic/models/backblaze_credential.dart'; import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; - import 'package:selfprivacy/logic/models/server_details.dart'; import 'package:selfprivacy/logic/models/user.dart'; import 'app_config_repository.dart'; + export 'package:provider/provider.dart'; part 'app_config_state.dart'; @@ -83,9 +83,10 @@ class AppConfigCubit extends Cubit { var ip4 = state.hetznerServer!.ip4; var domainName = state.cloudFlareDomain!.domainName; - var isMatch = await repository.isDnsAddressesMatch(domainName, ip4); + var matches = await repository.isDnsAddressesMatch( + domainName, ip4, state.dnsMatches); - if (isMatch) { + if (matches.values.every((value) => value)) { var server = await repository.startServer( state.hetznerServer!, ); @@ -101,6 +102,12 @@ class AppConfigCubit extends Cubit { ); resetServerIfServerIsOkay(); } else { + emit( + state.copyWith( + isLoading: false, + dnsMatches: matches, + ), + ); startServerIfDnsIsOkay(); } }; @@ -108,7 +115,7 @@ class AppConfigCubit extends Cubit { if (isImmediate) { work(); } else { - var pauseDuration = Duration(seconds: 60); + var pauseDuration = Duration(seconds: 30); emit(TimerState( dataState: state, timerStart: DateTime.now(), @@ -239,6 +246,7 @@ class AppConfigCubit extends Cubit { var isServerWorking = await repository.isHttpServerWorking(); if (isServerWorking) { + await repository.createDkimRecord(state.cloudFlareDomain!); await repository.saveHasFinalChecked(true); emit(state.finish()); @@ -288,6 +296,7 @@ class AppConfigCubit extends Cubit { isServerResetedFirstTime: false, isServerResetedSecondTime: false, isLoading: false, + dnsMatches: null, )); } diff --git a/lib/logic/cubit/app_config/app_config_repository.dart b/lib/logic/cubit/app_config/app_config_repository.dart index 025c2f6d..76a63b25 100644 --- a/lib/logic/cubit/app_config/app_config_repository.dart +++ b/lib/logic/cubit/app_config/app_config_repository.dart @@ -1,20 +1,21 @@ +import 'package:basic_utils/basic_utils.dart'; import 'package:dio/dio.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:hive/hive.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/models/backblaze_credential.dart'; import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/logic/models/server_details.dart'; import 'package:selfprivacy/logic/models/user.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/models/message.dart'; -import 'package:basic_utils/basic_utils.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; + import 'app_config_cubit.dart'; -import 'package:easy_localization/easy_localization.dart'; class AppConfigRepository { Box box = Hive.box(BNames.appConfig); @@ -49,6 +50,7 @@ class AppConfigRepository { isServerResetedSecondTime: box.get(BNames.isServerResetedSecondTime, defaultValue: false), isLoading: box.get(BNames.isLoading, defaultValue: false), + dnsMatches: null, ); } @@ -68,7 +70,8 @@ class AppConfigRepository { return serverDetails; } - Future isDnsAddressesMatch(String? domainName, String? ip4) async { + Future> isDnsAddressesMatch(String? domainName, String? ip4, + Map? skippedMatches) async { var addresses = [ '$domainName', 'api.$domainName', @@ -77,7 +80,13 @@ class AppConfigRepository { 'password.$domainName' ]; + var matches = {}; + for (var address in addresses) { + if (skippedMatches != null && skippedMatches[address] == true) { + matches[address] = true; + continue; + } var lookupRecordRes = await DnsUtils.lookupRecord( address, RRecordType.A, @@ -98,11 +107,13 @@ class AppConfigRepository { if (lookupRecordRes == null || lookupRecordRes.isEmpty || lookupRecordRes[0].data != ip4) { - return false; + matches[address] = false; + } else { + matches[address] = true; } } - return true; + return matches; } Future createServer( @@ -189,9 +200,23 @@ class AppConfigRepository { ); } + Future createDkimRecord(CloudFlareDomain cloudFlareDomain) async { + var cloudflareApi = CloudflareApi(); + var api = ServerApi(); + + var dkimRecordString = await api.getDkim(); + + await cloudflareApi.setDkim(dkimRecordString, cloudFlareDomain); + } + Future isHttpServerWorking() async { var api = ServerApi(); var isHttpServerWorking = await api.isHttpServerWorking(); + try { + await api.getDkim(); + } catch (e) { + return false; + } return isHttpServerWorking; } diff --git a/lib/logic/cubit/app_config/app_config_state.dart b/lib/logic/cubit/app_config/app_config_state.dart index a2892b05..0608d903 100644 --- a/lib/logic/cubit/app_config/app_config_state.dart +++ b/lib/logic/cubit/app_config/app_config_state.dart @@ -89,6 +89,7 @@ class TimerState extends AppConfigNotFinished { isServerResetedFirstTime: dataState.isServerResetedFirstTime, isServerResetedSecondTime: dataState.isServerResetedSecondTime, isLoading: isLoading, + dnsMatches: dataState.dnsMatches, ); final AppConfigNotFinished dataState; @@ -105,6 +106,7 @@ class TimerState extends AppConfigNotFinished { class AppConfigNotFinished extends AppConfigState { final bool isLoading; + final Map? dnsMatches; AppConfigNotFinished({ String? hetznerKey, @@ -117,6 +119,7 @@ class AppConfigNotFinished extends AppConfigState { required bool isServerResetedFirstTime, required bool isServerResetedSecondTime, required this.isLoading, + required this.dnsMatches, }) : super( hetznerKey: hetznerKey, cloudFlareKey: cloudFlareKey, @@ -139,7 +142,8 @@ class AppConfigNotFinished extends AppConfigState { hetznerServer, isServerStarted, isServerResetedFirstTime, - isLoading + isLoading, + dnsMatches, ]; AppConfigNotFinished copyWith({ @@ -153,6 +157,7 @@ class AppConfigNotFinished extends AppConfigState { bool? isServerResetedFirstTime, bool? isServerResetedSecondTime, bool? isLoading, + Map? dnsMatches, }) => AppConfigNotFinished( hetznerKey: hetznerKey ?? this.hetznerKey, @@ -167,6 +172,7 @@ class AppConfigNotFinished extends AppConfigState { isServerResetedSecondTime: isServerResetedSecondTime ?? this.isServerResetedSecondTime, isLoading: isLoading ?? this.isLoading, + dnsMatches: dnsMatches ?? this.dnsMatches, ); AppConfigFinished finish() => AppConfigFinished( @@ -195,6 +201,7 @@ class AppConfigEmpty extends AppConfigNotFinished { isServerResetedFirstTime: false, isServerResetedSecondTime: false, isLoading: false, + dnsMatches: null, ); } diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index 4bee9ea8..e79978c2 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -1,12 +1,12 @@ import 'dart:async'; +import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/backblaze.dart'; +import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; import 'package:selfprivacy/logic/models/backup.dart'; -import 'package:selfprivacy/logic/api_maps/server.dart'; -import 'package:selfprivacy/logic/api_maps/backblaze.dart'; -import 'package:easy_localization/easy_localization.dart'; part 'backups_state.dart'; @@ -85,8 +85,8 @@ class BackupsCubit extends AppConfigDependendCubit { Future createBucket() async { emit(state.copyWith(preventActions: true)); - final domain = - appConfigCubit.state.cloudFlareDomain!.domainName.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); + final domain = appConfigCubit.state.cloudFlareDomain!.domainName + .replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); final serverId = appConfigCubit.state.hetznerServer!.id; var bucketName = 'selfprivacy-$domain-$serverId'; // If bucket name is too long, shorten it diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart new file mode 100644 index 00000000..227ac227 --- /dev/null +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -0,0 +1,184 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/dns_records.dart'; + +import '../../api_maps/cloudflare.dart'; +import '../../api_maps/server.dart'; + +part 'dns_records_state.dart'; + +class DnsRecordsCubit extends AppConfigDependendCubit { + DnsRecordsCubit(AppConfigCubit appConfigCubit) + : super(appConfigCubit, + DnsRecordsState(dnsState: DnsRecordsStatus.refreshing)); + + final api = ServerApi(); + final cloudflare = CloudflareApi(); + + Future load() async { + emit(DnsRecordsState( + dnsState: DnsRecordsStatus.refreshing, + dnsRecords: _getDesiredDnsRecords( + appConfigCubit.state.cloudFlareDomain?.domainName, "", ""))); + print('Loading DNS status'); + if (appConfigCubit.state is AppConfigFinished) { + final CloudFlareDomain? domain = appConfigCubit.state.cloudFlareDomain; + final String? ipAddress = appConfigCubit.state.hetznerServer?.ip4; + if (domain != null && ipAddress != null) { + final List records = + await cloudflare.getDnsRecords(cloudFlareDomain: domain); + final dkimPublicKey = await api.getDkim(); + final desiredRecords = + _getDesiredDnsRecords(domain.domainName, ipAddress, dkimPublicKey); + List foundRecords = []; + for (final record in desiredRecords) { + if (record.description == + 'providers.domain.record_description.dkim') { + final foundRecord = records.firstWhere( + (r) => r.name == record.name && r.type == record.type, + orElse: () => DnsRecord( + name: record.name, + type: record.type, + content: '', + ttl: 800, + proxied: false)); + // remove all spaces and tabulators from + // the foundRecord.content and the record.content + // to compare them + final foundContent = + foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); + final content = record.content.replaceAll(RegExp(r'\s+'), ''); + if (foundContent == content) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } else { + if (records.any((r) => + r.name == record.name && + r.type == record.type && + r.content == record.content)) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } + } + emit(DnsRecordsState( + dnsRecords: foundRecords, + dnsState: foundRecords.any((r) => r.isSatisfied == false) + ? DnsRecordsStatus.error + : DnsRecordsStatus.good, + )); + } else { + emit(DnsRecordsState()); + } + } + } + + @override + void onChange(Change change) { + // print(change); + super.onChange(change); + } + + @override + Future clear() async { + emit(DnsRecordsState(dnsState: DnsRecordsStatus.error)); + } + + Future refresh() async { + emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing)); + await load(); + } + + Future fix() async { + emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing)); + final CloudFlareDomain? domain = appConfigCubit.state.cloudFlareDomain; + final String? ipAddress = appConfigCubit.state.hetznerServer?.ip4; + final dkimPublicKey = await api.getDkim(); + await cloudflare.removeSimilarRecords(cloudFlareDomain: domain!); + await cloudflare.createMultipleDnsRecords( + cloudFlareDomain: domain, ip4: ipAddress); + await cloudflare.setDkim(dkimPublicKey, domain); + await load(); + } + + List _getDesiredDnsRecords( + String? domainName, String? ipAddress, String? dkimPublicKey) { + if (domainName == null || ipAddress == null || dkimPublicKey == null) { + return []; + } + return [ + DesiredDnsRecord( + name: domainName, + content: ipAddress, + description: 'providers.domain.record_description.root', + ), + DesiredDnsRecord( + name: 'api.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.api', + ), + DesiredDnsRecord( + name: 'cloud.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.cloud', + ), + DesiredDnsRecord( + name: 'git.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.git', + ), + DesiredDnsRecord( + name: 'meet.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.meet', + ), + DesiredDnsRecord( + name: 'social.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.social', + ), + DesiredDnsRecord( + name: 'password.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.password', + ), + DesiredDnsRecord( + name: 'vpn.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.vpn', + ), + DesiredDnsRecord( + name: domainName, + content: domainName, + description: 'providers.domain.record_description.mx', + type: 'MX', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: '_dmarc.$domainName', + content: 'v=DMARC1; p=none', + description: 'providers.domain.record_description.dmarc', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: domainName, + content: 'v=spf1 a mx ip4:$ipAddress -all', + description: 'providers.domain.record_description.spf', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: 'selector._domainkey.$domainName', + content: dkimPublicKey, + description: 'providers.domain.record_description.dkim', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + ]; + } +} diff --git a/lib/logic/cubit/dns_records/dns_records_state.dart b/lib/logic/cubit/dns_records/dns_records_state.dart new file mode 100644 index 00000000..dc594e74 --- /dev/null +++ b/lib/logic/cubit/dns_records/dns_records_state.dart @@ -0,0 +1,76 @@ +part of 'dns_records_cubit.dart'; + +enum DnsRecordsStatus { + uninitialized, + refreshing, + good, + error, +} + +enum DnsRecordsCategory { + services, + email, + other, +} + +class DnsRecordsState extends AppConfigDependendState { + const DnsRecordsState({ + this.dnsState = DnsRecordsStatus.uninitialized, + this.dnsRecords = const [], + }); + + final DnsRecordsStatus dnsState; + final List dnsRecords; + + @override + List get props => [ + dnsState, + dnsRecords, + ]; + + DnsRecordsState copyWith({ + DnsRecordsStatus? dnsState, + List? dnsRecords, + }) { + return DnsRecordsState( + dnsState: dnsState ?? this.dnsState, + dnsRecords: dnsRecords ?? this.dnsRecords, + ); + } +} + +class DesiredDnsRecord { + const DesiredDnsRecord({ + required this.name, + this.type = "A", + required this.content, + this.description = '', + this.category = DnsRecordsCategory.services, + this.isSatisfied = false, + }); + + final String name; + final String type; + final String content; + final String description; + final DnsRecordsCategory category; + final bool isSatisfied; + + DesiredDnsRecord copyWith({ + String? name, + String? type, + String? content, + String? description, + DnsRecordsCategory? category, + bool? isSatisfied, + }) { + return DesiredDnsRecord( + name: name ?? this.name, + type: type ?? this.type, + content: content ?? this.content, + description: description ?? this.description, + category: category ?? this.category, + isSatisfied: isSatisfied ?? this.isSatisfied, + ); + } +} diff --git a/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart b/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart index 2654caa9..7ee8c8fa 100644 --- a/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart @@ -1,10 +1,11 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; -import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; -import 'package:easy_localization/easy_localization.dart'; + +import '../validations/validations.dart'; class CloudFlareFormCubit extends FormCubit { CloudFlareFormCubit(this.initializingCubit) { @@ -15,8 +16,12 @@ class CloudFlareFormCubit extends FormCubit { RequiredStringValidation('validations.required'.tr()), ValidationModel( (s) => regExp.hasMatch(s), 'validations.key_format'.tr()), - LegnthStringValidationWithLenghShowing( - 40, 'validations.length'.tr(args: ["40"])) + LengthStringValidationWithLengthShowing( + 40, + 'validations.length'.tr( + args: ["40"], + ), + ) ], ); diff --git a/lib/logic/cubit/forms/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/initializing/domain_cloudflare.dart index 1551027c..78a244c8 100644 --- a/lib/logic/cubit/forms/initializing/domain_cloudflare.dart +++ b/lib/logic/cubit/forms/initializing/domain_cloudflare.dart @@ -42,7 +42,7 @@ class DomainSetupCubit extends Cubit { ); initializingCubit.setDomain(domain); - emit(DomainSetted()); + emit(DomainSet()); } } @@ -67,4 +67,4 @@ class Loaded extends DomainSetupState { Loaded(this.domain); } -class DomainSetted extends DomainSetupState {} +class DomainSet extends DomainSetupState {} diff --git a/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart index 26ee8593..55af50d9 100644 --- a/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart @@ -1,10 +1,11 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.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'; import 'package:easy_localization/easy_localization.dart'; +import 'package:selfprivacy/logic/api_maps/hetzner.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; + +import '../validations/validations.dart'; class HetznerFormCubit extends FormCubit { HetznerFormCubit(this.initializingCubit) { @@ -15,7 +16,7 @@ class HetznerFormCubit extends FormCubit { RequiredStringValidation('validations.required'.tr()), ValidationModel( (s) => regExp.hasMatch(s), 'validations.key_format'.tr()), - LegnthStringValidationWithLenghShowing( + LengthStringValidationWithLengthShowing( 64, 'validations.length'.tr(args: ["64"])) ], ); diff --git a/lib/logic/cubit/forms/user/ssh_form_cubit.dart b/lib/logic/cubit/forms/user/ssh_form_cubit.dart new file mode 100644 index 00000000..d51cac6b --- /dev/null +++ b/lib/logic/cubit/forms/user/ssh_form_cubit.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; +import 'package:selfprivacy/logic/models/job.dart'; +import 'package:selfprivacy/logic/models/user.dart'; + +class SshFormCubit extends FormCubit { + SshFormCubit({ + required this.jobsCubit, + required this.user, + }) { + var keyRegExp = RegExp( + r"^(ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$"); + + key = FieldCubit( + initalValue: '', + validations: [ + ValidationModel( + (newKey) => user.sshKeys.any((key) => key == newKey), + 'validations.key_already_exists'.tr(), + ), + RequiredStringValidation('validations.required'.tr()), + ValidationModel((s) { + print(s); + print(keyRegExp.hasMatch(s)); + return !keyRegExp.hasMatch(s); + }, 'validations.invalid_format'.tr()), + ], + ); + + super.addFields([key]); + } + + @override + FutureOr onSubmit() { + print(key.state.isValid); + jobsCubit.addJob(CreateSSHKeyJob(user: user, publicKey: key.state.value)); + } + + late FieldCubit key; + + final JobsCubit jobsCubit; + final User user; +} diff --git a/lib/logic/cubit/forms/user/user_form_cubit.dart b/lib/logic/cubit/forms/user/user_form_cubit.dart index 32a170e5..24f67437 100644 --- a/lib/logic/cubit/forms/user/user_form_cubit.dart +++ b/lib/logic/cubit/forms/user/user_form_cubit.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; import 'package:selfprivacy/logic/models/user.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/utils/password_generator.dart'; class UserFormCubit extends FormCubit { @@ -25,7 +25,7 @@ class UserFormCubit extends FormCubit { (s) => s.toLowerCase() == 'root', 'validations.root_name'.tr()), ValidationModel( (login) => users.any((user) => user.login == login), - 'validations.user_alredy_exist'.tr(), + 'validations.user_already_exist'.tr(), ), RequiredStringValidation('validations.required'.tr()), ValidationModel( @@ -34,7 +34,8 @@ class UserFormCubit extends FormCubit { ); password = FieldCubit( - initalValue: isEdit ? user!.password : StringGenerators.userPassword(), + initalValue: + isEdit ? (user?.password ?? '') : StringGenerators.userPassword(), validations: [ RequiredStringValidation('validations.required'.tr()), ValidationModel((s) => passwordRegExp.hasMatch(s), diff --git a/lib/logic/cubit/forms/validations/validations.dart b/lib/logic/cubit/forms/validations/validations.dart index 0c4fe493..aff4ec92 100644 --- a/lib/logic/cubit/forms/validations/validations.dart +++ b/lib/logic/cubit/forms/validations/validations.dart @@ -1,7 +1,7 @@ import 'package:cubit_form/cubit_form.dart'; -class LegnthStringValidationWithLenghShowing extends ValidationModel { - LegnthStringValidationWithLenghShowing(int length, String errorText) +class LengthStringValidationWithLengthShowing extends ValidationModel { + LengthStringValidationWithLengthShowing(int length, String errorText) : super((n) => n.length != length, errorText); @override diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart index 6e62b3f1..11ce2bc1 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart @@ -21,7 +21,7 @@ class HetznerMetricsRepository { break; } - var api = HetznerApi(hasLoger: true); + var api = HetznerApi(hasLogger: true); var results = await Future.wait([ api.getMetrics(start, end, 'cpu'), diff --git a/lib/logic/cubit/jobs/jobs_cubit.dart b/lib/logic/cubit/jobs/jobs_cubit.dart index a2c1b414..f2ce57d1 100644 --- a/lib/logic/cubit/jobs/jobs_cubit.dart +++ b/lib/logic/cubit/jobs/jobs_cubit.dart @@ -1,14 +1,13 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; -import 'package:selfprivacy/logic/get_it/ssh.dart'; import 'package:selfprivacy/logic/models/job.dart'; -import 'package:equatable/equatable.dart'; -import 'package:selfprivacy/logic/models/user.dart'; + export 'package:provider/provider.dart'; -import 'package:easy_localization/easy_localization.dart'; part 'jobs_state.dart'; @@ -37,7 +36,7 @@ class JobsCubit extends Cubit { emit(newState); } - void createOrRemoveServiceToggleJob(ServiceToggleJob job) { + void createOrRemoveServiceToggleJob(ToggleJob job) { var newJobsList = []; if (state is JobsStateWithJobs) { newJobsList.addAll((state as JobsStateWithJobs).jobList); @@ -99,28 +98,26 @@ class JobsCubit extends Cubit { if (state is JobsStateWithJobs) { var jobs = (state as JobsStateWithJobs).jobList; emit(JobsStateLoading()); - var newUsers = []; var hasServiceJobs = false; for (var job in jobs) { if (job is CreateUserJob) { - newUsers.add(job.user); - await api.createUser(job.user); + await usersCubit.createUser(job.user); } if (job is DeleteUserJob) { - final deleted = await api.deleteUser(job.user); - if (deleted) usersCubit.remove(job.user); + await usersCubit.deleteUser(job.user); } if (job is ServiceToggleJob) { hasServiceJobs = true; await api.switchService(job.type, job.needToTurnOn); } if (job is CreateSSHKeyJob) { - await getIt().generateKeys(); - api.sendSsh(getIt().savedPubKey!); + await usersCubit.addSshKey(job.user, job.publicKey); + } + if (job is DeleteSSHKeyJob) { + await usersCubit.deleteSshKey(job.user, job.publicKey); } } - usersCubit.addUsers(newUsers); await api.pullConfigurationUpdate(); await api.apply(); if (hasServiceJobs) { @@ -128,8 +125,6 @@ class JobsCubit extends Cubit { } emit(JobsStateEmpty()); - - getIt().navigator!.pop(); } } } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart index 3d46142d..b4969037 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart @@ -2,7 +2,9 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_repository.dart'; +import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; import 'package:selfprivacy/logic/models/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/timezone_settings.dart'; part 'server_detailed_info_state.dart'; @@ -16,7 +18,12 @@ class ServerDetailsCubit extends Cubit { if (isReadyToCheck) { emit(ServerDetailsLoading()); var data = await repository.load(); - emit(Loaded(serverInfo: data, checkTime: DateTime.now())); + emit(Loaded( + serverInfo: data.hetznerServerInfo, + autoUpgradeSettings: data.autoUpgradeSettings, + serverTimezone: data.serverTimezone, + checkTime: DateTime.now(), + )); } else { emit(ServerDetailsNotReady()); } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart index bcd34625..a5d6c07e 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart @@ -1,9 +1,33 @@ import 'package:selfprivacy/logic/api_maps/hetzner.dart'; +import 'package:selfprivacy/logic/api_maps/server.dart'; +import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; import 'package:selfprivacy/logic/models/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/timezone_settings.dart'; class ServerDetailsRepository { - Future load() async { - var client = HetznerApi(); - return await client.getInfo(); + var hetznerAPi = HetznerApi(); + var selfprivacyServer = ServerApi(); + + Future<_ServerDetailsRepositoryDto> load() async { + print('load'); + return _ServerDetailsRepositoryDto( + autoUpgradeSettings: await selfprivacyServer.getAutoUpgradeSettings(), + hetznerServerInfo: await hetznerAPi.getInfo(), + serverTimezone: await selfprivacyServer.getServerTimezone(), + ); } } + +class _ServerDetailsRepositoryDto { + final HetznerServerInfo hetznerServerInfo; + + final TimeZoneSettings serverTimezone; + + final AutoUpgradeSettings autoUpgradeSettings; + + _ServerDetailsRepositoryDto({ + required this.hetznerServerInfo, + required this.serverTimezone, + required this.autoUpgradeSettings, + }); +} diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart index cf017658..b4524751 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart @@ -17,13 +17,24 @@ class Loading extends ServerDetailsState {} class Loaded extends ServerDetailsState { final HetznerServerInfo serverInfo; + + final TimeZoneSettings serverTimezone; + + final AutoUpgradeSettings autoUpgradeSettings; final DateTime checkTime; Loaded({ required this.serverInfo, + required this.serverTimezone, + required this.autoUpgradeSettings, required this.checkTime, }); @override - List get props => [serverInfo, checkTime]; + List get props => [ + serverInfo, + serverTimezone, + autoUpgradeSettings, + checkTime, + ]; } diff --git a/lib/logic/cubit/services/services_cubit.dart b/lib/logic/cubit/services/services_cubit.dart index 5bbfcecc..6d5dfef7 100644 --- a/lib/logic/cubit/services/services_cubit.dart +++ b/lib/logic/cubit/services/services_cubit.dart @@ -2,7 +2,6 @@ import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; part 'services_state.dart'; diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index a38db016..ecd29515 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -3,35 +3,305 @@ import 'package:equatable/equatable.dart'; import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/models/user.dart'; + +import '../../api_maps/server.dart'; + export 'package:provider/provider.dart'; part 'users_state.dart'; class UsersCubit extends Cubit { - UsersCubit() : super(UsersState([])); + UsersCubit() + : super(UsersState( + [], User(login: 'root'), User(login: 'loading...'))); Box box = Hive.box(BNames.users); + Box configBox = Hive.box(BNames.appConfig); - void load() async { + final api = ServerApi(); + + Future load() async { var loadedUsers = box.values.toList(); - + final primaryUser = + configBox.get(BNames.rootUser, defaultValue: User(login: 'loading...')); + List rootKeys = [ + ...configBox.get(BNames.rootKeys, defaultValue: []) + ]; if (loadedUsers.isNotEmpty) { - emit(UsersState(loadedUsers)); + emit(UsersState( + loadedUsers, User(login: 'root', sshKeys: rootKeys), primaryUser)); + } + + final usersFromServer = await api.getUsersList(); + if (usersFromServer.isSuccess) { + final updatedList = + mergeLocalAndServerUsers(loadedUsers, usersFromServer.data); + emit(UsersState( + updatedList, User(login: 'root', sshKeys: rootKeys), primaryUser)); + } + + final usersWithSshKeys = await loadSshKeys(state.users); + // Update the users it the box + box.clear(); + box.addAll(usersWithSshKeys); + + final rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; + configBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); + final primaryUserWithSshKeys = + (await loadSshKeys([state.primaryUser])).first; + configBox.put(BNames.rootUser, primaryUserWithSshKeys); + emit(UsersState( + usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys)); + } + + List mergeLocalAndServerUsers( + List localUsers, List serverUsers) { + // If local user not exists on server, add it with isFoundOnServer = false + // If server user not exists on local, add it + + List mergedUsers = []; + List serverUsersCopy = List.from(serverUsers); + + for (var localUser in localUsers) { + if (serverUsersCopy.contains(localUser.login)) { + mergedUsers.add(User( + login: localUser.login, + isFoundOnServer: true, + password: localUser.password, + sshKeys: localUser.sshKeys, + )); + serverUsersCopy.remove(localUser.login); + } else { + mergedUsers.add(User( + login: localUser.login, + isFoundOnServer: false, + password: localUser.password, + note: localUser.note, + )); + } + } + + for (var serverUser in serverUsersCopy) { + mergedUsers.add(User( + login: serverUser, + isFoundOnServer: true, + )); + } + + return mergedUsers; + } + + Future> loadSshKeys(List users) async { + List updatedUsers = []; + + for (var user in users) { + if (user.isFoundOnServer || + user.login == 'root' || + user.login == state.primaryUser.login) { + final sshKeys = await api.getUserSshKeys(user); + print('sshKeys for $user: ${sshKeys.data}'); + if (sshKeys.isSuccess) { + updatedUsers.add(User( + login: user.login, + isFoundOnServer: true, + password: user.password, + sshKeys: sshKeys.data, + note: user.note, + )); + } else { + updatedUsers.add(User( + login: user.login, + isFoundOnServer: true, + password: user.password, + note: user.note, + )); + } + } else { + updatedUsers.add(User( + login: user.login, + isFoundOnServer: false, + password: user.password, + note: user.note, + )); + } + } + return updatedUsers; + } + + Future refresh() async { + List updatedUsers = List.from(state.users); + final usersFromServer = await api.getUsersList(); + if (usersFromServer.isSuccess) { + updatedUsers = + mergeLocalAndServerUsers(updatedUsers, usersFromServer.data); + } + final usersWithSshKeys = await loadSshKeys(updatedUsers); + box.clear(); + box.addAll(usersWithSshKeys); + final rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; + configBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); + final primaryUserWithSshKeys = + (await loadSshKeys([state.primaryUser])).first; + configBox.put(BNames.rootUser, primaryUserWithSshKeys); + emit(UsersState( + usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys)); + return; + } + + Future createUser(User user) async { + // If user exists on server, do nothing + if (state.users.any((u) => u.login == user.login && u.isFoundOnServer)) { + return; + } + // If user is root or primary user, do nothing + if (user.login == 'root' || user.login == state.primaryUser.login) { + return; + } + final result = await api.createUser(user); + var loadedUsers = List.from(state.users); + loadedUsers.add(result.data); + await box.clear(); + await box.addAll(loadedUsers); + emit(state.copyWith(users: loadedUsers)); + } + + Future deleteUser(User user) async { + // If user is primary or root, don't delete + if (user.login == state.primaryUser.login || user.login == 'root') { + return; + } + var loadedUsers = List.from(state.users); + final result = await api.deleteUser(user); + if (result) { + loadedUsers.removeWhere((u) => u.login == user.login); + await box.clear(); + await box.addAll(loadedUsers); + emit(state.copyWith(users: loadedUsers)); } } - void addUsers(List users) async { - var newUserList = [...state.users, ...users]; - - await box.addAll(users); - emit(UsersState(newUserList)); + Future addSshKey(User user, String publicKey) async { + // If adding root key, use api.addRootSshKey + // Otherwise, use api.addUserSshKey + if (user.login == 'root') { + final result = await api.addRootSshKey(publicKey); + if (result.isSuccess) { + // Add ssh key to the array of root keys + final rootKeys = + configBox.get(BNames.rootKeys, defaultValue: []) as List; + rootKeys.add(publicKey); + configBox.put(BNames.rootKeys, rootKeys); + emit(state.copyWith( + rootUser: User( + login: state.rootUser.login, + isFoundOnServer: true, + password: state.rootUser.password, + sshKeys: rootKeys, + note: state.rootUser.note, + ), + )); + } + } else { + final result = await api.addUserSshKey(user, publicKey); + if (result.isSuccess) { + // If it is primary user, update primary user + if (user.login == state.primaryUser.login) { + List primaryUserKeys = + List.from(state.primaryUser.sshKeys); + primaryUserKeys.add(publicKey); + final updatedUser = User( + login: state.primaryUser.login, + isFoundOnServer: true, + password: state.primaryUser.password, + sshKeys: primaryUserKeys, + note: state.primaryUser.note, + ); + configBox.put(BNames.rootUser, updatedUser); + emit(state.copyWith( + primaryUser: updatedUser, + )); + } else { + // If it is not primary user, update user + List userKeys = List.from(user.sshKeys); + userKeys.add(publicKey); + final updatedUser = User( + login: user.login, + isFoundOnServer: true, + password: user.password, + sshKeys: userKeys, + note: user.note, + ); + await box.putAt(box.values.toList().indexOf(user), updatedUser); + emit(state.copyWith( + users: box.values.toList(), + )); + } + } + } } - void remove(User user) async { - var users = [...state.users]; - var index = users.indexOf(user); - users.remove(user); - await box.deleteAt(index); + Future deleteSshKey(User user, String publicKey) async { + // All keys are deleted via api.deleteUserSshKey - emit(UsersState(users)); + final result = await api.deleteUserSshKey(user, publicKey); + if (result.isSuccess) { + // If it is root user, delete key from root keys + // If it is primary user, update primary user + // If it is not primary user, update user + + if (user.login == 'root') { + final rootKeys = + configBox.get(BNames.rootKeys, defaultValue: []) as List; + rootKeys.remove(publicKey); + configBox.put(BNames.rootKeys, rootKeys); + emit(state.copyWith( + rootUser: User( + login: state.rootUser.login, + isFoundOnServer: true, + password: state.rootUser.password, + sshKeys: rootKeys, + note: state.rootUser.note, + ), + )); + return; + } + if (user.login == state.primaryUser.login) { + List primaryUserKeys = + List.from(state.primaryUser.sshKeys); + primaryUserKeys.remove(publicKey); + final updatedUser = User( + login: state.primaryUser.login, + isFoundOnServer: true, + password: state.primaryUser.password, + sshKeys: primaryUserKeys, + note: state.primaryUser.note, + ); + configBox.put(BNames.rootUser, updatedUser); + emit(state.copyWith( + primaryUser: updatedUser, + )); + return; + } + List userKeys = List.from(user.sshKeys); + userKeys.remove(publicKey); + final updatedUser = User( + login: user.login, + isFoundOnServer: true, + password: user.password, + sshKeys: userKeys, + note: user.note, + ); + await box.putAt(box.values.toList().indexOf(user), updatedUser); + emit(state.copyWith( + users: box.values.toList(), + )); + } + } + + @override + void onChange(Change change) { + super.onChange(change); + + print('UsersState changed'); + print(change); } } diff --git a/lib/logic/cubit/users/users_state.dart b/lib/logic/cubit/users/users_state.dart index 88c56146..429b5cfa 100644 --- a/lib/logic/cubit/users/users_state.dart +++ b/lib/logic/cubit/users/users_state.dart @@ -1,12 +1,26 @@ part of 'users_cubit.dart'; class UsersState extends Equatable { - const UsersState(this.users); + const UsersState(this.users, this.rootUser, this.primaryUser); final List users; + final User rootUser; + final User primaryUser; @override - List get props => users; + List get props => [users, rootUser, primaryUser]; + + UsersState copyWith({ + List? users, + User? rootUser, + User? primaryUser, + }) { + return UsersState( + users ?? this.users, + rootUser ?? this.rootUser, + primaryUser ?? this.primaryUser, + ); + } bool get isEmpty => users.isEmpty; } diff --git a/lib/logic/models/auto_upgrade_settings.dart b/lib/logic/models/auto_upgrade_settings.dart new file mode 100644 index 00000000..6007e622 --- /dev/null +++ b/lib/logic/models/auto_upgrade_settings.dart @@ -0,0 +1,22 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'auto_upgrade_settings.g.dart'; + +@JsonSerializable(createToJson: true) +class AutoUpgradeSettings extends Equatable { + final bool enable; + final bool allowReboot; + + AutoUpgradeSettings({ + required this.enable, + required this.allowReboot, + }); + + @override + List get props => [enable, allowReboot]; + factory AutoUpgradeSettings.fromJson(Map json) => + _$AutoUpgradeSettingsFromJson(json); + + Map toJson() => _$AutoUpgradeSettingsToJson(this); +} diff --git a/lib/logic/models/auto_upgrade_settings.g.dart b/lib/logic/models/auto_upgrade_settings.g.dart new file mode 100644 index 00000000..e6accc2f --- /dev/null +++ b/lib/logic/models/auto_upgrade_settings.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auto_upgrade_settings.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AutoUpgradeSettings _$AutoUpgradeSettingsFromJson(Map json) => + AutoUpgradeSettings( + enable: json['enable'] as bool, + allowReboot: json['allowReboot'] as bool, + ); + +Map _$AutoUpgradeSettingsToJson( + AutoUpgradeSettings instance) => + { + 'enable': instance.enable, + 'allowReboot': instance.allowReboot, + }; diff --git a/lib/logic/models/backup.g.dart b/lib/logic/models/backup.g.dart index c1b50d03..c784abe1 100644 --- a/lib/logic/models/backup.g.dart +++ b/lib/logic/models/backup.g.dart @@ -6,46 +6,16 @@ part of 'backup.dart'; // JsonSerializableGenerator // ************************************************************************** -Backup _$BackupFromJson(Map json) { - return Backup( - time: DateTime.parse(json['time'] as String), - id: json['short_id'] as String, - ); -} - -BackupStatus _$BackupStatusFromJson(Map json) { - return BackupStatus( - status: _$enumDecode(_$BackupStatusEnumEnumMap, json['status']), - progress: (json['progress'] as num).toDouble(), - errorMessage: json['error_message'] as String?, - ); -} - -K _$enumDecode( - Map enumValues, - Object? source, { - K? unknownValue, -}) { - if (source == null) { - throw ArgumentError( - 'A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}', +Backup _$BackupFromJson(Map json) => Backup( + time: DateTime.parse(json['time'] as String), + id: json['short_id'] as String, ); - } - return enumValues.entries.singleWhere( - (e) => e.value == source, - orElse: () { - if (unknownValue == null) { - throw ArgumentError( - '`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}', - ); - } - return MapEntry(unknownValue, enumValues.values.first); - }, - ).key; -} +BackupStatus _$BackupStatusFromJson(Map json) => BackupStatus( + status: $enumDecode(_$BackupStatusEnumEnumMap, json['status']), + progress: (json['progress'] as num).toDouble(), + errorMessage: json['error_message'] as String?, + ); const _$BackupStatusEnumEnumMap = { BackupStatusEnum.noKey: 'NO_KEY', diff --git a/lib/logic/models/dns_records.dart b/lib/logic/models/dns_records.dart index 80f1335e..25aad046 100644 --- a/lib/logic/models/dns_records.dart +++ b/lib/logic/models/dns_records.dart @@ -3,8 +3,8 @@ import 'package:json_annotation/json_annotation.dart'; part 'dns_records.g.dart'; @JsonSerializable(createToJson: true, createFactory: false) -class DnsRecords { - DnsRecords({ +class DnsRecord { + DnsRecord({ required this.type, required this.name, required this.content, @@ -20,5 +20,5 @@ class DnsRecords { final int priority; final bool proxied; - toJson() => _$DnsRecordsToJson(this); + toJson() => _$DnsRecordToJson(this); } diff --git a/lib/logic/models/dns_records.g.dart b/lib/logic/models/dns_records.g.dart index 315cb447..c8c12c34 100644 --- a/lib/logic/models/dns_records.g.dart +++ b/lib/logic/models/dns_records.g.dart @@ -6,8 +6,7 @@ part of 'dns_records.dart'; // JsonSerializableGenerator // ************************************************************************** -Map _$DnsRecordsToJson(DnsRecords instance) => - { +Map _$DnsRecordToJson(DnsRecord instance) => { 'type': instance.type, 'name': instance.name, 'content': instance.content, diff --git a/lib/logic/models/hetzner_server_info.g.dart b/lib/logic/models/hetzner_server_info.g.dart index 40055d19..73e6be68 100644 --- a/lib/logic/models/hetzner_server_info.g.dart +++ b/lib/logic/models/hetzner_server_info.g.dart @@ -6,42 +6,16 @@ part of 'hetzner_server_info.dart'; // JsonSerializableGenerator // ************************************************************************** -HetznerServerInfo _$HetznerServerInfoFromJson(Map json) { - return HetznerServerInfo( - json['id'] as int, - json['name'] as String, - _$enumDecode(_$ServerStatusEnumMap, json['status']), - DateTime.parse(json['created'] as String), - HetznerServerTypeInfo.fromJson(json['server_type'] as Map), - HetznerServerInfo.locationFromJson(json['datacenter'] as Map), - ); -} - -K _$enumDecode( - Map enumValues, - Object? source, { - K? unknownValue, -}) { - if (source == null) { - throw ArgumentError( - 'A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}', +HetznerServerInfo _$HetznerServerInfoFromJson(Map json) => + HetznerServerInfo( + json['id'] as int, + json['name'] as String, + $enumDecode(_$ServerStatusEnumMap, json['status']), + DateTime.parse(json['created'] as String), + HetznerServerTypeInfo.fromJson( + json['server_type'] as Map), + HetznerServerInfo.locationFromJson(json['datacenter'] as Map), ); - } - - return enumValues.entries.singleWhere( - (e) => e.value == source, - orElse: () { - if (unknownValue == null) { - throw ArgumentError( - '`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}', - ); - } - return MapEntry(unknownValue, enumValues.values.first); - }, - ).key; -} const _$ServerStatusEnumMap = { ServerStatus.running: 'running', @@ -56,29 +30,26 @@ const _$ServerStatusEnumMap = { }; HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson( - Map json) { - return HetznerServerTypeInfo( - json['cores'] as int, - json['memory'] as num, - json['disk'] as int, - (json['prices'] as List) - .map((e) => HetznerPriceInfo.fromJson(e as Map)) - .toList(), - ); -} + Map json) => + HetznerServerTypeInfo( + json['cores'] as int, + json['memory'] as num, + json['disk'] as int, + (json['prices'] as List) + .map((e) => HetznerPriceInfo.fromJson(e as Map)) + .toList(), + ); -HetznerPriceInfo _$HetznerPriceInfoFromJson(Map json) { - return HetznerPriceInfo( - HetznerPriceInfo.getPrice(json['price_hourly'] as Map), - HetznerPriceInfo.getPrice(json['price_monthly'] as Map), - ); -} +HetznerPriceInfo _$HetznerPriceInfoFromJson(Map json) => + HetznerPriceInfo( + HetznerPriceInfo.getPrice(json['price_hourly'] as Map), + HetznerPriceInfo.getPrice(json['price_monthly'] as Map), + ); -HetznerLocation _$HetznerLocationFromJson(Map json) { - return HetznerLocation( - json['country'] as String, - json['city'] as String, - json['description'] as String, - json['network_zone'] as String, - ); -} +HetznerLocation _$HetznerLocationFromJson(Map json) => + HetznerLocation( + json['country'] as String, + json['city'] as String, + json['description'] as String, + json['network_zone'] as String, + ); diff --git a/lib/logic/models/job.dart b/lib/logic/models/job.dart index e9767803..698cd1fb 100644 --- a/lib/logic/models/job.dart +++ b/lib/logic/models/job.dart @@ -1,8 +1,8 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/utils/password_generator.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'user.dart'; @@ -42,24 +42,53 @@ class DeleteUserJob extends Job { List get props => [id, title, user]; } -class ServiceToggleJob extends Job { - ServiceToggleJob({ +class ToggleJob extends Job { + ToggleJob({ required this.type, - required this.needToTurnOn, - }) : super( - title: - '${needToTurnOn ? "jobs.serviceTurnOn".tr() : "jobs.serviceTurnOff".tr()} ${type.title}'); + required String title, + }) : super(title: title); - final ServiceTypes type; - final bool needToTurnOn; + final dynamic type; @override - List get props => [id, title, type, needToTurnOn]; + List get props => [...super.props, type]; +} + +class ServiceToggleJob extends ToggleJob { + ServiceToggleJob({ + required ServiceTypes type, + required this.needToTurnOn, + }) : super( + title: + '${needToTurnOn ? "jobs.serviceTurnOn".tr() : "jobs.serviceTurnOff".tr()} ${type.title}', + type: type, + ); + + final bool needToTurnOn; } class CreateSSHKeyJob extends Job { - CreateSSHKeyJob() : super(title: '${"more.create_ssh_key".tr()}'); + CreateSSHKeyJob({ + required this.user, + required this.publicKey, + }) : super(title: '${"jobs.create_ssh_key".tr(args: [user.login])}'); + + final User user; + final String publicKey; @override - List get props => [id, title]; + List get props => [id, title, user, publicKey]; +} + +class DeleteSSHKeyJob extends Job { + DeleteSSHKeyJob({ + required this.user, + required this.publicKey, + }) : super(title: '${"jobs.delete_ssh_key".tr(args: [user.login])}'); + + final User user; + final String publicKey; + + @override + List get props => [id, title, user, publicKey]; } diff --git a/lib/logic/models/message.dart b/lib/logic/models/message.dart index 5ae0465e..79f21a54 100644 --- a/lib/logic/models/message.dart +++ b/lib/logic/models/message.dart @@ -1,6 +1,6 @@ import 'package:intl/intl.dart'; -final formater = new DateFormat('hh:mm'); +final formatter = new DateFormat('hh:mm'); class Message { Message({this.text, this.type = MessageType.normal}) : time = DateTime.now(); @@ -8,7 +8,7 @@ class Message { final String? text; final DateTime time; final MessageType type; - String get timeString => formater.format(time); + String get timeString => formatter.format(time); static Message warn({String? text}) => Message( text: text, diff --git a/lib/logic/models/server_configurations.dart b/lib/logic/models/server_configurations.dart new file mode 100644 index 00000000..73915566 --- /dev/null +++ b/lib/logic/models/server_configurations.dart @@ -0,0 +1,22 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'server_configurations.g.dart'; + +@JsonSerializable(createToJson: true) +class AutoUpgradeConfigurations extends Equatable { + const AutoUpgradeConfigurations({ + required this.enable, + required this.allowReboot, + }); + + final bool enable; + final bool allowReboot; + + factory AutoUpgradeConfigurations.fromJson(Map json) => + _$AutoUpgradeConfigurationsFromJson(json); + Map toJson() => _$AutoUpgradeConfigurationsToJson(this); + + @override + List get props => [enable, allowReboot]; +} diff --git a/lib/logic/models/server_configurations.g.dart b/lib/logic/models/server_configurations.g.dart new file mode 100644 index 00000000..a198ae2e --- /dev/null +++ b/lib/logic/models/server_configurations.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'server_configurations.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AutoUpgradeConfigurations _$AutoUpgradeConfigurationsFromJson( + Map json) => + AutoUpgradeConfigurations( + enable: json['enable'] as bool, + allowReboot: json['allowReboot'] as bool, + ); + +Map _$AutoUpgradeConfigurationsToJson( + AutoUpgradeConfigurations instance) => + { + 'enable': instance.enable, + 'allowReboot': instance.allowReboot, + }; diff --git a/lib/logic/models/timezone_settings.dart b/lib/logic/models/timezone_settings.dart new file mode 100644 index 00000000..76d28aff --- /dev/null +++ b/lib/logic/models/timezone_settings.dart @@ -0,0 +1,18 @@ +import 'package:timezone/timezone.dart'; + +class TimeZoneSettings { + final Location timezone; + + TimeZoneSettings(this.timezone); + + Map toJson() { + return { + 'timezone': timezone.name, + }; + } + + factory TimeZoneSettings.fromString(String string) { + var location = timeZoneDatabase.locations[string]!; + return TimeZoneSettings(location); + } +} diff --git a/lib/logic/models/user.dart b/lib/logic/models/user.dart index 0d1c48b3..8161f61b 100644 --- a/lib/logic/models/user.dart +++ b/lib/logic/models/user.dart @@ -1,10 +1,8 @@ import 'dart:ui'; -import 'package:crypt/crypt.dart'; import 'package:equatable/equatable.dart'; -import 'package:selfprivacy/utils/color_utils.dart'; import 'package:hive/hive.dart'; -import 'package:selfprivacy/utils/password_generator.dart'; +import 'package:selfprivacy/utils/color_utils.dart'; part 'user.g.dart'; @@ -12,26 +10,33 @@ part 'user.g.dart'; class User extends Equatable { User({ required this.login, - required this.password, + this.password, + this.sshKeys = const [], + this.isFoundOnServer = true, + this.note, }); @HiveField(0) final String login; @HiveField(1) - final String password; + final String? password; + + @HiveField(2, defaultValue: const []) + final List sshKeys; + + @HiveField(3, defaultValue: true) + final bool isFoundOnServer; + + @HiveField(4) + final String? note; @override - List get props => [login, password]; + List get props => [login, password, sshKeys, isFoundOnServer, note]; Color get color => stringToColor(login); - Crypt get hashPassword => Crypt.sha512( - password, - salt: StringGenerators.passwordSalt(), - ); - String toString() { - return login; + return '$login, ${isFoundOnServer ? 'found' : 'not found'}, ${sshKeys.length} ssh keys, note: $note'; } } diff --git a/lib/logic/models/user.g.dart b/lib/logic/models/user.g.dart index 796ea5dd..a1889dc1 100644 --- a/lib/logic/models/user.g.dart +++ b/lib/logic/models/user.g.dart @@ -18,18 +18,27 @@ class UserAdapter extends TypeAdapter { }; return User( login: fields[0] as String, - password: fields[1] as String, + password: fields[1] as String?, + sshKeys: fields[2] == null ? [] : (fields[2] as List).cast(), + isFoundOnServer: fields[3] == null ? true : fields[3] as bool, + note: fields[4] as String?, ); } @override void write(BinaryWriter writer, User obj) { writer - ..writeByte(2) + ..writeByte(5) ..writeByte(0) ..write(obj.login) ..writeByte(1) - ..write(obj.password); + ..write(obj.password) + ..writeByte(2) + ..write(obj.sshKeys) + ..writeByte(3) + ..write(obj.isFoundOnServer) + ..writeByte(4) + ..write(obj.note); } @override diff --git a/lib/main.dart b/lib/main.dart index 028291d2..9a2d0233 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,13 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.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 'package:wakelock/wakelock.dart'; +import 'package:timezone/data/latest.dart' as tz; import 'config/bloc_config.dart'; import 'config/bloc_observer.dart'; @@ -19,48 +20,51 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await HiveConfig.init(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - Bloc.observer = SimpleBlocObserver(); Wakelock.enable(); await getItSetup(); await EasyLocalization.ensureInitialized(); + tz.initializeTimeZones(); - runApp(MyApp()); + BlocOverrides.runZoned( + () => runApp(Localization(child: MyApp())), + blocObserver: SimpleBlocObserver(), + ); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return Localization( - child: BlocAndProviderConfig( - child: Builder(builder: (context) { - var appSettings = context.watch().state; - - return AnnotatedRegion( - value: SystemUiOverlayStyle.light, // Manually changnig appbar color - child: MaterialApp( - scaffoldMessengerKey: - getIt.get().scaffoldMessengerKey, - navigatorKey: getIt.get().navigatorKey, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - debugShowCheckedModeBanner: false, - title: 'SelfPrivacy', - theme: appSettings.isDarkModeOn ? darkTheme : ligtTheme, - home: appSettings.isOnbordingShowing - ? OnboardingPage(nextPage: InitializingPage()) - : RootPage(), - builder: (BuildContext context, Widget? widget) { - Widget error = Text('...rendering error...'); - if (widget is Scaffold || widget is Navigator) - error = Scaffold(body: Center(child: error)); - ErrorWidget.builder = - (FlutterErrorDetails errorDetails) => error; - return widget!; - }, - ), - ); - }), + child: AnnotatedRegion( + value: SystemUiOverlayStyle.light, // Manually changing appbar color + child: BlocAndProviderConfig( + child: BlocBuilder( + builder: (context, appSettings) { + return MaterialApp( + scaffoldMessengerKey: + getIt.get().scaffoldMessengerKey, + navigatorKey: getIt.get().navigatorKey, + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + debugShowCheckedModeBanner: false, + title: 'SelfPrivacy', + theme: appSettings.isDarkModeOn ? darkTheme : lightTheme, + home: appSettings.isOnbordingShowing + ? OnboardingPage(nextPage: InitializingPage()) + : RootPage(), + builder: (BuildContext context, Widget? widget) { + Widget error = Text('...rendering error...'); + if (widget is Scaffold || widget is Navigator) + error = Scaffold(body: Center(child: error)); + ErrorWidget.builder = + (FlutterErrorDetails errorDetails) => error; + return widget!; + }, + ); + }, + ), + ), ), ); } diff --git a/lib/ui/components/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart index 5d0e7204..398e7e89 100644 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ b/lib/ui/components/brand_cards/brand_cards.dart @@ -20,6 +20,9 @@ class BrandCards { shadow: bigShadow, borderRadius: BorderRadius.circular(10), ); + static Widget outlined({required Widget child}) => _OutlinedCard( + child: child, + ); } class _BrandCard extends StatelessWidget { @@ -52,6 +55,29 @@ class _BrandCard extends StatelessWidget { } } +class _OutlinedCard extends StatelessWidget { + const _OutlinedCard({ + Key? key, + required this.child, + }) : super(key: key); + + final Widget child; + @override + Widget build(BuildContext context) { + return Card( + elevation: 0.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + side: BorderSide( + color: Colors.grey.withOpacity(0.2), + width: 1, + ), + ), + child: child, + ); + } +} + final bigShadow = [ BoxShadow( offset: Offset(0, 4), diff --git a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart new file mode 100644 index 00000000..934c952f --- /dev/null +++ b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; + +class BrandHeroScreen extends StatelessWidget { + const BrandHeroScreen({ + Key? key, + this.headerTitle = '', + this.hasBackButton = true, + this.hasFlashButton = true, + required this.children, + this.heroIcon, + this.heroTitle, + this.heroSubtitle, + }) : super(key: key); + + final List children; + final String headerTitle; + final bool hasBackButton; + final bool hasFlashButton; + final IconData? heroIcon; + final String? heroTitle; + final String? heroSubtitle; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Scaffold( + appBar: PreferredSize( + preferredSize: Size.fromHeight(52.0), + child: BrandHeader( + title: headerTitle, + hasBackButton: hasBackButton, + hasFlashButton: hasFlashButton, + ), + ), + body: ListView( + padding: EdgeInsets.all(16.0), + children: [ + if (heroIcon != null) + Icon( + heroIcon, + size: 48.0, + ), + SizedBox(height: 16.0), + if (heroTitle != null) + Text(heroTitle!, + style: Theme.of(context).textTheme.headline2, + textAlign: TextAlign.center), + SizedBox(height: 8.0), + if (heroSubtitle != null) + Text(heroSubtitle!, + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center), + SizedBox(height: 16.0), + ...children, + ], + ), + ), + ); + } +} diff --git a/lib/ui/components/brand_loader/brand_loader.dart b/lib/ui/components/brand_loader/brand_loader.dart index 52b1b820..9cd5b571 100644 --- a/lib/ui/components/brand_loader/brand_loader.dart +++ b/lib/ui/components/brand_loader/brand_loader.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; 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 2df24868..3c544331 100644 --- a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart +++ b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart @@ -1,7 +1,7 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; -import 'package:easy_localization/easy_localization.dart'; final _kBottomTabBarHeight = 51; @@ -62,12 +62,12 @@ class _BrandTabBarState extends State { } _getIconButton(String label, IconData iconData, int index) { - var acitivColor = Theme.of(context).brightness == Brightness.dark + var activeColor = Theme.of(context).brightness == Brightness.dark ? BrandColors.white : BrandColors.black; var isActive = currentIndex == index; - var color = isActive ? acitivColor : BrandColors.inactive; + var color = isActive ? activeColor : BrandColors.inactive; return InkWell( onTap: () => widget.controller!.animateTo(index), child: Padding( diff --git a/lib/ui/components/brand_timer/brand_timer.dart b/lib/ui/components/brand_timer/brand_timer.dart index 00f512d3..b82df99b 100644 --- a/lib/ui/components/brand_timer/brand_timer.dart +++ b/lib/ui/components/brand_timer/brand_timer.dart @@ -1,9 +1,9 @@ import 'dart:async'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/utils/named_font_weight.dart'; -import 'package:easy_localization/easy_localization.dart'; class BrandTimer extends StatefulWidget { const BrandTimer({ @@ -30,7 +30,7 @@ class _BrandTimerState extends State { } _timerStart() { - _timeString = diffenceFromStart; + _timeString = differenceFromStart; timer = Timer.periodic(Duration(seconds: 1), (Timer t) { var timePassed = DateTime.now().difference(widget.startDateTime); if (timePassed > widget.duration) { @@ -62,11 +62,11 @@ class _BrandTimerState extends State { void _getTime() { setState(() { - _timeString = diffenceFromStart; + _timeString = differenceFromStart; }); } - String get diffenceFromStart => + String get differenceFromStart => _durationToString(DateTime.now().difference(widget.startDateTime)); String _durationToString(Duration duration) { diff --git a/lib/ui/components/jobs_content/jobs_content.dart b/lib/ui/components/jobs_content/jobs_content.dart index 8f766355..92298a06 100644 --- a/lib/ui/components/jobs_content/jobs_content.dart +++ b/lib/ui/components/jobs_content/jobs_content.dart @@ -5,7 +5,6 @@ import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; -import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; diff --git a/lib/ui/pages/backup_details/backup_details.dart b/lib/ui/pages/backup_details/backup_details.dart index 752be742..3040d1aa 100644 --- a/lib/ui/pages/backup_details/backup_details.dart +++ b/lib/ui/pages/backup_details/backup_details.dart @@ -1,6 +1,6 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; -import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; @@ -9,13 +9,11 @@ import 'package:selfprivacy/logic/models/state_types.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; -import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart'; -import 'package:easy_localization/easy_localization.dart'; -part 'header.dart'; +import '../../components/brand_cards/brand_cards.dart'; var navigatorKey = GlobalKey(); @@ -44,203 +42,200 @@ class _BackupDetailsState extends State var backups = context.watch().state.backups; var refreshing = context.watch().state.refreshing; - return Scaffold( - appBar: PreferredSize( - child: Column( - children: [ - Container( - height: 51, - alignment: Alignment.center, - padding: EdgeInsets.symmetric(horizontal: 15), - child: BrandText.h4('basis.details'.tr()), - ), - BrandDivider(), - ], - ), - preferredSize: Size.fromHeight(52), - ), - body: SingleChildScrollView( - physics: ClampingScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: paddingH15V0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Header( - providerState: providerState, - refreshing: refreshing, + return BrandHeroScreen( + heroIcon: BrandIcons.save, + heroTitle: 'providers.backup.card_title'.tr(), + heroSubtitle: 'providers.backup.bottom_sheet.1'.tr(), + children: [ + if (isReady && !isBackupInitialized) + BrandButton.rised( + onPressed: preventActions + ? null + : () async { + await context.read().createBucket(); + }, + text: 'providers.backup.initialize'.tr(), + ), + if (backupStatus == BackupStatusEnum.initializing) + BrandText.body1('providers.backup.waitingForRebuild'.tr()), + if (backupStatus != BackupStatusEnum.initializing && + backupStatus != BackupStatusEnum.noKey) + BrandCards.outlined( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (backupStatus == BackupStatusEnum.initialized) + ListTile( + onTap: preventActions + ? null + : () async { + await context.read().createBackup(); + }, + leading: Icon( + Icons.add_circle_outline_rounded, + ), + title: Text( + 'providers.backup.create_new'.tr(), + style: Theme.of(context).textTheme.headline6, + ), ), - SizedBox(height: 10), - BrandText.h2('providers.backup.card_title'.tr()), - SizedBox(height: 10), - BrandText.body1('providers.backup.bottom_sheet.1'.tr()), - SizedBox(height: 20), - if (isReady && !isBackupInitialized) - BrandButton.rised( - onPressed: preventActions - ? null - : () async { - await context.read().createBucket(); - }, - text: 'providers.backup.initialize'.tr(), + if (backupStatus == BackupStatusEnum.backingUp) + ListTile( + title: Text( + 'providers.backup.creating'.tr( + args: [(backupProgress * 100).round().toString()]), + style: Theme.of(context).textTheme.headline6, ), - if (backupStatus == BackupStatusEnum.initializing) - BrandText.body1('providers.backup.waitingForRebuild'.tr()), - if (backupStatus != BackupStatusEnum.initializing && - backupStatus != BackupStatusEnum.noKey) - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - side: BorderSide( - color: Colors.grey.withOpacity(0.2), - width: 1, - ), - ), - elevation: 0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (backupStatus == BackupStatusEnum.initialized) - ListTile( - onTap: preventActions - ? null - : () async { - await context - .read() - .createBackup(); - }, - leading: Icon( - Icons.add_circle_outline_rounded, - color: BrandColors.textColor1, - ), - title: BrandText.h5( - 'providers.backup.create_new'.tr()), - ), - if (backupStatus == BackupStatusEnum.backingUp) - ListTile( - title: BrandText.h5('providers.backup.creating' - .tr(args: [ - (backupProgress * 100).round().toString() - ])), - subtitle: LinearProgressIndicator( - value: backupProgress, - backgroundColor: Colors.grey.withOpacity(0.2), - ), - ), - if (backupStatus == BackupStatusEnum.restoring) - ListTile( - title: BrandText.h5('providers.backup.restoring' - .tr(args: [ - (backupProgress * 100).round().toString() - ])), - subtitle: LinearProgressIndicator( - backgroundColor: Colors.grey.withOpacity(0.2), - ), - ), - if (backupStatus == BackupStatusEnum.error) - ListTile( - leading: Icon( - Icons.error_outline, - color: BrandColors.red1, - ), - title: BrandText.h5( - 'providers.backup.error_pending'.tr()), - ), - ], - ), + subtitle: LinearProgressIndicator( + value: backupProgress, + backgroundColor: Colors.grey.withOpacity(0.2), ), - SizedBox(height: 16), - // Card with a list of existing backups - // Each list item has a date - // When clicked, starts the restore action - if (backupStatus != BackupStatusEnum.initializing && - backupStatus != BackupStatusEnum.noKey) - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - side: BorderSide( - color: Colors.grey.withOpacity(0.2), - width: 1, - ), - ), - elevation: 0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - leading: Icon( - Icons.refresh, - color: BrandColors.textColor1, - ), - title: - BrandText.h5('providers.backup.restore'.tr()), - ), - Divider( - height: 1.0, - ), - if (backups.isEmpty) - ListTile( - leading: Icon( - Icons.error_outline, - ), - title: Text('providers.backup.no_backups'.tr()), - ), - if (backups.isNotEmpty) - Column( - children: backups.map((backup) { - return ListTile( - onTap: preventActions - ? null - : () { - var nav = getIt(); - nav.showPopUpDialog(BrandAlert( - title: 'providers.backup.restoring' - .tr(), - contentText: - 'providers.backup.restore_alert' - .tr(args: [ - backup.time.toString() - ]), - actions: [ - ActionButton( - text: 'basis.cancel'.tr(), - ), - ActionButton( - onPressed: () => { - context - .read() - .restoreBackup(backup.id) - }, - text: 'modals.yes'.tr(), - ) - ], - )); - }, - title: Text( - MaterialLocalizations.of(context) - .formatShortDate(backup.time) + - ' ' + - TimeOfDay.fromDateTime(backup.time) - .format(context), - ), - ); - }).toList(), - ), - ], - ), + ), + if (backupStatus == BackupStatusEnum.restoring) + ListTile( + title: Text( + 'providers.backup.restoring'.tr( + args: [(backupProgress * 100).round().toString()]), + style: Theme.of(context).textTheme.headline6, ), - if (backupStatus == BackupStatusEnum.error) - BrandText.body1(backupError.toString()), - ], - ), + subtitle: LinearProgressIndicator( + backgroundColor: Colors.grey.withOpacity(0.2), + ), + ), + if (backupStatus == BackupStatusEnum.error) + ListTile( + leading: Icon( + Icons.error_outline, + color: BrandColors.red1, + ), + title: Text( + 'providers.backup.error_pending'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + ), + ], ), - SizedBox(height: 10), - ], + ), + SizedBox(height: 16), + // Card with a list of existing backups + // Each list item has a date + // When clicked, starts the restore action + if (backupStatus != BackupStatusEnum.initializing && + backupStatus != BackupStatusEnum.noKey) + BrandCards.outlined( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + leading: Icon( + Icons.refresh, + ), + title: Text( + 'providers.backup.restore'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + ), + Divider( + height: 1.0, + ), + if (backups.isEmpty) + ListTile( + leading: Icon( + Icons.error_outline, + ), + title: Text('providers.backup.no_backups'.tr()), + ), + if (backups.isNotEmpty) + Column( + children: backups.map((backup) { + return ListTile( + onTap: preventActions + ? null + : () { + var nav = getIt(); + nav.showPopUpDialog(BrandAlert( + title: 'providers.backup.restoring'.tr(), + contentText: 'providers.backup.restore_alert' + .tr(args: [backup.time.toString()]), + actions: [ + ActionButton( + text: 'basis.cancel'.tr(), + ), + ActionButton( + onPressed: () => { + context + .read() + .restoreBackup(backup.id) + }, + text: 'modals.yes'.tr(), + ) + ], + )); + }, + title: Text( + MaterialLocalizations.of(context) + .formatShortDate(backup.time) + + ' ' + + TimeOfDay.fromDateTime(backup.time) + .format(context), + ), + ); + }).toList(), + ), + ], + ), + ), + SizedBox(height: 16), + BrandCards.outlined( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + title: Text( + 'providers.backup.refresh'.tr(), + ), + onTap: refreshing + ? null + : () => {context.read().updateBackups()}, + enabled: !refreshing, + ), + if (providerState != StateType.uninitialized) + Column( + children: [ + Divider( + height: 1.0, + ), + ListTile( + title: Text( + 'providers.backup.refetchBackups'.tr(), + ), + onTap: preventActions + ? null + : () => { + context + .read() + .forceUpdateBackups() + }, + ), + Divider( + height: 1.0, + ), + ListTile( + title: Text( + 'providers.backup.reuploadKey'.tr(), + ), + onTap: preventActions + ? null + : () => {context.read().reuploadKey()}, + ), + ], + ), + ], + ), ), - ), + if (backupStatus == BackupStatusEnum.error) + BrandText.body1(backupError.toString()), + ], ); } } diff --git a/lib/ui/pages/backup_details/header.dart b/lib/ui/pages/backup_details/header.dart deleted file mode 100644 index 0dd93b53..00000000 --- a/lib/ui/pages/backup_details/header.dart +++ /dev/null @@ -1,80 +0,0 @@ -part of 'backup_details.dart'; - -class _Header extends StatelessWidget { - const _Header( - {Key? key, required this.providerState, required this.refreshing}) - : super(key: key); - - final StateType providerState; - final bool refreshing; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - IconStatusMask( - status: providerState, - child: Icon( - BrandIcons.save, - size: 40, - color: Colors.white, - ), - ), - Spacer(), - Padding( - padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: 2, - ), - child: IconButton( - onPressed: refreshing - ? null - : () => {context.read().updateBackups()}, - icon: const Icon(Icons.refresh_rounded), - ), - ), - Padding( - padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: 2, - ), - child: PopupMenuButton<_PopupMenuItemType>( - enabled: providerState != StateType.uninitialized, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - onSelected: (_PopupMenuItemType result) { - switch (result) { - case _PopupMenuItemType.reuploadKey: - context.read().reuploadKey(); - break; - case _PopupMenuItemType.refetchBackups: - context.read().forceUpdateBackups(); - break; - } - }, - icon: Icon(Icons.more_vert), - itemBuilder: (BuildContext context) => [ - PopupMenuItem<_PopupMenuItemType>( - value: _PopupMenuItemType.reuploadKey, - child: Container( - padding: EdgeInsets.only(left: 5), - child: Text('providers.backup.reuploadKey'.tr()), - ), - ), - PopupMenuItem<_PopupMenuItemType>( - value: _PopupMenuItemType.refetchBackups, - child: Container( - padding: EdgeInsets.only(left: 5), - child: Text('providers.backup.refetchBackups'.tr()), - ), - ), - ], - ), - ), - ], - ); - } -} - -enum _PopupMenuItemType { reuploadKey, refetchBackups } diff --git a/lib/ui/pages/dns_details/dns_details.dart b/lib/ui/pages/dns_details/dns_details.dart new file mode 100644 index 00000000..8973e963 --- /dev/null +++ b/lib/ui/pages/dns_details/dns_details.dart @@ -0,0 +1,219 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; + +class DnsDetailsPage extends StatefulWidget { + @override + _DnsDetailsPageState createState() => _DnsDetailsPageState(); +} + +class _DnsDetailsPageState extends State { + Widget _getStateCard(DnsRecordsStatus dnsState, Function fixCallback) { + var description = ''; + var subtitle = ''; + var icon = Icon( + Icons.check, + color: Colors.green, + ); + switch (dnsState) { + case DnsRecordsStatus.uninitialized: + description = 'providers.domain.states.uninitialized'.tr(); + icon = Icon( + Icons.refresh, + ); + break; + case DnsRecordsStatus.refreshing: + description = 'providers.domain.states.refreshing'.tr(); + icon = Icon( + Icons.refresh, + ); + break; + case DnsRecordsStatus.good: + description = 'providers.domain.states.ok'.tr(); + icon = Icon( + Icons.check, + color: Colors.green, + ); + break; + case DnsRecordsStatus.error: + description = 'providers.domain.states.error'.tr(); + subtitle = 'providers.domain.states.error_subtitle'.tr(); + icon = Icon( + Icons.error, + color: Colors.red, + ); + break; + } + return ListTile( + onTap: dnsState == DnsRecordsStatus.error ? () => fixCallback() : null, + title: Text( + description, + style: Theme.of(context).textTheme.headline6, + ), + subtitle: subtitle != '' ? Text(subtitle) : null, + leading: icon, + ); + } + + @override + Widget build(BuildContext context) { + var isReady = context.watch().state is AppConfigFinished; + final domain = getIt().cloudFlareDomain?.domainName ?? ''; + var dnsCubit = context.watch().state; + + print(dnsCubit.dnsState); + + if (!isReady) { + return BrandHeroScreen( + hasBackButton: true, + headerTitle: '', + heroIcon: BrandIcons.globe, + heroTitle: 'providers.domain.screen_title'.tr(), + children: [ + BrandCards.outlined( + child: ListTile( + title: Text( + 'not_ready_card.in_menu'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + ), + ), + ], + ); + } + + return BrandHeroScreen( + hasBackButton: true, + heroSubtitle: domain, + heroIcon: BrandIcons.globe, + heroTitle: 'providers.domain.screen_title'.tr(), + children: [ + BrandCards.outlined( + child: Column( + children: [ + _getStateCard(dnsCubit.dnsState, () { + context.read().fix(); + }), + ], + ), + ), + + SizedBox(height: 16.0), + // Outlined card with a list of A records and their + // status. + BrandCards.outlined( + child: Column( + children: [ + ListTile( + title: Text( + 'providers.domain.cards.services.title'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + subtitle: Text( + 'providers.domain.cards.services.subtitle'.tr(), + style: Theme.of(context).textTheme.caption, + ), + ), + ...dnsCubit.dnsRecords + .where( + (dnsRecord) => + dnsRecord.category == DnsRecordsCategory.services, + ) + .map( + (dnsRecord) => Column( + children: [ + Divider( + height: 1.0, + ), + ListTile( + leading: Icon( + dnsRecord.isSatisfied + ? Icons.check + : dnsCubit.dnsState == + DnsRecordsStatus.refreshing + ? Icons.refresh + : Icons.error, + color: dnsRecord.isSatisfied + ? Colors.green + : dnsCubit.dnsState == + DnsRecordsStatus.refreshing + ? Colors.grey + : Colors.red, + ), + title: Text( + dnsRecord.description.tr(), + style: Theme.of(context).textTheme.labelLarge, + ), + subtitle: Text( + dnsRecord.name, + style: Theme.of(context).textTheme.caption, + ), + ), + ], + ), + ) + .toList(), + ], + ), + ), + SizedBox(height: 16.0), + BrandCards.outlined( + child: Column( + children: [ + ListTile( + title: Text( + 'providers.domain.cards.email.title'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + subtitle: Text( + 'providers.domain.cards.email.subtitle'.tr(), + style: Theme.of(context).textTheme.caption, + ), + ), + ...dnsCubit.dnsRecords + .where( + (dnsRecord) => + dnsRecord.category == DnsRecordsCategory.email, + ) + .map( + (dnsRecord) => Column( + children: [ + Divider( + height: 1.0, + ), + ListTile( + leading: Icon( + dnsRecord.isSatisfied + ? Icons.check + : dnsCubit.dnsState == + DnsRecordsStatus.refreshing + ? Icons.refresh + : Icons.error, + color: dnsRecord.isSatisfied + ? Colors.green + : dnsCubit.dnsState == + DnsRecordsStatus.refreshing + ? Colors.grey + : Colors.red, + ), + title: Text( + dnsRecord.description.tr(), + style: Theme.of(context).textTheme.labelLarge, + ), + ), + ], + ), + ) + .toList(), + ], + ), + ), + ], + ); + } +} diff --git a/lib/ui/pages/initializing/initializing.dart b/lib/ui/pages/initializing/initializing.dart index 958637e3..7ec44f08 100644 --- a/lib/ui/pages/initializing/initializing.dart +++ b/lib/ui/pages/initializing/initializing.dart @@ -1,14 +1,13 @@ import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/initializing/backblaze_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/initializing/cloudflare_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/initializing/domain_cloudflare.dart'; import 'package:selfprivacy/logic/cubit/forms/initializing/hetzner_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/initializing/root_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/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; @@ -19,7 +18,6 @@ import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; 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'; -import 'package:easy_localization/easy_localization.dart'; class InitializingPage extends StatelessWidget { @override @@ -438,7 +436,7 @@ class InitializingPage extends StatelessWidget { } Widget _stepCheck(AppConfigCubit appConfigCubit) { - assert(appConfigCubit.state is AppConfigNotFinished, 'wronge state'); + assert(appConfigCubit.state is AppConfigNotFinished, 'wrong state'); var state = appConfigCubit.state as TimerState; late int doneCount; late String? text; @@ -467,6 +465,22 @@ class InitializingPage extends StatelessWidget { SizedBox(height: 10), BrandText.body2(text), SizedBox(height: 10), + if (doneCount == 0 && state.dnsMatches != null) + Column( + children: state.dnsMatches!.entries.map((entry) { + var domain = entry.key; + var isCorrect = entry.value; + return Row( + children: [ + if (isCorrect) Icon(Icons.check, color: Colors.green), + if (!isCorrect) Icon(Icons.schedule, color: Colors.amber), + SizedBox(width: 10), + Text(domain), + ], + ); + }).toList(), + ), + SizedBox(height: 10), if (!state.isLoading) Row( children: [ diff --git a/lib/ui/pages/more/console/console.dart b/lib/ui/pages/more/console/console.dart index 5b89ee87..2e129f79 100644 --- a/lib/ui/pages/more/console/console.dart +++ b/lib/ui/pages/more/console/console.dart @@ -3,7 +3,6 @@ import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/get_it/console.dart'; import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 3c06ef90..535278a0 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -1,30 +1,21 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:ionicons/ionicons.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/config/text_themes.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; -import 'package:selfprivacy/logic/get_it/ssh.dart'; -import 'package:selfprivacy/logic/models/job.dart'; -import 'package:selfprivacy/logic/models/state_types.dart'; -import 'package:selfprivacy/ui/components/action_button/action_button.dart'; -import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_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/pages/initializing/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/rootRoute.dart'; +import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:share_plus/share_plus.dart'; +import '../../../logic/cubit/users/users_cubit.dart'; import 'about/about.dart'; import 'app_settings/app_setting.dart'; import 'console/console.dart'; @@ -35,9 +26,6 @@ class MorePage extends StatelessWidget { @override Widget build(BuildContext context) { - var jobsCubit = context.watch(); - var isReady = context.watch().state is AppConfigFinished; - return Scaffold( appBar: PreferredSize( child: BrandHeader( @@ -83,73 +71,12 @@ class MorePage extends StatelessWidget { iconData: BrandIcons.terminal, goTo: Console(), ), - _MoreMenuTapItem( - title: 'more.create_ssh_key'.tr(), - iconData: Ionicons.key_outline, - onTap: isReady - ? () { - if (getIt().isSSHKeyGenerated) { - showDialog( - context: context, - builder: (BuildContext context) { - return _SSHExitsDetails( - onShareTap: () { - Share.share( - getIt().savedPrivateKey!); - }, - onDeleteTap: () { - showDialog( - context: context, - builder: (_) { - return BrandAlert( - title: 'modals.3'.tr(), - contentText: - 'more.delete_ssh_text'.tr(), - actions: [ - ActionButton( - text: 'more.yes_delete'.tr(), - isRed: true, - onPressed: () { - getIt().clear(); - Navigator.of(context).pop(); - }), - ActionButton( - text: 'basis.cancel'.tr(), - ), - ], - ); - }, - ); - }, - onCopyTap: () { - Clipboard.setData(ClipboardData( - text: getIt() - .savedPrivateKey!)); - getIt() - .showSnackBar('more.copied_ssh'.tr()); - }, - ); - }, - ); - } else { - showDialog( - context: context, - builder: (BuildContext context) { - return _MoreDetails( - title: 'more.create_ssh_key'.tr(), - icon: Ionicons.key_outline, - onTap: () { - jobsCubit.createShhJobIfNotExist( - CreateSSHKeyJob()); - }, - text: 'more.generate_key_text'.tr(), - ); - }, - ); - } - } - : null, - ), + _NavItem( + title: 'more.create_ssh_key'.tr(), + iconData: Ionicons.key_outline, + goTo: SshKeysPage( + user: context.read().state.rootUser, + )), ], ), ) @@ -159,150 +86,6 @@ class MorePage extends StatelessWidget { } } -class _SSHExitsDetails extends StatelessWidget { - const _SSHExitsDetails({ - Key? key, - required this.onDeleteTap, - required this.onShareTap, - required this.onCopyTap, - }) : super(key: key); - final Function onDeleteTap; - final Function onShareTap; - final Function onCopyTap; - - @override - Widget build(BuildContext context) { - var textStyle = body1Style.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : BrandColors.black); - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: SingleChildScrollView( - child: Container( - width: 350, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: paddingH15V30, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox(height: 10), - Text( - 'more.ssh_key_exist_text'.tr(), - style: textStyle, - ), - SizedBox(height: 10), - Container( - child: BrandButton.text( - onPressed: () { - Navigator.of(context).pop(); - onShareTap(); - }, - title: 'more.share'.tr(), - ), - ), - Container( - alignment: Alignment.centerLeft, - child: BrandButton.text( - onPressed: () { - Navigator.of(context).pop(); - onDeleteTap(); - }, - title: 'basis.delete'.tr(), - ), - ), - Container( - child: BrandButton.text( - onPressed: () { - Navigator.of(context).pop(); - onCopyTap(); - }, - title: 'more.copy_buffer'.tr(), - ), - ), - ], - ), - ) - ], - ), - ), - ), - ); - } -} - -class _MoreDetails extends StatelessWidget { - const _MoreDetails({ - Key? key, - required this.icon, - required this.title, - required this.onTap, - required this.text, - }) : super(key: key); - final String title; - final IconData icon; - final Function onTap; - final String text; - @override - Widget build(BuildContext context) { - var textStyle = body1Style.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : BrandColors.black); - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: SingleChildScrollView( - child: Container( - width: 350, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: paddingH15V30, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IconStatusMask( - status: StateType.stable, - child: Icon(icon, size: 40, color: Colors.white), - ), - SizedBox(height: 10), - BrandText.h2(title), - SizedBox(height: 10), - Text( - text, - style: textStyle, - ), - SizedBox(height: 40), - Center( - child: Container( - child: BrandButton.rised( - onPressed: () { - Navigator.of(context).pop(); - onTap(); - }, - text: 'more.generate_key'.tr(), - ), - ), - ), - ], - ), - ) - ], - ), - ), - ), - ); - } -} - class _NavItem extends StatelessWidget { const _NavItem({ Key? key, @@ -328,30 +111,6 @@ class _NavItem extends StatelessWidget { } } -class _MoreMenuTapItem extends StatelessWidget { - const _MoreMenuTapItem({ - Key? key, - required this.iconData, - required this.onTap, - required this.title, - }) : super(key: key); - - final IconData iconData; - final VoidCallback? onTap; - final String title; - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: _MoreMenuItem( - isActive: onTap != null, - iconData: iconData, - title: title, - ), - ); - } -} - class _MoreMenuItem extends StatelessWidget { const _MoreMenuItem({ Key? key, diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index 3d04290a..dc53ed2f 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -1,11 +1,11 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; -import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; +import 'package:selfprivacy/logic/cubit/dns_records/dns_records_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'; import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; @@ -14,9 +14,9 @@ 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/helpers/modals.dart'; import 'package:selfprivacy/ui/pages/backup_details/backup_details.dart'; +import 'package:selfprivacy/ui/pages/dns_details/dns_details.dart'; import 'package:selfprivacy/ui/pages/server_details/server_details.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:selfprivacy/utils/ui_helpers.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; var navigatorKey = GlobalKey(); @@ -32,6 +32,18 @@ class _ProvidersPageState extends State { Widget build(BuildContext context) { var isReady = context.watch().state is AppConfigFinished; var isBackupInitialized = context.watch().state.isInitialized; + var dnsStatus = context.watch().state.dnsState; + + StateType getDnsStatus() { + if (dnsStatus == DnsRecordsStatus.uninitialized || + dnsStatus == DnsRecordsStatus.refreshing) { + return StateType.uninitialized; + } + if (dnsStatus == DnsRecordsStatus.error) { + return StateType.warning; + } + return StateType.stable; + } final cards = ProviderType.values .map( @@ -42,7 +54,9 @@ class _ProvidersPageState extends State { state: isReady ? (type == ProviderType.backup && !isBackupInitialized ? StateType.uninitialized - : StateType.stable) + : (type == ProviderType.domain) + ? getDnsStatus() + : StateType.stable) : StateType.uninitialized, type: type, ), @@ -102,33 +116,21 @@ class _Card extends StatelessWidget { break; case ProviderType.domain: - title = 'providers.domain.card_title'.tr(); + title = 'providers.domain.screen_title'.tr(); message = domainName; stableText = 'providers.domain.status'.tr(); - onTap = () => showBrandBottomSheet( - context: context, - builder: (BuildContext context) { - return _ProviderDetails( - provider: provider, - statusText: stableText, - ); - }, - ); + onTap = () => Navigator.of(context).push(materialRoute( + DnsDetailsPage(), + )); break; case ProviderType.backup: title = 'providers.backup.card_title'.tr(); stableText = 'providers.backup.status'.tr(); - onTap = () => showBrandBottomSheet( - context: context, - builder: (BuildContext context) { - return BrandBottomSheet( - isExpended: true, - child: BackupDetails(), - ); - }, - ); + onTap = () => Navigator.of(context).push(materialRoute( + BackupDetails(), + )); break; } return GestureDetector( @@ -155,77 +157,3 @@ class _Card extends StatelessWidget { ); } } - -class _ProviderDetails extends StatelessWidget { - const _ProviderDetails({ - Key? key, - required this.provider, - required this.statusText, - }) : super(key: key); - - final ProviderModel provider; - final String? statusText; - - @override - Widget build(BuildContext context) { - late String title; - late List children; - - var config = context.watch().state; - - var domainName = UiHelpers.getDomainName(config); - - switch (provider.type) { - case ProviderType.server: - throw ('wrong type'); - case ProviderType.domain: - title = 'providers.domain.card_title'.tr(); - children = [ - BrandText.body1('providers.domain.bottom_sheet.1'.tr()), - SizedBox(height: 10), - BrandText.body1(domainName), - SizedBox(height: 10), - BrandText.body1('providers.domain.status'.tr()), - ]; - break; - case ProviderType.backup: - title = 'providers.backup.card_title'.tr(); - children = [ - BrandText.body1('providers.backup.bottom_sheet.1'.tr()), - SizedBox(height: 10), - BrandText.body1( - 'providers.backup.bottom_sheet.2'.tr(args: [domainName, 'Time'])), - SizedBox(height: 10), - BrandText.body1('providers.backup.status'.tr()), - ]; - break; - } - return BrandBottomSheet( - child: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 40), - Padding( - padding: paddingH15V0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IconStatusMask( - status: provider.state, - child: Icon(provider.icon, size: 40, color: Colors.white), - ), - SizedBox(height: 10), - BrandText.h1(title), - SizedBox(height: 10), - ...children, - SizedBox(height: 30), - ], - ), - ) - ], - ), - ), - ); - } -} diff --git a/lib/ui/pages/providers/settings/settings.dart b/lib/ui/pages/providers/settings/settings.dart deleted file mode 100644 index 04a71ab1..00000000 --- a/lib/ui/pages/providers/settings/settings.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; -import 'package:selfprivacy/config/brand_theme.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/ui/components/switch_block/switch_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; - -class SettingsPage extends StatelessWidget { - const SettingsPage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListView( - padding: paddingH15V0, - children: [ - SizedBox(height: 10), - BrandHeader(title: 'basis.settings'.tr(), hasBackButton: true), - BrandDivider(), - SwitcherBlock( - onChange: (_) {}, - child: _TextColumn( - title: 'Allow Auto-upgrade', - value: 'Wether to allow automatic packages upgrades', - ), - isActive: true, - ), - SwitcherBlock( - onChange: (_) {}, - child: _TextColumn( - title: 'Reboot after upgrade', - value: 'Reboot without prompt after applying updates', - ), - isActive: false, - ), - _Button( - onTap: () {}, - child: _TextColumn( - title: 'Server Timezone', - value: 'Europe/Kyiv', - ), - ), - _Button( - onTap: () {}, - child: _TextColumn( - title: 'Server Locale', - value: 'Default', - ), - ), - _Button( - onTap: () {}, - child: _TextColumn( - hasWarning: true, - title: 'Factory Reset', - value: 'Restore default settings on your server', - ), - ) - ], - ); - } -} - -class _Button extends StatelessWidget { - const _Button({ - Key? key, - required this.onTap, - required this.child, - }) : super(key: key); - - final Widget child; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Container( - padding: EdgeInsets.only(top: 20, bottom: 5), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - )), - child: child, - ), - ); - } -} - -class _TextColumn extends StatelessWidget { - const _TextColumn({ - Key? key, - required this.title, - required this.value, - this.hasWarning = false, - }) : super(key: key); - - final String title; - final String value; - final bool hasWarning; - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BrandText.body1( - title, - style: TextStyle(color: hasWarning ? BrandColors.warning : null), - ), - SizedBox(height: 5), - BrandText.body1( - value, - style: TextStyle( - fontSize: 13, - height: 1.53, - color: hasWarning ? BrandColors.warning : BrandColors.gray1, - ), - ), - ], - ); - } -} diff --git a/lib/ui/pages/rootRoute.dart b/lib/ui/pages/rootRoute.dart index 426c3627..74a28880 100644 --- a/lib/ui/pages/rootRoute.dart +++ b/lib/ui/pages/rootRoute.dart @@ -1,6 +1,6 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/ui/components/brand_tab_bar/brand_tab_bar.dart'; import 'package:selfprivacy/ui/pages/more/more.dart'; import 'package:selfprivacy/ui/pages/providers/providers.dart'; @@ -30,6 +30,8 @@ class _RootPageState extends State tabController.dispose(); } + var selfprivacyServer = ServerApi(); + @override Widget build(BuildContext context) { return SafeArea( diff --git a/lib/ui/pages/server_details/server_details.dart b/lib/ui/pages/server_details/server_details.dart index 06d6d376..5368df30 100644 --- a/lib/ui/pages/server_details/server_details.dart +++ b/lib/ui/pages/server_details/server_details.dart @@ -8,20 +8,27 @@ import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.da import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart'; import 'package:selfprivacy/logic/models/state_types.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_icons/brand_icons.dart'; +import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart'; import 'package:selfprivacy/ui/components/brand_radio_tile/brand_radio_tile.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/ui/components/switch_block/switch_bloc.dart'; +import 'package:selfprivacy/ui/pages/server_details/time_zone/lang.dart'; import 'package:selfprivacy/utils/named_font_weight.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:timezone/timezone.dart'; import 'cpu_chart.dart'; import 'network_charts.dart'; +import 'package:selfprivacy/utils/extensions/duration.dart'; part 'server_settings.dart'; part 'text_details.dart'; part 'chart.dart'; part 'header.dart'; +part 'time_zone/time_zone.dart'; var navigatorKey = GlobalKey(); @@ -56,57 +63,57 @@ class _ServerDetailsState extends State var isReady = context.watch().state is AppConfigFinished; var providerState = isReady ? StateType.stable : StateType.uninitialized; - return Scaffold( - appBar: PreferredSize( - child: Column( + return BlocProvider( + create: (context) => ServerDetailsCubit()..check(), + child: Scaffold( + appBar: PreferredSize( + child: Column( + children: [ + Container( + height: 51, + alignment: Alignment.center, + padding: EdgeInsets.symmetric(horizontal: 15), + child: BrandText.h4('basis.details'.tr()), + ), + BrandDivider(), + ], + ), + preferredSize: Size.fromHeight(52), + ), + body: TabBarView( + physics: NeverScrollableScrollPhysics(), + controller: tabController, children: [ - Container( - height: 51, - alignment: Alignment.center, - padding: EdgeInsets.symmetric(horizontal: 15), - child: BrandText.h4('basis.details'.tr()), + SingleChildScrollView( + physics: ClampingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: paddingH15V0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Header( + providerState: providerState, + tabController: tabController), + BrandText.body1('providers.server.bottom_sheet.1'.tr()), + ], + ), + ), + SizedBox(height: 10), + BlocProvider( + create: (context) => HetznerMetricsCubit()..restart(), + child: _Chart(), + ), + SizedBox(height: 20), + _TextDetails(), + ], + ), ), - BrandDivider(), + _ServerSettings(tabController: tabController), ], ), - preferredSize: Size.fromHeight(52), - ), - body: TabBarView( - physics: NeverScrollableScrollPhysics(), - controller: tabController, - children: [ - SingleChildScrollView( - physics: ClampingScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: paddingH15V0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Header( - providerState: providerState, - tabController: tabController), - BrandText.body1('providers.server.bottom_sheet.1'.tr()), - ], - ), - ), - SizedBox(height: 10), - BlocProvider( - create: (context) => HetznerMetricsCubit()..restart(), - child: _Chart(), - ), - SizedBox(height: 20), - BlocProvider( - create: (context) => ServerDetailsCubit()..check(), - child: _TextDetails(), - ), - ], - ), - ), - _ServerSettings(tabController: tabController), - ], ), ); } diff --git a/lib/ui/pages/server_details/server_settings.dart b/lib/ui/pages/server_details/server_settings.dart index 6e2ed6b1..c4d6ed02 100644 --- a/lib/ui/pages/server_details/server_settings.dart +++ b/lib/ui/pages/server_details/server_settings.dart @@ -10,6 +10,12 @@ class _ServerSettings extends StatelessWidget { @override Widget build(BuildContext context) { + var serverDetailsState = context.watch().state; + if (serverDetailsState is ServerDetailsNotReady) { + return Text('not ready'); + } else if (serverDetailsState is! Loaded) { + return BrandLoader.horizontal(); + } return ListView( padding: paddingH15V0, children: [ @@ -38,7 +44,7 @@ class _ServerSettings extends StatelessWidget { title: 'Allow Auto-upgrade', value: 'Wether to allow automatic packages upgrades', ), - isActive: true, + isActive: serverDetailsState.autoUpgradeSettings.enable, ), SwitcherBlock( onChange: (_) {}, @@ -46,30 +52,17 @@ class _ServerSettings extends StatelessWidget { title: 'Reboot after upgrade', value: 'Reboot without prompt after applying updates', ), - isActive: false, + isActive: serverDetailsState.autoUpgradeSettings.allowReboot, ), _Button( - onTap: () {}, + onTap: () { + Navigator.of(context).push(materialRoute(SelectTimezone())); + }, child: _TextColumn( title: 'Server Timezone', - value: 'Europe/Kyiv', + value: serverDetailsState.serverTimezone.timezone.name, ), ), - _Button( - onTap: () {}, - child: _TextColumn( - title: 'Server Locale', - value: 'Default', - ), - ), - _Button( - onTap: () {}, - child: _TextColumn( - hasWarning: true, - title: 'Factory Reset', - value: 'Restore default settings on your server', - ), - ) ], ); } diff --git a/lib/ui/pages/server_details/text_details.dart b/lib/ui/pages/server_details/text_details.dart index 936bd00f..a4620f0e 100644 --- a/lib/ui/pages/server_details/text_details.dart +++ b/lib/ui/pages/server_details/text_details.dart @@ -28,7 +28,7 @@ class _TextDetails extends StatelessWidget { TableRow( children: [ getRowTitle('Last check:'), - getRowValue(formater.format(checkTime)), + getRowValue(formatter.format(checkTime)), ], ), TableRow( @@ -168,4 +168,4 @@ class _TempMessage extends StatelessWidget { } } -final DateFormat formater = DateFormat('HH:mm:ss'); +final DateFormat formatter = DateFormat('HH:mm:ss'); diff --git a/lib/ui/pages/server_details/time_zone/lang.dart b/lib/ui/pages/server_details/time_zone/lang.dart new file mode 100644 index 00000000..4ea55019 --- /dev/null +++ b/lib/ui/pages/server_details/time_zone/lang.dart @@ -0,0 +1,431 @@ +final russian = { + "Pacific/Midway": "Мидуэй", + "Pacific/Niue": "Ниуэ", + "Pacific/Pago_Pago": "Паго-Паго", + "America/Adak": "Адак", + "Pacific/Honolulu": "Гонолулу", + "Pacific/Johnston": "Джонстон", + "Pacific/Rarotonga": "Раротонга", + "Pacific/Tahiti": "Таити", + "US/Hawaii": "Гавайи", + "Pacific/Marquesas": "Маркизские острова", + "America/Sitka": "Ситка", + "America/Anchorage": "Анкоридж", + "America/Metlakatla": "Метлакатла", + "America/Juneau": "Джуно", + "US/Alaska": "Аляска", + "America/Nome": "Ном", + "America/Yakutat": "Якутат", + "Pacific/Gambier": "Гамбье", + "America/Tijuana": "Тихуана", + "Pacific/Pitcairn": "Питкэрн", + "US/Pacific": "США/Тихий океан", + "Canada/Pacific": "США/Тихий океан", + "America/Los_Angeles": "Лос-Анджелес", + "America/Vancouver": "Ванкувер", + "America/Santa_Isabel": "Санта-Изабель", + "America/Chihuahua": "Чихуахуа", + "America/Cambridge_Bay": "Кембридж-Бэй", + "America/Inuvik": "Инувик", + "America/Boise": "Бойсе", + "America/Dawson": "Доусон", + "America/Mazatlan": "Масатлан", + "America/Dawson_Creek": "Доусон-Крик", + "US/Arizona": "Аризона", + "America/Denver": "Денвер", + "US/Mountain": "гора", + "America/Edmonton": "Эдмонтон", + "America/Yellowknife": "Йеллоунайф", + "America/Ojinaga": "Охинага", + "America/Phoenix": "Феникс", + "America/Whitehorse": "Белая лошадь", + "Canada/Mountain": "гора", + "America/Hermosillo": "Эрмосильо", + "America/Creston": "Крестон", + "America/Swift_Current": "Свифт Керрент", + "America/Tegucigalpa": "Тегусигальпа", + "America/Regina": "Регина", + "America/Rankin_Inlet": "Ранкин-Инлет", + "America/Rainy_River": "Райни-Ривер", + "America/Winnipeg": "Виннипег", + "America/North_Dakota/Center": "Северная Дакота/Центр", + "America/North_Dakota/Beulah": "Северная Дакота/Беула", + "America/Monterrey": "Монтеррей", + "America/Mexico_City": "Мехико", + "US/Central": "Центральный", + "America/Merida": "Мерида", + "America/Menominee": "Меномини", + "America/Matamoros": "Матаморос", + "America/Managua": "Манагуа", + "America/North_Dakota/New_Salem": "Северная Дакота/Нью-Салем", + "Pacific/Galapagos": "Галапагосские острова", + "America/Indiana/Tell_City": "Индиана/Телл-Сити", + "America/Indiana/Knox": "Индиана/Нокс", + "Canada/Central": "Центральный", + "America/Guatemala": "Гватемала", + "America/El_Salvador": "Сальвадор", + "America/Costa_Rica": "Коста-Рика", + "America/Chicago": "Чикаго", + "America/Belize": "Белиз", + "America/Bahia_Banderas": "Баия де Бандерас", + "America/Resolute": "Резольют", + "America/Atikokan": "Атикокан", + "America/Lima": "Лима", + "America/Bogota": "Богота", + "America/Cancun": "Канкун", + "America/Cayman": "Кайман", + "America/Detroit": "Детройт", + "America/Indiana/Indianapolis": "Индиана/Индианаполис", + "America/Eirunepe": "Эйрунепе", + "America/Grand_Turk": "Гранд-Терк", + "America/Guayaquil": "Гуаякиль", + "America/Havana": "Гавана", + "America/Indiana/Marengo": "Индиана/Маренго", + "America/Indiana/Petersburg": "Индиана/Петербург", + "America/Indiana/Vevay": "Индиана/Вева", + "America/Indiana/Vincennes": "Индиана/Винсеннес", + "America/Indiana/Winamac": "Индиана/Винамак", + "America/Iqaluit": "Икалуит", + "America/Jamaica": "Ямайка", + "America/Kentucky/Louisville": "Кентукки/Луисвилл", + "America/Nassau": "Нассау", + "America/Toronto": "Торонто", + "America/Montreal": "Монреаль", + "America/Pangnirtung": "Пангниртунг", + "America/Port-au-Prince": "Порт-о-Пренс", + "America/Kentucky/Monticello": "Кентукки/Монтичелло", + "Canada/Eastern": "Канада/Восточное", + "US/Eastern": "США/Восточное", + "America/Thunder_Bay": "Тандер-Бей", + "Pacific/Easter": "Пасха", + "America/Panama": "Панама", + "America/Nipigon": "Нипигон", + "America/Rio_Branco": "Рио-Бранко", + "America/New_York": "Нью-Йорк", + "Canada/Atlantic": "Атлантика", + "America/Kralendijk": "Кралендейк", + "America/La_Paz": "Ла-Пас", + "America/Halifax": "Галифакс", + "America/Lower_Princes": "Лоуэр-Принс-Куотер", + "America/Manaus": "Манаус", + "America/Marigot": "Мариго", + "America/Martinique": "Мартиника", + "America/Moncton": "Монктон", + "America/Guyana": "Гайана", + "America/Montserrat": "Монтсеррат", + "America/Guadeloupe": "Гваделупа", + "America/Grenada": "Гренада", + "America/Goose_Bay": "Гуз-Бей", + "America/Glace_Bay": "Глас Бэй", + "America/Curacao": "Кюрасао", + "America/Cuiaba": "Куяба", + "America/Port_of_Spain": "Порт-оф-Спейн", + "America/Porto_Velho": "Порту-Велью", + "America/Puerto_Rico": "Пуэрто-Рико", + "America/Caracas": "Каракас", + "America/Santo_Domingo": "Санто-Доминго", + "America/St_Barthelemy": "Святой Бартелеми", + "Atlantic/Bermuda": "Бермуды", + "America/St_Kitts": "Сент-Китс", + "America/St_Lucia": "Святая Люсия", + "America/St_Thomas": "Сент-Томас", + "America/St_Vincent": "Сент-Винсент", + "America/Thule": "Туле", + "America/Campo_Grande": "Кампу-Гранди", + "America/Boa_Vista": "Боа-Виста", + "America/Tortola": "Тортола", + "America/Aruba": "Аруба", + "America/Blanc-Sablon": "Блан-Саблон", + "America/Barbados": "Барбадос", + "America/Anguilla": "Ангилья", + "America/Antigua": "Антигуа", + "America/Dominica": "Доминика", + "Canada/Newfoundland": "Ньюфаундленд", + "America/St_Johns": "Сент-Джонс", + "America/Sao_Paulo": "Сан-Паулу", + "Atlantic/Stanley": "Стэнли", + "America/Miquelon": "Микелон", + "America/Argentina/Salta": "Аргентина/Сальта", + "America/Montevideo": "Монтевидео", + "America/Argentina/Rio_Gallegos": "Аргентина/Рио-Гальегос", + "America/Argentina/Mendoza": "Аргентина/Мендоса", + "America/Argentina/La_Rioja": "Аргентина/Ла-Риоха", + "America/Argentina/Jujuy": "Аргентина/Жужуй", + "Antarctica/Rothera": "Ротера", + "America/Argentina/Cordoba": "Аргентина/Кордова", + "America/Argentina/Catamarca": "Аргентина/Катамарка", + "America/Argentina/Ushuaia": "Аргентина/Ушуая", + "America/Argentina/Tucuman": "Аргентина/Тукуман", + "America/Paramaribo": "Парамарибо", + "America/Argentina/San_Luis": "Аргентина/Сан-Луис", + "America/Recife": "Ресифи", + "America/Argentina/Buenos_Aires": "Аргентина/Буэнос-Айрес", + "America/Asuncion": "Асунсьон", + "America/Maceio": "Масейо", + "America/Santarem": "Сантарен", + "America/Santiago": "Сантьяго", + "Antarctica/Palmer": "Палмер", + "America/Argentina/San_Juan": "Аргентина/Сан-Хуан", + "America/Fortaleza": "Форталеза", + "America/Cayenne": "Кайенна", + "America/Godthab": "Годтаб", + "America/Belem": "Белен", + "America/Araguaina": "Арагуайна", + "America/Bahia": "Баия", + "Atlantic/South_Georgia": "Южная_Грузия", + "America/Noronha": "Норонья", + "Atlantic/Azores": "Азорские острова", + "Atlantic/Cape_Verde": "Кабо-Верде", + "America/Scoresbysund": "Скорсбисунд", + "Africa/Accra": "Аккра", + "Atlantic/Faroe": "Фарерские острова", + "Europe/Guernsey": "Гернси", + "Africa/Dakar": "Дакар", + "Europe/Isle_of_Man": "Остров Мэн", + "Africa/Conakry": "Конакри", + "Africa/Abidjan": "Абиджан", + "Atlantic/Canary": "канарейка", + "Africa/Banjul": "Банжул", + "Europe/Jersey": "Джерси", + "Atlantic/St_Helena": "Остров Святой Елены", + "Africa/Bissau": "Бисау", + "Europe/London": "Лондон", + "Africa/Nouakchott": "Нуакшот", + "Africa/Lome": "Ломе", + "America/Danmarkshavn": "Данмарксхавн", + "Africa/Ouagadougou": "Уагадугу", + "Europe/Lisbon": "Лиссабон", + "Africa/Sao_Tome": "Сан-Томе", + "Africa/Monrovia": "Монровия", + "Atlantic/Reykjavik": "Рейкьявик", + "Antarctica/Troll": "Тролль", + "Atlantic/Madeira": "Мадейра", + "Africa/Bamako": "Бамако", + "Europe/Dublin": "Дублин", + "Africa/Freetown": "Фритаун", + "Europe/Monaco": "Монако", + "Europe/Skopje": "Скопье", + "Europe/Amsterdam": "Амстердам", + "Africa/Tunis": "Тунис", + "Arctic/Longyearbyen": "Лонгйир", + "Africa/Bangui": "Банги", + "Africa/Lagos": "Лагос", + "Africa/Douala": "Дуала", + "Africa/Libreville": "Либревиль", + "Europe/Belgrade": "Белград", + "Europe/Stockholm": "Стокгольм", + "Europe/Berlin": "Берлин", + "Europe/Zurich": "Цюрих", + "Europe/Zagreb": "Загреб", + "Europe/Warsaw": "Варшава", + "Africa/Luanda": "Луанда", + "Africa/Porto-Novo": "Порто-Ново", + "Africa/Brazzaville": "Браззавиль", + "Europe/Vienna": "Вена", + "Europe/Vatican": "Ватикан", + "Europe/Vaduz": "Вадуц", + "Europe/Tirane": "Тиран", + "Europe/Bratislava": "Братислава", + "Europe/Brussels": "Брюссель", + "Europe/Paris": "Париж", + "Europe/Sarajevo": "Сараево", + "Europe/San_Marino": "Сан-Марино", + "Europe/Rome": "Рим", + "Africa/El_Aaiun": "Эль-Аайун", + "Africa/Casablanca": "Касабланка", + "Europe/Malta": "Мальта", + "Africa/Ceuta": "Сеута", + "Europe/Gibraltar": "Гибралтар", + "Africa/Malabo": "Малабо", + "Europe/Busingen": "Бузинген", + "Africa/Ndjamena": "Нджамена", + "Europe/Andorra": "Андорра", + "Europe/Oslo": "Осло", + "Europe/Luxembourg": "Люксембург", + "Africa/Niamey": "Ниамей", + "Europe/Copenhagen": "Копенгаген", + "Europe/Madrid": "Мадрид", + "Europe/Budapest": "Будапешт", + "Africa/Algiers": "Алжир", + "Europe/Ljubljana": "Любляна", + "Europe/Podgorica": "Подгорица", + "Africa/Kinshasa": "Киншаса", + "Europe/Prague": "Прага", + "Europe/Riga": "Рига", + "Africa/Bujumbura": "Бужумбура", + "Africa/Lubumbashi": "Лубумбаши", + "Europe/Bucharest": "Бухарест", + "Africa/Blantyre": "Блантайр", + "Asia/Nicosia": "Никосия", + "Europe/Sofia": "София", + "Asia/Jerusalem": "Иерусалим", + "Europe/Tallinn": "Таллинн", + "Europe/Uzhgorod": "Ужгород", + "Africa/Lusaka": "Лусака", + "Europe/Mariehamn": "Мариехамн", + "Asia/Hebron": "Хеврон", + "Asia/Gaza": "Газа", + "Asia/Damascus": "Дамаск", + "Europe/Zaporozhye": "Запорожье", + "Asia/Beirut": "Бейрут", + "Africa/Juba": "Джуба", + "Africa/Harare": "Хараре", + "Europe/Athens": "Афины", + "Europe/Kiev": "Киев", + "Europe/Kaliningrad": "Калининград", + "Africa/Khartoum": "Хартум", + "Africa/Cairo": "Каир", + "Africa/Kigali": "Кигали", + "Asia/Amman": "Амман", + "Africa/Maputo": "Мапуту", + "Africa/Gaborone": "Габороне", + "Africa/Tripoli": "Триполи", + "Africa/Maseru": "Масеру", + "Africa/Windhoek": "Виндхук", + "Africa/Johannesburg": "Йоханнесбург", + "Europe/Chisinau": "Кишинев", + "Africa/Mbabane": "Мбабане", + "Europe/Vilnius": "Вильнюс", + "Europe/Helsinki": "Хельсинки", + "Europe/Moscow": "Москва", + "Africa/Kampala": "Кампала", + "Africa/Nairobi": "Найроби", + "Africa/Asmara": "Асмэра", + "Europe/Istanbul": "Стамбул", + "Asia/Riyadh": "Эр-Рияд", + "Asia/Qatar": "Катар", + "Europe/Minsk": "Минск", + "Indian/Comoro": "Коморо", + "Asia/Kuwait": "Кувейт", + "Africa/Addis_Ababa": "Аддис-Абеба", + "Africa/Dar_es_Salaam": "Дар-эс-Салам", + "Europe/Volgograd": "Волгоград", + "Indian/Antananarivo": "Антананариву", + "Asia/Bahrain": "Бахрейн", + "Asia/Baghdad": "Багдад", + "Indian/Mayotte": "Майотта", + "Africa/Djibouti": "Джибути", + "Europe/Simferopol": "Симферополь", + "Asia/Aden": "Аден", + "Antarctica/Syowa": "Сёва", + "Africa/Mogadishu": "Могадишо", + "Asia/Tehran": "Тегеран", + "Asia/Yerevan": "Ереван", + "Asia/Tbilisi": "Тбилиси", + "Asia/Muscat": "Мускат", + "Europe/Samara": "Самара", + "Indian/Mahe": "Маэ", + "Asia/Baku": "Баку", + "Indian/Mauritius": "Маврикий", + "Indian/Reunion": "Воссоединение", + "Asia/Dubai": "Дубай", + "Asia/Kabul": "Кабул", + "Asia/Ashgabat": "Ашхабад", + "Antarctica/Mawson": "Моусон", + "Asia/Aqtau": "Актау", + "Asia/Yekaterinburg": "Екатеринбург", + "Asia/Aqtobe": "Актобе", + "Asia/Dushanbe": "Душанбе", + "Asia/Tashkent": "Ташкент", + "Asia/Samarkand": "Самарканд", + "Asia/Qyzylorda": "Кызылорда", + "Asia/Oral": "Оральный", + "Asia/Karachi": "Карачи", + "Indian/Kerguelen": "Кергелен", + "Indian/Maldives": "Мальдивы", + "Asia/Kolkata": "Калькутта", + "Asia/Colombo": "Коломбо", + "Asia/Kathmandu": "Катманду", + "Antarctica/Vostok": "Восток", + "Asia/Almaty": "Алматы", + "Asia/Urumqi": "Урумчи", + "Asia/Thimphu": "Тхимпху", + "Asia/Omsk": "Омск", + "Asia/Dhaka": "Дакка", + "Indian/Chagos": "Чагос", + "Asia/Bishkek": "Бишкек", + "Asia/Rangoon": "Рангун", + "Indian/Cocos": "кокосы", + "Asia/Bangkok": "Бангкок", + "Asia/Hovd": "Ховд", + "Asia/Novokuznetsk": "Новокузнецк", + "Asia/Vientiane": "Вьентьян", + "Asia/Krasnoyarsk": "Красноярск", + "Antarctica/Davis": "Дэвис", + "Asia/Novosibirsk": "Новосибирск", + "Asia/Phnom_Penh": "Пномпень", + "Asia/Pontianak": "Понтианак", + "Asia/Jakarta": "Джакарта", + "Asia/Ho_Chi_Minh": "Хо Ши Мин", + "Indian/Christmas": "Рождество", + "Asia/Manila": "Манила", + "Asia/Makassar": "Макассар", + "Asia/Macau": "Макао", + "Asia/Kuala_Lumpur": "Куала-Лумпур", + "Asia/Singapore": "Сингапур", + "Asia/Shanghai": "Шанхай", + "Asia/Irkutsk": "Иркутск", + "Asia/Kuching": "Кучинг", + "Asia/Hong_Kong": "Гонконг", + "Australia/Perth": "Перт", + "Asia/Taipei": "Тайбэй", + "Asia/Brunei": "Бруней", + "Asia/Choibalsan": "Чойбалсан", + "Asia/Ulaanbaatar": "Улан-Батор", + "Australia/Eucla": "Евкла", + "Asia/Yakutsk": "Якутск", + "Asia/Dili": "Дили", + "Pacific/Palau": "Палау", + "Asia/Jayapura": "Джаяпура", + "Asia/Seoul": "Сеул", + "Asia/Pyongyang": "Пхеньян", + "Asia/Khandyga": "Хандыга", + "Asia/Chita": "Чита", + "Asia/Tokyo": "Токио", + "Australia/Darwin": "Дарвин", + "Pacific/Saipan": "Сайпан", + "Australia/Brisbane": "Брисбен", + "Pacific/Port_Moresby": "Порт-Морсби", + "Pacific/Chuuk": "Чуук", + "Antarctica/DumontDUrville": "Дюмон-д'Юрвиль", + "Pacific/Guam": "Гуам", + "Australia/Lindeman": "Линдеман", + "Asia/Ust-Nera": "Усть-Нера", + "Asia/Vladivostok": "Владивосток", + "Australia/Broken_Hill": "Брокен-Хилл", + "Australia/Adelaide": "Аделаида", + "Asia/Sakhalin": "Сахалин", + "Pacific/Guadalcanal": "Гуадалканал", + "Pacific/Efate": "Эфате", + "Antarctica/Casey": "Кейси", + "Antarctica/Macquarie": "Маккуори", + "Pacific/Kosrae": "Косрае", + "Australia/Sydney": "Сидней", + "Pacific/Noumea": "Нумеа", + "Australia/Melbourne": "Мельбурн", + "Australia/Lord_Howe": "Остров Лорд-Хау", + "Australia/Hobart": "Хобарт", + "Pacific/Pohnpei": "Понпеи", + "Australia/Currie": "Карри", + "Asia/Srednekolymsk": "Среднеколымск", + "Asia/Magadan": "Магадан", + "Pacific/Kwajalein": "Кваджалейн", + "Pacific/Majuro": "Маджуро", + "Pacific/Funafuti": "Фунафути", + "Asia/Anadyr": "Анадырь", + "Pacific/Nauru": "Науру", + "Asia/Kamchatka": "Камчатка", + "Pacific/Fiji": "Фиджи", + "Pacific/Norfolk": "Норфолк", + "Pacific/Tarawa": "Тарава", + "Pacific/Wallis": "Уоллис", + "Pacific/Wake": "Будить", + "Pacific/Tongatapu": "Тонгатапу", + "Antarctica/McMurdo": "МакМердо", + "Pacific/Enderbury": "Эндербери", + "Pacific/Fakaofo": "Факаофо", + "Pacific/Auckland": "Окленд", + "Pacific/Chatham": "Чатем", + "Pacific/Kiritimati": "Киритимати", + "Pacific/Apia": "Апиа", +}; diff --git a/lib/ui/pages/server_details/time_zone/time_zone.dart b/lib/ui/pages/server_details/time_zone/time_zone.dart new file mode 100644 index 00000000..04f192c0 --- /dev/null +++ b/lib/ui/pages/server_details/time_zone/time_zone.dart @@ -0,0 +1,107 @@ +part of '../server_details.dart'; + +final List locations = timeZoneDatabase.locations.values.toList() + ..sort((l1, l2) => + l1.currentTimeZone.offset.compareTo(l2.currentTimeZone.offset)); + +class SelectTimezone extends StatefulWidget { + SelectTimezone({Key? key}) : super(key: key); + + @override + _SelectTimezoneState createState() => _SelectTimezoneState(); +} + +class _SelectTimezoneState extends State { + final ScrollController controller = ScrollController(); + + @override + void initState() { + WidgetsBinding.instance!.addPostFrameCallback(_afterLayout); + super.initState(); + } + + void _afterLayout(_) { + var t = DateTime.now().timeZoneOffset; + var index = locations.indexWhere((element) => + Duration(milliseconds: element.currentTimeZone.offset) == t); + print(t); + + if (index >= 0) { + controller.animateTo(60.0 * index, + duration: Duration(milliseconds: 300), curve: Curves.easeIn); + } + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: PreferredSize( + child: BrandHeader( + title: 'select timezone', + hasBackButton: true, + ), + preferredSize: Size.fromHeight(52), + ), + body: ListView( + controller: controller, + children: locations + .asMap() + .map((key, value) { + var duration = + Duration(milliseconds: value.currentTimeZone.offset); + var area = value.currentTimeZone.abbreviation + .replaceAll(RegExp(r'[\d+()-]'), ''); + + String timezoneName = value.name; + if (context.locale.toString() == 'ru') { + timezoneName = russian[value.name] ?? + () { + var arr = value.name.split('/')..removeAt(0); + return arr.join('/'); + }(); + } + + return MapEntry( + key, + Container( + height: 60, + padding: EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + BrandText.body1( + timezoneName, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + BrandText.small( + 'GMT ${duration.toDayHourMinuteFormat()} ${area.isNotEmpty ? '($area)' : ''}', + style: TextStyle( + fontSize: 13, + )), + ], + ), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BrandColors.dividerColor, + )), + ), + ), + ); + }) + .values + .toList(), + ), + ); + } +} diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index 410d5048..5070bd81 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -101,13 +101,13 @@ class _Card extends StatelessWidget { var jobsCubit = context.watch(); var jobState = jobsCubit.state; - var switchebleService = switchableServices.contains(serviceType); - var hasSwitchJob = switchebleService && + var switchableService = switchableServices.contains(serviceType); + var hasSwitchJob = switchableService && jobState is JobsStateWithJobs && jobState.jobList .any((el) => el is ServiceToggleJob && el.type == serviceType); - var isSwithOn = isReady && + var isSwitchOn = isReady && (!switchableServices.contains(serviceType) || serviceState.isEnableByType(serviceType)); @@ -115,7 +115,7 @@ class _Card extends StatelessWidget { var domainName = UiHelpers.getDomainName(config); return GestureDetector( - onTap: isSwithOn + onTap: isSwitchOn ? () => showDialog( context: context, // isScrollControlled: true, @@ -124,7 +124,7 @@ class _Card extends StatelessWidget { return _ServiceDetails( serviceType: serviceType, status: - isSwithOn ? StateType.stable : StateType.uninitialized, + isSwitchOn ? StateType.stable : StateType.uninitialized, title: serviceType.title, icon: serviceType.icon, changeTab: changeTab, @@ -140,10 +140,10 @@ class _Card extends StatelessWidget { children: [ IconStatusMask( status: - isSwithOn ? StateType.stable : StateType.uninitialized, + isSwitchOn ? StateType.stable : StateType.uninitialized, child: Icon(serviceType.icon, size: 30, color: Colors.white), ), - if (isReady && switchebleService) ...[ + if (isReady && switchableService) ...[ Spacer(), Builder( builder: (context) { diff --git a/lib/ui/pages/ssh_keys/new_ssh_key.dart b/lib/ui/pages/ssh_keys/new_ssh_key.dart new file mode 100644 index 00000000..4d7a3625 --- /dev/null +++ b/lib/ui/pages/ssh_keys/new_ssh_key.dart @@ -0,0 +1,76 @@ +part of 'ssh_keys.dart'; + +class _NewSshKey extends StatelessWidget { + final User user; + + _NewSshKey(this.user); + + @override + Widget build(BuildContext context) { + return BrandBottomSheet( + child: BlocProvider( + create: (context) { + var jobCubit = context.read(); + var jobState = jobCubit.state; + if (jobState is JobsStateWithJobs) { + var jobs = jobState.jobList; + jobs.forEach((job) { + if (job is CreateSSHKeyJob && job.user.login == user.login) { + user.sshKeys.add(job.publicKey); + } + }); + } + return SshFormCubit( + jobsCubit: jobCubit, + user: user, + ); + }, + child: Builder(builder: (context) { + var formCubitState = context.watch().state; + + return BlocListener( + listener: (context, state) { + if (state.isSubmitted) { + Navigator.pop(context); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + BrandHeader( + title: user.login, + ), + SizedBox(width: 14), + Padding( + padding: paddingH15V0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IntrinsicHeight( + child: CubitFormTextField( + formFieldCubit: context.read().key, + decoration: InputDecoration( + labelText: 'ssh.input_label'.tr(), + ), + ), + ), + SizedBox(height: 30), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'ssh.create'.tr(), + ), + SizedBox(height: 30), + ], + ), + ), + ], + ), + ); + }), + ), + ); + } +} diff --git a/lib/ui/pages/ssh_keys/ssh_keys.dart b/lib/ui/pages/ssh_keys/ssh_keys.dart new file mode 100644 index 00000000..b67087c6 --- /dev/null +++ b/lib/ui/pages/ssh_keys/ssh_keys.dart @@ -0,0 +1,143 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/forms/user/ssh_form_cubit.dart'; +import 'package:selfprivacy/logic/models/job.dart'; +import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; + +import '../../../config/brand_colors.dart'; +import '../../../config/brand_theme.dart'; +import '../../../logic/cubit/jobs/jobs_cubit.dart'; +import '../../../logic/models/user.dart'; +import '../../components/brand_button/brand_button.dart'; +import '../../components/brand_header/brand_header.dart'; + +part 'new_ssh_key.dart'; + +// Get user object as a parameter +class SshKeysPage extends StatefulWidget { + final User user; + + SshKeysPage({Key? key, required this.user}) : super(key: key); + + @override + _SshKeysPageState createState() => _SshKeysPageState(); +} + +class _SshKeysPageState extends State { + @override + Widget build(BuildContext context) { + return BrandHeroScreen( + heroTitle: 'ssh.title'.tr(), + heroSubtitle: widget.user.login, + heroIcon: BrandIcons.key, + children: [ + if (widget.user.login == 'root') + Column( + children: [ + // Show alert card if user is root + BrandCards.outlined( + child: ListTile( + leading: Icon( + Icons.warning_rounded, + color: Theme.of(context).colorScheme.error, + ), + title: Text('ssh.root.title'.tr()), + subtitle: Text('ssh.root.subtitle'.tr()), + ), + ) + ], + ), + BrandCards.outlined( + child: Column( + children: [ + ListTile( + title: Text( + 'ssh.create'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + leading: Icon(Icons.add_circle_outline_rounded), + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: _NewSshKey(widget.user)); + }, + ); + }, + ), + Divider(height: 0), + // show a list of ListTiles with ssh keys + // Clicking on one should delete it + Column( + children: widget.user.sshKeys.map((key) { + final publicKey = + key.split(' ').length > 1 ? key.split(' ')[1] : key; + final keyType = key.split(' ')[0]; + final keyName = key.split(' ').length > 2 + ? key.split(' ')[2] + : 'ssh.no_key_name'.tr(); + return ListTile( + title: Text('$keyName ($keyType)'), + // do not overflow text + subtitle: Text(publicKey, + maxLines: 1, overflow: TextOverflow.ellipsis), + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('ssh.delete'.tr()), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text('ssh.delete_confirm_question'.tr()), + Text('$keyName ($keyType)'), + Text(publicKey), + ], + ), + ), + actions: [ + TextButton( + child: Text('basis.cancel'.tr()), + onPressed: () { + Navigator.of(context)..pop(); + }, + ), + TextButton( + child: Text( + 'basis.delete'.tr(), + style: TextStyle( + color: BrandColors.red1, + ), + ), + onPressed: () { + context.read().addJob( + DeleteSSHKeyJob( + user: widget.user, publicKey: key)); + Navigator.of(context) + ..pop() + ..pop(); + }, + ), + ], + ); + }, + ); + }); + }).toList(), + ) + ], + ), + ), + ], + ); + } +} diff --git a/lib/ui/pages/users/user.dart b/lib/ui/pages/users/user.dart index 1d1fb9a8..a748a374 100644 --- a/lib/ui/pages/users/user.dart +++ b/lib/ui/pages/users/user.dart @@ -34,7 +34,11 @@ class _User extends StatelessWidget { Flexible( child: isRootUser ? BrandText.h4Underlined(user.login) - : BrandText.h4(user.login), + // cross out text if user not found on server + : BrandText.h4(user.login, + style: user.isFoundOnServer + ? null + : TextStyle(decoration: TextDecoration.lineThrough)), ), ], ), diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index 89d5d97f..a40a7b9a 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -141,25 +141,44 @@ class _UserDetails extends StatelessWidget { alignment: Alignment.centerLeft, child: BrandText.h4('${user.login}@$domainName'), ), - SizedBox(height: 14), - BrandText.small('basis.password'.tr()), - Container( - height: 40, - alignment: Alignment.centerLeft, - child: BrandText.h4(user.password), - ), + if (user.password != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 14), + BrandText.small('basis.password'.tr()), + Container( + height: 40, + alignment: Alignment.centerLeft, + child: BrandText.h4(user.password), + ), + ], + ), SizedBox(height: 24), BrandDivider(), SizedBox(height: 20), - BrandButton.emptyWithIconText( - title: 'users.send_regisration_data'.tr(), - icon: Icon(BrandIcons.share), - onPressed: () { + ListTile( + onTap: () { + Navigator.of(context) + .push(materialRoute(SshKeysPage(user: user))); + }, + title: Text('ssh.title'.tr()), + subtitle: user.sshKeys.length > 0 + ? Text('ssh.subtitle_with_keys' + .tr(args: [user.sshKeys.length.toString()])) + : Text('ssh.subtitle_without_keys'.tr()), + trailing: Icon(BrandIcons.key)), + SizedBox(height: 20), + ListTile( + onTap: () { Share.share( 'login: ${user.login}, password: ${user.password}'); }, + title: Text( + 'users.send_registration_data'.tr(), + ), + trailing: Icon(BrandIcons.share), ), - SizedBox(height: 20), ], ), ) diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 306b7479..72c519a0 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -1,10 +1,9 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:hive/hive.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/config/text_themes.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/user/user_form_cubit.dart'; @@ -19,44 +18,45 @@ 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/not_ready_card/not_ready_card.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/ui/helpers/modals.dart'; +import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart'; import 'package:selfprivacy/utils/ui_helpers.dart'; import 'package:share_plus/share_plus.dart'; +import '../../../utils/route_transitions/basic.dart'; + +part 'empty.dart'; part 'fab.dart'; part 'new_user.dart'; -part 'user_details.dart'; part 'user.dart'; -part 'empty.dart'; +part 'user_details.dart'; class UsersPage extends StatelessWidget { const UsersPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - final usersCubitState = context.watch().state; + // final usersCubitState = context.watch().state; var isReady = context.watch().state is AppConfigFinished; - final users = [...usersCubitState.users]; - //Todo: listen box events - User? user = Hive.box(BNames.appConfig).get(BNames.rootUser); - if (user != null) { - users.insert(0, user); - } - final isEmpty = users.isEmpty; + // final primaryUser = usersCubitState.primaryUser; + // final users = [primaryUser, ...usersCubitState.users]; + // final isEmpty = users.isEmpty; Widget child; if (!isReady) { child = isNotReady(); } else { - child = isEmpty - ? Container( - alignment: Alignment.center, - child: _NoUsers( - text: 'users.add_new_user'.tr(), - ), - ) - : ListView.builder( + child = BlocBuilder( + builder: (context, state) { + print('Rebuild users page'); + final primaryUser = state.primaryUser; + final users = [primaryUser, ...state.users]; + + return RefreshIndicator( + onRefresh: () async { + context.read().refresh(); + }, + child: ListView.builder( itemCount: users.length, itemBuilder: (BuildContext context, int index) { return _User( @@ -64,7 +64,10 @@ class UsersPage extends StatelessWidget { isRootUser: index == 0, ); }, - ); + ), + ); + }, + ); } return Scaffold( diff --git a/lib/utils/color_utils.dart b/lib/utils/color_utils.dart index 2a2da026..6785c171 100644 --- a/lib/utils/color_utils.dart +++ b/lib/utils/color_utils.dart @@ -2,13 +2,13 @@ import 'package:flutter/material.dart'; Color stringToColor(String string) { var number = string.codeUnits.reduce((a, b) => a + b); - var index = number % colorPallete.length; - return colorPallete[index]; + var index = number % colorPalette.length; + return colorPalette[index]; } var originalColor = Color(0xFFDBD8BD); var count = 40; -var colorPallete = List.generate( +var colorPalette = List.generate( count, (index) => HSLColor.fromColor(originalColor) .withHue((index) * 360.0 / count) diff --git a/lib/utils/extensions/duration.dart b/lib/utils/extensions/duration.dart new file mode 100644 index 00000000..a81627c0 --- /dev/null +++ b/lib/utils/extensions/duration.dart @@ -0,0 +1,41 @@ +// ignore_for_file: unnecessary_this + +extension DurationFormatter on Duration { + String toDayHourMinuteSecondFormat() { + return [ + this.inHours.remainder(24), + this.inMinutes.remainder(60), + this.inSeconds.remainder(60) + ].map((seg) { + return seg.toString().padLeft(2, '0'); + }).join(':'); + } + + String toDayHourMinuteFormat() { + var designator = this >= Duration.zero ? '+' : '-'; + + var segments = [ + this.inHours.remainder(24).abs(), + this.inMinutes.remainder(60).abs(), + ].map((seg) { + return seg.toString().padLeft(2, '0'); + }); + + return '$designator${segments.first}:${segments.last}'; + } + + String toHoursMinutesSecondsFormat() { + // WAT: https://flutterigniter.com/how-to-format-duration/ + return this.toString().split('.').first.padLeft(8, "0"); + } + + String toDayHourMinuteFormat2() { + var segments = [ + this.inHours.remainder(24), + this.inMinutes.remainder(60), + ].map((seg) { + return seg.toString().padLeft(2, '0'); + }); + return segments.first + " h" + " " + segments.last + " min"; + } +} diff --git a/lib/utils/extensions/text_extensions.dart b/lib/utils/extensions/text_extensions.dart index 7e378d0c..26932a11 100644 --- a/lib/utils/extensions/text_extensions.dart +++ b/lib/utils/extensions/text_extensions.dart @@ -1,4 +1,3 @@ -import 'dart:ui'; import 'package:flutter/cupertino.dart'; extension TextExtension on Text { diff --git a/pubspec.lock b/pubspec.lock index f4a8a397..24f40613 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,63 +7,63 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "22.0.0" + version: "31.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "1.7.2" + version: "2.8.0" archive: dependency: transitive description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.1.2" + version: "3.2.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.3.0" asn1lib: dependency: transitive description: name: asn1lib url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.1.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" auto_size_text: dependency: "direct main" description: name: auto_size_text url: "https://pub.dartlang.org" source: hosted - version: "3.0.0-nullsafety.0" + version: "3.0.0" basic_utils: dependency: "direct main" description: name: basic_utils url: "https://pub.dartlang.org" source: hosted - version: "3.5.0" + version: "4.2.0" bloc: dependency: transitive description: name: bloc url: "https://pub.dartlang.org" source: hosted - version: "7.2.1" + version: "8.0.2" boolean_selector: dependency: transitive description: @@ -77,7 +77,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.1" build_config: dependency: transitive description: @@ -91,49 +91,49 @@ packages: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" build_resolvers: dependency: transitive description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.6" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.7" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.1.0" + version: "7.2.3" built_collection: dependency: transitive description: name: built_collection url: "https://pub.dartlang.org" source: hosted - version: "5.1.0" + version: "5.1.1" built_value: dependency: transitive description: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.1.2" + version: "8.1.4" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -154,7 +154,7 @@ packages: name: cli_util url: "https://pub.dartlang.org" source: hosted - version: "0.3.3" + version: "0.3.5" clock: dependency: transitive description: @@ -196,7 +196,7 @@ packages: name: crypt url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.2.1" crypto: dependency: transitive description: @@ -210,28 +210,28 @@ packages: name: cubit_form url: "https://pub.dartlang.org" source: hosted - version: "1.0.18" + version: "2.0.1" cupertino_icons: dependency: "direct main" description: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.4" dart_style: dependency: transitive description: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.2.1" dio: dependency: "direct main" description: name: dio url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.4" easy_localization: dependency: "direct main" description: @@ -267,13 +267,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.3" - extended_masked_text: - dependency: transitive - description: - name: extended_masked_text - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.1" fake_async: dependency: transitive description: @@ -308,7 +301,7 @@ packages: name: fl_chart url: "https://pub.dartlang.org" source: hosted - version: "0.40.0" + version: "0.45.0" flutter: dependency: "direct main" description: flutter @@ -320,7 +313,7 @@ packages: name: flutter_bloc url: "https://pub.dartlang.org" source: hosted - version: "7.3.3" + version: "8.0.1" flutter_launcher_icons: dependency: "direct dev" description: @@ -339,21 +332,56 @@ packages: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.6.5" + version: "0.6.9" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.5" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage url: "https://pub.dartlang.org" source: hosted - version: "4.2.1" + version: "5.0.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -384,21 +412,21 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" graphs: dependency: transitive description: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" hive: dependency: "direct main" description: name: hive url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" hive_flutter: dependency: "direct main" description: @@ -412,21 +440,21 @@ packages: name: hive_generator url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.2" http: dependency: transitive description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.13.3" + version: "0.13.4" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.2.0" http_parser: dependency: transitive description: @@ -440,7 +468,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.1" intl: dependency: transitive description: @@ -475,42 +503,56 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.4.0" json_serializable: dependency: "direct dev" description: name: json_serializable url: "https://pub.dartlang.org" source: hosted - version: "4.1.4" + version: "6.1.4" local_auth: dependency: "direct main" description: name: local_auth url: "https://pub.dartlang.org" source: hosted - version: "1.1.7" + version: "1.1.11" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" markdown: dependency: transitive description: name: markdown url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.1" + mask_text_input_formatter: + dependency: transitive + description: + name: mask_text_input_formatter + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: @@ -524,7 +566,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" modal_bottom_sheet: dependency: "direct main" description: @@ -559,7 +601,7 @@ packages: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" package_info: dependency: "direct main" description: @@ -580,70 +622,77 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.9" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.5" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.5" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.1" + version: "2.0.5" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.4.0" platform: dependency: transitive description: name: platform url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.2" pointycastle: dependency: "direct main" description: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.3.2" + version: "3.5.1" pool: dependency: transitive description: @@ -664,35 +713,35 @@ packages: name: process url: "https://pub.dartlang.org" source: hosted - version: "4.2.3" + version: "4.2.4" provider: dependency: "direct main" description: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.0" + version: "6.0.2" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" pubspec_parse: dependency: transitive description: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.2.0" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.1+1" rsa_encrypt: dependency: "direct main" description: @@ -706,7 +755,7 @@ packages: name: share_plus url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "3.0.5" share_plus_linux: dependency: transitive description: @@ -748,21 +797,35 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.0.13" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" shared_preferences_platform_interface: dependency: transitive description: @@ -776,14 +839,14 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" shelf: dependency: transitive description: @@ -823,14 +886,14 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.2.1" source_helper: dependency: transitive description: name: source_helper url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.3.1" source_map_stack_trace: dependency: transitive description: @@ -858,7 +921,7 @@ packages: name: ssh_key url: "https://pub.dartlang.org" source: hosted - version: "0.7.0" + version: "0.7.1" stack_trace: dependency: transitive description: @@ -900,21 +963,28 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.17.10" + version: "1.19.5" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.8" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.0" + version: "0.4.9" + timezone: + dependency: "direct main" + description: + name: timezone + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.0" timing: dependency: transitive description: @@ -942,98 +1012,112 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.9" + version: "6.0.20" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.15" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.15" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.0.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.0.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.8" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.0" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" vm_service: dependency: transitive description: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "6.2.0" + version: "7.5.0" wakelock: dependency: "direct main" description: name: wakelock url: "https://pub.dartlang.org" source: hosted - version: "0.5.3+3" + version: "0.6.1+1" wakelock_macos: dependency: transitive description: name: wakelock_macos url: "https://pub.dartlang.org" source: hosted - version: "0.1.0+2" + version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "0.2.1+2" + version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web url: "https://pub.dartlang.org" source: hosted - version: "0.2.0+2" + version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows url: "https://pub.dartlang.org" source: hosted - version: "0.1.0+1" + version: "0.2.0" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" web_socket_channel: dependency: transitive description: @@ -1054,21 +1138,21 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.2.7" + version: "2.4.1" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.2.0+1" xml: dependency: transitive description: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.1.2" + version: "5.3.1" yaml: dependency: transitive description: @@ -1077,5 +1161,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.13.4 <3.0.0" - flutter: ">=2.5.0" + dart: ">=2.15.0 <3.0.0" + flutter: ">=2.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 88a6a95d..48e0a837 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,53 +1,54 @@ name: selfprivacy description: selfprivacy.org publish_to: 'none' -version: 0.4.2+10 +version: 0.5.0+11 environment: sdk: '>=2.13.4 <3.0.0' - flutter: ">=2.5.0" + flutter: ">=2.10.0" dependencies: - flutter: - sdk: flutter - crypt: ^4.0.1 - cubit_form: ^1.0.0-nullsafety.0 - cupertino_icons: ^1.0.2 - dio: ^4.0.0-beta7 + auto_size_text: ^3.0.0 + basic_utils: ^4.2.0 + crypt: ^4.2.1 + cubit_form: ^2.0.1 + cupertino_icons: ^1.0.4 + dio: ^4.0.4 easy_localization: ^3.0.0 either_option: ^2.0.1-dev.1 equatable: ^2.0.3 - fl_chart: ^0.40.0 - flutter_bloc: ^7.3.3 - flutter_markdown: ^0.6.0 - flutter_secure_storage: ^4.1.0 + fl_chart: ^0.45.0 + flutter: + sdk: flutter + flutter_bloc: ^8.0.1 + flutter_markdown: ^0.6.9 + flutter_secure_storage: ^5.0.2 get_it: ^7.2.0 - hive: ^2.0.0 - hive_flutter: ^1.0.0 - json_annotation: ^4.0.0 + hive: ^2.0.5 + hive_flutter: ^1.1.0 + ionicons: ^0.1.2 + json_annotation: ^4.4.0 + local_auth: ^1.1.11 modal_bottom_sheet: ^2.0.0 nanoid: ^1.0.0 - package_info: ^2.0.0 - pretty_dio_logger: ^1.1.1 - provider: ^6.0.0 - share_plus: ^2.1.4 - url_launcher: ^6.0.2 - wakelock: ^0.5.0+2 - basic_utils: ^3.4.0 - ionicons: ^0.1.2 - pointycastle: ^3.3.2 + package_info: ^2.0.2 + pointycastle: ^3.5.1 + pretty_dio_logger: ^1.2.0-beta-1 + provider: ^6.0.2 rsa_encrypt: ^2.0.0 - ssh_key: ^0.7.0 - local_auth: ^1.1.7 - auto_size_text: ^3.0.0-nullsafety.0 + share_plus: ^3.0.5 + ssh_key: ^0.7.1 + timezone: ^0.8.0 + url_launcher: ^6.0.20 + wakelock: ^0.6.1+1 dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.1.1 - flutter_launcher_icons: ^0.9.0 + flutter_launcher_icons: ^0.9.2 hive_generator: ^1.0.0 - json_serializable: ^4.0.2 + json_serializable: ^6.1.4 flutter_icons: android: "launcher_icon"