Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 3 additions & 15 deletions lib/data/database/app_database.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
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';
Expand All @@ -19,8 +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_flutter/drift_flutter.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:uuid/uuid.dart';

part 'app_database.g.dart';
Expand Down Expand Up @@ -63,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);
Expand Down Expand Up @@ -162,15 +162,3 @@ 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,
);
}
31 changes: 31 additions & 0 deletions lib/data/database/connection_native.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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);
// 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');
}
}
}
Comment thread
iamvirul marked this conversation as resolved.
}
return NativeDatabase.createInBackground(file);
});
}
12 changes: 12 additions & 0 deletions lib/data/database/connection_web.dart
Original file line number Diff line number Diff line change
@@ -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'),
),
);
}
6 changes: 4 additions & 2 deletions lib/data/sync/sync_tables_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
SyncColumn('created_at', _ts),
SyncColumn('updated_at', _ts),
],
Expand Down Expand Up @@ -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),
],
Expand Down
13 changes: 7 additions & 6 deletions lib/features/settings/presentation/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -183,23 +183,24 @@ class SettingsScreen extends ConsumerWidget {
}

Future<void> _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<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text(context.l10n.importDatabase),
content: Text(context.l10n.importDatabaseMessage),
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)),
],
),
);
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)));
}
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
6 changes: 6 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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?';
Expand Down
3 changes: 3 additions & 0 deletions lib/l10n/app_localizations_si.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,9 @@ class AppLocalizationsSi extends AppLocalizations {
@override
String get importDatabaseConfirm => 'ආයාත කරන්න';

@override
String get fileLabel => 'ගොනුව';

@override
String get importDatabaseMessage =>
'සටහන් ගොනුවෙන් ඇතුළු කෙරේ. පවතින සටහන් නොවෙනස්. ඉදිරියට යන්නද?';
Expand Down
3 changes: 3 additions & 0 deletions lib/l10n/app_localizations_ta.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,9 @@ class AppLocalizationsTa extends AppLocalizations {
@override
String get importDatabaseConfirm => 'இறக்குமதி';

@override
String get fileLabel => 'கோப்பு';

@override
String get importDatabaseMessage =>
'காப்பு கோப்பில் இருந்து பதிவுகள் சேர்க்கப்படும். தற்போதைய பதிவுகள் மாற்றப்படாது. தொடரவா?';
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_si.arb
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@
"skipped": "මඟ හැරියේ:",
"errors": "දෝෂ:",
"importDatabaseConfirm": "ආයාත කරන්න",
"fileLabel": "ගොනුව",
"importDatabaseMessage": "සටහන් ගොනුවෙන් ඇතුළු කෙරේ. පවතින සටහන් නොවෙනස්. ඉදිරියට යන්නද?",
"dbBackend": "ද්විතියික",
"dbSqliteLocal": "SQLite (දේශීය)",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_ta.arb
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@
"skipped": "தவிர்க்கப்பட்டது:",
"errors": "பிழைகள்:",
"importDatabaseConfirm": "இறக்குமதி",
"fileLabel": "கோப்பு",
"importDatabaseMessage": "காப்பு கோப்பில் இருந்து பதிவுகள் சேர்க்கப்படும். தற்போதைய பதிவுகள் மாற்றப்படாது. தொடரவா?",
"dbBackend": "நுட்பம்",
"dbSqliteLocal": "SQLite (உள்ளூர்)",
Expand Down
11 changes: 7 additions & 4 deletions lib/providers/settings_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -337,15 +337,18 @@ class SettingsActions {
return Uint8List.fromList(utf8.encode(const JsonEncoder.withIndent(' ').convert(payload)));
}

Future<String> importDatabaseFromJson() async {
Future<PlatformFile?> 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<String> importDatabaseFromJson(PlatformFile picked) async {
final bytes = await _readPickedFile(picked);
if (bytes == null) return 'Could not read file';

try {
Expand Down Expand Up @@ -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';
Expand Down
Loading