[Quicker] 减减闪填 - 源码归档

动作:减减闪填

不想再记缩写?不想再背地址?连按两下减号 '--',常用回复、收货地址、代码模版即刻弹出,支持云端自动同步到您的所有电脑。

更新时间:2025-12-28 22:33
原始文件:EqualFill.cs

核心代码

//css_reference Microsoft.Web.WebView2.Wpf.dll;
//css_reference Microsoft.Web.WebView2.Core.dll;
//css_reference Newtonsoft.Json.dll;
//css_reference System.Windows.Forms.dll;
//css_reference System.Drawing.dll;
//css_reference PresentationFramework.dll;
//css_reference PresentationCore.dll;
//css_reference WindowsBase.dll;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Effects;
using Microsoft.Web.WebView2.Wpf;
using Microsoft.Web.WebView2.Core;
using Quicker.Public;
using Newtonsoft.Json;
using System.Runtime.InteropServices;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using Color = System.Windows.Media.Color;
using ColorConverter = System.Windows.Media.ColorConverter;
using Application = System.Windows.Application;
using Point = System.Windows.Point;
using Brushes = System.Windows.Media.Brushes;
using Window = System.Windows.Window;

public static void Exec(IStepContext context)
{
    string dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Quicker", "MinusMinusFill");
    if (!Directory.Exists(dataDir)) Directory.CreateDirectory(dataDir);
    string logFile = Path.Combine(dataDir, "debug.log");

    bool createdNew;
    using (var mutex = new System.Threading.Mutex(true, "Quicker_EqualFill_Unique_Mutex", out createdNew))
    {
        if (!createdNew)
        {
            File.AppendAllText(logFile, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [Instance] Action is already running. Exiting.\r\n");
            return;
        }
        var manager = new EqualFillManager(context);
        manager.Start();
    }
}

public class EqualFillManager
{
    private IStepContext _context;
    private Window _win;
    private WebView2 _webView;
    private string _dataDir;
    private string _iconUrl = "https://files.getquicker.net/_icons/9130C3D3815DFF4AA2F9C5A9DCC3F59C3AC12CF3.png";
    private NotifyIcon _trayIcon;
    private Window _settingsWin;
    private CoreWebView2Environment _webViewEnv;
    private Task<CoreWebView2Environment> _envTask;
    private IntPtr _hookId = IntPtr.Zero;
    private LowLevelKeyboardProc _hookProc;
    private bool _isRunning = true;
    private string _inputBuffer = "";
    private DateTime _lastInputTime = DateTime.MinValue;
    private int _pendingBackspace = 0;
    private EqualFillConfig _config;

    // --- 云同步相关 ---
    private Action<string> Log; 
    private string _syncStatus = "已同步";
    private Dictionary<string, CancellationTokenSource> _cloudSyncDebouncers = new Dictionary<string, CancellationTokenSource>();


    public class EqualFillConfig
    {
        public string TriggerMode { get; set; } = "TextCommand"; // TextCommand, Combination, DoubleTap
        public string TriggerValue { get; set; } = "--";
        public string HotkeyModifiers { get; set; } = "None"; // Ctrl, Alt, Shift, Win
        public string HotkeyKey { get; set; } = "";
        public List<string> Groups { get; set; } 
    }

    #region Win32 API
    private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll")]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetModuleHandle(string lpModuleName);

    [DllImport("user32.dll")]
    private static extern IntPtr GetForegroundWindow();

    [DllImport("user32.dll")]
    private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

    [DllImport("user32.dll")]
    private static extern bool GetGUIThreadInfo(uint idThread, ref GUITHREADINFO lpgui);

    [DllImport("user32.dll")]
    private static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);

    [DllImport("user32.dll")]
    private static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId);

    [DllImport("user32.dll")]
    private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);

    [DllImport("user32.dll")]
    private static extern bool GetCaretPos(out POINT lpPoint);

    [DllImport("kernel32.dll")]
    private static extern uint GetCurrentThreadId();

    [DllImport("dwmapi.dll")]
    public static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
    public const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;

    [DllImport("user32.dll")]
    public static extern bool SetForegroundWindow(IntPtr hWnd);

    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
    private const int SW_SHOW = 5;

    [StructLayout(LayoutKind.Sequential)]
    private struct GUITHREADINFO
    {
        public int cbSize;
        public uint flags;
        public IntPtr hwndActive;
        public IntPtr hwndFocus;
        public IntPtr hwndCapture;
        public IntPtr hwndMenuOwner;
        public IntPtr hwndMoveSize;
        public IntPtr hwndCaret;
        public RECT rcCaret;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct RECT { public int Left, Top, Right, Bottom; }

    [StructLayout(LayoutKind.Sequential)]
    public struct POINT { public int X; public int Y; }

    [StructLayout(LayoutKind.Sequential)]
    private struct KBDLLHOOKSTRUCT
    {
        public uint vkCode;
        public uint scanCode;
        public uint flags;
        public uint time;
        public IntPtr dwExtraInfo;
    }

    private const int WH_KEYBOARD_LL = 13;
    private const int WM_KEYDOWN = 0x0100;
    #endregion

    public EqualFillManager(IStepContext context)
    {
        _context = context;
        _hookProc = HookCallback;
        
        // 规范化路径:%AppData%\Quicker\MinusMinusFill
        _dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Quicker", "MinusMinusFill");
        if (!Directory.Exists(_dataDir)) Directory.CreateDirectory(_dataDir);

        // 初始化日志
        Log = (msg) => {
            string line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {msg}\r\n";
            try {
                string logFile = Path.Combine(_dataDir, "debug.log");
                File.AppendAllText(logFile, line);
            } catch {}
            System.Diagnostics.Debug.WriteLine(line);
        };
        CloudStateHelper.Logger = (m) => Log(m);
        Log("===== EqualFillManager Initialized =====");
        
        MigrateOldData();
        LoadConfig();
    }

    private void LoadConfig()
    {
        string configPath = Path.Combine(_dataDir, "config.json");
        try
        {
            if (File.Exists(configPath))
            {
                var loaded = JsonConvert.DeserializeObject<EqualFillConfig>(File.ReadAllText(configPath));
                if (loaded != null && loaded.Groups != null && loaded.Groups.Count > 0)
                {
                    _config = loaded;
                    return;
                }
            }
        }
        catch { }

        // 走到这里说明加载失败或文件不存在
        _config = new EqualFillConfig
        {
            TriggerMode = "TextCommand",
            TriggerValue = "--",
            HotkeyModifiers = "None",
            HotkeyKey = "",
            Groups = new List<string> { "默认", "代码", "办公", "常用" }
        };
        SaveConfig();
    }

    private void SaveConfig()
    {
        try
        {
            string configPath = Path.Combine(_dataDir, "config.json");
            string json = JsonConvert.SerializeObject(_config, Formatting.Indented);
            File.WriteAllText(configPath, json);
            _trayIcon?.ShowBalloonTip(2000, "减减闪填", "配置已保存到磁盘", ToolTipIcon.Info);
            Log("[Config] Saved config to disk. Data: " + json);
            CloudPush("config", json);
        }
        catch (Exception ex)
        {
            _trayIcon?.ShowBalloonTip(3000, "保存失败", ex.Message, ToolTipIcon.Error);
        }
    }

    private void MigrateOldData()
    {
        string oldDir = @"f:\桌面\片段管理\quicker_equal_fill";
        if (Directory.Exists(oldDir))
        {
            string[] files = { "snippets.json", "history.json", "last_search.txt", "config.json" };
            foreach (var file in files)
            {
                string source = Path.Combine(oldDir, file);
                string target = Path.Combine(_dataDir, file);
                if (File.Exists(source) && !File.Exists(target))
                {
                    try { File.Copy(source, target); } catch {}
                }
            }
        }
    }

    public void Start()
    {
        InitTrayIcon();
        InstallHook();
        
        // 预加载窗口
        Application.Current.Dispatcher.Invoke(() => InitializeWindow());

        // 启动时云拉取
        Task.Run(() => CloudPullAll());

        while (_isRunning)
        {
            System.Windows.Forms.Application.DoEvents();
            Thread.Sleep(10);
        }

        // 彻底清理资源
        UninstallHook();
        Application.Current.Dispatcher.Invoke(() => {
            if (_win != null)
            {
                _win.Closing -= null; // 移除之前的拦截逻辑
                _win.Close(); // 真正关闭窗口,销毁 WebView2
            }
        });
        if (_trayIcon != null) { _trayIcon.Visible = false; _trayIcon.Dispose(); }
    }

    private void SetSyncStatus(string status, bool isError = false)
    {
        _syncStatus = status;
        Application.Current.Dispatcher.Invoke(() => {
            if (_webView != null && _webView.CoreWebView2 != null)
            {
                _webView.CoreWebView2.ExecuteScriptAsync($"if(typeof updateSyncStatus === 'function') updateSyncStatus('{status}', {isError.ToString().ToLower()});");
            }
        });
    }

    private void CloudPush(string type, string content, int delayMs = 3000)
    {
        if (_cloudSyncDebouncers.TryGetValue(type, out var oldCts)) oldCts.Cancel();
        var cts = new CancellationTokenSource();
        _cloudSyncDebouncers[type] = cts;

        Task.Delay(delayMs, cts.Token).ContinueWith(t => {
            if (t.IsCanceled) return;
            SetSyncStatus("正在同步...");
            Log($"[CloudSync] Starting push: {type}");
            
            string key = "EqualFill_" + type;
            bool success = CloudStateHelper.SaveText(key, content, 1440 * 30); // 30天有效期
            
            Log($"[CloudSync] Push result for {type}: {success}");
            SetSyncStatus(success ? "云端已同步" : "云端同步失败", !success);
        });
    }

    private void CloudPullAll()
    {
        try
        {
            Log("[CloudSync] Checking cloud for updates...");
            SetSyncStatus("正在检查云端...");
            string[] types = { "config", "snippets", "history" };
            bool anyChanged = false;

            foreach (var type in types)
            {
                string key = "EqualFill_" + type;
                string cloudContent = CloudStateHelper.ReadText(key, 1440 * 30);

                if (!string.IsNullOrEmpty(cloudContent))
                {
                    Log($"[CloudSync] Fetched {type} from cloud. Length: {cloudContent.Length}");
                    string localPath = Path.Combine(_dataDir, type + ".json");
                    string localContent = File.Exists(localPath) ? File.ReadAllText(localPath) : "";
                    
                    if (cloudContent != localContent)
                    {
                        File.WriteAllText(localPath, cloudContent);
                        if (type == "config") _config = JsonConvert.DeserializeObject<EqualFillConfig>(cloudContent);
                        anyChanged = true;
                        Log($"[CloudSync] Updated local {type} from cloud.");
                    } else {
                        Log($"[CloudSync] Local {type} is already up to date.");
                    }
                } else {
                    Log($"[CloudSync] Cloud {type} is empty or expired (Key: {key}).");
                }
            }
            
            if (anyChanged)
            {
                Application.Current.Dispatcher.Invoke(() => {
                    if (_win != null && _webView != null && _webView.CoreWebView2 != null)
                    {
                        string snippetsPath = Path.Combine(_dataDir, "snippets.json");
                        string historyPath = Path.Combine(_dataDir, "history.json");
                        string sJson = File.Exists(snippetsPath) ? File.ReadAllText(snippetsPath) : "[]";
                        string hJson = File.Exists(historyPath) ? File.ReadAllText(historyPath) : "[]";
                        _webView.CoreWebView2.ExecuteScriptAsync($"refreshSnippets({sJson}); refreshHistory({hJson}); setGroups({JsonConvert.SerializeObject(_config.Groups)});");
                    }
                    else
                    {
                        InitializeWindow(); 
                    }
                });
                Log("[CloudSync] Sync: Local files updated from cloud.");
            }
            Log("[CloudSync] Initial sync completed.");
            SetSyncStatus("已同步");
        }
        catch (Exception ex)
        {
            Log("[CloudSync] Error during pull: " + ex.Message);
            SetSyncStatus("检查失败", true);
        }
    }


    private void InstallHook()
    {
        using (var curProcess = System.Diagnostics.Process.GetCurrentProcess())
        using (var curModule = curProcess.MainModule)
        {
            _hookId = SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, GetModuleHandle(curModule.ModuleName), 0);
        }
    }

    private void UninstallHook()
    {
        if (_hookId != IntPtr.Zero) UnhookWindowsHookEx(_hookId);
    }

    private void InitTrayIcon()
    {
        _trayIcon = new NotifyIcon();
        try
        {
            string iconPath = Path.Combine(_dataDir, "icon.png");
            if (!File.Exists(iconPath))
            {
                using (var client = new System.Net.WebClient())
                {
                    client.DownloadFile(_iconUrl, iconPath);
                }
            }
            using (var stream = File.OpenRead(iconPath))
            {
                var bitmap = (Bitmap)System.Drawing.Image.FromStream(stream);
                _trayIcon.Icon = Icon.FromHandle(bitmap.GetHicon());
            }
        }
        catch { _trayIcon.Icon = SystemIcons.Application; }

        _trayIcon.Text = "减减闪填 - 运行中";
        _trayIcon.Visible = true;
        
        var menu = new ContextMenuStrip();
        menu.Items.Add("显示主窗口", null, (s, e) => Application.Current.Dispatcher.Invoke(() => ShowWindow()));
        menu.Items.Add("设置 (快捷键/分组)", null, (s, e) => Application.Current.Dispatcher.Invoke(() => ShowSettingsWindow()));
        menu.Items.Add("-");
        menu.Items.Add("导入片段...", null, (s, e) => ImportSnippetsFromFile());
        menu.Items.Add("从 Quicker 导入...", null, (s, e) => ImportFromQuicker());
        menu.Items.Add("导出片段...", null, (s, e) => ExportSnippetsToFile());
        menu.Items.Add("-");
        menu.Items.Add("退出", null, (s, e) => _isRunning = false);
        _trayIcon.ContextMenuStrip = menu;
    }

    private void ImportSnippetsFromFile()
    {
        using (var dialog = new OpenFileDialog())
        {
            dialog.Title = "选择要导入的片段文件";
            dialog.Filter = "JSON 文件|*.json|所有文件|*.*";
            dialog.FilterIndex = 1;
            
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                try
                {
                    string json = File.ReadAllText(dialog.FileName);
                    var newItems = JsonConvert.DeserializeObject<List<dynamic>>(json);
                    
                    string snippetsPath = Path.Combine(_dataDir, "snippets.json");
                    var existing = File.Exists(snippetsPath) 
                        ? JsonConvert.DeserializeObject<List<dynamic>>(File.ReadAllText(snippetsPath)) 
                        : new List<dynamic>();
                    
                    int added = 0;
                    foreach (var item in newItems)
                    {
                        string title = (string)item.title;
                        bool exists = existing.Any(e => (string)e.title == title);
                        if (!exists)
                        {
                            existing.Add(item);
                            added++;
                        }
                    }
                    
                    string updatedJson = JsonConvert.SerializeObject(existing, Formatting.Indented);
                    File.WriteAllText(snippetsPath, updatedJson);
                    CloudPush("snippets", updatedJson);
                    System.Windows.Forms.MessageBox.Show($"导入成功!新增 {added} 个片段,跳过 {newItems.Count - added} 个重复项。", "导入完成", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                catch (Exception ex)
                {
                    System.Windows.Forms.MessageBox.Show($"导入失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }
    }

    private void ImportFromQuicker()
    {
        using (var dialog = new OpenFileDialog())
        {
            dialog.Title = "选择 Quicker 文本指令备份文件 (JSON)";
            dialog.Filter = "JSON 文件|*.json|所有文件|*.*";
            
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                try
                {
                    string json = File.ReadAllText(dialog.FileName);
                    var quickerItems = JsonConvert.DeserializeObject<List<dynamic>>(json);
                    
                    string snippetsPath = Path.Combine(_dataDir, "snippets.json");
                    var existing = File.Exists(snippetsPath) 
                        ? JsonConvert.DeserializeObject<List<dynamic>>(File.ReadAllText(snippetsPath)) 
                        : new List<dynamic>();
                    
                    int added = 0;
                    foreach (var item in quickerItems)
                    {
                        string qTitle = (string)item.Title;
                        string qCmdText = (string)item.CmdText;
                        string qData = (string)item.Data;

                        // Quicker 映射:Title 优先,没 Title 用指令码作为标题
                        string title = !string.IsNullOrWhiteSpace(qTitle) ? qTitle : qCmdText;
                        string pinyin = qCmdText;
                        string content = qData;

                        if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(content)) continue;

                        // 判重逻辑:标题和内容都一致才跳过
                        bool exists = existing.Any(e => (string)e.title == title && (string)e.content == content);
                        if (!exists)
                        {
                            existing.Add(new {
                                title = title,
                                pinyin = pinyin,
                                content = content,
                                count = 0,
                                lastUsed = 0
                            });
                            added++;
                        }
                    }
                    
                    string updatedJson = JsonConvert.SerializeObject(existing, Formatting.Indented);
                    File.WriteAllText(snippetsPath, updatedJson);
                    CloudPush("snippets", updatedJson);
                    System.Windows.Forms.MessageBox.Show($"导入成功!从 Quicker 转换并新增 {added} 个片段,跳过 {quickerItems.Count - added} 个重复项。", "Quicker 导入完成", MessageBoxButtons.OK, MessageBoxIcon.Information);
                    
                    // 如果窗口开着,刷新一下
                    if (_webView != null && _webView.CoreWebView2 != null)
                    {
                        string newJson = JsonConvert.SerializeObject(existing);
                        _webView.CoreWebView2.ExecuteScriptAsync($"refreshSnippets({newJson})");
                    }
                }
                catch (Exception ex)
                {
                    System.Windows.Forms.MessageBox.Show($"导入失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }
    }

    private void ExportSnippetsToFile()
    {
        using (var dialog = new SaveFileDialog())
        {
            dialog.Title = "导出片段到文件";
            dialog.Filter = "JSON 文件|*.json";
            dialog.FileName = $"snippets_export_{DateTime.Now:yyyyMMdd}.json";
            
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                try
                {
                    string snippetsPath = Path.Combine(_dataDir, "snippets.json");
                    string json = File.Exists(snippetsPath) ? File.ReadAllText(snippetsPath) : "[]";
                    File.WriteAllText(dialog.FileName, json);
                    System.Windows.Forms.MessageBox.Show($"导出成功!文件保存至:\n{dialog.FileName}", "导出完成", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                catch (Exception ex)
                {
                    System.Windows.Forms.MessageBox.Show($"导出失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }
    }

    private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
        {
            var hookStruct = Marshal.PtrToStructure<KBDLLHOOKSTRUCT>(lParam);
            uint vkCode = hookStruct.vkCode;

            if (_config.TriggerMode == "TextCommand")
            {
                if (string.IsNullOrEmpty(_config.TriggerValue)) return CallNextHookEx(_hookId, nCode, wParam, lParam);
                char c = GetCharFromVk(vkCode);
                if (c != '\0')
                {
                    if ((DateTime.Now - _lastInputTime).TotalMilliseconds > 1000) _inputBuffer = "";
                    _inputBuffer += c;
                    _lastInputTime = DateTime.Now;

                    if (_inputBuffer.EndsWith(_config.TriggerValue))
                    {
                        int backspaceCount = _config.TriggerValue.Length - 1; 
                        _inputBuffer = "";
                        Application.Current.Dispatcher.Invoke(() => ShowWindow(backspaceCount));
                        return (IntPtr)1;
                    }
                }
            }
            else if (_config.TriggerMode == "DoubleTap")
            {
                if (string.IsNullOrEmpty(_config.TriggerValue)) return CallNextHookEx(_hookId, nCode, wParam, lParam);
                char c = GetCharFromVk(vkCode);
                string target = _config.TriggerValue[0].ToString();
                if (c.ToString() == target)
                {
                    if ((DateTime.Now - _lastInputTime).TotalMilliseconds < 400 && _inputBuffer == target)
                    {
                        _inputBuffer = "";
                        Application.Current.Dispatcher.Invoke(() => ShowWindow(1));
                        return (IntPtr)1;
                    }
                    _inputBuffer = target;
                    _lastInputTime = DateTime.Now;
                }
                else { _inputBuffer = ""; }
            }
            else if (_config.TriggerMode == "Combination")
            {
                if (string.IsNullOrEmpty(_config.HotkeyKey)) return CallNextHookEx(_hookId, nCode, wParam, lParam);
                bool ctrl = (System.Windows.Forms.Control.ModifierKeys & System.Windows.Forms.Keys.Control) != 0;
                bool alt = (System.Windows.Forms.Control.ModifierKeys & System.Windows.Forms.Keys.Alt) != 0;
                bool shift = (System.Windows.Forms.Control.ModifierKeys & System.Windows.Forms.Keys.Shift) != 0;
                
                string modifiers = "";
                if (ctrl) modifiers += "Ctrl+";
                if (alt) modifiers += "Alt+";
                if (shift) modifiers += "Shift+";
                
                string currentKey = vkCode.ToString(); // 简化处理,实际开发中需转为字符
                if (modifiers + GetCharFromVk(vkCode).ToString().ToUpper() == _config.HotkeyModifiers.Replace("None", "") + _config.HotkeyKey.ToUpper())
                {
                    Application.Current.Dispatcher.Invoke(() => ShowWindow(0));
                    return (IntPtr)1;
                }
            }
        }
        return CallNextHookEx(_hookId, nCode, wParam, lParam);
    }

    private char GetCharFromVk(uint vk)
    {
        // 简单映射,后续可根据需要完善
        if (vk >= 65 && vk <= 90) return (char)(vk + 32); // a-z
        if (vk >= 48 && vk <= 57) return (char)vk; // 0-9
        if (vk == 189) return '-';
        if (vk == 187) return '=';
        if (vk == 186) return ';';
        if (vk == 190) return '.';
        if (vk == 191) return '/';
        if (vk == 219) return '[';
        if (vk == 221) return ']';
        if (vk == 222) return '\'';
        if (vk == 192) return '`';
        return '\0';
    }

    private void InitializeWindow()
    {
        if (_win != null) return;

        _win = new Window
        {
            Title = "减减闪填 - Quicker",
            Width = 500,
            Height = 400,
            WindowStyle = WindowStyle.None,
            AllowsTransparency = true,
            Background = Brushes.Transparent,
            Topmost = true,
            ShowInTaskbar = false,
            Visibility = Visibility.Hidden
        };

        _win.Closing += (s, e) => {
            if (_isRunning) {
                e.Cancel = true;
                _win.Hide();
            }
        };

        _win.SourceInitialized += (s, e) => {
            IntPtr hwnd = new System.Windows.Interop.WindowInteropHelper(_win).Handle;
            int useDarkMode = 1;
            DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ref useDarkMode, sizeof(int));
        };

        var border = new Border
        {
            Background = new SolidColorBrush(Color.FromArgb(230, 20, 20, 25)),
            CornerRadius = new CornerRadius(12),
            BorderBrush = new SolidColorBrush(Color.FromArgb(50, 255, 255, 255)),
            BorderThickness = new Thickness(1),
            Effect = new DropShadowEffect { BlurRadius = 25, Opacity = 0.6, ShadowDepth = 0 }
        };

        _webView = new WebView2();
        _webView.DefaultBackgroundColor = System.Drawing.Color.FromArgb(255, 18, 18, 20);
        border.Child = _webView;
        _win.Content = border;

        _webView.CoreWebView2InitializationCompleted += (s, e) => {
            string snippetsPath = Path.Combine(_dataDir, "snippets.json");
            string snippetsJson = File.Exists(snippetsPath) ? File.ReadAllText(snippetsPath) : "[]";
            string historyPath = Path.Combine(_dataDir, "history.json");
            string historyJson = File.Exists(historyPath) ? File.ReadAllText(historyPath) : "[]";
            string html = GetHtmlContent(snippetsJson, "", false, historyJson);
            _webView.NavigateToString(html);
        };
        _webView.WebMessageReceived += (s, e) =>
        {
            var msg = JsonConvert.DeserializeObject<dynamic>(e.WebMessageAsJson);
            if (msg.action == "get_sync_status")
            {
                // 设置同步状态(包括是否错误,基于当前状态文字猜测)
                SetSyncStatus(_syncStatus, _syncStatus.Contains("失败"));
            }
            else if (msg.action == "paste")
            {
                string title = msg.title;
                string content = msg.content;
                string sPath = Path.Combine(_dataDir, "snippets.json");
                if (File.Exists(sPath))
                {
                    var list = JsonConvert.DeserializeObject<List<dynamic>>(File.ReadAllText(sPath));
                    var item = list.FirstOrDefault(x => (string)x.title == title);
                    if (item != null)
                    {
                        item.count = (item.count ?? 0) + 1;
                        item.lastUsed = DateTimeOffset.Now.ToUnixTimeMilliseconds();
                        string updatedJson = JsonConvert.SerializeObject(list, Formatting.Indented);
                        File.WriteAllText(sPath, updatedJson);
                        CloudPush("snippets", updatedJson);
                    }
                }
                _win.Hide();
                System.Windows.Forms.Clipboard.SetText(content);
                string bs = _pendingBackspace > 0 ? string.Join("", Enumerable.Repeat("{BACKSPACE}", _pendingBackspace)) : "";
                System.Windows.Forms.SendKeys.SendWait(bs + "^v");
            }
            else if (msg.action == "save_search")
            {
                File.WriteAllText(Path.Combine(_dataDir, "last_search.txt"), (string)msg.text);
            }
            else if (msg.action == "clear_last_search")
            {
                File.WriteAllText(Path.Combine(_dataDir, "last_search.txt"), "");
            }
            else if (msg.action == "save_history")
            {
                string text = (string)msg.text;
                if (string.IsNullOrWhiteSpace(text)) return;
                string historyPath = Path.Combine(_dataDir, "history.json");
                List<string> history = File.Exists(historyPath) ? JsonConvert.DeserializeObject<List<string>>(File.ReadAllText(historyPath)) : new List<string>();
                history.Remove(text);
                history.Insert(0, text);
                if (history.Count > 10) history = history.Take(10).ToList();
                string updatedHistoryJson = JsonConvert.SerializeObject(history);
                File.WriteAllText(historyPath, updatedHistoryJson);
                CloudPush("history", updatedHistoryJson);
                _webView.CoreWebView2.ExecuteScriptAsync($"refreshHistory({updatedHistoryJson})");
            }
            else if (msg.action == "create")
            {
                string title = (string)msg.title;
                string snippetsPath = Path.Combine(_dataDir, "snippets.json");
                var list = File.Exists(snippetsPath) ? JsonConvert.DeserializeObject<List<dynamic>>(File.ReadAllText(snippetsPath)) : new List<dynamic>();
                if (!list.Any(x => (string)x.title == title))
                {
                    list.Add(new { title = title, pinyin = "", content = title, count = 0, lastUsed = 0 });
                    string updatedJson = JsonConvert.SerializeObject(list, Formatting.Indented);
                    File.WriteAllText(snippetsPath, updatedJson);
                    CloudPush("snippets", updatedJson);
                }
                _win.Hide();
                string bs = _pendingBackspace > 0 ? string.Join("", Enumerable.Repeat("{BACKSPACE}", _pendingBackspace)) : "";
                if (msg.silent == true) { _trayIcon?.ShowBalloonTip(2000, "减减闪填", "已存入库:" + title, ToolTipIcon.Info); }
                else { System.Windows.Forms.Clipboard.SetText(title); System.Windows.Forms.SendKeys.SendWait(bs + "^v"); }
            }
            else if (msg.action == "create_and_edit")
            {
                string title = (string)msg.title;
                string snippetsPath = Path.Combine(_dataDir, "snippets.json");
                var list = File.Exists(snippetsPath) ? JsonConvert.DeserializeObject<List<dynamic>>(File.ReadAllText(snippetsPath)) : new List<dynamic>();
                var itemToEdit = list.FirstOrDefault(x => (string)x.title == title);
                if (itemToEdit == null) { 
                    itemToEdit = new { title = title, pinyin = "", content = title, count = 0, lastUsed = 0 }; 
                    list.Add(itemToEdit); 
                    string updatedJson = JsonConvert.SerializeObject(list, Formatting.Indented);
                    File.WriteAllText(snippetsPath, updatedJson); 
                    CloudPush("snippets", updatedJson);
                }
                _webView.CoreWebView2.ExecuteScriptAsync($"refreshSnippets({JsonConvert.SerializeObject(list)}); openEdit({JsonConvert.SerializeObject(itemToEdit)});");
            }
            else if (msg.action == "update_snippet")
            {
                string oldTitle = (string)msg.oldTitle;
                string snippetsPath = Path.Combine(_dataDir, "snippets.json");
                var list = JsonConvert.DeserializeObject<List<dynamic>>(File.ReadAllText(snippetsPath));
                var item = list.FirstOrDefault(x => (string)x.title == oldTitle);
                if (item != null) { 
                    item.title = (string)msg.title; 
                    item.pinyin = (string)msg.pinyin; 
                    item.content = (string)msg.content; 
                    item.group = (string)msg.group; 
                    string updatedJson = JsonConvert.SerializeObject(list, Formatting.Indented);
                    File.WriteAllText(snippetsPath, updatedJson); 
                    Log("[Snippet] Updated snippet via Ctrl+Enter. Data size: " + updatedJson.Length);
                    CloudPush("snippets", updatedJson);
                    _webView.CoreWebView2.ExecuteScriptAsync($"refreshSnippets({updatedJson}); showToast2('已同步并退出');");
                }
                _win.Hide();
            }

            else if (msg.action == "update_snippet_only")
            {
                string oldTitle = (string)msg.oldTitle;
                string snippetsPath = Path.Combine(_dataDir, "snippets.json");
                var list = JsonConvert.DeserializeObject<List<dynamic>>(File.ReadAllText(snippetsPath));
                var item = list.FirstOrDefault(x => (string)x.title == oldTitle);
                if (item != null) 
                { 
                    item.title = (string)msg.title; 
                    item.pinyin = (string)msg.pinyin; 
                    item.content = (string)msg.content; 
                    item.group = (string)msg.group; 
                    string updatedJson = JsonConvert.SerializeObject(list, Formatting.Indented);
                    File.WriteAllText(snippetsPath, updatedJson); 
                    CloudPush("snippets", updatedJson, 5000); // 仅保存时延迟长一点
                    _webView.CoreWebView2.ExecuteScriptAsync($"refreshSnippets({updatedJson}); showToast2('已保存');"); 
                }
            }
            else if (msg.action == "pin") { _win.Topmost = (bool)msg.pinned; }
            else if (msg.action == "delete_snippet")
            {
                string title = (string)msg.title;
                string snippetsPath = Path.Combine(_dataDir, "snippets.json");
                var list = JsonConvert.DeserializeObject<List<dynamic>>(File.ReadAllText(snippetsPath));
                var item = list.FirstOrDefault(x => (string)x.title == title);
                if (item != null) { 
                    list.Remove(item); 
                    string updatedJson = JsonConvert.SerializeObject(list, Formatting.Indented);
                    Log("[Snippet] Deleted snippet: " + title);
                    File.WriteAllText(snippetsPath, updatedJson);
                    CloudPush("snippets", updatedJson);
                    _webView.CoreWebView2.ExecuteScriptAsync($"refreshSnippets({updatedJson})"); 
                }
            }
            else if (msg.action == "delete_history")
            {
                string text = (string)msg.text;
                string historyPath = Path.Combine(_dataDir, "history.json");
                var history = JsonConvert.DeserializeObject<List<string>>(File.ReadAllText(historyPath));
                if (history.Remove(text)) { 
                    string updatedJson = JsonConvert.SerializeObject(history);
                    File.WriteAllText(historyPath, updatedJson); 
                    CloudPush("history", updatedJson);
                    _webView.CoreWebView2.ExecuteScriptAsync($"refreshHistory({updatedJson})"); 
                }
            }
            if (msg.action == "move_snippet")
            {
                string title = (string)msg.title;
                int direction = (int)msg.direction;
                string snippetsPath = Path.Combine(_dataDir, "snippets.json");
                var list = JsonConvert.DeserializeObject<List<dynamic>>(File.ReadAllText(snippetsPath));
                int idx = list.FindIndex(x => (string)x.title == title);
                int nIdx = idx + direction;
                if (nIdx >= 0 && nIdx < list.Count) { 
                    var temp = list[idx]; 
                    list[idx] = list[nIdx]; 
                    list[nIdx] = temp; 
                    string updatedJson = JsonConvert.SerializeObject(list, Formatting.Indented);
                    File.WriteAllText(snippetsPath, updatedJson); 
                    Log("[Snippet] Moved snippet: " + title + " Direction: " + direction);
                    CloudPush("snippets", updatedJson);
                    _webView.CoreWebView2.ExecuteScriptAsync($"refreshSnippets({updatedJson}); selectedIndex = {nIdx}; updateListsUI();"); 
                }
            }
            else if (msg.action == "save_config")
            {
                _config.TriggerMode = (string)msg.config.TriggerMode;
                _config.TriggerValue = (string)msg.config.TriggerValue;
                _config.HotkeyModifiers = (string)msg.config.HotkeyModifiers;
                _config.HotkeyKey = (string)msg.config.HotkeyKey;
                _config.Groups = msg.config.Groups.ToObject<List<string>>();
                SaveConfig();
                _webView.CoreWebView2.ExecuteScriptAsync($"showToast2('配置已保存')");
            }
            else if (msg.action == "close")
            {
                _win.Hide();
                if (msg.clearTrigger == true) { Thread.Sleep(50); System.Windows.Forms.SendKeys.SendWait("{BACKSPACE}"); }
            }
        };

        InitializeWebView();
        _win.MouseDown += (s, e) => { if (e.LeftButton == MouseButtonState.Pressed) _win.DragMove(); };
        _win.Activated += (s, e) => _webView.Focus();
    }

    private void ShowWindow(int backspaceCount = 1)
    {
        Application.Current.Dispatcher.Invoke(() => {
            if (_win == null) InitializeWindow();

            // 1. 同步最新数据
            string snippetsPath = Path.Combine(_dataDir, "snippets.json");
            string historyPath = Path.Combine(_dataDir, "history.json");
            string snippetsJson = File.Exists(snippetsPath) ? File.ReadAllText(snippetsPath) : "[]";
            string historyJson = File.Exists(historyPath) ? File.ReadAllText(historyPath) : "[]";

            if (_webView != null && _webView.CoreWebView2 != null)
            {
                _webView.CoreWebView2.ExecuteScriptAsync("if(typeof resetState === 'function') resetState();");
                _webView.CoreWebView2.ExecuteScriptAsync($"if(typeof refreshSnippets === 'function') refreshSnippets({snippetsJson});");
                _webView.CoreWebView2.ExecuteScriptAsync($"if(typeof refreshHistory === 'function') refreshHistory({historyJson});");
                _webView.CoreWebView2.ExecuteScriptAsync($"if(typeof setGroups === 'function') setGroups({JsonConvert.SerializeObject(_config.Groups)});");
                _webView.CoreWebView2.ExecuteScriptAsync($"if(typeof setConfig === 'function') setConfig({JsonConvert.SerializeObject(_config)});");
            }

            _pendingBackspace = backspaceCount;
            
            // 2. 位置定位逻辑保持一致
            var rawCaretPos = GetRawCaretPosition();
            Point wpfPos = ScreenToWpf(rawCaretPos);
            double left = wpfPos.X;
            double top = wpfPos.Y - 155;
            
            if (left + _win.Width > SystemParameters.WorkArea.Width) left = SystemParameters.WorkArea.Width - _win.Width - 10;
            if (left < 0) left = 10;
            if (top + _win.Height > SystemParameters.WorkArea.Height) top = SystemParameters.WorkArea.Height - _win.Height - 10;
            if (top < 0) top = 10;

            _win.Left = left;
            _win.Top = top;
            _win.Show();
            _win.Activate();
            _webView.Focus();

            IntPtr hwnd = new System.Windows.Interop.WindowInteropHelper(_win).Handle;
            ShowWindow(hwnd, SW_SHOW);
            SetForegroundWindow(hwnd);
        });
    }


    private Task<CoreWebView2Environment> GetWebViewEnv()
    {
        if (_envTask == null)
        {
            string udfPath = Path.Combine(_dataDir, "webview_udf_main");
            if (!Directory.Exists(udfPath)) Directory.CreateDirectory(udfPath);
            _envTask = CoreWebView2Environment.CreateAsync(null, udfPath);
        }
        return _envTask;
    }

    private void ShowSettingsWindow()
    {
        if (_settingsWin != null)
        {
            _settingsWin.Activate();
            return;
        }

        _settingsWin = new Window
        {
            Title = "减减闪填 - 设置",
            Width = 400,
            Height = 520,
            WindowStartupLocation = WindowStartupLocation.CenterScreen,
            Background = Brushes.Transparent,
            AllowsTransparency = true,
            WindowStyle = WindowStyle.None,
            Topmost = false,
            ShowInTaskbar = true,
            FontFamily = new System.Windows.Media.FontFamily("Microsoft YaHei")
        };

        _settingsWin.MouseDown += (s, e) => { if (e.LeftButton == MouseButtonState.Pressed) _settingsWin.DragMove(); };

        // 主容器
        var border = new Border
        {
            Background = new SolidColorBrush(Color.FromArgb(245, 25, 25, 30)),
            CornerRadius = new CornerRadius(12),
            BorderBrush = new SolidColorBrush(Color.FromArgb(80, 255, 255, 255)),
            BorderThickness = new Thickness(1),
            Padding = new Thickness(25)
        };

        var mainStack = new StackPanel();
        
        // 标题
        mainStack.Children.Add(new TextBlock { 
            Text = "设置与快捷键", 
            Foreground = Brushes.White, 
            FontSize = 18, 
            FontWeight = FontWeights.Bold, 
            Margin = new Thickness(0, 0, 0, 20) 
        });

        // 通用样式函数
        Action<string, FrameworkElement> AddRow = (label, control) => {
            mainStack.Children.Add(new TextBlock { Text = label, Foreground = new SolidColorBrush(Color.FromRgb(150, 150, 150)), FontSize = 12, Margin = new Thickness(0, 10, 0, 5) });
            mainStack.Children.Add(control);
        };

        // 1. 触发模式
        var modeCombo = new System.Windows.Controls.ComboBox { Height = 32, VerticalContentAlignment = VerticalAlignment.Center };
        modeCombo.Items.Add("TextCommand (文本指令)");
        modeCombo.Items.Add("DoubleTap (双击按键)");
        modeCombo.Items.Add("Combination (组合键)");
        modeCombo.SelectedIndex = _config.TriggerMode == "TextCommand" ? 0 : (_config.TriggerMode == "DoubleTap" ? 1 : 2);
        AddRow("触发模式", modeCombo);

        // 2. 指令文本
        var valueInput = new System.Windows.Controls.TextBox { Text = _config.TriggerValue, Height = 32, VerticalContentAlignment = VerticalAlignment.Center, Background = new SolidColorBrush(Color.FromRgb(40,40,45)), Foreground = Brushes.White, BorderThickness = new Thickness(1) };
        AddRow("指令文本 / 触发按键 (如 -- 或 =)", valueInput);

        // 3. 修饰符
        var modCombo = new System.Windows.Controls.ComboBox { Height = 32, VerticalContentAlignment = VerticalAlignment.Center };
        string[] mods = { "None", "Ctrl+", "Alt+", "Shift+", "Ctrl+Alt+" };
        foreach(var m in mods) modCombo.Items.Add(m);
        modCombo.SelectedItem = mods.Contains(_config.HotkeyModifiers) ? _config.HotkeyModifiers : "None";
        AddRow("组合键修饰符 (仅组合键模式)", modCombo);

        // 4. 主键
        var keyInput = new System.Windows.Controls.TextBox { Text = _config.HotkeyKey, Height = 32, VerticalContentAlignment = VerticalAlignment.Center, Background = new SolidColorBrush(Color.FromRgb(40,40,45)), Foreground = Brushes.White, BorderThickness = new Thickness(1) };
        AddRow("组合键主键 (如 Space, Q, F1)", keyInput);

        // 5. 分组
        var groupsInput = new System.Windows.Controls.TextBox { Text = string.Join(",", _config.Groups), Height = 32, VerticalContentAlignment = VerticalAlignment.Center, Background = new SolidColorBrush(Color.FromRgb(40,40,45)), Foreground = Brushes.White, BorderThickness = new Thickness(1) };
        AddRow("自定义分组 (逗号分隔)", groupsInput);

        // 按钮区
        var btnStack = new StackPanel { Orientation = System.Windows.Controls.Orientation.Horizontal, HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = new Thickness(0, 30, 0, 0) };
        
        var folderBtn = new System.Windows.Controls.Button { Content = "数据文件夹", Width = 90, Height = 32, Margin = new Thickness(0, 0, 10, 0), Background = Brushes.Transparent, Foreground = new SolidColorBrush(Color.FromRgb(150,150,150)), BorderThickness = new Thickness(0), Cursor = System.Windows.Input.Cursors.Hand };
        folderBtn.Click += (s, e) => { try { System.Diagnostics.Process.Start(_dataDir); } catch {} };
        
        var cancelBtn = new System.Windows.Controls.Button { Content = "取消", Width = 70, Height = 32, Margin = new Thickness(0, 0, 10, 0), Background = new SolidColorBrush(Color.FromRgb(60,60,65)), Foreground = Brushes.White, BorderThickness = new Thickness(0) };
        cancelBtn.Click += (s, e) => _settingsWin.Close();
        
        var saveBtn = new System.Windows.Controls.Button { Content = "保存配置", Width = 90, Height = 32, Background = new SolidColorBrush(Color.FromRgb(59, 130, 246)), Foreground = Brushes.White, BorderThickness = new Thickness(0) };
        saveBtn.Click += (s, e) => {
            _config.TriggerMode = modeCombo.SelectedIndex == 0 ? "TextCommand" : (modeCombo.SelectedIndex == 1 ? "DoubleTap" : "Combination");
            _config.TriggerValue = valueInput.Text;
            _config.HotkeyModifiers = modCombo.SelectedItem.ToString();
            _config.HotkeyKey = keyInput.Text;
            _config.Groups = groupsInput.Text.Split(',').Select(g => g.Trim()).Where(g => !string.IsNullOrEmpty(g)).ToList();
            SaveConfig();
            
            // 同步数据到主窗口
            if (_webView != null && _webView.CoreWebView2 != null)
            {
                _webView.CoreWebView2.ExecuteScriptAsync($"if(typeof setGroups === 'function') setGroups({JsonConvert.SerializeObject(_config.Groups)});");
                _webView.CoreWebView2.ExecuteScriptAsync($"if(typeof setConfig === 'function') setConfig({JsonConvert.SerializeObject(_config)});");
            }
            _settingsWin.Close();
        };

        btnStack.Children.Add(folderBtn);
        btnStack.Children.Add(cancelBtn);
        btnStack.Children.Add(saveBtn);
        mainStack.Children.Add(btnStack);

        border.Child = mainStack;
        _settingsWin.Content = border;
        _settingsWin.Closed += (s, e) => _settingsWin = null;
        _settingsWin.Show();
    }

    private Point GetRawCaretPosition()
    {
        try
        {
            IntPtr foregroundWindow = GetForegroundWindow();
            uint foregroundThreadId = GetWindowThreadProcessId(foregroundWindow, IntPtr.Zero);
            uint currentThreadId = GetCurrentThreadId();

            GUITHREADINFO gti = new GUITHREADINFO { cbSize = Marshal.SizeOf(typeof(GUITHREADINFO)) };
            if (GetGUIThreadInfo(foregroundThreadId, ref gti))
            {
                // 检查 hwndCaret 是否有效,且坐标非常规零点(某些应用会返回 0,0)
                if (gti.hwndCaret != IntPtr.Zero)
                {
                    POINT p = new POINT { X = gti.rcCaret.Left, Y = gti.rcCaret.Top };
                    if (p.X != 0 || p.Y != 0)
                    {
                        ClientToScreen(gti.hwndCaret, ref p);
                        return new Point(p.X, p.Y);
                    }
                }
            }

            // 备选方案:通过 AttachThreadInput 获取
            if (foregroundThreadId != 0 && foregroundThreadId != currentThreadId)
            {
                if (AttachThreadInput(currentThreadId, foregroundThreadId, true))
                {
                    try
                    {
                        POINT p;
                        if (GetCaretPos(out p))
                        {
                            if (p.X != 0 || p.Y != 0)
                            {
                                ClientToScreen(foregroundWindow, ref p);
                                return new Point(p.X, p.Y);
                            }
                        }
                    }
                    finally
                    {
                        AttachThreadInput(currentThreadId, foregroundThreadId, false);
                    }
                }
            }
        }
        catch { /* 忽略错误,进入兜底 */ }

        // 最终兜底:使用当前鼠标位置
        var mouse = System.Windows.Forms.Cursor.Position;
        return new Point(mouse.X, mouse.Y);
    }

    private Point ScreenToWpf(Point screenPoint)
    {
        // 尝试获取 DPI 缩放
        try {
            var matrix = PresentationSource.FromVisual(Application.Current.MainWindow)?.CompositionTarget?.TransformToDevice;
            if (matrix != null)
            {
                return new Point(screenPoint.X / matrix.Value.M11, screenPoint.Y / matrix.Value.M22);
            }
        } catch {}
        
        // 兜底方案:使用系统屏幕 DPI
        using (var g = System.Drawing.Graphics.FromHwnd(IntPtr.Zero))
        {
            double scaleX = g.DpiX / 96.0;
            double scaleY = g.DpiY / 96.0;
            return new Point(screenPoint.X / scaleX, screenPoint.Y / scaleY);
        }
    }

    private async void InitializeWebView()
    {
        try
        {
            var env = await GetWebViewEnv();
            if (_win != null)
            {
                new System.Windows.Interop.WindowInteropHelper(_win).EnsureHandle();
                await _webView.EnsureCoreWebView2Async(env);
            }
        }
        catch (Exception ex)
        {
            _trayIcon?.ShowBalloonTip(3000, "主窗口错误", "WebView2 初始化失败: " + ex.Message, ToolTipIcon.Error);
        }
    }

    private string GetHtmlContent(string snippetsJson, string lastSearch, bool flip, string historyJson)
    {
        string arrowPos = flip ? "bottom: -10px; border-top: 10px solid var(--border-color); border-bottom: none;" : "top: -10px; border-bottom: 10px solid var(--border-color); border-top: none;";
        
        return @"
<!DOCTYPE html>
<html>
<head>
    <meta charset='UTF-8'>
    <style>
        :root {
            --bg: #121214;
            --accent: #3b82f6;
            --accent-glow: rgba(59, 130, 246, 0.2);
            --text: #f4f4f5;
            --text-dim: #71717a;
            --border-color: #27272a;
            --item-hover: rgba(39, 39, 42, 0.6);
            --item-selected: rgba(59, 130, 246, 0.15);
            --surface: #18181b;
        }
        html, body {
            background: var(--bg);
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            border-radius: 12px;
            overflow: hidden;
            font-family: 'Inter', system-ui, -apple-system, sans-serif;
        }
        body {
            background: var(--bg);
            color: var(--text);
            display: flex;
            flex-direction: column; 
            border: 1px solid var(--border-color);
            box-sizing: border-box;
            height: 100vh;
            margin: 0;
            overflow: hidden;
            position: relative;
        }
        *::-webkit-scrollbar { display: none; }
        * { -ms-overflow-style: none; scrollbar-width: none; }
        .history-section {
            max-height: 0;
            overflow-y: auto;
            background: rgba(0, 0, 0, 0.2);
            display: flex;
            flex-direction: column-reverse; 
            padding: 0 5px;
            transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s;
            opacity: 0;
            pointer-events: none;
        }
        .history-section.expanded {
            max-height: 250px; /* 增加展示空间 */
            padding: 5px;
            opacity: 1;
            pointer-events: auto;
            border-bottom: 1px dashed rgba(255,255,255,0.05);
        }
        .history-item.selected {
            background: rgba(33, 150, 243, 0.2);
            opacity: 1;
            transform: translateX(2px);
            border-radius: 4px;
        }
        .search-container {
            padding: 8px 12px;
            background: var(--surface);
            border-bottom: 1px solid rgba(255, 255, 255, 0.06);
            display: flex;
            align-items: center;
            gap: 8px;
            z-index: 10;
        }
        .search-box {
            flex: 1;
            background: transparent;
            border: none;
            color: white;
            font-size: 14px;
            font-weight: 500;
            outline: none;
            placeholder-color: var(--text-dim);
        }
        .list-container {
            flex: 1;
            overflow-y: auto;
            padding: 5px;
            background: var(--bg);
        }
        .footer-hint {
            padding: 5px 12px;
            font-size: 10px;
            color: var(--text-dim);
            background: #151517;
            border-top: 1px solid var(--border-color);
            display: flex;
            justify-content: space-between;
            align-items: center;
            z-index: 5;
        }
        .sync-status {
            display: flex;
            align-items: center;
            gap: 4px;
            opacity: 0.8;
        }
        .sync-icon {
            width: 12px;
            height: 12px;
            fill: currentColor;
            transition: color 0.3s ease;
        }
        .sync-status { transition: all 0.3s ease; }
        .sync-status.error { color: #f43f5e; }
        .sync-status.syncing { color: var(--accent); }
        .sync-status.success { color: #10b981; }
        .edit-view {
            display: none;
            position: absolute; /* 全屏覆盖 */
            top: 0; left: 0; right: 0; bottom: 0;
            background: var(--bg);
            z-index: 2000;
            padding: 20px;
            flex-direction: column;
            overflow-y: auto;
        }
        .pin-btn, .create-btn {
            width: 28px;
            height: 28px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 6px;
            color: var(--text-dim);
            cursor: pointer;
            transition: all 0.2s;
            flex-shrink: 0;
        }
        .pin-btn:hover, .create-btn:hover {
            background: rgba(255, 255, 255, 0.05);
            color: var(--text);
        }
        .pin-btn.active {
            color: var(--accent);
            background: rgba(59, 130, 246, 0.1);
        }
        #previewView {
            display: none;
            position: absolute;
            top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(18, 18, 20, 0.98);
            z-index: 2001; 
            padding: 20px;
            flex-direction: column;
        }
        .footer-hint span { color: var(--accent); opacity: 0.8; }
        .edit-header { 
            display: flex; 
            justify-content: space-between; 
            align-items: baseline;
            margin-bottom: 12px;
        }
        .edit-short-hint { font-size: 10px; color: var(--text-dim); }
        .edit-short-hint kbd { 
            background: #333; 
            padding: 1px 4px; 
            border-radius: 3px; 
            color: #eee;
            font-family: inherit;
        }
        .edit-row { margin-bottom: 10px; }
        .edit-label { font-size: 11px; color: var(--text-dim); margin-bottom: 4px; }
        .edit-input {
            width: 100%;
            background: #1a1a1d;
            border: 1px solid var(--border-color);
            border-radius: 4px;
            padding: 8px 12px;
            color: white;
            font-size: 13px;
            outline: none;
            box-sizing: border-box;
        }
        .edit-input:focus { border-color: var(--accent); }
        .edit-actions { 
            display: flex; 
            gap: 10px; 
            margin-top: auto; /* 推到最底部 */
            padding-top: 10px;
            padding-bottom: 5px;
        }
        .btn {
            flex: 1;
            padding: 10px;
            border-radius: 4px;
            border: none;
            cursor: pointer;
            font-size: 12px;
            transition: opacity 0.2s;
        }
        .btn-save { background: var(--accent); color: white; }
        .btn-cancel { background: #333; color: white; }
        .snippet-item {
            padding: 10px 14px;
            border-radius: 8px;
            cursor: pointer;
            margin: 2px 6px;
            display: flex;
            align-items: center;
            gap: 12px;
            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
            border: 1px solid transparent;
        }
        .snippet-item:hover {
            background: var(--item-hover);
            border-color: var(--border-color);
        }
        .snippet-item.selected {
            background: var(--item-selected);
            border-color: rgba(59, 130, 246, 0.3);
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
        }
        .section-label { font-size: 10px; color: var(--text-dim); padding: 5px 10px; text-transform: uppercase; letter-spacing: 1px; }
        .empty-hint {
            padding: 20px;
            text-align: center;
            color: var(--text-dim);
            font-size: 13px;
        }
        .title { font-size: 13px; }
        .pinyin { font-size: 11px; color: var(--text-dim); margin-left: 8px; font-family: monospace; }
        .hint { font-size: 9px; color: var(--accent); opacity: 0.8; }
        .group-tabs {
            display: flex;
            gap: 6px;
            padding: 4px 12px;
            background: var(--bg);
            overflow-x: auto;
            border-bottom: 1px solid rgba(255, 255, 255, 0.06);
        }
        .group-tab {
            font-size: 11px;
            padding: 4px 10px;
            border-radius: 4px;
            background: #1a1a1d;
            color: var(--text-dim);
            cursor: pointer;
            white-space: nowrap;
            transition: all 0.2s;
        }
        .group-tab.active {
            background: var(--accent);
            color: white;
        }
        select.edit-input {
            appearance: none;
            background-image: url('data:image/svg+xml;utf8,<svg fill=''white'' height=''24'' viewBox=''0 0 24 24'' width=''24'' xmlns=''http://www.w3.org/2000/svg''><path d=''M7 10l5 5 5-5z''/></svg>');
            background-repeat: no-repeat;
            background-position-x: 98%;
            background-position-y: 50%;
        }
        .settings-only-mode #groupTabs,
        .settings-only-mode .search-container,
        .settings-only-mode .list-container,
        .settings-only-mode .footer-hint,
        .settings-only-mode .arrow {
            display: none !important;
        }
        .settings-only-mode .edit-view#settingsView {
            display: flex !important;
            box-shadow: none;
            border-radius: 12px;
        }
        /* 右键菜单样式 */
        .context-menu {
            display: none;
            position: fixed;
            z-index: 10000;
            background: #27272a;
            border: 1px solid #3f3f46;
            border-radius: 8px;
            padding: 4px;
            min-width: 120px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.5);
        }
        .context-menu-item {
            padding: 8px 12px;
            font-size: 13px;
            color: var(--text);
            cursor: pointer;
            border-radius: 4px;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .context-menu-item:hover {
            background: var(--accent);
            color: white;
        }
        .context-menu-item.delete:hover {
            background: #ef4444;
        }
        #toast2 {
            position: fixed;
            bottom: 40px;
            left: 50%;
            transform: translate(-50%, 20px);
            background: #10b981;
            color: white;
            padding: 8px 18px;
            border-radius: 20px;
            font-size: 13px;
            z-index: 10001;
            opacity: 0;
            transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28);
            pointer-events: none;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
        }
        #toast2.show { 
            opacity: 1; 
            transform: translate(-50%, 0);
        }
    </style>
</head>
<body>
    <div class='arrow'></div>
    <div id='editView' class='edit-view'>
        <div class='edit-header'>
            <div style='font-size: 16px; font-weight: 500;'>编辑片段</div>
            <div class='edit-short-hint'>
                <kbd>Ctrl+Enter</kbd> 保存并退出
            </div>
        </div>
        <div class='edit-row'>
            <div class='edit-label'>标题</div>
            <input type='text' id='editTitle' class='edit-input' tabindex='1'>
        </div>
        <div class='edit-row'>
            <div class='edit-label'>拼音缩写</div>
            <input type='text' id='editPinyin' class='edit-input' tabindex='2'>
        </div>
        <div class='edit-row'>
            <div class='edit-label'>内容</div>
            <textarea id='editContent' class='edit-input' style='height: 120px; resize: none;' tabindex='3'></textarea>
        </div>
        <div class='edit-row'>
            <div class='edit-label'>分组</div>
            <select id='editGroup' class='edit-input'></select>
        </div>
        <div class='edit-actions'>
            <button class='btn btn-cancel' onclick='closeEdit()'>取消 (Esc)</button>
            <button class='btn btn-save' onclick='saveEditOnly()'>保存 (Ctrl+S)</button>
        </div>
    </div>
    
    <div id='settingsView' class='edit-view'>
        <div class='edit-header'>设置与快捷键</div>
        <div class='edit-row'>
            <div class='edit-label'>触发模式</div>
            <select id='settingMode' class='edit-input'>
                <option value='TextCommand'>文本指令 (如 --, ;;)</option>
                <option value='DoubleTap'>双击按键 (如双击 =)</option>
                <option value='Combination'>组合键 (如 Ctrl+Space)</option>
            </select>
        </div>
        <div class='edit-row'>
            <div class='edit-label'>指令文本 / 触发按键</div>
            <input type='text' id='settingValue' class='edit-input' placeholder='输入如 -- 或 f'>
        </div>
        <div class='edit-row'>
            <div class='edit-label'>组合键修饰符 (仅组合键模式)</div>
            <select id='settingModifiers' class='edit-input'>
                <option value='None'>无</option>
                <option value='Ctrl+'>Ctrl</option>
                <option value='Alt+'>Alt</option>
                <option value='Shift+'>Shift</option>
                <option value='Ctrl+Alt+'>Ctrl+Alt</option>
            </select>
        </div>
        <div class='edit-row'>
            <div class='edit-label'>组合键主键 (如 Space, Q, F1)</div>
            <input type='text' id='settingKey' class='edit-input'>
        </div>
        <div class='edit-row'>
            <div class='edit-label'>自定义分组 (逗号分隔)</div>
            <input type='text' id='settingGroups' class='edit-input'>
        </div>
        <div class='edit-actions'>
            <button class='btn btn-cancel' onclick='closeSettings()'>取消</button>
            <button class='btn btn-save' onclick='saveConfig()'>保存配置</button>
        </div>
    </div>
    
    <div id='historyList' class='history-section'></div>

    </div>
    
    <div class='search-container'>
        <input type='text' id='search' class='search-box' placeholder='搜索或创建...' autofocus>
        <div id='createBtn' class='create-btn' title='新建片段'>
            <svg viewBox='0 0 24 24' width='16' height='16'><path fill='currentColor' d='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'></path></svg>
        </div>
    </div>
    <div id='groupTabs' class='group-tabs' style='border-top: none; border-bottom: 1px solid var(--border-color);'></div>

    <div id='list' class='list-container'></div>

    <div class='footer-hint'>
        <div style='display: flex; gap: 15px;'>
            <div><span>↑↓</span> 导航/历史</div>
            <div><span>Enter</span> 粘贴</div>
            <div><span>→</span> 编辑</div>
            <div><span>Del</span> 删除</div>
        </div>
        <div id='syncStatus' class='sync-status'>
            <svg class='sync-icon' viewBox='0 0 24 24'><path d='M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z'/></svg>
            <span id='syncText'>已同步</span>
        </div>
    </div>
    <div id='toast'></div>
    <div id='toast2'></div>
    <div id='contextMenu' class='context-menu'>
        <div class='context-menu-item' onclick='handleContextEdit()'>
            <svg viewBox='0 0 24 24' width='14' height='14'><path fill='currentColor' d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'></path></svg>
            编辑片段 (→)
        </div>
        <div class='context-menu-item delete' onclick='handleContextDelete()'>
            <svg viewBox='0 0 24 24' width='14' height='14'><path fill='currentColor' d='M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'></path></svg>
            删除片段 (Del)
        </div>
    </div>

    <script>
        const snippets = " + snippetsJson + @";
        const historyData = " + historyJson + @";
        const initialSearch = " + JsonConvert.ToString(lastSearch) + @";
        const listEl = document.getElementById('list');
        const historyListEl = document.getElementById('historyList');
        const searchInput = document.getElementById('search');
        const editView = document.getElementById('editView');
        
        let selectedIndex = 0; // 主列表索引
        let historySelectedIndex = -1; // 历史列表索引,-1 表示焦点在搜索框或主列表
        let currentFocus = 'main'; // 'main' 或 'history'
        
        let filtered = [];
        let filteredHistory = [];
        let isEditing = false;
        let isPreviewing = false;
        let editingItem = null;
        let lastBackspaceTime = 0;

        function showToast(text) {
            const toast = document.getElementById('toast');
            toast.innerText = text;
            toast.style.display = 'block';
            setTimeout(() => { toast.style.display = 'none'; }, 2000);
        }
        
        function showToast2(text) {
            const t = document.getElementById('toast2');
            t.innerText = text;
            if (t.classList.contains('show')) return; // 避免重复触发
            t.classList.add('show');
            setTimeout(() => t.classList.remove('show'), 2000);
        }

        function refreshSnippets(newData) {
            snippets.length = 0;
            newData.forEach(s => snippets.push(s));
            render(searchInput.value);
        }

        function refreshHistory(newData) {
            historyData.length = 0;
            newData.forEach(h => historyData.push(h));
            render(searchInput.value);
        }

        function render(filter = '') {
            if (isEditing || isPreviewing) return;
            
            // 排序逻辑:次数优先,时间次之
            snippets.sort((a, b) => {
                const countA = a.count || 0;
                const countB = b.count || 0;
                if (countB !== countA) return countB - countA;
                
                const timeA = a.lastUsed || 0;
                const timeB = b.lastUsed || 0;
                return timeB - timeA;
            });

            // 1. 处理主片段列表 (增加分组过滤)
            let temp = [...snippets];
            if (currentGroup !== '全部') {
                temp = temp.filter(s => s.group === currentGroup);
            }

            if (filter.trim() === '') {
                filtered = temp;
            } else {
                filtered = temp.filter(s => 
                    s.title.toLowerCase().includes(filter.toLowerCase()) || 
                    s.pinyin.toLowerCase().includes(filter.toLowerCase())
                );
            }
            
            // 2. 处理历史记录列表 (始终存在)
            if (filter.trim() === '') {
                filteredHistory = historyData.map(h => ({ title: h, content: h, isHistory: true }));
            } else {
                filteredHistory = historyData
                    .filter(h => h.toLowerCase().includes(filter.toLowerCase()))
                    .map(h => ({ title: h, content: h, isHistory: true }));
            }

            updateListsUI();
        }

        function updateListsUI() {
            // 根据焦点的状态准确控制上拉面板
            if (currentFocus === 'history') {
                historyListEl.classList.add('expanded');
                historyListEl.style.opacity = '1';
                historyListEl.style.pointerEvents = 'auto';
            } else {
                historyListEl.classList.remove('expanded');
                historyListEl.style.opacity = '0';
                historyListEl.style.pointerEvents = 'none';
            }

            if (currentFocus === 'main' && searchInput.value.trim() !== '') {
                historySelectedIndex = -1;
            }

            // 更新主列表 (下方)
            listEl.innerHTML = '';
            if (filtered.length === 0 && searchInput.value.trim() !== '') {
                listEl.innerHTML = `
                    <div class='empty-hint' style='display:flex; flex-direction:column; align-items:center; opacity:0.6; padding-top:40px;'>
                        <div style='width:60px; height:60px; border-radius:30px; background:var(--surface); display:flex; align-items:center; justify-content:center; margin-bottom:15px; border:1px solid var(--border-color);'>
                            <div style='font-size:24px;'></div>
                        </div>
                        <div style='color:var(--accent); font-weight:500; margin-bottom:4px;'>未找到匹配片段</div>
                        <div style='font-size:12px;'>Enter: 新建并编辑 | Ctrl+Enter: 存库退出</div>
                    </div>`;
            } else {
                filtered.forEach((s, i) => {
                    const div = document.createElement('div');
                    div.className = 'snippet-item' + (currentFocus === 'main' && i === selectedIndex ? ' selected' : '');
                    div.innerHTML = `
                        <div style='flex:1; min-width:0;'>
                            <div style='display:flex; align-items:baseline; gap:8px;'>
                                <div class='title' style='font-weight:600; color:var(--text); fontSize:14px;'>${s.title}</div>
                                <div class='pinyin' style='font-size:10px; color:var(--text-dim); text-transform:uppercase;'>${s.pinyin || ''}</div>
                            </div>
                            <div style='font-size:12px; color:var(--text-dim); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:3px;'>${s.content.replace(/\n/g, ' ')}</div>
                        </div>`;
                    div.onclick = () => { currentFocus = 'main'; selectedIndex = i; select(); };
                    div.oncontextmenu = (e) => {
                        e.preventDefault();
                        currentFocus = 'main';
                        selectedIndex = i;
                        showContextMenu(e.clientX, e.clientY);
                        updateListsUI();
                    };
                    listEl.appendChild(div);
                });
            }

            // 更新历史列表 (上方)
            historyListEl.innerHTML = '';
            if (filteredHistory.length > 0) {
                filteredHistory.forEach((h, i) => {
                    const div = document.createElement('div');
                    div.className = 'snippet-item history-item' + (currentFocus === 'history' && i === historySelectedIndex ? ' selected' : '');
                    div.style = 'padding: 8px 14px; margin: 1px 6px;';
                    div.innerHTML = `
                        <div style='font-size:11px; color:#555; margin-right:8px;'></div>
                        <div class='title' style='font-size:13px; color:var(--text); flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;'>${h.title}</div>`;
                    div.onclick = () => { currentFocus = 'history'; historySelectedIndex = i; select(); };
                    historyListEl.appendChild(div);
                });

                // 添加一个标签
                const label = document.createElement('div');
                label.className = 'section-label';
                label.style = 'order: 9999; margin-top: 5px; opacity: 0.5;';
                label.innerText = '最近历史';
                historyListEl.appendChild(label);
            }
            // 自动滚动到选中项
            if (currentFocus === 'main') {
                const selected = listEl.querySelector('.selected');
                if (selected) selected.scrollIntoView({ block: 'nearest' });
            } else if (currentFocus === 'history') {
                const selected = historyListEl.querySelector('.selected');
                if (selected) selected.scrollIntoView({ block: 'nearest' });
            }
        }

        function openEdit(item) {
            isEditing = true;
            editingItem = item;
            document.getElementById('editTitle').value = item.title;
            document.getElementById('editPinyin').value = item.pinyin || item.title.substring(0, 4);
            document.getElementById('editContent').value = item.content;
            
            const groupSelect = document.getElementById('editGroup');
            groupSelect.innerHTML = '';
            groups.forEach(g => {
                const opt = document.createElement('option');
                opt.value = g;
                opt.innerText = g;
                if (item.group === g) opt.selected = true;
                groupSelect.appendChild(opt);
            });

            editView.style.display = 'flex';
            document.getElementById('editTitle').focus();
        }

        function updateSyncStatus(text, isError) {
            const el = document.getElementById('syncStatus');
            const txt = document.getElementById('syncText');
            if (!el || !txt) return;
            txt.innerText = text;
            el.classList.remove('error', 'syncing', 'success');
            
            if (isError) {
                el.classList.add('error');
            } else if (text.includes('正在')) {
                el.classList.add('syncing');
            } else if (text.includes('已') || text === '同步完成') {
                el.classList.add('success');
                setTimeout(() => el.classList.remove('success'), 3000);
            }
        }

        const contextMenu = document.getElementById('contextMenu');
        function showContextMenu(x, y) {
            contextMenu.style.display = 'block';
            contextMenu.style.left = x + 'px';
            contextMenu.style.top = y + 'px';
            
            // 防止超出屏幕
            const rect = contextMenu.getBoundingClientRect();
            if (x + rect.width > window.innerWidth) contextMenu.style.left = (window.innerWidth - rect.width - 5) + 'px';
            if (y + rect.height > window.innerHeight) contextMenu.style.top = (window.innerHeight - rect.height - 5) + 'px';
        }

        window.onclick = () => { contextMenu.style.display = 'none'; };
        
        function handleContextEdit() {
            if (currentFocus === 'main' && filtered.length > 0) {
                openEdit(filtered[selectedIndex]);
            }
        }
        
        function handleContextDelete() {
            if (currentFocus === 'main' && filtered.length > 0) {
                const item = filtered[selectedIndex];
                if (confirm('确定要删除片段 ""' + item.title + '"" 吗?')) {
                    window.chrome.webview.postMessage({ action: 'delete_snippet', title: item.title });
                }
            }
        }

        function closeEdit() {
            isEditing = false;
            editView.style.display = 'none';
            searchInput.focus();
        }

        function saveEdit() {
            const title = document.getElementById('editTitle').value.trim();
            const pinyin = document.getElementById('editPinyin').value.trim();
            const content = document.getElementById('editContent').value;
            const group = document.getElementById('editGroup').value;
            
            if (!title) return;
            
            const oldTitle = editingItem.title;
            // 更新本地数据 (为了 Hide 之前的反馈感)
            editingItem.title = title;
            editingItem.pinyin = pinyin;
            editingItem.content = content;
            editingItem.group = group;

            window.chrome.webview.postMessage({ 
                action: 'update_snippet', 
                oldTitle: oldTitle,
                title: title, 
                pinyin: pinyin, 
                content: content,
                group: group
            });
        }

        // Ctrl+S 保存并退回到主列表
        function saveEditOnly() {
            const title = document.getElementById('editTitle').value.trim();
            const pinyin = document.getElementById('editPinyin').value.trim();
            const content = document.getElementById('editContent').value;
            const group = document.getElementById('editGroup').value;
            
            if (!title) return;
            
            const oldTitle = editingItem.title;
            
            // 更新本地数据并同步
            window.chrome.webview.postMessage({ 
                action: 'update_snippet_only', 
                oldTitle: oldTitle,
                title: title, 
                pinyin: pinyin, 
                content: content,
                group: group
            });
            
            closeEdit(); // Ctrl+S 后退回到主列表
        }

        // 预览功能
        function openPreview(item) {
            isPreviewing = true;
            const preview = document.getElementById('previewView');
            document.getElementById('previewTitle').textContent = item.title;
            document.getElementById('previewContent').textContent = item.content;
            preview.style.display = 'flex';
        }

        function closePreview() {
            isPreviewing = false;
            document.getElementById('previewView').style.display = 'none';
            searchInput.focus();
        }

        function select(isCtrl) {
            let item = null;
            if (currentFocus === 'history' && filteredHistory.length > 0) {
                item = filteredHistory[historySelectedIndex];
                // 历史记录点击后,填充到输入框并把焦点切回输入框/主列表
                searchInput.value = item.title;
                currentFocus = 'main';
                historySelectedIndex = -1;
                render(searchInput.value);
                return;
            } else if (currentFocus === 'main' && filtered.length > 0) {
                item = filtered[selectedIndex];
                window.chrome.webview.postMessage({ action: 'save_history', text: searchInput.value });
                window.chrome.webview.postMessage({ action: 'paste', title: item.title, content: item.content });
            } else {
                const val = searchInput.value.trim();
                if (val) {
                    if (isCtrl) {
                        window.chrome.webview.postMessage({ action: 'create', title: val, silent: true });
                    } else {
                        window.chrome.webview.postMessage({ action: 'create_and_edit', title: val });
                    }
                }
            }
        }

        searchInput.oninput = (e) => {
            selectedIndex = 0;
            historySelectedIndex = -1;
            currentFocus = 'main';
            historyListEl.classList.remove('expanded'); // 输入时收起上拉框
            render(e.target.value);
            window.chrome.webview.postMessage({ action: 'save_search', text: e.target.value });
        };

        const config = " + JsonConvert.SerializeObject(_config) + @";
        let currentGroup = '全部';
        let groups = config.Groups || [];

        function setGroups(newGroups) {
            groups = newGroups;
            config.Groups = newGroups;
            updateGroupTabs();
        }

        function setConfig(newConfig) {
            Object.assign(config, newConfig);
            if (newConfig.Groups) groups = newConfig.Groups;
            if (typeof updateGroupTabs === 'function') updateGroupTabs();
        }

        function updateGroupTabs() {
            const tabsEl = document.getElementById('groupTabs');
            tabsEl.innerHTML = '<div class=""group-tab ' + (currentGroup === ""全部"" ? ""active"" : """") + '"" onclick=""switchGroup(\'全部\')"">全部</div>';
            groups.forEach(g => {
                const active = currentGroup === g ? 'active' : '';
                tabsEl.innerHTML += `<div class=""group-tab ${active}"" onclick=""switchGroup('${g}')"">${g}</div>`;
            });
        }

        function switchGroup(g) {
            currentGroup = g;
            selectedIndex = 0;
            updateGroupTabs();
            render(searchInput.value);
        }

        function openSettings() {
            document.getElementById('settingsView').style.display = 'flex';
            document.getElementById('settingMode').value = config.TriggerMode;
            document.getElementById('settingValue').value = config.TriggerValue;
            document.getElementById('settingModifiers').value = config.HotkeyModifiers;
            document.getElementById('settingKey').value = config.HotkeyKey;
            document.getElementById('settingGroups').value = groups.join(',');
        }

        function saveConfig() {
            const mode = document.getElementById('settingMode').value;
            const val = document.getElementById('settingValue').value.trim();
            const key = document.getElementById('settingKey').value.trim();
            
            if (mode === 'TextCommand' && !val) {
                alert('文本指令内容不能为空!');
                return;
            }
            if (mode === 'DoubleTap' && !val) {
                alert('双击触发按键不能为空!');
                return;
            }
            if (mode === 'Combination' && !key) {
                alert('组合键主键不能为空!');
                return;
            }

            config.TriggerMode = mode;
            config.TriggerValue = val;
            config.HotkeyModifiers = document.getElementById('settingModifiers').value;
            config.HotkeyKey = key;
            groups = document.getElementById('settingGroups').value.split(',').map(g => g.trim()).filter(g => g);
            config.Groups = groups;
            
            window.chrome.webview.postMessage({ action: 'save_config', config: config });
            showToast2('配置已保存');
            
            if (document.body.classList.contains('settings-only-mode')) {
                // 独立窗口
            } else {
                document.getElementById('settingsView').style.display = 'none';
                updateGroupTabs();
            }
        }

        function closeSettings() {
            if (document.body.classList.contains('settings-only-mode')) {
                window.chrome.webview.postMessage({ action: 'close' });
            } else {
                document.getElementById('settingsView').style.display = 'none';
            }
        }

        window.onkeydown = (e) => {
            if (isEditing) {
                if (e.key === 'Escape') { closeEdit(); }
                if (e.key === 'Enter' && e.ctrlKey) saveEdit();
                if (e.key === 's' && e.ctrlKey) { e.preventDefault(); saveEditOnly(); }
                return;
            }
            if (document.getElementById('settingsView').style.display === 'flex') {
                if (e.key === 'Escape') { closeSettings(); }
                if ((e.key === 's' && e.ctrlKey) || (e.key === 'Enter' && e.ctrlKey)) { e.preventDefault(); saveConfig(); }
                return;
            }
            if (isPreviewing) {
                if (e.key === 'Escape' || e.key === 'ArrowRight') closePreview();
                return;
            }

            if (e.key === 'Backspace' && searchInput.value === '') {
                const now = Date.now();
                if (now - lastBackspaceTime < 300) {
                    window.chrome.webview.postMessage({ action: 'close' });
                }
                lastBackspaceTime = now;
            }

            if (e.key === 'Delete') {
                if (currentFocus === 'main' && filtered.length > 0) {
                    const item = filtered[selectedIndex];
                    if (confirm(`确定要删除片段「${item.title}」吗?`)) {
                        window.chrome.webview.postMessage({ action: 'delete_snippet', title: item.title });
                    }
                } else if (currentFocus === 'history' && filteredHistory.length > 0) {
                    const item = filteredHistory[historySelectedIndex];
                    // 历史记录直接删除,无需确认
                    window.chrome.webview.postMessage({ action: 'delete_history', text: item.title });
                }
                return;
            }

            if (e.key === 'Tab') {
                e.preventDefault();
                const allGroups = ['全部', ...groups];
                let idx = allGroups.indexOf(currentGroup);
                if (e.shiftKey) {
                    idx = (idx - 1 + allGroups.length) % allGroups.length;
                } else {
                    idx = (idx + 1) % allGroups.length;
                }
                switchGroup(allGroups[idx]);
            }

            if (e.key === 'ArrowUp') {
                e.preventDefault();
                if (currentFocus === 'main') {
                    if (selectedIndex > 0) {
                        selectedIndex--;
                    } else if (searchInput.value.trim() === '') {
                        // 处于搜索框顶端,按下进入历史模式
                        currentFocus = 'history';
                        historySelectedIndex = 0;
                    }
                } else if (currentFocus === 'history') {
                    if (historySelectedIndex < filteredHistory.length - 1) {
                        historySelectedIndex++;
                    }
                }
                updateListsUI();
            } else if (e.key === 'ArrowDown') {
                e.preventDefault();
                if (currentFocus === 'history') {
                    if (historySelectedIndex > 0) {
                        historySelectedIndex--;
                    } else {
                        // 连按向下,回到输入框并收起历史
                        currentFocus = 'main';
                        historySelectedIndex = -1;
                        selectedIndex = 0;
                        historyListEl.classList.remove('expanded');
                    }
                } else {
                    // 主列表中往下按
                    if (selectedIndex < filtered.length - 1) {
                        selectedIndex++;
                    }
                }
                updateListsUI();
            } else if (e.key === 'Enter') {
                select(e.ctrlKey);
            } else if (e.key === 'ArrowRight' || e.key === 'F2') {
                if (currentFocus === 'main' && filtered.length > 0) openEdit(filtered[selectedIndex]);
            } else if (e.key === 'Escape') {
                window.chrome.webview.postMessage({ action: 'close' });
            }
        };

        window.resetState = () => {
            isEditing = false;
            isPreviewing = false;
            editView.style.display = 'none';
            document.getElementById('previewView').style.display = 'none';
            searchInput.value = '';
            selectedIndex = 0;
            historySelectedIndex = -1;
            currentFocus = 'main';
            render('');
            setTimeout(() => searchInput.focus(), 50);
        };

        searchInput.value = initialSearch;
        render(initialSearch);
        if (typeof updateGroupTabs === 'function') updateGroupTabs();
        setTimeout(() => searchInput.focus(), 100);

        // 按钮点击事件
        document.getElementById('createBtn').onclick = () => {
            const val = searchInput.value.trim();
            if (val) window.chrome.webview.postMessage({ action: 'create_and_edit', title: val });
        };

        let isPinned = false;
        document.getElementById('pinBtn').onclick = function() {
            isPinned = !isPinned;
            this.classList.toggle('active', isPinned);
            window.chrome.webview.postMessage({ action: 'pin', pinned: isPinned });
        };
        
        // 初始同步状态请求
        window.chrome.webview.postMessage({ action: 'get_sync_status' });
    </script>
</body>
</html>";
    }
}

// ---------------------------------------------------------
// CloudState Helper (Reflected + Log)
// ---------------------------------------------------------
static class CloudStateHelper {
    public static Action<string> Logger;
    private static Type _t; private static object _i; private static System.Reflection.MethodInfo _s, _r;
    static CloudStateHelper() { Init(); }
    static void Init() {
        if (_i!=null) return;
        foreach(var a in AppDomain.CurrentDomain.GetAssemblies()){
            _t=a.GetType("Quicker.Public.CloudState"); if(_t!=null)break;
        }
        if(_t==null)return;
        var f=_t.GetField("OWJCWqTrDY4Wyj5Oc7t3", System.Reflection.BindingFlags.Static|System.Reflection.BindingFlags.NonPublic|System.Reflection.BindingFlags.Public);
        if(f!=null)_i=f.GetValue(null);
        if(_i==null){_i=Activator.CreateInstance(_t,true); if(f!=null)try{f.SetValue(null,_i);}catch{}}
        _s=_t.GetMethod("SaveTextAsync", new[]{typeof(string),typeof(string),typeof(double)});
        _r=_t.GetMethod("ReadTextAsync", new[]{typeof(string),typeof(double)});
    }
    public static bool SaveText(string k, string v, double e) {
        if(Logger!=null) Logger($"[Helper] SaveText Key={k} Len={v?.Length}");
        if(_i==null)Init(); if(_s==null)return false;
        try {
            var task = Task.Run(async () => {
                var t = (Task)_s.Invoke(_i, new object[]{k, v ?? "", e});
                await t;
            });
            return task.Wait(10000); // 10秒超时
        } catch (Exception ex) {
            string msg = ex is AggregateException age ? age.InnerException?.Message : ex.Message;
            if(Logger!=null) Logger("[HelperErr] " + msg);
            return false;
        }
    }
    public static string ReadText(string k, double e) {
        if(Logger!=null) Logger($"[Helper] ReadText Key={k}");
        if(_i==null)Init(); if(_r==null)return null;
        try {
            var task = Task.Run(async () => {
                var t = (Task)_r.Invoke(_i, new object[]{k, e});
                await t;
                return t.GetType().GetProperty("Result")?.GetValue(t) as string;
            });
            if (task.Wait(10000)) return task.Result;
            return null;
        } catch (Exception ex) {
            string msg = ex is AggregateException age ? age.InnerException?.Message : ex.Message;
            if(Logger!=null) Logger("[HelperErr] " + msg);
            return null;
        }
    }
}


动作配置 (JSON)

{
  "ActionId": "48b6705d-521d-4a53-942f-bd783ab6dedb",
  "Title": "减减闪填",
  "Description": "不想再记缩写?不想再背地址?连按两下减号 '--',常用回复、收货地址、代码模版即刻弹出,支持云端自动同步到您的所有电脑。",
  "Icon": "https://files.getquicker.net/_icons/9130C3D3815DFF4AA2F9C5A9DCC3F59C3AC12CF3.png",
  "Variables": [
    {
      "Type": 0,
      "Desc": "结果文本(自动)",
      "IsOutput": false,
      "Key": "text"
    },
    {
      "Type": 0,
      "Desc": "返回值(自动)",
      "IsOutput": false,
      "Key": "rtn"
    },
    {
      "Type": 0,
      "Desc": "错误信息(自动)",
      "IsOutput": false,
      "Key": "errMessage"
    },
    {
      "Type": 0,
      "Desc": "错误详情(自动)",
      "IsOutput": false,
      "Key": "errorDesc"
    },
    {
      "Type": 2,
      "Desc": "后台执行",
      "DefaultValue": "True",
      "IsInput": false,
      "Key": "后台执行线程"
    },
    {
      "Type": 2,
      "Desc": "静默启动",
      "DefaultValue": "true",
      "IsInput": false,
      "Key": "silent"
    }
  ],
  "References": [
    "Microsoft.Web.WebView2.Wpf.dll",
    "Microsoft.Web.WebView2.Core.dll",
    "Newtonsoft.Json.dll"
  ]
}

使用方法

  1. 确保您已安装 动作构建 (Action Builder) 动作。
  2. 将本文提供的 .cs.json 文件下载到同一目录
  3. 选中 .json 文件,运行「动作构建」即可生成并安装动作。
posted @ 2025-12-27 22:04  luoluoluo22  阅读(15)  评论(0)    收藏  举报