diff --git a/assets/translations/az.json b/assets/translations/az.json index 81d678c7..aae7107a 100644 --- a/assets/translations/az.json +++ b/assets/translations/az.json @@ -60,7 +60,7 @@ "title": "Tətbiq parametrləri", "dark_theme_title": "Qaranlıq mövzu", "reset_config_title": "Tətbiq Sıfırlayın", - "reset_config_description": "API və Super İstifadəçi Açarlarını sıfırlayın", + "reset_config_description": "API və Super İstifadəçi Açarlarını sıfırlayın.", "delete_server_title": "Serveri silin", "dark_theme_description": "Rəng mövzusunu dəyişdirin", "delete_server_description": "Əməliyyat serveri siləcək. Bundan sonra o, əlçatmaz olacaq.", diff --git a/assets/translations/be.json b/assets/translations/be.json index 3a5e84a8..c6dbf7e9 100644 --- a/assets/translations/be.json +++ b/assets/translations/be.json @@ -259,7 +259,7 @@ "privacy_policy": "Палітыка прыватнасці" }, "application_settings": { - "reset_config_description": "Скінуць API ключы i суперкарыстальніка", + "reset_config_description": "Скінуць API ключы i суперкарыстальніка.", "delete_server_description": "Дзеянне прывядзе да выдалення сервера. Пасля гэтага ён будзе недаступны.", "title": "Налады праграмы", "dark_theme_title": "Цёмная тэма", diff --git a/assets/translations/cs.json b/assets/translations/cs.json index 818cd320..5186b74e 100644 --- a/assets/translations/cs.json +++ b/assets/translations/cs.json @@ -60,7 +60,7 @@ "title": "Nastavení aplikace", "dark_theme_title": "Tmavé téma", "reset_config_title": "Obnovení konfigurace aplikace", - "reset_config_description": "Obnovení klíčů api a uživatele root", + "reset_config_description": "Obnovení klíčů API a uživatele root.", "delete_server_title": "Odstranit server", "dark_theme_description": "Přepnutí tématu aplikace", "delete_server_description": "Tím odstraníte svůj server. Nebude již přístupný.", diff --git a/assets/translations/de.json b/assets/translations/de.json index 9b8b1093..86cc3518 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -64,7 +64,7 @@ "dark_theme_title": "Dunkles Thema", "dark_theme_description": "Ihr Anwendungsdesign wechseln", "reset_config_title": "Anwendungseinstellungen zurücksetzen", - "reset_config_description": "API Sclüssel und root Benutzer zurücksetzen", + "reset_config_description": "API Sclüssel und root Benutzer zurücksetzen.", "delete_server_title": "Server löschen", "delete_server_description": "Das wird Ihren Server löschen. Es wird nicht mehr zugänglich sein.", "system_dark_theme_title": "Standard-Systemthema", diff --git a/assets/translations/en.json b/assets/translations/en.json index c7c17430..0a25ff43 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -68,7 +68,7 @@ "dark_theme_description": "Switch your application theme", "dangerous_settings": "Dangerous settings", "reset_config_title": "Reset application config", - "reset_config_description": "Reset api keys and root user", + "reset_config_description": "Resets API keys and root user.", "delete_server_title": "Delete server", "delete_server_description": "This removes your server. It will be no longer accessible." }, @@ -204,8 +204,23 @@ "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.", + "backups_encryption_key_not_found": "Encryption key not found yet, please try again later.", "pending_jobs": "Currently running backup jobs", - "snapshots_title": "Snapshot list" + "snapshots_title": "Snapshot list", + "forget_snapshot": "Forget snapshot", + "forget_snapshot_alert": "You are about to delete this snapshot. Are you sure? This action usually cannot be undone.", + "forget_snapshot_error": "Couldn't forget snapshot", + "snapshot_modal_heading": "Snapshot details", + "snapshot_service_title": "Service", + "snapshot_creation_time_title": "Creation time", + "snapshot_id_title": "Snapshot ID", + "snapshot_modal_select_strategy": "Select the restore strategy", + "snapshot_modal_download_verify_option_title": "Download, verify and then replace", + "snapshot_modal_download_verify_option_description": "Less risk, but more free space needed. Downloads entire snapshot to the temporary storage, verifies it and then replaces the current data.", + "snapshot_modal_inplace_option_title": "Replace in place", + "snapshot_modal_inplace_option_description": "Less free space needed, but more risk. Replaces current data with the snapshot data during the download.", + "snapshot_modal_service_not_found": "This is a snapshot of a service you don't have on your server anymore. Usually this shouldn't happen, and we cannot do the automatic restore. You can still download the snapshot and restore it manually. Contact SelfPrivacy support if you need help.", + "restore_started": "Restore started, check the jobs list for the current status" }, "storage": { "card_title": "Server Storage", @@ -543,4 +558,4 @@ "reset_onboarding_description": "Reset onboarding switch to show onboarding screen again", "cubit_statuses": "Cubit loading statuses" } -} \ No newline at end of file +} diff --git a/assets/translations/es.json b/assets/translations/es.json index e01f9167..2c240edd 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -38,7 +38,7 @@ "application_settings": { "reset_config_title": "Restablecer la configuración de la aplicación", "dark_theme_description": "Cambia el tema de tu aplicación", - "reset_config_description": "Restablecer claves api y usuario root", + "reset_config_description": "Restablecer claves API y usuario root.", "delete_server_title": "Eliminar servidor", "delete_server_description": "Esto elimina su servidor. Ya no será accesible.", "title": "Ajustes de la aplicación", @@ -89,4 +89,4 @@ "about_us_page": { "title": "Sobre nosotros" } -} +} \ No newline at end of file diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 945d1f5a..e015c782 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -64,7 +64,7 @@ "delete_server_title": "Supprimer le serveur", "delete_server_description": "Cela va supprimer votre serveur. Celui-ci ne sera plus accessible.", "dark_theme_title": "Thème sombre", - "reset_config_description": "Réinitialiser les clés API et l'utilisateur root" + "reset_config_description": "Réinitialiser les clés API et l'utilisateur root." }, "ssh": { "title": "Clés SSH", diff --git a/assets/translations/lv.json b/assets/translations/lv.json index b4a38c8e..7fbfb797 100644 --- a/assets/translations/lv.json +++ b/assets/translations/lv.json @@ -66,7 +66,7 @@ "dark_theme_description": "Lietojumprogrammas dizaina pārslēgšana", "dangerous_settings": "Bīstamie iestatījumi", "reset_config_title": "Atiestatīt lietojumprogrammas konfigurāciju", - "reset_config_description": "Atiestatīt API atslēgas un saknes lietotāju", + "reset_config_description": "Atiestatīt API atslēgas un saknes lietotāju.", "delete_server_title": "Izdzēst serveri", "delete_server_description": "Šis izdzēš jūsu serveri. Tas vairs nebūs pieejams." }, @@ -215,4 +215,4 @@ "not_ready_card": { "in_menu": "Serveris vēl nav iestatīts. Lūdzu, pabeidziet iestatīšanu, izmantojot iestatīšanas vedni, lai turpinātu darbu." } -} +} \ No newline at end of file diff --git a/assets/translations/pl.json b/assets/translations/pl.json index 0f0acdb0..5a90cc86 100644 --- a/assets/translations/pl.json +++ b/assets/translations/pl.json @@ -64,7 +64,7 @@ "dark_theme_title": "Ciemny motyw aplikacji", "dark_theme_description": "Zmień kolor motywu aplikacji", "reset_config_title": "Resetowanie", - "reset_config_description": "Zresetuj klucze API i użytkownika root", + "reset_config_description": "Zresetuj klucze API i użytkownika root.", "delete_server_title": "Usuń serwer", "delete_server_description": "Ta czynność usunie serwer. Po tym będzie niedostępny.", "system_dark_theme_description": "Użyj jasnego lub ciemnego motywu w zależności od ustawień systemu", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index d1decc6d..d5c27d86 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -65,7 +65,7 @@ "dark_theme_title": "Тёмная тема", "dark_theme_description": "Сменить цветовую тему", "reset_config_title": "Сброс настроек", - "reset_config_description": "Сбросить API ключи, а также root пользователя", + "reset_config_description": "Сбросить API ключи и root пользователя.", "delete_server_title": "Удалить сервер", "delete_server_description": "Действие приведёт к удалению сервера. После этого он будет недоступен.", "system_dark_theme_title": "Системная тема по-умолчанию", @@ -201,7 +201,8 @@ "autobackup_set_period": "Установить период", "autobackup_period_set": "Период установлен", "backups_encryption_key": "Ключ шифрования", - "snapshots_title": "Список снимков" + "snapshots_title": "Список снимков", + "forget_snapshot_error": "Не удалось забыть снимок" }, "storage": { "card_title": "Хранилище", diff --git a/assets/translations/sk.json b/assets/translations/sk.json index c45523d4..c79d251e 100644 --- a/assets/translations/sk.json +++ b/assets/translations/sk.json @@ -114,7 +114,7 @@ "dark_theme_title": "Temná téma", "dark_theme_description": "Zmeniť tému aplikácie", "reset_config_title": "Resetovať nastavenia aplikácie", - "reset_config_description": "Resetovať kľúče API a užívateľa root", + "reset_config_description": "Resetovať kľúče API a užívateľa root.", "delete_server_title": "Zmazať server", "delete_server_description": "Tým sa odstráni váš server. Už nebude prístupným.", "system_dark_theme_description": "Použitie svetlej alebo tmavej témy v závislosti od nastavení systému", diff --git a/assets/translations/sl.json b/assets/translations/sl.json index c8bcd3b8..4cbf91d1 100644 --- a/assets/translations/sl.json +++ b/assets/translations/sl.json @@ -67,7 +67,6 @@ "dark_theme_description": "Spreminjanje barvne teme", "dangerous_settings": "Nevarne nastavitve", "reset_config_title": "Ponastavitev konfiguracije aplikacije", - "reset_config_description": "Сбросить API ключи, а также root пользователя", "delete_server_title": "Brisanje strežnika", "delete_server_description": "To dejanje povzroči izbris strežnika. Nato bo nedosegljiv." }, @@ -254,4 +253,4 @@ "title": "VPN Strežnik", "subtitle": "Zasebni strežnik VPN" } -} +} \ No newline at end of file diff --git a/assets/translations/th.json b/assets/translations/th.json index 3c314087..aa54a203 100644 --- a/assets/translations/th.json +++ b/assets/translations/th.json @@ -59,7 +59,7 @@ "title": "การตั้งค่าแอปพลิเคชัน", "dark_theme_title": "ธีมมืด", "reset_config_title": "รีเซ็ตค่าดั้งเดิมการตั้งค่าของแอปพลิเคชั่น", - "reset_config_description": "รีเซ็ต api key และผู้ใช้งาน root", + "reset_config_description": "รีเซ็ต API key และผู้ใช้งาน root", "delete_server_title": "ลบเซิฟเวอร์" }, "ssh": { diff --git a/assets/translations/uk.json b/assets/translations/uk.json index 0d5726bf..d3384ca5 100644 --- a/assets/translations/uk.json +++ b/assets/translations/uk.json @@ -41,7 +41,7 @@ "reset_config_title": "Скинути налаштування", "dark_theme_title": "Темна тема", "dark_theme_description": "Змінити тему додатка", - "reset_config_description": "Скинути API ключі та root користувача", + "reset_config_description": "Скинути API ключі та root користувача.", "delete_server_title": "Видалити сервер", "delete_server_description": "Це видалить ваш сервер. Він більше не буде доступний." }, diff --git a/lib/logic/api_maps/graphql_maps/schema/backups.graphql b/lib/logic/api_maps/graphql_maps/schema/backups.graphql index fae80432..9b60564c 100644 --- a/lib/logic/api_maps/graphql_maps/schema/backups.graphql +++ b/lib/logic/api_maps/graphql_maps/schema/backups.graphql @@ -81,13 +81,21 @@ mutation InitializeRepository($repository: InitializeRepositoryInput!) { } } -mutation RestoreBackup($snapshotId: String!) { +mutation RestoreBackup($snapshotId: String!, $strategy: RestoreStrategy! = DOWNLOAD_VERIFY_OVERWRITE) { backup { - restoreBackup(snapshotId: $snapshotId) { + restoreBackup(snapshotId: $snapshotId, strategy: $strategy) { ...basicMutationReturnFields job { ...basicApiJobsFields } } } +} + +mutation ForgetSnapshot($snapshotId: String!) { + backup { + forgetSnapshot(snapshotId: $snapshotId) { + ...basicMutationReturnFields + } + } } \ No newline at end of file diff --git a/lib/logic/api_maps/graphql_maps/schema/backups.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/backups.graphql.dart index 428da3e4..7a814aac 100644 --- a/lib/logic/api_maps/graphql_maps/schema/backups.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/backups.graphql.dart @@ -4982,9 +4982,13 @@ class _CopyWithStubImpl$Mutation$InitializeRepository$backup } class Variables$Mutation$RestoreBackup { - factory Variables$Mutation$RestoreBackup({required String snapshotId}) => + factory Variables$Mutation$RestoreBackup({ + required String snapshotId, + required Enum$RestoreStrategy strategy, + }) => Variables$Mutation$RestoreBackup._({ r'snapshotId': snapshotId, + r'strategy': strategy, }); Variables$Mutation$RestoreBackup._(this._$data); @@ -4993,16 +4997,23 @@ class Variables$Mutation$RestoreBackup { final result$data = {}; final l$snapshotId = data['snapshotId']; result$data['snapshotId'] = (l$snapshotId as String); + final l$strategy = data['strategy']; + result$data['strategy'] = + fromJson$Enum$RestoreStrategy((l$strategy as String)); return Variables$Mutation$RestoreBackup._(result$data); } Map _$data; String get snapshotId => (_$data['snapshotId'] as String); + Enum$RestoreStrategy get strategy => + (_$data['strategy'] as Enum$RestoreStrategy); Map toJson() { final result$data = {}; final l$snapshotId = snapshotId; result$data['snapshotId'] = l$snapshotId; + final l$strategy = strategy; + result$data['strategy'] = toJson$Enum$RestoreStrategy(l$strategy); return result$data; } @@ -5025,13 +5036,22 @@ class Variables$Mutation$RestoreBackup { if (l$snapshotId != lOther$snapshotId) { return false; } + final l$strategy = strategy; + final lOther$strategy = other.strategy; + if (l$strategy != lOther$strategy) { + return false; + } return true; } @override int get hashCode { final l$snapshotId = snapshotId; - return Object.hashAll([l$snapshotId]); + final l$strategy = strategy; + return Object.hashAll([ + l$snapshotId, + l$strategy, + ]); } } @@ -5044,7 +5064,10 @@ abstract class CopyWith$Variables$Mutation$RestoreBackup { factory CopyWith$Variables$Mutation$RestoreBackup.stub(TRes res) = _CopyWithStubImpl$Variables$Mutation$RestoreBackup; - TRes call({String? snapshotId}); + TRes call({ + String? snapshotId, + Enum$RestoreStrategy? strategy, + }); } class _CopyWithImpl$Variables$Mutation$RestoreBackup @@ -5060,11 +5083,16 @@ class _CopyWithImpl$Variables$Mutation$RestoreBackup static const _undefined = {}; - TRes call({Object? snapshotId = _undefined}) => + TRes call({ + Object? snapshotId = _undefined, + Object? strategy = _undefined, + }) => _then(Variables$Mutation$RestoreBackup._({ ..._instance._$data, if (snapshotId != _undefined && snapshotId != null) 'snapshotId': (snapshotId as String), + if (strategy != _undefined && strategy != null) + 'strategy': (strategy as Enum$RestoreStrategy), })); } @@ -5074,7 +5102,11 @@ class _CopyWithStubImpl$Variables$Mutation$RestoreBackup TRes _res; - call({String? snapshotId}) => _res; + call({ + String? snapshotId, + Enum$RestoreStrategy? strategy, + }) => + _res; } class Mutation$RestoreBackup { @@ -5223,7 +5255,18 @@ const documentNodeMutationRestoreBackup = DocumentNode(definitions: [ ), defaultValue: DefaultValueNode(value: null), directives: [], - ) + ), + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'strategy')), + type: NamedTypeNode( + name: NameNode(value: 'RestoreStrategy'), + isNonNull: true, + ), + defaultValue: DefaultValueNode( + value: EnumValueNode( + name: NameNode(value: 'DOWNLOAD_VERIFY_OVERWRITE'))), + directives: [], + ), ], directives: [], selectionSet: SelectionSetNode(selections: [ @@ -5240,7 +5283,11 @@ const documentNodeMutationRestoreBackup = DocumentNode(definitions: [ ArgumentNode( name: NameNode(value: 'snapshotId'), value: VariableNode(name: NameNode(value: 'snapshotId')), - ) + ), + ArgumentNode( + name: NameNode(value: 'strategy'), + value: VariableNode(name: NameNode(value: 'strategy')), + ), ], directives: [], selectionSet: SelectionSetNode(selections: [ @@ -5727,3 +5774,702 @@ class _CopyWithStubImpl$Mutation$RestoreBackup$backup$restoreBackup CopyWith$Fragment$basicApiJobsFields get job => CopyWith$Fragment$basicApiJobsFields.stub(_res); } + +class Variables$Mutation$ForgetSnapshot { + factory Variables$Mutation$ForgetSnapshot({required String snapshotId}) => + Variables$Mutation$ForgetSnapshot._({ + r'snapshotId': snapshotId, + }); + + Variables$Mutation$ForgetSnapshot._(this._$data); + + factory Variables$Mutation$ForgetSnapshot.fromJson( + Map data) { + final result$data = {}; + final l$snapshotId = data['snapshotId']; + result$data['snapshotId'] = (l$snapshotId as String); + return Variables$Mutation$ForgetSnapshot._(result$data); + } + + Map _$data; + + String get snapshotId => (_$data['snapshotId'] as String); + Map toJson() { + final result$data = {}; + final l$snapshotId = snapshotId; + result$data['snapshotId'] = l$snapshotId; + return result$data; + } + + CopyWith$Variables$Mutation$ForgetSnapshot + get copyWith => CopyWith$Variables$Mutation$ForgetSnapshot( + this, + (i) => i, + ); + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (!(other is Variables$Mutation$ForgetSnapshot) || + runtimeType != other.runtimeType) { + return false; + } + final l$snapshotId = snapshotId; + final lOther$snapshotId = other.snapshotId; + if (l$snapshotId != lOther$snapshotId) { + return false; + } + return true; + } + + @override + int get hashCode { + final l$snapshotId = snapshotId; + return Object.hashAll([l$snapshotId]); + } +} + +abstract class CopyWith$Variables$Mutation$ForgetSnapshot { + factory CopyWith$Variables$Mutation$ForgetSnapshot( + Variables$Mutation$ForgetSnapshot instance, + TRes Function(Variables$Mutation$ForgetSnapshot) then, + ) = _CopyWithImpl$Variables$Mutation$ForgetSnapshot; + + factory CopyWith$Variables$Mutation$ForgetSnapshot.stub(TRes res) = + _CopyWithStubImpl$Variables$Mutation$ForgetSnapshot; + + TRes call({String? snapshotId}); +} + +class _CopyWithImpl$Variables$Mutation$ForgetSnapshot + implements CopyWith$Variables$Mutation$ForgetSnapshot { + _CopyWithImpl$Variables$Mutation$ForgetSnapshot( + this._instance, + this._then, + ); + + final Variables$Mutation$ForgetSnapshot _instance; + + final TRes Function(Variables$Mutation$ForgetSnapshot) _then; + + static const _undefined = {}; + + TRes call({Object? snapshotId = _undefined}) => + _then(Variables$Mutation$ForgetSnapshot._({ + ..._instance._$data, + if (snapshotId != _undefined && snapshotId != null) + 'snapshotId': (snapshotId as String), + })); +} + +class _CopyWithStubImpl$Variables$Mutation$ForgetSnapshot + implements CopyWith$Variables$Mutation$ForgetSnapshot { + _CopyWithStubImpl$Variables$Mutation$ForgetSnapshot(this._res); + + TRes _res; + + call({String? snapshotId}) => _res; +} + +class Mutation$ForgetSnapshot { + Mutation$ForgetSnapshot({ + required this.backup, + this.$__typename = 'Mutation', + }); + + factory Mutation$ForgetSnapshot.fromJson(Map json) { + final l$backup = json['backup']; + final l$$__typename = json['__typename']; + return Mutation$ForgetSnapshot( + backup: Mutation$ForgetSnapshot$backup.fromJson( + (l$backup as Map)), + $__typename: (l$$__typename as String), + ); + } + + final Mutation$ForgetSnapshot$backup backup; + + final String $__typename; + + Map toJson() { + final _resultData = {}; + final l$backup = backup; + _resultData['backup'] = l$backup.toJson(); + final l$$__typename = $__typename; + _resultData['__typename'] = l$$__typename; + return _resultData; + } + + @override + int get hashCode { + final l$backup = backup; + final l$$__typename = $__typename; + return Object.hashAll([ + l$backup, + l$$__typename, + ]); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (!(other is Mutation$ForgetSnapshot) || + runtimeType != other.runtimeType) { + return false; + } + final l$backup = backup; + final lOther$backup = other.backup; + if (l$backup != lOther$backup) { + return false; + } + final l$$__typename = $__typename; + final lOther$$__typename = other.$__typename; + if (l$$__typename != lOther$$__typename) { + return false; + } + return true; + } +} + +extension UtilityExtension$Mutation$ForgetSnapshot on Mutation$ForgetSnapshot { + CopyWith$Mutation$ForgetSnapshot get copyWith => + CopyWith$Mutation$ForgetSnapshot( + this, + (i) => i, + ); +} + +abstract class CopyWith$Mutation$ForgetSnapshot { + factory CopyWith$Mutation$ForgetSnapshot( + Mutation$ForgetSnapshot instance, + TRes Function(Mutation$ForgetSnapshot) then, + ) = _CopyWithImpl$Mutation$ForgetSnapshot; + + factory CopyWith$Mutation$ForgetSnapshot.stub(TRes res) = + _CopyWithStubImpl$Mutation$ForgetSnapshot; + + TRes call({ + Mutation$ForgetSnapshot$backup? backup, + String? $__typename, + }); + CopyWith$Mutation$ForgetSnapshot$backup get backup; +} + +class _CopyWithImpl$Mutation$ForgetSnapshot + implements CopyWith$Mutation$ForgetSnapshot { + _CopyWithImpl$Mutation$ForgetSnapshot( + this._instance, + this._then, + ); + + final Mutation$ForgetSnapshot _instance; + + final TRes Function(Mutation$ForgetSnapshot) _then; + + static const _undefined = {}; + + TRes call({ + Object? backup = _undefined, + Object? $__typename = _undefined, + }) => + _then(Mutation$ForgetSnapshot( + backup: backup == _undefined || backup == null + ? _instance.backup + : (backup as Mutation$ForgetSnapshot$backup), + $__typename: $__typename == _undefined || $__typename == null + ? _instance.$__typename + : ($__typename as String), + )); + CopyWith$Mutation$ForgetSnapshot$backup get backup { + final local$backup = _instance.backup; + return CopyWith$Mutation$ForgetSnapshot$backup( + local$backup, (e) => call(backup: e)); + } +} + +class _CopyWithStubImpl$Mutation$ForgetSnapshot + implements CopyWith$Mutation$ForgetSnapshot { + _CopyWithStubImpl$Mutation$ForgetSnapshot(this._res); + + TRes _res; + + call({ + Mutation$ForgetSnapshot$backup? backup, + String? $__typename, + }) => + _res; + CopyWith$Mutation$ForgetSnapshot$backup get backup => + CopyWith$Mutation$ForgetSnapshot$backup.stub(_res); +} + +const documentNodeMutationForgetSnapshot = DocumentNode(definitions: [ + OperationDefinitionNode( + type: OperationType.mutation, + name: NameNode(value: 'ForgetSnapshot'), + variableDefinitions: [ + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'snapshotId')), + type: NamedTypeNode( + name: NameNode(value: 'String'), + isNonNull: true, + ), + defaultValue: DefaultValueNode(value: null), + directives: [], + ) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'backup'), + alias: null, + arguments: [], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'forgetSnapshot'), + alias: null, + arguments: [ + ArgumentNode( + name: NameNode(value: 'snapshotId'), + value: VariableNode(name: NameNode(value: 'snapshotId')), + ) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FragmentSpreadNode( + name: NameNode(value: 'basicMutationReturnFields'), + 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, + ), + ]), + ), + fragmentDefinitionbasicMutationReturnFields, +]); +Mutation$ForgetSnapshot _parserFn$Mutation$ForgetSnapshot( + Map data) => + Mutation$ForgetSnapshot.fromJson(data); +typedef OnMutationCompleted$Mutation$ForgetSnapshot = FutureOr Function( + Map?, + Mutation$ForgetSnapshot?, +); + +class Options$Mutation$ForgetSnapshot + extends graphql.MutationOptions { + Options$Mutation$ForgetSnapshot({ + String? operationName, + required Variables$Mutation$ForgetSnapshot variables, + graphql.FetchPolicy? fetchPolicy, + graphql.ErrorPolicy? errorPolicy, + graphql.CacheRereadPolicy? cacheRereadPolicy, + Object? optimisticResult, + Mutation$ForgetSnapshot? typedOptimisticResult, + graphql.Context? context, + OnMutationCompleted$Mutation$ForgetSnapshot? onCompleted, + graphql.OnMutationUpdate? update, + graphql.OnError? onError, + }) : onCompletedWithParsed = onCompleted, + super( + variables: variables.toJson(), + 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$ForgetSnapshot(data), + ), + update: update, + onError: onError, + document: documentNodeMutationForgetSnapshot, + parserFn: _parserFn$Mutation$ForgetSnapshot, + ); + + final OnMutationCompleted$Mutation$ForgetSnapshot? onCompletedWithParsed; + + @override + List get properties => [ + ...super.onCompleted == null + ? super.properties + : super.properties.where((property) => property != onCompleted), + onCompletedWithParsed, + ]; +} + +class WatchOptions$Mutation$ForgetSnapshot + extends graphql.WatchQueryOptions { + WatchOptions$Mutation$ForgetSnapshot({ + String? operationName, + required Variables$Mutation$ForgetSnapshot variables, + graphql.FetchPolicy? fetchPolicy, + graphql.ErrorPolicy? errorPolicy, + graphql.CacheRereadPolicy? cacheRereadPolicy, + Object? optimisticResult, + Mutation$ForgetSnapshot? typedOptimisticResult, + graphql.Context? context, + Duration? pollInterval, + bool? eagerlyFetchResults, + bool carryForwardDataOnException = true, + bool fetchResults = false, + }) : super( + variables: variables.toJson(), + operationName: operationName, + fetchPolicy: fetchPolicy, + errorPolicy: errorPolicy, + cacheRereadPolicy: cacheRereadPolicy, + optimisticResult: optimisticResult ?? typedOptimisticResult?.toJson(), + context: context, + document: documentNodeMutationForgetSnapshot, + pollInterval: pollInterval, + eagerlyFetchResults: eagerlyFetchResults, + carryForwardDataOnException: carryForwardDataOnException, + fetchResults: fetchResults, + parserFn: _parserFn$Mutation$ForgetSnapshot, + ); +} + +extension ClientExtension$Mutation$ForgetSnapshot on graphql.GraphQLClient { + Future> mutate$ForgetSnapshot( + Options$Mutation$ForgetSnapshot options) async => + await this.mutate(options); + graphql.ObservableQuery watchMutation$ForgetSnapshot( + WatchOptions$Mutation$ForgetSnapshot options) => + this.watchMutation(options); +} + +class Mutation$ForgetSnapshot$backup { + Mutation$ForgetSnapshot$backup({ + required this.forgetSnapshot, + this.$__typename = 'BackupMutations', + }); + + factory Mutation$ForgetSnapshot$backup.fromJson(Map json) { + final l$forgetSnapshot = json['forgetSnapshot']; + final l$$__typename = json['__typename']; + return Mutation$ForgetSnapshot$backup( + forgetSnapshot: Mutation$ForgetSnapshot$backup$forgetSnapshot.fromJson( + (l$forgetSnapshot as Map)), + $__typename: (l$$__typename as String), + ); + } + + final Mutation$ForgetSnapshot$backup$forgetSnapshot forgetSnapshot; + + final String $__typename; + + Map toJson() { + final _resultData = {}; + final l$forgetSnapshot = forgetSnapshot; + _resultData['forgetSnapshot'] = l$forgetSnapshot.toJson(); + final l$$__typename = $__typename; + _resultData['__typename'] = l$$__typename; + return _resultData; + } + + @override + int get hashCode { + final l$forgetSnapshot = forgetSnapshot; + final l$$__typename = $__typename; + return Object.hashAll([ + l$forgetSnapshot, + l$$__typename, + ]); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (!(other is Mutation$ForgetSnapshot$backup) || + runtimeType != other.runtimeType) { + return false; + } + final l$forgetSnapshot = forgetSnapshot; + final lOther$forgetSnapshot = other.forgetSnapshot; + if (l$forgetSnapshot != lOther$forgetSnapshot) { + return false; + } + final l$$__typename = $__typename; + final lOther$$__typename = other.$__typename; + if (l$$__typename != lOther$$__typename) { + return false; + } + return true; + } +} + +extension UtilityExtension$Mutation$ForgetSnapshot$backup + on Mutation$ForgetSnapshot$backup { + CopyWith$Mutation$ForgetSnapshot$backup + get copyWith => CopyWith$Mutation$ForgetSnapshot$backup( + this, + (i) => i, + ); +} + +abstract class CopyWith$Mutation$ForgetSnapshot$backup { + factory CopyWith$Mutation$ForgetSnapshot$backup( + Mutation$ForgetSnapshot$backup instance, + TRes Function(Mutation$ForgetSnapshot$backup) then, + ) = _CopyWithImpl$Mutation$ForgetSnapshot$backup; + + factory CopyWith$Mutation$ForgetSnapshot$backup.stub(TRes res) = + _CopyWithStubImpl$Mutation$ForgetSnapshot$backup; + + TRes call({ + Mutation$ForgetSnapshot$backup$forgetSnapshot? forgetSnapshot, + String? $__typename, + }); + CopyWith$Mutation$ForgetSnapshot$backup$forgetSnapshot + get forgetSnapshot; +} + +class _CopyWithImpl$Mutation$ForgetSnapshot$backup + implements CopyWith$Mutation$ForgetSnapshot$backup { + _CopyWithImpl$Mutation$ForgetSnapshot$backup( + this._instance, + this._then, + ); + + final Mutation$ForgetSnapshot$backup _instance; + + final TRes Function(Mutation$ForgetSnapshot$backup) _then; + + static const _undefined = {}; + + TRes call({ + Object? forgetSnapshot = _undefined, + Object? $__typename = _undefined, + }) => + _then(Mutation$ForgetSnapshot$backup( + forgetSnapshot: forgetSnapshot == _undefined || forgetSnapshot == null + ? _instance.forgetSnapshot + : (forgetSnapshot as Mutation$ForgetSnapshot$backup$forgetSnapshot), + $__typename: $__typename == _undefined || $__typename == null + ? _instance.$__typename + : ($__typename as String), + )); + CopyWith$Mutation$ForgetSnapshot$backup$forgetSnapshot + get forgetSnapshot { + final local$forgetSnapshot = _instance.forgetSnapshot; + return CopyWith$Mutation$ForgetSnapshot$backup$forgetSnapshot( + local$forgetSnapshot, (e) => call(forgetSnapshot: e)); + } +} + +class _CopyWithStubImpl$Mutation$ForgetSnapshot$backup + implements CopyWith$Mutation$ForgetSnapshot$backup { + _CopyWithStubImpl$Mutation$ForgetSnapshot$backup(this._res); + + TRes _res; + + call({ + Mutation$ForgetSnapshot$backup$forgetSnapshot? forgetSnapshot, + String? $__typename, + }) => + _res; + CopyWith$Mutation$ForgetSnapshot$backup$forgetSnapshot + get forgetSnapshot => + CopyWith$Mutation$ForgetSnapshot$backup$forgetSnapshot.stub(_res); +} + +class Mutation$ForgetSnapshot$backup$forgetSnapshot + implements Fragment$basicMutationReturnFields$$GenericMutationReturn { + Mutation$ForgetSnapshot$backup$forgetSnapshot({ + required this.code, + required this.message, + required this.success, + this.$__typename = 'GenericMutationReturn', + }); + + factory Mutation$ForgetSnapshot$backup$forgetSnapshot.fromJson( + Map json) { + final l$code = json['code']; + final l$message = json['message']; + final l$success = json['success']; + final l$$__typename = json['__typename']; + return Mutation$ForgetSnapshot$backup$forgetSnapshot( + code: (l$code as int), + message: (l$message as String), + success: (l$success as bool), + $__typename: (l$$__typename as String), + ); + } + + final int code; + + final String message; + + final bool success; + + final String $__typename; + + Map toJson() { + final _resultData = {}; + 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; + return _resultData; + } + + @override + int get hashCode { + final l$code = code; + final l$message = message; + final l$success = success; + final l$$__typename = $__typename; + return Object.hashAll([ + l$code, + l$message, + l$success, + l$$__typename, + ]); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (!(other is Mutation$ForgetSnapshot$backup$forgetSnapshot) || + 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; + } + return true; + } +} + +extension UtilityExtension$Mutation$ForgetSnapshot$backup$forgetSnapshot + on Mutation$ForgetSnapshot$backup$forgetSnapshot { + CopyWith$Mutation$ForgetSnapshot$backup$forgetSnapshot< + Mutation$ForgetSnapshot$backup$forgetSnapshot> + get copyWith => CopyWith$Mutation$ForgetSnapshot$backup$forgetSnapshot( + this, + (i) => i, + ); +} + +abstract class CopyWith$Mutation$ForgetSnapshot$backup$forgetSnapshot { + factory CopyWith$Mutation$ForgetSnapshot$backup$forgetSnapshot( + Mutation$ForgetSnapshot$backup$forgetSnapshot instance, + TRes Function(Mutation$ForgetSnapshot$backup$forgetSnapshot) then, + ) = _CopyWithImpl$Mutation$ForgetSnapshot$backup$forgetSnapshot; + + factory CopyWith$Mutation$ForgetSnapshot$backup$forgetSnapshot.stub( + TRes res) = + _CopyWithStubImpl$Mutation$ForgetSnapshot$backup$forgetSnapshot; + + TRes call({ + int? code, + String? message, + bool? success, + String? $__typename, + }); +} + +class _CopyWithImpl$Mutation$ForgetSnapshot$backup$forgetSnapshot + implements CopyWith$Mutation$ForgetSnapshot$backup$forgetSnapshot { + _CopyWithImpl$Mutation$ForgetSnapshot$backup$forgetSnapshot( + this._instance, + this._then, + ); + + final Mutation$ForgetSnapshot$backup$forgetSnapshot _instance; + + final TRes Function(Mutation$ForgetSnapshot$backup$forgetSnapshot) _then; + + static const _undefined = {}; + + TRes call({ + Object? code = _undefined, + Object? message = _undefined, + Object? success = _undefined, + Object? $__typename = _undefined, + }) => + _then(Mutation$ForgetSnapshot$backup$forgetSnapshot( + 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), + )); +} + +class _CopyWithStubImpl$Mutation$ForgetSnapshot$backup$forgetSnapshot + implements CopyWith$Mutation$ForgetSnapshot$backup$forgetSnapshot { + _CopyWithStubImpl$Mutation$ForgetSnapshot$backup$forgetSnapshot(this._res); + + TRes _res; + + call({ + int? code, + String? message, + bool? success, + String? $__typename, + }) => + _res; +} diff --git a/lib/logic/api_maps/graphql_maps/schema/schema.graphql b/lib/logic/api_maps/graphql_maps/schema/schema.graphql index 1f57a51a..368c2b90 100644 --- a/lib/logic/api_maps/graphql_maps/schema/schema.graphql +++ b/lib/logic/api_maps/graphql_maps/schema/schema.graphql @@ -94,7 +94,8 @@ type BackupMutations { removeRepository: GenericBackupConfigReturn! setAutobackupPeriod(period: Int = null): GenericBackupConfigReturn! startBackup(serviceId: String!): GenericJobMutationReturn! - restoreBackup(snapshotId: String!): GenericJobMutationReturn! + restoreBackup(snapshotId: String!, strategy: RestoreStrategy! = DOWNLOAD_VERIFY_OVERWRITE): GenericJobMutationReturn! + forgetSnapshot(snapshotId: String!): GenericMutationReturn! forceSnapshotsReload: GenericMutationReturn! } @@ -241,6 +242,11 @@ input RecoveryKeyLimitsInput { uses: Int = null } +enum RestoreStrategy { + INPLACE + DOWNLOAD_VERIFY_OVERWRITE +} + enum ServerProvider { HETZNER DIGITALOCEAN diff --git a/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart index e92ecc5e..8a78a6f8 100644 --- a/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart +++ b/lib/logic/api_maps/graphql_maps/schema/schema.graphql.dart @@ -1338,6 +1338,30 @@ Enum$DnsProvider fromJson$Enum$DnsProvider(String value) { } } +enum Enum$RestoreStrategy { INPLACE, DOWNLOAD_VERIFY_OVERWRITE, $unknown } + +String toJson$Enum$RestoreStrategy(Enum$RestoreStrategy e) { + switch (e) { + case Enum$RestoreStrategy.INPLACE: + return r'INPLACE'; + case Enum$RestoreStrategy.DOWNLOAD_VERIFY_OVERWRITE: + return r'DOWNLOAD_VERIFY_OVERWRITE'; + case Enum$RestoreStrategy.$unknown: + return r'$unknown'; + } +} + +Enum$RestoreStrategy fromJson$Enum$RestoreStrategy(String value) { + switch (value) { + case r'INPLACE': + return Enum$RestoreStrategy.INPLACE; + case r'DOWNLOAD_VERIFY_OVERWRITE': + return Enum$RestoreStrategy.DOWNLOAD_VERIFY_OVERWRITE; + default: + return Enum$RestoreStrategy.$unknown; + } +} + enum Enum$ServerProvider { HETZNER, DIGITALOCEAN, $unknown } String toJson$Enum$ServerProvider(Enum$ServerProvider e) { diff --git a/lib/logic/api_maps/graphql_maps/server_api/backups_api.dart b/lib/logic/api_maps/graphql_maps/server_api/backups_api.dart index b652db03..ed54bd53 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/backups_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/backups_api.dart @@ -209,14 +209,17 @@ mixin BackupsApi on GraphQLApiMap { Future> restoreBackup( final String snapshotId, + final BackupRestoreStrategy strategy, ) async { QueryResult response; GenericResult? result; try { final GraphQLClient client = await getClient(); - final variables = - Variables$Mutation$RestoreBackup(snapshotId: snapshotId); + final variables = Variables$Mutation$RestoreBackup( + snapshotId: snapshotId, + strategy: strategy.toGraphQL, + ); final options = Options$Mutation$RestoreBackup(variables: variables); response = await client.mutate$RestoreBackup(options); if (response.hasException) { @@ -245,4 +248,42 @@ mixin BackupsApi on GraphQLApiMap { return result; } + + Future> forgetSnapshot( + final String snapshotId, + ) async { + QueryResult response; + GenericResult? result; + + try { + final GraphQLClient client = await getClient(); + final variables = Variables$Mutation$ForgetSnapshot( + snapshotId: snapshotId, + ); + final options = Options$Mutation$ForgetSnapshot(variables: variables); + response = await client.mutate$ForgetSnapshot(options); + if (response.hasException) { + final message = response.exception.toString(); + print(message); + result = GenericResult( + success: false, + data: null, + message: message, + ); + } + result = GenericResult( + success: true, + data: response.parsedData!.backup.forgetSnapshot.success, + ); + } catch (e) { + print(e); + result = GenericResult( + success: false, + data: null, + message: e.toString(), + ); + } + + return result; + } } diff --git a/lib/logic/api_maps/graphql_maps/server_api/server_actions_api.dart b/lib/logic/api_maps/graphql_maps/server_api/server_actions_api.dart index f6fd5201..05269d33 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/server_actions_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/server_actions_api.dart @@ -20,15 +20,23 @@ mixin ServerActionsApi on GraphQLApiMap { return result; } - Future reboot() async { + Future> reboot() async { + DateTime? time; try { final GraphQLClient client = await getClient(); - return await _commonBoolRequest( - () async => client.mutate$RebootSystem(), - ); + final response = await client.mutate$RebootSystem(); + if (response.hasException) { + print(response.exception.toString()); + } + if (response.parsedData!.rebootSystem.success) { + time = DateTime.now(); + } } catch (e) { - return false; + print(e); + return GenericResult(data: time, success: false); } + + return GenericResult(data: time, success: true); } Future pullConfigurationUpdate() async { diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index ef3cec4d..938b606a 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -41,7 +41,6 @@ class BackupsCubit extends ServerInstallationDependendCubit { refreshing: false, ), ); - print(state); } } @@ -113,9 +112,7 @@ class BackupsCubit extends ServerInstallationDependendCubit { final BackblazeBucket? bucket = getIt().backblazeBucket; if (bucket == null) { emit(state.copyWith(isInitialized: false)); - print('bucket is null'); } else { - print('bucket is not null'); final GenericResult result = await api.initializeRepository( InitializeRepositoryInput( provider: BackupsProviderType.backblaze, @@ -125,7 +122,6 @@ class BackupsCubit extends ServerInstallationDependendCubit { password: bucket.applicationKey, ), ); - print('result is $result'); if (result.success == false) { getIt() .showSnackBar(result.message ?? 'Unknown error'); @@ -185,9 +181,12 @@ class BackupsCubit extends ServerInstallationDependendCubit { emit(state.copyWith(preventActions: false)); } - Future restoreBackup(final String backupId) async { + Future restoreBackup( + final String backupId, + final BackupRestoreStrategy strategy, + ) async { emit(state.copyWith(preventActions: true)); - await api.restoreBackup(backupId); + await api.restoreBackup(backupId, strategy); emit(state.copyWith(preventActions: false)); } @@ -211,6 +210,30 @@ class BackupsCubit extends ServerInstallationDependendCubit { await updateBackups(); } + Future forgetSnapshot(final String snapshotId) async { + final result = await api.forgetSnapshot(snapshotId); + if (!result.success) { + getIt().showSnackBar('jobs.generic_error'.tr()); + return; + } + + if (result.data == false) { + getIt() + .showSnackBar('backup.forget_snapshot_error'.tr()); + } + + // Optimistic update + final backups = state.backups; + final index = + backups.indexWhere((final snapshot) => snapshot.id == snapshotId); + if (index != -1) { + backups.removeAt(index); + emit(state.copyWith(backups: backups)); + } + + await updateBackups(); + } + @override void clear() async { emit(const BackupsState()); diff --git a/lib/logic/cubit/client_jobs/client_jobs_cubit.dart b/lib/logic/cubit/client_jobs/client_jobs_cubit.dart index 8b6a66b4..59a33673 100644 --- a/lib/logic/cubit/client_jobs/client_jobs_cubit.dart +++ b/lib/logic/cubit/client_jobs/client_jobs_cubit.dart @@ -54,8 +54,8 @@ class JobsCubit extends Cubit { Future rebootServer() async { emit(JobsStateLoading()); - final bool isSuccessful = await api.reboot(); - if (isSuccessful) { + final rebootResult = await api.reboot(); + if (rebootResult.success && rebootResult.data != null) { getIt().showSnackBar('jobs.reboot_success'.tr()); } else { getIt().showSnackBar('jobs.reboot_failed'.tr()); diff --git a/lib/logic/cubit/forms/user/ssh_form_cubit.dart b/lib/logic/cubit/forms/user/ssh_form_cubit.dart index 9ed389d2..707e54c3 100644 --- a/lib/logic/cubit/forms/user/ssh_form_cubit.dart +++ b/lib/logic/cubit/forms/user/ssh_form_cubit.dart @@ -21,7 +21,7 @@ class SshFormCubit extends FormCubit { ValidationModel( (final String newKey) => user.sshKeys.any((final String key) => key == newKey), - 'validations.already_exists'.tr(), + 'validations.already_exist'.tr(), ), RequiredStringValidation('validations.required'.tr()), ValidationModel( diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 1518d7ac..c3fd6811 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -260,12 +260,12 @@ class ServerInstallationRepository { Future restart() async { final server = getIt().serverDetails!; - final result = await ProvidersController.currentServerProvider!.restart( - server.id, - ); + final result = await ServerApi().reboot(); if (result.success && result.data != null) { server.copyWith(startTime: result.data); + } else { + getIt().showSnackBar('jobs.reboot_failed'.tr()); } return server; diff --git a/lib/logic/models/backup.dart b/lib/logic/models/backup.dart index fda68375..2199e223 100644 --- a/lib/logic/models/backup.dart +++ b/lib/logic/models/backup.dart @@ -1,5 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/backups.graphql.dart'; +import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/schema.graphql.dart'; import 'package:selfprivacy/logic/models/hive/backups_credential.dart'; class Backup { @@ -58,3 +59,26 @@ class BackupConfiguration { final String? locationName; final BackupsProviderType provider; } + +enum BackupRestoreStrategy { + inplace, + downloadVerifyOverwrite, + unknown; + + factory BackupRestoreStrategy.fromGraphQL( + final Enum$RestoreStrategy strategy, + ) => + switch (strategy) { + Enum$RestoreStrategy.INPLACE => inplace, + Enum$RestoreStrategy.DOWNLOAD_VERIFY_OVERWRITE => + downloadVerifyOverwrite, + Enum$RestoreStrategy.$unknown => unknown, + }; + + Enum$RestoreStrategy get toGraphQL => switch (this) { + inplace => Enum$RestoreStrategy.INPLACE, + downloadVerifyOverwrite => + Enum$RestoreStrategy.DOWNLOAD_VERIFY_OVERWRITE, + unknown => Enum$RestoreStrategy.$unknown, + }; +} diff --git a/lib/ui/components/cards/outlined_card.dart b/lib/ui/components/cards/outlined_card.dart index 91f13b44..d60fa9f0 100644 --- a/lib/ui/components/cards/outlined_card.dart +++ b/lib/ui/components/cards/outlined_card.dart @@ -3,17 +3,22 @@ import 'package:flutter/material.dart'; class OutlinedCard extends StatelessWidget { const OutlinedCard({ required this.child, + this.borderColor, + this.borderWidth, super.key, }); final Widget child; + final Color? borderColor; + final double? borderWidth; @override Widget build(final BuildContext context) => Card( elevation: 0.0, shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), side: BorderSide( - color: Theme.of(context).colorScheme.outline, + color: borderColor ?? Theme.of(context).colorScheme.outline, + width: borderWidth ?? 1.0, ), ), clipBehavior: Clip.antiAlias, diff --git a/lib/ui/pages/backups/backup_details.dart b/lib/ui/pages/backups/backup_details.dart index 44995577..1d08dfac 100644 --- a/lib/ui/pages/backups/backup_details.dart +++ b/lib/ui/pages/backups/backup_details.dart @@ -18,6 +18,7 @@ import 'package:selfprivacy/ui/helpers/modals.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/snapshot_modal.dart'; import 'package:selfprivacy/ui/router/router.dart'; import 'package:selfprivacy/utils/extensions/duration.dart'; @@ -62,14 +63,22 @@ class BackupDetailsPage extends StatelessWidget { heroTitle: 'backup.card_title'.tr(), heroSubtitle: 'backup.description'.tr(), children: [ - BrandButton.rised( - onPressed: preventActions - ? null - : () async { - await context.read().initializeBackups(); - }, - text: 'backup.initialize'.tr(), - ), + if (preventActions) + const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + ), + ), + if (!preventActions) + BrandButton.rised( + onPressed: preventActions + ? null + : () async { + await context.read().initializeBackups(); + }, + text: 'backup.initialize'.tr(), + ), ], ); } @@ -176,7 +185,9 @@ class BackupDetailsPage extends StatelessWidget { 'backup.backups_encryption_key_subtitle'.tr(), ), ), - const SizedBox(height: 16), + const SizedBox(height: 8), + const Divider(), + const SizedBox(height: 8), if (backupJobs.isNotEmpty) Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -233,16 +244,40 @@ class BackupDetailsPage extends StatelessWidget { onTap: preventActions ? null : () { - showPopUpAlert( - alertTitle: 'backup.restoring'.tr(), - description: 'backup.restore_alert'.tr( - args: [backup.time.toString()], + 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, + ) => + SnapshotModal( + snapshot: backup, + scrollController: scrollController, + ), ), - actionButtonTitle: 'modals.yes'.tr(), + ); + }, + onLongPress: preventActions + ? null + : () { + showPopUpAlert( + alertTitle: 'backup.forget_snapshot'.tr(), + description: + 'backup.forget_snapshot_alert'.tr(), + actionButtonTitle: + 'backup.forget_snapshot'.tr(), actionButtonOnPressed: () => { - context - .read() - .restoreBackup(backup.id) + context.read().forgetSnapshot( + backup.id, + ) }, ); }, diff --git a/lib/ui/pages/backups/backups_list.dart b/lib/ui/pages/backups/backups_list.dart index 5241693a..4af870ef 100644 --- a/lib/ui/pages/backups/backups_list.dart +++ b/lib/ui/pages/backups/backups_list.dart @@ -9,6 +9,7 @@ import 'package:selfprivacy/logic/models/backup.dart'; import 'package:selfprivacy/logic/models/service.dart'; import 'package:selfprivacy/ui/helpers/modals.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/pages/backups/snapshot_modal.dart'; @RoutePage() class BackupsListPage extends StatelessWidget { @@ -47,14 +48,35 @@ class BackupsListPage extends StatelessWidget { onTap: preventActions ? null : () { - showPopUpAlert( - alertTitle: 'backup.restoring'.tr(), - description: 'backup.restore_alert'.tr( - args: [backup.time.toString()], + 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) => + SnapshotModal( + snapshot: backup, + scrollController: scrollController, + ), ), - actionButtonTitle: 'modals.yes'.tr(), + ); + }, + onLongPress: preventActions + ? null + : () { + showPopUpAlert( + alertTitle: 'backup.forget_snapshot'.tr(), + description: 'backup.forget_snapshot_alert'.tr(), + actionButtonTitle: 'backup.forget_snapshot'.tr(), actionButtonOnPressed: () => { - context.read().restoreBackup(backup.id) + context.read().forgetSnapshot( + backup.id, + ) }, ); }, diff --git a/lib/ui/pages/backups/copy_encryption_key_modal.dart b/lib/ui/pages/backups/copy_encryption_key_modal.dart index 42bc1ba9..dca9d705 100644 --- a/lib/ui/pages/backups/copy_encryption_key_modal.dart +++ b/lib/ui/pages/backups/copy_encryption_key_modal.dart @@ -6,6 +6,7 @@ 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'; +import 'package:selfprivacy/ui/components/info_box/info_box.dart'; class CopyEncryptionKeyModal extends StatefulWidget { const CopyEncryptionKeyModal({ @@ -32,8 +33,27 @@ class _CopyEncryptionKeyModalState extends State { @override Widget build(final BuildContext context) { - final String encryptionKey = - context.watch().state.backblazeBucket!.encryptionKey; + final String? encryptionKey = + context.watch().state.backblazeBucket?.encryptionKey; + if (encryptionKey == null) { + 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), + InfoBox( + text: 'backup.backups_encryption_key_not_found'.tr(), + isWarning: true, + ), + ], + ); + } return ListView( controller: widget.scrollController, padding: const EdgeInsets.all(16), diff --git a/lib/ui/pages/backups/snapshot_modal.dart b/lib/ui/pages/backups/snapshot_modal.dart new file mode 100644 index 00000000..147fe72a --- /dev/null +++ b/lib/ui/pages/backups/snapshot_modal.dart @@ -0,0 +1,232 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart'; +import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; +import 'package:selfprivacy/logic/models/backup.dart'; +import 'package:selfprivacy/logic/models/json/server_job.dart'; +import 'package:selfprivacy/logic/models/service.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/components/cards/outlined_card.dart'; +import 'package:selfprivacy/ui/components/info_box/info_box.dart'; + +class SnapshotModal extends StatefulWidget { + const SnapshotModal({ + required this.snapshot, + required this.scrollController, + super.key, + }); + + final Backup snapshot; + final ScrollController scrollController; + + @override + State createState() => _SnapshotModalState(); +} + +class _SnapshotModalState extends State { + BackupRestoreStrategy selectedStrategy = + BackupRestoreStrategy.downloadVerifyOverwrite; + + @override + Widget build(final BuildContext context) { + final List busyServices = context + .watch() + .state + .backupJobList + .where( + (final ServerJob job) => + job.status == JobStatusEnum.running || + job.status == JobStatusEnum.created, + ) + .map((final ServerJob job) => job.typeId.split('.')[1]) + .toList(); + + final bool isServiceBusy = busyServices.contains(widget.snapshot.serviceId); + + final Service? service = context + .read() + .state + .getServiceById(widget.snapshot.serviceId); + + return ListView( + controller: widget.scrollController, + padding: const EdgeInsets.all(16), + children: [ + const SizedBox(height: 16), + Text( + 'backup.snapshot_modal_heading'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ListTile( + leading: service != null + ? SvgPicture.string( + service.svgIcon, + height: 24, + width: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurface, + BlendMode.srcIn, + ), + ) + : const Icon( + Icons.question_mark_outlined, + ), + title: Text( + 'backup.snapshot_service_title'.tr(), + ), + subtitle: Text( + service?.displayName ?? widget.snapshot.fallbackServiceName, + ), + ), + ListTile( + leading: Icon( + Icons.access_time_outlined, + color: Theme.of(context).colorScheme.onSurface, + ), + title: Text( + 'backup.snapshot_creation_time_title'.tr(), + ), + subtitle: Text( + '${MaterialLocalizations.of(context).formatShortDate(widget.snapshot.time)} ${TimeOfDay.fromDateTime(widget.snapshot.time).format(context)}', + ), + ), + ListTile( + leading: Icon( + Icons.numbers_outlined, + color: Theme.of(context).colorScheme.onSurface, + ), + title: Text( + 'backup.snapshot_id_title'.tr(), + ), + subtitle: Text( + widget.snapshot.id, + ), + ), + if (service != null) + Column( + children: [ + const SizedBox(height: 8), + Text( + 'backup.snapshot_modal_select_strategy'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + _BackupStrategySelectionCard( + isSelected: selectedStrategy == + BackupRestoreStrategy.downloadVerifyOverwrite, + onTap: () { + setState(() { + selectedStrategy = + BackupRestoreStrategy.downloadVerifyOverwrite; + }); + }, + title: + 'backup.snapshot_modal_download_verify_option_title'.tr(), + subtitle: + 'backup.snapshot_modal_download_verify_option_description' + .tr(), + ), + const SizedBox(height: 8), + _BackupStrategySelectionCard( + isSelected: selectedStrategy == BackupRestoreStrategy.inplace, + onTap: () { + setState(() { + selectedStrategy = BackupRestoreStrategy.inplace; + }); + }, + title: 'backup.snapshot_modal_inplace_option_title'.tr(), + subtitle: + 'backup.snapshot_modal_inplace_option_description'.tr(), + ), + const SizedBox(height: 8), + // Restore backup button + BrandButton.filled( + onPressed: isServiceBusy + ? null + : () { + context.read().restoreBackup( + widget.snapshot.id, + selectedStrategy, + ); + Navigator.of(context).pop(); + getIt() + .showSnackBar('backup.restore_started'.tr()); + }, + text: 'backup.restore'.tr(), + ), + ], + ) + else + Padding( + padding: const EdgeInsets.all(16.0), + child: InfoBox( + isWarning: true, + text: 'backup.snapshot_modal_service_not_found'.tr(), + ), + ) + ], + ); + } +} + +class _BackupStrategySelectionCard extends StatelessWidget { + const _BackupStrategySelectionCard({ + required this.isSelected, + required this.title, + required this.subtitle, + required this.onTap, + }); + + final bool isSelected; + final String title; + final String subtitle; + final void Function() onTap; + + @override + Widget build(final BuildContext context) => OutlinedCard( + borderColor: isSelected ? Theme.of(context).colorScheme.primary : null, + borderWidth: isSelected ? 3 : 1, + child: InkResponse( + highlightShape: BoxShape.rectangle, + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + if (isSelected) + Icon( + Icons.radio_button_on_outlined, + color: Theme.of(context).colorScheme.primary, + ) + else + Icon( + Icons.radio_button_off_outlined, + color: Theme.of(context).colorScheme.outline, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + subtitle, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ], + ), + ), + ), + ); +} diff --git a/lib/ui/pages/server_storage/binds_migration/services_migration.dart b/lib/ui/pages/server_storage/binds_migration/services_migration.dart index f07c275e..edb7474e 100644 --- a/lib/ui/pages/server_storage/binds_migration/services_migration.dart +++ b/lib/ui/pages/server_storage/binds_migration/services_migration.dart @@ -55,6 +55,20 @@ class _ServicesMigrationPageState extends State { }); } + bool get isVolumePicked { + bool isChangeFound = false; + for (final Service service in widget.services) { + for (final String serviceId in serviceToDisk.keys) { + if (serviceId == service.id && + serviceToDisk[serviceId] != service.storageUsage.volume!) { + isChangeFound = true; + } + } + } + + return isChangeFound; + } + /// Check the services and if a service is moved (in a serviceToDisk entry) /// subtract the used storage from the old volume and add it to the new volume. /// The old volume is the volume the service is currently on, shown in services list. @@ -157,40 +171,41 @@ class _ServicesMigrationPageState extends State { ), ), const SizedBox(height: 16), - BrandButton.filled( - child: Text('storage.start_migration_button'.tr()), - onPressed: () { - if (widget.isMigration) { - context.read().migrateToBinds( - serviceToDisk, - ); - } else { - for (final service in widget.services) { - if (serviceToDisk[service.id] != null) { - context.read().moveService( - service.id, - serviceToDisk[service.id]!, - ); + if (widget.isMigration || (!widget.isMigration && isVolumePicked)) + BrandButton.filled( + child: Text('storage.start_migration_button'.tr()), + onPressed: () { + if (widget.isMigration) { + context.read().migrateToBinds( + serviceToDisk, + ); + } else { + for (final service in widget.services) { + if (serviceToDisk[service.id] != null) { + context.read().moveService( + service.id, + serviceToDisk[service.id]!, + ); + } } } - } - context.router.popUntilRoot(); - showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - builder: (final BuildContext context) => - DraggableScrollableSheet( - expand: false, - maxChildSize: 0.9, - minChildSize: 0.4, - initialChildSize: 0.6, - builder: (final context, final scrollController) => - JobsContent(controller: scrollController), - ), - ); - }, - ), + context.router.popUntilRoot(); + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (final BuildContext context) => + DraggableScrollableSheet( + expand: false, + maxChildSize: 0.9, + minChildSize: 0.4, + initialChildSize: 0.6, + builder: (final context, final scrollController) => + JobsContent(controller: scrollController), + ), + ); + }, + ), const SizedBox(height: 32), ], ), diff --git a/lib/ui/pages/setup/initializing/dns_provider_picker.dart b/lib/ui/pages/setup/initializing/dns_provider_picker.dart index a05b1233..772c093e 100644 --- a/lib/ui/pages/setup/initializing/dns_provider_picker.dart +++ b/lib/ui/pages/setup/initializing/dns_provider_picker.dart @@ -109,6 +109,7 @@ class ProviderInputDataPage extends StatelessWidget { ), const SizedBox(height: 32), CubitFormTextField( + autofocus: true, formFieldCubit: providerCubit.apiKey, textAlign: TextAlign.center, scrollPadding: const EdgeInsets.only(bottom: 70), diff --git a/lib/ui/pages/setup/initializing/initializing.dart b/lib/ui/pages/setup/initializing/initializing.dart index c74fa4a4..5d54935a 100644 --- a/lib/ui/pages/setup/initializing/initializing.dart +++ b/lib/ui/pages/setup/initializing/initializing.dart @@ -273,6 +273,7 @@ class InitializingPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ CubitFormTextField( + autofocus: true, formFieldCubit: context.read().keyId, textAlign: TextAlign.center, scrollPadding: const EdgeInsets.only(bottom: 70), @@ -448,6 +449,7 @@ class InitializingPage extends StatelessWidget { ), const SizedBox(height: 32), CubitFormTextField( + autofocus: true, formFieldCubit: context.read().userName, textAlign: TextAlign.center, scrollPadding: const EdgeInsets.only(bottom: 70), diff --git a/lib/ui/pages/setup/initializing/server_provider_picker.dart b/lib/ui/pages/setup/initializing/server_provider_picker.dart index bde1435c..41c4c9ea 100644 --- a/lib/ui/pages/setup/initializing/server_provider_picker.dart +++ b/lib/ui/pages/setup/initializing/server_provider_picker.dart @@ -116,6 +116,7 @@ class ProviderInputDataPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ CubitFormTextField( + autofocus: true, formFieldCubit: providerCubit.apiKey, textAlign: TextAlign.center, scrollPadding: const EdgeInsets.only(bottom: 70), diff --git a/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart b/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart index 4f6cf352..d1dce974 100644 --- a/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart +++ b/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart @@ -65,6 +65,7 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget { ignoreBreakpoints: true, children: [ CubitFormTextField( + autofocus: true, formFieldCubit: context.read().tokenField, decoration: InputDecoration( diff --git a/lib/ui/pages/setup/recovering/recover_by_old_token.dart b/lib/ui/pages/setup/recovering/recover_by_old_token.dart index 42d60f34..1a777f83 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -76,6 +76,7 @@ class RecoverByOldToken extends StatelessWidget { ignoreBreakpoints: true, children: [ CubitFormTextField( + autofocus: true, formFieldCubit: context.read().tokenField, decoration: InputDecoration( diff --git a/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart index b39dc2da..ad18bc95 100644 --- a/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart +++ b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart @@ -36,6 +36,7 @@ class RecoverByRecoveryKey extends StatelessWidget { context.read().revertRecoveryStep, children: [ CubitFormTextField( + autofocus: true, formFieldCubit: context.read().tokenField, decoration: InputDecoration( diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart index a4d04aae..4b27e3ad 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart @@ -36,6 +36,7 @@ class RecoveryConfirmBackblaze extends StatelessWidget { hasFlashButton: false, children: [ CubitFormTextField( + autofocus: true, formFieldCubit: context.read().keyId, decoration: const InputDecoration( border: OutlineInputBorder(), diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_dns.dart b/lib/ui/pages/setup/recovering/recovery_confirm_dns.dart index 02a2afeb..e49efe9e 100644 --- a/lib/ui/pages/setup/recovering/recovery_confirm_dns.dart +++ b/lib/ui/pages/setup/recovering/recovery_confirm_dns.dart @@ -39,6 +39,7 @@ class RecoveryConfirmDns extends StatelessWidget { context.read().revertRecoveryStep, children: [ CubitFormTextField( + autofocus: true, formFieldCubit: context.read().apiKey, decoration: InputDecoration( border: const OutlineInputBorder(), diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index be5eb2ea..3ab5109a 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -121,6 +121,7 @@ class SelectDomainToRecover extends StatelessWidget { }, children: [ CubitFormTextField( + autofocus: true, formFieldCubit: context.read().serverDomainField, decoration: InputDecoration( diff --git a/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart b/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart index 86a1bf44..40f13eaa 100644 --- a/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart +++ b/lib/ui/pages/setup/recovering/recovery_server_provider_connected.dart @@ -38,6 +38,7 @@ class RecoveryServerProviderConnected extends StatelessWidget { }, children: [ CubitFormTextField( + autofocus: true, formFieldCubit: context.read().apiKey, decoration: InputDecoration( border: const OutlineInputBorder(), diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index d7ed2aca..9212307a 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -55,6 +55,7 @@ class NewUserPage extends StatelessWidget { const SizedBox(width: 14), IntrinsicHeight( child: CubitFormTextField( + autofocus: true, formFieldCubit: context.read().login, decoration: InputDecoration( labelText: 'users.login'.tr(), diff --git a/lib/ui/pages/users/reset_password.dart b/lib/ui/pages/users/reset_password.dart index 64785d3a..12bb41ae 100644 --- a/lib/ui/pages/users/reset_password.dart +++ b/lib/ui/pages/users/reset_password.dart @@ -41,6 +41,7 @@ class ResetPassword extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ CubitFormTextField( + autofocus: true, formFieldCubit: context.read().password, decoration: InputDecoration( diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index 72180e8c..7ee3a7bd 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -296,6 +296,7 @@ class NewSshKey extends StatelessWidget { children: [ IntrinsicHeight( child: CubitFormTextField( + autofocus: true, formFieldCubit: context.read().key, decoration: InputDecoration( labelText: 'ssh.input_label'.tr(),