Compare commits

...

16 Commits

Author SHA1 Message Date
Inex Code d240e493b1 Add user synchronization and SSH keys screen 2022-03-03 20:38:30 +03:00
Inex Code e4bdd47848 Merge branch 'feature/service-configurations' into dkim
# Conflicts:
#	lib/logic/api_maps/hetzner.dart
#	lib/logic/api_maps/server.dart
#	lib/logic/cubit/backups/backups_cubit.dart
#	lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart
#	lib/logic/models/job.dart
#	lib/ui/pages/initializing/initializing.dart
#	lib/ui/pages/providers/providers.dart
#	pubspec.lock
#	pubspec.yaml
2022-02-18 02:37:15 +03:00
kherel 7eff0968d0 update 2022-02-08 22:01:08 +01:00
kherel 83ff387998 update 2022-02-07 08:53:13 +01:00
kherel 9566a6ad10 update 2022-01-25 18:02:32 +01:00
Illia Chub d79b41a3fe Added fastlane changelog for latest app vesion 2022-01-25 18:02:31 +01:00
Inex Code 3cd187e416 Enforce alphanumeric bucket id 2022-01-25 18:02:31 +01:00
Inex Code 2dfb92f650 Fix wrong server name escaping symbol 2022-01-25 18:02:31 +01:00
Inex Code 72100e483a Replace all non-alphanumeric symbols during hetzner creation 2022-01-25 18:02:31 +01:00
Inex Code de13b09f23 Add gifs to illustrate API token instructions 2022-01-25 18:02:31 +01:00
Inex Code 2e8908053c Bump version 2022-01-25 18:02:31 +01:00
Inex Code 8622ed30f1 Add more instructions to UI 2022-01-25 18:02:31 +01:00
Inex Code 5957e5720b Add user deletion (for real) 2022-01-25 18:02:31 +01:00
Inex Code e72df08453 Show that we are doing something when upgrading/restarting server 2022-01-25 18:02:31 +01:00
Inex Code 532a3ab197 Bump version to 0.4.1 2022-01-25 18:02:31 +01:00
Inex Code 3a63e75e8e Fix nixos-infect link to use master, not rolling 2022-01-25 18:02:31 +01:00
52 changed files with 1672 additions and 713 deletions

View File

@ -301,7 +301,9 @@
"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",

View File

@ -62,6 +62,11 @@
"6": "Действие приведет к удалению сервера. После этого он будет недоступен."
}
},
"ssh": {
"title": "SSH ключи",
"create": "Добавить SSH ключ",
"delete": "Удалить SSH ключ"
},
"onboarding": {
"_comment": "страницы онбординга",
"page1_title": "Цифровая независимость доступна каждому",
@ -302,7 +307,9 @@
"upgradeSuccess": "Запущено обновление сервера",
"upgradeFailed": "Обновить сервер не вышло",
"upgradeServer": "Обновить сервер",
"rebootServer": "Перезагрузить сервер"
"rebootServer": "Перезагрузить сервер",
"create_ssh_key": "Создать SSH ключ для {}",
"delete_ssh_key": "Удалить SSH ключ для {}"
},
"validations": {
"required": "Обязательное поле.",

View File

@ -61,4 +61,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
COCOAPODS: 1.10.1
COCOAPODS: 1.11.2

View File

@ -157,7 +157,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1020;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -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';

View File

@ -6,7 +6,6 @@ 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 {

View File

@ -135,7 +135,7 @@ class HetznerApi extends ApiMap {
/// 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)}' CF_TOKEN=$cloudFlareKey DB_PASSWORD=${escapeQuotes(dbPassword)} API_TOKEN=$apiToken HOSTNAME=${escapeQuotes(hostname)} bash 2>&1 | tee /tmp/infect.log";
"#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);
final data = {

View File

@ -5,12 +5,28 @@ 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 hasLogger;
bool isWithToken;
@ -48,31 +64,151 @@ 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 {
res = (json.decode(response.data) as List<dynamic>)
.map((e) => e as String)
.toList();
} 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 {
@ -87,7 +223,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;
@ -123,16 +260,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 {
@ -159,13 +287,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 {
@ -208,13 +336,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 {
@ -227,21 +355,54 @@ 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');
client.close();
close(client);
// if got 404 raise exception
if (response.statusCode == HttpStatus.notFound) {

View File

@ -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';

View File

@ -79,7 +79,7 @@ class DnsRecordsCubit extends AppConfigDependendCubit<DnsRecordsState> {
@override
void onChange(Change<DnsRecordsState> change) {
print(change);
// print(change);
super.onChange(change);
}

View File

@ -17,7 +17,11 @@ class CloudFlareFormCubit extends FormCubit {
ValidationModel<String>(
(s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
LengthStringValidationWithLengthShowing(
40, 'validations.length'.tr(args: ["40"]))
40,
'validations.length'.tr(
args: ["40"],
),
)
],
);

View File

@ -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),

View File

@ -1,3 +1,5 @@
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';
@ -5,10 +7,9 @@ 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 +38,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);
@ -103,12 +104,10 @@ class JobsCubit extends Cubit<JobsState> {
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;
@ -116,11 +115,10 @@ class JobsCubit extends Cubit<JobsState> {
}
if (job is CreateSSHKeyJob) {
await getIt<SSHModel>().generateKeys();
api.sendSsh(getIt<SSHModel>().savedPubKey!);
api.addRootSshKey(getIt<SSHModel>().savedPubKey!);
}
}
usersCubit.addUsers(newUsers);
await api.pullConfigurationUpdate();
await api.apply();
if (hasServiceJobs) {

View File

@ -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());
}

View File

@ -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,
});
}

View File

@ -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,
];
}

View File

@ -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';

View File

@ -3,35 +3,295 @@ 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 = state.users;
final usersFromServer = await api.getUsersList();
if (usersFromServer.isSuccess) {
updatedUsers =
mergeLocalAndServerUsers(state.users, 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);
await box.add(result.data);
emit(state.copyWith(users: box.values.toList()));
}
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;
}
final result = await api.deleteUser(user);
if (result) {
await box.deleteAt(box.values.toList().indexOf(user));
emit(state.copyWith(users: box.values.toList()));
}
}
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 = 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 = 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 = 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 = 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) {
print(change);
super.onChange(change);
}
}

View File

@ -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;
}

View 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);
}

View 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,
};

View File

@ -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',

View File

@ -20,5 +20,5 @@ class DnsRecord {
final int priority;
final bool proxied;
toJson() => _$DnsRecordsToJson(this);
toJson() => _$DnsRecordToJson(this);
}

View File

@ -6,8 +6,7 @@ part of 'dns_records.dart';
// JsonSerializableGenerator
// **************************************************************************
Map<String, dynamic> _$DnsRecordsToJson(DnsRecord instance) =>
<String, dynamic>{
Map<String, dynamic> _$DnsRecordToJson(DnsRecord instance) => <String, dynamic>{
'type': instance.type,
'name': instance.name,
'content': instance.content,

View File

@ -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,
);

View File

@ -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];
}

View 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];
}

View 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,
};

View 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);
}
}

View File

@ -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';
}
}

View File

@ -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

View File

@ -7,6 +7,7 @@ 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:wakelock/wakelock.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'config/bloc_config.dart';
import 'config/bloc_observer.dart';
@ -19,12 +20,15 @@ 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 {

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';

View File

@ -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';

View File

@ -1,8 +1,6 @@
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';

View File

@ -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';

View File

@ -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';
@ -83,73 +74,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 +89,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,

View File

@ -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,
),
),
],
);
}
}

View File

@ -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(

View File

@ -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),
],
),
);
}

View File

@ -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',
),
)
],
);
}

View 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": "Апиа",
};

View 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(),
),
);
}
}

View File

@ -0,0 +1,75 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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 '../../../logic/models/user.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),
),
Divider(height: 0),
// show a list of ListTiles with ssh keys
// Clicking on one should delete it
Column(
children: widget.user.sshKeys.map((key) {
return ListTile(
title:
Text('${key.split(' ')[2]} (${key.split(' ')[0]})'),
// do not overflow text
subtitle: Text(key.split(' ')[1],
maxLines: 1, overflow: TextOverflow.ellipsis),
onTap: () {
// TODO: delete ssh key
});
}).toList(),
)
],
),
),
],
);
}
}

View File

@ -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)),
),
],
),

View File

@ -141,13 +141,19 @@ 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),
@ -160,6 +166,19 @@ class _UserDetails extends StatelessWidget {
},
),
SizedBox(height: 20),
BrandDivider(),
SizedBox(height: 20),
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)),
],
),
)

View File

@ -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,16 +18,18 @@ 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);
@ -37,12 +38,8 @@ class UsersPage extends StatelessWidget {
Widget build(BuildContext context) {
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 primaryUser = usersCubitState.primaryUser;
final users = [primaryUser, ...usersCubitState.users];
final isEmpty = users.isEmpty;
Widget child;
@ -56,14 +53,19 @@ class UsersPage extends StatelessWidget {
text: 'users.add_new_user'.tr(),
),
)
: ListView.builder(
itemCount: users.length,
itemBuilder: (BuildContext context, int index) {
return _User(
user: users[index],
isRootUser: index == 0,
);
: RefreshIndicator(
onRefresh: () async {
context.read<UsersCubit>().refresh();
},
child: ListView.builder(
itemCount: users.length,
itemBuilder: (BuildContext context, int index) {
return _User(
user: users[index],
isRootUser: index == 0,
);
},
),
);
}

View 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";
}
}

View File

@ -1,4 +1,3 @@
import 'dart:ui';
import 'package:flutter/cupertino.dart';
extension TextExtension on Text {

View File

@ -21,7 +21,7 @@ packages:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.11"
version: "3.2.0"
args:
dependency: transitive
description:
@ -56,14 +56,14 @@ packages:
name: basic_utils
url: "https://pub.dartlang.org"
source: hosted
version: "3.9.4"
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:
@ -210,7 +210,7 @@ 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:
@ -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.6"
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:
@ -353,7 +346,42 @@ packages:
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
@ -426,7 +454,7 @@ packages:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.2.0"
http_parser:
dependency: transitive
description:
@ -489,7 +517,7 @@ packages:
name: local_auth
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.10"
version: "1.1.11"
logging:
dependency: transitive
description:
@ -504,6 +532,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
@ -587,7 +622,7 @@ packages:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.0.9"
path_provider_android:
dependency: transitive
description:
@ -657,7 +692,7 @@ packages:
name: pointycastle
url: "https://pub.dartlang.org"
source: hosted
version: "3.5.0"
version: "3.5.1"
pool:
dependency: transitive
description:
@ -720,7 +755,7 @@ packages:
name: share_plus
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
version: "3.0.5"
share_plus_linux:
dependency: transitive
description:
@ -769,28 +804,28 @@ packages:
name: shared_preferences_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.10"
version: "2.0.11"
shared_preferences_ios:
dependency: transitive
description:
name: shared_preferences_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.9"
version: "2.1.0"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
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:
@ -811,7 +846,7 @@ packages:
name: shared_preferences_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.1.0"
shelf:
dependency: transitive
description:
@ -943,6 +978,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
@ -970,35 +1012,35 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.18"
version: "6.0.20"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.14"
version: "6.0.15"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.14"
version: "6.0.15"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
version: "3.0.0"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
version: "3.0.0"
url_launcher_platform_interface:
dependency: transitive
description:
@ -1019,7 +1061,7 @@ packages:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "3.0.0"
vector_math:
dependency: transitive
description:
@ -1040,7 +1082,7 @@ packages:
name: wakelock
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.6"
version: "0.6.1+1"
wakelock_macos:
dependency: transitive
description:
@ -1096,7 +1138,7 @@ packages:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.11"
version: "2.4.1"
xdg_directories:
dependency: transitive
description:

View File

@ -8,44 +8,45 @@ environment:
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: ^6.1.4