Compare commits
12 Commits
01b3bea1c6
...
developer
| Author | SHA1 | Date | |
|---|---|---|---|
| f7a6c50c0b | |||
| 7323364b9c | |||
| 86d399c706 | |||
| 339fbe357f | |||
| d224a5aaa9 | |||
| d1cd33de07 | |||
| 1ab485dffe | |||
| 76ce9a01b1 | |||
| 562662c1bd | |||
| 7e4236e2f3 | |||
| 93ee801f27 | |||
| f25506ec5b |
18
AGENTS.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Rtty 项目指引(Codex 会话自动加载)
|
||||||
|
|
||||||
|
Rtty 是一个仿 dinotty 的分布式虚拟终端系统,**只保留终端同步能力**:Rust 服务端
|
||||||
|
(Axum + portable-pty + alacritty_terminal 真相源)通过 WebSocket 同步到
|
||||||
|
Flutter 桌面端(原始 ANSI 渲染)与移动端(语义快照)。
|
||||||
|
|
||||||
|
**对接前必读**:
|
||||||
|
- [KNOWLEDGE_GRAPH.md](./KNOWLEDGE_GRAPH.md) —— 知识图谱:架构、协议、数据流、不变量、踩坑记录
|
||||||
|
- [PROJECT_SUMMARY.md](./PROJECT_SUMMARY.md) —— 项目总览与目录导航
|
||||||
|
|
||||||
|
**关键约束**:
|
||||||
|
- 服务端 alacritty 引擎是唯一真相源;Raw/快照/重绘帧都在引擎锁内广播(严格有序)
|
||||||
|
- 协议见 KNOWLEDGE_GRAPH.md §5;移动端颜色分段 `segments` 是扩展字段
|
||||||
|
- 会话空闲 60s 回收;`RTTY_TOKEN` 可选鉴权;桌面端输入走二进制帧
|
||||||
|
- 本机 flutter/git 命令需要提权(沙盒外)执行;`PUB_CACHE=C:\Users\NianJiu\AppData\Local\Pub\Cache`
|
||||||
|
|
||||||
|
**⚠️ 最重要的一条**:服务端不能在文件写入受限的环境(沙盒)里运行,否则子进程
|
||||||
|
(如 claude)无法写入用户状态文件,表现为 TUI 应用确认后卡死/退出。
|
||||||
20
CLAUDE.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Rtty 项目指引(Claude Code 会话自动加载)
|
||||||
|
|
||||||
|
Rtty 是一个仿 dinotty 的分布式虚拟终端系统,**只保留终端同步能力**:Rust 服务端
|
||||||
|
(Axum + portable-pty + alacritty_terminal 真相源)通过 WebSocket 同步到
|
||||||
|
Flutter 桌面端(原始 ANSI 渲染)与移动端(语义快照)。
|
||||||
|
|
||||||
|
**对接前必读**:
|
||||||
|
- [KNOWLEDGE_GRAPH.md](./KNOWLEDGE_GRAPH.md) —— 知识图谱:架构、协议、数据流、不变量、踩坑记录
|
||||||
|
- [PROJECT_SUMMARY.md](./PROJECT_SUMMARY.md) —— 项目总览与目录导航
|
||||||
|
|
||||||
|
**关键约束**:
|
||||||
|
- 服务端 alacritty 引擎是唯一真相源;Raw/快照/重绘帧都在引擎锁内广播(严格有序)
|
||||||
|
- 协议见 KNOWLEDGE_GRAPH.md §5;移动端颜色分段 `segments` 是扩展字段
|
||||||
|
- 会话空闲 60s 回收;`RTTY_TOKEN` 可选鉴权;桌面端输入走二进制帧
|
||||||
|
|
||||||
|
**运行**:`cargo run`(监听 ws://0.0.0.0:8080);桌面端 `cd desktop && flutter run -d windows`;
|
||||||
|
移动端 `cd mobile && flutter build apk --debug`。
|
||||||
|
|
||||||
|
**⚠️ 最重要的一条**:服务端不能在文件写入受限的环境(沙盒)里运行,否则子进程
|
||||||
|
(如 claude)无法写入用户状态文件,表现为 TUI 应用确认后卡死/退出。
|
||||||
@@ -22,10 +22,11 @@ tracing = "0.1"
|
|||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
dashmap = "6.0" # 高并发线程安全的 SessionMap
|
dashmap = "6.0" # 高并发线程安全的 SessionMap
|
||||||
|
futures-util = "0.3"
|
||||||
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
strip = true
|
strip = true
|
||||||
|
|||||||
218
KNOWLEDGE_GRAPH.md
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
# Rtty 知识图谱(Knowledge Graph)
|
||||||
|
|
||||||
|
> 面向新会话的快速对接文档。本文件记录项目的**实体、关系、流程、协议与约束**;
|
||||||
|
> 项目总览与目录导航见 [PROJECT_SUMMARY.md](./PROJECT_SUMMARY.md)。
|
||||||
|
> 新会话请先读本文件 + PROJECT_SUMMARY.md,即可直接对接代码库。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 项目身份
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| 名称 | Rtty(rtty-server) |
|
||||||
|
| 定位 | 仿 [dinotty](https://github.com/xichan96/dinotty) 的分布式虚拟终端系统,**只保留「终端同步」核心能力** |
|
||||||
|
| 技术栈 | 后端 Rust(Axum + portable-pty + alacritty_terminal);前端 Flutter(桌面 Windows + 移动 Android) |
|
||||||
|
| 仓库 | `https://gitea.ngmao.com/CNWei/Rtty.git`,默认分支 `developer` |
|
||||||
|
| 会话 ID | 形如 `session-0`;也接受客户端自定义 ID(按 ID 原子绑定) |
|
||||||
|
|
||||||
|
## 2. 架构总览
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph Server["rtty-server (Rust)"]
|
||||||
|
PTY["portable-pty<br/>Shell 子进程"]
|
||||||
|
ENGINE["alacritty_terminal Term<br/>真相源:网格/历史/ANSI 解析"]
|
||||||
|
WS["Axum WebSocket<br/>会话表 + 广播"]
|
||||||
|
PTY -->|原始字节流| ENGINE -->|快照/重绘| WS
|
||||||
|
end
|
||||||
|
subgraph Desktop["desktop (Flutter Windows)"]
|
||||||
|
DB["RttyPtyBackend<br/>WebSocket 桥接"]
|
||||||
|
DA["flutter_alacritty 引擎<br/>渲染原始 ANSI"]
|
||||||
|
end
|
||||||
|
subgraph Mobile["mobile (Flutter Android)"]
|
||||||
|
MC["RttyMobileClient<br/>语义快照消费"]
|
||||||
|
MU["快照列表渲染<br/>颜色/光标/Keybar"]
|
||||||
|
end
|
||||||
|
WS -->|"二进制原始 ANSI<br/>client=desktop"| DB --> DA
|
||||||
|
WS -->|"JSON 语义快照<br/>client=mobile"| MC --> MU
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 核心实体与关系
|
||||||
|
|
||||||
|
### 3.1 服务端(`src/`)
|
||||||
|
|
||||||
|
| 实体 | 文件 | 职责 |
|
||||||
|
|---|---|---|
|
||||||
|
| `AppState` | `main.rs` | 全局状态:`sessions: DashMap<id, Arc<Session>>` + 配置 |
|
||||||
|
| `ServerConfig` | `config.rs` | 环境变量配置:`RTTY_HOST/PORT/TOKEN/SHELL/COLS/ROWS/MAX_SCROLLBACK/MAX_CLIENTS/IDLE_TIMEOUT` |
|
||||||
|
| `PtySession` | `terminal/pty.rs` | portable-pty 封装:reader 独占交出、writer 多客户端共享(`Arc<Mutex<Box<dyn Write>>>`)、resize/kill/try_wait |
|
||||||
|
| `TerminalEngine` | `terminal/engine.rs` | alacritty `Term` 真相源;`feed()` 逐字节喂解析器;`snapshot()` 生成移动端快照;`redraw_ansi()` 生成全屏 ANSI 重绘;`SessionListener` 把 `Event::PtyWrite`(终端应答)写回 PTY |
|
||||||
|
| `Session` | `ws/handler.rs` | 一个会话 = PTY + 引擎 + broadcast 输出通道 + 控制权 + 客户端计数(`clients`/`mobile_clients`) |
|
||||||
|
| `ClientKind` | `ws/handler.rs` | 按 `?client=` 参数区分:`desktop`(原始 ANSI 二进制) / `mobile`(JSON 快照),默认 desktop |
|
||||||
|
|
||||||
|
### 3.2 桌面端(`desktop/lib/`)
|
||||||
|
|
||||||
|
| 实体 | 文件 | 职责 |
|
||||||
|
|---|---|---|
|
||||||
|
| `RttyPtyBackend` | `src/rtty_pty_backend.dart` | 实现 flutter_alacritty 的 `PtyBackend`:二进制输入、指数退避自动重连、30s 心跳、token、`onReady(id, cols, rows, resumed)` |
|
||||||
|
| `TerminalScreen` | `src/terminal_screen.dart` | TerminalEngine + TerminalView 双向接线、resize 防抖(80ms) |
|
||||||
|
| `ConnectionBar` | `src/connection_bar.dart` | HOST/PORT/TOKEN/SESSION 输入 + 状态灯 + `SID · RESUMED/NEW` 徽章 |
|
||||||
|
| `RttyTerminalConfig` | `src/terminal_config.dart` | 配色/字体映射到 `TerminalConfig` |
|
||||||
|
|
||||||
|
### 3.3 移动端(`mobile/lib/`)
|
||||||
|
|
||||||
|
| 实体 | 文件 | 职责 |
|
||||||
|
|---|---|---|
|
||||||
|
| `RttyMobileClient` | `src/rtty_mobile_client.dart` | `?client=mobile` 连接、快照/分段解析、自动重连、心跳、`sendInput/sendResize/claimControl` |
|
||||||
|
| `TerminalPage` | `main.dart` | 连接面板 + 快照列表渲染(颜色/光标块/光标可见滚动)+ 输入框 + Keybar(ESC/TAB/CTRL+C/CTRL+L/方向键/ENTER) |
|
||||||
|
|
||||||
|
## 4. 数据流
|
||||||
|
|
||||||
|
### 4.1 输出路径(PTY → 客户端)
|
||||||
|
|
||||||
|
```
|
||||||
|
Shell 输出
|
||||||
|
→ PtySession.reader(spawn_blocking 独占)
|
||||||
|
→ TerminalEngine.feed()(引擎锁内)
|
||||||
|
→ 引擎锁内 broadcast:
|
||||||
|
Raw(chunk) → desktop 客户端收二进制
|
||||||
|
Snapshot(快照) → 仅当 mobile_clients>0 时生成 → mobile 客户端收 JSON
|
||||||
|
→ desktop 订阅时/每次 resize:redraw_ansi() 注入 broadcast(全屏重绘)
|
||||||
|
```
|
||||||
|
|
||||||
|
**顺序不变量**:Raw / Snapshot / 重绘帧全部在**引擎锁内**发出,保证与实时输出严格有序(桌面重连恢复依赖此不变量)。
|
||||||
|
|
||||||
|
### 4.2 输入路径(客户端 → PTY)
|
||||||
|
|
||||||
|
```
|
||||||
|
desktop: 引擎输出字节 → backend.write() → WebSocket 二进制帧 → 服务端 Message::Binary → pty.write()
|
||||||
|
mobile: 输入框/Keybar → sendInput() → JSON {type:"input",data} → 服务端 → pty.write()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 会话生命周期
|
||||||
|
|
||||||
|
```
|
||||||
|
连接(?session=X)
|
||||||
|
→ 鉴权(RTTY_TOKEN 时校验 ?token=)
|
||||||
|
→ get_or_create_session:按 ID 原子「查-建-插」;已存在 → resumed=true,否则新建 resumed=false
|
||||||
|
→ Ready{id,cols,rows,resumed} →(mobile 发初始快照)→ 订阅广播 →(desktop 注入全屏重绘)
|
||||||
|
→ 持续双向收发
|
||||||
|
断开/退出
|
||||||
|
→ clients-- ;为 0 时启动 idle_timeout(默认 60s)清理
|
||||||
|
→ 兜底:退出监控(100ms 轮询 try_wait,上限 5 分钟)+ 读 EOF(Unix)→ cleanup_session(幂等)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. WebSocket 协议
|
||||||
|
|
||||||
|
端点:`ws://host:port/ws?client=desktop|mobile&session=<id>&token=<token>`
|
||||||
|
|
||||||
|
### 5.1 客户端 → 服务端(JSON 文本帧)
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{"type":"input","data":"ls\r"} // 输入,写入 PTY
|
||||||
|
{"type":"resize","cols":120,"rows":32}
|
||||||
|
{"type":"claim_control"} // 声明控制权(无控制者时授予)
|
||||||
|
{"type":"ping"} // 心跳 → 服务端回 Pong
|
||||||
|
```
|
||||||
|
|
||||||
|
桌面端也可用**二进制帧**直接发原始输入字节(无损)。
|
||||||
|
|
||||||
|
### 5.2 服务端 → 客户端
|
||||||
|
|
||||||
|
| type | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `ready` | `{id, cols, rows, resumed}`;resumed=true 表示恢复既有会话 |
|
||||||
|
| `mobile_snapshot` | `{data: MobileSnapshot}`,仅发给 mobile |
|
||||||
|
| `control_response` | `{granted, holder}` |
|
||||||
|
| `pong` | 心跳应答 |
|
||||||
|
| `error` | `{message}`;鉴权失败等(客户端据此停止自动重连) |
|
||||||
|
| `session_closed` | 会话结束(客户端停止自动重连) |
|
||||||
|
|
||||||
|
### 5.3 MobileSnapshot 结构
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"cursor_x": 0, "cursor_y": 0,
|
||||||
|
"cols": 120, "rows": 32,
|
||||||
|
"display_offset": 0, "scrollback_lines": 0,
|
||||||
|
"lines": ["..."], // 纯文本行(去尾空白)
|
||||||
|
"segments": [[{ // 着色分段,与 lines 一一对应
|
||||||
|
"text": "...", "fg": null, "bg": null,
|
||||||
|
"bold": false, "italic": false, "underline": false
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
颜色编码:`null` = 默认色;`"n:k"` = 命名色 0-15;`"r,g,b"` = 具体 RGB(服务端已把 256 色索引按标准色表转成 RGB)。
|
||||||
|
|
||||||
|
## 6. 关键设计决策与不变量
|
||||||
|
|
||||||
|
1. **服务端 alacritty 引擎是唯一真相源**:断线重连恢复、移动端快照、桌面端重绘全部源于引擎网格。
|
||||||
|
2. **桌面端重连恢复**:订阅时注入 `redraw_ansi()`(`ESC[2J ESC[H` + 逐行 SGR + 光标归位);重连后画面完整,无需客户端重拉。
|
||||||
|
3. **惰性快照**:仅当存在 mobile 订阅者(`mobile_clients>0`)才生成快照,纯桌面场景零额外开销。
|
||||||
|
4. **二进制输入**:桌面端输入走二进制帧,避免 UTF-8 有损转换。
|
||||||
|
5. **控制权强制**:有控制者时非控制者输入被拒;`claim_control` 仅无控制者时授予。
|
||||||
|
6. **会话 ID 绑定**:请求的 ID 不存在时按该 ID 新建(原子 entry),存在则复用(resumed=true),杜绝并发重复创建。
|
||||||
|
7. **最小鉴权**:`RTTY_TOKEN` 全局令牌(可选);未设置时仅适合内网。
|
||||||
|
8. **快照为文本+颜色语义**,不是像素级画面:适合查看与轻量交互,不适合全屏 TUI 深度操作(vim/claude 建议用桌面端)。
|
||||||
|
|
||||||
|
## 7. 构建 / 运行 / 测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 服务端(仓库根)
|
||||||
|
cargo run # 监听 ws://0.0.0.0:8080
|
||||||
|
$env:RTTY_TOKEN="xxx" # 可选:启用鉴权
|
||||||
|
|
||||||
|
# 桌面端
|
||||||
|
cd desktop && flutter run -d windows
|
||||||
|
flutter build windows --release
|
||||||
|
|
||||||
|
# 移动端
|
||||||
|
cd mobile && flutter build apk --debug
|
||||||
|
adb install -r build/app/outputs/flutter-apk/app-debug.apk
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
cargo test # 服务端单测
|
||||||
|
flutter test # 桌面端(服务端离线时集成测试自动跳过)
|
||||||
|
flutter test # 移动端 widget 测试
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 环境与操作要点(重要,踩坑记录)
|
||||||
|
|
||||||
|
1. **⚠️ 服务端必须在无文件写入限制的环境运行**:若在沙盒/受限环境启动服务端,其子进程(claude 等 TUI 程序)将无法写用户状态文件(如 `~/.claude.json`),表现为:claude 信任确认页勾选后卡死/退出、后续画面消失。**修复:在沙盒外启动服务端**(本机当前正确运行方式:提权执行后启动 `target/debug/rtty-server.exe`)。
|
||||||
|
2. **本机 Flutter 环境**:Flutter 3.44.4(`D:\flutter`),`PUB_CACHE=C:\Users\NianJiu\AppData\Local\Pub\Cache`;flutter/git 命令需要提权(沙盒外)执行,因为要写 `D:\flutter\bin\cache` 与 `.git`。
|
||||||
|
3. **adb**:手机 `3060945816001X7`;`adb install -r` 可能触发手机安装确认弹窗,重试一次即可。
|
||||||
|
4. **手机连接地址**:用 PC 局域网 IP(如 `192.168.31.249:8080`),不要用 127.0.0.1。
|
||||||
|
5. **Android 明文 WebSocket**:Manifest 已开 `usesCleartextTraffic` + `INTERNET` 权限。
|
||||||
|
6. **会话空闲清理**:默认 60s 无客户端即回收会话(PTY 被杀);测试/抓快照要保证有客户端在线。
|
||||||
|
|
||||||
|
## 9. 当前状态与路线图
|
||||||
|
|
||||||
|
### 已实现
|
||||||
|
- 服务端 VTE 真相源(alacritty_terminal)、会话持久化 + `resumed` 语义、多端解耦广播
|
||||||
|
- 桌面端全屏重绘恢复 + 自动重连 + 二进制输入 + token + 心跳
|
||||||
|
- 移动端最小验证 App:快照渲染(颜色/光标)、自动重连、Keybar(含 ENTER)、token
|
||||||
|
- 最小鉴权(`RTTY_TOKEN`)、Ping/Pong、并发会话创建竞态修复
|
||||||
|
- **claude 端到端可用**(claude 在服务端 PTY 运行,手机可看可发消息;前提:服务端非沙盒)
|
||||||
|
|
||||||
|
### 已知限制
|
||||||
|
- 移动端是"语义快照视图":文本+颜色,无完整终端渲染器;全屏 TUI(vim/claude 深度操作)体验有限
|
||||||
|
- 移动端无文本换行(Word Wrap)、无 Keybar 自定义、无粘贴支持
|
||||||
|
- 鉴权仅全局令牌,无细粒度权限/会话绑定
|
||||||
|
|
||||||
|
### 规划
|
||||||
|
- 移动端 TUI 透传模式(全屏快照渲染 + 虚拟键盘)
|
||||||
|
- 智能换行 / 卡片化命令交互 / Keybar 自定义
|
||||||
|
- 分布式横向扩展、服务端细粒度鉴权
|
||||||
|
|
||||||
|
## 10. 常见问题速查
|
||||||
|
|
||||||
|
| 现象 | 原因/解法 |
|
||||||
|
|---|---|
|
||||||
|
| 手机连不上 | 用 LAN IP;确认服务端在跑(`Test-NetConnection ip -Port 8080`) |
|
||||||
|
| 手机显示空白 | 会话被空闲回收(60s)→ 重新连接;或确认 `?client=mobile` 已带上 |
|
||||||
|
| claude 勾选确认后画面消失 | **服务端运行在受限环境** → 改为沙盒外启动 |
|
||||||
|
| 桌面端重连后画面空白 | 检查服务端为最新构建(含 `redraw_ansi`);旧二进制无重绘功能 |
|
||||||
|
| `flutter` 命令卡住 | 本机 flutter 需提权运行;设置 `PUB_CACHE` 指向 NianJiu 的 Pub 缓存 |
|
||||||
|
| 输入乱码/被吞 | 桌面端应走二进制帧;`\r` 进 JSON 必须转义(用 `ConvertTo-Json` 而非手拼) |
|
||||||
191
PROJECT_SUMMARY.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# 🚀 Rtty 项目实现总结与目录导航
|
||||||
|
|
||||||
|
> **Rtty** 是一款基于 Rust + Flutter 构建的高性能、跨平台分布式虚拟终端系统。
|
||||||
|
> 服务端(Rust + Alacritty 引擎)在内存中维持"真相源",通过 WebSocket 与各端解耦;
|
||||||
|
> 桌面端(Flutter + `flutter_alacritty`)共享同一 Alacritty 引擎做工业级 ANSI 渲染;
|
||||||
|
> 移动端(Flutter)消费语义化快照做卡片化重排。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、总体架构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph Server["rtty-server (Rust)"]
|
||||||
|
PTY["portable-pty<br/>伪终端 / Shell"]
|
||||||
|
ENGINE["alacritty_terminal<br/>真相源 · ANSI 解析 · 屏幕网格"]
|
||||||
|
WS["Axum WebSocket<br/>session 管理 / 广播"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Desktop["rtty-desktop (Flutter Windows)"]
|
||||||
|
BACKEND["RttyPtyBackend<br/>WebSocket 桥接"]
|
||||||
|
DENGINE["flutter_alacritty<br/>Alacritty 引擎渲染"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Mobile["rtty-mobile (Flutter)"]
|
||||||
|
MCLIENT["RttyMobileClient<br/>语义快照消费"]
|
||||||
|
MUI["卡片化 / 换行 / Keybar"]
|
||||||
|
end
|
||||||
|
|
||||||
|
PTY --> ENGINE --> WS
|
||||||
|
WS -->|"原始 ANSI 二进制帧<br/>client=desktop"| BACKEND --> DENGINE
|
||||||
|
WS -->|"语义化 JSON 快照<br/>client=mobile"| MCLIENT --> MUI
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、目录树状图与功能对应
|
||||||
|
|
||||||
|
### 1. 服务端内核 `src/`(Rust)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.rs # 程序入口:全局 AppState(会话表)、Axum 路由、配置加载
|
||||||
|
├── config.rs # ServerConfig:host/port/shell/尺寸/scrollback/并发限制/空闲超时(环境变量 RTTY_* 可覆盖)
|
||||||
|
├── terminal/ # 终端核心(PTY + Alacritty 引擎)
|
||||||
|
│ ├── mod.rs # terminal 模块声明
|
||||||
|
│ ├── pty.rs # PtySession:portable-pty 伪终端封装(拉起 Shell、读写、resize、kill)
|
||||||
|
│ └── engine.rs # TerminalEngine:alacritty_terminal 封装(ANSI 解析、网格维护、语义快照生成、PtyWrite 回写)
|
||||||
|
└── ws/ # WebSocket 通信层
|
||||||
|
├── mod.rs # ws 模块声明
|
||||||
|
├── protocol.rs # 协议:ClientMessage(input/resize/claim_control/ping)、ServerMessage(ready/snapshot/control/error/session_closed)、MobileSnapshot
|
||||||
|
└── handler.rs # 会话管理:多端解耦广播、并发限制、控制权强制、会话清理(EOF + 进程监控 + 空闲超时)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 功能 | 对应文件 |
|
||||||
|
|------|---------|
|
||||||
|
| 伪终端创建 / Shell 拉起 / 读写 / resize | `src/terminal/pty.rs` |
|
||||||
|
| ANSI 解析 / 屏幕网格 / 滚动历史 / 语义快照 | `src/terminal/engine.rs` |
|
||||||
|
| 断线重连状态恢复(初始快照) | `src/ws/handler.rs` |
|
||||||
|
| 多端解耦(PC 收 ANSI、移动收快照) | `src/ws/handler.rs` |
|
||||||
|
| 控制权强制(非控制者输入被拒) | `src/ws/handler.rs` |
|
||||||
|
| 会话清理(泄漏防护) | `src/ws/handler.rs` |
|
||||||
|
| 配置加载(环境变量) | `src/config.rs` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 桌面端 `desktop/`(Flutter Windows)
|
||||||
|
|
||||||
|
```
|
||||||
|
desktop/
|
||||||
|
├── lib/
|
||||||
|
│ ├── main.dart # 入口:初始化 RustLib(flutter_rust_bridge)后启动应用
|
||||||
|
│ └── src/
|
||||||
|
│ ├── rtty_pty_backend.dart # RttyPtyBackend:实现 flutter_alacritty 的 PtyBackend 接口,桥接 WebSocket
|
||||||
|
│ ├── terminal_config.dart # RttyTerminalConfig:把 Rtty 配色/字体映射为 Alacritty TerminalConfig
|
||||||
|
│ ├── terminal_screen.dart # 主页面:TerminalEngine + TerminalView、连接控制、resize 防抖、双向接线
|
||||||
|
│ ├── connection_bar.dart # 顶部连接控制条:状态灯、host/port/session 输入、会话徽章、连接按钮
|
||||||
|
│ └── theme.dart # 工业终端主题(深炭黑 + 磷光青绿)
|
||||||
|
├── test/ # 单元测试(服务端离线时集成测试自动跳过)
|
||||||
|
│ ├── e2e_ws_test.dart # RttyPtyBackend 连真实服务端接收原始 ANSI 输出
|
||||||
|
│ ├── regression_ws_test.dart# 多端解耦 + 控制权强制 + 会话清理回归
|
||||||
|
│ ├── support.dart # 测试共享工具(服务端探测)
|
||||||
|
│ └── widget_test.dart # Widget 渲染测试
|
||||||
|
├── integration_test/ # 真实 Windows 桌面集成测试
|
||||||
|
│ ├── e2e_test.dart # 连接 + 渲染 + 命令回显闭环
|
||||||
|
│ └── cd_resize_test.dart # cd 跨目录 + resize 内容完整性
|
||||||
|
├── windows/ # Windows 平台 CMake/runner(flutter 生成)
|
||||||
|
├── pubspec.yaml # 依赖:web_socket_channel、flutter_alacritty
|
||||||
|
└── README.md # 桌面端说明
|
||||||
|
```
|
||||||
|
|
||||||
|
| 功能 | 对应文件 |
|
||||||
|
|------|---------|
|
||||||
|
| Alacritty Rust 引擎初始化 | `lib/main.dart`(`await RustLib.init()`) |
|
||||||
|
| WebSocket ↔ PtyBackend 桥接(远程数据源) | `lib/src/rtty_pty_backend.dart` |
|
||||||
|
| 引擎接线(输出喂渲染、输入回传服务端) | `lib/src/terminal_screen.dart` |
|
||||||
|
| 100% 工业级 ANSI 渲染(vim/htop/tmux) | `lib/src/terminal_screen.dart` + `flutter_alacritty` |
|
||||||
|
| resize 防抖(TUI resize 稳定性) | `lib/src/terminal_screen.dart` |
|
||||||
|
| 连接控制条 / 会话管理 | `lib/src/connection_bar.dart` |
|
||||||
|
| 主题 / 配色 | `lib/src/theme.dart`、`lib/src/terminal_config.dart` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 移动端 `mobile/`(Flutter)
|
||||||
|
|
||||||
|
```
|
||||||
|
mobile/
|
||||||
|
├── lib/
|
||||||
|
│ ├── main.dart # 入口
|
||||||
|
│ └── src/
|
||||||
|
│ ├── rtty_mobile_client.dart # RttyMobileClient:消费 mobile_snapshot 语义快照,发送 input/resize/claim_control
|
||||||
|
│ └── theme.dart # 移动端主题(卡片化美学)
|
||||||
|
├── android/ # Android 平台工程(flutter 生成)
|
||||||
|
├── ios/ # iOS 平台工程(flutter 生成)
|
||||||
|
└── pubspec.yaml # 依赖:web_socket_channel
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **当前状态**:移动端为脚手架骨架(客户端连接层与主题已就绪),卡片化重排、Keybar 等核心 UI 尚未实现,尚未提交 Git。这是规划中的下一个里程碑。
|
||||||
|
|
||||||
|
| 功能 | 对应文件 |
|
||||||
|
|------|---------|
|
||||||
|
| 语义快照 WebSocket 消费 | `mobile/lib/src/rtty_mobile_client.dart` |
|
||||||
|
| 智能换行 / 卡片化 / Keybar | ⏳ 待实现 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 根目录其他文件
|
||||||
|
|
||||||
|
```
|
||||||
|
Rtty/
|
||||||
|
├── src/ # 服务端源码(见上)
|
||||||
|
├── desktop/ # 桌面端(见上)
|
||||||
|
├── mobile/ # 移动端(见上)
|
||||||
|
├── Cargo.toml # 服务端依赖与 release 优化配置
|
||||||
|
├── Cargo.lock # Rust 依赖锁
|
||||||
|
├── README.md # 项目设计总结与架构指南
|
||||||
|
├── LICENSE # 许可证
|
||||||
|
└── .gitignore # 忽略 target/.idea/Cargo.lock 等
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、核心功能清单(实现 vs 规划)
|
||||||
|
|
||||||
|
| 功能 | 服务端 | 桌面端 | 移动端 | 状态 |
|
||||||
|
|------|:---:|:---:|:---:|:---:|
|
||||||
|
| PTY / Shell 会话 | ✅ | — | — | 已实现 |
|
||||||
|
| Alacritty 引擎(ANSI 解析 / 网格) | ✅ | ✅ | — | 已实现 |
|
||||||
|
| 多端状态保持(断线重连恢复) | ✅ | ✅ | ⏳ | 已实现 |
|
||||||
|
| 多端解耦(PC ANSI / 移动快照) | ✅ | ✅ | ⏳ | 已实现 |
|
||||||
|
| 控制权强制 | ✅ | — | ⏳ | 已实现 |
|
||||||
|
| 会话清理(空闲超时) | ✅ | — | — | 已实现 |
|
||||||
|
| 100% 工业级 ANSI 渲染 | — | ✅ | — | 已实现 |
|
||||||
|
| 命令执行 / cd / TUI resize 稳定 | — | ✅ | — | 已实现 |
|
||||||
|
| 智能文本换行(Word Wrap) | — | — | ⏳ | 规划 |
|
||||||
|
| 卡片化命令交互 | — | — | ⏳ | 规划 |
|
||||||
|
| 移动端 Keybar | — | — | ⏳ | 规划 |
|
||||||
|
| 分布式横向扩展 | ⏳ | — | — | 规划 |
|
||||||
|
| 服务端鉴权 | ⏳ | — | — | 规划 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、运行与构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 服务端(仓库根目录)
|
||||||
|
cargo run # 监听 ws://0.0.0.0:8080
|
||||||
|
|
||||||
|
# 2. 桌面端
|
||||||
|
cd desktop
|
||||||
|
flutter run -d windows # 运行
|
||||||
|
flutter build windows --release # 构建(含 Rust 引擎编译)
|
||||||
|
|
||||||
|
# 3. 测试
|
||||||
|
cargo test # 服务端
|
||||||
|
flutter test # 桌面端单元测试(服务端离线时集成测试跳过)
|
||||||
|
flutter test integration_test -d windows # 桌面端真实桌面集成测试
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、目录导航速查
|
||||||
|
|
||||||
|
- **要改终端解析/网格逻辑** → `src/terminal/engine.rs`
|
||||||
|
- **要改 PTY/进程管理** → `src/terminal/pty.rs`
|
||||||
|
- **要改协议/消息格式** → `src/ws/protocol.rs`
|
||||||
|
- **要改会话/多端/清理逻辑** → `src/ws/handler.rs`
|
||||||
|
- **要改桌面端连接桥接** → `desktop/lib/src/rtty_pty_backend.dart`
|
||||||
|
- **要改桌面端渲染/接线** → `desktop/lib/src/terminal_screen.dart`
|
||||||
|
- **要改桌面端 UI/主题** → `desktop/lib/src/connection_bar.dart`、`theme.dart`
|
||||||
|
- **要改移动端连接层** → `mobile/lib/src/rtty_mobile_client.dart`
|
||||||
45
desktop/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
30
desktop/.metadata
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "ad70ec4617166f1c38e5d2bfd388af71fda14f06"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
- platform: windows
|
||||||
|
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
63
desktop/README.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Rtty Desktop (桌面端)
|
||||||
|
|
||||||
|
Rtty 的 Flutter Windows 桌面终端客户端。连接 `rtty-server`(Rust 后端),通过
|
||||||
|
**`flutter_alacritty`**(Alacritty Rust 引擎的 Flutter 绑定)做 **100% 工业级
|
||||||
|
ANSI 渲染**,完美支持 vim / neovim / htop / tmux 等全屏 TUI 应用。
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
rtty-server (Rust, alacritty_terminal 真相源)
|
||||||
|
│ WebSocket (ws://host:8080/ws?client=desktop)
|
||||||
|
│ • JSON 控制帧 (ready / error / session_closed)
|
||||||
|
│ • 二进制帧 = 原始 ANSI 字节流
|
||||||
|
▼
|
||||||
|
rtty-desktop (Flutter / Windows)
|
||||||
|
• RttyPtyBackend —— 实现 flutter_alacritty 的 PtyBackend 接口,桥接 WebSocket
|
||||||
|
• TerminalEngine —— Alacritty Rust 引擎(解析 ANSI / 维护网格 / GPU 渲染)
|
||||||
|
• TerminalView —— 终端渲染 + 输入/剪贴板/滚动
|
||||||
|
• ConnectionBar —— 顶部连接控制条
|
||||||
|
```
|
||||||
|
|
||||||
|
关键点:`PtyBackend` 抽象正是 `flutter_alacritty` 为远程(WebSocket/SSH)数据源
|
||||||
|
预留的扩展点。本地版用 `FlutterPtyBackend`(自带 PTY),本项目用自定义
|
||||||
|
`RttyPtyBackend` 把 WebSocket 原始 ANSI 流接入引擎,实现"远程 PTY + 本地
|
||||||
|
alacritty 渲染",服务端与 PC 端共享同一 Alacritty 引擎。
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 先启动 Rust 服务端(仓库根目录)
|
||||||
|
cd .. && cargo run
|
||||||
|
|
||||||
|
# 2. 启动桌面端
|
||||||
|
cd desktop
|
||||||
|
flutter run -d windows
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# release 会连同 Alacritty Rust 引擎一起编译(首次较慢)
|
||||||
|
flutter build windows --release
|
||||||
|
# 产物:build/windows/x64/runner/Release/rtty_desktop.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Widget 测试(无需服务端)
|
||||||
|
flutter test test/widget_test.dart
|
||||||
|
|
||||||
|
# 端到端集成测试(需先启动 Rust 服务端;离线时自动跳过)
|
||||||
|
flutter test test/e2e_ws_test.dart
|
||||||
|
flutter test test/regression_ws_test.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
## 协议要点
|
||||||
|
|
||||||
|
- 连接 `ws://host:port/ws?client=desktop`(`client` 声明端类型,实现多端解耦)。
|
||||||
|
- 服务端 `ready` 帧携带会话 ID 与初始尺寸,驱动引擎网格。
|
||||||
|
- PTY 输出以二进制帧下发,经 `RttyPtyBackend.output` 喂给引擎渲染。
|
||||||
|
- 键盘输入由引擎产生,经 `RttyPtyBackend.write` 发回服务端写入远端 PTY。
|
||||||
|
- 窗口尺寸变化经 `TerminalView.onPtyResize` → `RttyPtyBackend.resize` 同步。
|
||||||
28
desktop/analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
88
desktop/integration_test/cd_resize_test.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// 专项验证:cd 跨目录 + resize 后内容不重复。
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_alacritty/flutter_alacritty.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
import 'package:rtty_desktop/main.dart' as app;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets('cd 跨目录 + resize 内容完整性', (WidgetTester tester) async {
|
||||||
|
await app.main();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('CONNECT'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// 等待连接。
|
||||||
|
final deadline = DateTime.now().add(const Duration(seconds: 8));
|
||||||
|
while (DateTime.now().isBefore(deadline)) {
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
if (find.text('AWAITING CONNECTION').evaluate().isEmpty) break;
|
||||||
|
}
|
||||||
|
expect(find.text('AWAITING CONNECTION'), findsNothing);
|
||||||
|
|
||||||
|
final engine = tester
|
||||||
|
.state<TerminalViewState>(find.byType(TerminalView))
|
||||||
|
.widget
|
||||||
|
.engine;
|
||||||
|
engine.initializeEmpty(24, 80);
|
||||||
|
|
||||||
|
void send(String s) =>
|
||||||
|
engine.write(Uint8List.fromList(s.codeUnits));
|
||||||
|
|
||||||
|
String gridText() {
|
||||||
|
final g = engine.gridForView;
|
||||||
|
final sb = StringBuffer();
|
||||||
|
for (var r = 0; r < g.rows; r++) {
|
||||||
|
for (var c = 0; c < g.columns; c++) {
|
||||||
|
final cp = g.codepointAt(r, c);
|
||||||
|
sb.writeCharCode(cp == 0 ? 32 : cp);
|
||||||
|
}
|
||||||
|
sb.write('\n');
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> waitFor(bool Function(String) pred,
|
||||||
|
{int secs = 6}) async {
|
||||||
|
final end = DateTime.now().add(Duration(seconds: secs));
|
||||||
|
while (DateTime.now().isBefore(end)) {
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
if (pred(gridText())) return true;
|
||||||
|
}
|
||||||
|
return pred(gridText());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 1. cd 跨目录 ----
|
||||||
|
final target = r'C:\Users\NianJiu\rttytest\subdir';
|
||||||
|
send('cd $target\r\n');
|
||||||
|
final cdOk = await waitFor((t) => t.contains('rttytest\\subdir'));
|
||||||
|
debugPrint('cd 后 prompt 含目标目录: $cdOk');
|
||||||
|
if (!cdOk) debugPrint('--- grid ---\n${gridText()}');
|
||||||
|
expect(cdOk, isTrue, reason: 'cd 到绝对路径后 prompt 应显示新目录');
|
||||||
|
|
||||||
|
// ---- 2. resize 后内容不重复 ----
|
||||||
|
// 记录 resize 前的非空行。
|
||||||
|
send('dir\r\n');
|
||||||
|
final beforeOk = await waitFor((t) => t.contains('subdir') && t.contains('dir'));
|
||||||
|
debugPrint('dir 输出: $beforeOk');
|
||||||
|
|
||||||
|
// 连续 resize 数次(模拟窗口拖动),验证内容不产生重复/错乱。
|
||||||
|
for (final (c, r) in [(100, 30), (120, 35), (90, 28), (110, 32)]) {
|
||||||
|
engine.resize(columns: c, rows: r);
|
||||||
|
await tester.pump(const Duration(milliseconds: 150));
|
||||||
|
}
|
||||||
|
engine.resize(columns: 100, rows: 30);
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
// 重置引擎网格尺寸后,发送简单命令,验证仍能正常回显(无重复错乱)。
|
||||||
|
send('echo RESIZE_OK\r\n');
|
||||||
|
final ok = await waitFor((t) => t.contains('RESIZE_OK'));
|
||||||
|
debugPrint('resize 后命令回显: $ok');
|
||||||
|
expect(ok, isTrue, reason: 'resize 后命令应正常回显且无重复');
|
||||||
|
|
||||||
|
debugPrint('--- 最终 grid ---\n${gridText()}');
|
||||||
|
});
|
||||||
|
}
|
||||||
80
desktop/integration_test/e2e_test.dart
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// 端到端集成测试:在真实 Windows 桌面运行完整链路。
|
||||||
|
//
|
||||||
|
// 验证:RustLib.init() → 点 CONNECT → TerminalEngine(Alacritty Rust 引擎)创建
|
||||||
|
// → 连上服务端 → 命令回显(内容渲染 + 输入通路)。
|
||||||
|
//
|
||||||
|
// 运行前需先启动 Rust 服务端(cd .. && cargo run)。
|
||||||
|
// 运行:flutter test integration_test -d windows
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_alacritty/flutter_alacritty.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
import 'package:rtty_desktop/main.dart' as app;
|
||||||
|
import 'package:rtty_desktop/src/terminal_screen.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets('桌面端连接服务端并渲染命令输出', (WidgetTester tester) async {
|
||||||
|
await app.main();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// 未连接提示。
|
||||||
|
expect(find.text('AWAITING CONNECTION'), findsOneWidget);
|
||||||
|
|
||||||
|
// 点击 CONNECT。
|
||||||
|
await tester.tap(find.text('CONNECT'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// 等待连接就绪(进入 connected 状态,提示页消失)。
|
||||||
|
final deadline = DateTime.now().add(const Duration(seconds: 8));
|
||||||
|
while (DateTime.now().isBefore(deadline)) {
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
if (find.text('AWAITING CONNECTION').evaluate().isEmpty) break;
|
||||||
|
}
|
||||||
|
expect(find.text('AWAITING CONNECTION'), findsNothing,
|
||||||
|
reason: '连接后应进入终端渲染');
|
||||||
|
|
||||||
|
// Alacritty TerminalView 应渲染。
|
||||||
|
expect(find.byType(TerminalView), findsOneWidget,
|
||||||
|
reason: 'Alacritty TerminalView 应渲染');
|
||||||
|
|
||||||
|
// 通过引擎输出通路发送一条命令(模拟用户输入),验证:
|
||||||
|
// 服务端回显 → backend.output → engine.feed → 网格渲染。
|
||||||
|
final engine = tester.state<TerminalViewState>(find.byType(TerminalView)).widget.engine;
|
||||||
|
final token = 'RTTY_INT_${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
engine.write(Uint8List.fromList('echo $token\r\n'.codeUnits));
|
||||||
|
|
||||||
|
// 轮询引擎网格,等待命令回显进入渲染。
|
||||||
|
String? found;
|
||||||
|
final outDeadline = DateTime.now().add(const Duration(seconds: 8));
|
||||||
|
while (DateTime.now().isBefore(outDeadline)) {
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
final gridText = _gridText(engine);
|
||||||
|
if (gridText.contains(token)) {
|
||||||
|
found = gridText;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(found, isNotNull, reason: '命令回显应进入引擎网格');
|
||||||
|
debugPrint('回显捕获: ${found!.trim().split('\n').last}');
|
||||||
|
|
||||||
|
// 输入通路也验证:TerminalView 聚焦时按键应经 engine.write → backend → 服务端。
|
||||||
|
expect(find.byType(TerminalScreen), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从引擎镜像网格提取可视文本。
|
||||||
|
String _gridText(TerminalEngine engine) {
|
||||||
|
final grid = engine.gridForView;
|
||||||
|
final sb = StringBuffer();
|
||||||
|
for (var r = 0; r < grid.rows; r++) {
|
||||||
|
for (var c = 0; c < grid.columns; c++) {
|
||||||
|
final cp = grid.codepointAt(r, c);
|
||||||
|
sb.writeCharCode(cp == 0 ? 32 : cp);
|
||||||
|
}
|
||||||
|
sb.write('\n');
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
33
desktop/lib/main.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_alacritty/flutter_alacritty.dart';
|
||||||
|
|
||||||
|
import 'src/terminal_screen.dart';
|
||||||
|
import 'src/theme.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// 初始化 flutter_alacritty 的 Alacritty Rust 引擎(FFI 绑定)。
|
||||||
|
// cargokit 已将 Rust cdylib 打包进应用,无需外部库路径。
|
||||||
|
await RustLib.init();
|
||||||
|
|
||||||
|
// 锁定竖排字符方向(终端场景无需旋转)。
|
||||||
|
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
|
||||||
|
|
||||||
|
runApp(const RttyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class RttyApp extends StatelessWidget {
|
||||||
|
const RttyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Rtty Desktop',
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: RttyTheme.app(),
|
||||||
|
home: const TerminalScreen(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
336
desktop/lib/src/connection_bar.dart
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'rtty_pty_backend.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// 顶部连接控制条。
|
||||||
|
///
|
||||||
|
/// 精炼的工业风控制条:状态指示灯 + 服务器地址输入 + 会话 ID + 连接按钮。
|
||||||
|
/// 断开状态下可编辑,连接成功后锁定并显示会话信息。
|
||||||
|
class ConnectionBar extends StatelessWidget {
|
||||||
|
const ConnectionBar({
|
||||||
|
super.key,
|
||||||
|
required this.state,
|
||||||
|
required this.hostController,
|
||||||
|
required this.portController,
|
||||||
|
required this.tokenController,
|
||||||
|
required this.sessionController,
|
||||||
|
required this.sessionId,
|
||||||
|
required this.resumed,
|
||||||
|
required this.terminalSize,
|
||||||
|
required this.onConnect,
|
||||||
|
required this.onDisconnect,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ConnState state;
|
||||||
|
final TextEditingController hostController;
|
||||||
|
final TextEditingController portController;
|
||||||
|
final TextEditingController tokenController;
|
||||||
|
final TextEditingController sessionController;
|
||||||
|
final String? sessionId;
|
||||||
|
final bool resumed;
|
||||||
|
final String terminalSize;
|
||||||
|
final VoidCallback onConnect;
|
||||||
|
final VoidCallback onDisconnect;
|
||||||
|
|
||||||
|
bool get _connected =>
|
||||||
|
state == ConnState.connected || state == ConnState.connecting;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: RttyTheme.surface,
|
||||||
|
border: Border(bottom: BorderSide(color: RttyTheme.border)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
// 窗口过窄时横向滚动,避免字段溢出。
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_StatusLight(state: state),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_ConnectionLabel(state: state),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
_Divider(),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
_EditableField(
|
||||||
|
label: 'HOST',
|
||||||
|
controller: hostController,
|
||||||
|
enabled: !_connected,
|
||||||
|
width: 130,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_EditableField(
|
||||||
|
label: 'PORT',
|
||||||
|
controller: portController,
|
||||||
|
enabled: !_connected,
|
||||||
|
width: 64,
|
||||||
|
numeric: true,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_EditableField(
|
||||||
|
label: 'TOKEN',
|
||||||
|
controller: tokenController,
|
||||||
|
enabled: !_connected,
|
||||||
|
width: 110,
|
||||||
|
hint: 'optional',
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (!_connected) ...[
|
||||||
|
_EditableField(
|
||||||
|
label: 'SESSION',
|
||||||
|
controller: sessionController,
|
||||||
|
enabled: true,
|
||||||
|
width: 110,
|
||||||
|
hint: 'auto',
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
_SessionBadge(sessionId: sessionId, resumed: resumed),
|
||||||
|
],
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
_TerminalSizeBadge(text: terminalSize),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_ConnectButton(
|
||||||
|
state: state,
|
||||||
|
onConnect: onConnect,
|
||||||
|
onDisconnect: onDisconnect,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatusLight extends StatelessWidget {
|
||||||
|
const _StatusLight({required this.state});
|
||||||
|
|
||||||
|
final ConnState state;
|
||||||
|
|
||||||
|
Color get _color => switch (state) {
|
||||||
|
ConnState.disconnected => RttyTheme.textFaint,
|
||||||
|
ConnState.connecting => RttyTheme.accent,
|
||||||
|
ConnState.connected => RttyTheme.primary,
|
||||||
|
ConnState.error => RttyTheme.danger,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _color,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: _color.withValues(alpha: 0.55),
|
||||||
|
blurRadius: 6,
|
||||||
|
spreadRadius: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectionLabel extends StatelessWidget {
|
||||||
|
const _ConnectionLabel({required this.state});
|
||||||
|
|
||||||
|
final ConnState state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final (label, color) = switch (state) {
|
||||||
|
ConnState.disconnected => ('DISCONNECTED', RttyTheme.textFaint),
|
||||||
|
ConnState.connecting => ('CONNECTING', RttyTheme.accent),
|
||||||
|
ConnState.connected => ('CONNECTED', RttyTheme.primary),
|
||||||
|
ConnState.error => ('CONNECTION ERROR', RttyTheme.danger),
|
||||||
|
};
|
||||||
|
return Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 1.4,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Divider extends StatelessWidget {
|
||||||
|
const _Divider();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(width: 1, height: 24, color: RttyTheme.border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditableField extends StatelessWidget {
|
||||||
|
const _EditableField({
|
||||||
|
required this.label,
|
||||||
|
required this.controller,
|
||||||
|
required this.enabled,
|
||||||
|
required this.width,
|
||||||
|
this.numeric = false,
|
||||||
|
this.hint,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final TextEditingController controller;
|
||||||
|
final bool enabled;
|
||||||
|
final double width;
|
||||||
|
final bool numeric;
|
||||||
|
final String? hint;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: width,
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
enabled: enabled,
|
||||||
|
keyboardType:
|
||||||
|
numeric ? TextInputType.number : TextInputType.text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: RttyTheme.text,
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: 'Consolas',
|
||||||
|
decorationThickness: 0,
|
||||||
|
),
|
||||||
|
cursorColor: RttyTheme.primary,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
labelStyle: const TextStyle(
|
||||||
|
color: RttyTheme.textFaint,
|
||||||
|
fontSize: 10,
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: const TextStyle(color: RttyTheme.textFaint, fontSize: 12),
|
||||||
|
isDense: true,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: const BorderSide(color: RttyTheme.border),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: const BorderSide(color: RttyTheme.primaryDim),
|
||||||
|
),
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: RttyTheme.border.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SessionBadge extends StatelessWidget {
|
||||||
|
const _SessionBadge({required this.sessionId, required this.resumed});
|
||||||
|
|
||||||
|
final String? sessionId;
|
||||||
|
final bool resumed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: RttyTheme.surfaceHi,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(color: RttyTheme.border),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'SID ${sessionId ?? '—'}${resumed ? ' · RESUMED' : ' · NEW'}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: RttyTheme.textDim,
|
||||||
|
fontSize: 11,
|
||||||
|
fontFamily: 'Consolas',
|
||||||
|
letterSpacing: 0.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TerminalSizeBadge extends StatelessWidget {
|
||||||
|
const _TerminalSizeBadge({required this.text});
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: RttyTheme.textFaint,
|
||||||
|
fontSize: 11,
|
||||||
|
fontFamily: 'Consolas',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectButton extends StatelessWidget {
|
||||||
|
const _ConnectButton({
|
||||||
|
required this.state,
|
||||||
|
required this.onConnect,
|
||||||
|
required this.onDisconnect,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ConnState state;
|
||||||
|
final VoidCallback onConnect;
|
||||||
|
final VoidCallback onDisconnect;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final connected = state == ConnState.connected;
|
||||||
|
final connecting = state == ConnState.connecting;
|
||||||
|
|
||||||
|
final bg = connected
|
||||||
|
? RttyTheme.primaryDim.withValues(alpha: 0.25)
|
||||||
|
: RttyTheme.primary.withValues(alpha: 0.14);
|
||||||
|
final fg = connected ? RttyTheme.danger : RttyTheme.primary;
|
||||||
|
final label = connected
|
||||||
|
? 'DISCONNECT'
|
||||||
|
: connecting
|
||||||
|
? 'CONNECTING…'
|
||||||
|
: 'CONNECT';
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: 34,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: connected ? onDisconnect : onConnect,
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: bg,
|
||||||
|
foregroundColor: fg,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
side: BorderSide(color: fg.withValues(alpha: 0.5)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
280
desktop/lib/src/rtty_pty_backend.dart
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_alacritty/flutter_alacritty.dart';
|
||||||
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|
||||||
|
/// 连接状态。
|
||||||
|
enum ConnState { disconnected, connecting, connected, error }
|
||||||
|
|
||||||
|
/// 一次连接的参数(断线重连时复用)。
|
||||||
|
class _ConnectParams {
|
||||||
|
const _ConnectParams({
|
||||||
|
required this.host,
|
||||||
|
required this.port,
|
||||||
|
this.session,
|
||||||
|
this.token,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String host;
|
||||||
|
final int port;
|
||||||
|
final String? session;
|
||||||
|
final String? token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将 Rtty Rust 服务端桥接为 flutter_alacritty 的 [PtyBackend]。
|
||||||
|
///
|
||||||
|
/// 这正是该库为远程(WebSocket/SSH)数据源预留的扩展点:
|
||||||
|
/// - [output]:把 WebSocket 二进制帧(服务端原始 ANSI)作为 PTY 输出流;
|
||||||
|
/// - [write]:把引擎产生的输入字节发回服务端写入远端 PTY;
|
||||||
|
/// - [resize]:同步终端尺寸到服务端;
|
||||||
|
/// - [exitCode]:服务端会话结束时完成。
|
||||||
|
///
|
||||||
|
/// 额外能力:
|
||||||
|
/// - 断线自动重连(指数退避),重连成功后服务端会重放全屏,画面无缝恢复;
|
||||||
|
/// - 每 30s 发送一次心跳,保持连接活跃并探测服务端存活。
|
||||||
|
class RttyPtyBackend implements PtyBackend {
|
||||||
|
RttyPtyBackend();
|
||||||
|
|
||||||
|
final StreamController<Uint8List> _outputCtl =
|
||||||
|
StreamController<Uint8List>.broadcast();
|
||||||
|
final Completer<int> _exitCodeCtl = Completer<int>();
|
||||||
|
|
||||||
|
WebSocketChannel? _channel;
|
||||||
|
StreamSubscription? _sub;
|
||||||
|
|
||||||
|
ConnState _state = ConnState.disconnected;
|
||||||
|
ConnState get state => _state;
|
||||||
|
|
||||||
|
String? _sessionId;
|
||||||
|
String? get sessionId => _sessionId;
|
||||||
|
|
||||||
|
/// 是否恢复了已存在的会话(false = 本次连接新建了会话)。
|
||||||
|
bool _resumed = false;
|
||||||
|
bool get resumed => _resumed;
|
||||||
|
|
||||||
|
String? _error;
|
||||||
|
String? get error => _error;
|
||||||
|
|
||||||
|
/// 用户主动断开/关闭:不再自动重连。
|
||||||
|
bool _manualClose = true;
|
||||||
|
/// 不可恢复错误(鉴权失败 / 会话结束):不再自动重连。
|
||||||
|
bool _fatal = false;
|
||||||
|
|
||||||
|
int _reconnectAttempts = 0;
|
||||||
|
Timer? _reconnectTimer;
|
||||||
|
Timer? _pingTimer;
|
||||||
|
_ConnectParams? _params;
|
||||||
|
|
||||||
|
/// 就绪回调(收到 ready 帧,携带会话 ID、初始尺寸与恢复标记)。
|
||||||
|
void Function(String id, int cols, int rows, bool resumed)? onReady;
|
||||||
|
|
||||||
|
/// 状态变化回调。
|
||||||
|
void Function(ConnState state)? onStateChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Uint8List> get output => _outputCtl.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> get exitCode => _exitCodeCtl.future;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ValueListenable<bool>? get isForegroundProcessRunning => null;
|
||||||
|
|
||||||
|
/// 连接到 Rtty 服务端。
|
||||||
|
Future<void> connect({
|
||||||
|
required String host,
|
||||||
|
required int port,
|
||||||
|
String? session,
|
||||||
|
String? token,
|
||||||
|
}) async {
|
||||||
|
await close();
|
||||||
|
_manualClose = false;
|
||||||
|
_fatal = false;
|
||||||
|
_reconnectAttempts = 0;
|
||||||
|
_params = _ConnectParams(host: host, port: port, session: session, token: token);
|
||||||
|
_setState(ConnState.connecting);
|
||||||
|
_error = null;
|
||||||
|
_openChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openChannel() {
|
||||||
|
final p = _params;
|
||||||
|
if (p == null || _manualClose || _fatal) return;
|
||||||
|
|
||||||
|
final query = StringBuffer('?client=desktop');
|
||||||
|
if (p.token != null && p.token!.isNotEmpty) {
|
||||||
|
query.write('&token=${Uri.encodeQueryComponent(p.token!)}');
|
||||||
|
}
|
||||||
|
if (p.session != null && p.session!.isNotEmpty) {
|
||||||
|
query.write('&session=${Uri.encodeQueryComponent(p.session!)}');
|
||||||
|
}
|
||||||
|
final uri = Uri.parse('ws://${p.host}:${p.port}/ws$query');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final channel = IOWebSocketChannel.connect(uri);
|
||||||
|
_channel = channel;
|
||||||
|
_sub = channel.stream.listen(
|
||||||
|
_handleIncoming,
|
||||||
|
onError: (Object e) {
|
||||||
|
_error = e.toString();
|
||||||
|
_setState(ConnState.error);
|
||||||
|
_scheduleReconnect();
|
||||||
|
},
|
||||||
|
onDone: _handleDone,
|
||||||
|
);
|
||||||
|
// 收到 ready 帧后才转 connected;期间保持 connecting。
|
||||||
|
_startPing();
|
||||||
|
} catch (e) {
|
||||||
|
_error = e.toString();
|
||||||
|
_setState(ConnState.error);
|
||||||
|
_scheduleReconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 连接被对端关闭(或底层错误)后的统一出口。
|
||||||
|
void _handleDone() {
|
||||||
|
_stopPing();
|
||||||
|
if (_manualClose) {
|
||||||
|
if (_state == ConnState.connected) _setState(ConnState.disconnected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_fatal) {
|
||||||
|
if (!_exitCodeCtl.isCompleted) _exitCodeCtl.complete(1);
|
||||||
|
if (_state != ConnState.error) _setState(ConnState.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 网络断开:进入指数退避重连。
|
||||||
|
if (_state != ConnState.disconnected) _setState(ConnState.disconnected);
|
||||||
|
_scheduleReconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 指数退避重连:500ms → 1s → 2s → …,上限 10s;收到 ready 后重置。
|
||||||
|
void _scheduleReconnect() {
|
||||||
|
if (_manualClose || _fatal) return;
|
||||||
|
_reconnectTimer?.cancel();
|
||||||
|
final backoffMs =
|
||||||
|
math.min(500 * (1 << math.min(_reconnectAttempts, 5)), 10000);
|
||||||
|
_reconnectAttempts++;
|
||||||
|
_reconnectTimer = Timer(Duration(milliseconds: backoffMs), () {
|
||||||
|
if (_manualClose || _fatal) return;
|
||||||
|
_setState(ConnState.connecting);
|
||||||
|
_openChannel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleIncoming(dynamic message) {
|
||||||
|
if (message is List<int>) {
|
||||||
|
// PC 端原始 ANSI 字节流 → 喂给引擎渲染。
|
||||||
|
_outputCtl.add(Uint8List.fromList(message));
|
||||||
|
} else if (message is String) {
|
||||||
|
_handleJsonFrame(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleJsonFrame(String text) {
|
||||||
|
final Object? decoded;
|
||||||
|
try {
|
||||||
|
decoded = jsonDecode(text);
|
||||||
|
} catch (_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (decoded is! Map<String, dynamic>) return;
|
||||||
|
|
||||||
|
switch (decoded['type']) {
|
||||||
|
case 'ready':
|
||||||
|
_sessionId = decoded['id'] as String? ?? '';
|
||||||
|
_resumed = decoded['resumed'] == true;
|
||||||
|
final cols = (decoded['cols'] as num?)?.toInt() ?? 80;
|
||||||
|
final rows = (decoded['rows'] as num?)?.toInt() ?? 24;
|
||||||
|
_reconnectAttempts = 0; // 重连成功,重置退避。
|
||||||
|
_setState(ConnState.connected);
|
||||||
|
onReady?.call(_sessionId ?? '', cols, rows, _resumed);
|
||||||
|
case 'mobile_snapshot':
|
||||||
|
// 桌面端使用原始 ANSI 渲染,忽略语义化快照。
|
||||||
|
break;
|
||||||
|
case 'pong':
|
||||||
|
// 心跳应答,无需处理。
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
_error = decoded['message'] as String? ?? 'server error';
|
||||||
|
_fatal = true; // 鉴权失败等不可恢复错误,不再重连。
|
||||||
|
if (!_exitCodeCtl.isCompleted) _exitCodeCtl.complete(1);
|
||||||
|
_setState(ConnState.error);
|
||||||
|
case 'session_closed':
|
||||||
|
_fatal = true; // 会话已结束,不再重连。
|
||||||
|
if (!_exitCodeCtl.isCompleted) _exitCodeCtl.complete(0);
|
||||||
|
_setState(ConnState.disconnected);
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(Uint8List data) {
|
||||||
|
if (_state != ConnState.connected || _channel == null) return;
|
||||||
|
// 二进制帧原样透传:避免经 UTF-8 字符串中转时损坏任意字节序列。
|
||||||
|
_channel!.sink.add(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void resize(int rows, int columns) {
|
||||||
|
if (_state != ConnState.connected || _channel == null) return;
|
||||||
|
_channel!.sink
|
||||||
|
.add(jsonEncode({'type': 'resize', 'cols': columns, 'rows': rows}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void kill() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户主动关闭连接并释放资源。
|
||||||
|
Future<void> close() async {
|
||||||
|
_manualClose = true;
|
||||||
|
_fatal = false;
|
||||||
|
_reconnectTimer?.cancel();
|
||||||
|
_reconnectTimer = null;
|
||||||
|
_stopPing();
|
||||||
|
await _teardownChannel();
|
||||||
|
_sessionId = null;
|
||||||
|
_resumed = false;
|
||||||
|
if (!_exitCodeCtl.isCompleted) _exitCodeCtl.complete(0);
|
||||||
|
if (_state != ConnState.disconnected) _setState(ConnState.disconnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _teardownChannel() async {
|
||||||
|
await _sub?.cancel();
|
||||||
|
_sub = null;
|
||||||
|
await _channel?.sink.close();
|
||||||
|
_channel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startPing() {
|
||||||
|
_stopPing();
|
||||||
|
_pingTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||||
|
if (_state == ConnState.connected && _channel != null) {
|
||||||
|
_channel!.sink.add(jsonEncode({'type': 'ping'}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopPing() {
|
||||||
|
_pingTimer?.cancel();
|
||||||
|
_pingTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setState(ConnState state) {
|
||||||
|
if (_state == state) return;
|
||||||
|
_state = state;
|
||||||
|
onStateChanged?.call(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
close();
|
||||||
|
_outputCtl.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
desktop/lib/src/terminal_config.dart
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter_alacritty/flutter_alacritty.dart';
|
||||||
|
|
||||||
|
/// 将 [RttyTheme] 的工业终端配色映射为 flutter_alacritty 的 [TerminalConfig]。
|
||||||
|
class RttyTerminalConfig {
|
||||||
|
RttyTerminalConfig._();
|
||||||
|
|
||||||
|
static TerminalConfig build() {
|
||||||
|
final defaults = TerminalConfig.defaults();
|
||||||
|
return defaults.copyWith(
|
||||||
|
colors: const TerminalColors(
|
||||||
|
background: 0xFF0B0E11,
|
||||||
|
foreground: 0xFFCBD5DF,
|
||||||
|
selection: 0x553A6EA5,
|
||||||
|
ansi: [
|
||||||
|
0xFF0B0E11, // black
|
||||||
|
0xFFE5534B, // red
|
||||||
|
0xFF3DDC97, // green
|
||||||
|
0xFFFFB454, // yellow
|
||||||
|
0xFF5AB0FF, // blue
|
||||||
|
0xFFD77BFF, // magenta
|
||||||
|
0xFF4ED6E0, // cyan
|
||||||
|
0xFFDDE6EE, // white
|
||||||
|
0xFF4A5866, // bright black
|
||||||
|
0xFFFF6B6B, // bright red
|
||||||
|
0xFF6BFFC2, // bright green
|
||||||
|
0xFFFFCE7A, // bright yellow
|
||||||
|
0xFF82C4FF, // bright blue
|
||||||
|
0xFFE8A6FF, // bright magenta
|
||||||
|
0xFF82EDF4, // bright cyan
|
||||||
|
0xFFFFFDF8, // bright white
|
||||||
|
],
|
||||||
|
searchMatchBg: 0xFFB45454,
|
||||||
|
searchMatchFg: 0xFF14100A,
|
||||||
|
searchFocusedBg: 0xFFFFB454,
|
||||||
|
searchFocusedFg: 0xFF14100A,
|
||||||
|
hintStartFg: 0xFF14100A,
|
||||||
|
hintStartBg: 0xFFFFB454,
|
||||||
|
cursorText: 0xFF06130D,
|
||||||
|
cursorBody: 0xFF3DDC97,
|
||||||
|
),
|
||||||
|
font: const FontConfig(
|
||||||
|
family: 'Consolas',
|
||||||
|
fallback: ['Cascadia Mono', 'JetBrains Mono', 'Menlo', 'monospace'],
|
||||||
|
size: 14.0,
|
||||||
|
lineHeight: 1.15,
|
||||||
|
),
|
||||||
|
scrolling: defaults.scrolling.copyWith(history: 10000),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
389
desktop/lib/src/terminal_screen.dart
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_alacritty/flutter_alacritty.dart';
|
||||||
|
|
||||||
|
import 'connection_bar.dart';
|
||||||
|
import 'rtty_pty_backend.dart';
|
||||||
|
import 'terminal_config.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// 主终端页面。
|
||||||
|
///
|
||||||
|
/// 使用 `flutter_alacritty` 的 Rust 引擎(TerminalEngine + TerminalView)渲染,
|
||||||
|
/// 通过 [RttyPtyBackend] 桥接远程 Rtty Rust 服务端的 WebSocket 数据流。
|
||||||
|
/// 断开时展示终端提示页,连接后进入实时工业级 ANSI 渲染。
|
||||||
|
class TerminalScreen extends StatefulWidget {
|
||||||
|
const TerminalScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TerminalScreen> createState() => _TerminalScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TerminalScreenState extends State<TerminalScreen> {
|
||||||
|
late final TerminalConfig _config;
|
||||||
|
TerminalEngine? _engine;
|
||||||
|
TerminalController _controller = TerminalController();
|
||||||
|
final FocusNode _focus = FocusNode();
|
||||||
|
|
||||||
|
RttyPtyBackend? _backend;
|
||||||
|
StreamSubscription<Uint8List>? _engineOutputSub;
|
||||||
|
StreamSubscription<Uint8List>? _backendOutputSub;
|
||||||
|
|
||||||
|
/// resize 防抖:窗口拖动时 onPtyResize 会高频触发,合并为最后一次。
|
||||||
|
Timer? _resizeDebounce;
|
||||||
|
int _pendingCols = 0;
|
||||||
|
int _pendingRows = 0;
|
||||||
|
|
||||||
|
final _host = TextEditingController(text: '127.0.0.1');
|
||||||
|
final _port = TextEditingController(text: '8080');
|
||||||
|
final _token = TextEditingController();
|
||||||
|
final _session = TextEditingController();
|
||||||
|
|
||||||
|
ConnState _state = ConnState.disconnected;
|
||||||
|
String? _sessionId;
|
||||||
|
bool _resumed = false;
|
||||||
|
int _cols = 0;
|
||||||
|
int _rows = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_config = RttyTerminalConfig.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tearDownSession();
|
||||||
|
_host.dispose();
|
||||||
|
_port.dispose();
|
||||||
|
_token.dispose();
|
||||||
|
_session.dispose();
|
||||||
|
_controller.dispose();
|
||||||
|
_focus.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _connect() async {
|
||||||
|
final host = _host.text.trim();
|
||||||
|
final port = int.tryParse(_port.text.trim()) ?? 8080;
|
||||||
|
final token = _token.text.trim();
|
||||||
|
final session = _session.text.trim();
|
||||||
|
if (host.isEmpty) return;
|
||||||
|
|
||||||
|
_tearDownSession();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_engine = TerminalEngine(config: _config);
|
||||||
|
_controller = TerminalController()..attach(_engine!);
|
||||||
|
_backend = RttyPtyBackend();
|
||||||
|
});
|
||||||
|
|
||||||
|
final engine = _engine!;
|
||||||
|
final backend = _backend!;
|
||||||
|
|
||||||
|
// 双向接线:backend 输出(服务端 ANSI)→ 引擎渲染;
|
||||||
|
// 引擎输出(用户输入)→ backend 发回服务端写入远端 PTY。
|
||||||
|
_engineOutputSub = engine.output.listen(backend.write);
|
||||||
|
_backendOutputSub = backend.output.listen(engine.feed);
|
||||||
|
|
||||||
|
backend.onStateChanged = (s) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _state = s);
|
||||||
|
if (s == ConnState.disconnected || s == ConnState.error) {
|
||||||
|
_sessionId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
backend.onReady = (id, cols, rows, resumed) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_sessionId = id;
|
||||||
|
_resumed = resumed;
|
||||||
|
_cols = cols;
|
||||||
|
_rows = rows;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
await backend.connect(host: host, port: port, session: session, token: token);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _disconnect() => _tearDownSession();
|
||||||
|
|
||||||
|
void _tearDownSession() {
|
||||||
|
_resizeDebounce?.cancel();
|
||||||
|
_resizeDebounce = null;
|
||||||
|
_engineOutputSub?.cancel();
|
||||||
|
_engineOutputSub = null;
|
||||||
|
_backendOutputSub?.cancel();
|
||||||
|
_backendOutputSub = null;
|
||||||
|
|
||||||
|
final b = _backend;
|
||||||
|
if (b != null) {
|
||||||
|
b.onStateChanged = null;
|
||||||
|
b.onReady = null;
|
||||||
|
b.kill();
|
||||||
|
b.dispose();
|
||||||
|
}
|
||||||
|
_backend = null;
|
||||||
|
_engine?.dispose();
|
||||||
|
_engine = null;
|
||||||
|
_sessionId = null;
|
||||||
|
_resumed = false;
|
||||||
|
_state = ConnState.disconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePtyResize(int cols, int rows) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_cols = cols;
|
||||||
|
_rows = rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 合并连续 resize:只发送最后一次,避免 TUI 频繁 SIGWINCH 重绘错乱。
|
||||||
|
_pendingCols = cols;
|
||||||
|
_pendingRows = rows;
|
||||||
|
_resizeDebounce?.cancel();
|
||||||
|
_resizeDebounce = Timer(const Duration(milliseconds: 80), () {
|
||||||
|
final b = _backend;
|
||||||
|
if (b == null) return;
|
||||||
|
// PtyBackend.resize(rows, columns) — 注意参数顺序为行/列。
|
||||||
|
b.resize(_pendingRows, _pendingCols);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final sizeText = _state == ConnState.connected ? '$_cols × $_rows' : '—';
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: RttyTheme.background,
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
ConnectionBar(
|
||||||
|
state: _state,
|
||||||
|
hostController: _host,
|
||||||
|
portController: _port,
|
||||||
|
tokenController: _token,
|
||||||
|
sessionController: _session,
|
||||||
|
sessionId: _sessionId,
|
||||||
|
resumed: _resumed,
|
||||||
|
terminalSize: sizeText,
|
||||||
|
onConnect: _connect,
|
||||||
|
onDisconnect: _disconnect,
|
||||||
|
),
|
||||||
|
Expanded(child: _buildBody()),
|
||||||
|
const _StatusBar(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody() {
|
||||||
|
final engine = _engine;
|
||||||
|
if (_state == ConnState.connected && engine != null) {
|
||||||
|
return Container(
|
||||||
|
color: RttyTheme.background,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: RttyTheme.background,
|
||||||
|
border: Border.all(color: RttyTheme.border),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
||||||
|
child: TerminalView(
|
||||||
|
engine,
|
||||||
|
controller: _controller,
|
||||||
|
focusNode: _focus,
|
||||||
|
autofocus: true,
|
||||||
|
onPtyResize: _handlePtyResize,
|
||||||
|
theme: _config.theme,
|
||||||
|
textStyle: _config.style,
|
||||||
|
mouseCursor: SystemMouseCursors.text,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _IdlePane(state: _state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 未连接时的提示面板。
|
||||||
|
class _IdlePane extends StatelessWidget {
|
||||||
|
const _IdlePane({required this.state});
|
||||||
|
|
||||||
|
final ConnState state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final (title, subtitle) = switch (state) {
|
||||||
|
ConnState.disconnected => (
|
||||||
|
'AWAITING CONNECTION',
|
||||||
|
'输入服务端地址后点击 CONNECT 建立终端会话',
|
||||||
|
),
|
||||||
|
ConnState.connecting => (
|
||||||
|
'CONNECTING',
|
||||||
|
'正在协商 WebSocket 终端会话…',
|
||||||
|
),
|
||||||
|
ConnState.error => (
|
||||||
|
'CONNECTION LOST',
|
||||||
|
'无法建立连接,请检查服务端是否已启动',
|
||||||
|
),
|
||||||
|
ConnState.connected => ('', ''),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_ScanlineGlyph(state: state),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: RttyTheme.textDim,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: const TextStyle(color: RttyTheme.textFaint, fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 终端提示区的装饰性扫描线字形。
|
||||||
|
class _ScanlineGlyph extends StatelessWidget {
|
||||||
|
const _ScanlineGlyph({required this.state});
|
||||||
|
|
||||||
|
final ConnState state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = switch (state) {
|
||||||
|
ConnState.connected => RttyTheme.primary,
|
||||||
|
ConnState.connecting => RttyTheme.accent,
|
||||||
|
ConnState.error => RttyTheme.danger,
|
||||||
|
_ => RttyTheme.textFaint,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: color.withValues(alpha: 0.5), width: 1.5),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: CustomPaint(painter: _ScanlinePainter(color)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ScanlinePainter extends CustomPainter {
|
||||||
|
_ScanlinePainter(this.color);
|
||||||
|
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = color.withValues(alpha: 0.18)
|
||||||
|
..strokeWidth = 1;
|
||||||
|
const spacing = 6.0;
|
||||||
|
for (double y = 0; y < size.height; y += spacing) {
|
||||||
|
canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
|
||||||
|
}
|
||||||
|
final cursorPaint = Paint()
|
||||||
|
..color = color.withValues(alpha: 0.8)
|
||||||
|
..strokeWidth = 2;
|
||||||
|
canvas.drawRect(
|
||||||
|
Rect.fromLTWH(size.width * 0.3, size.height * 0.45, 24, 14),
|
||||||
|
cursorPaint..style = PaintingStyle.stroke,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _ScanlinePainter old) => old.color != color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 底部状态栏:会话 ID、连接状态、按键提示。
|
||||||
|
class _StatusBar extends StatelessWidget {
|
||||||
|
const _StatusBar();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 26,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: RttyTheme.surface,
|
||||||
|
border: Border(top: BorderSide(color: RttyTheme.border)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
children: const [
|
||||||
|
_StatusDot(),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'RTTY DESKTOP',
|
||||||
|
style: TextStyle(
|
||||||
|
color: RttyTheme.textFaint,
|
||||||
|
fontSize: 10,
|
||||||
|
letterSpacing: 1.6,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
_Hint('拖选复制', RttyTheme.textFaint),
|
||||||
|
SizedBox(width: 14),
|
||||||
|
_Hint('Ctrl+Shift+C / V 复制粘贴', RttyTheme.textFaint),
|
||||||
|
Spacer(),
|
||||||
|
Text(
|
||||||
|
'FLUTTER × ALACRITTY ENGINE',
|
||||||
|
style: TextStyle(color: RttyTheme.textFaint, fontSize: 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatusDot extends StatelessWidget {
|
||||||
|
const _StatusDot();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: RttyTheme.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Hint extends StatelessWidget {
|
||||||
|
const _Hint(this.text, this.color);
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(color: color, fontSize: 10, fontFamily: 'Consolas'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
desktop/lib/src/theme.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Rtty 桌面端主题。
|
||||||
|
///
|
||||||
|
/// 设计方向:工业终端美学。深炭黑底 + 磷光青绿功能色,
|
||||||
|
/// 借鉴老式 CRT 终端的磷光质感,克制而精确。
|
||||||
|
class RttyTheme {
|
||||||
|
RttyTheme._();
|
||||||
|
|
||||||
|
// ---- 基础色板 ----
|
||||||
|
static const Color background = Color(0xFF0B0E11); // 炭黑
|
||||||
|
static const Color surface = Color(0xFF12161B); // 面板
|
||||||
|
static const Color surfaceHi = Color(0xFF1A2027); // 高亮面板
|
||||||
|
static const Color border = Color(0xFF232B33); // 边框
|
||||||
|
static const Color primary = Color(0xFF3DDC97); // 磷光青绿
|
||||||
|
static const Color primaryDim = Color(0xFF1E7A5A);
|
||||||
|
static const Color accent = Color(0xFFFFB454); // 琥珀
|
||||||
|
static const Color danger = Color(0xFFE5534B); // 红
|
||||||
|
static const Color text = Color(0xFFD5DEE7);
|
||||||
|
static const Color textDim = Color(0xFF7B8A99);
|
||||||
|
static const Color textFaint = Color(0xFF4A5866);
|
||||||
|
|
||||||
|
static ThemeData app() {
|
||||||
|
final base = ThemeData.dark(useMaterial3: true);
|
||||||
|
return base.copyWith(
|
||||||
|
scaffoldBackgroundColor: background,
|
||||||
|
colorScheme: ColorScheme.dark(
|
||||||
|
primary: primary,
|
||||||
|
secondary: accent,
|
||||||
|
surface: surface,
|
||||||
|
error: danger,
|
||||||
|
onPrimary: const Color(0xFF06130D),
|
||||||
|
onSurface: text,
|
||||||
|
),
|
||||||
|
textTheme: base.textTheme.copyWith(
|
||||||
|
bodySmall: base.textTheme.bodySmall?.copyWith(color: textDim),
|
||||||
|
labelSmall: base.textTheme.labelSmall?.copyWith(
|
||||||
|
color: textFaint,
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
689
desktop/pubspec.lock
Normal file
@@ -0,0 +1,689 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
adaptive_number:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: adaptive_number
|
||||||
|
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.1"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
build_cli_annotations:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_cli_annotations
|
||||||
|
sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
code_assets:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_assets
|
||||||
|
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+4"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
|
cupertino_icons:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cupertino_icons
|
||||||
|
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.9"
|
||||||
|
desktop_drop:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: desktop_drop
|
||||||
|
sha256: aa1e797255bfbc76f9eb5aa4f61e5b68dbf69962ab1be6495816d2f251bc0d1f
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.1"
|
||||||
|
ed25519_edwards:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ed25519_edwards
|
||||||
|
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.1"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.3"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_alacritty:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_alacritty
|
||||||
|
sha256: "0a4eaaecf09fd86ac3d6bf6cc9b82ace49ba888799fc27934982baedd8512c2d"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
flutter_driver:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_lints
|
||||||
|
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.0"
|
||||||
|
flutter_pty_new:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_pty_new
|
||||||
|
sha256: "8894d9e7b4e85af5d6c080bc18d14956a0023e8394f96e81e8ab5c3528e7a0c2"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
flutter_rust_bridge:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_rust_bridge
|
||||||
|
sha256: e87d6b9ee934dcd24a128ccb2bd91905d2d5fe5c06245d6a8f5477d4907a437a
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.12.0"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
freezed_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: freezed_annotation
|
||||||
|
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
|
fuchsia_remote_debug_protocol:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
|
http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
|
integration_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
jni:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni
|
||||||
|
sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
|
jni_flutter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni_flutter
|
||||||
|
sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
|
jni_util:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni_util
|
||||||
|
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.12.0"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.2"
|
||||||
|
leak_tracker_flutter_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_flutter_testing
|
||||||
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.10"
|
||||||
|
leak_tracker_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_testing
|
||||||
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
lints:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.19"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.0"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.18.0"
|
||||||
|
objective_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: objective_c
|
||||||
|
sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "9.5.0"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
path_provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.6"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.1"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.0"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.2"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.2"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.6"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
process:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: process
|
||||||
|
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.5"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
record_use:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_use
|
||||||
|
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
|
rust_lib_flutter_alacritty:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rust_lib_flutter_alacritty
|
||||||
|
sha256: "258f038b57e2c9548ab901624971f8dd05973f07858f7662158504c72a9dd7b6"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.2"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
sync_http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sync_http
|
||||||
|
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.11"
|
||||||
|
toml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: toml
|
||||||
|
sha256: "35a35f782228656a2af31e8c73d1353cc4ef3d683fd68af1111b44631879c05e"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.18.0"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
universal_platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: universal_platform
|
||||||
|
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.2"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.32"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.4.1"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.5"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.5"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "15.2.0"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket
|
||||||
|
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
web_socket_channel:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: web_socket_channel
|
||||||
|
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
|
webdriver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webdriver
|
||||||
|
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.12.2 <4.0.0"
|
||||||
|
flutter: ">=3.44.0"
|
||||||
93
desktop/pubspec.yaml
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
name: rtty_desktop
|
||||||
|
description: "A new Flutter project."
|
||||||
|
# The following line prevents the package from being accidentally published to
|
||||||
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
# The following defines the version and build number for your application.
|
||||||
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
|
# followed by an optional build number separated by a +.
|
||||||
|
# Both the version and the builder number may be overridden in flutter
|
||||||
|
# build by specifying --build-name and --build-number, respectively.
|
||||||
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||||
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||||
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||||
|
# Read more about iOS versioning at
|
||||||
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.12.2
|
||||||
|
|
||||||
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
|
# dependencies can be manually updated by changing the version numbers below to
|
||||||
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
|
# versions available, run `flutter pub outdated`.
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# The following adds the Cupertino Icons font to your application.
|
||||||
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
cupertino_icons: ^1.0.8
|
||||||
|
web_socket_channel: ^3.0.3
|
||||||
|
flutter_alacritty: ^2.4.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
|
# package. See that file for information about deactivating specific lint
|
||||||
|
# rules and activating additional ones.
|
||||||
|
flutter_lints: ^6.0.0
|
||||||
|
integration_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
|
flutter:
|
||||||
|
|
||||||
|
# The following line ensures that the Material Icons font is
|
||||||
|
# included with your application, so that you can use the icons in
|
||||||
|
# the material Icons class.
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
|
# To add assets to your application, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|
||||||
|
# For details regarding adding assets from package dependencies, see
|
||||||
|
# https://flutter.dev/to/asset-from-package
|
||||||
|
|
||||||
|
# To add custom fonts to your application, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts from package dependencies,
|
||||||
|
# see https://flutter.dev/to/font-from-package
|
||||||
68
desktop/test/e2e_ws_test.dart
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// 端到端集成测试:桌面端连接层(RttyPtyBackend)↔ Rust 服务端。
|
||||||
|
//
|
||||||
|
// 运行前需先启动 Rust 服务端:cd .. && cargo run
|
||||||
|
// 运行:flutter test test/e2e_ws_test.dart
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:rtty_desktop/src/rtty_pty_backend.dart';
|
||||||
|
|
||||||
|
import 'support.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
test('RttyPtyBackend connects to Rust server and receives raw ANSI output',
|
||||||
|
() async {
|
||||||
|
if (!await isServerUp('127.0.0.1', 8080)) {
|
||||||
|
markTestSkipped('Rtty server not running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final backend = RttyPtyBackend();
|
||||||
|
|
||||||
|
final states = <ConnState>[];
|
||||||
|
String? sessionId;
|
||||||
|
final raw = StringBuffer();
|
||||||
|
backend.onStateChanged = (s) => states.add(s);
|
||||||
|
backend.onReady = (id, cols, rows, resumed) =>
|
||||||
|
sessionId = '$id:$cols:$rows:$resumed';
|
||||||
|
backend.output.listen((Uint8List b) {
|
||||||
|
raw.write(String.fromCharCodes(b));
|
||||||
|
});
|
||||||
|
|
||||||
|
await backend.connect(host: '127.0.0.1', port: 8080);
|
||||||
|
|
||||||
|
// 等待连接就绪(socket 已连接 + 收到 ready 帧)。
|
||||||
|
final deadline = DateTime.now().add(const Duration(seconds: 5));
|
||||||
|
while ((backend.state != ConnState.connected || sessionId == null) &&
|
||||||
|
DateTime.now().isBefore(deadline)) {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(backend.state, ConnState.connected,
|
||||||
|
reason: '未能连接服务端 states=$states');
|
||||||
|
expect(sessionId, isNotNull, reason: '未收到 ready 帧 states=$states');
|
||||||
|
|
||||||
|
// 发送命令,等待回显。
|
||||||
|
backend.write(Uint8List.fromList(
|
||||||
|
'echo RTTY_E2E_${DateTime.now().millisecondsSinceEpoch}\r\n'.codeUnits));
|
||||||
|
|
||||||
|
String? matchedLine;
|
||||||
|
final outDeadline = DateTime.now().add(const Duration(seconds: 8));
|
||||||
|
while (DateTime.now().isBefore(outDeadline)) {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 150));
|
||||||
|
final text = raw.toString();
|
||||||
|
final lines = text.split('\n');
|
||||||
|
matchedLine = lines.where((l) => l.contains('RTTY_E2E')).lastOrNull;
|
||||||
|
if (matchedLine != null) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(matchedLine, isNotNull,
|
||||||
|
reason: '未捕获到命令回显。流末尾:'
|
||||||
|
'${raw.toString().length > 200 ? raw.toString().substring(raw.toString().length - 200) : raw.toString()}');
|
||||||
|
|
||||||
|
await backend.close();
|
||||||
|
expect(backend.state, ConnState.disconnected);
|
||||||
|
});
|
||||||
|
}
|
||||||
77
desktop/test/regression_ws_test.dart
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// 多端解耦 + 控制权强制回归验证(连真实 Rust 服务端)。
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
|
||||||
|
import 'support.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
test('多端解耦 + 控制权强制 + 会话清理', () async {
|
||||||
|
if (!await isServerUp('127.0.0.1', 8080)) {
|
||||||
|
markTestSkipped('Rtty server not running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final base = 'ws://127.0.0.1:8080/ws';
|
||||||
|
|
||||||
|
// 1. 桌面端(client=desktop):收 raw 二进制,不收 snapshot。
|
||||||
|
final desktop = IOWebSocketChannel.connect(Uri.parse('$base?client=desktop'));
|
||||||
|
final dOut = <String>[];
|
||||||
|
desktop.stream.listen((m) {
|
||||||
|
if (m is List<int>) {
|
||||||
|
dOut.add('binary:${utf8.decode(m, allowMalformed: true)}');
|
||||||
|
} else if (m is String) {
|
||||||
|
dOut.add('text:${m.length > 100 ? m.substring(0, 100) : m}');
|
||||||
|
} else {
|
||||||
|
dOut.add('$m');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 700));
|
||||||
|
final dHasBinary = dOut.any((e) => e.startsWith('binary:'));
|
||||||
|
final dHasSnapshot = dOut.any((e) => e.contains('mobile_snapshot'));
|
||||||
|
expect(dHasBinary, isTrue, reason: '桌面端应收到原始 ANSI 二进制 $dOut');
|
||||||
|
expect(dHasSnapshot, isFalse, reason: '桌面端不应收到语义快照 $dOut');
|
||||||
|
|
||||||
|
// 2. 移动端(client=mobile):收 snapshot,不收 binary。
|
||||||
|
final mobile = IOWebSocketChannel.connect(Uri.parse('$base?client=mobile'));
|
||||||
|
final mOut = <String>[];
|
||||||
|
mobile.stream.listen((m) {
|
||||||
|
if (m is List<int>) {
|
||||||
|
mOut.add('binary:${utf8.decode(m, allowMalformed: true)}');
|
||||||
|
} else if (m is String) {
|
||||||
|
mOut.add('text:${m.length > 100 ? m.substring(0, 100) : m}');
|
||||||
|
} else {
|
||||||
|
mOut.add('$m');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 700));
|
||||||
|
final mHasSnapshot = mOut.any((e) => e.contains('mobile_snapshot'));
|
||||||
|
final mHasBinary = mOut.any((e) => e.startsWith('binary:'));
|
||||||
|
expect(mHasSnapshot, isTrue, reason: '移动端应收到语义快照 $mOut');
|
||||||
|
expect(mHasBinary, isFalse, reason: '移动端不应收到二进制 $mOut');
|
||||||
|
|
||||||
|
// 3. 桌面端发命令,确认 raw 输出回显。
|
||||||
|
desktop.sink.add(jsonEncode({'type': 'input', 'data': 'echo D_E2E\r\n'}));
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 900));
|
||||||
|
expect(dOut.join('\n').contains('D_E2E'), isTrue, reason: '桌面端命令回显');
|
||||||
|
|
||||||
|
// 4. 控制权强制:桌面端 claim 后,移动端输入被拒。
|
||||||
|
desktop.sink.add(jsonEncode({'type': 'claim_control'}));
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 400));
|
||||||
|
mobile.sink
|
||||||
|
.add(jsonEncode({'type': 'input', 'data': 'echo BLOCKED_SHOULD_NOT_SHOW\r\n'}));
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 900));
|
||||||
|
final both = (dOut.join('\n') + mOut.join('\n'));
|
||||||
|
expect(both.contains('BLOCKED_SHOULD_NOT_SHOW'), isFalse,
|
||||||
|
reason: '非控制者输入应被阻止');
|
||||||
|
|
||||||
|
// 5. 会话清理:exit 触发会话移除。
|
||||||
|
desktop.sink.add(jsonEncode({'type': 'input', 'data': 'exit\r\n'}));
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 1300));
|
||||||
|
|
||||||
|
desktop.sink.close();
|
||||||
|
mobile.sink.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
14
desktop/test/support.dart
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// 集成测试共享工具。
|
||||||
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
|
||||||
|
/// 探测 Rtty 服务端是否可达;返回 false 时集成测试应被跳过。
|
||||||
|
Future<bool> isServerUp(String host, int port) async {
|
||||||
|
try {
|
||||||
|
final ws = IOWebSocketChannel.connect(Uri.parse('ws://$host:$port/ws'));
|
||||||
|
await ws.ready.timeout(const Duration(seconds: 2));
|
||||||
|
ws.sink.close();
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
desktop/test/widget_test.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:rtty_desktop/main.dart';
|
||||||
|
import 'package:rtty_desktop/src/terminal_screen.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Rtty desktop renders terminal screen', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const RttyApp());
|
||||||
|
|
||||||
|
// 未连接状态应显示连接提示与连接控制条。
|
||||||
|
expect(find.text('AWAITING CONNECTION'), findsOneWidget);
|
||||||
|
expect(find.text('CONNECT'), findsOneWidget);
|
||||||
|
expect(find.byType(TerminalScreen), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
17
desktop/windows/.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
flutter/ephemeral/
|
||||||
|
|
||||||
|
# Visual Studio user-specific files.
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Visual Studio build-related files.
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
108
desktop/windows/CMakeLists.txt
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(rtty_desktop LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "rtty_desktop")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(VERSION 3.14...3.25)
|
||||||
|
|
||||||
|
# Define build configuration option.
|
||||||
|
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||||
|
if(IS_MULTICONFIG)
|
||||||
|
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
|
||||||
|
CACHE STRING "" FORCE)
|
||||||
|
else()
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||||
|
STRING "Flutter build mode" FORCE)
|
||||||
|
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||||
|
"Debug" "Profile" "Release")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
# Define settings for the Profile build mode.
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
|
||||||
|
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||||
|
|
||||||
|
# Use Unicode for all projects.
|
||||||
|
add_definitions(-DUNICODE -D_UNICODE)
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_17)
|
||||||
|
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
|
||||||
|
target_compile_options(${TARGET} PRIVATE /EHsc)
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# Application build; see runner/CMakeLists.txt.
|
||||||
|
add_subdirectory("runner")
|
||||||
|
|
||||||
|
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
# === Installation ===
|
||||||
|
# Support files are copied into place next to the executable, so that it can
|
||||||
|
# run in place. This is done instead of making a separate bundle (as on Linux)
|
||||||
|
# so that building and running from within Visual Studio will work.
|
||||||
|
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||||
|
# Make the "install" step default, as it's required to run.
|
||||||
|
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
||||||
|
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||||
|
|
||||||
|
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Copy the native assets provided by the build.dart from all packages.
|
||||||
|
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
|
||||||
|
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||||
|
# from a previous install.
|
||||||
|
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Install the AOT library on non-Debug builds only.
|
||||||
|
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
CONFIGURATIONS Profile;Release
|
||||||
|
COMPONENT Runtime)
|
||||||
109
desktop/windows/flutter/CMakeLists.txt
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
||||||
|
|
||||||
|
# Set fallback configurations for older versions of the flutter tool.
|
||||||
|
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
|
||||||
|
set(FLUTTER_TARGET_PLATFORM "windows-x64")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# === Flutter Library ===
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"flutter_export.h"
|
||||||
|
"flutter_windows.h"
|
||||||
|
"flutter_messenger.h"
|
||||||
|
"flutter_plugin_registrar.h"
|
||||||
|
"flutter_texture_registrar.h"
|
||||||
|
)
|
||||||
|
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Wrapper ===
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_CORE
|
||||||
|
"core_implementations.cc"
|
||||||
|
"standard_codec.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
|
||||||
|
"plugin_registrar.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
list(APPEND CPP_WRAPPER_SOURCES_APP
|
||||||
|
"flutter_engine.cc"
|
||||||
|
"flutter_view_controller.cc"
|
||||||
|
)
|
||||||
|
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
|
||||||
|
|
||||||
|
# Wrapper sources needed for a plugin.
|
||||||
|
add_library(flutter_wrapper_plugin STATIC
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
)
|
||||||
|
apply_standard_settings(flutter_wrapper_plugin)
|
||||||
|
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||||
|
POSITION_INDEPENDENT_CODE ON)
|
||||||
|
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||||
|
CXX_VISIBILITY_PRESET hidden)
|
||||||
|
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
|
||||||
|
target_include_directories(flutter_wrapper_plugin PUBLIC
|
||||||
|
"${WRAPPER_ROOT}/include"
|
||||||
|
)
|
||||||
|
add_dependencies(flutter_wrapper_plugin flutter_assemble)
|
||||||
|
|
||||||
|
# Wrapper sources needed for the runner.
|
||||||
|
add_library(flutter_wrapper_app STATIC
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
)
|
||||||
|
apply_standard_settings(flutter_wrapper_app)
|
||||||
|
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
|
||||||
|
target_include_directories(flutter_wrapper_app PUBLIC
|
||||||
|
"${WRAPPER_ROOT}/include"
|
||||||
|
)
|
||||||
|
add_dependencies(flutter_wrapper_app flutter_assemble)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
|
||||||
|
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
${PHONY_OUTPUT}
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||||
|
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CPP_WRAPPER_SOURCES_CORE}
|
||||||
|
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||||
|
${CPP_WRAPPER_SOURCES_APP}
|
||||||
|
)
|
||||||
17
desktop/windows/flutter/generated_plugin_registrant.cc
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <desktop_drop/desktop_drop_plugin.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
DesktopDropPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
|
}
|
||||||
15
desktop/windows/flutter/generated_plugin_registrant.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
#define GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
|
||||||
|
#include <flutter/plugin_registry.h>
|
||||||
|
|
||||||
|
// Registers Flutter plugins.
|
||||||
|
void RegisterPlugins(flutter::PluginRegistry* registry);
|
||||||
|
|
||||||
|
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||||
28
desktop/windows/flutter/generated_plugins.cmake
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#
|
||||||
|
# Generated file, do not edit.
|
||||||
|
#
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
desktop_drop
|
||||||
|
url_launcher_windows
|
||||||
|
)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
flutter_pty_new
|
||||||
|
jni
|
||||||
|
rust_lib_flutter_alacritty
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
||||||
|
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||||
|
endforeach(plugin)
|
||||||
|
|
||||||
|
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||||
|
endforeach(ffi_plugin)
|
||||||
40
desktop/windows/runner/CMakeLists.txt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# Define the application target. To change its name, change BINARY_NAME in the
|
||||||
|
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||||
|
# work.
|
||||||
|
#
|
||||||
|
# Any new source files that you add to the application should be added here.
|
||||||
|
add_executable(${BINARY_NAME} WIN32
|
||||||
|
"flutter_window.cpp"
|
||||||
|
"main.cpp"
|
||||||
|
"utils.cpp"
|
||||||
|
"win32_window.cpp"
|
||||||
|
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||||
|
"Runner.rc"
|
||||||
|
"runner.exe.manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply the standard set of build settings. This can be removed for applications
|
||||||
|
# that need different build settings.
|
||||||
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Add preprocessor definitions for the build version.
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||||
|
|
||||||
|
# Disable Windows macros that collide with C++ standard library functions.
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||||
|
|
||||||
|
# Add dependency libraries and include directories. Add any application-specific
|
||||||
|
# dependencies here.
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
|
||||||
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
|
|
||||||
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
121
desktop/windows/runner/Runner.rc
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Microsoft Visual C++ generated resource script.
|
||||||
|
//
|
||||||
|
#pragma code_page(65001)
|
||||||
|
#include "resource.h"
|
||||||
|
|
||||||
|
#define APSTUDIO_READONLY_SYMBOLS
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 2 resource.
|
||||||
|
//
|
||||||
|
#include "winres.h"
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#undef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// English (United States) resources
|
||||||
|
|
||||||
|
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||||
|
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||||
|
|
||||||
|
#ifdef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// TEXTINCLUDE
|
||||||
|
//
|
||||||
|
|
||||||
|
1 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"resource.h\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
2 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"#include ""winres.h""\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
3 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
#endif // APSTUDIO_INVOKED
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Icon
|
||||||
|
//
|
||||||
|
|
||||||
|
// Icon with lowest ID value placed first to ensure application icon
|
||||||
|
// remains consistent on all systems.
|
||||||
|
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Version
|
||||||
|
//
|
||||||
|
|
||||||
|
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
||||||
|
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
||||||
|
#else
|
||||||
|
#define VERSION_AS_NUMBER 1,0,0,0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(FLUTTER_VERSION)
|
||||||
|
#define VERSION_AS_STRING FLUTTER_VERSION
|
||||||
|
#else
|
||||||
|
#define VERSION_AS_STRING "1.0.0"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION VERSION_AS_NUMBER
|
||||||
|
PRODUCTVERSION VERSION_AS_NUMBER
|
||||||
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
|
#ifdef _DEBUG
|
||||||
|
FILEFLAGS VS_FF_DEBUG
|
||||||
|
#else
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
#endif
|
||||||
|
FILEOS VOS__WINDOWS32
|
||||||
|
FILETYPE VFT_APP
|
||||||
|
FILESUBTYPE 0x0L
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904e4"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "com.example" "\0"
|
||||||
|
VALUE "FileDescription", "rtty_desktop" "\0"
|
||||||
|
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||||
|
VALUE "InternalName", "rtty_desktop" "\0"
|
||||||
|
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
|
||||||
|
VALUE "OriginalFilename", "rtty_desktop.exe" "\0"
|
||||||
|
VALUE "ProductName", "rtty_desktop" "\0"
|
||||||
|
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1252
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
||||||
|
#endif // English (United States) resources
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 3 resource.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#endif // not APSTUDIO_INVOKED
|
||||||
71
desktop/windows/runner/flutter_window.cpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#include "flutter_window.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||||
|
: project_(project) {}
|
||||||
|
|
||||||
|
FlutterWindow::~FlutterWindow() {}
|
||||||
|
|
||||||
|
bool FlutterWindow::OnCreate() {
|
||||||
|
if (!Win32Window::OnCreate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT frame = GetClientArea();
|
||||||
|
|
||||||
|
// The size here must match the window dimensions to avoid unnecessary surface
|
||||||
|
// creation / destruction in the startup path.
|
||||||
|
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||||||
|
frame.right - frame.left, frame.bottom - frame.top, project_);
|
||||||
|
// Ensure that basic setup of the controller was successful.
|
||||||
|
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RegisterPlugins(flutter_controller_->engine());
|
||||||
|
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||||
|
|
||||||
|
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||||
|
this->Show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flutter can complete the first frame before the "show window" callback is
|
||||||
|
// registered. The following call ensures a frame is pending to ensure the
|
||||||
|
// window is shown. It is a no-op if the first frame hasn't completed yet.
|
||||||
|
flutter_controller_->ForceRedraw();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlutterWindow::OnDestroy() {
|
||||||
|
if (flutter_controller_) {
|
||||||
|
flutter_controller_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::OnDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT
|
||||||
|
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
// Give Flutter, including plugins, an opportunity to handle window messages.
|
||||||
|
if (flutter_controller_) {
|
||||||
|
std::optional<LRESULT> result =
|
||||||
|
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
||||||
|
lparam);
|
||||||
|
if (result) {
|
||||||
|
return *result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
case WM_FONTCHANGE:
|
||||||
|
flutter_controller_->engine()->ReloadSystemFonts();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
||||||
|
}
|
||||||
33
desktop/windows/runner/flutter_window.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#ifndef RUNNER_FLUTTER_WINDOW_H_
|
||||||
|
#define RUNNER_FLUTTER_WINDOW_H_
|
||||||
|
|
||||||
|
#include <flutter/dart_project.h>
|
||||||
|
#include <flutter/flutter_view_controller.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "win32_window.h"
|
||||||
|
|
||||||
|
// A window that does nothing but host a Flutter view.
|
||||||
|
class FlutterWindow : public Win32Window {
|
||||||
|
public:
|
||||||
|
// Creates a new FlutterWindow hosting a Flutter view running |project|.
|
||||||
|
explicit FlutterWindow(const flutter::DartProject& project);
|
||||||
|
virtual ~FlutterWindow();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Win32Window:
|
||||||
|
bool OnCreate() override;
|
||||||
|
void OnDestroy() override;
|
||||||
|
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// The project to run.
|
||||||
|
flutter::DartProject project_;
|
||||||
|
|
||||||
|
// The Flutter instance hosted by this window.
|
||||||
|
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // RUNNER_FLUTTER_WINDOW_H_
|
||||||
43
desktop/windows/runner/main.cpp
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#include <flutter/dart_project.h>
|
||||||
|
#include <flutter/flutter_view_controller.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "flutter_window.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
|
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||||
|
// Attach to console when present (e.g., 'flutter run') or create a
|
||||||
|
// new console when running with a debugger.
|
||||||
|
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||||
|
CreateAndAttachConsole();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize COM, so that it is available for use in the library and/or
|
||||||
|
// plugins.
|
||||||
|
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||||
|
|
||||||
|
flutter::DartProject project(L"data");
|
||||||
|
|
||||||
|
std::vector<std::string> command_line_arguments =
|
||||||
|
GetCommandLineArguments();
|
||||||
|
|
||||||
|
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||||
|
|
||||||
|
FlutterWindow window(project);
|
||||||
|
Win32Window::Point origin(10, 10);
|
||||||
|
Win32Window::Size size(1280, 720);
|
||||||
|
if (!window.Create(L"rtty_desktop", origin, size)) {
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
window.SetQuitOnClose(true);
|
||||||
|
|
||||||
|
::MSG msg;
|
||||||
|
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
||||||
|
::TranslateMessage(&msg);
|
||||||
|
::DispatchMessage(&msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
::CoUninitialize();
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
16
desktop/windows/runner/resource.h
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//{{NO_DEPENDENCIES}}
|
||||||
|
// Microsoft Visual C++ generated include file.
|
||||||
|
// Used by Runner.rc
|
||||||
|
//
|
||||||
|
#define IDI_APP_ICON 101
|
||||||
|
|
||||||
|
// Next default values for new objects
|
||||||
|
//
|
||||||
|
#ifdef APSTUDIO_INVOKED
|
||||||
|
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
#define _APS_NEXT_RESOURCE_VALUE 102
|
||||||
|
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||||
|
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||||
|
#define _APS_NEXT_SYMED_VALUE 101
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
BIN
desktop/windows/runner/resources/app_icon.ico
Normal file
|
After Width: | Height: | Size: 33 KiB |
14
desktop/windows/runner/runner.exe.manifest
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- Windows 10 and Windows 11 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
69
desktop/windows/runner/utils.cpp
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include <flutter_windows.h>
|
||||||
|
#include <io.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
void CreateAndAttachConsole() {
|
||||||
|
if (::AllocConsole()) {
|
||||||
|
FILE *unused;
|
||||||
|
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
|
||||||
|
_dup2(_fileno(stdout), 1);
|
||||||
|
}
|
||||||
|
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
|
||||||
|
_dup2(_fileno(stdout), 2);
|
||||||
|
}
|
||||||
|
std::ios::sync_with_stdio();
|
||||||
|
FlutterDesktopResyncOutputStreams();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> GetCommandLineArguments() {
|
||||||
|
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
|
||||||
|
int argc;
|
||||||
|
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
|
||||||
|
if (argv == nullptr) {
|
||||||
|
return std::vector<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> command_line_arguments;
|
||||||
|
|
||||||
|
// Skip the first argument as it's the binary name.
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
::LocalFree(argv);
|
||||||
|
|
||||||
|
return command_line_arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
|
||||||
|
if (utf16_string == nullptr) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
// First, find the length of the string with a safe upper bound (CWE-126).
|
||||||
|
// UNICODE_STRING_MAX_CHARS (32767) is the maximum length of a UNICODE_STRING.
|
||||||
|
int input_length = static_cast<int>(wcsnlen(utf16_string, UNICODE_STRING_MAX_CHARS));
|
||||||
|
// Now use that bounded length to determine the required buffer size.
|
||||||
|
// When an explicit length is passed, WideCharToMultiByte does not include
|
||||||
|
// the null terminator in its returned size.
|
||||||
|
int target_length = ::WideCharToMultiByte(
|
||||||
|
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||||
|
input_length, nullptr, 0, nullptr, nullptr);
|
||||||
|
std::string utf8_string;
|
||||||
|
if (target_length == 0 || static_cast<size_t>(target_length) > utf8_string.max_size()) {
|
||||||
|
return utf8_string;
|
||||||
|
}
|
||||||
|
utf8_string.resize(target_length);
|
||||||
|
int converted_length = ::WideCharToMultiByte(
|
||||||
|
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||||
|
input_length, utf8_string.data(), target_length, nullptr, nullptr);
|
||||||
|
if (converted_length == 0) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
return utf8_string;
|
||||||
|
}
|
||||||
19
desktop/windows/runner/utils.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#ifndef RUNNER_UTILS_H_
|
||||||
|
#define RUNNER_UTILS_H_
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Creates a console for the process, and redirects stdout and stderr to
|
||||||
|
// it for both the runner and the Flutter library.
|
||||||
|
void CreateAndAttachConsole();
|
||||||
|
|
||||||
|
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
|
||||||
|
// encoded in UTF-8. Returns an empty std::string on failure.
|
||||||
|
std::string Utf8FromUtf16(const wchar_t* utf16_string);
|
||||||
|
|
||||||
|
// Gets the command line arguments passed in as a std::vector<std::string>,
|
||||||
|
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
|
||||||
|
std::vector<std::string> GetCommandLineArguments();
|
||||||
|
|
||||||
|
#endif // RUNNER_UTILS_H_
|
||||||
288
desktop/windows/runner/win32_window.cpp
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
#include "win32_window.h"
|
||||||
|
|
||||||
|
#include <dwmapi.h>
|
||||||
|
#include <flutter_windows.h>
|
||||||
|
|
||||||
|
#include "resource.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// Window attribute that enables dark mode window decorations.
|
||||||
|
///
|
||||||
|
/// Redefined in case the developer's machine has a Windows SDK older than
|
||||||
|
/// version 10.0.22000.0.
|
||||||
|
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||||
|
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||||
|
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||||
|
#endif
|
||||||
|
|
||||||
|
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||||
|
|
||||||
|
/// Registry key for app theme preference.
|
||||||
|
///
|
||||||
|
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
|
||||||
|
/// value indicates apps should use light mode.
|
||||||
|
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
|
||||||
|
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||||
|
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
|
||||||
|
|
||||||
|
// The number of Win32Window objects that currently exist.
|
||||||
|
static int g_active_window_count = 0;
|
||||||
|
|
||||||
|
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||||
|
|
||||||
|
// Scale helper to convert logical scaler values to physical using passed in
|
||||||
|
// scale factor
|
||||||
|
int Scale(int source, double scale_factor) {
|
||||||
|
return static_cast<int>(source * scale_factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
||||||
|
// This API is only needed for PerMonitor V1 awareness mode.
|
||||||
|
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
|
||||||
|
HMODULE user32_module = LoadLibraryA("User32.dll");
|
||||||
|
if (!user32_module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto enable_non_client_dpi_scaling =
|
||||||
|
reinterpret_cast<EnableNonClientDpiScaling*>(
|
||||||
|
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||||
|
if (enable_non_client_dpi_scaling != nullptr) {
|
||||||
|
enable_non_client_dpi_scaling(hwnd);
|
||||||
|
}
|
||||||
|
FreeLibrary(user32_module);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Manages the Win32Window's window class registration.
|
||||||
|
class WindowClassRegistrar {
|
||||||
|
public:
|
||||||
|
~WindowClassRegistrar() = default;
|
||||||
|
|
||||||
|
// Returns the singleton registrar instance.
|
||||||
|
static WindowClassRegistrar* GetInstance() {
|
||||||
|
if (!instance_) {
|
||||||
|
instance_ = new WindowClassRegistrar();
|
||||||
|
}
|
||||||
|
return instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the name of the window class, registering the class if it hasn't
|
||||||
|
// previously been registered.
|
||||||
|
const wchar_t* GetWindowClass();
|
||||||
|
|
||||||
|
// Unregisters the window class. Should only be called if there are no
|
||||||
|
// instances of the window.
|
||||||
|
void UnregisterWindowClass();
|
||||||
|
|
||||||
|
private:
|
||||||
|
WindowClassRegistrar() = default;
|
||||||
|
|
||||||
|
static WindowClassRegistrar* instance_;
|
||||||
|
|
||||||
|
bool class_registered_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
||||||
|
|
||||||
|
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||||
|
if (!class_registered_) {
|
||||||
|
WNDCLASS window_class{};
|
||||||
|
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||||
|
window_class.lpszClassName = kWindowClassName;
|
||||||
|
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
window_class.cbClsExtra = 0;
|
||||||
|
window_class.cbWndExtra = 0;
|
||||||
|
window_class.hInstance = GetModuleHandle(nullptr);
|
||||||
|
window_class.hIcon =
|
||||||
|
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||||
|
window_class.hbrBackground = 0;
|
||||||
|
window_class.lpszMenuName = nullptr;
|
||||||
|
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||||
|
RegisterClass(&window_class);
|
||||||
|
class_registered_ = true;
|
||||||
|
}
|
||||||
|
return kWindowClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||||
|
UnregisterClass(kWindowClassName, nullptr);
|
||||||
|
class_registered_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::Win32Window() {
|
||||||
|
++g_active_window_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::~Win32Window() {
|
||||||
|
--g_active_window_count;
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::Create(const std::wstring& title,
|
||||||
|
const Point& origin,
|
||||||
|
const Size& size) {
|
||||||
|
Destroy();
|
||||||
|
|
||||||
|
const wchar_t* window_class =
|
||||||
|
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
||||||
|
|
||||||
|
const POINT target_point = {static_cast<LONG>(origin.x),
|
||||||
|
static_cast<LONG>(origin.y)};
|
||||||
|
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
|
||||||
|
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
|
||||||
|
double scale_factor = dpi / 96.0;
|
||||||
|
|
||||||
|
HWND window = CreateWindow(
|
||||||
|
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
|
||||||
|
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
||||||
|
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
||||||
|
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
||||||
|
|
||||||
|
if (!window) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateTheme(window);
|
||||||
|
|
||||||
|
return OnCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::Show() {
|
||||||
|
return ShowWindow(window_handle_, SW_SHOWNORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
||||||
|
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||||
|
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||||
|
|
||||||
|
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||||
|
EnableFullDpiSupportIfAvailable(window);
|
||||||
|
that->window_handle_ = window;
|
||||||
|
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||||
|
return that->MessageHandler(window, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(window, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT
|
||||||
|
Win32Window::MessageHandler(HWND hwnd,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
switch (message) {
|
||||||
|
case WM_DESTROY:
|
||||||
|
window_handle_ = nullptr;
|
||||||
|
Destroy();
|
||||||
|
if (quit_on_close_) {
|
||||||
|
PostQuitMessage(0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_DPICHANGED: {
|
||||||
|
auto newRectSize = reinterpret_cast<RECT*>(lparam);
|
||||||
|
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||||
|
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||||
|
|
||||||
|
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
||||||
|
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WM_SIZE: {
|
||||||
|
RECT rect = GetClientArea();
|
||||||
|
if (child_content_ != nullptr) {
|
||||||
|
// Size and position the child window.
|
||||||
|
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||||
|
rect.bottom - rect.top, TRUE);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
case WM_ACTIVATE:
|
||||||
|
if (child_content_ != nullptr) {
|
||||||
|
SetFocus(child_content_);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_DWMCOLORIZATIONCOLORCHANGED:
|
||||||
|
UpdateTheme(hwnd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::Destroy() {
|
||||||
|
OnDestroy();
|
||||||
|
|
||||||
|
if (window_handle_) {
|
||||||
|
DestroyWindow(window_handle_);
|
||||||
|
window_handle_ = nullptr;
|
||||||
|
}
|
||||||
|
if (g_active_window_count == 0) {
|
||||||
|
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
||||||
|
return reinterpret_cast<Win32Window*>(
|
||||||
|
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::SetChildContent(HWND content) {
|
||||||
|
child_content_ = content;
|
||||||
|
SetParent(content, window_handle_);
|
||||||
|
RECT frame = GetClientArea();
|
||||||
|
|
||||||
|
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||||
|
frame.bottom - frame.top, true);
|
||||||
|
|
||||||
|
SetFocus(child_content_);
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT Win32Window::GetClientArea() {
|
||||||
|
RECT frame;
|
||||||
|
GetClientRect(window_handle_, &frame);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND Win32Window::GetHandle() {
|
||||||
|
return window_handle_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
||||||
|
quit_on_close_ = quit_on_close;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::OnCreate() {
|
||||||
|
// No-op; provided for subclasses.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::OnDestroy() {
|
||||||
|
// No-op; provided for subclasses.
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::UpdateTheme(HWND const window) {
|
||||||
|
DWORD light_mode;
|
||||||
|
DWORD light_mode_size = sizeof(light_mode);
|
||||||
|
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
|
||||||
|
kGetPreferredBrightnessRegValue,
|
||||||
|
RRF_RT_REG_DWORD, nullptr, &light_mode,
|
||||||
|
&light_mode_size);
|
||||||
|
|
||||||
|
if (result == ERROR_SUCCESS) {
|
||||||
|
BOOL enable_dark_mode = light_mode == 0;
|
||||||
|
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||||
|
&enable_dark_mode, sizeof(enable_dark_mode));
|
||||||
|
}
|
||||||
|
}
|
||||||
102
desktop/windows/runner/win32_window.h
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||||
|
#define RUNNER_WIN32_WINDOW_H_
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
||||||
|
// inherited from by classes that wish to specialize with custom
|
||||||
|
// rendering and input handling
|
||||||
|
class Win32Window {
|
||||||
|
public:
|
||||||
|
struct Point {
|
||||||
|
unsigned int x;
|
||||||
|
unsigned int y;
|
||||||
|
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Size {
|
||||||
|
unsigned int width;
|
||||||
|
unsigned int height;
|
||||||
|
Size(unsigned int width, unsigned int height)
|
||||||
|
: width(width), height(height) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Win32Window();
|
||||||
|
virtual ~Win32Window();
|
||||||
|
|
||||||
|
// Creates a win32 window with |title| that is positioned and sized using
|
||||||
|
// |origin| and |size|. New windows are created on the default monitor. Window
|
||||||
|
// sizes are specified to the OS in physical pixels, hence to ensure a
|
||||||
|
// consistent size this function will scale the inputted width and height as
|
||||||
|
// as appropriate for the default monitor. The window is invisible until
|
||||||
|
// |Show| is called. Returns true if the window was created successfully.
|
||||||
|
bool Create(const std::wstring& title, const Point& origin, const Size& size);
|
||||||
|
|
||||||
|
// Show the current window. Returns true if the window was successfully shown.
|
||||||
|
bool Show();
|
||||||
|
|
||||||
|
// Release OS resources associated with window.
|
||||||
|
void Destroy();
|
||||||
|
|
||||||
|
// Inserts |content| into the window tree.
|
||||||
|
void SetChildContent(HWND content);
|
||||||
|
|
||||||
|
// Returns the backing Window handle to enable clients to set icon and other
|
||||||
|
// window properties. Returns nullptr if the window has been destroyed.
|
||||||
|
HWND GetHandle();
|
||||||
|
|
||||||
|
// If true, closing this window will quit the application.
|
||||||
|
void SetQuitOnClose(bool quit_on_close);
|
||||||
|
|
||||||
|
// Return a RECT representing the bounds of the current client area.
|
||||||
|
RECT GetClientArea();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Processes and route salient window messages for mouse handling,
|
||||||
|
// size change and DPI. Delegates handling of these to member overloads that
|
||||||
|
// inheriting classes can handle.
|
||||||
|
virtual LRESULT MessageHandler(HWND window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept;
|
||||||
|
|
||||||
|
// Called when CreateAndShow is called, allowing subclass window-related
|
||||||
|
// setup. Subclasses should return false if setup fails.
|
||||||
|
virtual bool OnCreate();
|
||||||
|
|
||||||
|
// Called when Destroy is called.
|
||||||
|
virtual void OnDestroy();
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class WindowClassRegistrar;
|
||||||
|
|
||||||
|
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
||||||
|
// is passed when the non-client area is being created and enables automatic
|
||||||
|
// non-client DPI scaling so that the non-client area automatically
|
||||||
|
// responds to changes in DPI. All other messages are handled by
|
||||||
|
// MessageHandler.
|
||||||
|
static LRESULT CALLBACK WndProc(HWND const window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept;
|
||||||
|
|
||||||
|
// Retrieves a class instance pointer for |window|
|
||||||
|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
||||||
|
|
||||||
|
// Update the window frame's theme to match the system theme.
|
||||||
|
static void UpdateTheme(HWND const window);
|
||||||
|
|
||||||
|
bool quit_on_close_ = false;
|
||||||
|
|
||||||
|
// window handle for top level window.
|
||||||
|
HWND window_handle_ = nullptr;
|
||||||
|
|
||||||
|
// window handle for hosted content.
|
||||||
|
HWND child_content_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // RUNNER_WIN32_WINDOW_H_
|
||||||
45
mobile/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
33
mobile/.metadata
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "ad70ec4617166f1c38e5d2bfd388af71fda14f06"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
- platform: android
|
||||||
|
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
- platform: ios
|
||||||
|
create_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
base_revision: ad70ec4617166f1c38e5d2bfd388af71fda14f06
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
17
mobile/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# rtty_mobile
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
||||||
|
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
28
mobile/analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
14
mobile/android/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
45
mobile/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.rtty_mobile"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.example.rtty_mobile"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
7
mobile/android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
48
mobile/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- 访问服务端 WebSocket 需要网络权限。 -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<application
|
||||||
|
android:label="rtty_mobile"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.example.rtty_mobile
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
BIN
mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
mobile/android/app/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
18
mobile/android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
7
mobile/android/app/src/profile/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
24
mobile/android/build.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
6
mobile/android/gradle.properties
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
# This newDsl flag was added by the Flutter template
|
||||||
|
android.newDsl=false
|
||||||
|
# This builtInKotlin flag was added by the Flutter template
|
||||||
|
android.builtInKotlin=false
|
||||||
5
mobile/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
|
||||||
26
mobile/android/settings.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "9.0.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
34
mobile/ios/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
||||||
24
mobile/ios/Flutter/AppFrameworkInfo.plist
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1
mobile/ios/Flutter/Debug.xcconfig
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
1
mobile/ios/Flutter/Release.xcconfig
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
644
mobile/ios/Runner.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,644 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 54;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
|
remoteInfo = Runner;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
name = "Embed Frameworks";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||||
|
);
|
||||||
|
path = RunnerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = RunnerTests;
|
||||||
|
productName = RunnerTests;
|
||||||
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1510;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
331C8080294A63A400263BE5 = {
|
||||||
|
CreatedOnToolsVersion = 14.0;
|
||||||
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D = {
|
||||||
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||||
|
);
|
||||||
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
331C807F294A63A400263BE5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
331C807D294A63A400263BE5 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C146FB1CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C147001CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.rttyMobile;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.rttyMobile.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.rttyMobile.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.rttyMobile.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
97C147031CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147041CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
97C147061CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.rttyMobile;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147071CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.rttyMobile;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
331C8088294A63A400263BE5 /* Debug */,
|
||||||
|
331C8089294A63A400263BE5 /* Release */,
|
||||||
|
331C808A294A63A400263BE5 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147031CF9000F007C117D /* Debug */,
|
||||||
|
97C147041CF9000F007C117D /* Release */,
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147061CF9000F007C117D /* Debug */,
|
||||||
|
97C147071CF9000F007C117D /* Release */,
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
};
|
||||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
}
|
||||||
7
mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
7
mobile/ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
16
mobile/ios/Runner/AppDelegate.swift
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
23
mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Launch Screen Assets
|
||||||
|
|
||||||
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||||
|
|
||||||
|
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||||
37
mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="LaunchImage" width="168" height="185"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
26
mobile/ios/Runner/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Flutter View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||