Add Explorer Workbench with hierarchical, off-UI Storage analysis.

Storage queries run in the background with cancellation and covering indexes so switching views no longer freezes the UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-22 12:43:05 +02:00
commit e9aba73552
130 changed files with 15110 additions and 0 deletions

850
src/Explorer.App/App.xaml Normal file
View File

@@ -0,0 +1,850 @@
<Application x:Class="Explorer.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Explorer.App"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Themes/Dark.xaml"/>
</ResourceDictionary.MergedDictionaries>
<FontFamily x:Key="Symbol">Segoe MDL2 Assets</FontFamily>
<BooleanToVisibilityConverter x:Key="BoolVis"/>
<local:ActiveThicknessConverter x:Key="ActiveThickness"/>
<local:FractionWidthConverter x:Key="FractionWidth"/>
<local:IndentConverter x:Key="Indent"/>
<local:ViewModeToVisibilityConverter x:Key="ViewDetails" Match="Details"/>
<local:ViewModeToVisibilityConverter x:Key="ViewList" Match="List"/>
<local:ViewModeToVisibilityConverter x:Key="ViewPreview" Match="Preview"/>
<local:ShellIconConverter x:Key="ShellIcon"/>
<local:ThumbnailConverter x:Key="Thumbnail"/>
<local:TransferActionTextConverter x:Key="TransferAction"/>
<DataTemplate x:Key="NameWithIcon">
<StackPanel Orientation="Horizontal" ToolTip="{Binding SizeTooltip}">
<Image Width="16" Height="16" Margin="0,0,8,0" RenderOptions.BitmapScalingMode="HighQuality"
Source="{Binding Converter={StaticResource ShellIcon}}"/>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" Foreground="{DynamicResource Fg}"/>
<TextBlock Text="{Binding CloudStatus}" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="11"
Foreground="{DynamicResource FgMuted}"
Visibility="{Binding HasCloudStatus, Converter={StaticResource BoolVis}}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="PreviewTile">
<StackPanel Width="96" Margin="2">
<Grid Width="96" Height="96">
<Image Width="96" Height="96" Stretch="Uniform"
RenderOptions.BitmapScalingMode="HighQuality"
Source="{Binding Converter={StaticResource Thumbnail}}">
<Image.Style>
<Style TargetType="Image">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsImage}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
<Image Width="64" Height="64" Stretch="Uniform"
HorizontalAlignment="Center" VerticalAlignment="Center"
RenderOptions.BitmapScalingMode="HighQuality"
Source="{Binding Converter={StaticResource ShellIcon}, ConverterParameter=large}">
<Image.Style>
<Style TargetType="Image">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsImage}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</Grid>
<TextBlock Text="{Binding Name}" TextAlignment="Center" TextWrapping="Wrap" TextTrimming="CharacterEllipsis"
MaxHeight="36" Margin="0,4,0,0" Foreground="{DynamicResource Fg}"/>
</StackPanel>
</DataTemplate>
<Style x:Key="CrumbButton" TargetType="Button">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="8,3"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="3"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource FillHover}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="PaneAddressBar">
<DockPanel LastChildFill="True" Margin="6,6,6,4">
<Button DockPanel.Dock="Left" Content="↑" Width="32" Height="28" Margin="0,0,6,0"
Style="{StaticResource CrumbButton}"
Command="{Binding UpCommand}"
IsEnabled="{Binding CanGoUp}"
ToolTip="Up one folder"/>
<Border Background="{DynamicResource InputBg}" BorderBrush="{DynamicResource Stroke}"
BorderThickness="1" CornerRadius="4" Padding="4,0" MinHeight="28">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
Focusable="False">
<ItemsControl ItemsSource="{Binding Breadcrumb}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Style="{StaticResource CrumbButton}"
Command="{Binding DataContext.GoBreadcrumbCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"
ToolTip="{Binding Path}">
<StackPanel Orientation="Horizontal">
<TextBlock FontFamily="{StaticResource Symbol}" Text="&#xE977;" FontSize="14"
Margin="0,0,6,0" VerticalAlignment="Center" Foreground="{DynamicResource Accent}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Label}" Value="This PC">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="{Binding Label}" VerticalAlignment="Center" Foreground="{DynamicResource Fg}"/>
</StackPanel>
</Button>
<TextBlock Text="" Margin="2,0,2,0" VerticalAlignment="Center" Foreground="{DynamicResource FgMuted}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsLast}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</DockPanel>
</DataTemplate>
<Style x:Key="PaneChrome" TargetType="Border">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="BorderBrush" Value="{DynamicResource Stroke}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="{DynamicResource Panel}"/>
<Setter Property="Padding" Value="0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding DataContext.IsSplit, RelativeSource={RelativeSource AncestorType=Grid}}" Value="True">
<Setter Property="BorderThickness" Value="1"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsActive}" Value="True"/>
<Condition Binding="{Binding DataContext.IsSplit, RelativeSource={RelativeSource AncestorType=Grid}}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter Property="BorderBrush" Value="{DynamicResource Accent}"/>
<Setter Property="BorderThickness" Value="3,1,1,1"/>
<Setter Property="Background" Value="{DynamicResource ActivePaneFill}"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="ExpandToggle" TargetType="ToggleButton">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Width" Value="16"/>
<Setter Property="Height" Value="16"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border Background="Transparent" Padding="2">
<Path x:Name="Arrow"
Data="M 3 1 L 9 7 L 3 13 Z"
Fill="{DynamicResource FgMuted}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Arrow" Property="Data" Value="M 1 3 L 13 3 L 7 11 Z"/>
<Setter TargetName="Arrow" Property="Fill" Value="{DynamicResource Fg}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Arrow" Property="Fill" Value="{DynamicResource Fg}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="Button">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="{DynamicResource Fill}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Stroke}"/>
<Setter Property="Padding" Value="10,6"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource FillHover}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TextBox">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="{DynamicResource InputBg}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Stroke}"/>
<Setter Property="CaretBrush" Value="{DynamicResource Fg}"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="MinHeight" Value="32"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<Style TargetType="ComboBox">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="{DynamicResource InputBg}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Stroke}"/>
<Setter Property="MinHeight" Value="32"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ComboBoxItem">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="{DynamicResource Panel}"/>
<Setter Property="Padding" Value="8,4"/>
<Style.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter Property="Background" Value="{DynamicResource FillHover}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource ListSelection}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
</Trigger>
</Style.Triggers>
</Style>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<ToggleButton x:Name="Toggle" ClickMode="Press" Focusable="False"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Border Background="{DynamicResource InputBg}" BorderBrush="{DynamicResource Stroke}" BorderThickness="1" CornerRadius="4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="22"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="1" Data="M 0 0 L 8 0 L 4 5 Z" Fill="{DynamicResource Fg}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</Border>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<ContentPresenter Margin="8,0,28,0" VerticalAlignment="Center" HorizontalAlignment="Left"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}"
IsHitTestVisible="False"/>
<Popup x:Name="PART_Popup" IsOpen="{TemplateBinding IsDropDownOpen}" Placement="Bottom" AllowsTransparency="True" Focusable="False" PopupAnimation="Slide">
<Border MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}" BorderThickness="1">
<ScrollViewer>
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Contained"/>
</ScrollViewer>
</Border>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="PathComboBox" TargetType="ComboBox" BasedOn="{StaticResource {x:Type ComboBox}}">
<Setter Property="IsEditable" Value="True"/>
<Setter Property="IsTextSearchEnabled" Value="False"/>
<Setter Property="StaysOpenOnEdit" Value="True"/>
<Setter Property="MaxDropDownHeight" Value="280"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<Border Background="{DynamicResource InputBg}" BorderBrush="{DynamicResource Stroke}"
BorderThickness="1" CornerRadius="4"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="26"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="PART_EditableTextBox"
Background="Transparent"
BorderThickness="0"
Foreground="{DynamicResource Fg}"
CaretBrush="{DynamicResource Fg}"
Padding="8,6"
VerticalContentAlignment="Center"
FontSize="13"/>
<ToggleButton Grid.Column="1" Focusable="False" ClickMode="Press"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Border Background="Transparent">
<Path Data="M 0 0 L 8 0 L 4 5 Z" Fill="{DynamicResource Fg}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
</Grid>
<Popup x:Name="PART_Popup" IsOpen="{TemplateBinding IsDropDownOpen}" Placement="Bottom"
AllowsTransparency="True" Focusable="False" PopupAnimation="Slide">
<Border MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}" BorderThickness="1">
<ScrollViewer>
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Contained"/>
</ScrollViewer>
</Border>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="CheckBox">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style TargetType="ContextMenu">
<Setter Property="Background" Value="{DynamicResource Panel}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Stroke}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContextMenu">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="Separator">
<Setter Property="Background" Value="{DynamicResource Stroke}"/>
<Setter Property="Margin" Value="8,4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Separator">
<Border Height="1" Background="{TemplateBinding Background}" Margin="{TemplateBinding Margin}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="MenuItem">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="12,6"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="MenuItem">
<Border x:Name="Bd" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}">
<ContentPresenter ContentSource="Header" RecognizesAccessKey="True"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource FillHover}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{DynamicResource FgMuted}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ProgressBar">
<Setter Property="Height" Value="4"/>
<Setter Property="Foreground" Value="{DynamicResource Accent}"/>
<Setter Property="Background" Value="{DynamicResource Fill}"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style x:Key="StorageSizeBar" TargetType="ProgressBar" BasedOn="{StaticResource {x:Type ProgressBar}}">
<Setter Property="Height" Value="12"/>
<Setter Property="Minimum" Value="0"/>
<Setter Property="Maximum" Value="1"/>
<Setter Property="Margin" Value="8,0"/>
</Style>
<Style x:Key="CaptionButton" TargetType="Button">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Width" Value="46"/>
<Setter Property="Height" Value="40"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="shell:WindowChrome.IsHitTestVisibleInChrome" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource FillHover}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="CaptionCloseButton" TargetType="Button" BasedOn="{StaticResource CaptionButton}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource Danger}"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="PlainListItem" TargetType="ListViewItem">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="6,6"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<Border x:Name="Bd" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource FillHover}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ListSelection}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ListBox">
<Setter Property="Background" Value="{DynamicResource Panel}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Stroke}"/>
</Style>
<Style TargetType="ListView">
<Setter Property="Background" Value="{DynamicResource Panel}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Stroke}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
</Style>
<Style TargetType="ListViewItem">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<Grid>
<GridViewRowPresenter x:Name="GridRow"
Content="{TemplateBinding Content}"
VerticalAlignment="Center"/>
<ContentPresenter x:Name="Content"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentStringFormat="{TemplateBinding ContentStringFormat}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center"
Visibility="Collapsed"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="GridView.ColumnCollection" Value="{x:Null}">
<Setter TargetName="Content" Property="Visibility" Value="Visible"/>
<Setter TargetName="GridRow" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource FillHover}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ListSelection}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{DynamicResource FgMuted}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="IconListItem" TargetType="ListViewItem" BasedOn="{StaticResource {x:Type ListViewItem}}">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="Margin" Value="2"/>
</Style>
<Style x:Key="ColumnHeaderGripper" TargetType="Thumb">
<Setter Property="Width" Value="8"/>
<Setter Property="Cursor" Value="SizeWE"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border Background="Transparent">
<Border Width="1" HorizontalAlignment="Center" Background="{DynamicResource Stroke}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="GridViewColumnHeader">
<Setter Property="Foreground" Value="{DynamicResource FgMuted}"/>
<Setter Property="Background" Value="{DynamicResource Panel}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Stroke}"/>
<Setter Property="BorderThickness" Value="0,0,1,1"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewColumnHeader">
<Grid>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center"/>
</Border>
<Thumb x:Name="PART_HeaderGripper" Style="{StaticResource ColumnHeaderGripper}"
HorizontalAlignment="Right"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource FillHover}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
</Trigger>
<Trigger Property="Role" Value="Padding">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewColumnHeader">
<Border Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}"
BorderThickness="0,0,0,1"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TreeView">
<Setter Property="Background" Value="{DynamicResource Panel}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="4,0,4,8"/>
</Style>
<Style TargetType="TreeViewItem">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="4,3"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TreeViewItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ToggleButton x:Name="Expander"
Style="{StaticResource ExpandToggle}"
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
ClickMode="Press"/>
<Border x:Name="Bd"
Grid.Column="1"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
CornerRadius="3"
SnapsToDevicePixels="True">
<ContentPresenter x:Name="PART_Header"
ContentSource="Header"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
</Border>
<ItemsPresenter x:Name="ItemsHost" Grid.Row="1" Grid.Column="1"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="False">
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="HasItems" Value="False">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource TreeSelection}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="True"/>
<Condition Property="IsSelectionActive" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource FillHover}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
</MultiTrigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource FillHover}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="True"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource TreeSelection}"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{DynamicResource FgMuted}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsPlaceholder}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="TabControl">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TabPanel Grid.Row="0" IsItemsHost="True" Margin="4,4,4,0"/>
<Border Grid.Row="1" Background="{DynamicResource Bg}">
<ContentPresenter x:Name="PART_SelectedContentHost"
ContentSource="SelectedContent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="TabItem">
<Setter Property="Foreground" Value="{DynamicResource FgMuted}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="12,8"/>
<Setter Property="MinWidth" Value="140"/>
<Setter Property="MinHeight" Value="32"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
<Border x:Name="Bd"
Padding="{TemplateBinding Padding}"
BorderBrush="Transparent"
BorderThickness="0,0,0,2"
Background="Transparent"
Margin="0,0,4,0">
<ContentPresenter ContentSource="Header" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource Accent}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource FillHover}"/>
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ScrollBarThumb" TargetType="Thumb">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border x:Name="Bd"
Background="{DynamicResource ScrollThumb}"
CornerRadius="4"
Margin="3,2"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ScrollThumbHover}"/>
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ScrollThumbPressed}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ScrollBarThumbHorizontal" TargetType="Thumb" BasedOn="{StaticResource ScrollBarThumb}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border x:Name="Bd"
Background="{DynamicResource ScrollThumb}"
CornerRadius="4"
Margin="2,3"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ScrollThumbHover}"/>
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ScrollThumbPressed}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ScrollBarPageButton" TargetType="RepeatButton">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RepeatButton">
<Border Background="Transparent"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="VerticalScrollBar" TargetType="ScrollBar">
<Border Background="{DynamicResource ScrollTrack}" SnapsToDevicePixels="True">
<Track x:Name="PART_Track" IsDirectionReversed="True">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageUpCommand" Style="{StaticResource ScrollBarPageButton}"/>
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollBarThumb}"/>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageDownCommand" Style="{StaticResource ScrollBarPageButton}"/>
</Track.IncreaseRepeatButton>
</Track>
</Border>
</ControlTemplate>
<ControlTemplate x:Key="HorizontalScrollBar" TargetType="ScrollBar">
<Border Background="{DynamicResource ScrollTrack}" SnapsToDevicePixels="True">
<Track x:Name="PART_Track" IsDirectionReversed="False">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageLeftCommand" Style="{StaticResource ScrollBarPageButton}"/>
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollBarThumbHorizontal}"/>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageRightCommand" Style="{StaticResource ScrollBarPageButton}"/>
</Track.IncreaseRepeatButton>
</Track>
</Border>
</ControlTemplate>
<Style TargetType="ScrollBar">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Stylus.IsPressAndHoldEnabled" Value="False"/>
<Setter Property="Width" Value="12"/>
<Setter Property="MinWidth" Value="12"/>
<Setter Property="Template" Value="{StaticResource VerticalScrollBar}"/>
<Style.Triggers>
<Trigger Property="Orientation" Value="Horizontal">
<Setter Property="Width" Value="Auto"/>
<Setter Property="MinWidth" Value="0"/>
<Setter Property="Height" Value="12"/>
<Setter Property="MinHeight" Value="12"/>
<Setter Property="Template" Value="{StaticResource HorizontalScrollBar}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="ScrollViewer">
<Setter Property="Background" Value="Transparent"/>
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,56 @@
using System.IO;
using System.Windows;
using Explorer.Presentation.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
namespace Explorer.App;
public partial class App : System.Windows.Application
{
private IHost? _host;
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
DispatcherUnhandledException += (_, args) =>
{
Log.Error(args.Exception, "Unhandled UI exception");
args.Handled = true;
};
var env = new Windows.WindowsAppEnvironment();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.File(
Path.Combine(env.LogDirectory, "explorer-.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 14)
.CreateLogger();
_host = Host.CreateDefaultBuilder()
.UseSerilog()
.ConfigureServices((_, services) => services.AddExplorer())
.Build();
var vm = _host.Services.GetRequiredService<MainViewModel>();
await vm.InitializeAsync().ConfigureAwait(true);
await _host.StartAsync().ConfigureAwait(true);
var window = _host.Services.GetRequiredService<MainWindow>();
window.DataContext = vm;
window.Show();
}
protected override async void OnExit(ExitEventArgs e)
{
if (_host is not null)
{
await _host.StopAsync(TimeSpan.FromSeconds(3)).ConfigureAwait(true);
_host.Dispose();
}
Log.CloseAndFlush();
base.OnExit(e);
}
}

View File

@@ -0,0 +1,86 @@
using Explorer.Analysis;
using Explorer.Application;
using Explorer.Domain;
using Explorer.Domain.Abstractions;
using Explorer.FileOperations;
using Explorer.Indexing;
using Explorer.Plugin.Abstractions;
using Explorer.Plugin.OneDrive;
using Explorer.Presentation.ViewModels;
using Explorer.Search;
using Explorer.Storage.Sqlite;
using Explorer.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Explorer.App;
public static class AppServices
{
public static IServiceCollection AddExplorer(this IServiceCollection services)
{
services.AddSingleton<IClock, SystemClock>();
services.AddSingleton<IAppEnvironment, WindowsAppEnvironment>();
services.AddSingleton<IVolumeService, WindowsVolumeService>();
services.AddSingleton<IFileSystemEnumerator, WindowsFileSystemEnumerator>();
services.AddSingleton<IUsnJournal, WindowsUsnJournal>();
services.AddSingleton<IShellFileOperations, WindowsShellFileOperations>();
services.AddSingleton<IOsClipboard, Services.WpfClipboard>();
services.AddSingleton<IIndexStore>(sp =>
{
var env = sp.GetRequiredService<IAppEnvironment>();
var logger = sp.GetRequiredService<ILogger<SqliteIndexStore>>();
return new SqliteIndexStore(env.DatabasePath, logger);
});
services.AddSingleton<IStorageProvider, OneDriveStorageProvider>();
services.AddSingleton<StorageProviderRegistry>();
services.AddSingleton<IHydrationGuard, HydrationGuard>();
services.AddSingleton<SourceManager>();
services.AddSingleton<PathHistoryStore>();
services.AddSingleton<CloudPlaceStore>();
services.AddSingleton<BrowseService>();
services.AddSingleton<FilesystemScanner>();
services.AddSingleton<FolderReconciler>();
services.AddSingleton<UsnChangeApplier>();
services.AddSingleton<IndexingCoordinator>();
services.AddSingleton<DirectoryWatcherHub>();
services.AddSingleton<SearchService>();
services.AddSingleton<AnalysisService>();
services.AddSingleton<TransferQueue>();
services.AddSingleton<FileOperationService>();
services.AddSingleton<DuplicateHashWorker>();
services.AddSingleton<HistoryRollupService>();
services.AddSingleton<MainViewModel>();
services.AddSingleton<MainWindow>();
services.AddHostedService(sp => sp.GetRequiredService<IndexingCoordinator>());
services.AddHostedService(sp => sp.GetRequiredService<TransferQueue>());
services.AddHostedService(sp => sp.GetRequiredService<DuplicateHashWorker>());
services.AddHostedService(sp => sp.GetRequiredService<HistoryRollupService>());
services.AddHostedService<WatcherHostedService>();
return services;
}
}
public sealed class WatcherHostedService : BackgroundService
{
private readonly DirectoryWatcherHub _hub;
private readonly SourceManager _sources;
public WatcherHostedService(DirectoryWatcherHub hub, SourceManager sources)
{
_hub = hub;
_sources = sources;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(20));
await _hub.RefreshAsync(stoppingToken).ConfigureAwait(false);
while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false))
{
await _sources.RefreshOnlineStateAsync(stoppingToken).ConfigureAwait(false);
await _hub.RefreshAsync(stoppingToken).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly:ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@@ -0,0 +1,107 @@
using System.Globalization;
using System.IO;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Explorer.Domain;
using Explorer.Presentation;
namespace Explorer.App;
public sealed class ActiveThicknessConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is true ? new Thickness(2) : new Thickness(0);
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public sealed class FractionWidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var fraction = value is double d ? d : 0;
return Math.Max(2, fraction * 360);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public sealed class IndentConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var depth = value is double d ? d : value is int i ? i : 0;
return new Thickness(depth, 0, 8, 0);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public sealed class ViewModeToVisibilityConverter : IValueConverter
{
public FolderViewMode Match { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is FolderViewMode mode && mode == Match ? Visibility.Visible : Visibility.Collapsed;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public sealed class ThumbnailConverter : IValueConverter
{
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is FolderItemViewModel { IsImage: true, MayHydrateOnRead: false, FullPath: var path })
{
return ThumbnailCache.Load(path, decodeWidth: 240);
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public sealed class TransferActionTextConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is TransferStatus.Failed ? "Dismiss" : "Cancel";
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
internal static class ThumbnailCache
{
public static ImageSource? Load(string path, int decodeWidth)
{
try
{
if (!File.Exists(path))
{
return null;
}
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.UriSource = new Uri(path);
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.CreateOptions = BitmapCreateOptions.IgnoreImageCache | BitmapCreateOptions.IgnoreColorProfile;
bmp.DecodePixelWidth = decodeWidth;
bmp.EndInit();
bmp.Freeze();
return bmp;
}
catch
{
return null;
}
}
}

View File

@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>..\..\explorer-workbench-icons\explorer-workbench.ico</ApplicationIcon>
<RootNamespace>Explorer.App</RootNamespace>
<AssemblyName>Explorer.App</AssemblyName>
<ApplicationTitle>Explorer Workbench</ApplicationTitle>
</PropertyGroup>
<ItemGroup>
<Resource Include="..\..\explorer-workbench-icons\explorer-workbench.ico" Link="Assets\explorer-workbench.ico" />
<Resource Include="..\..\explorer-workbench-icons\explorer-workbench-20.png" Link="Assets\explorer-workbench-20.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Explorer.Analysis\Explorer.Analysis.csproj" />
<ProjectReference Include="..\Explorer.Application\Explorer.Application.csproj" />
<ProjectReference Include="..\Explorer.Domain\Explorer.Domain.csproj" />
<ProjectReference Include="..\Explorer.FileOperations\Explorer.FileOperations.csproj" />
<ProjectReference Include="..\Explorer.Indexing\Explorer.Indexing.csproj" />
<ProjectReference Include="..\Explorer.Plugin.Abstractions\Explorer.Plugin.Abstractions.csproj" />
<ProjectReference Include="..\Explorer.Plugin.OneDrive\Explorer.Plugin.OneDrive.csproj" />
<ProjectReference Include="..\Explorer.Presentation\Explorer.Presentation.csproj" />
<ProjectReference Include="..\Explorer.Search\Explorer.Search.csproj" />
<ProjectReference Include="..\Explorer.Storage.Sqlite\Explorer.Storage.Sqlite.csproj" />
<ProjectReference Include="..\Explorer.Windows\Explorer.Windows.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,65 @@
using System.Windows;
using System.Windows.Controls;
namespace Explorer.App;
/// <summary>
/// Makes the first GridView column consume leftover width so Details/List fill the pane.
/// </summary>
public static class ListViewLayout
{
public static readonly DependencyProperty StretchFirstColumnProperty =
DependencyProperty.RegisterAttached(
"StretchFirstColumn",
typeof(bool),
typeof(ListViewLayout),
new PropertyMetadata(false, OnStretchChanged));
public static void SetStretchFirstColumn(ListView element, bool value) => element.SetValue(StretchFirstColumnProperty, value);
public static bool GetStretchFirstColumn(ListView element) => (bool)element.GetValue(StretchFirstColumnProperty);
private static void OnStretchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ListView list)
{
return;
}
if (e.NewValue is true)
{
list.SizeChanged += OnListSizeChanged;
list.Loaded += OnListSizeChanged;
}
else
{
list.SizeChanged -= OnListSizeChanged;
list.Loaded -= OnListSizeChanged;
}
}
private static void OnListSizeChanged(object sender, RoutedEventArgs e)
{
if (sender is ListView list)
{
Stretch(list);
}
}
public static void Stretch(ListView list)
{
if (list.View is not GridView view || view.Columns.Count == 0 || list.ActualWidth <= 0)
{
return;
}
double reserved = 8 + SystemParameters.VerticalScrollBarWidth;
for (var i = 1; i < view.Columns.Count; i++)
{
var width = view.Columns[i].Width;
reserved += double.IsNaN(width) ? 120 : width;
}
view.Columns[0].Width = Math.Max(96, list.ActualWidth - reserved);
}
}

View File

@@ -0,0 +1,630 @@
<Window x:Class="Explorer.App.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Explorer.Presentation.ViewModels;assembly=Explorer.Presentation"
xmlns:local="clr-namespace:Explorer.App"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
Title="Explorer Workbench"
Icon="pack://application:,,,/Assets/explorer-workbench.ico"
Height="820" Width="1280"
MinHeight="500" MinWidth="800"
Background="{DynamicResource Bg}"
Foreground="{DynamicResource Fg}"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
ResizeMode="CanResize"
UseLayoutRounding="True"
SnapsToDevicePixels="True"
PreviewKeyDown="OnPreviewKeyDown">
<shell:WindowChrome.WindowChrome>
<shell:WindowChrome CaptionHeight="40"
ResizeBorderThickness="6"
GlassFrameThickness="0"
CornerRadius="0"
UseAeroCaptionButtons="False"/>
</shell:WindowChrome.WindowChrome>
<DockPanel>
<Border DockPanel.Dock="Top" Height="40" Background="{DynamicResource Panel}"
BorderBrush="{DynamicResource Stroke}" BorderThickness="0,0,0,1"
MouseLeftButtonDown="OnTitleBarMouseDown">
<Grid>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="12,0,0,0" IsHitTestVisible="False">
<Image Width="20" Height="20" Margin="0,0,8,0" VerticalAlignment="Center"
RenderOptions.BitmapScalingMode="HighQuality"
Source="pack://application:,,,/Assets/explorer-workbench-20.png"/>
<TextBlock Text="Explorer Workbench" FontWeight="SemiBold" VerticalAlignment="Center" Foreground="{DynamicResource Fg}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Style="{StaticResource CaptionButton}" Content="─" Click="OnMinimize" ToolTip="Minimize"/>
<Button x:Name="MaxRestoreButton" Style="{StaticResource CaptionButton}" Content="☐" Click="OnMaxRestore" ToolTip="Maximize"/>
<Button Style="{StaticResource CaptionCloseButton}" Content="✕" Click="OnCloseWindow" ToolTip="Close"/>
</StackPanel>
</Grid>
</Border>
<Border DockPanel.Dock="Top" Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}" BorderThickness="0,0,0,1" Padding="8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="280"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Button Content="←" Command="{Binding BackCommand}" Width="36" Margin="0,0,4,0" ToolTip="Back (Alt+Left)"/>
<Button Content="→" Command="{Binding ForwardCommand}" Width="36" Margin="0,0,4,0" ToolTip="Forward (Alt+Right)"/>
<Button Content="↑" Command="{Binding UpCommand}" Width="36" Margin="0,0,4,0" ToolTip="Up (Alt+Up)"/>
<Button Content="↻" Command="{Binding RefreshCommand}" Width="36" Margin="0,0,12,0" ToolTip="Refresh (F5)"/>
<Button Content="New tab" Command="{Binding NewTabCommand}" Margin="0,0,4,0"/>
<Button Content="Split" Command="{Binding SplitCommand}" Margin="0,0,4,0"/>
<Button Content="Index" Command="{Binding BuildIndexCommand}" Margin="0,0,4,0"/>
<Button Content="Details" Command="{Binding SetViewCommand}" CommandParameter="Details" Margin="8,0,4,0"/>
<Button Content="List" Command="{Binding SetViewCommand}" CommandParameter="List" Margin="0,0,4,0"/>
<Button Content="Preview" Command="{Binding SetViewCommand}" CommandParameter="Preview"/>
</StackPanel>
<ComboBox Grid.Column="1" Margin="12,0" Style="{StaticResource PathComboBox}"
ItemsSource="{Binding PathHistory}"
Text="{Binding PathText, UpdateSourceTrigger=PropertyChanged}"
PreviewKeyDown="OnPathKeyDown"
SelectionChanged="OnPathHistorySelected"/>
<TextBox Grid.Column="2" Text="{Binding Search.Text, UpdateSourceTrigger=PropertyChanged}"
KeyDown="OnSearchKeyDown"/>
<StackPanel Grid.Column="3" Orientation="Horizontal" Margin="8,0,0,0">
<Button Content="Search" Command="{Binding SearchCommand}" Margin="0,0,4,0"/>
<Button Content="Theme" Command="{Binding ToggleThemeCommand}"/>
</StackPanel>
</Grid>
</Border>
<Border DockPanel.Dock="Bottom" Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}" BorderThickness="0,1,0,0" Padding="8,6">
<Grid>
<TextBlock Text="{Binding Footer}" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<ItemsControl ItemsSource="{Binding Transfers.Jobs}" Margin="0,0,12,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0,0,10,0">
<TextBlock VerticalAlignment="Center" FontSize="11" Foreground="{DynamicResource FgMuted}"
Text="{Binding Op, StringFormat={}{0}:}"/>
<TextBlock VerticalAlignment="Center" FontSize="11" Margin="4,0,0,0" Text="{Binding Status}"/>
<Button Margin="6,0,0,0" Padding="6,2" FontSize="11"
Content="{Binding Status, Converter={StaticResource TransferAction}}"
Command="{Binding DataContext.CancelTransferCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Storage" Command="{Binding Analysis.OpenCommand}" Margin="0,0,6,0"/>
<Button Content="Duplicates" Command="{Binding Duplicates.OpenCommand}" Margin="0,0,6,0"/>
<Button Content="Add network" Click="OnAddNetwork" Margin="0,0,6,0"/>
<Button Content="Add OneDrive" Click="OnAddOneDrive"/>
</StackPanel>
</Grid>
</Border>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="260" MinWidth="160"/>
<ColumnDefinition Width="6"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<DockPanel Grid.Column="0" Background="{DynamicResource Panel}">
<TextBlock DockPanel.Dock="Top" Margin="12,10,12,6" FontWeight="SemiBold" Foreground="{DynamicResource Fg}" Text="Locations"/>
<TreeView x:Name="NavTree" ItemsSource="{Binding Tree.Roots}"
SelectedItemChanged="OnTreeSelected"
TreeViewItem.Expanded="OnTreeExpanded"
HorizontalContentAlignment="Stretch">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<TextBlock FontFamily="{StaticResource Symbol}" Text="{Binding Glyph}" Margin="0,0,8,0" Foreground="{DynamicResource Accent}"/>
<TextBlock Text="{Binding Label}" Foreground="{DynamicResource Fg}"/>
<TextBlock Text="{Binding Status}" Margin="8,0,0,0" Foreground="{DynamicResource FgMuted}" FontSize="11"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</DockPanel>
<GridSplitter Grid.Column="1" Width="6" HorizontalAlignment="Stretch" Background="{DynamicResource Stroke}"/>
<DockPanel Grid.Column="2" LastChildFill="True">
<TabControl x:Name="Tabs" ItemsSource="{Binding Tabs}" SelectedItem="{Binding ActiveTab}"
SelectionChanged="OnTabChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" MinWidth="120">
<TextBlock Text="⋮" Margin="0,0,8,0" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Title}" Margin="0,0,8,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" MaxWidth="160"/>
<Button Content="×" Padding="4,0" MinWidth="22" Click="OnCloseTab" Tag="{Binding}"/>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Grid Loaded="OnPaneSplitLoaded"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="0"/>
<ColumnDefinition Width="0" MinWidth="0"/>
<ColumnDefinition Width="0" MinWidth="0"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" DataContext="{Binding Left}" Style="{StaticResource PaneChrome}"
PreviewMouseDown="OnPaneChromeMouseDown">
<DockPanel LastChildFill="True">
<ContentControl DockPanel.Dock="Top" Content="{Binding}" ContentTemplate="{StaticResource PaneAddressBar}"
Focusable="False"/>
<Border DockPanel.Dock="Top" Background="{DynamicResource Banner}" Padding="10,8"
Visibility="{Binding ShowIndexBanner, Converter={StaticResource BoolVis}}">
<DockPanel>
<Button DockPanel.Dock="Right" Content="Build index" Click="OnBuildIndex"/>
<TextBlock Text="{Binding IndexBannerText}" TextWrapping="Wrap" VerticalAlignment="Center"
Foreground="{DynamicResource Fg}"/>
</DockPanel>
</Border>
<Grid>
<ListView ItemsSource="{Binding Items}"
MouseDoubleClick="OnItemDoubleClick"
SelectionChanged="OnSelectionChanged"
AllowDrop="True"
PreviewMouseLeftButtonDown="OnListMouseDown"
Drop="OnListDrop"
DragOver="OnListDragOver"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
GotFocus="OnPaneFocus"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
local:ListViewLayout.StretchFirstColumn="True"
Visibility="{Binding ViewMode, Converter={StaticResource ViewDetails}}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" CellTemplate="{StaticResource NameWithIcon}"/>
<GridViewColumn Header="Date modified" Width="148" DisplayMemberBinding="{Binding ModifiedLabel}"/>
<GridViewColumn Header="Type" Width="100" DisplayMemberBinding="{Binding TypeLabel}"/>
<GridViewColumn Header="Size" Width="110" DisplayMemberBinding="{Binding SizeLabel}"/>
</GridView>
</ListView.View>
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Open" Click="OnCtxOpen"/>
<Separator/>
<MenuItem Header="Cut" Command="{Binding DataContext.CutCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Copy" Command="{Binding DataContext.CopyCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Paste" Command="{Binding DataContext.PasteCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Delete" Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Rename" Click="OnCtxRename"/>
<Separator/>
<MenuItem Header="New folder" Command="{Binding DataContext.NewFolderCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Copy path" Command="{Binding DataContext.CopyPathCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<Separator/>
<MenuItem Header="Refresh" Command="{Binding DataContext.RefreshCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Rescan folder" Command="{Binding DataContext.RescanFolderCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<Separator Visibility="{Binding DataContext.ShowCloudPin, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource BoolVis}}"/>
<MenuItem Header="Always keep on this device"
Command="{Binding DataContext.PinCloudCommand, RelativeSource={RelativeSource AncestorType=Window}}"
Visibility="{Binding DataContext.ShowCloudPin, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource BoolVis}}"/>
<MenuItem Header="Free up space"
Command="{Binding DataContext.FreeUpCloudSpaceCommand, RelativeSource={RelativeSource AncestorType=Window}}"
Visibility="{Binding DataContext.ShowCloudDehydrate, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource BoolVis}}"/>
</ContextMenu>
</ListView.ContextMenu>
</ListView>
<ListView ItemsSource="{Binding Items}"
MouseDoubleClick="OnItemDoubleClick"
SelectionChanged="OnSelectionChanged"
AllowDrop="True"
PreviewMouseLeftButtonDown="OnListMouseDown"
Drop="OnListDrop"
DragOver="OnListDragOver"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
GotFocus="OnPaneFocus"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
local:ListViewLayout.StretchFirstColumn="True"
Visibility="{Binding ViewMode, Converter={StaticResource ViewList}}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" CellTemplate="{StaticResource NameWithIcon}"/>
</GridView>
</ListView.View>
</ListView>
<ListView ItemsSource="{Binding Items}"
ItemTemplate="{StaticResource PreviewTile}"
ItemContainerStyle="{StaticResource IconListItem}"
MouseDoubleClick="OnItemDoubleClick"
SelectionChanged="OnSelectionChanged"
AllowDrop="True"
PreviewMouseLeftButtonDown="OnListMouseDown"
Drop="OnListDrop"
DragOver="OnListDragOver"
GotFocus="OnPaneFocus"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
HorizontalContentAlignment="Left"
Visibility="{Binding ViewMode, Converter={StaticResource ViewPreview}}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Width="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=ScrollViewer}}"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
</Grid>
</DockPanel>
</Border>
<GridSplitter Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ResizeBehavior="PreviousAndNext"
Background="{DynamicResource Stroke}"
Visibility="{Binding IsSplit, Converter={StaticResource BoolVis}}"
DragCompleted="OnSplitDragCompleted"/>
<Border Grid.Column="2"
DataContext="{Binding Right}"
Style="{StaticResource PaneChrome}"
PreviewMouseDown="OnPaneChromeMouseDown"
Visibility="{Binding DataContext.IsSplit, RelativeSource={RelativeSource AncestorType=Grid}, Converter={StaticResource BoolVis}}">
<DockPanel LastChildFill="True">
<ContentControl DockPanel.Dock="Top" Content="{Binding}" ContentTemplate="{StaticResource PaneAddressBar}"
Focusable="False"/>
<Border DockPanel.Dock="Top" Background="{DynamicResource Banner}" Padding="10,8"
Visibility="{Binding ShowIndexBanner, Converter={StaticResource BoolVis}}">
<DockPanel>
<Button DockPanel.Dock="Right" Content="Build index" Click="OnBuildIndex"/>
<TextBlock Text="{Binding IndexBannerText}" TextWrapping="Wrap" VerticalAlignment="Center"
Foreground="{DynamicResource Fg}"/>
</DockPanel>
</Border>
<Grid>
<ListView ItemsSource="{Binding Items}"
MouseDoubleClick="OnItemDoubleClick"
SelectionChanged="OnSelectionChanged"
AllowDrop="True"
PreviewMouseLeftButtonDown="OnListMouseDown"
Drop="OnListDrop"
DragOver="OnListDragOver"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
GotFocus="OnPaneFocus"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
local:ListViewLayout.StretchFirstColumn="True"
Visibility="{Binding ViewMode, Converter={StaticResource ViewDetails}}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" CellTemplate="{StaticResource NameWithIcon}"/>
<GridViewColumn Header="Date modified" Width="148" DisplayMemberBinding="{Binding ModifiedLabel}"/>
<GridViewColumn Header="Type" Width="100" DisplayMemberBinding="{Binding TypeLabel}"/>
<GridViewColumn Header="Size" Width="110" DisplayMemberBinding="{Binding SizeLabel}"/>
</GridView>
</ListView.View>
</ListView>
<ListView ItemsSource="{Binding Items}"
MouseDoubleClick="OnItemDoubleClick"
SelectionChanged="OnSelectionChanged"
AllowDrop="True"
PreviewMouseLeftButtonDown="OnListMouseDown"
Drop="OnListDrop"
DragOver="OnListDragOver"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
GotFocus="OnPaneFocus"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
local:ListViewLayout.StretchFirstColumn="True"
Visibility="{Binding ViewMode, Converter={StaticResource ViewList}}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" CellTemplate="{StaticResource NameWithIcon}"/>
</GridView>
</ListView.View>
</ListView>
<ListView ItemsSource="{Binding Items}"
ItemTemplate="{StaticResource PreviewTile}"
ItemContainerStyle="{StaticResource IconListItem}"
MouseDoubleClick="OnItemDoubleClick"
SelectionChanged="OnSelectionChanged"
AllowDrop="True"
PreviewMouseLeftButtonDown="OnListMouseDown"
Drop="OnListDrop"
DragOver="OnListDragOver"
GotFocus="OnPaneFocus"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
HorizontalContentAlignment="Left"
Visibility="{Binding ViewMode, Converter={StaticResource ViewPreview}}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Width="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=ScrollViewer}}"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
</Grid>
</DockPanel>
</Border>
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
<Border Grid.Column="2" Background="#99000000" Visibility="{Binding Search.IsOpen, Converter={StaticResource BoolVis}}">
<Border Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}" BorderThickness="1" Margin="40" Padding="16">
<DockPanel>
<DockPanel DockPanel.Dock="Top" LastChildFill="True" Margin="0,0,0,12">
<Button DockPanel.Dock="Right" Content="Close" Click="OnCloseSearch" Margin="8,0,0,0"/>
<TextBlock Text="Search" FontSize="18" FontWeight="SemiBold" Foreground="{DynamicResource Fg}" VerticalAlignment="Center"/>
</DockPanel>
<Grid DockPanel.Dock="Top" Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="220"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Name" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center" Margin="0,0,8,8"/>
<TextBox Grid.Column="1" Margin="0,0,16,8" Text="{Binding Search.Text, UpdateSourceTrigger=PropertyChanged}"
KeyDown="OnSearchKeyDown"/>
<TextBlock Grid.Column="2" Text="Scope" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center" Margin="0,0,8,8"/>
<ComboBox Grid.Column="3" Margin="0,0,0,8"
ItemsSource="{Binding Search.ScopeChoices}"
DisplayMemberPath="Label"
SelectedValuePath="Kind"
SelectedValue="{Binding Search.Scope}"/>
<TextBlock Grid.Row="1" Text="Extension" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center" Margin="0,0,8,0"/>
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" Margin="0,0,16,0">
<TextBox Width="90" Text="{Binding Search.Extension, UpdateSourceTrigger=PropertyChanged}"/>
<CheckBox Content="Files" Margin="16,0,8,0" VerticalAlignment="Center" Foreground="{DynamicResource Fg}"
IsChecked="{Binding Search.FilesOnly}"/>
<CheckBox Content="Folders" VerticalAlignment="Center" Foreground="{DynamicResource Fg}"
IsChecked="{Binding Search.FoldersOnly}"/>
</StackPanel>
<TextBlock Grid.Row="1" Grid.Column="2" Text="Size" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center" Margin="0,0,8,0"/>
<StackPanel Grid.Row="1" Grid.Column="3" Orientation="Horizontal">
<TextBox Width="90" Margin="0,0,8,0" Text="{Binding Search.MinSizeText, UpdateSourceTrigger=PropertyChanged}"
ToolTip="Minimum size, e.g. 10MB"/>
<TextBox Width="90" Text="{Binding Search.MaxSizeText, UpdateSourceTrigger=PropertyChanged}"
ToolTip="Maximum size, e.g. 1GB"/>
</StackPanel>
</Grid>
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
<Button DockPanel.Dock="Right" Content="Search" Command="{Binding SearchCommand}"/>
<TextBlock Text="{Binding Search.Status}" Foreground="{DynamicResource Fg}" VerticalAlignment="Center"/>
</DockPanel>
<ProgressBar DockPanel.Dock="Top" Margin="0,0,0,8" IsIndeterminate="True"
Visibility="{Binding Search.IsBusy, Converter={StaticResource BoolVis}}"/>
<ListView ItemsSource="{Binding Search.Results}" MouseDoubleClick="OnSearchDoubleClick"
VirtualizingPanel.IsVirtualizing="True"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
Background="{DynamicResource Panel}" Foreground="{DynamicResource Fg}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="220" CellTemplate="{StaticResource NameWithIcon}"/>
<GridViewColumn Header="Path" Width="420" DisplayMemberBinding="{Binding FullPath}"/>
<GridViewColumn Header="Size" Width="100" DisplayMemberBinding="{Binding SizeLabel}"/>
</GridView>
</ListView.View>
</ListView>
</DockPanel>
</Border>
</Border>
<Border Grid.Column="2" Background="#99000000" Visibility="{Binding Analysis.IsOpen, Converter={StaticResource BoolVis}}">
<Border Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}" BorderThickness="1" Margin="24" Padding="16">
<DockPanel>
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,12">
<Button DockPanel.Dock="Right" Content="Close" Command="{Binding Analysis.CloseCommand}"/>
<TextBlock Text="Storage" FontSize="18" FontWeight="SemiBold" Foreground="{DynamicResource Fg}" VerticalAlignment="Center"/>
</DockPanel>
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
<TextBlock DockPanel.Dock="Left" Text="Location" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center" Margin="0,0,8,0"/>
<ComboBox DockPanel.Dock="Left" ItemsSource="{Binding Analysis.Scopes}" SelectedItem="{Binding Analysis.SelectedScope}"
DisplayMemberPath="Label" Width="220" Margin="0,0,16,0"/>
<TextBlock DockPanel.Dock="Left" Text="View" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center" Margin="0,0,8,0"/>
<ComboBox DockPanel.Dock="Left" ItemsSource="{Binding Analysis.Pages}" SelectedItem="{Binding Analysis.Page}" Width="150" Margin="0,0,16,0"/>
<TextBlock Text="{Binding Analysis.Status}" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center"/>
</DockPanel>
<WrapPanel DockPanel.Dock="Top" Margin="0,0,0,10">
<Button Content="Open" Command="{Binding OpenStorageHereCommand}" Margin="0,0,6,0" IsEnabled="{Binding Analysis.CanAct}"/>
<Button Content="Open in other pane" Command="{Binding OpenStorageOtherCommand}" Margin="0,0,6,0"
IsEnabled="{Binding Analysis.CanAct}"
Visibility="{Binding ActiveTab.IsSplit, Converter={StaticResource BoolVis}}"/>
<Button Content="Open in new tab" Command="{Binding OpenStorageTabCommand}" Margin="0,0,6,0" IsEnabled="{Binding Analysis.CanAct}"/>
<Button Content="Search here" Command="{Binding SearchStorageCommand}" Margin="0,0,6,0" IsEnabled="{Binding Analysis.CanAct}"/>
<Button Content="Rescan" Command="{Binding RescanStorageCommand}" Margin="0,0,6,0" IsEnabled="{Binding Analysis.CanAct}"/>
<Button Content="Copy path" Command="{Binding CopyStoragePathCommand}" IsEnabled="{Binding Analysis.CanAct}"/>
</WrapPanel>
<ProgressBar DockPanel.Dock="Top" Margin="0,0,0,8" Height="4" IsIndeterminate="True"
IsHitTestVisible="False"
Visibility="{Binding Analysis.IsBusy, Converter={StaticResource BoolVis}}"/>
<Grid>
<Grid Visibility="{Binding Analysis.IsTree, Converter={StaticResource BoolVis}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Margin="6,0,22,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="170"/>
<ColumnDefinition Width="220"/>
<ColumnDefinition Width="88"/>
<ColumnDefinition Width="72"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Name" Foreground="{DynamicResource FgMuted}"/>
<TextBlock Grid.Column="1" Text="Contents" Foreground="{DynamicResource FgMuted}"/>
<TextBlock Grid.Column="2" Text="Relative size" Foreground="{DynamicResource FgMuted}" Margin="8,0"/>
<TextBlock Grid.Column="3" Text="Size" HorizontalAlignment="Right" Foreground="{DynamicResource FgMuted}"/>
<TextBlock Grid.Column="4" Text="State" Margin="8,0,0,0" Foreground="{DynamicResource FgMuted}"/>
</Grid>
<ListView Grid.Row="1" ItemsSource="{Binding Analysis.VisibleNodes}"
SelectedItem="{Binding Analysis.SelectedNode}"
MouseDoubleClick="OnStorageTreeDoubleClick"
ItemContainerStyle="{StaticResource PlainListItem}"
Background="{DynamicResource Panel}" Foreground="{DynamicResource Fg}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.CanContentScroll="True">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="170"/>
<ColumnDefinition Width="220"/>
<ColumnDefinition Width="88"/>
<ColumnDefinition Width="72"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="{Binding Indent, Converter={StaticResource Indent}}">
<Grid Width="18" Height="18" Margin="0,0,6,0">
<Button Width="18" Height="18" Padding="0"
Command="{Binding DataContext.Analysis.ToggleNodeCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Visibility="{Binding CanExpand, Converter={StaticResource BoolVis}}">
<TextBlock FontSize="11" Foreground="{DynamicResource FgMuted}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="▸"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsExpanded}" Value="True">
<Setter Property="Text" Value="▾"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Button>
</Grid>
<TextBlock FontFamily="{StaticResource Symbol}" Text="{Binding Glyph}" FontSize="14"
Foreground="{DynamicResource Accent}" Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Name}" Foreground="{DynamicResource Fg}" TextTrimming="CharacterEllipsis"
VerticalAlignment="Center" ToolTip="{Binding FullPath}"/>
</StackPanel>
<TextBlock Grid.Column="1" Text="{Binding CountLabel}" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center"/>
<ProgressBar Grid.Column="2" Style="{StaticResource StorageSizeBar}" Value="{Binding Fraction}"
ToolTip="{Binding SizeLabel}"/>
<TextBlock Grid.Column="3" Text="{Binding SizeLabel}" HorizontalAlignment="Right" Foreground="{DynamicResource Fg}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="4" Text="{Binding State}" Margin="8,0,0,0" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center"
Visibility="{Binding HasState, Converter={StaticResource BoolVis}}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Open" Command="{Binding DataContext.OpenStorageHereCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Open in other pane" Command="{Binding DataContext.OpenStorageOtherCommand, RelativeSource={RelativeSource AncestorType=Window}}"
Visibility="{Binding DataContext.ActiveTab.IsSplit, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource BoolVis}}"/>
<MenuItem Header="Open in new tab" Command="{Binding DataContext.OpenStorageTabCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Search within this location" Command="{Binding DataContext.SearchStorageCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Rescan this location" Command="{Binding DataContext.RescanStorageCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Copy path" Command="{Binding DataContext.CopyStoragePathCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
</ContextMenu>
</ListView.ContextMenu>
</ListView>
</Grid>
<Grid Visibility="{Binding Analysis.IsRanking, Converter={StaticResource BoolVis}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Margin="6,0,22,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="88"/>
<ColumnDefinition Width="72"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Name" Foreground="{DynamicResource FgMuted}"/>
<TextBlock Grid.Column="1" Text="Location" Foreground="{DynamicResource FgMuted}" Margin="8,0"/>
<TextBlock Grid.Column="2" Text="Contents" Foreground="{DynamicResource FgMuted}"/>
<TextBlock Grid.Column="3" Text="Relative size" Foreground="{DynamicResource FgMuted}" Margin="8,0"/>
<TextBlock Grid.Column="4" Text="Size" HorizontalAlignment="Right" Foreground="{DynamicResource FgMuted}"/>
<TextBlock Grid.Column="5" Text="State" Margin="8,0,0,0" Foreground="{DynamicResource FgMuted}"/>
</Grid>
<ListView Grid.Row="1" ItemsSource="{Binding Analysis.Rows}"
SelectedItem="{Binding Analysis.SelectedRow}"
MouseDoubleClick="OnStorageRankDoubleClick"
ItemContainerStyle="{StaticResource PlainListItem}"
Background="{DynamicResource Panel}" Foreground="{DynamicResource Fg}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.CanContentScroll="True">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="88"/>
<ColumnDefinition Width="72"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" Foreground="{DynamicResource Fg}" TextTrimming="CharacterEllipsis"
VerticalAlignment="Center" ToolTip="{Binding Path}"/>
<TextBlock Grid.Column="1" Text="{Binding ShortPath}" Foreground="{DynamicResource FgMuted}"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" Margin="8,0" ToolTip="{Binding Path}"/>
<TextBlock Grid.Column="2" Text="{Binding CountLabel}" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center"/>
<ProgressBar Grid.Column="3" Style="{StaticResource StorageSizeBar}" Value="{Binding Fraction}"
ToolTip="{Binding SizeLabel}"/>
<TextBlock Grid.Column="4" Text="{Binding SizeLabel}" HorizontalAlignment="Right" Foreground="{DynamicResource Fg}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="5" Text="{Binding State}" Margin="8,0,0,0" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center"
Visibility="{Binding HasState, Converter={StaticResource BoolVis}}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Open" Command="{Binding DataContext.OpenStorageHereCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Open in other pane" Command="{Binding DataContext.OpenStorageOtherCommand, RelativeSource={RelativeSource AncestorType=Window}}"
Visibility="{Binding DataContext.ActiveTab.IsSplit, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource BoolVis}}"/>
<MenuItem Header="Open in new tab" Command="{Binding DataContext.OpenStorageTabCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Search within this location" Command="{Binding DataContext.SearchStorageCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Rescan this location" Command="{Binding DataContext.RescanStorageCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="Copy path" Command="{Binding DataContext.CopyStoragePathCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
</ContextMenu>
</ListView.ContextMenu>
</ListView>
</Grid>
</Grid>
</DockPanel>
</Border>
</Border>
<Border Grid.Column="2" Background="#99000000" Visibility="{Binding Duplicates.IsOpen, Converter={StaticResource BoolVis}}">
<Border Background="{DynamicResource Panel}" BorderBrush="{DynamicResource Stroke}" BorderThickness="1" Margin="80" Padding="16">
<DockPanel>
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
<Button DockPanel.Dock="Right" Content="Close" Click="OnCloseDuplicates"/>
<TextBlock Text="Duplicates" FontSize="18" FontWeight="SemiBold" Foreground="{DynamicResource Fg}"/>
</DockPanel>
<TextBlock DockPanel.Dock="Top" Text="{Binding Duplicates.Status}" Foreground="{DynamicResource Fg}" Margin="0,0,0,8"/>
<ProgressBar DockPanel.Dock="Top" Margin="0,0,0,8" IsIndeterminate="True"
Visibility="{Binding Duplicates.IsBusy, Converter={StaticResource BoolVis}}"/>
<ListBox ItemsSource="{Binding Duplicates.Groups}" Background="{DynamicResource Panel}" Foreground="{DynamicResource Fg}"
BorderBrush="{DynamicResource Stroke}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="{DynamicResource Fg}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="8,6"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</DockPanel>
</Border>
</Border>
</Grid>
</DockPanel>
</Window>

View File

@@ -0,0 +1,525 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using Explorer.Domain;
using Explorer.Presentation;
using Explorer.Presentation.ViewModels;
namespace Explorer.App;
public partial class MainWindow : Window
{
private Point _dragStart;
private bool _dragPending;
public MainWindow()
{
InitializeComponent();
DataContextChanged += (_, _) =>
{
if (DataContext is MainViewModel vm)
{
vm.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(MainViewModel.Theme))
{
ApplyTheme(vm.Theme);
}
};
}
};
}
private MainViewModel Vm => (MainViewModel)DataContext;
private void ApplyTheme(string theme)
{
var dicts = System.Windows.Application.Current.Resources.MergedDictionaries;
dicts.Clear();
var uri = theme == "Light"
? new Uri("Themes/Light.xaml", UriKind.Relative)
: new Uri("Themes/Dark.xaml", UriKind.Relative);
dicts.Add(new ResourceDictionary { Source = uri });
}
private void OnTitleBarMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton != MouseButton.Left)
{
return;
}
if (e.ClickCount == 2)
{
ToggleMaximized();
return;
}
DragMove();
}
private void OnMinimize(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
private void OnMaxRestore(object sender, RoutedEventArgs e) => ToggleMaximized();
private void OnCloseWindow(object sender, RoutedEventArgs e) => Close();
private void ToggleMaximized()
{
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
MaxRestoreButton.Content = WindowState == WindowState.Maximized ? "❐" : "☐";
}
private void OnPaneChromeMouseDown(object sender, MouseButtonEventArgs e)
{
if (sender is FrameworkElement { DataContext: ExplorerPaneViewModel pane })
{
Vm.ActiveTab.Activate(pane);
}
}
private void OnPathKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
Vm.GoCommand.Execute(null);
e.Handled = true;
}
}
private void OnPathHistorySelected(object sender, SelectionChangedEventArgs e)
{
if (sender is not ComboBox combo || e.AddedItems.Count == 0 || e.AddedItems[0] is not string path)
{
return;
}
if (NavigationTreeViewModel.PathsEqual(path, Vm.ActivePane.CurrentPath))
{
return;
}
if (combo.IsDropDownOpen || combo.IsKeyboardFocusWithin)
{
Vm.PathText = path;
Vm.GoCommand.Execute(null);
}
}
private void OnSearchKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
Vm.SearchCommand.Execute(null);
}
}
private async void OnTreeSelected(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (Vm.Tree.IsRevealing)
{
return;
}
if (e.NewValue is NavNodeViewModel node)
{
await Vm.TreeSelectAsync(node).ConfigureAwait(true);
}
}
private async void OnTreeExpanded(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is TreeViewItem { DataContext: NavNodeViewModel node })
{
await Vm.Tree.EnsureChildrenAsync(node).ConfigureAwait(true);
}
}
private void OnTabChanged(object sender, SelectionChangedEventArgs e)
{
if (Tabs.SelectedItem is ExplorerTabViewModel tab)
{
Vm.ActiveTab = tab;
}
}
private void OnPaneSplitLoaded(object sender, RoutedEventArgs e)
{
if (sender is not Grid grid)
{
return;
}
ApplySplitFromGrid(grid);
if (Equals(grid.Tag, "split-wired"))
{
return;
}
grid.Tag = "split-wired";
ExplorerTabViewModel? current = null;
PropertyChangedEventHandler? handler = null;
void Wire(ExplorerTabViewModel? tab)
{
if (current is not null && handler is not null)
{
current.PropertyChanged -= handler;
}
current = tab;
if (tab is null)
{
return;
}
handler = (_, args) =>
{
if (args.PropertyName is nameof(ExplorerTabViewModel.IsSplit)
or nameof(ExplorerTabViewModel.SplitRatio))
{
ApplySplitLayout(grid, tab);
}
};
tab.PropertyChanged += handler;
ApplySplitLayout(grid, tab);
}
Wire(grid.DataContext as ExplorerTabViewModel);
grid.DataContextChanged += (_, _) => Wire(grid.DataContext as ExplorerTabViewModel);
grid.Unloaded += (_, _) => Wire(null);
}
private void OnSplitDragCompleted(object sender, DragCompletedEventArgs e)
{
if (sender is not GridSplitter splitter || splitter.Parent is not Grid grid)
{
return;
}
if (grid.DataContext is not ExplorerTabViewModel tab || !tab.IsSplit)
{
return;
}
var leftWidth = grid.ColumnDefinitions[0].ActualWidth;
var rightWidth = grid.ColumnDefinitions[2].ActualWidth;
var total = leftWidth + rightWidth;
if (total < 8)
{
return;
}
tab.SetSplitRatio(leftWidth / total);
ApplySplitLayout(grid, tab);
}
private static void ApplySplitFromGrid(Grid grid)
{
if (grid.DataContext is ExplorerTabViewModel tab)
{
ApplySplitLayout(grid, tab);
}
}
private static void ApplySplitLayout(Grid grid, ExplorerTabViewModel tab)
{
var left = grid.ColumnDefinitions[0];
var mid = grid.ColumnDefinitions[1];
var right = grid.ColumnDefinitions[2];
if (tab.IsSplit)
{
var ratio = Math.Clamp(tab.SplitRatio, ExplorerTabViewModel.MinSplitRatio, ExplorerTabViewModel.MaxSplitRatio);
left.Width = new GridLength(ratio, GridUnitType.Star);
left.MinWidth = 140;
mid.Width = new GridLength(6);
mid.MinWidth = 6;
right.Width = new GridLength(1.0 - ratio, GridUnitType.Star);
right.MinWidth = 140;
}
else
{
left.Width = new GridLength(1, GridUnitType.Star);
left.MinWidth = 0;
mid.Width = new GridLength(0);
mid.MinWidth = 0;
right.Width = new GridLength(0);
right.MinWidth = 0;
}
foreach (var splitter in grid.Children.OfType<GridSplitter>())
{
splitter.IsHitTestVisible = tab.IsSplit;
}
}
private void OnCloseTab(object sender, RoutedEventArgs e)
{
if (sender is Button { Tag: ExplorerTabViewModel tab })
{
Vm.CloseTab(tab);
}
}
private async void OnItemDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is ListView { SelectedItem: FolderItemViewModel item })
{
ActivatePaneFromList((ListView)sender);
await Vm.ActivePane.OpenItemAsync(item).ConfigureAwait(true);
Vm.PathText = Vm.ActivePane.CurrentPath;
}
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListView list)
{
return;
}
ActivatePaneFromList(list);
Vm.ActivePane.SelectedItems.Clear();
foreach (FolderItemViewModel item in list.SelectedItems)
{
Vm.ActivePane.SelectedItems.Add(item);
}
Vm.RefreshCloudActions();
}
private void OnPaneFocus(object sender, RoutedEventArgs e)
{
if (sender is ListView list)
{
ActivatePaneFromList(list);
}
}
private void ActivatePaneFromList(ListView list)
{
var tab = Vm.ActiveTab;
if (list.ItemsSource == tab.Right.Items)
{
tab.Activate(tab.Right);
}
else
{
tab.Activate(tab.Left);
}
}
private void OnListMouseDown(object sender, MouseButtonEventArgs e)
{
_dragStart = e.GetPosition(null);
_dragPending = true;
}
private void OnListDragOver(object sender, DragEventArgs e)
{
e.Effects = (e.KeyStates & DragDropKeyStates.ShiftKey) != 0 ? DragDropEffects.Move : DragDropEffects.Copy;
e.Handled = true;
}
private async void OnListDrop(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent(DataFormats.FileDrop) || sender is not ListView list)
{
return;
}
ActivatePaneFromList(list);
var files = (string[])e.Data.GetData(DataFormats.FileDrop)!;
var move = (e.KeyStates & DragDropKeyStates.ShiftKey) != 0 || e.AllowedEffects == DragDropEffects.Move;
await Vm.DropAsync(files, Vm.ActivePane.CurrentPath, move).ConfigureAwait(true);
await Vm.RefreshAsync().ConfigureAwait(true);
}
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
base.OnPreviewMouseMove(e);
if (!_dragPending || e.LeftButton != MouseButtonState.Pressed)
{
return;
}
var pos = e.GetPosition(null);
if (Math.Abs(pos.X - _dragStart.X) < SystemParameters.MinimumHorizontalDragDistance
&& Math.Abs(pos.Y - _dragStart.Y) < SystemParameters.MinimumVerticalDragDistance)
{
return;
}
_dragPending = false;
var paths = Vm.ActivePane.SelectedItems.Select(i => i.FullPath).ToArray();
if (paths.Length == 0)
{
return;
}
var data = new DataObject(DataFormats.FileDrop, paths);
DragDrop.DoDragDrop(this, data, DragDropEffects.Copy | DragDropEffects.Move);
}
private async void OnSearchDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is ListView { SelectedItem: FolderItemViewModel item })
{
Vm.Search.IsOpen = false;
if (item.IsDirectory)
{
await Vm.ActivePane.NavigateAsync(item.FullPath).ConfigureAwait(true);
}
else
{
await Vm.ActivePane.NavigateAsync(PathRules.Parent(item.FullPath)).ConfigureAwait(true);
}
Vm.PathText = Vm.ActivePane.CurrentPath;
}
}
private async void OnStorageTreeDoubleClick(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is DependencyObject origin && IsInsideButton(origin))
{
return;
}
if (Vm.Analysis.CanAct)
{
await Vm.OpenStorageHereAsync().ConfigureAwait(true);
}
}
private static bool IsInsideButton(DependencyObject origin)
{
for (var current = origin; current is not null;)
{
if (current is Button)
{
return true;
}
current = current is System.Windows.Media.Visual
? System.Windows.Media.VisualTreeHelper.GetParent(current)
: LogicalTreeHelper.GetParent(current);
}
return false;
}
private async void OnStorageRankDoubleClick(object sender, MouseButtonEventArgs e)
{
if (Vm.Analysis.CanAct)
{
await Vm.OpenStorageHereAsync().ConfigureAwait(true);
}
}
private void OnCloseSearch(object sender, RoutedEventArgs e) => Vm.Search.IsOpen = false;
private void OnCloseAnalysis(object sender, RoutedEventArgs e) => Vm.Analysis.Close();
private void OnCloseDuplicates(object sender, RoutedEventArgs e) => Vm.Duplicates.IsOpen = false;
private void OnBuildIndex(object sender, RoutedEventArgs e) => Vm.BuildIndexCommand.Execute(null);
private void OnCtxOpen(object sender, RoutedEventArgs e) => Vm.OpenSelectedCommand.Execute(null);
private void OnCtxRename(object sender, RoutedEventArgs e)
{
var item = Vm.ActivePane.SelectedItems.FirstOrDefault();
if (item is null)
{
return;
}
var name = PromptWindow.Ask(this, "Rename", "New name:", item.Name);
if (!string.IsNullOrWhiteSpace(name) && name != item.Name)
{
Vm.RenameSelected(name);
}
}
private void OnAddNetwork(object sender, RoutedEventArgs e)
{
var path = PromptWindow.Ask(this, "Add network location", "Network path (\\\\server\\share):", @"\\");
if (!string.IsNullOrWhiteSpace(path) && path != @"\\")
{
Vm.PromptUnc = path;
Vm.AddNetworkCommand.Execute(null);
}
}
private async void OnAddOneDrive(object sender, RoutedEventArgs e)
{
var picker = new Microsoft.Win32.OpenFolderDialog
{
Title = "Add OneDrive folder",
Multiselect = false
};
if (picker.ShowDialog(this) != true || string.IsNullOrWhiteSpace(picker.FolderName))
{
return;
}
await Vm.AddCloudFolderAsync(picker.FolderName).ConfigureAwait(true);
}
private async void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
var ctrl = Keyboard.Modifiers.HasFlag(ModifierKeys.Control);
var alt = Keyboard.Modifiers.HasFlag(ModifierKeys.Alt);
if (e.Key == Key.F5)
{
await Vm.RefreshAsync().ConfigureAwait(true);
e.Handled = true;
}
else if (e.Key == Key.F2)
{
OnCtxRename(sender, e);
e.Handled = true;
}
else if (ctrl && e.Key == Key.C)
{
Vm.Copy();
}
else if (ctrl && e.Key == Key.X)
{
Vm.Cut();
}
else if (ctrl && e.Key == Key.V)
{
await Vm.PasteAsync().ConfigureAwait(true);
}
else if (e.Key == Key.Delete)
{
await Vm.DeleteAsync().ConfigureAwait(true);
}
else if (ctrl && e.Key == Key.T)
{
await Vm.NewTabAsync().ConfigureAwait(true);
}
else if (ctrl && e.Key == Key.W)
{
Vm.CloseTab(Vm.ActiveTab);
}
else if (alt && e.Key == Key.Left)
{
await Vm.BackAsync().ConfigureAwait(true);
}
else if (alt && e.Key == Key.Right)
{
await Vm.ForwardAsync().ConfigureAwait(true);
}
else if (alt && e.Key == Key.Up)
{
await Vm.UpAsync().ConfigureAwait(true);
}
else if (e.Key == Key.Enter)
{
await Vm.OpenSelectedAsync().ConfigureAwait(true);
}
}
}

View File

@@ -0,0 +1,21 @@
<Window x:Class="Explorer.App.PromptWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Explorer Workbench"
Icon="pack://application:,,,/Assets/explorer-workbench.ico"
Height="220" Width="520"
MinHeight="200" MinWidth="420"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource Bg}" Foreground="{DynamicResource Fg}"
ResizeMode="NoResize">
<DockPanel Margin="20">
<TextBlock x:Name="Message" DockPanel.Dock="Top" TextWrapping="Wrap" Margin="0,0,0,16"
FontSize="13" Foreground="{DynamicResource Fg}"/>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,16,0,0">
<Button Content="OK" MinWidth="88" Height="32" IsDefault="True" Click="OnOk" Margin="0,0,8,0"/>
<Button Content="Cancel" MinWidth="88" Height="32" IsCancel="True"/>
</StackPanel>
<TextBox x:Name="Input" MinHeight="36" FontSize="14" VerticalContentAlignment="Center"
Padding="10,8"/>
</DockPanel>
</Window>

View File

@@ -0,0 +1,30 @@
using System.Windows;
namespace Explorer.App;
public partial class PromptWindow : Window
{
public PromptWindow(string title, string message, string initial)
{
InitializeComponent();
Title = title;
Message.Text = message;
Input.Text = initial;
Input.SelectAll();
Loaded += (_, _) => Input.Focus();
}
public string Value => Input.Text;
private void OnOk(object sender, RoutedEventArgs e)
{
DialogResult = true;
Close();
}
public static string? Ask(Window owner, string title, string message, string initial)
{
var dlg = new PromptWindow(title, message, initial) { Owner = owner };
return dlg.ShowDialog() == true ? dlg.Value : null;
}
}

View File

@@ -0,0 +1,50 @@
using System.Collections.Specialized;
using System.IO;
using System.Windows;
using Explorer.Domain.Abstractions;
namespace Explorer.App.Services;
public sealed class WpfClipboard : IOsClipboard
{
public void SetFiles(IReadOnlyList<string> paths, bool cut)
{
var list = new StringCollection();
foreach (var p in paths)
{
list.Add(p);
}
var data = new DataObject();
data.SetFileDropList(list);
data.SetData("Preferred DropEffect", new MemoryStream([(byte)(cut ? DragDropEffects.Move : DragDropEffects.Copy), 0, 0, 0]));
Clipboard.SetDataObject(data, copy: true);
}
public bool TryGetFiles(out IReadOnlyList<string> paths, out bool cut)
{
paths = [];
cut = false;
if (!Clipboard.ContainsFileDropList())
{
return false;
}
var list = Clipboard.GetFileDropList().Cast<string>().Where(s => s is not null).Cast<string>().ToList();
paths = list;
try
{
if (Clipboard.GetData("Preferred DropEffect") is MemoryStream ms)
{
var b = ms.ToArray();
cut = b.Length > 0 && b[0] == (byte)DragDropEffects.Move;
}
}
catch
{
cut = false;
}
return list.Count > 0;
}
}

View File

@@ -0,0 +1,159 @@
using System.Collections.Concurrent;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Data;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Explorer.Domain;
using Explorer.Presentation;
namespace Explorer.App;
public sealed class ShellIconConverter : IValueConverter
{
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var large = parameter is string p && p.Equals("large", StringComparison.OrdinalIgnoreCase);
return value switch
{
FolderItemViewModel item => ShellIconCache.Get(item.FullPath, item.IsDirectory, large, item.MayHydrateOnRead),
FileSystemItem fs => ShellIconCache.Get(fs.FullPath, fs.IsDirectory, large, AttributeFlags.MayHydrateOnRead(fs.Attributes)),
string path => ShellIconCache.Get(path, Directory.Exists(path), large),
_ => ShellIconCache.Get(null, true, large)
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
internal static class ShellIconCache
{
private static readonly ConcurrentDictionary<string, ImageSource> Cache = new(StringComparer.OrdinalIgnoreCase);
public static ImageSource Get(string? path, bool isDirectory, bool large = false, bool avoidHydration = false)
{
var key = (large ? "L:" : "S:") + (avoidHydration ? "A:" : "") + CacheKey(path, isDirectory);
return Cache.GetOrAdd(key, _ => Load(path, isDirectory, large, avoidHydration));
}
private static string CacheKey(string? path, bool isDirectory)
{
if (string.IsNullOrWhiteSpace(path) || path == "This PC")
{
return isDirectory ? "dir:generic" : "file:generic";
}
if (isDirectory)
{
return PathRules.IsDriveRoot(path) || (path.Length <= 3 && path.Contains(':', StringComparison.Ordinal))
? "drive:" + path.TrimEnd('\\').ToUpperInvariant()
: "dir:generic";
}
var ext = Path.GetExtension(path);
if (ext.Equals(".exe", StringComparison.OrdinalIgnoreCase)
|| ext.Equals(".lnk", StringComparison.OrdinalIgnoreCase)
|| ext.Equals(".ico", StringComparison.OrdinalIgnoreCase)
|| ext.Equals(".dll", StringComparison.OrdinalIgnoreCase))
{
return "path:" + path;
}
return "ext:" + (string.IsNullOrEmpty(ext) ? ".file" : ext.ToLowerInvariant());
}
private static ImageSource Load(string? path, bool isDirectory, bool large, bool avoidHydration = false)
{
try
{
var info = new ShFileInfo();
uint flags = ShgfiIcon | (large ? ShgfiLargeIcon : ShgfiSmallIcon);
uint attrs = isDirectory ? FileAttributeDirectory : FileAttributeNormal;
string psz;
if (string.IsNullOrWhiteSpace(path) || path == "This PC")
{
psz = isDirectory ? "folder" : "file";
flags |= ShgfiUseFileAttributes;
}
else if (isDirectory && PathRules.IsDriveRoot(path))
{
psz = PathRules.EnsureDirectoryTrailingSlashIfRoot(path);
}
else if (isDirectory)
{
psz = path;
flags |= ShgfiUseFileAttributes;
}
else
{
psz = path;
var ext = Path.GetExtension(path);
if (avoidHydration
|| !ext.Equals(".exe", StringComparison.OrdinalIgnoreCase)
&& !ext.Equals(".lnk", StringComparison.OrdinalIgnoreCase)
&& !ext.Equals(".ico", StringComparison.OrdinalIgnoreCase)
&& File.Exists(path) == false)
{
flags |= ShgfiUseFileAttributes;
}
}
SHGetFileInfo(psz, attrs, ref info, (uint)Marshal.SizeOf<ShFileInfo>(), flags);
if (info.hIcon == 0)
{
return Fallback();
}
try
{
var source = Imaging.CreateBitmapSourceFromHIcon(info.hIcon, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
source.Freeze();
return source;
}
finally
{
DestroyIcon(info.hIcon);
}
}
catch
{
return Fallback();
}
}
private static ImageSource Fallback()
{
var bmp = BitmapSource.Create(16, 16, 96, 96, PixelFormats.Bgra32, null, new byte[16 * 16 * 4], 16 * 4);
bmp.Freeze();
return bmp;
}
private const uint ShgfiIcon = 0x100;
private const uint ShgfiLargeIcon = 0x0;
private const uint ShgfiSmallIcon = 0x1;
private const uint ShgfiUseFileAttributes = 0x10;
private const uint FileAttributeDirectory = 0x10;
private const uint FileAttributeNormal = 0x80;
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern nint SHGetFileInfo(string pszPath, uint dwFileAttributes, ref ShFileInfo psfi, uint cbFileInfo, uint uFlags);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool DestroyIcon(nint hIcon);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct ShFileInfo
{
public nint hIcon;
public int iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
public string szTypeName;
}
}

View File

@@ -0,0 +1,22 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="Bg" Color="#1C1C1C"/>
<SolidColorBrush x:Key="Panel" Color="#252526"/>
<SolidColorBrush x:Key="Fill" Color="#2D2D30"/>
<SolidColorBrush x:Key="FillHover" Color="#3E3E42"/>
<SolidColorBrush x:Key="InputBg" Color="#1E1E1E"/>
<SolidColorBrush x:Key="Fg" Color="#F3F3F3"/>
<SolidColorBrush x:Key="FgMuted" Color="#C8C8C8"/>
<SolidColorBrush x:Key="Stroke" Color="#3F3F46"/>
<SolidColorBrush x:Key="Accent" Color="#60CDFF"/>
<SolidColorBrush x:Key="Danger" Color="#E81123"/>
<SolidColorBrush x:Key="Banner" Color="#264F78"/>
<SolidColorBrush x:Key="ActivePane" Color="#60CDFF"/>
<SolidColorBrush x:Key="ActivePaneFill" Color="#1E2A32"/>
<SolidColorBrush x:Key="ListSelection" Color="#264F78"/>
<SolidColorBrush x:Key="TreeSelection" Color="#2B4B63"/>
<SolidColorBrush x:Key="ScrollTrack" Color="#252526"/>
<SolidColorBrush x:Key="ScrollThumb" Color="#5A5A5E"/>
<SolidColorBrush x:Key="ScrollThumbHover" Color="#7A7A7E"/>
<SolidColorBrush x:Key="ScrollThumbPressed" Color="#9A9A9E"/>
</ResourceDictionary>

View File

@@ -0,0 +1,22 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="Bg" Color="#F3F3F3"/>
<SolidColorBrush x:Key="Panel" Color="#FFFFFF"/>
<SolidColorBrush x:Key="Fill" Color="#E8E8E8"/>
<SolidColorBrush x:Key="FillHover" Color="#DADADA"/>
<SolidColorBrush x:Key="InputBg" Color="#FFFFFF"/>
<SolidColorBrush x:Key="Fg" Color="#1A1A1A"/>
<SolidColorBrush x:Key="FgMuted" Color="#5A5A5A"/>
<SolidColorBrush x:Key="Stroke" Color="#D0D0D0"/>
<SolidColorBrush x:Key="Accent" Color="#0078D4"/>
<SolidColorBrush x:Key="Danger" Color="#C42B1C"/>
<SolidColorBrush x:Key="Banner" Color="#D6E8F7"/>
<SolidColorBrush x:Key="ActivePane" Color="#0078D4"/>
<SolidColorBrush x:Key="ActivePaneFill" Color="#E8F4FC"/>
<SolidColorBrush x:Key="ListSelection" Color="#CDE6F7"/>
<SolidColorBrush x:Key="TreeSelection" Color="#D6E8F7"/>
<SolidColorBrush x:Key="ScrollTrack" Color="#EDEDED"/>
<SolidColorBrush x:Key="ScrollThumb" Color="#B0B0B0"/>
<SolidColorBrush x:Key="ScrollThumbHover" Color="#8A8A8A"/>
<SolidColorBrush x:Key="ScrollThumbPressed" Color="#6A6A6A"/>
</ResourceDictionary>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Explorer.App"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>