diff --git a/App.xaml b/App.xaml
index 2bf3cea..0c688a0 100644
--- a/App.xaml
+++ b/App.xaml
@@ -4,6 +4,10 @@
xmlns:local="clr-namespace:ytLive"
StartupUri="MainWindow.xaml">
-
+
+
+
+
+
diff --git a/App.xaml.cs b/App.xaml.cs
index 1ff0ff3..462beaa 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -1,13 +1,34 @@
-using System.Configuration;
-using System.Data;
-using System.Windows;
-
-namespace ytLive;
-
-///
-/// Interaction logic for App.xaml
-///
-public partial class App : Application
-{
-}
-
+using System.Windows;
+using System.Windows.Threading;
+using ytLive.Helpers;
+
+namespace ytLive;
+
+///
+/// Interaction logic for App.xaml
+///
+public partial class App : Application
+{
+ public App()
+ {
+ AppLog.Write("App ctor");
+ }
+
+ protected override void OnStartup(StartupEventArgs e)
+ {
+ // The app is a WinExe (no visible console) and is often debugged from
+ // WSL where it can't even run, so startup checkpoints and any unhandled
+ // exception are written to %APPDATA%\ytLlive\startup.log via AppLog.
+ AppDomain.CurrentDomain.UnhandledException += (_, args) =>
+ AppLog.Write(args.ExceptionObject is Exception ex
+ ? "AppDomain unhandled exception: " + ex
+ : "AppDomain unhandled exception object: " + args.ExceptionObject);
+
+ DispatcherUnhandledException += (_, args) =>
+ AppLog.Write(args.Exception, "Dispatcher unhandled exception");
+
+ AppLog.Write("App OnStartup begin");
+ base.OnStartup(e);
+ AppLog.Write("App OnStartup end");
+ }
+}
diff --git a/GoLiveWindow.xaml b/GoLiveWindow.xaml
index 5ec4d72..a662e23 100644
--- a/GoLiveWindow.xaml
+++ b/GoLiveWindow.xaml
@@ -7,58 +7,9 @@
ShowInTaskbar="False" Background="#1a1a2e">
-
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -77,17 +28,17 @@
-
-
+
-
-
+
-
-
+
@@ -95,9 +46,9 @@
+ Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
+ Style="{StaticResource YtButton}" MinWidth="110"/>
diff --git a/GoLiveWindow.xaml.cs b/GoLiveWindow.xaml.cs
index 8ae83c6..dba79cd 100644
--- a/GoLiveWindow.xaml.cs
+++ b/GoLiveWindow.xaml.cs
@@ -1,4 +1,5 @@
using System.Windows;
+using ytLive.Helpers;
using ytLive.ViewModels;
namespace ytLive;
@@ -7,7 +8,9 @@ public partial class GoLiveWindow : Window
{
public GoLiveWindow(GoLiveViewModel viewModel)
{
+ AppLog.Write("GoLiveWindow ctor: before InitializeComponent");
InitializeComponent();
+ AppLog.Write("GoLiveWindow ctor: after InitializeComponent");
DataContext = viewModel;
viewModel.StartRequested += () => DialogResult = true;
viewModel.CancelRequested += () => DialogResult = false;
diff --git a/Helpers/AppLog.cs b/Helpers/AppLog.cs
new file mode 100644
index 0000000..4bc7e0f
--- /dev/null
+++ b/Helpers/AppLog.cs
@@ -0,0 +1,40 @@
+using System.IO;
+
+namespace ytLive.Helpers;
+
+///
+/// Minimal file logger used to diagnose startup/shutdown crashes on Windows,
+/// where the WPF app has no visible console. Writes to
+/// %APPDATA%\ytLlive\startup.log, appending synchronously so entries survive
+/// a hard crash.
+///
+public static class AppLog
+{
+ private static readonly string LogPath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "ytLlive",
+ "startup.log");
+
+ private static readonly object Gate = new();
+
+ public static void Write(string message)
+ {
+ try
+ {
+ lock (Gate)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!);
+ File.AppendAllText(LogPath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {message}{Environment.NewLine}");
+ }
+ }
+ catch
+ {
+ // Never let logging itself crash the app.
+ }
+ }
+
+ public static void Write(Exception exception, string message)
+ {
+ Write($"{message}: {exception}");
+ }
+}
diff --git a/Helpers/FocusPreservingListBox.cs b/Helpers/FocusPreservingListBox.cs
new file mode 100644
index 0000000..695d368
--- /dev/null
+++ b/Helpers/FocusPreservingListBox.cs
@@ -0,0 +1,72 @@
+using System.Collections.Specialized;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Threading;
+
+namespace ytLive.Helpers;
+
+///
+/// A ListBox that keeps selection and keyboard focus coherent when the
+/// selected item is deleted. Focus lands on the item now at the deleted
+/// position (the previous item when the last one was removed) or leaves
+/// the box entirely when it becomes empty. Drag-to-reorder is untouched.
+///
+public class FocusPreservingListBox : ListBox
+{
+ ///
+ /// Predicate deciding which items the delete-focus fallback may land on
+ /// (e.g. skip hidden scenes). Null means every item is eligible.
+ ///
+ public Func