剪切板历史工具

我们来详细解释一下这段代码。

这是一段为 JSBox 应用编写的 剪贴板历史管理器 脚本。JSBox 是一个在 iOS 上运行 JavaScript 代码的强大工具,可以通过脚本实现各种自动化功能。

核心功能概览

这个脚本的核心目标是:

  1. 自动记录:在后台静默监控系统剪贴板,每当有新的内容被复制时,自动保存下来。

  2. 持久化存储:将剪贴板历史记录安全地存储在 iOS 的钥匙串(Keychain)中,即使关闭脚本或重启手机,数据也不会丢失。

  3. 管理与交互:提供一个用户界面,让你可以浏览、搜索、复制、置顶、编辑和删除历史记录。


代码分段详解

我们将代码分成几个逻辑部分来理解。

1. 全局设置 (Settings)

  
const KC_KEY = "CLIPBOARD_HISTORY"; // 在钥匙串中存储数据用的“钥匙”
  

  
// Settings
  
const MAX_ITEMS = 200;             // 最多保存200条历史记录
  
const POLL_INTERVAL = 1.5;         // 每1.5秒检查一次剪贴板
  
const MIN_TEXT_LEN = 1;            // 文本长度至少为1才记录
  
const IGNORE_SAME_CONSEC = true;   // 如果连续复制同样的内容,则忽略
  

这部分定义了脚本的基本行为:

  • KC_KEY: 一个唯一的字符串,用作在钥匙串中存取数据的标识符。

  • MAX_ITEMS: 为了防止历史记录无限增长占用过多空间,这里限制最多只保存200条。当超过时,最旧的记录会被删除。

  • POLL_INTERVAL: 轮询间隔,即脚本每隔多久检查一次剪贴板有没有新内容。1.5秒是一个比较均衡的设置。

  • MIN_TEXT_LEN: 太短的内容(比如一个空格)通常没有意义,所以不记录。

  • IGNORE_SAME_CONSEC: 这是一个优化。如果你反复按 Cmd+C 复制同一个东西,脚本只会记录一次。

2. 数据存储与管理函数 (Utilities)

这部分函数负责处理数据的读取、保存和清理,它们都与钥匙串($keychain)交互。

  • loadHistory(): 从钥匙串中读取历史记录。它使用 JSON.parse 将存储的字符串转换回 JavaScript 数组。如果读取失败或没有数据,返回一个空数组。

  • saveHistory(list): 将历史记录数组(list)保存到钥匙串。在保存前,它会确保记录不超过 MAX_ITEMS 的限制,并使用 JSON.stringify 将数组转换为字符串。

  • clearAll(): 清空所有历史记录,通过从钥匙串中移除对应的 KC_KEY 实现。

3. 核心数据操作函数

这部分函数是业务逻辑的核心,负责增、删、改、查历史记录。

  • normalizeText(t): 一个辅助函数,用于“标准化”文本。它会统一换行符(\r\n -> \n)并去除首尾的空格,这样可以确保比较文本时的一致性。

  • upsertItem(list, text, opts): 这是最核心的函数之一,实现了 "Update or Insert"(更新或插入) 的逻辑。

    • 如果 text 在历史记录中 已存在,它会更新这条记录的时间戳,并将其移动到列表顶部(如果记录被置顶,则移动到所有置顶项的顶部)。

    • 如果 text新内容,它会创建一个新的记录对象(包含文本、当前时间戳、是否置顶),并将其插入到列表顶部(同样,置顶项优先)。

    • 这个函数保证了最新使用或复制的内容总是在最前面,非常符合使用习惯。

  • pinToggle(list, index, filtered): 切换指定记录的 置顶 状态。它会修改记录的 pinned 属性,然后对整个列表重新排序,确保所有置顶项都在非置顶项之前,最后保存。

  • deleteItem(list, index, filtered): 删除指定的记录。

4. UI 界面与状态管理

这部分代码负责构建用户界面并处理界面的数据更新。

  • state: 一个全局状态对象,存储了当前所有的历史记录 (list)、经过搜索过滤后的记录 (filtered)、搜索关键词 (q) 等。UI 的显示完全依赖于这个 state 对象。

  • filterList(): 根据搜索框中的关键词 (state.q) 过滤历史记录。它支持多个关键词搜索,并会对搜索结果进行排序,让匹配度更高的结果排在前面。

  • reloadListView(): 刷新列表视图。它会先调用 filterList() 获取最新的显示数据,然后将数据格式化(比如处理标题、副标题、时间戳、置顶标记 📌),最后更新到界面上的列表($("list"))中。

  • copyItem(index): 当用户点击列表中的某一项时触发。它会将该项的文本复制到系统剪贴板,并调用 upsertItem 将其更新到列表顶部。

  • editSheet(index): 当用户选择编辑某一项时,弹出一个新的页面,其中包含一个文本框,允许用户修改内容并保存。

5. 剪贴板自动监控 (Polling)

  • startPolling(): 启动一个定时器 ($timer.schedule),按照前面设定的 POLL_INTERVAL 间隔,周期性地执行一个任务。

  • 定时器任务:检查当前剪贴板的内容,如果内容是新的(与上次复制的不同,也与列表顶部的不同),就调用 upsertItem 将其添加到历史记录中,并刷新界面。

  • stopPolling(): 停止定时器。这很重要,当脚本界面关闭时,需要调用它来停止后台检查,以节省系统资源。

6. 主界面定义 ($ui.render)

这是脚本的入口,定义了整个应用的界面布局和事件处理。

  • views 数组:

    • Toolbar (工具栏): 位于顶部的视图,包含了“导入”、“导出”、“清空”三个按钮和一个“搜索框”。

      • 导入: 手动将当前剪贴板的内容添加到历史记录。

      • 导出: 将所有历史记录格式化成纯文本,并弹出系统分享菜单。

      • 清空: 弹出确认框,防止误操作,确认后清空所有历史。

      • 搜索框: 输入文字时会实时调用 reloadListView 筛选列表。

    • List (列表): 显示剪贴板历史的主体部分。

      • template: 定义了列表中每一行的样式(一个标题标签和一个副标题标签)。

      • actions: 定义了列表项 左滑 时出现的按钮(“置顶/取消”和“删除”)。

      • events: 定义了列表的交互事件。

        • didSelect: 单击 列表项,触发 copyItem 复制内容。

        • didLongPress: 长按 列表项,弹出一个菜单,提供复制、置顶、编辑、删除、分享等更多操作。

  • events 对象:

    • appeared: 当脚本界面 出现 时触发。在这里,它会加载历史记录,并启动 startPolling 开始监控剪贴板。

    • disappeared: 当脚本界面 消失(比如切换到其他应用或关闭)时触发。在这里,它会调用 stopPolling 停止监控,节省电量。

总结

总而言之,这是一个设计精良、功能完备的剪贴板历史管理工具。它利用 JSBox 提供的原生 UI 和系统能力($clipboard, $keychain, $timer),实现了自动记录、安全存储、高效管理和友好交互等一系列功能。代码结构清晰,通过 state 对象管理状态,通过函数分离不同职责,是一个很好的 JSBox 脚本范例。