using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Billing.Models; using SQLite; using Xamarin.Essentials; using Resource = Billing.Languages.Resource; namespace Billing.Store { public class StoreHelper { public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal); public static readonly string CacheFolder = FileSystem.CacheDirectory; public static string DatabasePath => Path.Combine(PersonalFolder, dbfile); #region Sqlite3 private const string dbfile = "data.db3"; private static SQLiteAsyncConnection database; #endregion public static async Task ReloadDatabase(string file) { var path = DatabasePath; if (string.Equals(file, path, StringComparison.OrdinalIgnoreCase)) { return false; } try { if (database != null) { await database.CloseAsync(); } } catch (Exception ex) { Helper.Error("database.close", ex); } try { File.Copy(file, path, true); } catch (Exception ex) { Helper.Error("file.import", ex); } try { database = new SQLiteAsyncConnection(path, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache); } catch (Exception ex) { Helper.Error("database.connect", ex); } return true; } public static readonly AsyncLazy Instance = new(async () => { var instance = new StoreHelper(); try { await database.CreateTablesAsync(); } catch (Exception ex) { Helper.Error("database.init.table", ex); } try { var count = await database.ExecuteScalarAsync("SELECT COUNT(Id) FROM [Category]"); if (count <= 0) { await database.InsertAsync(new Account { Name = Resource.Cash, Icon = "wallet" }); } } catch (Exception ex) { Helper.Error("database.init.account", ex); } try { var count = await database.ExecuteScalarAsync("SELECT COUNT(Id) FROM [Category]"); if (count <= 0) { // init categories await database.InsertAllAsync(new List { // sample categories new() { Name = Resource.Clothing, Icon = "clothes" }, new() { Name = Resource.Food, Icon = "food" }, new() { Name = Resource.Daily, Icon = "daily" }, new() { Name = Resource.Trans, Icon = "trans" }, new() { Name = Resource.Entertainment, Icon = "face" }, new() { Name = Resource.Learn, Icon = "learn" }, new() { Name = Resource.Medical, Icon = "medical" }, new() { Name = Resource.OtherSpending, Icon = "plus" }, new() { Type = CategoryType.Income, Name = Resource.Earnings, Icon = "#brand#btc" }, new() { Type = CategoryType.Income, Name = Resource.OtherIncome, Icon = "plus" }, // sub-categories new() { ParentId = 1, Name = Resource.Jewellery, Icon = "gem" }, new() { ParentId = 1, Name = Resource.Cosmetics, Icon = "makeup" }, new() { ParentId = 2, Name = Resource.Brunch, Icon = "brunch" }, new() { ParentId = 2, Name = Resource.Dinner, Icon = "dinner" }, new() { ParentId = 2, Name = Resource.Drinks, Icon = "drink" }, new() { ParentId = 2, Name = Resource.Fruit, Icon = "fruit" }, new() { ParentId = 3, Name = Resource.UtilityBill, Icon = "bill" }, new() { ParentId = 3, Name = Resource.PropertyFee, Icon = "fee" }, new() { ParentId = 3, Name = Resource.Rent, Icon = "rent" }, new() { ParentId = 3, Name = Resource.Maintenance, Icon = "maintenance" }, new() { ParentId = 4, Name = Resource.LightRail, Icon = "rail" }, new() { ParentId = 4, Name = Resource.Taxi, Icon = "taxi" }, new() { ParentId = 5, Name = Resource.Fitness, Icon = "fitness" }, new() { ParentId = 5, Name = Resource.Party, Icon = "party" }, new() { ParentId = 9, Type = CategoryType.Income, Name = Resource.Salary, Icon = "#brand#buffer" }, new() { ParentId = 9, Type = CategoryType.Income, Name = Resource.Bonus, Icon = "dollar" }, }); } } catch (Exception ex) { Helper.Error("database.init.category", ex); } return instance; }); public static async Task SaveAccountItemAsync(Account account) { var instance = await Instance; return await instance.SaveItemAsync(account); } public static async Task DeleteAccountItemAsync(Account account) { var instance = await Instance; return await instance.DeleteItemAsync(account); } public static async Task SaveBillItemAsync(Bill bill) { var instance = await Instance; return await instance.SaveItemAsync(bill); } public static async Task DeleteBillItemAsync(Bill bill) { var instance = await Instance; return await instance.DeleteItemAsync(bill); } public static async Task SaveCategoryItemAsync(Category category) { var instance = await Instance; return await instance.SaveItemAsync(category); } public static async Task DeleteCategoryItemAsync(Category category) { var instance = await Instance; return await instance.DeleteItemAsync(category); } public static async Task GetLogsCount() { await Instance; return await database.ExecuteScalarAsync("SELECT COUNT(Id) FROM [Logs]"); } public static async Task SaveLogItemAsync(Logs log) { var instance = await Instance; return await instance.SaveItemAsync(log); } public static string GetLogFile() { return Path.Combine(CacheFolder, "logs.csv"); } public static async Task ExportLogs() { try { var instance = await Instance; var logs = await instance.GetListAsync(); var file = GetLogFile(); using var writer = new StreamWriter(File.Open(file, FileMode.Create, FileAccess.Write)); writer.WriteLine("Id,DateTime,Category,Detail"); foreach (var log in logs) { var category = log.Category?.Replace("\n", " \\n "); var detail = log.Detail?.Replace("\n", " \\n "); writer.WriteLine($"{log.Id},{log.LogTime},{category},{detail}"); } writer.Flush(); await database.ExecuteAsync("DELETE FROM [Logs]; DELETE FROM [sqlite_sequence] WHERE [name] = 'Logs'"); return file; } catch { return null; } } private StoreHelper() { if (database == null) { try { database = new SQLiteAsyncConnection(DatabasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache); } catch (Exception ex) { Helper.Error("database.ctor.connect", ex); } } } public Task> GetListAsync(string query, params object[] args) where T : new() { try { return database.QueryAsync(query, args); } catch (Exception ex) { Helper.Error("db.read", $"failed to read db, query: {string.Format(query, args)}, error: {ex.Message}"); } return default; } public Task GetItemAsync(int id) where T : IIdItem, new() { try { var source = new TaskCompletionSource(); Task.Run(async () => { var item = await database.FindWithQueryAsync($"SELECT * FROM [{typeof(T).Name}] WHERE [Id] = ? LIMIT 1", id); source.SetResult(item); }); return source.Task; } catch (Exception ex) { Helper.Error("db.read", $"failed to get item, table: {typeof(T)}, id: {id}, error: {ex.Message}"); } return default; } #region Helper public Task> GetListAsync() where T : new() { try { return database.Table().ToListAsync(); } catch (Exception ex) { Helper.Error("db.read", $"failed to read db, error: {ex.Message}"); } return default; } public Task SaveItemAsync(T item) where T : IIdItem { try { if (item.Id > 0) { return database.UpdateAsync(item); } else { return database.InsertAsync(item); } } catch (Exception ex) { Helper.Error("db.write", $"failed to insert/update item, table: {typeof(T)}, id: {item.Id}, item: {item}, error: {ex.Message}"); } return Task.FromResult(0); } public Task DeleteItemAsync(T item) where T : IIdItem { try { return database.DeleteAsync(item); } catch (Exception ex) { Helper.Error("db.delete", $"failed to delete item, table: {typeof(T)}, id: {item.Id}, item: {item}, error: {ex.Message}"); } return Task.FromResult(0); } #endregion } }