Compare commits

...

8 Commits

Author SHA1 Message Date
dettlaff d77dcc7167 fix: update english howto get token from the ssh
continuous-integration/drone/push Build is failing Details
2024-06-06 18:26:10 +03:00
NaiJi ✨ 291a6507ae feat(jobs): Implement garbage collection job (#506)
continuous-integration/drone/push Build is passing Details
Co-authored-by: Inex Code <inex.code@selfprivacy.org>
Reviewed-on: #506
Reviewed-by: Inex Code <inex.code@selfprivacy.org>
Co-authored-by: NaiJi <naijiworld@protonmail.com>
Co-committed-by: NaiJi <naijiworld@protonmail.com>
2024-05-25 12:32:21 +03:00
Inex Code 4930fc2387 feat: Show the error screen when libsecret fails
continuous-integration/drone/push Build is passing Details
2024-05-02 15:05:38 +03:00
Inex Code 11d0e58334 fix: Flatpak builds didn't work 2024-04-26 18:08:04 +03:00
NaiJi ✨ a6b846cc78 feat(backups): Show how much space a service uses on backup (#500)
continuous-integration/drone/push Build is passing Details
Fixes #434

![image](/attachments/351cc025-8dae-44f2-9bca-18f8950e0780)

Co-authored-by: Inex Code <inex.code@selfprivacy.org>
Reviewed-on: #500
Reviewed-by: Inex Code <inex.code@selfprivacy.org>
Co-authored-by: NaiJi  <naiji@noreply.git.selfprivacy.org>
Co-committed-by: NaiJi  <naiji@noreply.git.selfprivacy.org>
2024-04-24 13:18:02 +03:00
NaiJi ✨ 6819192219 feat: Add country names to installation process (#501)
continuous-integration/drone/push Build is passing Details
Fixes #494

Reviewed-on: #501
Reviewed-by: Inex Code <inex.code@selfprivacy.org>
Co-authored-by: NaiJi  <naiji@noreply.git.selfprivacy.org>
Co-committed-by: NaiJi  <naiji@noreply.git.selfprivacy.org>
2024-04-24 12:54:32 +03:00
NaiJi ✨ ffdb9d92fb Merge pull request 'fix(backups): Implement filtering for enabled services for backups' (#499) from filter-enabled-backup-services into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #499
Reviewed-by: Inex Code <inex.code@selfprivacy.org>
2024-04-17 18:48:56 +03:00
NaiJi ✨ 1c42598787 fix(backups): Implement filtering for enabled services for backups
- Resolve: #433
2024-04-16 23:03:11 +04:00
31 changed files with 1083 additions and 171 deletions

6
.gitignore vendored
View File

@ -40,3 +40,9 @@ app.*.symbols
# Obfuscation related
app.*.map.json
# Flatpak
.flatpak-builder/
flatpak-build/
flatpak-repo/
*.flatpak

View File

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View File

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View File

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View File

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View File

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View File

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View File

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View File

@ -1,19 +1,7 @@
Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json`
[Login as root user to your server](https://selfprivacy.org/docs/how-to-guides/root_ssh/) and enter this command:
```sh
cat /etc/nixos/userdata/tokens.json
sp-print-api-token
```
This file will have a similar construction:
```json
{
"tokens": [
{
"token": "token_to_copy",
"name": "device_name",
"date": "date"
}
```
Copy the token from the file and paste it in the next window.
Copy the token from the terminal and paste it in the next window.

View File

@ -565,6 +565,8 @@
"upgrade_success": "Server upgrade started",
"upgrade_failed": "Failed to upgrade server",
"upgrade_server": "Upgrade server",
"collect_nix_garbage": "Collect system garbage",
"collect_nix_garbage_failed": "Failed to collect system garbage",
"reboot_server": "Reboot server",
"create_ssh_key": "Create SSH key for {}",
"delete_ssh_key": "Delete SSH key for {}",
@ -606,5 +608,16 @@
"reset_onboarding": "Reset onboarding switch",
"reset_onboarding_description": "Reset onboarding switch to show onboarding screen again",
"cubit_statuses": "Cubit loading statuses"
},
"countries": {
"germany": "Germany",
"netherlands": "Netherlands",
"singapore": "Singapore",
"united_kingdom": "United Kingdom",
"canada": "Canada",
"india": "India",
"australia": "Australia",
"united_states": "United States",
"finland": "Finland"
}
}
}

View File

@ -1,6 +1,6 @@
app-id: org.selfprivacy.app
runtime: org.freedesktop.Platform
runtime-version: '22.08'
runtime-version: '23.08'
sdk: org.freedesktop.Sdk
command: selfprivacy
finish-args:
@ -11,6 +11,7 @@ finish-args:
- "--share=network"
- "--own-name=org.selfprivacy.app"
- "--device=dri"
- "--talk-name=org.freedesktop.secrets"
modules:
- name: selfprivacy
buildsystem: simple
@ -35,7 +36,7 @@ modules:
sources:
- type: git
url: https://gitlab.gnome.org/GNOME/libsecret.git
tag: 0.20.5
tag: 0.21.4
- name: libjsoncpp
buildsystem: meson
config-opts:

View File

@ -1,6 +1,6 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
@ -28,33 +28,47 @@ class HiveConfig {
await Hive.openBox(BNames.appSettingsBox);
final HiveAesCipher cipher = HiveAesCipher(
await getEncryptedKey(BNames.serverInstallationEncryptionKey),
);
try {
final HiveAesCipher cipher = HiveAesCipher(
await getEncryptedKey(BNames.serverInstallationEncryptionKey),
);
await Hive.openBox<User>(BNames.usersDeprecated);
await Hive.openBox<User>(BNames.usersBox, encryptionCipher: cipher);
await Hive.openBox<User>(BNames.usersDeprecated);
await Hive.openBox<User>(BNames.usersBox, encryptionCipher: cipher);
final Box<User> deprecatedUsers = Hive.box<User>(BNames.usersDeprecated);
if (deprecatedUsers.isNotEmpty) {
final Box<User> users = Hive.box<User>(BNames.usersBox);
await users.addAll(deprecatedUsers.values.toList());
await deprecatedUsers.clear();
final Box<User> deprecatedUsers = Hive.box<User>(BNames.usersDeprecated);
if (deprecatedUsers.isNotEmpty) {
final Box<User> users = Hive.box<User>(BNames.usersBox);
await users.addAll(deprecatedUsers.values.toList());
await deprecatedUsers.clear();
}
await Hive.openBox(
BNames.serverInstallationBox,
encryptionCipher: cipher,
);
} on PlatformException catch (e) {
print('HiveConfig: Error while opening boxes: $e');
rethrow;
}
await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher);
}
static Future<Uint8List> getEncryptedKey(final String encKey) async {
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
final bool hasEncryptionKey = await secureStorage.containsKey(key: encKey);
if (!hasEncryptionKey) {
final List<int> key = Hive.generateSecureKey();
await secureStorage.write(key: encKey, value: base64UrlEncode(key));
}
try {
final bool hasEncryptionKey =
await secureStorage.containsKey(key: encKey);
if (!hasEncryptionKey) {
final List<int> key = Hive.generateSecureKey();
await secureStorage.write(key: encKey, value: base64UrlEncode(key));
}
final String? string = await secureStorage.read(key: encKey);
return base64Url.decode(string!);
final String? string = await secureStorage.read(key: encKey);
return base64Url.decode(string!);
} on PlatformException catch (e) {
print('HiveConfig: Error while getting encryption key: $e');
rethrow;
}
}
}

View File

@ -443,6 +443,7 @@ type SystemMutations {
runSystemUpgrade: GenericJobMutationReturn!
rebootSystem: GenericMutationReturn!
pullRepositoryChanges: GenericMutationReturn!
nixCollectGarbage: GenericJobMutationReturn!
}
type SystemProviderInfo {

View File

@ -79,6 +79,17 @@ mutation RunSystemUpgrade {
}
}
mutation NixCollectGarbage {
system {
nixCollectGarbage {
...basicMutationReturnFields
job {
...basicApiJobsFields
}
}
}
}
mutation RunSystemUpgradeFallback {
system {
runSystemUpgrade {

View File

@ -7043,6 +7043,663 @@ class _CopyWithStubImpl$Mutation$RunSystemUpgrade$system$runSystemUpgrade<TRes>
CopyWith$Fragment$basicApiJobsFields.stub(_res);
}
class Mutation$NixCollectGarbage {
Mutation$NixCollectGarbage({
required this.system,
this.$__typename = 'Mutation',
});
factory Mutation$NixCollectGarbage.fromJson(Map<String, dynamic> json) {
final l$system = json['system'];
final l$$__typename = json['__typename'];
return Mutation$NixCollectGarbage(
system: Mutation$NixCollectGarbage$system.fromJson(
(l$system as Map<String, dynamic>)),
$__typename: (l$$__typename as String),
);
}
final Mutation$NixCollectGarbage$system system;
final String $__typename;
Map<String, dynamic> toJson() {
final _resultData = <String, dynamic>{};
final l$system = system;
_resultData['system'] = l$system.toJson();
final l$$__typename = $__typename;
_resultData['__typename'] = l$$__typename;
return _resultData;
}
@override
int get hashCode {
final l$system = system;
final l$$__typename = $__typename;
return Object.hashAll([
l$system,
l$$__typename,
]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (!(other is Mutation$NixCollectGarbage) ||
runtimeType != other.runtimeType) {
return false;
}
final l$system = system;
final lOther$system = other.system;
if (l$system != lOther$system) {
return false;
}
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) {
return false;
}
return true;
}
}
extension UtilityExtension$Mutation$NixCollectGarbage
on Mutation$NixCollectGarbage {
CopyWith$Mutation$NixCollectGarbage<Mutation$NixCollectGarbage>
get copyWith => CopyWith$Mutation$NixCollectGarbage(
this,
(i) => i,
);
}
abstract class CopyWith$Mutation$NixCollectGarbage<TRes> {
factory CopyWith$Mutation$NixCollectGarbage(
Mutation$NixCollectGarbage instance,
TRes Function(Mutation$NixCollectGarbage) then,
) = _CopyWithImpl$Mutation$NixCollectGarbage;
factory CopyWith$Mutation$NixCollectGarbage.stub(TRes res) =
_CopyWithStubImpl$Mutation$NixCollectGarbage;
TRes call({
Mutation$NixCollectGarbage$system? system,
String? $__typename,
});
CopyWith$Mutation$NixCollectGarbage$system<TRes> get system;
}
class _CopyWithImpl$Mutation$NixCollectGarbage<TRes>
implements CopyWith$Mutation$NixCollectGarbage<TRes> {
_CopyWithImpl$Mutation$NixCollectGarbage(
this._instance,
this._then,
);
final Mutation$NixCollectGarbage _instance;
final TRes Function(Mutation$NixCollectGarbage) _then;
static const _undefined = <dynamic, dynamic>{};
TRes call({
Object? system = _undefined,
Object? $__typename = _undefined,
}) =>
_then(Mutation$NixCollectGarbage(
system: system == _undefined || system == null
? _instance.system
: (system as Mutation$NixCollectGarbage$system),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String),
));
CopyWith$Mutation$NixCollectGarbage$system<TRes> get system {
final local$system = _instance.system;
return CopyWith$Mutation$NixCollectGarbage$system(
local$system, (e) => call(system: e));
}
}
class _CopyWithStubImpl$Mutation$NixCollectGarbage<TRes>
implements CopyWith$Mutation$NixCollectGarbage<TRes> {
_CopyWithStubImpl$Mutation$NixCollectGarbage(this._res);
TRes _res;
call({
Mutation$NixCollectGarbage$system? system,
String? $__typename,
}) =>
_res;
CopyWith$Mutation$NixCollectGarbage$system<TRes> get system =>
CopyWith$Mutation$NixCollectGarbage$system.stub(_res);
}
const documentNodeMutationNixCollectGarbage = DocumentNode(definitions: [
OperationDefinitionNode(
type: OperationType.mutation,
name: NameNode(value: 'NixCollectGarbage'),
variableDefinitions: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'system'),
alias: null,
arguments: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'nixCollectGarbage'),
alias: null,
arguments: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FragmentSpreadNode(
name: NameNode(value: 'basicMutationReturnFields'),
directives: [],
),
FieldNode(
name: NameNode(value: 'job'),
alias: null,
arguments: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FragmentSpreadNode(
name: NameNode(value: 'basicApiJobsFields'),
directives: [],
),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
]),
),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
]),
),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
]),
),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
]),
),
fragmentDefinitionbasicMutationReturnFields,
fragmentDefinitionbasicApiJobsFields,
]);
Mutation$NixCollectGarbage _parserFn$Mutation$NixCollectGarbage(
Map<String, dynamic> data) =>
Mutation$NixCollectGarbage.fromJson(data);
typedef OnMutationCompleted$Mutation$NixCollectGarbage = FutureOr<void>
Function(
Map<String, dynamic>?,
Mutation$NixCollectGarbage?,
);
class Options$Mutation$NixCollectGarbage
extends graphql.MutationOptions<Mutation$NixCollectGarbage> {
Options$Mutation$NixCollectGarbage({
String? operationName,
graphql.FetchPolicy? fetchPolicy,
graphql.ErrorPolicy? errorPolicy,
graphql.CacheRereadPolicy? cacheRereadPolicy,
Object? optimisticResult,
Mutation$NixCollectGarbage? typedOptimisticResult,
graphql.Context? context,
OnMutationCompleted$Mutation$NixCollectGarbage? onCompleted,
graphql.OnMutationUpdate<Mutation$NixCollectGarbage>? update,
graphql.OnError? onError,
}) : onCompletedWithParsed = onCompleted,
super(
operationName: operationName,
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
cacheRereadPolicy: cacheRereadPolicy,
optimisticResult: optimisticResult ?? typedOptimisticResult?.toJson(),
context: context,
onCompleted: onCompleted == null
? null
: (data) => onCompleted(
data,
data == null
? null
: _parserFn$Mutation$NixCollectGarbage(data),
),
update: update,
onError: onError,
document: documentNodeMutationNixCollectGarbage,
parserFn: _parserFn$Mutation$NixCollectGarbage,
);
final OnMutationCompleted$Mutation$NixCollectGarbage? onCompletedWithParsed;
@override
List<Object?> get properties => [
...super.onCompleted == null
? super.properties
: super.properties.where((property) => property != onCompleted),
onCompletedWithParsed,
];
}
class WatchOptions$Mutation$NixCollectGarbage
extends graphql.WatchQueryOptions<Mutation$NixCollectGarbage> {
WatchOptions$Mutation$NixCollectGarbage({
String? operationName,
graphql.FetchPolicy? fetchPolicy,
graphql.ErrorPolicy? errorPolicy,
graphql.CacheRereadPolicy? cacheRereadPolicy,
Object? optimisticResult,
Mutation$NixCollectGarbage? typedOptimisticResult,
graphql.Context? context,
Duration? pollInterval,
bool? eagerlyFetchResults,
bool carryForwardDataOnException = true,
bool fetchResults = false,
}) : super(
operationName: operationName,
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
cacheRereadPolicy: cacheRereadPolicy,
optimisticResult: optimisticResult ?? typedOptimisticResult?.toJson(),
context: context,
document: documentNodeMutationNixCollectGarbage,
pollInterval: pollInterval,
eagerlyFetchResults: eagerlyFetchResults,
carryForwardDataOnException: carryForwardDataOnException,
fetchResults: fetchResults,
parserFn: _parserFn$Mutation$NixCollectGarbage,
);
}
extension ClientExtension$Mutation$NixCollectGarbage on graphql.GraphQLClient {
Future<graphql.QueryResult<Mutation$NixCollectGarbage>>
mutate$NixCollectGarbage(
[Options$Mutation$NixCollectGarbage? options]) async =>
await this.mutate(options ?? Options$Mutation$NixCollectGarbage());
graphql.ObservableQuery<
Mutation$NixCollectGarbage> watchMutation$NixCollectGarbage(
[WatchOptions$Mutation$NixCollectGarbage? options]) =>
this.watchMutation(options ?? WatchOptions$Mutation$NixCollectGarbage());
}
class Mutation$NixCollectGarbage$system {
Mutation$NixCollectGarbage$system({
required this.nixCollectGarbage,
this.$__typename = 'SystemMutations',
});
factory Mutation$NixCollectGarbage$system.fromJson(
Map<String, dynamic> json) {
final l$nixCollectGarbage = json['nixCollectGarbage'];
final l$$__typename = json['__typename'];
return Mutation$NixCollectGarbage$system(
nixCollectGarbage:
Mutation$NixCollectGarbage$system$nixCollectGarbage.fromJson(
(l$nixCollectGarbage as Map<String, dynamic>)),
$__typename: (l$$__typename as String),
);
}
final Mutation$NixCollectGarbage$system$nixCollectGarbage nixCollectGarbage;
final String $__typename;
Map<String, dynamic> toJson() {
final _resultData = <String, dynamic>{};
final l$nixCollectGarbage = nixCollectGarbage;
_resultData['nixCollectGarbage'] = l$nixCollectGarbage.toJson();
final l$$__typename = $__typename;
_resultData['__typename'] = l$$__typename;
return _resultData;
}
@override
int get hashCode {
final l$nixCollectGarbage = nixCollectGarbage;
final l$$__typename = $__typename;
return Object.hashAll([
l$nixCollectGarbage,
l$$__typename,
]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (!(other is Mutation$NixCollectGarbage$system) ||
runtimeType != other.runtimeType) {
return false;
}
final l$nixCollectGarbage = nixCollectGarbage;
final lOther$nixCollectGarbage = other.nixCollectGarbage;
if (l$nixCollectGarbage != lOther$nixCollectGarbage) {
return false;
}
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) {
return false;
}
return true;
}
}
extension UtilityExtension$Mutation$NixCollectGarbage$system
on Mutation$NixCollectGarbage$system {
CopyWith$Mutation$NixCollectGarbage$system<Mutation$NixCollectGarbage$system>
get copyWith => CopyWith$Mutation$NixCollectGarbage$system(
this,
(i) => i,
);
}
abstract class CopyWith$Mutation$NixCollectGarbage$system<TRes> {
factory CopyWith$Mutation$NixCollectGarbage$system(
Mutation$NixCollectGarbage$system instance,
TRes Function(Mutation$NixCollectGarbage$system) then,
) = _CopyWithImpl$Mutation$NixCollectGarbage$system;
factory CopyWith$Mutation$NixCollectGarbage$system.stub(TRes res) =
_CopyWithStubImpl$Mutation$NixCollectGarbage$system;
TRes call({
Mutation$NixCollectGarbage$system$nixCollectGarbage? nixCollectGarbage,
String? $__typename,
});
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<TRes>
get nixCollectGarbage;
}
class _CopyWithImpl$Mutation$NixCollectGarbage$system<TRes>
implements CopyWith$Mutation$NixCollectGarbage$system<TRes> {
_CopyWithImpl$Mutation$NixCollectGarbage$system(
this._instance,
this._then,
);
final Mutation$NixCollectGarbage$system _instance;
final TRes Function(Mutation$NixCollectGarbage$system) _then;
static const _undefined = <dynamic, dynamic>{};
TRes call({
Object? nixCollectGarbage = _undefined,
Object? $__typename = _undefined,
}) =>
_then(Mutation$NixCollectGarbage$system(
nixCollectGarbage:
nixCollectGarbage == _undefined || nixCollectGarbage == null
? _instance.nixCollectGarbage
: (nixCollectGarbage
as Mutation$NixCollectGarbage$system$nixCollectGarbage),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String),
));
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<TRes>
get nixCollectGarbage {
final local$nixCollectGarbage = _instance.nixCollectGarbage;
return CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage(
local$nixCollectGarbage, (e) => call(nixCollectGarbage: e));
}
}
class _CopyWithStubImpl$Mutation$NixCollectGarbage$system<TRes>
implements CopyWith$Mutation$NixCollectGarbage$system<TRes> {
_CopyWithStubImpl$Mutation$NixCollectGarbage$system(this._res);
TRes _res;
call({
Mutation$NixCollectGarbage$system$nixCollectGarbage? nixCollectGarbage,
String? $__typename,
}) =>
_res;
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<TRes>
get nixCollectGarbage =>
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage.stub(
_res);
}
class Mutation$NixCollectGarbage$system$nixCollectGarbage
implements Fragment$basicMutationReturnFields$$GenericJobMutationReturn {
Mutation$NixCollectGarbage$system$nixCollectGarbage({
required this.code,
required this.message,
required this.success,
this.$__typename = 'GenericJobMutationReturn',
this.job,
});
factory Mutation$NixCollectGarbage$system$nixCollectGarbage.fromJson(
Map<String, dynamic> json) {
final l$code = json['code'];
final l$message = json['message'];
final l$success = json['success'];
final l$$__typename = json['__typename'];
final l$job = json['job'];
return Mutation$NixCollectGarbage$system$nixCollectGarbage(
code: (l$code as int),
message: (l$message as String),
success: (l$success as bool),
$__typename: (l$$__typename as String),
job: l$job == null
? null
: Fragment$basicApiJobsFields.fromJson(
(l$job as Map<String, dynamic>)),
);
}
final int code;
final String message;
final bool success;
final String $__typename;
final Fragment$basicApiJobsFields? job;
Map<String, dynamic> toJson() {
final _resultData = <String, dynamic>{};
final l$code = code;
_resultData['code'] = l$code;
final l$message = message;
_resultData['message'] = l$message;
final l$success = success;
_resultData['success'] = l$success;
final l$$__typename = $__typename;
_resultData['__typename'] = l$$__typename;
final l$job = job;
_resultData['job'] = l$job?.toJson();
return _resultData;
}
@override
int get hashCode {
final l$code = code;
final l$message = message;
final l$success = success;
final l$$__typename = $__typename;
final l$job = job;
return Object.hashAll([
l$code,
l$message,
l$success,
l$$__typename,
l$job,
]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (!(other is Mutation$NixCollectGarbage$system$nixCollectGarbage) ||
runtimeType != other.runtimeType) {
return false;
}
final l$code = code;
final lOther$code = other.code;
if (l$code != lOther$code) {
return false;
}
final l$message = message;
final lOther$message = other.message;
if (l$message != lOther$message) {
return false;
}
final l$success = success;
final lOther$success = other.success;
if (l$success != lOther$success) {
return false;
}
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) {
return false;
}
final l$job = job;
final lOther$job = other.job;
if (l$job != lOther$job) {
return false;
}
return true;
}
}
extension UtilityExtension$Mutation$NixCollectGarbage$system$nixCollectGarbage
on Mutation$NixCollectGarbage$system$nixCollectGarbage {
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<
Mutation$NixCollectGarbage$system$nixCollectGarbage>
get copyWith =>
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage(
this,
(i) => i,
);
}
abstract class CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<
TRes> {
factory CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage(
Mutation$NixCollectGarbage$system$nixCollectGarbage instance,
TRes Function(Mutation$NixCollectGarbage$system$nixCollectGarbage) then,
) = _CopyWithImpl$Mutation$NixCollectGarbage$system$nixCollectGarbage;
factory CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage.stub(
TRes res) =
_CopyWithStubImpl$Mutation$NixCollectGarbage$system$nixCollectGarbage;
TRes call({
int? code,
String? message,
bool? success,
String? $__typename,
Fragment$basicApiJobsFields? job,
});
CopyWith$Fragment$basicApiJobsFields<TRes> get job;
}
class _CopyWithImpl$Mutation$NixCollectGarbage$system$nixCollectGarbage<TRes>
implements
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<TRes> {
_CopyWithImpl$Mutation$NixCollectGarbage$system$nixCollectGarbage(
this._instance,
this._then,
);
final Mutation$NixCollectGarbage$system$nixCollectGarbage _instance;
final TRes Function(Mutation$NixCollectGarbage$system$nixCollectGarbage)
_then;
static const _undefined = <dynamic, dynamic>{};
TRes call({
Object? code = _undefined,
Object? message = _undefined,
Object? success = _undefined,
Object? $__typename = _undefined,
Object? job = _undefined,
}) =>
_then(Mutation$NixCollectGarbage$system$nixCollectGarbage(
code:
code == _undefined || code == null ? _instance.code : (code as int),
message: message == _undefined || message == null
? _instance.message
: (message as String),
success: success == _undefined || success == null
? _instance.success
: (success as bool),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String),
job: job == _undefined
? _instance.job
: (job as Fragment$basicApiJobsFields?),
));
CopyWith$Fragment$basicApiJobsFields<TRes> get job {
final local$job = _instance.job;
return local$job == null
? CopyWith$Fragment$basicApiJobsFields.stub(_then(_instance))
: CopyWith$Fragment$basicApiJobsFields(local$job, (e) => call(job: e));
}
}
class _CopyWithStubImpl$Mutation$NixCollectGarbage$system$nixCollectGarbage<
TRes>
implements
CopyWith$Mutation$NixCollectGarbage$system$nixCollectGarbage<TRes> {
_CopyWithStubImpl$Mutation$NixCollectGarbage$system$nixCollectGarbage(
this._res);
TRes _res;
call({
int? code,
String? message,
bool? success,
String? $__typename,
Fragment$basicApiJobsFields? job,
}) =>
_res;
CopyWith$Fragment$basicApiJobsFields<TRes> get job =>
CopyWith$Fragment$basicApiJobsFields.stub(_res);
}
class Mutation$RunSystemUpgradeFallback {
Mutation$RunSystemUpgradeFallback({
required this.system,

View File

@ -144,4 +144,38 @@ mixin ServerActionsApi on GraphQLApiMap {
);
}
}
Future<GenericResult<ServerJob?>> collectNixGarbage() async {
try {
final GraphQLClient client = await getClient();
final result = await client.mutate$NixCollectGarbage();
if (result.hasException) {
return GenericResult(
success: false,
data: null,
);
} else if (result.parsedData!.system.nixCollectGarbage.success &&
result.parsedData!.system.nixCollectGarbage.job != null) {
return GenericResult(
success: true,
data: ServerJob.fromGraphQL(
result.parsedData!.system.nixCollectGarbage.job!,
),
message: result.parsedData!.system.nixCollectGarbage.message,
);
} else {
return GenericResult(
success: false,
message: result.parsedData!.system.nixCollectGarbage.message,
data: null,
);
}
} catch (e) {
return GenericResult(
success: false,
message: e.toString(),
data: null,
);
}
}
}

View File

@ -178,6 +178,45 @@ class JobsCubit extends Cubit<JobsState> {
}
}
Future<void> collectNixGarbage() async {
if (state is JobsStateEmpty) {
emit(
JobsStateLoading(
[CollectNixGarbageJob(status: JobStatusEnum.running)],
null,
const [],
),
);
final result =
await getIt<ApiConnectionRepository>().api.collectNixGarbage();
if (result.success && result.data != null) {
emit(
JobsStateLoading(
[CollectNixGarbageJob(status: JobStatusEnum.finished)],
result.data!.uid,
const [],
),
);
} else if (result.success) {
emit(
JobsStateFinished(
[CollectNixGarbageJob(status: JobStatusEnum.finished)],
null,
const [],
),
);
} else {
emit(
JobsStateFinished(
[CollectNixGarbageJob(status: JobStatusEnum.error)],
null,
const [],
),
);
}
}
}
Future<void> acknowledgeFinished() async {
if (state is! JobsStateFinished) {
return;

View File

@ -62,6 +62,36 @@ class UpgradeServerJob extends ClientJob {
);
}
class CollectNixGarbageJob extends ClientJob {
CollectNixGarbageJob({
super.status,
super.message,
super.id,
}) : super(title: 'jobs.collect_nix_garbage'.tr());
@override
bool canAddTo(final List<ClientJob> jobs) =>
!jobs.any((final job) => job is CollectNixGarbageJob);
@override
Future<(bool, String)> execute() async {
final result =
await getIt<ApiConnectionRepository>().api.collectNixGarbage();
return (result.success, result.message ?? '');
}
@override
CollectNixGarbageJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
CollectNixGarbageJob(
status: status,
message: message,
id: id,
);
}
class RebootServerJob extends ClientJob {
RebootServerJob({
super.status,

View File

@ -65,14 +65,59 @@ class DigitalOceanLocation {
emoji = '🇮🇳';
break;
case 'syd':
emoji = '🇦🇺';
break;
case 'nyc':
case 'sfo':
emoji = '🇺🇸';
break;
}
return emoji;
}
String get countryDisplayKey {
String displayKey = 'countries.';
switch (slug.substring(0, 3)) {
case 'fra':
displayKey += 'germany';
break;
case 'ams':
displayKey += 'netherlands';
break;
case 'sgp':
displayKey += 'singapore';
break;
case 'lon':
displayKey += 'united_kingdom';
break;
case 'tor':
displayKey += 'canada';
break;
case 'blr':
displayKey += 'india';
break;
case 'syd':
displayKey += 'australia';
break;
case 'nyc':
case 'sfo':
displayKey += 'united_states';
break;
default:
displayKey = slug;
}
return displayKey;
}
}
@JsonSerializable()

View File

@ -155,6 +155,27 @@ class HetznerLocation {
}
return emoji;
}
String get countryDisplayKey {
String displayKey = 'countries.';
switch (country.substring(0, 2)) {
case 'DE':
displayKey += 'germany';
break;
case 'FI':
displayKey += 'finland';
break;
case 'US':
displayKey += 'united_states';
break;
default:
displayKey = country;
}
return displayKey;
}
}
/// A Volume is a highly-available, scalable, and SSD-based block storage for Servers.

View File

@ -2,12 +2,14 @@ class ServerProviderLocation {
ServerProviderLocation({
required this.title,
required this.identifier,
required this.countryDisplayKey,
this.description,
this.flag = '',
});
final String title;
final String identifier;
final String countryDisplayKey;
final String? description;
final String flag;
}

View File

@ -438,6 +438,7 @@ class DigitalOceanServerProvider extends ServerProvider {
description: rawLocation.name,
flag: rawLocation.flag,
identifier: rawLocation.slug,
countryDisplayKey: rawLocation.countryDisplayKey,
);
} catch (e) {
continue;

View File

@ -156,6 +156,7 @@ class HetznerServerProvider extends ServerProvider {
description: server.location.description,
flag: server.location.flag,
identifier: server.location.name,
countryDisplayKey: server.location.countryDisplayKey,
),
),
);
@ -456,6 +457,7 @@ class HetznerServerProvider extends ServerProvider {
description: rawLocation.description,
flag: rawLocation.flag,
identifier: rawLocation.name,
countryDisplayKey: rawLocation.countryDisplayKey,
);
} catch (e) {
continue;

View File

@ -1,5 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/bloc_config.dart';
import 'package:selfprivacy/config/bloc_observer.dart';
@ -9,13 +10,20 @@ import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/config/localization.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
import 'package:selfprivacy/ui/pages/errors/failed_to_init_secure_storage.dart';
import 'package:selfprivacy/ui/router/router.dart';
// import 'package:wakelock/wakelock.dart';
import 'package:timezone/data/latest.dart' as tz;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await HiveConfig.init();
try {
await HiveConfig.init();
} on PlatformException catch (e) {
runApp(
FailedToInitSecureStorageScreen(e: e),
);
}
// await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// try {

View File

@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
@ -63,7 +64,7 @@ class JobsContent extends StatelessWidget {
context.read<ServerInstallationCubit>().state;
if (state is JobsStateEmpty) {
widgets = [
const SizedBox(height: 80),
const Gap(80),
Center(
child: Text(
'jobs.empty'.tr(),
@ -75,12 +76,12 @@ class JobsContent extends StatelessWidget {
if (installationState is ServerInstallationFinished) {
widgets = [
...widgets,
const SizedBox(height: 80),
const Gap(80),
BrandButton.rised(
onPressed: () => context.read<JobsCubit>().upgradeServer(),
text: 'jobs.upgrade_server'.tr(),
),
const SizedBox(height: 10),
const Gap(10),
BrandButton.text(
title: 'jobs.reboot_server'.tr(),
onPressed: () {
@ -189,7 +190,7 @@ class JobsContent extends StatelessWidget {
style:
Theme.of(context).textTheme.labelSmall,
),
const SizedBox(height: 8),
const Gap(8),
LinearProgressIndicator(
value: rebuildJob?.progress == null
? 0.0
@ -206,7 +207,7 @@ class JobsContent extends StatelessWidget {
minHeight: 7.0,
borderRadius: BorderRadius.circular(7.0),
),
const SizedBox(height: 8),
const Gap(8),
if (rebuildJob?.error != null ||
rebuildJob?.result != null ||
rebuildJob?.statusText != null)
@ -282,7 +283,7 @@ class JobsContent extends StatelessWidget {
(final job) => job.uid == state.rebuildJobUid,
);
if (rebuildJob == null) {
return const SizedBox();
return const Gap(0);
}
return Row(
children: [
@ -322,7 +323,7 @@ class JobsContent extends StatelessWidget {
rebuildJob.description,
style: Theme.of(context).textTheme.labelSmall,
),
const SizedBox(height: 8),
const Gap(8),
LinearProgressIndicator(
value: rebuildJob.progress == null
? 0.0
@ -339,7 +340,7 @@ class JobsContent extends StatelessWidget {
minHeight: 7.0,
borderRadius: BorderRadius.circular(7.0),
),
const SizedBox(height: 8),
const Gap(8),
if (rebuildJob.error != null ||
rebuildJob.result != null ||
rebuildJob.statusText != null)
@ -360,7 +361,7 @@ class JobsContent extends StatelessWidget {
);
},
),
const SizedBox(height: 16),
const Gap(16),
BrandButton.rised(
onPressed: () => context.read<JobsCubit>().acknowledgeFinished(),
text: 'basis.done'.tr(),
@ -403,7 +404,7 @@ class JobsContent extends StatelessWidget {
),
),
),
const SizedBox(width: 8),
const Gap(8),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
@ -423,7 +424,7 @@ class JobsContent extends StatelessWidget {
],
),
),
const SizedBox(height: 16),
const Gap(16),
BrandButton.rised(
onPressed: hasBlockingJobs
? null
@ -436,18 +437,18 @@ class JobsContent extends StatelessWidget {
controller: controller,
padding: paddingH15V0,
children: [
const SizedBox(height: 16),
const Gap(16),
Center(
child: Text(
'jobs.title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
),
const SizedBox(height: 20),
const Gap(20),
...widgets,
const SizedBox(height: 8),
const Gap(8),
const Divider(height: 0),
const SizedBox(height: 8),
const Gap(8),
if (serverJobs.isNotEmpty)
Padding(
padding: const EdgeInsets.all(8.0),
@ -489,7 +490,7 @@ class JobsContent extends StatelessWidget {
},
),
),
const SizedBox(height: 24),
const Gap(24),
],
);
},

View File

@ -127,7 +127,9 @@ class _HeroSliverAppBarState extends State<HeroSliverAppBar> {
Widget build(final BuildContext context) {
final isMobile =
widget.ignoreBreakpoints ? true : Breakpoints.small.isActive(context);
final isJobsListEmpty = context.watch<JobsCubit>().state is JobsStateEmpty;
final isJobsListEmpty = widget.hasFlashButton
? context.watch<JobsCubit>().state is JobsStateEmpty
: true;
return SliverAppBar(
expandedHeight:
widget.hasHeroIcon ? 148.0 + _size.height : 72.0 + _size.height,

View File

@ -38,8 +38,14 @@ class BackupDetailsPage extends StatelessWidget {
: StateType.uninitialized;
final bool preventActions = backupsState.preventActions;
final List<Backup> backups = backupsState.backups;
final List<Service> services =
context.watch<ServicesBloc>().state.servicesThatCanBeBackedUp;
final List<Service> services = context
.watch<ServicesBloc>()
.state
.servicesThatCanBeBackedUp
.where(
(final service) => service.isEnabled,
)
.toList();
final Duration? autobackupPeriod = backupsState.autobackupPeriod;
final List<ServerJob> backupJobs = context
.watch<ServerJobsBloc>()

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/logic/models/service.dart';
@ -103,6 +104,29 @@ class _CreateBackupsModalState extends State<CreateBackupsModal> {
...widget.services.map(
(final Service service) {
final bool busy = busyServices.contains(service.id);
final List<Widget> descriptionWidgets = [];
if (busy) {
descriptionWidgets.add(Text('backup.service_busy'.tr()));
} else {
descriptionWidgets.add(
Text(
'service_page.uses'.tr(
namedArgs: {
'usage': service.storageUsage.used.toString(),
'volume': context
.read<VolumesBloc>()
.state
.getVolume(service.storageUsage.volume ?? '')
.displayName,
},
),
style: Theme.of(context).textTheme.labelMedium,
),
);
descriptionWidgets.add(
Text(service.backupDescription),
);
}
return CheckboxListTile.adaptive(
onChanged: !busy
? (final bool? value) {
@ -122,8 +146,9 @@ class _CreateBackupsModalState extends State<CreateBackupsModal> {
title: Text(
service.displayName,
),
subtitle: Text(
busy ? 'backup.service_busy'.tr() : service.backupDescription,
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: descriptionWidgets,
),
secondary: SvgPicture.string(
service.svgIcon,

View File

@ -0,0 +1,71 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/pages/more/about_application.dart';
class FailedToInitSecureStorageScreen extends StatelessWidget {
const FailedToInitSecureStorageScreen({
required this.e,
super.key,
});
final PlatformException e;
@override
Widget build(final BuildContext context) => MaterialApp(
home: BrandHeroScreen(
heroIcon: Icons.error_outline,
heroTitle: 'Failed to initialize secure storage',
hasBackButton: false,
children: [
const Text(
'SelfPrivacy requires a secure storage provided by your operating system to encrypt sensitive data, but it failed to initialize.',
),
if (Platform.isLinux)
const Text(
'Please make sure that the libsecret library is installed.',
),
const Gap(16),
Text('Error: ${e.message}'),
const Gap(16),
const Divider(),
const Gap(16),
const LinkListTile(
title: 'Our website',
subtitle: 'selfprivacy.org',
uri: 'https://selfprivacy.org/',
icon: Icons.language_outlined,
),
const LinkListTile(
title: 'Documentation',
subtitle: 'selfprivacy.org/docs',
uri: 'https://selfprivacy.org/docs/',
icon: Icons.library_books_outlined,
),
const LinkListTile(
title: 'Privacy Policy',
subtitle: 'selfprivacy.org/privacy-policy',
uri: 'https://selfprivacy.org/privacy-policy/',
icon: Icons.policy_outlined,
),
const LinkListTile(
title: 'Matrix support chat',
subtitle: '#chat:selfprivacy.org',
uri: 'https://matrix.to/#/#chat:selfprivacy.org',
icon: Icons.question_answer_outlined,
longPressText: '#chat:selfprivacy.org',
),
const LinkListTile(
title: 'Telegram support chat',
subtitle: '@selfprivacy_chat',
uri: 'https://t.me/selfprivacy_chat',
icon: Icons.question_answer_outlined,
longPressText: '@selfprivacy_chat',
),
],
),
);
}

View File

@ -2,7 +2,9 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:gap/gap.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/logic/models/service.dart';
@ -44,6 +46,7 @@ class _ServerStoragePageState extends State<ServerStoragePage> {
hasBackButton: true,
heroTitle: 'storage.card_title'.tr(),
bodyPadding: const EdgeInsets.symmetric(vertical: 16.0),
hasFlashButton: true,
children: [
...widget.diskStatus.diskVolumes.map(
(final volume) => Column(
@ -59,13 +62,20 @@ class _ServerStoragePageState extends State<ServerStoragePage> {
)
.toList(),
),
const SizedBox(height: 16),
const Gap(16),
const Divider(),
const SizedBox(height: 16),
const Gap(16),
],
),
),
const SizedBox(height: 8),
const Gap(8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: BrandOutlinedButton(
title: 'jobs.collect_nix_garbage'.tr(),
onPressed: context.read<JobsCubit>().collectNixGarbage,
),
),
],
);
}
@ -93,7 +103,7 @@ class ServerStorageSection extends StatelessWidget {
volume: volume,
),
),
const SizedBox(height: 16),
const Gap(16),
...services.map(
(final service) => ServerConsumptionListTile(
service: service,
@ -106,7 +116,7 @@ class ServerStorageSection extends StatelessWidget {
),
),
if (volume.isResizable) ...[
const SizedBox(height: 16),
const Gap(16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: BrandOutlinedButton(

View File

@ -120,6 +120,14 @@ class SelectLocationPage extends StatelessWidget {
.titleMedium,
),
const SizedBox(height: 8),
Text(
location.countryDisplayKey.tr(),
style: Theme.of(context)
.textTheme
.bodyMedium,
),
if (location.description != null)
const SizedBox(height: 4),
if (location.description != null)
Text(
location.description!,