// ==UserScript== // @name Rive汉化 // @namespace Rive_CN // @version 1.3 // @description Rive 编辑器汉化 + 自定义字体 + 面板管理 // @author 一身惆怅 // @match https://editor.rive.app/* // @run-at document-start // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect editor.rive.app // @connect cdn.jsdelivr.net // @license MIT // ==/UserScript== (() => { 'use strict'; const CONFIG = { DB_NAME: 'RiveCN_Cache', DB_VERSION: 1, STORE_NAME: 'data', CN_FONT_FAMILY: 'RiveCN', INJECT_FONT_ASSET: 'assets/fonts/rivecn.ttf', CACHE_TTL_DAYS: 21, REQUEST_TIMEOUT_MS: 60_000, HOOK_MAX_MS: 45_000, UI_MIN_SHOW_MS: 650, DEBUG_WARN_ONCE: false, }; const warnOnce = (() => { const seen = new Set(); return (k, ...args) => { if (!CONFIG.DEBUG_WARN_ONCE) return; if (seen.has(k)) return; seen.add(k); console.warn('[Rive汉化]', ...args); }; })(); const nowMs = () => Date.now(); const ttlMs = CONFIG.CACHE_TTL_DAYS * 24 * 60 * 60 * 1000; const wrapCache = (v, t = nowMs()) => ({ t, v }); const unwrapCache = (x) => (x && typeof x === 'object' && 'v' in x ? x.v : x); const cacheTime = (x) => (x && typeof x === 'object' && 't' in x ? x.t : 0); const isFresh = (x) => { if (!ttlMs) return true; const t = cacheTime(x); if (!t) return true; return nowMs() - t <= ttlMs; }; const PRESET_FONTS = [ { id: 'preset_harmony', name: '鸿蒙黑体', url: 'https://cdn.jsdelivr.net/npm/@fontpkg/harmony-os-sans-sc@1.0.3/HarmonyOS_Sans_SC_Regular.ttf', typeHint: 'ttf', }, { id: 'preset_sourcehan', name: '思源黑体', url: 'https://cdn.jsdelivr.net/npm/source-han-sans-cn@1.0.0/SourceHanSansCN-Regular.otf', typeHint: 'otf', }, { id: 'preset_lxgw_wenkai_lite', name: '霞鹜文楷Lite', url: 'https://cdn.jsdelivr.net/gh/lxgw/LxgwWenKai-Lite@main/fonts/TTF/LXGWWenKaiLite-Regular.ttf', typeHint: 'ttf', }, ]; const DEFAULT_FONT_ID = PRESET_FONTS[0].id; const isPreset = (id) => PRESET_FONTS.some((x) => x.id === id); const getPreset = (id) => PRESET_FONTS.find((x) => x.id === id) || null; //字典 const DICT = { "LOADING RIVE": "Rive 加载中", "LOADING FILE...": "文件加载中...", "Filename": "文件名", "Revision History": "版本历史", "Current version": "当前版本", "Create revision...": "创建版本...", "Create new revision": "创建新版本", "Rename revision": "重命名版本", "Name your revision": "为版本命名", "Revision name": "版本名称", "Open revision history": "打开版本历史", "Show autosave revisions": "显示自动保存版本", "Restoring": "正在恢复", "Restored": "已恢复", "revision": "版本", "Preview revision": "预览版本", "Restore revision": "恢复版本", "Name revision...": "命名版本...", "Rename revision...": "重命名版本...", "Fetching revision history...": "正在获取版本历史...", "Collapse all": "全部折叠", "Expand all": "全部展开", "Save": "保存", "Documentation": "文档", "View Changelog": "查看更新日志", "Rive Community": "Rive 社区", "Get the Desktop App": "获取桌面版应用", "Relative Data Binds": "相对数据绑定", "All Viewports": "所有视口", "Options": "选项", "Audio": "音频", "Help": "帮助", "Select": "选择", "Position": "位置", "Corner": "角", "Corner Radius": "圆角半径", "Scale": "缩放", "Rotate": "旋转", "Translate": "平移", "Opacity": "不透明度", "Follow": "跟随", "Follow Path": "跟随路径", "Text Follow Path": "文本沿路径排列", "Radial": "径向", "Trim": "修剪", "Trim Start": "修剪起点", "Trim End": "修剪终点", "Select Text": "选择文本", "W": "宽", "H": "高", "L": "左", "Rotation": "旋转", "Gradient End": "渐变终点", "Gradient Start": "渐变起点", "Angle": "角度", "Length": "长度", "In": "入", "Out": "出", "Color": "颜色", "Selected Colors": "选中颜色", "Trim Offset": "修剪偏移", "Draw Rule": "绘制规则", "Radius Top Left": "左上角半径", "Clip": "裁剪", "Main Artboard": "主画板", "New": "新建", "Freeze": "冻结", "Freeze active": "冻结当前状态", "Draw in World Space": "世界空间绘制", "X Axis": "X 轴", "Y Axis": "Y 轴", "Handle Source": "控制柄源", "Connection lost. Reconnecting in": "连接已断开,正在重新连接", "Reconnect Now": "立即重连", "Design background": "设计背景", "Animate background": "动画背景", "Copy Fill": "复制填充", "Copy Stroke": "复制描边", "Copy Styles": "复制样式", "Paste Styles": "粘贴样式", "Paste with timelines...": "粘贴(含时间轴)...", "Paste timelines": "粘贴时间轴", "Paste timelines to Artboard": "粘贴时间轴至画板", "Paste timelines at selection": "粘贴时间轴至选中项", "Paste timelines...": "粘贴时间轴...", "To children": "应用到子对象", "To selection": "应用到选中项", "Default": "默认", "Multiple": "多个", "Values": "值", "Move": "移动", "Bring forward": "上移一层", "Send backward": "下移一层", "Bring to front": "置于顶层", "Send to back": "置于底层", "Artboard": "画板", "Add to Active Artboard": "添加到当前画板", "Nest Component": "嵌套组件", "Nest Artboard": "嵌套画板", "Layout": "布局", "Row": "行", "Column": "列", "Group": "编组", "List Path": "列表路径", "Solo": "单独显示", "Effect Group": "效果组", "Pen": "钢笔", "Rectangle": "矩形", "Ellipse": "椭圆", "Triangle": "三角形", "Polygon": "多边形", "Star": "星形", "Bone": "骨骼", "Weight": "权重", "Joystick": "摇杆", "Event": "事件", "Property Group": "属性组", "Arrangement Tools": "排列工具", "Bone Tools": "骨骼工具", "Create Tools": "创建工具", "Text Tool": "文本工具", "Text Input - Multiline": "文本输入 - 多行", "Text Tools": "文本工具", "Layout Tool": "布局工具", "Component Tool": "组件工具", "Components": "组件", "Component": "组件", "Stage Control Tools": "舞台控制工具", "Script Tools": "脚本工具", "Blank Script": "空白脚本", "Custom Property Tool": "自定义属性工具", "No active artboard.": "当前无活动画板。", "No weighted vertices.": "无加权顶点。", "Play default State Machine": "播放默认状态机", "Set default State Machine": "设置默认状态机", "Default State Machine": "默认状态机", "Create a State Machine": "创建状态机", "Snapping": "对齐吸附", "Bones": "骨骼", "Targets": "目标", "Motion Paths": "运动路径", "Joysticks": "摇杆", "Events": "事件", "Inactive Solos": "非活动单独显示", "Layouts": "布局", "Show Final Playback": "显示最终播放效果", "Rulers": "标尺", "Guide": "参考线", "Guides": "参考线", "Lock guides": "锁定参考线", "Locked guides": "参考线已锁定", "Unlocked guides": "参考线已解锁", "Clear guides": "清除参考线", "Guide layer": "参考线图层", "Transform Tools": "变换工具", "Orient": "方向", "Distance": "距离", "Start": "起点", "End": "终点", "Offset": "偏移", "Snap to pixel": "吸附到像素", "Gizmo": "变换控制器", "User Cursors": "用户光标", "Pause Component Updates": "暂停组件更新", "View only mode.": "仅查看模式。", " You cannot make edits to this file": " 您无法编辑此文件", "Close Revision History": "关闭版本历史", "Failed to save revision": "保存版本失败", "Saved": "已保存", "Vertices": "顶点", "Thickness": "粗细", "Strokes": "描边", "Fills and Strokes": "填充与描边", "Background": "背景", "Foreground": "前景", "Hold": "保持", "Linear": "线性", "Draw Order": "绘制顺序", "Cubic": "三次", "Cubic Value": "三次值", "Elastic": "弹性", "Translation": "平移", "Origin": "原点", "Align origin to baseline": "原点对齐基线", "Baseline": "基线", "Top": "顶部", "Origin X": "原点 X", "Origin Y": "原点 Y", "Threshold": "阈值", "Align Top": "顶部对齐", "Align Middle": "垂直居中", "Align Bottom": "底部对齐", "Align Left": "左对齐", "Align Center": "水平居中", "Align Right": "右对齐", "Auto Width": "自动宽度", "Auto Height": "自动高度", "Fixed Size": "固定尺寸", "Wrap": "换行", "No Wrap": "不换行", "Straight": "直线", "Mirrored": "镜像", "Detached": "分离", "Asymmetric": "非对称", "Select a key": "选择关键帧", "team": "团队", "org": "组织", "Blend": "混合", "Normal": "正常", "Darken": "变暗", "Multiply": "正片叠底", "Color Burn": "颜色加深", "Lighten": "变亮", "Screen": "滤色", "Color Dodge": "颜色减淡", "Overlay": "叠加", "Soft Light": "柔光", "Hard Light": "强光", "Difference": "差值", "Exclusion": "排除", "Hue": "色相", "Saturation": "饱和度", "Luminosity": "明度", "Scripting": "脚本", "Optimization": "优化", "Debug": "调试", "None": "无", "Medium": "中等", "Full": "完整", "Trim Path": "修剪路径", "Sequential": "顺序", "Synced": "同步", "Apply transform": "应用变换", "Export Options": "导出选项", "Behavior": "行为", "Assets": "资源", "Type": "类型", "Include": "包含", "Clip Options": "裁剪选项", "Fill Options": "填充选项", "Feather Options": "羽化选项", "Fill Rule": "填充规则", "Even-Odd": "奇偶", "Non-Zero": "非零", "Clockwise": "顺时针", "Draw Order Rule": "绘制顺序规则", "Stroke Options": "描边选项", "Path Effects": "路径效果", "Path Effect": "路径效果", "Dash Effect": "虚线效果", "Trim Effect": "修剪效果", "Script Effects": "脚本效果", "Select a single object to add another effect": "选择单个对象以添加其他效果", "Can't edit effects across selection": "无法跨选择编辑效果", "IK Constraint": "IK 约束", "Distance Constraint": "距离约束", "Transform Constraint": "变换约束", "Follow Path Constraint": "跟随路径约束", "Rotation Constraint": "旋转约束", "Translation Constraint": "平移约束", "Scale Constraint": "缩放约束", "Strength": "强度", "Mix": "混合", "Mode": "模式", "Closer": "更近", "Exact": "精确", "Further": "更远", "Transform": "变换", "Source Space": "源空间", "Dest Space": "目标空间", "Space": "空间", "Min/Max Space": "最小/最大空间", "World": "世界", "Local": "本地", "Copy": "复制", "Copy X": "复制 X", "Copy Y": "复制 Y", "Min": "最小", "Max": "最大", "Fill": "填充", "Stroke": "描边", "Feather": "羽化", "Opaque Target": "不透明目标", "Add Feather": "添加羽化", "Remove Feather": "移除羽化", "Amount": "数量", "Direction": "方向", "Fill Rule has been set to Clockwise": "填充规则已设置为顺时针", "Vector feathering requires Rive's Clockwise Fill Rule. This fill rule behaves differently on self-intersecting paths, which may appear different from your original design.": "矢量羽化需要使用 Rive 的顺时针填充规则。此填充规则在自相交路径上的表现可能有所不同,最终效果可能与原始设计存在差异。", "Revert": "恢复", "Remove feather to edit fill rule": "移除羽化以编辑填充规则", "Feather X": "羽化 X", "Feather Y": "羽化 Y", "Feather Amount": "羽化程度", "Feather Inner": "内部羽化", "Prevent pointer events passing through targets": "阻止指针事件穿透目标", "Current target can't be made opaque": "当前目标无法设置为不透明", "This is the default draw order position based on Hierarchy.": "这是基于层级的默认绘制顺序位置。", "above target": "目标上方", "below target": "目标下方", "Draw order": "绘制顺序", "Cyclic dependency": "循环依赖", "No target selected": "未选择目标", "Creates a cycle. Learn more.": "将产生循环。了解更多。", "Target": "目标", "Set target": "设置目标", "Target - Square": "目标 - 方形", "Target - Triangle": "目标 - 三角形", "Target Small": "小目标", "Target - Square Small": "目标 - 小方形", "Target - Triangle Small": "目标 - 小三角形", "Ungroup": "取消编组", "Active": "活动", "Component Instance": "组件实例", "Nested artboard cycle": "嵌套画板循环", "Go to Artboard": "转到画板", "Go to component": "转到组件", "No artboard": "无画板", "Style": "样式", "Select a drawable target (no groups) on the Stage or in the Hierarchy.": "请在舞台或层级中选择一个可绘制对象(非编组)。", "Inner Radius": "内半径", "Constraints": "约束", "Select a target": "选择目标", "Connect to...": "连接到...", "Connect from...": "连接自...", "Connect to and from...": "双向连接...", "Constrained object cannot use itself as a target.": "受约束对象不能将自身作为目标。", "Offset is not available for Bones": "偏移不适用于骨骼", "Bone Count": "骨骼数量", "Invert Direction": "反转方向", "Invert": "反转", "Add State": "添加状态", "Add Blend State (1D)": "添加混合状态 (1D)", "Add Blend State (Additive)": "添加混合状态 (叠加)", "Delete Layer": "删除图层", "Duplicate Layer": "复制图层", "Disable Layer": "禁用图层", "Enable Layer": "启用图层", "Inputs": "输入", "Level-up your State Machine": "进阶状态机", "Explore Data Binding": "探索数据绑定", "State Machine": "状态机", "Scripts": "脚本", "Script": "脚本", "from": "来自", "Layers": "图层", "Trigger": "触发器", "Boolean": "布尔值", "Number": "数值", "Listeners": "监听器", "Blank": "空白", "Input Change": "输入变化", "View Model Change": "视图模型变化", "Align Target": "对齐目标", "Scripted Action": "脚本动作", "Entry": "入口", "Any State": "任意状态", "Exit": "退出", "Animation State": "动画状态", "Speed": "速度", "Time": "时间", "s": "秒", "ms": "毫秒", "%": "%", "Supports ms, s, and %": "支持毫秒、秒和百分比", "Console": "控制台", "Problems": "问题", "Changes": "更改", "Testing": "测试", "Compiling...": "编译中...", "Start playback to view a log of events": "开始播放以查看事件日志", "Single Timeline": "单个时间轴", "Blend 1D": "混合 1D", "Capture Base State": "捕获基础状态", "Blend Additive": "叠加混合", "Animation": "动画", "Timeline": "时间轴", "Folder": "文件夹", "Timelines": "时间轴", "Mix by view model": "按视图模型混合", "Mix by value": "按值混合", "Mix by input": "按输入混合", "Animations": "动画", "Animation options": "动画选项", "Playing": "播放中", "Pause Component": "暂停组件", "Quantize FPS": "量化帧率", "Play": "播放", "Stop": "停止", "Play at": "播放位置", "Stop at": "停止位置", "Input": "输入", "Exit Source": "退出源", "Allow exit during transition": "过渡期间允许退出", "Pause source when exiting": "退出时暂停源", "Add a layer to your State Machine to create states.": "向状态机添加图层以创建状态。", "Randomization": "随机化", "Randomize exit": "随机退出", "Randomization factor": "随机因子", "Factor:": "因子:", "Operation": "操作", "Cap": "端点", "Join": "拐角", "Interpolation": "插值", "Open graph editor": "打开曲线编辑器", "Apply auto bezier": "应用自动贝塞尔", "Auto bezier coefficient": "自动贝塞尔系数", "Default Interpolation": "默认插值", "Set as default interpolation": "设为默认插值", "Default interpolation changed": "默认插值已更改", "Falloff Interpolation": "衰减插值", "Graph editor": "曲线编辑器", "Show selected": "显示选中项", "Move playhead to next key": "移动播放头至下一关键帧", "Move playhead to previous key": "移动播放头至上一关键帧", "Move keys": "移动关键帧", "Move playhead": "移动播放头", "No keyframes selected": "未选中关键帧", "Navigate timeline": "浏览时间轴", "Animating": "动画制作中", "Expose to parent artboard": "暴露给父画板", "Ease In": "缓入", "Ease Out": "缓出", "Ease In Out": "缓入缓出", "Amplitude": "振幅", "Period": "周期", "Create Condition": "创建条件", "Zero": "零", "Infer from Name": "从名称推断", "Left to Right": "从左到右", "Top to Bottom": "从上到下", "Merge timelines...": "合并时间轴...", "Create New": "新建", "Merge Into": "合并到", "Copy selection into...": "复制选中项到...", "Move selection into...": "移动选中项到...", "Set work area": "设置工作区", "To keyframe": "到关键帧", "From keyframe": "从关键帧", "New timeline": "新建时间轴", "Select the animations you wish to paste: ": "选择要粘贴的动画:", "Merge with existing animations by name": "按名称与现有动画合并", "Subtract Path": "减去路径", "Read more": "了解更多", "Convert to custom path?": "转换为自定义路径?", "Allows you to modify the position of each vertex but removes procedural properties (e.g. width, height, number of points). Any keys applied to these properties will also be removed. ": "允许修改每个顶点的位置,但会移除程序化属性(如宽度、高度、点数)。应用于这些属性的任何关键帧也将被移除。", "Remove": "移除", "Remove joystick keys?": "移除摇杆关键帧?", "The selected animation contains keys for this joystick. The keys need to be removed to avoid a recursive loop. ": "选中的动画包含此摇杆的关键帧。需要移除这些关键帧以避免递归循环。", "Convert": "转换", "Cancel": "取消", "Edit Vertices": "编辑顶点", "Don't show again": "不再提示", "Mesh": "网格", "Apply Mesh": "应用网格", "Edit Mesh": "编辑网格", "Remove Mesh": "移除网格", "Editing Mesh": "编辑网格中", "Select which bones to bind": "选择要绑定的骨骼", "Bind Bones": "绑定骨骼", "Learn more about bone binding": "了解更多骨骼绑定", "Done": "完成", "Done Editing": "完成编辑", "Reset": "重置", "New Contour": "新建轮廓", "Close Path": "闭合路径", "Open Path": "开放路径", "Hole": "镂空", "Reverse Direction": "反转方向", "Convert Radial Corners": "转换圆角", "Key All Vertices": "为所有顶点添加关键帧", "Key Selected Vertices": "为选中顶点添加关键帧", "Pick a shape to use as a clipping source": "选择一个形状作为裁剪源", "No connection. Retrying...": "无连接。正在重试...", "Rive requires an active internet connection": "Rive 需要网络连接", "It's time to update": "需要更新", "Rive's latest changes require an update to the desktop app": "Rive 的最新更新需要升级桌面应用", "Rive needs more space": "Rive 需要更多空间", "Increase window size to display the Rive Editor": "请增大窗口尺寸以显示 Rive 编辑器", "Update Rive": "更新 Rive", "Packages": "包", "license": "许可证", "Terms of Service": "服务条款", "Privacy Policy": "隐私政策", "version": "版本", "Rive, Inc. All rights reserved.": "Rive, Inc. 保留所有权利。", "Delete folder and contents?": "删除文件夹及其内容?", "Contains animations. Are you sure you want to delete it?": "包含动画。确定要删除吗?", "Contains inputs. Are you sure you want to delete it?": "包含输入。确定要删除吗?", "Contains listeners. Are you sure you want to delete it?": "包含监听器。确定要删除吗?", "Are you sure?": "确定要继续吗?", "You're about to delete multiple animations": "即将删除多个动画", "You're about to delete multiple inputs": "即将删除多个输入", "You're about to delete multiple listeners": "即将删除多个监听器", "You're about to delete multiple converters": "即将删除多个转换器", "You're about to delete multiple enums": "即将删除多个枚举", "You're about to delete stage items in animate mode": "即将在动画模式下删除舞台项目", "Assets inside the folder will also be deleted.": "文件夹内的资源也将被删除。", "View models, enums, and converters inside the folder will also be deleted.": "文件夹内的视图模型、枚举和转换器也将被删除。", "Yes": "是", "Select a target to display events.": "选择目标以显示事件。", "This artboard has no events. Select a different target or create an event.": "此画板没有事件。请选择其他目标或创建事件。", "Confirm": "确认", "Create a Timeline or State Machine to start animating": "创建时间轴或状态机以开始制作动画", "Create animations": "创建动画", "Select a Timeline or State Machine": "选择时间轴或状态机", "Flatten text to shape": "将文本转为形状", "Converts the text component to a shape with paths. All keys associated with the text component will be removed": "将文本组件转换为带路径的形状。与文本组件关联的所有关键帧将被移除", "Flatten": "扁平化", "TAB": "标签页", "Create all data binds relative?": "创建所有相对数据绑定?", "Any data bound property will be created as a relative data bind": "任何数据绑定属性都将创建为相对数据绑定", "No": "否", "Create an artboard to get started": "创建画板以开始", "Select Preset": "选择预设", "Create Artboard": "创建画板", "Artboards define the dimensions and background color of a scene. Make as many as you like!": "画板定义场景的尺寸和背景颜色。可按需创建!", "Create artboard": "创建画板", "Get Started video": "入门视频", "Created by": "创建者", "Publish to the Community for a chance to get featured": "发布到社区有机会获得推荐", "Delete State Machine": "删除状态机", "Are you sure you want to delete this State Machine?": "确定要删除此状态机吗?", "Your cloud render queue is empty": "云端渲染队列为空", "Not Started": "未开始", "Rendering": "渲染中", "Completed": "已完成", "Start All": "全部开始", "Duration": "时长", "Name": "名称", "Format": "格式", "Size": "尺寸", "Height": "高度", "Width": "宽度", "Frame rate": "帧率", "Bit rate": "比特率", "Matte": "遮罩", "Made with Rive": "使用 Rive 制作", "Delete": "删除", "Download": "下载", "kbps": "kbps", "Render Presets": "渲染预设", "Upgrade!": "升级!", "Unlock all render options.": "解锁所有渲染选项。", "Learn more.": "了解更多。", "Render mode": "渲染模式", "Design only": "仅设计", "Design": "设计", "Go to Render Queue": "转到渲染队列", "Render Queue": "渲染队列", "Add to Render Queue": "添加到渲染队列", "Open Render Queue": "打开渲染队列", "Queue and Start Render": "排队并开始渲染", "Queue All": "全部排队", "No scheduled tasks": "无计划任务", "No completed tasks": "无已完成任务", "Points": "点", "Radius": "半径", "Top Left": "左上", "Top Right": "右上", "Bot. Left": "左下", "Bot. Right": "右下", "Text": "文本", "Font Options": "字体选项", "Features": "特性", "Variables": "变量", "Text Modifiers": "文本修饰器", "Range": "范围", "Auto Orient Glyphs": "字形自动朝向", "Auto Orient Lines": "行自动朝向", "Text Run": "文本段", "Text Input": "文本输入", "Text Styles": "文本样式", "Group Text Styles": "编组文本样式", "Ungroup Text Styles": "取消编组文本样式", "Value": "值", "Font Size": "字号", "Bind Text": "绑定文本", "Combine Text Runs": "合并文本段", "Run from Selection": "从选择创建文本段", "Select characters to create run": "选择字符以创建文本段", "Can only create runs on design mode": "仅在设计模式下可创建文本段", "Flatten to Shape": "转为形状", "Merge with Next": "与下一个合并", "Merge with Previous": "与上一个合并", "Delete Text Run": "删除文本段", "Edit Text Run": "编辑文本段", "Cannot remove final run": "无法移除最后一个文本段", "Highlight text runs": "高亮文本段", "Maintain baseline": "保持基线", "Variable": "变量", "Exclude Spaces": "排除空格", "Modifier group must have at least one range": "修饰器组必须至少有一个范围", "Range View Mode": "范围视图模式", "Modulo": "取模", "Text Modifier Range Values": "文本修饰器范围值", "Use Horizontal Field Drag": "使用水平字段拖动", "Access All Alternates": "访问所有替代字形", "Above-base Forms": "基上方形", "Above-base Mark Positioning": "基上标记定位", "Above-base Substitutions": "基上替换", "Alternative Fractions": "备选分数", "Akhand": "连体字", "Below-base Forms": "基下方形", "Below-base Mark Positioning": "基下标记定位", "Below-base Substitutions": "基下替换", "Contextual Alternates": "上下文替代字形", "Case-Sensitive Forms": "大小写敏感形式", "Glyph Composition": "字形组合", "Conjunct Form After Ro": "Ro 后连字形式", "Contextual Half-width Spacing": "上下文半角间距", "Conjunct Forms": "连字形式", "Contextual Ligatures": "上下文连字", "Centered CJK Punctuation": "居中 CJK 标点", "Capital Spacing": "大写字母间距", "Contextual Swash": "上下文花体", "Cursive Positioning": "草书定位", "Petite Capitals From Capitals": "大写转小型大写", "Small Capitals From Capitals": "大写转小型大写", "Distances": "距离", "Discretionary Ligatures": "自由连字", "Denominators": "分母", "Dotless Forms": "无点形式", "Expert Forms": "专家形式", "Final Glyph on Line Alternates": "行尾字形替代", "Terminal Forms #2": "终端形式 #2", "Terminal Forms #3": "终端形式 #3", "Terminal Forms": "终端形式", "Flattened accent forms": "扁平重音形式", "Fractions": "分数", "Full Widths": "全角", "Half Forms": "半角形式", "Halant Forms": "去元音形式", "Alternate Half Widths": "备选半角", "Historical Forms": "历史形式", "Horizontal Kana Alternates": "横向假名替代", "Historical Ligatures": "历史连字", "Hangul": "韩文", "Hojo Kanji Forms": "Hojo 汉字形式", "Half Widths": "半角", "Initial Forms": "首字母形式", "Isolated Forms": "独立形式", "Italics": "斜体", "Justification Alternates": "两端对齐替代", "JIS78 Forms": "JIS78 形式", "JIS83 Forms": "JIS83 形式", "JIS90 Forms": "JIS90 形式", "JIS2004 Forms": "JIS2004 形式", "Kerning": "字距调整", "Left Bounds": "左边界", "Standard Ligatures": "标准连字", "Leading Jamo Forms": "前导韩文字母形式", "Lining Figures": " lining 数字", "Localized Forms": "本地化形式", "Left-to-right alternates": "从左到右替代", "Left-to-right mirrored forms": "从左到右镜像形式", "Mark Positioning": "标记定位", "Medial Forms #2": "中间形式 #2", "Medial Forms": "中间形式", "Mathematical Greek": "数学希腊字母", "Mark to Mark Positioning": "标记到标记定位", "Mark Positioning via Substitution": "通过替换定位标记", "Alternate Annotation Forms": "备选注释形式", "NLC Kanji Forms": "NLC 汉字形式", "Nukta Forms": "Nukta 形式", "Numerators": "分子", "Oldstyle Figures": "老式数字", "Optical Bounds": "光学边界", "Ordinals": "序数", "Ornaments": "装饰", "Proportional Alternate Widths": "比例替代宽度", "Petite Capitals": "小型大写", "Proportional Kana": "比例假名", "Proportional Figures": "比例数字", "Pre-Base Forms": "基前形式", "Pre-base Substitutions": "基前替换", "Post-base Forms": "基后形式", "Post-base Substitutions": "基后替换", "Proportional Widths": "比例宽度", "Quarter Widths": "四分之一宽度", "Randomize": "随机", "Required Contextual Alternates": "必需上下文替代", "Rakar Forms": "Rakar 形式", "Required Ligatures": "必需连字", "Reph Forms": "Reph 形式", "Right Bounds": "右边界", "Right-to-left alternates": "从右到左替代", "Right-to-left mirrored forms": "从右到左镜像形式", "Ruby Notation Forms": "注音标记形式", "Required Variation Alternates": "必需变体替代", "Stylistic Alternates": "风格替代", "Scientific Inferiors": "科学下标", "Optical size": "光学尺寸", "Small Capitals": "小型大写", "Simplified Forms": "简化形式", "Math script style alternates": "数学脚本风格替代", "Stretching Glyph Decomposition": "拉伸字形分解", "Subscript": "下标", "Superscript": "上标", "Swash": "花体", "Titling": "标题", "Trailing Jamo Forms": "尾随韩文字母形式", "Traditional Name Forms": "传统名称形式", "Tabular Figures": "等宽数字", "Traditional Forms": "传统形式", "Third Widths": "三分之一宽度", "Unicase": "单大小写", "Alternate Vertical Metrics": "备选垂直度量", "Vattu Variants": "Vattu 变体", "Vertical Contextual Half-width Spacing": "垂直上下文半角间距", "Vertical Writing": "垂直书写", "Alternate Vertical Half Metrics": "备选垂直半度量", "Vowel Jamo Forms": "元音韩文字母形式", "Vertical Kana Alternates": "纵向假名替代", "Vertical Kerning": "垂直字距调整", "Proportional Alternate Vertical Metrics": "比例替代垂直度量", "Vertical Alternates and Rotation": "垂直替代和旋转", "Vertical Alternates for Rotation": "旋转用垂直替代", "Slashed Zero": "斜线零", "Currently Unavailable": "当前不可用", "Publishing custom fonts to the Community is currently unavailable": "目前无法将自定义字体发布到社区", "OK": "确定", "Handle": "控制柄", "Optionally set control source": "可选设置控制源", "Visible": "可见", "Hidden": "隐藏", "Clipped": "已裁剪", "Ellipsis": "省略号", "weight": "字重", "width": "字宽", "slant": "倾斜", "Range From": "范围起点", "Range To": "范围终点", "Falloff From": "衰减起点", "Falloff To": "衰减终点", "Zoom": "缩放", "Resolution": "分辨率", "Zoom in": "放大", "Zoom out": "缩小", "Zoom to fit": "适应窗口", "Zoom to 100%": "缩放至 100%", "Duplicate": "复制", "Sort": "排序", "A-Z": "A-Z", "Z-A": "Z-A", "Set as default": "设为默认", "Search": "搜索", "Goto line": "跳转到行", "Loading...": "加载中...", "Replace with...": "替换为...", "Loading": "加载中", "This file": "此文件", "All files": "所有文件", "Artboards": "画板", "Objects": "对象", "Publish": "发布", "Post": "帖子", "Type a title...": "输入标题...", "Type a description and don't forget to use #hashtags to help other artists discover your work…": "输入描述,别忘了使用 #标签 帮助其他艺术家发现您的作品…", "User needs to be verified": "用户需要验证", "Verify your email to start publishing to the Community": "验证邮箱后即可发布到社区", "Publish a new revision of this\nfile to the Community": "发布此文件的新版本到社区", "Publish an Update": "发布更新", "Add a title to publish": "添加标题以发布", "Add a description to publish": "添加描述以发布", "Add a title and a description to publish": "添加标题和描述以发布", "Has been published": "已发布", "Create Component": "创建组件", "Revert to Artboard": "恢复为画板", "Exclude from export": "从导出中排除", "Revert to Artboard?": "恢复为画板?", "Delete Components?": "删除组件?", "Delete Library Components?": "删除库组件?", "This component is being referenced in another artboard. Are you sure you want to remove it?": "此组件在另一个画板中被引用。确定要移除吗?", "A component is being referenced in another artboard. Are you sure you want to remove it?": "有组件在另一个画板中被引用。确定要移除吗?", "This component has previously been published. Are you sure you want to remove it?": "此组件之前已发布。确定要移除吗?", "A component has previously been published. Are you sure you want to remove it?": "有组件之前已发布。确定要移除吗?", "Deleting components from your assets will also delete them from the stage. Are you sure?": "从资源中删除组件也会从舞台中删除。确定要继续吗?", "Library": "库", "Latest version": "最新版本", "Open Library File": "打开库文件", "View in Library Browser": "在库浏览器中查看", "Replace Library (Debug)": "替换库(调试)", "Publish Library": "发布库", "Publish Library...": "发布库...", "Publish Update": "发布更新", "Publish Library Update...": "发布库更新...", "This file is now a library": "此文件现在是库", "This file is not a library": "此文件不是库", "Republish Libary": "重新发布库", "Unpublish Libary": "取消发布库", "We cannot update a library without any components.": "无法更新没有任何组件的库。", "A new version of the library has been published.": "库的新版本已发布。", "API error A new version of the library has been published.": "API 错误,库的新版本已发布。", "Cannot create a library without any components. Mark an Artboard as a compoent.": "无法创建没有任何组件的库。请将画板标记为组件。", "Description of changes (optional)": "更改说明(可选)", "Publishing...": "发布中...", "No libraries found": "未找到库", "No components yet": "暂无组件", "Set an artboard as a component to publish a library": "将画板设置为组件以发布库", "No artboard components yet": "暂无画板组件", "No data components yet": "暂无数据组件", "No shared scripts yet": "暂无共享脚本", "Go to Library": "转到库", "Updating...": "更新中...", "Update Selected": "更新选中项", "Latest Version:": "最新版本:", "Version:": "版本:", "Library:": "库:", "Component Information": "组件信息", "Dependencies": "依赖项", "Missing Dependencies": "缺失依赖项", "Add Missing Dependencies": "添加缺失依赖项", "Update Component": "更新组件", "Update Components": "更新组件", "Adding selected library components": "正在添加选中的库组件", "Unable to import selected components: Library asset could not be found": "无法导入选中的组件:找不到库资源", "Unable to import selected components: Library attributes could not be retrieved": "无法导入选中的组件:无法获取库属性", "Successfully added components": "组件添加成功", "This file is no longer a library": "此文件不再是库", "This file has been republished as a library": "此文件已重新发布为库", "Unable to locate this library": "无法定位此库", "Asset": "资源", "Upload": "上传", "Information": "信息", "Source Information": "源信息", "Dimensions": "尺寸", "File Size": "文件大小", "Used": "已使用", "Created": "创建时间", "Replace": "替换", "More Info": "更多信息", "Generate Artboard": "生成画板", "assets selected": "个资源已选中", "Asset upload unavailable when previewing a file.": "预览文件时无法上传资源。", "items": "项", "Sample Rate": "采样率", "Channels": "声道", "Mono": "单声道", "Stereo": "立体声", "Unknown": "未知", "Volume": "音量", "Waveform": "波形", "Clipped Waveform": "裁剪波形", "Buffering...": "缓冲中...", "Audio Clip": "音频片段", "Clips": "片段", "Created audio clip": "已创建音频片段", "Create clip": "创建片段", "Select an audio asset to clip": "选择音频资源以剪辑", "Libraries": "库", "Include in export": "包含在导出中", "Export component to .riv": "导出组件为 .riv", "Export asset to .riv": "导出资源为 .riv", "Export as component": "导出为组件", "Main artboard must be exported": "主画板必须导出", "Nested components must be exported": "嵌套组件必须导出", "Log in": "登录", "sign up": "注册", "Verifying": "验证中", "or": "或", "Username or email": "用户名或邮箱", "Password": "密码", "Forgot password?": "忘记密码?", "Log in below or ": "在下方登录或 ", " to create a Rive account": " 创建 Rive 账户", "Log in with SSO": "使用 SSO 登录", "Log in with password": "使用密码登录", "Email": "邮箱", "Copied": "已复制", "Creating...": "创建中...", "Share File": "分享文件", "Share Link": "分享链接", "Generate Share Link...": "生成分享链接...", "Anyone with the link can view your file.": "任何拥有链接的人都可以查看您的文件。", "Thumbnail": "缩略图", "Create": "创建", "Share": "分享", "Manage your links": "管理链接", "Title": "标题", "Generate a link to share your Rive creations with others. Share the generated link, or use the embed code to add your animation to a web page.": "生成链接与他人分享您的 Rive 作品。分享生成的链接,或使用嵌入代码将动画添加到网页中。", "Generate link": "生成链接", "Generating link...": "正在生成链接...", "Share link": "分享链接", "Embed link": "嵌入链接", "Embed code": "嵌入代码", "Framer code": "Framer 代码", "Create new link": "创建新链接", "Manage links": "管理链接", "Enable": "启用", "Disable a link to prevent others from viewing it.": "禁用链接以阻止他人查看。", "Rive Renderer": "Rive 渲染器", "Multitouch": "多点触控", "Enable Multitouch": "启用多点触控", "Use the Rive Renderer, more info at ": "使用 Rive 渲染器,更多信息请访问 ", "Pointer Down": "指针按下", "Pointer Up": "指针抬起", "Pointer Enter": "指针进入", "Pointer Move": "指针移动", "Pointer Exit": "指针离开", "Click": "点击", "Pointer Drag": "指针拖动", "View Model": "视图模型", "Select input": "选择输入", "Select event": "选择事件", "align": "对齐", "Preserve offset": "保留偏移", "set": "设置", "fire": "触发", "true": "真", "false": "假", "toggle": "切换", "Report": "报告", "Play Audio": "播放音频", "Report Event": "报告事件", "Property": "属性", "Properties": "属性", "String": "字符串", "Enum": "枚举", "Reset Input Values": "重置输入值", "Reset Value": "重置值", "Layout Cell": "布局单元格", "Layout Style": "布局样式", "Enable Layout Animation": "启用布局动画", "Layout Children": "布局子对象", "Layout Flex": "弹性布局", "Layout Constraints": "布局约束", "Layout selection": "布局选择", "Convert to Layout": "转换为布局", "Add Child Layout": "添加子布局", "Add Artboard List": "添加画板列表", "Add Component": "添加组件", "Child Layout": "子布局", "Min width": "最小宽度", "Min height": "最小高度", "Max width": "最大宽度", "Max height": "最大高度", "Clear constraints": "清除约束", "Display": "显示", "Hide": "隐藏", "Show": "显示", "Constrain Size": "约束尺寸", "Relative": "相对", "Absolute": "绝对", "Static": "静态", "Position: Absolute": "位置:绝对", "Position: Constrained by Parent": "位置:受父级约束", "Position: Mixed": "位置:混合", "Flex Direction": "弹性方向", "Row Reverse": "行反转", "Column Reverse": "列反转", "Flex Wrap": "弹性换行", "Wrap Reverse": "换行反转", "Inherit": "继承", "Right to Left": "从右到左", "Align Content": "对齐内容", "Align Items": "对齐项目", "Align Self": "自身对齐", "Justify Content": "内容对齐", "Justify Items": "项目对齐", "Justify Self": "自身两端对齐", "Auto": "自动", "Flex Start": "弹性起点", "Flex End": "弹性终点", "Center": "居中", "Stretch": "拉伸", "Space Between": "两端对齐", "Space Around": "均匀分布", "Space Evenly": "等间距", "Add child Layout": "添加子布局", "Nest a layout node within this one.": "在此布局中嵌套一个布局节点。", "Layout is in a bad state": "布局状态异常", "Percent": "百分比", "Gap": "间距", "Horizontal gap": "水平间距", "Vertical gap": "垂直间距", "Dash": "虚线", "Add gap": "添加间距", "Add dash": "添加虚线", "Grow": "增长", "Shrink": "收缩", "Base size": "基础尺寸", "Aspect Ratio": "宽高比", "L": "左", "R": "右", "T": "上", "B": "下", "Border": "边框", "Margin": "外边距", "Padding": "内边距", "Inset": "内嵌", "Size Determined by Children": "尺寸由子对象决定", "Grid Auto Flow": "网格自动流", "Row Dense": "行密集", "Column Dense": "列密集", "S": "起", "E": "终", "Mn": "最小", "Mx": "最大", "Line": "行", "Span": "跨距", "Fixed": "固定", "Min Content": "最小内容", "Max Content": "最大内容", "Fit Content": "适应内容", "Fraction": "分数", "Grid Row": "网格行", "Grid Column": "网格列", "Grid Template Rows": "网格模板行", "Grid Template Columns": "网格模板列", "Grid Auto Rows": "网格自动行", "Grid Auto Columns": "网格自动列", "Fit": "适应", "Cover": "覆盖", "Contain": "包含", "Fit Width": "适应宽度", "Fit Height": "适应高度", "Scale Down": "缩小", "Resize Artboard": "调整画板尺寸", "Alignment": "对齐", "Top Center": "顶部居中", "Center Left": "左侧居中", "Center Right": "右侧居中", "Bottom Left": "左下", "Bottom Center": "底部居中", "Bottom Right": "右下", "Start Expanded": "起点展开", "Center Expanded": "居中展开", "End Expanded": "终点展开", "Left": "左", "Right": "右", "Top": "上", "Bottom": "下", "Left Units": "左单位", "Right Units": "右单位", "Top Units": "上单位", "Bottom Units": "下单位", "W Units": "宽度单位", "H Units": "高度单位", "W": "宽", "H": "高", "Horizontal": "水平", "Vertical": "垂直", "Horizontal Units": "水平单位", "Vertical Units": "垂直单位", "Position Type": "位置类型", "Overflow": "溢出", "Text Wrap": "文本换行", "Animation Type": "动画类型", "Custom": "自定义", "Animation Duration": "动画时长", "Layouts cannot be grouped": "布局无法编组", "Nested Artboards in Layout mode cannot be grouped": "布局模式下的嵌套画板无法编组", "Hug": "贴合", "Width Scale Type": "宽度缩放类型", "Height Scale Type": "高度缩放类型", "Artboards must contain a relative Layout or Nested Artboard Layout in order to hug": "画板必须包含相对布局或嵌套画板布局才能贴合", "Nine Slice": "九宫格", "Skip Runtime Modal": "跳过运行时弹窗", "Learn how to load and control Rive files on these platforms.": "了解如何在这些平台上加载和控制 Rive 文件。", "Supported platforms": "支持的平台", "Run Rive anywhere": "随处运行 Rive", "The .riv format runs anywhere with our open-source runtimes. ": ".riv 格式可通过我们的开源运行时随处运行。 ", " to export your Rive file and run it across apps, games, vehicles, websites, and products.": " 导出您的 Rive 文件并在应用、游戏、车辆、网站和产品中运行。", "Upgrade to an editor seat": "升级到编辑者席位", "Upgrade to a paid workspace": "升级到付费工作区", "The .riv format runs anywhere with our open-source runtimes. Thank you for purchasing Rive, we appreciate your support.": ".riv 格式可通过我们的开源运行时随处运行。感谢您购买 Rive,感谢您的支持。", "Export your Rive file": "导出 Rive 文件", "Upgrade to export": "升级以导出", "Join the Community for help": "加入社区获取帮助", "Test with demo files": "使用演示文件测试", "Skip this modal next time": "下次跳过此弹窗", "Free": "免费", "Pro Team": "专业团队", "Stick with Free": "继续使用免费版", "Design, animate, and code in the Rive Editor.": "在 Rive 编辑器中设计、制作动画和编写代码。", "Ship to apps, products, games, and vehicles with up to 3 seats.": "部署到应用、产品、游戏和车辆,最多 3 个席位。", "Build with Libraries and scale collaboration up to 25 seats.": "使用库构建并扩展协作至 25 个席位。", "3 files": "3 个文件", "Google Fonts": "Google 字体", "10 MB asset size limit": "资源大小限制 10 MB", "Unlimited personal files": "无限个人文件", "3 collaborative files": "3 个协作文件", " / mo. billed yearly": " / 月,按年计费", " / mo. billed monthly": " / 月,按月计费", "Unlimited Files": "无限文件", "Folders and Tags": "文件夹和标签", "Share links": "分享链接", "Custom font upload": "自定义字体上传", "Backup file format": "备份文件格式", "Unlimited Revision History": "无限版本历史", "/seat/mo": "/席位/月", " / user / mo. billed monthly": " / 用户 / 月,按月计费", "All Pro features": "所有专业功能", "Invite team members": "邀请团队成员", "Real-time collaboration": "实时协作", "Batch export": "批量导出", "Exports": "导出", "Unlimited collaborative files and projects": "无限协作文件和项目", "$5/seat monthly agent credits": "$5/席位 每月智能体积分", "Everything in Cadet": "Cadet 包含的所有内容", "Rive CDN asset hosting": "Rive CDN 资源托管", "Embed link hosting": "嵌入链接托管", "Rive support": "Rive 支持", "$16/seat monthly agent credits": "$16/席位 每月智能体积分", "Contact Us": "联系我们", "Scale, support, and security built for large organizations.": "为大型组织构建的规模、支持和安全性。", "Multiple Workspaces for subteams": "子团队的多个工作区", "Bring your own S3 bucket": "自带 S3 存储桶", "Manage company sharing permissions": "管理公司共享权限", "Rive dedicated support": "Rive 专属支持", "Dedicated onboarding and training": "专属入门和培训", "Custom runtime": "自定义运行时", "Centralized billing": "集中计费", "$40/seat monthly agent credits": "$40/席位 每月智能体积分", "Contact a team admin to upgrade your plan": "联系团队管理员升级计划", "Contact an admin to upgrade your plan": "联系管理员升级计划", "Upgrade": "升级", "Select plan": "选择计划", "Continue": "继续", "This team is no longer active": "此团队已停用", "Contact the team administrator to continue creating and editing files. Right-click items to download.": "联系团队管理员以继续创建和编辑文件。右键单击项目下载。", "Reactivate your plan to continue creating and editing files. Right-click items to download.": "重新激活计划以继续创建和编辑文件。右键单击项目下载。", "Your plan has been suspended. You can reactivate it at any time to continue editing files. ": "您的计划已暂停。可随时重新激活以继续编辑文件。 ", "Manage plan": "管理计划", "This team is no longer active. Contact your teams's administrator to reactivate it. ": "此团队已停用。请联系团队管理员重新激活。 ", "This plan is pending account action": "此计划待账户操作", "Contact the team administrator to start creating and editing files. ": "联系团队管理员以开始创建和编辑文件。 ", "Your plan is pending account action": "您的计划待账户操作", "Manage your plan resolve any pending account actions to continue creating and editing files. ": "管理计划并解决待处理的账户操作以继续创建和编辑文件。 ", "Your plan has not been started yet. View Manage Plan to complete the setup.": "您的计划尚未启动。查看管理计划以完成设置。", "This team is not active yet. Contact your teams's administrator to complete setup. ": "此团队尚未激活。请联系团队管理员完成设置。 ", "We were unable to process your payment. Update billing to keep your plan active. ": "无法处理您的付款。请更新账单以保持计划活跃。 ", "Update billing": "更新账单", "Your plan has been cancelled. You can continue editing and creating files until": "您的计划已取消。您可以继续编辑和创建文件直到", "Welcome to Rive. ": "欢迎使用 Rive。 ", "Create up to 3 files with the free plan. ": "使用免费计划最多创建 3 个文件。 ", "Dismiss": "关闭", "You've reached your file limit. Upgrade to unlock unlimited files. ": "您已达到文件限制。升级以解锁无限文件。 ", "You've exceeded the file limit. Delete some files or upgrade to start editing again. ": "您已超过文件限制。删除一些文件或升级以继续编辑。 ", "You've exceeded the file limit": "您已超过文件限制", "Delete some files or upgrade to start editing again": "删除一些文件或升级以继续编辑", "File exported.": "文件已导出。", "Export canceled.": "导出已取消。", "Previous": "上一个", "For runtime": "用于运行时", "Export": "导出", "Export for Runtime": "导出用于运行时", "Export options": "导出选项", "Remove name exports": "移除名称导出", "Export all names": "导出所有名称", "Convert data binds...": "转换数据绑定...", "to Relative": "为相对", "to Absolute": "为绝对", "Exporting file, this can take a minute.": "正在导出文件,可能需要一分钟。", "For backup": "用于备份", "No scripts to export.": "没有可导出的脚本。", "Animations required to share.": "需要动画才能分享。", "Publish to Marketplace...": "发布到市场...", "Add to Cloud Renderer...": "添加到云端渲染器...", "Render video is only available in animate mode.": "渲染视频仅在动画模式下可用。", "Nothing to render.": "没有可渲染的内容。", "Only animations can be renderered": "只有动画可被渲染", "View Cloud Renderer...": "查看云端渲染器...", "Starting...": "正在启动...", "Starting... (Can take up to a minute)": "正在启动...(可能需要一分钟)", "Initializing": "初始化中", "Quality": "质量", "Apply Changes": "应用更改", "Uploading": "上传中", "Analyzing": "分析中", "Processing": "处理中", "Ready": "就绪", "Replacing": "替换中", "Lossy": "有损", "Lossless": "无损", "Not Found": "未找到", "Try re-uploading this asset": "尝试重新上传此资源", "Try replacing or re-uploading this asset": "尝试替换或重新上传此资源", "Family": "系列", "Version": "版本", "Designer": "设计师", "Manufacturer": "制造商", "License": "许可证", "Copyright": "版权", "Custom Fonts": "自定义字体", "View License": "查看许可证", "Languages": "语言", "Glyph": "字形", "View Glyphs": "查看字形", "View Scripts": "查看脚本", "No Script": "无脚本", "Custom Audio Files": "自定义音频文件", "Soundly Audio Files": "Soundly 音频文件", "Browse": "浏览", "Hosted Assets": "托管资源", "Your asset will be hosted on Rive's CDN for runtimes to download. Anyone with the link to your asset will be able to access it.": "您的资源将托管在 Rive 的 CDN 上供运行时下载。任何拥有资源链接的人都可以访问。", "Learn more about hosted assets.": "了解更多关于托管资源的信息。", "Hierarchy": "层级", "General": "常规", "Open URL": "打开网址", "Images": "图片", "Lottie": "Lottie", "Fonts": "字体", "Failed to create script": "创建脚本失败", "Failed to compile some tests": "编译部分测试失败", "Failed to compile tests": "编译测试失败", "Open in Script Editor": "在脚本编辑器中打开", "Run Tests": "运行测试", "Viewports": "视口", "Create Viewport": "创建视口", "Create Script": "创建脚本", "Existing Script": "现有脚本", "New Stage Viewport": "新建舞台视口", "URL Properties": "URL 属性", "Audio Properties": "音频属性", "URL": "URL", "Parent": "父级", "Self": "自身", "Open": "打开", "Show in File Browser": "在文件浏览器中显示", "Download Backup": "下载备份", "Rename": "重命名", "Copy Link": "复制链接", "Cut": "剪切", "Paste": "粘贴", "Put Back": "放回", "New Folder": "新建文件夹", "New File": "新建文件", "Remix File to": "混音文件到", "Wrap in": "包裹于", "Reverse": "反转", "Reverse All": "全部反转", "Convert to Solo": "转换为单独显示", "Failed to convert Group to Solo": "将编组转换为单独显示失败", "Wrap in Layout": "包裹于布局", "Wrap Nine Slice": "包裹九宫格", "Export name": "导出名称", "My Files": "我的文件", "Recents": "最近", "Members": "成员", "Marketplace": "市场", "Community": "社区", "View in the Community": "在社区中查看", "Delete file?": "删除文件?", "Trash can't be deleted": "回收站无法删除", "SSO login is required to access this workspace.\nPlease login with SSO or check with your workspace admin that your are linked to your workspaces Identity Provider.": "访问此工作区需要 SSO 登录。\n请使用 SSO 登录或与工作区管理员确认您已链接到工作区的身份提供商。", "Home": "主页", "Sign Out": "退出登录", "Manage Account": "管理账户", "Manage Team": "管理团队", "Upgrade Team": "升级团队", "Invite": "邀请", "Leave Team": "离开团队", "pending": "待处理", "Remove Team Member": "移除团队成员", "View all": "查看全部", "Upgrade to Pro": "升级到专业版", "Create or import a file to get started": "创建或导入文件以开始", "No files to view yet": "暂无可查看的文件", "Deleted files can be found here": "已删除的文件可在此找到", "Last Modified": "最后修改", "Viewers cannot create files": "查看者无法创建文件", "Viewers cannot create folders": "查看者无法创建文件夹", "Owner": "所有者", "Create Team": "创建团队", "Create a team": "创建团队", "Create new team": "创建新团队", "1. Choose a team name": "1. 选择团队名称", "Team name": "团队名称", "2. Invite team members": "2. 邀请团队成员", "Email addresses separated by comma": "邮箱地址以逗号分隔", "Choose a team name": "选择团队名称", "Name must contain an alphanumeric character": "名称必须包含字母数字字符", "Invalid email address": "无效的邮箱地址", "Check email addresses are valid": "检查邮箱地址是否有效", "Team Overview": "团队概览", "Verify your email to create a team. ": "验证邮箱以创建团队。 ", "Resend verification email.": "重新发送验证邮件。", "Verification email sent": "验证邮件已发送", "Something went wrong. Contact us.": "出现问题。请联系我们。", "Skip for now": "暂时跳过", "Create Workspace": "创建工作区", "New Workspace": "新建工作区", "Switch Workspace": "切换工作区", "1. Choose a name": "1. 选择名称", "Workspace name": "工作区名称", "2. Invite members": "2. 邀请成员", "Choose a name": "选择名称", "Workspace Overview": "工作区概览", "Verify your email to create a workspace. ": "验证邮箱以创建工作区。 ", "Leave Workspace": "离开工作区", "Create Project": "创建项目", "Project name": "项目名称", "Legacy": "旧版", "Overview": "概览", "Project overview": "项目概览", "Project members": "项目成员", "Other team members": "其他团队成员", "Other groups": "其他组", "Admin": "管理员", "Leave project": "离开项目", "Rename project": "重命名项目", "Library Sharing": "库共享", "Share": "共享", "Make libraries in this project available across the workspace": "使此项目中的库在工作区范围内可用", "Allow": "允许", "Update": "更新", "Archive project": "归档项目", "Manage access": "管理访问权限", "Editors": "编辑者", "Viewers": "查看者", "Are you sure you want to archive ": "确定要归档 ", "Archive": "归档", "Groups": "编组", "Invalid name": "无效名称", "Confirm changes": "确认更改", "Payment Confirmation": "付款确认", "Credit Confirmation": "积分确认", "Make payment": "付款", "Go back": "返回", "These changes will reduce your monthly bill. Any pro-rated amount will be credited to your account.": "这些更改将减少您的月度账单。任何按比例计算的金额将记入您的账户。", "These changes will incur a cost of": "这些更改将产生费用", "The billing method tied to your account will be charged.": "与您账户关联的计费方式将被收费。", "Archived": "已归档", "View": "查看", "No projects yet": "暂无项目", "Payment Failed": "付款失败", "Manage billing": "管理账单", "Payment Requires Action": "付款需要操作", "Projects require an admin": "项目需要管理员", "Projects need at least one admin or workspace access": "项目至少需要一名管理员或工作区访问权限", "Reset changes": "重置更改", "Requires editor access": "需要编辑者权限", "Workspace viewers cannot create projects. Request editor access from an admin.": "工作区查看者无法创建项目。请向管理员申请编辑者权限。", "Invite Members": "邀请成员", "Manage Workspace": "管理工作区", "Upgrade Workspace": "升级工作区", "Plan": "计划", "Status": "状态", "What best describes your role?": "最能描述您角色的选项?", "Select role": "选择角色", "Product Designer": "产品设计师", "Game Designer": "游戏设计师", "Animator": "动画师", "Developer": "开发者", "Other": "其他", "I'd rather not say": "不便透露", "Get started": "开始使用", "Select a role to continue.": "选择角色以继续。", "Enter your role to continue.": "输入您的角色以继续。", "Got it": "知道了", "Welcome to Rive": "欢迎使用 Rive", "Find recent files, tutorials, and inspiration from the community in the Home section.": "在主页部分找到最近的文件、教程和社区灵感。", "Switch between workspaces": "在工作区之间切换", "Workspaces are great for both teams and individuals. Create as many as you like, and switch between them here.": "工作区非常适合团队和个人。可创建任意数量,并在此切换。", "Create projects": "创建项目", "Organise your files into projects with control over who can view and edit.": "将文件组织到项目中,控制谁可以查看和编辑。", "Your personal files": "您的个人文件", "Create unlimited free files within your personal spaces. You get a personal space with every workspace.": "在个人空间中创建无限免费文件。每个工作区都有一个个人空间。", "Create files": "创建文件", "Get started by creating a Rive file. Export your .riv to use in your app, game, or website.": "从创建 Rive 文件开始。导出 .riv 文件以在应用、游戏或网站中使用。", "Tip: ": "提示:", "Hover and hold ": "悬停并按住 ", " for help": " 获取帮助", "Oops,\nsomething went wrong!": "哎呀,\n出了点问题!", "If the problem persists, please contact us.": "如果问题仍然存在,请联系我们。", "Something went wrong": "出了点问题", "Shortcuts": "快捷键", "Show shortcuts": "显示快捷键", "Hide shortcuts": "隐藏快捷键", "Tools": "工具", "Selection": "选择", "Stage": "舞台", "Bone weights": "骨骼权重", "hold": "按住", "Force crash the application": "强制崩溃应用程序", "Artboard tool": "画板工具", "Nested artboard tool": "嵌套画板工具", "Auto tool": "自动工具", "Bone tool": "骨骼工具", "Joystick tool": "摇杆工具", "Event tool": "事件工具", "Zoom tool": "缩放工具", "Layout tool": "布局工具", "Row tool": "行工具", "Column tool": "列工具", "Wrap in layout": "包裹于布局", "Dependency graph": "依赖图", "Color picker": "颜色选择器", "Cycle hovered stage items": "循环切换悬停的舞台项目", "Copy style": "复制样式", "De-select": "取消选择", "Mesh tool": "网格工具", "Find": "查找", "Force create a revision": "强制创建版本", "Group": "编组", "Solo Group": "单独显示组", "Ik": "IK", "Move keys 1 frame back": "关键帧后移 1 帧", "Move keys 1 frame forward": "关键帧前移 1 帧", "Move keys 10 frames back": "关键帧后移 10 帧", "Move keys 10 frames forward": "关键帧前移 10 帧", "Multi select": "多选", "Skip 10 frames forward": "向前跳过 10 帧", "Next frame": "下一帧", "Next key": "下一关键帧", "Node / Group tool": "节点/编组工具", "Pan": "平移", "Pose": "姿势", "Skip 10 frames back": "向后跳过 10 帧", "Previous frame": "上一帧", "Previous key": "上一关键帧", "Redo": "重做", "Rotate tool": "旋转工具", "Scale tool": "缩放工具", "Select all": "全选", "Select children": "选择子对象", "Solo tool": "单独显示工具", "Group Effect tool": "效果组工具", "Pen tool": "钢笔工具", "Rectangle tool": "矩形工具", "Text tool": "文本工具", "Ellipse tool": "椭圆工具", "Move timeline to end": "时间轴移至末尾", "Move timeline to start": "时间轴移至开头", "Play default animation": "播放默认动画", "Translate tool": "平移工具", "Undo": "撤销", "Weight tool": "权重工具", "Zoom to selection": "缩放到选中项", "Zoom In": "放大", "Zoom Out": "缩小", "Key selected": "为选中项添加关键帧", "Key bone length": "骨骼长度关键帧", "key-rotation": "旋转关键帧", "key-scale": "缩放关键帧", "key-translation": "平移关键帧", "Next": "下一个", "Reset rulers": "重置标尺", "selection-filter-all": "选择筛选-全部", "selection-filter-bone": "选择筛选-骨骼", "selection-filter-next": "选择筛选-下一个", "selection-filter-prev": "选择筛选-上一个", "selection-filter-vertex": "选择筛选-顶点", "reveal-in-hierarchy": "在层级中显示", "Switch mode": "切换模式", "Send backwards": "下移一层", "Send to front": "置于顶层", "Select parent": "选择父级", "Select child / edit vertices": "选择子对象/编辑顶点", "Freeze tool": "冻结工具", "Disable snapping": "禁用吸附", "Mouse wheel zoom": "鼠标滚轮缩放", "Select hovered item's child": "选择悬停项目的子对象", "Alternate": "交替", "Create shape symmetrically": "对称创建形状", "Create shape from center": "从中心创建形状", "Bound resize from origin": "从原点调整大小", "Resize keyframe selection": "调整关键帧选择范围", "Navigation tree up": "导航树上移", "Navigation tree down ": "导航树下移", "Navigation tree out": "导航树退出", "Navigation tree in": "导航树进入", "Move left": "向左移动", "Move right": "向右移动", "Move up": "向上移动", "Move down": "向下移动", "Close tab": "关闭标签页", "Show actions": "显示操作", "Align top": "顶部对齐", "Align left": "左对齐", "Align right": "右对齐", "Align bottom": "底部对齐", "Align center": "居中对齐", "Align middle": "垂直居中", "Origin top": "原点顶部", "Origin left": "原点左侧", "Origin right": "原点右侧", "Origin bottom": "原点底部", "Origin center": "原点中心", "Origin middle": "原点中间", "Quit Rive": "退出 Rive", "Hierarchy tab": "层级标签页", "Assets panel tab": "资源面板标签页", "Data tab": "数据标签页", "Paste style": "粘贴样式", "Duplicate on drag": "拖动时复制", "Expand keyed properties": "展开关键帧属性", "Maximize timeline": "最大化时间轴", "Previous animation": "上一个动画", "Next animation": "下一个动画", "Move playhead to start": "播放头移至开头", "Move playhead to end": "播放头移至末尾", "Toggle shortcuts panel": "切换快捷键面板", "Key all vertices": "所有顶点添加关键帧", "Make component": "创建组件", "Remove component": "移除组件", "Preview bound values": "预览绑定值", "Help tool": "帮助工具", "Filter transform properties": "筛选变换属性", "Filter position properties": "筛选位置属性", "Filter scale properties": "筛选缩放属性", "Filter rotation property": "筛选旋转属性", "Filter opacity property": "筛选不透明度属性", "Disable state machine layer": "禁用状态机图层", "About Rive": "关于 Rive", "Show Licenses": "显示许可证", "Check for Updates...": "检查更新...", "Falloff": "衰减", "F": "起", "Clamp": "钳制", "Units": "单位", "Range Type": "范围类型", "Characters": "字符", "Characters (no spaces)": "字符(不含空格)", "Words": "单词", "Lines": "行", "Percentage": "百分比", "Index": "索引", "Add": "添加", "Subtract": "减去", "Paragraph Spacing": "段落间距", "Line Height": "行高", "Letter Spacing": "字母间距", "Spacing": "间距", "Character Variant": "字符变体", "Quantize": "量化", "Learn more": "了解更多", "Confirm Script Selection": "确认脚本选择", "Export Scripts": "导出脚本", "Run": "运行", "All": "全部", "Previewing Revision:": "预览版本:", "Restore": "恢复", "Fetching...": "获取中...", "Download Revision": "下载版本", "Tags": "标签", "Add Tag": "添加标签", "Add Group Effect": "添加效果组", "Remove Group Effect": "移除效果组", "Create Tag": "创建标签", "Remove Tag": "移除标签", "New Tag": "新标签", "Edit Tags": "编辑标签", "Collapse Tags": "折叠标签", "Reveal Tags": "显示标签", "Locked": "已锁定", "Lock": "锁定", "Unlock": "解锁", "Filtered": "已筛选", "Remove Filters": "移除筛选", "Conditions": "条件", "Selected Conditions": "选中的条件", "No conditions": "无条件", "Group Transitions": "编组过渡", "Ungroup Transitions": "取消编组过渡", "Change Source": "更改源", "Change Target": "更改目标", "Transition": "过渡", "transitions": "过渡", "listeners": "监听器", "Create an input in the Inputs panel to use as condition": "在输入面板中创建输入以用作条件", "New Input": "新建输入", "New Scripted Condition": "新建脚本条件", "on": "开启", "Enter": "进入", "Minimize Fields": "最小化字段", "Expand Fields": "展开字段", "Resizable Panel": "可调整面板", "Pin Panel": "固定面板", "Select a transition or listener to view conditions and actions": "选择过渡或监听器以查看条件和操作", "Voyager": "Voyager", "Team": "团队", "Cadet": "Cadet", "Enterprise": "企业版", "Student": "学生", "3 seat max": "最多 3 个席位", "25 seat max": "最多 25 个席位", "$10M+ annual revenue": "年收入超过 1000 万美元", "Upload .rev files from file browser": "从文件浏览器上传 .rev 文件", "Open the file browser by selecting the Rive tab in order to upload a .rev backup file": "选择 Rive 标签页打开文件浏览器以上传 .rev 备份文件", "Upload .riv files from file browser": "从文件浏览器上传 .riv 文件", "Open the file browser by selecting the Rive tab in order to upload a .riv file": "选择 Rive 标签页打开文件浏览器以上传 .riv 文件", "You have joined the team.": "您已加入团队。", "You have joined ": "您已加入 ", "Team joined": "已加入团队", "Sounds": "声音", "Soundly": "Soundly", "Browse sounds": "浏览声音", "Data": "数据", "Bind": "绑定", "Model": "模型", "V. Model": "视图模型", "ViewModel": "视图模型", "Instance": "实例", "Instances": "实例", "Path": "路径", "Default Instance": "默认实例", "Selected Properties": "选中的属性", "Create view model": "创建视图模型", "Bind view model": "绑定视图模型", "Select view model": "选择视图模型", "Bind instance": "绑定实例", "Add List Item": "添加列表项", "List Item": "列表项", "Mixed": "混合", "Preview Bound Values": "预览绑定值", "Data Context": "数据上下文", "Select Data Context": "选择数据上下文", "View Models": "视图模型", "Enums": "枚举", "Create Enum": "创建枚举", "Select Enum": "选择枚举", "Edit keys and values separately": "分别编辑键和值", "List": "列表", "List Attributes": "列表属性", "Vertex X": "顶点 X", "Vertex Y": "顶点 Y", "Cubic In X": "三次入 X", "Cubic In Y": "三次入 Y", "Cubic Out X": "三次出 X", "Cubic Out Y": "三次出 Y", "In Distance": "入距离", "Out Distance": "出距离", "In Rotation": "入旋转", "Out Rotation": "出旋转", "Text Content": "文本内容", "Text Style": "文本样式", "List Index values cannot be edited": "列表索引值无法编辑", "Symbols": "符号", "List Index": "列表索引", "Image": "图片", "Edit Instance": "编辑实例", "Enum value": "枚举值", "Use artboard linked to view model": "使用链接到视图模型的画板", "Instances of": "实例属于", "Instance of": "实例属于", "Name cannot be empty": "名称不能为空", "Identifier cannot start with a number": "标识符不能以数字开头", "Identifier can only contain letters, numbers, and underscores": "标识符只能包含字母、数字和下划线", "ViewModels should use PascalCase (e.g. MyViewModel)": "视图模型应使用帕斯卡命名法(如 MyViewModel)", "Properties should use camelCase (e.g. myProperty)": "属性应使用驼峰命名法(如 myProperty)", "Enums should use PascalCase (e.g. MyEnum)": "枚举应使用帕斯卡命名法(如 MyEnum)", "Data Bind": "数据绑定", "Auto Bind": "自动绑定", "Update Bind": "更新绑定", "Unbind": "取消绑定", "Couldn't find the applicable artboard": "找不到适用的画板", "Couldn't resolve bind parameters": "无法解析绑定参数", "Couldn't resolve data type": "无法解析数据类型", "Unable to create properties for multiple view models": "无法为多个视图模型创建属性", "Invalid property": "无效属性", "Converter": "转换器", "Converters": "转换器", "Converter Group": "转换器组", "Existing": "现有", "No converter assigned": "未分配转换器", "Converter contains errors": "转换器包含错误", "Round Number": "数值取整", "Decimals": "小数位", "Pad Length": "填充长度", "Pad Text": "填充文本", "Round Decimals": "小数位取整", "Remove Trailing Zeros": "移除尾随零", "From a Number": "从数值", "From a Color": "从颜色", "Red": "红", "Green": "绿", "Blue": "蓝", "Alpha": "透明度", "Luminance": "亮度", "Hex": "十六进制", "Input Lower": "输入下限", "Input Upper": "输入上限", "Output Lower": "输出下限", "Output Upper": "输出上限", "Clamp Lower": "钳制下限", "Clamp Upper": "钳制上限", "Parentheses": "括号", "Comma": "逗号", "Function": "函数", "Close Parenthesis": "右括号", "Open Parenthesis": "左括号", "Reset Formula?": "重置公式?", "Are you sure you want to clear all formula arguments?": "确定要清除所有公式参数吗?", "One Way": "单向", "One Way to Source": "单向到源", "Two Way": "双向", "Bidirectional": "双向", "Bidirectional (prefer target value)": "双向(优先目标值)", "Bidirectional (prefer source value)": "双向(优先源值)", "Bind Once": "绑定一次", "Bind Relative": "相对绑定", "Target to Source": "目标到源", "Bind Color": "绑定颜色", "Update Bind Color": "更新绑定颜色", "Unbind Color": "取消绑定颜色", "Bind Stroke": "绑定描边", "Update Bind Stroke": "更新绑定描边", "Unbind Stroke": "取消绑定描边", "Remove Binding": "移除绑定", "Source": "源", "Add a converter": "添加转换器", "Select a property": "选择属性", "Property Groups": "属性组", "Match Source View Model": "匹配源视图模型", "Computed Transform": "计算变换", "Show dependencies": "显示依赖项", "Hide dependencies": "隐藏依赖项", "Show parent/child": "显示父/子", "Show draw order": "显示绘制顺序", "Show constraints": "显示约束", "Show bones": "显示骨骼", "Show clips": "显示裁剪", "Show data binds": "显示数据绑定", "Paste With Timelines": "粘贴时间轴", "Merge by": "合并方式", "Don't merge": "不合并", "Paste without timelines": "不带时间轴粘贴", "Node": "节点", "Leaf": "叶", "You will lose all animations and state machines added to this nested artboard": "将丢失添加到此嵌套画板的所有动画和状态机", "Align Pos": "对齐位置", "Clear Cached File State": "清除缓存文件状态", "This will remove all locally cached state of your files, such as selected artboards and animations. Are you sure you want to clear this state?": "这将删除文件的所有本地缓存状态,如选中的画板和动画。确定要清除此状态吗?", "Clear File State": "清除文件状态", "N-Slice": "N 宫格", "N-Slice selection": "N 宫格选择", "Convert to N-Slice": "转换为 N 宫格", "Remove N-Slice": "移除 N 宫格", "Apply N-Slice": "应用 N 宫格", "Edit N-Slice": "编辑 N 宫格", "Initial Size": "初始尺寸", "Failed to add N-Slice": "添加 N 宫格失败", "X Axes": "X 轴", "Y Axes": "Y 轴", "Tiles": "瓦片", "Tile": "瓦片", "Axis": "轴", "Deform": "变形", "Remove Mesh?": "移除网格?", "Applying an N-Slice will replace the current mesh. Do you want to continue?": "应用 N 宫格将替换当前网格。确定要继续吗?", "Remove N-Slice?": "移除 N 宫格?", "Applying a mesh will replace the current N-Slice. Do you want to continue?": "应用网格将替换当前 N 宫格。确定要继续吗?", "Editing N-Slice": "编辑 N 宫格中", "Scroll Constraint": "滚动约束", "Scroll Thumb Constraint": "滚动滑块约束", "Scroll": "滚动", "Scroll Bar Thumb": "滚动条滑块", "Scroll Content": "滚动内容", "Content": "内容", "Physics": "物理", "Scroll Index": "滚动索引", "Clamped": "钳制", "Friction": "摩擦", "Speed Multiplier": "速度倍数", "Elastic Factor": "弹性因子", "Snap": "吸附", "Auto Size": "自动尺寸", "Target requires a Content Scroll constraint": "目标需要内容滚动约束", "Invalid target": "无效目标", "Enter at your own risk 🚧": "风险自负 🚧", "Early Access is where the magic (and occasional chaos) happens. Features are still evolving, and things might break, change, or behave unexpectedly. If you're experimenting, great! But maybe don't use these for mission-critical graphics just yet.": "早期访问是奇迹(偶尔也是混乱)发生的地方。功能仍在发展中,可能会出现问题、更改或意外行为。如果您正在实验,很好!但暂时不要用于关键图形。", "I like to live dangerously": "我愿意冒险尝试", "Unlock Early Access": "解锁早期访问", "Want to try Rive's latest experiments before the rest of the world? Early Access is included in Cadet, Voyager, and Enterprise plans.\nUpgrade now and start breaking things (in a good way).": "想在全世界之前尝试 Rive 的最新实验吗?早期访问包含在 Cadet、Voyager 和企业计划中。\n立即升级并开始突破创新。", "Upgrade this workspace for Early Access": "升级此工作区以获取早期访问", "Early Access is included in Cadet, Voyager, and Enterprise plans. Select another workspace or upgrade this one to start breaking things (in a good way).": "早期访问包含在 Cadet、Voyager 和企业计划中。选择其他工作区或升级此工作区以开始突破创新。", "Upgrade and experiment": "升级并实验", "You're in Early Access. Expect exciting new features, a few surprises, and for files to behave differently in production.": "您处于早期访问中。期待令人兴奋的新功能、一些惊喜,以及文件在生产环境中的不同表现。", "Sign up for updates": "注册获取更新", "Sign in with another account": "使用其他账户登录", "Heads up, you're in Early Access 🚧": "注意,您处于早期访问 🚧", "You're exporting from Early Access. Unreleased features are still evolving, and your export might break, change, or behave unexpectedly.": "您正在从早期访问导出。未发布的功能仍在发展中,导出可能会出现问题、更改或意外行为。", "Artboard List": "画板列表", "View Model mapping": "视图模型映射", "Virtualize": "虚拟化", "Carousel": "轮播", "Carousel requires virtualization": "轮播需要虚拟化", "Add a list to enable virtualization": "添加列表以启用虚拟化", "Add a list to enable carousel": "添加列表以启用轮播", "Update library components for: ": "更新库组件:", "Currently": "当前", "Latest": "最新", "Update selected": "更新选中项", "Detach": "分离", "Sync Instances": "同步实例", "Library Options": "库选项", "Update Library": "更新库", "Refresh": "刷新", "Add to File": "添加到文件", "Publish update": "发布更新", "Publish these components:": "发布这些组件:", "Failed to load available libraries": "加载可用库失败", "Failed to create a library - try again later.": "创建库失败 - 请稍后重试。", "Failed to publish a library update - try again later.": "发布库更新失败 - 请稍后重试。", "The Rive Renderer is not supported on this device.": "此设备不支持 Rive 渲染器。", "Split Left": "左侧分割", "Split Right": "右侧分割", "Split Up": "上方分割", "Split Down": "下方分割", "Close": "关闭", "Search Files": "搜索文件", "Search artboards, components, objects...": "搜索画板、组件、对象...", "Search Animations": "搜索动画", "Find in Files": "在文件中查找", "Create script: ": "创建脚本:", "Search scripts...": "搜索脚本...", "Agent": "智能体", "Agent Panel": "智能体面板", "Close Agent Panel": "关闭智能体面板", "Open in Viewport": "在视口中打开", "Debug Panel": "调试面板", "Close Debug Panel": "关闭调试面板", "Toggle Presentation Mode": "切换演示模式", "Collapse Code Editor": "折叠代码编辑器", "New Tab": "新建标签页", "New Chat": "新聊天", "Split Editor Right": "右侧分割编辑器", "Split Editor Down": "下方分割编辑器", "Panel Options": "面板选项", "Copy Console": "复制控制台", "Clear Console": "清除控制台", "Clear Console on Play": "播放时清除控制台", "Close Chat": "关闭聊天", "Clear All Chats": "清除所有聊天", "Close Other Chats": "关闭其他聊天", "Loading chats...": "加载聊天中...", "Failed": "失败", "Success": "成功", "Running...": "运行中...", "Interrupted": "已中断", "Unknown operation": "未知操作", "Viewing file": "查看文件", "Creating file": "创建文件", "Replacing text": "替换文本", "Inserting text": "插入文本", "Editing file": "编辑文件", "Generate Converter from Script": "从脚本生成转换器", "Generate a Path Effect from Script": "从脚本生成路径效果", "Run Test": "运行测试", "Run Test Group": "运行测试组", "Run All Tests": "运行所有测试", "Close Active Tab": "关闭活动标签页", "Close Other Tabs": "关闭其他标签页", "Close All Tabs": "关闭所有标签页", "Close Pane": "关闭面板", "Delete Scripts?": "删除脚本?", "This script is being used by an object on an artboard. Are you sure you want to remove it?": "此脚本正在被画板上的对象使用。确定要移除吗?", "A script is being used by an object on an artboard. Are you sure you want to remove it?": "有脚本正在被画板上的对象使用。确定要移除吗?", "Namespace": "命名空间", "Make a game menu with 10 items": "制作一个包含 10 个项目的游戏菜单", "Draw a configurable 3D platonic solid": "绘制可配置的 3D 正多面体", "Create a music player UI using layouts": "使用布局创建音乐播放器 UI", "Make an infinite scroll menu with layouts": "使用布局制作无限滚动菜单", "Create a particle system with components": "使用组件创建粒子系统", "Make a health bar using layouts and text": "使用布局和文本制作生命条", "Give my character physics properties": "为角色添加物理属性", "Arrange components into configurable grid": "将组件排列成可配置网格", "Code, design, and animate with AI": "使用 AI 编码、设计和制作动画", "Code, design, and \nanimate with AI": "使用 AI 编码、设计\n和制作动画", "Rive's AI agent helps you write code, design, and animate. Generate scripts, responsive layouts, data models, and even animation keys.": "Rive 的 AI 智能体帮助您编写代码、设计和制作动画。生成脚本、响应式布局、数据模型甚至动画关键帧。", "Learn More": "了解更多", "What do you want to build?": "您想构建什么?", "Accept all changes": "接受所有更改", "Reject all changes": "拒绝所有更改", "Animate": "动画", "Ask me anything...": "问我任何问题...", "Attract and Repel": "吸引与排斥", "Black": "黑体", "Black Italic": "黑斜体", "Bold": "粗体", "Bold Italic": "粗斜体", "Bubble Button": "气泡按钮", "bubble tap Interactive Mini Games": "泡泡水龙头互动小游戏", "Cloudy Walk": "云中漫步", "CODE, DESIGN, AND ANIMATE WITH AI": "使用 AI 编码、设计和制作动画", "Connection lost. Reconnecting in 0...": "连接已断开。正在重新连接 0...", "Convert Inputs to ViewModels": "将输入转换为视图模型", "Converter Script": "转换器脚本", "Create a music player UI using layout": "使用布局创建音乐播放器界面", "Create a particle system with component": "使用组件创建粒子系统", "Draw a configurable 3D platonic soli": "绘制可配置的 3D 柏拉图立体", "Expression Grid": "表情网格", "ExtraBold": "特粗", "ExtraBold Italic": "特粗斜体", "ExtraLight": "特细", "ExtraLight Italic": "特细斜体", "Give my character physics propertie": "为角色添加物理属性", "Hover House": "悬浮房屋", "Hungry Snake": "贪吃蛇", "Inter": "Inter", "Interactive Icon Set": "互动图标集", "Interactive Sasquatich": "互动式大脚怪", "Italic": "斜体", "Layout Script": "布局脚本", "LEARN MORE": "了解更多", "Light": "细体", "Light Italic": "细斜体", "Listener Action Script": "监听器动作脚本", "Masonry Layout": "瀑布流布局", "Medium Italic": "中等斜体", "Node Script": "节点脚本", "Path Effect Script": "路径效果脚本", "Personal Files": "个人文件", "Puzzle 01: Fix the Broken Button": "谜题 01:修复损坏的按钮", "Regular": "常规", "Room Decor Mini Game": "房间装饰小游戏", "Scripting Converter": "脚本转换器", "Scripting Path Effect - Boil": "脚本路径效果 - 沸腾", "SemiBold": "半粗", "SemiBold Italic": "半粗斜体", "show_collapsed": "显示折叠项", "Slot Machine Game with Scripting": "支持脚本编程的老虎机游戏", "Smart": "智能", "StudioRun - A Cosmic Game by TheLittleLabs": "工作室运营-小实验室制作的宇宙游戏", "Test Script": "测试脚本", "Text Modifier Range": "文本修饰器范围", "Thin": "极细", "Thin Italic": "极细斜体", "Transition Condition Script": "过渡条件脚本", "UPGRADE": "升级", "Test": "测试", "Listener Action": "监听动作", "Transition Condition": "过渡条件", "stage": "舞台", "design": "设计", "animate": "动画", "No properties yet": "暂无属性", "All Glyphs": "全部字形", "Automatic": "自动", "Bopomofo": "注音符号", "Common": "通用", "Cyrillic": "西里尔文", "Embedded": "内嵌", "Force Export": "强制导出", "Glyphs Used": "已用字形", "Greek": "希腊文", "Hosted": "托管", "Inherited": "继承", "Latin": "拉丁文", "Prevent Export": "禁止导出", "Referenced": "引用", "Source/Type": "来源/类型", "Triangle Path": "三角路径", "Bezier": "贝塞尔曲线", "N-Slice Group": "九宫格切片", "Custom Shape": "自定义形状", "Rectangle Path": "矩形路径", "Star Path": "星形路径", "Polygon Path": "多边形路径", "Ellipse Path": "椭圆路径", ": ViewModel": ":视图模型", "fps": "帧率", "Shape Layout": "形状布局", "Current": "当前", "Playback Speed": "播放速度", "Snap Keys": "关键帧吸附", "Expanded": "已展开", "Collapsed": "已折叠", "One Shot": "单次播放", "Loop": "循环", "Ping Pong": "往返循环", "Work Area": "工作区", "Queued": "队列", "Edited Just now": "刚刚编辑", //网络请求翻译 "Shared Project": "共享项目", "Untitled": "未命名", "Marketplace Examples": "市场范例", "Learn Rive": "学习 Rive", "Quickstart Tutorial": "快速入门教程", "Get started with a range of features throughout Rive as we create an interactive menu": "通过创建交互式菜单,系统掌握 Rive 的核心功能", "Scripting lets you iterate on code, design, and animation in one collaborative editor.": "在统一的协作编辑器中,高效迭代代码、设计与动画内容。", "Responsive Layouts": "响应式布局", "Create responsive designs with fine-tuned control over what participates in the layouts system.": "精细控制参与布局系统的元素,实现精准的响应式设计。", "Data Binding": "数据绑定", "A powerful way to create reactive connections between editor elements, data, and code.": "在编辑器元素、数据源与代码之间建立响应式关联的强大机制。", "Vector Feathering": "矢量羽化", "Soften the edge of vector paths without the typical performance impact.": "在无损性能的前提下,柔化矢量路径的边缘效果。", "Design Mode vs. Animate Mode": "设计模式与动画模式", "Get to know the Rive features not typically found in other design tools.": "深入了解 Rive 独有的、区别于传统设计工具的核心工作流。", "Build interactive animations that are ready to run with Rive's State Machine.": "借助 Rive 状态机,构建可直接部署的交互式动画逻辑。", "Animation mixing": "动画混合", "Build layered interactions that look complex, but are simple under the hood.": "通过分层混合技术,实现视觉复杂但结构简洁的交互动效。", "Explore a game-changing feature that enhances communication between designers and developers.": "探索这一变革性功能,显著提升设计师与开发者之间的协作效率。", "Run anywhere": "全平台运行", "Load and control your animations in apps, games, and websites with the open-source libraries of Rive's runtimes.": "依托 Rive 运行时的开源库,轻松将动画集成至应用程序、游戏及网站中,并实现精准控制。", }; //模板翻译规则 const UNIT_MAP = { time: '次', times: '次', glyph: '字形', glyphs: '字形' }; const TIME_UNITS = { minute: '分钟', minutes: '分钟', hour: '小时', hours: '小时', day: '天', days: '天', week: '周', weeks: '周', month: '个月', months: '个月', year: '年', years: '年', }; const VERSION_NUM_RE = /^[0-9]+(?:\.[0-9]+){1,6}$/; const NUMBERED_LABELS = { 'Style': '样式', 'Axis': '轴', 'Draw Rule': '绘制规则', 'Calculate': '计算', 'Convert to Number': '转换为数字', 'Convert to String': '转换为字符串', 'Converter Group': '转换器组', 'Enum': '枚举', 'Formula': '公式', 'Interpolator': '插值器', 'List to Length': '列表转长度', 'Number to List': '数字转列表', 'Pad String': '字符串填充', 'Range Map': '范围映射', 'Remove Trailing Zeroes': '移除末尾零', 'Remove Trailing Zeros': '移除末尾零', 'Round Number': '数字取整', 'State Machine': '状态机', 'Timeline': '时间轴', 'Toggle': '开关', 'Trim String': '裁剪字符串', 'View Model': '视图模型', 'ViewModel': '视图模型', 'Layer': '图层', 'Forward': '前进', 'Backward': '后退', 'Stylistic Set': '风格集', 'SOC2 Type': 'SOC2 类型', 'Select item': '选择项目', }; const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const NUMBERED_LABEL_ALT = Object.keys(NUMBERED_LABELS).slice().sort((a, b) => b.length - a.length).map(escapeRegExp).join('|'); const PATTERNS = [ { re: /^(Forward|Backward)\s+(\d+)$/i, fn: (m) => `${m[1].toLowerCase() === 'forward' ? '前进' : '后退'} ${m[2]} 帧` }, { re: /^Incremental from\s+(\d+)$/i, fn: (m) => `从 ${m[1]} 递增` }, { re: /^Select item\s+(\d+)$/i, fn: (m) => `选择项目 ${m[1]}` }, { re: /^Stylistic Set\s+(\d+)$/i, fn: (m) => `风格集 ${m[1]}` }, { re: /^SOC2 Type\s+(\d+)$/i, fn: (m) => `SOC2 类型 ${m[1]}` }, { re: /^Bump\s+(down|left|right|up)\s+(\d+)px$/i, fn: (m) => ({ down: '下移', left: '左移', right: '右移', up: '上移' }[m[1].toLowerCase()] || m[1]) + ` ${m[2]}px` }, { re: /^Nudge\s+(\d+)px\s+(down|left|right|up)$/i, fn: (m) => ({ down: '下移', left: '左移', right: '右移', up: '上移' }[m[2].toLowerCase()] || m[2]) + ` ${m[1]}px` }, { re: /^(\d+)\s+(time|times|Glyph|Glyphs|glyph|glyphs)$/i, fn: (m) => `${m[1]} ${UNIT_MAP[m[2].toLowerCase()] || m[2]}` }, { re: /^Compiled in\s+(\d+(?:\.\d+)?)ms$/i, fn: (m) => `编译耗时 ${m[1]}毫秒` }, { re: /^Connection lost\.\s*Reconnecting in\s+(\d+)\.\.\.$/i, fn: (m) => `连接已断开。将在 ${m[1]} 秒后重连…` }, { re: /^"?(Are you sure you want to delete)\s+'([^']+)'\?"?$/i, fn: (m) => `确定要删除“${m[2]}”吗?` }, { re: /^Edited\s+(\d+)\s+(minute|minutes|hour|hours|day|days|week|weeks|month|months|year|years)\s+ago$/i, fn: (m) => `${m[1]} ${TIME_UNITS[m[2].toLowerCase()] || m[2]}前编辑` }, { re: /^Published\s+(\d+)\s+(minute|minutes|hour|hours|day|days|week|weeks|month|months|year|years)\s+ago$/i, fn: (m) => `${m[1]} ${TIME_UNITS[m[2].toLowerCase()] || m[2]}前发布` }, { re: /^BETA\s+([0-9]+(?:\.[0-9]+){1,6})$/i, fn: (m) => `测试版 ${m[1]}` }, { re: /^Version\s+([0-9]+(?:\.[0-9]+){1,6})$/i, fn: (m) => `版本 ${m[1]}` }, { re: new RegExp(`^(${NUMBERED_LABEL_ALT})\\s*(\\d+)$`, 'i'), fn: (m) => { const rawKey = m[1]; const n = m[2]; const k = Object.keys(NUMBERED_LABELS).find((x) => x.toLowerCase() === rawKey.toLowerCase()) || rawKey; return `${NUMBERED_LABELS[k] || rawKey} ${n}`; }, }, { re: /^photo_(\d{4}-\d{2}-\d{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})$/i, fn: (m) => `照片_${m[1]}` }, ]; // 翻译入口 const shouldTranslate = (s) => { if (typeof s !== 'string') return false; const t = s.trim(); if (!t) return false; if (/[\u4e00-\u9fff]/.test(t)) return false; if (t.length > 240) return false; if (/^\d+$/.test(t)) return false; if (VERSION_NUM_RE.test(t)) return false; return true; }; const TRAN_MEMO = new Map(); const TRAN_MEMO_MAX = 2000; const translateText = (s) => { if (!shouldTranslate(s)) return s; const hit = TRAN_MEMO.get(s); if (hit) return hit; let out = DICT[s] || s; if (out === s) { for (const rule of PATTERNS) { const m = s.match(rule.re); if (m) { try { out = rule.fn(m); } catch { out = s; } break; } } } TRAN_MEMO.set(s, out); if (TRAN_MEMO.size > TRAN_MEMO_MAX) TRAN_MEMO.clear(); return out; }; // IndexedDB const DB = (() => { let dbp; const open = () => { if (dbp) return dbp; dbp = new Promise((resolve, reject) => { const req = indexedDB.open(CONFIG.DB_NAME, CONFIG.DB_VERSION); req.onupgradeneeded = (e) => { const db = e.target.result; if (!db.objectStoreNames.contains(CONFIG.STORE_NAME)) db.createObjectStore(CONFIG.STORE_NAME); }; req.onsuccess = (e) => resolve(e.target.result); req.onerror = () => reject(req.error); }); return dbp; }; const get = async (key) => { try { const db = await open(); return await new Promise((resolve) => { const tx = db.transaction(CONFIG.STORE_NAME, 'readonly'); const req = tx.objectStore(CONFIG.STORE_NAME).get(key); req.onsuccess = () => resolve(req.result ?? null); req.onerror = () => resolve(null); }); } catch { return null; } }; const set = async (key, value) => { const db = await open(); return await new Promise((resolve, reject) => { const tx = db.transaction(CONFIG.STORE_NAME, 'readwrite'); tx.objectStore(CONFIG.STORE_NAME).put(value, key); tx.oncomplete = () => resolve(true); tx.onerror = () => reject(tx.error); }); }; const del = async (key) => { try { const db = await open(); return await new Promise((resolve) => { const tx = db.transaction(CONFIG.STORE_NAME, 'readwrite'); tx.objectStore(CONFIG.STORE_NAME).delete(key); tx.oncomplete = () => resolve(true); tx.onerror = () => resolve(false); }); } catch { return false; } }; const clear = async () => { try { const db = await open(); return await new Promise((resolve) => { const tx = db.transaction(CONFIG.STORE_NAME, 'readwrite'); tx.objectStore(CONFIG.STORE_NAME).clear(); tx.oncomplete = () => resolve(true); tx.onerror = () => resolve(false); }); } catch { return false; } }; return { open, get, set, delete: del, clear }; })(); const K = { fontList: 'fonts:list', currentFontId: 'fonts:current', meta: (id) => `fonts:meta:${id}`, buf: (id) => `fonts:buf:${id}`, legacyFontKey: 'font', legacyMetaKey: 'font_meta', }; const formatBytes = (n) => { if (!n || n <= 0) return '—'; const kb = n / 1024; if (kb < 1024) return `${kb.toFixed(kb < 10 ? 2 : 1)} KB`; const mb = kb / 1024; return `${mb.toFixed(mb < 10 ? 2 : 1)} MB`; }; const formatTime = (ts) => { if (!ts) return '—'; try { const d = new Date(ts); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); return `${y}-${m}-${dd} ${hh}:${mm}`; } catch { return '—'; } }; const guessFontExt = (meta, preset) => { const name = String(meta?.name || ''); const url = String(meta?.url || preset?.url || ''); const type = String(meta?.type || '').toLowerCase(); const lowerName = name.toLowerCase(); const lowerUrl = url.toLowerCase(); if (lowerName.endsWith('.otf') || lowerUrl.includes('.otf')) return 'otf'; if (lowerName.endsWith('.ttf') || lowerUrl.includes('.ttf')) return 'ttf'; if (type.includes('opentype') || type.includes('otf')) return 'otf'; if (type.includes('truetype') || type.includes('ttf')) return 'ttf'; if (preset?.typeHint === 'otf') return 'otf'; if (preset?.typeHint === 'ttf') return 'ttf'; return ''; }; const fontContentType = (ext) => { if (ext === 'otf') return 'font/otf'; if (ext === 'ttf') return 'font/ttf'; return 'application/octet-stream'; }; const PREVIEW_STYLE_ID = 'rivecn_preview_fontfaces'; const ensureGlobalPreviewStyle = () => { let el = document.getElementById(PREVIEW_STYLE_ID); if (el) return el; el = document.createElement('style'); el.id = PREVIEW_STYLE_ID; el.type = 'text/css'; (document.head || document.documentElement).appendChild(el); return el; }; const gmRequest = ({ url, method = 'GET', responseType = 'text', timeout = CONFIG.REQUEST_TIMEOUT_MS, headers, onProgress }) => new Promise((resolve, reject) => { GM_xmlhttpRequest({ method, url, responseType, timeout, headers, onload: (res) => (res.status >= 200 && res.status < 300 ? resolve(res) : reject(new Error(`HTTP ${res.status}`))), onerror: () => reject(new Error('网络错误')), ontimeout: () => reject(new Error('请求超时')), onprogress: onProgress ? (e) => { try { onProgress(Number(e?.loaded || 0), Number(e?.total || 0)); } catch { } } : undefined, }); }); // LoaderUI const LoaderUI = (() => { let host, shadow, overlayEl, progressFill, statusEl, detailEl, stepsEl, actionsEl, tShown = 0; const steps = new Map(); const css = ` :host{all:initial} .ov{ position:fixed; inset:0; z-index:2147483647; display:flex; align-items:center; justify-content:center; background: radial-gradient(1200px 700px at 15% 15%, rgba(59,130,246,.18), transparent 62%), radial-gradient(900px 600px at 85% 25%, rgba(139,92,246,.18), transparent 66%), radial-gradient(900px 600px at 45% 90%, rgba(16,185,129,.10), transparent 60%), linear-gradient(180deg, #0a0d14, #07080d); color:#fff; font-family:ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji"; transition:opacity .22s ease; } .card{ width:min(580px, calc(100vw - 48px)); border-radius:22px; padding:22px 22px 18px; background:linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.04)); border:1px solid rgba(255,255,255,.10); box-shadow:0 32px 90px rgba(0,0,0,.58), 0 0 0 1px rgba(255,255,255,.06) inset; backdrop-filter: blur(18px); } .top{display:flex; align-items:center; gap:12px; margin-bottom:14px} .badge{ width:42px; height:42px; border-radius:14px; background:linear-gradient(135deg, rgba(59,130,246,.95), rgba(139,92,246,.95)); box-shadow:0 16px 46px rgba(59,130,246,.18); position:relative; overflow:hidden; } .badge:before{ content:""; position:absolute; inset:1px; border-radius:13px; background: radial-gradient(circle at 30% 20%, rgba(255,255,255,.40), transparent 56%), rgba(0,0,0,.08); } .title{font-weight:860; letter-spacing:.2px; font-size:18px; line-height:1.1} .sub{margin-top:3px; font-size:12px; color:rgba(255,255,255,.56)} .bar{ margin-top:8px; width:100%; height:9px; border-radius:999px; background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.08); overflow:hidden; } .fill{ height:100%; width:0%; border-radius:999px; background:linear-gradient(90deg, rgba(59,130,246,.95), rgba(139,92,246,.95)); box-shadow:0 0 0 1px rgba(255,255,255,.07) inset, 0 12px 28px rgba(59,130,246,.14); transition:width .18s ease; } .status{margin-top:14px; font-size:14px; color:rgba(255,255,255,.92); min-height:20px} .detail{margin-top:6px; font-size:12px; color:rgba(255,255,255,.56); min-height:16px} .steps{margin-top:12px; min-height:36px} .chips{display:flex; flex-wrap:wrap; gap:8px; margin-top:6px} .chip{ display:inline-flex; align-items:center; gap:8px; padding:6px 10px; border-radius:999px; font-size:12px; letter-spacing:.2px; border:1px solid rgba(255,255,255,.10); } .dot{width:8px; height:8px; border-radius:50%} .actions{margin-top:16px; display:flex; gap:10px; justify-content:flex-end; min-height:44px} button{ appearance:none; border:1px solid rgba(255,255,255,.10); border-radius:12px; padding:10px 14px; font-size:14px; cursor:pointer; user-select:none; transition:transform .08s ease, filter .15s ease, background .15s ease; } button:active{transform:scale(.98)} button:hover{filter:brightness(1.06)} .p{ background:linear-gradient(135deg, rgba(59,130,246,.95), rgba(139,92,246,.95)); color:#fff; box-shadow:0 12px 34px rgba(0,0,0,.35), 0 0 0 1px rgba(255,255,255,.06) inset; } .s{ background:rgba(255,255,255,.06); color:rgba(255,255,255,.92); backdrop-filter: blur(10px); box-shadow:0 12px 34px rgba(0,0,0,.24); } .err{color:rgba(239,68,68,.96)} `; const fmtBytes = (n) => { if (!Number.isFinite(n) || n <= 0) return ''; const u = ['B', 'KB', 'MB', 'GB']; let i = 0; let v = n; while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; } const d = i === 0 ? 0 : i === 1 ? 0 : 1; return `${v.toFixed(d)}${u[i]}`; }; const ensure = () => { if (host) return; host = document.createElement('div'); shadow = host.attachShadow({ mode: 'closed' }); const style = document.createElement('style'); style.textContent = css; overlayEl = document.createElement('div'); overlayEl.className = 'ov'; overlayEl.setAttribute('role', 'status'); overlayEl.setAttribute('aria-live', 'polite'); const card = document.createElement('div'); card.className = 'card'; const top = document.createElement('div'); top.className = 'top'; const badge = document.createElement('div'); badge.className = 'badge'; const titleWrap = document.createElement('div'); const title = document.createElement('div'); title.className = 'title'; title.textContent = 'Rive汉化脚本 · 一身惆怅'; const sub = document.createElement('div'); sub.className = 'sub'; sub.textContent = '读得懂的温度,看得见的美感'; titleWrap.append(title, sub); top.append(badge, titleWrap); const bar = document.createElement('div'); bar.className = 'bar'; progressFill = document.createElement('div'); progressFill.className = 'fill'; bar.append(progressFill); statusEl = document.createElement('div'); statusEl.className = 'status'; detailEl = document.createElement('div'); detailEl.className = 'detail'; stepsEl = document.createElement('div'); stepsEl.className = 'steps'; actionsEl = document.createElement('div'); actionsEl.className = 'actions'; card.append(top, bar, statusEl, detailEl, stepsEl, actionsEl); overlayEl.append(card); shadow.append(style, overlayEl); document.documentElement.append(host); renderSteps(); setProgress(0); setStatus('准备中…', ''); }; const renderSteps = () => { if (!stepsEl) return; stepsEl.innerHTML = ''; const wrap = document.createElement('div'); wrap.className = 'chips'; for (const [name, state] of steps.entries()) { const chip = document.createElement('div'); chip.className = 'chip'; const dot = document.createElement('div'); dot.className = 'dot'; const isDone = state === 'done'; const isActive = state === 'active'; chip.style.background = isActive ? 'rgba(59,130,246,.16)' : isDone ? 'rgba(255,255,255,.06)' : 'rgba(255,255,255,.04)'; chip.style.color = isDone ? 'rgba(255,255,255,.70)' : 'rgba(255,255,255,.88)'; dot.style.background = isDone ? 'rgba(34,197,94,.92)' : isActive ? 'rgba(59,130,246,.95)' : 'rgba(255,255,255,.25)'; const txt = document.createElement('div'); txt.textContent = isDone ? `✓ ${name}` : name; chip.append(dot, txt); wrap.append(chip); } stepsEl.append(wrap); }; const show = () => { ensure(); overlayEl.style.opacity = '1'; statusEl.classList.toggle('err', false); tShown = nowMs(); }; const hide = async () => { if (!overlayEl) return; const dt = nowMs() - tShown; if (dt < CONFIG.UI_MIN_SHOW_MS) await new Promise((r) => setTimeout(r, CONFIG.UI_MIN_SHOW_MS - dt)); overlayEl.style.opacity = '0'; await new Promise((r) => setTimeout(r, 240)); try { host?.remove(); } catch { } host = shadow = overlayEl = progressFill = statusEl = detailEl = stepsEl = actionsEl = null; steps.clear(); }; const setProgress = (pct) => { if (!progressFill) return; progressFill.style.width = `${Math.min(100, Math.max(0, pct))}%`; }; const setStatus = (msg, detail = '') => { if (statusEl) statusEl.textContent = msg || ''; if (detailEl) detailEl.textContent = detail || ''; }; const setStep = (name, done = false) => { for (const [k, v] of steps.entries()) { if (v === 'active' && k !== name) steps.set(k, 'pending'); } steps.set(name, done ? 'done' : 'active'); renderSteps(); }; const setActions = (arr) => { if (!actionsEl) return; actionsEl.innerHTML = ''; if (!arr?.length) return; for (const it of arr) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = it.kind === 'secondary' ? 's' : 'p'; btn.textContent = it.text; btn.onclick = it.onClick; actionsEl.append(btn); } }; const error = (msg) => { if (statusEl) statusEl.classList.add('err'); setStatus(`❌ ${msg}`, '你可以清除缓存后重试'); setActions([ { text: '清除全部缓存', kind: 'secondary', onClick: async () => { try { await DB.clear(); } catch { } location.reload(); } }, { text: '刷新页面', kind: 'primary', onClick: () => location.reload() }, ]); }; const progressFromNetwork = (loaded, total, fromPct, toPct) => { if (!Number.isFinite(loaded) || loaded <= 0) return; if (!Number.isFinite(total) || total <= 0) return; const r = Math.min(1, Math.max(0, loaded / total)); setProgress(fromPct + (toPct - fromPct) * r); setStatus('下载中…', `${fmtBytes(loaded)} / ${fmtBytes(total)}`); }; return { show, hide, setProgress, setStatus, setStep, setActions, error, progressFromNetwork }; })(); const FontStore = (() => { let memFontCache = new Map(); let memInflight = new Map(); const ensurePresetsAndList = async () => { let ids = await DB.get(K.fontList); ids = Array.isArray(ids) ? ids : null; if (!ids) { ids = PRESET_FONTS.map((x) => x.id); await DB.set(K.fontList, ids); } else { const set = new Set(ids); for (let i = PRESET_FONTS.length - 1; i >= 0; i--) { const pid = PRESET_FONTS[i].id; if (!set.has(pid)) ids.unshift(pid); } const dedup = []; const seen = new Set(); for (const x of ids) { if (seen.has(x)) continue; seen.add(x); dedup.push(x); } await DB.set(K.fontList, dedup); } for (const p of PRESET_FONTS) { const metaRaw = await DB.get(K.meta(p.id)); if (!metaRaw) { await DB.set( K.meta(p.id), wrapCache( { id: p.id, name: p.name, source: 'remote', url: p.url, size: 0, cachedAt: 0, type: p.typeHint || '', deletable: false, }, 0 ) ); } } const cur = await DB.get(K.currentFontId); if (!cur) await DB.set(K.currentFontId, DEFAULT_FONT_ID); }; const migrateLegacyIfAny = async () => { const legacyBufRaw = await DB.get(K.legacyFontKey); const legacyMetaRaw = await DB.get(K.legacyMetaKey); if (!legacyBufRaw && !legacyMetaRaw) return; try { const buf = legacyBufRaw ? unwrapCache(legacyBufRaw) : null; const meta = legacyMetaRaw ? unwrapCache(legacyMetaRaw) : null; if (buf && buf.byteLength > 10_000) { const id = `legacy_${nowMs()}`; const name = meta?.name || 'Legacy Font'; const ids = await DB.get(K.fontList); const list = Array.isArray(ids) ? ids : PRESET_FONTS.map((x) => x.id); await DB.set(K.fontList, [...list, id]); await DB.set( K.meta(id), wrapCache( { id, name, source: 'local', addedAt: nowMs(), size: meta?.size || buf.byteLength, type: meta?.type || '', lastModified: meta?.lastModified || 0, deletable: true, }, 0 ) ); await DB.set(K.buf(id), wrapCache(buf, 0)); await DB.set(K.currentFontId, id); } } catch (e) { warnOnce('legacy_migrate', '旧版字体数据迁移失败', e); } finally { await DB.delete(K.legacyFontKey).catch(() => { }); await DB.delete(K.legacyMetaKey).catch(() => { }); } }; const getFontList = async () => { await ensurePresetsAndList(); const ids = await DB.get(K.fontList); return Array.isArray(ids) ? ids : PRESET_FONTS.map((x) => x.id); }; const getCurrentFontId = async () => { await ensurePresetsAndList(); return (await DB.get(K.currentFontId)) || DEFAULT_FONT_ID; }; const setCurrentFontId = async (id) => { await ensurePresetsAndList(); await DB.set(K.currentFontId, id); memFontCache.clear(); memInflight.clear(); }; const getMeta = async (id) => { const raw = await DB.get(K.meta(id)); return raw ? unwrapCache(raw) : null; }; const setMeta = async (id, meta) => { await DB.set(K.meta(id), wrapCache(meta, 0)); }; const getCacheInfo = async (id) => { if (!isPreset(id)) return { cached: true, stale: false, cachedAt: 0, size: 0 }; const raw = await DB.get(K.buf(id)); const buf = raw ? unwrapCache(raw) : null; const ok = !!(buf && buf.byteLength > 10_000); const fresh = ok && isFresh(raw); const stale = ok && !fresh; const cachedAt = ok ? cacheTime(raw) : 0; const size = ok ? (buf?.byteLength || 0) : 0; return { cached: fresh, stale, cachedAt, size }; }; const getFontContentType = async (id) => { const meta = await getMeta(id); const preset = getPreset(id); const ext = guessFontExt(meta, preset); return fontContentType(ext); }; const addLocalFont = async (file, buf) => { await ensurePresetsAndList(); const id = `local_${nowMs()}`; const ids = await getFontList(); await DB.set(K.fontList, [...ids, id]); const meta = { id, name: file?.name || 'local-font', source: 'local', addedAt: nowMs(), size: file?.size || buf?.byteLength || 0, type: file?.type || '', lastModified: file?.lastModified || 0, deletable: true, }; await DB.set(K.meta(id), wrapCache(meta, 0)); await DB.set(K.buf(id), wrapCache(buf, 0)); memFontCache.set(id, buf); return id; }; const deleteFont = async (id) => { if (isPreset(id)) return false; const ids = await getFontList(); await DB.set(K.fontList, ids.filter((x) => x !== id)); await DB.delete(K.meta(id)).catch(() => { }); await DB.delete(K.buf(id)).catch(() => { }); memFontCache.delete(id); memInflight.delete(id); if ((await getCurrentFontId()) === id) await setCurrentFontId(DEFAULT_FONT_ID); return true; }; const clearRemoteCache = async (id) => { if (!isPreset(id)) return false; await DB.delete(K.buf(id)).catch(() => { }); memFontCache.delete(id); memInflight.delete(id); const meta = await getMeta(id); if (meta) { meta.size = 0; meta.cachedAt = 0; await setMeta(id, meta); } return true; }; const clearFontCacheOnly = async () => { const ids = await getFontList(); for (const id of ids) if (isPreset(id)) await clearRemoteCache(id); }; const getOrFetchFontBytes = async (id, { silent = false } = {}) => { if (memFontCache.has(id)) return memFontCache.get(id); if (memInflight.has(id)) return memInflight.get(id); const p = (async () => { const meta = await getMeta(id); const preset = getPreset(id); if (meta?.source === 'local') { const raw = await DB.get(K.buf(id)); const buf = raw ? unwrapCache(raw) : null; if (buf?.byteLength > 10_000) { memFontCache.set(id, buf); return buf; } throw new Error('本地字体缺失或损坏'); } const cachedRaw = await DB.get(K.buf(id)); let staleBuf = null; if (cachedRaw) { const cachedAny = unwrapCache(cachedRaw); if (cachedAny?.byteLength > 10_000) { if (isFresh(cachedRaw)) { memFontCache.set(id, cachedAny); return cachedAny; } staleBuf = cachedAny; } } const url = meta?.url || preset?.url; if (!url) throw new Error('网络字体 URL 缺失'); if (!silent) { LoaderUI.show(); LoaderUI.setStep('字体'); LoaderUI.setStatus('下载字体…', preset?.name || '网络字体'); LoaderUI.setProgress(12); } const t0 = nowMs(); try { const res = await gmRequest({ url, responseType: 'arraybuffer', timeout: CONFIG.REQUEST_TIMEOUT_MS, onProgress: silent ? null : (l, t) => { LoaderUI.progressFromNetwork(l, t, 12, 62); if (l > 0 && t0) { const dt = (nowMs() - t0) / 1000; if (dt > 0.25) LoaderUI.setStatus('下载字体…', `${(l / 1024 / 1024).toFixed(1)}MB / ${dt.toFixed(1)}s`); } }, }); const buf = res.response; if (!buf?.byteLength || buf.byteLength <= 10_000) throw new Error('字体下载失败或过小'); memFontCache.set(id, buf); await DB.set(K.buf(id), wrapCache(buf)); const newMeta = (await getMeta(id)) || { id, source: 'remote', url }; newMeta.name = newMeta.name || preset?.name || id; newMeta.source = 'remote'; newMeta.url = url; newMeta.size = buf.byteLength; newMeta.cachedAt = nowMs(); await setMeta(id, newMeta); if (!silent) { LoaderUI.setStatus('字体下载完成', formatBytes(buf.byteLength)); LoaderUI.setProgress(74); LoaderUI.setStep('字体', true); LoaderUI.setStep('注入'); LoaderUI.setStatus('准备注入…', '返回字体字节'); LoaderUI.setProgress(96); } return buf; } catch (e) { if (staleBuf?.byteLength > 10_000) { memFontCache.set(id, staleBuf); if (!silent) { LoaderUI.setStatus('网络不可用,已回退到过期缓存', preset?.name || meta?.name || ''); LoaderUI.setProgress(78); LoaderUI.setStep('字体', true); LoaderUI.setStep('注入'); LoaderUI.setProgress(96); } return staleBuf; } throw e; } })(); memInflight.set(id, p); try { return await p; } finally { memInflight.delete(id); } }; return { ensurePresetsAndList, migrateLegacyIfAny, getFontList, getCurrentFontId, setCurrentFontId, getMeta, getCacheInfo, getFontContentType, addLocalFont, deleteFont, clearRemoteCache, clearFontCacheOnly, getOrFetchFontBytes, }; })(); const pickFile = (accept = '') => new Promise((resolve) => { const input = document.createElement('input'); input.type = 'file'; if (accept) input.accept = accept; input.style.position = 'fixed'; input.style.left = '-9999px'; input.style.top = '-9999px'; document.documentElement.appendChild(input); const cleanup = () => { try { input.remove(); } catch { } }; input.addEventListener('change', () => { const file = input.files?.[0]; if (!file) { cleanup(); resolve(null); return; } const reader = new FileReader(); reader.onload = () => { const buf = reader.result; cleanup(); resolve({ file, buf }); }; reader.onerror = () => { cleanup(); resolve({ file, buf: null, error: reader.error || new Error('读取失败') }); }; reader.readAsArrayBuffer(file); }, { once: true }); input.click(); }); const readFileAsArrayBuffer = (file) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(reader.error || new Error('读取失败')); reader.readAsArrayBuffer(file); }); const isFontFile = (file) => { const name = String(file?.name || '').toLowerCase(); const type = String(file?.type || '').toLowerCase(); return name.endsWith('.ttf') || name.endsWith('.otf') || type.includes('font') || type.includes('opentype') || type.includes('truetype'); }; const PanelUI = (() => { let host, shadow; let els = {}; let isOpen = false; let dragDepth = 0; let previewUrlMap = new Map(); const icons = { riveLogo: ``, x: ``, upload: ``, trash: ``, broom: ``, refresh: ` `, check: ``, play: ``, database: ``, }; const css = ` :host{all:initial} *{box-sizing:border-box} :host{ --blue:rgba(59,130,246,.95); --violet:rgba(139,92,246,.95); --green:rgba(34,197,94,.92); --red:rgba(239,68,68,.95); font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji"; color:rgba(255,255,255,.92); writing-mode: horizontal-tb; text-orientation: mixed; } .drawer{ position:fixed; right:16px; top:16px; width:min(560px, calc(100vw - 32px)); max-height:calc(100vh - 32px); border-radius:22px; background: radial-gradient(1000px 600px at 10% 15%, rgba(59,130,246,.13), transparent 62%), radial-gradient(900px 560px at 90% 20%, rgba(139,92,246,.12), transparent 64%), linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03)); border:1px solid rgba(255,255,255,.10); box-shadow: 0 22px 70px rgba(0,0,0,.62), 0 0 0 1px rgba(255,255,255,.06) inset; backdrop-filter: blur(18px); display:none; overflow:hidden; z-index:2147483646; writing-mode: horizontal-tb; text-orientation: mixed; } .drawer *{ writing-mode: horizontal-tb; text-orientation: mixed; } .drawer.show{display:flex; flex-direction:column} .head{ padding:16px 16px 12px; display:flex; align-items:flex-start; justify-content:space-between; gap:12px; border-bottom:1px solid rgba(255,255,255,.08); } .titleRow{display:flex; align-items:center; gap:10px; min-width:0} .logo{ width:40px; height:40px; border-radius:14px; border:1px solid rgba(255,255,255,.10); background:linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03)); box-shadow:0 16px 44px rgba(0,0,0,.28), 0 0 0 1px rgba(255,255,255,.05) inset; position:relative; overflow:hidden; flex:0 0 auto; display:flex; align-items:center; justify-content:center; } .logo:before{ content:""; position:absolute; inset:1px; border-radius:13px; background: radial-gradient(circle at 30% 20%, rgba(255,255,255,.22), transparent 58%), rgba(0,0,0,.10); } .logo svg{ position:relative; width:22px; height:22px; color:rgba(255,255,255,.92); filter:drop-shadow(0 10px 18px rgba(0,0,0,.22)); } .hgroup{min-width:0} .hgroup h2{margin:0; font-size:14px; font-weight:850; letter-spacing:.25px} .hgroup p{margin:4px 0 0; font-size:12px; color:rgba(255,255,255,.62); line-height:1.45} .iconBtn{ appearance:none; border:1px solid rgba(255,255,255,.10); background:rgba(255,255,255,.06); color:rgba(255,255,255,.92); width:38px; height:38px; border-radius:14px; display:inline-flex; align-items:center; justify-content:center; cursor:pointer; transition:transform .08s ease, background .15s ease, border-color .15s ease, filter .15s ease; flex:0 0 auto; } .iconBtn:hover{background:rgba(255,255,255,.09); border-color:rgba(255,255,255,.14); filter:brightness(1.06)} .iconBtn:active{transform:scale(.98)} .iconBtn svg{width:18px; height:18px} /* 其余 CSS 与上一版一致(保持完整功能) */ `; const tailCss = (() => { return ` .body{ padding:14px 16px 16px; overflow:auto; display:flex; flex-direction:column; gap:12px; scrollbar-width:thin; scrollbar-color: rgba(255,255,255,.22) transparent; scrollbar-gutter: stable both-edges; } .body::-webkit-scrollbar{ width:10px; height:10px; } .body::-webkit-scrollbar-track{ background:transparent; } .body::-webkit-scrollbar-thumb{ background: rgba(255,255,255,.16); border-radius:999px; border:2px solid transparent; background-clip: content-box; } .body::-webkit-scrollbar-thumb:hover{ background: rgba(255,255,255,.24); background-clip: content-box; } .sectionTitle{ display:flex; align-items:center; justify-content:space-between; margin-top:2px; } .sectionTitle .l{ display:flex; align-items:center; gap:10px; font-size:12px; font-weight:800; letter-spacing:.2px; color:rgba(255,255,255,.78); white-space:nowrap; word-break:keep-all; } .secTitleWrap{display:inline-flex;align-items:center;gap:8px;white-space:nowrap} .secTitleText{white-space:nowrap;word-break:keep-all} .secTitleWrap svg{width:18px;height:18px;flex:0 0 auto} .list{display:flex; flex-direction:column; gap:10px} .card{ border:1px solid rgba(255,255,255,.10); background:rgba(255,255,255,.06); border-radius:16px; padding:12px; display:flex; align-items:flex-start; justify-content:space-between; gap:10px; transition:transform .10s ease, border-color .15s ease, background .15s ease; min-width:0; } .card:hover{border-color:rgba(255,255,255,.14); background:rgba(255,255,255,.07)} .card.used{ border-color:rgba(34,197,94,.24); background:rgba(34,197,94,.08); } .left{min-width:0; flex:1 1 auto} .nameRow{display:flex; align-items:center; gap:8px; min-width:0; flex-wrap:wrap;} .name{ font-weight:850; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:320px; } .tag{ font-size:11px; padding:2px 8px; border-radius:999px; border:1px solid rgba(255,255,255,.12); background:rgba(255,255,255,.06); color:rgba(255,255,255,.76); white-space:nowrap; word-break:keep-all; overflow-wrap:normal; writing-mode:horizontal-tb; text-orientation:mixed; flex:0 0 auto; max-width:100%; line-height:1.15; } .tag.used{border-color:rgba(34,197,94,.28); background:rgba(34,197,94,.12)} .meta{ margin-top:6px; display:flex; gap:10px; flex-wrap:wrap; font-size:12px; color:rgba(255,255,255,.62); } .meta .cached{color:rgba(59,130,246,.92)} .meta .stale{color:rgba(245,158,11,.92)} .actions{ display:flex; flex-direction:column; gap:8px; align-items:stretch; justify-content:flex-start; flex:0 0 auto; } .btn{ appearance:none; border:1px solid rgba(255,255,255,.12); background:rgba(255,255,255,.06); color:rgba(255,255,255,.90); border-radius:14px; padding:8px 10px; font-size:12px; cursor:pointer; display:inline-flex; align-items:center; justify-content:center; gap:8px; transition:transform .08s ease, background .15s ease, border-color .15s ease, filter .15s ease; width:92px; white-space:nowrap; user-select:none; } .btn:hover{background:rgba(255,255,255,.10); border-color:rgba(255,255,255,.16); filter:brightness(1.05)} .btn:active{transform:scale(.98)} .btn:disabled{opacity:.5; cursor:not-allowed} .btn svg{width:16px; height:16px} .btn.primary{border-color:rgba(59,130,246,.35); background:rgba(59,130,246,.14)} .btn.danger{border-color:rgba(239,68,68,.32); background:rgba(239,68,68,.12)} .btn.ghost{border-color:rgba(255,255,255,.10); background:rgba(255,255,255,.04)} .row2{display:flex; gap:10px} .row2 .btn{flex:1; justify-content:center; width:auto} .dropHint{ border:1px dashed rgba(255,255,255,.18); background:rgba(255,255,255,.04); border-radius:18px; padding:14px; color:rgba(255,255,255,.70); font-size:12px; display:flex; align-items:center; justify-content:space-between; gap:12px; } .dropHint strong{color:rgba(255,255,255,.90)} .small{font-size:12px; color:rgba(255,255,255,.56); line-height:1.5} .dropOverlay{ position:absolute; inset:0; display:none; background:rgba(0,0,0,.28); backdrop-filter: blur(8px); } .dropOverlay.show{display:flex} .dropOverlay .center{ margin:auto; width:min(520px, calc(100vw - 56px)); border-radius:22px; padding:18px; border:1px solid rgba(255,255,255,.14); background:linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.05)); box-shadow:0 30px 90px rgba(0,0,0,.50); text-align:center; } .dropOverlay .big{ display:flex; align-items:center; justify-content:center; gap:10px; font-weight:900; letter-spacing:.2px; } .dropOverlay .big svg{width:22px; height:22px} .dropOverlay .sub{ margin-top:8px; font-size:12px; color:rgba(255,255,255,.65); } .preview{ margin-top:10px; padding:10px 12px; border-radius:14px; border:1px solid rgba(255,255,255,.10); background:rgba(0,0,0,.14); } .preview .cn{ font-size:14px; font-weight:700; line-height:1.35; color:rgba(255,255,255,.92); } .preview .en{ margin-top:6px; font-size:12px; line-height:1.35; color:rgba(255,255,255,.66); } `; })(); const toPreviewFamily = (id) => `RiveCNPreview_${String(id).replace(/[^a-z0-9_]/gi, '_')}`; const escUrl = (u) => String(u || '').replace(/'/g, '%27'); const revokePreviewUrl = (id) => { const v = previewUrlMap.get(id); if (!v) return; try { if (v?.url) URL.revokeObjectURL(v.url); } catch { } previewUrlMap.delete(id); }; const disposeAllPreviewUrls = () => { for (const k of previewUrlMap.keys()) revokePreviewUrl(k); previewUrlMap.clear(); }; const ensurePreviewBlobUrl = async (id, meta, preset, cacheInfo) => { const source = meta?.source || (preset ? 'remote' : 'local'); if (source === 'remote' && !(cacheInfo?.cached || cacheInfo?.stale)) { revokePreviewUrl(id); return null; } if (previewUrlMap.has(id)) return previewUrlMap.get(id); const raw = await DB.get(K.buf(id)); const buf = raw ? unwrapCache(raw) : null; if (!buf?.byteLength || buf.byteLength <= 10_000) { revokePreviewUrl(id); return null; } let ext = guessFontExt(meta, preset) || ''; if (!ext) { const n = String(meta?.name || '').toLowerCase(); ext = n.endsWith('.otf') ? 'otf' : 'ttf'; } const blobType = fontContentType(ext); const url = URL.createObjectURL(new Blob([buf], { type: blobType })); const obj = { url, ext }; previewUrlMap.set(id, obj); return obj; }; const ensure = () => { if (host) return; host = document.createElement('div'); shadow = host.attachShadow({ mode: 'closed' }); const style = document.createElement('style'); style.textContent = css + tailCss; const drawer = document.createElement('div'); drawer.className = 'drawer'; drawer.setAttribute('role', 'dialog'); drawer.setAttribute('aria-modal', 'false'); drawer.innerHTML = `

Rive 汉化设置面板

本地字体支持拖拽添加,切换后刷新生效。

${icons.database}字体列表
拖拽字体文件到面板
支持 .ttf / .otf
提示:网络字体缓存按 ${CONFIG.CACHE_TTL_DAYS} 天 TTL 自动更新;“清空缓存”会强制下次重新下载。
`; shadow.append(style, drawer); document.documentElement.append(host); els.drawer = drawer; els.list = drawer.querySelector('[data-el="list"]'); els.dropOverlay = drawer.querySelector('[data-el="dropOverlay"]'); drawer.addEventListener('click', async (e) => { const t = e.target?.closest?.('[data-act]'); if (!t) return; const act = t.getAttribute('data-act'); if (act === 'close') hide(); if (act === 'add') await handleAddByPicker(); if (act === 'clearFont') await handleClearFontCache(); if (act === 'clearAll') await handleClearAllCache(); }); els.list.addEventListener('click', async (e) => { const btn = e.target?.closest?.('button[data-font-act]'); if (!btn) return; const act = btn.getAttribute('data-font-act'); const id = btn.getAttribute('data-font-id'); if (!act || !id) return; if (act === 'use') { LoaderUI.show(); try { LoaderUI.setStep('切换'); LoaderUI.setStatus('写入当前字体…', ''); LoaderUI.setProgress(38); await FontStore.setCurrentFontId(id); LoaderUI.setProgress(100); LoaderUI.setStep('切换', true); } finally { await LoaderUI.hide().catch(() => { }); } location.reload(); } if (act === 'del') { if (!confirm('确定删除该本地字体吗?')) return; LoaderUI.show(); try { LoaderUI.setStep('删除'); LoaderUI.setStatus('删除字体数据…', ''); LoaderUI.setProgress(42); await FontStore.deleteFont(id); LoaderUI.setProgress(100); LoaderUI.setStep('删除', true); } finally { await LoaderUI.hide().catch(() => { }); } await refresh().catch(() => { }); } if (act === 'clear') { LoaderUI.show(); try { LoaderUI.setStep('清空'); LoaderUI.setStatus('清空网络字体缓存…', getPreset(id)?.name || ''); LoaderUI.setProgress(46); await FontStore.clearRemoteCache(id); LoaderUI.setProgress(100); LoaderUI.setStep('清空', true); } finally { await LoaderUI.hide().catch(() => { }); } await refresh().catch(() => { }); } }); const onKeyDown = (e) => { if (!isOpen) return; if (e.key === 'Escape') hide(); }; els._bindEsc = () => window.addEventListener('keydown', onKeyDown, true); els._unbindEsc = () => window.removeEventListener('keydown', onKeyDown, true); }; const show = async () => { ensure(); isOpen = true; els.drawer.classList.add('show'); dragDepth = 0; els._bindEsc?.(); bindDrag(); await refresh().catch(() => { }); }; const hide = () => { if (!els.drawer) return; isOpen = false; els.drawer.classList.remove('show'); dragDepth = 0; if (els.dropOverlay) els.dropOverlay.classList.remove('show'); els._unbindEsc?.(); unbindDrag(); disposeAllPreviewUrls(); try { ensureGlobalPreviewStyle().textContent = ''; } catch { } }; const bindDrag = () => { if (els._dragBound) return; const onDragEnter = (e) => { if (!isOpen) return; dragDepth++; if (els.dropOverlay) els.dropOverlay.classList.add('show'); e.preventDefault(); e.stopPropagation(); }; const onDragOver = (e) => { if (!isOpen) return; e.preventDefault(); e.stopPropagation(); }; const onDragLeave = (e) => { if (!isOpen) return; dragDepth = Math.max(0, dragDepth - 1); if (dragDepth === 0 && els.dropOverlay) els.dropOverlay.classList.remove('show'); e.preventDefault(); e.stopPropagation(); }; const onDrop = async (e) => { if (!isOpen) return; e.preventDefault(); e.stopPropagation(); dragDepth = 0; if (els.dropOverlay) els.dropOverlay.classList.remove('show'); const files = Array.from(e.dataTransfer?.files || []); const file = files.find(isFontFile); if (!file) return alert('未检测到可用字体文件(仅支持 .ttf/.otf)。'); await handleAddFile(file); }; window.addEventListener('dragenter', onDragEnter, true); window.addEventListener('dragover', onDragOver, true); window.addEventListener('dragleave', onDragLeave, true); window.addEventListener('drop', onDrop, true); els._dragBound = { onDragEnter, onDragOver, onDragLeave, onDrop }; }; const unbindDrag = () => { const d = els._dragBound; if (!d) return; window.removeEventListener('dragenter', d.onDragEnter, true); window.removeEventListener('dragover', d.onDragOver, true); window.removeEventListener('dragleave', d.onDragLeave, true); window.removeEventListener('drop', d.onDrop, true); els._dragBound = null; }; const renderFontCard = ({ id, meta, currentId, cache, previewFamily, previewReady }) => { const preset = getPreset(id); const name = meta?.name || preset?.name || id; const source = meta?.source || (preset ? 'remote' : 'local'); const used = id === currentId; const tag = used ? `使用中` : `${source === 'local' ? '本地' : '网络'}`; const hasAnyCache = !!(cache?.cached || cache?.stale); const cacheLabel = cache?.cached ? '已缓存' : cache?.stale ? '已缓存(过期)' : '未缓存'; const cacheCls = cache?.cached ? 'cached' : cache?.stale ? 'stale' : ''; const metaLine = source === 'remote' ? ` 体积:${hasAnyCache ? formatBytes(cache.size) : '—'} ${cacheLabel} ${hasAnyCache ? `缓存:${formatTime(cache.cachedAt || 0)}` : ''} ` : ` 添加:${formatTime(meta?.addedAt || 0)} 体积:${formatBytes(meta?.size || 0)} `; const useBtn = ` `; const rightBtn = source === 'remote' ? ` ` : ` `; const preview = previewReady ? `
在一个协作式编辑器中设计、编码和动画制作。
Design, code, and animate in one collaborative editor .
` : ''; return `
${name}
${tag}
${metaLine}
${preview}
${useBtn}${rightBtn}
`; }; const refresh = async () => { if (!els.list) return; const [ids, currentId] = await Promise.all([FontStore.getFontList(), FontStore.getCurrentFontId()]); const cssParts = []; const parts = []; const keepIds = new Set(); for (const id of ids) { const [meta, cache] = await Promise.all([FontStore.getMeta(id), FontStore.getCacheInfo(id)]); const preset = getPreset(id); const previewFamily = toPreviewFamily(id); const previewObj = await ensurePreviewBlobUrl(id, meta, preset, cache); const previewReady = !!previewObj?.url; if (previewReady) { keepIds.add(id); const fmt = previewObj.ext === 'otf' ? 'opentype' : 'truetype'; cssParts.push( `@font-face{font-family:'${previewFamily}';src:url('${escUrl(previewObj.url)}') format('${fmt}');font-display:swap;}` ); } parts.push(renderFontCard({ id, meta, currentId, cache, previewFamily, previewReady })); } for (const k of Array.from(previewUrlMap.keys())) { if (!keepIds.has(k)) { const v = previewUrlMap.get(k); try { if (v?.url) URL.revokeObjectURL(v.url); } catch { } previewUrlMap.delete(k); } } try { ensureGlobalPreviewStyle().textContent = cssParts.join('\n'); } catch { } els.list.innerHTML = parts.join(''); }; const handleAddByPicker = async () => { const picked = await pickFile('.ttf,.otf,font/ttf,font/otf'); if (!picked) return; if (picked.error) return alert(`读取失败:${picked.error?.message || picked.error}`); const { file, buf } = picked; if (!buf?.byteLength || buf.byteLength <= 10_000) return alert('字体文件无效或过小(建议使用 ttf/otf)。'); await handleAddBuffer(file, buf); }; const handleAddFile = async (file) => { try { LoaderUI.show(); LoaderUI.setStep('读取'); LoaderUI.setStatus('读取字体文件…', file.name); LoaderUI.setProgress(20); const buf = await readFileAsArrayBuffer(file); if (!buf?.byteLength || buf.byteLength <= 10_000) throw new Error('字体文件无效或过小'); LoaderUI.setStep('读取', true); LoaderUI.setProgress(42); await handleAddBuffer(file, buf, { keepLoader: true }); } catch (e) { LoaderUI.error(e?.message || '添加字体失败'); } finally { await LoaderUI.hide().catch(() => { }); } }; const handleAddBuffer = async (file, buf, { keepLoader = false } = {}) => { if (!keepLoader) LoaderUI.show(); try { LoaderUI.setStep('保存'); LoaderUI.setStatus('保存到 IndexedDB…', file.name); LoaderUI.setProgress(52); const id = await FontStore.addLocalFont(file, buf); LoaderUI.setStep('保存', true); LoaderUI.setStep('启用'); LoaderUI.setStatus('设置为当前字体…', ''); LoaderUI.setProgress(78); await FontStore.setCurrentFontId(id); LoaderUI.setStep('启用', true); LoaderUI.setProgress(100); } finally { if (!keepLoader) await LoaderUI.hide().catch(() => { }); } alert(`已添加并启用本地字体:${file.name}\n将刷新页面生效。`); location.reload(); }; const handleClearFontCache = async () => { if (!confirm('清除字体缓存:将清除所有网络预设字体缓存(不删除本地字体)。继续?')) return; LoaderUI.show(); try { LoaderUI.setStep('清除'); LoaderUI.setStatus('清理网络字体缓存…', ''); LoaderUI.setProgress(48); await FontStore.clearFontCacheOnly(); LoaderUI.setProgress(100); LoaderUI.setStep('清除', true); } finally { await LoaderUI.hide().catch(() => { }); } alert('网络字体缓存已清除。'); await refresh().catch(() => { }); }; const handleClearAllCache = async () => { if (!confirm('清除全部缓存:将删除所有字体(含本地字体)与设置。继续?')) return; LoaderUI.show(); try { LoaderUI.setStep('清空'); LoaderUI.setStatus('清空 IndexedDB…', ''); LoaderUI.setProgress(52); await DB.clear(); LoaderUI.setProgress(100); LoaderUI.setStep('清空', true); } finally { await LoaderUI.hide().catch(() => { }); } alert('全部缓存已清除,刷新页面生效。'); }; return { show }; })(); const isFontManifestUrl = (u) => typeof u === 'string' && u.includes('FontManifest.json'); const isInjectedFontUrl = (u) => typeof u === 'string' && u.includes(CONFIG.INJECT_FONT_ASSET); const patchFontManifestJson = (manifest) => { if (!Array.isArray(manifest)) return manifest; const family = CONFIG.CN_FONT_FAMILY; const asset = CONFIG.INJECT_FONT_ASSET; let entry = manifest.find((x) => x && x.family === family); if (!entry) { manifest.push({ family, fonts: [{ asset }] }); return manifest; } if (!Array.isArray(entry.fonts)) entry.fonts = []; if (!entry.fonts.some((f) => f && f.asset === asset)) entry.fonts.unshift({ asset }); return manifest; }; const originalFetch = unsafeWindow.fetch?.bind(unsafeWindow); if (originalFetch) { unsafeWindow.fetch = new Proxy(originalFetch, { apply(target, thisArg, args) { try { const input = args?.[0]; const urlStr = input instanceof Request ? input.url : input instanceof URL ? input.toString() : typeof input === 'string' ? input : String(input); if (isFontManifestUrl(urlStr)) { return Reflect.apply(target, thisArg, args).then(async (resp) => { try { if (!resp || resp.type === 'opaque') return resp; const ct = resp.headers?.get?.('content-type') || ''; if (!ct.includes('application/json')) return resp; const clone = resp.clone(); const json = await clone.json(); const patched = patchFontManifestJson(json); const headers = new Headers(resp.headers); headers.set('content-type', 'application/json; charset=utf-8'); headers.delete('content-length'); return new Response(JSON.stringify(patched), { status: resp.status, statusText: resp.statusText, headers, }); } catch { return resp; } }); } if (isInjectedFontUrl(urlStr)) { return (async () => { try { await DB.open().catch(() => { }); await FontStore.migrateLegacyIfAny().catch(() => { }); await FontStore.ensurePresetsAndList().catch(() => { }); } catch { } const currentId = await FontStore.getCurrentFontId().catch(() => DEFAULT_FONT_ID); let buf; try { buf = await FontStore.getOrFetchFontBytes(currentId, { silent: false }); } catch (e) { LoaderUI.error(e?.message || '字体加载失败'); buf = await FontStore.getOrFetchFontBytes(DEFAULT_FONT_ID, { silent: true }); } finally { await LoaderUI.hide().catch(() => { }); } const ct = await FontStore.getFontContentType(currentId).catch(() => 'application/octet-stream'); return new Response(new Uint8Array(buf), { status: 200, statusText: 'OK', headers: { 'Content-Type': ct }, }); })(); } return Reflect.apply(target, thisArg, args); } catch { return Reflect.apply(target, thisArg, args); } }, }); } const patchCanvasKit = (ck) => { try { if (!ck || ck.__rcn_patched) return; ck.__rcn_patched = true; const PB = ck.ParagraphBuilder; if (!PB?.prototype) return; if (typeof PB.prototype.addText === 'function' && !PB.prototype.__rcn_addText) { const origAdd = PB.prototype.addText; PB.prototype.addText = function (text) { try { if (typeof text === 'string') return origAdd.call(this, translateText(text)); } catch { } return origAdd.call(this, text); }; PB.prototype.__rcn_addText = true; } if (typeof PB.prototype.pushStyle === 'function' && !PB.prototype.__rcn_pushStyle) { const origPush = PB.prototype.pushStyle; PB.prototype.pushStyle = function (style) { try { if (style && typeof style === 'object') { const fam = style.fontFamilies; if (Array.isArray(fam)) { if (fam[0] !== CONFIG.CN_FONT_FAMILY) { style.fontFamilies = [CONFIG.CN_FONT_FAMILY, ...fam.filter((x) => x && x !== CONFIG.CN_FONT_FAMILY)]; } } else { style.fontFamilies = [CONFIG.CN_FONT_FAMILY]; } } } catch { } return origPush.call(this, style); }; PB.prototype.__rcn_pushStyle = true; } } catch { } }; const hookGlobal = (name, cb) => { try { const desc = Object.getOwnPropertyDescriptor(unsafeWindow, name); const canDefine = !desc || desc.configurable; if (!canDefine) { if (unsafeWindow[name]) cb(unsafeWindow[name]); return; } let _v = unsafeWindow[name]; Object.defineProperty(unsafeWindow, name, { configurable: true, enumerable: true, get() { return _v; }, set(v) { _v = v; try { cb(v); } catch { } }, }); if (_v) cb(_v); } catch { } }; const startHook = () => { hookGlobal('flutterCanvasKit', patchCanvasKit); hookGlobal('CanvasKit', patchCanvasKit); hookGlobal('CanvasKitInit', (v) => { if (typeof v !== 'function' || v.__rcn_wrapped) return; const wrapped = (...args) => Promise.resolve(v(...args)).then((ck) => { patchCanvasKit(ck); return ck; }); wrapped.__rcn_wrapped = true; unsafeWindow.CanvasKitInit = wrapped; }); const t0 = nowMs(); const timer = setInterval(() => { const ck1 = unsafeWindow.flutterCanvasKit; const ck2 = unsafeWindow.CanvasKit; if (ck1) patchCanvasKit(ck1); if (ck2) patchCanvasKit(ck2); if ((ck1 && ck1.__rcn_patched) || (ck2 && ck2.__rcn_patched)) { clearInterval(timer); return; } if (nowMs() - t0 > CONFIG.HOOK_MAX_MS) clearInterval(timer); }, 180); }; startHook(); (async () => { try { await DB.open().catch(() => { }); await FontStore.migrateLegacyIfAny().catch(() => { }); await FontStore.ensurePresetsAndList().catch(() => { }); } catch { } })(); if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('⚙️ 设置面板', () => PanelUI.show()); } })();