In the Socials drawer, under the show/hide switch: text colour, an optional background plate with its own colour and opacity, and a text drop shadow. The bar had been hard-wired white-on-transparent since it shipped. - `SocialsConfig` gains five additive properties (BarTextColor, BarBackgroundEnabled, BarBackgroundColor, BarBackgroundOpacity, BarTextShadow). - Persistence: five new Socials columns, added by the per-column idempotent migration pattern (a half-migrated DB heals instead of failing), written in Save and read back in Load with defaults for pre-existing rows. - `SocialBarRenderer.Render(entries, SocialBarStyle?)` — the look is a separate value type so the renderer stays a pure "entries + look" function and the preview can never read a half-applied config mid-edit. A malformed hex falls back to white/black instead of throwing inside a render tick. - The plate hugs the entries (rounded 20px, padding) so it reads as a caption bar, not a band across the frame. Off by default, because transparent is what creators have today. - The colour pickers reuse `BorderColorPalette` — one catalogue of the app's colours, per the TASK 60/63 rule. Guard: SocialBarAppearanceTests paints the real strip and inspects pixels — default is white with no plate, a red text colour reaches the glyphs, a 55% blue plate yields half-transparent blue pixels, and the shadow adds dark pixels that were not there before.
633 lines
26 KiB
C#
633 lines
26 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',
|
|
BackgroundEffect TEXT NOT NULL DEFAULT 'None',
|
|
BackgroundStrength REAL NOT NULL DEFAULT 0.5,
|
|
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
|
|
);
|
|
""",
|
|
// v9 → v10 (TASK 39): YPP journey-tracker snapshots, appended on every
|
|
// stats refresh. The history is the raw data a later analytics slice
|
|
// reads to compute velocity/(de)acceleration/ETA.
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS YppSnapshot (
|
|
CapturedAtUtc TEXT NOT NULL PRIMARY KEY,
|
|
SubscriberCount INTEGER NOT NULL,
|
|
VideoCount INTEGER NOT NULL,
|
|
TotalViews INTEGER NOT NULL,
|
|
UploadsLast90Days INTEGER NOT NULL,
|
|
CommunityGuidelinesGoodStanding INTEGER NOT NULL,
|
|
CopyrightStrikesGoodStanding INTEGER NOT NULL,
|
|
ContentIdClaimsGoodStanding INTEGER NOT NULL,
|
|
OverallGoodStanding INTEGER NOT NULL
|
|
);
|
|
""",
|
|
};
|
|
foreach (var sql in statements)
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = sql;
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
MigrateSourceTable();
|
|
MigrateWebcamConfigTable();
|
|
MigrateSceneTable();
|
|
MigrateSceneSocialBarColumn();
|
|
MigrateSceneThumbnailColumn();
|
|
MigrateSocialsTable();
|
|
MigrateSocialEntryTable();
|
|
if (GetUserVersion() < 3)
|
|
MigrateToV3();
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "PRAGMA user_version = 10;";
|
|
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("AlertDisplayMethod"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN AlertDisplayMethod INTEGER NOT NULL DEFAULT 0;";
|
|
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("IsBrandLogo"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN IsBrandLogo 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();
|
|
}
|
|
|
|
// TASK 47: alert video config. The default clip asset id + the creator's
|
|
// replacement path (a FILE reference, never a stored BLOB) + the pill,
|
|
// ticker toggle and volume live on the row.
|
|
if (!columns.Contains("AlertVideoAssetId"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN AlertVideoAssetId TEXT;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("AlertVideoPath"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN AlertVideoPath TEXT;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("AlertUseDefaultVideo"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN AlertUseDefaultVideo INTEGER NOT NULL DEFAULT 1;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("AlertShowTicker"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN AlertShowTicker INTEGER NOT NULL DEFAULT 1;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
if (!columns.Contains("AlertVideoVolume"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN AlertVideoVolume REAL NOT NULL DEFAULT 1;";
|
|
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();
|
|
}
|
|
|
|
// TASK 51: per-webcam background effect + strength (additive, no version bump).
|
|
if (!columns.Contains("BackgroundEffect"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE WebcamSceneConfig ADD COLUMN BackgroundEffect TEXT NOT NULL DEFAULT 'None';";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (!columns.Contains("BackgroundStrength"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE WebcamSceneConfig ADD COLUMN BackgroundStrength REAL NOT NULL DEFAULT 0.5;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
// TASK 57 (the "vv" feature): the vertical (9:16) tier keeps its OWN
|
|
// camera placement per scene — position, size, shape and the pre-Round
|
|
// rect. Additive, no version bump: a NULL column means "this scene has
|
|
// no vertical placement yet" (the scene keeps its 16:9 placement).
|
|
AddWebcamConfigColumnIfMissing(columns, "VerticalX", "REAL");
|
|
AddWebcamConfigColumnIfMissing(columns, "VerticalY", "REAL");
|
|
AddWebcamConfigColumnIfMissing(columns, "VerticalWidth", "REAL");
|
|
AddWebcamConfigColumnIfMissing(columns, "VerticalHeight", "REAL");
|
|
AddWebcamConfigColumnIfMissing(columns, "VerticalClipShape", "TEXT");
|
|
AddWebcamConfigColumnIfMissing(columns, "VerticalRectWidth", "REAL");
|
|
AddWebcamConfigColumnIfMissing(columns, "VerticalRectHeight", "REAL");
|
|
}
|
|
|
|
// One guarded ADD COLUMN for the webcam config's vertical-placement columns.
|
|
private void AddWebcamConfigColumnIfMissing(HashSet<string> columns, string name, string type)
|
|
{
|
|
if (columns.Contains(name)) return;
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = $"ALTER TABLE WebcamSceneConfig ADD COLUMN {name} {type};";
|
|
cmd.ExecuteNonQuery();
|
|
columns.Add(name);
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
// 2026-10-10: Scene gains ThumbnailAssetId (the top-strip tile artwork, stored
|
|
// as an Asset row like every image). Additive, no version bump — old DBs just
|
|
// get a NULL column and the strip falls back to the scene's background art.
|
|
private void MigrateSceneThumbnailColumn()
|
|
{
|
|
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("ThumbnailAssetId")) return;
|
|
|
|
using var alter = _connection.CreateCommand();
|
|
alter.CommandText = "ALTER TABLE Scene ADD COLUMN ThumbnailAssetId TEXT;";
|
|
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));
|
|
}
|
|
|
|
// Each column is added on its own, guarded by presence — the same idempotent pattern the
|
|
// webcam table uses, so a half-migrated DB heals instead of failing.
|
|
AddColumnIfMissing("BarEnabled", "INTEGER NOT NULL DEFAULT 1");
|
|
AddColumnIfMissing("TextColor", "TEXT NOT NULL DEFAULT '#FFFFFF'");
|
|
AddColumnIfMissing("BackgroundEnabled", "INTEGER NOT NULL DEFAULT 0");
|
|
AddColumnIfMissing("BackgroundColor", "TEXT NOT NULL DEFAULT '#000000'");
|
|
AddColumnIfMissing("BackgroundOpacity", "REAL NOT NULL DEFAULT 0.55");
|
|
AddColumnIfMissing("TextShadow", "INTEGER NOT NULL DEFAULT 0");
|
|
|
|
void AddColumnIfMissing(string name, string spec)
|
|
{
|
|
if (columns.Contains(name)) return;
|
|
using var alter = _connection.CreateCommand();
|
|
alter.CommandText = $"ALTER TABLE Socials ADD COLUMN {name} {spec};";
|
|
alter.ExecuteNonQuery();
|
|
columns.Add(name);
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|