mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-01-08 00:51:20 +00:00
Merge master into platform-path
This commit is contained in:
commit
603946ba73
|
@ -34,7 +34,8 @@
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"alert": "Alert"
|
"alert": "Alert",
|
||||||
|
"copied_to_clipboard": "Copied to clipboard!"
|
||||||
},
|
},
|
||||||
"more_page": {
|
"more_page": {
|
||||||
"configuration_wizard": "Setup wizard",
|
"configuration_wizard": "Setup wizard",
|
||||||
|
@ -196,6 +197,11 @@
|
||||||
"autobackup_custom_hint": "Enter custom period in minutes",
|
"autobackup_custom_hint": "Enter custom period in minutes",
|
||||||
"autobackup_set_period": "Set period",
|
"autobackup_set_period": "Set period",
|
||||||
"autobackup_period_set": "Period set",
|
"autobackup_period_set": "Period set",
|
||||||
|
"backups_encryption_key": "Encryption key",
|
||||||
|
"backups_encryption_key_subtitle": "Keep it in a safe place.",
|
||||||
|
"backups_encryption_key_copy": "Copy the encryption key",
|
||||||
|
"backups_encryption_key_show": "Show the encryption key",
|
||||||
|
"backups_encryption_key_description": "This key is used to encrypt your backups. If you lose it, you will not be able to restore your backups. Keep it in a safe place, as it will be useful if you ever need to restore from backups manually.",
|
||||||
"pending_jobs": "Currently running backup jobs",
|
"pending_jobs": "Currently running backup jobs",
|
||||||
"snapshots_title": "Snapshot list"
|
"snapshots_title": "Snapshot list"
|
||||||
},
|
},
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
"done": "Готово",
|
"done": "Готово",
|
||||||
"continue": "Продолжить",
|
"continue": "Продолжить",
|
||||||
"alert": "Уведомление",
|
"alert": "Уведомление",
|
||||||
|
"copied_to_clipboard": "Скопировано в буфер обмена!",
|
||||||
"app_name": "SelfPrivacy"
|
"app_name": "SelfPrivacy"
|
||||||
},
|
},
|
||||||
"more_page": {
|
"more_page": {
|
||||||
|
@ -197,6 +198,7 @@
|
||||||
"autobackup_custom_hint": "Введите период в минутах",
|
"autobackup_custom_hint": "Введите период в минутах",
|
||||||
"autobackup_set_period": "Установить период",
|
"autobackup_set_period": "Установить период",
|
||||||
"autobackup_period_set": "Период установлен",
|
"autobackup_period_set": "Период установлен",
|
||||||
|
"backups_encryption_key": "Ключ шифрования",
|
||||||
"snapshots_title": "Список снимков"
|
"snapshots_title": "Список снимков"
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
|
@ -536,4 +538,4 @@
|
||||||
"ignore_tls_description": "Приложение не будет проверять сертификаты TLS при подключении к серверу.",
|
"ignore_tls_description": "Приложение не будет проверять сертификаты TLS при подключении к серверу.",
|
||||||
"ignore_tls": "Не проверять сертификаты TLS"
|
"ignore_tls": "Не проверять сертификаты TLS"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -71,15 +71,14 @@ abstract class GraphQLApiMap {
|
||||||
'https://api.$rootAddress/graphql',
|
'https://api.$rootAddress/graphql',
|
||||||
httpClient: ioClient,
|
httpClient: ioClient,
|
||||||
parser: ResponseLoggingParser(),
|
parser: ResponseLoggingParser(),
|
||||||
|
defaultHeaders: {'Accept-Language': _locale},
|
||||||
);
|
);
|
||||||
|
|
||||||
final String token = _getApiToken();
|
|
||||||
|
|
||||||
final Link graphQLLink = RequestLoggingLink().concat(
|
final Link graphQLLink = RequestLoggingLink().concat(
|
||||||
isWithToken
|
isWithToken
|
||||||
? AuthLink(
|
? AuthLink(
|
||||||
getToken: () async =>
|
getToken: () async =>
|
||||||
customToken == '' ? 'Bearer $token' : customToken,
|
customToken == '' ? 'Bearer $_token' : customToken,
|
||||||
).concat(httpLink)
|
).concat(httpLink)
|
||||||
: httpLink,
|
: httpLink,
|
||||||
);
|
);
|
||||||
|
@ -95,13 +94,16 @@ abstract class GraphQLApiMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GraphQLClient> getSubscriptionClient() async {
|
Future<GraphQLClient> getSubscriptionClient() async {
|
||||||
final String token = _getApiToken();
|
|
||||||
|
|
||||||
final WebSocketLink webSocketLink = WebSocketLink(
|
final WebSocketLink webSocketLink = WebSocketLink(
|
||||||
'ws://api.$rootAddress/graphql',
|
'ws://api.$rootAddress/graphql',
|
||||||
config: SocketClientConfig(
|
config: SocketClientConfig(
|
||||||
autoReconnect: true,
|
autoReconnect: true,
|
||||||
headers: token.isEmpty ? null : {'Authorization': 'Bearer $token'},
|
headers: _token.isEmpty
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
'Authorization': 'Bearer $_token',
|
||||||
|
'Accept-Language': _locale,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -111,7 +113,9 @@ abstract class GraphQLApiMap {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getApiToken() {
|
String get _locale => getIt.get<ApiConfigModel>().localeCode ?? 'en';
|
||||||
|
|
||||||
|
String get _token {
|
||||||
String token = '';
|
String token = '';
|
||||||
final serverDetails = getIt<ApiConfigModel>().serverDetails;
|
final serverDetails = getIt<ApiConfigModel>().serverDetails;
|
||||||
if (serverDetails != null) {
|
if (serverDetails != null) {
|
||||||
|
|
|
@ -721,12 +721,18 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
||||||
await repository.saveIsServerResetedSecondTime(true);
|
await repository.saveIsServerResetedSecondTime(true);
|
||||||
await repository.saveHasFinalChecked(true);
|
await repository.saveHasFinalChecked(true);
|
||||||
await repository.saveIsRecoveringServer(false);
|
await repository.saveIsRecoveringServer(false);
|
||||||
|
final serverType = await ProvidersController.currentServerProvider!
|
||||||
|
.getServerType(state.serverDetails!.id);
|
||||||
|
await repository.saveServerType(serverType.data!);
|
||||||
|
await ProvidersController.currentServerProvider!
|
||||||
|
.trySetServerLocation(serverType.data!.location.identifier);
|
||||||
final User mainUser = await repository.getMainUser();
|
final User mainUser = await repository.getMainUser();
|
||||||
await repository.saveRootUser(mainUser);
|
await repository.saveRootUser(mainUser);
|
||||||
final ServerInstallationRecovery updatedState =
|
final ServerInstallationRecovery updatedState =
|
||||||
(state as ServerInstallationRecovery).copyWith(
|
(state as ServerInstallationRecovery).copyWith(
|
||||||
backblazeCredential: backblazeCredential,
|
backblazeCredential: backblazeCredential,
|
||||||
rootUser: mainUser,
|
rootUser: mainUser,
|
||||||
|
serverTypeIdentificator: serverType.data!.identifier,
|
||||||
);
|
);
|
||||||
emit(updatedState.finish());
|
emit(updatedState.finish());
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,21 +75,44 @@ class ServerInstallationRepository {
|
||||||
|
|
||||||
if (box.get(BNames.hasFinalChecked, defaultValue: false)) {
|
if (box.get(BNames.hasFinalChecked, defaultValue: false)) {
|
||||||
TlsOptions.verifyCertificate = true;
|
TlsOptions.verifyCertificate = true;
|
||||||
return ServerInstallationFinished(
|
if (serverTypeIdentificator == null && serverDetails != null) {
|
||||||
installationDialoguePopUp: null,
|
final finalServerType = await ProvidersController.currentServerProvider!
|
||||||
providerApiToken: providerApiToken!,
|
.getServerType(serverDetails.id);
|
||||||
serverTypeIdentificator: serverTypeIdentificator!,
|
await saveServerType(finalServerType.data!);
|
||||||
dnsApiToken: dnsApiToken!,
|
await ProvidersController.currentServerProvider!
|
||||||
serverDomain: serverDomain!,
|
.trySetServerLocation(finalServerType.data!.location.identifier);
|
||||||
backblazeCredential: backblazeCredential!,
|
return ServerInstallationFinished(
|
||||||
serverDetails: serverDetails!,
|
installationDialoguePopUp: null,
|
||||||
rootUser: box.get(BNames.rootUser),
|
providerApiToken: providerApiToken!,
|
||||||
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
|
serverTypeIdentificator: finalServerType.data!.identifier,
|
||||||
isServerResetedFirstTime:
|
dnsApiToken: dnsApiToken!,
|
||||||
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
|
serverDomain: serverDomain!,
|
||||||
isServerResetedSecondTime:
|
backblazeCredential: backblazeCredential!,
|
||||||
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
|
serverDetails: serverDetails,
|
||||||
);
|
rootUser: box.get(BNames.rootUser),
|
||||||
|
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
|
||||||
|
isServerResetedFirstTime:
|
||||||
|
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
|
||||||
|
isServerResetedSecondTime:
|
||||||
|
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return ServerInstallationFinished(
|
||||||
|
installationDialoguePopUp: null,
|
||||||
|
providerApiToken: providerApiToken!,
|
||||||
|
serverTypeIdentificator: serverTypeIdentificator!,
|
||||||
|
dnsApiToken: dnsApiToken!,
|
||||||
|
serverDomain: serverDomain!,
|
||||||
|
backblazeCredential: backblazeCredential!,
|
||||||
|
serverDetails: serverDetails!,
|
||||||
|
rootUser: box.get(BNames.rootUser),
|
||||||
|
isServerStarted: box.get(BNames.isServerStarted, defaultValue: false),
|
||||||
|
isServerResetedFirstTime:
|
||||||
|
box.get(BNames.isServerResetedFirstTime, defaultValue: false),
|
||||||
|
isServerResetedSecondTime:
|
||||||
|
box.get(BNames.isServerResetedSecondTime, defaultValue: false),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (box.get(BNames.isRecoveringServer, defaultValue: false) &&
|
if (box.get(BNames.isRecoveringServer, defaultValue: false) &&
|
||||||
|
|
|
@ -9,6 +9,7 @@ class ApiConfigModel {
|
||||||
final Box _box = Hive.box(BNames.serverInstallationBox);
|
final Box _box = Hive.box(BNames.serverInstallationBox);
|
||||||
|
|
||||||
ServerHostingDetails? get serverDetails => _serverDetails;
|
ServerHostingDetails? get serverDetails => _serverDetails;
|
||||||
|
String? get localeCode => _localeCode;
|
||||||
String? get serverProviderKey => _serverProviderKey;
|
String? get serverProviderKey => _serverProviderKey;
|
||||||
String? get serverLocation => _serverLocation;
|
String? get serverLocation => _serverLocation;
|
||||||
String? get serverType => _serverType;
|
String? get serverType => _serverType;
|
||||||
|
@ -20,6 +21,7 @@ class ApiConfigModel {
|
||||||
ServerDomain? get serverDomain => _serverDomain;
|
ServerDomain? get serverDomain => _serverDomain;
|
||||||
BackblazeBucket? get backblazeBucket => _backblazeBucket;
|
BackblazeBucket? get backblazeBucket => _backblazeBucket;
|
||||||
|
|
||||||
|
String? _localeCode;
|
||||||
String? _serverProviderKey;
|
String? _serverProviderKey;
|
||||||
String? _serverLocation;
|
String? _serverLocation;
|
||||||
String? _dnsProviderKey;
|
String? _dnsProviderKey;
|
||||||
|
@ -31,6 +33,10 @@ class ApiConfigModel {
|
||||||
ServerDomain? _serverDomain;
|
ServerDomain? _serverDomain;
|
||||||
BackblazeBucket? _backblazeBucket;
|
BackblazeBucket? _backblazeBucket;
|
||||||
|
|
||||||
|
Future<void> setLocaleCode(final String value) async {
|
||||||
|
_localeCode = value;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> storeServerProviderType(final ServerProviderType value) async {
|
Future<void> storeServerProviderType(final ServerProviderType value) async {
|
||||||
await _box.put(BNames.serverProvider, value);
|
await _box.put(BNames.serverProvider, value);
|
||||||
_serverProvider = value;
|
_serverProvider = value;
|
||||||
|
@ -82,6 +88,7 @@ class ApiConfigModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
|
_localeCode = null;
|
||||||
_serverProviderKey = null;
|
_serverProviderKey = null;
|
||||||
_dnsProvider = null;
|
_dnsProvider = null;
|
||||||
_serverLocation = null;
|
_serverLocation = null;
|
||||||
|
@ -95,6 +102,7 @@ class ApiConfigModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
|
_localeCode = 'en';
|
||||||
_serverProviderKey = _box.get(BNames.hetznerKey);
|
_serverProviderKey = _box.get(BNames.hetznerKey);
|
||||||
_serverLocation = _box.get(BNames.serverLocation);
|
_serverLocation = _box.get(BNames.serverLocation);
|
||||||
_dnsProviderKey = _box.get(BNames.cloudFlareKey);
|
_dnsProviderKey = _box.get(BNames.cloudFlareKey);
|
||||||
|
|
|
@ -88,6 +88,93 @@ class DigitalOceanServerProvider extends ServerProvider {
|
||||||
return GenericResult(success: true, data: servers);
|
return GenericResult(success: true, data: servers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<GenericResult<ServerType?>> getServerType(final int serverId) async {
|
||||||
|
ServerType? serverType;
|
||||||
|
dynamic server;
|
||||||
|
final result = await _adapter.api().getServers();
|
||||||
|
if (result.data.isEmpty || !result.success) {
|
||||||
|
return GenericResult(
|
||||||
|
success: result.success,
|
||||||
|
data: serverType,
|
||||||
|
code: result.code,
|
||||||
|
message: result.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List rawServers = result.data;
|
||||||
|
for (final rawServer in rawServers) {
|
||||||
|
if (rawServer['networks']['v4'].isNotEmpty) {
|
||||||
|
for (final v4 in rawServer['networks']['v4']) {
|
||||||
|
if (v4['type'].toString() == 'public') {
|
||||||
|
server = rawServer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server == null) {
|
||||||
|
const String msg = 'getServerType: no server!';
|
||||||
|
print(msg);
|
||||||
|
return GenericResult(
|
||||||
|
success: false,
|
||||||
|
data: serverType,
|
||||||
|
message: msg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final rawLocationsResult = await getAvailableLocations();
|
||||||
|
if (rawLocationsResult.data.isEmpty || !rawLocationsResult.success) {
|
||||||
|
return GenericResult(
|
||||||
|
success: rawLocationsResult.success,
|
||||||
|
data: serverType,
|
||||||
|
code: rawLocationsResult.code,
|
||||||
|
message: rawLocationsResult.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerProviderLocation? location;
|
||||||
|
for (final rawLocation in rawLocationsResult.data) {
|
||||||
|
if (rawLocation.identifier == server['region']['slug']) {
|
||||||
|
location = rawLocation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location == null) {
|
||||||
|
const String msg = 'getServerType: no location!';
|
||||||
|
print(msg);
|
||||||
|
return GenericResult(
|
||||||
|
success: false,
|
||||||
|
data: serverType,
|
||||||
|
message: msg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerType? type;
|
||||||
|
final rawSize = DigitalOceanServerType.fromJson(server['size']);
|
||||||
|
for (final rawRegion in rawSize.regions) {
|
||||||
|
if (rawRegion == server['region']['slug']) {
|
||||||
|
type = ServerType(
|
||||||
|
title: rawSize.description,
|
||||||
|
identifier: rawSize.slug,
|
||||||
|
ram: rawSize.memory / 1024,
|
||||||
|
cores: rawSize.vcpus,
|
||||||
|
disk: DiskSize(byte: rawSize.disk * 1024 * 1024 * 1024),
|
||||||
|
price: Price(
|
||||||
|
value: rawSize.priceMonthly,
|
||||||
|
currency: currency,
|
||||||
|
),
|
||||||
|
location: location,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GenericResult(
|
||||||
|
success: true,
|
||||||
|
data: type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
|
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
|
||||||
final LaunchInstallationData installationData,
|
final LaunchInstallationData installationData,
|
||||||
|
|
|
@ -88,6 +88,78 @@ class HetznerServerProvider extends ServerProvider {
|
||||||
return GenericResult(success: true, data: servers);
|
return GenericResult(success: true, data: servers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<GenericResult<ServerType?>> getServerType(final int serverId) async {
|
||||||
|
ServerType? serverType;
|
||||||
|
HetznerServerInfo? server;
|
||||||
|
final result = await _adapter.api().getServers();
|
||||||
|
if (result.data.isEmpty || !result.success) {
|
||||||
|
return GenericResult(
|
||||||
|
success: result.success,
|
||||||
|
data: serverType,
|
||||||
|
code: result.code,
|
||||||
|
message: result.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<HetznerServerInfo> hetznerServers = result.data;
|
||||||
|
for (final hetznerServer in hetznerServers) {
|
||||||
|
if (hetznerServer.publicNet.ipv4 != null ||
|
||||||
|
hetznerServer.id == serverId) {
|
||||||
|
server = hetznerServer;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server == null) {
|
||||||
|
const String msg = 'getServerType: no server!';
|
||||||
|
print(msg);
|
||||||
|
return GenericResult(
|
||||||
|
success: false,
|
||||||
|
data: serverType,
|
||||||
|
message: msg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double? priceValue;
|
||||||
|
for (final price in server.serverType.prices) {
|
||||||
|
if (price.location == server.location.name) {
|
||||||
|
priceValue = price.monthly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priceValue == null) {
|
||||||
|
const String msg = 'getServerType: no price!';
|
||||||
|
print(msg);
|
||||||
|
return GenericResult(
|
||||||
|
success: false,
|
||||||
|
data: serverType,
|
||||||
|
message: msg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GenericResult(
|
||||||
|
success: true,
|
||||||
|
data: ServerType(
|
||||||
|
title: server.serverType.description,
|
||||||
|
identifier: server.serverType.name,
|
||||||
|
ram: server.serverType.memory.toDouble(),
|
||||||
|
cores: server.serverType.cores,
|
||||||
|
disk: DiskSize(byte: server.serverType.disk * 1024 * 1024 * 1024),
|
||||||
|
price: Price(
|
||||||
|
value: priceValue,
|
||||||
|
currency: currency,
|
||||||
|
),
|
||||||
|
location: ServerProviderLocation(
|
||||||
|
title: server.location.city,
|
||||||
|
description: server.location.description,
|
||||||
|
flag: server.location.flag,
|
||||||
|
identifier: server.location.name,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
|
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
|
||||||
final LaunchInstallationData installationData,
|
final LaunchInstallationData installationData,
|
||||||
|
|
|
@ -24,6 +24,11 @@ abstract class ServerProvider {
|
||||||
/// Only with public IPv4 addresses.
|
/// Only with public IPv4 addresses.
|
||||||
Future<GenericResult<List<ServerBasicInfo>>> getServers();
|
Future<GenericResult<List<ServerBasicInfo>>> getServers();
|
||||||
|
|
||||||
|
/// Returns actual [ServerType] of the
|
||||||
|
/// requested server entry assigned
|
||||||
|
/// to the authorized user.
|
||||||
|
Future<GenericResult<ServerType?>> getServerType(final int serverId);
|
||||||
|
|
||||||
/// Tries to launch installation of SelfPrivacy on
|
/// Tries to launch installation of SelfPrivacy on
|
||||||
/// the requested server entry for the authorized account.
|
/// the requested server entry for the authorized account.
|
||||||
/// Depending on a server provider, the algorithm
|
/// Depending on a server provider, the algorithm
|
||||||
|
|
|
@ -71,34 +71,38 @@ class SelfprivacyApp extends StatelessWidget {
|
||||||
builder: (
|
builder: (
|
||||||
final BuildContext context,
|
final BuildContext context,
|
||||||
final AppSettingsState appSettings,
|
final AppSettingsState appSettings,
|
||||||
) =>
|
) {
|
||||||
MaterialApp.router(
|
getIt.get<ApiConfigModel>().setLocaleCode(
|
||||||
routeInformationParser: _appRouter.defaultRouteParser(),
|
context.locale.languageCode,
|
||||||
routerDelegate: _appRouter.delegate(),
|
);
|
||||||
scaffoldMessengerKey:
|
return MaterialApp.router(
|
||||||
getIt.get<NavigationService>().scaffoldMessengerKey,
|
routeInformationParser: _appRouter.defaultRouteParser(),
|
||||||
localizationsDelegates: context.localizationDelegates,
|
routerDelegate: _appRouter.delegate(),
|
||||||
supportedLocales: context.supportedLocales,
|
scaffoldMessengerKey:
|
||||||
locale: context.locale,
|
getIt.get<NavigationService>().scaffoldMessengerKey,
|
||||||
debugShowCheckedModeBanner: false,
|
localizationsDelegates: context.localizationDelegates,
|
||||||
title: 'SelfPrivacy',
|
supportedLocales: context.supportedLocales,
|
||||||
theme: lightThemeData,
|
locale: context.locale,
|
||||||
darkTheme: darkThemeData,
|
debugShowCheckedModeBanner: false,
|
||||||
themeMode: appSettings.isAutoDarkModeOn
|
title: 'SelfPrivacy',
|
||||||
? ThemeMode.system
|
theme: lightThemeData,
|
||||||
: appSettings.isDarkModeOn
|
darkTheme: darkThemeData,
|
||||||
? ThemeMode.dark
|
themeMode: appSettings.isAutoDarkModeOn
|
||||||
: ThemeMode.light,
|
? ThemeMode.system
|
||||||
builder: (final BuildContext context, final Widget? widget) {
|
: appSettings.isDarkModeOn
|
||||||
Widget error = const Text('...rendering error...');
|
? ThemeMode.dark
|
||||||
if (widget is Scaffold || widget is Navigator) {
|
: ThemeMode.light,
|
||||||
error = Scaffold(body: Center(child: error));
|
builder: (final BuildContext context, final Widget? widget) {
|
||||||
}
|
Widget error = const Text('...rendering error...');
|
||||||
ErrorWidget.builder =
|
if (widget is Scaffold || widget is Navigator) {
|
||||||
(final FlutterErrorDetails errorDetails) => error;
|
error = Scaffold(body: Center(child: error));
|
||||||
return widget!;
|
}
|
||||||
},
|
ErrorWidget.builder =
|
||||||
),
|
(final FlutterErrorDetails errorDetails) => error;
|
||||||
|
return widget!;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,6 +16,7 @@ import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
||||||
import 'package:selfprivacy/ui/helpers/modals.dart';
|
import 'package:selfprivacy/ui/helpers/modals.dart';
|
||||||
import 'package:selfprivacy/ui/pages/backups/change_period_modal.dart';
|
import 'package:selfprivacy/ui/pages/backups/change_period_modal.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/backups/copy_encryption_key_modal.dart';
|
||||||
import 'package:selfprivacy/ui/pages/backups/create_backups_modal.dart';
|
import 'package:selfprivacy/ui/pages/backups/create_backups_modal.dart';
|
||||||
import 'package:selfprivacy/ui/router/router.dart';
|
import 'package:selfprivacy/ui/router/router.dart';
|
||||||
import 'package:selfprivacy/utils/extensions/duration.dart';
|
import 'package:selfprivacy/utils/extensions/duration.dart';
|
||||||
|
@ -144,6 +145,37 @@ class BackupDetailsPage extends StatelessWidget {
|
||||||
: 'backup.autobackup_period_never'.tr(),
|
: 'backup.autobackup_period_never'.tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
onTap: preventActions
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (final BuildContext context) =>
|
||||||
|
DraggableScrollableSheet(
|
||||||
|
expand: false,
|
||||||
|
maxChildSize: 0.9,
|
||||||
|
minChildSize: 0.5,
|
||||||
|
initialChildSize: 0.7,
|
||||||
|
builder: (final context, final scrollController) =>
|
||||||
|
CopyEncryptionKeyModal(
|
||||||
|
scrollController: scrollController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.key_outlined,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'backup.backups_encryption_key'.tr(),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'backup.backups_encryption_key_subtitle'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (backupJobs.isNotEmpty)
|
if (backupJobs.isNotEmpty)
|
||||||
Column(
|
Column(
|
||||||
|
|
|
@ -20,9 +20,6 @@ class ChangeAutobackupsPeriodModal extends StatefulWidget {
|
||||||
|
|
||||||
class _ChangeAutobackupsPeriodModalState
|
class _ChangeAutobackupsPeriodModalState
|
||||||
extends State<ChangeAutobackupsPeriodModal> {
|
extends State<ChangeAutobackupsPeriodModal> {
|
||||||
// This is a modal with radio buttons to select the autobackup period
|
|
||||||
// Period might be none, selected from predefined list or custom
|
|
||||||
// Store in state the selected period
|
|
||||||
Duration? selectedPeriod;
|
Duration? selectedPeriod;
|
||||||
|
|
||||||
static const List<Duration> autobackupPeriods = [
|
static const List<Duration> autobackupPeriods = [
|
||||||
|
|
141
lib/ui/pages/backups/copy_encryption_key_modal.dart
Normal file
141
lib/ui/pages/backups/copy_encryption_key_modal.dart
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
|
||||||
|
|
||||||
|
class CopyEncryptionKeyModal extends StatefulWidget {
|
||||||
|
const CopyEncryptionKeyModal({
|
||||||
|
required this.scrollController,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ScrollController scrollController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CopyEncryptionKeyModal> createState() => _CopyEncryptionKeyModalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CopyEncryptionKeyModalState extends State<CopyEncryptionKeyModal> {
|
||||||
|
bool isKeyVisible = false;
|
||||||
|
bool copiedToClipboard = false;
|
||||||
|
Timer? copyToClipboardTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
copyToClipboardTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) {
|
||||||
|
final String encryptionKey =
|
||||||
|
context.watch<BackupsCubit>().state.backblazeBucket!.encryptionKey;
|
||||||
|
return ListView(
|
||||||
|
controller: widget.scrollController,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'backup.backups_encryption_key'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'backup.backups_encryption_key_description'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
SelectableText(
|
||||||
|
encryptionKey,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
setState(
|
||||||
|
() {
|
||||||
|
isKeyVisible = !isKeyVisible;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
opacity: isKeyVisible ? 0 : 1,
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.visibility_outlined),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'backup.backups_encryption_key_show'.tr(),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
setState(
|
||||||
|
() {
|
||||||
|
copiedToClipboard = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
copyToClipboardTimer?.cancel();
|
||||||
|
copyToClipboardTimer = Timer(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
() {
|
||||||
|
setState(() {
|
||||||
|
copiedToClipboard = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Clipboard.setData(
|
||||||
|
ClipboardData(
|
||||||
|
text: encryptionKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.copy_all_outlined),
|
||||||
|
label: Text(
|
||||||
|
copiedToClipboard
|
||||||
|
? 'basis.copied_to_clipboard'.tr()
|
||||||
|
: 'backup.backups_encryption_key_copy'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -139,7 +139,9 @@ class _ServicePageState extends State<ServicePage> {
|
||||||
),
|
),
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
enabled: !serviceDisabled && !serviceLocked,
|
enabled: !serviceDisabled &&
|
||||||
|
!serviceLocked &&
|
||||||
|
service.storageUsage.volume != null,
|
||||||
),
|
),
|
||||||
if (service.canBeBackedUp)
|
if (service.canBeBackedUp)
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
Loading…
Reference in a new issue