Update README.md with new navigation structure and UI adjustments for video library manager. Enhance LibraryStyles.axaml to improve layout of filter and navigation bars. Revise MainWindow.axaml to accommodate updated UI components and ensure better user experience across different screen sizes.

This commit is contained in:
Leonid Pershin
2026-08-09 12:44:47 +03:00
parent 5acb42d11d
commit 9e239e045f
3 changed files with 478 additions and 455 deletions
+4
View File
@@ -148,6 +148,10 @@ dotnet test
захотел бы владеть этой компоновкой целиком. Четыре вкладки сущностей делят одну панель:
различается только вид метки, и четыре почти одинаковых разметки разошлись бы при первой же
правке.
Живут они уровнем ниже оконной панели, а не в ней: сверху то, что верно для всего
приложения (поиск, тема, настройки, добавить папку), под ним — навигация и органы управления
текущей страницей. В один ряд это не влезало: вкладки съедали ширину у поиска, и на 1200 px
они наезжали друг на друга. Сортировка уехала туда же — она про страницу, а не про окно.
- **Метки лежат на карточке, а не запрашиваются.** Отбор по тегу, актёру или студии — это
предикат, который DynamicData прогоняет по каждой карточке в фоновом потоке; ходить оттуда
в базу значило бы запрос на карточку. Поэтому `GetLibraryAsync` грузит видео вместе с
+7 -3
View File
@@ -112,9 +112,13 @@
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!-- A row of controls that belongs to the page below it rather than to the window. -->
<Style Selector="Border.filterBar">
<Setter Property="Padding" Value="20,10" />
<!--
A row of controls that belongs to the page below it rather than to the window. Lighter
than the window bars on purpose: it sits inside the content, and giving it the same weight
would make the window read as three stacked strips of chrome.
-->
<Style Selector="Border.filterBar, Border.navBar">
<Setter Property="Padding" Value="20,9" />
<Setter Property="BorderBrush" Value="{DynamicResource SurfaceBorderBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
</Style>
+155 -140
View File
@@ -156,24 +156,6 @@
<TextBlock Classes="sectionTitle" Text="Видеотека" />
<TextBlock Classes="cardMeta" Text="{Binding Videos.Count, StringFormat='{}{0} видео'}" />
</StackPanel>
<!-- Navigation. Text tabs rather than icons: "Актёры" and "Студии" have no icon a
reader would guess, and a wrong guess costs a click to find out. -->
<ItemsControl ItemsSource="{Binding Sections}" VerticalAlignment="Center" Margin="10,0,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="2" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:SectionTabViewModel">
<Button Classes="sectionTab"
Classes.active="{Binding IsSelected}"
Command="{Binding SelectCommand}"
Content="{Binding Title}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<TextBox Grid.Column="1"
@@ -181,21 +163,11 @@
MaxWidth="420"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
PlaceholderText="Поиск по названию…"
PlaceholderText="Поиск по названию, тегу, актёру…"
Text="{Binding SearchText}" />
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<ComboBox MinWidth="180"
ItemsSource="{Binding SortOptions}"
SelectedItem="{Binding SelectedSort}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SortOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Command="{Binding ScanCommand}"
IsEnabled="{Binding !IsScanning}"
ToolTip.Tip="Пересканировать библиотеку">
@@ -235,125 +207,168 @@
<!-- Библиотека -->
<Panel IsVisible="{Binding !IsPlayerOpen}">
<Grid RowDefinitions="Auto,*" IsVisible="{Binding IsVideosSection}">
<Grid RowDefinitions="Auto,*">
<!-- The label the grid is narrowed to. Present only while one is active, so the row
costs nothing when the grid shows everything. -->
<Border Grid.Row="0"
Classes="filterBar"
IsVisible="{Binding ActiveLabel, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<TextBlock Classes="cardMeta" VerticalAlignment="Center" Text="Отбор:" />
<TextBlock Classes="panelTitle"
VerticalAlignment="Center"
Text="{Binding ActiveLabel.Name}" />
<Button Classes="transport"
Padding="4"
Command="{Binding ClearLabelFilterCommand}"
ToolTip.Tip="Показать все видео">
<icons:MaterialIcon Kind="Close" Width="13" Height="13" />
</Button>
</StackPanel>
</Border>
<!--
Navigation and the controls that belong to whichever page is open. Below the window
chrome rather than in it: the bar above holds what is true of the whole application,
and cramming the tabs in beside the search left neither enough room.
-->
<Border Grid.Row="0" Classes="navBar">
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="14">
<ScrollViewer Grid.Row="1" Padding="20,18" HorizontalScrollBarVisibility="Disabled">
<ItemsRepeater ItemsSource="{Binding Videos}" ItemTemplate="{StaticResource VideoCardTemplate}">
<ItemsRepeater.Layout>
<UniformGridLayout ItemsStretch="Fill"
MinItemWidth="230"
MinItemHeight="212"
MinColumnSpacing="16"
MinRowSpacing="16" />
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
</Grid>
<!-- ================= Теги, актёры, студии, коллекции ================= -->
<Grid RowDefinitions="Auto,*" IsVisible="{Binding IsEntitySection}">
<Border Grid.Row="0" Classes="filterBar">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
<TextBox Grid.Column="0"
MaxWidth="360"
HorizontalAlignment="Left"
PlaceholderText="Поиск…"
Text="{Binding EntitySearchText}" />
<ComboBox Grid.Column="1"
MinWidth="180"
ItemsSource="{Binding EntitySortOptions}"
SelectedItem="{Binding SelectedEntitySort}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:EntitySortOption">
<TextBlock Text="{Binding Label}" />
<!-- Text tabs rather than icons: "Актёры" and "Студии" have no icon a reader
would guess, and a wrong guess costs a click to find out. -->
<ItemsControl Grid.Column="0" ItemsSource="{Binding Sections}" VerticalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="2" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:SectionTabViewModel">
<Button Classes="sectionTab"
Classes.active="{Binding IsSelected}"
Command="{Binding SelectCommand}"
Content="{Binding Title}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- The label the grid is narrowed to; absent while it shows everything. -->
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Spacing="6"
VerticalAlignment="Center"
IsVisible="{Binding ActiveLabel, Converter={x:Static ObjectConverters.IsNotNull}}">
<Border Classes="badge">
<TextBlock Text="{Binding ActiveLabel.Name}" />
</Border>
<Button Classes="transport"
Padding="3"
Command="{Binding ClearLabelFilterCommand}"
ToolTip.Tip="Показать все видео">
<icons:MaterialIcon Kind="Close" Width="12" Height="12" />
</Button>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<TextBox Width="200"
PlaceholderText="Поиск…"
Text="{Binding EntitySearchText}"
IsVisible="{Binding IsEntitySection}" />
<ComboBox MinWidth="170"
ItemsSource="{Binding EntitySortOptions}"
SelectedItem="{Binding SelectedEntitySort}"
IsVisible="{Binding IsEntitySection}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:EntitySortOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox MinWidth="190"
ItemsSource="{Binding SortOptions}"
SelectedItem="{Binding SelectedSort}"
IsVisible="{Binding IsVideosSection}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SortOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</Grid>
</Border>
<ScrollViewer Grid.Row="1" Padding="20,18" HorizontalScrollBarVisibility="Disabled">
<ItemsRepeater ItemsSource="{Binding Entities}">
<ItemsRepeater.Layout>
<UniformGridLayout ItemsStretch="Fill"
MinItemWidth="220"
MinItemHeight="56"
MinColumnSpacing="12"
MinRowSpacing="12" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="vm:LabelSummaryViewModel">
<Button Classes="entity" Command="{Binding OpenCommand}">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="10">
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding Name}"
TextTrimming="CharacterEllipsis"
ToolTip.Tip="{Binding Name}"
FontSize="13"
Foreground="{DynamicResource TextPrimaryBrush}" />
<Border Grid.Column="1" Classes="badge" VerticalAlignment="Center">
<TextBlock Text="{Binding CountText}" />
</Border>
</Grid>
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
<!-- One page at a time; the empty-state prompt sits over the grid, not over the bar. -->
<Panel Grid.Row="1">
<ScrollViewer Padding="20,18"
HorizontalScrollBarVisibility="Disabled"
IsVisible="{Binding IsVideosSection}">
<ItemsRepeater ItemsSource="{Binding Videos}" ItemTemplate="{StaticResource VideoCardTemplate}">
<ItemsRepeater.Layout>
<UniformGridLayout ItemsStretch="Fill"
MinItemWidth="230"
MinItemHeight="212"
MinColumnSpacing="16"
MinRowSpacing="16" />
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
<!-- ============= Теги, актёры, студии, коллекции ============= -->
<ScrollViewer Padding="20,18"
HorizontalScrollBarVisibility="Disabled"
IsVisible="{Binding IsEntitySection}">
<ItemsRepeater ItemsSource="{Binding Entities}">
<ItemsRepeater.Layout>
<UniformGridLayout ItemsStretch="Fill"
MinItemWidth="220"
MinItemHeight="56"
MinColumnSpacing="12"
MinRowSpacing="12" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="vm:LabelSummaryViewModel">
<Button Classes="entity" Command="{Binding OpenCommand}">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="10">
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding Name}"
TextTrimming="CharacterEllipsis"
ToolTip.Tip="{Binding Name}"
FontSize="13"
Foreground="{DynamicResource TextPrimaryBrush}" />
<Border Grid.Column="1" Classes="badge" VerticalAlignment="Center">
<TextBlock Text="{Binding CountText}" />
</Border>
</Grid>
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
<!-- ===================== Метаданные ===================== -->
<ContentControl Content="{Binding MetadataScan}"
ContentTemplate="{StaticResource MetadataScanTemplate}"
IsVisible="{Binding IsMetadataSection}" />
<!-- Empty state -->
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="14"
MaxWidth="420"
IsVisible="{Binding IsLibraryPromptVisible}">
<icons:MaterialIcon Kind="VideoBoxOff"
Width="52"
Height="52"
Opacity="0.35"
Foreground="{DynamicResource TextTertiaryBrush}"
HorizontalAlignment="Center" />
<TextBlock Classes="sectionTitle"
HorizontalAlignment="Center"
Text="Здесь пока пусто" />
<TextBlock Classes="subtle"
HorizontalAlignment="Center"
TextAlignment="Center"
TextWrapping="Wrap"
Text="Укажите папку с видеофайлами — PLib пройдётся по ней, соберёт превью и покажет всё карточками." />
<Button Classes="Primary"
HorizontalAlignment="Center"
Command="{Binding AddFolderCommand}"
Content="Выбрать папку" />
</StackPanel>
</Panel>
</Grid>
<!-- ======================= Метаданные ======================= -->
<ContentControl Content="{Binding MetadataScan}"
ContentTemplate="{StaticResource MetadataScanTemplate}"
IsVisible="{Binding IsMetadataSection}" />
<!-- Empty state -->
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="14"
MaxWidth="420"
IsVisible="{Binding IsLibraryPromptVisible}">
<icons:MaterialIcon Kind="VideoBoxOff"
Width="52"
Height="52"
Opacity="0.35"
Foreground="{DynamicResource TextTertiaryBrush}"
HorizontalAlignment="Center" />
<TextBlock Classes="sectionTitle"
HorizontalAlignment="Center"
Text="Здесь пока пусто" />
<TextBlock Classes="subtle"
HorizontalAlignment="Center"
TextAlignment="Center"
TextWrapping="Wrap"
Text="Укажите папку с видеофайлами — PLib пройдётся по ней, соберёт превью и покажет всё карточками." />
<Button Classes="Primary"
HorizontalAlignment="Center"
Command="{Binding AddFolderCommand}"
Content="Выбрать папку" />
</StackPanel>
</Panel>
<!-- Страница медиа. Content is cleared when the page closes, which detaches the