7f14ffb11e
The creator confirmed the clip fix ("the video plays now"), then asked where the
scrolling text was — it was invisible, for a structural reason. AlertTickerFrame
existed only as a frame-pump callback blitted into the OUTPUT; PreviewPane.xaml had
no element for it, because the strip is master-width and global, not a Source, so it
cannot ride a per-element Image. Nothing was wrong in the renderer: there was no
consumer in the preview. Same class of defect as the missing IsAlertBox trigger, one
layer up (MyMistakes RULE 5/6).
- tickerPreviewSink on AlertOverlayLayer, published from RefreshAlertPreviews() so it
is always the UI thread; MainViewModel.AlertTicker writes it into one reused
WriteableBitmap bound to a new global AlertTickerElement, mirroring SocialBarElement.
- Source.AlertDisplayMethod + panel "Display" selector: TickerScroll / Flash / Solid.
Flash pulses 0.5s on / 0.5s off for the whole alert; Solid is centred and still.
- The marquee was also unreadable: a fixed 140px/s took ~17s per pass, so a 10s alert
showed the text once, entering from the right and never crossing. Paced in reads per
alert instead (TickerReadsPerAlert = 3 inside the alert's own length, speed derived
from it) — never px/s. Research (websearch: how do OBS/Streamlabs/StreamElements
alert boxes present announcement timing?) settled the unit: Streamlabs exposes "Alert
Duration: choose how long your alert stays on your stream" and "Text Delay", never a
scroll-speed slider (https://support.streamlabs.com/hc/en-us/articles/52499995174299-Setting-up-Your-Streamlabs-Alerts).
Run is phase-started half a frame in so the first frame isn't blank.
- Persistence: AlertDisplayMethod INTEGER NOT NULL DEFAULT 0 via the idempotent
table_info migration, appended LAST in the SELECT because the Source reader is
positional (GetInt32(32..34)) — a mid-list insert would silently shift a neighbour.
Tests: 15 new facts (suite 339/339). RealApp STA host: the pane draws the strip and
collapses at alert end; the layer publishes a real 1920x48 frame for all three methods
and nothing when the ticker is off; three passes counted in 10s by the pill's leading
edge resetting (a seamless marquee never blanks, so an empty frame cannot count a pass);
Solid byte-identical at every moment; Flash on for half of each second; the panel shows
and writes back the choice; the DB round-trips all three alert fields together.
Incidental finding: a bound ItemsSource ComboBox in LeftPanel.xaml broke
LayerReorderPersistenceTests.RealMouseDrag (that test injects PHYSICAL mouse input, so a
load-time re-measure moves the rows out from under the cursor). Rewritten as inline
ComboBoxItems, the shape the chat Font selector already uses in that panel. Recorded as
MyMistakes RULE (8).
553 lines
22 KiB
C#
553 lines
22 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
|
|
);
|
|
""",
|
|
// 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();
|
|
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("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();
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|