🎉 Initial commit: PiAgentUI — AI Agent Desktop & Web UI
Build & Push Frontend Docker Image / build-and-push (push) Canceled after 0s
Build Validation / validate (push) Canceled after 6s
Build & Push Backend Docker Image / build-and-push (push) Failing after 10m10s
Build & Push Gateway Docker Image / build-and-push (push) Failing after 10m9s

A professional workspace for AI coding agents with:
- Real-time streaming chat with SSE
- Session management (fork, clone, tree navigation)
- MCP tools browser & metadata panel
- Skills browser (global, project, package sources)
- Voice input & STT provider configuration
- Integrated terminal & file explorer
- Model selector with capability metadata
- Keyboard-first UI with command palette
- 8 languages (en, pt-BR, es, ja, zh-CN, ar, bn, hi)
- Tauri 2 desktop app
- React 19 + TypeScript + Vite 8 + Tailwind CSS 4
- Pi Agent extension runtime

Originally forked from OpenCodeUI, now an independent project.
License: GPL-3.0-only
This commit is contained in:
Evandro committed 2026-06-01 00:50:06 -03:00
commit b3489e1ede
983 files changed
+158475

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
/src/main/java/com/opencodeui/app/generated
/src/main/jniLibs/**/*.so
/src/main/assets/tauri.conf.json
/tauri.build.gradle.kts
/proguard-tauri.pro
/tauri.properties
@@ -0,0 +1,95 @@
import java.util.Properties
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rust")
}
val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
// 读取签名配置(CI 环境通过 keystore.properties 注入)
val keystoreProperties = Properties().apply {
val propFile = file("keystore.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
android {
compileSdk = 36
namespace = "com.opencodeui.app"
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "com.opencodeui.app"
minSdk = 24
targetSdk = 36
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
signingConfigs {
create("release") {
val ksFile = keystoreProperties.getProperty("storeFile")
if (ksFile != null) {
storeFile = file(ksFile)
storePassword = keystoreProperties.getProperty("password")
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("password")
}
}
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging {
jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isMinifyEnabled = true
val ksFile = keystoreProperties.getProperty("storeFile")
if (ksFile != null) {
signingConfig = signingConfigs.getByName("release")
}
proguardFiles(
*fileTree(".") { include("**/*.pro") }
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
.toList().toTypedArray()
)
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
}
rust {
rootDirRel = "../../../"
}
dependencies {
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("com.google.android.material:material:1.12.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
apply(from = "tauri.build.gradle.kts")
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- AndroidTV support -->
<uses-feature android:name="android.software.leanback" android:required="false" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.opencodeui"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:label="@string/main_activity_title"
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- AndroidTV support -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
@@ -0,0 +1,304 @@
package com.opencodeui.app
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
class MainActivity : TauriActivity() {
private val handler = Handler(Looper.getMainLooper())
private var cachedInsetsJs: String? = null
private var themeSyncRunnable: Runnable? = null
private var cachedWebView: WebView? = null
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
// 初始状态栏样式(后续由 WebView 主题同步驱动)
val controller = WindowInsetsControllerCompat(window, window.decorView)
controller.isAppearanceLightStatusBars = true
controller.isAppearanceLightNavigationBars = true
// 禁用系统对比度强制(避免状态栏自动加黑/渐变)
if (Build.VERSION.SDK_INT >= 29) {
window.isStatusBarContrastEnforced = false
window.isNavigationBarContrastEnforced = false
}
// 监听 WindowInsets 变化:
// 1. 对内容容器 setPadding,让 WebView 物理 resize(键盘弹出时 window.innerHeight 自动变小)
// 2. 注入 CSS 变量供前端做精细布局
val contentView = findViewById<View>(android.R.id.content)
ViewCompat.setOnApplyWindowInsetsListener(contentView) { view, windowInsets ->
val systemInsets = windowInsets.getInsets(
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
)
val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
// 键盘弹出时底部取 IME 和系统栏的较大值,让 WebView 整体 resize
val bottomPadding = maxOf(imeInsets.bottom, systemInsets.bottom)
view.setPadding(
systemInsets.left,
systemInsets.top,
systemInsets.right,
bottomPadding
)
cachedInsetsJs = """
(function() {
var s = document.documentElement.style;
s.setProperty('--safe-area-inset-top', '0px');
s.setProperty('--safe-area-inset-bottom', '0px');
s.setProperty('--safe-area-inset-left', '0px');
s.setProperty('--safe-area-inset-right', '0px');
})();
""".trimIndent()
// 立即尝试注入
tryInjectInsets(view)
WindowInsetsCompat.CONSUMED
}
// WebView 可能还没创建好,轮询几次确保注入成功
scheduleInsetsInjection(contentView, 0)
}
override fun onResume() {
super.onResume()
startThemeSync()
}
override fun onPause() {
stopThemeSync()
super.onPause()
}
private fun startThemeSync() {
if (themeSyncRunnable != null) return
themeSyncRunnable = Runnable {
val rootView = window.decorView.findViewById<View>(android.R.id.content)
syncSystemBars(rootView)
handler.postDelayed(themeSyncRunnable!!, 800L)
}
handler.post(themeSyncRunnable!!)
}
private fun stopThemeSync() {
themeSyncRunnable?.let { handler.removeCallbacks(it) }
themeSyncRunnable = null
}
private fun syncSystemBars(rootView: View) {
val webView = cachedWebView ?: findWebView(rootView) ?: return
// 在 JS 端用 Canvas 2D 将 getComputedStyle 返回的任意格式颜色
// 统一转为 #rrggbb hex,避免不同 WebView 版本返回不同格式
// (rgb 逗号/空格分隔, color(srgb ...), oklch(...) 等)
val js = """
(function() {
var mode = document.documentElement.getAttribute('data-mode') || 'system';
var raw = getComputedStyle(document.documentElement).getPropertyValue('--color-bg-100').trim();
if (!raw) return JSON.stringify({ mode: mode, bg: '' });
var c = document.createElement('canvas').getContext('2d');
c.fillStyle = raw;
var hex = c.fillStyle;
return JSON.stringify({ mode: mode, bg: hex });
})();
""".trimIndent()
webView.evaluateJavascript(js) { result ->
applySystemBarsFromJs(result)
}
}
private fun applySystemBarsFromJs(result: String?) {
if (result == null || result == "null") return
val unescaped = result
.trim('"')
.replace("\\\\", "\\")
.replace("\\\"", "\"")
val json = try {
org.json.JSONObject(unescaped)
} catch (_: Exception) {
return
}
val mode = json.optString("mode", "system")
val bg = json.optString("bg", "")
val color = parseCssColor(bg) ?: return
val isLightBg = isColorLight(color)
val controller = WindowInsetsControllerCompat(window, window.decorView)
controller.isAppearanceLightStatusBars = isLightBg && mode != "dark"
controller.isAppearanceLightNavigationBars = isLightBg && mode != "dark"
window.statusBarColor = color
window.navigationBarColor = color
window.decorView.setBackgroundColor(color)
}
private inner class SystemBarBridge {
@android.webkit.JavascriptInterface
fun setSystemBars(mode: String, bg: String) {
val color = parseCssColor(bg) ?: return
val isLightBg = isColorLight(color)
val controller = WindowInsetsControllerCompat(window, window.decorView)
controller.isAppearanceLightStatusBars = isLightBg && mode != "dark"
controller.isAppearanceLightNavigationBars = isLightBg && mode != "dark"
window.statusBarColor = color
window.navigationBarColor = color
window.decorView.setBackgroundColor(color)
}
@android.webkit.JavascriptInterface
fun vibrate(ms: Int) {
val duration = ms.coerceIn(1, 50).toLong()
val vibrator = if (android.os.Build.VERSION.SDK_INT >= 31) {
val vm = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vm.defaultVibrator
} else {
@Suppress("DEPRECATION")
getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
if (!vibrator.hasVibrator()) return
if (android.os.Build.VERSION.SDK_INT >= 26) {
vibrator.vibrate(VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(duration)
}
}
}
private fun parseCssColor(value: String): Int? {
val v = value.trim()
if (v.isEmpty()) return null
// 优先处理 #hex 格式(JS 端已统一转为此格式)
if (v.startsWith("#")) {
return try {
Color.parseColor(v)
} catch (_: Exception) {
null
}
}
// 兼容处理 rgb/rgba — 同时支持逗号分隔和空格分隔
if (v.startsWith("rgb")) {
val inner = v.substringAfter('(').substringBefore(')')
// 提取所有数字(整数或浮点)
val nums = Regex("""\d+\.?\d*""").findAll(inner).map { it.value }.toList()
if (nums.size < 3) return null
val r = nums[0].toFloatOrNull()?.toInt()?.coerceIn(0, 255) ?: return null
val g = nums[1].toFloatOrNull()?.toInt()?.coerceIn(0, 255) ?: return null
val b = nums[2].toFloatOrNull()?.toInt()?.coerceIn(0, 255) ?: return null
return Color.rgb(r, g, b)
}
// 兼容处理 hsl/hsla — 同时支持逗号分隔和空格分隔
if (v.startsWith("hsl")) {
val inner = v.substringAfter('(').substringBefore(')')
.replace("%", "")
val parts = Regex("""\d+\.?\d*""").findAll(inner).map { it.value }.toList()
if (parts.size < 3) return null
val h = parts[0].toFloatOrNull() ?: return null
val s = (parts[1].toFloatOrNull() ?: return null) / 100f
val l = (parts[2].toFloatOrNull() ?: return null) / 100f
return hslToColor(h, s, l)
}
// 最后尝试 Color.parseColor (支持 named colors 等)
return try {
Color.parseColor(v)
} catch (_: Exception) {
null
}
}
private fun hslToColor(h: Float, s: Float, l: Float): Int {
val c = (1 - kotlin.math.abs(2 * l - 1)) * s
val hh = (h % 360) / 60f
val x = c * (1 - kotlin.math.abs(hh % 2 - 1))
val (r1, g1, b1) = when {
hh < 1 -> Triple(c, x, 0f)
hh < 2 -> Triple(x, c, 0f)
hh < 3 -> Triple(0f, c, x)
hh < 4 -> Triple(0f, x, c)
hh < 5 -> Triple(x, 0f, c)
else -> Triple(c, 0f, x)
}
val m = l - c / 2
val r = ((r1 + m) * 255).toInt().coerceIn(0, 255)
val g = ((g1 + m) * 255).toInt().coerceIn(0, 255)
val b = ((b1 + m) * 255).toInt().coerceIn(0, 255)
return Color.rgb(r, g, b)
}
private fun isColorLight(color: Int): Boolean {
val r = Color.red(color) / 255f
val g = Color.green(color) / 255f
val b = Color.blue(color) / 255f
val luminance = 0.299f * r + 0.587f * g + 0.114f * b
return luminance > 0.6f
}
/**
* 延迟重试注入 insets,确保 WebView 加载完成后 CSS 变量被设置
* 最多重试 10 次,间隔递增
*/
private fun scheduleInsetsInjection(rootView: View, attempt: Int) {
if (attempt >= 10) return
val delay = if (attempt < 3) 200L else 1000L
handler.postDelayed({
if (tryInjectInsets(rootView)) {
// 注入成功后再补几次,确保页面导航后也有值
if (attempt < 5) {
scheduleInsetsInjection(rootView, attempt + 1)
}
} else {
scheduleInsetsInjection(rootView, attempt + 1)
}
}, delay)
}
/**
* 尝试向 WebView 注入 insets CSS 变量
* @return 是否找到了 WebView 并成功注入
*/
private fun tryInjectInsets(view: View): Boolean {
val js = cachedInsetsJs ?: return false
val webView = findWebView(view) ?: return false
cachedWebView = webView
ensureJsBridge(webView)
webView.evaluateJavascript(js, null)
return true
}
private fun ensureJsBridge(webView: WebView) {
try {
webView.addJavascriptInterface(SystemBarBridge(), "__opencode_android")
} catch (_: Exception) {
// ignore - may be added already
}
}
private fun findWebView(view: View): WebView? {
if (view is WebView) return view
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
findWebView(view.getChildAt(i))?.let { return it }
}
}
return null
}
}
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@mipmap/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

@@ -0,0 +1,14 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme (dark). -->
<style name="Theme.opencodeui" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- 状态栏和导航栏透明,实现沉浸式 -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<!-- 暗色模式:浅色状态栏图标 -->
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar" tools:targetApi="27">false</item>
<!-- 确保内容延伸到状态栏下方 -->
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">false</item>
</style>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
@@ -0,0 +1,4 @@
<resources>
<string name="app_name">OpenCode</string>
<string name="main_activity_title">OpenCode</string>
</resources>
@@ -0,0 +1,14 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.opencodeui" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- 状态栏和导航栏透明,实现沉浸式 -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<!-- 亮色状态栏图标(深色图标,适合浅色背景) -->
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar" tools:targetApi="27">true</item>
<!-- 确保内容延伸到状态栏下方 -->
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">false</item>
</style>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>