diff --git a/i18n/en.json b/i18n/en.json index e2e0fd3c4f..73338ff6cc 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1002,6 +1002,8 @@ "explorer": "Explorer", "export": "Export", "export_as_json": "Export as JSON", + "export_database": "Export Database", + "export_database_description": "Export the SQLite database", "extension": "Extension", "external": "External", "external_libraries": "External Libraries", diff --git a/mobile/lib/widgets/settings/beta_sync_settings/beta_sync_settings.dart b/mobile/lib/widgets/settings/beta_sync_settings/beta_sync_settings.dart index 3d37fb102b..e69717d3a4 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/beta_sync_settings.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/beta_sync_settings.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:drift/drift.dart' as drift_db; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -10,6 +12,9 @@ import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; class BetaSyncSettings extends HookConsumerWidget { const BetaSyncSettings({ @@ -69,6 +74,67 @@ class BetaSyncSettings extends HookConsumerWidget { }); } + Future exportDatabase() async { + try { + // WAL Checkpoint to ensure all changes are written to the database + await ref + .read(driftProvider) + .customStatement("pragma wal_checkpoint(truncate)"); + final documentsDir = await getApplicationDocumentsDirectory(); + final dbFile = File(path.join(documentsDir.path, 'immich.sqlite')); + + if (!await dbFile.exists()) { + if (context.mounted) { + context.scaffoldMessenger.showSnackBar( + SnackBar( + content: Text("Database file not found".t(context: context)), + ), + ); + } + return; + } + + final timestamp = DateTime.now().millisecondsSinceEpoch; + final exportFile = File( + path.join( + documentsDir.path, + 'immich_export_$timestamp.sqlite', + ), + ); + + await dbFile.copy(exportFile.path); + + await Share.shareXFiles( + [XFile(exportFile.path)], + text: 'Immich Database Export', + ); + + Future.delayed(const Duration(seconds: 30), () async { + if (await exportFile.exists()) { + await exportFile.delete(); + } + }); + + if (context.mounted) { + context.scaffoldMessenger.showSnackBar( + SnackBar( + content: + Text("Database exported successfully".t(context: context)), + ), + ); + } + } catch (e) { + if (context.mounted) { + context.scaffoldMessenger.showSnackBar( + SnackBar( + content: + Text("Failed to export database: $e".t(context: context)), + ), + ); + } + } + } + return FutureBuilder>( future: loadCounts(), builder: (context, snapshot) { @@ -232,6 +298,19 @@ class BetaSyncSettings extends HookConsumerWidget { ), const SizedBox(height: 24), _SectionHeaderText(text: "actions".t(context: context)), + ListTile( + title: Text( + "export_database".t(context: context), + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + "export_database_description".t(context: context), + ), + leading: const Icon(Icons.download), + onTap: exportDatabase, + ), ListTile( title: Text( "reset_sqlite".t(context: context),