diff --git a/Assets/llamacasty text.jpg b/Assets/llamacasty text.jpg new file mode 100644 index 0000000..3e2b1ac Binary files /dev/null and b/Assets/llamacasty text.jpg differ diff --git a/Assets/llamacasty text.png b/Assets/llamacasty text.png new file mode 100644 index 0000000..b96b047 Binary files /dev/null and b/Assets/llamacasty text.png differ diff --git a/Assets/llamacasty-text-nobg-2.png b/Assets/llamacasty-text-nobg-2.png new file mode 100644 index 0000000..cf1f677 Binary files /dev/null and b/Assets/llamacasty-text-nobg-2.png differ diff --git a/Assets/llamacasty-text-nobg.png b/Assets/llamacasty-text-nobg.png new file mode 100644 index 0000000..97290c7 Binary files /dev/null and b/Assets/llamacasty-text-nobg.png differ diff --git a/Assets/thin logo wordmark.png b/Assets/thin logo wordmark.png new file mode 100644 index 0000000..0e789ee Binary files /dev/null and b/Assets/thin logo wordmark.png differ diff --git a/Controls/LeftPanel.xaml b/Controls/LeftPanel.xaml index b8c1312..59ef205 100644 --- a/Controls/LeftPanel.xaml +++ b/Controls/LeftPanel.xaml @@ -596,8 +596,18 @@ HorizontalAlignment="Center" TextAlignment="Center" MaxWidth="240"/> + + + + + + + + - : ICommand +{ + private readonly Action _execute; + private readonly Predicate? _canExecute; + + public RelayCommand(Action execute, Predicate? canExecute = null) + { + _execute = execute; + _canExecute = canExecute; + } + + public event EventHandler? CanExecuteChanged + { + add => CommandManager.RequerySuggested += value; + remove => CommandManager.RequerySuggested -= value; + } + + public bool CanExecute(object? parameter) + { + if (parameter is T t) return _canExecute?.Invoke(t) ?? true; + return _canExecute?.Invoke(default) ?? true; + } + + public void Execute(object? parameter) + { + if (parameter is T t) _execute(t); + else _execute(default); + } +} diff --git a/Models/YouTube.cs b/Models/YouTube.cs index da7c6ea..5a32b0e 100644 --- a/Models/YouTube.cs +++ b/Models/YouTube.cs @@ -70,4 +70,5 @@ public class ChatMessage /// subscriber events locally (YouTube's insert API only supports text), so the /// buffer can label them honestly — simulated events never reach YouTube. public bool IsSimulated { get; set; } + public bool IsPinned { get; set; } } diff --git a/ViewModels/MainViewModel.Chat.cs b/ViewModels/MainViewModel.Chat.cs index 496671d..db806a9 100644 --- a/ViewModels/MainViewModel.Chat.cs +++ b/ViewModels/MainViewModel.Chat.cs @@ -22,6 +22,16 @@ public partial class MainViewModel : ViewModelBase /// left panel can subscribe to its changes. public ObservableCollection ChatMessages => _chatLayer.Messages; + private ChatMessage? _pinnedChatMessage; + public ChatMessage? PinnedChatMessage + { + get => _pinnedChatMessage; + set => SetProperty(ref _pinnedChatMessage, value); + } + + public RelayCommand PinChatMessageCommand => new((m) => { PinnedChatMessage = m; }); + public RelayCommand ClearPinnedChatMessageCommand => new(_ => { PinnedChatMessage = null; }); + /// The Add → YouTube Chat menu item: enabled only while no chat layer exists /// anywhere — one chat overlay per layout. public bool CanAddYouTubeChat