런처 Agent Compare 기능 1차 이식 및 현재 런처 구조 연결
- Agent Compare 기준으로 런처 빠른 실행 칩, 검색 히스토리 탐색, 선택 항목 미리보기 패널을 현재 런처에 이식 - 하단 위젯 바, QuickLook(F3), 화면 OCR(F4), 관련 서비스/partial 파일을 현재 LauncherWindow/LauncherViewModel 구조에 연결 - UsageRankingService 상위 항목 조회와 SearchHistoryService를 추가해 실행 상위 경로/검색 기록이 실제 런처 동작에 반영되도록 정리 - README.md, docs/DEVELOPMENT.md에 이식 범위와 검증 결과를 2026-04-05 11:58 (KST) 기준으로 기록 검증 결과 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
This commit is contained in:
67
src/AxCopilot/Views/LauncherWindow.Shell.cs
Normal file
67
src/AxCopilot/Views/LauncherWindow.Shell.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class LauncherWindow
|
||||
{
|
||||
private QuickLookWindow? _quickLookWindow;
|
||||
|
||||
private async void QuickActionChip_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (((FrameworkElement)sender).DataContext is not QuickActionChip chip)
|
||||
return;
|
||||
|
||||
var expanded = Environment.ExpandEnvironmentVariables(chip.Path);
|
||||
Hide();
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Run(() =>
|
||||
System.Diagnostics.Process.Start(
|
||||
new System.Diagnostics.ProcessStartInfo(expanded)
|
||||
{
|
||||
UseShellExecute = true
|
||||
}));
|
||||
_ = Task.Run(() => Services.UsageRankingService.RecordExecution(chip.Path));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Services.LogService.Error($"빠른 실행 칩 열기 실패: {expanded} - {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal void ToggleQuickLook()
|
||||
{
|
||||
if (_quickLookWindow != null)
|
||||
{
|
||||
_quickLookWindow.Close();
|
||||
_quickLookWindow = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_vm.SelectedItem?.Data is not Services.IndexEntry indexEntry)
|
||||
return;
|
||||
|
||||
var path = Environment.ExpandEnvironmentVariables(indexEntry.Path);
|
||||
if (!System.IO.File.Exists(path) && !System.IO.Directory.Exists(path))
|
||||
return;
|
||||
|
||||
var qlLeft = Left + ActualWidth + 8;
|
||||
var qlTop = Top;
|
||||
|
||||
var screen = System.Windows.Forms.Screen.FromHandle(
|
||||
new System.Windows.Interop.WindowInteropHelper(this).Handle);
|
||||
if (qlLeft + 400 > screen.WorkingArea.Right)
|
||||
qlLeft = Left - 408;
|
||||
|
||||
_quickLookWindow = new QuickLookWindow(path, this)
|
||||
{
|
||||
Left = qlLeft,
|
||||
Top = qlTop
|
||||
};
|
||||
_quickLookWindow.Closed += (_, _) => _quickLookWindow = null;
|
||||
_quickLookWindow.Show();
|
||||
}
|
||||
}
|
||||
209
src/AxCopilot/Views/LauncherWindow.Widgets.cs
Normal file
209
src/AxCopilot/Views/LauncherWindow.Widgets.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class LauncherWindow
|
||||
{
|
||||
private DispatcherTimer? _widgetTimer;
|
||||
private static readonly SolidColorBrush DotOnline = new(Color.FromRgb(0x10, 0xB9, 0x81));
|
||||
private static readonly SolidColorBrush DotOffline = new(Color.FromRgb(0x9E, 0x9E, 0x9E));
|
||||
private int _widgetBatteryTick;
|
||||
private int _widgetWeatherTick;
|
||||
|
||||
internal void StartWidgetUpdates()
|
||||
{
|
||||
var settings = CurrentApp?.SettingsService?.Settings;
|
||||
|
||||
PerformanceMonitorService.Instance.StartPolling();
|
||||
ServerStatusService.Instance.Start(settings);
|
||||
PomodoroService.Instance.StateChanged -= OnPomoStateChanged;
|
||||
PomodoroService.Instance.StateChanged += OnPomoStateChanged;
|
||||
ServerStatusService.Instance.StatusChanged -= OnServerStatusChanged;
|
||||
ServerStatusService.Instance.StatusChanged += OnServerStatusChanged;
|
||||
|
||||
_vm.UpdateWidgets();
|
||||
UpdateServerDots();
|
||||
UpdateBatteryWidget();
|
||||
_ = RefreshWeatherAsync();
|
||||
|
||||
if (_widgetTimer == null)
|
||||
{
|
||||
_widgetTimer = new DispatcherTimer(DispatcherPriority.Background)
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
_widgetTimer.Tick += (_, _) =>
|
||||
{
|
||||
_vm.UpdateWidgets();
|
||||
UpdateServerDots();
|
||||
if (_vm.Widget_PerfText.Length > 0 && _widgetBatteryTick++ % 30 == 0)
|
||||
UpdateBatteryWidget();
|
||||
if (_widgetWeatherTick++ % 120 == 0)
|
||||
_ = RefreshWeatherAsync();
|
||||
};
|
||||
}
|
||||
|
||||
_widgetTimer.Start();
|
||||
UpdatePomoWidgetStyle();
|
||||
}
|
||||
|
||||
internal void StopWidgetUpdates()
|
||||
{
|
||||
_widgetTimer?.Stop();
|
||||
PerformanceMonitorService.Instance.StopPolling();
|
||||
PomodoroService.Instance.StateChanged -= OnPomoStateChanged;
|
||||
ServerStatusService.Instance.StatusChanged -= OnServerStatusChanged;
|
||||
}
|
||||
|
||||
private void OnPomoStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_vm.UpdateWidgets();
|
||||
UpdatePomoWidgetStyle();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnServerStatusChanged(object? sender, EventArgs e)
|
||||
=> Dispatcher.InvokeAsync(UpdateServerDots);
|
||||
|
||||
private void UpdateServerDots()
|
||||
{
|
||||
var server = ServerStatusService.Instance;
|
||||
if (OllamaStatusDot != null)
|
||||
OllamaStatusDot.Fill = server.OllamaOnline ? DotOnline : DotOffline;
|
||||
if (LlmStatusDot != null)
|
||||
LlmStatusDot.Fill = server.LlmOnline ? DotOnline : DotOffline;
|
||||
if (McpStatusDot != null)
|
||||
McpStatusDot.Fill = server.McpOnline ? DotOnline : DotOffline;
|
||||
}
|
||||
|
||||
private void UpdatePomoWidgetStyle()
|
||||
{
|
||||
if (WgtPomo == null)
|
||||
return;
|
||||
|
||||
var running = PomodoroService.Instance.IsRunning;
|
||||
WgtPomo.Background = running
|
||||
? new SolidColorBrush(Color.FromArgb(0x1E, 0xF5, 0x9E, 0x0B))
|
||||
: new SolidColorBrush(Color.FromArgb(0x0D, 0xF5, 0x9E, 0x0B));
|
||||
}
|
||||
|
||||
private void WgtPerf_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
_vm.InputText = "info ";
|
||||
InputBox?.Focus();
|
||||
}
|
||||
|
||||
private void WgtPomo_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
_vm.InputText = "pomo ";
|
||||
InputBox?.Focus();
|
||||
}
|
||||
|
||||
private void WgtNote_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
_vm.InputText = "note ";
|
||||
InputBox?.Focus();
|
||||
}
|
||||
|
||||
private void WgtServer_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
var settings = CurrentApp?.SettingsService?.Settings;
|
||||
ServerStatusService.Instance.Refresh(settings);
|
||||
_vm.InputText = "port";
|
||||
InputBox?.Focus();
|
||||
}
|
||||
|
||||
private void WgtWeather_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
var internalMode = CurrentApp?.SettingsService?.Settings.InternalModeEnabled ?? true;
|
||||
if (!internalMode)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("https://wttr.in") { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"날씨 페이지 열기 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WeatherWidgetService.Invalidate();
|
||||
_ = RefreshWeatherAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void WgtCal_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("ms-clock:") { UseShellExecute = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("outlookcal:") { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"달력 열기 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WgtBattery_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("ms-settings:powersleep") { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"전원 설정 열기 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateBatteryWidget()
|
||||
{
|
||||
try
|
||||
{
|
||||
var power = System.Windows.Forms.SystemInformation.PowerStatus;
|
||||
var pct = power.BatteryLifePercent;
|
||||
if (pct > 1.0f || pct < 0f)
|
||||
{
|
||||
_vm.Widget_BatteryVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_vm.Widget_BatteryVisible = true;
|
||||
var pctInt = (int)(pct * 100);
|
||||
var charging = power.PowerLineStatus == System.Windows.Forms.PowerLineStatus.Online;
|
||||
_vm.Widget_BatteryText = charging ? $"{pctInt}% 충전" : $"{pctInt}%";
|
||||
_vm.Widget_BatteryIcon = charging ? "\uE83E"
|
||||
: pctInt >= 85 ? "\uEBA7"
|
||||
: pctInt >= 70 ? "\uEBA5"
|
||||
: pctInt >= 50 ? "\uEBA3"
|
||||
: pctInt >= 25 ? "\uEBA1"
|
||||
: "\uEBA0";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"배터리 위젯 갱신 실패: {ex.Message}");
|
||||
_vm.Widget_BatteryVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshWeatherAsync()
|
||||
{
|
||||
var internalMode = CurrentApp?.SettingsService?.Settings.InternalModeEnabled ?? true;
|
||||
await WeatherWidgetService.RefreshAsync(internalMode);
|
||||
await Dispatcher.InvokeAsync(() => { _vm.Widget_WeatherText = WeatherWidgetService.CachedText; });
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@
|
||||
WindowStartupLocation="Manual"
|
||||
Loaded="Window_Loaded"
|
||||
Deactivated="Window_Deactivated"
|
||||
LocationChanged="Window_LocationChanged"
|
||||
IsVisibleChanged="Window_IsVisibleChanged"
|
||||
PreviewKeyDown="Window_PreviewKeyDown"
|
||||
KeyDown="Window_KeyDown">
|
||||
|
||||
@@ -201,10 +203,16 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ─── 입력 영역 ─── -->
|
||||
<Grid Grid.Row="0" Margin="20,16,20,16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -413,8 +421,57 @@
|
||||
Foreground="{DynamicResource HintText}"
|
||||
Margin="3,0,0,0"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<ItemsControl Grid.Row="1"
|
||||
Grid.ColumnSpan="3"
|
||||
ItemsSource="{Binding QuickActionItems}"
|
||||
Margin="0,10,0,0"
|
||||
Visibility="{Binding ShowQuickActions, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Margin="0,0,6,6"
|
||||
Padding="9,5"
|
||||
CornerRadius="10"
|
||||
Cursor="Hand"
|
||||
Background="{Binding Background}"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
MouseLeftButtonUp="QuickActionChip_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Symbol}"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,5,0"/>
|
||||
<TextBlock Text="{Binding Title}"
|
||||
FontFamily="Segoe UI, Malgun Gothic"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"
|
||||
MaxWidth="100"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
|
||||
<!-- ─── 파일 액션 모드 breadcrumb 바 ─── -->
|
||||
@@ -691,9 +748,31 @@
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<Border Grid.Row="5"
|
||||
x:Name="PreviewPanel"
|
||||
Visibility="{Binding HasPreview, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
CornerRadius="8"
|
||||
Margin="10,0,10,8"
|
||||
Padding="12,8"
|
||||
MaxHeight="100">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<ScrollViewer.Resources>
|
||||
<Style TargetType="ScrollBar" BasedOn="{StaticResource SlimScrollBar}"/>
|
||||
</ScrollViewer.Resources>
|
||||
<TextBlock Text="{Binding PreviewText}"
|
||||
FontFamily="Segoe UI Mono, Consolas, Malgun Gothic"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
TextWrapping="Wrap"
|
||||
LineHeight="16"/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 인덱싱 상태 바 ─── -->
|
||||
<TextBlock x:Name="IndexStatusText"
|
||||
Grid.Row="5"
|
||||
Grid.Row="6"
|
||||
Visibility="Collapsed"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
@@ -701,9 +780,179 @@
|
||||
Margin="0,0,0,8"
|
||||
Opacity="0.7"/>
|
||||
|
||||
<Border x:Name="WidgetBar"
|
||||
Grid.Row="7"
|
||||
BorderBrush="{DynamicResource SeparatorColor}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="10,7,10,9">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="6"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="6"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="6"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border x:Name="WgtPerf" Grid.Column="0"
|
||||
CornerRadius="5" Padding="8,5"
|
||||
Background="#0D60A5FA"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonUp="WgtPerf_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="#60A5FA"
|
||||
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="{Binding Widget_PerfText}"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="WgtPomo" Grid.Column="2"
|
||||
CornerRadius="5" Padding="8,5"
|
||||
Background="#0DF59E0B"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonUp="WgtPomo_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="#F59E0B"
|
||||
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock x:Name="WgtPomoText"
|
||||
Text="{Binding Widget_PomoText}"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="WgtNote" Grid.Column="4"
|
||||
CornerRadius="5" Padding="8,5"
|
||||
Background="#0D8B5CF6"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonUp="WgtNote_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="#8B5CF6"
|
||||
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="{Binding Widget_NoteText}"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="WgtServer" Grid.Column="6"
|
||||
CornerRadius="5" Padding="8,5"
|
||||
Background="#0D10B981"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonUp="WgtServer_Click">
|
||||
<StackPanel Orientation="Horizontal" x:Name="WgtServerContent">
|
||||
<Ellipse x:Name="OllamaStatusDot"
|
||||
Width="6" Height="6"
|
||||
Fill="#9E9E9E"
|
||||
VerticalAlignment="Center" Margin="0,0,3,0"/>
|
||||
<TextBlock Text="Ollama"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<Ellipse x:Name="LlmStatusDot"
|
||||
Width="6" Height="6"
|
||||
Fill="#9E9E9E"
|
||||
VerticalAlignment="Center" Margin="0,0,3,0"/>
|
||||
<TextBlock Text="API"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<Ellipse x:Name="McpStatusDot"
|
||||
Width="6" Height="6"
|
||||
Fill="#9E9E9E"
|
||||
VerticalAlignment="Center" Margin="0,0,3,0"/>
|
||||
<TextBlock x:Name="McpNameText"
|
||||
Text="{Binding Widget_McpName}"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Grid Margin="0,4,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="6"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="6"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border x:Name="WgtWeather" Grid.Column="0"
|
||||
CornerRadius="5" Padding="8,5"
|
||||
Background="#0D3B82F6"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonUp="WgtWeather_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="#60A5FA"
|
||||
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="{Binding Widget_WeatherText}"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxWidth="100"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="WgtCal" Grid.Column="2"
|
||||
CornerRadius="5" Padding="8,5"
|
||||
Background="#0DEC4899"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonUp="WgtCal_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="#EC4899"
|
||||
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="{Binding Widget_CalText}"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="WgtBattery" Grid.Column="4"
|
||||
CornerRadius="5" Padding="8,5"
|
||||
Background="#0D10B981"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonUp="WgtBattery_Click"
|
||||
Visibility="{Binding Widget_BatteryVisible, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Widget_BatteryIcon}"
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="#10B981"
|
||||
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="{Binding Widget_BatteryText}"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 토스트 오버레이 ─── -->
|
||||
<Border x:Name="ToastOverlay"
|
||||
Grid.Row="4" Grid.RowSpan="2"
|
||||
Grid.Row="4" Grid.RowSpan="4"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,0,12"
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace AxCopilot.Views;
|
||||
|
||||
public partial class LauncherWindow : Window
|
||||
{
|
||||
private static App? CurrentApp => System.Windows.Application.Current as App;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
@@ -34,6 +36,7 @@ public partial class LauncherWindow : Window
|
||||
|
||||
private readonly LauncherViewModel _vm;
|
||||
private System.Windows.Threading.DispatcherTimer? _indexStatusTimer;
|
||||
private System.Windows.Threading.DispatcherTimer? _toastTimer;
|
||||
|
||||
/// <summary>Ctrl+, 단축키로 설정 창을 여는 콜백 (App.xaml.cs에서 주입)</summary>
|
||||
public Action? OpenSettingsAction { get; set; }
|
||||
@@ -71,16 +74,9 @@ public partial class LauncherWindow : Window
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
var svc = app.IndexService;
|
||||
IndexStatusText.Text = $"✓ {svc.LastIndexCount:N0}개 항목 색인됨 ({svc.LastIndexDuration.TotalSeconds:F1}초)";
|
||||
IndexStatusText.Visibility = Visibility.Visible;
|
||||
// 기존 타이머 정리 후 5초 후 자동 숨기기
|
||||
_indexStatusTimer?.Stop();
|
||||
_indexStatusTimer = new System.Windows.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(5)
|
||||
};
|
||||
_indexStatusTimer.Tick += (_, _) => { IndexStatusText.Visibility = Visibility.Collapsed; _indexStatusTimer.Stop(); };
|
||||
_indexStatusTimer.Start();
|
||||
ShowIndexStatus(
|
||||
$"✓ {svc.LastIndexCount:N0}개 항목 색인됨 ({svc.LastIndexDuration.TotalSeconds:F1}초)",
|
||||
TimeSpan.FromSeconds(5));
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -88,7 +84,7 @@ public partial class LauncherWindow : Window
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CenterOnScreen();
|
||||
ApplyInitialPlacement();
|
||||
ApplyTheme();
|
||||
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, () =>
|
||||
{
|
||||
@@ -151,7 +147,7 @@ public partial class LauncherWindow : Window
|
||||
_vm.OnShown();
|
||||
_vm.InputText = "";
|
||||
base.Show();
|
||||
CenterOnScreen();
|
||||
ApplyInitialPlacement();
|
||||
AnimateIn();
|
||||
|
||||
// 포그라운드 강제 + 포커스를 3단계로 보장
|
||||
@@ -695,6 +691,64 @@ public partial class LauncherWindow : Window
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyInitialPlacement()
|
||||
{
|
||||
if (!TryRestoreRememberedPosition())
|
||||
CenterOnScreen();
|
||||
|
||||
UpdateRememberedPositionCache();
|
||||
}
|
||||
|
||||
private bool TryRestoreRememberedPosition()
|
||||
{
|
||||
var launcher = CurrentApp?.SettingsService?.Settings?.Launcher;
|
||||
if (launcher == null || !launcher.RememberPosition) return false;
|
||||
if (launcher.LastLeft < 0 || launcher.LastTop < 0) return false;
|
||||
|
||||
var rememberPoint = new Point(launcher.LastLeft, launcher.LastTop);
|
||||
if (!IsVisibleOnAnyScreen(rememberPoint)) return false;
|
||||
|
||||
Left = launcher.LastLeft;
|
||||
Top = launcher.LastTop;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsVisibleOnAnyScreen(Point point)
|
||||
{
|
||||
foreach (var screen in FormsScreen.AllScreens)
|
||||
{
|
||||
var bounds = screen.WorkingArea;
|
||||
if (point.X >= bounds.Left && point.X <= bounds.Right - 40 &&
|
||||
point.Y >= bounds.Top && point.Y <= bounds.Bottom - 40)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateRememberedPositionCache()
|
||||
{
|
||||
var launcher = CurrentApp?.SettingsService?.Settings?.Launcher;
|
||||
if (launcher == null || !launcher.RememberPosition || !IsLoaded) return;
|
||||
|
||||
launcher.LastLeft = Left;
|
||||
launcher.LastTop = Top;
|
||||
}
|
||||
|
||||
private void SaveRememberedPosition()
|
||||
{
|
||||
var app = CurrentApp;
|
||||
var settingsService = app?.SettingsService;
|
||||
if (settingsService == null) return;
|
||||
var launcher = settingsService.Settings.Launcher;
|
||||
if (launcher == null || !launcher.RememberPosition || !IsLoaded) return;
|
||||
|
||||
UpdateRememberedPositionCache();
|
||||
settingsService.Save();
|
||||
}
|
||||
|
||||
// 지원 테마 이름 목록
|
||||
private static readonly HashSet<string> KnownThemes =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -1031,8 +1085,7 @@ public partial class LauncherWindow : Window
|
||||
{
|
||||
var app = (App)System.Windows.Application.Current;
|
||||
_ = app.IndexService?.BuildAsync(CancellationToken.None);
|
||||
IndexStatusText.Text = "⟳ 인덱스 재구축 중…";
|
||||
IndexStatusText.Visibility = Visibility.Visible;
|
||||
ShowIndexStatus("⟳ 인덱스 재구축 중…", TimeSpan.FromSeconds(8));
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
@@ -1256,6 +1309,36 @@ public partial class LauncherWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── F3 → 파일 빠른 미리보기 (QuickLook 토글) ───────────────────────
|
||||
if (e.Key == Key.F3)
|
||||
{
|
||||
ToggleQuickLook();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── F4 → 화면 영역 OCR 즉시 실행 ─────────────────────────────────
|
||||
if (e.Key == Key.F4)
|
||||
{
|
||||
Hide();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var handler = new Handlers.OcrHandler();
|
||||
var item = new SDK.LauncherItem(
|
||||
"화면 영역 텍스트 추출", "", null, "__ocr_region__");
|
||||
await handler.ExecuteAsync(item, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Services.LogService.Error($"F4 OCR 실행 오류: {ex.Message}");
|
||||
}
|
||||
});
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Ctrl+1~9 → n번째 결과 즉시 실행 ───────────────────────────────
|
||||
if (mod == ModifierKeys.Control)
|
||||
{
|
||||
@@ -1295,6 +1378,8 @@ public partial class LauncherWindow : Window
|
||||
"[ 기능 ]",
|
||||
"F1 도움말",
|
||||
"F2 파일 이름 바꾸기",
|
||||
"F3 파일 빠른 미리보기",
|
||||
"F4 화면 OCR",
|
||||
"F5 인덱스 새로 고침",
|
||||
"Delete 항목 제거",
|
||||
"Ctrl+, 설정",
|
||||
@@ -1331,14 +1416,14 @@ public partial class LauncherWindow : Window
|
||||
var fadeIn = (System.Windows.Media.Animation.Storyboard)FindResource("ToastFadeIn");
|
||||
fadeIn.Begin(this);
|
||||
|
||||
_indexStatusTimer?.Stop();
|
||||
_indexStatusTimer = new System.Windows.Threading.DispatcherTimer
|
||||
_toastTimer?.Stop();
|
||||
_toastTimer = new System.Windows.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(2)
|
||||
};
|
||||
_indexStatusTimer.Tick += (_, _) =>
|
||||
_toastTimer.Tick += (_, _) =>
|
||||
{
|
||||
_indexStatusTimer.Stop();
|
||||
_toastTimer.Stop();
|
||||
// 페이드아웃 후 Collapsed
|
||||
var fadeOut = (System.Windows.Media.Animation.Storyboard)FindResource("ToastFadeOut");
|
||||
EventHandler? onCompleted = null;
|
||||
@@ -1350,6 +1435,24 @@ public partial class LauncherWindow : Window
|
||||
fadeOut.Completed += onCompleted;
|
||||
fadeOut.Begin(this);
|
||||
};
|
||||
_toastTimer.Start();
|
||||
}
|
||||
|
||||
private void ShowIndexStatus(string message, TimeSpan duration)
|
||||
{
|
||||
IndexStatusText.Text = message;
|
||||
IndexStatusText.Visibility = Visibility.Visible;
|
||||
|
||||
_indexStatusTimer?.Stop();
|
||||
_indexStatusTimer = new System.Windows.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = duration
|
||||
};
|
||||
_indexStatusTimer.Tick += (_, _) =>
|
||||
{
|
||||
_indexStatusTimer.Stop();
|
||||
IndexStatusText.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
_indexStatusTimer.Start();
|
||||
}
|
||||
|
||||
@@ -1560,6 +1663,28 @@ public partial class LauncherWindow : Window
|
||||
if (_vm.CloseOnFocusLost) Hide();
|
||||
}
|
||||
|
||||
private void Window_LocationChanged(object sender, EventArgs e)
|
||||
{
|
||||
UpdateRememberedPositionCache();
|
||||
}
|
||||
|
||||
private void Window_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.NewValue is not bool isVisible)
|
||||
return;
|
||||
|
||||
if (isVisible)
|
||||
{
|
||||
StartWidgetUpdates();
|
||||
return;
|
||||
}
|
||||
|
||||
_quickLookWindow?.Close();
|
||||
_quickLookWindow = null;
|
||||
StopWidgetUpdates();
|
||||
SaveRememberedPosition();
|
||||
}
|
||||
|
||||
private void ScrollToSelected()
|
||||
{
|
||||
if (_vm.SelectedItem != null)
|
||||
|
||||
161
src/AxCopilot/Views/QuickLookWindow.xaml
Normal file
161
src/AxCopilot/Views/QuickLookWindow.xaml
Normal file
@@ -0,0 +1,161 @@
|
||||
<Window x:Class="AxCopilot.Views.QuickLookWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Commander 빠른 미리보기"
|
||||
Width="400"
|
||||
Height="500"
|
||||
MinWidth="260"
|
||||
MinHeight="200"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="Manual"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
Topmost="True">
|
||||
<Border Background="{DynamicResource LauncherBackground}"
|
||||
CornerRadius="12"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
Margin="6">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="22" ShadowDepth="4" Opacity="0.32" Color="Black" Direction="270"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0"
|
||||
CornerRadius="12,12,0,0"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
MouseLeftButtonDown="TitleBar_MouseDown">
|
||||
<Grid Margin="14,0,8,0">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="FileTypeIcon"
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="15"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,1,10,0"/>
|
||||
<TextBlock x:Name="FileNameText"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxWidth="270"/>
|
||||
</StackPanel>
|
||||
<Border HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="4"
|
||||
Padding="8,4"
|
||||
Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnClose_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#40C05050"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<ScrollViewer x:Name="ImageScrollViewer"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Visibility="Collapsed"
|
||||
Background="{DynamicResource LauncherBackground}">
|
||||
<Image x:Name="PreviewImage"
|
||||
Stretch="Uniform"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="8"/>
|
||||
</ScrollViewer>
|
||||
|
||||
<ScrollViewer x:Name="TextScrollViewer"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Visibility="Collapsed">
|
||||
<TextBlock x:Name="PreviewText"
|
||||
FontFamily="Cascadia Code, Consolas, Courier New"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="14,12"/>
|
||||
</ScrollViewer>
|
||||
|
||||
<ScrollViewer x:Name="PdfScrollViewer"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Visibility="Collapsed">
|
||||
<TextBlock x:Name="PdfPreviewText"
|
||||
FontFamily="Segoe UI, Malgun Gothic"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="14,12"/>
|
||||
</ScrollViewer>
|
||||
|
||||
<StackPanel x:Name="InfoPanel"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Visibility="Collapsed">
|
||||
<TextBlock x:Name="InfoTypeIcon"
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="52"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock x:Name="InfoTypeName"
|
||||
FontSize="14"
|
||||
FontWeight="Medium"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,10,0,0"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
MaxWidth="320"/>
|
||||
<TextBlock x:Name="InfoSubText"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="2"
|
||||
CornerRadius="0,0,12,12"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="14,7">
|
||||
<Grid>
|
||||
<TextBlock x:Name="FooterPath"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="FooterMeta"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
143
src/AxCopilot/Views/QuickLookWindow.xaml.cs
Normal file
143
src/AxCopilot/Views/QuickLookWindow.xaml.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Imaging;
|
||||
using UglyToad.PdfPig;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class QuickLookWindow : Window
|
||||
{
|
||||
private static readonly HashSet<string> ImageExts = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".ico", ".tiff", ".tif"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> TextExts = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".txt", ".md", ".cs", ".vb", ".fs", ".py", ".js", ".ts", ".jsx", ".tsx",
|
||||
".json", ".xml", ".xaml", ".yaml", ".yml", ".toml", ".ini", ".conf",
|
||||
".log", ".csv", ".html", ".htm", ".css", ".scss", ".less",
|
||||
".sql", ".sh", ".bash", ".bat", ".cmd", ".ps1",
|
||||
".config", ".env", ".gitignore", ".editorconfig",
|
||||
".java", ".cpp", ".c", ".h", ".hpp", ".rs", ".go", ".rb", ".php", ".swift",
|
||||
".vue", ".svelte", ".dockerfile"
|
||||
};
|
||||
|
||||
public QuickLookWindow(string path, Window owner)
|
||||
{
|
||||
InitializeComponent();
|
||||
Owner = owner;
|
||||
KeyDown += OnKeyDown;
|
||||
Loaded += (_, _) => LoadPreview(path);
|
||||
}
|
||||
|
||||
private void OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key is Key.Escape or Key.F3)
|
||||
{
|
||||
Close();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
DragMove();
|
||||
}
|
||||
|
||||
private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Close();
|
||||
|
||||
private void LoadPreview(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
FileNameText.Text = Path.GetFileName(path);
|
||||
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
ShowInfo("\uE838", "폴더", path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
ShowInfo("\uE783", "파일을 찾을 수 없습니다.", path);
|
||||
return;
|
||||
}
|
||||
|
||||
var info = new FileInfo(path);
|
||||
var ext = Path.GetExtension(path);
|
||||
FooterPath.Text = path;
|
||||
FooterMeta.Text = $"{FormatSize(info.Length)} · {info.LastWriteTime:yyyy-MM-dd HH:mm}";
|
||||
|
||||
if (ImageExts.Contains(ext))
|
||||
{
|
||||
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
var image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||
image.StreamSource = fs;
|
||||
image.EndInit();
|
||||
image.Freeze();
|
||||
|
||||
PreviewImage.Source = image;
|
||||
ImageScrollViewer.Visibility = Visibility.Visible;
|
||||
FileTypeIcon.Text = "\uE91B";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
using var doc = PdfDocument.Open(path);
|
||||
var page = doc.NumberOfPages > 0 ? doc.GetPage(1).Text : "";
|
||||
PdfPreviewText.Text = page.Length > 1200 ? page[..1200] + "…" : page;
|
||||
PdfScrollViewer.Visibility = Visibility.Visible;
|
||||
FileTypeIcon.Text = "\uEA90";
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextExts.Contains(ext))
|
||||
{
|
||||
string text;
|
||||
try
|
||||
{
|
||||
text = File.ReadAllText(path, Encoding.UTF8);
|
||||
}
|
||||
catch
|
||||
{
|
||||
text = File.ReadAllText(path);
|
||||
}
|
||||
|
||||
PreviewText.Text = text.Length > 4000 ? text[..4000] + "\n…" : text;
|
||||
TextScrollViewer.Visibility = Visibility.Visible;
|
||||
FileTypeIcon.Text = "\uE8A5";
|
||||
return;
|
||||
}
|
||||
|
||||
ShowInfo("\uE7C3", $"파일 · {ext.TrimStart('.').ToUpperInvariant()}", path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowInfo("\uEA39", "미리보기를 열지 못했습니다.", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowInfo(string icon, string title, string detail)
|
||||
{
|
||||
InfoTypeIcon.Text = icon;
|
||||
InfoTypeName.Text = title;
|
||||
InfoSubText.Text = detail;
|
||||
InfoPanel.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private static string FormatSize(long bytes) => bytes switch
|
||||
{
|
||||
< 1024 => $"{bytes} B",
|
||||
< 1048576 => $"{bytes / 1024.0:F1} KB",
|
||||
< 1073741824 => $"{bytes / 1048576.0:F1} MB",
|
||||
_ => $"{bytes / 1073741824.0:F2} GB"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user