Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6944db8ab4 | |||
| cb54637e9f | |||
| 1f4624c636 | |||
| 544208e002 |
@@ -0,0 +1,172 @@
|
|||||||
|
<UserControl x:Class="ytLive.Controls.OverlayHost"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:Helpers="clr-namespace:ytLive.Helpers">
|
||||||
|
<Grid Grid.RowSpan="3"
|
||||||
|
Visibility="{Binding IsAnyOverlayOpen, Converter={StaticResource BoolToVis}}">
|
||||||
|
<Button Background="#99000000" BorderThickness="0" Cursor="Hand"
|
||||||
|
Command="{Binding CloseOverlayCommand}">
|
||||||
|
<Button.Template>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Button.Template>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Border Background="#0A0C1A" CornerRadius="8" Width="650" MaxHeight="700"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||||
|
Padding="28" BorderBrush="#333" BorderThickness="1">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<Grid Grid.Row="0">
|
||||||
|
<TextBlock Text="{Binding OverlayTitle}" FontSize="18" FontWeight="Bold"
|
||||||
|
Foreground="#e0e0e0" VerticalAlignment="Center"/>
|
||||||
|
<Button HorizontalAlignment="Right" ToolTip="Close" Style="{StaticResource IconButton}"
|
||||||
|
Command="{Binding CloseOverlayCommand}">
|
||||||
|
<Path Data="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"
|
||||||
|
Fill="#d0d0d0" Width="14" Height="14" Stretch="Uniform"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- App Settings: broadcast fields live in the preview's
|
||||||
|
Text pull-out now — this panel keeps app-level prefs only.
|
||||||
|
(Currently empty; the overlay shell stays for future use.) -->
|
||||||
|
<StackPanel Grid.Row="1" Margin="0,16,0,0"
|
||||||
|
Visibility="{Binding IsSettingsOpen, Converter={StaticResource BoolToVis}}">
|
||||||
|
<TextBlock Text="Default Location for Recordings" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
||||||
|
<TextBox Style="{StaticResource YtTextBox}"
|
||||||
|
Text="{Binding RecordFolderDisplay, Mode=OneWay}"
|
||||||
|
IsReadOnly="True" Margin="0,0,0,12" Foreground="#a0a0b0"/>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="Browse…" Command="{Binding ChooseRecordFolderCommand}"
|
||||||
|
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
|
||||||
|
<Button Content="Use Downloads" Command="{Binding ResetRecordFolderCommand}"
|
||||||
|
Style="{StaticResource YtButtonSecondary}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Report Bug -->
|
||||||
|
<StackPanel Grid.Row="1" Margin="0,16,0,0"
|
||||||
|
Visibility="{Binding IsBugOpen, Converter={StaticResource BoolToVis}}">
|
||||||
|
<TextBlock Text="What went wrong?" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
||||||
|
<TextBox Style="{StaticResource YtTextBox}" Height="110" AcceptsReturn="True" TextWrapping="Wrap"
|
||||||
|
Margin="0,0,0,12"
|
||||||
|
Text="{Binding BugReportText, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
VerticalScrollBarVisibility="Auto"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Email (optional)" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
||||||
|
<TextBox Style="{StaticResource YtTextBox}" Margin="0,0,0,12"
|
||||||
|
Text="{Binding BugReportEmail, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Opens a pre-filled email to the LlamaCasty team." Style="{StaticResource YtLabel}"/>
|
||||||
|
<Button Content="Send Report" Command="{Binding SubmitBugCommand}"
|
||||||
|
Style="{StaticResource YtButton}" HorizontalAlignment="Right" Margin="0,16,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Feature Request -->
|
||||||
|
<StackPanel Grid.Row="1" Margin="0,16,0,0"
|
||||||
|
Visibility="{Binding IsFeatureOpen, Converter={StaticResource BoolToVis}}">
|
||||||
|
<TextBlock Text="What would you like to see?" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
||||||
|
<TextBox Style="{StaticResource YtTextBox}" Height="110" AcceptsReturn="True" TextWrapping="Wrap"
|
||||||
|
Margin="0,0,0,12"
|
||||||
|
Text="{Binding FeatureRequestText, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
VerticalScrollBarVisibility="Auto"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Email (optional)" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
||||||
|
<TextBox Style="{StaticResource YtTextBox}" Margin="0,0,0,12"
|
||||||
|
Text="{Binding FeatureRequestEmail, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Opens a pre-filled email to the LlamaCasty team." Style="{StaticResource YtLabel}"/>
|
||||||
|
<Button Content="Send Request" Command="{Binding SubmitFeatureCommand}"
|
||||||
|
Style="{StaticResource YtButton}" HorizontalAlignment="Right" Margin="0,16,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- About -->
|
||||||
|
<Grid Grid.Row="1" Margin="0,16,0,0"
|
||||||
|
Background="#171928"
|
||||||
|
Visibility="{Binding IsAboutOpen, Converter={StaticResource BoolToVis}}">
|
||||||
|
|
||||||
|
<!-- Hub view -->
|
||||||
|
<StackPanel Visibility="{Binding IsHubVisible, Converter={StaticResource BoolToVis}}">
|
||||||
|
<Image Source="/ytLive;component/Assets/llamacasty-logo.jpg" Height="125" Stretch="Uniform"
|
||||||
|
HorizontalAlignment="Center" Margin="0,0,0,12"/>
|
||||||
|
<TextBlock Text="{Binding AppVersionLabel}" Foreground="#a0a0b0" FontSize="12"
|
||||||
|
HorizontalAlignment="Center" Margin="0,4,0,0"/>
|
||||||
|
<TextBlock Text="Live streaming for YouTube — simple enough that even the most right-brained person can intuit it."
|
||||||
|
Foreground="#a0a0b0" FontSize="13" TextWrapping="Wrap" MaxWidth="380"
|
||||||
|
TextAlignment="Center" Margin="0,16,0,0"/>
|
||||||
|
<TextBlock Text="Free forever — live streams carry a brief "made with LlamaCasty!" flash. A subscription removes it and adds alerts."
|
||||||
|
Foreground="#888" FontSize="12" TextWrapping="Wrap" MaxWidth="380"
|
||||||
|
TextAlignment="Center" Margin="0,12,0,0"/>
|
||||||
|
<TextBlock Text="Made by gramps · llama chile shop" Foreground="#a0a0b0" FontSize="12"
|
||||||
|
HorizontalAlignment="Center" Margin="0,16,0,8"/>
|
||||||
|
|
||||||
|
<!-- Creator hub links -->
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,8">
|
||||||
|
<Button Content="llama chile shop on YouTube" Command="{Binding OpenChannelCommand}"
|
||||||
|
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
|
||||||
|
<Button Content="Mastodon" Command="{Binding OpenMastodonCommand}"
|
||||||
|
Style="{StaticResource YtButtonSecondary}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
|
<Button Content="Buy me a coffee" Command="{Binding OpenCoffeeCommand}"
|
||||||
|
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
|
||||||
|
<Button Content="Unlock Premium" Command="{Binding OpenPremiumCommand}"
|
||||||
|
Style="{StaticResource YtButtonSecondary}"
|
||||||
|
IsEnabled="{Binding IsPremiumAvailable}"
|
||||||
|
ToolTip="Open the LlamaCasty subscription page"/>
|
||||||
|
<Button Content="Enter License Key" Command="{Binding OpenLicenseEntryCommand}"
|
||||||
|
Style="{StaticResource YtButtonSecondary}"
|
||||||
|
ToolTip="Activate a license key to remove the watermark"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Content="Licenses & legal" Command="{Binding OpenLicensesCommand}"
|
||||||
|
Style="{StaticResource YtButtonSecondary}" HorizontalAlignment="Center"
|
||||||
|
Margin="0,16,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Licensing/legal panel -->
|
||||||
|
<StackPanel Visibility="{Binding IsLicensesOpen, Converter={StaticResource BoolToVis}}">
|
||||||
|
<TextBlock Text="Third-party notices & license texts" FontSize="15" FontWeight="Bold"
|
||||||
|
Foreground="#e0e0e0" Margin="0,0,0,8"/>
|
||||||
|
<Border Background="#10101e" CornerRadius="4" Padding="10" MaxHeight="360"
|
||||||
|
BorderBrush="#333" BorderThickness="1">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<TextBlock Text="{Binding LicensesText}" Foreground="#b8b8c8" FontSize="11"
|
||||||
|
FontFamily="Consolas" TextWrapping="Wrap"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
<Button Content="← Back to About" Style="{StaticResource YtButtonSecondary}"
|
||||||
|
HorizontalAlignment="Left" Margin="0,12,0,0"
|
||||||
|
Click="BackToAbout_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- License key entry panel -->
|
||||||
|
<StackPanel Visibility="{Binding IsLicenseEntryOpen, Converter={StaticResource BoolToVis}}">
|
||||||
|
<TextBlock Text="Activate LlamaCasty Premium" FontSize="15" FontWeight="Bold"
|
||||||
|
Foreground="#e0e0e0" Margin="0,0,0,8"/>
|
||||||
|
<TextBlock Text="Enter your license key to remove the branding watermark."
|
||||||
|
Foreground="#a0a0b0" FontSize="12" TextWrapping="Wrap" Margin="0,0,0,12"/>
|
||||||
|
<TextBlock Text="License Key" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
||||||
|
<TextBox Style="{StaticResource YtTextBox}" Margin="0,0,0,12"
|
||||||
|
Text="{Binding LicenseKey, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
MaxLength="100"/>
|
||||||
|
<Button Content="Activate" Command="{Binding ActivateLicenseCommand}"
|
||||||
|
Style="{StaticResource YtButton}" HorizontalAlignment="Right"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
<TextBlock Text="{Binding LicenseMessage}" Foreground="#a0a0b0" FontSize="12"
|
||||||
|
TextWrapping="Wrap" Margin="0,0,0,8"/>
|
||||||
|
<Button Content="← Back to About" Style="{StaticResource YtButtonSecondary}"
|
||||||
|
HorizontalAlignment="Left" Margin="0,8,0,0"
|
||||||
|
Click="BackToAbout_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using ytLive.ViewModels;
|
||||||
|
|
||||||
|
namespace ytLive.Controls;
|
||||||
|
|
||||||
|
public partial class OverlayHost : UserControl
|
||||||
|
{
|
||||||
|
public OverlayHost()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BackToAbout_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is MainViewModel vm)
|
||||||
|
{
|
||||||
|
vm.IsLicensesOpen = false;
|
||||||
|
vm.IsLicenseEntryOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Controls — index
|
||||||
|
|
||||||
|
The phase-2 carved pieces of what used to be one monolithic `MainWindow.xaml`
|
||||||
|
(1,681 lines → a 123-line shell). Each is a `UserControl` in the `ytLive.Controls`
|
||||||
|
namespace, extracted one roll-back commit at a time (refactor-commit-12..18).
|
||||||
|
See [`schema.md`](../schema.md) for the memory-map conventions.
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `BottomBar.xaml(.cs)` | Footer (Row 3): gear menu (stop/mic/game-mute/quality dropdown), stream stats (bitrate/fps/dropped/duration/health), resolution dropdown |
|
||||||
|
| `HealthBanner.xaml(.cs)` | Row 1 status banner + the 7 value→visibility converters live in the app theme, not here |
|
||||||
|
| `TopBar.xaml(.cs)` | Row 0 stream controls: REC/ON-AIR pills+signs, elapsed timer, start/stop, account avatar/name (`RefreshAvatar()`) |
|
||||||
|
| `PreviewPane.xaml(.cs)` | Row 1 Col 1 center: the 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, text pull-out drawer, and the live-controls row (TRAX / game / mic meters + sliders + speakers + socials) |
|
||||||
|
| `SceneThumbnailStrip.xaml(.cs)` | Row 0 scene-picker strip (`ItemsControl` over `Scenes`), staged-red / live-green border highlight |
|
||||||
|
| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept |
|
||||||
|
| `OverlayHost.xaml(.cs)` | Root-grid overlay dialogs: Settings / Report Bug / Feature Request / About (+ licenses + license-key entry). `BackToAbout_Click` lives here |
|
||||||
|
| `SceneThumbnailStrip.xaml.cs` | Code-behind for the strip above |
|
||||||
|
| `LeftPanel.xaml.cs` | Code-behind for the panel above |
|
||||||
|
|
||||||
|
## Window ↔ control contract
|
||||||
|
|
||||||
|
The shell (`MainWindow.xaml(.cs)`) wires up each control and routes the few
|
||||||
|
cross-cutting calls through it:
|
||||||
|
|
||||||
|
- Ctor finds each control by `x:Name` (`TopBar`, `PreviewPane`, `LeftPanel`)
|
||||||
|
AFTER `InitializeComponent`, **before** any method touches it (null-ref guard).
|
||||||
|
- `_previewPane.IsClickInsidePreview/Drawer` + `_leftPanel.IsClickInside` →
|
||||||
|
deselection guard in `Window_PreviewMouseLeftButtonDown`.
|
||||||
|
- `_previewPane.UpdateSelectionOverlay(bool)` + `_leftPanel.OnSelectionChanged(vm)`
|
||||||
|
→ selection/edit-mode routing from the VM's `SelectedElement` change.
|
||||||
|
- `_topBar.RefreshAvatar()` on `IsConnected`/`AccountAvatarUrl` change.
|
||||||
|
- Handlers in moved controls cast `(DataContext as MainViewModel)` — controls
|
||||||
|
inherit the window's DataContext; never pass the VM through a property.
|
||||||
|
|
||||||
|
## Gotchas learned the hard way
|
||||||
|
|
||||||
|
- **A UserControl cannot see `Window.Resources`** (StaticResource resolves by
|
||||||
|
element lookup, so it stops at the control boundary). The 7 converters
|
||||||
|
(`BoolToVis`, `InverseBoolToVis`, `NotNullToVis`, `NullToVis`, `HiddenBoolToVis`,
|
||||||
|
`EnumToBool`, `AllTrueToVis`) were promoted from `MainWindow.Resources` into
|
||||||
|
`Themes/Controls.xaml` (see [`Themes/index.md`](../Themes/index.md)).
|
||||||
|
- **Moved XAML can lose its namespace**: a block cut from MainWindow carries no
|
||||||
|
root xmlns, so any `models:`/`Helpers:` ref in it breaks the build with
|
||||||
|
`MC2000: Value cannot be null (key)`. Declare `xmlns:models` / `xmlns:Helpers`
|
||||||
|
on each control's root (LeftPanel hit this).
|
||||||
|
- **Name-looked-up host stubs must stay in the window**, not a control:
|
||||||
|
`WebViewHostPanel` (ctor `InitWebView2(WebViewHostPanel)`) and `ToastArea`
|
||||||
|
(`NotificationService.InArea("ToastArea")`) are resolved by name from the
|
||||||
|
window root — so `OverlayHost` deliberately leaves them as thin window-root
|
||||||
|
children (Commit 18).
|
||||||
|
- When cutting visual wrappers, don't drop the outer `Border`'s
|
||||||
|
background/glow/margin — Commit 15 initially lost the preview's outer border
|
||||||
|
and had to restore it same-commit.
|
||||||
|
|
||||||
|
Related: [`ViewModels/index.md`](../ViewModels/index.md) (MainViewModel the
|
||||||
|
controls bind to), [`Themes/index.md`](../Themes/index.md) (styles/converters),
|
||||||
|
[`Helpers/index.md`](../Helpers/index.md) (ViewModelBase, converters), the
|
||||||
|
`MainWindow.xaml` shell at repo root that hosts these.
|
||||||
+65
-54
@@ -4,39 +4,43 @@
|
|||||||
|
|
||||||
**`main`**, working tree is **CLEAN**.
|
**`main`**, working tree is **CLEAN**.
|
||||||
|
|
||||||
Commits `46978b8` (AGENTS push policy), `fc33022` (HANDOFF), and refactor
|
Both split phases are now complete. Phase 1 (view-model partials) is pushed;
|
||||||
commits `0–5` through `e286719` are **pushed** (incl. `refactor-commit-0..5`).
|
Phase 2 (MainWindow.xaml → UserControls) is complete but **local-only**.
|
||||||
Refactor commits **6–11 are LOCAL ONLY** (policy 2026-08-30: no per-commit
|
|
||||||
push; `git push --tags` only at a user-agreed milestone / "push it"):
|
|
||||||
|
|
||||||
| Commit | Hash | Tag | Partial | Lines |
|
**Pushed** (reached `origin/main` == `14d3ed4`, incl. tags): Phase 1 commit `0`
|
||||||
|--------|------|-----|---------|-------|
|
and refactor commits `1..11` (`refactor-commit-0..11`), plus policy/HANDOFF.
|
||||||
| 6 Audio | `c1126a5` | `refactor-commit-6` | `MainViewModel.Audio.cs` | 408 |
|
|
||||||
| 7 Trax | `535a501` | `refactor-commit-7` | `MainViewModel.Trax.cs` | 132 |
|
|
||||||
| 8 Socials | `c41ff7f` | `refactor-commit-8` | `MainViewModel.Socials.cs` | 161 |
|
|
||||||
| 9 Streaming | `e1006a0` | `refactor-commit-9` | `MainViewModel.Streaming.cs` | 787 |
|
|
||||||
| 10 Chat | `3b77a13` | `refactor-commit-10` | `MainViewModel.Chat.cs` | 194 |
|
|
||||||
| 11 Overlays | `1efde5a` | `refactor-commit-11` | `MainViewModel.Overlays.cs` | 194 |
|
|
||||||
|
|
||||||
Nightly summary: **Phase 1 complete.** The plan was 11 partials + scaffold;
|
**LOCAL ONLY (Phase 2 — 12, 13, 14, 15, 16, 17, 18)** — policy 2026-08-30: no
|
||||||
all live in `ViewModels/`. Core `MainViewModel.cs` shrank 1,890 → **758
|
per-commit push; `git push` only at a user-agreed milestone / "push it".
|
||||||
lines**. Every change: clean build (0 warnings), `scripts/verify.sh` (246
|
**Push pending on explicit user approval:**
|
||||||
passed + 2 known failures only), `scripts/scope-check.sh` with explicit file
|
|
||||||
args, `ViewModels/index.md` updated in the SAME commit, tagged
|
| Commit | Hash | Tag | Control | From Loc |
|
||||||
`refactor-commit-N`, no per-commit push.
|
|--------|------|-----|---------|----------|
|
||||||
|
| 12 | `bc23b64` | `refactor-commit-12` | `Controls/BottomBar` | footer |
|
||||||
|
| 13 | `6914252` | `refactor-commit-13` | `Controls/HealthBanner` | row 1 banner |
|
||||||
|
| 14 | `90f5ad5` | `refactor-commit-14` | `Controls/TopBar` | row 0 top bar |
|
||||||
|
| 15 | `c5e3466` | `refactor-commit-15` | `Controls/PreviewPane` | center preview |
|
||||||
|
| 16 | `73397cf` | `refactor-commit-16` | `Controls/SceneThumbnailStrip` | row 0 strip |
|
||||||
|
| 17 | `c9fd1bd` | `refactor-commit-17` | `Controls/LeftPanel` | row 1 col 0 |
|
||||||
|
| 18 | `544208e` | `refactor-commit-18` | `Controls/OverlayHost` | overlay dialogs |
|
||||||
|
|
||||||
## What's In Flight
|
## What's In Flight
|
||||||
|
|
||||||
Nothing code-in-flight — working tree clean, Phase 1 done.
|
Nothing code-in-flight — working tree clean, **Phase 2 complete**.
|
||||||
|
|
||||||
**Next (queued, not started): Phase 2 — split `MainWindow.xaml` (1,681 lines)
|
**Phase 2 result:** `MainWindow.xaml` carved 1,681 → **123 lines** (a shell over
|
||||||
into UserControls.** Same rules: one work unit per control, docs in the same
|
6 UserControls under `Controls/`, namespace `ytLive.Controls`). Every change:
|
||||||
commit, verify + scope-check, commit locally, `git push` only on user go.
|
0-warning build, `scripts/verify.sh` (246 pass + 2 known only), explicit-file
|
||||||
|
`scripts/scope-check.sh`, `ViewModels/index.md` in the SAME commit, tagged
|
||||||
|
`refactor-commit-N`, no per-commit push.
|
||||||
|
|
||||||
**Push pending:** refactor commits 6–11 + their tags are local. The Session-1
|
**Next:**
|
||||||
push checkpoint was never reached because the user directive was to run the
|
- If user says "push": `git push origin main && git push --tags` (7 commits 12-18
|
||||||
series to completion; Phase 1 completion is the natural milestone to push on
|
+ their tags).
|
||||||
explicit user confirmation.
|
- Then optional Phase 3 if queued. Nothing is queued beyond Phase 2.
|
||||||
|
|
||||||
|
**Pending bus:** `ViewModels/index.md` Phase 2 tracker has rows 12–18 (closed);
|
||||||
|
Phase 1 tracker closed. `Controls/` has `index.md`-style facts only if needed.
|
||||||
|
|
||||||
## Landmines
|
## Landmines
|
||||||
|
|
||||||
@@ -45,44 +49,51 @@ explicit user confirmation.
|
|||||||
`RoundClipInteractionTests.Round_Clip_Corner_Is_Grabbable_And_Shape_Is_Circle`.
|
`RoundClipInteractionTests.Round_Clip_Corner_Is_Grabbable_And_Shape_Is_Circle`.
|
||||||
- **Tests occasionally abort early (236/237)** — known flake, accepted by `verify.sh`.
|
- **Tests occasionally abort early (236/237)** — known flake, accepted by `verify.sh`.
|
||||||
- **App running locks `ytLive.exe`** → build fails MSB3027. Kill PID before building.
|
- **App running locks `ytLive.exe`** → build fails MSB3027. Kill PID before building.
|
||||||
- **Implicit usings are SDK-default but partials must still carry their own
|
- **WinForm crash from `Window_PreviewMouseLeftButtonDown`** — handled in-side the
|
||||||
domain usings** (they do not inherit core's): `ICommand`/`ObservableCollection`/
|
preview/left panel; don't add handlers that deref `OriginalSource` unguarded.
|
||||||
`WriteableBitmap`/`DispatcherTimer` etc. are NOT implicit. Grep-verified usings:
|
- **Controls cannot see `Window.Resources`** (converter rule, Commit 13): any
|
||||||
- Audio: Input, Media, Threading, `NAudio.CoreAudioApi`, `ytLive`, Helpers, Models, Services, Services.Audio
|
StaticResource the window once supplied must live at app level in
|
||||||
- Trax: IO, Media, `Microsoft.Win32`, Helpers, Models, Services.Audio
|
`Themes/Controls.xaml`. The 7 converters were promoted there.
|
||||||
- Socials: Windows, Input, Media, `ytLive`, Helpers, Models, Services, Services.Compositor
|
- **Some control XAML needs its own `xmlns:models` / `xmlns:Helpers`** — a moved
|
||||||
- Streaming: ObjectModel, IO, Windows, Input, Threading, `Microsoft.Win32`, Helpers, Models, Services, Services.Encoder
|
block lost its namespace when it left MainWindow's root (LeftPanel hit this,
|
||||||
- Chat: ObjectModel, Media, Media.Imaging, Threading, Helpers, Models, Services
|
MC2000 "key null"). Always add the model/helper xmlns on each control root.
|
||||||
- Overlays: Diagnostics, IO, Input, Helpers
|
- **Name-looked-up host stubs must stay in the window:** `WebViewHostPanel`
|
||||||
|
(ctor `InitWebView2(WebViewHostPanel)`) and `ToastArea`
|
||||||
|
(`NotificationService.InArea("ToastArea")`) are resolved by name from the
|
||||||
|
window — keep them as window-root children, don't bury them in a control.
|
||||||
|
OverlayHost deliberately left them in place (Commit 18) for this reason.
|
||||||
- **Pre-commit gate:** `scripts/verify.sh` (0-warning build + full tests) and
|
- **Pre-commit gate:** `scripts/verify.sh` (0-warning build + full tests) and
|
||||||
`scripts/scope-check.sh "file1" …` — scope-check REQUIRES explicit file args.
|
`scripts/scope-check.sh "file1" …` — scope-check REQUIRES explicit file args.
|
||||||
- Build from WSL with the Windows dotnet host only (see `AGENTS.md`).
|
- Build from WSL with the Windows dotnet host only (see `AGENTS.md`).
|
||||||
|
|
||||||
## Todo state
|
## Todo state
|
||||||
|
|
||||||
- MainViewModel refactor: **Phase 1 complete (11/11 partials + scaffold).**
|
- MainViewModel refactor: **Phase 1 complete (11/11 partials)**, pushed.
|
||||||
`ViewModels/index.md` has a per-partial table row (code↔commit map in the
|
- MainWindow.xaml refactor: **Phase 2 complete (6/6 controls)**, local-only.
|
||||||
tracker section). Core row now describes only what remains (ctor wiring,
|
|
||||||
lifecycle, source commands, WebView2 hosting, frame-pump seam,
|
|
||||||
`SelectedElement`, `BrandFlash*`, `BuildCompositorOptions`/`ResolveOutputFrame`).
|
|
||||||
- TASK 3: 27/30 (preview compositor 16, text source 17, alerts 20 still open).
|
- TASK 3: 27/30 (preview compositor 16, text source 17, alerts 20 still open).
|
||||||
- TASK 4: ✅ shipped. TASK 9: items 1–3 shipped; 4–7 open. TASK 10: steps 1–7; Velopack pending.
|
- TASK 4: ✅ shipped. TASK 9: items 1–3 shipped; 4–7 open. TASK 10: steps 1–7; Velopack pending.
|
||||||
- TASK 18: shipped, creator verification pending. TASK 19/23, 20, 21-A: shipped.
|
- TASK 18: shipped, creator verification pending. TASK 19/23, 20, 21-A: shipped.
|
||||||
- TASK 22 (audio sync offset): not started — required for v1.
|
- TASK 22 (audio sync offset): not started — required for v1.
|
||||||
- Monitoring: cross-partial private access is the point of partial classes —
|
|
||||||
no moved member re-declared anywhere; moved setters reference other partials
|
|
||||||
purely by name.
|
|
||||||
|
|
||||||
## Session summary (2026-08-30)
|
## Session summary (2026-08-30)
|
||||||
|
|
||||||
- Ran the MainViewModel split to completion under the standing directive
|
- Ran the **MainWindow.xaml split to completion** under the standing "continue
|
||||||
("continue until you can continue no longer" — no interruption prompts).
|
without stopping" directive — no interruption prompts, no `question` tool.
|
||||||
- Commits 9 (Streaming), 10 (Chat), 11 (Overlays) landed locally, each ~8:40
|
- Commits 12–18 landed locally, each: cut → 0-warning build → verify (246 pass +
|
||||||
back-to-back: cut → 0-warning build → verify (246 pass + 2 known) →
|
2 known) → scope-check → index.md in-commit → `git tag refactor-commit-N`. No push.
|
||||||
scope-check → index.md in-commit → `git tag refactor-commit-N`. No push.
|
- **Commit 15 (PreviewPane** the hardest: 1920×1080 canvas + drag/select/resize/
|
||||||
- Streaming was the hardest cut (interleaved props, master-frame consts,
|
hit-test, webcam menu handlers, social glow, audio meters. A dropped outer
|
||||||
session timer, health pipeline, record rename-on-stop); Chat and Overlays
|
`Border` (background/glow/margin) was caught and restored same-commit.
|
||||||
were clean contiguous cuts. One build fix each: Streaming needed
|
- **Commit 17 (LeftPanel)** the biggest: property editors + chat autoscroll
|
||||||
`ObjectModel`/`Input`, Chat needed `Media.Imaging` for `WriteableBitmap`.
|
(moved to self-subscribe on `Loaded`), drag-to-reorder, snapshot revert/accept
|
||||||
- Docs kept honest throughout: `index.md` rows for Streaming/Chat/Overlays,
|
family; window routes via `_leftPanel.IsClickInside` and
|
||||||
About-hub row re-pointed to the Overlays partial, core row trimmed each time.
|
`_leftPanel.OnSelectionChanged(vm)`.
|
||||||
|
- **Commit 18 (OverlayHost)** the scope-decision: moved only the three dialogs;
|
||||||
|
kept `WebViewHostPanel` + `ToastArea` as window-root stubs because their
|
||||||
|
name-based lookups (WebView2 init, toast `InArea`) would break if buried in a
|
||||||
|
control — a spin-guard decision, not a guess.
|
||||||
|
- Two XAML extraction gotchas recorded: (a) a moved block losing its `xmlns`
|
||||||
|
→ MC2000 "key null" (Line n col m), fixed by adding `xmlns:models` on the
|
||||||
|
control root; (b) re-deriving line numbers after each edit before `sed`/awk
|
||||||
|
deletion ranges (the edit tool shifts numbers).
|
||||||
|
- Push checkpoint reached; awaiting explicit "push" for commits 12–18 + tags.
|
||||||
|
|||||||
+1
-167
@@ -108,173 +108,7 @@
|
|||||||
|
|
||||||
<controls:BottomBar Grid.Row="3"/>
|
<controls:BottomBar Grid.Row="3"/>
|
||||||
<!-- ═══ OVERLAY: Settings / Bug / Feature / About ═══ -->
|
<!-- ═══ OVERLAY: Settings / Bug / Feature / About ═══ -->
|
||||||
<Grid Grid.RowSpan="3"
|
<controls:OverlayHost Grid.RowSpan="3"/>
|
||||||
Visibility="{Binding IsAnyOverlayOpen, Converter={StaticResource BoolToVis}}">
|
|
||||||
<Button Background="#99000000" BorderThickness="0" Cursor="Hand"
|
|
||||||
Command="{Binding CloseOverlayCommand}">
|
|
||||||
<Button.Template>
|
|
||||||
<ControlTemplate TargetType="Button">
|
|
||||||
<Border Background="{TemplateBinding Background}"/>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Button.Template>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Border Background="#0A0C1A" CornerRadius="8" Width="650" MaxHeight="700"
|
|
||||||
VerticalAlignment="Center" HorizontalAlignment="Center"
|
|
||||||
Padding="28" BorderBrush="#333" BorderThickness="1">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<Grid Grid.Row="0">
|
|
||||||
<TextBlock Text="{Binding OverlayTitle}" FontSize="18" FontWeight="Bold"
|
|
||||||
Foreground="#e0e0e0" VerticalAlignment="Center"/>
|
|
||||||
<Button HorizontalAlignment="Right" ToolTip="Close" Style="{StaticResource IconButton}"
|
|
||||||
Command="{Binding CloseOverlayCommand}">
|
|
||||||
<Path Data="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"
|
|
||||||
Fill="#d0d0d0" Width="14" Height="14" Stretch="Uniform"/>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- App Settings: broadcast fields live in the preview's
|
|
||||||
Text pull-out now — this panel keeps app-level prefs only.
|
|
||||||
(Currently empty; the overlay shell stays for future use.) -->
|
|
||||||
<StackPanel Grid.Row="1" Margin="0,16,0,0"
|
|
||||||
Visibility="{Binding IsSettingsOpen, Converter={StaticResource BoolToVis}}">
|
|
||||||
<TextBlock Text="Default Location for Recordings" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
|
||||||
<TextBox Style="{StaticResource YtTextBox}"
|
|
||||||
Text="{Binding RecordFolderDisplay, Mode=OneWay}"
|
|
||||||
IsReadOnly="True" Margin="0,0,0,12" Foreground="#a0a0b0"/>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<Button Content="Browse…" Command="{Binding ChooseRecordFolderCommand}"
|
|
||||||
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
|
|
||||||
<Button Content="Use Downloads" Command="{Binding ResetRecordFolderCommand}"
|
|
||||||
Style="{StaticResource YtButtonSecondary}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Report Bug -->
|
|
||||||
<StackPanel Grid.Row="1" Margin="0,16,0,0"
|
|
||||||
Visibility="{Binding IsBugOpen, Converter={StaticResource BoolToVis}}">
|
|
||||||
<TextBlock Text="What went wrong?" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
|
||||||
<TextBox Style="{StaticResource YtTextBox}" Height="110" AcceptsReturn="True" TextWrapping="Wrap"
|
|
||||||
Margin="0,0,0,12"
|
|
||||||
Text="{Binding BugReportText, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
VerticalScrollBarVisibility="Auto"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Email (optional)" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
|
||||||
<TextBox Style="{StaticResource YtTextBox}" Margin="0,0,0,12"
|
|
||||||
Text="{Binding BugReportEmail, UpdateSourceTrigger=PropertyChanged}"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Opens a pre-filled email to the LlamaCasty team." Style="{StaticResource YtLabel}"/>
|
|
||||||
<Button Content="Send Report" Command="{Binding SubmitBugCommand}"
|
|
||||||
Style="{StaticResource YtButton}" HorizontalAlignment="Right" Margin="0,16,0,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Feature Request -->
|
|
||||||
<StackPanel Grid.Row="1" Margin="0,16,0,0"
|
|
||||||
Visibility="{Binding IsFeatureOpen, Converter={StaticResource BoolToVis}}">
|
|
||||||
<TextBlock Text="What would you like to see?" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
|
||||||
<TextBox Style="{StaticResource YtTextBox}" Height="110" AcceptsReturn="True" TextWrapping="Wrap"
|
|
||||||
Margin="0,0,0,12"
|
|
||||||
Text="{Binding FeatureRequestText, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
VerticalScrollBarVisibility="Auto"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Email (optional)" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
|
||||||
<TextBox Style="{StaticResource YtTextBox}" Margin="0,0,0,12"
|
|
||||||
Text="{Binding FeatureRequestEmail, UpdateSourceTrigger=PropertyChanged}"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Opens a pre-filled email to the LlamaCasty team." Style="{StaticResource YtLabel}"/>
|
|
||||||
<Button Content="Send Request" Command="{Binding SubmitFeatureCommand}"
|
|
||||||
Style="{StaticResource YtButton}" HorizontalAlignment="Right" Margin="0,16,0,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- About -->
|
|
||||||
<Grid Grid.Row="1" Margin="0,16,0,0"
|
|
||||||
Background="#171928"
|
|
||||||
Visibility="{Binding IsAboutOpen, Converter={StaticResource BoolToVis}}">
|
|
||||||
|
|
||||||
<!-- Hub view -->
|
|
||||||
<StackPanel Visibility="{Binding IsHubVisible, Converter={StaticResource BoolToVis}}">
|
|
||||||
<Image Source="/ytLive;component/Assets/llamacasty-logo.jpg" Height="125" Stretch="Uniform"
|
|
||||||
HorizontalAlignment="Center" Margin="0,0,0,12"/>
|
|
||||||
<TextBlock Text="{Binding AppVersionLabel}" Foreground="#a0a0b0" FontSize="12"
|
|
||||||
HorizontalAlignment="Center" Margin="0,4,0,0"/>
|
|
||||||
<TextBlock Text="Live streaming for YouTube — simple enough that even the most right-brained person can intuit it."
|
|
||||||
Foreground="#a0a0b0" FontSize="13" TextWrapping="Wrap" MaxWidth="380"
|
|
||||||
TextAlignment="Center" Margin="0,16,0,0"/>
|
|
||||||
<TextBlock Text="Free forever — live streams carry a brief "made with LlamaCasty!" flash. A subscription removes it and adds alerts."
|
|
||||||
Foreground="#888" FontSize="12" TextWrapping="Wrap" MaxWidth="380"
|
|
||||||
TextAlignment="Center" Margin="0,12,0,0"/>
|
|
||||||
<TextBlock Text="Made by gramps · llama chile shop" Foreground="#a0a0b0" FontSize="12"
|
|
||||||
HorizontalAlignment="Center" Margin="0,16,0,8"/>
|
|
||||||
|
|
||||||
<!-- Creator hub links -->
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,8">
|
|
||||||
<Button Content="llama chile shop on YouTube" Command="{Binding OpenChannelCommand}"
|
|
||||||
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
|
|
||||||
<Button Content="Mastodon" Command="{Binding OpenMastodonCommand}"
|
|
||||||
Style="{StaticResource YtButtonSecondary}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<Button Content="Buy me a coffee" Command="{Binding OpenCoffeeCommand}"
|
|
||||||
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
|
|
||||||
<Button Content="Unlock Premium" Command="{Binding OpenPremiumCommand}"
|
|
||||||
Style="{StaticResource YtButtonSecondary}"
|
|
||||||
IsEnabled="{Binding IsPremiumAvailable}"
|
|
||||||
ToolTip="Open the LlamaCasty subscription page"/>
|
|
||||||
<Button Content="Enter License Key" Command="{Binding OpenLicenseEntryCommand}"
|
|
||||||
Style="{StaticResource YtButtonSecondary}"
|
|
||||||
ToolTip="Activate a license key to remove the watermark"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Button Content="Licenses & legal" Command="{Binding OpenLicensesCommand}"
|
|
||||||
Style="{StaticResource YtButtonSecondary}" HorizontalAlignment="Center"
|
|
||||||
Margin="0,16,0,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Licensing/legal panel -->
|
|
||||||
<StackPanel Visibility="{Binding IsLicensesOpen, Converter={StaticResource BoolToVis}}">
|
|
||||||
<TextBlock Text="Third-party notices & license texts" FontSize="15" FontWeight="Bold"
|
|
||||||
Foreground="#e0e0e0" Margin="0,0,0,8"/>
|
|
||||||
<Border Background="#10101e" CornerRadius="4" Padding="10" MaxHeight="360"
|
|
||||||
BorderBrush="#333" BorderThickness="1">
|
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
|
||||||
<TextBlock Text="{Binding LicensesText}" Foreground="#b8b8c8" FontSize="11"
|
|
||||||
FontFamily="Consolas" TextWrapping="Wrap"/>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Border>
|
|
||||||
<Button Content="← Back to About" Style="{StaticResource YtButtonSecondary}"
|
|
||||||
HorizontalAlignment="Left" Margin="0,12,0,0"
|
|
||||||
Click="BackToAbout_Click"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- License key entry panel -->
|
|
||||||
<StackPanel Visibility="{Binding IsLicenseEntryOpen, Converter={StaticResource BoolToVis}}">
|
|
||||||
<TextBlock Text="Activate LlamaCasty Premium" FontSize="15" FontWeight="Bold"
|
|
||||||
Foreground="#e0e0e0" Margin="0,0,0,8"/>
|
|
||||||
<TextBlock Text="Enter your license key to remove the branding watermark."
|
|
||||||
Foreground="#a0a0b0" FontSize="12" TextWrapping="Wrap" Margin="0,0,0,12"/>
|
|
||||||
<TextBlock Text="License Key" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
|
||||||
<TextBox Style="{StaticResource YtTextBox}" Margin="0,0,0,12"
|
|
||||||
Text="{Binding LicenseKey, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
MaxLength="100"/>
|
|
||||||
<Button Content="Activate" Command="{Binding ActivateLicenseCommand}"
|
|
||||||
Style="{StaticResource YtButton}" HorizontalAlignment="Right"
|
|
||||||
Margin="0,0,0,8"/>
|
|
||||||
<TextBlock Text="{Binding LicenseMessage}" Foreground="#a0a0b0" FontSize="12"
|
|
||||||
TextWrapping="Wrap" Margin="0,0,0,8"/>
|
|
||||||
<Button Content="← Back to About" Style="{StaticResource YtButtonSecondary}"
|
|
||||||
HorizontalAlignment="Left" Margin="0,8,0,0"
|
|
||||||
Click="BackToAbout_Click"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- ═══ WEB SOURCE HOST: hidden panel for WebView2 controls ═══ -->
|
<!-- ═══ WEB SOURCE HOST: hidden panel for WebView2 controls ═══ -->
|
||||||
<Canvas x:Name="WebViewHostPanel" Grid.RowSpan="4"
|
<Canvas x:Name="WebViewHostPanel" Grid.RowSpan="4"
|
||||||
|
|||||||
@@ -135,15 +135,6 @@ public partial class MainWindow : Window
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BackToAbout_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is ViewModels.MainViewModel vm)
|
|
||||||
{
|
|
||||||
vm.IsLicensesOpen = false;
|
|
||||||
vm.IsLicenseEntryOpen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
private void Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.OriginalSource is not DependencyObject original) return;
|
if (e.OriginalSource is not DependencyObject original) return;
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using ytLive.Helpers;
|
||||||
|
using ytLive.Models;
|
||||||
|
using ytLive.Services;
|
||||||
|
using ytLive.ViewModels;
|
||||||
|
|
||||||
|
namespace ytLive.ViewModels;
|
||||||
|
|
||||||
|
public partial class MainViewModel
|
||||||
|
{
|
||||||
|
private void AddSource(object? parameter)
|
||||||
|
{
|
||||||
|
var scene = StagedScene;
|
||||||
|
if (scene == null) return;
|
||||||
|
|
||||||
|
var sourceType = parameter is SourceType typed
|
||||||
|
? typed
|
||||||
|
: Enum.TryParse<SourceType>(parameter?.ToString(), true, out var parsed) ? parsed : SourceType.Image;
|
||||||
|
var baseName = sourceType switch
|
||||||
|
{
|
||||||
|
SourceType.Image => "Image",
|
||||||
|
SourceType.TextOverlay => "Text",
|
||||||
|
SourceType.Countdown => "Countdown",
|
||||||
|
SourceType.WebSource => "Web Resource",
|
||||||
|
SourceType.ChatBox => "YouTube Chat",
|
||||||
|
_ => "Layer",
|
||||||
|
};
|
||||||
|
|
||||||
|
// The (+) menu only adds regular layers. The Background is a locked
|
||||||
|
// per-screen layer managed by EnsureBackground/NormalizeBackgrounds —
|
||||||
|
// never user-created, never deleted. YouTube Chat is one-per-layout.
|
||||||
|
if (sourceType is SourceType.DisplayCapture or SourceType.WindowCapture or SourceType.Background)
|
||||||
|
return;
|
||||||
|
if (sourceType == SourceType.ChatBox && !CanAddYouTubeChat)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Web resources use "Web Resource-N" naming (0-indexed per scene)
|
||||||
|
var webCount = scene.Elements.OfType<Source>().Count(s => s.Type == SourceType.WebSource);
|
||||||
|
var source = new Source
|
||||||
|
{
|
||||||
|
Name = sourceType == SourceType.WebSource
|
||||||
|
? $"{baseName}-{webCount}"
|
||||||
|
: NextSourceName(scene, baseName),
|
||||||
|
Type = sourceType,
|
||||||
|
X = sourceType == SourceType.ChatBox ? 1400 : 0,
|
||||||
|
Y = sourceType == SourceType.ChatBox ? 20 : 0,
|
||||||
|
Width = sourceType == SourceType.WebSource ? 640 : sourceType == SourceType.ChatBox ? 500 : 0,
|
||||||
|
Height = sourceType == SourceType.WebSource ? 480 : sourceType == SourceType.ChatBox ? 648 : 0,
|
||||||
|
};
|
||||||
|
scene.Elements.Add(source);
|
||||||
|
|
||||||
|
if (sourceType == SourceType.WebSource)
|
||||||
|
{
|
||||||
|
_webView2Manager?.Register(source);
|
||||||
|
source.PropertyChanged += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(Source.WebUri))
|
||||||
|
_webView2Manager?.OnWebUriChanged(source);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceType == SourceType.ChatBox)
|
||||||
|
{
|
||||||
|
source.PropertyChanged += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName is nameof(Source.ChatFontSize) or nameof(Source.ChatFontColor)
|
||||||
|
or nameof(Source.ChatBackgroundColor) or nameof(Source.ChatBackgroundOpacity)
|
||||||
|
or nameof(Source.ChatMaxMessages) or nameof(Source.ChatShowTimestamps)
|
||||||
|
or nameof(Source.ChatShowBadges) or nameof(Source.ChatMemberColor)
|
||||||
|
or nameof(Source.ChatSuperChatColor) or nameof(Source.ChatFontFamily)
|
||||||
|
or nameof(Source.ChatFadeSeconds) or nameof(Source.Width) or nameof(Source.Height))
|
||||||
|
UpdateChatBoxPreview(source);
|
||||||
|
};
|
||||||
|
UpdateChatBoxPreview(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(ShowEmptySceneHint));
|
||||||
|
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
|
||||||
|
UpdateActiveBackground();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate resource names get an incrementing suffix with no space: Image,
|
||||||
|
// Image2, Image3… The next free number is derived from the names actually in
|
||||||
|
// the scene, so deleting a middle resource never collides with a survivor.
|
||||||
|
private static string NextSourceName(Scene scene, string baseName)
|
||||||
|
{
|
||||||
|
var taken = scene.Elements.OfType<Source>()
|
||||||
|
.Select(s => s.Name)
|
||||||
|
.Where(n => string.Equals(n, baseName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (n.Length > baseName.Length
|
||||||
|
&& n.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& int.TryParse(n.Substring(baseName.Length), out _)))
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (!taken.Contains(baseName)) return baseName;
|
||||||
|
for (var i = 2; ; i++)
|
||||||
|
if (!taken.Contains($"{baseName}{i}"))
|
||||||
|
return $"{baseName}{i}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddImage()
|
||||||
|
{
|
||||||
|
var scene = StagedScene;
|
||||||
|
if (scene == null) return;
|
||||||
|
|
||||||
|
var candidates = BuildImageCandidates();
|
||||||
|
if (candidates.Count == 0)
|
||||||
|
{
|
||||||
|
var bytes = PickImageBytes("Choose an image");
|
||||||
|
if (bytes != null) AddImageSource(bytes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialog = new ReuseImageDialog(new ReuseImageViewModel(candidates))
|
||||||
|
{
|
||||||
|
Owner = Application.Current.MainWindow
|
||||||
|
};
|
||||||
|
if (dialog.ShowDialog() != true) return;
|
||||||
|
|
||||||
|
if (dialog.WantsNew)
|
||||||
|
{
|
||||||
|
var bytes = PickImageBytes("Choose an image");
|
||||||
|
if (bytes != null) AddImageSource(bytes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddReusedImage(dialog.PickedAssetId!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ReuseImageCandidate> BuildImageCandidates()
|
||||||
|
{
|
||||||
|
var candidates = new List<ReuseImageCandidate>();
|
||||||
|
foreach (var scene in Scenes)
|
||||||
|
foreach (var source in scene.Elements.OfType<Source>().Where(s => s.Type == SourceType.Image && !string.IsNullOrWhiteSpace(s.AssetId)))
|
||||||
|
{
|
||||||
|
candidates.Add(new ReuseImageCandidate
|
||||||
|
{
|
||||||
|
SceneName = scene.Name,
|
||||||
|
SourceName = source.Name,
|
||||||
|
AssetId = source.AssetId!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[]? PickImageBytes(string title)
|
||||||
|
{
|
||||||
|
var dialog = new OpenFileDialog
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Filter = "Image files (*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp)|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp|All files (*.*)|*.*"
|
||||||
|
};
|
||||||
|
if (dialog.ShowDialog() != true) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return File.ReadAllBytes(dialog.FileName);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_notifications.Warning("Couldn't read that image", ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? AddAsset(byte[] bytes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var image = ImageCache.FromBytes(bytes);
|
||||||
|
var id = _layoutStore.UpsertAsset(bytes, image?.PixelWidth ?? 0, image?.PixelHeight ?? 0);
|
||||||
|
if (id != null && image != null) ImageCache.Put(id, image);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Asset store failed: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddImageSource(byte[] bytes)
|
||||||
|
{
|
||||||
|
if (bytes.Length == 0) return;
|
||||||
|
var assetId = AddAsset(bytes);
|
||||||
|
if (assetId != null) AddReusedImage(assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddReusedImage(string assetId)
|
||||||
|
{
|
||||||
|
var scene = StagedScene;
|
||||||
|
if (scene == null || string.IsNullOrWhiteSpace(assetId)) return;
|
||||||
|
|
||||||
|
var source = new Source { Name = NextSourceName(scene, "Image"), Type = SourceType.Image, AssetId = assetId };
|
||||||
|
|
||||||
|
var image = ImageCache.Get(assetId);
|
||||||
|
if (image != null)
|
||||||
|
{
|
||||||
|
var scale = Math.Min(640.0 / image.PixelWidth, 480.0 / image.PixelHeight);
|
||||||
|
source.Width = image.PixelWidth * scale;
|
||||||
|
source.Height = image.PixelHeight * scale;
|
||||||
|
source.X = (1920 - source.Width) / 2;
|
||||||
|
source.Y = (1080 - source.Height) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.Elements.Add(source);
|
||||||
|
SelectedElement = source;
|
||||||
|
OnPropertyChanged(nameof(ShowEmptySceneHint));
|
||||||
|
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
|
||||||
|
UpdateActiveBackground();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes an element from the active scene. For a webcam config this drops the
|
||||||
|
// scene's usage and releases one camera reference (CameraManager stops the
|
||||||
|
// session when the last using scene lets go). When the last config anywhere is
|
||||||
|
// removed, the webcam identity is cleared too — re-adding opens the picker
|
||||||
|
// again instead of silently resurrecting the old camera.
|
||||||
|
private void RemoveElement(SceneElement? element)
|
||||||
|
{
|
||||||
|
var scene = StagedScene;
|
||||||
|
if (scene == null || element == null) return;
|
||||||
|
if (element is Source { IsBackground: true }) return;
|
||||||
|
if (element is WebcamSceneConfig && _webcam != null)
|
||||||
|
{
|
||||||
|
if (SelectedElement == element)
|
||||||
|
SelectedElement = null;
|
||||||
|
_ = _cameraManager.ReleaseAsync(_webcam.DeviceId);
|
||||||
|
}
|
||||||
|
if (element is Source { Type: SourceType.WebSource } webSource)
|
||||||
|
_webView2Manager?.Unregister(webSource.Id);
|
||||||
|
scene.Elements.Remove(element);
|
||||||
|
|
||||||
|
if (element is WebcamSceneConfig && _webcam != null
|
||||||
|
&& !Scenes.Any(s => s.Elements.OfType<WebcamSceneConfig>().Any()))
|
||||||
|
{
|
||||||
|
_webcam = null;
|
||||||
|
OnPropertyChanged(nameof(CanChangeWebcam));
|
||||||
|
OnPropertyChanged(nameof(CanAddWebcam));
|
||||||
|
OnPropertyChanged(nameof(CanShowWebcamInStagedScene));
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(ShowEmptySceneHint));
|
||||||
|
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
|
||||||
|
UpdateActiveBackground();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using ytLive.Models;
|
||||||
|
using ytLive.Services;
|
||||||
|
using ytLive.ViewModels;
|
||||||
|
|
||||||
|
namespace ytLive.ViewModels;
|
||||||
|
|
||||||
|
public partial class MainViewModel
|
||||||
|
{
|
||||||
|
private void RegisterLoadedWebSources()
|
||||||
|
{
|
||||||
|
if (_webView2Manager == null) return;
|
||||||
|
|
||||||
|
foreach (var source in Scenes.SelectMany(s => s.Elements).OfType<Source>()
|
||||||
|
.Where(s => s.Type == SourceType.WebSource))
|
||||||
|
{
|
||||||
|
_webView2Manager.Register(source);
|
||||||
|
source.PropertyChanged += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(Source.WebUri))
|
||||||
|
_webView2Manager.OnWebUriChanged(source);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitWebView2(Panel hostPanel)
|
||||||
|
{
|
||||||
|
_webView2Manager = new WebView2Manager(hostPanel, System.Windows.Application.Current.Dispatcher);
|
||||||
|
_webView2Manager.PreviewBitmapChanged += OnWebView2PreviewBitmapChanged;
|
||||||
|
RegisterLoadedWebSources();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWebView2PreviewBitmapChanged(string sourceId, WriteableBitmap bitmap)
|
||||||
|
{
|
||||||
|
foreach (var scene in Scenes)
|
||||||
|
foreach (var el in scene.Elements.OfType<Source>().Where(s => s.Id == sourceId))
|
||||||
|
el.VideoImageSource = bitmap;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -8,10 +6,7 @@ using System.Windows.Controls;
|
|||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
using Microsoft.Win32;
|
|
||||||
using NAudio.CoreAudioApi;
|
|
||||||
using ytLive.Helpers;
|
using ytLive.Helpers;
|
||||||
using ytLive.Models;
|
using ytLive.Models;
|
||||||
using ytLive.Services;
|
using ytLive.Services;
|
||||||
@@ -383,35 +378,6 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
AppLog.Write("LoadLayout end");
|
AppLog.Write("LoadLayout end");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterLoadedWebSources()
|
|
||||||
{
|
|
||||||
if (_webView2Manager == null) return;
|
|
||||||
|
|
||||||
foreach (var source in Scenes.SelectMany(s => s.Elements).OfType<Source>()
|
|
||||||
.Where(s => s.Type == SourceType.WebSource))
|
|
||||||
{
|
|
||||||
_webView2Manager.Register(source);
|
|
||||||
source.PropertyChanged += (_, e) =>
|
|
||||||
{
|
|
||||||
if (e.PropertyName == nameof(Source.WebUri))
|
|
||||||
_webView2Manager.OnWebUriChanged(source);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void InitWebView2(Panel hostPanel)
|
|
||||||
{
|
|
||||||
_webView2Manager = new WebView2Manager(hostPanel, System.Windows.Application.Current.Dispatcher);
|
|
||||||
_webView2Manager.PreviewBitmapChanged += OnWebView2PreviewBitmapChanged;
|
|
||||||
RegisterLoadedWebSources();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnWebView2PreviewBitmapChanged(string sourceId, WriteableBitmap bitmap)
|
|
||||||
{
|
|
||||||
foreach (var scene in Scenes)
|
|
||||||
foreach (var el in scene.Elements.OfType<Source>().Where(s => s.Id == sourceId))
|
|
||||||
el.VideoImageSource = bitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Shutdown()
|
public void Shutdown()
|
||||||
{
|
{
|
||||||
@@ -463,240 +429,6 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(BackgroundVisible));
|
OnPropertyChanged(nameof(BackgroundVisible));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddSource(object? parameter)
|
|
||||||
{
|
|
||||||
var scene = StagedScene;
|
|
||||||
if (scene == null) return;
|
|
||||||
|
|
||||||
var sourceType = parameter is SourceType typed
|
|
||||||
? typed
|
|
||||||
: Enum.TryParse<SourceType>(parameter?.ToString(), true, out var parsed) ? parsed : SourceType.Image;
|
|
||||||
var baseName = sourceType switch
|
|
||||||
{
|
|
||||||
SourceType.Image => "Image",
|
|
||||||
SourceType.TextOverlay => "Text",
|
|
||||||
SourceType.Countdown => "Countdown",
|
|
||||||
SourceType.WebSource => "Web Resource",
|
|
||||||
SourceType.ChatBox => "YouTube Chat",
|
|
||||||
_ => "Layer",
|
|
||||||
};
|
|
||||||
|
|
||||||
// The (+) menu only adds regular layers. The Background is a locked
|
|
||||||
// per-screen layer managed by EnsureBackground/NormalizeBackgrounds —
|
|
||||||
// never user-created, never deleted. YouTube Chat is one-per-layout.
|
|
||||||
if (sourceType is SourceType.DisplayCapture or SourceType.WindowCapture or SourceType.Background)
|
|
||||||
return;
|
|
||||||
if (sourceType == SourceType.ChatBox && !CanAddYouTubeChat)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Web resources use "Web Resource-N" naming (0-indexed per scene)
|
|
||||||
var webCount = scene.Elements.OfType<Source>().Count(s => s.Type == SourceType.WebSource);
|
|
||||||
var source = new Source
|
|
||||||
{
|
|
||||||
Name = sourceType == SourceType.WebSource
|
|
||||||
? $"{baseName}-{webCount}"
|
|
||||||
: NextSourceName(scene, baseName),
|
|
||||||
Type = sourceType,
|
|
||||||
X = sourceType == SourceType.ChatBox ? 1400 : 0,
|
|
||||||
Y = sourceType == SourceType.ChatBox ? 20 : 0,
|
|
||||||
Width = sourceType == SourceType.WebSource ? 640 : sourceType == SourceType.ChatBox ? 500 : 0,
|
|
||||||
Height = sourceType == SourceType.WebSource ? 480 : sourceType == SourceType.ChatBox ? 648 : 0,
|
|
||||||
};
|
|
||||||
scene.Elements.Add(source);
|
|
||||||
|
|
||||||
if (sourceType == SourceType.WebSource)
|
|
||||||
{
|
|
||||||
_webView2Manager?.Register(source);
|
|
||||||
source.PropertyChanged += (_, e) =>
|
|
||||||
{
|
|
||||||
if (e.PropertyName == nameof(Source.WebUri))
|
|
||||||
_webView2Manager?.OnWebUriChanged(source);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceType == SourceType.ChatBox)
|
|
||||||
{
|
|
||||||
source.PropertyChanged += (_, e) =>
|
|
||||||
{
|
|
||||||
if (e.PropertyName is nameof(Source.ChatFontSize) or nameof(Source.ChatFontColor)
|
|
||||||
or nameof(Source.ChatBackgroundColor) or nameof(Source.ChatBackgroundOpacity)
|
|
||||||
or nameof(Source.ChatMaxMessages) or nameof(Source.ChatShowTimestamps)
|
|
||||||
or nameof(Source.ChatShowBadges) or nameof(Source.ChatMemberColor)
|
|
||||||
or nameof(Source.ChatSuperChatColor) or nameof(Source.ChatFontFamily)
|
|
||||||
or nameof(Source.ChatFadeSeconds) or nameof(Source.Width) or nameof(Source.Height))
|
|
||||||
UpdateChatBoxPreview(source);
|
|
||||||
};
|
|
||||||
UpdateChatBoxPreview(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
OnPropertyChanged(nameof(ShowEmptySceneHint));
|
|
||||||
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
|
|
||||||
UpdateActiveBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duplicate resource names get an incrementing suffix with no space: Image,
|
|
||||||
// Image2, Image3… The next free number is derived from the names actually in
|
|
||||||
// the scene, so deleting a middle resource never collides with a survivor.
|
|
||||||
private static string NextSourceName(Scene scene, string baseName)
|
|
||||||
{
|
|
||||||
var taken = scene.Elements.OfType<Source>()
|
|
||||||
.Select(s => s.Name)
|
|
||||||
.Where(n => string.Equals(n, baseName, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| (n.Length > baseName.Length
|
|
||||||
&& n.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& int.TryParse(n.Substring(baseName.Length), out _)))
|
|
||||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
||||||
if (!taken.Contains(baseName)) return baseName;
|
|
||||||
for (var i = 2; ; i++)
|
|
||||||
if (!taken.Contains($"{baseName}{i}"))
|
|
||||||
return $"{baseName}{i}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddImage()
|
|
||||||
{
|
|
||||||
var scene = StagedScene;
|
|
||||||
if (scene == null) return;
|
|
||||||
|
|
||||||
var candidates = BuildImageCandidates();
|
|
||||||
if (candidates.Count == 0)
|
|
||||||
{
|
|
||||||
var bytes = PickImageBytes("Choose an image");
|
|
||||||
if (bytes != null) AddImageSource(bytes);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dialog = new ReuseImageDialog(new ReuseImageViewModel(candidates))
|
|
||||||
{
|
|
||||||
Owner = Application.Current.MainWindow
|
|
||||||
};
|
|
||||||
if (dialog.ShowDialog() != true) return;
|
|
||||||
|
|
||||||
if (dialog.WantsNew)
|
|
||||||
{
|
|
||||||
var bytes = PickImageBytes("Choose an image");
|
|
||||||
if (bytes != null) AddImageSource(bytes);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AddReusedImage(dialog.PickedAssetId!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ReuseImageCandidate> BuildImageCandidates()
|
|
||||||
{
|
|
||||||
var candidates = new List<ReuseImageCandidate>();
|
|
||||||
foreach (var scene in Scenes)
|
|
||||||
foreach (var source in scene.Elements.OfType<Source>().Where(s => s.Type == SourceType.Image && !string.IsNullOrWhiteSpace(s.AssetId)))
|
|
||||||
{
|
|
||||||
candidates.Add(new ReuseImageCandidate
|
|
||||||
{
|
|
||||||
SceneName = scene.Name,
|
|
||||||
SourceName = source.Name,
|
|
||||||
AssetId = source.AssetId!
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return candidates;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[]? PickImageBytes(string title)
|
|
||||||
{
|
|
||||||
var dialog = new OpenFileDialog
|
|
||||||
{
|
|
||||||
Title = title,
|
|
||||||
Filter = "Image files (*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp)|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp|All files (*.*)|*.*"
|
|
||||||
};
|
|
||||||
if (dialog.ShowDialog() != true) return null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return File.ReadAllBytes(dialog.FileName);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_notifications.Warning("Couldn't read that image", ex.Message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? AddAsset(byte[] bytes)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var image = ImageCache.FromBytes(bytes);
|
|
||||||
var id = _layoutStore.UpsertAsset(bytes, image?.PixelWidth ?? 0, image?.PixelHeight ?? 0);
|
|
||||||
if (id != null && image != null) ImageCache.Put(id, image);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Asset store failed: {ex.Message}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddImageSource(byte[] bytes)
|
|
||||||
{
|
|
||||||
if (bytes.Length == 0) return;
|
|
||||||
var assetId = AddAsset(bytes);
|
|
||||||
if (assetId != null) AddReusedImage(assetId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddReusedImage(string assetId)
|
|
||||||
{
|
|
||||||
var scene = StagedScene;
|
|
||||||
if (scene == null || string.IsNullOrWhiteSpace(assetId)) return;
|
|
||||||
|
|
||||||
var source = new Source { Name = NextSourceName(scene, "Image"), Type = SourceType.Image, AssetId = assetId };
|
|
||||||
|
|
||||||
var image = ImageCache.Get(assetId);
|
|
||||||
if (image != null)
|
|
||||||
{
|
|
||||||
var scale = Math.Min(640.0 / image.PixelWidth, 480.0 / image.PixelHeight);
|
|
||||||
source.Width = image.PixelWidth * scale;
|
|
||||||
source.Height = image.PixelHeight * scale;
|
|
||||||
source.X = (1920 - source.Width) / 2;
|
|
||||||
source.Y = (1080 - source.Height) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.Elements.Add(source);
|
|
||||||
SelectedElement = source;
|
|
||||||
OnPropertyChanged(nameof(ShowEmptySceneHint));
|
|
||||||
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
|
|
||||||
UpdateActiveBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes an element from the active scene. For a webcam config this drops the
|
|
||||||
// scene's usage and releases one camera reference (CameraManager stops the
|
|
||||||
// session when the last using scene lets go). When the last config anywhere is
|
|
||||||
// removed, the webcam identity is cleared too — re-adding opens the picker
|
|
||||||
// again instead of silently resurrecting the old camera.
|
|
||||||
private void RemoveElement(SceneElement? element)
|
|
||||||
{
|
|
||||||
var scene = StagedScene;
|
|
||||||
if (scene == null || element == null) return;
|
|
||||||
if (element is Source { IsBackground: true }) return;
|
|
||||||
if (element is WebcamSceneConfig && _webcam != null)
|
|
||||||
{
|
|
||||||
if (SelectedElement == element)
|
|
||||||
SelectedElement = null;
|
|
||||||
_ = _cameraManager.ReleaseAsync(_webcam.DeviceId);
|
|
||||||
}
|
|
||||||
if (element is Source { Type: SourceType.WebSource } webSource)
|
|
||||||
_webView2Manager?.Unregister(webSource.Id);
|
|
||||||
scene.Elements.Remove(element);
|
|
||||||
|
|
||||||
if (element is WebcamSceneConfig && _webcam != null
|
|
||||||
&& !Scenes.Any(s => s.Elements.OfType<WebcamSceneConfig>().Any()))
|
|
||||||
{
|
|
||||||
_webcam = null;
|
|
||||||
OnPropertyChanged(nameof(CanChangeWebcam));
|
|
||||||
OnPropertyChanged(nameof(CanAddWebcam));
|
|
||||||
OnPropertyChanged(nameof(CanShowWebcamInStagedScene));
|
|
||||||
}
|
|
||||||
|
|
||||||
OnPropertyChanged(nameof(ShowEmptySceneHint));
|
|
||||||
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
|
|
||||||
UpdateActiveBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scene-element → latest frame, for the live compositor. The map mirrors the
|
// Scene-element → latest frame, for the live compositor. The map mirrors the
|
||||||
// preview: webcam by DeviceId, live captures (the background) by CaptureKey,
|
// preview: webcam by DeviceId, live captures (the background) by CaptureKey,
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ Progress (Commit N/12 · tag):
|
|||||||
- [x] **Commit 15 · `refactor-commit-15`** — **PreviewPane** → `Controls/PreviewPane.xaml(.cs)` (Row 1 Col 1 center preview pane). Moved the 1920×1080 canvas (OverlayCanvas/SelectionOverlay/BrandFlashLayer/SocialBarElement), text pull-out, and the live-controls row (TRAX/game/mic meters + sliders + speakers + socials button). Code-behind moved: preview drag/select/resize/hit-test (Preview_Mouse*, HitHandle, HitElement), webcam-context-menu handlers (WebcamMenu_*), social-bar glow (SocialBar_*, SocialsButton_*, SocialGlowTick), audio sliders/speakers (VolumeSlider_*, GameVolumeSlider_*, MicSpeaker_*, GameSpeaker_*, SetSliderValueFromClick, FindParent), TraxButton_*. Window keeps the source-list left panel + thumbnail strip + edit UI, and routes selection-overlay via `_previewPane.UpdateSelectionOverlay(bool)` plus `_previewPane.IsClickInsidePreview/Drawer`. `BackgroundMenu_*` stayed in the window (used by the left-panel source context menu).
|
- [x] **Commit 15 · `refactor-commit-15`** — **PreviewPane** → `Controls/PreviewPane.xaml(.cs)` (Row 1 Col 1 center preview pane). Moved the 1920×1080 canvas (OverlayCanvas/SelectionOverlay/BrandFlashLayer/SocialBarElement), text pull-out, and the live-controls row (TRAX/game/mic meters + sliders + speakers + socials button). Code-behind moved: preview drag/select/resize/hit-test (Preview_Mouse*, HitHandle, HitElement), webcam-context-menu handlers (WebcamMenu_*), social-bar glow (SocialBar_*, SocialsButton_*, SocialGlowTick), audio sliders/speakers (VolumeSlider_*, GameVolumeSlider_*, MicSpeaker_*, GameSpeaker_*, SetSliderValueFromClick, FindParent), TraxButton_*. Window keeps the source-list left panel + thumbnail strip + edit UI, and routes selection-overlay via `_previewPane.UpdateSelectionOverlay(bool)` plus `_previewPane.IsClickInsidePreview/Drawer`. `BackgroundMenu_*` stayed in the window (used by the left-panel source context menu).
|
||||||
- [x] **Commit 16 · `refactor-commit-16`** — **SceneThumbnailStrip** → `Controls/SceneThumbnailStrip.xaml(.cs)` (Row 0 scene-picker strip). `Thumbnail_MouseLeftButtonDown` moved into the control (transitions via `DataContext.TransitionToScene`).
|
- [x] **Commit 16 · `refactor-commit-16`** — **SceneThumbnailStrip** → `Controls/SceneThumbnailStrip.xaml(.cs)` (Row 0 scene-picker strip). `Thumbnail_MouseLeftButtonDown` moved into the control (transitions via `DataContext.TransitionToScene`).
|
||||||
- [x] **Commit 17 · `refactor-commit-17`** — **LeftPanel** → `Controls/LeftPanel.xaml(.cs)` (Row 1 Col 0 left panel: layers/properties edit UI + live chat list). Moved ALL property-editor and chat handlers: `SourceNameBox_*`/`CommitSourceEdit`, `AddSourceButton_Click`, `OnChatMessagesChanged` (now self-subscribed on Loaded), `MirrorButton_Click`, `ShapeButton_Click`, `SourceMenu_Remove`, `BackgroundMenu_*`, `OpacitySlider_ValueChanged`, list drag-to-reorder (`_dragStartPoint`+`List_Preview*`+`EndListDrag`+`FindItemContainerAt`), and the snapshot family (`TakeSnapshot`, `WebcamBorder*`, `Countdown*`, `Web*`). Window exposes via `_leftPanel.IsClickInside(original)` (deselect guard) and `_leftPanel.OnSelectionChanged(vm)` (opacity label + snapshot). Pending: overlay host region.
|
- [x] **Commit 17 · `refactor-commit-17`** — **LeftPanel** → `Controls/LeftPanel.xaml(.cs)` (Row 1 Col 0 left panel: layers/properties edit UI + live chat list). Moved ALL property-editor and chat handlers: `SourceNameBox_*`/`CommitSourceEdit`, `AddSourceButton_Click`, `OnChatMessagesChanged` (now self-subscribed on Loaded), `MirrorButton_Click`, `ShapeButton_Click`, `SourceMenu_Remove`, `BackgroundMenu_*`, `OpacitySlider_ValueChanged`, list drag-to-reorder (`_dragStartPoint`+`List_Preview*`+`EndListDrag`+`FindItemContainerAt`), and the snapshot family (`TakeSnapshot`, `WebcamBorder*`, `Countdown*`, `Web*`). Window exposes via `_leftPanel.IsClickInside(original)` (deselect guard) and `_leftPanel.OnSelectionChanged(vm)` (opacity label + snapshot). Pending: overlay host region.
|
||||||
|
- [x] **Commit 18 · `refactor-commit-18`** — **OverlayHost** → `Controls/OverlayHost.xaml(.cs)` (settings/bug/feature/about dialogs). `BackToAbout_Click` moved with it (uses inherited `DataContext`). Window keeps the thin `WebViewHostPanel` + `ToastArea` root stubs so name lookups are untouched (WebView2 ctor init, `NotificationService.InArea("ToastArea")`) — toast/WebView2 behavior unchanged. **Phase 2 COMPLETE** — MainWindow.xaml carved down to a 123-line shell over 6 controls (TopBar, HealthBanner, BottomBar, SceneThumbnailStrip, LeftPanel, PreviewPane, OverlayHost). Pending: session wrap-up (HANDOFF.md) + push on approval.
|
||||||
|
|
||||||
|
**Phase 3 — 500-line compliance (production `.cs` only).** Bring all production
|
||||||
|
source files under 500 lines. One roll-back commit per file; same drill
|
||||||
|
(0-warning build, `verify.sh` 246+2, explicit-file scope-check, tracker in same
|
||||||
|
commit, tag `refactor-commit-N`, no per-commit push).
|
||||||
|
- [x] **Commit A · `refactor-commit-A`** — **core** → `MainViewModel.cs` 758 → **495**. Moved source add/remove/image block to new **`MainViewModel.Sources.cs`** (234 lines: `AddSource`, `NextSourceName`, `AddImage`, `BuildImageCandidates`, `PickImageBytes`, `AddAsset`, `AddImageSource`, `AddReusedImage`, `RemoveElement`) and WebView2 hosting (`RegisterLoadedWebSources`, `InitWebView2`, `OnWebView2PreviewBitmapChanged`) to new **`MainViewModel.Web.cs`**. Pruned now-dead usings (`Microsoft.Win32`, `NAudio.CoreAudioApi`, `System.Windows.Media.Imaging`, `Collections.ObjectModel/Specialized`). Pending: Streaming, Background, SocialsDialogViewModel, LayoutStore.
|
||||||
The partial `.cs` files land next to `MainViewModel.cs` in this folder as the split
|
The partial `.cs` files land next to `MainViewModel.cs` in this folder as the split
|
||||||
proceeds; each partial carries its own `using`s and re-declares nothing from core.
|
proceeds; each partial carries its own `using`s and re-declares nothing from core.
|
||||||
Reference in New Issue
Block a user