mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-01-07 00:24:18 +00:00
Merge pull request '0.5.0 DKIM' (#85) from inex/selfprivacy.org.app:dkim into master
Reviewed-on: https://git.selfprivacy.org/kherel/selfprivacy.org.app/pulls/85
This commit is contained in:
commit
3e2a86ede1
|
@ -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'
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:name="${applicationName}"
|
||||
android:label="SelfPrivacy"
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
<activity
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.5.10'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-all.zip
|
||||
|
|
|
@ -62,6 +62,20 @@
|
|||
"6": "This removes the Server. It will be no longer accessible"
|
||||
}
|
||||
},
|
||||
"ssh": {
|
||||
"title": "SSH keys",
|
||||
"create": "Create SSH key",
|
||||
"delete": "Delete SSH key",
|
||||
"delete_confirm_question": "Are you sure you want to delete SSH key?",
|
||||
"subtitle_with_keys": "{} keys",
|
||||
"subtitle_without_keys": "No keys",
|
||||
"no_key_name": "Unnamed key",
|
||||
"root": {
|
||||
"title": "These are superuser keys",
|
||||
"subtitle": "Owners of these keys get full access to the server and can do anything on it. Only add your own keys to the server."
|
||||
},
|
||||
"input_label": "Public ED25519 or RSA key"
|
||||
},
|
||||
"onboarding": {
|
||||
"_comment": "Onboarding pages",
|
||||
"page1_title": "Digital independence, available to all of us",
|
||||
|
@ -91,6 +105,38 @@
|
|||
"status": "Status — Good",
|
||||
"bottom_sheet": {
|
||||
"1": "It's your personal internet address that will point to the server and other services of yours."
|
||||
},
|
||||
"screen_title": "Domain and DNS",
|
||||
"states": {
|
||||
"ok": "Records are OK",
|
||||
"error": "Problems found",
|
||||
"error_subtitle": "Tap here to fix them",
|
||||
"refreshing": "Refreshing status...",
|
||||
"uninitialized": "Data is not retrieved yet"
|
||||
},
|
||||
"record_description": {
|
||||
"root": "Root domain",
|
||||
"api": "SelfPrivacy API",
|
||||
"cloud": "File cloud",
|
||||
"git": "Git server",
|
||||
"meet": "Video conference",
|
||||
"social": "Social network",
|
||||
"password": "Password manager",
|
||||
"vpn": "VPN",
|
||||
"mx": "MX record",
|
||||
"dmarc": "DMARC record",
|
||||
"spf": "SPF record",
|
||||
"dkim": "DKIM key"
|
||||
},
|
||||
"cards": {
|
||||
"services": {
|
||||
"title": "Services",
|
||||
"subtitle": "Type “A” records required for each service."
|
||||
},
|
||||
"email": {
|
||||
"title": "Email",
|
||||
"subtitle": "Records necessary for secure email exchange."
|
||||
}
|
||||
}
|
||||
},
|
||||
"backup": {
|
||||
|
@ -111,6 +157,7 @@
|
|||
"restoring": "Restoring from backup",
|
||||
"error_pending": "Server returned error, check it below",
|
||||
"restore_alert": "You are about to restore from backup created on {}. All current data will be lost. Are you sure?",
|
||||
"refresh": "Refresh status",
|
||||
"refetchBackups": "Refetch backup list",
|
||||
"refetchingList": "In a few minutes list will be updated"
|
||||
}
|
||||
|
@ -119,7 +166,8 @@
|
|||
"_comment": "Card shown when user skips initial setup",
|
||||
"1": "Please finish application setup using ",
|
||||
"2": "@:more.configuration_wizard",
|
||||
"3": " for further work"
|
||||
"3": " for further work",
|
||||
"in_menu": "Server is not set up yet. Please finish setup using setup wizard for further work."
|
||||
},
|
||||
"services": {
|
||||
"_comment": "Вкладка сервисы",
|
||||
|
@ -202,7 +250,7 @@
|
|||
"delete_confirm_question": "Are you sure?",
|
||||
"reset_password": "Reset password",
|
||||
"account": "Account",
|
||||
"send_regisration_data": "Share login credentials"
|
||||
"send_registration_data": "Share login credentials"
|
||||
},
|
||||
"initializing": {
|
||||
"_comment": "initializing page",
|
||||
|
@ -267,14 +315,17 @@
|
|||
"upgradeSuccess": "Server upgrade started",
|
||||
"upgradeFailed": "Failed to upgrade server",
|
||||
"upgradeServer": "Upgrade server",
|
||||
"rebootServer": "Reboot server"
|
||||
"rebootServer": "Reboot server",
|
||||
"create_ssh_key": "Create SSH key for {}",
|
||||
"delete_ssh_key": "Delete SSH key for {}"
|
||||
},
|
||||
"validations": {
|
||||
"required": "Required",
|
||||
"invalid_format": "Invalid format",
|
||||
"root_name": "User name cannot be 'root'",
|
||||
"key_format": "Invalid key format",
|
||||
"length": "Length is [] shoud be {}",
|
||||
"user_alredy_exist": "Already exists"
|
||||
"length": "Length is [] should be {}",
|
||||
"user_already_exist": "Already exists",
|
||||
"key_already_exists": "This key already exists"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,20 @@
|
|||
"6": "Действие приведет к удалению сервера. После этого он будет недоступен."
|
||||
}
|
||||
},
|
||||
"ssh": {
|
||||
"title": "SSH ключи",
|
||||
"create": "Добавить SSH ключ",
|
||||
"delete": "Удалить SSH ключ",
|
||||
"delete_confirm_question": "Вы уверены что хотите удалить следующий ключ?",
|
||||
"subtitle_with_keys": "Ключей: {}",
|
||||
"subtitle_without_keys": "Ключей нет",
|
||||
"no_key_name": "Безымянный ключ",
|
||||
"root": {
|
||||
"title": "Это ключи суперпользователя",
|
||||
"subtitle": "Владельцы указанных здесь ключей получают полный доступ к данным и настройкам сервера. Добавляйте исключительно свои ключи."
|
||||
},
|
||||
"input_label": "Публичный ED25519 или RSA ключ"
|
||||
},
|
||||
"onboarding": {
|
||||
"_comment": "страницы онбординга",
|
||||
"page1_title": "Цифровая независимость доступна каждому",
|
||||
|
@ -91,6 +105,38 @@
|
|||
"status": "Статус — в норме",
|
||||
"bottom_sheet": {
|
||||
"1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы."
|
||||
},
|
||||
"screen_title": "Домен и DNS",
|
||||
"states": {
|
||||
"ok": "Записи в норме",
|
||||
"error": "Обнаружены проблемы",
|
||||
"error_subtitle": "Нажмите здесь, чтобы исправить",
|
||||
"refreshing": "Обновление данных...",
|
||||
"uninitialized": "Данные ещё не получены"
|
||||
},
|
||||
"record_description": {
|
||||
"root": "Корневой домен",
|
||||
"api": "SelfPrivacy API",
|
||||
"cloud": "Файловое облако",
|
||||
"git": "Git сервер",
|
||||
"meet": "Видеоконференции",
|
||||
"social": "Социальная сеть",
|
||||
"password": "Менеджер паролей",
|
||||
"vpn": "VPN",
|
||||
"mx": "MX запись",
|
||||
"dmarc": "DMARC запись",
|
||||
"spf": "SPF запись",
|
||||
"dkim": "DKIM ключ"
|
||||
},
|
||||
"cards": {
|
||||
"services": {
|
||||
"title": "Сервисы",
|
||||
"subtitle": "Записи типа “A” необходимые для работы сервисов."
|
||||
},
|
||||
"email": {
|
||||
"title": "Электронная почта",
|
||||
"subtitle": "Записи необходимые для безопасного обмена электронной почтой."
|
||||
}
|
||||
}
|
||||
},
|
||||
"backup": {
|
||||
|
@ -98,7 +144,7 @@
|
|||
"status": "Статус — в норме",
|
||||
"bottom_sheet": {
|
||||
"1": "Выручит Вас в любой ситуации: хакерская атака, удаление сервера и т.д.",
|
||||
"2": "Использовано 3Gb из бесплатых 10Gb. Последнее копирование была сделано вчера в {}."
|
||||
"2": "Использовано 3Gb из бесплатых 10Gb. Последнее копирование была сделано вчера в {}."
|
||||
},
|
||||
"reuploadKey": "Принудительно обновить ключ",
|
||||
"reuploadedKey": "Ключ на сервере обновлён",
|
||||
|
@ -111,6 +157,7 @@
|
|||
"restoring": "Восстановление из копии",
|
||||
"error_pending": "Сервер вернул ошибку: проверьте её ниже.",
|
||||
"restore_alert": "Вы собираетесь восстановить из копии созданной {}. Все текущие данные будут потеряны. Вы уверены?",
|
||||
"refresh": "Обновить статус",
|
||||
"refetchBackups": "Обновить список копий",
|
||||
"refetchingList": "Через несколько минут список будет обновлён"
|
||||
|
||||
|
@ -120,7 +167,8 @@
|
|||
"_comment": "Карточка показывающая когда человек скипнул настройку, на карте текст из 3 блоков, средний содержит ссыку на мастер подключения",
|
||||
"1": "Завершите настройку приложения используя ",
|
||||
"2": "@:more.configuration_wizard",
|
||||
"3": " для продолжения работы"
|
||||
"3": " для продолжения работы",
|
||||
"in_menu": "Сервер ещё не настроен, воспользуйтесь мастером подключения."
|
||||
},
|
||||
"services": {
|
||||
"_comment": "Вкладка сервисы",
|
||||
|
@ -203,7 +251,7 @@
|
|||
"delete_confirm_question": "Вы действительно хотите удалить учетную запись?",
|
||||
"reset_password": "Сбросить пароль",
|
||||
"account": "Учетная запись",
|
||||
"send_regisration_data": "Поделиться реквизитами"
|
||||
"send_registration_data": "Поделиться реквизитами"
|
||||
},
|
||||
"initializing": {
|
||||
"_comment": "initializing page",
|
||||
|
@ -268,7 +316,9 @@
|
|||
"upgradeSuccess": "Запущено обновление сервера",
|
||||
"upgradeFailed": "Обновить сервер не вышло",
|
||||
"upgradeServer": "Обновить сервер",
|
||||
"rebootServer": "Перезагрузить сервер"
|
||||
"rebootServer": "Перезагрузить сервер",
|
||||
"create_ssh_key": "Создать SSH ключ для {}",
|
||||
"delete_ssh_key": "Удалить SSH ключ для {}"
|
||||
},
|
||||
"validations": {
|
||||
"required": "Обязательное поле.",
|
||||
|
@ -276,6 +326,7 @@
|
|||
"root_name": "Имя пользователя не может быть'root'.",
|
||||
"key_format": "Неверный формат.",
|
||||
"length": "Длина строки [] должна быть {}.",
|
||||
"user_alredy_exist": "Имя уже используется."
|
||||
"user_already_exist": "Имя уже используется.",
|
||||
"key_already_exists": "Этот ключ уже добавлен."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,4 +61,4 @@ SPEC CHECKSUMS:
|
|||
|
||||
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
||||
|
||||
COCOAPODS: 1.10.1
|
||||
COCOAPODS: 1.11.2
|
||||
|
|
|
@ -157,7 +157,7 @@
|
|||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1020;
|
||||
LastUpgradeCheck = 1300;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
LastUpgradeVersion = "1300"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
|
||||
|
@ -20,6 +21,7 @@ class BlocAndProviderConfig extends StatelessWidget {
|
|||
var appConfigCubit = AppConfigCubit()..load();
|
||||
var servicesCubit = ServicesCubit(appConfigCubit);
|
||||
var backupsCubit = BackupsCubit(appConfigCubit);
|
||||
var dnsRecordsCubit = DnsRecordsCubit(appConfigCubit);
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
|
@ -33,6 +35,7 @@ class BlocAndProviderConfig extends StatelessWidget {
|
|||
BlocProvider(create: (_) => 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),
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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<User>(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<Uint8List> getEncriptedKey(String encKey) async {
|
||||
static Future<Uint8List> 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';
|
||||
|
|
|
@ -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<Dio> 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<BaseOptions> get options;
|
||||
|
||||
abstract final String rootAddress;
|
||||
abstract final bool hasLoger;
|
||||
abstract final bool hasLogger;
|
||||
abstract final bool isWithToken;
|
||||
|
||||
ValidateStatus? validateStatus;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<List<DnsRecord>> 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 = <DnsRecord>[];
|
||||
|
||||
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<void> createMultipleDnsRecords({
|
||||
String? ip4,
|
||||
required CloudFlareDomain cloudFlareDomain,
|
||||
|
@ -113,33 +144,33 @@ class CloudflareApi extends ApiMap {
|
|||
close(client);
|
||||
}
|
||||
|
||||
List<DnsRecords> projectDnsRecords(String? domainName, String? ip4) {
|
||||
var domainA = DnsRecords(type: 'A', name: domainName, content: ip4);
|
||||
List<DnsRecord> 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 <DnsRecords>[
|
||||
return <DnsRecord>[
|
||||
domainA,
|
||||
apiA,
|
||||
cloudA,
|
||||
|
@ -154,6 +185,27 @@ class CloudflareApi extends ApiMap {
|
|||
];
|
||||
}
|
||||
|
||||
Future<void> 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<List<String>> 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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<D> {
|
||||
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<bool> createUser(User user) async {
|
||||
bool res;
|
||||
Future<ApiResponse<User>> 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<ApiResponse<List<String>>> getUsersList() async {
|
||||
List<String> 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<List<String>>(
|
||||
statusCode: response.statusCode ?? HttpStatus.internalServerError,
|
||||
data: res,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ApiResponse<void>> 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<void>(
|
||||
statusCode: response.statusCode ?? HttpStatus.internalServerError,
|
||||
data: null,
|
||||
errorMessage: response.data?.containsKey('error') ?? false
|
||||
? response.data['error']
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ApiResponse<void>> 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<void>(
|
||||
statusCode: response.statusCode ?? HttpStatus.internalServerError,
|
||||
data: null,
|
||||
errorMessage: response.data?.containsKey('error') ?? false
|
||||
? response.data['error']
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ApiResponse<List<String>>> getUserSshKeys(User user) async {
|
||||
List<String> res;
|
||||
Response response;
|
||||
|
||||
var client = await getClient();
|
||||
response = await client.get('/services/ssh/keys/${user.login}');
|
||||
try {
|
||||
res = (response.data as List<dynamic>).map((e) => e as String).toList();
|
||||
} catch (e) {
|
||||
print(e);
|
||||
res = [];
|
||||
}
|
||||
|
||||
close(client);
|
||||
return ApiResponse<List<String>>(
|
||||
statusCode: response.statusCode ?? HttpStatus.internalServerError,
|
||||
data: res,
|
||||
errorMessage: response.data is List
|
||||
? null
|
||||
: response.data?.containsKey('error') ?? false
|
||||
? response.data['error']
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ApiResponse<void>> 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<void>(
|
||||
statusCode: response.statusCode ?? HttpStatus.internalServerError,
|
||||
data: null,
|
||||
errorMessage: response.data?.containsKey('error') ?? false
|
||||
? response.data['error']
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> 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<void> switchService(ServiceTypes type, bool needToTurnOn) async {
|
||||
var client = await getClient();
|
||||
client.post('/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}');
|
||||
client.close();
|
||||
}
|
||||
|
||||
Future<void> sendSsh(String ssh) async {
|
||||
var client = await getClient();
|
||||
client.put(
|
||||
'/services/ssh/key/send',
|
||||
data: {"public_key": ssh},
|
||||
);
|
||||
client.close();
|
||||
close(client);
|
||||
}
|
||||
|
||||
Future<Map<ServiceTypes, bool>> servicesPowerCheck() async {
|
||||
|
@ -158,13 +288,13 @@ class ServerApi extends ApiMap {
|
|||
'bucket': bucket.bucketName,
|
||||
},
|
||||
);
|
||||
client.close();
|
||||
close(client);
|
||||
}
|
||||
|
||||
Future<void> startBackup() async {
|
||||
var client = await getClient();
|
||||
client.put('/services/restic/backup/create');
|
||||
client.close();
|
||||
close(client);
|
||||
}
|
||||
|
||||
Future<List<Backup>> getBackups() async {
|
||||
|
@ -207,13 +337,13 @@ class ServerApi extends ApiMap {
|
|||
Future<void> forceBackupListReload() async {
|
||||
var client = await getClient();
|
||||
client.get('/services/restic/backup/reload');
|
||||
client.close();
|
||||
close(client);
|
||||
}
|
||||
|
||||
Future<void> restoreBackup(String backupId) async {
|
||||
var client = await getClient();
|
||||
client.put('/services/restic/backup/restore', data: {'backupId': backupId});
|
||||
client.close();
|
||||
close(client);
|
||||
}
|
||||
|
||||
Future<bool> pullConfigurationUpdate() async {
|
||||
|
@ -226,27 +356,79 @@ class ServerApi extends ApiMap {
|
|||
Future<bool> reboot() async {
|
||||
var client = await getClient();
|
||||
Response response = await client.get('/system/reboot');
|
||||
client.close();
|
||||
close(client);
|
||||
return response.statusCode == HttpStatus.ok;
|
||||
}
|
||||
|
||||
Future<bool> upgrade() async {
|
||||
var client = await getClient();
|
||||
Response response = await client.get('/system/configuration/upgrade');
|
||||
client.close();
|
||||
close(client);
|
||||
return response.statusCode == HttpStatus.ok;
|
||||
}
|
||||
|
||||
Future<AutoUpgradeSettings> getAutoUpgradeSettings() async {
|
||||
var client = await getClient();
|
||||
Response response = await client.get('/system/configuration/autoUpgrade');
|
||||
close(client);
|
||||
return AutoUpgradeSettings.fromJson(response.data);
|
||||
}
|
||||
|
||||
Future<void> updateAutoUpgradeSettings(AutoUpgradeSettings settings) async {
|
||||
var client = await getClient();
|
||||
await client.put(
|
||||
'/system/configuration/autoUpgrade',
|
||||
data: settings.toJson(),
|
||||
);
|
||||
close(client);
|
||||
}
|
||||
|
||||
Future<TimeZoneSettings> getServerTimezone() async {
|
||||
var client = await getClient();
|
||||
Response response = await client.get('/system/configuration/timezone');
|
||||
close(client);
|
||||
|
||||
return TimeZoneSettings.fromString(response.data);
|
||||
}
|
||||
|
||||
Future<void> updateServerTimezone(TimeZoneSettings settings) async {
|
||||
var client = await getClient();
|
||||
await client.put(
|
||||
'/system/configuration/timezone',
|
||||
data: settings.toJson(),
|
||||
);
|
||||
close(client);
|
||||
}
|
||||
|
||||
Future<String> 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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<AppConfigState> {
|
|||
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<AppConfigState> {
|
|||
);
|
||||
resetServerIfServerIsOkay();
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
dnsMatches: matches,
|
||||
),
|
||||
);
|
||||
startServerIfDnsIsOkay();
|
||||
}
|
||||
};
|
||||
|
@ -108,7 +115,7 @@ class AppConfigCubit extends Cubit<AppConfigState> {
|
|||
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<AppConfigState> {
|
|||
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<AppConfigState> {
|
|||
isServerResetedFirstTime: false,
|
||||
isServerResetedSecondTime: false,
|
||||
isLoading: false,
|
||||
dnsMatches: null,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -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<bool> isDnsAddressesMatch(String? domainName, String? ip4) async {
|
||||
Future<Map<String, bool>> isDnsAddressesMatch(String? domainName, String? ip4,
|
||||
Map<String, bool>? skippedMatches) async {
|
||||
var addresses = <String>[
|
||||
'$domainName',
|
||||
'api.$domainName',
|
||||
|
@ -77,7 +80,13 @@ class AppConfigRepository {
|
|||
'password.$domainName'
|
||||
];
|
||||
|
||||
var matches = <String, bool>{};
|
||||
|
||||
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<void> createServer(
|
||||
|
@ -189,9 +200,23 @@ class AppConfigRepository {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> createDkimRecord(CloudFlareDomain cloudFlareDomain) async {
|
||||
var cloudflareApi = CloudflareApi();
|
||||
var api = ServerApi();
|
||||
|
||||
var dkimRecordString = await api.getDkim();
|
||||
|
||||
await cloudflareApi.setDkim(dkimRecordString, cloudFlareDomain);
|
||||
}
|
||||
|
||||
Future<bool> isHttpServerWorking() async {
|
||||
var api = ServerApi();
|
||||
var isHttpServerWorking = await api.isHttpServerWorking();
|
||||
try {
|
||||
await api.getDkim();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return isHttpServerWorking;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String, bool>? 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<String, bool>? 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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<BackupsState> {
|
|||
|
||||
Future<void> 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
|
||||
|
|
184
lib/logic/cubit/dns_records/dns_records_cubit.dart
Normal file
184
lib/logic/cubit/dns_records/dns_records_cubit.dart
Normal file
|
@ -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<DnsRecordsState> {
|
||||
DnsRecordsCubit(AppConfigCubit appConfigCubit)
|
||||
: super(appConfigCubit,
|
||||
DnsRecordsState(dnsState: DnsRecordsStatus.refreshing));
|
||||
|
||||
final api = ServerApi();
|
||||
final cloudflare = CloudflareApi();
|
||||
|
||||
Future<void> 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<DnsRecord> records =
|
||||
await cloudflare.getDnsRecords(cloudFlareDomain: domain);
|
||||
final dkimPublicKey = await api.getDkim();
|
||||
final desiredRecords =
|
||||
_getDesiredDnsRecords(domain.domainName, ipAddress, dkimPublicKey);
|
||||
List<DesiredDnsRecord> 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<DnsRecordsState> change) {
|
||||
// print(change);
|
||||
super.onChange(change);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clear() async {
|
||||
emit(DnsRecordsState(dnsState: DnsRecordsStatus.error));
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing));
|
||||
await load();
|
||||
}
|
||||
|
||||
Future<void> 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<DesiredDnsRecord> _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,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
76
lib/logic/cubit/dns_records/dns_records_state.dart
Normal file
76
lib/logic/cubit/dns_records/dns_records_state.dart
Normal file
|
@ -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<DesiredDnsRecord> dnsRecords;
|
||||
|
||||
@override
|
||||
List<Object> get props => [
|
||||
dnsState,
|
||||
dnsRecords,
|
||||
];
|
||||
|
||||
DnsRecordsState copyWith({
|
||||
DnsRecordsStatus? dnsState,
|
||||
List<DesiredDnsRecord>? 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<String>(
|
||||
(s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
|
||||
LegnthStringValidationWithLenghShowing(
|
||||
40, 'validations.length'.tr(args: ["40"]))
|
||||
LengthStringValidationWithLengthShowing(
|
||||
40,
|
||||
'validations.length'.tr(
|
||||
args: ["40"],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ class DomainSetupCubit extends Cubit<DomainSetupState> {
|
|||
);
|
||||
|
||||
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 {}
|
||||
|
|
|
@ -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<String>(
|
||||
(s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
|
||||
LegnthStringValidationWithLenghShowing(
|
||||
LengthStringValidationWithLengthShowing(
|
||||
64, 'validations.length'.tr(args: ["64"]))
|
||||
],
|
||||
);
|
||||
|
|
46
lib/logic/cubit/forms/user/ssh_form_cubit.dart
Normal file
46
lib/logic/cubit/forms/user/ssh_form_cubit.dart
Normal file
|
@ -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<String>((s) {
|
||||
print(s);
|
||||
print(keyRegExp.hasMatch(s));
|
||||
return !keyRegExp.hasMatch(s);
|
||||
}, 'validations.invalid_format'.tr()),
|
||||
],
|
||||
);
|
||||
|
||||
super.addFields([key]);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<void> onSubmit() {
|
||||
print(key.state.isValid);
|
||||
jobsCubit.addJob(CreateSSHKeyJob(user: user, publicKey: key.state.value));
|
||||
}
|
||||
|
||||
late FieldCubit<String> key;
|
||||
|
||||
final JobsCubit jobsCubit;
|
||||
final User user;
|
||||
}
|
|
@ -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<String>(
|
||||
|
@ -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<String>((s) => passwordRegExp.hasMatch(s),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:cubit_form/cubit_form.dart';
|
||||
|
||||
class LegnthStringValidationWithLenghShowing extends ValidationModel<String> {
|
||||
LegnthStringValidationWithLenghShowing(int length, String errorText)
|
||||
class LengthStringValidationWithLengthShowing extends ValidationModel<String> {
|
||||
LengthStringValidationWithLengthShowing(int length, String errorText)
|
||||
: super((n) => n.length != length, errorText);
|
||||
|
||||
@override
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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<JobsState> {
|
|||
emit(newState);
|
||||
}
|
||||
|
||||
void createOrRemoveServiceToggleJob(ServiceToggleJob job) {
|
||||
void createOrRemoveServiceToggleJob(ToggleJob job) {
|
||||
var newJobsList = <Job>[];
|
||||
if (state is JobsStateWithJobs) {
|
||||
newJobsList.addAll((state as JobsStateWithJobs).jobList);
|
||||
|
@ -99,28 +98,26 @@ class JobsCubit extends Cubit<JobsState> {
|
|||
if (state is JobsStateWithJobs) {
|
||||
var jobs = (state as JobsStateWithJobs).jobList;
|
||||
emit(JobsStateLoading());
|
||||
var newUsers = <User>[];
|
||||
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<SSHModel>().generateKeys();
|
||||
api.sendSsh(getIt<SSHModel>().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<JobsState> {
|
|||
}
|
||||
|
||||
emit(JobsStateEmpty());
|
||||
|
||||
getIt<NavigationService>().navigator!.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ServerDetailsState> {
|
|||
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());
|
||||
}
|
||||
|
|
|
@ -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<HetznerServerInfo> 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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<Object> get props => [serverInfo, checkTime];
|
||||
List<Object> get props => [
|
||||
serverInfo,
|
||||
serverTimezone,
|
||||
autoUpgradeSettings,
|
||||
checkTime,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<UsersState> {
|
||||
UsersCubit() : super(UsersState(<User>[]));
|
||||
UsersCubit()
|
||||
: super(UsersState(
|
||||
<User>[], User(login: 'root'), User(login: 'loading...')));
|
||||
Box<User> box = Hive.box<User>(BNames.users);
|
||||
Box configBox = Hive.box(BNames.appConfig);
|
||||
|
||||
void load() async {
|
||||
final api = ServerApi();
|
||||
|
||||
Future<void> load() async {
|
||||
var loadedUsers = box.values.toList();
|
||||
|
||||
final primaryUser =
|
||||
configBox.get(BNames.rootUser, defaultValue: User(login: 'loading...'));
|
||||
List<String> 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<User> mergeLocalAndServerUsers(
|
||||
List<User> localUsers, List<String> serverUsers) {
|
||||
// If local user not exists on server, add it with isFoundOnServer = false
|
||||
// If server user not exists on local, add it
|
||||
|
||||
List<User> mergedUsers = [];
|
||||
List<String> 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<List<User>> loadSshKeys(List<User> users) async {
|
||||
List<User> 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<void> refresh() async {
|
||||
List<User> updatedUsers = List<User>.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<void> 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<User>.from(state.users);
|
||||
loadedUsers.add(result.data);
|
||||
await box.clear();
|
||||
await box.addAll(loadedUsers);
|
||||
emit(state.copyWith(users: loadedUsers));
|
||||
}
|
||||
|
||||
Future<void> 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<User>.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<User> users) async {
|
||||
var newUserList = <User>[...state.users, ...users];
|
||||
|
||||
await box.addAll(users);
|
||||
emit(UsersState(newUserList));
|
||||
Future<void> 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<String>;
|
||||
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<String> primaryUserKeys =
|
||||
List<String>.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<String> userKeys = List<String>.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<void> 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<String>;
|
||||
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<String> primaryUserKeys =
|
||||
List<String>.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<String> userKeys = List<String>.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<UsersState> change) {
|
||||
super.onChange(change);
|
||||
|
||||
print('UsersState changed');
|
||||
print(change);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<User> users;
|
||||
final User rootUser;
|
||||
final User primaryUser;
|
||||
|
||||
@override
|
||||
List<Object> get props => users;
|
||||
List<Object> get props => [users, rootUser, primaryUser];
|
||||
|
||||
UsersState copyWith({
|
||||
List<User>? users,
|
||||
User? rootUser,
|
||||
User? primaryUser,
|
||||
}) {
|
||||
return UsersState(
|
||||
users ?? this.users,
|
||||
rootUser ?? this.rootUser,
|
||||
primaryUser ?? this.primaryUser,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isEmpty => users.isEmpty;
|
||||
}
|
||||
|
|
22
lib/logic/models/auto_upgrade_settings.dart
Normal file
22
lib/logic/models/auto_upgrade_settings.dart
Normal file
|
@ -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<Object?> get props => [enable, allowReboot];
|
||||
factory AutoUpgradeSettings.fromJson(Map<String, dynamic> json) =>
|
||||
_$AutoUpgradeSettingsFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AutoUpgradeSettingsToJson(this);
|
||||
}
|
20
lib/logic/models/auto_upgrade_settings.g.dart
Normal file
20
lib/logic/models/auto_upgrade_settings.g.dart
Normal file
|
@ -0,0 +1,20 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'auto_upgrade_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AutoUpgradeSettings _$AutoUpgradeSettingsFromJson(Map<String, dynamic> json) =>
|
||||
AutoUpgradeSettings(
|
||||
enable: json['enable'] as bool,
|
||||
allowReboot: json['allowReboot'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AutoUpgradeSettingsToJson(
|
||||
AutoUpgradeSettings instance) =>
|
||||
<String, dynamic>{
|
||||
'enable': instance.enable,
|
||||
'allowReboot': instance.allowReboot,
|
||||
};
|
|
@ -6,46 +6,16 @@ part of 'backup.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Backup _$BackupFromJson(Map<String, dynamic> json) {
|
||||
return Backup(
|
||||
time: DateTime.parse(json['time'] as String),
|
||||
id: json['short_id'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
BackupStatus _$BackupStatusFromJson(Map<String, dynamic> json) {
|
||||
return BackupStatus(
|
||||
status: _$enumDecode(_$BackupStatusEnumEnumMap, json['status']),
|
||||
progress: (json['progress'] as num).toDouble(),
|
||||
errorMessage: json['error_message'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
K _$enumDecode<K, V>(
|
||||
Map<K, V> enumValues,
|
||||
Object? source, {
|
||||
K? unknownValue,
|
||||
}) {
|
||||
if (source == null) {
|
||||
throw ArgumentError(
|
||||
'A value must be provided. Supported values: '
|
||||
'${enumValues.values.join(', ')}',
|
||||
Backup _$BackupFromJson(Map<String, dynamic> 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<String, dynamic> 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',
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -6,8 +6,7 @@ part of 'dns_records.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Map<String, dynamic> _$DnsRecordsToJson(DnsRecords instance) =>
|
||||
<String, dynamic>{
|
||||
Map<String, dynamic> _$DnsRecordToJson(DnsRecord instance) => <String, dynamic>{
|
||||
'type': instance.type,
|
||||
'name': instance.name,
|
||||
'content': instance.content,
|
||||
|
|
|
@ -6,42 +6,16 @@ part of 'hetzner_server_info.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
HetznerServerInfo _$HetznerServerInfoFromJson(Map<String, dynamic> 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<String, dynamic>),
|
||||
HetznerServerInfo.locationFromJson(json['datacenter'] as Map),
|
||||
);
|
||||
}
|
||||
|
||||
K _$enumDecode<K, V>(
|
||||
Map<K, V> enumValues,
|
||||
Object? source, {
|
||||
K? unknownValue,
|
||||
}) {
|
||||
if (source == null) {
|
||||
throw ArgumentError(
|
||||
'A value must be provided. Supported values: '
|
||||
'${enumValues.values.join(', ')}',
|
||||
HetznerServerInfo _$HetznerServerInfoFromJson(Map<String, dynamic> 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<String, dynamic>),
|
||||
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<String, dynamic> json) {
|
||||
return HetznerServerTypeInfo(
|
||||
json['cores'] as int,
|
||||
json['memory'] as num,
|
||||
json['disk'] as int,
|
||||
(json['prices'] as List<dynamic>)
|
||||
.map((e) => HetznerPriceInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
Map<String, dynamic> json) =>
|
||||
HetznerServerTypeInfo(
|
||||
json['cores'] as int,
|
||||
json['memory'] as num,
|
||||
json['disk'] as int,
|
||||
(json['prices'] as List<dynamic>)
|
||||
.map((e) => HetznerPriceInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
HetznerPriceInfo _$HetznerPriceInfoFromJson(Map<String, dynamic> json) {
|
||||
return HetznerPriceInfo(
|
||||
HetznerPriceInfo.getPrice(json['price_hourly'] as Map),
|
||||
HetznerPriceInfo.getPrice(json['price_monthly'] as Map),
|
||||
);
|
||||
}
|
||||
HetznerPriceInfo _$HetznerPriceInfoFromJson(Map<String, dynamic> json) =>
|
||||
HetznerPriceInfo(
|
||||
HetznerPriceInfo.getPrice(json['price_hourly'] as Map),
|
||||
HetznerPriceInfo.getPrice(json['price_monthly'] as Map),
|
||||
);
|
||||
|
||||
HetznerLocation _$HetznerLocationFromJson(Map<String, dynamic> json) {
|
||||
return HetznerLocation(
|
||||
json['country'] as String,
|
||||
json['city'] as String,
|
||||
json['description'] as String,
|
||||
json['network_zone'] as String,
|
||||
);
|
||||
}
|
||||
HetznerLocation _$HetznerLocationFromJson(Map<String, dynamic> json) =>
|
||||
HetznerLocation(
|
||||
json['country'] as String,
|
||||
json['city'] as String,
|
||||
json['description'] as String,
|
||||
json['network_zone'] as String,
|
||||
);
|
||||
|
|
|
@ -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<Object> 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<Object> get props => [id, title, type, needToTurnOn];
|
||||
List<Object> 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<Object> get props => [id, title];
|
||||
List<Object> 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<Object> get props => [id, title, user, publicKey];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
22
lib/logic/models/server_configurations.dart
Normal file
22
lib/logic/models/server_configurations.dart
Normal file
|
@ -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<String, dynamic> json) =>
|
||||
_$AutoUpgradeConfigurationsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$AutoUpgradeConfigurationsToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [enable, allowReboot];
|
||||
}
|
21
lib/logic/models/server_configurations.g.dart
Normal file
21
lib/logic/models/server_configurations.g.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'server_configurations.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AutoUpgradeConfigurations _$AutoUpgradeConfigurationsFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
AutoUpgradeConfigurations(
|
||||
enable: json['enable'] as bool,
|
||||
allowReboot: json['allowReboot'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AutoUpgradeConfigurationsToJson(
|
||||
AutoUpgradeConfigurations instance) =>
|
||||
<String, dynamic>{
|
||||
'enable': instance.enable,
|
||||
'allowReboot': instance.allowReboot,
|
||||
};
|
18
lib/logic/models/timezone_settings.dart
Normal file
18
lib/logic/models/timezone_settings.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import 'package:timezone/timezone.dart';
|
||||
|
||||
class TimeZoneSettings {
|
||||
final Location timezone;
|
||||
|
||||
TimeZoneSettings(this.timezone);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'timezone': timezone.name,
|
||||
};
|
||||
}
|
||||
|
||||
factory TimeZoneSettings.fromString(String string) {
|
||||
var location = timeZoneDatabase.locations[string]!;
|
||||
return TimeZoneSettings(location);
|
||||
}
|
||||
}
|
|
@ -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<String> sshKeys;
|
||||
|
||||
@HiveField(3, defaultValue: true)
|
||||
final bool isFoundOnServer;
|
||||
|
||||
@HiveField(4)
|
||||
final String? note;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [login, password];
|
||||
List<Object?> 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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,18 +18,27 @@ class UserAdapter extends TypeAdapter<User> {
|
|||
};
|
||||
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<String>(),
|
||||
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
|
||||
|
|
|
@ -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<AppSettingsCubit>().state;
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light, // Manually changnig appbar color
|
||||
child: MaterialApp(
|
||||
scaffoldMessengerKey:
|
||||
getIt.get<NavigationService>().scaffoldMessengerKey,
|
||||
navigatorKey: getIt.get<NavigationService>().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<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light, // Manually changing appbar color
|
||||
child: BlocAndProviderConfig(
|
||||
child: BlocBuilder<AppSettingsCubit, AppSettingsState>(
|
||||
builder: (context, appSettings) {
|
||||
return MaterialApp(
|
||||
scaffoldMessengerKey:
|
||||
getIt.get<NavigationService>().scaffoldMessengerKey,
|
||||
navigatorKey: getIt.get<NavigationService>().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!;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
61
lib/ui/components/brand_hero_screen/brand_hero_screen.dart
Normal file
61
lib/ui/components/brand_hero_screen/brand_hero_screen.dart
Normal file
|
@ -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<Widget> 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: <Widget>[
|
||||
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,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.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<BrandTabBar> {
|
|||
}
|
||||
|
||||
_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(
|
||||
|
|
|
@ -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<BrandTimer> {
|
|||
}
|
||||
|
||||
_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<BrandTimer> {
|
|||
|
||||
void _getTime() {
|
||||
setState(() {
|
||||
_timeString = diffenceFromStart;
|
||||
_timeString = differenceFromStart;
|
||||
});
|
||||
}
|
||||
|
||||
String get diffenceFromStart =>
|
||||
String get differenceFromStart =>
|
||||
_durationToString(DateTime.now().difference(widget.startDateTime));
|
||||
|
||||
String _durationToString(Duration duration) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<NavigatorState>();
|
||||
|
||||
|
@ -44,203 +42,200 @@ class _BackupDetailsState extends State<BackupDetails>
|
|||
var backups = context.watch<BackupsCubit>().state.backups;
|
||||
var refreshing = context.watch<BackupsCubit>().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<BackupsCubit>().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<BackupsCubit>().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<BackupsCubit>().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<BackupsCubit>()
|
||||
.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<NavigationService>();
|
||||
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<BackupsCubit>()
|
||||
.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<NavigationService>();
|
||||
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<BackupsCubit>()
|
||||
.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<BackupsCubit>().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<BackupsCubit>()
|
||||
.forceUpdateBackups()
|
||||
},
|
||||
),
|
||||
Divider(
|
||||
height: 1.0,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'providers.backup.reuploadKey'.tr(),
|
||||
),
|
||||
onTap: preventActions
|
||||
? null
|
||||
: () => {context.read<BackupsCubit>().reuploadKey()},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (backupStatus == BackupStatusEnum.error)
|
||||
BrandText.body1(backupError.toString()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<BackupsCubit>().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<BackupsCubit>().reuploadKey();
|
||||
break;
|
||||
case _PopupMenuItemType.refetchBackups:
|
||||
context.read<BackupsCubit>().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 }
|
219
lib/ui/pages/dns_details/dns_details.dart
Normal file
219
lib/ui/pages/dns_details/dns_details.dart
Normal file
|
@ -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<DnsDetailsPage> {
|
||||
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<AppConfigCubit>().state is AppConfigFinished;
|
||||
final domain = getIt<ApiConfigModel>().cloudFlareDomain?.domainName ?? '';
|
||||
var dnsCubit = context.watch<DnsRecordsCubit>().state;
|
||||
|
||||
print(dnsCubit.dnsState);
|
||||
|
||||
if (!isReady) {
|
||||
return BrandHeroScreen(
|
||||
hasBackButton: true,
|
||||
headerTitle: '',
|
||||
heroIcon: BrandIcons.globe,
|
||||
heroTitle: 'providers.domain.screen_title'.tr(),
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
BrandCards.outlined(
|
||||
child: Column(
|
||||
children: [
|
||||
_getStateCard(dnsCubit.dnsState, () {
|
||||
context.read<DnsRecordsCubit>().fix();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 16.0),
|
||||
// Outlined card with a list of A records and their
|
||||
// status.
|
||||
BrandCards.outlined(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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: [
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<JobsCubit>();
|
||||
var isReady = context.watch<AppConfigCubit>().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<SSHModel>().isSSHKeyGenerated) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return _SSHExitsDetails(
|
||||
onShareTap: () {
|
||||
Share.share(
|
||||
getIt<SSHModel>().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<SSHModel>().clear();
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
ActionButton(
|
||||
text: 'basis.cancel'.tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
onCopyTap: () {
|
||||
Clipboard.setData(ClipboardData(
|
||||
text: getIt<SSHModel>()
|
||||
.savedPrivateKey!));
|
||||
getIt<NavigationService>()
|
||||
.showSnackBar('more.copied_ssh'.tr());
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
showDialog<void>(
|
||||
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<UsersCubit>().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,
|
||||
|
|
|
@ -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<NavigatorState>();
|
||||
|
||||
|
@ -32,6 +32,18 @@ class _ProvidersPageState extends State<ProvidersPage> {
|
|||
Widget build(BuildContext context) {
|
||||
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
|
||||
var isBackupInitialized = context.watch<BackupsCubit>().state.isInitialized;
|
||||
var dnsStatus = context.watch<DnsRecordsCubit>().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<ProvidersPage> {
|
|||
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<void>(
|
||||
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<Widget> children;
|
||||
|
||||
var config = context.watch<AppConfigCubit>().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),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<RootPage>
|
|||
tabController.dispose();
|
||||
}
|
||||
|
||||
var selfprivacyServer = ServerApi();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
|
|
|
@ -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<NavigatorState>();
|
||||
|
||||
|
@ -56,57 +63,57 @@ class _ServerDetailsState extends State<ServerDetails>
|
|||
var isReady = context.watch<AppConfigCubit>().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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,12 @@ class _ServerSettings extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var serverDetailsState = context.watch<ServerDetailsCubit>().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',
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
431
lib/ui/pages/server_details/time_zone/lang.dart
Normal file
431
lib/ui/pages/server_details/time_zone/lang.dart
Normal file
|
@ -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": "Апиа",
|
||||
};
|
107
lib/ui/pages/server_details/time_zone/time_zone.dart
Normal file
107
lib/ui/pages/server_details/time_zone/time_zone.dart
Normal file
|
@ -0,0 +1,107 @@
|
|||
part of '../server_details.dart';
|
||||
|
||||
final List<Location> 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<SelectTimezone> {
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -101,13 +101,13 @@ class _Card extends StatelessWidget {
|
|||
var jobsCubit = context.watch<JobsCubit>();
|
||||
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<void>(
|
||||
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) {
|
||||
|
|
76
lib/ui/pages/ssh_keys/new_ssh_key.dart
Normal file
76
lib/ui/pages/ssh_keys/new_ssh_key.dart
Normal file
|
@ -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<JobsCubit>();
|
||||
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<SshFormCubit>().state;
|
||||
|
||||
return BlocListener<SshFormCubit, FormCubitState>(
|
||||
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<SshFormCubit>().key,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'ssh.input_label'.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
BrandButton.rised(
|
||||
onPressed: formCubitState.isSubmitting
|
||||
? null
|
||||
: () => context.read<SshFormCubit>().trySubmit(),
|
||||
text: 'ssh.create'.tr(),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
143
lib/ui/pages/ssh_keys/ssh_keys.dart
Normal file
143
lib/ui/pages/ssh_keys/ssh_keys.dart
Normal file
|
@ -0,0 +1,143 @@
|
|||
import 'package:cubit_form/cubit_form.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/logic/cubit/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<SshKeysPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BrandHeroScreen(
|
||||
heroTitle: 'ssh.title'.tr(),
|
||||
heroSubtitle: widget.user.login,
|
||||
heroIcon: BrandIcons.key,
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
ListTile(
|
||||
title: Text(
|
||||
'ssh.create'.tr(),
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: Icon(Icons.add_circle_outline_rounded),
|
||||
onTap: () {
|
||||
showModalBottomSheet<void>(
|
||||
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: <Widget>[
|
||||
Text('ssh.delete_confirm_question'.tr()),
|
||||
Text('$keyName ($keyType)'),
|
||||
Text(publicKey),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
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<JobsCubit>().addJob(
|
||||
DeleteSSHKeyJob(
|
||||
user: widget.user, publicKey: key));
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}).toList(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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<UsersCubit>().state;
|
||||
// final usersCubitState = context.watch<UsersCubit>().state;
|
||||
var isReady = context.watch<AppConfigCubit>().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<UsersCubit, UsersState>(
|
||||
builder: (context, state) {
|
||||
print('Rebuild users page');
|
||||
final primaryUser = state.primaryUser;
|
||||
final users = [primaryUser, ...state.users];
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<UsersCubit>().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(
|
||||
|
|
|
@ -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)
|
||||
|
|
41
lib/utils/extensions/duration.dart
Normal file
41
lib/utils/extensions/duration.dart
Normal file
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:ui';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
extension TextExtension on Text {
|
||||
|
|
288
pubspec.lock
288
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"
|
||||
|
|
59
pubspec.yaml
59
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"
|
||||
|
|
Loading…
Reference in a new issue