dcb3637cca
LayoutStore.cs 1402 -> 47-line shell + 5 partials, grouped by functionality (line count is a consequence, not the goal): - LayoutStore.Migrations.cs: all DB schema migration (EnsureSchema, GetUserVersion, MigrateSourceTable/WebcamConfigTable/ToV3/SceneTable/SceneSocialBarColumn/ SocialsTable/SocialEntryTable) - LayoutStore.Load.cs: Load() scene/source read-back - LayoutStore.Save.cs: Save() full scene tree write - LayoutStore.Settings.cs: key/value settings + mic/record-folder/reusable-stream + license/transition/broadcast/hotkey/window-state GetSetting/UpsertSetting - LayoutStore.Assets.cs: GetAssetBytes/UpsertAsset blob IO - LayoutStore.cs: shell — Instance, fields, ctor, Dispose Converted to public partial class LayoutStore : IDisposable. Pruned per-file dead usings. Phase 3 DOWN: no production .cs exceeds 500 lines. Zero behavior change; build 0 warnings; 246 pass, only the 2 known failures. Docs: ViewModels/index.md Phase 3 tracker updated in same commit.
496 lines
19 KiB
C#
496 lines
19 KiB
C#
using Microsoft.Data.Sqlite;
|
|
using ytLive.Models;
|
|
|
|
namespace ytLive.Services;
|
|
|
|
public partial class LayoutStore : IDisposable
|
|
{
|
|
private void EnsureSchema()
|
|
{
|
|
string[] statements =
|
|
{
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS Scene (
|
|
Id TEXT PRIMARY KEY,
|
|
Name TEXT NOT NULL,
|
|
IsHidden INTEGER NOT NULL DEFAULT 0,
|
|
IsChatScene INTEGER NOT NULL DEFAULT 0,
|
|
HasBackground INTEGER NOT NULL DEFAULT 1,
|
|
SortOrder INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
""",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS Asset (
|
|
Id TEXT PRIMARY KEY,
|
|
Hash TEXT NOT NULL UNIQUE,
|
|
Data BLOB NOT NULL,
|
|
PixelWidth INTEGER NOT NULL DEFAULT 0,
|
|
PixelHeight INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
""",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS Source (
|
|
Id TEXT PRIMARY KEY,
|
|
SceneId TEXT NOT NULL REFERENCES Scene(Id) ON DELETE CASCADE,
|
|
AssetId TEXT REFERENCES Asset(Id),
|
|
Type TEXT NOT NULL,
|
|
Name TEXT NOT NULL,
|
|
IsEnabled INTEGER NOT NULL DEFAULT 1,
|
|
X REAL NOT NULL DEFAULT 0,
|
|
Y REAL NOT NULL DEFAULT 0,
|
|
Width REAL NOT NULL DEFAULT 0,
|
|
Height REAL NOT NULL DEFAULT 0,
|
|
Opacity REAL NOT NULL DEFAULT 1,
|
|
MonitorIndex INTEGER,
|
|
DeviceId TEXT,
|
|
ClipShape TEXT NOT NULL DEFAULT 'Traditional',
|
|
IsMirrored INTEGER NOT NULL DEFAULT 0,
|
|
IsBackground INTEGER NOT NULL DEFAULT 0,
|
|
CaptureKey TEXT,
|
|
SortOrder INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
""",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS Webcam (
|
|
Id TEXT PRIMARY KEY,
|
|
DeviceId TEXT NOT NULL UNIQUE,
|
|
Name TEXT NOT NULL
|
|
);
|
|
""",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS WebcamSceneConfig (
|
|
SceneId TEXT NOT NULL REFERENCES Scene(Id) ON DELETE CASCADE,
|
|
WebcamId TEXT NOT NULL REFERENCES Webcam(Id) ON DELETE CASCADE,
|
|
IsVisible INTEGER NOT NULL DEFAULT 1,
|
|
X REAL NOT NULL DEFAULT 0,
|
|
Y REAL NOT NULL DEFAULT 0,
|
|
Width REAL NOT NULL DEFAULT 0,
|
|
Height REAL NOT NULL DEFAULT 0,
|
|
Opacity REAL NOT NULL DEFAULT 1,
|
|
ClipShape TEXT NOT NULL DEFAULT 'Traditional',
|
|
IsMirrored INTEGER NOT NULL DEFAULT 0,
|
|
RectWidth REAL,
|
|
RectHeight REAL,
|
|
BorderColor TEXT,
|
|
BorderOpacity REAL NOT NULL DEFAULT 1,
|
|
BorderWidth INTEGER NOT NULL DEFAULT 0,
|
|
BorderAnimation TEXT NOT NULL DEFAULT 'None',
|
|
SortOrder INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (SceneId, WebcamId)
|
|
);
|
|
""",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS Socials (
|
|
Id TEXT PRIMARY KEY,
|
|
BarPosition TEXT NOT NULL DEFAULT 'Bottom',
|
|
BarJustify TEXT NOT NULL DEFAULT 'Center'
|
|
);
|
|
""",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS SocialEntry (
|
|
Id TEXT PRIMARY KEY,
|
|
SocialsId TEXT NOT NULL REFERENCES Socials(Id) ON DELETE CASCADE,
|
|
Service TEXT NOT NULL,
|
|
Handle TEXT NOT NULL,
|
|
ProfileUrl TEXT NOT NULL,
|
|
SortOrder INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
""",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS Music (
|
|
Id TEXT PRIMARY KEY,
|
|
TrackPath TEXT NOT NULL,
|
|
IsEnabled INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
""",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS Settings (
|
|
Key TEXT PRIMARY KEY,
|
|
Value TEXT NOT NULL
|
|
);
|
|
""",
|
|
};
|
|
foreach (var sql in statements)
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = sql;
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
MigrateSourceTable();
|
|
MigrateWebcamConfigTable();
|
|
MigrateSceneTable();
|
|
MigrateSceneSocialBarColumn();
|
|
MigrateSocialsTable();
|
|
MigrateSocialEntryTable();
|
|
if (GetUserVersion() < 3)
|
|
MigrateToV3();
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "PRAGMA user_version = 9;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
|
|
|
|
// v1 → v2: Source gains ClipShape + IsMirrored (webcam clip/mirror prefs).
|
|
// CREATE TABLE IF NOT EXISTS doesn't touch existing tables, so pre-v2 DBs
|
|
// get the columns here instead.
|
|
private void MigrateSourceTable()
|
|
{
|
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "PRAGMA table_info(Source);";
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
columns.Add(reader.GetString(1));
|
|
}
|
|
|
|
if (!columns.Contains("ClipShape"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ClipShape TEXT NOT NULL DEFAULT 'Traditional';";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (!columns.Contains("IsMirrored"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN IsMirrored INTEGER NOT NULL DEFAULT 0;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (columns.Contains("IsBackdrop"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source RENAME COLUMN IsBackdrop TO IsBackground;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
else if (!columns.Contains("IsBackground"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN IsBackground INTEGER NOT NULL DEFAULT 0;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (!columns.Contains("CaptureKey"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN CaptureKey TEXT;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
// TASK 3.18: ChatBox overlay source
|
|
if (!columns.Contains("ChatFontSize"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatFontSize REAL NOT NULL DEFAULT 16;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("ChatFontColor"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatFontColor TEXT NOT NULL DEFAULT '#FFFFFF';";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("ChatBackgroundColor"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatBackgroundColor TEXT NOT NULL DEFAULT '#80000000';";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("ChatMaxMessages"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatMaxMessages INTEGER NOT NULL DEFAULT 5;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("ChatShowTimestamps"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatShowTimestamps INTEGER NOT NULL DEFAULT 1;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("ChatShowBadges"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatShowBadges INTEGER NOT NULL DEFAULT 1;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("ChatMemberColor"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatMemberColor TEXT NOT NULL DEFAULT '#e94560';";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("ChatSuperChatColor"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatSuperChatColor TEXT NOT NULL DEFAULT '#eab308';";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("ShowDesktop"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ShowDesktop INTEGER NOT NULL DEFAULT 0;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (!columns.Contains("WebUri"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN WebUri TEXT;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
// TASK 21: Media source (video/audio file playback)
|
|
if (!columns.Contains("MediaPath"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN MediaPath TEXT;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("MediaIsLooping"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN MediaIsLooping INTEGER NOT NULL DEFAULT 0;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("MediaVolume"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN MediaVolume REAL NOT NULL DEFAULT 1;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("MediaPlaybackState"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN MediaPlaybackState TEXT NOT NULL DEFAULT 'Stopped';";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
// Rename legacy "Backdrop" display name to "Background" in existing rows
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "UPDATE Source SET Name = 'Background' WHERE Name = 'Backdrop';";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
|
|
// v3 → v4: WebcamSceneConfig gains RectWidth/RectHeight (the pre-Round rect,
|
|
// so a reloaded Round webcam restores its aspect on toggle-back).
|
|
private void MigrateWebcamConfigTable()
|
|
{
|
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "PRAGMA table_info(WebcamSceneConfig);";
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
columns.Add(reader.GetString(1));
|
|
}
|
|
|
|
if (!columns.Contains("RectWidth"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE WebcamSceneConfig ADD COLUMN RectWidth REAL;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (!columns.Contains("RectHeight"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE WebcamSceneConfig ADD COLUMN RectHeight REAL;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
|
|
// v2 → v3: the webcam leaves the Source table. Any webcam Source rows become
|
|
// one Webcam identity (first row — one camera app-wide) + a WebcamSceneConfig
|
|
// per scene that had one, then the webcam rows are deleted. No backfill:
|
|
// scenes without the webcam stay webcam-free.
|
|
private void MigrateToV3()
|
|
{
|
|
var rows = new List<(string Id, string SceneId, string Name, double X, double Y, double W, double H, double Opacity, string DeviceId, string ClipShape, bool IsMirrored)>();
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = """
|
|
SELECT Id, SceneId, Name, X, Y, Width, Height, Opacity, DeviceId, ClipShape, IsMirrored
|
|
FROM Source WHERE Type = 'Webcam' ORDER BY SortOrder;
|
|
""";
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
{
|
|
rows.Add((
|
|
reader.GetString(0),
|
|
reader.GetString(1),
|
|
reader.GetString(2),
|
|
reader.GetDouble(3),
|
|
reader.GetDouble(4),
|
|
reader.GetDouble(5),
|
|
reader.GetDouble(6),
|
|
reader.GetDouble(7),
|
|
reader.IsDBNull(8) ? string.Empty : reader.GetString(8),
|
|
reader.GetString(9),
|
|
reader.GetInt32(10) != 0));
|
|
}
|
|
}
|
|
if (rows.Count == 0) return;
|
|
|
|
using var tx = _connection.BeginTransaction();
|
|
var webcamId = Guid.NewGuid().ToString();
|
|
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "INSERT INTO Webcam (Id, DeviceId, Name) VALUES ($id, $device, $name);";
|
|
cmd.Transaction = tx;
|
|
cmd.Parameters.AddWithValue("$id", webcamId);
|
|
cmd.Parameters.AddWithValue("$device", rows[0].DeviceId);
|
|
cmd.Parameters.AddWithValue("$name", rows[0].Name);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
foreach (var row in rows)
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = """
|
|
INSERT INTO WebcamSceneConfig
|
|
(SceneId, WebcamId, IsVisible, X, Y, Width, Height, Opacity,
|
|
ClipShape, IsMirrored, BorderColor, BorderOpacity, BorderWidth, BorderAnimation, SortOrder)
|
|
VALUES ($sceneId, $webcamId, 1, $x, $y, $w, $h, $opacity,
|
|
$clip, $mirrored, NULL, 1, 0, 'None', 0);
|
|
""";
|
|
cmd.Transaction = tx;
|
|
cmd.Parameters.AddWithValue("$sceneId", row.SceneId);
|
|
cmd.Parameters.AddWithValue("$webcamId", webcamId);
|
|
cmd.Parameters.AddWithValue("$x", row.X);
|
|
cmd.Parameters.AddWithValue("$y", row.Y);
|
|
cmd.Parameters.AddWithValue("$w", row.W);
|
|
cmd.Parameters.AddWithValue("$h", row.H);
|
|
cmd.Parameters.AddWithValue("$opacity", row.Opacity);
|
|
cmd.Parameters.AddWithValue("$clip", row.ClipShape);
|
|
cmd.Parameters.AddWithValue("$mirrored", row.IsMirrored ? 1 : 0);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "DELETE FROM Source WHERE Type = 'Webcam';";
|
|
cmd.Transaction = tx;
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
tx.Commit();
|
|
}
|
|
|
|
// v5 → v6: Scene gains HasBackground. Added columns default to true; the
|
|
// ONE-TIME backfill (runs only when the column is first added) turns every
|
|
// non-Live canonical scene off and drops their background sources — the fix for
|
|
// DBs saved before the Live-only policy. MainViewModel.NormalizeBackgrounds
|
|
// re-runs the same normalization on every load for DBs that miss the backfill.
|
|
private void MigrateSceneTable()
|
|
{
|
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "PRAGMA table_info(Scene);";
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
columns.Add(reader.GetString(1));
|
|
}
|
|
|
|
if (columns.Contains("HasBackdrop"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Scene RENAME COLUMN HasBackdrop TO HasBackground;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
else if (columns.Contains("HasBackground"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!columns.Contains("HasBackground"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Scene ADD COLUMN HasBackground INTEGER NOT NULL DEFAULT 1;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
const string noBackground = "('starting', 'brb', 'chat', 'ending')";
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = $"UPDATE Scene SET HasBackground = 0 WHERE LOWER(TRIM(Name)) IN {noBackground};";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = $"""
|
|
DELETE FROM Source
|
|
WHERE IsBackground = 1 AND SceneId IN
|
|
(SELECT Id FROM Scene WHERE LOWER(TRIM(Name)) IN {noBackground});
|
|
""";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
|
|
// v6 → v7: Scene gains HasSocialBar (the per-scene social-bar toggle).
|
|
// Defaults to 0 (off) — the creator adds it explicitly via the footer [+].
|
|
private void MigrateSceneSocialBarColumn()
|
|
{
|
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "PRAGMA table_info(Scene);";
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
columns.Add(reader.GetString(1));
|
|
}
|
|
|
|
if (columns.Contains("HasSocialBar")) return;
|
|
|
|
using var alter = _connection.CreateCommand();
|
|
alter.CommandText = "ALTER TABLE Scene ADD COLUMN HasSocialBar INTEGER NOT NULL DEFAULT 0;";
|
|
alter.ExecuteNonQuery();
|
|
}
|
|
|
|
// v7 → v8: Socials gains BarEnabled (the dialog's show/hide switch). The old
|
|
// BarJustify column stays for back-compat but is no longer read or written.
|
|
private void MigrateSocialsTable()
|
|
{
|
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "PRAGMA table_info(Socials);";
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
columns.Add(reader.GetString(1));
|
|
}
|
|
|
|
if (columns.Contains("BarEnabled")) return;
|
|
|
|
using var alter = _connection.CreateCommand();
|
|
alter.CommandText = "ALTER TABLE Socials ADD COLUMN BarEnabled INTEGER NOT NULL DEFAULT 1;";
|
|
alter.ExecuteNonQuery();
|
|
}
|
|
|
|
// v8 → v9: SocialEntry gains Software (the fediverse instance's nodeinfo
|
|
// software name) so a reloaded entry still shows the right service logo.
|
|
private void MigrateSocialEntryTable()
|
|
{
|
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "PRAGMA table_info(SocialEntry);";
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
columns.Add(reader.GetString(1));
|
|
}
|
|
|
|
if (columns.Contains("Software")) return;
|
|
|
|
using var alter = _connection.CreateCommand();
|
|
alter.CommandText = "ALTER TABLE SocialEntry ADD COLUMN Software TEXT;";
|
|
alter.ExecuteNonQuery();
|
|
}
|
|
}
|