Resolution dropdown, list-focus preservation, unified dark theme, startup logging, and memory map
- Bottom-bar resolution dropdown (1080p60/1080p30/720p60/480p30) with tooltip on finding upload bandwidth; disabled while live; no in-app speed test - FocusPreservingListBox keeps selection/focus coherent when a selected scene/source is deleted (skip hidden scenes; leave list when empty) - Themes/Controls.xaml: single dark-theme dictionary merged once in App.xaml; custom ComboBox template fixes SelectionBoxItem rendering; GoLiveWindow and ReuseImageDialog consolidated onto shared styles - AppLog file logger + AppDomain/Dispatcher exception hooks; checkpointed MainWindow/VM/dialog constructors (caught MenuItemRole.Separator XAML crash) - Memory map: schema.md conventions, per-directory index.md, updated ai.md/ TASKS.md/README.md (OAuth creds real; token persistence still pending)
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
using System.IO;
|
||||
|
||||
namespace ytLive.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class FocusPreservingListBox : ListBox
|
||||
{
|
||||
/// <summary>
|
||||
/// Predicate deciding which items the delete-focus fallback may land on
|
||||
/// (e.g. skip hidden scenes). Null means every item is eligible.
|
||||
/// </summary>
|
||||
public Func<object, bool>? IsSelectable { get; set; }
|
||||
|
||||
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
base.OnItemsChanged(e);
|
||||
|
||||
if (e.Action != NotifyCollectionChangedAction.Remove) return;
|
||||
var hadFocus = IsKeyboardFocusWithin || IsKeyboardFocused;
|
||||
if (!hadFocus) return;
|
||||
if (Mouse.Captured == this) return;
|
||||
|
||||
if (Items.Count == 0)
|
||||
{
|
||||
SelectedItem = null;
|
||||
MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
|
||||
return;
|
||||
}
|
||||
|
||||
var targetIndex = FindSelectableIndex(Math.Clamp(e.OldStartingIndex, 0, Items.Count - 1));
|
||||
if (targetIndex < 0)
|
||||
{
|
||||
SelectedItem = null;
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedIndex = targetIndex;
|
||||
var item = Items[targetIndex];
|
||||
|
||||
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, () =>
|
||||
{
|
||||
ScrollIntoView(item);
|
||||
if (ItemContainerGenerator.ContainerFromItem(item) is ListBoxItem container)
|
||||
container.Focus();
|
||||
});
|
||||
}
|
||||
|
||||
private int FindSelectableIndex(int start)
|
||||
{
|
||||
for (var i = start; i >= 0; i--)
|
||||
if (IsItemSelectable(i)) return i;
|
||||
for (var i = start + 1; i < Items.Count; i++)
|
||||
if (IsItemSelectable(i)) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private bool IsItemSelectable(int index)
|
||||
{
|
||||
if (index < 0 || index >= Items.Count) return false;
|
||||
var item = Items[index];
|
||||
return item != null && (IsSelectable?.Invoke(item) ?? true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Helpers — index
|
||||
|
||||
Cross-cutting utilities. See [`schema.md`](../schema.md) for the memory-map conventions.
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `ViewModelBase.cs` | `INotifyPropertyChanged` + `SetProperty<T>()` — base for all ViewModels |
|
||||
| `RelayCommand.cs` | `ICommand` implementation for all button actions |
|
||||
| `AppLog.cs` | File logger to `%APPDATA%\ytLlive\startup.log`; startup checkpoints + unhandled-exception capture (the crash this map is named for) |
|
||||
| `FocusPreservingListBox.cs` | `ListBox` subclass that keeps selection/focus coherent when the selected item is deleted; `IsSelectable` predicate skips hidden scenes; drag-reorder guard |
|
||||
| `OAuthCredentials.cs` | Baked-in Google OAuth client ID/secret (desktop app; loopback callback) |
|
||||
| `ImageCache.cs` | Image byte caching (assets live in the DB) |
|
||||
| `InverseBoolToVisibilityConverter.cs` / `NotNullToVisibilityConverter.cs` | XAML value converters for visibility bindings |
|
||||
|
||||
Related: [`Themes/Controls.xaml`](../Themes/Controls.xaml) styles the lists this
|
||||
class backs; [`Models/index.md`](../Models/index.md) and
|
||||
[`ViewModels/index.md`](../ViewModels/index.md) for the objects it works on.
|
||||
Reference in New Issue
Block a user