import 'package:computer/computer.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/app/bak/backup2.dart'; import 'package:server_box/data/model/app/bak/backup_source.dart'; import 'package:server_box/data/model/app/bak/utils.dart'; /// Service class for handling backup operations class BackupService { /// Perform backup operation with the given source static Future backup(BuildContext context, BackupSource source) async { try { final saved = await SecureStoreProps.bakPwd.read(); final password = saved?.isEmpty == true ? null : saved; final path = await BackupV2.backup(null, password?.isEmpty == true ? null : password); await source.saveContent(path); if (source is ClipboardBackupSource) { context.showSnackBar(libL10n.success); } } catch (e, s) { context.showErrDialog(e, s, libL10n.backup); } } /// Perform restore operation with the given source static Future restore(BuildContext context, BackupSource source) async { final text = await source.getContent(); if (text == null) { // Show empty message for clipboard source if (source is ClipboardBackupSource) { context.showSnackBar(libL10n.empty); } return; } await restoreFromText(context, text); } /// Handle restore from text with decryption support static Future restoreFromText(BuildContext context, String text) async { // Check if backup is encrypted final isEncrypted = Cryptor.isEncrypted(text); String? password; if (!isEncrypted) { try { final (backup, err) = await context.showLoadingDialog( fn: () => Computer.shared.start(MergeableUtils.fromJsonString, text), ); if (err != null || backup == null) return; await _confirmAndRestore(context, backup); } catch (e, s) { Loggers.app.warning('Import backup failed', e, s); context.showErrDialog(e, s, libL10n.restore); } return; } // Try with saved password first final savedPassword = await SecureStoreProps.bakPwd.read(); if (savedPassword != null && savedPassword.isNotEmpty) { try { final (backup, err) = await context.showLoadingDialog( fn: () => Computer.shared.start((args) => MergeableUtils.fromJsonString(args.$1, args.$2), ( text, savedPassword, )), ); if (err == null && backup != null) { await _confirmAndRestore(context, backup); return; } } catch (e) { // Saved password failed, will prompt for manual input } } // Prompt for password with retry logic while (true) { password = await _showPasswordDialog(context, title: libL10n.pwd, hint: l10n.backupEncrypted); if (password == null) return; // User cancelled try { final (backup, err) = await context.showLoadingDialog( fn: () => Computer.shared.start((args) => MergeableUtils.fromJsonString(args.$1, args.$2), ( text, password, )), ); if (err != null || backup == null) continue; await _confirmAndRestore(context, backup); return; } catch (e) { if (e.toString().contains('incorrect password') || e.toString().contains('Failed to decrypt')) { final retry = await context.showRoundDialog( title: l10n.backupPasswordWrong, child: Text(l10n.backupPasswordWrong), actions: [ TextButton(onPressed: () => context.pop(false), child: Text(libL10n.cancel)), TextButton(onPressed: () => context.pop(true), child: Text(libL10n.retry)), ], ); if (retry != true) return; continue; // Try again } else { // Other error, show and exit context.showErrDialog(e, null, libL10n.restore); return; } } } } /// Confirm and execute restore operation static Future _confirmAndRestore(BuildContext context, (dynamic, String) backup) async { await context.showRoundDialog( title: libL10n.restore, child: Text(libL10n.askContinue('${libL10n.restore} ${libL10n.backup}(${backup.$2})')), actions: Btn.ok( onTap: () async { await backup.$1.merge(force: true); context.pop(); }, ).toList, ); } /// Show password input dialog static Future _showPasswordDialog( BuildContext context, { String? initial, String? title, String? hint, }) async { final controller = TextEditingController(text: initial ?? ''); final result = await context.showRoundDialog( title: title ?? libL10n.pwd, child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(hint ?? l10n.backupPasswordTip, style: UIs.textGrey), UIs.height13, Input( label: l10n.backupPassword, controller: controller, obscureText: true, onSubmitted: (_) => context.pop(controller.text), ), ], ), actions: [ Btn.cancel(), TextButton(onPressed: () => context.pop(controller.text), child: Text(libL10n.ok)), ], ); controller.dispose(); return result; } }