[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"
]
}
使用方法
- 确保您已安装 动作构建 (Action Builder) 动作。
- 将本文提供的
.cs和.json文件下载到同一目录。 - 选中
.json文件,运行「动作构建」即可生成并安装动作。
浙公网安备 33010602011771号