972 lines
22 KiB
C#
972 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Media;
|
|
using System.Windows.Threading;
|
|
using AxCopilot.Core;
|
|
using AxCopilot.Handlers;
|
|
using AxCopilot.Models;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Services;
|
|
|
|
namespace AxCopilot.ViewModels;
|
|
|
|
public class LauncherViewModel : INotifyPropertyChanged
|
|
{
|
|
private sealed class FavJson
|
|
{
|
|
[JsonPropertyName("name")]
|
|
public string Name { get; set; } = "";
|
|
|
|
[JsonPropertyName("path")]
|
|
public string Path { get; set; } = "";
|
|
}
|
|
|
|
private readonly CommandResolver _resolver;
|
|
|
|
private readonly SettingsService _settings;
|
|
|
|
private string _inputText = "";
|
|
|
|
private LauncherItem? _selectedItem;
|
|
|
|
private bool _isLoading;
|
|
|
|
private CancellationTokenSource? _searchCts;
|
|
|
|
private Timer? _debounceTimer;
|
|
|
|
private const int DebounceMs = 30;
|
|
|
|
private string _lastSearchQuery = "";
|
|
|
|
private bool _isActionMode;
|
|
|
|
private LauncherItem? _actionSourceItem;
|
|
|
|
private string _savedQuery = "";
|
|
|
|
private readonly HashSet<ClipboardEntry> _mergeQueue = new HashSet<ClipboardEntry>();
|
|
|
|
private string _placeholderText = L10n.Get("placeholder");
|
|
|
|
private static readonly Dictionary<string, (string Label, string Symbol, string ColorHex)> PrefixMap = new Dictionary<string, (string, string, string)>
|
|
{
|
|
{
|
|
"@",
|
|
("URL", "\ue774", "#0078D4")
|
|
},
|
|
{
|
|
"~",
|
|
("워크", "\ue8a1", "#C50F1F")
|
|
},
|
|
{
|
|
">",
|
|
("명령", "\ue756", "#323130")
|
|
},
|
|
{
|
|
"$",
|
|
("클립", "\ue77f", "#8764B8")
|
|
},
|
|
{
|
|
"cd",
|
|
("폴더", "\ue8b7", "#107C10")
|
|
},
|
|
{
|
|
"#",
|
|
("히스", "\ue81c", "#B7791F")
|
|
},
|
|
{
|
|
";",
|
|
("스닛", "\ue70b", "#0F6CBD")
|
|
},
|
|
{
|
|
"=",
|
|
("계산", "\ue8ef", "#4B5EFC")
|
|
},
|
|
{
|
|
"!",
|
|
("AI", "\ue8bd", "#8B2FC9")
|
|
},
|
|
{
|
|
"?",
|
|
("검색", "\ue774", "#006EAF")
|
|
},
|
|
{
|
|
"/",
|
|
("시스템", "\ue7e8", "#4A4A4A")
|
|
},
|
|
{
|
|
"kill ",
|
|
("킬", "\uea39", "#CC2222")
|
|
},
|
|
{
|
|
"media ",
|
|
("미디어", "\ue768", "#1A6B3C")
|
|
},
|
|
{
|
|
"info ",
|
|
("시스템", "\ue7f4", "#5B4E7E")
|
|
},
|
|
{
|
|
"port",
|
|
("포트", "\ue968", "#006699")
|
|
},
|
|
{
|
|
"emoji",
|
|
("이모지", "\ue76e", "#F59E0B")
|
|
},
|
|
{
|
|
"color",
|
|
("색상", "\ue771", "#EC4899")
|
|
},
|
|
{
|
|
"recent",
|
|
("최근", "\ue81c", "#059669")
|
|
},
|
|
{
|
|
"note",
|
|
("메모", "\ue70b", "#7C3AED")
|
|
},
|
|
{
|
|
"uninstall",
|
|
("제거", "\ue74d", "#DC2626")
|
|
},
|
|
{
|
|
"env",
|
|
("환경변수", "\ue8d7", "#0D9488")
|
|
},
|
|
{
|
|
"json",
|
|
("JSON", "\ue930", "#D97706")
|
|
},
|
|
{
|
|
"encode ",
|
|
("인코딩", "\ue8cb", "#6366F1")
|
|
},
|
|
{
|
|
"snap",
|
|
("스냅", "\ue8a0", "#B45309")
|
|
},
|
|
{
|
|
"cap",
|
|
("캡처", "\ue722", "#BE185D")
|
|
},
|
|
{
|
|
"help",
|
|
("도움말", "\ue946", "#6B7280")
|
|
}
|
|
};
|
|
|
|
public BulkObservableCollection<LauncherItem> Results { get; } = new BulkObservableCollection<LauncherItem>();
|
|
|
|
public string InputText
|
|
{
|
|
get
|
|
{
|
|
return _inputText;
|
|
}
|
|
set
|
|
{
|
|
if (_inputText == value)
|
|
{
|
|
return;
|
|
}
|
|
_inputText = value;
|
|
OnPropertyChanged("InputText");
|
|
OnPropertyChanged("HasActivePrefix");
|
|
OnPropertyChanged("ActivePrefixLabel");
|
|
OnPropertyChanged("ActivePrefixSymbol");
|
|
OnPropertyChanged("ActivePrefixBrush");
|
|
OnPropertyChanged("IsClipboardMode");
|
|
OnPropertyChanged("ShowMergeHint");
|
|
OnPropertyChanged("MergeHintText");
|
|
_searchCts?.Cancel();
|
|
_debounceTimer?.Dispose();
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
Results.Clear();
|
|
return;
|
|
}
|
|
string captured = value;
|
|
_debounceTimer = new Timer(delegate(object? _)
|
|
{
|
|
Application current = Application.Current;
|
|
if (current != null)
|
|
{
|
|
((DispatcherObject)current).Dispatcher.InvokeAsync<object>((Func<object>)(() => _ = SearchAsync(captured)));
|
|
}
|
|
}, null, 30, -1);
|
|
}
|
|
}
|
|
|
|
public LauncherItem? SelectedItem
|
|
{
|
|
get
|
|
{
|
|
return _selectedItem;
|
|
}
|
|
set
|
|
{
|
|
_selectedItem = value;
|
|
OnPropertyChanged("SelectedItem");
|
|
}
|
|
}
|
|
|
|
public bool IsLoading
|
|
{
|
|
get
|
|
{
|
|
return _isLoading;
|
|
}
|
|
set
|
|
{
|
|
_isLoading = value;
|
|
OnPropertyChanged("IsLoading");
|
|
}
|
|
}
|
|
|
|
public string PlaceholderText
|
|
{
|
|
get
|
|
{
|
|
return _placeholderText;
|
|
}
|
|
private set
|
|
{
|
|
_placeholderText = value;
|
|
OnPropertyChanged("PlaceholderText");
|
|
}
|
|
}
|
|
|
|
public bool EnableIconAnimation => _settings.Settings.Launcher.EnableIconAnimation;
|
|
|
|
public bool EnableRainbowGlow => _settings.Settings.Launcher.EnableRainbowGlow;
|
|
|
|
public bool EnableSelectionGlow => _settings.Settings.Launcher.EnableSelectionGlow;
|
|
|
|
public bool ShowLauncherBorder => _settings.Settings.Launcher.ShowLauncherBorder;
|
|
|
|
public string ThemeSetting => _settings.Settings.Launcher.Theme;
|
|
|
|
public CustomThemeColors? CustomThemeColors => _settings.Settings.Launcher.CustomTheme;
|
|
|
|
public string WindowPosition => _settings.Settings.Launcher.Position;
|
|
|
|
public bool ShowNumberBadges => _settings.Settings.Launcher.ShowNumberBadges;
|
|
|
|
public bool CloseOnFocusLost => _settings.Settings.Launcher.CloseOnFocusLost;
|
|
|
|
public bool EnableActionMode => _settings.Settings.Launcher.EnableActionMode;
|
|
|
|
public bool HasActivePrefix => _settings.Settings.Launcher.ShowPrefixBadge && _inputText.Length > 0 && PrefixMap.Keys.Any((string k) => (k != "!" || IsAiEnabled()) && _inputText.StartsWith(k, StringComparison.OrdinalIgnoreCase));
|
|
|
|
public string? ActivePrefix => PrefixMap.Keys.FirstOrDefault((string k) => (k != "!" || IsAiEnabled()) && _inputText.StartsWith(k, StringComparison.OrdinalIgnoreCase));
|
|
|
|
public string ActivePrefixLabel
|
|
{
|
|
get
|
|
{
|
|
object result;
|
|
if (ActivePrefix != null && PrefixMap.TryGetValue(ActivePrefix, out (string, string, string) value))
|
|
{
|
|
(result, _, _) = value;
|
|
}
|
|
else
|
|
{
|
|
result = "";
|
|
}
|
|
return (string)result;
|
|
}
|
|
}
|
|
|
|
public string ActivePrefixSymbol
|
|
{
|
|
get
|
|
{
|
|
(string, string, string) value;
|
|
return (ActivePrefix != null && PrefixMap.TryGetValue(ActivePrefix, out value)) ? value.Item2 : "\ue721";
|
|
}
|
|
}
|
|
|
|
public SolidColorBrush ActivePrefixBrush
|
|
{
|
|
get
|
|
{
|
|
if (ActivePrefix != null && PrefixMap.TryGetValue(ActivePrefix, out (string, string, string) value))
|
|
{
|
|
Color color = (Color)ColorConverter.ConvertFromString(value.Item3);
|
|
return new SolidColorBrush(color);
|
|
}
|
|
return new SolidColorBrush(Color.FromRgb(75, 94, 252));
|
|
}
|
|
}
|
|
|
|
public bool IsActionMode
|
|
{
|
|
get
|
|
{
|
|
return _isActionMode;
|
|
}
|
|
private set
|
|
{
|
|
_isActionMode = value;
|
|
OnPropertyChanged("IsActionMode");
|
|
OnPropertyChanged("ShowActionModeBar");
|
|
OnPropertyChanged("ActionModeBreadcrumb");
|
|
}
|
|
}
|
|
|
|
public bool ShowActionModeBar => IsActionMode;
|
|
|
|
public string ActionModeBreadcrumb => _actionSourceItem?.Title ?? "";
|
|
|
|
public bool IsClipboardMode => _inputText.StartsWith("#", StringComparison.Ordinal);
|
|
|
|
public int MergeCount => _mergeQueue.Count;
|
|
|
|
public bool ShowMergeHint => _mergeQueue.Count > 0 && IsClipboardMode;
|
|
|
|
public string MergeHintText => (_mergeQueue.Count > 0) ? $"{_mergeQueue.Count}개 선택됨 · Shift+Enter로 합치기 · Esc로 취소" : "";
|
|
|
|
public event EventHandler? CloseRequested;
|
|
|
|
public event EventHandler<string>? NotificationRequested;
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
public void RefreshPlaceholder()
|
|
{
|
|
PlaceholderText = (_settings.Settings.Launcher.EnableRandomPlaceholder ? L10n.GetRandomPlaceholder() : L10n.Get("placeholder"));
|
|
}
|
|
|
|
private static bool IsAiEnabled()
|
|
{
|
|
return (Application.Current as App)?.SettingsService?.Settings.AiEnabled ?? true;
|
|
}
|
|
|
|
public bool CanEnterActionMode()
|
|
{
|
|
return !IsActionMode && SelectedItem?.Data is IndexEntry;
|
|
}
|
|
|
|
public bool IsItemMarkedForMerge(LauncherItem item)
|
|
{
|
|
return item.Data is ClipboardEntry item2 && _mergeQueue.Contains(item2);
|
|
}
|
|
|
|
public LauncherViewModel(CommandResolver resolver, SettingsService settings)
|
|
{
|
|
_resolver = resolver;
|
|
_settings = settings;
|
|
}
|
|
|
|
public void OnShown()
|
|
{
|
|
if (IsActionMode)
|
|
{
|
|
IsActionMode = false;
|
|
_actionSourceItem = null;
|
|
}
|
|
Results.Clear();
|
|
_lastSearchQuery = "";
|
|
ClearMerge();
|
|
}
|
|
|
|
internal Task TriggerImeSearchAsync(string text)
|
|
{
|
|
string text2 = text.Trim().ToLowerInvariant();
|
|
if (text2 == _lastSearchQuery && Results.Count > 0)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
return SearchAsync(text);
|
|
}
|
|
|
|
private async Task SearchAsync(string query)
|
|
{
|
|
_searchCts = new CancellationTokenSource();
|
|
CancellationToken ct = _searchCts.Token;
|
|
string queryKey = query.Trim().ToLowerInvariant();
|
|
bool isSameQuery = queryKey == _lastSearchQuery && Results.Count > 0;
|
|
if (!isSameQuery)
|
|
{
|
|
Results.Clear();
|
|
}
|
|
if (string.IsNullOrWhiteSpace(query))
|
|
{
|
|
_lastSearchQuery = "";
|
|
}
|
|
else
|
|
{
|
|
if ((!_settings.Settings.Launcher.EnableFavorites && query.StartsWith("fav", StringComparison.OrdinalIgnoreCase)) || (!_settings.Settings.Launcher.EnableRecent && query.StartsWith("recent", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
return;
|
|
}
|
|
LauncherItem prevSelected = (isSameQuery ? SelectedItem : null);
|
|
IsLoading = true;
|
|
try
|
|
{
|
|
IEnumerable<LauncherItem> items = await _resolver.ResolveAsync(query, ct);
|
|
if (ct.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
Results.ReplaceAll(items);
|
|
_lastSearchQuery = queryKey;
|
|
if (isSameQuery && prevSelected != null)
|
|
{
|
|
LauncherItem restored = Results.FirstOrDefault((LauncherItem r) => r.Data == prevSelected.Data || r.Title == prevSelected.Title);
|
|
SelectedItem = restored ?? Results.FirstOrDefault();
|
|
}
|
|
else
|
|
{
|
|
SelectedItem = Results.FirstOrDefault();
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
Exception ex3 = ex2;
|
|
LogService.Error("검색 오류: " + ex3.Message);
|
|
}
|
|
finally
|
|
{
|
|
if (!ct.IsCancellationRequested)
|
|
{
|
|
IsLoading = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task ExecuteSelectedAsync()
|
|
{
|
|
FileActionData fileAction = default(FileActionData);
|
|
int num;
|
|
if (IsActionMode)
|
|
{
|
|
object obj = SelectedItem?.Data;
|
|
fileAction = obj as FileActionData;
|
|
num = (((object)fileAction != null) ? 1 : 0);
|
|
}
|
|
else
|
|
{
|
|
num = 0;
|
|
}
|
|
if (num != 0)
|
|
{
|
|
ExecuteFileAction(fileAction);
|
|
ExitActionMode();
|
|
this.CloseRequested?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
else if (!(SelectedItem == null))
|
|
{
|
|
this.CloseRequested?.Invoke(this, EventArgs.Empty);
|
|
try
|
|
{
|
|
await _resolver.ExecuteAsync(SelectedItem, InputText, CancellationToken.None);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this.NotificationRequested?.Invoke(this, "실행 실패: " + ex.Message);
|
|
LogService.Error("Execute 오류: " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool ShowDelayTimerItems()
|
|
{
|
|
if (!(SelectedItem?.Data is string text))
|
|
{
|
|
return false;
|
|
}
|
|
if (text.StartsWith("delay:"))
|
|
{
|
|
return false;
|
|
}
|
|
if (!_resolver.RegisteredHandlers.TryGetValue(ActivePrefix ?? "", out IActionHandler value) || !(value is ScreenCaptureHandler screenCaptureHandler))
|
|
{
|
|
return false;
|
|
}
|
|
List<LauncherItem> list = screenCaptureHandler.GetDelayItems(text).ToList();
|
|
Results.Clear();
|
|
foreach (LauncherItem item in list)
|
|
{
|
|
Results.Add(item);
|
|
}
|
|
SelectedItem = Results.FirstOrDefault();
|
|
return true;
|
|
}
|
|
|
|
public void SelectNext()
|
|
{
|
|
if (Results.Count != 0)
|
|
{
|
|
int num = ((SelectedItem != null) ? Results.IndexOf(SelectedItem) : (-1));
|
|
SelectedItem = Results[(num + 1) % Results.Count];
|
|
}
|
|
}
|
|
|
|
public void SelectPrev()
|
|
{
|
|
if (Results.Count != 0)
|
|
{
|
|
int num = ((SelectedItem != null) ? Results.IndexOf(SelectedItem) : 0);
|
|
SelectedItem = Results[(num - 1 + Results.Count) % Results.Count];
|
|
}
|
|
}
|
|
|
|
public string GetLargeTypeText()
|
|
{
|
|
if (SelectedItem == null)
|
|
{
|
|
return "";
|
|
}
|
|
if (SelectedItem.Data is string text && !string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return text;
|
|
}
|
|
if (SelectedItem.Data is ClipboardEntry { IsText: not false } clipboardEntry)
|
|
{
|
|
return clipboardEntry.Text;
|
|
}
|
|
return SelectedItem.Title;
|
|
}
|
|
|
|
public void EnterActionMode(LauncherItem item)
|
|
{
|
|
if (_settings.Settings.Launcher.EnableActionMode && item.Data is IndexEntry indexEntry)
|
|
{
|
|
_actionSourceItem = item;
|
|
_savedQuery = _inputText;
|
|
IsActionMode = true;
|
|
string text = Environment.ExpandEnvironmentVariables(indexEntry.Path);
|
|
bool flag = Directory.Exists(text);
|
|
string fileName = Path.GetFileName(text);
|
|
Results.Clear();
|
|
Results.Add(MakeAction("경로 복사", text, FileAction.CopyPath, "\ue77f", "#8764B8"));
|
|
Results.Add(MakeAction("전체 경로 복사", text, FileAction.CopyFullPath, "\ue77f", "#C55A11"));
|
|
Results.Add(MakeAction("파일 탐색기에서 열기", "Explorer에서 위치 선택됨으로 표시", FileAction.OpenExplorer, "\ue8b7", "#107C10"));
|
|
if (!flag)
|
|
{
|
|
Results.Add(MakeAction("관리자 권한으로 실행", "UAC 권한 상승 후 실행", FileAction.RunAsAdmin, "\ue72e", "#C50F1F"));
|
|
}
|
|
Results.Add(MakeAction("터미널에서 열기", flag ? text : (Path.GetDirectoryName(text) ?? text), FileAction.OpenTerminal, "\ue756", "#323130"));
|
|
if (!flag)
|
|
{
|
|
Results.Add(MakeAction("파일 속성 보기", "Windows 속성 대화 상자 열기", FileAction.ShowProperties, "\ue946", "#6B2C91"));
|
|
}
|
|
Results.Add(MakeAction("이름 바꾸기", fileName, FileAction.Rename, "\ue8ac", "#D97706"));
|
|
Results.Add(MakeAction("휴지통으로 삭제", "복구 가능한 삭제 · 확인 후 실행", FileAction.DeleteToRecycleBin, "\ue74d", "#C50F1F"));
|
|
SelectedItem = Results.FirstOrDefault();
|
|
}
|
|
static LauncherItem MakeAction(string title, string subtitle, FileAction action, string symbol, string colorHex)
|
|
{
|
|
FileActionData data = new FileActionData(subtitle, action);
|
|
return new LauncherItem(title, subtitle, null, data, null, symbol);
|
|
}
|
|
}
|
|
|
|
public void ExitActionMode()
|
|
{
|
|
IsActionMode = false;
|
|
_actionSourceItem = null;
|
|
string savedQuery = _savedQuery;
|
|
_savedQuery = "";
|
|
SearchAsync(savedQuery);
|
|
}
|
|
|
|
private static void ExecuteFileAction(FileActionData data)
|
|
{
|
|
string path = data.Path;
|
|
switch (data.Action)
|
|
{
|
|
case FileAction.CopyPath:
|
|
((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate
|
|
{
|
|
Clipboard.SetText(path);
|
|
});
|
|
break;
|
|
case FileAction.OpenExplorer:
|
|
if (File.Exists(path))
|
|
{
|
|
Process.Start("explorer.exe", "/select,\"" + path + "\"");
|
|
}
|
|
else
|
|
{
|
|
Process.Start("explorer.exe", "\"" + path + "\"");
|
|
}
|
|
break;
|
|
case FileAction.RunAsAdmin:
|
|
try
|
|
{
|
|
Process.Start(new ProcessStartInfo(path)
|
|
{
|
|
UseShellExecute = true,
|
|
Verb = "runas"
|
|
});
|
|
break;
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
LogService.Warn("관리자 실행 취소: " + ex2.Message);
|
|
break;
|
|
}
|
|
case FileAction.CopyFullPath:
|
|
((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate
|
|
{
|
|
Clipboard.SetText(path);
|
|
});
|
|
break;
|
|
case FileAction.ShowProperties:
|
|
try
|
|
{
|
|
ProcessStartInfo startInfo = new ProcessStartInfo("explorer.exe")
|
|
{
|
|
Arguments = "/select,\"" + path + "\"",
|
|
UseShellExecute = true
|
|
};
|
|
Process.Start(startInfo);
|
|
ProcessStartInfo processStartInfo = new ProcessStartInfo
|
|
{
|
|
FileName = "rundll32.exe",
|
|
Arguments = "shell32.dll,ShellExec_RunDLL \"properties\" \"" + path + "\"",
|
|
UseShellExecute = false
|
|
};
|
|
try
|
|
{
|
|
Process.Start(new ProcessStartInfo(path)
|
|
{
|
|
UseShellExecute = true,
|
|
Verb = "properties"
|
|
});
|
|
break;
|
|
}
|
|
catch
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception ex3)
|
|
{
|
|
LogService.Warn("속성 열기 실패: " + ex3.Message);
|
|
break;
|
|
}
|
|
case FileAction.Rename:
|
|
break;
|
|
case FileAction.DeleteToRecycleBin:
|
|
break;
|
|
case FileAction.OpenTerminal:
|
|
{
|
|
string text = (File.Exists(path) ? (Path.GetDirectoryName(path) ?? path) : path);
|
|
try
|
|
{
|
|
Process.Start("wt.exe", "-d \"" + text + "\"");
|
|
break;
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
Process.Start("cmd.exe", "/k cd /d \"" + text + "\"");
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogService.Warn("터미널 열기 실패: " + ex.Message);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool CopySelectedPath()
|
|
{
|
|
if (SelectedItem?.Data is IndexEntry indexEntry)
|
|
{
|
|
string path = Path.GetFileName(Environment.ExpandEnvironmentVariables(indexEntry.Path));
|
|
((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate
|
|
{
|
|
Clipboard.SetText(path);
|
|
});
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public bool CopySelectedFullPath()
|
|
{
|
|
if (SelectedItem?.Data is IndexEntry indexEntry)
|
|
{
|
|
string path = Environment.ExpandEnvironmentVariables(indexEntry.Path);
|
|
((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate
|
|
{
|
|
Clipboard.SetText(path);
|
|
});
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public bool OpenSelectedInExplorer()
|
|
{
|
|
if (SelectedItem?.Data is IndexEntry indexEntry)
|
|
{
|
|
string text = Environment.ExpandEnvironmentVariables(indexEntry.Path);
|
|
if (File.Exists(text))
|
|
{
|
|
Process.Start("explorer.exe", "/select,\"" + text + "\"");
|
|
}
|
|
else if (Directory.Exists(text))
|
|
{
|
|
Process.Start("explorer.exe", "\"" + text + "\"");
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public bool RunSelectedAsAdmin()
|
|
{
|
|
if (SelectedItem?.Data is IndexEntry indexEntry)
|
|
{
|
|
string fileName = Environment.ExpandEnvironmentVariables(indexEntry.Path);
|
|
try
|
|
{
|
|
Process.Start(new ProcessStartInfo(fileName)
|
|
{
|
|
UseShellExecute = true,
|
|
Verb = "runas"
|
|
});
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogService.Warn("관리자 실행 취소: " + ex.Message);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public bool ShowSelectedProperties()
|
|
{
|
|
if (SelectedItem?.Data is IndexEntry indexEntry)
|
|
{
|
|
string text = Environment.ExpandEnvironmentVariables(indexEntry.Path);
|
|
try
|
|
{
|
|
Process.Start(new ProcessStartInfo(text)
|
|
{
|
|
UseShellExecute = true,
|
|
Verb = "properties"
|
|
});
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
Process.Start("explorer.exe", "/select,\"" + text + "\"");
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public bool RemoveSelectedFromRecent()
|
|
{
|
|
if (SelectedItem == null || Results.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
int val = Results.IndexOf(SelectedItem);
|
|
Results.Remove(SelectedItem);
|
|
if (Results.Count > 0)
|
|
{
|
|
SelectedItem = Results[Math.Min(val, Results.Count - 1)];
|
|
}
|
|
else
|
|
{
|
|
SelectedItem = null;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public void ClearInput()
|
|
{
|
|
InputText = "";
|
|
}
|
|
|
|
public void SelectFirst()
|
|
{
|
|
if (Results.Count > 0)
|
|
{
|
|
SelectedItem = Results[0];
|
|
}
|
|
}
|
|
|
|
public void SelectLast()
|
|
{
|
|
if (Results.Count > 0)
|
|
{
|
|
BulkObservableCollection<LauncherItem> results = Results;
|
|
SelectedItem = results[results.Count - 1];
|
|
}
|
|
}
|
|
|
|
public bool? ToggleFavorite()
|
|
{
|
|
if (!(SelectedItem?.Data is IndexEntry indexEntry))
|
|
{
|
|
return null;
|
|
}
|
|
string path = Environment.ExpandEnvironmentVariables(indexEntry.Path);
|
|
string text = Path.GetFileNameWithoutExtension(path);
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
text = Path.GetFileName(path);
|
|
}
|
|
string path2 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "favorites.json");
|
|
try
|
|
{
|
|
JsonSerializerOptions options = new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true,
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
List<FavJson> list = new List<FavJson>();
|
|
if (File.Exists(path2))
|
|
{
|
|
list = JsonSerializer.Deserialize<List<FavJson>>(File.ReadAllText(path2), options) ?? new List<FavJson>();
|
|
}
|
|
FavJson favJson = list.FirstOrDefault((FavJson f) => f.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
|
|
if (favJson != null)
|
|
{
|
|
list.Remove(favJson);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path2));
|
|
File.WriteAllText(path2, JsonSerializer.Serialize(list, options));
|
|
return false;
|
|
}
|
|
list.Insert(0, new FavJson
|
|
{
|
|
Name = text,
|
|
Path = path
|
|
});
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path2));
|
|
File.WriteAllText(path2, JsonSerializer.Serialize(list, options));
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogService.Warn("즐겨찾기 토글 실패: " + ex.Message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public bool OpenSelectedInTerminal()
|
|
{
|
|
string text2;
|
|
if (SelectedItem?.Data is IndexEntry indexEntry)
|
|
{
|
|
string text = Environment.ExpandEnvironmentVariables(indexEntry.Path);
|
|
text2 = (Directory.Exists(text) ? text : (Path.GetDirectoryName(text) ?? text));
|
|
}
|
|
else
|
|
{
|
|
text2 = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
|
}
|
|
try
|
|
{
|
|
Process.Start("wt.exe", "-d \"" + text2 + "\"");
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
Process.Start("cmd.exe", "/k cd /d \"" + text2 + "\"");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogService.Warn("터미널 열기 실패: " + ex.Message);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void NavigateToDownloads()
|
|
{
|
|
string text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads");
|
|
InputText = "cd " + text;
|
|
}
|
|
|
|
public void ToggleMergeItem(LauncherItem? item)
|
|
{
|
|
if (item?.Data is ClipboardEntry { IsText: not false } clipboardEntry)
|
|
{
|
|
if (!_mergeQueue.Remove(clipboardEntry))
|
|
{
|
|
_mergeQueue.Add(clipboardEntry);
|
|
}
|
|
OnPropertyChanged("MergeCount");
|
|
OnPropertyChanged("ShowMergeHint");
|
|
OnPropertyChanged("MergeHintText");
|
|
}
|
|
}
|
|
|
|
public void ExecuteMerge()
|
|
{
|
|
if (_mergeQueue.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
List<string> list = (from r in Results
|
|
where r.Data is ClipboardEntry item && _mergeQueue.Contains(item)
|
|
select ((ClipboardEntry)r.Data).Text).ToList();
|
|
if (list.Count == 0)
|
|
{
|
|
list = _mergeQueue.Select((ClipboardEntry e) => e.Text).ToList();
|
|
}
|
|
string merged = string.Join("\n", list);
|
|
try
|
|
{
|
|
((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate
|
|
{
|
|
Clipboard.SetText(merged);
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogService.Warn("병합 클립보드 실패: " + ex.Message);
|
|
}
|
|
ClearMerge();
|
|
this.CloseRequested?.Invoke(this, EventArgs.Empty);
|
|
LogService.Info($"클립보드 병합: {list.Count}개 항목");
|
|
}
|
|
|
|
public void ClearMerge()
|
|
{
|
|
_mergeQueue.Clear();
|
|
OnPropertyChanged("MergeCount");
|
|
OnPropertyChanged("ShowMergeHint");
|
|
OnPropertyChanged("MergeHintText");
|
|
}
|
|
|
|
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
|
{
|
|
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
}
|
|
}
|