import 'dart:io'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class ProfileData { final int id; final String firstName; final String? lastName; final int phone; final int? photoId; final String? baseUrl; final String? baseRawUrl; final String country; final int accountStatus; final int updateTime; ProfileData({ required this.id, required this.firstName, this.lastName, required this.phone, this.photoId, this.baseUrl, this.baseRawUrl, required this.country, required this.accountStatus, required this.updateTime, }); factory ProfileData.fromServerMap(Map contact) { final names = contact['names']; String firstName = ''; String? lastName; if (names is List && names.isNotEmpty) { final name = names.firstWhere( (n) => n is Map && n['type'] == 'ONEME', orElse: () => names.first, ) as Map; firstName = (name['firstName'] as String?) ?? ''; lastName = name['lastName'] as String?; } return ProfileData( id: contact['id'] as int, firstName: firstName, lastName: lastName, phone: contact['phone'] as int, photoId: contact['photoId'] as int?, baseUrl: contact['baseUrl'] as String?, baseRawUrl: contact['baseRawUrl'] as String?, country: (contact['country'] as String?) ?? '', accountStatus: (contact['accountStatus'] as int?) ?? 0, updateTime: (contact['updateTime'] as int?) ?? 0, ); } factory ProfileData.fromDbRow(Map row) { return ProfileData( id: row['id'] as int, firstName: row['first_name'] as String, lastName: row['last_name'] as String?, phone: row['phone'] as int, photoId: row['photo_id'] as int?, baseUrl: row['base_url'] as String?, baseRawUrl: row['base_raw_url'] as String?, country: row['country'] as String, accountStatus: row['account_status'] as int, updateTime: row['update_time'] as int, ); } Map toDbRow() => { 'id': id, 'first_name': firstName, 'last_name': lastName, 'phone': phone, 'photo_id': photoId, 'base_url': baseUrl, 'base_raw_url': baseRawUrl, 'country': country, 'account_status': accountStatus, 'update_time': updateTime, }; } abstract class SyncKey { static const chatsSync = 'chats_sync'; static const contactsSync = 'contacts_sync'; static const callsSync = 'calls_sync'; static const draftsSync = 'drafts_sync'; static const bannersSync = 'banners_sync'; static const presenceSync = 'presence_sync'; static const lastLogin = 'last_login'; static const configHash = 'config_hash'; static const chatCacheFingerprint = 'chat_cache_fingerprint'; static const serverTime = 'server_time'; } class AppDatabase { static Database? _db; static Future init() async { if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { sqfliteFfiInit(); databaseFactory = databaseFactoryFfi; } } static Future get _instance async { _db ??= await _open(); return _db!; } static Future _open() async { final dbPath = await getDatabasesPath(); return openDatabase( join(dbPath, 'komet.db'), version: 5, onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'), onCreate: (db, _) => _createTables(db), onUpgrade: (db, oldVersion, newVersion) async { if (oldVersion < 2) { await db.execute( 'ALTER TABLE profile ADD COLUMN is_active INTEGER NOT NULL DEFAULT 0', ); await db.execute('DROP TABLE IF EXISTS sync_state'); await db.execute(_syncStateSchema); } if (oldVersion < 3) { await db.execute(_chatsCacheSchema); } if (oldVersion < 4) { await db.execute(_contactsSchema); } if (oldVersion < 5) { await db.execute('DROP TABLE IF EXISTS chats_cache'); await db.execute(_chatsCacheSchema); } }, ); } static Future _createTables(Database db) async { await db.execute(''' CREATE TABLE profile ( id INTEGER PRIMARY KEY, first_name TEXT NOT NULL, last_name TEXT, phone INTEGER NOT NULL, photo_id INTEGER, base_url TEXT, base_raw_url TEXT, country TEXT NOT NULL DEFAULT '', account_status INTEGER NOT NULL DEFAULT 0, update_time INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 0 ) '''); await db.execute(_syncStateSchema); await db.execute(_chatsCacheSchema); await db.execute(_contactsSchema); } static const _contactsSchema = ''' CREATE TABLE contacts ( id INTEGER PRIMARY KEY, account_id INTEGER NOT NULL REFERENCES profile(id) ON DELETE CASCADE, first_name TEXT NOT NULL, last_name TEXT, phone INTEGER NOT NULL, photo_id INTEGER, base_url TEXT, base_raw_url TEXT, update_time INTEGER NOT NULL DEFAULT 0 ) '''; static const _syncStateSchema = ''' CREATE TABLE sync_state ( account_id INTEGER NOT NULL REFERENCES profile(id) ON DELETE CASCADE, key TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (account_id, key) ) '''; static const _chatsCacheSchema = ''' CREATE TABLE chats_cache ( id INTEGER NOT NULL, account_id INTEGER NOT NULL REFERENCES profile(id) ON DELETE CASCADE, type TEXT NOT NULL, title TEXT, icon_url TEXT, last_msg_id INTEGER, last_msg_time INTEGER, last_msg_text TEXT, last_msg_sender INTEGER, unread_count INTEGER NOT NULL DEFAULT 0, last_event_time INTEGER NOT NULL DEFAULT 0, cached_at INTEGER NOT NULL, fav_index INTEGER, dont_disturb_until INTEGER NOT NULL DEFAULT 0, is_online INTEGER NOT NULL DEFAULT 0, seen_time INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (id, account_id) ) '''; static Future saveProfile(ProfileData profile) async { final db = await _instance; await db.insert( 'profile', profile.toDbRow(), conflictAlgorithm: ConflictAlgorithm.replace, ); } static Future loadProfile(int accountId) async { final db = await _instance; final rows = await db.query( 'profile', where: 'id = ?', whereArgs: [accountId], limit: 1, ); if (rows.isEmpty) return null; return ProfileData.fromDbRow(rows.first); } static Future> loadAllProfiles() async { final db = await _instance; final rows = await db.query('profile', orderBy: 'is_active DESC, id ASC'); return rows.map(ProfileData.fromDbRow).toList(); } static Future loadActiveProfile() async { final db = await _instance; final rows = await db.query('profile', where: 'is_active = 1', limit: 1); if (rows.isEmpty) return null; return ProfileData.fromDbRow(rows.first); } static Future setActiveAccount(int accountId) async { final db = await _instance; await db.transaction((txn) async { await txn.update('profile', {'is_active': 0}); await txn.update( 'profile', {'is_active': 1}, where: 'id = ?', whereArgs: [accountId], ); }); } static Future deleteAccount(int accountId) async { final db = await _instance; await db.delete('profile', where: 'id = ?', whereArgs: [accountId]); } static Future setSyncValue( int accountId, String key, String value, ) async { final db = await _instance; await db.insert('sync_state', { 'account_id': accountId, 'key': key, 'value': value, }, conflictAlgorithm: ConflictAlgorithm.replace); } static Future getSyncValue(int accountId, String key) async { final db = await _instance; final rows = await db.query( 'sync_state', where: 'account_id = ? AND key = ?', whereArgs: [accountId, key], limit: 1, ); if (rows.isEmpty) return null; return rows.first['value'] as String; } static Future> getAllSyncValues(int accountId) async { final db = await _instance; final rows = await db.query( 'sync_state', where: 'account_id = ?', whereArgs: [accountId], ); return { for (final row in rows) row['key'] as String: row['value'] as String, }; } static Future close() async { await _db?.close(); _db = null; } // Chats cache static Future saveChats(List> rows) async { final db = await _instance; final batch = db.batch(); for (final row in rows) { batch.insert( 'chats_cache', row, conflictAlgorithm: ConflictAlgorithm.replace, ); } await batch.commit(noResult: true); } static Future>> loadChats(int accountId) async { final db = await _instance; return db.query( 'chats_cache', where: 'account_id = ?', whereArgs: [accountId], orderBy: 'last_event_time DESC', ); } static Future clearChatsCache(int accountId) async { final db = await _instance; await db.delete( 'chats_cache', where: 'account_id = ?', whereArgs: [accountId], ); } static Future saveContacts(List> rows) async { final db = await _instance; final batch = db.batch(); for (final row in rows) { batch.insert( 'contacts', row, conflictAlgorithm: ConflictAlgorithm.replace, ); } await batch.commit(noResult: true); } static Future>> loadContacts(int accountId) async { final db = await _instance; return db.query( 'contacts', where: 'account_id = ?', whereArgs: [accountId], ); } }