From 9d563cf0ad91a53e463edfb329b2f535481ea901 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Wed, 24 Jun 2026 02:56:04 +0530 Subject: [PATCH 1/6] fix: move DB to Application Support, open file picker before confirmation dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Store bms_local.sqlite in getApplicationSupportDirectory() instead of Documents/; drift_flutter defaults to Documents on desktop which is wrong for app data. Uses LazyDatabase + NativeDatabase with explicit path. - Invert import DB flow: open NSOpenPanel first, then show confirmation dialog with the selected filename. The previous dialog→NSOpenPanel sequence caused the macOS window to go black and become unresponsive because NSOpenPanel captured Metal render focus mid-animation. --- lib/data/database/app_database.dart | 28 +++++++++++++------ .../presentation/settings_screen.dart | 13 +++++---- lib/providers/settings_provider.dart | 11 +++++--- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/lib/data/database/app_database.dart b/lib/data/database/app_database.dart index adca6fd..0e321cb 100644 --- a/lib/data/database/app_database.dart +++ b/lib/data/database/app_database.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:bcrypt/bcrypt.dart'; import 'package:bms/data/database/daos/audit_log_dao.dart'; import 'package:bms/data/database/daos/cheques_dao.dart'; @@ -19,8 +21,11 @@ import 'package:bms/data/database/tables/returns_table.dart'; import 'package:bms/data/database/tables/suppliers_table.dart'; import 'package:bms/data/database/tables/users_table.dart'; import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; part 'app_database.g.dart'; @@ -164,13 +169,18 @@ class AppDatabase extends _$AppDatabase { } QueryExecutor _openConnection() { - return driftDatabase( - name: 'bms_local', - web: kIsWeb - ? DriftWebOptions( - sqlite3Wasm: Uri.parse('sqlite3.wasm'), - driftWorker: Uri.parse('drift_worker.js'), - ) - : null, - ); + if (kIsWeb) { + return driftDatabase( + name: 'bms_local', + web: DriftWebOptions( + sqlite3Wasm: Uri.parse('sqlite3.wasm'), + driftWorker: Uri.parse('drift_worker.js'), + ), + ); + } + return LazyDatabase(() async { + final dir = await getApplicationSupportDirectory(); + final file = File(p.join(dir.path, 'bms_local.sqlite')); + return NativeDatabase.createInBackground(file); + }); } diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index acb4f8e..55e588e 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -183,11 +183,16 @@ class SettingsScreen extends ConsumerWidget { } Future _importDb(BuildContext context, WidgetRef ref) async { + // Open file picker first — avoids the dialog→NSOpenPanel transition + // that causes the macOS window to go black and become unresponsive. + final picked = await ref.read(settingsActionsProvider).pickDatabaseJsonFile(); + if (picked == null || !context.mounted) return; + final confirmed = await showDialog( context: context, builder: (_) => AlertDialog( title: Text(context.l10n.importDatabase), - content: Text(context.l10n.importDatabaseMessage), + content: Text('${context.l10n.importDatabaseMessage}\n\nFile: ${picked.name}'), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: Text(context.l10n.cancel)), ElevatedButton(onPressed: () => Navigator.pop(context, true), child: Text(context.l10n.importDatabaseConfirm)), @@ -195,11 +200,7 @@ class SettingsScreen extends ConsumerWidget { ), ); if (confirmed != true || !context.mounted) return; - // Give Flutter one frame to repaint before NSOpenPanel captures focus, - // otherwise the macOS window goes black. - await Future.delayed(const Duration(milliseconds: 100)); - if (!context.mounted) return; - final msg = await ref.read(settingsActionsProvider).importDatabaseFromJson(); + final msg = await ref.read(settingsActionsProvider).importDatabaseFromJson(picked); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); } diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 89aed05..d7f36eb 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -337,15 +337,18 @@ class SettingsActions { return Uint8List.fromList(utf8.encode(const JsonEncoder.withIndent(' ').convert(payload))); } - Future importDatabaseFromJson() async { + Future pickDatabaseJsonFile() async { final result = await FilePicker.pickFiles( type: FileType.custom, allowedExtensions: ['json'], withData: true, ); - if (result == null || result.files.isEmpty) return 'Cancelled'; + if (result == null || result.files.isEmpty) return null; + return result.files.first; + } - final bytes = await _readPickedFile(result.files.first); + Future importDatabaseFromJson(PlatformFile picked) async { + final bytes = await _readPickedFile(picked); if (bytes == null) return 'Could not read file'; try { @@ -397,7 +400,7 @@ class SettingsActions { action: 'create', userId: _actorId, userName: _actorName, - newValue: {'format': 'json', 'source': result.files.first.name}, + newValue: {'format': 'json', 'source': picked.name}, ); return 'Import complete'; From 60b975f4c02ac55902770ec4ecc507a93204c787 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Wed, 24 Jun 2026 03:05:15 +0530 Subject: [PATCH 2/6] fix: correct suppliers and customers sync column definitions suppliers: removed credit_limit (does not exist in the Drift schema), added payment_terms (nullable text) and is_active (integer/bool). customers: added missing is_active column; kept credit_limit which exists as creditLimit -> credit_limit in SQLite. --- lib/data/sync/sync_tables_registry.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/data/sync/sync_tables_registry.dart b/lib/data/sync/sync_tables_registry.dart index a5c3e59..ca7dfdc 100644 --- a/lib/data/sync/sync_tables_registry.dart +++ b/lib/data/sync/sync_tables_registry.dart @@ -79,8 +79,9 @@ const _customers = SyncTable( SyncColumn('name', _txt), SyncColumn('phone', _txt, nullable: true), SyncColumn('address', _txt, nullable: true), + SyncColumn('credit_limit', _real), SyncColumn('balance', _real), - SyncColumn('credit_limit', _real, nullable: true), + SyncColumn('is_active', _ts), SyncColumn('created_at', _ts), SyncColumn('updated_at', _ts), ], @@ -108,8 +109,9 @@ const _suppliers = SyncTable( SyncColumn('name', _txt), SyncColumn('phone', _txt, nullable: true), SyncColumn('address', _txt, nullable: true), + SyncColumn('payment_terms', _txt, nullable: true), SyncColumn('balance', _real), - SyncColumn('credit_limit', _real, nullable: true), + SyncColumn('is_active', _ts), SyncColumn('created_at', _ts), SyncColumn('updated_at', _ts), ], From 17b5512413bb8bca27e73a0df73d6f7ad82a0c7e Mon Sep 17 00:00:00 2001 From: iamvirul Date: Wed, 24 Jun 2026 03:06:11 +0530 Subject: [PATCH 3/6] docs: update changelog for DB path and sync registry fixes --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf1afaf..2300746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Fixed +- Database now stored in `Application Support` directory instead of `Documents`; `drift_flutter` defaulted to `getApplicationDocumentsDirectory()` on desktop, placing `bms_local.sqlite` in `~/Documents/` which is inappropriate for app data; switched to `LazyDatabase` + `NativeDatabase` with `getApplicationSupportDirectory()` (macOS: `~/Library/Application Support/lk.getbms.bms/bms_local.sqlite`) +- Import Database file picker no longer causes macOS window to go black and become unresponsive; inverted flow so `NSOpenPanel` opens first, then a confirmation dialog shows the selected filename - avoids the dialog-to-native-panel transition that caused the Metal renderer to lose focus +- MySQL sync `suppliers` table: removed incorrect `credit_limit` column (belongs to customers, not suppliers), added `payment_terms` and `is_active` to match the Drift schema +- MySQL sync `customers` table: added missing `is_active` column to match the Drift schema - EULA screen now always displays in English regardless of the selected app language; all UI strings (title, buttons, checkbox, dialogs) are hardcoded English so the legal agreement is consistent across locales - Replaced placeholder briefcase icon in EULA header with the actual BMS SVG logo - Expanded EULA from 10 to 15 sections: added Definitions, Grant of License, License Tiers, Restrictions, IP Ownership, License Key enforcement, User Data and Privacy, Updates and Support, Warranty Disclaimer, Liability Cap, Indemnification, Termination, Governing Law, Severability, and Entire Agreement From 7e747bf1e7ae47335950992c2d61f265fb710eb9 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Wed, 24 Jun 2026 03:31:26 +0530 Subject: [PATCH 4/6] fix: implement one-time migration for database file location and update localization for file label --- lib/data/database/app_database.dart | 9 +++++++++ lib/features/settings/presentation/settings_screen.dart | 4 ++-- lib/l10n/app_en.arb | 1 + lib/l10n/app_localizations.dart | 6 ++++++ lib/l10n/app_localizations_en.dart | 3 +++ lib/l10n/app_localizations_si.dart | 3 +++ lib/l10n/app_localizations_ta.dart | 3 +++ lib/l10n/app_si.arb | 1 + lib/l10n/app_ta.arb | 1 + 9 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/data/database/app_database.dart b/lib/data/database/app_database.dart index 0e321cb..0278466 100644 --- a/lib/data/database/app_database.dart +++ b/lib/data/database/app_database.dart @@ -181,6 +181,15 @@ QueryExecutor _openConnection() { return LazyDatabase(() async { final dir = await getApplicationSupportDirectory(); final file = File(p.join(dir.path, 'bms_local.sqlite')); + // One-time migration: drift_flutter previously defaulted to Documents/. + // Copy the legacy file on first launch if the new location is empty. + if (!file.existsSync()) { + final docsDir = await getApplicationDocumentsDirectory(); + final legacy = File(p.join(docsDir.path, 'bms_local.sqlite')); + if (legacy.existsSync()) { + await legacy.copy(file.path); + } + } return NativeDatabase.createInBackground(file); }); } diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index 55e588e..a87de0e 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -183,7 +183,7 @@ class SettingsScreen extends ConsumerWidget { } Future _importDb(BuildContext context, WidgetRef ref) async { - // Open file picker first — avoids the dialog→NSOpenPanel transition + // Open file picker first - avoids the dialog->NSOpenPanel transition // that causes the macOS window to go black and become unresponsive. final picked = await ref.read(settingsActionsProvider).pickDatabaseJsonFile(); if (picked == null || !context.mounted) return; @@ -192,7 +192,7 @@ class SettingsScreen extends ConsumerWidget { context: context, builder: (_) => AlertDialog( title: Text(context.l10n.importDatabase), - content: Text('${context.l10n.importDatabaseMessage}\n\nFile: ${picked.name}'), + content: Text('${context.l10n.importDatabaseMessage}\n\n${context.l10n.fileLabel}: ${picked.name}'), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: Text(context.l10n.cancel)), ElevatedButton(onPressed: () => Navigator.pop(context, true), child: Text(context.l10n.importDatabaseConfirm)), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index cd7a0ec..3a9df5e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -392,6 +392,7 @@ "skipped": "Skipped:", "errors": "Errors:", "importDatabaseConfirm": "Import", + "fileLabel": "File", "importDatabaseMessage": "This will add records from the backup file. Existing records will not be overwritten. Continue?", "dbBackend": "Backend", "dbSqliteLocal": "SQLite (local)", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index db2c99e..7427a6f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2260,6 +2260,12 @@ abstract class AppLocalizations { /// **'Import'** String get importDatabaseConfirm; + /// No description provided for @fileLabel. + /// + /// In en, this message translates to: + /// **'File'** + String get fileLabel; + /// No description provided for @importDatabaseMessage. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 8b16334..6f51ba0 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1117,6 +1117,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get importDatabaseConfirm => 'Import'; + @override + String get fileLabel => 'File'; + @override String get importDatabaseMessage => 'This will add records from the backup file. Existing records will not be overwritten. Continue?'; diff --git a/lib/l10n/app_localizations_si.dart b/lib/l10n/app_localizations_si.dart index ac64ff0..72adc47 100644 --- a/lib/l10n/app_localizations_si.dart +++ b/lib/l10n/app_localizations_si.dart @@ -1115,6 +1115,9 @@ class AppLocalizationsSi extends AppLocalizations { @override String get importDatabaseConfirm => 'ආයාත කරන්න'; + @override + String get fileLabel => 'ගොනුව'; + @override String get importDatabaseMessage => 'සටහන් ගොනුවෙන් ඇතුළු කෙරේ. පවතින සටහන් නොවෙනස්. ඉදිරියට යන්නද?'; diff --git a/lib/l10n/app_localizations_ta.dart b/lib/l10n/app_localizations_ta.dart index df84cd9..6f5e204 100644 --- a/lib/l10n/app_localizations_ta.dart +++ b/lib/l10n/app_localizations_ta.dart @@ -1120,6 +1120,9 @@ class AppLocalizationsTa extends AppLocalizations { @override String get importDatabaseConfirm => 'இறக்குமதி'; + @override + String get fileLabel => 'கோப்பு'; + @override String get importDatabaseMessage => 'காப்பு கோப்பில் இருந்து பதிவுகள் சேர்க்கப்படும். தற்போதைய பதிவுகள் மாற்றப்படாது. தொடரவா?'; diff --git a/lib/l10n/app_si.arb b/lib/l10n/app_si.arb index 9fca919..f75e670 100644 --- a/lib/l10n/app_si.arb +++ b/lib/l10n/app_si.arb @@ -381,6 +381,7 @@ "skipped": "මඟ හැරියේ:", "errors": "දෝෂ:", "importDatabaseConfirm": "ආයාත කරන්න", + "fileLabel": "ගොනුව", "importDatabaseMessage": "සටහන් ගොනුවෙන් ඇතුළු කෙරේ. පවතින සටහන් නොවෙනස්. ඉදිරියට යන්නද?", "dbBackend": "ද්විතියික", "dbSqliteLocal": "SQLite (දේශීය)", diff --git a/lib/l10n/app_ta.arb b/lib/l10n/app_ta.arb index 8f1912f..de7ddba 100644 --- a/lib/l10n/app_ta.arb +++ b/lib/l10n/app_ta.arb @@ -381,6 +381,7 @@ "skipped": "தவிர்க்கப்பட்டது:", "errors": "பிழைகள்:", "importDatabaseConfirm": "இறக்குமதி", + "fileLabel": "கோப்பு", "importDatabaseMessage": "காப்பு கோப்பில் இருந்து பதிவுகள் சேர்க்கப்படும். தற்போதைய பதிவுகள் மாற்றப்படாது. தொடரவா?", "dbBackend": "நுட்பம்", "dbSqliteLocal": "SQLite (உள்ளூர்)", From c9055f01c8aad923f72e2f9518033219ddac99b3 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Thu, 25 Jun 2026 12:43:54 +0530 Subject: [PATCH 5/6] fix: use conditional import for db connection to fix web build --- lib/data/database/app_database.dart | 37 ++---------------------- lib/data/database/connection_native.dart | 23 +++++++++++++++ lib/data/database/connection_web.dart | 12 ++++++++ 3 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 lib/data/database/connection_native.dart create mode 100644 lib/data/database/connection_web.dart diff --git a/lib/data/database/app_database.dart b/lib/data/database/app_database.dart index 0278466..7cb05c2 100644 --- a/lib/data/database/app_database.dart +++ b/lib/data/database/app_database.dart @@ -1,6 +1,6 @@ -import 'dart:io'; - import 'package:bcrypt/bcrypt.dart'; +import 'package:bms/data/database/connection_web.dart' + if (dart.library.io) 'package:bms/data/database/connection_native.dart'; import 'package:bms/data/database/daos/audit_log_dao.dart'; import 'package:bms/data/database/daos/cheques_dao.dart'; import 'package:bms/data/database/daos/customers_dao.dart'; @@ -21,11 +21,6 @@ import 'package:bms/data/database/tables/returns_table.dart'; import 'package:bms/data/database/tables/suppliers_table.dart'; import 'package:bms/data/database/tables/users_table.dart'; import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -import 'package:drift_flutter/drift_flutter.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; part 'app_database.g.dart'; @@ -68,7 +63,7 @@ part 'app_database.g.dart'; ], ) class AppDatabase extends _$AppDatabase { - AppDatabase() : super(_openConnection()); + AppDatabase() : super(openAppDatabaseConnection()); // Used in tests with NativeDatabase.memory() so no disk I/O is needed. AppDatabase.forTesting(super.e); @@ -167,29 +162,3 @@ class AppDatabase extends _$AppDatabase { ); } } - -QueryExecutor _openConnection() { - if (kIsWeb) { - return driftDatabase( - name: 'bms_local', - web: DriftWebOptions( - sqlite3Wasm: Uri.parse('sqlite3.wasm'), - driftWorker: Uri.parse('drift_worker.js'), - ), - ); - } - return LazyDatabase(() async { - final dir = await getApplicationSupportDirectory(); - final file = File(p.join(dir.path, 'bms_local.sqlite')); - // One-time migration: drift_flutter previously defaulted to Documents/. - // Copy the legacy file on first launch if the new location is empty. - if (!file.existsSync()) { - final docsDir = await getApplicationDocumentsDirectory(); - final legacy = File(p.join(docsDir.path, 'bms_local.sqlite')); - if (legacy.existsSync()) { - await legacy.copy(file.path); - } - } - return NativeDatabase.createInBackground(file); - }); -} diff --git a/lib/data/database/connection_native.dart b/lib/data/database/connection_native.dart new file mode 100644 index 0000000..ad70cf0 --- /dev/null +++ b/lib/data/database/connection_native.dart @@ -0,0 +1,23 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +QueryExecutor openAppDatabaseConnection() { + return LazyDatabase(() async { + final dir = await getApplicationSupportDirectory(); + final file = File(p.join(dir.path, 'bms_local.sqlite')); + // One-time migration: drift_flutter previously defaulted to Documents/. + // Copy the legacy file on first launch if the new location is empty. + if (!file.existsSync()) { + final docsDir = await getApplicationDocumentsDirectory(); + final legacy = File(p.join(docsDir.path, 'bms_local.sqlite')); + if (legacy.existsSync()) { + await legacy.copy(file.path); + } + } + return NativeDatabase.createInBackground(file); + }); +} diff --git a/lib/data/database/connection_web.dart b/lib/data/database/connection_web.dart new file mode 100644 index 0000000..c022126 --- /dev/null +++ b/lib/data/database/connection_web.dart @@ -0,0 +1,12 @@ +import 'package:drift/drift.dart'; +import 'package:drift_flutter/drift_flutter.dart'; + +QueryExecutor openAppDatabaseConnection() { + return driftDatabase( + name: 'bms_local', + web: DriftWebOptions( + sqlite3Wasm: Uri.parse('sqlite3.wasm'), + driftWorker: Uri.parse('drift_worker.js'), + ), + ); +} From 111ab7140c74636211810a5787281f779f762d41 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Thu, 25 Jun 2026 13:22:08 +0530 Subject: [PATCH 6/6] fix: copy WAL sidecars during legacy DB migration to preserve uncheckpointed data --- lib/data/database/connection_native.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/data/database/connection_native.dart b/lib/data/database/connection_native.dart index ad70cf0..f13a87f 100644 --- a/lib/data/database/connection_native.dart +++ b/lib/data/database/connection_native.dart @@ -16,6 +16,14 @@ QueryExecutor openAppDatabaseConnection() { final legacy = File(p.join(docsDir.path, 'bms_local.sqlite')); if (legacy.existsSync()) { await legacy.copy(file.path); + // Copy WAL sidecars so committed-but-uncheckpointed data is not lost. + // SQLite replays and checkpoints the WAL automatically on next open. + for (final suffix in ['-wal', '-shm']) { + final sidecar = File('${legacy.path}$suffix'); + if (sidecar.existsSync()) { + await sidecar.copy('${file.path}$suffix'); + } + } } } return NativeDatabase.createInBackground(file);