Repository: nightmare-space/code_lfa
Branch: main
Commit: 2eb268555e91
Files: 55
Total size: 61.3 MB
Directory structure:
gitextract_qyg_xcvc/
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ └── bug_report.md
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .gitmodules
├── .metadata
├── .vscode/
│ ├── launch.json
│ └── settings.json
├── CHANGELOG.md
├── LICENSE
├── README-ZH.md
├── README.md
├── analysis_options.yaml
├── android/
│ ├── .gitignore
│ ├── app/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── debug/
│ │ │ └── AndroidManifest.xml
│ │ ├── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java/
│ │ │ │ └── com/
│ │ │ │ └── nightmare/
│ │ │ │ └── code/
│ │ │ │ ├── MainActivity.java
│ │ │ │ ├── OrientationListener.java
│ │ │ │ ├── WebViewFragment.java
│ │ │ │ └── WebViewUtil.java
│ │ │ └── res/
│ │ │ ├── drawable/
│ │ │ │ └── launch_background.xml
│ │ │ ├── drawable-v21/
│ │ │ │ └── launch_background.xml
│ │ │ ├── layout/
│ │ │ │ ├── my_activity_layout.xml
│ │ │ │ └── webview.xml
│ │ │ ├── values/
│ │ │ │ └── styles.xml
│ │ │ ├── values-night/
│ │ │ │ └── styles.xml
│ │ │ └── xml/
│ │ │ └── network_security_config.xml
│ │ └── profile/
│ │ └── AndroidManifest.xml
│ ├── build.gradle
│ ├── gradle/
│ │ └── wrapper/
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ └── settings.gradle
├── assets/
│ ├── privacy_policy.md
│ └── ubuntu-noble-aarch64-pd-v4.18.0.tar.xz
├── lib/
│ ├── config.dart
│ ├── generated/
│ │ ├── intl/
│ │ │ ├── messages_all.dart
│ │ │ ├── messages_en.dart
│ │ │ └── messages_zh_CN.dart
│ │ └── l10n.dart
│ ├── l10n/
│ │ ├── intl_en.arb
│ │ └── intl_zh_CN.arb
│ ├── main.dart
│ ├── script.dart
│ ├── terminal_controller.dart
│ ├── terminal_page.dart
│ ├── terminal_theme.dart
│ └── utils.dart
├── package.json
├── pubspec.yaml
├── pubspec_overrides.yaml
└── scripts/
├── check_hardlink.sh
├── gen_icon.sh
├── inject.js
└── properties.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
assets/code-server-4.103.1-linux-arm64.tar.gz filter=lfs diff=lfs merge=lfs -text
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**重要警告**
如果是不会使用 VS Code
如果不会使用 Ubuntu
请 Google 或者问 GPT
该 Issue 只收集 Code FA 本身的问题,以及建议
不教学 VS Code 的使用方法,不教学 Ubuntu 的使用方法
如还是询问无关信息,我会直接关闭 Issue
================================================
FILE: .github/workflows/main.yml
================================================
name: "Build & Release"
# see https://medium.com/@colonal/automating-flutter-builds-and-releases-with-github-actions-77ccf4a1ccdd
on:
workflow_dispatch:
jobs:
build:
name: Build & Release
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Extract version from pubspec.yaml
id: extract_version
run: |
version=$(grep '^version: ' pubspec.yaml | cut -d ' ' -f 2 | tr -d '\r')
echo "VERSION=$version" >> $GITHUB_ENV
code_server=$(grep '^code_server: ' pubspec.yaml | cut -d ' ' -f 2 | tr -d '\r')
echo "CSVERSION=$code_server" >> $GITHUB_ENV
- name: Download code-server
run: wget -O assets/code-server-${{ env.CSVERSION }}-linux-arm64.tar.gz https://github.com/coder/code-server/releases/download/v${{ env.CSVERSION }}/code-server-${{ env.CSVERSION }}-linux-arm64.tar.gz
- name: Set Up Java
uses: actions/setup-java@v3.12.0
with:
distribution: 'oracle'
java-version: '17'
- name: Set Up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.4'
channel: 'stable'
- name: Install Dependencies
run: flutter pub get
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks
- name: Create key.properties
run: |
echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" > android/key.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
echo "storeFile=keystore.jks" >> android/key.properties
- name: Build APK
run: flutter build apk --release --split-per-abi --dart-define=VERSION=${{ env.VERSION }} --dart-define=CSVERSION=${{ env.CSVERSION }}
#FIXME: - name: Check if Tag Exists
#FIXME: id: check_tag
#FIXME: run: |
#FIXME: if git rev-parse "v${{ env.VERSION }}" >/dev/null 2>&1; then
#FIXME: echo "TAG_EXISTS=true" >> $GITHUB_ENV
#FIXME: else
#FIXME: echo "TAG_EXISTS=false" >> $GITHUB_ENV
#FIXME: fi
- name: Modify Tag
#FIXME: if: env.TAG_EXISTS == 'true'
id: modify_tag
run: |
new_version="${{ env.VERSION }}-build-${{ github.run_number }}"
echo "VERSION=$new_version" >> $GITHUB_ENV
- name: Rename Artifacts
run: |
mv build/app/outputs/apk/release/app-arm64-*release.apk "build/app/outputs/apk/release/CodeFA_${{ env.VERSION }}_Android_arm64.apk"
mv build/app/outputs/apk/release/app-armeabi-*release.apk "build/app/outputs/apk/release/CodeFA_${{ env.VERSION }}_Android_arm_v7a.apk"
mv build/app/outputs/apk/release/app-x86_64-*release.apk "build/app/outputs/apk/release/CodeFA_${{ env.VERSION }}_Android_x86_64.apk"
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: Releases
path: |
build/app/outputs/apk/release/*_arm64.apk
- name: Create Release
uses: ncipollo/release-action@v1
with:
artifacts: "build/app/outputs/apk/release/*_arm64.apk"
tag: v${{ env.VERSION }}
token: ${{ secrets.TOKEN }}
================================================
FILE: .gitignore
================================================
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# 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
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# 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
code-server-3.12.0-linux-arm64.tar.gz
code-server-4.0.1-linux-arm64.tar.gz
code-server-4.4.0-linux-arm64.tar.gz
/tmp
.cxx
dist/
================================================
FILE: .gitmodules
================================================
[submodule "global_repository"]
path = global_repository
url = https://github.com/nightmare-space/global_repository
[submodule "settings"]
path = settings
url = https://github.com/nightmare-space/settings
================================================
FILE: .metadata
================================================
# 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: "8495dee1fd4aacbe9de707e7581203232f591b2f"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
- platform: android
create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
- platform: ios
create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
- platform: linux
create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
- platform: macos
create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
- platform: web
create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
- platform: windows
create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f
# 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'
================================================
FILE: .vscode/launch.json
================================================
{
"version": "0.2.0",
"configurations": [
{
"name": "Flutter",
"type": "dart",
"request": "launch",
"program": "lib/main.dart"
},
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"editor.formatOnSave": true,
"dart.lineLength": 200,
"files.associations": {
"*.arb": "json"
},
"[json]": {
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.detectIndentation": false,
"editor.defaultFormatter": "vscode.json-language-features"
}
}
================================================
FILE: CHANGELOG.md
================================================
## 1.6.0
- Upgrade proot-distro 4.28.0
- Upgrade code-server to 4.103.1
- Upgrade ubuntu to noble-aarch64-pd-v4.18.0
- 优化进度条逻辑,不再使用`魔数`
- 支持 I18n,目前支持中文和英语
- 代码上的诸多优化
## 1.5.1
病重了好几个月,现在由我之前住院的主治勉强救活,不知道当前的状态能撑多久
- 修复了一个偶现的问题
## 1.5.0
很抱歉,所有还在用 Code FA 的用户,这个项目的维护并不积极,Code LFA 免费且开源,所有的投入都是我无偿奉献出我的时间,而且我很难从这一切中找到平衡,后续也会面临经济上的一些问题
简单了解过我的朋友可能知道,我手上的项目非常的多,也非常的忙
项目更名为 code_lfa
Code LFA(Code Launcher For Android)
如名称所示,这只是一个启动器,并不是自己实现的 VS Code,在过去,它经常会带来一些歧义,更有甚者会有人辱骂我违反了开源协议
我并未使用任何 code-server 的代码,code-server 也是以压缩包的方式存在 Code LFA 中,更何况 Code LFA 本身也是开源的
后续会更新 Readme,部分朋友总是觉得安装依赖是 Code FA 的问题,所以我准备加了一些简单的说明
其实绝大部分的问题,都是大家完全不会使用 Ubuntu 导致的,也不会使用 apt
不要把在 Windows 上使用 VS Code 的习惯带到 Code LFA 上,不要问怎么安装这怎么安装那,遇到问题,请问 GPT : 如何在 Ubuntu 的 VS Code 的 Terminal 中上安装 xxx
### 主要更新
**1.移除 termux 环境**
在以往的版本中,Code LFA 其实包含了一个完整的 termux 环境,简单说是,里面内置了一个和 termux 一模一样的类 Linux 环境,而这都是需要修改包名,重新编译 termux-package 的 bootstrap,这个过程非常复杂,而我精力分散后,这部分几乎无法维护,并且会增加 26M 的 apk 体积
这部分去除后,原有的包体积增加只需要 1.6M
相关依赖来源
- bash: [bash-in-magisk](https://github.com/i-Taylo/bash-in-magisk), proot-distro 语法依赖
- busybox: [busybox-ndk](https://github.com/Magisk-Modules-Repo/busybox-ndk),proot-distro 需要依赖很多安卓本身没有的命令
- proot、libtalloc、loader: 这个仍然需要自编译 termux-package,但是好在动态链接很少,不需要经常更新
我也思考过,移除 bash 和 proot-distro,但其实 proot-distro 帮我们处理了很多事情,如果最后仅精简成一行 proot 命令的话,可能启动的 ubuntu 会有一些问题
**2.升级 Target SDK 到35**
为后续上架 Google Play 做准备
**3.升级默认 code-server 到 4.96.2**
目前有开发者为 Code LFA 提交了一个 PR 以实现工作流产出 Apk,但我当前没有精力测试
**启动界面点击屏幕即可展示/隐藏终端**
**4.优化启动界面 UI**
一个遗留了很久的问题,目前我尽可能让它看起来美观一点,并加了玄学的进度条
也许大家启动失败的时候,我可以根据进度条的位置和终端输出来判断问题
**5.移除 Tar 依赖**
在最早的版本中,Code LFA 是无法使用直接从 code-server 下载的 .gz 包的,需要先解压,再压缩,因为压缩包中有一些 hardlink,在安卓上不支持
后来改用了 Dart Tar 来处理
现在移除了这部分,尽可能减少 Code LFA 的代码和依赖
直接用 busybox tar 来解压,然后 hardlink 目前是针对这个 code-server 版本写死的,后续可能会解析 tar tvf 的结果,再动态拷贝硬链接的文件,
```bash
tar tvf 'assets/code-server-4.96.2-linux-arm64.tar.gz' | grep '^hr' tar.txt
hrwxr-xr-x 0 root root 0 Dec 21 05:39 code-server-4.96.2-linux-arm64/node_modules/argon2/build-tmp-napi-v3/Release/obj.target/argon2.node link to code-server-4.96.2-linux-arm64/node_modules/argon2/build-tmp-napi-v3/Release/argon2.node
hrw-r--r-- 0 root root 0 Dec 21 05:39 code-server-4.96.2-linux-arm64/node_modules/argon2/build-tmp-napi-v3/Release/obj.target/argon2.a link to code-server-4.96.2-linux-arm64/node_modules/argon2/build-tmp-napi-v3/Release/argon2.a
hrwxr-xr-x 0 root root 0 Dec 21 05:39 code-server-4.96.2-linux-arm64/node_modules/argon2/lib/binding/napi-v3/argon2.node link to code-server-4.96.2-linux-arm64/node_modules/argon2/build-tmp-napi-v3/Release/argon2.node
hrwxr-xr-x 0 root root 0 Dec 21 05:39 code-server-4.96.2-linux-arm64/lib/vscode/node_modules/@parcel/watcher/build/Release/watcher.node link to code-server-4.96.2-linux-arm64/lib/vscode/node_modules/@parcel/watcher/build/Release/obj.target/watcher.node
hrw-r--r-- 0 root root 0 Dec 21 05:39 code-server-4.96.2-linux-arm64/lib/vscode/node_modules/@parcel/watcher/build/node-addon-api/nothing.a link to code-server-4.96.2-linux-arm64/lib/vscode/node_modules/@parcel/watcher/build/Release/nothing.a
hrwxr-xr-x 0 root root 0 Dec 21 05:39 code-server-4.96.2-linux-arm64/lib/vscode/node_modules/kerberos/build/Release/obj.target/kerberos.node link to code-server-4.96.2-linux-arm64/lib/vscode/node_modules/kerberos/build/Release/kerberos.node
hrwxr-xr-x 0 root root 0 Dec 21 05:39 code-server-4.96.2-linux-arm64/lib/vscode/node_modules/native-watchdog/build/Release/obj.target/watchdog.node link to code-server-4.96.2-linux-arm64/lib/vscode/node_modules/native-watchdog/build/Release/watchdog.node
hrwxr-xr-x 0 root root 0 Dec 21 05:39 code-server-4.96.2-linux-arm64/lib/vscode/node_modules/@vscode/windows-registry/build/Release/winregistry.node link to code-server-4.96.2-linux-arm64/lib/vscode/node_modules/@vscode/windows-registry/build/Release/obj.target/winregistry.node
hrwxr-xr-x 0 root root 0 Dec 21 05:39 code-server-4.96.2-linux-arm64/lib/vscode/node_modules/@vscode/windows-process-tree/build/Release/obj.target/windows_process_tree.node link to code-server-4.96.2-linux-arm64/lib/vscode/node_modules/@vscode/windows-process-tree/build/Release/windows_process_tree.node
hrwxr-xr-x 0 root root 0 Dec 21 05:39 code-server-4.96.2-linux-arm64/lib/vscode/node_modules/@vscode/spdlog/build/Release/obj.target/spdlog.node link to code-server-4.96.2-linux-arm64/lib/vscode/node_modules/@vscode/spdlog/build/Release/spdlog.node
hrwxr-xr-x 0 root root 0 Dec 21 05:39 code-server-4.96.2-linux-arm64/lib/vscode/node_modules/@vscode/deviceid/build/Release/obj.target/windows.node link to code-server-4.96.2-linux-arm64/lib/vscode/node_modules/@vscode/deviceid/build/Release/windows.node
```
### 其他更新
- 多 Activty 切换成 Fragment,这个还没确定会保留,目前是为了兼容 AR 眼镜
- 升级 Ubuntu 到 22.04,版本号没变,但资源更新了一下
- 代码精简,优化,移除无用依赖
- 删除了一些 Git 历史文件,所以同步仓库源的朋友需要重新 clone 一下
## TODO 中英文
## 1.4.0
- 支持剪切板啦
- 升级自带 code-server 到 4.95.2
## 1.3.0
- 支持剪切板啦
- 升级自带 code-server 到 4.90.3
## 1.2.1
- 采取大家的建议,将新版的 code-server 内置到 apk 中,不再需要下载,开箱即用
- 加了一些启动日志,在卡住不动的时候,方便排查问题
- 修复了其他的一些小问题
如果还有白屏的情况,更多可能是因为系统自带 WebView 版本不够导致的,尝试用 GooglePlay 升级 WebView 试试
或者用其他浏览器打开 http://127.0.0.1:10000
友善反馈是解决一切问题的必要条件
## 1.2.0
- 支持 code-server-v4.13.0
- 去除开屏广告,起初我加上这个就是为了挣钱,来平衡我对软件付出的时间,最后发现用户量很少,目前这样的广告没收益,这很现实,想挣钱,也很现实,拉黑也是任何人都有的权利。
- 解决之前 tar 符号链接的问题,之前版本需要重新解压再打包 tar,所以大家只能下我给的tar去使用,现在可以直接支持从 code-server 下载的arm64压缩包
- 支持完全离线模式,之前会请求一部分服务,导致首次始终是需要联网的,现在不需要了。
- 简化启动时的终端输出
迁移服务器的时候 terminal 的源不小心被删了,重新编译了一下,这些都是需要花的时间成本,包括服务器,开发成本,大家如果觉得有帮助,只需要给这个项目点一个star,就是最大的支持,万分感谢。
https://github.com/nightmare-space/vscode_for_android
## 1.1.9
- 不再会往root/home写入收款二维码
- 更改多版本加载策略
- 加入开屏广告(介意慎更)
## 1.1.8
- 限制Android版本
## 1.1.7
- 增加了一件简陋的版本选择页面,现在只需要把对应的版本放在 /Sdcard 下然后输入版本号即可加载对应的 Code Server
Code Server 下载地址 http://nightmare.press:5244/AliYun
## 1.1.6
- ubuntu 版本升级到 22.04,并切换至相应的源
- 此版本会覆盖升级 ubuntu 版本,会自动备份 home 文件夹
- 精简控制台输出
- 修复 g++ 不能使用的问题
- 修复中文插件不能使用的问题
注意!!!目前 oss 不能上传文件,需要去 QQ 群961959652下载
Code Server zip包放到外置储存
## 1.1.5
- 修复 apt update 失败的问题ß
## 1.1.4
- 更新Code Server核心至4.5.0
- 补充隐私政策
## 1.1.3
- 支持屏幕旋转
## 1.1.2
- 支持自动下载 code-server
## 1.1.1
- 修复Code FA屏幕旋转不跟随系统的问题
## 1.1.0
- 增加进入VS Code页面的按钮
- code-server核心升级至v4.4.0
注意!!!
为了后续版本升级,从这个版本开始,code-server不再集成到apk内部
需要下载code-server到外置储存
链接如下(code-serer github release下载的不行)
https://nightmare-my.oss-cn-beijing.aliyuncs.com/code-server-4.4.0-linux-arm64.tar.gz
## 1.0.0
- 第一个版本
================================================
FILE: LICENSE
================================================
BSD 3-Clause License
Copyright (c) 2023, nightmare-space
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README-ZH.md
================================================
# Code FA
Language: 中文简体 | [English](README.md)
这是一个使用 code-server 实现的 VS Code 安卓版。这个方案也有些人实现了,这里也是提供其中一种。
体积会比较大,由于所需要的资源都是整个运行初始化需要的,所以将资源集成到服务器,再动态下载的意义不大。
所以大家综合权衡这种方案与其他开发者的方案。
原理是运行 code-server 再使用 webview 加载视图,会有一些bug,但已经能有一些可观的表现。
这个项目是开源的,上层框架是 Flutter,加载 VS Code 是在 Flutter 中实现,VS Code 运行在 Android WebView 中。
工作比较忙,可能处理问题较慢,见谅。
Cheers! 🍻
## 功能特性
- 完全本地运行的 Code Server
- 支持最新4.13.0版本
- 支持快速升级 Code-Server 版本
- 支持自定义 Code-Server 版本
- 支持无网络环境下运行
## 开始使用
1.下载 [code-server-4.13.0-linux-arm64.tar.gz](https://github.com/coder/code-server/releases/download/v4.13.0/code-server-4.13.0-linux-arm64.tar.gz)
2.将下载的文件放到 /sdcard,注意不用解压,不要更改他的文件名
3.启动 Code FA,Engoy it!
## 更改 Code-Server 版本
1.在 /sdcard 中创建一个名为 `code_version` 的文件,文件内容为版本号,例如 `4.13.0`,不要有换行
2.下载对应的版本,放到 /sdcard 中,注意不用解压,不要更改它的文件名
3.启动 Code FA,Engoy it!
## 注意
Code Server 是在一个 Ubuntu 中运行的,并非 termux 环境,所以,你需要装任何的依赖,只需要查询 Ubuntu 安装依赖的方式即可
## 安装内部依赖
这一节本不该出现在这,因为它本就不是 Code FA 的问题,但是极多的人不会使用 Ubuntu 安装简单的依赖,所有的相关问题都抛给 Code FA
同步包
```bash
apt update
```
安装任何依赖
```bash
apt install python3
apt install clang
...
```
## Git History
[](https://star-history.com/#nightmare-space/vscode_for_android&Date)
================================================
FILE: README.md
================================================
# Code FA
Language: English | [中文简体](README-ZH.md)

[](https://github.com/nightmare-space/code_lfa/commits/master)
[](https://github.com/nightmare-space/code_lfa/pulls)
[](https://github.com/nightmare-space/code_lfa)
[](https://github.com/nightmare-space/code_lfa/blob/master/LICENSE)
     [](https://app.codacy.com/gh/nightmare-space/code_lfa?utm_source=github.com&utm_medium=referral&utm_content=nightmare-space/code_lfa&utm_campaign=Badge_Grade)
This is an Android version of VS Code implemented using code-server. Some have already implemented similar solutions, and this is one of them.
The package size is relatively large since the resources required are necessary for the initial run, so integrating them into the server and dynamically downloading them is not very meaningful.
Users should weigh this solution against others available from different developers.
The principle is to run code-server and then use a webview to load the view. There might be some bugs, but it performs reasonably well.
This project is open source, with the upper framework being Flutter. The loading of VS Code is implemented in Flutter, and VS Code runs in the Android WebView.
I'm quite busy, so responses to issues might be slow. Thank you for your understanding.
Cheers! 🍻
## Features
- Fully local operation of Code Server
- Supports the latest version 4.103.1
- Supports quick updates to Code-Server versions
- Supports custom Code-Server versions
- Can run without an internet connection
## Changing Code-Server Version
1. Create a file named `code_version` in /sdcard with the version number as its content, such as `4.103.1`, without any line breaks.
2. Download code-server such as [code-server-4.103.1-linux-arm64.tar.gz](https://github.com/coder/code-server/releases/download/v4.13.0/code-server-4.103.1-linux-arm64.tar.gz)
3. Place the downloaded file in /sdcard. Do not unzip or change its filename.
4. Download the corresponding version and place it in /sdcard. Do not unzip or change its filename.
5. Launch Code FA, and enjoy it!
## Note
Code Server runs in an Ubuntu environment, not in a Termux environment. Therefore, if you need to install any dependencies, simply look up how to install them on Ubuntu.
## Installing Internal Dependencies
This section shouldn't really be here because it's not a Code FA issue, but many people don't know how to install simple dependencies on Ubuntu, and all related questions end up being directed to Code FA.
Update package lists
```bash
apt update
```
Install any dependency
```bash
apt install python3
apt install clang
...
```
## Git History
[](https://star-history.com/#nightmare-space/vscode_for_android&Date)
================================================
FILE: analysis_options.yaml
================================================
# 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-lang.github.io/linter/lints/index.html.
#
# 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
================================================
FILE: android/.gitignore
================================================
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks
================================================
FILE: android/app/build.gradle
================================================
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'dev.flutter.flutter-gradle-plugin'
}
// 本地编译注释以下代码
// start
def keystorePropertiesFile = rootProject.file('key.properties')
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
// end
android {
namespace 'com.nightmare.code'
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = 17
targetCompatibility = 17
}
kotlinOptions {
jvmTarget = 17
}
signingConfigs {
// 本地编译注释以下代码
// start
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
// end
}
defaultConfig {
applicationId 'com.nightmare.code'
minSdk = 24
targetSdk = 35
versionCode = flutter.versionCode
versionName = flutter.versionName
}
lintOptions {
disable 'InvalidPackage'
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
}
debug {
signingConfig signingConfigs.release
minifyEnabled false
shrinkResources false
}
}
project.android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.versionCodeOverride = flutter.versionCode
}
}
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
}
flutter {
source '../..'
}
dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// implementation fileTree(include: ['*.jar', '*aar'], dir: 'libs')
}
================================================
FILE: android/app/src/debug/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nightmare.code">
<!-- Flutter 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>
================================================
FILE: android/app/src/main/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.nightmare.code">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:icon="@mipmap/ic_launcher"
android:extractNativeLibs="true"
android:label="Code LFA"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="n">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<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>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
</application>
</manifest>
================================================
FILE: android/app/src/main/java/com/nightmare/code/MainActivity.java
================================================
package com.nightmare.code;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import io.flutter.embedding.android.FlutterFragment;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterEngineCache;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.GeneratedPluginRegistrant;
public class MainActivity extends FragmentActivity {
FlutterFragment flutterFragment;
private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment";
Context mContext;
FragmentManager fragmentManager = getSupportFragmentManager();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = this;
setContentView(R.layout.my_activity_layout);
flutterFragment = (FlutterFragment) fragmentManager.findFragmentByTag(TAG_FLUTTER_FRAGMENT);
FlutterEngine flutterEngine = new FlutterEngine(this, null, false);
flutterEngine.getDartExecutor().executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault());
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "vscode_channel").setMethodCallHandler((call, result) -> {
switch (call.method) {
case "open_webview": {
runOnUiThread(() -> {
fragmentManager.beginTransaction()
.replace(R.id.fl_container, new WebViewFragment())
.commit();
result.success("success");
});
}
break;
case "lib_path": {
result.success(mContext.getApplicationContext().getApplicationInfo().nativeLibraryDir);
}
break;
default:
result.notImplemented();
}
});
GeneratedPluginRegistrant.registerWith(flutterEngine);
FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);
if (flutterFragment == null) {
flutterFragment = FlutterFragment.withCachedEngine("my_engine_id").build();
}
fragmentManager
.beginTransaction()
.add(R.id.fl_container, flutterFragment, TAG_FLUTTER_FRAGMENT)
.commit();
}
@Override
public void onPostResume() {
super.onPostResume();
flutterFragment.onPostResume();
}
@Override
protected void onNewIntent(@NonNull Intent intent) {
super.onNewIntent(intent);
flutterFragment.onNewIntent(intent);
}
@Override
public void onBackPressed() {
super.onBackPressed();
flutterFragment.onBackPressed();
}
@Override
public void onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
flutterFragment.onRequestPermissionsResult(
requestCode,
permissions,
grantResults
);
}
@Override
public void onUserLeaveHint() {
flutterFragment.onUserLeaveHint();
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
flutterFragment.onTrimMemory(level);
}
}
================================================
FILE: android/app/src/main/java/com/nightmare/code/OrientationListener.java
================================================
package com.nightmare.code;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.util.Log;
import android.view.OrientationEventListener;
class OrientationListener extends OrientationEventListener {
static final String TAG = "Nightmare";
Activity context;
public OrientationListener(Activity context) {
super(context);
this.context = context;
}
@SuppressLint("SourceLockedOrientationActivity")
@Override
public void onOrientationChanged(int orientation) {
// Log.d(TAG, "orention" + orientation);
int screenOrientation = context.getResources().getConfiguration().orientation;
if (((orientation >= 0) && (orientation < 45)) || (orientation > 315)) {//设置竖屏
if (screenOrientation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT && orientation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) {
Log.d(TAG, "设置竖屏");
context.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
} else if (orientation > 225 && orientation < 315) { //设置横屏
Log.d(TAG, "设置横屏");
if (screenOrientation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
context.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
} else if (orientation > 45 && orientation < 135) {// 设置反向横屏
Log.d(TAG, "反向横屏");
if (screenOrientation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
context.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
}
} else if (orientation > 135 && orientation < 225) {
Log.d(TAG, "反向竖屏");
if (screenOrientation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) {
context.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
}
}
}
}
================================================
FILE: android/app/src/main/java/com/nightmare/code/WebViewFragment.java
================================================
package com.nightmare.code;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class WebViewFragment extends Fragment {
OrientationListener myOrientoinListener;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
WebView mWebView;
myOrientoinListener = new OrientationListener(getActivity());
checkState();
getContext().getContentResolver().registerContentObserver(
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
true,
new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
checkState();
}
}
);
mWebView = new WebView(getContext());
// 设置 mWebView 为 MatchParent
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mWebView.setLayoutParams(layoutParams);
//访问网页
WebSettings mWebSettings = mWebView.getSettings();
//允许使用JS
mWebSettings.setJavaScriptEnabled(true);
mWebSettings.setJavaScriptCanOpenWindowsAutomatically(true);
mWebSettings.setUseWideViewPort(true);
mWebSettings.setAllowFileAccess(true);
// 下面这行不写不得行
mWebSettings.setDomStorageEnabled(true);
mWebSettings.setDatabaseEnabled(true);
// mWebSettings.setAppCacheEnabled(true);
mWebSettings.setLoadWithOverviewMode(true);
mWebSettings.setDefaultTextEncodingName("utf-8");
mWebSettings.setLoadsImagesAutomatically(true);
mWebSettings.setSupportMultipleWindows(true);
mWebView.addJavascriptInterface(new JavaScriptBridge(getContext()), "Android");
mWebView.setWebChromeClient(webChromeClient);
// feat 剪切板内容获取的hook
mWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
// 注入JavaScript代码
String jsCode = "const originalReadText = navigator.clipboard.readText; " +
"navigator.clipboard.readText = function () { " +
"console.log('Intercepted clipboard read'); " +
"return Android.getClipboardData(); " +
"return originalReadText.call(navigator.clipboard).then(text => { " +
"console.log('Clipboard content:', text); " +
"return text; " +
"}); " +
"};";
view.evaluateJavascript(jsCode, null);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//使用WebView加载显示url
view.loadUrl(url);
//返回true
return true;
}
});
mWebView.loadUrl("http://127.0.0.1:20000");
return mWebView;
}
public static class JavaScriptBridge {
Context mContext;
JavaScriptBridge(Context c) {
mContext = c;
}
@JavascriptInterface
public String getClipboardData() {
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = clipboard.getPrimaryClip();
if (clip != null && clip.getItemCount() > 0) {
return clip.getItemAt(0).getText().toString();
}
return "";
}
}
void checkState() {
boolean autoRotateOn = (android.provider.Settings.System.getInt(getContext().getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 1);
//检查系统是否开启自动旋转
if (autoRotateOn) {
myOrientoinListener.enable();
} else {
myOrientoinListener.disable();
}
}
WebChromeClient webChromeClient = new WebChromeClient() {
//=========多窗口的问题==========================================================
@Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
WebView childView = new WebView(getContext());//Parent WebView cannot host it's own popup window.
childView.setBackgroundColor(Color.GREEN);
childView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
});
WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(childView);//setWebView和getWebView两个方法
resultMsg.sendToTarget();
return true;
}
//=========多窗口的问题==========================================================
};
@Override
public void onStop() {
super.onStop();
myOrientoinListener.disable();
}
}
================================================
FILE: android/app/src/main/java/com/nightmare/code/WebViewUtil.java
================================================
package com.nightmare.code;
public class WebViewUtil {
}
================================================
FILE: android/app/src/main/res/drawable/launch_background.xml
================================================
<?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>
================================================
FILE: android/app/src/main/res/drawable-v21/launch_background.xml
================================================
<?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>
================================================
FILE: android/app/src/main/res/layout/my_activity_layout.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/fl_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
android:id="@+id/splash_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: android/app/src/main/res/layout/webview.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<WebView
android:id="@+id/wv_webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
================================================
FILE: android/app/src/main/res/values/styles.xml
================================================
<?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
Flutter 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:windowIsTranslucent">true</item>
<item name="android:windowBackground">?android:colorBackground</item>
<item name="android:windowAnimationStyle">@style/windowAnimation</item>
</style>
<style name="WebTheme" parent="@android:style/Theme.NoTitleBar.Fullscreen">
<item name="android:windowBackground">?android:colorBackground</item>
<item name="android:windowAnimationStyle">@style/windowAnimation</item>
</style>
<style name="windowAnimation" parent="@android:style/Animation.Translucent">
<item name="android:activityOpenEnterAnimation">@null</item>
<item name="android:activityOpenExitAnimation">@null</item>
<item name="android:activityCloseEnterAnimation">@null</item>
<item name="android:activityCloseExitAnimation">@null</item>
<item name="android:taskOpenEnterAnimation">@null</item>
<item name="android:taskOpenExitAnimation">@null</item>
<item name="android:taskCloseEnterAnimation">@null</item>
<item name="android:taskCloseExitAnimation">@null</item>
<item name="android:taskToFrontEnterAnimation">@null</item>
<item name="android:taskToFrontExitAnimation">@null</item>
<item name="android:taskToBackEnterAnimation">@null</item>
<item name="android:taskToBackExitAnimation">@null</item>
<item name="android:windowEnterAnimation">@null</item>
<item name="android:windowExitAnimation">@null</item>
</style>
</resources>
================================================
FILE: android/app/src/main/res/values-night/styles.xml
================================================
<?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
Flutter 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>
================================================
FILE: android/app/src/main/res/xml/network_security_config.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
================================================
FILE: android/app/src/profile/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nightmare.code">
<!-- Flutter 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>
================================================
FILE: android/build.gradle
================================================
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
================================================
FILE: android/gradle/wrapper/gradle-wrapper.properties
================================================
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
================================================
FILE: android/gradle.properties
================================================
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
================================================
FILE: android/settings.gradle
================================================
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file('local.properties').withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty('flutter.sdk')
assert flutterSdkPath != null, 'flutter.sdk not set in local.properties'
return 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 '8.3.0' apply false
id 'org.jetbrains.kotlin.android' version '2.0.0' apply false
}
include ':app'
================================================
FILE: assets/privacy_policy.md
================================================
# 隐私政策
更新日期:**2025/8/22**
生效日期:**2025/8/22**
## 导言
_Code LFA_ 是由「梦魇兽」(以下简称“我们”)提供的产品。本《隐私政策》旨在向您说明我们如何收集、使用、存储、共享与保护您的信息,以及您如何行使相关权利。请您在使用前仔细阅读并充分理解本政策的全部内容。
您使用或继续使用我们的服务,即表示您已阅读并同意本《隐私政策》。如对本政策有任何疑问,您可通过邮箱 **mengyanshou@gmail.com** 与我们联系。
## 1. 适用范围
本政策适用于您在使用 _Code FA_ 移动应用过程中的个人信息处理活动(包括后续版本、功能模块更新或扩展)。
## 2. 我们收集的信息
为实现产品功能、保障运行安全与改进体验,我们仅在必要、合理、最小化的范围内收集信息:
- 您主动提供的信息:在反馈、问题咨询或与我们联系时,您可能会主动提供的邮箱、问题描述、截图等。若您不主动提供,我们不会获取此类信息。
- 在使用过程中自动产生的信息(用于运行与安全):
- 设备信息:设备型号、操作系统版本、系统语言、分辨率、应用版本、设备标识(如 Android ID/IDFV,去标识化使用)。
- 日志信息与诊断数据:崩溃日志、性能数据、网络请求状态与时间戳,用于排查问题与提升稳定性。
- 网络信息:网络类型、连接状态,仅用于确保服务可用性与错误定位。
- 本地存储与缓存:为提升加载速度与离线可用性,我们可能在本地缓存必要的配置或资源文件,不用于识别您的个人身份。
说明:我们不要求您注册账户;在默认情况下,我们不收集可直接识别您身份的信息(如姓名、证件号码等),除非您在沟通中主动提供。
## 3. 权限使用说明
为实现特定功能,我们仅在您使用相关功能或获得您授权时,申请相应系统权限:
- 网络权限:用于连接服务、更新内容与错误上报(必要)。
- 存储/媒体访问:用于读写项目/资源文件、缓存加速(在功能需要时申请)。
- 通知权限:用于向您推送必要的服务提醒(如开启时)。
- 相机/相册/麦克风等敏感权限:仅在您主动使用对应功能时请求,且仅用于实现该功能,不会在后台持续获取。
您可在设备系统设置中随时管理或撤回权限授权,撤回后可能影响相关功能的正常使用,但不影响不依赖该权限的其他功能。
## 4. 我们如何使用信息
我们将收集的信息用于以下目的:
- 提供与维护产品功能与服务;
- 进行安全防护、异常监测、问题定位与性能优化;
- 统计与分析匿名化使用数据,以改进功能与体验;
- 在获得您授权或遵循法律要求的情况下,开展必要的产品通知与用户支持。
我们不进行与本政策无关的处理活动;如需超出原目的使用,将再次征得您的明示同意。
## 5. 信息共享、转让与公开披露
- 共享:除以下情形外,我们不会与第三方共享可识别您身份的信息:
- 经您明示同意或主动选择;
- 基于法律法规、行政或司法要求;
- 为实现服务所必需的委托处理(我们会对受托方进行安全评估,并要求其仅在受托目的范围内处理)。
- 广告与统计(如适用):如为投放广告或评估效果,需要向广告或统计合作方共享去标识化数据与设备广告标识(如 AAID/IDFA)。我们要求合作方遵循最小化原则与安全要求,不得用于识别您的真实身份。您可在系统中重置或限制广告标识。
- 转让与公开披露:如发生合并、收购或破产清算,我们将告知您相关情况,并要求新的持有方继续受本政策约束;法律法规另有规定或取得您同意的除外。
我们不会出售您的个人信息;非经您同意,不会公开披露您的个人信息。
## 6. 第三方服务/SDK(如适用)
为实现广告投放、崩溃分析、性能统计等功能,我们可能在版本迭代中接入第三方服务或 SDK。接入后我们将通过应用内“第三方信息共享清单”或更新本政策的方式公示具体名称、收集目的、数据类型与退出方式,并确保其合规处理。若您对第三方处理存在异议,可通过“联系我们”进行反馈。
## 7. 信息的存储与跨境传输
- 存储地点:我们将境内收集的用户个人信息存储在中国境内。
- 存储期限:仅在达成本政策所述目的或符合法律法规最长期限所必需的期间内保存;超期后将删除或匿名化处理。
- 跨境传输:目前不进行个人信息的跨境传输。若未来确有需要,我们将向您告知目的、接收方、安全保障与潜在风险,并在依法评估与取得您的明示同意后实施。
## 8. 信息安全
我们采用合理可行的安全措施保护信息,含传输加密、访问控制、最小权限、审计与备份等。尽管已尽最大努力,互联网环境并非百分之百安全;如发生安全事件,我们将按法律要求启动应急响应并告知您处置情况与可行的自我保护建议。
## 9. 您的权利
在法律规定范围内,您可以行使以下权利:
- 访问、复制您的相关信息;
- 更正、补充不准确的信息;
- 删除不必要或超期保存的信息;
- 撤回同意或限制处理(不影响撤回前基于同意的处理活动);
- 获取对自动化决策的解释并拒绝仅基于自动化决策的结果(如适用)。
如您无法通过产品内设置完成上述操作,可通过本政策提供的联系方式与我们取得联系。我们将在验证您的身份后尽快处理,一般不超过15个工作日;复杂情况我们会同步进度或原因。
## 10. 未成年人保护
我们非常重视对未成年人的保护。若您为未成年人,请在监护人同意与指导下使用本产品。我们建议监护人阅读本政策,并指导未成年人不要在未经同意的情况下向我们提供个人信息。监护人如对未成年人信息处理有疑问或请求删除,可与我们联系。
## 11. 本政策的更新
我们可能因业务、法律法规或功能变更适时更新本政策。重大变更(如处理目的、处理者、处理类型、共享对象发生变化)将通过应用内显著位置提示或弹窗方式告知,并在合理期限内征得您的同意后生效。
## 12. 联系我们
如对本《隐私政策》或个人信息保护事宜有任何问题、意见或建议,请通过邮箱与我们联系:**mengyanshou@gmail.com**。我们将尽快审核并在不超过15个工作日内回复。
---
附:权限与信息使用清单
- 网络:用于 Code Server 访问网路
- 存储/媒体访问:读取 Code Server 版本信息、配置文件
================================================
FILE: assets/ubuntu-noble-aarch64-pd-v4.18.0.tar.xz
================================================
[File too large to display: 61.2 MB]
================================================
FILE: lib/config.dart
================================================
const bool product = bool.fromEnvironment('dart.vm.product');
const String debugCSV = '4.103.1';
class Config {
Config._();
/// The package name of the app
static const String packageName = 'com.nightmare.code';
static const String versionName = String.fromEnvironment('VERSION');
static const String defaultCodeServerVersion = product ? String.fromEnvironment('CSVERSION') : debugCSV;
static String codeServerVersion = defaultCodeServerVersion;
static int port = 20000;
static String ubuntuFileName = 'ubuntu-noble-aarch64-pd-v4.18.0.tar.xz';
}
================================================
FILE: lib/generated/intl/messages_all.dart
================================================
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that looks up messages for specific locales by
// delegating to the appropriate library.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:implementation_imports, file_names, unnecessary_new
// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
// ignore_for_file:argument_type_not_assignable, invalid_assignment
// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
// ignore_for_file:comment_references
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart';
import 'messages_en.dart' as messages_en;
import 'messages_zh_CN.dart' as messages_zh_cn;
typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = {
'en': () => new SynchronousFuture(null),
'zh_CN': () => new SynchronousFuture(null),
};
MessageLookupByLibrary? _findExact(String localeName) {
switch (localeName) {
case 'en':
return messages_en.messages;
case 'zh_CN':
return messages_zh_cn.messages;
default:
return null;
}
}
/// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) {
var availableLocale = Intl.verifiedLocale(
localeName,
(locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null,
);
if (availableLocale == null) {
return new SynchronousFuture(false);
}
var lib = _deferredLibraries[availableLocale];
lib == null ? new SynchronousFuture(false) : lib();
initializeInternalMessageLookup(() => new CompositeMessageLookup());
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new SynchronousFuture(true);
}
bool _messagesExistFor(String locale) {
try {
return _findExact(locale) != null;
} catch (e) {
return false;
}
}
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
var actualLocale = Intl.verifiedLocale(
locale,
_messagesExistFor,
onFailure: (_) => null,
);
if (actualLocale == null) return null;
return _findExact(actualLocale);
}
================================================
FILE: lib/generated/intl/messages_en.dart
================================================
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a en locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'en';
static String m0(param) => "Copy code-server${param} to data directory";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"copy_code_server": m0,
"copy_proot_distro": MessageLookupByLibrary.simpleMessage(
"Copy proot-distro to data directory",
),
"copy_ubuntu": MessageLookupByLibrary.simpleMessage(
"Copy ubuntu to data directory",
),
"create_busybox_symlink": MessageLookupByLibrary.simpleMessage(
"Create Busybox symlink",
),
"create_terminal_obj": MessageLookupByLibrary.simpleMessage(
"Create PTY Terminal Instance",
),
"current_code_version": MessageLookupByLibrary.simpleMessage(
"Current VS Code Server Version",
),
"define_functions": MessageLookupByLibrary.simpleMessage(
"Define functions to be used",
),
"gen_script": MessageLookupByLibrary.simpleMessage(
"Generate Fix Hardlink Script",
),
"installed": MessageLookupByLibrary.simpleMessage("Installed"),
"installing": MessageLookupByLibrary.simpleMessage("Installing"),
"listen_vscode_start": MessageLookupByLibrary.simpleMessage(
"Listen for VS Code start status to jump to Web View",
),
"ubuntu_not_installed": MessageLookupByLibrary.simpleMessage(
"Ubuntu not installed, installing",
),
"uninstalled": MessageLookupByLibrary.simpleMessage("Not Installed"),
};
}
================================================
FILE: lib/generated/intl/messages_zh_CN.dart
================================================
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a zh_CN locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'zh_CN';
static String m0(param) => "拷贝 code-server${param} 到数据目录";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"copy_code_server": m0,
"copy_proot_distro": MessageLookupByLibrary.simpleMessage(
"拷贝 proot-distro 到数据目录",
),
"copy_ubuntu": MessageLookupByLibrary.simpleMessage("拷贝 ubuntu 到数据目录"),
"create_busybox_symlink": MessageLookupByLibrary.simpleMessage(
"创建 Busybox 符号链接",
),
"create_terminal_obj": MessageLookupByLibrary.simpleMessage("创建 PTY 终端实例"),
"current_code_version": MessageLookupByLibrary.simpleMessage(
"当前 VS Code Server 版本",
),
"define_functions": MessageLookupByLibrary.simpleMessage("定义需要使用的函数"),
"gen_script": MessageLookupByLibrary.simpleMessage("生成硬链接修复脚本"),
"installed": MessageLookupByLibrary.simpleMessage("已安装"),
"installing": MessageLookupByLibrary.simpleMessage("安装中"),
"listen_vscode_start": MessageLookupByLibrary.simpleMessage(
"监听VS Code启动状态以跳转Web View",
),
"ubuntu_not_installed": MessageLookupByLibrary.simpleMessage(
"Ubuntu 未安装, 安装中",
),
"uninstalled": MessageLookupByLibrary.simpleMessage("未安装"),
};
}
================================================
FILE: lib/generated/l10n.dart
================================================
// GENERATED CODE - DO NOT MODIFY BY HAND
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'intl/messages_all.dart';
// **************************************************************************
// Generator: Flutter Intl IDE plugin
// Made by Localizely
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars
// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each
// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes
class S {
S();
static S? _current;
static S get current {
assert(
_current != null,
'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.',
);
return _current!;
}
static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
static Future<S> load(Locale locale) {
final name = (locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
final instance = S();
S._current = instance;
return instance;
});
}
static S of(BuildContext context) {
final instance = S.maybeOf(context);
assert(
instance != null,
'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?',
);
return instance!;
}
static S? maybeOf(BuildContext context) {
return Localizations.of<S>(context, S);
}
/// `Create PTY Terminal Instance`
String get create_terminal_obj {
return Intl.message(
'Create PTY Terminal Instance',
name: 'create_terminal_obj',
desc: '',
args: [],
);
}
/// `Current VS Code Server Version`
String get current_code_version {
return Intl.message(
'Current VS Code Server Version',
name: 'current_code_version',
desc: '',
args: [],
);
}
/// `Copy proot-distro to data directory`
String get copy_proot_distro {
return Intl.message(
'Copy proot-distro to data directory',
name: 'copy_proot_distro',
desc: '',
args: [],
);
}
/// `Copy ubuntu to data directory`
String get copy_ubuntu {
return Intl.message(
'Copy ubuntu to data directory',
name: 'copy_ubuntu',
desc: '',
args: [],
);
}
/// `Create Busybox symlink`
String get create_busybox_symlink {
return Intl.message(
'Create Busybox symlink',
name: 'create_busybox_symlink',
desc: '',
args: [],
);
}
/// `Copy code-server{param} to data directory`
String copy_code_server(Object param) {
return Intl.message(
'Copy code-server$param to data directory',
name: 'copy_code_server',
desc: '',
args: [param],
);
}
/// `Define functions to be used`
String get define_functions {
return Intl.message(
'Define functions to be used',
name: 'define_functions',
desc: '',
args: [],
);
}
/// `Ubuntu not installed, installing`
String get ubuntu_not_installed {
return Intl.message(
'Ubuntu not installed, installing',
name: 'ubuntu_not_installed',
desc: '',
args: [],
);
}
/// `Listen for VS Code start status to jump to Web View`
String get listen_vscode_start {
return Intl.message(
'Listen for VS Code start status to jump to Web View',
name: 'listen_vscode_start',
desc: '',
args: [],
);
}
/// `Installed`
String get installed {
return Intl.message('Installed', name: 'installed', desc: '', args: []);
}
/// `Installing`
String get installing {
return Intl.message('Installing', name: 'installing', desc: '', args: []);
}
/// `Not Installed`
String get uninstalled {
return Intl.message(
'Not Installed',
name: 'uninstalled',
desc: '',
args: [],
);
}
/// `Generate Fix Hardlink Script`
String get gen_script {
return Intl.message(
'Generate Fix Hardlink Script',
name: 'gen_script',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
const AppLocalizationDelegate();
List<Locale> get supportedLocales {
return const <Locale>[
Locale.fromSubtags(languageCode: 'en'),
Locale.fromSubtags(languageCode: 'zh', countryCode: 'CN'),
];
}
@override
bool isSupported(Locale locale) => _isSupported(locale);
@override
Future<S> load(Locale locale) => S.load(locale);
@override
bool shouldReload(AppLocalizationDelegate old) => false;
bool _isSupported(Locale locale) {
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return true;
}
}
return false;
}
}
================================================
FILE: lib/l10n/intl_en.arb
================================================
{
"@@locale": "en",
"create_terminal_obj": "Create PTY Terminal Instance",
"current_code_version": "Current VS Code Server Version",
"copy_proot_distro": "Copy proot-distro to data directory",
"copy_ubuntu": "Copy ubuntu to data directory",
"create_busybox_symlink": "Create Busybox symlink",
"copy_code_server": "Copy code-server{param} to data directory",
"define_functions": "Define functions to be used",
"ubuntu_not_installed": "Ubuntu not installed, installing",
"listen_vscode_start": "Listen for VS Code start status to jump to Web View",
"installed": "Installed",
"installing": "Installing",
"uninstalled": "Not Installed",
"gen_script": "Generate Fix Hardlink Script"
}
================================================
FILE: lib/l10n/intl_zh_CN.arb
================================================
{
"@@locale": "zh_CN",
"create_terminal_obj": "创建 PTY 终端实例",
"current_code_version": "当前 VS Code Server 版本",
"copy_proot_distro": "拷贝 proot-distro 到数据目录",
"copy_ubuntu": "拷贝 ubuntu 到数据目录",
"create_busybox_symlink": "创建 Busybox 符号链接",
"copy_code_server": "拷贝 code-server{param} 到数据目录",
"define_functions": "定义需要使用的函数",
"ubuntu_not_installed": "Ubuntu 未安装, 安装中",
"listen_vscode_start": "监听VS Code启动状态以跳转Web View",
"installed": "已安装",
"installing": "安装中",
"uninstalled": "未安装",
"gen_script": "生成硬链接修复脚本"
}
================================================
FILE: lib/main.dart
================================================
// import 'package:behavior_api/behavior_api.dart';
import 'package:behavior_api/behavior_api.dart';
import 'package:code_lfa/config.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:global_repository/global_repository.dart';
import 'package:settings/settings.dart';
import 'package:code_lfa/terminal_page.dart';
import 'generated/l10n.dart';
// Notice: behavior will submit Device
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 隐藏系统 UI
// Hide system UI
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [
// SystemUiOverlay.top,
// SystemUiOverlay.bottom,
]);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
));
RuntimeEnvir.initEnvirWithPackageName('com.nightmare.code');
await initSettingStore(RuntimeEnvir.configPath);
runApp(const CodeLFA());
initApi('Code LFA', Config.versionName);
}
class CodeLFA extends StatelessWidget {
const CodeLFA({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Code LFA',
theme: ThemeData(
colorSchemeSeed: Colors.primaries[3],
),
// locale: const Locale('zh', 'CN'),
// locale: const Locale('en'),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: TerminalPage(),
);
}
}
================================================
FILE: lib/script.dart
================================================
import 'package:global_repository/global_repository.dart';
import 'config.dart';
import 'generated/l10n.dart';
// proot distro,ubuntu path
String prootDistroPath = '${RuntimeEnvir.usrPath}/var/lib/proot-distro';
String ubuntuPath = '$prootDistroPath/installed-rootfs/ubuntu';
String ubuntuName = Config.ubuntuFileName.replaceAll(RegExp('-pd.*'), '');
String common = '''
export TMPDIR=${RuntimeEnvir.tmpPath}
export BIN=${RuntimeEnvir.binPath}
export UBUNTU_PATH=$ubuntuPath
export UBUNTU=${Config.ubuntuFileName}
export UBUNTU_NAME=$ubuntuName
export CSPORT=${Config.port}
export CSVERSION=${Config.codeServerVersion}
export L_NOT_INSTALLED=${S.current.uninstalled}
export L_INSTALLING=${S.current.installing}
export L_INSTALLED=${S.current.installed}
clear_lines(){
printf "\\033[1A" # Move cursor up one line
printf "\\033[K" # Clear the line
printf "\\033[1A" # Move cursor up one line
printf "\\033[K" # Clear the line
}
progress_echo(){
echo -e "\\033[31m- \$@\\033[0m"
echo "\$@" > "\$TMPDIR/progress_des"
}
bump_progress(){
current=0
if [ -f "\$TMPDIR/progress" ]; then
current=\$(cat "\$TMPDIR/progress" 2>/dev/null || echo 0)
fi
next=\$((current + 1))
printf "\$next" > "\$TMPDIR/progress"
}
''';
// 切换到清华源
// Switch to Tsinghua source
String changeUbuntuNobleSource = r'''
change_ubuntu_source(){
cat <<EOF > $UBUNTU_PATH/etc/apt/sources.list
# 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释
# Defaultly commented out source mirrors to speed up apt update, uncomment if needed
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ noble main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ noble main restricted universe multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ noble-updates main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ noble-updates main restricted universe multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ noble-backports main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ noble-backports main restricted universe multiverse
# 以下安全更新软件源包含了官方源与镜像站配置,如有需要可自行修改注释切换
# The following security update software sources include both official and mirror configurations, modify comments to switch if needed
# deb http://ports.ubuntu.com/ubuntu-ports/ noble-security main restricted universe multiverse
# deb-src http://ports.ubuntu.com/ubuntu-ports/ noble-security main restricted universe multiverse
# 预发布软件源,不建议启用
# The following pre-release software sources are not recommended to be enabled
# deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ noble-proposed main restricted universe multiverse
# # deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ noble-proposed main restricted universe multiverse
EOF
}
''';
/// 安装ubuntu的shell
String genCodeConfig = r'''
gen_code_server_config(){
mkdir -p $UBUNTU_PATH/root/.config/code-server 2>/dev/null
echo "
bind-addr: 0.0.0.0:$CSPORT
auth: none
password: none
cert: false
" > $UBUNTU_PATH/root/.config/code-server/config.yaml
}
''';
String installUbuntu = r'''
install_ubuntu(){
mkdir -p $UBUNTU_PATH 2>/dev/null
if [ -z "$(ls -A $UBUNTU_PATH)" ]; then
progress_echo "Ubuntu $L_NOT_INSTALLED, $L_INSTALLING..."
ls ~/$UBUNTU
busybox tar xvf ~/$UBUNTU -C $UBUNTU_PATH/ | while read line; do
# echo -ne "\033[2K\0337\r$line\0338"
echo -ne "\033[2K\r$line"
done
echo
mv $UBUNTU_PATH/$UBUNTU_NAME/* $UBUNTU_PATH/
rm -rf $UBUNTU_PATH/$UBUNTU_NAME
echo 'export PATH=/opt/code-server-$CSVERSION-linux-arm64/bin:$PATH' >> $UBUNTU_PATH/root/.bashrc
echo 'export ANDROID_DATA=/home/' >> $UBUNTU_PATH/root/.bashrc
else
VERSION=`cat $UBUNTU_PATH/etc/issue.net 2>/dev/null`
# VERSION=`cat $UBUNTU_PATH/etc/issue 2>/dev/null | sed 's/\\n//g' | sed 's/\\l//g'`
progress_echo "Ubuntu $L_INSTALLED -> $VERSION"
fi
change_ubuntu_source
echo 'nameserver 8.8.8.8' > $UBUNTU_PATH/etc/resolv.conf
}
''';
// 安装 proot-distro 的脚本
// install proot-distro script
String installProotDistro = r'''
install_proot_distro(){
proot_distro_path=`which proot-distro`
if [ -z "$proot_distro_path" ]; then
progress_echo "proot-distro $L_NOT_INSTALLED, $L_INSTALLING..."
cd ~
busybox unzip proot-distro.zip -d proot-distro
cd ~/proot-distro
bash ./install.sh
else
progress_echo "proot-distro $L_INSTALLED"
fi
}
''';
@Deprecated('Use genFixCodeServerHardLinkShell instead')
String fixCodeServerHardLink = r'''
fix_code_server_hard_link(){
cd $UBUNTU_PATH/opt/code-server-$CSVERSION-linux-arm64/
ls node_modules/argon2/build-tmp-napi-v3/Release
cp node_modules/argon2/build-tmp-napi-v3/Release/argon2.node node_modules/argon2/build-tmp-napi-v3/Release/obj.target/argon2.node
cp node_modules/argon2/build-tmp-napi-v3/Release/argon2.a node_modules/argon2/build-tmp-napi-v3/Release/obj.target/argon2.a
cp node_modules/argon2/build-tmp-napi-v3/Release/argon2.node node_modules/argon2/lib/binding/napi-v3/argon2.node
cp lib/vscode/node_modules/@parcel/watcher/build/Release/obj.target/watcher.node lib/vscode/node_modules/@parcel/watcher/build/Release/watcher.node
cp lib/vscode/node_modules/@parcel/watcher/build/Release/nothing.a lib/vscode/node_modules/@parcel/watcher/build/node-addon-api/nothing.a
cp lib/vscode/node_modules/kerberos/build/Release/kerberos.node lib/vscode/node_modules/kerberos/build/Release/obj.target/kerberos.node
cp lib/vscode/node_modules/native-watchdog/build/Release/watchdog.node lib/vscode/node_modules/native-watchdog/build/Release/obj.target/watchdog.node
cp lib/vscode/node_modules/@vscode/windows-registry/build/Release/obj.target/winregistry.node lib/vscode/node_modules/@vscode/windows-registry/build/Release/winregistry.node
cp lib/vscode/node_modules/@vscode/windows-process-tree/build/Release/windows_process_tree.node lib/vscode/node_modules/@vscode/windows-process-tree/build/Release/obj.target/windows_process_tree.node
cp lib/vscode/node_modules/@vscode/spdlog/build/Release/spdlog.node lib/vscode/node_modules/@vscode/spdlog/build/Release/obj.target/spdlog.node
cp lib/vscode/node_modules/@vscode/deviceid/build/Release/windows.node lib/vscode/node_modules/@vscode/deviceid/build/Release/obj.target/windows.node
}
''';
String genFixCodeServerHardLinkShell(Map<String, String> map) {
final buf = StringBuffer();
buf.writeln(r'fix_code_server_hard_link(){');
buf.writeln(r' cd $UBUNTU_PATH/opt');
map.forEach((key, value) {
buf.writeln(' cp $value $key');
});
buf.writeln('}');
return buf.toString();
}
// TODO(Lin): 用 ESC 7 8 来实现,不然在手机上仍然会打印出很多行
String installVSCodeServer = r'''
install_vs_code(){
if [ ! -d "$UBUNTU_PATH/opt/code-server-$CSVERSION-linux-arm64" ];then
tar zxfh $TMPDIR/code-server-$CSVERSION-linux-arm64.tar.gz -C $UBUNTU_PATH/opt | while read line; do
echo -ne "\033[2K\r$line"
done
# progress_echo "pwd: `pwd`"
fix_code_server_hard_link
# progress_echo "pwd: `pwd`"
else
progress_echo "Code Server $L_INSTALLED"
fi
}
''';
String loginUbuntu = r'''
login_ubuntu(){
bash $BIN/proot-distro login --bind /storage/emulated/0:/sdcard/ ubuntu --isolated -- /opt/code-server-$CSVERSION-linux-arm64/bin/code-server
}
''';
String commonScript = '''
$common
$changeUbuntuNobleSource
$installVSCodeServer
$genCodeConfig
$installUbuntu
$loginUbuntu
$installProotDistro
clear_lines
start_vs_code(){
install_proot_distro
# return
sleep 1
bump_progress
install_ubuntu
sleep 1
bump_progress
install_vs_code
sleep 1
bump_progress
gen_code_server_config
sleep 1
bump_progress
login_ubuntu
}
''';
================================================
FILE: lib/terminal_controller.dart
================================================
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:ui';
import 'package:flutter_pty/flutter_pty.dart';
import 'package:get/get.dart';
import 'package:global_repository/global_repository.dart';
import 'package:settings/settings.dart';
import 'package:xterm/xterm.dart';
import 'config.dart';
import 'generated/l10n.dart';
import 'script.dart';
import 'utils.dart';
class HomeController extends GetxController {
bool vsCodeStaring = false;
SettingNode privacySetting = 'privacy'.setting;
Pty? pseudoTerminal;
late Terminal terminal = Terminal(
maxLines: 10000,
onResize: (width, height, pixelWidth, pixelHeight) {
pseudoTerminal?.resize(height, width);
},
onOutput: (data) {
pseudoTerminal?.writeString(data);
},
);
bool webviewHasOpen = false;
File progressFile = File('${RuntimeEnvir.tmpPath}/progress');
File progressDesFile = File('${RuntimeEnvir.tmpPath}/progress_des');
double progress = 0.0;
double step = 17;
String currentProgress = '';
// 进度 +1
// Progress +1
void bumpProgress() {
try {
int current = 0;
if (progressFile.existsSync()) {
final content = progressFile.readAsStringSync().trim();
if (content.isNotEmpty) {
current = int.tryParse(content) ?? 0;
}
} else {
progressFile.createSync(recursive: true);
}
progressFile.writeAsStringSync('${current + 1}');
} catch (e) {
progressFile.writeAsStringSync('1');
}
update();
}
// 监听输出,当输出中包含启动成功的标志时,启动 Code Server
// Listen for output and start the Code Server when the success flag is detected
Future<void> vsCodeStartWhenSuccessBind() async {
terminal.writeProgress('${S.current.listen_vscode_start}...');
final Completer completer = Completer();
Utf8Decoder decoder = const Utf8Decoder(allowMalformed: true);
pseudoTerminal!.output.cast<List<int>>().transform(decoder).listen((event) async {
if (event.contains('http://0.0.0.0:${Config.port}')) {
Log.e(event);
if (!completer.isCompleted) {
completer.complete();
}
}
if (event.contains('already')) {
Log.e(event);
if (!completer.isCompleted) {
completer.complete();
}
}
terminal.write(event);
});
await completer.future;
bumpProgress();
await Future.delayed(const Duration(milliseconds: 100));
webviewHasOpen = true;
openWebView();
Future.delayed(const Duration(milliseconds: 2000), () {
vsCodeStaring = false;
update();
});
}
// 初始化环境,将动态库中的文件链接到数据目录
// Init environment and link files from the dynamic library to the data directory
Future<void> initEnvir() async {
List<String> androidFiles = ['libbash.so', 'libbusybox.so', 'liblibtalloc.so.2.so', 'libloader.so', 'libproot.so', 'libsudo.so'];
String libPath = await getLibPath();
Log.i('libPath -> $libPath');
for (int i = 0; i < androidFiles.length; i++) {
// when android target sdk > 28
// cannot execute file in /data/data/com.xxx/files/usr/bin
// so we need create a link to /data/data/com.xxx/files/usr/bin
final sourcePath = '$libPath/${androidFiles[i]}';
String fileName = androidFiles[i].replaceAll(RegExp('^lib|\\.so\$'), '');
String filePath = '${RuntimeEnvir.binPath}/$fileName';
// custom path, termux-api will invoke
File file = File(filePath);
FileSystemEntityType type = await FileSystemEntity.type(filePath);
Log.i('$fileName type -> $type');
if (type != FileSystemEntityType.notFound && type != FileSystemEntityType.link) {
// old version adb is plain file
Log.i('find plain file -> $fileName, delete it');
await file.delete();
}
Link link = Link(filePath);
if (link.existsSync()) {
link.deleteSync();
}
try {
Log.i('create link -> $fileName ${link.path}');
link.createSync(sourcePath);
} catch (e) {
Log.e('installAdbToEnvir error -> $e');
}
}
}
// 同步当前进度
// Sync the current progress
void syncProgress() {
progressFile.createSync(recursive: true);
progressFile.writeAsStringSync('0');
progressFile.watch(events: FileSystemEvent.all).listen((event) async {
if (event.type == FileSystemEvent.modify) {
String content = await progressFile.readAsString();
Log.e('content -> $content');
if (content.isEmpty) {
return;
}
progress = int.parse(content) / step;
Log.e('progress -> $progress');
update();
}
});
progressDesFile.createSync(recursive: true);
progressDesFile.writeAsStringSync('');
progressDesFile.watch(events: FileSystemEvent.all).listen((event) async {
if (event.type == FileSystemEvent.modify) {
String content = await progressDesFile.readAsString();
currentProgress = content;
update();
}
});
}
// 创建 busybox 的软连接,来确保 proot-distro 会用到的命令正常运行
// create busybox symlinks, to ensure proot-distro can use the commands normally
void createBusyboxLink() {
try {
List<String> links = [
...['awk', 'ash', 'basename', 'bzip2', 'curl', 'cp', 'chmod', 'cut', 'cat', 'du', 'dd', 'find', 'grep', 'gzip'],
...['hexdump', 'head', 'id', 'lscpu', 'mkdir', 'realpath', 'rm', 'sed', 'stat', 'sh', 'tr', 'tar', 'uname', 'xargs', 'xz', 'xxd']
];
for (String linkName in links) {
Link link = Link('${RuntimeEnvir.binPath}/$linkName');
if (!link.existsSync()) {
link.createSync('${RuntimeEnvir.binPath}/busybox');
}
}
Link link = Link('${RuntimeEnvir.binPath}/file');
link.createSync('/system/bin/file');
} catch (e) {
Log.e('Create link failed -> $e');
}
}
Future<void> loadCodeVersion() async {
if (GetPlatform.isAndroid) {
PermissionStatus status = await Permission.manageExternalStorage.request();
Log.i('status -> $status');
if (!status.isGranted) {
return;
}
}
File file = File('/sdcard/code_version');
try {
if (!file.existsSync()) {
file.createSync();
file.writeAsStringSync(Config.defaultCodeServerVersion);
}
} catch (e) {
Log.e('Create code_version file failed -> $e');
}
if (file.existsSync()) Config.codeServerVersion = file.readAsStringSync();
if (Config.codeServerVersion.isEmpty) {
Config.codeServerVersion = Config.defaultCodeServerVersion;
}
}
bool get useCustomCodeServer => Config.codeServerVersion != Config.defaultCodeServerVersion;
void setProgress(String description) {
currentProgress = description;
terminal.writeProgress(currentProgress);
}
Future<void> loadCodeServer() async {
loadCodeVersion();
bumpProgress();
// 创建相关文件夹
// Create related folders
Directory(RuntimeEnvir.tmpPath).createSync(recursive: true);
Directory(RuntimeEnvir.homePath).createSync(recursive: true);
Directory(RuntimeEnvir.binPath).createSync(recursive: true);
bumpProgress();
await initEnvir();
bumpProgress();
// -
setProgress('${S.current.create_terminal_obj}...');
pseudoTerminal = createPTY(rows: terminal.viewHeight, columns: terminal.viewWidth);
bumpProgress();
// -
terminal.writeProgress('${S.current.current_code_version}:${Config.codeServerVersion} [${useCustomCodeServer ? 'custom' : ''}]');
setProgress('${S.current.copy_proot_distro}...');
await AssetsUtils.copyAssetToPath('assets/proot-distro.zip', '${RuntimeEnvir.homePath}/proot-distro.zip');
bumpProgress();
// -
setProgress('${S.current.copy_ubuntu}...');
await AssetsUtils.copyAssetToPath('assets/${Config.ubuntuFileName}', '${RuntimeEnvir.homePath}/${Config.ubuntuFileName}');
bumpProgress();
// -
setProgress('${S.current.create_busybox_symlink}...');
createBusyboxLink();
bumpProgress();
// -
String codeServerName = 'code-server-${Config.codeServerVersion}-linux-arm64.tar.gz';
String sourcePath = useCustomCodeServer ? '/sdcard/$codeServerName' : 'assets/$codeServerName';
setProgress('${S.current.copy_code_server('[$sourcePath]')} ${RuntimeEnvir.tmpPath}...');
try {
if (useCustomCodeServer) {
File codeServerOnSdcard = File(sourcePath);
File targetFile = File('${RuntimeEnvir.tmpPath}/$codeServerName');
if (targetFile.lengthSync() == codeServerOnSdcard.lengthSync()) {
Log.i('code server already copied, skip');
}
await codeServerOnSdcard.copy(targetFile.path);
} else {
await AssetsUtils.copyAssetToPath(
sourcePath,
'${RuntimeEnvir.tmpPath}/$codeServerName',
);
}
} catch (e) {
Log.e('Copy code server failed -> $e');
terminal.write('Copy code server failed -> $e');
return;
}
// -
final codeServerPath = '${RuntimeEnvir.tmpPath}/$codeServerName';
setProgress('${S.current.gen_script}...');
String fixHardLinkShell = '';
try {
Map<String, String> hardLinks = await getHardLinkMap(codeServerPath);
fixHardLinkShell = genFixCodeServerHardLinkShell(hardLinks);
Log.i('fixHardLinkShell -> $fixHardLinkShell');
} catch (e) {
terminal.write('Get hard link failed, will cause code-server start failed -> $e\r\n');
return;
}
bumpProgress();
bumpProgress();
// -
vsCodeStartWhenSuccessBind();
bumpProgress();
File('${RuntimeEnvir.homePath}/common.sh').writeAsStringSync('$commonScript\n$fixHardLinkShell');
bumpProgress();
startVsCode(pseudoTerminal!);
}
Future<void> startVsCode(Pty pseudoTerminal) async {
vsCodeStaring = true;
update();
pseudoTerminal.writeString('source ${RuntimeEnvir.homePath}/common.sh\nstart_vs_code\n');
// pseudoTerminal.writeString('bash\n');
}
@override
void onInit() {
super.onInit();
// 为 Google Play 上架做准备
// For Google Play
Future.delayed(Duration.zero, () async {
if (privacySetting.get() == null) {
await Get.to(PrivacyAgreePage(
onAgreeTap: () {
privacySetting.set(true);
Get.back();
},
));
}
syncProgress();
loadCodeServer();
});
}
}
================================================
FILE: lib/terminal_page.dart
================================================
import 'package:code_lfa/utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:global_repository/global_repository.dart';
import 'package:xterm/xterm.dart';
import 'terminal_controller.dart';
import 'terminal_theme.dart';
class TerminalPage extends StatefulWidget {
const TerminalPage({super.key});
@override
State<TerminalPage> createState() => _TerminalPageState();
}
class _TerminalPageState extends State<TerminalPage> {
HomeController controller = Get.put(HomeController());
ManjaroTerminalTheme terminalTheme = ManjaroTerminalTheme();
bool visible = false || kDebugMode;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: visible ? terminalTheme.background : Theme.of(context).colorScheme.surface,
body: SafeArea(
child: PopScope(
onPopInvokedWithResult: (didPop, result) {
controller.pseudoTerminal!.writeString('\x03');
Get.back();
},
canPop: true,
child: GestureDetector(
onTap: () {
visible = !visible;
setState(() {});
},
behavior: HitTestBehavior.translucent,
child: Stack(
alignment: Alignment.center,
children: [
Padding(
padding: EdgeInsets.all(8.w),
child: Visibility(
visible: visible,
// IgnorePointer
child: AbsorbPointer(
absorbing: false,
child: TerminalView(
controller.terminal,
readOnly: false,
backgroundOpacity: 1,
theme: ManjaroTerminalTheme(),
),
),
),
),
Center(
child: Material(
borderRadius: BorderRadius.circular(12.w),
color: Theme.of(context).colorScheme.surface,
child: SizedBox(
width: 300.w,
child: Padding(
padding: EdgeInsets.all(12.w),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: RepaintBoundary(
child: LoadingProgress(
minRadius: 6,
strokeWidth: 3,
increaseRadius: 3,
),
),
),
SizedBox(height: 12.w),
GetBuilder<HomeController>(builder: (controller) {
return Column(
children: [
Stack(
children: [
Container(
height: 5.w,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.opacity02,
borderRadius: BorderRadius.circular(3.w),
),
),
AnimatedContainer(
duration: 300.milliseconds,
height: 5.w,
width: 300.w * controller.progress,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(3.w),
),
),
],
),
SizedBox(height: 8.w),
Text(
controller.currentProgress.trim(),
style: TextStyle(
fontSize: 12.w,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
);
}),
],
),
),
),
),
),
],
),
),
),
),
);
}
}
================================================
FILE: lib/terminal_theme.dart
================================================
import 'dart:ui';
import 'package:xterm/xterm.dart';
class ManjaroTerminalTheme extends TerminalTheme {
ManjaroTerminalTheme({
super.cursor = const Color(0xaaf6f5f4),
super.selection = const Color(0XAAAEAFAD),
super.foreground = const Color(0xffe5e5e5),
super.background = const Color(0xff1c1c1e),
super.black = const Color(0xff241f31),
super.white = const Color(0xffc0bfbc),
super.red = const Color(0xffc01c28),
super.green = const Color(0xff2ec27e),
super.yellow = const Color(0xfff5c211),
super.blue = const Color(0xff1e78e4),
super.magenta = const Color(0xff9841bb),
super.cyan = const Color(0xff0ab9dc),
super.brightBlack = const Color(0xff5e5c64),
super.brightRed = const Color(0xffed333b),
super.brightGreen = const Color(0xff57e389),
super.brightYellow = const Color(0xfff8e45c),
super.brightBlue = const Color(0xff51a1ff),
super.brightMagenta = const Color(0xffc061cb),
super.brightCyan = const Color(0xff4fd2fd),
super.brightWhite = const Color(0xfff6f5f4),
super.searchHitBackground = const Color(0XFF000000),
super.searchHitBackgroundCurrent = const Color(0XFF31FF26),
super.searchHitForeground = const Color(0XFF000000),
});
}
================================================
FILE: lib/utils.dart
================================================
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:flutter/services.dart';
import 'package:flutter_pty/flutter_pty.dart';
import 'package:global_repository/global_repository.dart';
import 'package:tar/tar.dart';
import 'package:xterm/xterm.dart';
/// 收集 .tar.gz 中的硬链接映射
/// key 为链接文件在归档中的路径(entry.name)
/// value 为链接所指向的目标路径(header.linkName)
/// Collect hard link mappings in .tar.gz
/// key is the path of the link file in the archive (entry.name)
/// value is the target path of the link (header.linkName)
Future<Map<String, String>> getHardLinkMap(String tarGzPath) async {
final result = <String, String>{};
final stream = File(tarGzPath).openRead().transform(gzip.decoder);
final reader = TarReader(stream);
while (await reader.moveNext()) {
final entry = reader.current;
if (entry.type == TypeFlag.link) {
final name = entry.header.name;
final target = entry.header.linkName ?? '';
if (name.isNotEmpty && target.isNotEmpty) {
result[name] = target;
}
}
}
return result;
}
/// 使用 archive_io + TarFile 收集 .tar.gz 中的硬链接映射
/// key 为链接文件在归档中的路径(tf.filename)
/// value 为链接所指向的目标路径(tf.nameOfLinkedFile)
/// Collect hard link mappings in .tar.gz
/// key is the path of the link file in the archive (tf.filename)
/// value is the target path of the link (tf.nameOfLinkedFile)
Future<Map<String, String>> getHardLinkMapByArchive(String tarGzPath) async {
final result = <String, String>{};
final input = InputFileStream(tarGzPath);
try {
// 解压至内存
final memOut = OutputMemoryStream();
GZipDecoder().decodeStream(input, memOut);
final tarBytes = memOut.getBytes();
// 逐条读取 TarFile,保留 typeFlag/linkName 等信息
final mem = InputMemoryStream(tarBytes);
while (!mem.isEOS) {
final tf = TarFile.read(mem);
if (tf.filename.isEmpty) {
// 安全退出:遇到结尾填充块
break;
}
// typeFlag: '1' 为硬链接,'2' 为符号链接
if (tf.typeFlag == '1') {
final name = tf.filename;
final target = tf.nameOfLinkedFile;
if (name.isNotEmpty && target != null && target.isNotEmpty) {
result[name] = target;
}
}
}
} finally {
input.close();
}
return result;
}
MethodChannel _channel = const MethodChannel('vscode_channel');
/// 打开 WebView
/// Opens the WebView
void openWebView() {
_channel.invokeMethod('open_webview');
}
/// 获取 Apk So 库路径
/// Gets the path of the Apk So library
Future<String> getLibPath() async {
return await _channel.invokeMethod('lib_path');
}
Pty createPTY({
String? shell,
int rows = 25,
int columns = 80,
}) {
Map<String, String> envir = Map.from(Platform.environment);
envir['HOME'] = RuntimeEnvir.homePath;
// proot-distro install need
envir['TERMUX_PREFIX'] = RuntimeEnvir.usrPath;
envir['TERM'] = 'xterm-256color';
envir['PATH'] = RuntimeEnvir.path;
// proot deps
envir['PROOT_LOADER'] = '${RuntimeEnvir.binPath}/loader';
envir['LD_LIBRARY_PATH'] = RuntimeEnvir.binPath;
return Pty.start(
'${RuntimeEnvir.binPath}/${shell ?? 'bash'}',
arguments: [],
environment: envir,
workingDirectory: RuntimeEnvir.homePath,
rows: rows,
columns: columns,
);
}
extension TerminalExt on Terminal {
void writeProgress(String data) {
write('\x1b[31m- $data\x1b[0m\n\r');
}
}
extension PTYExt on Pty {
void writeString(String data) {
write(Uint8List.fromList(utf8.encode(data)));
}
Future<void> defineFunction(String function) async {
Log.i('define function start');
Completer defineFunctionLock = Completer();
Directory tmpDir = Directory(RuntimeEnvir.tmpPath);
await tmpDir.create(recursive: true);
String shortHash = hashCode.toRadixString(16).substring(0, 4);
File shellFile = File('${tmpDir.path}/shell$shortHash');
String patchFunction = '$function\n'
r'''
#printf "\033[A"
#printf "\033[2K"
#printf "\033[A"
#printf "\033[2K"''';
await shellFile.writeAsString(patchFunction);
shellFile.watch(events: FileSystemEvent.delete).listen((event) {
defineFunctionLock.complete();
});
File('${tmpDir.path}/shell${shortHash}backup').writeAsStringSync(function);
// writeString('printf "\\033[?1049h"\n');
writeString('source ${shellFile.path} &&');
writeString('rm -rf ${shellFile.path} \n');
//terminal?.buffer.eraseLine();
// await Future.delayed(const Duration(milliseconds: 100));
// writeString('printf "\\033[?1049l"\n');
await defineFunctionLock.future;
Log.i('define function -> done');
}
}
================================================
FILE: package.json
================================================
{
"scripts": {
"Android: Run Release": "nic release -r",
"Android: Build Apk(Split Api)": "nic release -s",
"Android: Build AAB": "nic release -a",
"Mac: Install From Local Code": "nic release -m",
"Mac: Build Mac App": "nic release --build-mac",
"Mac: Generate Mac App(tar)": "nic release --gen-mac-app",
"Mac: Generate Mac App(dmg)": "nic release --build-mac-dmg",
"Default: Upload Build Package": "nic default -u",
"Default: Prepare Package": "nic default rename",
"Default: Clean Build Cache": "nic release --clean-build",
"Gen Intl Code": "dart run intl_utils:generate",
"Gen Model": "dart run build_runner build --delete-conflicting-outputs",
"Start Services": "adb shell sh /storage/emulated/0/Android/data/com.nightmare.sula/files/start.sh"
}
}
================================================
FILE: pubspec.yaml
================================================
name: code_lfa
description: VS Code for Android
publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.6.0+22
code_server: 4.103.1
environment:
sdk: ">=3.3.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
# pub repo
signale: ^0.0.9
intl: ^0.19.0
archive: ^4.0.7
async: ^2.11.0
dio: ^5.7.0
cupertino_icons: ^1.0.2
get: ^4.6.5
device_info_plus: ^11.1.1
tar: ^2.0.0
path: ^1.9.1
# get repo
xterm:
git:
url: https://github.com/TerminalStudio/xterm.dart
ref: master
settings:
git: https://github.com/nightmare-space/settings
flutter_pty:
git: https://github.com/TerminalStudio/flutter_pty
global_repository:
git: https://github.com/nightmare-space/global_repository
behavior_api:
git:
url: https://github.com/nightmare-space/empty_package
path: behavior_api
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
intl_utils: ^2.8.7
flutter:
uses-material-design: true
assets:
- assets/
flutter_intl:
enabled: true
================================================
FILE: pubspec_overrides.yaml
================================================
dependency_overrides:
permission_handler: ^11.3.1
device_info_plus: ^10.1.2
behavior_api:
path: ../../nightmare-space/behavior_api
global_repository:
path: ../../nightmare-space/global_repository
intl: 0.20.2
xterm:
path: /Users/lori/Desktop/Nightmare/github_repo/xterm.dart
================================================
FILE: scripts/check_hardlink.sh
================================================
tar tvf 'assets/code-server-4.103.1-linux-arm64.tar.gz' | grep '^hr'
================================================
FILE: scripts/gen_icon.sh
================================================
mkdir mipmap-hdpi && sips -z 72 72 icon.png --out mipmap-hdpi/ic_launcher.png
mkdir mipmap-mdpi && sips -z 48 48 icon.png --out mipmap-mdpi/ic_launcher.png
mkdir mipmap-xhdpi && sips -z 96 96 icon.png --out mipmap-xhdpi/ic_launcher.png
mkdir mipmap-xxhdpi && sips -z 144 144 icon.png --out mipmap-xxhdpi/ic_launcher.png
mkdir mipmap-xxxhdpi && sips -z 192 192 icon.png --out mipmap-xxxhdpi/ic_launcher.png
================================================
FILE: scripts/inject.js
================================================
const originalReadText = navigator.clipboard.readText;
navigator.clipboard.readText = function () {
console.log("Intercepted clipboard read");
return Android.getClipboardData();
// 调用原始方法
return originalReadText.call(navigator.clipboard).then(text => {
console.log("Clipboard content:", text);
// 这里可以修改或处理文本
return text;
});
};
================================================
FILE: scripts/properties.sh
================================================
VERSION='1.6.0'
VERSION_CODE='22'
TARGET_PATH=VSCode
APP_NAME='CodeLFA'
APP_NAME_CN='CodeLFA'
BUILD_ARG='--dart-define=CSVERSION=4.103.1'
gitextract_qyg_xcvc/
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ └── bug_report.md
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .gitmodules
├── .metadata
├── .vscode/
│ ├── launch.json
│ └── settings.json
├── CHANGELOG.md
├── LICENSE
├── README-ZH.md
├── README.md
├── analysis_options.yaml
├── android/
│ ├── .gitignore
│ ├── app/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── debug/
│ │ │ └── AndroidManifest.xml
│ │ ├── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java/
│ │ │ │ └── com/
│ │ │ │ └── nightmare/
│ │ │ │ └── code/
│ │ │ │ ├── MainActivity.java
│ │ │ │ ├── OrientationListener.java
│ │ │ │ ├── WebViewFragment.java
│ │ │ │ └── WebViewUtil.java
│ │ │ └── res/
│ │ │ ├── drawable/
│ │ │ │ └── launch_background.xml
│ │ │ ├── drawable-v21/
│ │ │ │ └── launch_background.xml
│ │ │ ├── layout/
│ │ │ │ ├── my_activity_layout.xml
│ │ │ │ └── webview.xml
│ │ │ ├── values/
│ │ │ │ └── styles.xml
│ │ │ ├── values-night/
│ │ │ │ └── styles.xml
│ │ │ └── xml/
│ │ │ └── network_security_config.xml
│ │ └── profile/
│ │ └── AndroidManifest.xml
│ ├── build.gradle
│ ├── gradle/
│ │ └── wrapper/
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ └── settings.gradle
├── assets/
│ ├── privacy_policy.md
│ └── ubuntu-noble-aarch64-pd-v4.18.0.tar.xz
├── lib/
│ ├── config.dart
│ ├── generated/
│ │ ├── intl/
│ │ │ ├── messages_all.dart
│ │ │ ├── messages_en.dart
│ │ │ └── messages_zh_CN.dart
│ │ └── l10n.dart
│ ├── l10n/
│ │ ├── intl_en.arb
│ │ └── intl_zh_CN.arb
│ ├── main.dart
│ ├── script.dart
│ ├── terminal_controller.dart
│ ├── terminal_page.dart
│ ├── terminal_theme.dart
│ └── utils.dart
├── package.json
├── pubspec.yaml
├── pubspec_overrides.yaml
└── scripts/
├── check_hardlink.sh
├── gen_icon.sh
├── inject.js
└── properties.sh
SYMBOL INDEX (72 symbols across 15 files)
FILE: android/app/src/main/java/com/nightmare/code/MainActivity.java
class MainActivity (line 19) | public class MainActivity extends FragmentActivity {
method onCreate (line 25) | @Override
method onPostResume (line 64) | @Override
method onNewIntent (line 70) | @Override
method onBackPressed (line 76) | @Override
method onRequestPermissionsResult (line 82) | @Override
method onUserLeaveHint (line 96) | @Override
method onTrimMemory (line 101) | @Override
FILE: android/app/src/main/java/com/nightmare/code/OrientationListener.java
class OrientationListener (line 9) | class OrientationListener extends OrientationEventListener {
method OrientationListener (line 13) | public OrientationListener(Activity context) {
method onOrientationChanged (line 18) | @SuppressLint("SourceLockedOrientationActivity")
FILE: android/app/src/main/java/com/nightmare/code/WebViewFragment.java
class WebViewFragment (line 27) | public class WebViewFragment extends Fragment {
method onCreateView (line 31) | @Nullable
class JavaScriptBridge (line 98) | public static class JavaScriptBridge {
method JavaScriptBridge (line 101) | JavaScriptBridge(Context c) {
method getClipboardData (line 105) | @JavascriptInterface
method checkState (line 116) | void checkState() {
method onCreateWindow (line 129) | @Override
method onStop (line 148) | @Override
FILE: android/app/src/main/java/com/nightmare/code/WebViewUtil.java
class WebViewUtil (line 3) | public class WebViewUtil {
FILE: lib/config.dart
class Config (line 4) | class Config {
FILE: lib/generated/intl/messages_all.dart
type Future (line 22) | typedef Future<dynamic> LibraryLoader();
function _findExact (line 28) | MessageLookupByLibrary? _findExact(String localeName)
function initializeMessages (line 40) | Future<bool> initializeMessages(String localeName)
function _messagesExistFor (line 56) | bool _messagesExistFor(String locale)
function _findGeneratedMessagesFor (line 64) | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale)
FILE: lib/generated/intl/messages_en.dart
type String (line 18) | typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup (line 20) | class MessageLookup extends MessageLookupByLibrary {
method m0 (line 23) | String m0(param)
method _notInlinedMessages (line 26) | Map<String, Function> _notInlinedMessages(_)
FILE: lib/generated/intl/messages_zh_CN.dart
type String (line 18) | typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup (line 20) | class MessageLookup extends MessageLookupByLibrary {
method m0 (line 23) | String m0(param)
method _notInlinedMessages (line 26) | Map<String, Function> _notInlinedMessages(_)
FILE: lib/generated/l10n.dart
class S (line 15) | class S {
method load (line 30) | Future<S> load(Locale locale)
method of (line 44) | S of(BuildContext context)
method maybeOf (line 53) | S? maybeOf(BuildContext context)
method copy_code_server (line 108) | String copy_code_server(Object param)
class AppLocalizationDelegate (line 178) | class AppLocalizationDelegate extends LocalizationsDelegate<S> {
method isSupported (line 189) | bool isSupported(Locale locale)
method load (line 191) | Future<S> load(Locale locale)
method shouldReload (line 193) | bool shouldReload(AppLocalizationDelegate old)
method _isSupported (line 195) | bool _isSupported(Locale locale)
FILE: lib/main.dart
function main (line 15) | Future<void> main()
class CodeLFA (line 34) | class CodeLFA extends StatelessWidget {
method build (line 39) | Widget build(BuildContext context)
FILE: lib/script.dart
function genFixCodeServerHardLinkShell (line 142) | String genFixCodeServerHardLinkShell(Map<String, String> map)
FILE: lib/terminal_controller.dart
class HomeController (line 15) | class HomeController extends GetxController {
method bumpProgress (line 38) | void bumpProgress()
method vsCodeStartWhenSuccessBind (line 58) | Future<void> vsCodeStartWhenSuccessBind()
method initEnvir (line 90) | Future<void> initEnvir()
method syncProgress (line 126) | void syncProgress()
method createBusyboxLink (line 154) | void createBusyboxLink()
method loadCodeVersion (line 174) | Future<void> loadCodeVersion()
method setProgress (line 199) | void setProgress(String description)
method loadCodeServer (line 204) | Future<void> loadCodeServer()
method startVsCode (line 277) | Future<void> startVsCode(Pty pseudoTerminal)
method onInit (line 285) | void onInit()
FILE: lib/terminal_page.dart
class TerminalPage (line 10) | class TerminalPage extends StatefulWidget {
method createState (line 14) | State<TerminalPage> createState()
class _TerminalPageState (line 17) | class _TerminalPageState extends State<TerminalPage> {
method build (line 23) | Widget build(BuildContext context)
FILE: lib/terminal_theme.dart
class ManjaroTerminalTheme (line 5) | class ManjaroTerminalTheme extends TerminalTheme {
FILE: lib/utils.dart
function getHardLinkMap (line 18) | Future<Map<String, String>> getHardLinkMap(String tarGzPath)
function getHardLinkMapByArchive (line 42) | Future<Map<String, String>> getHardLinkMapByArchive(String tarGzPath)
function openWebView (line 79) | void openWebView()
function getLibPath (line 85) | Future<String> getLibPath()
function createPTY (line 89) | Pty createPTY({
function writeProgress (line 115) | void writeProgress(String data)
function writeString (line 121) | void writeString(String data)
function defineFunction (line 125) | Future<void> defineFunction(String function)
Condensed preview — 55 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (112K chars).
[
{
"path": ".gitattributes",
"chars": 82,
"preview": "assets/code-server-4.103.1-linux-arm64.tar.gz filter=lfs diff=lfs merge=lfs -text\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 256,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**重要警告**\n\n如果是不会"
},
{
"path": ".github/workflows/main.yml",
"chars": 3493,
"preview": "name: \"Build & Release\"\r\n# see https://medium.com/@colonal/automating-flutter-builds-and-releases-with-github-actions-77"
},
{
"path": ".gitignore",
"chars": 863,
"preview": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.i"
},
{
"path": ".gitmodules",
"chars": 209,
"preview": "[submodule \"global_repository\"]\n\tpath = global_repository\n\turl = https://github.com/nightmare-space/global_repository\n[s"
},
{
"path": ".metadata",
"chars": 1706,
"preview": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrade"
},
{
"path": ".vscode/launch.json",
"chars": 209,
"preview": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"name\": \"Flutter\",\n \"type\": \"dart\",\n "
},
{
"path": ".vscode/settings.json",
"chars": 321,
"preview": "{\n \"editor.formatOnSave\": true,\n \"dart.lineLength\": 200,\n \"files.associations\": {\n \"*.arb\": \"json\"\n }"
},
{
"path": "CHANGELOG.md",
"chars": 6547,
"preview": "## 1.6.0\n- Upgrade proot-distro 4.28.0\n- Upgrade code-server to 4.103.1\n- Upgrade ubuntu to noble-aarch64-pd-v4.18.0\n- 优"
},
{
"path": "LICENSE",
"chars": 1502,
"preview": "BSD 3-Clause License\n\nCopyright (c) 2023, nightmare-space\n\nRedistribution and use in source and binary forms, with or wi"
},
{
"path": "README-ZH.md",
"chars": 1293,
"preview": "# Code FA\n\nLanguage: 中文简体 | [English](README.md)\n\n这是一个使用 code-server 实现的 VS Code 安卓版。这个方案也有些人实现了,这里也是提供其中一种。\n\n体积会比较大,由于所"
},
{
"path": "README.md",
"chars": 3819,
"preview": "# Code FA\n\nLanguage: English | [中文简体](README-ZH.md)\n\n\n jcenter()\n }\n}\n\nrootProject.buildDir = '../build'\nsubprojec"
},
{
"path": "android/gradle/wrapper/gradle-wrapper.properties",
"chars": 230,
"preview": "#Fri Jun 23 08:50:38 CEST 2017\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER"
},
{
"path": "android/gradle.properties",
"chars": 78,
"preview": "org.gradle.jvmargs=-Xmx4G\nandroid.useAndroidX=true\nandroid.enableJetifier=true"
},
{
"path": "android/settings.gradle",
"chars": 726,
"preview": "pluginManagement {\n def flutterSdkPath = {\n def properties = new Properties()\n file('local.properties')"
},
{
"path": "assets/privacy_policy.md",
"chars": 2636,
"preview": "# 隐私政策\n\n更新日期:**2025/8/22** \n生效日期:**2025/8/22**\n\n## 导言\n\n_Code LFA_ 是由「梦魇兽」(以下简称“我们”)提供的产品。本《隐私政策》旨在向您说明我们如何收集、使用、存储、共享与保"
},
{
"path": "lib/config.dart",
"chars": 567,
"preview": "const bool product = bool.fromEnvironment('dart.vm.product');\nconst String debugCSV = '4.103.1';\n\nclass Config {\n Confi"
},
{
"path": "lib/generated/intl/messages_all.dart",
"chars": 2297,
"preview": "// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart\n// This is a library that looks up messa"
},
{
"path": "lib/generated/intl/messages_en.dart",
"chars": 2431,
"preview": "// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart\n// This is a library that provides messa"
},
{
"path": "lib/generated/intl/messages_zh_CN.dart",
"chars": 2205,
"preview": "// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart\n// This is a library that provides messa"
},
{
"path": "lib/generated/l10n.dart",
"chars": 4988,
"preview": "// GENERATED CODE - DO NOT MODIFY BY HAND\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\nimpor"
},
{
"path": "lib/l10n/intl_en.arb",
"chars": 707,
"preview": "{\n \"@@locale\": \"en\",\n \"create_terminal_obj\": \"Create PTY Terminal Instance\",\n \"current_code_version\": \"Current VS Cod"
},
{
"path": "lib/l10n/intl_zh_CN.arb",
"chars": 530,
"preview": "{\n \"@@locale\": \"zh_CN\",\n \"create_terminal_obj\": \"创建 PTY 终端实例\",\n \"current_code_version\": \"当前 VS Code Server 版本\",\n \"co"
},
{
"path": "lib/main.dart",
"chars": 1866,
"preview": "// import 'package:behavior_api/behavior_api.dart';\nimport 'package:behavior_api/behavior_api.dart';\nimport 'package:cod"
},
{
"path": "lib/script.dart",
"chars": 7707,
"preview": "import 'package:global_repository/global_repository.dart';\nimport 'config.dart';\nimport 'generated/l10n.dart';\n\n// proot"
},
{
"path": "lib/terminal_controller.dart",
"chars": 10344,
"preview": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:ui';\nimport 'package:flutter_pty/flutter_pty."
},
{
"path": "lib/terminal_page.dart",
"chars": 5122,
"preview": "import 'package:code_lfa/utils.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\n"
},
{
"path": "lib/terminal_theme.dart",
"chars": 1238,
"preview": "import 'dart:ui';\n\nimport 'package:xterm/xterm.dart';\n\nclass ManjaroTerminalTheme extends TerminalTheme {\n ManjaroTermi"
},
{
"path": "lib/utils.dart",
"chars": 4608,
"preview": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:archive/archive.dart';\nimport 'package:fl"
},
{
"path": "package.json",
"chars": 808,
"preview": "{\n \"scripts\": {\n \"Android: Run Release\": \"nic release -r\",\n \"Android: Build Apk(Split Api)\": \"nic release -s\",\n "
},
{
"path": "pubspec.yaml",
"chars": 1099,
"preview": "name: code_lfa\ndescription: VS Code for Android\npublish_to: \"none\" # Remove this line if you wish to publish to pub.dev\n"
},
{
"path": "pubspec_overrides.yaml",
"chars": 298,
"preview": "dependency_overrides:\n permission_handler: ^11.3.1\n device_info_plus: ^10.1.2\n behavior_api:\n path: ../../nightmar"
},
{
"path": "scripts/check_hardlink.sh",
"chars": 68,
"preview": "tar tvf 'assets/code-server-4.103.1-linux-arm64.tar.gz' | grep '^hr'"
},
{
"path": "scripts/gen_icon.sh",
"chars": 405,
"preview": "mkdir mipmap-hdpi && sips -z 72 72 icon.png --out mipmap-hdpi/ic_launcher.png\nmkdir mipmap-mdpi && sips -z 48 48 icon.pn"
},
{
"path": "scripts/inject.js",
"chars": 373,
"preview": "const originalReadText = navigator.clipboard.readText;\n\nnavigator.clipboard.readText = function () {\n console.log(\"In"
},
{
"path": "scripts/properties.sh",
"chars": 138,
"preview": "VERSION='1.6.0'\nVERSION_CODE='22'\nTARGET_PATH=VSCode\nAPP_NAME='CodeLFA'\nAPP_NAME_CN='CodeLFA'\nBUILD_ARG='--dart-define=C"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the nightmare-space/code_lfa GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 55 files (61.3 MB), approximately 27.2k tokens, and a symbol index with 72 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.