193 lines
5.5 KiB
C#
193 lines
5.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Media.Imaging;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Services;
|
|
|
|
namespace AxCopilot.Handlers;
|
|
|
|
public class ClipboardHistoryHandler : IActionHandler
|
|
{
|
|
[StructLayout(LayoutKind.Explicit, Size = 40)]
|
|
private struct INPUT
|
|
{
|
|
[FieldOffset(0)]
|
|
public uint Type;
|
|
|
|
[FieldOffset(8)]
|
|
public KEYBDINPUT ki;
|
|
}
|
|
|
|
private struct KEYBDINPUT
|
|
{
|
|
public ushort wVk;
|
|
|
|
public ushort wScan;
|
|
|
|
public uint dwFlags;
|
|
|
|
public uint time;
|
|
|
|
public nint dwExtraInfo;
|
|
}
|
|
|
|
private readonly ClipboardHistoryService _historyService;
|
|
|
|
private static readonly Dictionary<string, string> CategoryFilters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
{ "url", "URL" },
|
|
{ "코드", "코드" },
|
|
{ "code", "코드" },
|
|
{ "경로", "경로" },
|
|
{ "path", "경로" },
|
|
{ "핀", "핀" },
|
|
{ "pin", "핀" }
|
|
};
|
|
|
|
public string? Prefix => "#";
|
|
|
|
public PluginMetadata Metadata => new PluginMetadata("ClipboardHistory", "클립보드 히스토리 — # 뒤에 검색어 (또는 빈 입력으로 전체 보기)", "1.0", "AX");
|
|
|
|
public ClipboardHistoryHandler(ClipboardHistoryService historyService)
|
|
{
|
|
_historyService = historyService;
|
|
}
|
|
|
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
|
{
|
|
IReadOnlyList<ClipboardEntry> history = _historyService.History;
|
|
if (history.Count == 0)
|
|
{
|
|
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("클립보드 히스토리가 없습니다", "텍스트를 복사하면 이 곳에 기록됩니다", null, null, null, "\ue81c")));
|
|
}
|
|
string q = query.Trim().ToLowerInvariant();
|
|
string catFilter = null;
|
|
foreach (var (text3, text4) in CategoryFilters)
|
|
{
|
|
if (q == text3 || q.StartsWith(text3 + " "))
|
|
{
|
|
catFilter = text4;
|
|
object obj;
|
|
if (q.Length <= text3.Length)
|
|
{
|
|
obj = "";
|
|
}
|
|
else
|
|
{
|
|
string text5 = q;
|
|
int num = text3.Length + 1;
|
|
obj = text5.Substring(num, text5.Length - num).Trim();
|
|
}
|
|
q = (string)obj;
|
|
break;
|
|
}
|
|
}
|
|
IEnumerable<ClipboardEntry> source = history.AsEnumerable();
|
|
if (catFilter == "핀")
|
|
{
|
|
source = source.Where((ClipboardEntry e) => e.IsPinned);
|
|
}
|
|
else if (catFilter != null)
|
|
{
|
|
source = source.Where((ClipboardEntry e) => e.Category == catFilter);
|
|
}
|
|
if (!string.IsNullOrEmpty(q))
|
|
{
|
|
source = source.Where((ClipboardEntry e) => e.Preview.ToLowerInvariant().Contains(q));
|
|
}
|
|
IOrderedEnumerable<ClipboardEntry> source2 = from e in source
|
|
orderby e.IsPinned descending, e.CopiedAt descending
|
|
select e;
|
|
List<LauncherItem> list = source2.Select(delegate(ClipboardEntry e)
|
|
{
|
|
string text6 = (e.IsPinned ? "\ud83d\udccc " : "");
|
|
string value = ((e.Category != "일반") ? ("[" + e.Category + "] ") : "");
|
|
return new LauncherItem(text6 + e.Preview, $"{value}{e.RelativeTime} · {e.CopiedAt:MM/dd HH:mm}", null, e, null, e.IsPinned ? "\ue728" : (e.IsText ? "\ue81c" : "\ueb9f"));
|
|
}).ToList();
|
|
if (list.Count == 0)
|
|
{
|
|
list.Add(new LauncherItem("'" + query + "'에 해당하는 항목 없음", "#pin #url #코드 #경로 로 필터링 가능", null, null, null, "\ue81c"));
|
|
}
|
|
return Task.FromResult((IEnumerable<LauncherItem>)list);
|
|
}
|
|
|
|
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
|
{
|
|
object data = item.Data;
|
|
if (!(data is ClipboardEntry entry))
|
|
{
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
_historyService.SuppressNextCapture();
|
|
_historyService.PromoteEntry(entry);
|
|
if (!entry.IsText && entry.Image != null)
|
|
{
|
|
BitmapSource originalImg = ClipboardHistoryService.LoadOriginalImage(entry.OriginalImagePath);
|
|
Clipboard.SetImage(originalImg ?? entry.Image);
|
|
}
|
|
else if (!string.IsNullOrEmpty(entry.Text))
|
|
{
|
|
Clipboard.SetText(entry.Text);
|
|
nint prevWindow = WindowTracker.PreviousWindow;
|
|
if (prevWindow != IntPtr.Zero)
|
|
{
|
|
await Task.Delay(300, ct);
|
|
uint lpdwProcessId;
|
|
uint targetThread = GetWindowThreadProcessId(prevWindow, out lpdwProcessId);
|
|
uint currentThread = GetCurrentThreadId();
|
|
AttachThreadInput(currentThread, targetThread, fAttach: true);
|
|
SetForegroundWindow(prevWindow);
|
|
AttachThreadInput(currentThread, targetThread, fAttach: false);
|
|
await Task.Delay(100, ct);
|
|
SendCtrlV();
|
|
}
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
LogService.Warn("클립보드 히스토리 붙여넣기 실패: " + ex2.Message);
|
|
}
|
|
}
|
|
|
|
private static void SendCtrlV()
|
|
{
|
|
INPUT[] array = new INPUT[4];
|
|
array[0].Type = 1u;
|
|
array[0].ki.wVk = 17;
|
|
array[1].Type = 1u;
|
|
array[1].ki.wVk = 86;
|
|
array[2].Type = 1u;
|
|
array[2].ki.wVk = 86;
|
|
array[2].ki.dwFlags = 2u;
|
|
array[3].Type = 1u;
|
|
array[3].ki.wVk = 17;
|
|
array[3].ki.dwFlags = 2u;
|
|
SendInput((uint)array.Length, array, Marshal.SizeOf<INPUT>());
|
|
}
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool SetForegroundWindow(nint hWnd);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint lpdwProcessId);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach);
|
|
|
|
[DllImport("kernel32.dll")]
|
|
private static extern uint GetCurrentThreadId();
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray)] INPUT[] pInputs, int cbSize);
|
|
}
|