From 1e50cfe32d0b6a26b1949811b8388eaab40a1ca0 Mon Sep 17 00:00:00 2001 From: Anna Date: Sun, 18 Feb 2024 10:16:12 -0500 Subject: [PATCH] feat: add database --- Command.cs | 43 +++++++++++++++--- Database.cs | 102 ++++++++++++++++++++++++++++++++++++++++++ Plugin.cs | 7 +++ Screenie.csproj | 7 ++- ScreenshotMetadata.cs | 9 +++- Ui/PluginUi.cs | 28 ++++++++---- global.json | 7 +++ packages.lock.json | 26 +++++++++++ 8 files changed, 210 insertions(+), 19 deletions(-) create mode 100644 Database.cs create mode 100644 global.json diff --git a/Command.cs b/Command.cs index 2f2d804..6c883cc 100644 --- a/Command.cs +++ b/Command.cs @@ -67,15 +67,44 @@ internal class Command : IDisposable { } string hash; - await using (var stream = new Blake3Stream(this.OpenFile(ext, meta))) { + var (inner, path) = this.OpenFile(ext, meta); + await using (var stream = new Blake3Stream(inner)) { await stream.WriteAsync(imageData); hash = Convert.ToHexString(stream.ComputeHash().AsSpan()); } - meta.Blake3Hash = hash; - var json = JsonConvert.SerializeObject(meta, Formatting.Indented); - // TODO: save this info to database - Plugin.Log.Info(json); + var saved = new SavedMetadata { + Path = path, + Blake3Hash = hash, + Metadata = meta, + }; + + this.Plugin.Database.Execute( + """ + insert into screenshots + (hash, path, active_character, location, location_sub, area, area_sub, territory_type, world, world_id, captured_at_local, captured_at_utc, eorzea_time, weather, ward, plot, visible_characters) + values ($hash, $path, jsonb($active_character), $location, $location_sub, $area, $area_sub, $territory_type, $world, $world_id, $captured_at_local, $captured_at_utc, $eorzea_time, $weather, $ward, $plot, jsonb($visible_characters)) + """, + new Dictionary { + ["$hash"] = saved.Blake3Hash, + ["$path"] = saved.Path, + ["$active_character"] = JsonConvert.SerializeObject(saved.Metadata.ActiveCharacter), + ["$location"] = saved.Metadata.Location, + ["$location_sub"] = saved.Metadata.LocationSub, + ["$area"] = saved.Metadata.Area, + ["$area_sub"] = saved.Metadata.AreaSub, + ["$territory_type"] = saved.Metadata.TerritoryType, + ["$world"] = saved.Metadata.World, + ["$world_id"] = saved.Metadata.WorldId, + ["$captured_at_local"] = saved.Metadata.CapturedAtLocal, + ["$captured_at_utc"] = saved.Metadata.CapturedAtUtc, + ["$eorzea_time"] = saved.Metadata.EorzeaTime, + ["$weather"] = saved.Metadata.Weather, + ["$ward"] = saved.Metadata.Ward, + ["$plot"] = saved.Metadata.Plot, + ["$visible_characters"] = JsonConvert.SerializeObject(saved.Metadata.VisibleCharacters), + } + ); var message = new SeStringBuilder() .AddText("Screenshot saved. [") @@ -114,7 +143,7 @@ internal class Command : IDisposable { return (stream.ToArray(), ext); } - private FileStream OpenFile(string ext, ScreenshotMetadata meta) { + private (FileStream, string) OpenFile(string ext, ScreenshotMetadata meta) { Directory.CreateDirectory(this.Plugin.Config.SaveDirectory); var fileName = this.Plugin.Config.SaveFileNameTemplate.Render(meta).ReplaceLineEndings(" "); @@ -125,6 +154,6 @@ internal class Command : IDisposable { var parent = Path.Join(path, ".."); Directory.CreateDirectory(parent); - return new FileStream(path, FileMode.Create, FileAccess.Write); + return (new FileStream(path, FileMode.Create, FileAccess.Write), path); } } diff --git a/Database.cs b/Database.cs new file mode 100644 index 0000000..c52a755 --- /dev/null +++ b/Database.cs @@ -0,0 +1,102 @@ +using System.Data.SQLite; + +namespace Screenie; + +internal class Database : IDisposable { + private Plugin Plugin { get; } + private SQLiteConnection Connection { get; } + + internal Database(Plugin plugin) { + this.Plugin = plugin; + + var dir = this.Plugin.Interface.GetPluginConfigDirectory(); + Directory.CreateDirectory(dir); + var dbPath = Path.Join(dir, "database.sqlite"); + + this.Connection = new SQLiteConnection($"Data Source={dbPath}"); + this.Connection.Open(); + this.RunMigrations(); + } + + internal Database(string dir) { + Directory.CreateDirectory(dir); + var dbPath = Path.Join(dir, "database.sqlite"); + + this.Connection = new SQLiteConnection($"Data Source={dbPath}"); + this.Connection.Open(); + this.RunMigrations(); + } + + public void Dispose() { + this.Connection.Close(); + this.Connection.Dispose(); + } + + private void RunMigrations() { + using var t = this.Connection.BeginTransaction(); + this.Execute("create table if not exists _migrations (id int not null primary key, query text not null)"); + + // get latest migration + long max; + { + using var reader = this.Query("select coalesce(max(id), 0) from _migrations"); + if (!reader.Read()) { + throw new Exception("could not get max migration"); + } + + max = reader.GetFieldValue(0); + } + + if (max < 1) { + const string migrationOne = """ + create table screenshots ( + hash text not null primary key, + path text not null, + active_character jsonb not null, + location text, + location_sub text, + area text, + area_sub text, + territory_type int not null, + world text, + world_id int not null, + captured_at_local timestamp, + captured_at_utc timestamp, + eorzea_time text not null, + weather text, + ward int, + plot int, + visible_characters jsonb not null + ) + """; + this.Execute(migrationOne); + this.Execute("insert into _migrations (id, query) values (1, $query)", new Dictionary { + ["$query"] = migrationOne, + }); + } + + t.Commit(); + } + + private SQLiteCommand GetCommand(string query, Dictionary? parameters = null) { + var command = this.Connection.CreateCommand(); + command.CommandText = query; + if (parameters != null) { + foreach (var (key, value) in parameters) { + command.Parameters.AddWithValue(key, value); + } + } + + return command; + } + + internal SQLiteDataReader Query(string query, Dictionary? parameters = null) { + using var command = this.GetCommand(query, parameters); + return command.ExecuteReader(); + } + + internal int Execute(string query, Dictionary? parameters = null) { + using var command = this.GetCommand(query, parameters); + return command.ExecuteNonQuery(); + } +} diff --git a/Plugin.cs b/Plugin.cs index 42b6f44..3469705 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -36,6 +36,7 @@ public class Plugin : IDalamudPlugin { internal IObjectTable ObjectTable { get; init; } internal Configuration Config { get; } + internal Database Database { get; } internal LinkHandlers LinkHandlers { get; } internal PluginUi Ui { get; } @@ -43,6 +44,7 @@ public class Plugin : IDalamudPlugin { public Plugin() { this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration(); + this.Database = new Database(this); this.LinkHandlers = new LinkHandlers(this); this.Ui = new PluginUi(this); this.Command = new Command(this); @@ -52,9 +54,14 @@ public class Plugin : IDalamudPlugin { this.Command.Dispose(); this.Ui.Dispose(); this.LinkHandlers.Dispose(); + this.Database.Dispose(); } internal void SaveConfig() { this.Interface.SavePluginConfig(this.Config); } + + public static void Main() { + using var db = new Database("/home/anna/code/Screenie"); + } } diff --git a/Screenie.csproj b/Screenie.csproj index abf8397..497d140 100644 --- a/Screenie.csproj +++ b/Screenie.csproj @@ -56,12 +56,15 @@ - + all - + + + + diff --git a/ScreenshotMetadata.cs b/ScreenshotMetadata.cs index 70478b8..324b763 100644 --- a/ScreenshotMetadata.cs +++ b/ScreenshotMetadata.cs @@ -13,8 +13,14 @@ using Map = Lumina.Excel.GeneratedSheets.Map; namespace Screenie; [Serializable] -public class ScreenshotMetadata { +public class SavedMetadata { public required string Blake3Hash; + public required string Path; + public required ScreenshotMetadata Metadata; +} + +[Serializable] +public class ScreenshotMetadata { public required Character? ActiveCharacter; public required string? Location; public required string? LocationSub; @@ -105,7 +111,6 @@ public class ScreenshotMetadata { .ToArray(); return new ScreenshotMetadata { - Blake3Hash = "", ActiveCharacter = active, Location = map?.PlaceName.Value?.Name.ToDalamudString().TextValue.WhitespaceToNull() ?? territory?.PlaceName.Value?.Name.ToDalamudString().TextValue.WhitespaceToNull(), diff --git a/Ui/PluginUi.cs b/Ui/PluginUi.cs index b20a14a..7ab197a 100644 --- a/Ui/PluginUi.cs +++ b/Ui/PluginUi.cs @@ -95,7 +95,15 @@ internal class PluginUi : IDisposable { ImGui.TextUnformatted("Filename format"); ImGui.SetNextItemWidth(-1); - anyChanged |= ImGui.InputTextMultiline("##filename-format", ref this.Plugin.Config.SaveFileNameFormat, 1024, new Vector2(-1, 150)); + ImGui.PushFont(UiBuilder.MonoFont); + using (new OnDispose(ImGui.PopFont)) { + anyChanged |= ImGui.InputTextMultiline( + "##filename-format", + ref this.Plugin.Config.SaveFileNameFormat, + 2048, + new Vector2(-1, 150) + ); + } var template = this.Plugin.Config.SaveFileNameTemplate; if (template.HasErrors) { @@ -142,14 +150,18 @@ internal class PluginUi : IDisposable { ImGui.SameLine(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Folder)) { - this.FileDialogManager.OpenFolderDialog("Choose screenshots folder", (b, s) => { - if (!b) { - return; - } + this.FileDialogManager.OpenFolderDialog( + "Choose screenshots folder", + (b, s) => { + if (!b) { + return; + } - this.Plugin.Config.SaveDirectory = s; - this.Plugin.SaveConfig(); - }); + this.Plugin.Config.SaveDirectory = s; + this.Plugin.SaveConfig(); + }, + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + ); } return changed; diff --git a/global.json b/global.json new file mode 100644 index 0000000..dad2db5 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/packages.lock.json b/packages.lock.json index 7a6f267..a554896 100644 --- a/packages.lock.json +++ b/packages.lock.json @@ -27,12 +27,33 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, + "Penumbra.Api": { + "type": "Direct", + "requested": "[1.0.14, )", + "resolved": "1.0.14", + "contentHash": "KqUuD4d30uhczvfH2YtcbwBxGnovBQAIudZ99eRxK0NeHwnQ3vdKe2BO2gZ2ZzT3JV6lu0NvosXXqqcQ0ZtqLw==" + }, "Scriban": { "type": "Direct", "requested": "[5.9.1, )", "resolved": "5.9.1", "contentHash": "Er0jZCXrHXtk+nnzmHVEnmz1pjfU+VL3GppO0UjtCMoZ0Se1plyPe1OLb6gM7ToSRA7nu/QIcdRFr29x8w8rQQ==" }, + "SQLite": { + "type": "Direct", + "requested": "[3.13.0, )", + "resolved": "3.13.0", + "contentHash": "MJfRiz2p6aMVOxrxGMdVzhpzI0oxTgZSwC8eVuOpV8L7yYaFUu8q/OFYwv9P0/aZ/pdEu24O6gma6wZJMTun9A==" + }, + "System.Data.SQLite.Core": { + "type": "Direct", + "requested": "[1.0.118, )", + "resolved": "1.0.118", + "contentHash": "2V1PsfBeqWlZxF/VtB8lQKPfDBayCU8zD5Xc3Mq7cILOa2ZqpPDSwMP0fTfk1gtGSStSk//DxKiGy6zwCQs7Uw==", + "dependencies": { + "Stub.System.Data.SQLite.Core.NetStandard": "[1.0.118]" + } + }, "System.Drawing.Common": { "type": "Direct", "requested": "[8.0.2, )", @@ -74,6 +95,11 @@ "Microsoft.Windows.SDK.Win32Metadata": "55.0.45-preview" } }, + "Stub.System.Data.SQLite.Core.NetStandard": { + "type": "Transitive", + "resolved": "1.0.118", + "contentHash": "4TS8IZvDj0ud6utxfXI6zv9Ditk4U9Kt9KqLyAIQGcU3GXp5oGBHgGZifq+APcqRCayuN/MSE8t9ZZmygtU28A==" + }, "System.Memory": { "type": "Transitive", "resolved": "4.5.5",