Files
AX-Copilot-Codex/.decompiledproj/AxCopilot/Services/ClipboardHistoryService.cs

582 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
namespace AxCopilot.Services;
public class ClipboardHistoryService : IDisposable
{
private class SavedClipEntry
{
public string Text { get; set; } = "";
public DateTime CopiedAt { get; set; }
public string? ImageBase64 { get; set; }
public string? OriginalImagePath { get; set; }
public bool IsPinned { get; set; }
public string Category { get; set; } = "일반";
}
private const int WM_CLIPBOARDUPDATE = 797;
private static readonly string HistoryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "clipboard_history.dat");
private static readonly string LegacyPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "clipboard_history.json");
private static readonly string ImageCachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "clipboard_images");
private const long MaxCacheSizeBytes = 524288000L;
private const int MaxCacheAgeDays = 30;
private static readonly JsonSerializerOptions JsonOpts = new JsonSerializerOptions
{
WriteIndented = false,
PropertyNameCaseInsensitive = true
};
private readonly SettingsService _settings;
private HwndSource? _msgSource;
private readonly object _lock = new object();
private volatile bool _ignoreNext;
private bool _disposed;
private readonly List<ClipboardEntry> _history = new List<ClipboardEntry>();
public IReadOnlyList<ClipboardEntry> History
{
get
{
lock (_lock)
{
return _history.ToList();
}
}
}
public event EventHandler? HistoryChanged;
public ClipboardHistoryService(SettingsService settings)
{
_settings = settings;
}
public void Initialize()
{
if (!_disposed && _msgSource == null && _settings.Settings.ClipboardHistory.Enabled)
{
LoadHistory();
((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate
{
HwndSourceParameters parameters = new HwndSourceParameters("axClipboardMonitor")
{
Width = 0,
Height = 0,
ParentWindow = new IntPtr(-3),
WindowStyle = 0,
ExtendedWindowStyle = 0
};
_msgSource = new HwndSource(parameters);
_msgSource.AddHook(WndProc);
AddClipboardFormatListener(_msgSource.Handle);
LogService.Info("클립보드 히스토리 서비스 시작");
});
}
}
public void SuppressNextCapture()
{
_ignoreNext = true;
}
public void PromoteEntry(ClipboardEntry entry)
{
lock (_lock)
{
_history.Remove(entry);
ClipboardEntry item = new ClipboardEntry(entry.Text, DateTime.Now)
{
Image = entry.Image,
OriginalImagePath = entry.OriginalImagePath,
IsPinned = entry.IsPinned,
Category = entry.Category
};
_history.Insert(0, item);
}
this.HistoryChanged?.Invoke(this, EventArgs.Empty);
SaveHistoryAsync();
}
public void ClearHistory()
{
lock (_lock)
{
_history.Clear();
}
this.HistoryChanged?.Invoke(this, EventArgs.Empty);
SaveHistoryAsync();
}
public void TogglePin(ClipboardEntry entry)
{
lock (_lock)
{
if (!entry.IsPinned)
{
int maxPinnedClipboardItems = _settings.Settings.Launcher.MaxPinnedClipboardItems;
int num = _history.Count((ClipboardEntry e) => e.IsPinned);
if (num >= maxPinnedClipboardItems)
{
return;
}
}
entry.IsPinned = !entry.IsPinned;
}
this.HistoryChanged?.Invoke(this, EventArgs.Empty);
SaveHistoryAsync();
}
private static string DetectCategory(string text)
{
string text2 = text.Trim();
if (Uri.TryCreate(text2, UriKind.Absolute, out Uri result) && (result.Scheme == "http" || result.Scheme == "https"))
{
return "URL";
}
if (text2.StartsWith("\\\\") || (text2.Length >= 3 && text2[1] == ':' && (text2[2] == '\\' || text2[2] == '/')))
{
return "경로";
}
if ((text2.Contains('{') && text2.Contains('}')) || text2.Contains("function ") || text2.Contains("class ") || text2.Contains("public ") || text2.Contains("private ") || text2.Contains("def ") || text2.Contains("import ") || text2.Contains("using "))
{
return "코드";
}
return "일반";
}
public void Reinitialize()
{
_disposed = false;
_msgSource = null;
Initialize();
LogService.Info("클립보드 히스토리 서비스 재초기화 완료");
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
if (_msgSource != null)
{
RemoveClipboardFormatListener(_msgSource.Handle);
_msgSource.Dispose();
_msgSource = null;
}
SaveHistorySync();
}
}
private void LoadHistory()
{
try
{
if (!File.Exists(HistoryPath) && File.Exists(LegacyPath))
{
MigrateLegacyFile();
}
if (!File.Exists(HistoryPath))
{
return;
}
byte[] encryptedData = File.ReadAllBytes(HistoryPath);
byte[] bytes = ProtectedData.Unprotect(encryptedData, null, DataProtectionScope.CurrentUser);
string json = Encoding.UTF8.GetString(bytes);
List<SavedClipEntry> list = JsonSerializer.Deserialize<List<SavedClipEntry>>(json, JsonOpts);
if (list == null)
{
return;
}
int maxItems = _settings.Settings.ClipboardHistory.MaxItems;
lock (_lock)
{
foreach (SavedClipEntry item in list.Take(maxItems))
{
if (!string.IsNullOrEmpty(item.ImageBase64))
{
BitmapSource bitmapSource = Base64ToImage(item.ImageBase64);
if (bitmapSource != null)
{
_history.Add(new ClipboardEntry("", item.CopiedAt)
{
Image = bitmapSource,
OriginalImagePath = item.OriginalImagePath,
IsPinned = item.IsPinned,
Category = item.Category
});
continue;
}
}
_history.Add(new ClipboardEntry(item.Text, item.CopiedAt)
{
IsPinned = item.IsPinned,
Category = item.Category
});
}
}
Task.Run((Action)CleanupImageCache);
LogService.Info($"클립보드 히스토리 {_history.Count}개 복원 (암호화)");
}
catch (Exception ex)
{
LogService.Warn("클립보드 히스토리 로드 실패: " + ex.Message);
}
}
private void MigrateLegacyFile()
{
try
{
string s = File.ReadAllText(LegacyPath);
byte[] bytes = Encoding.UTF8.GetBytes(s);
byte[] bytes2 = ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser);
Directory.CreateDirectory(Path.GetDirectoryName(HistoryPath));
File.WriteAllBytes(HistoryPath, bytes2);
File.Delete(LegacyPath);
LogService.Info("클립보드 히스토리 평문→암호화 마이그레이션 완료");
}
catch (Exception ex)
{
LogService.Warn("클립보드 히스토리 마이그레이션 실패: " + ex.Message);
}
}
private async Task SaveHistoryAsync()
{
try
{
List<SavedClipEntry> snapshot = BuildSnapshot();
string json = JsonSerializer.Serialize(snapshot, JsonOpts);
byte[] plain = Encoding.UTF8.GetBytes(json);
byte[] encrypted = ProtectedData.Protect(plain, null, DataProtectionScope.CurrentUser);
Directory.CreateDirectory(Path.GetDirectoryName(HistoryPath));
await File.WriteAllBytesAsync(HistoryPath, encrypted).ConfigureAwait(continueOnCapturedContext: false);
}
catch (Exception ex)
{
Exception ex2 = ex;
LogService.Warn("클립보드 히스토리 저장 실패: " + ex2.Message);
}
}
private void SaveHistorySync()
{
try
{
List<SavedClipEntry> value = BuildSnapshot();
string s = JsonSerializer.Serialize(value, JsonOpts);
byte[] bytes = Encoding.UTF8.GetBytes(s);
byte[] bytes2 = ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser);
Directory.CreateDirectory(Path.GetDirectoryName(HistoryPath));
File.WriteAllBytes(HistoryPath, bytes2);
}
catch
{
}
}
private List<SavedClipEntry> BuildSnapshot()
{
lock (_lock)
{
return _history.Select(delegate(ClipboardEntry e)
{
if (e.IsText)
{
return new SavedClipEntry
{
Text = e.Text,
CopiedAt = e.CopiedAt,
IsPinned = e.IsPinned,
Category = e.Category
};
}
string imageBase = ImageToBase64(e.Image);
return new SavedClipEntry
{
CopiedAt = e.CopiedAt,
ImageBase64 = imageBase,
OriginalImagePath = e.OriginalImagePath,
IsPinned = e.IsPinned,
Category = e.Category
};
}).ToList();
}
}
private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
{
if (msg == 797)
{
OnClipboardUpdate();
handled = false;
}
return IntPtr.Zero;
}
private void OnClipboardUpdate()
{
if (_ignoreNext)
{
_ignoreNext = false;
}
else
{
if (!_settings.Settings.ClipboardHistory.Enabled)
{
return;
}
((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate
{
try
{
ClipboardEntry entry = null;
if (Clipboard.ContainsText())
{
string text = Clipboard.GetText();
if (string.IsNullOrWhiteSpace(text) || text.Length > 10000)
{
return;
}
foreach (string excludePattern in _settings.Settings.ClipboardHistory.ExcludePatterns)
{
try
{
if (Regex.IsMatch(text.Trim(), excludePattern, RegexOptions.None, TimeSpan.FromMilliseconds(200.0)))
{
return;
}
}
catch
{
}
}
string category = (_settings.Settings.Launcher.EnableClipboardAutoCategory ? DetectCategory(text) : "일반");
entry = new ClipboardEntry(text, DateTime.Now)
{
Category = category
};
}
else if (Clipboard.ContainsImage())
{
BitmapSource image = Clipboard.GetImage();
if (image == null)
{
return;
}
string originalImagePath = SaveOriginalImage(image);
BitmapSource image2 = CreateThumbnail(image, 80);
entry = new ClipboardEntry("", DateTime.Now)
{
Image = image2,
OriginalImagePath = originalImagePath
};
}
if (!(entry == null))
{
lock (_lock)
{
if (entry.IsText)
{
if (_history.Count > 0 && _history[0].Text == entry.Text)
{
return;
}
_history.RemoveAll((ClipboardEntry e) => e.IsText && e.Text == entry.Text);
}
_history.Insert(0, entry);
int maxItems = _settings.Settings.ClipboardHistory.MaxItems;
while (_history.Count > maxItems)
{
int num = _history.FindLastIndex((ClipboardEntry e) => !e.IsPinned);
if (num < 0)
{
break;
}
_history.RemoveAt(num);
}
}
this.HistoryChanged?.Invoke(this, EventArgs.Empty);
SaveHistoryAsync();
}
}
catch (Exception ex)
{
LogService.Warn("클립보드 캡처 실패: " + ex.Message);
}
});
}
}
private static string? SaveOriginalImage(BitmapSource src)
{
try
{
Directory.CreateDirectory(ImageCachePath);
string path = $"clip_{DateTime.Now:yyyyMMdd_HHmmss_fff}.png";
string text = Path.Combine(ImageCachePath, path);
PngBitmapEncoder pngBitmapEncoder = new PngBitmapEncoder();
pngBitmapEncoder.Frames.Add(BitmapFrame.Create(src));
using FileStream stream = new FileStream(text, FileMode.Create);
pngBitmapEncoder.Save(stream);
return text;
}
catch (Exception ex)
{
LogService.Warn("원본 이미지 저장 실패: " + ex.Message);
return null;
}
}
public static BitmapSource? LoadOriginalImage(string? path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
return null;
}
try
{
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.UriSource = new Uri(path, UriKind.Absolute);
bitmapImage.EndInit();
((Freezable)bitmapImage).Freeze();
return bitmapImage;
}
catch
{
return null;
}
}
public static void CleanupImageCache()
{
try
{
if (!Directory.Exists(ImageCachePath))
{
return;
}
List<FileInfo> list = (from f in new DirectoryInfo(ImageCachePath).GetFiles("clip_*.png")
orderby f.LastWriteTime
select f).ToList();
DateTime cutoff = DateTime.Now.AddDays(-30.0);
foreach (FileInfo item in list.Where((FileInfo f) => f.LastWriteTime < cutoff).ToList())
{
try
{
item.Delete();
list.Remove(item);
}
catch
{
}
}
long num = list.Sum((FileInfo f) => f.Length);
while (num > 524288000 && list.Count > 0)
{
FileInfo fileInfo = list[0];
num -= fileInfo.Length;
try
{
fileInfo.Delete();
}
catch
{
}
list.RemoveAt(0);
}
}
catch (Exception ex)
{
LogService.Warn("이미지 캐시 정리 실패: " + ex.Message);
}
}
private static BitmapSource CreateThumbnail(BitmapSource src, int maxWidth)
{
if (src.PixelWidth <= maxWidth)
{
return src;
}
double num = (double)maxWidth / (double)src.PixelWidth;
return new TransformedBitmap(src, new ScaleTransform(num, num));
}
private static string? ImageToBase64(BitmapSource? img)
{
if (img == null)
{
return null;
}
try
{
PngBitmapEncoder pngBitmapEncoder = new PngBitmapEncoder();
pngBitmapEncoder.Frames.Add(BitmapFrame.Create(img));
using MemoryStream memoryStream = new MemoryStream();
pngBitmapEncoder.Save(memoryStream);
return Convert.ToBase64String(memoryStream.ToArray());
}
catch
{
return null;
}
}
private static BitmapSource? Base64ToImage(string? base64)
{
if (string.IsNullOrEmpty(base64))
{
return null;
}
try
{
byte[] buffer = Convert.FromBase64String(base64);
using MemoryStream bitmapStream = new MemoryStream(buffer);
BitmapDecoder bitmapDecoder = BitmapDecoder.Create(bitmapStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
return bitmapDecoder.Frames[0];
}
catch
{
return null;
}
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool AddClipboardFormatListener(nint hwnd);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool RemoveClipboardFormatListener(nint hwnd);
}